weekly release 2.6dev
[moodle.git] / lib / phpunit / classes / util.php
CommitLineData
7e7cfe7a
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 * Utility class.
19 *
20 * @package 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
0ea35584 26require_once(__DIR__.'/../../testing/classes/util.php');
7e7cfe7a
PS
27
28/**
29 * Collection of utility methods.
30 *
31 * @package core
32 * @category 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 */
0ea35584 36class phpunit_util extends testing_util {
7e7cfe7a
PS
37 /** @var array An array of original globals, restored after each test */
38 protected static $globals = array();
39
ef5b5e05
PS
40 /** @var array list of debugging messages triggered during the last test execution */
41 protected static $debuggings = array();
42
4c9e03f0
PS
43 /** @var phpunit_message_sink alternative target for moodle messaging */
44 protected static $messagesink = null;
45
62401e8f
PS
46 /** @var phpunit_message_sink alternative target for moodle messaging */
47 protected static $eventsink = null;
48
7e7cfe7a 49 /**
0ea35584 50 * @var array Files to skip when resetting dataroot folder
7e7cfe7a 51 */
0ea35584 52 protected static $datarootskiponreset = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
7e7cfe7a
PS
53
54 /**
0ea35584 55 * @var array Files to skip when dropping dataroot folder
7e7cfe7a 56 */
0ea35584 57 protected static $datarootskipondrop = array('.', '..', 'lock', 'webrunner.xml');
7e7cfe7a
PS
58
59 /**
60 * Load global $CFG;
61 * @internal
62 * @static
63 * @return void
64 */
65 public static function initialise_cfg() {
66 global $DB;
67 $dbhash = false;
68 try {
69 $dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest'));
70 } catch (Exception $e) {
71 // not installed yet
72 initialise_cfg();
73 return;
74 }
c5701ce7 75 if ($dbhash !== core_component::get_all_versions_hash()) {
7e7cfe7a
PS
76 // do not set CFG - the only way forward is to drop and reinstall
77 return;
78 }
79 // standard CFG init
80 initialise_cfg();
81 }
82
7e7cfe7a
PS
83 /**
84 * Reset contents of all database tables to initial values, reset caches, etc.
85 *
86 * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care!
87 *
88 * @static
71fc5003
PS
89 * @param bool $detectchanges
90 * true - changes in global state and database are reported as errors
91 * false - no errors reported
92 * null - only critical problems are reported as errors
7e7cfe7a
PS
93 * @return void
94 */
71fc5003 95 public static function reset_all_data($detectchanges = false) {
e17dbeeb 96 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION;
7e7cfe7a 97
4c9e03f0
PS
98 // Stop any message redirection.
99 phpunit_util::stop_message_redirection();
100
62401e8f
PS
101 // Stop any message redirection.
102 phpunit_util::stop_event_redirection();
103
d03d5113
PS
104 // Release memory and indirectly call destroy() methods to release resource handles, etc.
105 gc_collect_cycles();
106
ef5b5e05
PS
107 // Show any unhandled debugging messages, the runbare() could already reset it.
108 self::display_debugging_messages();
109 self::reset_debugging();
110
7e7cfe7a
PS
111 // reset global $DB in case somebody mocked it
112 $DB = self::get_global_backup('DB');
113
114 if ($DB->is_transaction_started()) {
115 // we can not reset inside transaction
116 $DB->force_transaction_rollback();
117 }
118
119 $resetdb = self::reset_database();
120 $warnings = array();
121
71fc5003 122 if ($detectchanges === true) {
7e7cfe7a
PS
123 if ($resetdb) {
124 $warnings[] = 'Warning: unexpected database modification, resetting DB state';
125 }
126
127 $oldcfg = self::get_global_backup('CFG');
128 $oldsite = self::get_global_backup('SITE');
129 foreach($CFG as $k=>$v) {
130 if (!property_exists($oldcfg, $k)) {
131 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
132 } else if ($oldcfg->$k !== $CFG->$k) {
133 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
134 }
135 unset($oldcfg->$k);
136
137 }
138 if ($oldcfg) {
139 foreach($oldcfg as $k=>$v) {
140 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
141 }
142 }
143
144 if ($USER->id != 0) {
145 $warnings[] = 'Warning: unexpected change of $USER';
146 }
147
148 if ($COURSE->id != $oldsite->id) {
149 $warnings[] = 'Warning: unexpected change of $COURSE';
150 }
82081c1f 151
70faad65
PS
152 }
153
154 if (ini_get('max_execution_time') != 0) {
155 // This is special warning for all resets because we do not want any
156 // libraries to mess with timeouts unintentionally.
157 // Our PHPUnit integration is not supposed to change it either.
158
71fc5003
PS
159 if ($detectchanges !== false) {
160 $warnings[] = 'Warning: max_execution_time was changed to '.ini_get('max_execution_time');
161 }
70faad65 162 set_time_limit(0);
7e7cfe7a
PS
163 }
164
165 // restore original globals
166 $_SERVER = self::get_global_backup('_SERVER');
167 $CFG = self::get_global_backup('CFG');
168 $SITE = self::get_global_backup('SITE');
596ea56f
JP
169 $_GET = array();
170 $_POST = array();
171 $_FILES = array();
172 $_REQUEST = array();
7e7cfe7a
PS
173 $COURSE = $SITE;
174
175 // reinitialise following globals
176 $OUTPUT = new bootstrap_renderer();
177 $PAGE = new moodle_page();
178 $FULLME = null;
179 $ME = null;
180 $SCRIPT = null;
181 $SESSION = new stdClass();
182 $_SESSION['SESSION'] =& $SESSION;
183
184 // set fresh new not-logged-in user
185 $user = new stdClass();
186 $user->id = 0;
187 $user->mnethostid = $CFG->mnet_localhost_id;
188 session_set_user($user);
189
190 // reset all static caches
d8a1f426 191 \core\event\manager::phpunit_reset();
7e7cfe7a 192 accesslib_clear_all_caches(true);
a46e11b5
PS
193 get_string_manager()->reset_caches(true);
194 reset_text_filters_cache(true);
7e7cfe7a 195 events_get_handlers('reset');
2f1e464a 196 core_text::reset_caches();
6fd1cf05
MG
197 if (class_exists('repository')) {
198 repository::reset_caches();
199 }
73a0f3ba 200 filter_manager::reset_caches();
6fd1cf05 201 //TODO MDL-25290: add more resets here and probably refactor them to new core function
7e7cfe7a 202
2beba297 203 // Reset course and module caches.
13d5c938 204 if (class_exists('format_base')) {
2beba297 205 // If file containing class is not loaded, there is no cache there anyway.
13d5c938
PS
206 format_base::reset_course_cache(0);
207 }
b46be6ad 208 get_fast_modinfo(0, 0, true);
13d5c938 209
98547432
210 // Reset other singletons.
211 if (class_exists('plugin_manager')) {
212 plugin_manager::reset_caches(true);
213 }
214 if (class_exists('available_update_checker')) {
215 available_update_checker::reset_caches(true);
216 }
dc11af19
DM
217 if (class_exists('available_update_deployer')) {
218 available_update_deployer::reset_caches(true);
219 }
98547432 220
7e7cfe7a
PS
221 // purge dataroot directory
222 self::reset_dataroot();
223
224 // restore original config once more in case resetting of caches changed CFG
225 $CFG = self::get_global_backup('CFG');
226
227 // inform data generator
228 self::get_data_generator()->reset();
229
230 // fix PHP settings
231 error_reporting($CFG->debug);
232
233 // verify db writes just in case something goes wrong in reset
234 if (self::$lastdbwrites != $DB->perf_get_writes()) {
235 error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
236 self::$lastdbwrites = $DB->perf_get_writes();
237 }
238
239 if ($warnings) {
240 $warnings = implode("\n", $warnings);
241 trigger_error($warnings, E_USER_WARNING);
242 }
243 }
244
245 /**
246 * Called during bootstrap only!
247 * @internal
248 * @static
249 * @return void
250 */
251 public static function bootstrap_init() {
252 global $CFG, $SITE, $DB;
253
254 // backup the globals
255 self::$globals['_SERVER'] = $_SERVER;
256 self::$globals['CFG'] = clone($CFG);
257 self::$globals['SITE'] = clone($SITE);
258 self::$globals['DB'] = $DB;
259
260 // refresh data in all tables, clear caches, etc.
261 phpunit_util::reset_all_data();
262 }
263
15bac12e
PS
264 /**
265 * Print some Moodle related info to console.
266 * @internal
267 * @static
268 * @return void
269 */
270 public static function bootstrap_moodle_info() {
271 global $CFG;
272
273 // All developers have to understand English, do not localise!
274
275 $release = null;
276 require("$CFG->dirroot/version.php");
277
278 echo "Moodle $release, $CFG->dbtype";
279 if ($hash = self::get_git_hash()) {
280 echo ", $hash";
281 }
282 echo "\n";
283 }
284
285 /**
286 * Try to get current git hash of the Moodle in $CFG->dirroot.
287 * @return string null if unknown, sha1 hash if known
288 */
289 public static function get_git_hash() {
290 global $CFG;
291
292 // This is a bit naive, but it should mostly work for all platforms.
293
294 if (!file_exists("$CFG->dirroot/.git/HEAD")) {
295 return null;
296 }
297
298 $ref = file_get_contents("$CFG->dirroot/.git/HEAD");
299 if ($ref === false) {
300 return null;
301 }
302
303 $ref = trim($ref);
304
305 if (strpos($ref, 'ref: ') !== 0) {
306 return null;
307 }
308
309 $ref = substr($ref, 5);
310
311 if (!file_exists("$CFG->dirroot/.git/$ref")) {
312 return null;
313 }
314
315 $hash = file_get_contents("$CFG->dirroot/.git/$ref");
316
317 if ($hash === false) {
318 return null;
319 }
320
321 $hash = trim($hash);
322
323 if (strlen($hash) != 40) {
324 return null;
325 }
326
327 return $hash;
328 }
329
7e7cfe7a
PS
330 /**
331 * Returns original state of global variable.
332 * @static
333 * @param string $name
334 * @return mixed
335 */
336 public static function get_global_backup($name) {
337 if ($name === 'DB') {
338 // no cloning of database object,
339 // we just need the original reference, not original state
340 return self::$globals['DB'];
341 }
342 if (isset(self::$globals[$name])) {
343 if (is_object(self::$globals[$name])) {
344 $return = clone(self::$globals[$name]);
345 return $return;
346 } else {
347 return self::$globals[$name];
348 }
349 }
350 return null;
351 }
352
7e7cfe7a
PS
353 /**
354 * Is this site initialised to run unit tests?
355 *
356 * @static
357 * @return int array errorcode=>message, 0 means ok
358 */
359 public static function testing_ready_problem() {
0ea35584 360 global $DB;
7e7cfe7a
PS
361
362 if (!self::is_test_site()) {
363 // dataroot was verified in bootstrap, so it must be DB
364 return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
365 }
366
0ea35584 367 $tables = $DB->get_tables(false);
7e7cfe7a
PS
368 if (empty($tables)) {
369 return array(PHPUNIT_EXITCODE_INSTALL, '');
370 }
371
0ea35584 372 if (!self::is_test_data_updated()) {
7e7cfe7a
PS
373 return array(PHPUNIT_EXITCODE_REINSTALL, '');
374 }
375
376 return array(0, '');
377 }
378
379 /**
380 * Drop all test site data.
381 *
382 * Note: To be used from CLI scripts only.
383 *
384 * @static
85b72a75 385 * @param bool $displayprogress if true, this method will echo progress information.
7e7cfe7a
PS
386 * @return void may terminate execution with exit code
387 */
85b72a75 388 public static function drop_site($displayprogress = false) {
7e7cfe7a
PS
389 global $DB, $CFG;
390
391 if (!self::is_test_site()) {
392 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
393 }
394
85b72a75
TH
395 // Purge dataroot
396 if ($displayprogress) {
397 echo "Purging dataroot:\n";
398 }
0ea35584 399
7e7cfe7a 400 self::reset_dataroot();
0ea35584
DM
401 testing_initdataroot($CFG->dataroot, 'phpunit');
402 self::drop_dataroot();
7e7cfe7a
PS
403
404 // drop all tables
0ea35584 405 self::drop_database($displayprogress);
7e7cfe7a
PS
406 }
407
408 /**
409 * Perform a fresh test site installation
410 *
411 * Note: To be used from CLI scripts only.
412 *
413 * @static
414 * @return void may terminate execution with exit code
415 */
416 public static function install_site() {
417 global $DB, $CFG;
418
419 if (!self::is_test_site()) {
420 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!');
421 }
422
423 if ($DB->get_tables()) {
424 list($errorcode, $message) = phpunit_util::testing_ready_problem();
425 if ($errorcode) {
426 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
427 } else {
428 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
429 }
430 }
431
432 $options = array();
433 $options['adminpass'] = 'admin';
434 $options['shortname'] = 'phpunit';
435 $options['fullname'] = 'PHPUnit test site';
436
437 install_cli_database($options, false);
438
439 // install timezone info
440 $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
441 update_timezone_records($timezones);
442
0ea35584
DM
443 // Store version hash in the database and in a file.
444 self::store_versions_hash();
7e7cfe7a 445
0ea35584
DM
446 // Store database data and structure.
447 self::store_database_state();
7e7cfe7a
PS
448 }
449
450 /**
451 * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist
452 * @static
453 * @return bool true means main config file created, false means only dataroot file created
454 */
455 public static function build_config_file() {
456 global $CFG;
457
458 $template = '
564fcb3b 459 <testsuite name="@component@ test suite">
7e7cfe7a
PS
460 <directory suffix="_test.php">@dir@</directory>
461 </testsuite>';
462 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
463
464 $suites = '';
465
46f6f7f2 466 $plugintypes = core_component::get_plugin_types();
7e7cfe7a
PS
467 ksort($plugintypes);
468 foreach ($plugintypes as $type=>$unused) {
bd3b3bba 469 $plugs = core_component::get_plugin_list($type);
7e7cfe7a
PS
470 ksort($plugs);
471 foreach ($plugs as $plug=>$fullplug) {
472 if (!file_exists("$fullplug/tests/")) {
473 continue;
474 }
475 $dir = substr($fullplug, strlen($CFG->dirroot)+1);
476 $dir .= '/tests';
477 $component = $type.'_'.$plug;
478
479 $suite = str_replace('@component@', $component, $template);
480 $suite = str_replace('@dir@', $dir, $suite);
481
482 $suites .= $suite;
483 }
484 }
485
486 $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
487
488 $result = false;
489 if (is_writable($CFG->dirroot)) {
490 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
0ea35584 491 testing_fix_file_permissions("$CFG->dirroot/phpunit.xml");
7e7cfe7a
PS
492 }
493 }
494
495 // relink - it seems that xml:base does not work in phpunit xml files, remove this nasty hack if you find a way to set xml base for relative refs
496 $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data);
497 $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|',
498 '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>',
499 $data);
500 file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
0ea35584 501 testing_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
7e7cfe7a
PS
502
503 return (bool)$result;
504 }
505
506 /**
507 * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist
508 *
509 * @static
510 * @return void, stops if can not write files
511 */
512 public static function build_component_config_files() {
513 global $CFG;
514
515 $template = '
516 <testsuites>
517 <testsuite name="@component@">
518 <directory suffix="_test.php">.</directory>
519 </testsuite>
520 </testsuites>';
521
522 // Use the upstream file as source for the distributed configurations
523 $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
524 $ftemplate = preg_replace('|<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate);
525
f8228402
DM
526 // Gets all the components with tests
527 $components = tests_finder::get_components_with_tests('phpunit');
7e7cfe7a
PS
528
529 // Create the corresponding phpunit.xml file for each component
530 foreach ($components as $cname => $cpath) {
531 // Calculate the component suite
532 $ctemplate = $template;
533 $ctemplate = str_replace('@component@', $cname, $ctemplate);
534
535 // Apply it to the file template
536 $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate);
537
538 // fix link to schema
539 $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/');
ed7259d1 540 $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents);
7e7cfe7a
PS
541
542 // Write the file
543 $result = false;
544 if (is_writable($cpath)) {
545 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) {
0ea35584 546 testing_fix_file_permissions("$cpath/phpunit.xml");
7e7cfe7a
PS
547 }
548 }
549 // Problems writing file, throw error
550 if (!$result) {
551 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions");
552 }
553 }
554 }
555
ef5b5e05
PS
556 /**
557 * To be called from debugging() only.
558 * @param string $message
559 * @param int $level
560 * @param string $from
561 */
562 public static function debugging_triggered($message, $level, $from) {
563 // Store only if debugging triggered from actual test,
564 // we need normal debugging outside of tests to find problems in our phpunit integration.
565 $backtrace = debug_backtrace();
566
567 foreach ($backtrace as $bt) {
568 $intest = false;
569 if (isset($bt['object']) and is_object($bt['object'])) {
570 if ($bt['object'] instanceof PHPUnit_Framework_TestCase) {
571 if (strpos($bt['function'], 'test') === 0) {
572 $intest = true;
573 break;
574 }
575 }
576 }
577 }
578 if (!$intest) {
579 return false;
580 }
581
582 $debug = new stdClass();
583 $debug->message = $message;
584 $debug->level = $level;
585 $debug->from = $from;
586
587 self::$debuggings[] = $debug;
588
589 return true;
590 }
591
592 /**
593 * Resets the list of debugging messages.
594 */
595 public static function reset_debugging() {
596 self::$debuggings = array();
597 }
598
599 /**
600 * Returns all debugging messages triggered during test.
601 * @return array with instances having message, level and stacktrace property.
602 */
603 public static function get_debugging_messages() {
604 return self::$debuggings;
605 }
606
607 /**
608 * Prints out any debug messages accumulated during test execution.
609 * @return bool false if no debug messages, true if debug triggered
610 */
611 public static function display_debugging_messages() {
612 if (empty(self::$debuggings)) {
613 return false;
614 }
615 foreach(self::$debuggings as $debug) {
616 echo 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n";
617 }
618
619 return true;
620 }
4c9e03f0
PS
621
622 /**
623 * Start message redirection.
624 *
625 * Note: Do not call directly from tests,
626 * use $sink = $this->redirectMessages() instead.
627 *
628 * @return phpunit_message_sink
629 */
630 public static function start_message_redirection() {
631 if (self::$messagesink) {
632 self::stop_message_redirection();
633 }
634 self::$messagesink = new phpunit_message_sink();
635 return self::$messagesink;
636 }
637
638 /**
639 * End message redirection.
640 *
641 * Note: Do not call directly from tests,
642 * use $sink->close() instead.
643 */
644 public static function stop_message_redirection() {
645 self::$messagesink = null;
646 }
647
648 /**
649 * Are messages redirected to some sink?
650 *
651 * Note: to be called from messagelib.php only!
652 *
653 * @return bool
654 */
655 public static function is_redirecting_messages() {
656 return !empty(self::$messagesink);
657 }
658
659 /**
660 * To be called from messagelib.php only!
661 *
662 * @param stdClass $message record from message_read table
663 * @return bool true means send message, false means message "sent" to sink.
664 */
665 public static function message_sent($message) {
666 if (self::$messagesink) {
667 self::$messagesink->add_message($message);
668 }
669 }
62401e8f
PS
670
671 /**
672 * Start event redirection.
673 *
674 * @private
675 * Note: Do not call directly from tests,
676 * use $sink = $this->redirectEvents() instead.
677 *
678 * @return phpunit_event_sink
679 */
680 public static function start_event_redirection() {
681 if (self::$eventsink) {
682 self::stop_event_redirection();
683 }
684 self::$eventsink = new phpunit_event_sink();
685 return self::$eventsink;
686 }
687
688 /**
689 * End event redirection.
690 *
691 * @private
692 * Note: Do not call directly from tests,
693 * use $sink->close() instead.
694 */
695 public static function stop_event_redirection() {
696 self::$eventsink = null;
697 }
698
699 /**
700 * Are events redirected to some sink?
701 *
702 * Note: to be called from \core\event\base only!
703 *
704 * @private
705 * @return bool
706 */
707 public static function is_redirecting_events() {
708 return !empty(self::$eventsink);
709 }
710
711 /**
712 * To be called from \core\event\base only!
713 *
714 * @private
715 * @param \core\event\base $event record from event_read table
716 * @return bool true means send event, false means event "sent" to sink.
717 */
718 public static function event_triggered(\core\event\base $event) {
719 if (self::$eventsink) {
720 self::$eventsink->add_event($event);
721 }
722 }
7e7cfe7a 723}