mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-12 18:02: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',
|
'ArcanistConduitException' => 'conduit/ArcanistConduitException.php',
|
||||||
'ArcanistConfigOption' => 'config/option/ArcanistConfigOption.php',
|
'ArcanistConfigOption' => 'config/option/ArcanistConfigOption.php',
|
||||||
'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php',
|
'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php',
|
||||||
'ArcanistConfigurationDrivenUnitTestEngine' => 'unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php',
|
|
||||||
'ArcanistConfigurationEngine' => 'config/ArcanistConfigurationEngine.php',
|
'ArcanistConfigurationEngine' => 'config/ArcanistConfigurationEngine.php',
|
||||||
'ArcanistConfigurationEngineExtension' => 'config/ArcanistConfigurationEngineExtension.php',
|
'ArcanistConfigurationEngineExtension' => 'config/ArcanistConfigurationEngineExtension.php',
|
||||||
'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php',
|
'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php',
|
||||||
|
@ -166,6 +165,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase.php',
|
'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase.php',
|
||||||
'ArcanistDefaultParametersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php',
|
'ArcanistDefaultParametersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php',
|
||||||
'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDefaultParametersXHPASTLinterRuleTestCase.php',
|
'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDefaultParametersXHPASTLinterRuleTestCase.php',
|
||||||
|
'ArcanistDefaultUnitFormatter' => 'unit/formatter/ArcanistDefaultUnitFormatter.php',
|
||||||
'ArcanistDefaultsConfigurationSource' => 'config/source/ArcanistDefaultsConfigurationSource.php',
|
'ArcanistDefaultsConfigurationSource' => 'config/source/ArcanistDefaultsConfigurationSource.php',
|
||||||
'ArcanistDeprecationXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeprecationXHPASTLinterRule.php',
|
'ArcanistDeprecationXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeprecationXHPASTLinterRule.php',
|
||||||
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeprecationXHPASTLinterRuleTestCase.php',
|
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeprecationXHPASTLinterRuleTestCase.php',
|
||||||
|
@ -279,6 +279,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistJSONLintRenderer' => 'lint/renderer/ArcanistJSONLintRenderer.php',
|
'ArcanistJSONLintRenderer' => 'lint/renderer/ArcanistJSONLintRenderer.php',
|
||||||
'ArcanistJSONLinter' => 'lint/linter/ArcanistJSONLinter.php',
|
'ArcanistJSONLinter' => 'lint/linter/ArcanistJSONLinter.php',
|
||||||
'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php',
|
'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php',
|
||||||
|
'ArcanistJSONUnitFormatter' => 'unit/formatter/ArcanistJSONUnitFormatter.php',
|
||||||
'ArcanistJscsLinter' => 'lint/linter/ArcanistJscsLinter.php',
|
'ArcanistJscsLinter' => 'lint/linter/ArcanistJscsLinter.php',
|
||||||
'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php',
|
'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php',
|
||||||
'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php',
|
'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php',
|
||||||
|
@ -471,8 +472,10 @@ phutil_register_library_map(array(
|
||||||
'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnexpectedReturnValueXHPASTLinterRule.php',
|
'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistUnexpectedReturnValueXHPASTLinterRule.php',
|
||||||
'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase.php',
|
'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase.php',
|
||||||
'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.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',
|
'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php',
|
||||||
'ArcanistUnitTestEngine' => 'unit/engine/ArcanistUnitTestEngine.php',
|
|
||||||
'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php',
|
'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php',
|
||||||
'ArcanistUnitTestResultTestCase' => 'unit/__tests__/ArcanistUnitTestResultTestCase.php',
|
'ArcanistUnitTestResultTestCase' => 'unit/__tests__/ArcanistUnitTestResultTestCase.php',
|
||||||
'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php',
|
'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php',
|
||||||
|
@ -947,7 +950,7 @@ phutil_register_library_map(array(
|
||||||
'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
|
'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
|
||||||
'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
|
'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
|
||||||
'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
|
'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
|
||||||
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
|
'PhutilUnitEngine' => 'unit/engine/PhutilUnitEngine.php',
|
||||||
'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php',
|
'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php',
|
||||||
'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
|
'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
|
||||||
'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
|
'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
|
||||||
|
@ -1250,7 +1253,6 @@ phutil_register_library_map(array(
|
||||||
'ArcanistConduitException' => 'Exception',
|
'ArcanistConduitException' => 'Exception',
|
||||||
'ArcanistConfigOption' => 'Phobject',
|
'ArcanistConfigOption' => 'Phobject',
|
||||||
'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine',
|
'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine',
|
||||||
'ArcanistConfigurationDrivenUnitTestEngine' => 'ArcanistUnitTestEngine',
|
|
||||||
'ArcanistConfigurationEngine' => 'Phobject',
|
'ArcanistConfigurationEngine' => 'Phobject',
|
||||||
'ArcanistConfigurationEngineExtension' => 'Phobject',
|
'ArcanistConfigurationEngineExtension' => 'Phobject',
|
||||||
'ArcanistConfigurationManager' => 'Phobject',
|
'ArcanistConfigurationManager' => 'Phobject',
|
||||||
|
@ -1274,6 +1276,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
|
'ArcanistDefaultUnitFormatter' => 'ArcanistUnitFormatter',
|
||||||
'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource',
|
'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource',
|
||||||
'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
|
@ -1315,7 +1318,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
'ArcanistFeatureWorkflow' => 'ArcanistWorkflow',
|
'ArcanistFeatureWorkflow' => 'ArcanistWorkflow',
|
||||||
'ArcanistFileConfigurationSource' => 'ArcanistConfigurationSource',
|
'ArcanistFileConfigurationSource' => 'ArcanistFilesystemConfigurationSource',
|
||||||
'ArcanistFileDataRef' => 'Phobject',
|
'ArcanistFileDataRef' => 'Phobject',
|
||||||
'ArcanistFileUploader' => 'Phobject',
|
'ArcanistFileUploader' => 'Phobject',
|
||||||
'ArcanistFilenameLinter' => 'ArcanistLinter',
|
'ArcanistFilenameLinter' => 'ArcanistLinter',
|
||||||
|
@ -1387,6 +1390,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer',
|
'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer',
|
||||||
'ArcanistJSONLinter' => 'ArcanistLinter',
|
'ArcanistJSONLinter' => 'ArcanistLinter',
|
||||||
'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase',
|
'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase',
|
||||||
|
'ArcanistJSONUnitFormatter' => 'ArcanistUnitFormatter',
|
||||||
'ArcanistJscsLinter' => 'ArcanistExternalLinter',
|
'ArcanistJscsLinter' => 'ArcanistExternalLinter',
|
||||||
'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase',
|
'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase',
|
||||||
'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
|
@ -1579,8 +1583,10 @@ phutil_register_library_map(array(
|
||||||
'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistUnexpectedReturnValueXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer',
|
'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer',
|
||||||
|
'ArcanistUnitEngine' => 'Phobject',
|
||||||
|
'ArcanistUnitFormatter' => 'Phobject',
|
||||||
|
'ArcanistUnitOverseer' => 'Phobject',
|
||||||
'ArcanistUnitRenderer' => 'Phobject',
|
'ArcanistUnitRenderer' => 'Phobject',
|
||||||
'ArcanistUnitTestEngine' => 'Phobject',
|
|
||||||
'ArcanistUnitTestResult' => 'Phobject',
|
'ArcanistUnitTestResult' => 'Phobject',
|
||||||
'ArcanistUnitTestResultTestCase' => 'PhutilTestCase',
|
'ArcanistUnitTestResultTestCase' => 'PhutilTestCase',
|
||||||
'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine',
|
'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine',
|
||||||
|
@ -2079,7 +2085,7 @@ phutil_register_library_map(array(
|
||||||
'PhutilUSEnglishLocale' => 'PhutilLocale',
|
'PhutilUSEnglishLocale' => 'PhutilLocale',
|
||||||
'PhutilUTF8StringTruncator' => 'Phobject',
|
'PhutilUTF8StringTruncator' => 'Phobject',
|
||||||
'PhutilUTF8TestCase' => 'PhutilTestCase',
|
'PhutilUTF8TestCase' => 'PhutilTestCase',
|
||||||
'PhutilUnitTestEngine' => 'ArcanistUnitTestEngine',
|
'PhutilUnitEngine' => 'ArcanistUnitEngine',
|
||||||
'PhutilUnitTestEngineTestCase' => 'PhutilTestCase',
|
'PhutilUnitTestEngineTestCase' => 'PhutilTestCase',
|
||||||
'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
|
'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
|
||||||
'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
|
'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
|
||||||
|
|
|
@ -263,4 +263,8 @@ abstract class ArcanistWorkflow extends Phobject {
|
||||||
return clone $prompt;
|
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
|
<?php
|
||||||
|
|
||||||
/**
|
final class PhutilUnitEngine
|
||||||
* Very basic unit test engine which runs libphutil tests.
|
extends ArcanistUnitEngine {
|
||||||
*/
|
|
||||||
final class PhutilUnitTestEngine extends ArcanistUnitTestEngine {
|
|
||||||
|
|
||||||
public function getEngineConfigurationName() {
|
const ENGINETYPE = 'phutil';
|
||||||
return 'phutil';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function supportsRunAllTests() {
|
public function runTests() {
|
||||||
return true;
|
$run_tests = $this->getAllTests();
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$test_cases = array();
|
$test_cases = array();
|
||||||
|
|
||||||
foreach ($run_tests as $test_class) {
|
foreach ($run_tests as $test_class) {
|
||||||
$test_case = newv($test_class, array())
|
$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_cases[] = $test_case;
|
$test_cases[] = $test_case;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +20,11 @@ final class PhutilUnitTestEngine extends ArcanistUnitTestEngine {
|
||||||
|
|
||||||
$results = array();
|
$results = array();
|
||||||
foreach ($test_cases as $test_case) {
|
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);
|
$results = array_mergev($results);
|
||||||
|
|
||||||
|
@ -78,7 +36,7 @@ final class PhutilUnitTestEngine extends ArcanistUnitTestEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getAllTests() {
|
private function getAllTests() {
|
||||||
$project_root = $this->getWorkingCopy()->getProjectRoot();
|
$project_root = $this->getPath();
|
||||||
|
|
||||||
$symbols = id(new PhutilSymbolLoader())
|
$symbols = id(new PhutilSymbolLoader())
|
||||||
->setType('class')
|
->setType('class')
|
|
@ -76,8 +76,7 @@ final class PhutilUnitTestEngineTestCase extends PhutilTestCase {
|
||||||
$failed = 0;
|
$failed = 0;
|
||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
|
|
||||||
$test_case = id(new PhutilTestCaseTestCase())
|
$test_case = new PhutilTestCaseTestCase();
|
||||||
->setWorkingCopy($this->getWorkingCopy());
|
|
||||||
|
|
||||||
foreach ($test_case->run() as $result) {
|
foreach ($test_case->run() as $result) {
|
||||||
if ($result->getResult() == ArcanistUnitTestResult::RESULT_FAIL) {
|
if ($result->getResult() == ArcanistUnitTestResult::RESULT_FAIL) {
|
||||||
|
@ -161,8 +160,7 @@ final class PhutilUnitTestEngineTestCase extends PhutilTestCase {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
$test_engine = id(new PhutilUnitTestEngine())
|
$test_engine = new PhutilUnitTestEngine();
|
||||||
->setWorkingCopy($this->getWorkingCopy());
|
|
||||||
|
|
||||||
$library = phutil_get_current_library_name();
|
$library = phutil_get_current_library_name();
|
||||||
$library_root = phutil_get_library_root($library);
|
$library_root = phutil_get_library_root($library);
|
||||||
|
|
|
@ -651,6 +651,9 @@ abstract class PhutilTestCase extends Phobject {
|
||||||
}
|
}
|
||||||
|
|
||||||
final protected function getLink($method) {
|
final protected function getLink($method) {
|
||||||
|
// TOOLSETS: Restore this.
|
||||||
|
return null;
|
||||||
|
|
||||||
$base_uri = $this
|
$base_uri = $this
|
||||||
->getWorkingCopy()
|
->getWorkingCopy()
|
||||||
->getProjectConfig('phabricator.uri');
|
->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
|
<?php
|
||||||
|
|
||||||
/**
|
final class ArcanistUnitWorkflow
|
||||||
* Runs unit tests which cover your changes.
|
extends ArcanistWorkflow {
|
||||||
*/
|
|
||||||
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;
|
|
||||||
|
|
||||||
public function getWorkflowName() {
|
public function getWorkflowName() {
|
||||||
return 'unit';
|
return 'unit';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCommandSynopses() {
|
public function getWorkflowInformation() {
|
||||||
return phutil_console_format(<<<EOTEXT
|
$help = pht(<<<EOTEXT
|
||||||
**unit** [__options__] [__paths__]
|
Run unit tests.
|
||||||
**unit** [__options__] --rev [__rev__]
|
|
||||||
EOTEXT
|
EOTEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return $this->newWorkflowInformation()
|
||||||
|
->addExample(pht('**unit** [__options__] __path__ __path__ ...'))
|
||||||
|
->addExample(pht('**unit** [__options__] --commit __commit__'))
|
||||||
|
->setHelp($help);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCommandHelp() {
|
public function getWorkflowArguments() {
|
||||||
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() {
|
|
||||||
return array(
|
return array(
|
||||||
'rev' => array(
|
$this->newWorkflowArgument('commit')
|
||||||
'param' => 'revision',
|
->setParameter('commit'),
|
||||||
'help' => pht(
|
$this->newWorkflowArgument('format')
|
||||||
'Run unit tests covering changes since a specific revision.'),
|
->setParameter('format'),
|
||||||
'supports' => array(
|
$this->newWorkflowArgument('everything'),
|
||||||
'git',
|
$this->newWorkflowArgument('paths')
|
||||||
'hg',
|
->setWildcard(true),
|
||||||
),
|
|
||||||
'nosupport' => array(
|
// TOOLSETS: Restore "--target".
|
||||||
'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',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function requiresWorkingCopy() {
|
public function runWorkflow() {
|
||||||
return true;
|
// 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();
|
$working_copy = $this->getWorkingCopy();
|
||||||
|
if ($working_copy) {
|
||||||
|
$directory = $working_copy->getPath();
|
||||||
|
} else {
|
||||||
|
$directory = getcwd();
|
||||||
|
}
|
||||||
|
|
||||||
$paths = $this->getArgument('paths');
|
$overseer = id(new ArcanistUnitOverseer())
|
||||||
$rev = $this->getArgument('rev');
|
->setDirectory($directory);
|
||||||
$everything = $this->getArgument('everything');
|
|
||||||
if ($everything && $paths) {
|
// 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(
|
throw new ArcanistUsageException(
|
||||||
pht(
|
pht(
|
||||||
'You can not specify paths with %s. The %s flag runs every test '.
|
'Unit test output format ("%s") is unknown. Supported formats '.
|
||||||
'associated with a tracked file in the working copy.',
|
'are: %s.',
|
||||||
'--everything',
|
$format_key,
|
||||||
'--everything'));
|
implode(', ', array_keys($formatters))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($everything) {
|
return $formatter;
|
||||||
$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,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue