1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-09 16:32:39 +01:00

[Wilds] Make "arc unit" run again, with many caveats

Summary:
Ref T13098. I made this change with a machete and a hacksaw. Major ideas:

The `--json`, `--ugly`, and `--output` flags are a new `--format <json|default>` flag instead. Formatters (JSON, Console) are modularized instead of being hard-coded. In a future diff, I will probably modularize this more into a general "sink/output" object and implement `--target X` ("send results to harbormaster build target X") as an output/sink, too, and then when you run `--format json --target X` we just send all the results to two outputs, and one sends them to the console while the other one uploads them.

The `--everything`, `arc unit path path path ...`, and `--rev` flags don't work yet, and `arc unit` always behaves like `arc unit --everything`. This is fine for `arcanist/` since the whole test suite currently runs in 5 seconds. I expect to restore these more or less as they previously existed later, once the working copy / repository stuff is in better shape.

The `--engine` flag is gone. This is an old flag and obsolete with `.arcunit`. `arc unit` now requires `.arcunit` to exist.

Some other flags are gone but I expect to restore them, at least in some form, later on.

Much of this barely works, but it //appears// that all the tests run, so we can start putting coverage on `arc alias`, Windows shell escaping, etc.

Test Plan: Ran `arc unit`, got something that looks like unit test results. When some of them didn't work, got failures.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13098

Differential Revision: https://secure.phabricator.com/D19710
This commit is contained in:
epriestley 2018-09-25 15:26:47 -07:00
parent 9d66610027
commit 493a5d1cc7
13 changed files with 349 additions and 789 deletions

View file

