diff --git a/.arcconfig b/.arcconfig --- a/.arcconfig +++ b/.arcconfig @@ -1,6 +1,6 @@ { "phabricator.uri": "https://secure.phabricator.com/", - "unit.engine": "PhutilUnitTestEngine", + "unit.engine": "ArcanistConfigurationDrivenTestEngine", "load": [ "src/" ] diff --git a/.arcunit b/.arcunit new file mode 100644 --- /dev/null +++ b/.arcunit @@ -0,0 +1,8 @@ +{ + "engines": { + "phutil": { + "type": "phutil", + "include": "(\\.php$)" + } + } +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -58,6 +58,7 @@ 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php', 'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php', 'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php', + 'ArcanistConfigurationDrivenTestEngine' => 'unit/engine/ArcanistConfigurationDrivenTestEngine.php', 'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php', 'ArcanistConsoleLintRenderer' => 'lint/renderer/ArcanistConsoleLintRenderer.php', 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php', @@ -331,6 +332,7 @@ 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistConfiguration' => 'Phobject', 'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine', + 'ArcanistConfigurationDrivenTestEngine' => 'ArcanistUnitTestEngine', 'ArcanistConfigurationManager' => 'Phobject', 'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer', 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', diff --git a/src/unit/engine/ArcanistConfigurationDrivenTestEngine.php b/src/unit/engine/ArcanistConfigurationDrivenTestEngine.php new file mode 100644 --- /dev/null +++ b/src/unit/engine/ArcanistConfigurationDrivenTestEngine.php @@ -0,0 +1,184 @@ +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>', + )); + } 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. + $test_engine = null; + } + + try { + PhutilTypeSpec::checkMap( + $spec, + array( + 'type' => 'string', + 'include' => 'optional regex | list', + 'exclude' => 'optional regex | list', + )); + } 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()) + ->setEnableAsyncTests($this->getEnableAsyncTests()) + ->setEnableCoverage($this->getEnableCoverage()); + + if ($engine->supportsRunAllTests()) { + $engine->setRunAllTests($this->getRunAllTests()); + } + + try { + $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(); + } + + 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; + } + +} diff --git a/src/unit/engine/ArcanistUnitTestEngine.php b/src/unit/engine/ArcanistUnitTestEngine.php --- a/src/unit/engine/ArcanistUnitTestEngine.php +++ b/src/unit/engine/ArcanistUnitTestEngine.php @@ -16,6 +16,10 @@ final public function __construct() {} + public function getEngineConfigurationName() { + return null; + } + final public function setRunAllTests($run_all_tests) { if (!$this->supportsRunAllTests() && $run_all_tests) { throw new Exception( diff --git a/src/unit/engine/PhutilUnitTestEngine.php b/src/unit/engine/PhutilUnitTestEngine.php --- a/src/unit/engine/PhutilUnitTestEngine.php +++ b/src/unit/engine/PhutilUnitTestEngine.php @@ -5,6 +5,10 @@ */ final class PhutilUnitTestEngine extends ArcanistUnitTestEngine { + public function getEngineConfigurationName() { + return 'phutil'; + } + protected function supportsRunAllTests() { return true; }