Proyectos de Subversion Moodle

Rev

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