PageRenderTime 37ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/hphp/test/frameworks/TestFinder.php

https://gitlab.com/Blueprint-Marketing/hhvm
PHP | 277 lines | 225 code | 14 blank | 38 comment | 35 complexity | 5a56306a37baeaad877118399d38cfd1 MD5 | raw file
  1. <?hh
  2. require_once __DIR__.'/../../../hphp/tools/command_line_lib.php';
  3. require_once __DIR__.'/SortedIterator.php';
  4. require_once __DIR__.'/utils.php';
  5. require_once __DIR__.'/TestFindModes.php';
  6. class TestFinder {
  7. private Set $test_files;
  8. public function __construct(private string $framework_name,
  9. private string $tests_file,
  10. private string $test_files_file,
  11. private string $test_path,
  12. private string $test_file_pattern,
  13. private string $config_file,
  14. private ?string $bootstrap_file) {
  15. $this->findTestFiles();
  16. }
  17. // BUG FIX NEEDED: Due to some recent updates to Reflection, this default
  18. // method for finding tests in frameworks is failing for certain
  19. // frameworks. The two failing frameworks are Monolog and ZF2. Luckily,
  20. // I was able to move those tests to use TestFindModes::TOKEN. But
  21. // there is a problem, either with this code (that worked before the
  22. // Reflection changes) or Reflection itself.
  23. // See git log 63e752f4f0bd141d4e401e7056b4850e7b9a4406 and git log
  24. // faec990edc3e3e8f3b491070b0e8cd90e9df7a4d for the addition of the
  25. // new ext_reflection-classes.php class.
  26. public function findTestMethods(): void {
  27. // For reflection in finding tests
  28. // Since PHPUnit 3.8, Autoload.php has gone away.
  29. // And the main source directory 'PHPUnit' was changed to 'src'.
  30. // So check for both and do the right thing
  31. if (file_exists(__DIR__."/vendor/phpunit/phpunit/src/")) {
  32. // For 3.8+
  33. require_once __DIR__."/vendor/autoload.php";
  34. } else if (file_exists(__DIR__."/vendor/phpunit/phpunit/PHPUnit/")) {
  35. // For 3.7 and below
  36. require_once __DIR__."/vendor/phpunit/phpunit/PHPUnit/Autoload.php";
  37. } else {
  38. // Fallback to token based test finding
  39. findTestMethodsViaToken();
  40. return;
  41. }
  42. if ($this->bootstrap_file !== null) {
  43. require_once $this->bootstrap_file;
  44. }
  45. $current_classes = get_declared_classes();
  46. $tests = "";
  47. foreach ($this->test_files as $tf) {
  48. if (strpos($tf, ".phpt") !== false) {
  49. $tests .= $tf.PHP_EOL;
  50. continue;
  51. }
  52. require_once $tf;
  53. // New classes will be brought in by the include; get the difference
  54. // between what was currently loaded.
  55. $file_classes = array_diff(get_declared_classes(), $current_classes);
  56. foreach ($file_classes as $class_name) {
  57. $class = new ReflectionClass($class_name);
  58. if ($class->isSubclassOf("PHPUnit_Framework_TestCase")) {
  59. $class_methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
  60. foreach($class_methods as $method) {
  61. if (strpos($method->getShortName(), "test") === 0 ||
  62. strpos($method->getDocComment(), "@test") !== false) {
  63. $tests .= $class->getName()."::".$method->getShortName().PHP_EOL;
  64. }
  65. }
  66. }
  67. }
  68. // We don't want to see the current classes again in the next
  69. // iteration of the loop. They will now be removed with the array_diff
  70. $current_classes = get_declared_classes();
  71. }
  72. file_put_contents($this->tests_file, $tests);
  73. }
  74. public function findTestsPHPT(): void {
  75. file_put_contents(
  76. $this->tests_file,
  77. implode(PHP_EOL, $this->test_files->toArray())
  78. );
  79. }
  80. public function findTestMethodsViaTokens(): void {
  81. $tests = "";
  82. foreach ($this->test_files as $test_file) {
  83. $php_code = file_get_contents($test_file);
  84. $tokens = token_get_all($php_code);
  85. $count = count($tokens);
  86. $nspace = "";
  87. $class_name = "";
  88. // Get the namespace the class is in, if any
  89. for ($i = 1; $i < $count; $i++) {
  90. if ($tokens[$i - 1][0] === T_NAMESPACE && // namespace keyword
  91. $tokens[$i][0] === T_WHITESPACE) {
  92. $i++;
  93. // Get the full namespace until we hit a ;
  94. while ($tokens[$i][0] !== ";") {
  95. $nspace .= $tokens[$i][1];
  96. $i++;
  97. }
  98. break;
  99. }
  100. }
  101. // Get the namespace qualified (if any) class name
  102. for ($i = 6; $i < $count; $i++) {
  103. if ($tokens[$i - 6][0] === T_CLASS && // class keyword
  104. $tokens[$i - 5][0] === T_WHITESPACE &&
  105. $tokens[$i - 4][0] === T_STRING && // class name
  106. $tokens[$i - 3][0] === T_WHITESPACE &&
  107. $tokens[$i - 2][0] === T_EXTENDS && // extends keyword
  108. $tokens[$i - 1][0] === T_WHITESPACE) {
  109. $classpos = $i - 4;
  110. // parent could be non-namespaced or be namespaced
  111. // So get through all T_NS_SEPARATOR and T_STRING
  112. // Last T_STRING before T_WHITESPACE will be parent name
  113. while ($tokens[$i][0] !== T_WHITESPACE) { $i++;}
  114. if (strpos($tokens[$i - 1][1], "TestCase") !== false || // parent name
  115. strpos($tokens[$i - 1][1], "test_case") !== false) {
  116. $class_name = $tokens[$classpos][1];
  117. }
  118. break;
  119. }
  120. }
  121. // Get all the test functions in the class
  122. for ($i = 2; $i < $count; $i++) {
  123. if ($tokens[$i - 2][0] === T_FUNCTION &&
  124. $tokens[$i - 1][0] === T_WHITESPACE &&
  125. $tokens[$i][0] === T_STRING &&
  126. strpos($tokens[$i][1], "test") === 0) {
  127. if ($nspace !== "") {
  128. $tests .= $nspace."\\";
  129. }
  130. $tests .= $class_name."::".$tokens[$i][1].PHP_EOL;
  131. }
  132. }
  133. }
  134. file_put_contents($this->tests_file, $tests);
  135. }
  136. private function findTestFiles(): void {
  137. $this->test_files = Set {};
  138. $exclude_pattern = "/\.disabled\.hhvm/";
  139. $exclude_dirs = Set {};
  140. $config_xml = simplexml_load_file($this->config_file);
  141. // The bootstrap file will be defined as a top level attribute to the
  142. // <phpunit> element and the path to it will be relative to the location
  143. // of the config file
  144. if ($this->bootstrap_file === null &&
  145. $config_xml->attributes()->bootstrap !== null) {
  146. $this->bootstrap_file = dirname($this->config_file)."/".
  147. $config_xml->attributes()->bootstrap;
  148. }
  149. // Some framework phpunit.xml files do not have a <testsuites><testsuite>
  150. // element, just a single <testsuite> element
  151. $test_suite = $config_xml->testsuites->testsuite;
  152. if ($test_suite === null || $test_suite->count() === 0) {
  153. $test_suite = $config_xml->testsuite;
  154. }
  155. foreach ($test_suite as $suite) {
  156. foreach($suite->exclude as $exclude) {
  157. $exclude_dirs->add($this->test_path."/".$exclude);
  158. }
  159. foreach($suite->file as $file) {
  160. if (file_exists($this->config_file."/".$file)) {
  161. $this->test_files->add($file);
  162. }
  163. }
  164. foreach($suite->directory as $dir) {
  165. $search_dirs = null;
  166. // If config doesn't provide a test suffix, assume the default
  167. $pattern = $dir->attributes()->count() === 0
  168. ? $this->test_file_pattern
  169. : "/".$dir->attributes()->suffix."/";
  170. $search_path = $this->test_path."/".$dir;
  171. // Gotta make sure these dirs don't contain wildcards (Looking at you
  172. // Symfony)
  173. if (strpos($search_path, "*") !== false ||
  174. strpos($search_path, "..") !== false) {
  175. $search_dirs = glob($search_path, GLOB_ONLYDIR);
  176. }
  177. if ($search_dirs !== null) {
  178. foreach($search_dirs as $sd) {
  179. $this->test_files->addAll(find_all_files($pattern,
  180. $sd,
  181. $exclude_pattern,
  182. $exclude_dirs));
  183. }
  184. } else {
  185. $this->test_files->addAll(find_all_files($pattern,
  186. $this->test_path."/".$dir,
  187. $exclude_pattern,
  188. $exclude_dirs));
  189. }
  190. }
  191. }
  192. foreach($this->test_files as $file) {
  193. file_put_contents($this->test_files_file, $file.PHP_EOL, FILE_APPEND);
  194. }
  195. }
  196. }
  197. function tf_help(): void {
  198. display_help("Finds the tests for the given framework",
  199. test_finder_option_map());
  200. }
  201. function test_finder_option_map(): OptionInfoMap {
  202. return Map {
  203. 'help' => Pair {'h', "Print help message."},
  204. 'framework-name:' => Pair {'', "The framework name."},
  205. 'config-file:' => Pair {'', "The full path to the framework xml ".
  206. "configuration file."},
  207. 'bootstrap-file:' => Pair {'', "The full path to the bootstrap file, ".
  208. "if it exists. This is optional."},
  209. 'tests-file:' => Pair {'', "The full path to the file where the ".
  210. "framework tests will be output."},
  211. 'test-files-file:' => Pair {'', "The full path to the file where the ".
  212. "framework test files will be output."},
  213. 'test-path:' => Pair {'', "The full path to the directory where ".
  214. "the testing will begin."},
  215. 'test-file-pattern:' => Pair {'', "The regex pattern which test files ".
  216. "are named for this framework. E.g., ".
  217. "xxxxTest.php or xxxxx.phpt"},
  218. 'test-find-mode:' => Pair {'', "The mode to use in order to find ".
  219. "the tests for this framework. Your ".
  220. "options are 'reflection' (the ".
  221. "default), 'token' or 'phpt'."},
  222. };
  223. }
  224. function tf_main(array $argv): void {
  225. $options = parse_options(test_finder_option_map());
  226. if ($options->containsKey('help')) {
  227. tf_help();
  228. exit(0);
  229. }
  230. try {
  231. $fn = (string) $options['framework-name'];
  232. $tf = (string) $options['tests-file'];
  233. $tff = (string) $options['test-files-file'];
  234. $tp = (string) $options['test-path'];
  235. $tfp = (string) $options['test-file-pattern'];
  236. $cf = (string) $options['config-file'];
  237. $bf = $options->containsKey('bootstrap-file')
  238. ? (string) $options['bootstrap-file']
  239. : null;
  240. $mode = (string) $options['test-find-mode'];
  241. } catch (Exception $e) {
  242. tf_help();
  243. echo "Provide required command line arguments!\n";
  244. exit(-1);
  245. throw $e; // unreachable, but makes typechecker happy - #2916
  246. }
  247. $tf = new TestFinder($fn, $tf, $tff, $tp, $tfp, $cf, $bf);
  248. // Mediawiki and others are clowntown when it comes to autoloading stuff
  249. // for reflection. Or I am a clown. Either way, workaround it.
  250. // May try spl_autoload_register() to workaround some clowniness, if possible.
  251. switch ($mode) {
  252. case TestFindModes::TOKEN:
  253. $tf->findTestMethodsViaTokens();
  254. break;
  255. case TestFindModes::PHPT:
  256. $tf->findTestsPHPT();
  257. break;
  258. default:
  259. $tf->findTestMethods();
  260. }
  261. exit(0);
  262. }
  263. tf_main($argv);