Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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
use core\di;
18
use core\hook;
19
 
20
/**
21
 * Advanced PHPUnit test case customised for Moodle.
22
 *
23
 * @package    core
24
 * @category   phpunit
25
 * @copyright  2012 Petr Skoda {@link http://skodak.org}
26
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
 */
28
abstract class advanced_testcase extends base_testcase {
29
    /** @var bool automatically reset everything? null means log changes */
30
    // phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
31
    private $resetAfterTest;
32
 
33
    /** @var moodle_transaction */
34
    private $testdbtransaction;
35
 
36
    /** @var int timestamp used for current time asserts */
37
    private $currenttimestart;
38
 
39
    /**
40
     * Constructs a test case with the given name.
41
     *
42
     * Note: use setUp() or setUpBeforeClass() in your test cases.
43
     *
44
     * @param string $name
45
     * @param array  $data
46
     * @param string $dataName
47
     */
48
    final public function __construct($name = null, array $data = [], $dataname = '') {
49
        parent::__construct($name, $data, $dataname);
50
 
51
        $this->setBackupGlobals(false);
52
        $this->setBackupStaticAttributes(false);
53
        $this->setPreserveGlobalState(false);
54
    }
55
 
56
    /**
57
     * Runs the bare test sequence.
58
     */
59
    final public function runBare(): void {
60
        global $DB;
61
 
62
        if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
63
            // This happens when previous test does not reset, we can not use transactions.
64
            $this->testdbtransaction = null;
65
        } else if ($DB->get_dbfamily() === 'postgres' || $DB->get_dbfamily() === 'mssql') {
66
            // Database must allow rollback of DDL, so no mysql here.
67
            $this->testdbtransaction = $DB->start_delegated_transaction();
68
        }
69
 
70
        try {
71
            $this->setCurrentTimeStart();
72
            parent::runBare();
73
            // Set DB reference in case somebody mocked it in test.
74
            $DB = phpunit_util::get_global_backup('DB');
75
 
76
            // Deal with any debugging messages.
77
            $debugerror = phpunit_util::display_debugging_messages(true);
78
            $this->resetDebugging();
79
            if (!empty($debugerror)) {
80
                trigger_error('Unexpected debugging() call detected.' . "\n" . $debugerror, E_USER_NOTICE);
81
            }
82
        } catch (Exception $ex) {
83
            $e = $ex;
84
        } catch (Throwable $ex) {
85
            // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
86
            $e = $ex;
87
        }
88
 
89
        if (isset($e)) {
90
            // Cleanup after failed expectation.
91
            self::resetAllData();
92
            throw $e;
93
        }
94
 
95
        if (!$this->testdbtransaction || $this->testdbtransaction->is_disposed()) {
96
            $this->testdbtransaction = null;
97
        }
98
 
99
        if ($this->resetAfterTest === true) {
100
            if ($this->testdbtransaction) {
101
                $DB->force_transaction_rollback();
102
                phpunit_util::reset_all_database_sequences();
103
                phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // No db reset necessary.
104
            }
105
            self::resetAllData(null);
106
        } else if ($this->resetAfterTest === false) {
107
            if ($this->testdbtransaction) {
108
                $this->testdbtransaction->allow_commit();
109
            }
110
            // Keep all data untouched for other tests.
111
        } else {
112
            // Reset but log what changed.
113
            if ($this->testdbtransaction) {
114
                try {
115
                    $this->testdbtransaction->allow_commit();
116
                } catch (dml_transaction_exception $e) {
117
                    self::resetAllData();
118
                    throw new coding_exception('Invalid transaction state detected in test ' . $this->getName());
119
                }
120
            }
121
            self::resetAllData(true);
122
        }
123
 
124
        // Reset context cache.
125
        context_helper::reset_caches();
126
 
127
        // Make sure test did not forget to close transaction.
128
        if ($DB->is_transaction_started()) {
129
            self::resetAllData();
130
            if (
131
                $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_PASSED
132
                || $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED
133
                || $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE
134
            ) {
135
                throw new coding_exception('Test ' . $this->getName() . ' did not close database transaction');
136
            }
137
        }
138
    }
