Changeset View
Changeset View
Standalone View
Standalone View
src/unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php
- This file was added.
| <?php | |||||
| final class ArcanistConfigurationDrivenUnitTestEngine | |||||
| extends ArcanistUnitTestEngine { | |||||
| protected function supportsRunAllTests() { | |||||
| $engines = $this->buildTestEngines(); | |||||
| foreach ($engines as $engine) { | |||||
| if ($engine->supportsRunAllTests()) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| return false; | |||||
| } | |||||
| public function buildTestEngines() { | |||||
| $working_copy = $this->getWorkingCopy(); | |||||
| $config_path = $working_copy->getProjectPath('.arcunit'); | |||||
| if (!Filesystem::pathExists($config_path)) { | |||||
| throw new ArcanistUsageException( | |||||
| pht( | |||||
| "Unable to find '%s' file to configure test engines. Create an ". | |||||
| "'%s' file in the root directory of the working copy.", | |||||
| '.arcunit', | |||||
| '.arcunit')); | |||||
| } | |||||
| $data = Filesystem::readFile($config_path); | |||||
| $config = null; | |||||
| try { | |||||
| $config = phutil_json_decode($data); | |||||
| } catch (PhutilJSONParserException $ex) { | |||||
| throw new PhutilProxyException( | |||||
| pht( | |||||
| "Expected '%s' file to be a valid JSON file, but ". | |||||
| "failed to decode '%s'.", | |||||
| '.arcunit', | |||||
| $config_path), | |||||
| $ex); | |||||
| } | |||||
| $test_engines = $this->loadAvailableTestEngines(); | |||||
| try { | |||||
| PhutilTypeSpec::checkMap( | |||||
| $config, | |||||
| array( | |||||
| 'engines' => 'map<string, map<string, wild>>', | |||||
| )); | |||||
| } catch (PhutilTypeCheckException $ex) { | |||||
| throw new PhutilProxyException( | |||||
| pht("Error in parsing '%s' file.", $config_path), | |||||
| $ex); | |||||
| } | |||||
| $built_test_engines = array(); | |||||
| $all_paths = $this->getPaths(); | |||||
| foreach ($config['engines'] as $name => $spec) { | |||||
| $type = idx($spec, 'type'); | |||||
| if ($type !== null) { | |||||
| if (empty($test_engines[$type])) { | |||||
| throw new ArcanistUsageException( | |||||
| pht( | |||||
| "Test engine '%s' specifies invalid type '%s'. ". | |||||
| "Available test engines are: %s.", | |||||
| $name, | |||||
| $type, | |||||
| implode(', ', array_keys($test_engines)))); | |||||
| } | |||||
| $test_engine = clone $test_engines[$type]; | |||||
| } else { | |||||
| // We'll raise an error below about the invalid "type" key. | |||||
| // TODO: Can we just do the type check first, and simplify this a bit? | |||||
| $test_engine = null; | |||||
| } | |||||
| try { | |||||
| PhutilTypeSpec::checkMap( | |||||
| $spec, | |||||
| array( | |||||
| 'type' => 'string', | |||||
| 'include' => 'optional regex | list<regex>', | |||||
| 'exclude' => 'optional regex | list<regex>', | |||||
| )); | |||||
| } catch (PhutilTypeCheckException $ex) { | |||||
| throw new PhutilProxyException( | |||||
| pht( | |||||
| "Error in parsing '%s' file, for test engine '%s'.", | |||||
| '.arcunit', | |||||
| $name), | |||||
| $ex); | |||||
| } | |||||
| $include = (array)idx($spec, 'include', array()); | |||||
| $exclude = (array)idx($spec, 'exclude', array()); | |||||
| $paths = $this->matchPaths( | |||||
| $all_paths, | |||||
| $include, | |||||
| $exclude); | |||||
| $test_engine->setPaths($paths); | |||||
| $built_test_engines[] = $test_engine; | |||||
| } | |||||
| return $built_test_engines; | |||||
| } | |||||
| public function run() { | |||||
| $paths = $this->getPaths(); | |||||
| // If we are running with `--everything` then `$paths` will be `null`. | |||||
| if (!$paths) { | |||||
| $paths = array(); | |||||
| } | |||||
| $engines = $this->buildTestEngines(); | |||||
| $results = array(); | |||||
| $exceptions = array(); | |||||
| foreach ($engines as $engine) { | |||||
| $engine | |||||
| ->setWorkingCopy($this->getWorkingCopy()) | |||||
| ->setEnableCoverage($this->getEnableCoverage()); | |||||
| // TODO: At some point, maybe we should emit a warning here if an engine | |||||
| // doesn't support `--everything`, to reduce surprise when `--everything` | |||||
| // does not really mean `--everything`. | |||||
| if ($engine->supportsRunAllTests()) { | |||||
| $engine->setRunAllTests($this->getRunAllTests()); | |||||
| } | |||||
| try { | |||||
| // TODO: Type check the results. | |||||
| $results[] = $engine->run(); | |||||
| } catch (ArcanistNoEffectException $ex) { | |||||
| $exceptions[] = $ex; | |||||
| } | |||||
| } | |||||
| if (!$results) { | |||||
| // If all engines throw an `ArcanistNoEffectException`, then we should | |||||
| // preserve this behavior. | |||||
| throw new ArcanistNoEffectException(pht('No tests to run.')); | |||||
| } | |||||
| return array_mergev($results); | |||||
| } | |||||
| private function loadAvailableTestEngines() { | |||||
| return id(new PhutilClassMapQuery()) | |||||
| ->setAncestorClass('ArcanistUnitTestEngine') | |||||
| ->setUniqueMethod('getEngineConfigurationName', true) | |||||
| ->execute(); | |||||
| } | |||||
| /** | |||||
| * TODO: This is copied from @{class:ArcanistConfigurationDrivenLintEngine}. | |||||
| */ | |||||
| private function matchPaths(array $paths, array $include, array $exclude) { | |||||
| $match = array(); | |||||
| foreach ($paths as $path) { | |||||
| $keep = false; | |||||
| if (!$include) { | |||||
| $keep = true; | |||||
| } else { | |||||
| foreach ($include as $rule) { | |||||
| if (preg_match($rule, $path)) { | |||||
| $keep = true; | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| if (!$keep) { | |||||
| continue; | |||||
| } | |||||
| if ($exclude) { | |||||
| foreach ($exclude as $rule) { | |||||
| if (preg_match($rule, $path)) { | |||||
| continue 2; | |||||
| } | |||||
| } | |||||
| } | |||||
| $match[] = $path; | |||||
| } | |||||
| return $match; | |||||
| } | |||||
| } | |||||