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; | |||||
} | |||||
} |