@ -142,7 +142,6 @@ phutil_register_library_map(array(
'ArcanistConduitException' => 'conduit/ArcanistConduitException.php',
'ArcanistConfigOption' => 'config/option/ArcanistConfigOption.php',
'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php',
'ArcanistConfigurationDrivenUnitTestEngine' => 'unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php',
'ArcanistConfigurationEngine' => 'config/ArcanistConfigurationEngine.php',
'ArcanistConfigurationEngineExtension' => 'config/ArcanistConfigurationEngineExtension.php',
'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php',
@ -166,6 +165,7 @@ phutil_register_library_map(array(
'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase.php',
'ArcanistDefaultParametersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php',
'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDefaultParametersXHPASTLinterRuleTestCase.php',
'ArcanistDefaultUnitFormatter' => 'unit/formatter/ArcanistDefaultUnitFormatter.php',
'ArcanistDefaultsConfigurationSource' => 'config/source/ArcanistDefaultsConfigurationSource.php',
'ArcanistDeprecationXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeprecationXHPASTLinterRule.php',
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeprecationXHPASTLinterRuleTestCase.php',
@ -279,6 +279,7 @@ phutil_register_library_map(array(
'ArcanistJSONLintRenderer' => 'lint/renderer/ArcanistJSONLintRenderer.php',
'ArcanistJSONLinter' => 'lint/linter/ArcanistJSONLinter.php',
'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php',
'ArcanistJSONUnitFormatter' => 'unit/formatter/ArcanistJSONUnitFormatter.php',
'ArcanistJscsLinter' => 'lint/linter/ArcanistJscsLinter.php',
'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php',
'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php',
@ -471,8 +472,10 @@ phutil_register_library_map(array(
'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnexpectedReturnValueXHPASTLinterRule.php',
'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase.php',
'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php',
'ArcanistUnitEngine' => 'unit/engine/ArcanistUnitEngine.php',
'ArcanistUnitFormatter' => 'unit/formatter/ArcanistUnitFormatter.php',
'ArcanistUnitOverseer' => 'unit/overseer/ArcanistUnitOverseer.php',
'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php',
'ArcanistUnitTestEngine' => 'unit/engine/ArcanistUnitTestEngine.php',
'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php',
'ArcanistUnitTestResultTestCase' => 'unit/__tests__/ArcanistUnitTestResultTestCase.php',
'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php',
@ -947,7 +950,7 @@ phutil_register_library_map(array(
'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
'PhutilUnitEngine' => 'unit/engine/PhutilUnitEngine.php',
'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php',
'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
@ -1250,7 +1253,6 @@ phutil_register_library_map(array(
'ArcanistConduitException' => 'Exception',
'ArcanistConfigOption' => 'Phobject',
'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine',
'ArcanistConfigurationDrivenUnitTestEngine' => 'ArcanistUnitTestEngine',
'ArcanistConfigurationEngine' => 'Phobject',
'ArcanistConfigurationEngineExtension' => 'Phobject',
'ArcanistConfigurationManager' => 'Phobject',
@ -1274,6 +1276,7 @@ phutil_register_library_map(array(
'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistDefaultUnitFormatter' => 'ArcanistUnitFormatter',
'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource',
'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@ -1315,7 +1318,7 @@ phutil_register_library_map(array(
'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistFeatureWorkflow' => 'ArcanistWorkflow',
'ArcanistFileConfigurationSource' => 'ArcanistConfigurationSource',
'ArcanistFileConfigurationSource' => 'ArcanistFilesystemConfigurationSource',
'ArcanistFileDataRef' => 'Phobject',
'ArcanistFileUploader' => 'Phobject',
'ArcanistFilenameLinter' => 'ArcanistLinter',
@ -1387,6 +1390,7 @@ phutil_register_library_map(array(
'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer',
'ArcanistJSONLinter' => 'ArcanistLinter',
'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistJSONUnitFormatter' => 'ArcanistUnitFormatter',
'ArcanistJscsLinter' => 'ArcanistExternalLinter',
'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@ -1579,8 +1583,10 @@ phutil_register_library_map(array(
'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer',
'ArcanistUnitEngine' => 'Phobject',
'ArcanistUnitFormatter' => 'Phobject',
'ArcanistUnitOverseer' => 'Phobject',
'ArcanistUnitRenderer' => 'Phobject',
'ArcanistUnitTestEngine' => 'Phobject',
'ArcanistUnitTestResult' => 'Phobject',
'ArcanistUnitTestResultTestCase' => 'PhutilTestCase',
'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine',
@ -2079,7 +2085,7 @@ phutil_register_library_map(array(
'PhutilUSEnglishLocale' => 'PhutilLocale',
'PhutilUTF8StringTruncator' => 'Phobject',
'PhutilUTF8TestCase' => 'PhutilTestCase',
'PhutilUnitTestEngine' => 'ArcanistUnitTestEngine',
'PhutilUnitEngine' => 'ArcanistUnitEngine',
'PhutilUnitTestEngineTestCase' => 'PhutilTestCase',
'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',

View file

@ -263,4 +263,8 @@ abstract class ArcanistWorkflow extends Phobject {
return clone $prompt;
}
protected function getWorkingCopy() {
return $this->getConfigurationEngine()->getWorkingCopy();
}
}

View file

@ -1,220 +0,0 @@
<?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);
}
if ($all_paths) {
$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() {
$renderer = $this->renderer;
$this->setRenderer(null);
$paths = $this->getPaths();
// If we are running with `--everything` then `$paths` will be `null`.
if (!$paths) {
$paths = array();
}
$engines = $this->buildTestEngines();
$all_results = array();
$exceptions = array();
foreach ($engines as $engine) {
$engine
->setWorkingCopy($this->getWorkingCopy())
->setEnableCoverage($this->getEnableCoverage())
->setConfigurationManager($this->getConfigurationManager())
->setRenderer($renderer);
// 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();
$all_results[] = $results;
foreach ($results as $result) {
// If the proxied engine renders its own test results then there
// is no need to render them again here.
if (!$engine->shouldEchoTestResults()) {
echo $renderer->renderUnitResult($result);
}
}
} catch (ArcanistNoEffectException $ex) {
$exceptions[] = $ex;
}
}
if (!$all_results) {
// If all engines throw an `ArcanistNoEffectException`, then we should
// preserve this behavior.
throw new ArcanistNoEffectException(pht('No tests to run.'));
}
return array_mergev($all_results);
}
public function shouldEchoTestResults() {
return false;
}
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;
}
}

View file

@ -0,0 +1,69 @@
<?php
abstract class ArcanistUnitEngine
extends Phobject {
private $overseer;
private $includePaths = array();
private $excludePaths = array();
final public function setIncludePaths(array $include_paths) {
$this->includePaths = $include_paths;
return $this;
}
final public function getIncludePaths() {
return $this->includePaths;
}
final public function setExcludePaths(array $exclude_paths) {
$this->excludePaths = $exclude_paths;
return $this;
}
final public function getExcludePaths() {
return $this->excludePaths;
}
final public function getUnitEngineType() {
return $this->getPhobjectClassConstant('ENGINETYPE');
}
final public function getPath($to_file = null) {
return Filesystem::concatenatePaths(
array(
$this->getOverseer()->getDirectory(),
$to_file,
));
}
final public function setOverseer(ArcanistUnitOverseer $overseer) {
$this->overseer = $overseer;
return $this;
}
final public function getOverseer() {
return $this->overseer;
}
public static function getAllUnitEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getUnitEngineType')
->execute();
}
abstract public function runTests();
final protected function didRunTests(array $tests) {
assert_instances_of($tests, 'ArcanistUnitTestResult');
// TOOLSETS: Pass this stuff to result output so it can print progress or
// stream results.
foreach ($tests as $test) {
echo "Ran Test: ".$test->getNamespace().'::'.$test->getName()."\n";
}
}
}

View file

@ -1,98 +0,0 @@
<?php
/**
* Manages unit test execution.
*/
abstract class ArcanistUnitTestEngine extends Phobject {
private $workingCopy;
private $paths = array();
private $enableCoverage;
private $runAllTests;
private $configurationManager;
protected $renderer;
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(
pht(
"Engine '%s' does not support %s.",
get_class($this),
'--everything'));
}
$this->runAllTests = $run_all_tests;
return $this;
}
final public function getRunAllTests() {
return $this->runAllTests;
}
protected function supportsRunAllTests() {
return false;
}
final public function setConfigurationManager(
ArcanistConfigurationManager $configuration_manager) {
$this->configurationManager = $configuration_manager;
return $this;
}
final public function getConfigurationManager() {
return $this->configurationManager;
}
final public function setWorkingCopy(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
final public function getWorkingCopy() {
return $this->workingCopy;
}
final public function setPaths(array $paths) {
$this->paths = $paths;
return $this;
}
final public function getPaths() {
return $this->paths;
}
final public function setEnableCoverage($enable_coverage) {
$this->enableCoverage = $enable_coverage;
return $this;
}
final public function getEnableCoverage() {
return $this->enableCoverage;
}
final public function setRenderer(ArcanistUnitRenderer $renderer = null) {
$this->renderer = $renderer;
return $this;
}
abstract public function run();
/**
* Modify the return value of this function in the child class, if you do
* not need to echo the test results after all the tests have been run. This
* is the case for example when the child class prints the tests results
* while the tests are running.
*/
public function shouldEchoTestResults() {
return true;
}
}

View file

@ -1,62 +1,16 @@
<?php
/**
* Very basic unit test engine which runs libphutil tests.
*/
final class PhutilUnitTestEngine extends ArcanistUnitTestEngine {
final class PhutilUnitEngine
extends ArcanistUnitEngine {
public function getEngineConfigurationName() {
return 'phutil';
}
const ENGINETYPE = 'phutil';
protected function supportsRunAllTests() {
return true;
}
public function run() {
if ($this->getRunAllTests()) {
$run_tests = $this->getAllTests();
} else {
$run_tests = $this->getTestsForPaths();
}
if (!$run_tests) {
throw new ArcanistNoEffectException(pht('No tests to run.'));
}
$enable_coverage = $this->getEnableCoverage();
if ($enable_coverage !== false) {
if (!function_exists('xdebug_start_code_coverage')) {
if ($enable_coverage === true) {
throw new ArcanistUsageException(
pht(
'You specified %s but %s is not available, so '.
'coverage can not be enabled for %s.',
'--coverage',
'XDebug',
__CLASS__));
}
} else {
$enable_coverage = true;
}
}
public function runTests() {
$run_tests = $this->getAllTests();
$test_cases = array();
foreach ($run_tests as $test_class) {
$test_case = newv($test_class, array())
->setEnableCoverage($enable_coverage)
->setWorkingCopy($this->getWorkingCopy());
if ($this->getPaths()) {
$test_case->setPaths($this->getPaths());
}
if ($this->renderer) {
$test_case->setRenderer($this->renderer);
}
$test_case = newv($test_class, array());
$test_cases[] = $test_case;
}
@ -66,7 +20,11 @@ final class PhutilUnitTestEngine extends ArcanistUnitTestEngine {
$results = array();
foreach ($test_cases as $test_case) {
$results[] = $test_case->run();
$result_list = $test_case->run();
$this->didRunTests($result_list);
$results[] = $result_list;
}
$results = array_mergev($results);
@ -78,7 +36,7 @@ final class PhutilUnitTestEngine extends ArcanistUnitTestEngine {
}
private function getAllTests() {
$project_root = $this->getWorkingCopy()->getProjectRoot();
$project_root = $this->getPath();
$symbols = id(new PhutilSymbolLoader())
->setType('class')

View file

@ -76,8 +76,7 @@ final class PhutilUnitTestEngineTestCase extends PhutilTestCase {
$failed = 0;
$skipped = 0;
$test_case = id(new PhutilTestCaseTestCase())
->setWorkingCopy($this->getWorkingCopy());
$test_case = new PhutilTestCaseTestCase();
foreach ($test_case->run() as $result) {
if ($result->getResult() == ArcanistUnitTestResult::RESULT_FAIL) {
@ -161,8 +160,7 @@ final class PhutilUnitTestEngineTestCase extends PhutilTestCase {
),
);
$test_engine = id(new PhutilUnitTestEngine())
->setWorkingCopy($this->getWorkingCopy());
$test_engine = new PhutilUnitTestEngine();
$library = phutil_get_current_library_name();
$library_root = phutil_get_library_root($library);

View file

@ -651,6 +651,9 @@ abstract class PhutilTestCase extends Phobject {
}
final protected function getLink($method) {
// TOOLSETS: Restore this.
return null;
$base_uri = $this
->getWorkingCopy()
->getProjectConfig('phabricator.uri');

View file

@ -0,0 +1,9 @@
<?php
final class ArcanistDefaultUnitFormatter
extends ArcanistUnitFormatter {
const FORMATTER_KEY = 'default';
}

View file

@ -0,0 +1,9 @@
<?php
final class ArcanistJSONUnitFormatter
extends ArcanistUnitFormatter {
const FORMATTER_KEY = 'json';
}

View file

@ -0,0 +1,17 @@
<?php
abstract class ArcanistUnitFormatter
extends Phobject {
final public function getUnitFormatterKey() {
return $this->getPhobjectClassConstant('FORMATTER_KEY');
}
public static function getAllUnitFormatters() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getUnitFormatterKey')
->execute();
}
}

View file

@ -0,0 +1,153 @@
<?php
final class ArcanistUnitOverseer
extends Phobject {
private $directory;
private $paths = array();
private $formatter;
public function setPaths($paths) {
$this->paths = $paths;
return $this;
}
public function getPaths() {
return $this->paths;
}
public function setFormatter(ArcanistUnitFormatter $formatter) {
$this->formatter = $formatter;
return $this;
}
public function getFormatter() {
return $this->formatter;
}
public function setDirectory($directory) {
$this->directory = $directory;
return $this;
}
public function getDirectory() {
return $this->directory;
}
public function execute() {
$engines = $this->loadEngines();
foreach ($engines as $engine) {
$engine->setOverseer($this);
}
$results = array();
foreach ($engines as $engine) {
$tests = $engine->runTests();
foreach ($tests as $test) {
$results[] = $test;
}
}
return $results;
}
private function loadEngines() {
$root = $this->getDirectory();
$arcunit_path = Filesystem::concatenatePaths(array($root, '.arcunit'));
$arcunit_display = Filesystem::readablePath($arcunit_path);
if (!Filesystem::pathExists($arcunit_path)) {
throw new Exception(
pht(
'No ".arcunit" file exists at path "%s". Create an ".arcunit" file '.
'to define how "arc unit" should run tests.',
$arcunit_display));
}
try {
$data = Filesystem::readFile($arcunit_path);
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Failed to read ".arcunit" file (at path "%s").',
$arcunit_display),
$ex);
}
try {
$spec = phutil_json_decode($data);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht(
'Expected ".arcunit" file (at path "%s") to be a valid JSON file, '.
'but it could not be parsed.',
$arcunit_display),
$ex);
}
try {
PhutilTypeSpec::checkMap(
$spec,
array(
'engines' => 'map<string, wild>',
));
} catch (PhutilTypeCheckException $ex) {
throw new PhutilProxyException(
pht(
'The ".arcunit" file (at path "%s") is not formatted correctly.',
$arcunit_display),
$ex);
}
$all_engines = ArcanistUnitEngine::getAllUnitEngines();
$engines = array();
foreach ($spec['engines'] as $key => $engine_spec) {
try {
PhutilTypeSpec::checkMap(
$engine_spec,
array(
'type' => 'string',
'include' => 'optional regex | list<regex>',
'exclude' => 'optional regex | list<regex>',
));
} catch (PhutilTypeCheckException $ex) {
throw new PhutilProxyException(
pht(
'The ".arcunit" file (at path "%s") is not formatted correctly: '.
'the engine with key "%s" is specified improperly.',
$arcunit_display,
$key));
}
$type = $engine_spec['type'];
if (!isset($all_engines[$type])) {
throw new Exception(
pht(
'The ".arcunit" file (at path "%s") specifies an engine (with '.
'key "%s") of an unknown type ("%s").',
$arcunit_display,
$key,
$type));
}
$engine = clone $all_engines[$type];
if (isset($engine_spec['include'])) {
$engine->setIncludePaths((array)$engine_spec['include']);
}
if (isset($engine_spec['exclude'])) {
$engine->setExcludePaths((array)$engine_spec['exclude']);
}
$engines[] = $engine;
}
return $engines;
}
}

View file

@ -1,430 +1,82 @@
<?php
/**
* Runs unit tests which cover your changes.
*/
final class ArcanistUnitWorkflow extends ArcanistWorkflow {
const RESULT_OKAY = 0;
const RESULT_UNSOUND = 1;
const RESULT_FAIL = 2;
const RESULT_SKIP = 3;
private $unresolvedTests;
private $testResults;
private $engine;
final class ArcanistUnitWorkflow
extends ArcanistWorkflow {
public function getWorkflowName() {
return 'unit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**unit** [__options__] [__paths__]
**unit** [__options__] --rev [__rev__]
public function getWorkflowInformation() {
$help = pht(<<<EOTEXT
Run unit tests.
EOTEXT
);
);
return $this->newWorkflowInformation()
->addExample(pht('**unit** [__options__] __path__ __path__ ...'))
->addExample(pht('**unit** [__options__] --commit __commit__'))
->setHelp($help);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
Run unit tests that cover specified paths. If no paths are specified,
unit tests covering all modified files will be run.
EOTEXT
);
}
public function getArguments() {
public function getWorkflowArguments() {
return array(
'rev' => array(
'param' => 'revision',
'help' => pht(
'Run unit tests covering changes since a specific revision.'),
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => pht(
'Arc unit does not currently support %s in SVN.',
'--rev'),
),
),
'engine' => array(
'param' => 'classname',
'help' => pht('Override configured unit engine for this project.'),
),
'coverage' => array(
'help' => pht('Always enable coverage information.'),
'conflicts' => array(
'no-coverage' => null,
),
),
'no-coverage' => array(
'help' => pht('Always disable coverage information.'),
),
'detailed-coverage' => array(
'help' => pht(
'Show a detailed coverage report on the CLI. Implies %s.',
'--coverage'),
),
'json' => array(
'help' => pht('Report results in JSON format.'),
),
'output' => array(
'param' => 'format',
'help' => pht(
"With 'full', show full pretty report (Default). ".
"With 'json', report results in JSON format. ".
"With 'ugly', use uglier (but more efficient) JSON formatting. ".
"With 'none', don't print results."),
'conflicts' => array(
'json' => pht('Only one output format allowed'),
'ugly' => pht('Only one output format allowed'),
),
),
'target' => array(
'param' => 'phid',
'help' => pht(
'(PROTOTYPE) Record a copy of the test results on the specified '.
'Harbormaster build target.'),
),
'everything' => array(
'help' => pht(
'Run every test associated with a tracked file in the working '.
'copy.'),
'conflicts' => array(
'rev' => pht('%s runs all tests.', '--everything'),
),
),
'ugly' => array(
'help' => pht(
'With %s, use uglier (but more efficient) formatting.',
'--json'),
),
'*' => 'paths',
$this->newWorkflowArgument('commit')
->setParameter('commit'),
$this->newWorkflowArgument('format')
->setParameter('format'),
$this->newWorkflowArgument('everything'),
$this->newWorkflowArgument('paths')
->setWildcard(true),
// TOOLSETS: Restore "--target".
);
}
public function requiresWorkingCopy() {
return true;
}
public function runWorkflow() {
// If we're in a working copy, run tests from the working copy root.
// Otherwise, run tests from the current working directory.
public function requiresRepositoryAPI() {
return true;
}
public function requiresConduit() {
return $this->shouldUploadResults();
}
public function requiresAuthentication() {
return $this->shouldUploadResults();
}
public function getEngine() {
return $this->engine;
}
public function run() {
$working_copy = $this->getWorkingCopy();
if ($working_copy) {
$directory = $working_copy->getPath();
} else {
$directory = getcwd();
}
$paths = $this->getArgument('paths');
$rev = $this->getArgument('rev');
$everything = $this->getArgument('everything');
if ($everything && $paths) {
$overseer = id(new ArcanistUnitOverseer())
->setDirectory($directory);
// TOOLSETS: For now, we're treating every invocation of "arc unit" as
// though it is "arc unit --everything", and ignoring the "--commit" flag
// and "paths" arguments.
$formatter = $this->newUnitFormatter();
$overseer->setFormatter($formatter);
$overseer->execute();
return 0;
}
private function newUnitFormatter() {
$formatters = ArcanistUnitFormatter::getAllUnitFormatters();
$format_key = $this->getArgument('format');
if (!strlen($format_key)) {
$format_key = ArcanistDefaultUnitFormatter::FORMATTER_KEY;
}
$formatter = idx($formatters, $format_key);
if (!$formatter) {
throw new ArcanistUsageException(
pht(
'You can not specify paths with %s. The %s flag runs every test '.
'associated with a tracked file in the working copy.',
'--everything',
'--everything'));
'Unit test output format ("%s") is unknown. Supported formats '.
'are: %s.',
$format_key,
implode(', ', array_keys($formatters))));
}
if ($everything) {
$paths = iterator_to_array($this->getRepositoryAPI()->getAllFiles());
} else {
$paths = $this->selectPathsForWorkflow($paths, $rev);
}
$this->engine = $this->newUnitTestEngine($this->getArgument('engine'));
if ($everything) {
$this->engine->setRunAllTests(true);
} else {
$this->engine->setPaths($paths);
}
$renderer = new ArcanistUnitConsoleRenderer();
$this->engine->setRenderer($renderer);
$enable_coverage = null; // Means "default".
if ($this->getArgument('coverage') ||
$this->getArgument('detailed-coverage')) {
$enable_coverage = true;
} else if ($this->getArgument('no-coverage')) {
$enable_coverage = false;
}
$this->engine->setEnableCoverage($enable_coverage);
$results = $this->engine->run();
$this->validateUnitEngineResults($this->engine, $results);
$this->testResults = $results;
$console = PhutilConsole::getConsole();
$output_format = $this->getOutputFormat();
if ($output_format !== 'full') {
$console->disableOut();
}
$unresolved = array();
$coverage = array();
foreach ($results as $result) {
$result_code = $result->getResult();
if ($this->engine->shouldEchoTestResults()) {
$console->writeOut('%s', $renderer->renderUnitResult($result));
}
if ($result_code != ArcanistUnitTestResult::RESULT_PASS) {
$unresolved[] = $result;
}
if ($result->getCoverage()) {
foreach ($result->getCoverage() as $file => $report) {
$coverage[$file][] = $report;
}
}
}
if ($coverage) {
$file_coverage = array_fill_keys(
$paths,
0);
$file_reports = array();
foreach ($coverage as $file => $reports) {
$report = ArcanistUnitTestResult::mergeCoverage($reports);
$cov = substr_count($report, 'C');
$uncov = substr_count($report, 'U');
if ($cov + $uncov) {
$coverage = $cov / ($cov + $uncov);
} else {
$coverage = 0;
}
$file_coverage[$file] = $coverage;
$file_reports[$file] = $report;
}
$console->writeOut("\n__%s__\n", pht('COVERAGE REPORT'));
asort($file_coverage);
foreach ($file_coverage as $file => $coverage) {
$console->writeOut(
" **%s%%** %s\n",
sprintf('% 3d', (int)(100 * $coverage)),
$file);
$full_path = $working_copy->getProjectRoot().'/'.$file;
if ($this->getArgument('detailed-coverage') &&
Filesystem::pathExists($full_path) &&
is_file($full_path) &&
array_key_exists($file, $file_reports)) {
$console->writeOut(
'%s',
$this->renderDetailedCoverageReport(
Filesystem::readFile($full_path),
$file_reports[$file]));
}
}
}
$this->unresolvedTests = $unresolved;
$overall_result = self::RESULT_OKAY;
foreach ($results as $result) {
$result_code = $result->getResult();
if ($result_code == ArcanistUnitTestResult::RESULT_FAIL ||
$result_code == ArcanistUnitTestResult::RESULT_BROKEN) {
$overall_result = self::RESULT_FAIL;
break;
} else if ($result_code == ArcanistUnitTestResult::RESULT_UNSOUND) {
$overall_result = self::RESULT_UNSOUND;
}
}
if ($output_format !== 'full') {
$console->enableOut();
}
$data = array_values(mpull($results, 'toDictionary'));
switch ($output_format) {
case 'ugly':
$console->writeOut('%s', json_encode($data));
break;
case 'json':
$json = new PhutilJSON();
$console->writeOut('%s', $json->encodeFormatted($data));
break;
case 'full':
// already printed
break;
case 'none':
// do nothing
break;
}
$target_phid = $this->getArgument('target');
if ($target_phid) {
$this->uploadTestResults($target_phid, $overall_result, $results);
}
return $overall_result;
}
public function getUnresolvedTests() {
return $this->unresolvedTests;
}
public function getTestResults() {
return $this->testResults;
}
private function renderDetailedCoverageReport($data, $report) {
$data = explode("\n", $data);
$out = '';
$n = 0;
foreach ($data as $line) {
$out .= sprintf('% 5d ', $n + 1);
$line = str_pad($line, 80, ' ');
if (empty($report[$n])) {
$c = 'N';
} else {
$c = $report[$n];
}
switch ($c) {
case 'C':
$out .= phutil_console_format(
'<bg:green> %s </bg>',
$line);
break;
case 'U':
$out .= phutil_console_format(
'<bg:red> %s </bg>',
$line);
break;
case 'X':
$out .= phutil_console_format(
'<bg:magenta> %s </bg>',
$line);
break;
default:
$out .= ' '.$line.' ';
break;
}
$out .= "\n";
$n++;
}
return $out;
}
private function getOutputFormat() {
if ($this->getArgument('ugly')) {
return 'ugly';
}
if ($this->getArgument('json')) {
return 'json';
}
$format = $this->getArgument('output');
$known_formats = array(
'none' => 'none',
'json' => 'json',
'ugly' => 'ugly',
'full' => 'full',
);
return idx($known_formats, $format, 'full');
}
/**
* Raise a tailored error when a unit test engine returns results in an
* invalid format.
*
* @param ArcanistUnitTestEngine The engine.
* @param wild Results from the engine.
*/
private function validateUnitEngineResults(
ArcanistUnitTestEngine $engine,
$results) {
if (!is_array($results)) {
throw new Exception(
pht(
'Unit test engine (of class "%s") returned invalid results when '.
'run (with method "%s"). Expected a list of "%s" objects as results.',
get_class($engine),
'run()',
'ArcanistUnitTestResult'));
}
foreach ($results as $key => $result) {
if (!($result instanceof ArcanistUnitTestResult)) {
throw new Exception(
pht(
'Unit test engine (of class "%s") returned invalid results when '.
'run (with method "%s"). Expected a list of "%s" objects as '.
'results, but value with key "%s" is not valid.',
get_class($engine),
'run()',
'ArcanistUnitTestResult',
$key));
}
}
}
public static function getHarbormasterTypeFromResult($unit_result) {
switch ($unit_result) {
case self::RESULT_OKAY:
case self::RESULT_SKIP:
$type = 'pass';
break;
default:
$type = 'fail';
break;
}
return $type;
}
private function shouldUploadResults() {
return ($this->getArgument('target') !== null);
}
private function uploadTestResults(
$target_phid,
$unit_result,
array $unit) {
// TODO: It would eventually be nice to stream test results up to the
// server as we go, but just get things working for now.
$message_type = self::getHarbormasterTypeFromResult($unit_result);
foreach ($unit as $key => $result) {
$dictionary = $result->toDictionary();
$unit[$key] = $this->getModernUnitDictionary($dictionary);
}
$this->getConduit()->callMethodSynchronous(
'harbormaster.sendmessage',
array(
'buildTargetPHID' => $target_phid,
'unit' => array_values($unit),
'type' => $message_type,
));
return $formatter;
}
}