Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]

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