MDL-60281 general: various strict corrections for PHP7.2
[moodle.git] / lib / phpunit / classes / base_testcase.php
blob54eb63fa7ed31c12fa6b8b5da5c134fe2016500e
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Base test case class.
20 * @package core
21 * @category test
22 * @author Tony Levi <tony.levi@blackboard.com>
23 * @copyright 2015 Blackboard (http://www.blackboard.com)
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 /**
29 * Base class for PHPUnit test cases customised for Moodle
31 * It is intended for functionality common to both basic and advanced_testcase.
33 * @package core
34 * @category test
35 * @author Tony Levi <tony.levi@blackboard.com>
36 * @copyright 2015 Blackboard (http://www.blackboard.com)
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 abstract class base_testcase extends PHPUnit_Framework_TestCase {
40 // @codingStandardsIgnoreStart
41 // Following code is legacy code from phpunit to support assertTag
42 // and assertNotTag.
44 /**
45 * Note: we are overriding this method to remove the deprecated error
46 * @see https://tracker.moodle.org/browse/MDL-47129
48 * @param array $matcher
49 * @param string $actual
50 * @param string $message
51 * @param boolean $ishtml
53 * @deprecated 3.0
55 public static function assertTag($matcher, $actual, $message = '', $ishtml = true) {
56 $dom = PHPUnit_Util_XML::load($actual, $ishtml);
57 $tags = self::findNodes($dom, $matcher, $ishtml);
58 $matched = count($tags) > 0 && $tags[0] instanceof DOMNode;
59 self::assertTrue($matched, $message);
62 /**
63 * Note: we are overriding this method to remove the deprecated error
64 * @see https://tracker.moodle.org/browse/MDL-47129
66 * @param array $matcher
67 * @param string $actual
68 * @param string $message
69 * @param boolean $ishtml
71 * @deprecated 3.0
73 public static function assertNotTag($matcher, $actual, $message = '', $ishtml = true) {
74 $dom = PHPUnit_Util_XML::load($actual, $ishtml);
75 $tags = self::findNodes($dom, $matcher, $ishtml);
76 $matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode;
77 self::assertFalse($matched, $message);
80 /**
81 * Validate list of keys in the associative array.
83 * @param array $hash
84 * @param array $validKeys
86 * @return array
88 * @throws PHPUnit_Framework_Exception
90 public static function assertValidKeys(array $hash, array $validKeys) {
91 $valids = array();
93 // Normalize validation keys so that we can use both indexed and
94 // associative arrays.
95 foreach ($validKeys as $key => $val) {
96 is_int($key) ? $valids[$val] = null : $valids[$key] = $val;
99 $validKeys = array_keys($valids);
101 // Check for invalid keys.
102 foreach ($hash as $key => $value) {
103 if (!in_array($key, $validKeys)) {
104 $unknown[] = $key;
108 if (!empty($unknown)) {
109 throw new PHPUnit_Framework_Exception(
110 'Unknown key(s): ' . implode(', ', $unknown)
114 // Add default values for any valid keys that are empty.
115 foreach ($valids as $key => $value) {
116 if (!isset($hash[$key])) {
117 $hash[$key] = $value;
121 return $hash;
125 * Parse out the options from the tag using DOM object tree.
127 * @param DOMDocument $dom
128 * @param array $options
129 * @param bool $isHtml
131 * @return array
133 public static function findNodes(DOMDocument $dom, array $options, $isHtml = true) {
134 $valid = array(
135 'id', 'class', 'tag', 'content', 'attributes', 'parent',
136 'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling'
139 $filtered = array();
140 $options = self::assertValidKeys($options, $valid);
142 // find the element by id
143 if ($options['id']) {
144 $options['attributes']['id'] = $options['id'];
147 if ($options['class']) {
148 $options['attributes']['class'] = $options['class'];
151 $nodes = array();
153 // find the element by a tag type
154 if ($options['tag']) {
155 if ($isHtml) {
156 $elements = self::getElementsByCaseInsensitiveTagName(
157 $dom,
158 $options['tag']
160 } else {
161 $elements = $dom->getElementsByTagName($options['tag']);
164 foreach ($elements as $element) {
165 $nodes[] = $element;
168 if (empty($nodes)) {
169 return false;
171 } // no tag selected, get them all
172 else {
173 $tags = array(
174 'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
175 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
176 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
177 'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
178 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
179 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
180 'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
181 'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
182 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
183 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
184 'tr', 'tt', 'ul', 'var',
185 // HTML5
186 'article', 'aside', 'audio', 'bdi', 'canvas', 'command',
187 'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption',
188 'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav',
189 'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section',
190 'source', 'summary', 'time', 'video', 'wbr'
193 foreach ($tags as $tag) {
194 if ($isHtml) {
195 $elements = self::getElementsByCaseInsensitiveTagName(
196 $dom,
197 $tag
199 } else {
200 $elements = $dom->getElementsByTagName($tag);
203 foreach ($elements as $element) {
204 $nodes[] = $element;
208 if (empty($nodes)) {
209 return false;
213 // filter by attributes
214 if ($options['attributes']) {
215 foreach ($nodes as $node) {
216 $invalid = false;
218 foreach ($options['attributes'] as $name => $value) {
219 // match by regexp if like "regexp:/foo/i"
220 if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
221 if (!preg_match($matches[1], $node->getAttribute($name))) {
222 $invalid = true;
224 } // class can match only a part
225 elseif ($name == 'class') {
226 // split to individual classes
227 $findClasses = explode(
228 ' ',
229 preg_replace("/\s+/", ' ', $value)
232 $allClasses = explode(
233 ' ',
234 preg_replace("/\s+/", ' ', $node->getAttribute($name))
237 // make sure each class given is in the actual node
238 foreach ($findClasses as $findClass) {
239 if (!in_array($findClass, $allClasses)) {
240 $invalid = true;
243 } // match by exact string
244 else {
245 if ($node->getAttribute($name) != $value) {
246 $invalid = true;
251 // if every attribute given matched
252 if (!$invalid) {
253 $filtered[] = $node;
257 $nodes = $filtered;
258 $filtered = array();
260 if (empty($nodes)) {
261 return false;
265 // filter by content
266 if ($options['content'] !== null) {
267 foreach ($nodes as $node) {
268 $invalid = false;
270 // match by regexp if like "regexp:/foo/i"
271 if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
272 if (!preg_match($matches[1], self::getNodeText($node))) {
273 $invalid = true;
275 } // match empty string
276 elseif ($options['content'] === '') {
277 if (self::getNodeText($node) !== '') {
278 $invalid = true;
280 } // match by exact string
281 elseif (strstr(self::getNodeText($node), $options['content']) === false) {
282 $invalid = true;
285 if (!$invalid) {
286 $filtered[] = $node;
290 $nodes = $filtered;
291 $filtered = array();
293 if (empty($nodes)) {
294 return false;
298 // filter by parent node
299 if ($options['parent']) {
300 $parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
301 $parentNode = isset($parentNodes[0]) ? $parentNodes[0] : null;
303 foreach ($nodes as $node) {
304 if ($parentNode !== $node->parentNode) {
305 continue;
308 $filtered[] = $node;
311 $nodes = $filtered;
312 $filtered = array();
314 if (empty($nodes)) {
315 return false;
319 // filter by child node
320 if ($options['child']) {
321 $childNodes = self::findNodes($dom, $options['child'], $isHtml);
322 $childNodes = !empty($childNodes) ? $childNodes : array();
324 foreach ($nodes as $node) {
325 foreach ($node->childNodes as $child) {
326 foreach ($childNodes as $childNode) {
327 if ($childNode === $child) {
328 $filtered[] = $node;
334 $nodes = $filtered;
335 $filtered = array();
337 if (empty($nodes)) {
338 return false;
342 // filter by adjacent-sibling
343 if ($options['adjacent-sibling']) {
344 $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml);
345 $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array();
347 foreach ($nodes as $node) {
348 $sibling = $node;
350 while ($sibling = $sibling->nextSibling) {
351 if ($sibling->nodeType !== XML_ELEMENT_NODE) {
352 continue;
355 foreach ($adjacentSiblingNodes as $adjacentSiblingNode) {
356 if ($sibling === $adjacentSiblingNode) {
357 $filtered[] = $node;
358 break;
362 break;
366 $nodes = $filtered;
367 $filtered = array();
369 if (empty($nodes)) {
370 return false;
374 // filter by ancestor
375 if ($options['ancestor']) {
376 $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
377 $ancestorNode = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null;
379 foreach ($nodes as $node) {
380 $parent = $node->parentNode;
382 while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
383 if ($parent === $ancestorNode) {
384 $filtered[] = $node;
387 $parent = $parent->parentNode;
391 $nodes = $filtered;
392 $filtered = array();
394 if (empty($nodes)) {
395 return false;
399 // filter by descendant
400 if ($options['descendant']) {
401 $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
402 $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
404 foreach ($nodes as $node) {
405 foreach (self::getDescendants($node) as $descendant) {
406 foreach ($descendantNodes as $descendantNode) {
407 if ($descendantNode === $descendant) {
408 $filtered[] = $node;
414 $nodes = $filtered;
415 $filtered = array();
417 if (empty($nodes)) {
418 return false;
422 // filter by children
423 if ($options['children']) {
424 $validChild = array('count', 'greater_than', 'less_than', 'only');
425 $childOptions = self::assertValidKeys(
426 $options['children'],
427 $validChild
430 foreach ($nodes as $node) {
431 $childNodes = $node->childNodes;
433 foreach ($childNodes as $childNode) {
434 if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
435 $childNode->nodeType !== XML_TEXT_NODE) {
436 $children[] = $childNode;
440 // we must have children to pass this filter
441 if (!empty($children)) {
442 // exact count of children
443 if ($childOptions['count'] !== null) {
444 if (count($children) !== $childOptions['count']) {
445 break;
447 } // range count of children
448 elseif ($childOptions['less_than'] !== null &&
449 $childOptions['greater_than'] !== null) {
450 if (count($children) >= $childOptions['less_than'] ||
451 count($children) <= $childOptions['greater_than']) {
452 break;
454 } // less than a given count
455 elseif ($childOptions['less_than'] !== null) {
456 if (count($children) >= $childOptions['less_than']) {
457 break;
459 } // more than a given count
460 elseif ($childOptions['greater_than'] !== null) {
461 if (count($children) <= $childOptions['greater_than']) {
462 break;
466 // match each child against a specific tag
467 if ($childOptions['only']) {
468 $onlyNodes = self::findNodes(
469 $dom,
470 $childOptions['only'],
471 $isHtml
474 // try to match each child to one of the 'only' nodes
475 foreach ($children as $child) {
476 $matched = false;
478 foreach ($onlyNodes as $onlyNode) {
479 if ($onlyNode === $child) {
480 $matched = true;
484 if (!$matched) {
485 break 2;
490 $filtered[] = $node;
494 $nodes = $filtered;
496 if (empty($nodes)) {
497 return;
501 // return the first node that matches all criteria
502 return !empty($nodes) ? $nodes : array();
506 * Recursively get flat array of all descendants of this node.
508 * @param DOMNode $node
510 * @return array
512 protected static function getDescendants(DOMNode $node) {
513 $allChildren = array();
514 $childNodes = $node->childNodes ? $node->childNodes : array();
516 foreach ($childNodes as $child) {
517 if ($child->nodeType === XML_CDATA_SECTION_NODE ||
518 $child->nodeType === XML_TEXT_NODE) {
519 continue;
522 $children = self::getDescendants($child);
523 $allChildren = array_merge($allChildren, $children, array($child));
526 return isset($allChildren) ? $allChildren : array();
530 * Gets elements by case insensitive tagname.
532 * @param DOMDocument $dom
533 * @param string $tag
535 * @return DOMNodeList
537 protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag) {
538 $elements = $dom->getElementsByTagName(strtolower($tag));
540 if ($elements->length == 0) {
541 $elements = $dom->getElementsByTagName(strtoupper($tag));
544 return $elements;
548 * Get the text value of this node's child text node.
550 * @param DOMNode $node
552 * @return string
554 protected static function getNodeText(DOMNode $node) {
555 if (!$node->childNodes instanceof DOMNodeList) {
556 return '';
559 $result = '';
561 foreach ($node->childNodes as $childNode) {
562 if ($childNode->nodeType === XML_TEXT_NODE ||
563 $childNode->nodeType === XML_CDATA_SECTION_NODE) {
564 $result .= trim($childNode->data) . ' ';
565 } else {
566 $result .= self::getNodeText($childNode);
570 return str_replace(' ', ' ', $result);
573 // @codingStandardsIgnoreEnd