PageRenderTime 60ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/admin/tool/unittest/simpletestlib.php

http://github.com/moodle/moodle
PHP | 1371 lines | 881 code | 146 blank | 344 comment | 109 complexity | 3118e0718e76bc9651d1f837154fde78 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  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.
  13. //
  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/>.
  16. /**
  17. * Utility functions to make unit testing easier.
  18. *
  19. * These functions, particularly the the database ones, are quick and
  20. * dirty methods for getting things done in test cases. None of these
  21. * methods should be used outside test code.
  22. *
  23. * Major Contirbutors
  24. * - T.J.Hunt@open.ac.uk
  25. *
  26. * @package tool
  27. * @subpackage unittest
  28. * @copyright &copy; 2006 The Open University
  29. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30. */
  31. defined('MOODLE_INTERNAL') || die();
  32. /**
  33. * Includes
  34. */
  35. require_once($CFG->libdir . '/simpletestlib/simpletest.php');
  36. require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
  37. require_once($CFG->libdir . '/simpletestlib/expectation.php');
  38. require_once($CFG->libdir . '/simpletestlib/reporter.php');
  39. require_once($CFG->libdir . '/simpletestlib/web_tester.php');
  40. require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
  41. /**
  42. * Recursively visit all the files in the source tree. Calls the callback
  43. * function with the pathname of each file found.
  44. *
  45. * @param $path the folder to start searching from.
  46. * @param $callback the function to call with the name of each file found.
  47. * @param $fileregexp a regexp used to filter the search (optional).
  48. * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
  49. * only files that match the regexp will be included. (default false).
  50. * @param array $ignorefolders will not go into any of these folders (optional).
  51. */
  52. function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
  53. $files = scandir($path);
  54. foreach ($files as $file) {
  55. $filepath = $path .'/'. $file;
  56. if (strpos($file, '.') === 0) {
  57. /// Don't check hidden files.
  58. continue;
  59. } else if (is_dir($filepath)) {
  60. if (!in_array($filepath, $ignorefolders)) {
  61. recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
  62. }
  63. } else if ($exclude xor preg_match($fileregexp, $filepath)) {
  64. call_user_func($callback, $filepath);
  65. }
  66. }
  67. }
  68. /**
  69. * An expectation for comparing strings ignoring whitespace.
  70. *
  71. * @package moodlecore
  72. * @subpackage simpletestex
  73. * @copyright &copy; 2006 The Open University
  74. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  75. */
  76. class IgnoreWhitespaceExpectation extends SimpleExpectation {
  77. var $expect;
  78. function IgnoreWhitespaceExpectation($content, $message = '%s') {
  79. $this->SimpleExpectation($message);
  80. $this->expect=$this->normalise($content);
  81. }
  82. function test($ip) {
  83. return $this->normalise($ip)==$this->expect;
  84. }
  85. function normalise($text) {
  86. return preg_replace('/\s+/m',' ',trim($text));
  87. }
  88. function testMessage($ip) {
  89. return "Input string [$ip] doesn't match the required value.";
  90. }
  91. }
  92. /**
  93. * An Expectation that two arrays contain the same list of values.
  94. *
  95. * @package moodlecore
  96. * @subpackage simpletestex
  97. * @copyright &copy; 2006 The Open University
  98. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  99. */
  100. class ArraysHaveSameValuesExpectation extends SimpleExpectation {
  101. var $expect;
  102. function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
  103. $this->SimpleExpectation($message);
  104. if (!is_array($expected)) {
  105. trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
  106. 'with an expected value that is not an array.');
  107. }
  108. $this->expect = $this->normalise($expected);
  109. }
  110. function test($actual) {
  111. return $this->normalise($actual) == $this->expect;
  112. }
  113. function normalise($array) {
  114. sort($array);
  115. return $array;
  116. }
  117. function testMessage($actual) {
  118. return 'Array [' . implode(', ', $actual) .
  119. '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
  120. }
  121. }
  122. /**
  123. * An Expectation that compares to objects, and ensures that for every field in the
  124. * expected object, there is a key of the same name in the actual object, with
  125. * the same value. (The actual object may have other fields to, but we ignore them.)
  126. *
  127. * @package moodlecore
  128. * @subpackage simpletestex
  129. * @copyright &copy; 2006 The Open University
  130. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  131. */
  132. class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
  133. var $expect;
  134. function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
  135. $this->SimpleExpectation($message);
  136. if (!is_object($expected)) {
  137. trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
  138. 'with an expected value that is not an object.');
  139. }
  140. $this->expect = $expected;
  141. }
  142. function test($actual) {
  143. foreach ($this->expect as $key => $value) {
  144. if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
  145. // OK
  146. } else if (is_null($value) && is_null($actual->$key)) {
  147. // OK
  148. } else {
  149. return false;
  150. }
  151. }
  152. return true;
  153. }
  154. function testMessage($actual) {
  155. $mismatches = array();
  156. foreach ($this->expect as $key => $value) {
  157. if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
  158. // OK
  159. } else if (is_null($value) && is_null($actual->$key)) {
  160. // OK
  161. } else if (!isset($actual->$key)) {
  162. $mismatches[] = $key . ' (expected [' . $value . '] but was missing.';
  163. } else {
  164. $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
  165. }
  166. }
  167. return 'Actual object does not have all the same fields with the same values as the expected object (' .
  168. implode(', ', $mismatches) . ').';
  169. }
  170. }
  171. abstract class XMLStructureExpectation extends SimpleExpectation {
  172. /**
  173. * Parse a string as XML and return a DOMDocument;
  174. * @param $html
  175. * @return unknown_type
  176. */
  177. protected function load_xml($html) {
  178. $prevsetting = libxml_use_internal_errors(true);
  179. $parser = new DOMDocument();
  180. if (!$parser->loadXML('<html>' . $html . '</html>')) {
  181. $parser = new DOMDocument();
  182. }
  183. libxml_clear_errors();
  184. libxml_use_internal_errors($prevsetting);
  185. return $parser;
  186. }
  187. function testMessage($html) {
  188. $parsererrors = $this->load_xml($html);
  189. if (is_array($parsererrors)) {
  190. foreach ($parsererrors as $key => $message) {
  191. $parsererrors[$key] = $message->message;
  192. }
  193. return 'Could not parse XML [' . $html . '] errors were [' .
  194. implode('], [', $parsererrors) . ']';
  195. }
  196. return $this->customMessage($html);
  197. }
  198. }
  199. /**
  200. * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
  201. *
  202. * @copyright 2009 Tim Hunt
  203. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  204. */
  205. class ContainsTagWithAttribute extends XMLStructureExpectation {
  206. protected $tag;
  207. protected $attribute;
  208. protected $value;
  209. function __construct($tag, $attribute, $value, $message = '%s') {
  210. parent::__construct($message);
  211. $this->tag = $tag;
  212. $this->attribute = $attribute;
  213. $this->value = $value;
  214. }
  215. function test($html) {
  216. $parser = $this->load_xml($html);
  217. if (is_array($parser)) {
  218. return false;
  219. }
  220. $list = $parser->getElementsByTagName($this->tag);
  221. foreach ($list as $node) {
  222. if ($node->attributes->getNamedItem($this->attribute)->nodeValue === (string) $this->value) {
  223. return true;
  224. }
  225. }
  226. return false;
  227. }
  228. function customMessage($html) {
  229. return 'Content [' . $html . '] does not contain the tag [' .
  230. $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
  231. }
  232. }
  233. /**
  234. * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
  235. * All attributes must be present and their values must match the expected values.
  236. * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
  237. *
  238. * @copyright 2009 Nicolas Connault
  239. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  240. */
  241. class ContainsTagWithAttributes extends XMLStructureExpectation {
  242. /**
  243. * @var string $tag The name of the Tag to search
  244. */
  245. protected $tag;
  246. /**
  247. * @var array $expectedvalues An associative array of parameters, all of which must be matched
  248. */
  249. protected $expectedvalues = array();
  250. /**
  251. * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
  252. */
  253. protected $forbiddenvalues = array();
  254. /**
  255. * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
  256. */
  257. protected $failurereason = 'nomatch';
  258. function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
  259. parent::__construct($message);
  260. $this->tag = $tag;
  261. $this->expectedvalues = $expectedvalues;
  262. $this->forbiddenvalues = $forbiddenvalues;
  263. }
  264. function test($html) {
  265. $parser = $this->load_xml($html);
  266. if (is_array($parser)) {
  267. return false;
  268. }
  269. $list = $parser->getElementsByTagName($this->tag);
  270. $foundamatch = false;
  271. // Iterating through inputs
  272. foreach ($list as $node) {
  273. if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
  274. continue;
  275. }
  276. // For the current expected attribute under consideration, check that values match
  277. $allattributesmatch = true;
  278. foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
  279. if ($node->getAttribute($expectedattribute) === '' && $expectedvalue !== '') {
  280. $this->failurereason = 'nomatch';
  281. continue 2; // Skip this tag, it doesn't have all the expected attributes
  282. }
  283. if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
  284. $allattributesmatch = false;
  285. $this->failurereason = 'nomatch';
  286. }
  287. }
  288. if ($allattributesmatch) {
  289. $foundamatch = true;
  290. // Now make sure this node doesn't have any of the forbidden attributes either
  291. $nodeattrlist = $node->attributes;
  292. foreach ($nodeattrlist as $domattrname => $domattr) {
  293. if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) === (string) $this->forbiddenvalues[$domattrname]) {
  294. $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
  295. $foundamatch = false;
  296. }
  297. }
  298. }
  299. }
  300. return $foundamatch;
  301. }
  302. function customMessage($html) {
  303. $output = 'Content [' . $html . '] ';
  304. if (preg_match('/forbiddenmatch:(.*):(.*)/', $this->failurereason, $matches)) {
  305. $output .= "contains the tag $this->tag with the forbidden attribute=>value pair: [$matches[1]=>$matches[2]]";
  306. } else if ($this->failurereason == 'nomatch') {
  307. $output .= 'does not contain the tag [' . $this->tag . '] with attributes [';
  308. foreach ($this->expectedvalues as $var => $val) {
  309. $output .= "$var=\"$val\" ";
  310. }
  311. $output = rtrim($output);
  312. $output .= '].';
  313. }
  314. return $output;
  315. }
  316. }
  317. /**
  318. * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
  319. * All attributes must be present and their values must match the expected values.
  320. * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
  321. *
  322. * @copyright 2010 The Open University
  323. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  324. */
  325. class ContainsSelectExpectation extends XMLStructureExpectation {
  326. /**
  327. * @var string $tag The name of the Tag to search
  328. */
  329. protected $name;
  330. /**
  331. * @var array $expectedvalues An associative array of parameters, all of which must be matched
  332. */
  333. protected $choices;
  334. /**
  335. * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
  336. */
  337. protected $selected;
  338. /**
  339. * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
  340. */
  341. protected $enabled;
  342. function __construct($name, $choices, $selected = null, $enabled = null, $message = '%s') {
  343. parent::__construct($message);
  344. $this->name = $name;
  345. $this->choices = $choices;
  346. $this->selected = $selected;
  347. $this->enabled = $enabled;
  348. }
  349. function test($html) {
  350. $parser = $this->load_xml($html);
  351. if (is_array($parser)) {
  352. return false;
  353. }
  354. $list = $parser->getElementsByTagName('select');
  355. // Iterating through inputs
  356. foreach ($list as $node) {
  357. if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
  358. continue;
  359. }
  360. if ($node->getAttribute('name') != $this->name) {
  361. continue;
  362. }
  363. if ($this->enabled === true && $node->getAttribute('disabled')) {
  364. continue;
  365. } else if ($this->enabled === false && $node->getAttribute('disabled') != 'disabled') {
  366. continue;
  367. }
  368. $options = $node->getElementsByTagName('option');
  369. reset($this->choices);
  370. foreach ($options as $option) {
  371. if ($option->getAttribute('value') != key($this->choices)) {
  372. continue 2;
  373. }
  374. if ($option->firstChild->wholeText != current($this->choices)) {
  375. continue 2;
  376. }
  377. if ($option->getAttribute('value') === $this->selected &&
  378. !$option->hasAttribute('selected')) {
  379. continue 2;
  380. }
  381. next($this->choices);
  382. }
  383. if (current($this->choices) !== false) {
  384. // The HTML did not contain all the choices.
  385. return false;
  386. }
  387. return true;
  388. }
  389. return false;
  390. }
  391. function customMessage($html) {
  392. if ($this->enabled === true) {
  393. $state = 'an enabled';
  394. } else if ($this->enabled === false) {
  395. $state = 'a disabled';
  396. } else {
  397. $state = 'a';
  398. }
  399. $output = 'Content [' . $html . '] does not contain ' . $state .
  400. ' <select> with name ' . $this->name . ' and choices ' .
  401. implode(', ', $this->choices);
  402. if ($this->selected) {
  403. $output .= ' with ' . $this->selected . ' selected).';
  404. }
  405. return $output;
  406. }
  407. }
  408. /**
  409. * The opposite of {@link ContainsTagWithAttributes}. The test passes only if
  410. * the HTML does not contain a tag with the given attributes.
  411. *
  412. * @copyright 2010 The Open University
  413. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  414. */
  415. class DoesNotContainTagWithAttributes extends ContainsTagWithAttributes {
  416. function __construct($tag, $expectedvalues, $message = '%s') {
  417. parent::__construct($tag, $expectedvalues, array(), $message);
  418. }
  419. function test($html) {
  420. return !parent::test($html);
  421. }
  422. function customMessage($html) {
  423. $output = 'Content [' . $html . '] ';
  424. $output .= 'contains the tag [' . $this->tag . '] with attributes [';
  425. foreach ($this->expectedvalues as $var => $val) {
  426. $output .= "$var=\"$val\" ";
  427. }
  428. $output = rtrim($output);
  429. $output .= '].';
  430. return $output;
  431. }
  432. }
  433. /**
  434. * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
  435. *
  436. * @copyright 2009 Tim Hunt
  437. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  438. */
  439. class ContainsTagWithContents extends XMLStructureExpectation {
  440. protected $tag;
  441. protected $content;
  442. function __construct($tag, $content, $message = '%s') {
  443. parent::__construct($message);
  444. $this->tag = $tag;
  445. $this->content = $content;
  446. }
  447. function test($html) {
  448. $parser = $this->load_xml($html);
  449. $list = $parser->getElementsByTagName($this->tag);
  450. foreach ($list as $node) {
  451. if ($node->textContent == $this->content) {
  452. return true;
  453. }
  454. }
  455. return false;
  456. }
  457. function testMessage($html) {
  458. return 'Content [' . $html . '] does not contain the tag [' .
  459. $this->tag . '] with contents [' . $this->content . '].';
  460. }
  461. }
  462. /**
  463. * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
  464. *
  465. * @copyright 2009 Nicolas Connault
  466. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  467. */
  468. class ContainsEmptyTag extends XMLStructureExpectation {
  469. protected $tag;
  470. function __construct($tag, $message = '%s') {
  471. parent::__construct($message);
  472. $this->tag = $tag;
  473. }
  474. function test($html) {
  475. $parser = $this->load_xml($html);
  476. $list = $parser->getElementsByTagName($this->tag);
  477. foreach ($list as $node) {
  478. if (!$node->hasAttributes() && !$node->hasChildNodes()) {
  479. return true;
  480. }
  481. }
  482. return false;
  483. }
  484. function testMessage($html) {
  485. return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
  486. }
  487. }
  488. /**
  489. * Simple class that implements the {@link moodle_recordset} API based on an
  490. * array of test data.
  491. *
  492. * See the {@link question_attempt_step_db_test} class in
  493. * question/engine/simpletest/testquestionattemptstep.php for an example of how
  494. * this is used.
  495. *
  496. * @copyright 2011 The Open University
  497. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  498. */
  499. class test_recordset extends moodle_recordset {
  500. protected $records;
  501. /**
  502. * Constructor
  503. * @param $table as for {@link testing_db_record_builder::build_db_records()}
  504. * but does not need a unique first column.
  505. */
  506. public function __construct(array $table) {
  507. $columns = array_shift($table);
  508. $this->records = array();
  509. foreach ($table as $row) {
  510. if (count($row) != count($columns)) {
  511. throw new coding_exception("Row contains the wrong number of fields.");
  512. }
  513. $rec = array();
  514. foreach ($columns as $i => $name) {
  515. $rec[$name] = $row[$i];
  516. }
  517. $this->records[] = $rec;
  518. }
  519. reset($this->records);
  520. }
  521. public function __destruct() {
  522. $this->close();
  523. }
  524. public function current() {
  525. return (object) current($this->records);
  526. }
  527. public function key() {
  528. if (is_null(key($this->records))) {
  529. return false;
  530. }
  531. $current = current($this->records);
  532. return reset($current);
  533. }
  534. public function next() {
  535. next($this->records);
  536. }
  537. public function valid() {
  538. return !is_null(key($this->records));
  539. }
  540. public function close() {
  541. $this->records = null;
  542. }
  543. }
  544. /**
  545. * This class lets you write unit tests that access a separate set of test
  546. * tables with a different prefix. Only those tables you explicitly ask to
  547. * be created will be.
  548. *
  549. * This class has failities for flipping $USER->id.
  550. *
  551. * The tear-down method for this class should automatically revert any changes
  552. * you make during test set-up using the metods defined here. That is, it will
  553. * drop tables for you automatically and revert to the real $DB and $USER->id.
  554. *
  555. * @package moodlecore
  556. * @subpackage simpletestex
  557. * @copyright &copy; 2006 The Open University
  558. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  559. */
  560. class UnitTestCaseUsingDatabase extends UnitTestCase {
  561. private $realdb;
  562. protected $testdb;
  563. private $realuserid = null;
  564. private $tables = array();
  565. private $realcfg;
  566. protected $testcfg;
  567. public function __construct($label = false) {
  568. global $DB, $CFG;
  569. // Complain if we get this far and $CFG->unittestprefix is not set.
  570. if (empty($CFG->unittestprefix)) {
  571. throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
  572. }
  573. // Only do this after the above text.
  574. parent::UnitTestCase($label);
  575. // Create the test DB instance.
  576. $this->realdb = $DB;
  577. $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
  578. $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
  579. // Set up test config
  580. $this->testcfg = (object)array(
  581. 'testcfg' => true, // Marker that this is a test config
  582. 'libdir' => $CFG->libdir, // Must use real one so require_once works
  583. 'dirroot' => $CFG->dirroot, // Must use real one
  584. 'dataroot' => $CFG->dataroot, // Use real one for now (maybe this should change?)
  585. 'ostype' => $CFG->ostype, // Real one
  586. 'wwwroot' => 'http://www.example.org', // Use fixed url
  587. 'siteadmins' => '0', // No admins
  588. 'siteguest' => '0' // No guest
  589. );
  590. $this->realcfg = $CFG;
  591. }
  592. /**
  593. * Switch to using the test database for all queries until further notice.
  594. */
  595. protected function switch_to_test_db() {
  596. global $DB;
  597. if ($DB === $this->testdb) {
  598. debugging('switch_to_test_db called when the test DB was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
  599. }
  600. $DB = $this->testdb;
  601. }
  602. /**
  603. * Revert to using the test database for all future queries.
  604. */
  605. protected function revert_to_real_db() {
  606. global $DB;
  607. if ($DB !== $this->testdb) {
  608. debugging('revert_to_real_db called when the test DB was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
  609. }
  610. $DB = $this->realdb;
  611. }
  612. /**
  613. * Switch to using the test $CFG for all queries until further notice.
  614. */
  615. protected function switch_to_test_cfg() {
  616. global $CFG;
  617. if (isset($CFG->testcfg)) {
  618. debugging('switch_to_test_cfg called when the test CFG was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
  619. }
  620. $CFG = $this->testcfg;
  621. }
  622. /**
  623. * Revert to using the real $CFG for all future queries.
  624. */
  625. protected function revert_to_real_cfg() {
  626. global $CFG;
  627. if (!isset($CFG->testcfg)) {
  628. debugging('revert_to_real_cfg called when the test CFG was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
  629. }
  630. $CFG = $this->realcfg;
  631. }
  632. /**
  633. * Switch $USER->id to a test value.
  634. *
  635. * It might be worth making this method do more robuse $USER switching in future,
  636. * however, this is sufficient for my needs at present.
  637. */
  638. protected function switch_global_user_id($userid) {
  639. global $USER;
  640. if (!is_null($this->realuserid)) {
  641. debugging('switch_global_user_id called when $USER->id was already switched to a different value. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
  642. } else {
  643. $this->realuserid = $USER->id;
  644. }
  645. $USER->id = $userid;
  646. }
  647. /**
  648. * Revert $USER->id to the real value.
  649. */
  650. protected function revert_global_user_id() {
  651. global $USER;
  652. if (is_null($this->realuserid)) {
  653. debugging('revert_global_user_id called without switch_global_user_id having been called first. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
  654. } else {
  655. $USER->id = $this->realuserid;
  656. $this->realuserid = null;
  657. }
  658. }
  659. /**
  660. * Recreates the system context record in the 'context' table
  661. *
  662. * Once we have switched to test db, if we have recreated the
  663. * context table and it's empty, it may be necessary to manually
  664. * create the system context record if unittests are going to
  665. * play with contexts.
  666. *
  667. * This is needed because the context_system::instance() method
  668. * is exceptional and always requires the record to exist, never
  669. * creating it :-( No problem for other contexts.
  670. *
  671. * Altenatively one complete install can be done, like
  672. * {@see accesslib_test::test_everything_in_accesslib} does, but that's
  673. * to much for some tests not requiring all the roles/caps/friends
  674. * to be present.
  675. *
  676. * Ideally some day we'll move a lot of these UnitTests to a complete
  677. * cloned installation with real data to play with. That day this
  678. * won't be necessary anymore.
  679. */
  680. protected function create_system_context_record() {
  681. global $DB;
  682. // If, for any reason, the record exists, do nothing
  683. if ($DB->record_exists('context', array('contextlevel'=>CONTEXT_SYSTEM))) {
  684. return;
  685. }
  686. $record = new stdClass();
  687. $record->contextlevel = CONTEXT_SYSTEM;
  688. $record->instanceid = 0;
  689. $record->depth = 1;
  690. $record->path = null;
  691. if (defined('SYSCONTEXTID')) {
  692. $record->id = SYSCONTEXTID;
  693. $DB->import_record('context', $record);
  694. $DB->get_manager()->reset_sequence('context');
  695. } else {
  696. $record->id = $DB->insert_record('context', $record);
  697. }
  698. // fix path
  699. $record->path = '/'.$record->id;
  700. $DB->set_field('context', 'path', $record->path, array('id' => $record->id));
  701. }
  702. /**
  703. * Check that the user has not forgotten to clean anything up, and if they
  704. * have, display a rude message and clean it up for them.
  705. */
  706. private function automatic_clean_up() {
  707. global $DB, $CFG;
  708. $cleanmore = false;
  709. // Drop any test tables that were created.
  710. foreach ($this->tables as $tablename => $notused) {
  711. $this->drop_test_table($tablename);
  712. }
  713. // Switch back to the real DB if necessary.
  714. if ($DB !== $this->realdb) {
  715. $this->revert_to_real_db();
  716. $cleanmore = true;
  717. }
  718. // Switch back to the real CFG if necessary.
  719. if (isset($CFG->testcfg)) {
  720. $this->revert_to_real_cfg();
  721. $cleanmore = true;
  722. }
  723. // revert_global_user_id if necessary.
  724. if (!is_null($this->realuserid)) {
  725. $this->revert_global_user_id();
  726. $cleanmore = true;
  727. }
  728. if ($cleanmore) {
  729. accesslib_clear_all_caches_for_unit_testing();
  730. $course = 'reset';
  731. get_fast_modinfo($course);
  732. }
  733. }
  734. public function tearDown() {
  735. $this->automatic_clean_up();
  736. parent::tearDown();
  737. }
  738. public function __destruct() {
  739. // Should not be necessary thanks to tearDown, but no harm in belt and braces.
  740. $this->automatic_clean_up();
  741. }
  742. /**
  743. * Create a test table just like a real one, getting getting the definition from
  744. * the specified install.xml file.
  745. * @param string $tablename the name of the test table.
  746. * @param string $installxmlfile the install.xml file in which this table is defined.
  747. * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
  748. * so you need only specify, for example, 'mod/quiz'.
  749. */
  750. protected function create_test_table($tablename, $installxmlfile) {
  751. global $CFG;
  752. $dbman = $this->testdb->get_manager();
  753. if (isset($this->tables[$tablename])) {
  754. debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
  755. return;
  756. }
  757. if ($dbman->table_exists($tablename)) {
  758. debugging('This table ' . $tablename . ' already exists from a previous execution. If the error persists you will need to review your code to ensure it is being created only once.', DEBUG_DEVELOPER);
  759. $dbman->drop_table(new xmldb_table($tablename));
  760. }
  761. $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
  762. $this->tables[$tablename] = 1;
  763. }
  764. /**
  765. * Convenience method for calling create_test_table repeatedly.
  766. * @param array $tablenames an array of table names.
  767. * @param string $installxmlfile the install.xml file in which this table is defined.
  768. * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
  769. * so you need only specify, for example, 'mod/quiz'.
  770. */
  771. protected function create_test_tables($tablenames, $installxmlfile) {
  772. foreach ($tablenames as $tablename) {
  773. $this->create_test_table($tablename, $installxmlfile);
  774. }
  775. }
  776. /**
  777. * Drop a test table.
  778. * @param $tablename the name of the test table.
  779. */
  780. protected function drop_test_table($tablename) {
  781. if (!isset($this->tables[$tablename])) {
  782. debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
  783. return;
  784. }
  785. $dbman = $this->testdb->get_manager();
  786. $table = new xmldb_table($tablename);
  787. $dbman->drop_table($table);
  788. unset($this->tables[$tablename]);
  789. }
  790. /**
  791. * Convenience method for calling drop_test_table repeatedly.
  792. * @param array $tablenames an array of table names.
  793. */
  794. protected function drop_test_tables($tablenames) {
  795. foreach ($tablenames as $tablename) {
  796. $this->drop_test_table($tablename);
  797. }
  798. }
  799. /**
  800. * Load a table with some rows of data. A typical call would look like:
  801. *
  802. * $config = $this->load_test_data('config_plugins',
  803. * array('plugin', 'name', 'value'), array(
  804. * array('frog', 'numlegs', 2),
  805. * array('frog', 'sound', 'croak'),
  806. * array('frog', 'action', 'jump'),
  807. * ));
  808. *
  809. * @param string $table the table name.
  810. * @param array $cols the columns to fill.
  811. * @param array $data the data to load.
  812. * @return array $objects corresponding to $data.
  813. */
  814. protected function load_test_data($table, array $cols, array $data) {
  815. $results = array();
  816. foreach ($data as $rowid => $row) {
  817. $obj = new stdClass;
  818. foreach ($cols as $key => $colname) {
  819. $obj->$colname = $row[$key];
  820. }
  821. $obj->id = $this->testdb->insert_record($table, $obj);
  822. $results[$rowid] = $obj;
  823. }
  824. return $results;
  825. }
  826. /**
  827. * Clean up data loaded with load_test_data. The call corresponding to the
  828. * example load above would be:
  829. *
  830. * $this->delete_test_data('config_plugins', $config);
  831. *
  832. * @param string $table the table name.
  833. * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
  834. */
  835. protected function delete_test_data($table, array $rows) {
  836. $ids = array();
  837. foreach ($rows as $row) {
  838. $ids[] = $row->id;
  839. }
  840. $this->testdb->delete_records_list($table, 'id', $ids);
  841. }
  842. }
  843. /**
  844. * @package moodlecore
  845. * @subpackage simpletestex
  846. * @copyright &copy; 2006 The Open University
  847. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  848. */
  849. class FakeDBUnitTestCase extends UnitTestCase {
  850. public $tables = array();
  851. public $pkfile;
  852. public $cfg;
  853. public $DB;
  854. /**
  855. * In the constructor, record the max(id) of each test table into a csv file.
  856. * If this file already exists, it means that a previous run of unit tests
  857. * did not complete, and has left data undeleted in the DB. This data is then
  858. * deleted and the file is retained. Otherwise it is created.
  859. *
  860. * throws moodle_exception if CSV file cannot be created
  861. */
  862. public function __construct($label = false) {
  863. global $DB, $CFG;
  864. if (empty($CFG->unittestprefix)) {
  865. return;
  866. }
  867. parent::UnitTestCase($label);
  868. // MDL-16483 Get PKs and save data to text file
  869. $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
  870. $this->cfg = $CFG;
  871. UnitTestDB::instantiate();
  872. $tables = $DB->get_tables();
  873. // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
  874. if (file_exists($this->pkfile)) {
  875. $this->truncate_test_tables($this->get_table_data($this->pkfile));
  876. } else { // Create the file
  877. $tabledata = '';
  878. foreach ($tables as $table) {
  879. if ($table != 'sessions') {
  880. if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
  881. $max_id = 0;
  882. }
  883. $tabledata .= "$table, $max_id\n";
  884. }
  885. }
  886. if (!file_put_contents($this->pkfile, $tabledata)) {
  887. $a = new stdClass();
  888. $a->filename = $this->pkfile;
  889. throw new moodle_exception('testtablescsvfileunwritable', 'tool_unittest', '', $a);
  890. }
  891. }
  892. }
  893. /**
  894. * Given an array of tables and their max id, truncates all test table records whose id is higher than the ones in the $tabledata array.
  895. * @param array $tabledata
  896. */
  897. private function truncate_test_tables($tabledata) {
  898. global $CFG, $DB;
  899. if (empty($CFG->unittestprefix)) {
  900. return;
  901. }
  902. $tables = $DB->get_tables();
  903. foreach ($tables as $table) {
  904. if ($table != 'sessions' && isset($tabledata[$table])) {
  905. // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
  906. }
  907. }
  908. }
  909. /**
  910. * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
  911. * 1. Table name
  912. * 2. Max id
  913. *
  914. * throws moodle_exception if file doesn't exist
  915. *
  916. * @param string $filename
  917. */
  918. public function get_table_data($filename) {
  919. global $CFG;
  920. if (empty($CFG->unittestprefix)) {
  921. return;
  922. }
  923. if (file_exists($this->pkfile)) {
  924. $handle = fopen($this->pkfile, 'r');
  925. $tabledata = array();
  926. while (($data = fgetcsv($handle, 1000, ",")) !== false) {
  927. $tabledata[$data[0]] = $data[1];
  928. }
  929. return $tabledata;
  930. } else {
  931. $a = new stdClass();
  932. $a->filename = $this->pkfile;
  933. throw new moodle_exception('testtablescsvfilemissing', 'tool_unittest', '', $a);
  934. return false;
  935. }
  936. }
  937. /**
  938. * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
  939. * Also detects if this config setting is properly set, and if the user table exists.
  940. * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
  941. */
  942. public function setUp() {
  943. global $DB, $CFG;
  944. if (empty($CFG->unittestprefix)) {
  945. return;
  946. }
  947. parent::setUp();
  948. $this->DB =& $DB;
  949. ob_start();
  950. }
  951. /**
  952. * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
  953. */
  954. public function tearDown() {
  955. global $DB, $CFG;
  956. if (empty($CFG->unittestprefix)) {
  957. return;
  958. }
  959. if (empty($DB)) {
  960. $DB = $this->DB;
  961. }
  962. $DB->cleanup();
  963. parent::tearDown();
  964. // Output buffering
  965. if (ob_get_length() > 0) {
  966. ob_end_flush();
  967. }
  968. }
  969. /**
  970. * This will execute once all the tests have been run. It should delete the text file holding info about database contents prior to the tests
  971. * It should also detect if data is missing from the original tables.
  972. */
  973. public function __destruct() {
  974. global $CFG, $DB;
  975. if (empty($CFG->unittestprefix)) {
  976. return;
  977. }
  978. $CFG = $this->cfg;
  979. $this->tearDown();
  980. UnitTestDB::restore();
  981. fulldelete($this->pkfile);
  982. }
  983. /**
  984. * Load a table with some rows of data. A typical call would look like:
  985. *
  986. * $config = $this->load_test_data('config_plugins',
  987. * array('plugin', 'name', 'value'), array(
  988. * array('frog', 'numlegs', 2),
  989. * array('frog', 'sound', 'croak'),
  990. * array('frog', 'action', 'jump'),
  991. * ));
  992. *
  993. * @param string $table the table name.
  994. * @param array $cols the columns to fill.
  995. * @param array $data the data to load.
  996. * @return array $objects corresponding to $data.
  997. */
  998. public function load_test_data($table, array $cols, array $data) {
  999. global $CFG, $DB;
  1000. if (empty($CFG->unittestprefix)) {
  1001. return;
  1002. }
  1003. $results = array();
  1004. foreach ($data as $rowid => $row) {
  1005. $obj = new stdClass;
  1006. foreach ($cols as $key => $colname) {
  1007. $obj->$colname = $row[$key];
  1008. }
  1009. $obj->id = $DB->insert_record($table, $obj);
  1010. $results[$rowid] = $obj;
  1011. }
  1012. return $results;
  1013. }
  1014. /**
  1015. * Clean up data loaded with load_test_data. The call corresponding to the
  1016. * example load above would be:
  1017. *
  1018. * $this->delete_test_data('config_plugins', $config);
  1019. *
  1020. * @param string $table the table name.
  1021. * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
  1022. */
  1023. public function delete_test_data($table, array $rows) {
  1024. global $CFG, $DB;
  1025. if (empty($CFG->unittestprefix)) {
  1026. return;
  1027. }
  1028. $ids = array();
  1029. foreach ($rows as $row) {
  1030. $ids[] = $row->id;
  1031. }
  1032. $DB->delete_records_list($table, 'id', $ids);
  1033. }
  1034. }
  1035. /**
  1036. * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
  1037. * static instantiate() method, and restores the original global $DB through restore().
  1038. * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
  1039. * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
  1040. * without subclassing it.
  1041. *
  1042. * @package moodlecore
  1043. * @subpackage simpletestex
  1044. * @copyright &copy; 2006 The Open University
  1045. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  1046. */
  1047. class UnitTestDB {
  1048. public static $DB;
  1049. private static $real_db;
  1050. public $table_data = array();
  1051. /**
  1052. * Call this statically to connect to the DB using the unittest prefix, instantiate
  1053. * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
  1054. */
  1055. public static function instantiate() {
  1056. global $CFG, $DB;
  1057. UnitTestDB::$real_db = clone($DB);
  1058. if (empty($CFG->unittestprefix)) {
  1059. print_error("prefixnotset", 'tool_unittest');
  1060. }
  1061. if (empty(UnitTestDB::$DB)) {
  1062. UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
  1063. UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
  1064. }
  1065. $manager = UnitTestDB::$DB->get_manager();
  1066. if (!$manager->table_exists('user')) {
  1067. print_error('tablesnotsetup', 'tool_unittest');
  1068. }
  1069. $DB = new UnitTestDB();
  1070. }
  1071. public function __call($method, $args) {
  1072. // Set args to null if they don't exist (up to 10 args should do)
  1073. if (!method_exists($this, $method)) {
  1074. return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
  1075. } else {
  1076. call_user_func_array(array($this, $method), $args);
  1077. }
  1078. }
  1079. public function __get($variable) {
  1080. return UnitTestDB::$DB->$variable;
  1081. }
  1082. public function __set($variable, $value) {
  1083. UnitTestDB::$DB->$variable = $value;
  1084. }
  1085. public function __isset($variable) {
  1086. return isset(UnitTestDB::$DB->$variable);
  1087. }
  1088. public function __unset($variable) {
  1089. unset(UnitTestDB::$DB->$variable);
  1090. }
  1091. /**
  1092. * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
  1093. */
  1094. public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
  1095. global $DB;
  1096. $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
  1097. $this->table_data[$table][] = $id;
  1098. return $id;
  1099. }
  1100. /**
  1101. * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
  1102. * throw an exception and cancel update.
  1103. *
  1104. * throws moodle_exception If trying to update a record not inserted by unit tests.
  1105. */
  1106. public function update_record($table, $dataobject, $bulk=false) {
  1107. global $DB;
  1108. if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
  1109. // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
  1110. $a = new stdClass();
  1111. $a->id = $dataobject->id;
  1112. $a->table = $table;
  1113. throw new moodle_exception('updatingnoninsertedrecord', 'tool_unittest', '', $a);
  1114. } else {
  1115. return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
  1116. }
  1117. }
  1118. /**
  1119. * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
  1120. * throw an exception and cancel delete.
  1121. *
  1122. * throws moodle_exception If trying to delete a record not inserted by unit tests.
  1123. */
  1124. public function delete_records($table, array $conditions=array()) {
  1125. global $DB;
  1126. $tables_to_ignore = array('context_temp');
  1127. $a = new stdClass();
  1128. $a->table = $table;
  1129. // Get ids matching conditions
  1130. if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
  1131. return UnitTestDB::$DB->delete_records($table, $conditions);
  1132. }
  1133. $proceed_with_delete = true;
  1134. if (!is_array($ids_to_delete)) {
  1135. $ids_to_delete = array($ids_to_delete);
  1136. }
  1137. foreach ($ids_to_delete as $id) {
  1138. if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
  1139. $proceed_with_delete = false;
  1140. $a->id = $id;
  1141. break;
  1142. }
  1143. }
  1144. if ($proceed_with_delete) {
  1145. return UnitTestDB::$DB->delete_records($table, $conditions);
  1146. } else {
  1147. throw new moodle_exception('deletingnoninsertedrecord', 'tool_unittest', '', $a);
  1148. }
  1149. }
  1150. /**
  1151. * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
  1152. * throw an exception and cancel delete.
  1153. *
  1154. * throws moodle_exception If trying to delete a record not inserted by unit tests.
  1155. */
  1156. public function delete_records_select($table, $select, array $params=null) {
  1157. global $DB;
  1158. $a = new stdClass();
  1159. $a->table = $table;
  1160. // Get ids matching conditions
  1161. if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
  1162. return UnitTestDB::$DB->delete_records_select($table, $select, $params);
  1163. }
  1164. $proceed_with_delete = true;
  1165. foreach ($ids_to_delete as $id) {
  1166. if (!in_array($id, $this->table_data[$table])) {
  1167. $proceed_with_delete = false;
  1168. $a->id = $id;
  1169. break;
  1170. }
  1171. }
  1172. if ($proceed_with_delete) {
  1173. return UnitTestDB::$DB->delete_records_select($table, $select, $params);
  1174. } else {
  1175. throw new moodle_exception('deletingnoninsertedrecord', 'tool_unittest', '', $a);
  1176. }
  1177. }
  1178. /**
  1179. * Removes from the test DB all the records that were inserted during unit tests,
  1180. */
  1181. public function cleanup() {
  1182. global $DB;
  1183. foreach ($this->table_data as $table => $ids) {
  1184. foreach ($ids as $id) {
  1185. $DB->delete_records($table, array('id' => $id));
  1186. }
  1187. }
  1188. }
  1189. /**
  1190. * Restores the global $DB object.
  1191. */
  1192. public static function restore() {
  1193. global $DB;
  1194. $DB = UnitTestDB::$real_db;
  1195. }
  1196. public function get_field($table, $return, array $conditions) {
  1197. if (!is_array($conditions)) {
  1198. throw new coding_exception('$conditions is not an array.');
  1199. }
  1200. return UnitTestDB::$DB->get_field($table, $return, $conditions);
  1201. }
  1202. }