Commit | Line | Data |
---|---|---|
5bd40408 PS |
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 | /** | |
18 | * Various PHPUnit classes and functions | |
19 | * | |
20 | * @package core_core | |
21 | * @category phpunit | |
22 | * @copyright 2012 Petr Skoda {@link http://skodak.org} | |
23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
24 | */ | |
25 | ||
26 | require_once 'PHPUnit/Autoload.php'; // necessary when loaded from cli/util.php script | |
27 | ||
28 | ||
29 | /** | |
30 | * Collection of utility methods. | |
31 | * | |
32 | * @package core_phpunit | |
33 | * @copyright 2012 Petr Skoda {@link http://skodak.org} | |
34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
35 | */ | |
36 | class phpunit_util { | |
37 | /** | |
38 | * @var array original content of all database tables | |
39 | */ | |
40 | protected static $tabledata = null; | |
41 | ||
42 | protected static $globals = array(); | |
43 | ||
44 | /** | |
45 | * Returns contents of all tables right after installation. | |
46 | * @static | |
47 | * @return array $table=>$records | |
48 | */ | |
49 | protected static function get_tabledata() { | |
50 | global $CFG; | |
51 | ||
52 | if (!isset(self::$tabledata)) { | |
53 | $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser"); | |
54 | self::$tabledata = unserialize($data); | |
55 | } | |
56 | ||
57 | if (!is_array(self::$tabledata)) { | |
58 | phpunit_bootstrap_error('Can not read dataroot/phpunit/tabledata.ser or invalid format!'); | |
59 | } | |
60 | ||
61 | return self::$tabledata; | |
62 | } | |
63 | ||
64 | /** | |
65 | * Initialise CFG using data from fresh new install. | |
66 | * @static | |
67 | */ | |
68 | public static function initialise_cfg() { | |
69 | global $CFG, $DB; | |
70 | ||
71 | if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) { | |
72 | // most probably PHPUnit CLI installer | |
73 | return; | |
74 | } | |
75 | ||
76 | if (!$DB->get_manager()->table_exists('config') or !$DB->count_records('config')) { | |
77 | @unlink("$CFG->dataroot/phpunit/tabledata.ser"); | |
78 | @unlink("$CFG->dataroot/phpunit/versionshash.txt"); | |
79 | self::$tabledata = null; | |
80 | return; | |
81 | } | |
82 | ||
83 | $data = self::get_tabledata(); | |
84 | ||
85 | foreach($data['config'] as $record) { | |
86 | $name = $record->name; | |
87 | $value = $record->value; | |
88 | if (property_exists($CFG, $name)) { | |
89 | // config.php settings always take precedence | |
90 | continue; | |
91 | } | |
92 | $CFG->{$name} = $value; | |
93 | } | |
94 | } | |
95 | ||
96 | /** | |
97 | * Reset contents of all database tables to initial values, reset caches, etc. | |
98 | * | |
99 | * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care! | |
100 | * | |
101 | * @static | |
102 | */ | |
103 | public static function reset_all_data() { | |
104 | global $DB, $CFG; | |
105 | ||
106 | $data = self::get_tabledata(); | |
107 | ||
108 | $trans = $DB->start_delegated_transaction(); // faster and safer | |
109 | foreach ($data as $table=>$records) { | |
110 | $DB->delete_records($table, array()); | |
111 | $resetseq = null; | |
112 | foreach ($records as $record) { | |
113 | if (is_null($resetseq)) { | |
114 | $resetseq = property_exists($record, 'id'); | |
115 | } | |
116 | $DB->import_record($table, $record, false, true); | |
117 | } | |
118 | if ($resetseq === true) { | |
119 | $DB->get_manager()->reset_sequence($table, true); | |
120 | } | |
121 | } | |
122 | $trans->allow_commit(); | |
123 | ||
124 | purge_all_caches(); | |
125 | ||
126 | $user = new stdClass(); | |
127 | $user->id = 0; | |
128 | $user->mnet = 0; | |
129 | $user->mnethostid = $CFG->mnet_localhost_id; | |
130 | session_set_user($user); | |
131 | accesslib_clear_all_caches_for_unit_testing(); | |
132 | } | |
133 | ||
134 | /** | |
135 | * Called during bootstrap only! | |
136 | * @static | |
137 | * @return void | |
138 | */ | |
139 | public static function init_globals() { | |
140 | global $CFG; | |
141 | ||
142 | self::$globals['CFG'] = clone($CFG); | |
143 | } | |
144 | ||
145 | /** | |
146 | * Returns original state of global variable. | |
147 | * @static | |
148 | * @param string $name | |
149 | * @return mixed | |
150 | */ | |
151 | public static function get_global_backup($name) { | |
152 | if (isset(self::$globals[$name])) { | |
153 | if (is_object(self::$globals[$name])) { | |
154 | $return = clone(self::$globals[$name]); | |
155 | return $return; | |
156 | } else { | |
157 | return self::$globals[$name]; | |
158 | } | |
159 | } | |
160 | return null; | |
161 | } | |
162 | ||
163 | /** | |
164 | * Does this site (db and dataroot) appear to be used for production? | |
165 | * We try very hard to prevent accidental damage done to production servers!! | |
166 | * | |
167 | * @static | |
168 | * @return bool | |
169 | */ | |
170 | public static function is_test_site() { | |
171 | global $DB, $CFG; | |
172 | ||
173 | if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) { | |
174 | // this is already tested in bootstrap script, | |
175 | // but anway presence of this file means the dataroot is for testing | |
176 | return false; | |
177 | } | |
178 | ||
179 | $tables = $DB->get_tables(false); | |
180 | if ($tables) { | |
181 | if (!$DB->get_manager()->table_exists('config')) { | |
182 | return false; | |
183 | } | |
184 | if (!get_config('core', 'phpunittest')) { | |
185 | return false; | |
186 | } | |
187 | } | |
188 | ||
189 | return true; | |
190 | } | |
191 | ||
192 | /** | |
193 | * Is this site initialised to run unit tests? | |
194 | * | |
195 | * @static | |
196 | * @return bool | |
197 | */ | |
198 | public static function is_testing_ready() { | |
199 | global $DB, $CFG; | |
200 | ||
201 | if (!self::is_test_site()) { | |
202 | return false; | |
203 | } | |
204 | ||
205 | $tables = $DB->get_tables(true); | |
206 | ||
207 | if (!$tables) { | |
208 | return false; | |
209 | } | |
210 | ||
211 | if (!get_config('core', 'phpunittest')) { | |
212 | return false; | |
213 | } | |
214 | ||
215 | if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) { | |
216 | return false; | |
217 | } | |
218 | ||
219 | if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) { | |
220 | return false; | |
221 | } | |
222 | ||
223 | $hash = phpunit_util::get_version_hash(); | |
224 | $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt"); | |
225 | ||
226 | if ($hash !== $oldhash) { | |
227 | return false; | |
228 | } | |
229 | ||
230 | return true; | |
231 | } | |
232 | ||
233 | /** | |
234 | * Drop all test site data. | |
235 | * | |
236 | * Note: To be used from CLI scripts only. | |
237 | * | |
238 | * @static | |
239 | * @return void, may terminate execution with exit code | |
240 | */ | |
241 | public static function drop_site() { | |
242 | global $DB, $CFG; | |
243 | ||
244 | if (!self::is_test_site()) { | |
245 | cli_error('Can not drop non-test sites!!', 131); | |
246 | } | |
247 | ||
248 | // drop dataroot | |
249 | remove_dir($CFG->dataroot, true); | |
250 | phpunit_bootstrap_initdataroot($CFG->dataroot); | |
251 | ||
252 | // drop all tables | |
253 | $trans = $DB->start_delegated_transaction(); | |
254 | $tables = $DB->get_tables(false); | |
255 | foreach ($tables as $tablename) { | |
256 | $DB->delete_records($tablename, array()); | |
257 | } | |
258 | $trans->allow_commit(); | |
259 | ||
260 | // now drop them | |
261 | foreach ($tables as $tablename) { | |
262 | $table = new xmldb_table($tablename); | |
263 | $DB->get_manager()->drop_table($table); | |
264 | } | |
265 | } | |
266 | ||
267 | /** | |
268 | * Perform a fresh test site installation | |
269 | * | |
270 | * Note: To be used from CLI scripts only. | |
271 | * | |
272 | * @static | |
273 | * @return void, may terminate execution with exit code | |
274 | */ | |
275 | public static function install_site() { | |
276 | global $DB, $CFG; | |
277 | ||
278 | if (!self::is_test_site()) { | |
279 | cli_error('Can not install non-test sites!!', 131); | |
280 | } | |
281 | ||
282 | if ($DB->get_tables()) { | |
283 | cli_error('Database tables already installed, drop the site first.', 133); | |
284 | } | |
285 | ||
286 | $options = array(); | |
287 | $options['adminpass'] = 'admin'; // removed later | |
288 | $options['shortname'] = 'phpunit'; | |
289 | $options['fullname'] = 'PHPUnit test site'; | |
290 | ||
291 | install_cli_database($options, false); | |
292 | ||
293 | // just in case remove admin password so that normal login is not possible | |
294 | $DB->set_field('user', 'password', 'not cached', array('username' => 'admin')); | |
295 | ||
296 | // add test db flag | |
297 | set_config('phpunittest', 'phpunittest'); | |
298 | ||
299 | // store data for all tables | |
300 | $data = array(); | |
301 | $tables = $DB->get_tables(); | |
302 | foreach ($tables as $table) { | |
303 | $data[$table] = $DB->get_records($table, array()); | |
304 | } | |
305 | $data = serialize($data); | |
306 | @unlink("$CFG->dataroot/phpunit/tabledata.ser"); | |
307 | file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data); | |
308 | ||
309 | // hash all plugin versions - helps with very fast detection of db structure changes | |
310 | $hash = phpunit_util::get_version_hash(); | |
311 | @unlink("$CFG->dataroot/phpunit/versionshash.txt"); | |
312 | file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash); | |
313 | } | |
314 | ||
315 | /** | |
316 | * Culculate unique version hash for all available plugins and core. | |
317 | * @static | |
318 | * @return string sha1 hash | |
319 | */ | |
320 | public static function get_version_hash() { | |
321 | global $CFG; | |
322 | ||
323 | $versions = array(); | |
324 | ||
325 | // main version first | |
326 | $version = null; | |
327 | include($CFG->dirroot.'/version.php'); | |
328 | $versions['core'] = $version; | |
329 | ||
330 | // modules | |
331 | $mods = get_plugin_list('mod'); | |
332 | ksort($mods); | |
333 | foreach ($mods as $mod => $fullmod) { | |
334 | $module = new stdClass(); | |
335 | $module->version = null; | |
336 | include($fullmod.'/version.php'); | |
337 | $versions[$mod] = $module->version; | |
338 | } | |
339 | ||
340 | // now the rest of plugins | |
341 | $plugintypes = get_plugin_types(); | |
342 | unset($plugintypes['mod']); | |
343 | ksort($plugintypes); | |
344 | foreach ($plugintypes as $type=>$unused) { | |
345 | $plugs = get_plugin_list($type); | |
346 | ksort($plugs); | |
347 | foreach ($plugs as $plug=>$fullplug) { | |
348 | $plugin = new stdClass(); | |
349 | $plugin->version = null; | |
350 | @include($fullplug.'/version.php'); | |
351 | $versions[$plug] = $plugin->version; | |
352 | } | |
353 | } | |
354 | ||
355 | $hash = sha1(serialize($versions)); | |
356 | ||
357 | return $hash; | |
358 | } | |
359 | ||
360 | /** | |
361 | * Builds /phpunit.xml file using defaults from /phpunit.xml.dist | |
362 | * @static | |
363 | * @return void | |
364 | */ | |
365 | public static function build_config_file() { | |
366 | global $CFG; | |
367 | ||
368 | $template = ' | |
369 | <testsuites> | |
370 | <testsuite name="@component@"> | |
371 | <directory suffix="_test.php">@dir@</directory> | |
372 | </testsuite> | |
373 | </testsuites>'; | |
374 | $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist"); | |
375 | ||
376 | $suites = ''; | |
377 | ||
378 | $plugintypes = get_plugin_types(); | |
379 | ksort($plugintypes); | |
380 | foreach ($plugintypes as $type=>$unused) { | |
381 | $plugs = get_plugin_list($type); | |
382 | ksort($plugs); | |
383 | foreach ($plugs as $plug=>$fullplug) { | |
384 | if (!file_exists("$fullplug/tests/")) { | |
385 | continue; | |
386 | } | |
387 | $dir = preg_replace("|$CFG->dirroot/|", '', $fullplug, 1); | |
388 | $dir .= '/tests'; | |
389 | $component = $type.'_'.$plug; | |
390 | ||
391 | $suite = str_replace('@component@', $component, $template); | |
392 | $suite = str_replace('@dir@', $dir, $suite); | |
393 | ||
394 | $suites .= $suite; | |
395 | } | |
396 | } | |
397 | ||
398 | $data = preg_replace('|<!--@plugin_suits_start@-->.*<!--@plugin_suits_end@-->|s', $suites, $data, 1); | |
399 | ||
400 | @unlink("$CFG->dirroot/phpunit.xml"); | |
401 | file_put_contents("$CFG->dirroot/phpunit.xml", $data); | |
402 | } | |
403 | } | |
404 | ||
405 | ||
406 | /** | |
407 | * Simplified emulation test case for legacy SimpleTest. | |
408 | * | |
409 | * Note: this is supposed to work for very simple tests only. | |
410 | * | |
411 | * @deprecated | |
412 | * @package core_phpunit | |
413 | * @author Petr Skoda | |
414 | * @copyright 2012 Petr Skoda {@link http://skodak.org} | |
415 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
416 | */ | |
417 | class UnitTestCase extends PHPUnit_Framework_TestCase { | |
418 | ||
419 | /** | |
420 | * @deprecated | |
421 | * @param bool $expected | |
422 | * @param string $message | |
423 | * @return void | |
424 | */ | |
425 | public function expectException($expected, $message = '') { | |
426 | // use phpdocs: @expectedException ExceptionClassName | |
427 | if (!$expected) { | |
428 | return; | |
429 | } | |
430 | $this->setExpectedException('moodle_exception', $message); | |
431 | } | |
432 | ||
433 | /** | |
434 | * @deprecated | |
435 | * @param bool $expected | |
436 | * @param string $message | |
437 | * @return void | |
438 | */ | |
439 | public static function expectError($expected = false, $message = '') { | |
440 | // not available in PHPUnit | |
441 | if (!$expected) { | |
442 | return; | |
443 | } | |
444 | self::skipIf(true); | |
445 | } | |
446 | ||
447 | /** | |
448 | * @deprecated | |
449 | * @static | |
450 | * @param mixed $actual | |
451 | * @param string $messages | |
452 | * @return void | |
453 | */ | |
454 | public static function assertTrue($actual, $messages = '') { | |
455 | parent::assertTrue((bool)$actual, $messages); | |
456 | } | |
457 | ||
458 | /** | |
459 | * @deprecated | |
460 | * @static | |
461 | * @param mixed $actual | |
462 | * @param string $messages | |
463 | * @return void | |
464 | */ | |
465 | public static function assertFalse($actual, $messages = '') { | |
466 | parent::assertFalse((bool)$actual, $messages); | |
467 | } | |
468 | ||
469 | /** | |
470 | * @deprecated | |
471 | * @static | |
472 | * @param mixed $expected | |
473 | * @param mixed $actual | |
474 | * @param string $message | |
475 | * @return void | |
476 | */ | |
477 | public static function assertEqual($expected, $actual, $message = '') { | |
478 | parent::assertEquals($expected, $actual, $message); | |
479 | } | |
480 | ||
481 | /** | |
482 | * @deprecated | |
483 | * @static | |
484 | * @param mixed $expected | |
485 | * @param mixed $actual | |
486 | * @param string $message | |
487 | * @return void | |
488 | */ | |
489 | public static function assertNotEqual($expected, $actual, $message = '') { | |
490 | parent::assertNotEquals($expected, $actual, $message); | |
491 | } | |
492 | ||
493 | /** | |
494 | * @deprecated | |
495 | * @static | |
496 | * @param mixed $expected | |
497 | * @param mixed $actual | |
498 | * @param string $message | |
499 | * @return void | |
500 | */ | |
501 | public static function assertIdentical($expected, $actual, $message = '') { | |
502 | parent::assertSame($expected, $actual, $message); | |
503 | } | |
504 | ||
505 | /** | |
506 | * @deprecated | |
507 | * @static | |
508 | * @param mixed $expected | |
509 | * @param mixed $actual | |
510 | * @param string $message | |
511 | * @return void | |
512 | */ | |
513 | public static function assertNotIdentical($expected, $actual, $message = '') { | |
514 | parent::assertNotSame($expected, $actual, $message); | |
515 | } | |
516 | ||
517 | /** | |
518 | * @deprecated | |
519 | * @static | |
520 | * @param mixed $actual | |
521 | * @param mixed $expected | |
522 | * @param string $message | |
523 | * @return void | |
524 | */ | |
525 | public static function assertIsA($actual, $expected, $message = '') { | |
526 | parent::assertInstanceOf($expected, $actual, $message); | |
527 | } | |
528 | } | |
529 | ||
530 | ||
531 | /** | |
532 | * The simplest PHPUnit test case customised for Moodle, | |
533 | * do not modify database or any globals. | |
534 | * | |
535 | * @package core_phpunit | |
536 | * @copyright 2012 Petr Skoda {@link http://skodak.org} | |
537 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
538 | */ | |
539 | class basic_testcase extends PHPUnit_Framework_TestCase { | |
540 | ||
541 | /** | |
542 | * Constructs a test case with the given name. | |
543 | * | |
544 | * @param string $name | |
545 | * @param array $data | |
546 | * @param string $dataName | |
547 | */ | |
548 | public function __construct($name = NULL, array $data = array(), $dataName = '') { | |
549 | parent::__construct($name, $data, $dataName); | |
550 | ||
551 | $this->setBackupGlobals(false); | |
552 | $this->setBackupStaticAttributes(false); | |
553 | $this->setRunTestInSeparateProcess(false); | |
554 | $this->setInIsolation(false); | |
555 | } | |
556 | ||
557 | /** | |
558 | * Runs the bare test sequence. | |
559 | * @return void | |
560 | */ | |
561 | public function runBare() { | |
562 | global $CFG, $USER, $DB; | |
563 | ||
564 | $dbwrites = $DB->perf_get_writes(); | |
565 | ||
566 | parent::runBare(); | |
567 | ||
568 | $oldcfg = phpunit_util::get_global_backup('CFG'); | |
569 | foreach($CFG as $k=>$v) { | |
570 | if (!property_exists($oldcfg, $k)) { | |
571 | unset($CFG->$k); | |
572 | error_log('warning: unexpected new $CFG->'.$k.' value in testcase: '.get_class($this).'->'.$this->getName(true)); | |
573 | } else if ($oldcfg->$k !== $CFG->$k) { | |
574 | $CFG->$k = $oldcfg->$k; | |
575 | error_log('warning: unexpected change of $CFG->'.$k.' value in testcase: '.get_class($this).'->'.$this->getName(true)); | |
576 | } | |
577 | unset($oldcfg->$k); | |
578 | ||
579 | } | |
580 | if ($oldcfg) { | |
581 | foreach($oldcfg as $k=>$v) { | |
582 | $CFG->$k = $v; | |
583 | error_log('warning: unexpected removal of $CFG->'.$k.' in testcase: '.get_class($this).'->'.$this->getName(true)); | |
584 | } | |
585 | } | |
586 | ||
587 | if ($USER->id != 0) { | |
588 | error_log('warning: unexpected change of $USER in testcase: '.get_class($this).'->'.$this->getName(true)); | |
589 | $USER = new stdClass(); | |
590 | $USER->id = 0; | |
591 | } | |
592 | ||
593 | if ($dbwrites != $DB->perf_get_writes()) { | |
594 | //TODO: find out what was changed exactly | |
595 | error_log('warning: unexpected database modification, resetting DB state in testcase: '.get_class($this).'->'.$this->getName(true)); | |
596 | phpunit_util::reset_all_data(); | |
597 | } | |
598 | ||
599 | //TODO: somehow find out if there are changes in dataroot | |
600 | } | |
601 | } | |
602 |