Changeset View
Changeset View
Standalone View
Standalone View
src/unit/engine/ConfigDrivenMetaEngine.php
- This file was added.
| <?php | |||||
| final class ConfigDrivenMetaEngine extends ArcanistUnitTestEngine{ // maybe make this a workflow. | |||||
| public function run() { | |||||
| $engines = $this->buildEngines(); | |||||
| $results = array(); | |||||
| foreach ($engines as $engine) { | |||||
| $engine | |||||
| ->setWorkingCopy($this->getWorkingCopy()) | |||||
| ->setConfigurationManager($this->getConfigurationManager()) | |||||
| ->setRenderer($this->renderer) | |||||
| ->setEnableCoverage($this->getEnableCoverage()) | |||||
| ->setEnableAsyncTests(false); // TODO handle this | |||||
| // There's something called Arguments(), but it should diaf. | |||||
| $results[] = $engine->run(); | |||||
| } | |||||
| $results = array_mergev($results); | |||||
| return $results; | |||||
| } | |||||
| public function buildEngines() { | |||||
| $working_copy = $this->getWorkingCopy(); | |||||
| $config_path = $working_copy->getProjectPath('.arcunit'); | |||||
| if (!Filesystem::pathExists($config_path)) { | |||||
| throw new Exception( | |||||
| "Unable to find '.arcunit' file to configure tests. Create a ". | |||||
| "'.arcunit' file in the root directory of the working copy."); | |||||
| } | |||||
| $data = Filesystem::readFile($config_path); | |||||
| $config = null; | |||||
| try { | |||||
| $config = phutil_json_decode($data); | |||||
| } catch (PhutilJSONParserException $ex) { | |||||
| throw new PhutilProxyException( | |||||
| pht( | |||||
| "Expected '.arcunit' file to be a valid JSON file, but failed to ". | |||||
| "decode %s", | |||||
| $config_path), | |||||
| $ex); | |||||
| } | |||||
| $engines = $this->loadAvailableEngines(); | |||||
| try { | |||||
| PhutilTypeSpec::checkMap( | |||||
| $config, | |||||
| array( | |||||
| 'exclude' => 'optional regex | list<regex>', | |||||
| 'engines' => 'map<string, map<string, wild>>', // I'm not sold on this name. Any suggestions? | |||||
| )); | |||||
| } catch (PhutilTypeCheckException $ex) { | |||||
| $message = pht( | |||||
| 'Error in parsing ".arcunit" file: %s', | |||||
| $ex->getMessage()); | |||||
| throw new PhutilProxyException($message, $ex); | |||||
| } | |||||
| $global_exclude = (array)idx($config, 'exclude', array()); | |||||
| $built_engines = array(); | |||||
| $all_paths = $this->getPaths(); | |||||
| foreach ($config['engines'] as $name => $spec) { | |||||
| $type = idx($spec, 'engine'); | |||||
| if ($type !== null) { | |||||
| if (empty($engines[$type])) { | |||||
| $list = implode(', ', array_keys($engines)); | |||||
| throw new Exception( | |||||
| "Engine '{$name}' specifies invalid type '{$type}'. Available ". | |||||
| "engines are: {$list}."); | |||||
| } | |||||
| $engine = clone $engines[$type]; | |||||
| $more = $engine->getEngineConfigurationOptions(); | |||||
| foreach ($more as $key => $option_spec) { | |||||
| PhutilTypeSpec::checkMap( | |||||
| $option_spec, | |||||
| array( | |||||
| 'type' => 'string', | |||||
| 'help' => 'string', | |||||
| )); | |||||
| $more[$key] = $option_spec['type']; | |||||
| } | |||||
| } else { | |||||
| // We'll raise an error below about the invalid "type" key. | |||||
| $engine = null; | |||||
| $more = array(); | |||||
| } | |||||
| try { | |||||
| PhutilTypeSpec::checkMap( | |||||
| $spec, | |||||
| array( | |||||
| 'engine' => 'string', | |||||
| 'paths' => 'optional map<regex, string>', | |||||
| 'exclude' => 'optional regex | list<regex>', | |||||
| ) + $more); | |||||
| } catch (PhutilTypeCheckException $ex) { | |||||
| $message = pht( | |||||
| 'Error in parsing ".arcunit" file, for engine "%s": %s', | |||||
| $name, | |||||
| $ex->getMessage()); | |||||
| throw new PhutilProxyException($message, $ex); | |||||
| } | |||||
| foreach ($more as $key => $value) { | |||||
| if (array_key_exists($key, $spec)) { | |||||
| try { | |||||
| $engine->setUnitConfigurationValue($key, $spec[$key]); | |||||
| } catch (Exception $ex) { | |||||
| $message = pht( | |||||
| 'Error in parsing ".arcunit" file, in key "%s" for '. | |||||
| 'engine "%s": %s', | |||||
| $key, | |||||
| $name, | |||||
| $ex->getMessage()); | |||||
| throw new PhutilProxyException($message, $ex); | |||||
| } | |||||
| } | |||||
| } | |||||
| $engine_paths = (array)idx($spec, 'paths', array()); | |||||
| $exclude = (array)idx($spec, 'exclude', array()); | |||||
| $console = PhutilConsole::getConsole(); | |||||
| $console->writeLog("Examining paths for engine \"%s\".\n", $name); | |||||
| if ($this->getRunAllTests()) { | |||||
| $engine->setRunAllTests($this->getRunAllTests()); | |||||
| $built_engines[] = $engine; | |||||
| } else { | |||||
| $paths = $this->matchPaths( // This is now a map of {file => maybe-test-file-for-it} | |||||
| $all_paths, | |||||
| $engine_paths, | |||||
| $exclude, | |||||
| $global_exclude); | |||||
| $console->writeLog( | |||||
| "Found %d matching paths for engine \"%s\".\n", | |||||
| count($paths), | |||||
| $name); | |||||
| if ($paths) { | |||||
| $engine->setPathsMap($paths); // TODO rename? | |||||
| $built_engines[] = $engine; | |||||
| } | |||||
| } | |||||
| } | |||||
| return $built_engines; | |||||
| } | |||||
| private function loadAvailableEngines() { | |||||
| $engines = id(new PhutilSymbolLoader()) | |||||
| ->setAncestorClass('ArcanistUnitTestEngine') | |||||
| ->loadObjects(); | |||||
| $map = array(); | |||||
| foreach ($engines as $engine) { | |||||
| $name = $engine->getEngineConfigurationName(); | |||||
| // This engine isn't selectable through configuration. | |||||
| if ($name === null) { | |||||
| continue; | |||||
| } | |||||
| if (empty($map[$name])) { | |||||
| $map[$name] = $engine; | |||||
| continue; | |||||
| } | |||||
| $orig_class = get_class($map[$name]); | |||||
| $this_class = get_class($engine); | |||||
| throw new Exception( | |||||
| "Two engines ({$orig_class}, {$this_class}) both have the same ". | |||||
| "configuration name ({$name}). Engines must have unique configuration ". | |||||
| "names."); | |||||
| } | |||||
| return $map; | |||||
| } | |||||
| private function matchPaths( | |||||
| array $paths, | |||||
| array $engine_paths, | |||||
| array $exclude, | |||||
| array $global_exclude) { | |||||
| $console = PhutilConsole::getConsole(); | |||||
| $match = array(); | |||||
| foreach ($paths as $path) { | |||||
| $console->writeLog("Examining path '%s'...\n", $path); | |||||
| $replaced = null; | |||||
| $keep = false; | |||||
| if (!$engine_paths) { | |||||
| $keep = true; | |||||
| $console->writeLog( | |||||
| " Including path by default because there is no 'paths' rule.\n"); | |||||
| $replaced = $path; | |||||
| } else { | |||||
| $console->writeLog(" Testing \"paths\" rules.\n"); | |||||
| foreach ($engine_paths as $rule => $replace) { | |||||
| if (preg_match($rule, $path)) { | |||||
| $keep = true; | |||||
| $replaced = preg_replace($rule, $replace, $path); | |||||
| $console->writeLog(" Path matches include rule: %s\n", $rule); | |||||
| break; | |||||
| } else { | |||||
| $console->writeLog( | |||||
| " Path does not match include rule: %s\n", | |||||
| $rule); | |||||
| } | |||||
| } | |||||
| } | |||||
| if (!$keep) { | |||||
| $console->writeLog( | |||||
| " Path does not match any rules, discarding.\n"); | |||||
| continue; | |||||
| } | |||||
| if ($exclude) { | |||||
| $console->writeLog(" Testing \"exclude\" rules.\n"); | |||||
| foreach ($exclude as $rule) { | |||||
| if (preg_match($rule, $path)) { | |||||
| $console->writeLog(" Path matches \"exclude\" rule: %s\n", $rule); | |||||
| continue 2; | |||||
| } else { | |||||
| $console->writeLog( | |||||
| " Path does not match \"exclude\" rule: %s\n", | |||||
| $rule); | |||||
| } | |||||
| } | |||||
| } | |||||
| if ($global_exclude) { | |||||
| $console->writeLog(" Testing global \"exclude\" rules.\n"); | |||||
| foreach ($global_exclude as $rule) { | |||||
| if (preg_match($rule, $path)) { | |||||
| $console->writeLog( | |||||
| " Path matches global \"exclude\" rule: %s\n", | |||||
| $rule); | |||||
| continue 2; | |||||
| } else { | |||||
| $console->writeLog( | |||||
| " Path does not match global \"exclude\" rule: %s\n", | |||||
| $rule); | |||||
| } | |||||
| } | |||||
| } | |||||
| $console->writeLog(" Path matches.\n"); | |||||
| $match[$path] = $replaced; | |||||
| } | |||||
| return $match; | |||||
| } | |||||
| protected function supportsRunAllTests() { | |||||
| return true; | |||||
| } | |||||
| } | |||||