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:
parent
9d66610027
commit
493a5d1cc7
13 changed files with 349 additions and 789 deletions
|
@ -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',
|
||||
|
|
|
@ -263,4 +263,8 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
return clone $prompt;
|
||||
}
|
||||
|
||||
protected function getWorkingCopy() {
|
||||
return $this->getConfigurationEngine()->getWorkingCopy();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
69
src/unit/engine/ArcanistUnitEngine.php
Normal file
69
src/unit/engine/ArcanistUnitEngine.php
Normal 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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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')
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
9
src/unit/formatter/ArcanistDefaultUnitFormatter.php
Normal file
9
src/unit/formatter/ArcanistDefaultUnitFormatter.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistDefaultUnitFormatter
|
||||
extends ArcanistUnitFormatter {
|
||||
|
||||
const FORMATTER_KEY = 'default';
|
||||
|
||||
|
||||
}
|
9
src/unit/formatter/ArcanistJSONUnitFormatter.php
Normal file
9
src/unit/formatter/ArcanistJSONUnitFormatter.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistJSONUnitFormatter
|
||||
extends ArcanistUnitFormatter {
|
||||
|
||||
const FORMATTER_KEY = 'json';
|
||||
|
||||
|
||||
}
|
17
src/unit/formatter/ArcanistUnitFormatter.php
Normal file
17
src/unit/formatter/ArcanistUnitFormatter.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
153
src/unit/overseer/ArcanistUnitOverseer.php
Normal file
153
src/unit/overseer/ArcanistUnitOverseer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue