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