MDL-54573 testing: don't uppercase dbtype
[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
c78f19d1
JM
37 /**
38 * @var string dataroot (likely to be $CFG->dataroot).
39 */
40 private static $dataroot = null;
41
0ea35584
DM
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
44121fbd
RT
62 /**
63 * @var array keep list of sequenceid used in a table.
64 */
65 private static $tablesequences = array();
66
e487a51d
RT
67 /**
68 * @var array list of updated tables.
69 */
70 public static $tableupdated = array();
71
0ea35584
DM
72 /**
73 * @var array original structure of all database tables
74 */
75 protected static $sequencenames = null;
76
c78f19d1
JM
77 /**
78 * @var string name of the json file where we store the list of dataroot files to not reset during reset_dataroot.
79 */
80 private static $originaldatafilesjson = 'originaldatafiles.json';
81
82 /**
83 * @var boolean set to true once $originaldatafilesjson file is created.
84 */
85 private static $originaldatafilesjsonadded = false;
86
fbb0c914
RS
87 /**
88 * @var int next sequence value for a single test cycle.
89 */
90 protected static $sequencenextstartingid = null;
44121fbd 91
c78f19d1
JM
92 /**
93 * Return the name of the JSON file containing the init filenames.
94 *
95 * @static
96 * @return string
97 */
98 public static function get_originaldatafilesjson() {
99 return self::$originaldatafilesjson;
100 }
101
102 /**
103 * Return the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
104 *
105 * @static
106 * @return string the dataroot.
107 */
108 public static function get_dataroot() {
109 global $CFG;
110
111 // By default it's the test framework dataroot.
112 if (empty(self::$dataroot)) {
113 self::$dataroot = $CFG->dataroot;
114 }
115
116 return self::$dataroot;
117 }
118
119 /**
120 * Set the dataroot. It's useful when mocking the dataroot when unit testing this class itself.
121 *
122 * @param string $dataroot the dataroot of the test framework.
123 * @static
124 */
125 public static function set_dataroot($dataroot) {
126 self::$dataroot = $dataroot;
127 }
128
0ea35584
DM
129 /**
130 * Returns the testing framework name
131 * @static
132 * @return string
133 */
134 protected static final function get_framework() {
135 $classname = get_called_class();
136 return substr($classname, 0, strpos($classname, '_'));
137 }
138
139 /**
140 * Get data generator
141 * @static
142 * @return testing_data_generator
143 */
144 public static function get_data_generator() {
145 if (is_null(self::$generator)) {
146 require_once(__DIR__.'/../generator/lib.php');
147 self::$generator = new testing_data_generator();
148 }
149 return self::$generator;
150 }
151
152 /**
153 * Does this site (db and dataroot) appear to be used for production?
154 * We try very hard to prevent accidental damage done to production servers!!
155 *
156 * @static
157 * @return bool
158 */
159 public static function is_test_site() {
160 global $DB, $CFG;
161
162 $framework = self::get_framework();
163
c78f19d1 164 if (!file_exists(self::get_dataroot() . '/' . $framework . 'testdir.txt')) {
0ea35584
DM
165 // this is already tested in bootstrap script,
166 // but anyway presence of this file means the dataroot is for testing
167 return false;
168 }
169
170 $tables = $DB->get_tables(false);
171 if ($tables) {
172 if (!$DB->get_manager()->table_exists('config')) {
173 return false;
174 }
175 if (!get_config('core', $framework . 'test')) {
176 return false;
177 }
178 }
179
180 return true;
181 }
182
183 /**
184 * Returns whether test database and dataroot were created using the current version codebase
185 *
1150aeb8 186 * @return bool
0ea35584 187 */
b831d479 188 public static function is_test_data_updated() {
0ea35584
DM
189 global $CFG;
190
191 $framework = self::get_framework();
192
c78f19d1 193 $datarootpath = self::get_dataroot() . '/' . $framework;
0ea35584
DM
194 if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) {
195 return false;
196 }
197
198 if (!file_exists($datarootpath . '/versionshash.txt')) {
199 return false;
200 }
201
c5701ce7 202 $hash = core_component::get_all_versions_hash();
0ea35584
DM
203 $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
204
205 if ($hash !== $oldhash) {
206 return false;
207 }
208
209 $dbhash = get_config('core', $framework . 'test');
210 if ($hash !== $dbhash) {
211 return false;
212 }
213
214 return true;
215 }
216
217 /**
218 * Stores the status of the database
219 *
220 * Serializes the contents and the structure and
221 * stores it in the test framework space in dataroot
222 */
223 protected static function store_database_state() {
224 global $DB, $CFG;
225
226 $framework = self::get_framework();
227
228 // store data for all tables
229 $data = array();
230 $structure = array();
231 $tables = $DB->get_tables();
232 foreach ($tables as $table) {
233 $columns = $DB->get_columns($table);
234 $structure[$table] = $columns;
235 if (isset($columns['id']) and $columns['id']->auto_increment) {
236 $data[$table] = $DB->get_records($table, array(), 'id ASC');
237 } else {
238 // there should not be many of these
239 $data[$table] = $DB->get_records($table, array());
240 }
241 }
242 $data = serialize($data);
c78f19d1 243 $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
0ea35584
DM
244 file_put_contents($datafile, $data);
245 testing_fix_file_permissions($datafile);
246
247 $structure = serialize($structure);
c78f19d1 248 $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
0ea35584
DM
249 file_put_contents($structurefile, $structure);
250 testing_fix_file_permissions($structurefile);
251 }
252
253 /**
254 * Stores the version hash in both database and dataroot
255 */
256 protected static function store_versions_hash() {
257 global $CFG;
258
259 $framework = self::get_framework();
c5701ce7 260 $hash = core_component::get_all_versions_hash();
0ea35584
DM
261
262 // add test db flag
263 set_config($framework . 'test', $hash);
264
265 // hash all plugin versions - helps with very fast detection of db structure changes
c78f19d1 266 $hashfile = self::get_dataroot() . '/' . $framework . '/versionshash.txt';
0ea35584
DM
267 file_put_contents($hashfile, $hash);
268 testing_fix_file_permissions($hashfile);
269 }
270
271 /**
272 * Returns contents of all tables right after installation.
273 * @static
274 * @return array $table=>$records
275 */
276 protected static function get_tabledata() {
22d55b39
RT
277 if (!isset(self::$tabledata)) {
278 $framework = self::get_framework();
0ea35584 279
22d55b39
RT
280 $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser';
281 if (!file_exists($datafile)) {
282 // Not initialised yet.
283 return array();
284 }
0ea35584 285
0ea35584
DM
286 $data = file_get_contents($datafile);
287 self::$tabledata = unserialize($data);
288 }
289
290 if (!is_array(self::$tabledata)) {
291 testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
292 }
293
294 return self::$tabledata;
295 }
296
297 /**
298 * Returns structure of all tables right after installation.
299 * @static
300 * @return array $table=>$records
301 */
302 public static function get_tablestructure() {
22d55b39
RT
303 if (!isset(self::$tablestructure)) {
304 $framework = self::get_framework();
0ea35584 305
22d55b39
RT
306 $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser';
307 if (!file_exists($structurefile)) {
308 // Not initialised yet.
309 return array();
310 }
0ea35584 311
0ea35584
DM
312 $data = file_get_contents($structurefile);
313 self::$tablestructure = unserialize($data);
314 }
315
316 if (!is_array(self::$tablestructure)) {
317 testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
318 }
319
320 return self::$tablestructure;
321 }
322
323 /**
324 * Returns the names of sequences for each autoincrementing id field in all standard tables.
325 * @static
326 * @return array $table=>$sequencename
327 */
328 public static function get_sequencenames() {
329 global $DB;
330
331 if (isset(self::$sequencenames)) {
332 return self::$sequencenames;
333 }
334
335 if (!$structure = self::get_tablestructure()) {
336 return array();
337 }
338
339 self::$sequencenames = array();
340 foreach ($structure as $table => $ignored) {
341 $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
342 if ($name !== false) {
343 self::$sequencenames[$table] = $name;
344 }
345 }
346
347 return self::$sequencenames;
348 }
349
350 /**
351 * Returns list of tables that are unmodified and empty.
352 *
353 * @static
354 * @return array of table names, empty if unknown
355 */
356 protected static function guess_unmodified_empty_tables() {
357 global $DB;
358
359 $dbfamily = $DB->get_dbfamily();
360
361 if ($dbfamily === 'mysql') {
362 $empties = array();
363 $prefix = $DB->get_prefix();
364 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
365 foreach ($rs as $info) {
366 $table = strtolower($info->name);
367 if (strpos($table, $prefix) !== 0) {
368 // incorrect table match caused by _
369 continue;
370 }
44121fbd 371
e487a51d 372 if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) {
0ea35584 373 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
e487a51d 374 $empties[$table] = $table;
0ea35584
DM
375 }
376 }
377 $rs->close();
378 return $empties;
379
380 } else if ($dbfamily === 'mssql') {
381 $empties = array();
382 $prefix = $DB->get_prefix();
383 $sql = "SELECT t.name
384 FROM sys.identity_columns i
385 JOIN sys.tables t ON t.object_id = i.object_id
386 WHERE t.name LIKE ?
387 AND i.name = 'id'
388 AND i.last_value IS NULL";
389 $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
390 foreach ($rs as $info) {
391 $table = strtolower($info->name);
392 if (strpos($table, $prefix) !== 0) {
393 // incorrect table match caused by _
394 continue;
395 }
396 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
397 $empties[$table] = $table;
398 }
399 $rs->close();
400 return $empties;
401
402 } else if ($dbfamily === 'oracle') {
403 $sequences = self::get_sequencenames();
404 $sequences = array_map('strtoupper', $sequences);
405 $lookup = array_flip($sequences);
406 $empties = array();
407 list($seqs, $params) = $DB->get_in_or_equal($sequences);
408 $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
409 $rs = $DB->get_recordset_sql($sql, $params);
410 foreach ($rs as $seq) {
411 $table = $lookup[$seq->sequence_name];
412 $empties[$table] = $table;
413 }
414 $rs->close();
415 return $empties;
416
417 } else {
418 return array();
419 }
420 }
421
fbb0c914
RS
422 /**
423 * Determine the next unique starting id sequences.
424 *
425 * @static
426 * @param array $records The records to use to determine the starting value for the table.
44121fbd 427 * @param string $table table name.
fbb0c914
RS
428 * @return int The value the sequence should be set to.
429 */
44121fbd
RT
430 private static function get_next_sequence_starting_value($records, $table) {
431 if (isset(self::$tablesequences[$table])) {
432 return self::$tablesequences[$table];
433 }
434
fbb0c914
RS
435 $id = self::$sequencenextstartingid;
436
437 // If there are records, calculate the minimum id we can use.
438 // It must be bigger than the last record's id.
439 if (!empty($records)) {
440 $lastrecord = end($records);
441 $id = max($id, $lastrecord->id + 1);
442 }
443
444 self::$sequencenextstartingid = $id + 1000;
44121fbd
RT
445
446 self::$tablesequences[$table] = $id;
447
fbb0c914
RS
448 return $id;
449 }
450
0ea35584
DM
451 /**
452 * Reset all database sequences to initial values.
453 *
454 * @static
455 * @param array $empties tables that are known to be unmodified and empty
456 * @return void
457 */
458 public static function reset_all_database_sequences(array $empties = null) {
459 global $DB;
460
461 if (!$data = self::get_tabledata()) {
462 // Not initialised yet.
463 return;
464 }
465 if (!$structure = self::get_tablestructure()) {
466 // Not initialised yet.
467 return;
468 }
469
e487a51d
RT
470 $updatedtables = self::$tableupdated;
471
fbb0c914
RS
472 // If all starting Id's are the same, it's difficult to detect coding and testing
473 // errors that use the incorrect id in tests. The classic case is cmid vs instance id.
474 // To reduce the chance of the coding error, we start sequences at different values where possible.
475 // In a attempt to avoid tables with existing id's we start at a high number.
476 // Reset the value each time all database sequences are reset.
ddffa9d6 477 if (defined('PHPUNIT_SEQUENCE_START') and PHPUNIT_SEQUENCE_START) {
529495f7
RS
478 self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START;
479 } else {
480 self::$sequencenextstartingid = 100000;
481 }
fbb0c914 482
0ea35584
DM
483 $dbfamily = $DB->get_dbfamily();
484 if ($dbfamily === 'postgres') {
485 $queries = array();
486 $prefix = $DB->get_prefix();
487 foreach ($data as $table => $records) {
e487a51d
RT
488 // If table is not modified then no need to do anything.
489 if (!isset($updatedtables[$table])) {
490 continue;
491 }
0ea35584 492 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
44121fbd 493 $nextid = self::get_next_sequence_starting_value($records, $table);
0ea35584
DM
494 $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
495 }
496 }
497 if ($queries) {
498 $DB->change_database_structure(implode(';', $queries));
499 }
500
501 } else if ($dbfamily === 'mysql') {
e487a51d 502 $queries = array();
0ea35584
DM
503 $sequences = array();
504 $prefix = $DB->get_prefix();
505 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
506 foreach ($rs as $info) {
507 $table = strtolower($info->name);
508 if (strpos($table, $prefix) !== 0) {
509 // incorrect table match caused by _
510 continue;
511 }
512 if (!is_null($info->auto_increment)) {
513 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
514 $sequences[$table] = $info->auto_increment;
515 }
516 }
517 $rs->close();
518 $prefix = $DB->get_prefix();
519 foreach ($data as $table => $records) {
e487a51d
RT
520 // If table is not modified then no need to do anything.
521 if (!isset($updatedtables[$table])) {
522 continue;
523 }
0ea35584
DM
524 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
525 if (isset($sequences[$table])) {
44121fbd 526 $nextid = self::get_next_sequence_starting_value($records, $table);
0ea35584 527 if ($sequences[$table] != $nextid) {
e487a51d 528 $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
0ea35584 529 }
0ea35584
DM
530 } else {
531 // some problem exists, fallback to standard code
532 $DB->get_manager()->reset_sequence($table);
533 }
534 }
535 }
e487a51d
RT
536 if ($queries) {
537 $DB->change_database_structure(implode(';', $queries));
538 }
0ea35584
DM
539
540 } else if ($dbfamily === 'oracle') {
541 $sequences = self::get_sequencenames();
542 $sequences = array_map('strtoupper', $sequences);
543 $lookup = array_flip($sequences);
544
545 $current = array();
546 list($seqs, $params) = $DB->get_in_or_equal($sequences);
547 $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
548 $rs = $DB->get_recordset_sql($sql, $params);
549 foreach ($rs as $seq) {
550 $table = $lookup[$seq->sequence_name];
551 $current[$table] = $seq->last_number;
552 }
553 $rs->close();
554
555 foreach ($data as $table => $records) {
e487a51d
RT
556 // If table is not modified then no need to do anything.
557 if (!isset($updatedtables[$table])) {
558 continue;
559 }
0ea35584 560 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
3fa0daa9
AN
561 $lastrecord = end($records);
562 if ($lastrecord) {
563 $nextid = $lastrecord->id + 1;
564 } else {
565 $nextid = 1;
566 }
0ea35584
DM
567 if (!isset($current[$table])) {
568 $DB->get_manager()->reset_sequence($table);
569 } else if ($nextid == $current[$table]) {
570 continue;
571 }
572 // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
573 $seqname = $sequences[$table];
574 $cachesize = $DB->get_manager()->generator->sequence_cache_size;
575 $DB->change_database_structure("DROP SEQUENCE $seqname");
576 $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
577 }
578 }
579
580 } else {
581 // note: does mssql support any kind of faster reset?
fbb0c914 582 // This also implies mssql will not use unique sequence values.
e487a51d 583 if (is_null($empties) and (empty($updatedtables))) {
0ea35584
DM
584 $empties = self::guess_unmodified_empty_tables();
585 }
586 foreach ($data as $table => $records) {
e487a51d
RT
587 // If table is not modified then no need to do anything.
588 if (isset($empties[$table]) or (!isset($updatedtables[$table]))) {
0ea35584
DM
589 continue;
590 }
591 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
592 $DB->get_manager()->reset_sequence($table);
593 }
594 }
595 }
596 }
597
598 /**
1150aeb8 599 * Reset all database tables to default values.
0ea35584 600 * @static
1150aeb8 601 * @return bool true if reset done, false if skipped
0ea35584
DM
602 */
603 public static function reset_database() {
604 global $DB;
605
0ea35584
DM
606 $tables = $DB->get_tables(false);
607 if (!$tables or empty($tables['config'])) {
608 // not installed yet
609 return false;
610 }
611
612 if (!$data = self::get_tabledata()) {
613 // not initialised yet
614 return false;
615 }
616 if (!$structure = self::get_tablestructure()) {
617 // not initialised yet
618 return false;
619 }
620
e487a51d
RT
621 $empties = array();
622 // Use local copy of self::$tableupdated, as list gets updated in for loop.
623 $updatedtables = self::$tableupdated;
624
625 // If empty tablesequences list then it's the very first run.
626 if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
627 // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
628 $empties = self::guess_unmodified_empty_tables();
629 }
630
631 // Check if any table has been modified by behat selenium process.
632 if (defined('BEHAT_SITE_RUNNING')) {
633 // Crazy way to reset :(.
634 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
635 if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
636 self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
637 unlink($tablesupdatedfile);
638 }
639 $updatedtables = self::$tableupdated;
640 }
0ea35584 641
5d4c8256
642 $borkedmysql = false;
643 if ($DB->get_dbfamily() === 'mysql') {
644 $version = $DB->get_server_info();
645 if (version_compare($version['version'], '5.6.0') == 1 and version_compare($version['version'], '5.6.16') == -1) {
646 // Everything that comes from Oracle is evil!
647 //
648 // See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
649 // You cannot reset the counter to a value less than or equal to to the value that is currently in use.
650 //
651 // From 5.6.16 release notes:
652 // InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
653 // (Bug #17250787, Bug #69882)
654 $borkedmysql = true;
655
656 } else if (version_compare($version['version'], '10.0.0') == 1) {
657 // And MariaDB is no better!
658 // Let's hope they pick the patch sometime later...
659 $borkedmysql = true;
660 }
661 }
662
663 if ($borkedmysql) {
664 $mysqlsequences = array();
665 $prefix = $DB->get_prefix();
666 $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
667 foreach ($rs as $info) {
668 $table = strtolower($info->name);
669 if (strpos($table, $prefix) !== 0) {
670 // Incorrect table match caused by _ char.
671 continue;
672 }
673 if (!is_null($info->auto_increment)) {
674 $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
675 $mysqlsequences[$table] = $info->auto_increment;
676 }
677 }
678 }
679
0ea35584 680 foreach ($data as $table => $records) {
e487a51d
RT
681 // If table is not modified then no need to do anything.
682 // $updatedtables tables is set after the first run, so check before checking for specific table update.
683 if (!empty($updatedtables) && !isset($updatedtables[$table])) {
684 continue;
685 }
686
5d4c8256 687 if ($borkedmysql) {
1226f3ca 688 if (empty($records)) {
28dab640
RT
689 if (!isset($empties[$table])) {
690 // Table has been modified and is not empty.
1226f3ca
RT
691 $DB->delete_records($table, null);
692 }
5d4c8256
693 continue;
694 }
695
696 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
697 $current = $DB->get_records($table, array(), 'id ASC');
698 if ($current == $records) {
699 if (isset($mysqlsequences[$table]) and $mysqlsequences[$table] == $structure[$table]['id']->auto_increment) {
700 continue;
701 }
702 }
703 }
704
705 // Use TRUNCATE as a workaround and reinsert everything.
706 $DB->delete_records($table, null);
707 foreach ($records as $record) {
708 $DB->import_record($table, $record, false, true);
709 }
710 continue;
711 }
712
0ea35584 713 if (empty($records)) {
28dab640
RT
714 if (!isset($empties[$table])) {
715 // Table has been modified and is not empty.
0ea35584
DM
716 $DB->delete_records($table, array());
717 }
718 continue;
719 }
720
721 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
722 $currentrecords = $DB->get_records($table, array(), 'id ASC');
723 $changed = false;
724 foreach ($records as $id => $record) {
725 if (!isset($currentrecords[$id])) {
726 $changed = true;
727 break;
728 }
729 if ((array)$record != (array)$currentrecords[$id]) {
730 $changed = true;
731 break;
732 }
733 unset($currentrecords[$id]);
734 }
735 if (!$changed) {
736 if ($currentrecords) {
737 $lastrecord = end($records);
738 $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
739 continue;
740 } else {
741 continue;
742 }
743 }
744 }
745
746 $DB->delete_records($table, array());
747 foreach ($records as $record) {
748 $DB->import_record($table, $record, false, true);
749 }
750 }
751
752 // reset all next record ids - aka sequences
753 self::reset_all_database_sequences($empties);
754
755 // remove extra tables
756 foreach ($tables as $table) {
757 if (!isset($data[$table])) {
758 $DB->get_manager()->drop_table(new xmldb_table($table));
759 }
760 }
761
e487a51d
RT
762 self::reset_updated_table_list();
763
0ea35584
DM
764 return true;
765 }
766
767 /**
768 * Purge dataroot directory
769 * @static
770 * @return void
771 */
772 public static function reset_dataroot() {
773 global $CFG;
774
775 $childclassname = self::get_framework() . '_util';
776
c78f19d1
JM
777 // Do not delete automatically installed files.
778 self::skip_original_data_files($childclassname);
779
97d0445a
RT
780 // Clear file status cache, before checking file_exists.
781 clearstatcache();
782
c78f19d1
JM
783 // Clean up the dataroot folder.
784 $handle = opendir(self::get_dataroot());
0ea35584
DM
785 while (false !== ($item = readdir($handle))) {
786 if (in_array($item, $childclassname::$datarootskiponreset)) {
787 continue;
788 }
c78f19d1
JM
789 if (is_dir(self::get_dataroot()."/$item")) {
790 remove_dir(self::get_dataroot()."/$item", false);
0ea35584 791 } else {
c78f19d1 792 unlink(self::get_dataroot()."/$item");
0ea35584
DM
793 }
794 }
795 closedir($handle);
c78f19d1
JM
796
797 // Clean up the dataroot/filedir folder.
798 if (file_exists(self::get_dataroot() . '/filedir')) {
799 $handle = opendir(self::get_dataroot() . '/filedir');
800 while (false !== ($item = readdir($handle))) {
801 if (in_array('filedir/' . $item, $childclassname::$datarootskiponreset)) {
802 continue;
803 }
804 if (is_dir(self::get_dataroot()."/filedir/$item")) {
805 remove_dir(self::get_dataroot()."/filedir/$item", false);
806 } else {
807 unlink(self::get_dataroot()."/filedir/$item");
808 }
809 }
810 closedir($handle);
811 }
812
0ea35584
DM
813 make_temp_directory('');
814 make_cache_directory('');
85b38061 815 make_localcache_directory('');
0ea35584
DM
816 // Reset the cache API so that it recreates it's required directories as well.
817 cache_factory::reset();
818 // Purge all data from the caches. This is required for consistency.
819 // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
820 // and now we will purge any other caches as well.
821 cache_helper::purge_all();
822 }
823
7514c2f2
DM
824 /**
825 * Gets a text-based site version description.
826 *
827 * @return string The site info
828 */
829 public static function get_site_info() {
e891c838 830 global $CFG;
7514c2f2
DM
831
832 $output = '';
833
834 // All developers have to understand English, do not localise!
e891c838 835 $env = self::get_environment();
7514c2f2 836
e891c838 837 $output .= "Moodle ".$env['moodleversion'];
7514c2f2
DM
838 if ($hash = self::get_git_hash()) {
839 $output .= ", $hash";
840 }
841 $output .= "\n";
842
af2dc48b 843 // Add php version.
e891c838
RT
844 require_once($CFG->libdir.'/environmentlib.php');
845 $output .= "Php: ". normalize_version($env['phpversion']);
af2dc48b
RT
846
847 // Add database type and version.
e891c838 848 $output .= ", " . $env['dbtype'] . ": " . $env['dbversion'];
af2dc48b
RT
849
850 // OS details.
e891c838 851 $output .= ", OS: " . $env['os'] . "\n";
af2dc48b 852
7514c2f2
DM
853 return $output;
854 }
855
856 /**
857 * Try to get current git hash of the Moodle in $CFG->dirroot.
858 * @return string null if unknown, sha1 hash if known
859 */
860 public static function get_git_hash() {
861 global $CFG;
862
863 // This is a bit naive, but it should mostly work for all platforms.
864
865 if (!file_exists("$CFG->dirroot/.git/HEAD")) {
866 return null;
867 }
868
244da9d1
DM
869 $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD");
870 if ($headcontent === false) {
7514c2f2
DM
871 return null;
872 }
873
244da9d1 874 $headcontent = trim($headcontent);
7514c2f2 875
244da9d1
DM
876 // If it is pointing to a hash we return it directly.
877 if (strlen($headcontent) === 40) {
878 return $headcontent;
879 }
880
881 if (strpos($headcontent, 'ref: ') !== 0) {
7514c2f2
DM
882 return null;
883 }
884
244da9d1 885 $ref = substr($headcontent, 5);
7514c2f2
DM
886
887 if (!file_exists("$CFG->dirroot/.git/$ref")) {
888 return null;
889 }
890
891 $hash = file_get_contents("$CFG->dirroot/.git/$ref");
892
893 if ($hash === false) {
894 return null;
895 }
896
897 $hash = trim($hash);
898
899 if (strlen($hash) != 40) {
900 return null;
901 }
902
903 return $hash;
904 }
905
e487a51d
RT
906 /**
907 * Set state of modified tables.
908 *
909 * @param string $sql sql which is updating the table.
910 */
911 public static function set_table_modified_by_sql($sql) {
912 global $DB;
913
914 $prefix = $DB->get_prefix();
915
916 preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
917 // Ignore random sql for testing like "XXUPDATE SET XSSD".
918 if (!empty($matches[1])) {
919 $table = trim($matches[1]);
920 $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
921 self::$tableupdated[$table] = true;
922
923 if (defined('BEHAT_SITE_RUNNING')) {
924 $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
925 if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
926 $tablesupdated[$table] = true;
927 } else {
928 $tablesupdated[$table] = true;
929 }
930 @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
931 }
932 }
933 }
934
935 /**
936 * Reset updated table list. This should be done after every reset.
937 */
938 public static function reset_updated_table_list() {
939 self::$tableupdated = array();
940 }
941
942 /**
943 * Returns the path to the file which holds list of tables updated in scenario.
944 * @return string
945 */
946 protected final static function get_tables_updated_by_scenario_list_path() {
947 return self::get_dataroot() . '/tablesupdatedbyscenario.txt';
948 }
949
0ea35584
DM
950 /**
951 * Drop the whole test database
952 * @static
1150aeb8 953 * @param bool $displayprogress
0ea35584
DM
954 */
955 protected static function drop_database($displayprogress = false) {
956 global $DB;
957
958 $tables = $DB->get_tables(false);
959 if (isset($tables['config'])) {
960 // config always last to prevent problems with interrupted drops!
961 unset($tables['config']);
962 $tables['config'] = 'config';
963 }
964
965 if ($displayprogress) {
966 echo "Dropping tables:\n";
967 }
968 $dotsonline = 0;
969 foreach ($tables as $tablename) {
970 $table = new xmldb_table($tablename);
971 $DB->get_manager()->drop_table($table);
972
973 if ($dotsonline == 60) {
974 if ($displayprogress) {
975 echo "\n";
976 }
977 $dotsonline = 0;
978 }
979 if ($displayprogress) {
980 echo '.';
981 }
982 $dotsonline += 1;
983 }
984 if ($displayprogress) {
985 echo "\n";
986 }
987 }
988
989 /**
990 * Drops the test framework dataroot
991 * @static
992 */
993 protected static function drop_dataroot() {
994 global $CFG;
995
996 $framework = self::get_framework();
997 $childclassname = $framework . '_util';
998
c78f19d1 999 $files = scandir(self::get_dataroot() . '/' . $framework);
0ea35584
DM
1000 foreach ($files as $file) {
1001 if (in_array($file, $childclassname::$datarootskipondrop)) {
1002 continue;
1003 }
c78f19d1 1004 $path = self::get_dataroot() . '/' . $framework . '/' . $file;
0ea35584
DM
1005 if (is_dir($path)) {
1006 remove_dir($path, false);
1007 } else {
1008 unlink($path);
1009 }
1010 }
c78f19d1
JM
1011
1012 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1013 if (file_exists($jsonfilepath)) {
1014 // Delete the json file.
1015 unlink($jsonfilepath);
1016 // Delete the dataroot filedir.
1017 remove_dir(self::get_dataroot() . '/filedir', false);
1018 }
1019 }
1020
1021 /**
1022 * Skip the original dataroot files to not been reset.
1023 *
1024 * @static
1025 * @param string $utilclassname the util class name..
1026 */
1027 protected static function skip_original_data_files($utilclassname) {
1028 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1029 if (file_exists($jsonfilepath)) {
1030
1031 $listfiles = file_get_contents($jsonfilepath);
1032
1033 // Mark each files as to not be reset.
1034 if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
1035 $originaldatarootfiles = json_decode($listfiles);
1036 // Keep the json file. Only drop_dataroot() should delete it.
1037 $originaldatarootfiles[] = self::$originaldatafilesjson;
1038 $utilclassname::$datarootskiponreset = array_merge($utilclassname::$datarootskiponreset,
1039 $originaldatarootfiles);
1040 self::$originaldatafilesjsonadded = true;
1041 }
1042 }
1043 }
1044
1045 /**
1046 * Save the list of the original dataroot files into a json file.
1047 */
1048 protected static function save_original_data_files() {
1049 global $CFG;
1050
1051 $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
1052
1053 // Save the original dataroot files if not done (only executed the first time).
1054 if (!file_exists($jsonfilepath)) {
1055
1056 $listfiles = array();
d3daf245
DM
1057 $listfiles['filedir/.'] = 'filedir/.';
1058 $listfiles['filedir/..'] = 'filedir/..';
c78f19d1
JM
1059
1060 $filedir = self::get_dataroot() . '/filedir';
1061 if (file_exists($filedir)) {
1062 $directory = new RecursiveDirectoryIterator($filedir);
1063 foreach (new RecursiveIteratorIterator($directory) as $file) {
1064 if ($file->isDir()) {
d3daf245 1065 $key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
c78f19d1 1066 } else {
d3daf245 1067 $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
c78f19d1 1068 }
d3daf245 1069 $listfiles[$key] = $key;
c78f19d1
JM
1070 }
1071 }
1072
1073 // Save the file list in a JSON file.
1074 $fp = fopen($jsonfilepath, 'w');
630bb85d 1075 fwrite($fp, json_encode(array_values($listfiles)));
c78f19d1
JM
1076 fclose($fp);
1077 }
0ea35584 1078 }
e891c838
RT
1079
1080 /**
1081 * Return list of environment versions on which tests will run.
1082 * Environment includes:
1083 * - moodleversion
1084 * - phpversion
1085 * - dbtype
1086 * - dbversion
1087 * - os
1088 *
1089 * @return array
1090 */
1091 public static function get_environment() {
1092 global $CFG, $DB;
1093
1094 $env = array();
1095
1096 // Add moodle version.
1097 $release = null;
1098 require("$CFG->dirroot/version.php");
1099 $env['moodleversion'] = $release;
1100
1101 // Add php version.
1102 $phpversion = phpversion();
1103 $env['phpversion'] = $phpversion;
1104
1105 // Add database type and version.
f6cbdf78 1106 $dbtype = $CFG->dbtype;
e891c838
RT
1107 $dbinfo = $DB->get_server_info();
1108 $dbversion = $dbinfo['version'];
e891c838
RT
1109 $env['dbversion'] = $dbversion;
1110
1111 // OS details.
1112 $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
1113 $env['os'] = $osdetails;
1114
1115 return $env;
1116 }
0ea35584 1117}