MDL-37458 testing tests finder externalization
[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
26
27/**
28 * Collection of utility methods.
29 *
30 * @package core
31 * @category phpunit
32 * @copyright 2012 Petr Skoda {@link http://skodak.org}
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35class phpunit_util {
36 /** @var string current version hash from php files */
37 protected static $versionhash = null;
38
39 /** @var array original content of all database tables*/
40 protected static $tabledata = null;
41
42 /** @var array original structure of all database tables */
43 protected static $tablestructure = null;
44
45 /** @var array original structure of all database tables */
46 protected static $sequencenames = null;
47
48 /** @var array An array of original globals, restored after each test */
49 protected static $globals = array();
50
51 /** @var int last value of db writes counter, used for db resetting */
52 public static $lastdbwrites = null;
53
5c3c2c81 54 /** @var testing_data_generator */
7e7cfe7a
PS
55 protected static $generator = null;
56
ef5b5e05
PS
57 /** @var array list of debugging messages triggered during the last test execution */
58 protected static $debuggings = array();
59
4c9e03f0
PS
60 /** @var phpunit_message_sink alternative target for moodle messaging */
61 protected static $messagesink = null;
62
7e7cfe7a
PS
63 /**
64 * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
65 *
66 * Note: do not call manually!
67 *
68 * @internal
69 * @static
70 * @return void
71 */
72 public static function acquire_test_lock() {
3f7211f4 73 test_lock::acquire('phpunit');
7e7cfe7a
PS
74 }
75
76 /**
77 * Note: do not call manually!
78 * @internal
79 * @static
80 * @return void
81 */
82 public static function release_test_lock() {
3f7211f4 83 test_lock::release('phpunit');
7e7cfe7a
PS
84 }
85
86 /**
87 * Load global $CFG;
88 * @internal
89 * @static
90 * @return void
91 */
92 public static function initialise_cfg() {
93 global $DB;
94 $dbhash = false;
95 try {
96 $dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest'));
97 } catch (Exception $e) {
98 // not installed yet
99 initialise_cfg();
100 return;
101 }
102 if ($dbhash !== phpunit_util::get_version_hash()) {
103 // do not set CFG - the only way forward is to drop and reinstall
104 return;
105 }
106 // standard CFG init
107 initialise_cfg();
108 }
109
110 /**
111 * Get data generator
112 * @static
5c3c2c81 113 * @return testing_data_generator
7e7cfe7a
PS
114 */
115 public static function get_data_generator() {
116 if (is_null(self::$generator)) {
5c3c2c81
DM
117 require_once(__DIR__.'/../../testing/generator/lib.php');
118 self::$generator = new testing_data_generator();
7e7cfe7a
PS
119 }
120 return self::$generator;
121 }
122
123 /**
124 * Returns contents of all tables right after installation.
125 * @static
126 * @return array $table=>$records
127 */
128 protected static function get_tabledata() {
129 global $CFG;
130
131 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
132 // not initialised yet
133 return array();
134 }
135
136 if (!isset(self::$tabledata)) {
137 $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser");
138 self::$tabledata = unserialize($data);
139 }
140
141 if (!is_array(self::$tabledata)) {
142 phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.');
143 }
144
145 return self::$tabledata;
146 }
147
148 /**
149 * Returns structure of all tables right after installation.
150 * @static
151 * @return array $table=>$records
152 */
153 public static function get_tablestructure() {
154 global $CFG;
155
156 if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
157 // not initialised yet
158 return array();
159 }
160
161 if (!isset(self::$tablestructure)) {
162 $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
163 self::$tablestructure = unserialize($data);
164 }
165
166 if (!is_array(self::$tablestructure)) {
167 phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.');
168 }
169
170 return self::$tablestructure;
171 }
172
173 /**
174 * Returns the names of sequences for each autoincrementing id field in all standard tables.
175 * @static
176 * @return array $table=>$sequencename
177 */
178 public static function get_sequencenames() {
179 global $DB;
180
181 if (isset(self::$sequencenames)) {
182 return self::$sequencenames;
183 }
184
185 if (!$structure = self::get_tablestructure()) {
186 return array();
187 }
188
189 self::$sequencenames = array();
190 foreach ($structure as $table=>$ignored) {
191 $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
192 if ($name !== false) {
193 self::$sequencenames[$table] = $name;
194 }
195 }
196
197 return self::$sequencenames;
198 }
199
200 /**
201 * Returns list of tables that are unmodified and empty.
202 *
203 * @static
204 * @return array of table names, empty if unknown
205 */
206 protected static function guess_unmodified_empty_tables() {
207 global $DB;
208
209 $dbfamily = $DB->get_dbfamily();
210
211 if ($dbfamily === 'mysql') {
212 $empties = array();
213 $prefix = $DB->get_prefix();
214 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
215 foreach ($rs as $info) {
216 $table = strtolower($info->name);
217 if (strpos($table, $prefix) !== 0) {
218 // incorrect table match caused by _
219 continue;
220 }
221 if (!is_null($info->auto_increment)) {
222 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
223 if ($info->auto_increment == 1) {
224 $empties[$table] = $table;
225 }
226 }
227 }
228 $rs->close();
229 return $empties;
230
231 } else if ($dbfamily === 'mssql') {
232 $empties = array();
233 $prefix = $DB->get_prefix();
234 $sql = "SELECT t.name
235 FROM sys.identity_columns i
236 JOIN sys.tables t ON t.object_id = i.object_id
237 WHERE t.name LIKE ?
238 AND i.name = 'id'
239 AND i.last_value IS NULL";
240 $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
241 foreach ($rs as $info) {
242 $table = strtolower($info->name);
243 if (strpos($table, $prefix) !== 0) {
244 // incorrect table match caused by _
245 continue;
246 }
247 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
248 $empties[$table] = $table;
249 }
250 $rs->close();
251 return $empties;
252
253 } else if ($dbfamily === 'oracle') {
254 $sequences = phpunit_util::get_sequencenames();
255 $sequences = array_map('strtoupper', $sequences);
256 $lookup = array_flip($sequences);
257 $empties = array();
258 list($seqs, $params) = $DB->get_in_or_equal($sequences);
259 $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
260 $rs = $DB->get_recordset_sql($sql, $params);
261 foreach ($rs as $seq) {
262 $table = $lookup[$seq->sequence_name];
263 $empties[$table] = $table;
264 }
265 $rs->close();
266 return $empties;
267
268 } else {
269 return array();
270 }
271 }
272
273 /**
274 * Reset all database sequences to initial values.
275 *
276 * @static
277 * @param array $empties tables that are known to be unmodified and empty
278 * @return void
279 */
280 public static function reset_all_database_sequences(array $empties = null) {
281 global $DB;
282
283 if (!$data = self::get_tabledata()) {
284 // not initialised yet
285 return;
286 }
287 if (!$structure = self::get_tablestructure()) {
288 // not initialised yet
289 return;
290 }
291
292 $dbfamily = $DB->get_dbfamily();
293 if ($dbfamily === 'postgres') {
294 $queries = array();
295 $prefix = $DB->get_prefix();
296 foreach ($data as $table=>$records) {
297 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
298 if (empty($records)) {
299 $nextid = 1;
300 } else {
301 $lastrecord = end($records);
302 $nextid = $lastrecord->id + 1;
303 }
304 $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
305 }
306 }
307 if ($queries) {
308 $DB->change_database_structure(implode(';', $queries));
309 }
310
311 } else if ($dbfamily === 'mysql') {
312 $sequences = array();
313 $prefix = $DB->get_prefix();
314 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
315 foreach ($rs as $info) {
316 $table = strtolower($info->name);
317 if (strpos($table, $prefix) !== 0) {
318 // incorrect table match caused by _
319 continue;
320 }
321 if (!is_null($info->auto_increment)) {
322 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
323 $sequences[$table] = $info->auto_increment;
324 }
325 }
326 $rs->close();
327 $prefix = $DB->get_prefix();
328 foreach ($data as $table=>$records) {
329 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
330 if (isset($sequences[$table])) {
331 if (empty($records)) {
332 $nextid = 1;
333 } else {
334 $lastrecord = end($records);
335 $nextid = $lastrecord->id + 1;
336 }
337 if ($sequences[$table] != $nextid) {
338 $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
339 }
340
341 } else {
342 // some problem exists, fallback to standard code
343 $DB->get_manager()->reset_sequence($table);
344 }
345 }
346 }
347
348 } else if ($dbfamily === 'oracle') {
349 $sequences = phpunit_util::get_sequencenames();
350 $sequences = array_map('strtoupper', $sequences);
351 $lookup = array_flip($sequences);
352
353 $current = array();
354 list($seqs, $params) = $DB->get_in_or_equal($sequences);
355 $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
356 $rs = $DB->get_recordset_sql($sql, $params);
357 foreach ($rs as $seq) {
358 $table = $lookup[$seq->sequence_name];
359 $current[$table] = $seq->last_number;
360 }
361 $rs->close();
362
363 foreach ($data as $table=>$records) {
364 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
365 $lastrecord = end($records);
366 if ($lastrecord) {
367 $nextid = $lastrecord->id + 1;
368 } else {
369 $nextid = 1;
370 }
371 if (!isset($current[$table])) {
372 $DB->get_manager()->reset_sequence($table);
373 } else if ($nextid == $current[$table]) {
374 continue;
375 }
376 // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
377 $seqname = $sequences[$table];
378 $cachesize = $DB->get_manager()->generator->sequence_cache_size;
379 $DB->change_database_structure("DROP SEQUENCE $seqname");
380 $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
381 }
382 }
383
384 } else {
385 // note: does mssql support any kind of faster reset?
386 if (is_null($empties)) {
387 $empties = self::guess_unmodified_empty_tables();
388 }
389 foreach ($data as $table=>$records) {
390 if (isset($empties[$table])) {
391 continue;
392 }
393 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
394 $DB->get_manager()->reset_sequence($table);
395 }
396 }
397 }
398 }
399
400 /**
401 * Reset all database tables to default values.
402 * @static
403 * @return bool true if reset done, false if skipped
404 */
405 public static function reset_database() {
406 global $DB;
407
408 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
409 return false;
410 }
411
412 $tables = $DB->get_tables(false);
413 if (!$tables or empty($tables['config'])) {
414 // not installed yet
415 return false;
416 }
417
418 if (!$data = self::get_tabledata()) {
419 // not initialised yet
420 return false;
421 }
422 if (!$structure = self::get_tablestructure()) {
423 // not initialised yet
424 return false;
425 }
426
427 $empties = self::guess_unmodified_empty_tables();
428
429 foreach ($data as $table=>$records) {
430 if (empty($records)) {
431 if (isset($empties[$table])) {
432 // table was not modified and is empty
433 } else {
434 $DB->delete_records($table, array());
435 }
436 continue;
437 }
438
439 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
440 $currentrecords = $DB->get_records($table, array(), 'id ASC');
441 $changed = false;
442 foreach ($records as $id=>$record) {
443 if (!isset($currentrecords[$id])) {
444 $changed = true;
445 break;
446 }
447 if ((array)$record != (array)$currentrecords[$id]) {
448 $changed = true;
449 break;
450 }
451 unset($currentrecords[$id]);
452 }
453 if (!$changed) {
454 if ($currentrecords) {
455 $lastrecord = end($records);
456 $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
457 continue;
458 } else {
459 continue;
460 }
461 }
462 }
463
464 $DB->delete_records($table, array());
465 foreach ($records as $record) {
466 $DB->import_record($table, $record, false, true);
467 }
468 }
469
470 // reset all next record ids - aka sequences
471 self::reset_all_database_sequences($empties);
472
473 // remove extra tables
474 foreach ($tables as $table) {
475 if (!isset($data[$table])) {
476 $DB->get_manager()->drop_table(new xmldb_table($table));
477 }
478 }
479
480 self::$lastdbwrites = $DB->perf_get_writes();
481
482 return true;
483 }
484
485 /**
486 * Purge dataroot directory
487 * @static
488 * @return void
489 */
490 public static function reset_dataroot() {
491 global $CFG;
492
493 $handle = opendir($CFG->dataroot);
494 $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
495 while (false !== ($item = readdir($handle))) {
496 if (in_array($item, $skip)) {
497 continue;
498 }
499 if (is_dir("$CFG->dataroot/$item")) {
500 remove_dir("$CFG->dataroot/$item", false);
501 } else {
502 unlink("$CFG->dataroot/$item");
503 }
504 }
505 closedir($handle);
506 make_temp_directory('');
507 make_cache_directory('');
508 make_cache_directory('htmlpurifier');
52642d0d 509 // Reset the cache API so that it recreates it's required directories as well.
52642d0d 510 cache_factory::reset();
34c84c72
SH
511 // Purge all data from the caches. This is required for consistency.
512 // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
513 // and now we will purge any other caches as well.
514 cache_helper::purge_all();
7e7cfe7a
PS
515 }
516
517 /**
518 * Reset contents of all database tables to initial values, reset caches, etc.
519 *
520 * Note: this is relatively slow (cca 2 seconds for pg and 7 for mysql) - please use with care!
521 *
522 * @static
523 * @param bool $logchanges log changes in global state and database in error log
524 * @return void
525 */
526 public static function reset_all_data($logchanges = false) {
dd420aba 527 global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE;
7e7cfe7a 528
4c9e03f0
PS
529 // Stop any message redirection.
530 phpunit_util::stop_message_redirection();
531
d03d5113
PS
532 // Release memory and indirectly call destroy() methods to release resource handles, etc.
533 gc_collect_cycles();
534
ef5b5e05
PS
535 // Show any unhandled debugging messages, the runbare() could already reset it.
536 self::display_debugging_messages();
537 self::reset_debugging();
538
7e7cfe7a
PS
539 // reset global $DB in case somebody mocked it
540 $DB = self::get_global_backup('DB');
541
542 if ($DB->is_transaction_started()) {
543 // we can not reset inside transaction
544 $DB->force_transaction_rollback();
545 }
546
547 $resetdb = self::reset_database();
548 $warnings = array();
549
550 if ($logchanges) {
551 if ($resetdb) {
552 $warnings[] = 'Warning: unexpected database modification, resetting DB state';
553 }
554
555 $oldcfg = self::get_global_backup('CFG');
556 $oldsite = self::get_global_backup('SITE');
557 foreach($CFG as $k=>$v) {
558 if (!property_exists($oldcfg, $k)) {
559 $warnings[] = 'Warning: unexpected new $CFG->'.$k.' value';
560 } else if ($oldcfg->$k !== $CFG->$k) {
561 $warnings[] = 'Warning: unexpected change of $CFG->'.$k.' value';
562 }
563 unset($oldcfg->$k);
564
565 }
566 if ($oldcfg) {
567 foreach($oldcfg as $k=>$v) {
568 $warnings[] = 'Warning: unexpected removal of $CFG->'.$k;
569 }
570 }
571
572 if ($USER->id != 0) {
573 $warnings[] = 'Warning: unexpected change of $USER';
574 }
575
576 if ($COURSE->id != $oldsite->id) {
577 $warnings[] = 'Warning: unexpected change of $COURSE';
578 }
579 }
580
581 // restore original globals
582 $_SERVER = self::get_global_backup('_SERVER');
583 $CFG = self::get_global_backup('CFG');
584 $SITE = self::get_global_backup('SITE');
585 $COURSE = $SITE;
586
587 // reinitialise following globals
588 $OUTPUT = new bootstrap_renderer();
589 $PAGE = new moodle_page();
590 $FULLME = null;
591 $ME = null;
592 $SCRIPT = null;
593 $SESSION = new stdClass();
594 $_SESSION['SESSION'] =& $SESSION;
595
596 // set fresh new not-logged-in user
597 $user = new stdClass();
598 $user->id = 0;
599 $user->mnethostid = $CFG->mnet_localhost_id;
600 session_set_user($user);
601
602 // reset all static caches
603 accesslib_clear_all_caches(true);
a46e11b5
PS
604 get_string_manager()->reset_caches(true);
605 reset_text_filters_cache(true);
7e7cfe7a
PS
606 events_get_handlers('reset');
607 textlib::reset_caches();
6fd1cf05
MG
608 if (class_exists('repository')) {
609 repository::reset_caches();
610 }
dd420aba 611 $GROUPLIB_CACHE = null;
6fd1cf05 612 //TODO MDL-25290: add more resets here and probably refactor them to new core function
7e7cfe7a 613
2beba297 614 // Reset course and module caches.
13d5c938 615 if (class_exists('format_base')) {
2beba297 616 // If file containing class is not loaded, there is no cache there anyway.
13d5c938
PS
617 format_base::reset_course_cache(0);
618 }
b46be6ad 619 get_fast_modinfo(0, 0, true);
13d5c938 620
98547432
621 // Reset other singletons.
622 if (class_exists('plugin_manager')) {
623 plugin_manager::reset_caches(true);
624 }
625 if (class_exists('available_update_checker')) {
626 available_update_checker::reset_caches(true);
627 }
dc11af19
DM
628 if (class_exists('available_update_deployer')) {
629 available_update_deployer::reset_caches(true);
630 }
98547432 631
7e7cfe7a
PS
632 // purge dataroot directory
633 self::reset_dataroot();
634
635 // restore original config once more in case resetting of caches changed CFG
636 $CFG = self::get_global_backup('CFG');
637
638 // inform data generator
639 self::get_data_generator()->reset();
640
641 // fix PHP settings
642 error_reporting($CFG->debug);
643
644 // verify db writes just in case something goes wrong in reset
645 if (self::$lastdbwrites != $DB->perf_get_writes()) {
646 error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
647 self::$lastdbwrites = $DB->perf_get_writes();
648 }
649
650 if ($warnings) {
651 $warnings = implode("\n", $warnings);
652 trigger_error($warnings, E_USER_WARNING);
653 }
654 }
655
656 /**
657 * Called during bootstrap only!
658 * @internal
659 * @static
660 * @return void
661 */
662 public static function bootstrap_init() {
663 global $CFG, $SITE, $DB;
664
665 // backup the globals
666 self::$globals['_SERVER'] = $_SERVER;
667 self::$globals['CFG'] = clone($CFG);
668 self::$globals['SITE'] = clone($SITE);
669 self::$globals['DB'] = $DB;
670
671 // refresh data in all tables, clear caches, etc.
672 phpunit_util::reset_all_data();
673 }
674
675 /**
676 * Returns original state of global variable.
677 * @static
678 * @param string $name
679 * @return mixed
680 */
681 public static function get_global_backup($name) {
682 if ($name === 'DB') {
683 // no cloning of database object,
684 // we just need the original reference, not original state
685 return self::$globals['DB'];
686 }
687 if (isset(self::$globals[$name])) {
688 if (is_object(self::$globals[$name])) {
689 $return = clone(self::$globals[$name]);
690 return $return;
691 } else {
692 return self::$globals[$name];
693 }
694 }
695 return null;
696 }
697
698 /**
699 * Does this site (db and dataroot) appear to be used for production?
700 * We try very hard to prevent accidental damage done to production servers!!
701 *
702 * @static
703 * @return bool
704 */
705 public static function is_test_site() {
706 global $DB, $CFG;
707
708 if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) {
709 // this is already tested in bootstrap script,
710 // but anyway presence of this file means the dataroot is for testing
711 return false;
712 }
713
714 $tables = $DB->get_tables(false);
715 if ($tables) {
716 if (!$DB->get_manager()->table_exists('config')) {
717 return false;
718 }
719 if (!get_config('core', 'phpunittest')) {
720 return false;
721 }
722 }
723
724 return true;
725 }
726
727 /**
728 * Is this site initialised to run unit tests?
729 *
730 * @static
731 * @return int array errorcode=>message, 0 means ok
732 */
733 public static function testing_ready_problem() {
734 global $CFG, $DB;
735
736 $tables = $DB->get_tables(false);
737
738 if (!self::is_test_site()) {
739 // dataroot was verified in bootstrap, so it must be DB
740 return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
741 }
742
743 if (empty($tables)) {
744 return array(PHPUNIT_EXITCODE_INSTALL, '');
745 }
746
747 if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
748 return array(PHPUNIT_EXITCODE_REINSTALL, '');
749 }
750
751 if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
752 return array(PHPUNIT_EXITCODE_REINSTALL, '');
753 }
754
755 $hash = phpunit_util::get_version_hash();
756 $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
757
758 if ($hash !== $oldhash) {
759 return array(PHPUNIT_EXITCODE_REINSTALL, '');
760 }
761
762 $dbhash = get_config('core', 'phpunittest');
763 if ($hash !== $dbhash) {
764 return array(PHPUNIT_EXITCODE_REINSTALL, '');
765 }
766
767 return array(0, '');
768 }
769
770 /**
771 * Drop all test site data.
772 *
773 * Note: To be used from CLI scripts only.
774 *
775 * @static
85b72a75 776 * @param bool $displayprogress if true, this method will echo progress information.
7e7cfe7a
PS
777 * @return void may terminate execution with exit code
778 */
85b72a75 779 public static function drop_site($displayprogress = false) {
7e7cfe7a
PS
780 global $DB, $CFG;
781
782 if (!self::is_test_site()) {
783 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
784 }
785
85b72a75
TH
786 // Purge dataroot
787 if ($displayprogress) {
788 echo "Purging dataroot:\n";
789 }
7e7cfe7a
PS
790 self::reset_dataroot();
791 phpunit_bootstrap_initdataroot($CFG->dataroot);
792 $keep = array('.', '..', 'lock', 'webrunner.xml');
793 $files = scandir("$CFG->dataroot/phpunit");
794 foreach ($files as $file) {
795 if (in_array($file, $keep)) {
796 continue;
797 }
798 $path = "$CFG->dataroot/phpunit/$file";
799 if (is_dir($path)) {
800 remove_dir($path, false);
801 } else {
802 unlink($path);
803 }
804 }
805
806 // drop all tables
807 $tables = $DB->get_tables(false);
808 if (isset($tables['config'])) {
809 // config always last to prevent problems with interrupted drops!
810 unset($tables['config']);
811 $tables['config'] = 'config';
812 }
85b72a75
TH
813
814 if ($displayprogress) {
815 echo "Dropping tables:\n";
816 }
817 $dotsonline = 0;
7e7cfe7a
PS
818 foreach ($tables as $tablename) {
819 $table = new xmldb_table($tablename);
820 $DB->get_manager()->drop_table($table);
85b72a75
TH
821
822 if ($dotsonline == 60) {
823 if ($displayprogress) {
824 echo "\n";
825 }
826 $dotsonline = 0;
827 }
828 if ($displayprogress) {
829 echo '.';
830 }
831 $dotsonline += 1;
832 }
833 if ($displayprogress) {
834 echo "\n";
7e7cfe7a
PS
835 }
836 }
837
838 /**
839 * Perform a fresh test site installation
840 *
841 * Note: To be used from CLI scripts only.
842 *
843 * @static
844 * @return void may terminate execution with exit code
845 */
846 public static function install_site() {
847 global $DB, $CFG;
848
849 if (!self::is_test_site()) {
850 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not install on non-test site!!');
851 }
852
853 if ($DB->get_tables()) {
854 list($errorcode, $message) = phpunit_util::testing_ready_problem();
855 if ($errorcode) {
856 phpunit_bootstrap_error(PHPUNIT_EXITCODE_REINSTALL, 'Database tables already present, Moodle PHPUnit test environment can not be initialised');
857 } else {
858 phpunit_bootstrap_error(0, 'Moodle PHPUnit test environment is already initialised');
859 }
860 }
861
862 $options = array();
863 $options['adminpass'] = 'admin';
864 $options['shortname'] = 'phpunit';
865 $options['fullname'] = 'PHPUnit test site';
866
867 install_cli_database($options, false);
868
869 // install timezone info
870 $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
871 update_timezone_records($timezones);
872
873 // add test db flag
874 $hash = phpunit_util::get_version_hash();
875 set_config('phpunittest', $hash);
876
877 // store data for all tables
878 $data = array();
879 $structure = array();
880 $tables = $DB->get_tables();
881 foreach ($tables as $table) {
882 $columns = $DB->get_columns($table);
883 $structure[$table] = $columns;
884 if (isset($columns['id']) and $columns['id']->auto_increment) {
885 $data[$table] = $DB->get_records($table, array(), 'id ASC');
886 } else {
887 // there should not be many of these
888 $data[$table] = $DB->get_records($table, array());
889 }
890 }
891 $data = serialize($data);
892 file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
893 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
894
895 $structure = serialize($structure);
896 file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
897 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
898
899 // hash all plugin versions - helps with very fast detection of db structure changes
900 file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
901 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
902 }
903
904 /**
905 * Calculate unique version hash for all plugins and core.
906 * @static
907 * @return string sha1 hash
908 */
909 public static function get_version_hash() {
910 global $CFG;
911
912 if (self::$versionhash) {
913 return self::$versionhash;
914 }
915
916 $versions = array();
917
918 // main version first
919 $version = null;
920 include($CFG->dirroot.'/version.php');
921 $versions['core'] = $version;
922
923 // modules
924 $mods = get_plugin_list('mod');
925 ksort($mods);
926 foreach ($mods as $mod => $fullmod) {
927 $module = new stdClass();
928 $module->version = null;
929 include($fullmod.'/version.php');
930 $versions[$mod] = $module->version;
931 }
932
933 // now the rest of plugins
934 $plugintypes = get_plugin_types();
935 unset($plugintypes['mod']);
936 ksort($plugintypes);
937 foreach ($plugintypes as $type=>$unused) {
938 $plugs = get_plugin_list($type);
939 ksort($plugs);
940 foreach ($plugs as $plug=>$fullplug) {
941 $plugin = new stdClass();
942 $plugin->version = null;
943 @include($fullplug.'/version.php');
944 $versions[$plug] = $plugin->version;
945 }
946 }
947
948 self::$versionhash = sha1(serialize($versions));
949
950 return self::$versionhash;
951 }
952
953 /**
954 * Builds dirroot/phpunit.xml and dataroot/phpunit/webrunner.xml files using defaults from /phpunit.xml.dist
955 * @static
956 * @return bool true means main config file created, false means only dataroot file created
957 */
958 public static function build_config_file() {
959 global $CFG;
960
961 $template = '
564fcb3b 962 <testsuite name="@component@ test suite">
7e7cfe7a
PS
963 <directory suffix="_test.php">@dir@</directory>
964 </testsuite>';
965 $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
966
967 $suites = '';
968
969 $plugintypes = get_plugin_types();
970 ksort($plugintypes);
971 foreach ($plugintypes as $type=>$unused) {
972 $plugs = get_plugin_list($type);
973 ksort($plugs);
974 foreach ($plugs as $plug=>$fullplug) {
975 if (!file_exists("$fullplug/tests/")) {
976 continue;
977 }
978 $dir = substr($fullplug, strlen($CFG->dirroot)+1);
979 $dir .= '/tests';
980 $component = $type.'_'.$plug;
981
982 $suite = str_replace('@component@', $component, $template);
983 $suite = str_replace('@dir@', $dir, $suite);
984
985 $suites .= $suite;
986 }
987 }
988
989 $data = preg_replace('|<!--@plugin_suites_start@-->.*<!--@plugin_suites_end@-->|s', $suites, $data, 1);
990
991 $result = false;
992 if (is_writable($CFG->dirroot)) {
993 if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
994 phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
995 }
996 }
997
998 // 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
999 $data = str_replace('lib/phpunit/', $CFG->dirroot.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'phpunit'.DIRECTORY_SEPARATOR, $data);
1000 $data = preg_replace('|<directory suffix="_test.php">([^<]+)</directory>|',
1001 '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>',
1002 $data);
1003 file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
1004 phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
1005
1006 return (bool)$result;
1007 }
1008
1009 /**
1010 * Builds phpunit.xml files for all components using defaults from /phpunit.xml.dist
1011 *
1012 * @static
1013 * @return void, stops if can not write files
1014 */
1015 public static function build_component_config_files() {
1016 global $CFG;
1017
1018 $template = '
1019 <testsuites>
1020 <testsuite name="@component@">
1021 <directory suffix="_test.php">.</directory>
1022 </testsuite>
1023 </testsuites>';
1024
1025 // Use the upstream file as source for the distributed configurations
1026 $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
1027 $ftemplate = preg_replace('|<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate);
1028
f8228402
DM
1029 // Gets all the components with tests
1030 $components = tests_finder::get_components_with_tests('phpunit');
7e7cfe7a
PS
1031
1032 // Create the corresponding phpunit.xml file for each component
1033 foreach ($components as $cname => $cpath) {
1034 // Calculate the component suite
1035 $ctemplate = $template;
1036 $ctemplate = str_replace('@component@', $cname, $ctemplate);
1037
1038 // Apply it to the file template
1039 $fcontents = str_replace('<!--@component_suite@-->', $ctemplate, $ftemplate);
1040
1041 // fix link to schema
1042 $level = substr_count(str_replace('\\', '/', $cpath), '/') - substr_count(str_replace('\\', '/', $CFG->dirroot), '/');
ed7259d1 1043 $fcontents = str_replace('lib/phpunit/', str_repeat('../', $level).'lib/phpunit/', $fcontents);
7e7cfe7a
PS
1044
1045 // Write the file
1046 $result = false;
1047 if (is_writable($cpath)) {
1048 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) {
1049 phpunit_boostrap_fix_file_permissions("$cpath/phpunit.xml");
1050 }
1051 }
1052 // Problems writing file, throw error
1053 if (!$result) {
1054 phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGWARNING, "Can not create $cpath/phpunit.xml configuration file, verify dir permissions");
1055 }
1056 }
1057 }
1058
ef5b5e05
PS
1059 /**
1060 * To be called from debugging() only.
1061 * @param string $message
1062 * @param int $level
1063 * @param string $from
1064 */
1065 public static function debugging_triggered($message, $level, $from) {
1066 // Store only if debugging triggered from actual test,
1067 // we need normal debugging outside of tests to find problems in our phpunit integration.
1068 $backtrace = debug_backtrace();
1069
1070 foreach ($backtrace as $bt) {
1071 $intest = false;
1072 if (isset($bt['object']) and is_object($bt['object'])) {
1073 if ($bt['object'] instanceof PHPUnit_Framework_TestCase) {
1074 if (strpos($bt['function'], 'test') === 0) {
1075 $intest = true;
1076 break;
1077 }
1078 }
1079 }
1080 }
1081 if (!$intest) {
1082 return false;
1083 }
1084
1085 $debug = new stdClass();
1086 $debug->message = $message;
1087 $debug->level = $level;
1088 $debug->from = $from;
1089
1090 self::$debuggings[] = $debug;
1091
1092 return true;
1093 }
1094
1095 /**
1096 * Resets the list of debugging messages.
1097 */
1098 public static function reset_debugging() {
1099 self::$debuggings = array();
1100 }
1101
1102 /**
1103 * Returns all debugging messages triggered during test.
1104 * @return array with instances having message, level and stacktrace property.
1105 */
1106 public static function get_debugging_messages() {
1107 return self::$debuggings;
1108 }
1109
1110 /**
1111 * Prints out any debug messages accumulated during test execution.
1112 * @return bool false if no debug messages, true if debug triggered
1113 */
1114 public static function display_debugging_messages() {
1115 if (empty(self::$debuggings)) {
1116 return false;
1117 }
1118 foreach(self::$debuggings as $debug) {
1119 echo 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n";
1120 }
1121
1122 return true;
1123 }
4c9e03f0
PS
1124
1125 /**
1126 * Start message redirection.
1127 *
1128 * Note: Do not call directly from tests,
1129 * use $sink = $this->redirectMessages() instead.
1130 *
1131 * @return phpunit_message_sink
1132 */
1133 public static function start_message_redirection() {
1134 if (self::$messagesink) {
1135 self::stop_message_redirection();
1136 }
1137 self::$messagesink = new phpunit_message_sink();
1138 return self::$messagesink;
1139 }
1140
1141 /**
1142 * End message redirection.
1143 *
1144 * Note: Do not call directly from tests,
1145 * use $sink->close() instead.
1146 */
1147 public static function stop_message_redirection() {
1148 self::$messagesink = null;
1149 }
1150
1151 /**
1152 * Are messages redirected to some sink?
1153 *
1154 * Note: to be called from messagelib.php only!
1155 *
1156 * @return bool
1157 */
1158 public static function is_redirecting_messages() {
1159 return !empty(self::$messagesink);
1160 }
1161
1162 /**
1163 * To be called from messagelib.php only!
1164 *
1165 * @param stdClass $message record from message_read table
1166 * @return bool true means send message, false means message "sent" to sink.
1167 */
1168 public static function message_sent($message) {
1169 if (self::$messagesink) {
1170 self::$messagesink->add_message($message);
1171 }
1172 }
7e7cfe7a 1173}