MDL-31857basic phpunit support
[moodle.git] / lib / phpunit / lib.php
CommitLineData
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
26require_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 */
36class 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 */
417class 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 */
539class 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