139
 
140
    /**
141
     * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
142
     */
143
    protected function createXMLDataSet() {
144
        throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_files() instead.');
145
    }
146
 
147
    /**
148
     * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
149
     */
150
    protected function createCsvDataSet() {
151
        throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_files() instead.');
152
    }
153
 
154
    /**
155
     * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
156
     */
157
    protected function createArrayDataSet() {
158
        throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_array() instead.');
159
    }
160
 
161
    /**
162
     * @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
163
     */
164
    protected function loadDataSet() {
165
        throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset->to_database() instead.');
166
    }
167
 
168
    /**
169
     * Creates a new dataset from CVS/XML files.
170
     *
171
     * This method accepts an array of full paths to CSV or XML files to be loaded
172
     * into the dataset. For CSV files, the name of the table which the file belongs
173
     * to needs to be specified. Example:
174
     *
175
     *   $fullpaths = [
176
     *       '/path/to/users.xml',
177
     *       'course' => '/path/to/courses.csv',
178
     *   ];
179
     *
180
     * @since Moodle 3.10
181
     *
182
     * @param array $files full paths to CSV or XML files to load.
183
     * @return phpunit_dataset
184
     */
185
    protected function dataset_from_files(array $files) {
186
        // We ignore $delimiter, $enclosure and $escape, use the default ones in your fixtures.
187
        $dataset = new phpunit_dataset();
188
        $dataset->from_files($files);
189
        return $dataset;
190
    }
191
 
192
    /**
193
     * Creates a new dataset from string (CSV or XML).
194
     *
195
     * @since Moodle 3.10
196
     *
197
     * @param string $content contents (CSV or XML) to load.
198
     * @param string $type format of the content to be loaded (csv or xml).
199
     * @param string $table name of the table which the file belongs to (only for CSV files).
200
     * @return phpunit_dataset
201
     */
202
    protected function dataset_from_string(string $content, string $type, ?string $table = null) {
203
        $dataset = new phpunit_dataset();
204
        $dataset->from_string($content, $type, $table);
205
        return $dataset;
206
    }
207
 
208
    /**
209
     * Creates a new dataset from PHP array.
210
     *
211
     * @since Moodle 3.10
212
     *
213
     * @param array $data array of tables, see {@see phpunit_dataset::from_array()} for supported formats.
214
     * @return phpunit_dataset
215
     */
216
    protected function dataset_from_array(array $data) {
217
        $dataset = new phpunit_dataset();
218
        $dataset->from_array($data);
219
        return $dataset;
220
    }
221
 
222
    /**
223
     * Call this method from test if you want to make sure that
224
     * the resetting of database is done the slow way without transaction
225
     * rollback.
226
     *
227
     * This is useful especially when testing stuff that is not compatible with transactions.
228
     *
229
     * @return void
230
     */
231
    public function preventResetByRollback() {
232
        if ($this->testdbtransaction && !$this->testdbtransaction->is_disposed()) {
233
            $this->testdbtransaction->allow_commit();
234
            $this->testdbtransaction = null;
235
        }
236
    }
237
 
238
    /**
239
     * Reset everything after current test.
240
     * @param bool $reset true means reset state back, false means keep all data for the next test,
241
     *      null means reset state and show warnings if anything changed
242
     * @return void
243
     */
244
    public function resetAfterTest($reset = true) {
245
        $this->resetAfterTest = $reset;
246
    }
247
 
248
    /**
249
     * Return debugging messages from the current test.
250
     * @return array with instances having 'message', 'level' and 'stacktrace' property.
251
     */
252
    public function getDebuggingMessages() {
253
        return phpunit_util::get_debugging_messages();
254
    }
255
 
256
    /**
257
     * Clear all previous debugging messages in current test
258
     * and revert to default DEVELOPER_DEBUG level.
259
     */
260
    public function resetDebugging() {
261
        phpunit_util::reset_debugging();
262
    }
263
 
264
    /**
265
     * Assert that exactly debugging was just called once.
266
     *
267
     * Discards the debugging message if successful.
268
     *
269
     * @param null|string $debugmessage null means any
270
     * @param null|string $debuglevel null means any
271
     * @param string $message
272
     */
273
    public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
274
        $debugging = $this->getDebuggingMessages();
275
        $debugdisplaymessage = "\n" . phpunit_util::display_debugging_messages(true);
276
        $this->resetDebugging();
277
 
278
        $count = count($debugging);
279
 
280
        if ($count == 0) {
281
            if ($message === '') {
282
                $message = 'Expectation failed, debugging() not triggered.';
283
            }
284
            $this->fail($message);
285
        }
286
        if ($count > 1) {
287
            if ($message === '') {
288
                $message = 'Expectation failed, debugging() triggered ' . $count . ' times.' . $debugdisplaymessage;
289
            }
290
            $this->fail($message);
291
        }
292
        $this->assertEquals(1, $count);
293
 
294
        $message .= $debugdisplaymessage;
295
        $debug = reset($debugging);
296
        if ($debugmessage !== null) {
297
            $this->assertSame($debugmessage, $debug->message, $message);
298
        }
299
        if ($debuglevel !== null) {
300
            $this->assertSame($debuglevel, $debug->level, $message);
301
        }
302
    }
303
 
304
    /**
305
     * Asserts how many times debugging has been called.
306
     *
307
     * @param int $expectedcount The expected number of times
308
     * @param array $debugmessages Expected debugging messages, one for each expected message.
309
     * @param array $debuglevels Expected debugging levels, one for each expected message.
310
     * @param string $message
311
     * @return void
312
     */
313
    public function assertdebuggingcalledcount($expectedcount, $debugmessages = [], $debuglevels = [], $message = '') {
314
        if (!is_int($expectedcount)) {
315
            throw new coding_exception('assertDebuggingCalledCount $expectedcount argument should be an integer.');
316
        }
317
 
318
        $debugging = $this->getDebuggingMessages();
319
        $message .= "\n" . phpunit_util::display_debugging_messages(true);
320
        $this->resetDebugging();
321
 
322
        $this->assertEquals($expectedcount, count($debugging), $message);
323
 
324
        if ($debugmessages) {
325
            if (!is_array($debugmessages) || count($debugmessages) != $expectedcount) {
326
                throw new coding_exception(
327
                    'assertDebuggingCalledCount $debugmessages should contain ' . $expectedcount . ' messages',
328
                );
329
            }
330
            foreach ($debugmessages as $key => $debugmessage) {
331
                $this->assertSame($debugmessage, $debugging[$key]->message, $message);
332
            }
333
        }
334
 
335
        if ($debuglevels) {
336
            if (!is_array($debuglevels) || count($debuglevels) != $expectedcount) {
337
                throw new coding_exception(
338
                    'assertDebuggingCalledCount $debuglevels should contain ' . $expectedcount . ' messages',
339
                );
340
            }
341
            foreach ($debuglevels as $key => $debuglevel) {
342
                $this->assertSame($debuglevel, $debugging[$key]->level, $message);
343
            }
344
        }
345
    }
346
 
347
    /**
348
     * Call when no debugging() messages expected.
349
     * @param string $message
350
     */
351
    public function assertDebuggingNotCalled($message = '') {
352
        $debugging = $this->getDebuggingMessages();
353
        $count = count($debugging);
354
 
355
        if ($message === '') {
356
            $message = 'Expectation failed, debugging() was triggered.';
357
        }
358
        $message .= "\n".phpunit_util::display_debugging_messages(true);
359
        $this->resetDebugging();
360
        $this->assertEquals(0, $count, $message);
361
    }
362
 
363
    /**
364
     * Assert that an event legacy data is equal to the expected value.
365
     *
366
     * @param mixed $expected expected data.
367
     * @param \core\event\base $event the event object.
368
     * @param string $message
369
     * @return void
370
     */
371
    public function assertEventLegacyData($expected, \core\event\base $event, $message = '') {
372
        $legacydata = phpunit_event_mock::testable_get_legacy_eventdata($event);
373
        if ($message === '') {
374
            $message = 'Event legacy data does not match expected value.';
375
        }
376
        $this->assertEquals($expected, $legacydata, $message);
377
    }
378
 
379
    /**
380
     * Assert that an event legacy log data is equal to the expected value.
381
     *
382
     * @param mixed $expected expected data.
383
     * @param \core\event\base $event the event object.
384
     * @param string $message
385
     * @return void
386
     */
387
    public function assertEventLegacyLogData($expected, \core\event\base $event, $message = '') {
388
        $legacydata = phpunit_event_mock::testable_get_legacy_logdata($event);
389
        if ($message === '') {
390
            $message = 'Event legacy log data does not match expected value.';
391
        }
392
        $this->assertEquals($expected, $legacydata, $message);
393
    }
394
 
395
    /**
396
     * Assert that various event methods are not using event->context
397
     *
398
     * While restoring context might not be valid and it should not be used by event url
399
     * or description methods.
400
     *
401
     * @param \core\event\base $event the event object.
402
     * @param string $message
403
     * @return void
404
     */
405
    public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
406
        // Save current event->context and set it to false.
407
        $eventcontext = phpunit_event_mock::testable_get_event_context($event);
408
        phpunit_event_mock::testable_set_event_context($event, false);
409
        if ($message === '') {
410
            $message = 'Event should not use context property of event in any method.';
411
        }
412
 
413
        // Test event methods should not use event->context.
414
        $event->get_url();
415
        $event->get_description();
416
 
417
        // Restore event->context (note that this is unreachable when the event uses context). But ok for correct events.
418
        phpunit_event_mock::testable_set_event_context($event, $eventcontext);
419
    }
420
 
421
    /**
422
     * Stores current time as the base for assertTimeCurrent().
423
     *
424
     * Note: this is called automatically before calling individual test methods.
425
     * @return int current time
426
     */
427
    public function setCurrentTimeStart() {
428
        $this->currenttimestart = time();
429
        return $this->currenttimestart;
430
    }
431
 
432
    /**
433
     * Assert that: start < $time < time()
434
     * @param int $time
435
     * @param string $message
436
     * @return void
437
     */
438
    public function assertTimeCurrent($time, $message = '') {
439
        $msg = ($message === '') ? 'Time is lower that allowed start value' : $message;
440
        $this->assertGreaterThanOrEqual($this->currenttimestart, $time, $msg);
441
        $msg = ($message === '') ? 'Time is in the future' : $message;
442
        $this->assertLessThanOrEqual(time(), $time, $msg);
443
    }
444
 
445
    /**
446
     * Starts message redirection.
447
     *
448
     * You can verify if messages were sent or not by inspecting the messages
449
     * array in the returned messaging sink instance. The redirection
450
     * can be stopped by calling $sink->close();
451
     *
452
     * @return phpunit_message_sink
453
     */
454
    public function redirectMessages() {
455
        return phpunit_util::start_message_redirection();
456
    }
457
 
458
    /**
459
     * Starts email redirection.
460
     *
461
     * You can verify if email were sent or not by inspecting the email
462
     * array in the returned phpmailer sink instance. The redirection
463
     * can be stopped by calling $sink->close();
464
     *
465
     * @return phpunit_message_sink
466
     */
467
    public function redirectEmails() {
468
        return phpunit_util::start_phpmailer_redirection();
469
    }
470
 
471
    /**
472
     * Starts event redirection.
473
     *
474
     * You can verify if events were triggered or not by inspecting the events
475
     * array in the returned event sink instance. The redirection
476
     * can be stopped by calling $sink->close();
477
     *
478
     * @return phpunit_event_sink
479
     */
480
    public function redirectEvents() {
481
        return phpunit_util::start_event_redirection();
482
    }
483
 
484
    /**
485
     * Override hook callbacks.
486
     *
487
     * @param string $hookname
488
     * @param callable $callback
489
     * @return void
490
     */
491
    public function redirectHook(string $hookname, callable $callback): void {
492
        di::get(hook\manager::class)->phpunit_redirect_hook($hookname, $callback);
493
    }
494
 
495
    /**
496
     * Remove all hook overrides.
497
     *
498
     * @return void
499
     */
500
    public function stopHookRedirections(): void {
501
        di::get(hook\manager::class)->phpunit_stop_redirections();
502
    }
503
 
504
    /**
505
     * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
506
     *
507
     * @param bool $detectchanges
508
     *      true  - changes in global state and database are reported as errors
509
     *      false - no errors reported
510
     *      null  - only critical problems are reported as errors
511
     * @return void
512
     */
513
    public static function resetAllData($detectchanges = false) {
514
        phpunit_util::reset_all_data($detectchanges);
515
    }
516
 
517
    /**
518
     * Set current $USER, reset access cache.
519
     * @static
520
     * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
521
     * @return void
522
     */
523
    public static function setUser($user = null) {
524
        global $CFG, $DB;
525
 
526
        if (is_object($user)) {
527
            $user = clone($user);
528
        } else if (!$user) {
529
            $user = new stdClass();
530
            $user->id = 0;
531
            $user->mnethostid = $CFG->mnet_localhost_id;
532
        } else {
533
            $user = $DB->get_record('user', ['id' => $user]);
534
        }
535
        unset($user->description);
536
        unset($user->access);
537
        unset($user->preference);
538
 
539
        // Enusre session is empty, as it may contain caches and user specific info.
540
        \core\session\manager::init_empty_session();
541
 
542
        \core\session\manager::set_user($user);
543
    }
544
 
545
    /**
546
     * Set current $USER to admin account, reset access cache.
547
     * @static
548
     * @return void
549
     */
550
    public static function setAdminUser() {
551
        self::setUser(2);
552
    }
553
 
554
    /**
555
     * Set current $USER to guest account, reset access cache.
556
     * @static
557
     * @return void
558
     */
559
    public static function setGuestUser() {
560
        self::setUser(1);
561
    }
562
 
563
    /**
564
     * Change server and default php timezones.
565
     *
566
     * @param string $servertimezone timezone to set in $CFG->timezone (not validated)
567
     * @param string $defaultphptimezone timezone to fake default php timezone (must be valid)
568
     */
569
    public static function setTimezone($servertimezone = 'Australia/Perth', $defaultphptimezone = 'Australia/Perth') {
570
        global $CFG;
571
        $CFG->timezone = $servertimezone;
572
        core_date::phpunit_override_default_php_timezone($defaultphptimezone);
573
        core_date::set_default_server_timezone();
574
    }
575
 
576
    /**
577
     * Get data generator
578
     * @static
579
     * @return testing_data_generator
580
     */
581
    public static function getDataGenerator() {
582
        return phpunit_util::get_data_generator();
583
    }
584
 
585
    /**
586
     * Returns UTL of the external test file.
587
     *
588
     * The result depends on the value of following constants:
589
     *  - TEST_EXTERNAL_FILES_HTTP_URL
590
     *  - TEST_EXTERNAL_FILES_HTTPS_URL
591
     *
592
     * They should point to standard external test files repository,
593
     * it defaults to 'http://download.moodle.org/unittest'.
594
     *
595
     * False value means skip tests that require external files.
596
     *
597
     * @param string $path
598
     * @param bool $https true if https required
599
     * @return string url
600
     */
601
    public function getExternalTestFileUrl($path, $https = false) {
602
        $path = ltrim($path, '/');
603
        if ($path) {
604
            $path = '/' . $path;
605
        }
606
        if ($https) {
607
            if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
608
                if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
609
                    $this->markTestSkipped('Tests using external https test files are disabled');
610
                }
611
                return TEST_EXTERNAL_FILES_HTTPS_URL . $path;
612
            }
613
            return 'https://download.moodle.org/unittest' . $path;
614
        }
615
 
616
        if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
617
            if (!TEST_EXTERNAL_FILES_HTTP_URL) {
618
                $this->markTestSkipped('Tests using external http test files are disabled');
619
            }
620
            return TEST_EXTERNAL_FILES_HTTP_URL . $path;
621
        }
622
        return 'http://download.moodle.org/unittest' . $path;
623
    }
624
 
625
    /**
626
     * Recursively visit all the files in the source tree. Calls the callback
627
     * function with the pathname of each file found.
628
     *
629
     * @param string $path the folder to start searching from.
630
     * @param string $callback the method of this class to call with the name of each file found.
631
     * @param string $fileregexp a regexp used to filter the search (optional).
632
     * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
633
     *     only files that match the regexp will be included. (default false).
634
     * @param array $ignorefolders will not go into any of these folders (optional).
635
     * @return void
636
     */
637
    public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
638
        $files = scandir($path);
639
 
640
        foreach ($files as $file) {
641
            $filepath = $path . '/' . $file;
642
            if (strpos($file, '.') === 0) {
643
                // Don't check hidden files.
644
                continue;
645
            } else if (is_dir($filepath)) {
646
                if (!in_array($filepath, $ignorefolders)) {
647
                    $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
648
                }
649
            } else if ($exclude xor preg_match($fileregexp, $filepath)) {
650
                $this->$callback($filepath);
651
            }
652
        }
653
    }
654
 
655
    /**
656
     * Wait for a second to roll over, ensures future calls to time() return a different result.
657
     *
658
     * This is implemented instead of sleep() as we do not need to wait a full second. In some cases
659
     * due to calls we may wait more than sleep() would have, on average it will be less.
660
     */
661
    public function waitForSecond() {
662
        $starttime = time();
663
        while (time() == $starttime) {
664
            usleep(50000);
665
        }
666
    }
667
 
668
    /**
669
     * Run adhoc tasks, optionally matching the specified classname.
670
     *
671
     * @param   string  $matchclass The name of the class to match on.
672
     * @param   int     $matchuserid The userid to match.
673
     */
674
    protected function runAdhocTasks($matchclass = '', $matchuserid = null) {
675
        global $DB;
676
 
677
        $params = [];
678
        if (!empty($matchclass)) {
679
            if (strpos($matchclass, '\\') !== 0) {
680
                $matchclass = '\\' . $matchclass;
681
            }
682
            $params['classname'] = $matchclass;
683
        }
684
 
685
        if (!empty($matchuserid)) {
686
            $params['userid'] = $matchuserid;
687
        }
688
 
689
        $lock = $this->createMock(\core\lock\lock::class);
690
        $cronlock = $this->createMock(\core\lock\lock::class);
691
 
692
        $tasks = $DB->get_recordset('task_adhoc', $params);
693
        foreach ($tasks as $record) {
694
            // Note: This is for cron only.
695
            // We do not lock the tasks.
696
            $task = \core\task\manager::adhoc_task_from_record($record);
697
 
698
            $user = null;
699
            if ($userid = $task->get_userid()) {
700
                // This task has a userid specified.
701
                $user = \core_user::get_user($userid);
702
 
703
                // User found. Check that they are suitable.
704
                \core_user::require_active_user($user, true, true);
705
            }
706
 
707
            $task->set_lock($lock);
708
            $cronlock->release();
709
 
710
            \core\cron::prepare_core_renderer();
711
            \core\cron::setup_user($user);
712
 
713
            $task->execute();
714
            \core\task\manager::adhoc_task_complete($task);
715
 
716
            unset($task);
717
        }
718
        $tasks->close();
719
    }
