Merge branch 'w12_MDL-31857_m23_phpunit' of git://github.com/skodak/moodle
[moodle.git] / lib / phpunit / lib.php
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/>.
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  */
26 require_once 'PHPUnit/Autoload.php'; // necessary when loaded from cli/util.php script
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;
42     protected static $globals = array();
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;
52         if (!isset(self::$tabledata)) {
53             $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser");
54             self::$tabledata = unserialize($data);
55         }
57         if (!is_array(self::$tabledata)) {
58             phpunit_bootstrap_error('Can not read dataroot/phpunit/tabledata.ser or invalid format!');
59         }
61         return self::$tabledata;
62     }
64     /**
65      * Initialise CFG using data from fresh new install.
66      * @static
67      */
68     public static function initialise_cfg() {
69         global $CFG, $DB;
71         if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
72             // most probably PHPUnit CLI installer
73             return;
74         }
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         }
83         $data = self::get_tabledata();
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     }
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;
106         $data = self::get_tabledata();
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();
124         purge_all_caches();
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     }
134     /**
135      * Called during bootstrap only!
136      * @static
137      * @return void
138      */
139     public static function init_globals() {
140         global $CFG;
142         self::$globals['CFG'] = clone($CFG);
143     }
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     }
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;
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         }
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         }
189         return true;
190     }
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;
201         if (!self::is_test_site()) {
202             return false;
203         }
205         $tables = $DB->get_tables(true);
207         if (!$tables) {
208             return false;
209         }
211         if (!get_config('core', 'phpunittest')) {
212              return false;
213         }
215         if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
216             return false;
217         }
219         if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
220             return false;
221         }
223         $hash = phpunit_util::get_version_hash();
224         $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
226         if ($hash !== $oldhash) {
227             return false;
228         }
230         return true;
231     }
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;
244         if (!self::is_test_site()) {
245             cli_error('Can not drop non-test sites!!', 131);
246         }
248         // drop dataroot
249         remove_dir($CFG->dataroot, true);
250         phpunit_bootstrap_initdataroot($CFG->dataroot);
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();
260         // now drop them
261         foreach ($tables as $tablename) {
262             $table = new xmldb_table($tablename);
263             $DB->get_manager()->drop_table($table);
264         }
265     }
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;
278         if (!self::is_test_site()) {
279             cli_error('Can not install non-test sites!!', 131);
280         }
282         if ($DB->get_tables()) {
283             cli_error('Database tables already installed, drop the site first.', 133);
284         }
286         $options = array();
287         $options['adminpass'] = 'admin'; // removed later
288         $options['shortname'] = 'phpunit';
289         $options['fullname'] = 'PHPUnit test site';
291         install_cli_database($options, false);
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'));
296         // add test db flag
297         set_config('phpunittest', 'phpunittest');
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);
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     }
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;
323         $versions = array();
325         // main version first
326         $version = null;
327         include($CFG->dirroot.'/version.php');
328         $versions['core'] = $version;
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         }
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         }
355         $hash = sha1(serialize($versions));
357         return $hash;
358     }
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;
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");
376         $suites = '';
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;
391                 $suite = str_replace('@component@', $component, $template);
392                 $suite = str_replace('@dir@', $dir, $suite);
394                 $suites .= $suite;
395             }
396         }
398         $data = preg_replace('|<!--@plugin_suits_start@-->.*<!--@plugin_suits_end@-->|s', $suites, $data, 1);
400         @unlink("$CFG->dirroot/phpunit.xml");
401         file_put_contents("$CFG->dirroot/phpunit.xml", $data);
402     }
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 {
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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 {
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);
551         $this->setBackupGlobals(false);
552         $this->setBackupStaticAttributes(false);
553         $this->setRunTestInSeparateProcess(false);
554         $this->setInIsolation(false);
555     }
557     /**
558      * Runs the bare test sequence.
559      * @return void
560      */
561     public function runBare() {
562         global $CFG, $USER, $DB;
564         $dbwrites = $DB->perf_get_writes();
566         parent::runBare();
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);
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         }
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         }
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         }
599         //TODO: somehow find out if there are changes in dataroot
600     }