MDL-37458 testing: Removing wrong comments
[moodle.git] / lib / testing / classes / util.php
CommitLineData
0ea35584
DM
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 * Testing util classes
19 *
20 * @abstract
21 * @package core
22 * @category test
23 * @copyright 2012 Petr Skoda {@link http://skodak.org}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27/**
28 * Utils for test sites creation
29 *
30 * @package core
31 * @category test
32 * @copyright 2012 Petr Skoda {@link http://skodak.org}
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35abstract class testing_util {
36
37 /**
38 * @var int last value of db writes counter, used for db resetting
39 */
40 public static $lastdbwrites = null;
41
42 /**
43 * @var testing_data_generator
44 */
45 protected static $generator = null;
46
47 /**
48 * @var string current version hash from php files
49 */
50 protected static $versionhash = null;
51
52 /**
53 * @var array original content of all database tables
54 */
55 protected static $tabledata = null;
56
57 /**
58 * @var array original structure of all database tables
59 */
60 protected static $tablestructure = null;
61
62 /**
63 * @var array original structure of all database tables
64 */
65 protected static $sequencenames = null;
66
67 /**
68 * Returns the testing framework name
69 * @static
70 * @return string
71 */
72 protected static final function get_framework() {
73 $classname = get_called_class();
74 return substr($classname, 0, strpos($classname, '_'));
75 }
76
77 /**
78 * Get data generator
79 * @static
80 * @return testing_data_generator
81 */
82 public static function get_data_generator() {
83 if (is_null(self::$generator)) {
84 require_once(__DIR__.'/../generator/lib.php');
85 self::$generator = new testing_data_generator();
86 }
87 return self::$generator;
88 }
89
90 /**
91 * Does this site (db and dataroot) appear to be used for production?
92 * We try very hard to prevent accidental damage done to production servers!!
93 *
94 * @static
95 * @return bool
96 */
97 public static function is_test_site() {
98 global $DB, $CFG;
99
100 $framework = self::get_framework();
101
102 if (!file_exists($CFG->dataroot . '/' . $framework . 'testdir.txt')) {
103 // this is already tested in bootstrap script,
104 // but anyway presence of this file means the dataroot is for testing
105 return false;
106 }
107
108 $tables = $DB->get_tables(false);
109 if ($tables) {
110 if (!$DB->get_manager()->table_exists('config')) {
111 return false;
112 }
113 if (!get_config('core', $framework . 'test')) {
114 return false;
115 }
116 }
117
118 return true;
119 }
120
121 /**
122 * Returns whether test database and dataroot were created using the current version codebase
123 *
1150aeb8 124 * @return bool
0ea35584
DM
125 */
126 protected static function is_test_data_updated() {
127 global $CFG;
128
129 $framework = self::get_framework();
130
131 $datarootpath = $CFG->dataroot . '/' . $framework;
132 if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) {
133 return false;
134 }
135
136 if (!file_exists($datarootpath . '/versionshash.txt')) {
137 return false;
138 }
139
140 $hash = self::get_version_hash();
141 $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
142
143 if ($hash !== $oldhash) {
144 return false;
145 }
146
147 $dbhash = get_config('core', $framework . 'test');
148 if ($hash !== $dbhash) {
149 return false;
150 }
151
152 return true;
153 }
154
155 /**
156 * Stores the status of the database
157 *
158 * Serializes the contents and the structure and
159 * stores it in the test framework space in dataroot
160 */
161 protected static function store_database_state() {
162 global $DB, $CFG;
163
164 $framework = self::get_framework();
165
166 // store data for all tables
167 $data = array();
168 $structure = array();
169 $tables = $DB->get_tables();
170 foreach ($tables as $table) {
171 $columns = $DB->get_columns($table);
172 $structure[$table] = $columns;
173 if (isset($columns['id']) and $columns['id']->auto_increment) {
174 $data[$table] = $DB->get_records($table, array(), 'id ASC');
175 } else {
176 // there should not be many of these
177 $data[$table] = $DB->get_records($table, array());
178 }
179 }
180 $data = serialize($data);
181 $datafile = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
182 file_put_contents($datafile, $data);
183 testing_fix_file_permissions($datafile);
184
185 $structure = serialize($structure);
186 $structurefile = $CFG->dataroot . '/' . $framework . '/tablestructure.ser';
187 file_put_contents($structurefile, $structure);
188 testing_fix_file_permissions($structurefile);
189 }
190
191 /**
192 * Stores the version hash in both database and dataroot
193 */
194 protected static function store_versions_hash() {
195 global $CFG;
196
197 $framework = self::get_framework();
198 $hash = self::get_version_hash();
199
200 // add test db flag
201 set_config($framework . 'test', $hash);
202
203 // hash all plugin versions - helps with very fast detection of db structure changes
204 $hashfile = $CFG->dataroot . '/' . $framework . '/versionshash.txt';
205 file_put_contents($hashfile, $hash);
206 testing_fix_file_permissions($hashfile);
207 }
208
209 /**
210 * Returns contents of all tables right after installation.
211 * @static
212 * @return array $table=>$records
213 */
214 protected static function get_tabledata() {
215 global $CFG;
216
217 $framework = self::get_framework();
218
219 $datafile = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
220 if (!file_exists($datafile)) {
221 // Not initialised yet.
222 return array();
223 }
224
225 if (!isset(self::$tabledata)) {
226 $data = file_get_contents($datafile);
227 self::$tabledata = unserialize($data);
228 }
229
230 if (!is_array(self::$tabledata)) {
231 testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
232 }
233
234 return self::$tabledata;
235 }
236
237 /**
238 * Returns structure of all tables right after installation.
239 * @static
240 * @return array $table=>$records
241 */
242 public static function get_tablestructure() {
243 global $CFG;
244
245 $framework = self::get_framework();
246
247 $structurefile = $CFG->dataroot . '/' . $framework . '/tablestructure.ser';
248 if (!file_exists($structurefile)) {
249 // Not initialised yet.
250 return array();
251 }
252
253 if (!isset(self::$tablestructure)) {
254 $data = file_get_contents($structurefile);
255 self::$tablestructure = unserialize($data);
256 }
257
258 if (!is_array(self::$tablestructure)) {
259 testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
260 }
261
262 return self::$tablestructure;
263 }
264
265 /**
266 * Returns the names of sequences for each autoincrementing id field in all standard tables.
267 * @static
268 * @return array $table=>$sequencename
269 */
270 public static function get_sequencenames() {
271 global $DB;
272
273 if (isset(self::$sequencenames)) {
274 return self::$sequencenames;
275 }
276
277 if (!$structure = self::get_tablestructure()) {
278 return array();
279 }
280
281 self::$sequencenames = array();
282 foreach ($structure as $table => $ignored) {
283 $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
284 if ($name !== false) {
285 self::$sequencenames[$table] = $name;
286 }
287 }
288
289 return self::$sequencenames;
290 }
291
292 /**
293 * Returns list of tables that are unmodified and empty.
294 *
295 * @static
296 * @return array of table names, empty if unknown
297 */
298 protected static function guess_unmodified_empty_tables() {
299 global $DB;
300
301 $dbfamily = $DB->get_dbfamily();
302
303 if ($dbfamily === 'mysql') {
304 $empties = array();
305 $prefix = $DB->get_prefix();
306 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
307 foreach ($rs as $info) {
308 $table = strtolower($info->name);
309 if (strpos($table, $prefix) !== 0) {
310 // incorrect table match caused by _
311 continue;
312 }
313 if (!is_null($info->auto_increment)) {
314 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
315 if ($info->auto_increment == 1) {
316 $empties[$table] = $table;
317 }
318 }
319 }
320 $rs->close();
321 return $empties;
322
323 } else if ($dbfamily === 'mssql') {
324 $empties = array();
325 $prefix = $DB->get_prefix();
326 $sql = "SELECT t.name
327 FROM sys.identity_columns i
328 JOIN sys.tables t ON t.object_id = i.object_id
329 WHERE t.name LIKE ?
330 AND i.name = 'id'
331 AND i.last_value IS NULL";
332 $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
333 foreach ($rs as $info) {
334 $table = strtolower($info->name);
335 if (strpos($table, $prefix) !== 0) {
336 // incorrect table match caused by _
337 continue;
338 }
339 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
340 $empties[$table] = $table;
341 }
342 $rs->close();
343 return $empties;
344
345 } else if ($dbfamily === 'oracle') {
346 $sequences = self::get_sequencenames();
347 $sequences = array_map('strtoupper', $sequences);
348 $lookup = array_flip($sequences);
349 $empties = array();
350 list($seqs, $params) = $DB->get_in_or_equal($sequences);
351 $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
352 $rs = $DB->get_recordset_sql($sql, $params);
353 foreach ($rs as $seq) {
354 $table = $lookup[$seq->sequence_name];
355 $empties[$table] = $table;
356 }
357 $rs->close();
358 return $empties;
359
360 } else {
361 return array();
362 }
363 }
364
365 /**
366 * Reset all database sequences to initial values.
367 *
368 * @static
369 * @param array $empties tables that are known to be unmodified and empty
370 * @return void
371 */
372 public static function reset_all_database_sequences(array $empties = null) {
373 global $DB;
374
375 if (!$data = self::get_tabledata()) {
376 // Not initialised yet.
377 return;
378 }
379 if (!$structure = self::get_tablestructure()) {
380 // Not initialised yet.
381 return;
382 }
383
384 $dbfamily = $DB->get_dbfamily();
385 if ($dbfamily === 'postgres') {
386 $queries = array();
387 $prefix = $DB->get_prefix();
388 foreach ($data as $table => $records) {
389 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
390 if (empty($records)) {
391 $nextid = 1;
392 } else {
393 $lastrecord = end($records);
394 $nextid = $lastrecord->id + 1;
395 }
396 $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
397 }
398 }
399 if ($queries) {
400 $DB->change_database_structure(implode(';', $queries));
401 }
402
403 } else if ($dbfamily === 'mysql') {
404 $sequences = array();
405 $prefix = $DB->get_prefix();
406 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
407 foreach ($rs as $info) {
408 $table = strtolower($info->name);
409 if (strpos($table, $prefix) !== 0) {
410 // incorrect table match caused by _
411 continue;
412 }
413 if (!is_null($info->auto_increment)) {
414 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
415 $sequences[$table] = $info->auto_increment;
416 }
417 }
418 $rs->close();
419 $prefix = $DB->get_prefix();
420 foreach ($data as $table => $records) {
421 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
422 if (isset($sequences[$table])) {
423 if (empty($records)) {
424 $nextid = 1;
425 } else {
426 $lastrecord = end($records);
427 $nextid = $lastrecord->id + 1;
428 }
429 if ($sequences[$table] != $nextid) {
430 $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
431 }
432
433 } else {
434 // some problem exists, fallback to standard code
435 $DB->get_manager()->reset_sequence($table);
436 }
437 }
438 }
439
440 } else if ($dbfamily === 'oracle') {
441 $sequences = self::get_sequencenames();
442 $sequences = array_map('strtoupper', $sequences);
443 $lookup = array_flip($sequences);
444
445 $current = array();
446 list($seqs, $params) = $DB->get_in_or_equal($sequences);
447 $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
448 $rs = $DB->get_recordset_sql($sql, $params);
449 foreach ($rs as $seq) {
450 $table = $lookup[$seq->sequence_name];
451 $current[$table] = $seq->last_number;
452 }
453 $rs->close();
454
455 foreach ($data as $table => $records) {
456 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
457 $lastrecord = end($records);
458 if ($lastrecord) {
459 $nextid = $lastrecord->id + 1;
460 } else {
461 $nextid = 1;
462 }
463 if (!isset($current[$table])) {
464 $DB->get_manager()->reset_sequence($table);
465 } else if ($nextid == $current[$table]) {
466 continue;
467 }
468 // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
469 $seqname = $sequences[$table];
470 $cachesize = $DB->get_manager()->generator->sequence_cache_size;
471 $DB->change_database_structure("DROP SEQUENCE $seqname");
472 $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
473 }
474 }
475
476 } else {
477 // note: does mssql support any kind of faster reset?
478 if (is_null($empties)) {
479 $empties = self::guess_unmodified_empty_tables();
480 }
481 foreach ($data as $table => $records) {
482 if (isset($empties[$table])) {
483 continue;
484 }
485 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
486 $DB->get_manager()->reset_sequence($table);
487 }
488 }
489 }
490 }
491
492 /**
1150aeb8 493 * Reset all database tables to default values.
0ea35584 494 * @static
1150aeb8 495 * @return bool true if reset done, false if skipped
0ea35584
DM
496 */
497 public static function reset_database() {
498 global $DB;
499
500 if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
501 return false;
502 }
503
504 $tables = $DB->get_tables(false);
505 if (!$tables or empty($tables['config'])) {
506 // not installed yet
507 return false;
508 }
509
510 if (!$data = self::get_tabledata()) {
511 // not initialised yet
512 return false;
513 }
514 if (!$structure = self::get_tablestructure()) {
515 // not initialised yet
516 return false;
517 }
518
519 $empties = self::guess_unmodified_empty_tables();
520
521 foreach ($data as $table => $records) {
522 if (empty($records)) {
523 if (isset($empties[$table])) {
524 // table was not modified and is empty
525 } else {
526 $DB->delete_records($table, array());
527 }
528 continue;
529 }
530
531 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
532 $currentrecords = $DB->get_records($table, array(), 'id ASC');
533 $changed = false;
534 foreach ($records as $id => $record) {
535 if (!isset($currentrecords[$id])) {
536 $changed = true;
537 break;
538 }
539 if ((array)$record != (array)$currentrecords[$id]) {
540 $changed = true;
541 break;
542 }
543 unset($currentrecords[$id]);
544 }
545 if (!$changed) {
546 if ($currentrecords) {
547 $lastrecord = end($records);
548 $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
549 continue;
550 } else {
551 continue;
552 }
553 }
554 }
555
556 $DB->delete_records($table, array());
557 foreach ($records as $record) {
558 $DB->import_record($table, $record, false, true);
559 }
560 }
561
562 // reset all next record ids - aka sequences
563 self::reset_all_database_sequences($empties);
564
565 // remove extra tables
566 foreach ($tables as $table) {
567 if (!isset($data[$table])) {
568 $DB->get_manager()->drop_table(new xmldb_table($table));
569 }
570 }
571
572 self::$lastdbwrites = $DB->perf_get_writes();
573
574 return true;
575 }
576
577 /**
578 * Purge dataroot directory
579 * @static
580 * @return void
581 */
582 public static function reset_dataroot() {
583 global $CFG;
584
585 $childclassname = self::get_framework() . '_util';
586
587 $handle = opendir($CFG->dataroot);
588 while (false !== ($item = readdir($handle))) {
589 if (in_array($item, $childclassname::$datarootskiponreset)) {
590 continue;
591 }
592 if (is_dir("$CFG->dataroot/$item")) {
593 remove_dir("$CFG->dataroot/$item", false);
594 } else {
595 unlink("$CFG->dataroot/$item");
596 }
597 }
598 closedir($handle);
599 make_temp_directory('');
600 make_cache_directory('');
601 make_cache_directory('htmlpurifier');
602 // Reset the cache API so that it recreates it's required directories as well.
603 cache_factory::reset();
604 // Purge all data from the caches. This is required for consistency.
605 // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
606 // and now we will purge any other caches as well.
607 cache_helper::purge_all();
608 }
609
610 /**
611 * Drop the whole test database
612 * @static
1150aeb8 613 * @param bool $displayprogress
0ea35584
DM
614 */
615 protected static function drop_database($displayprogress = false) {
616 global $DB;
617
618 $tables = $DB->get_tables(false);
619 if (isset($tables['config'])) {
620 // config always last to prevent problems with interrupted drops!
621 unset($tables['config']);
622 $tables['config'] = 'config';
623 }
624
625 if ($displayprogress) {
626 echo "Dropping tables:\n";
627 }
628 $dotsonline = 0;
629 foreach ($tables as $tablename) {
630 $table = new xmldb_table($tablename);
631 $DB->get_manager()->drop_table($table);
632
633 if ($dotsonline == 60) {
634 if ($displayprogress) {
635 echo "\n";
636 }
637 $dotsonline = 0;
638 }
639 if ($displayprogress) {
640 echo '.';
641 }
642 $dotsonline += 1;
643 }
644 if ($displayprogress) {
645 echo "\n";
646 }
647 }
648
649 /**
650 * Drops the test framework dataroot
651 * @static
652 */
653 protected static function drop_dataroot() {
654 global $CFG;
655
656 $framework = self::get_framework();
657 $childclassname = $framework . '_util';
658
659 $files = scandir($CFG->dataroot . '/' . $framework);
660 foreach ($files as $file) {
661 if (in_array($file, $childclassname::$datarootskipondrop)) {
662 continue;
663 }
664 $path = $CFG->dataroot . '/' . $framework . '/' . $file;
665 if (is_dir($path)) {
666 remove_dir($path, false);
667 } else {
668 unlink($path);
669 }
670 }
671 }
672
0ea35584
DM
673 /**
674 * Calculate unique version hash for all plugins and core.
675 * @static
676 * @return string sha1 hash
677 */
678 public static function get_version_hash() {
679 global $CFG;
680
681 if (self::$versionhash) {
682 return self::$versionhash;
683 }
684
685 $versions = array();
686
687 // main version first
688 $version = null;
689 include($CFG->dirroot.'/version.php');
690 $versions['core'] = $version;
691
692 // modules
693 $mods = get_plugin_list('mod');
694 ksort($mods);
695 foreach ($mods as $mod => $fullmod) {
696 $module = new stdClass();
697 $module->version = null;
698 include($fullmod.'/version.php');
699 $versions[$mod] = $module->version;
700 }
701
702 // now the rest of plugins
703 $plugintypes = get_plugin_types();
704 unset($plugintypes['mod']);
705 ksort($plugintypes);
706 foreach ($plugintypes as $type => $unused) {
707 $plugs = get_plugin_list($type);
708 ksort($plugs);
709 foreach ($plugs as $plug => $fullplug) {
710 $plugin = new stdClass();
711 $plugin->version = null;
712 @include($fullplug.'/version.php');
713 $versions[$plug] = $plugin->version;
714 }
715 }
716
717 self::$versionhash = sha1(serialize($versions));
718
719 return self::$versionhash;
720 }
721
722}