Commit | Line | Data |
---|---|---|
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 | */ | |
35 | abstract 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 PÅ |
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 PÅ |
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 | } |