Page MenuHomePhabricator

D10914.diff
No OneTemporary

D10914.diff

diff --git a/.arcconfig b/.arcconfig
--- a/.arcconfig
+++ b/.arcconfig
@@ -1,7 +1,7 @@
{
"project.name" : "arcanist",
"phabricator.uri" : "https://secure.phabricator.com/",
- "unit.engine" : "PhutilUnitTestEngine",
+ "unit.engine" : "ConfigDrivenMetaEngine",
"load" : [
"src/"
]
diff --git a/.arcunit b/.arcunit
new file mode 100644
--- /dev/null
+++ b/.arcunit
@@ -0,0 +1,7 @@
+{
+ "engines": {
+ "putil": {
+ "engine": "phutil"
+ }
+ }
+}
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
@@ -201,6 +201,7 @@
'ArcanistXUnitTestResultParser' => 'unit/engine/ArcanistXUnitTestResultParser.php',
'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php',
'ComprehensiveLintEngine' => 'lint/engine/ComprehensiveLintEngine.php',
+ 'ConfigDrivenMetaEngine' => 'unit/engine/ConfigDrivenMetaEngine.php',
'GoTestResultParser' => 'unit/engine/GoTestResultParser.php',
'GoTestResultParserTestCase' => 'unit/engine/__tests__/GoTestResultParserTestCase.php',
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
@@ -373,6 +374,7 @@
'ArcanistXMLLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'CSharpToolsTestEngine' => 'XUnitTestEngine',
'ComprehensiveLintEngine' => 'ArcanistLintEngine',
+ 'ConfigDrivenMetaEngine' => 'ArcanistUnitTestEngine',
'GoTestResultParser' => 'ArcanistTestResultParser',
'GoTestResultParserTestCase' => 'ArcanistTestCase',
'NoseTestEngine' => 'ArcanistUnitTestEngine',
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
@@ -6,7 +6,8 @@
abstract class ArcanistUnitTestEngine {
private $workingCopy;
- private $paths;
+ private $paths; // TODO deprecate
+ private $pathsMap;
private $arguments = array();
protected $diffID;
private $enableAsyncTests;
@@ -117,4 +118,18 @@
return true;
}
+ public function setPathsMap($paths_map) {
+ $this->pathsMap = $paths_map;
+ return $this;
+ }
+ public function getPathsMap() {
+ return $this->pathsMap;
+ }
+ public function getEngineConfigurationName() {
+ return null;
+ }
+ public function getEngineConfigurationOptions() {
+ return array();
+ }
+ public function setUnitConfigurationValue($key, $value) {}
}
diff --git a/src/unit/engine/ConfigDrivenMetaEngine.php b/src/unit/engine/ConfigDrivenMetaEngine.php
new file mode 100644
--- /dev/null
+++ b/src/unit/engine/ConfigDrivenMetaEngine.php
@@ -0,0 +1,269 @@
+<?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;
+ }
+}
diff --git a/src/unit/engine/NoseTestEngine.php b/src/unit/engine/NoseTestEngine.php
--- a/src/unit/engine/NoseTestEngine.php
+++ b/src/unit/engine/NoseTestEngine.php
@@ -3,11 +3,23 @@
/**
* Very basic 'nose' unit test engine wrapper.
*
- * Requires nose 1.1.3 for code coverage.
+ * Requires nose 1.1.3 and python-coverage 3.6 for code coverage.
*/
final class NoseTestEngine extends ArcanistUnitTestEngine {
+ private $sourcePath = './';
public function run() {
+ if ($this->getPaths()) {
+ return $this->runAutoGuessTests();
+ }
+
+ $paths = $this->getPathsMap();
+ $test_paths = array_unique($paths);
+
+ return $this->runTests($test_paths, $this->sourcePath);
+ }
+
+ private function runAutoGuessTests() { // Possibly not used by anyone anywhere.
$paths = $this->getPaths();
$affected_tests = array();
@@ -115,7 +127,6 @@
$reports = array();
$classes = $coverage_dom->getElementsByTagName('class');
-
foreach ($classes as $class) {
$path = $class->getAttribute('filename');
$root = $this->getWorkingCopy()->getProjectRoot();
@@ -159,4 +170,24 @@
return $reports;
}
+ public function getEngineConfigurationName() {
+ return 'pynose';
+ }
+
+ public function setUnitConfigurationValue($key, $value) {
+ switch ($key) {
+ case 'source_path':
+ $this->sourcePath = $value;
+ return;
+ }
+ return parent::setUnitConfigurationValue($key, $value);
+ }
+ public function getEngineConfigurationOptions() {
+ return array(
+ 'source_path' => array(
+ 'type' => 'optional string',
+ 'help' => 'Root of source code. Defaults to repository\'s root.',
+ ),
+ );
+ }
}
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
@@ -105,7 +105,11 @@
$look_here = array();
- foreach ($this->getPaths() as $path) {
+ $paths = $this->getPaths();
+ if (!$paths) {
+ $paths = array_values($this->getPathsMap());
+ }
+ foreach ($paths as $path) {
$library_root = phutil_get_library_root_for_path($path);
if (!$library_root) {
continue;
@@ -188,4 +192,7 @@
return !$this->renderer;
}
+ public function getEngineConfigurationName() {
+ return 'phutil';
+ }
}

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 23, 7:09 PM (18 h, 13 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6921852
Default Alt Text
D10914.diff (12 KB)

Event Timeline