Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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