Proyectos de Subversion Moodle

Rev

| 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 if ($dbfamily === 'oracle') {
397
            $sequences = self::get_sequencenames();
398
            $sequences = array_map('strtoupper', $sequences);
399
            $lookup = array_flip($sequences);
400
            $empties = [];
401
            [$seqs, $params] = $DB->get_in_or_equal($sequences);
402
            $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
403
            $rs = $DB->get_recordset_sql($sql, $params);
404
            foreach ($rs as $seq) {
405
                $table = $lookup[$seq->sequence_name];
406
                $empties[$table] = $table;
407
            }
408
            $rs->close();
409
            return $empties;
410
        } else {
411
            return [];
412
        }
413
    }
414
 
415
    /**
416
     * Determine the next unique starting id sequences.
417
     *
418
     * @static
419
     * @param array $records The records to use to determine the starting value for the table.
420
     * @param string $table table name.
421
     * @return int The value the sequence should be set to.
422
     */
423
    private static function get_next_sequence_starting_value($records, $table) {
424
        if (isset(self::$tablesequences[$table])) {
425
            return self::$tablesequences[$table];
426
        }
427
 
428
        $id = self::$sequencenextstartingid;
429
 
430
        // If there are records, calculate the minimum id we can use.
431
        // It must be bigger than the last record's id.
432
        if (!empty($records)) {
433
            $lastrecord = end($records);
434
            $id = max($id, $lastrecord->id + 1);
435
        }
436
 
437
        self::$sequencenextstartingid = $id + 1000;
438
 
439
        self::$tablesequences[$table] = $id;
440
 
441
        return $id;
442
    }
443
 
444
    /**
445
     * Reset all database sequences to initial values.
446
     *
447
     * @static
448
     * @param array $empties tables that are known to be unmodified and empty
449
     * @return void
450
     */
451
    public static function reset_all_database_sequences(array $empties = null) {
452
        global $DB;
453
 
454
        if (!$data = self::get_tabledata()) {
455
            // Not initialised yet.
456
            return;
457
        }
458
        if (!$structure = self::get_tablestructure()) {
459
            // Not initialised yet.
460
            return;
461
        }
462
 
463
        $updatedtables = self::$tableupdated;
464
 
465
        // If all starting Id's are the same, it's difficult to detect coding and testing
466
        // errors that use the incorrect id in tests.  The classic case is cmid vs instance id.
467
        // To reduce the chance of the coding error, we start sequences at different values where possible.
468
        // In a attempt to avoid tables with existing id's we start at a high number.
469
        // Reset the value each time all database sequences are reset.
470
        if (defined('PHPUNIT_SEQUENCE_START') && PHPUNIT_SEQUENCE_START) {
471
            self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START;
472
        } else {
473
            self::$sequencenextstartingid = 100000;
474
        }
475
 
476
        $dbfamily = $DB->get_dbfamily();
477
        if ($dbfamily === 'postgres') {
478
            $queries = [];
479
            $prefix = $DB->get_prefix();
480
            foreach ($data as $table => $records) {
481
                // If table is not modified then no need to do anything.
482
                if (!isset($updatedtables[$table])) {
483
                    continue;
484
                }
485
                if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) {
486
                    $nextid = self::get_next_sequence_starting_value($records, $table);
487
                    $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
488
                }
489
            }
490
            if ($queries) {
491
                $DB->change_database_structure(implode(';', $queries));
492
            }
493
        } else if ($dbfamily === 'mysql') {
494
            $queries = [];
495
            $sequences = [];
496
            $prefix = $DB->get_prefix();
497
            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", [$prefix . '%']);
498
            foreach ($rs as $info) {
499
                $table = strtolower($info->name);
500
                if (strpos($table, $prefix) !== 0) {
501
                    // Incorrect table match caused by _.
502
                    continue;
503
                }
504
                if (!is_null($info->auto_increment)) {
505
                    $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
506
                    $sequences[$table] = $info->auto_increment;
507
                }
508
            }
509
            $rs->close();
510
            $prefix = $DB->get_prefix();
511
            foreach ($data as $table => $records) {
512
                // If table is not modified then no need to do anything.
513
                if (!isset($updatedtables[$table])) {
514
                    continue;
515
                }
516
                if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) {
517
                    if (isset($sequences[$table])) {
518
                        $nextid = self::get_next_sequence_starting_value($records, $table);
519
                        if ($sequences[$table] != $nextid) {
520
                            $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid";
521
                        }
522
                    } else {
523
                        // Some problem exists, fallback to standard code.
524
                        $DB->get_manager()->reset_sequence($table);
525
                    }
526
                }
527
            }
