Changeset View
Changeset View
Standalone View
Standalone View
src/unit/engine/PhutilUnitTestEngine.php
| Show All 15 Lines | if ($this->getRunAllTests()) { | ||||
| $run_tests = $this->getTestsForPaths(); | $run_tests = $this->getTestsForPaths(); | ||||
| } | } | ||||
| if (!$run_tests) { | if (!$run_tests) { | ||||
| throw new ArcanistNoEffectException(pht('No tests to run.')); | throw new ArcanistNoEffectException(pht('No tests to run.')); | ||||
| } | } | ||||
| $enable_coverage = $this->getEnableCoverage(); | $enable_coverage = $this->getEnableCoverage(); | ||||
| if ($enable_coverage !== false) { | if ($enable_coverage !== false) { | ||||
| if (!function_exists('xdebug_start_code_coverage')) { | if (!function_exists('xdebug_start_code_coverage')) { | ||||
| if ($enable_coverage === true) { | if ($enable_coverage === true) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| pht( | pht( | ||||
| 'You specified %s but xdebug is not available, so '. | 'You specified %s but %s is not available, so '. | ||||
| 'coverage can not be enabled for %s.', | 'coverage can not be enabled for %s.', | ||||
| '--coverage', | '--coverage', | ||||
| 'XDebug', | |||||
| __CLASS__)); | __CLASS__)); | ||||
| } | } | ||||
| } else { | } else { | ||||
| $enable_coverage = true; | $enable_coverage = true; | ||||
| } | } | ||||
| } | } | ||||
| $project_root = $this->getWorkingCopy()->getProjectRoot(); | |||||
| $test_cases = array(); | $test_cases = array(); | ||||
| foreach ($run_tests as $test_class) { | foreach ($run_tests as $test_class) { | ||||
| $test_case = newv($test_class, array()); | $test_case = newv($test_class, array()) | ||||
| $test_case->setEnableCoverage($enable_coverage); | ->setEnableCoverage($enable_coverage) | ||||
| $test_case->setWorkingCopy($this->getWorkingCopy()); | ->setWorkingCopy($this->getWorkingCopy()); | ||||
epriestley: I think `id()` isn't necessary on `newv()`, only on `new X()`. PHP grammar lols | |||||
| if ($this->getPaths()) { | if ($this->getPaths()) { | ||||
| $test_case->setPaths($this->getPaths()); | $test_case->setPaths($this->getPaths()); | ||||
| } | } | ||||
| if ($this->renderer) { | if ($this->renderer) { | ||||
| $test_case->setRenderer($this->renderer); | $test_case->setRenderer($this->renderer); | ||||
| } | } | ||||
| $test_cases[] = $test_case; | $test_cases[] = $test_case; | ||||
| } | } | ||||
| foreach ($test_cases as $test_case) { | foreach ($test_cases as $test_case) { | ||||
| $test_case->willRunTestCases($test_cases); | $test_case->willRunTestCases($test_cases); | ||||
| } | } | ||||
| $results = array(); | $results = array(); | ||||
| Show All 38 Lines | foreach ($symbols as $symbol) { | ||||
| if ($in_working_copy[$library]) { | if ($in_working_copy[$library]) { | ||||
| $run_tests[] = $symbol['name']; | $run_tests[] = $symbol['name']; | ||||
| } | } | ||||
| } | } | ||||
| return $run_tests; | return $run_tests; | ||||
| } | } | ||||
| /** | |||||
| * Retrieve all relevant test cases. | |||||
| * | |||||
| * Looks for any class that extends @{class:PhutilTestCase} inside a | |||||
| * `__tests__` directory in any parent directory of every affected file. | |||||
| * | |||||
| * The idea is that "infrastructure/__tests__/" tests defines general tests | |||||
| * for all of "infrastructure/", and those tests run for any change in | |||||
| * "infrastructure/". However, "infrastructure/concrete/rebar/__tests__/" | |||||
| * defines more specific tests that run only when "rebar/" (or some | |||||
| * subdirectory) changes. | |||||
| * | |||||
| * @return list<string> The names of the test case classes to be executed. | |||||
| */ | |||||
| private function getTestsForPaths() { | private function getTestsForPaths() { | ||||
| $project_root = $this->getWorkingCopy()->getProjectRoot(); | $look_here = $this->getTestPaths(); | ||||
| $run_tests = array(); | |||||
| foreach ($look_here as $path_info) { | |||||
| $library = $path_info['library']; | |||||
| $path = $path_info['path']; | |||||
| $symbols = id(new PhutilSymbolLoader()) | |||||
| ->setType('class') | |||||
| ->setLibrary($library) | |||||
| ->setPathPrefix($path) | |||||
| ->setAncestorClass('PhutilTestCase') | |||||
| ->setConcreteOnly(true) | |||||
| ->selectAndLoadSymbols(); | |||||
| foreach ($symbols as $symbol) { | |||||
| $run_tests[$symbol['name']] = true; | |||||
| } | |||||
| } | |||||
| return array_keys($run_tests); | |||||
| } | |||||
| $look_here = array(); | /** | ||||
| * Returns the paths in which we should look for tests to execute. | |||||
| * | |||||
| * @return list<string> A list of paths in which to search for test cases. | |||||
| */ | |||||
| public function getTestPaths() { | |||||
| $root = $this->getWorkingCopy()->getProjectRoot(); | |||||
| $paths = array(); | |||||
| foreach ($this->getPaths() as $path) { | foreach ($this->getPaths() as $path) { | ||||
| $library_root = phutil_get_library_root_for_path($path); | $library_root = phutil_get_library_root_for_path($path); | ||||
| if (!$library_root) { | if (!$library_root) { | ||||
| continue; | continue; | ||||
| } | } | ||||
| $library_name = phutil_get_library_name_for_root($library_root); | $library_name = phutil_get_library_name_for_root($library_root); | ||||
| if (!$library_name) { | if (!$library_name) { | ||||
| throw new Exception( | throw new Exception( | ||||
| sprintf( | |||||
| "%s\n\n %s\n\n%s\n\n - %s\n - %s\n", | |||||
| pht( | pht( | ||||
| 'Attempting to run unit tests on a libphutil library '. | "Attempting to run unit tests on a libphutil library which has ". | ||||
| 'which has not been loaded, at:'), | "not been loaded, at:\n\n". | ||||
| " %s\n\n". | |||||
| "This probably means one of two things:\n\n". | |||||
| " - You may need to add this library to %s.\n". | |||||
| " - You may be running tests on a copy of libphutil or ". | |||||
| "arcanist using a different copy of libphutil or arcanist. ". | |||||
| "This operation is not supported.\n", | |||||
| $library_root, | $library_root, | ||||
| pht('This probably means one of two things:'), | '.arcconfig.')); | ||||
| pht( | |||||
| 'You may need to add this library to %s.', | |||||
| '.arcconfig.'), | |||||
| pht( | |||||
| 'You may be running tests on a copy of libphutil or '. | |||||
| 'arcanist using a different copy of libphutil or arcanist. '. | |||||
| 'This operation is not supported.'))); | |||||
| } | } | ||||
| $path = Filesystem::resolvePath($path, $project_root); | $path = Filesystem::resolvePath($path, $root); | ||||
| $library_path = Filesystem::readablePath($path, $library_root); | |||||
| if (!is_dir($path)) { | |||||
| $path = dirname($path); | |||||
| } | |||||
| if ($path == $library_root) { | if (!Filesystem::isDescendant($path, $library_root)) { | ||||
| $look_here[$library_name.':.'] = array( | |||||
| 'library' => $library_name, | |||||
| 'path' => '', | |||||
| ); | |||||
| } else if (!Filesystem::isDescendant($path, $library_root)) { | |||||
| // We have encountered some kind of symlink maze -- for instance, $path | // We have encountered some kind of symlink maze -- for instance, $path | ||||
| // is some symlink living outside the library that links into some file | // is some symlink living outside the library that links into some file | ||||
| // inside the library. Just ignore these cases, since the affected file | // inside the library. Just ignore these cases, since the affected file | ||||
| // does not actually lie within the library. | // does not actually lie within the library. | ||||
| continue; | continue; | ||||
| } else { | } | ||||
Not Done Inline ActionsI would expect && $library_path == $library_root to fail immediately in many cases and throw away most of the test we'd otherwise find. e.g., if you touch /a/b/c/d/e, we'd previously walk up and include /a/b, but now we'll fail out at /a/b/c/d or /a/b/c/d/e or something? epriestley: I would expect `&& $library_path == $library_root` to fail immediately in many cases and throw… | |||||
| $library_path = Filesystem::readablePath($path, $library_root); | |||||
| do { | if (is_file($path) && preg_match('@(?:^|/)__tests__/@', $path)) { | ||||
| $look_here[$library_name.':'.$library_path] = array( | $paths[$library_name.':'.$library_path] = array( | ||||
| 'library' => $library_name, | 'library' => $library_name, | ||||
| 'path' => $library_path, | 'path' => $library_path, | ||||
| ); | ); | ||||
| $library_path = dirname($library_path); | continue; | ||||
| } while ($library_path != '.'); | |||||
| } | |||||
| } | } | ||||
| // Look for any class that extends PhutilTestCase inside a `__tests__` | while (($library_path = dirname($library_path)) != '.') { | ||||
| // directory in any parent directory of every affected file. | $paths[$library_name.':'.$library_path] = array( | ||||
| // | 'library' => $library_name, | ||||
| // The idea is that "infrastructure/__tests__/" tests defines general tests | 'path' => $library_path.'/__tests__/', | ||||
| // for all of "infrastructure/", and those tests run for any change in | ); | ||||
| // "infrastructure/". However, "infrastructure/concrete/rebar/__tests__/" | |||||
| // defines more specific tests that run only when rebar/ (or some | |||||
| // subdirectory) changes. | |||||
| $run_tests = array(); | |||||
| foreach ($look_here as $path_info) { | |||||
| $library = $path_info['library']; | |||||
| $path = $path_info['path']; | |||||
| $symbols = id(new PhutilSymbolLoader()) | |||||
| ->setType('class') | |||||
| ->setLibrary($library) | |||||
| ->setPathPrefix(($path ? $path.'/' : '').'__tests__/') | |||||
| ->setAncestorClass('PhutilTestCase') | |||||
| ->setConcreteOnly(true) | |||||
| ->selectAndLoadSymbols(); | |||||
| foreach ($symbols as $symbol) { | |||||
| $run_tests[$symbol['name']] = true; | |||||
| } | } | ||||
| } | } | ||||
| $run_tests = array_keys($run_tests); | |||||
| return $run_tests; | return $paths; | ||||
| } | } | ||||
| public function shouldEchoTestResults() { | public function shouldEchoTestResults() { | ||||
| return !$this->renderer; | return !$this->renderer; | ||||
| } | } | ||||
| } | } | ||||
I think id() isn't necessary on newv(), only on new X(). PHP grammar lols