Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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   * PHPUnit autoloader for Moodle.
  19   *
  20   * @package    core
  21   * @category   phpunit
  22   * @copyright  2013 Petr Skoda {@link http://skodak.org}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  /**
  27   * Class phpunit_autoloader.
  28   *
  29   * Please notice that phpunit testcases obey frankenstyle naming rules,
  30   * that is full component prefix + _testcase postfix. The files are expected
  31   * in tests directory inside each component. There are some extra tests
  32   * directories which require both classname and file path.
  33   *
  34   * Examples:
  35   *
  36   * vendor/bin/phpunit core_component_testcase
  37   * vendor/bin/phpunit lib/tests/component_test.php
  38   * vendor/bin/phpunit core_component_testcase lib/tests/component_test.php
  39   *
  40   * @package    core
  41   * @category   phpunit
  42   * @copyright  2013 Petr Skoda {@link http://skodak.org}
  43   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class phpunit_autoloader implements \PHPUnit\Runner\TestSuiteLoader {
  46      public function load(string $suiteClassName, string $suiteClassFile = ''): ReflectionClass {
  47          global $CFG;
  48  
  49          // Let's guess what user entered on the commandline...
  50          if ($suiteClassFile) {
  51              // This means they either entered the class+path or path only.
  52              if (strpos($suiteClassName, '/') !== false) {
  53                  // Class names can not contain slashes,
  54                  // user entered only path without testcase class name.
  55                  return $this->guess_class_from_path($suiteClassFile);
  56              }
  57              if (strpos($suiteClassName, '\\') !== false and strpos($suiteClassFile, $suiteClassName.'.php') !== false) {
  58                  // This must be backslashed windows path.
  59                  return $this->guess_class_from_path($suiteClassFile);
  60              }
  61          }
  62  
  63          if (class_exists($suiteClassName, false)) {
  64              $class = new ReflectionClass($suiteClassName);
  65              return $class;
  66          }
  67  
  68          if ($suiteClassFile) {
  69              PHPUnit\Util\Fileloader::checkAndLoad($suiteClassFile);
  70              if (class_exists($suiteClassName, false)) {
  71                  $class = new ReflectionClass($suiteClassName);
  72                  return $class;
  73              }
  74  
  75              throw new PHPUnit\Framework\Exception(
  76                  sprintf("Class '%s' could not be found in '%s'.", $suiteClassName, $suiteClassFile)
  77              );
  78          }
  79  
  80          /*
  81           * Try standard testcase naming rules based on frankenstyle component:
  82           *   1/ test classes should use standard frankenstyle class names plus suffix "_testcase"
  83           *   2/ test classes should be stored in files with suffix "_test"
  84           */
  85  
  86          $parts = explode('_', $suiteClassName);
  87          $suffix = end($parts);
  88          $component = '';
  89  
  90          if ($suffix === 'testcase') {
  91              unset($parts[key($parts)]);
  92              while($parts) {
  93                  if (!$component) {
  94                      $component = array_shift($parts);
  95                  } else {
  96                      $component = $component . '_' . array_shift($parts);
  97                  }
  98                  // Try standard plugin and core subsystem locations.
  99                  if ($fulldir = core_component::get_component_directory($component)) {
 100                      $testfile = implode('_', $parts);
 101                      $fullpath = "{$fulldir}/tests/{$testfile}_test.php";
 102                      if (is_readable($fullpath)) {
 103                          include_once($fullpath);
 104                          if (class_exists($suiteClassName, false)) {
 105                              $class = new ReflectionClass($suiteClassName);
 106                              return $class;
 107                          }
 108                      }
 109                  }
 110              }
 111              // The last option is testsuite directories in main phpunit.xml file.
 112              $xmlfile = "$CFG->dirroot/phpunit.xml";
 113              if (is_readable($xmlfile) and $xml = file_get_contents($xmlfile)) {
 114                  $dom = new DOMDocument();
 115                  $dom->loadXML($xml);
 116                  $nodes = $dom->getElementsByTagName('testsuite');
 117                  foreach ($nodes as $node) {
 118                      /** @var DOMNode $node */
 119                      $suitename = trim($node->attributes->getNamedItem('name')->nodeValue);
 120                      if (strpos($suitename, 'core') !== 0 or strpos($suitename, ' ') !== false) {
 121                          continue;
 122                      }
 123                      // This is a nasty hack: testsuit names are sometimes used as prefix for testcases
 124                      // in non-standard core subsystem locations.
 125                      if (strpos($suiteClassName, $suitename) !== 0) {
 126                          continue;
 127                      }
 128                      foreach ($node->childNodes as $dirnode) {
 129                          /** @var DOMNode $dirnode */
 130                          $dir = trim($dirnode->textContent);
 131                          if (!$dir) {
 132                              continue;
 133                          }
 134                          $dir = $CFG->dirroot.'/'.$dir;
 135                          $parts = explode('_', $suitename);
 136                          $prefix = '';
 137                          while ($parts) {
 138                              if ($prefix) {
 139                                  $prefix = $prefix.'_'.array_shift($parts);
 140                              } else {
 141                                  $prefix = array_shift($parts);
 142                              }
 143                              $filename = substr($suiteClassName, strlen($prefix)+1);
 144                              $filename = preg_replace('/testcase$/', 'test', $filename);
 145                              if (is_readable("$dir/$filename.php")) {
 146                                  include_once("$dir/$filename.php");
 147                                  if (class_exists($suiteClassName, false)) {
 148                                      $class = new ReflectionClass($suiteClassName);
 149                                      return $class;
 150                                  }
 151                              }
 152                          }
 153                      }
 154                  }
 155              }
 156          }
 157  
 158          throw new PHPUnit\Framework\Exception(
 159              sprintf("Class '%s' could not be found in '%s'.", $suiteClassName, $suiteClassFile)
 160          );
 161      }
 162  
 163      protected function guess_class_from_path($file) {
 164          // Somebody is using just the file name, we need to look inside the file and guess the testcase
 165          // class name. Let's throw fatal error if there are more testcases in one file.
 166  
 167          $classes = get_declared_classes();
 168          PHPUnit\Util\Fileloader::checkAndLoad($file);
 169          $includePathFilename = stream_resolve_include_path($file);
 170          $loadedClasses = array_diff(get_declared_classes(), $classes);
 171  
 172          $candidates = array();
 173  
 174          foreach ($loadedClasses as $loadedClass) {
 175              $class = new ReflectionClass($loadedClass);
 176  
 177              if ($class->isSubclassOf('PHPUnit\Framework\TestCase') and !$class->isAbstract()) {
 178                  if (realpath($includePathFilename) === realpath($class->getFileName())) {
 179                      $candidates[] = $loadedClass;
 180                  }
 181              }
 182          }
 183  
 184          if (count($candidates) == 0) {
 185              throw new PHPUnit\Framework\Exception(
 186                  sprintf("File '%s' does not contain any test cases.", $file)
 187              );
 188          }
 189  
 190          if (count($candidates) > 1) {
 191              throw new PHPUnit\Framework\Exception(
 192                  sprintf("File '%s' contains multiple test cases: ".implode(', ', $candidates), $file)
 193              );
 194          }
 195  
 196          $classname = reset($candidates);
 197          return new ReflectionClass($classname);
 198      }
 199  
 200      public function reload(ReflectionClass $aClass): ReflectionClass {
 201          return $aClass;
 202      }
 203  }