528
            if ($queries) {
529
                $DB->change_database_structure(implode(';', $queries));
530
            }
531
        } else if ($dbfamily === 'oracle') {
532
            $sequences = self::get_sequencenames();
533
            $sequences = array_map('strtoupper', $sequences);
534
            $lookup = array_flip($sequences);
535
 
536
            $current = [];
537
            [$seqs, $params] = $DB->get_in_or_equal($sequences);
538
            $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
539
            $rs = $DB->get_recordset_sql($sql, $params);
540
            foreach ($rs as $seq) {
541
                $table = $lookup[$seq->sequence_name];
542
                $current[$table] = $seq->last_number;
543
            }
544
            $rs->close();
545
 
546
            foreach ($data as $table => $records) {
547
                // If table is not modified then no need to do anything.
548
                if (!isset($updatedtables[$table])) {
549
                    continue;
550
                }
551
                if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) {
552
                    $lastrecord = end($records);
553
                    if ($lastrecord) {
554
                        $nextid = $lastrecord->id + 1;
555
                    } else {
556
                        $nextid = 1;
557
                    }
558
                    if (!isset($current[$table])) {
559
                        $DB->get_manager()->reset_sequence($table);
560
                    } else if ($nextid == $current[$table]) {
561
                        continue;
562
                    }
563
                    // Reset as fast as possible.
564
                    // Alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle.
565
                    $seqname = $sequences[$table];
566
                    $cachesize = $DB->get_manager()->generator->sequence_cache_size;
567
                    $DB->change_database_structure("DROP SEQUENCE $seqname");
568
                    $DB->change_database_structure(
569
                        "CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize",
570
                    );
571
                }
572
            }
573
        } else {
574
            // Note: does mssql support any kind of faster reset?
575
            // This also implies mssql will not use unique sequence values.
576
            if (is_null($empties) && (empty($updatedtables))) {
577
                $empties = self::guess_unmodified_empty_tables();
578
            }
579
            foreach ($data as $table => $records) {
580
                // If table is not modified then no need to do anything.
581
                if (isset($empties[$table]) || (!isset($updatedtables[$table]))) {
582
                    continue;
583
                }
584
                if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) {
585
                    $DB->get_manager()->reset_sequence($table);
586
                }
587
            }
588
        }
589
    }
590
 
591
    /**
592
     * Reset all database tables to default values.
593
     * @static
594
     * @return bool true if reset done, false if skipped
595
     */
596
    public static function reset_database() {
597
        global $DB;
598
 
599
        $tables = $DB->get_tables(false);
600
        if (!$tables || empty($tables['config'])) {
601
            // Not installed yet.
602
            return false;
603
        }
604
 
605
        if (!$data = self::get_tabledata()) {
606
            // Not initialised yet.
607
            return false;
608
        }
609
        if (!$structure = self::get_tablestructure()) {
610
            // Not initialised yet.
611
            return false;
612
        }
613
 
614
        $empties = [];
615
        // Use local copy of self::$tableupdated, as list gets updated in for loop.
616
        $updatedtables = self::$tableupdated;
617
 
618
        // If empty tablesequences list then it's the very first run.
619
        if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) {
620
            // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run.
621
            $empties = self::guess_unmodified_empty_tables();
622
        }
623
 
624
        // Check if any table has been modified by behat selenium process.
625
        if (defined('BEHAT_SITE_RUNNING')) {
626
            // Crazy way to reset :(.
627
            $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
628
            if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) {
629
                self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated);
630
                unlink($tablesupdatedfile);
631
            }
632
            $updatedtables = self::$tableupdated;
633
        }
634
 
635
        foreach ($data as $table => $records) {
636
            // If table is not modified then no need to do anything.
637
            // $updatedtables tables is set after the first run, so check before checking for specific table update.
638
            if (!empty($updatedtables) && !isset($updatedtables[$table])) {
639
                continue;
640
            }
641
 
642
            if (empty($records)) {
643
                if (!isset($empties[$table])) {
644
                    // Table has been modified and is not empty.
645
                    $DB->delete_records($table, []);
646
                }
647
                continue;
648
            }
649
 
650
            if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) {
651
                $currentrecords = $DB->get_records($table, [], 'id ASC');
652
                $changed = false;
653
                foreach ($records as $id => $record) {
654
                    if (!isset($currentrecords[$id])) {
655
                        $changed = true;
656
                        break;
657
                    }
658
                    if ((array)$record != (array)$currentrecords[$id]) {
659
                        $changed = true;
660
                        break;
661
                    }
662
                    unset($currentrecords[$id]);
663
                }
664
                if (!$changed) {
665
                    if ($currentrecords) {
666
                        $lastrecord = end($records);
667
                        $DB->delete_records_select($table, "id > ?", [$lastrecord->id]);
668
                        continue;
669
                    } else {
670
                        continue;
671
                    }
672
                }
673
            }
674
 
675
            $DB->delete_records($table, []);
676
            foreach ($records as $record) {
677
                $DB->import_record($table, $record, false, true);
678
            }
679
        }
680
 
681
        // Reset all next record ids - aka sequences.
682
        self::reset_all_database_sequences($empties);
683
 
684
        // Remove extra tables.
685
        foreach ($tables as $table) {
686
            if (!isset($data[$table])) {
687
                $DB->get_manager()->drop_table(new xmldb_table($table));
688
            }
689
        }
690
 
691
        self::reset_updated_table_list();
692
 
693
        return true;
694
    }
695
 
696
    /**
697
     * Purge dataroot directory
698
     * @static
699
     * @return void
700
     */
701
    public static function reset_dataroot() {
702
        global $CFG;
703
 
704
        $childclassname = self::get_framework() . '_util';
705
 
706
        // Do not delete automatically installed files.
707
        self::skip_original_data_files($childclassname);
708
 
709
        // Clear file status cache, before checking file_exists.
710
        clearstatcache();
711
 
712
        // Clean up the dataroot folder.
713
        $handle = opendir(self::get_dataroot());
714
        while (false !== ($item = readdir($handle))) {
715
            if (in_array($item, $childclassname::$datarootskiponreset)) {
716
                continue;
717
            }
718
            if (is_dir(self::get_dataroot() . "/$item")) {
719
                remove_dir(self::get_dataroot() . "/$item", false);
720
            } else {
721
                unlink(self::get_dataroot() . "/$item");
722
            }
723
        }
724
        closedir($handle);
725
 
726
        // Clean up the dataroot/filedir folder.
727
        if (file_exists(self::get_dataroot() . '/filedir')) {
728
            $handle = opendir(self::get_dataroot() . '/filedir');
729
            while (false !== ($item = readdir($handle))) {
730
                if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) {
731
                    continue;
732
                }
733
                if (is_dir(self::get_dataroot() . "/filedir/$item")) {
734
                    remove_dir(self::get_dataroot() . "/filedir/$item", false);
735
                } else {
736
                    unlink(self::get_dataroot() . "/filedir/$item");
737
                }
738
            }
739
            closedir($handle);
740
        }
741
 
742
        make_temp_directory('');
743
        make_backup_temp_directory('');
744
        make_cache_directory('');
745
        make_localcache_directory('');
746
        // Purge all data from the caches. This is required for consistency between tests.
747
        // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
748
        // and now we will purge any other caches as well.  This must be done before the cache_factory::reset() as that
749
        // removes all definitions of caches and purge does not have valid caches to operate on.
750
        cache_helper::purge_all();
751
        // Reset the cache API so that it recreates it's required directories as well.
752
        cache_factory::reset();
753
    }
754
 
755
    /**
756
     * Gets a text-based site version description.
757
     *
758
     * @return string The site info
759
     */
760
    public static function get_site_info() {
761
        global $CFG;
762
 
763
        $output = '';
764
 
765
        // All developers have to understand English, do not localise!
766
        $env = self::get_environment();
767
 
768
        $output .= "Moodle " . $env['moodleversion'];
769
        if ($hash = self::get_git_hash()) {
770
            $output .= ", $hash";
771
        }
772
        $output .= "\n";
773
 
774
        // Add php version.
775
        require_once($CFG->libdir . '/environmentlib.php');
776
        $output .= "Php: " . normalize_version($env['phpversion']);
777
 
778
        // Add database type and version.
779
        $output .= ", " . $env['dbtype'] . ": " . $env['dbversion'];
780
 
781
        // OS details.
782
        $output .= ", OS: " . $env['os'] . "\n";
783
 
784
        return $output;
785
    }
786
 
787
    /**
788
     * Try to get current git hash of the Moodle in $CFG->dirroot.
789
     * @return string null if unknown, sha1 hash if known
790
     */
791
    public static function get_git_hash() {
792
        global $CFG;
793
 
794
        // This is a bit naive, but it should mostly work for all platforms.
795
 
796
        if (!file_exists("$CFG->dirroot/.git/HEAD")) {
797
            return null;
798
        }
799
 
800
        $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD");
801
        if ($headcontent === false) {
802
            return null;
803
        }
804
 
805
        $headcontent = trim($headcontent);
806
 
807
        // If it is pointing to a hash we return it directly.
808
        if (strlen($headcontent) === 40) {
809
            return $headcontent;
810
        }
811
 
812
        if (strpos($headcontent, 'ref: ') !== 0) {
813
            return null;
814
        }
815
 
816
        $ref = substr($headcontent, 5);
817
 
818
        if (!file_exists("$CFG->dirroot/.git/$ref")) {
819
            return null;
820
        }
821
 
822
        $hash = file_get_contents("$CFG->dirroot/.git/$ref");
823
 
824
        if ($hash === false) {
825
            return null;
826
        }
827
 
828
        $hash = trim($hash);
829
 
830
        if (strlen($hash) != 40) {
831
            return null;
832
        }
833
 
834
        return $hash;
835
    }
836
 
837
    /**
838
     * Set state of modified tables.
839
     *
840
     * @param string $sql sql which is updating the table.
841
     */
842
    public static function set_table_modified_by_sql($sql) {
843
        global $DB;
844
 
845
        $prefix = $DB->get_prefix();
846
 
847
        preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches);
848
        // Ignore random sql for testing like "XXUPDATE SET XSSD".
849
        if (!empty($matches[1])) {
850
            $table = trim($matches[1]);
851
            $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table);
852
            self::$tableupdated[$table] = true;
853
 
854
            if (defined('BEHAT_SITE_RUNNING')) {
855
                $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
856
                $tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true);
857
                if (!isset($tablesupdated[$table])) {
858
                    $tablesupdated[$table] = true;
859
                    @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT));
860
                }
861
            }
862
        }
863
    }
864
 
865
    /**
866
     * Reset updated table list. This should be done after every reset.
867
     */
868
    public static function reset_updated_table_list() {
869
        self::$tableupdated = [];
870
    }
871
 
872
    /**
873
     * Delete tablesupdatedbyscenario file. This should be called before suite,
874
     * to ensure full db reset.
875
     */
876
    public static function clean_tables_updated_by_scenario_list() {
877
        $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path();
878
        if (file_exists($tablesupdatedfile)) {
879
            unlink($tablesupdatedfile);
880
        }
881
 
882
        // Reset static cache of cli process.
883
        self::reset_updated_table_list();
884
    }
885
 
886
    /**
887
     * Returns the path to the file which holds list of tables updated in scenario.
888
     * @return string
889
     */
890
    final protected static function get_tables_updated_by_scenario_list_path() {
891
        return self::get_dataroot() . '/tablesupdatedbyscenario.json';
892
    }
893
 
894
    /**
895
     * Drop the whole test database
896
     * @static
897
     * @param bool $displayprogress
898
     */
899
    protected static function drop_database($displayprogress = false) {
900
        global $DB;
901
 
902
        $tables = $DB->get_tables(false);
903
        if (isset($tables['config'])) {
904
            // Config always last to prevent problems with interrupted drops!
905
            unset($tables['config']);
906
            $tables['config'] = 'config';
907
        }
908
 
909
        if ($displayprogress) {
910
            echo "Dropping tables:\n";
911
        }
912
        $dotsonline = 0;
913
        foreach ($tables as $tablename) {
914
            $table = new xmldb_table($tablename);
915
            $DB->get_manager()->drop_table($table);
916
 
917
            if ($dotsonline == 60) {
918
                if ($displayprogress) {
919
                    echo "\n";
920
                }
921
                $dotsonline = 0;
922
            }
923
            if ($displayprogress) {
924
                echo '.';
925
            }
926
            $dotsonline += 1;
927
        }
928
        if ($displayprogress) {
929
            echo "\n";
930
        }
931
    }
932
 
933
    /**
934
     * Drops the test framework dataroot
935
     * @static
936
     */
937
    protected static function drop_dataroot() {
938
        global $CFG;
939
 
940
        $framework = self::get_framework();
941
        $childclassname = $framework . '_util';
942
 
943
        $files = scandir(self::get_dataroot() . '/'  . $framework);
944
        foreach ($files as $file) {
945
            if (in_array($file, $childclassname::$datarootskipondrop)) {
946
                continue;
947
            }
948
            $path = self::get_dataroot() . '/' . $framework . '/' . $file;
949
            if (is_dir($path)) {
950
                remove_dir($path, false);
951
            } else {
952
                unlink($path);
953
            }
954
        }
955
 
956
        $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
957
        if (file_exists($jsonfilepath)) {
958
            // Delete the json file.
959
            unlink($jsonfilepath);
960
            // Delete the dataroot filedir.
961
            remove_dir(self::get_dataroot() . '/filedir', false);
962
        }
963
    }
964
 
965
    /**
966
     * Skip the original dataroot files to not been reset.
967
     *
968
     * @static
969
     * @param string $utilclassname the util class name..
970
     */
971
    protected static function skip_original_data_files($utilclassname) {
972
        $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
973
        if (file_exists($jsonfilepath)) {
974
            $listfiles = file_get_contents($jsonfilepath);
975
 
976
            // Mark each files as to not be reset.
977
            if (!empty($listfiles) && !self::$originaldatafilesjsonadded) {
978
                $originaldatarootfiles = json_decode($listfiles);
979
                // Keep the json file. Only drop_dataroot() should delete it.
980
                $originaldatarootfiles[] = self::$originaldatafilesjson;
981
                $utilclassname::$datarootskiponreset = array_merge(
982
                    $utilclassname::$datarootskiponreset,
983
                    $originaldatarootfiles
984
                );
985
                self::$originaldatafilesjsonadded = true;
986
            }
987
        }
988
    }
989
 
990
    /**
991
     * Save the list of the original dataroot files into a json file.
992
     */
993
    protected static function save_original_data_files() {
994
        global $CFG;
995
 
996
        $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson;
997
 
998
        // Save the original dataroot files if not done (only executed the first time).
999
        if (!file_exists($jsonfilepath)) {
1000
            $listfiles = [];
1001
            $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.';
1002
            $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..';
1003
            $listfiles[$currentdir] = $currentdir;
1004
            $listfiles[$parentdir] = $parentdir;
1005
 
1006
            $filedir = self::get_dataroot() . '/filedir';
1007
            if (file_exists($filedir)) {
1008
                $directory = new RecursiveDirectoryIterator($filedir);
1009
                foreach (new RecursiveIteratorIterator($directory) as $file) {
1010
                    if ($file->isDir()) {
1011
                        $key = substr($file->getPath(), strlen(self::get_dataroot() . '/'));
1012
                    } else {
1013
                        $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/'));
1014
                    }
1015
                    $listfiles[$key] = $key;
1016
                }
1017
            }
1018
 
1019
            // Save the file list in a JSON file.
1020
            $fp = fopen($jsonfilepath, 'w');
1021
            fwrite($fp, json_encode(array_values($listfiles)));
1022
            fclose($fp);
1023
        }
1024
    }
1025
 
1026
    /**
1027
     * Return list of environment versions on which tests will run.
1028
     * Environment includes:
1029
     * - moodleversion
1030
     * - phpversion
1031
     * - dbtype
1032
     * - dbversion
1033
     * - os
1034
     *
1035
     * @return array
1036
     */
1037
    public static function get_environment() {
1038
        global $CFG, $DB;
1039
 
1040
        $env = [];
1041
 
1042
        // Add moodle version.
1043
        $release = null;
1044
        require("$CFG->dirroot/version.php");
1045
        $env['moodleversion'] = $release;
1046
 
1047
        // Add php version.
1048
        $phpversion = phpversion();
1049
        $env['phpversion'] = $phpversion;
1050
 
1051
        // Add database type and version.
1052
        $dbtype = $CFG->dbtype;
1053
        $dbinfo = $DB->get_server_info();
1054
        $dbversion = $dbinfo['version'];
1055
        $env['dbtype'] = $dbtype;
1056
        $env['dbversion'] = $dbversion;
1057
 
1058
        // OS details.
1059
        $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m');
1060
        $env['os'] = $osdetails;
1061
 
1062
        return $env;
1063
    }
1064
}