PageRenderTime 53ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/simpletestlib.php

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