720
 
721
    /**
722
     * Run adhoc tasks.
723
     */
724
    protected function run_all_adhoc_tasks(): void {
725
        // Run the adhoc task.
726
        while ($task = \core\task\manager::get_next_adhoc_task(time())) {
727
            $task->execute();
728
            \core\task\manager::adhoc_task_complete($task);
729
        }
730
    }
731
 
732
    /**
733
     * Mock the clock with an incrementing clock.
734
     *
735
     * @param null|int $starttime
736
     * @return \incrementing_clock
737
     */
738
    public function mock_clock_with_incrementing(
739
        ?int $starttime = null,
740
    ): \incrementing_clock {
741
        require_once(dirname(__DIR__, 2) . '/testing/classes/incrementing_clock.php');
742
        $clock = new \incrementing_clock($starttime);
743
 
744
        \core\di::set(\core\clock::class, $clock);
745
 
746
        return $clock;
747
    }
748
 
749
    /**
750
     * Mock the clock with a frozen clock.
751
     *
752
     * @param null|int $time
753
     * @return \frozen_clock
754
     */
755
    public function mock_clock_with_frozen(
756
        ?int $time = null,
757
    ): \frozen_clock {
758
        require_once(dirname(__DIR__, 2) . '/testing/classes/frozen_clock.php');
759
        $clock = new \frozen_clock($time);
760
 
761
        \core\di::set(\core\clock::class, $clock);
762
 
763
        return $clock;
764
    }
765
 
766
    /**
767
     * Add a mocked plugintype to Moodle.
768
     *
769
     * A new plugintype name must be provided with a path to the plugintype's root.
770
     *
771
     * Please note that tests calling this method must be run in separate isolation mode.
772
     * Please avoid using this if at all possible.
773
     *
774
     * @param string $plugintype The name of the plugintype
775
     * @param string $path The path to the plugintype's root
776
     */
777
    protected function add_mocked_plugintype(
778
        string $plugintype,
779
        string $path,
780
    ): void {
781
        require_phpunit_isolation();
782
 
783
        $mockedcomponent = new \ReflectionClass(\core_component::class);
784
        $plugintypes = $mockedcomponent->getStaticPropertyValue('plugintypes');
785
 
786
        if (array_key_exists($plugintype, $plugintypes)) {
787
            throw new \coding_exception("The plugintype '{$plugintype}' already exists.");
788
        }
789
 
790
        $plugintypes[$plugintype] = $path;
791
        $mockedcomponent->setStaticPropertyValue('plugintypes', $plugintypes);
792
 
793
        $this->resetDebugging();
794
    }
795
 
796
    /**
797
     * Add a mocked plugin to Moodle.
798
     *
799
     * A new plugin name must be provided with a path to the plugin's root.
800
     * The plugin type must already exist (or have been mocked separately).
801
     *
802
     * Please note that tests calling this method must be run in separate isolation mode.
803
     * Please avoid using this if at all possible.
804
     *
805
     * @param string $plugintype The name of the plugintype
806
     * @param string $pluginname The name of the plugin
807
     * @param string $path The path to the plugin's root
808
     */
809
    protected function add_mocked_plugin(
810
        string $plugintype,
811
        string $pluginname,
812
        string $path,
813
    ): void {
814
        require_phpunit_isolation();
815
 
816
        $mockedcomponent = new \ReflectionClass(\core_component::class);
817
        $plugins = $mockedcomponent->getStaticPropertyValue('plugins');
818
 
819
        if (!array_key_exists($plugintype, $plugins)) {
820
            $plugins[$plugintype] = [];
821
        }
822
 
823
        $plugins[$plugintype][$pluginname] = $path;
824
        $mockedcomponent->setStaticPropertyValue('plugins', $plugins);
825
        $this->resetDebugging();
826
    }
827
}