Page MenuHomeSealhub

No OneTemporary

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 9cf2a511..15337967 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,297 +1,300 @@
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
* @generated
* @phutil-library-version 2
*/
phutil_register_library_map(array(
'__library_version__' => 2,
'class' =>
array(
'ArcanistAliasWorkflow' => 'workflow/ArcanistAliasWorkflow.php',
'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php',
'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php',
'ArcanistApacheLicenseLinter' => 'lint/linter/ArcanistApacheLicenseLinter.php',
'ArcanistArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistArcanistLinterTestCase.php',
'ArcanistBaseCommitParser' => 'parser/ArcanistBaseCommitParser.php',
'ArcanistBaseCommitParserTestCase' => 'parser/__tests__/ArcanistBaseCommitParserTestCase.php',
'ArcanistBaseTestResultParser' => 'unit/engine/ArcanistBaseTestResultParser.php',
'ArcanistBaseUnitTestEngine' => 'unit/engine/ArcanistBaseUnitTestEngine.php',
'ArcanistBaseWorkflow' => 'workflow/ArcanistBaseWorkflow.php',
'ArcanistBaseXHPASTLinter' => 'lint/linter/ArcanistBaseXHPASTLinter.php',
'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php',
'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php',
'ArcanistBritishTestCase' => 'configuration/__tests__/ArcanistBritishTestCase.php',
'ArcanistBrowseWorkflow' => 'workflow/ArcanistBrowseWorkflow.php',
'ArcanistBundle' => 'parser/ArcanistBundle.php',
'ArcanistBundleTestCase' => 'parser/__tests__/ArcanistBundleTestCase.php',
'ArcanistCallConduitWorkflow' => 'workflow/ArcanistCallConduitWorkflow.php',
'ArcanistCapabilityNotSupportedException' => 'workflow/exception/ArcanistCapabilityNotSupportedException.php',
'ArcanistChooseInvalidRevisionException' => 'exception/ArcanistChooseInvalidRevisionException.php',
'ArcanistChooseNoRevisionsException' => 'exception/ArcanistChooseNoRevisionsException.php',
'ArcanistCloseRevisionWorkflow' => 'workflow/ArcanistCloseRevisionWorkflow.php',
'ArcanistCloseWorkflow' => 'workflow/ArcanistCloseWorkflow.php',
'ArcanistCommentRemover' => 'parser/ArcanistCommentRemover.php',
'ArcanistCommentRemoverTestCase' => 'parser/__tests__/ArcanistCommentRemoverTestCase.php',
'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php',
'ArcanistConduitLinter' => 'lint/linter/ArcanistConduitLinter.php',
'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php',
'ArcanistCoverWorkflow' => 'workflow/ArcanistCoverWorkflow.php',
'ArcanistCppcheckLinter' => 'lint/linter/ArcanistCppcheckLinter.php',
'ArcanistCpplintLinter' => 'lint/linter/ArcanistCpplintLinter.php',
'ArcanistCpplintLinterTestCase' => 'lint/linter/__tests__/ArcanistCpplintLinterTestCase.php',
'ArcanistDiffChange' => 'parser/diff/ArcanistDiffChange.php',
'ArcanistDiffChangeType' => 'parser/diff/ArcanistDiffChangeType.php',
'ArcanistDiffHunk' => 'parser/diff/ArcanistDiffHunk.php',
'ArcanistDiffParser' => 'parser/ArcanistDiffParser.php',
'ArcanistDiffParserTestCase' => 'parser/__tests__/ArcanistDiffParserTestCase.php',
'ArcanistDiffUtils' => 'difference/ArcanistDiffUtils.php',
'ArcanistDiffUtilsTestCase' => 'difference/__tests__/ArcanistDiffUtilsTestCase.php',
'ArcanistDiffWorkflow' => 'workflow/ArcanistDiffWorkflow.php',
'ArcanistDifferentialCommitMessage' => 'differential/ArcanistDifferentialCommitMessage.php',
'ArcanistDifferentialCommitMessageParserException' => 'differential/ArcanistDifferentialCommitMessageParserException.php',
'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php',
'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php',
'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php',
'ArcanistEventType' => 'events/constant/ArcanistEventType.php',
'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php',
'ArcanistFeatureWorkflow' => 'workflow/ArcanistFeatureWorkflow.php',
'ArcanistFilenameLinter' => 'lint/linter/ArcanistFilenameLinter.php',
'ArcanistFlagWorkflow' => 'workflow/ArcanistFlagWorkflow.php',
'ArcanistFlake8Linter' => 'lint/linter/ArcanistFlake8Linter.php',
'ArcanistFlake8LinterTestCase' => 'lint/linter/__tests__/ArcanistFlake8LinterTestCase.php',
'ArcanistFutureLinter' => 'lint/linter/ArcanistFutureLinter.php',
'ArcanistGeneratedLinter' => 'lint/linter/ArcanistGeneratedLinter.php',
'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php',
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
'ArcanistGitHookPreReceiveWorkflow' => 'workflow/ArcanistGitHookPreReceiveWorkflow.php',
'ArcanistHelpWorkflow' => 'workflow/ArcanistHelpWorkflow.php',
'ArcanistHgClientChannel' => 'hgdaemon/ArcanistHgClientChannel.php',
'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php',
'ArcanistHgProxyServer' => 'hgdaemon/ArcanistHgProxyServer.php',
'ArcanistHgServerChannel' => 'hgdaemon/ArcanistHgServerChannel.php',
'ArcanistHookAPI' => 'repository/hookapi/ArcanistHookAPI.php',
'ArcanistInlinesWorkflow' => 'workflow/ArcanistInlinesWorkflow.php',
'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php',
'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php',
'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php',
'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php',
'ArcanistLicenseLinter' => 'lint/linter/ArcanistLicenseLinter.php',
'ArcanistLintConsoleRenderer' => 'lint/renderer/ArcanistLintConsoleRenderer.php',
'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php',
'ArcanistLintJSONRenderer' => 'lint/renderer/ArcanistLintJSONRenderer.php',
'ArcanistLintLikeCompilerRenderer' => 'lint/renderer/ArcanistLintLikeCompilerRenderer.php',
'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php',
'ArcanistLintNoneRenderer' => 'lint/renderer/ArcanistLintNoneRenderer.php',
'ArcanistLintPatcher' => 'lint/ArcanistLintPatcher.php',
'ArcanistLintRenderer' => 'lint/renderer/ArcanistLintRenderer.php',
'ArcanistLintResult' => 'lint/ArcanistLintResult.php',
'ArcanistLintSeverity' => 'lint/ArcanistLintSeverity.php',
'ArcanistLintSummaryRenderer' => 'lint/renderer/ArcanistLintSummaryRenderer.php',
'ArcanistLintWorkflow' => 'workflow/ArcanistLintWorkflow.php',
'ArcanistLinter' => 'lint/linter/ArcanistLinter.php',
'ArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistLinterTestCase.php',
'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php',
'ArcanistMarkCommittedWorkflow' => 'workflow/ArcanistMarkCommittedWorkflow.php',
'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php',
'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php',
'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php',
'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php',
'ArcanistNoEffectException' => 'exception/usage/ArcanistNoEffectException.php',
'ArcanistNoEngineException' => 'exception/usage/ArcanistNoEngineException.php',
'ArcanistNoLintLinter' => 'lint/linter/ArcanistNoLintLinter.php',
'ArcanistNoLintTestCaseMisnamed' => 'lint/linter/__tests__/ArcanistNoLintTestCase.php',
'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php',
'ArcanistPasteWorkflow' => 'workflow/ArcanistPasteWorkflow.php',
'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php',
'ArcanistPhpcsLinter' => 'lint/linter/ArcanistPhpcsLinter.php',
'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php',
'ArcanistPhutilTestCase' => 'unit/engine/phutil/ArcanistPhutilTestCase.php',
'ArcanistPhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php',
'ArcanistPhutilTestSkippedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php',
'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php',
'ArcanistPhutilXHPASTLinter' => 'lint/linter/ArcanistPhutilXHPASTLinter.php',
'ArcanistPhutilXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistPhutilXHPASTLinterTestCase.php',
'ArcanistPyFlakesLinter' => 'lint/linter/ArcanistPyFlakesLinter.php',
'ArcanistPyLintLinter' => 'lint/linter/ArcanistPyLintLinter.php',
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php',
'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php',
'ArcanistScalaSBTLinter' => 'lint/linter/ArcanistScalaSBTLinter.php',
'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php',
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php',
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
'ArcanistSpellingDefaultData' => 'lint/linter/spelling/ArcanistSpellingDefaultData.php',
'ArcanistSpellingLinter' => 'lint/linter/ArcanistSpellingLinter.php',
'ArcanistSpellingLinterTestCase' => 'lint/linter/__tests__/ArcanistSpellingLinterTestCase.php',
'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php',
'ArcanistSubversionHookAPI' => 'repository/hookapi/ArcanistSubversionHookAPI.php',
'ArcanistSvnHookPreCommitWorkflow' => 'workflow/ArcanistSvnHookPreCommitWorkflow.php',
'ArcanistTasksWorkflow' => 'workflow/ArcanistTasksWorkflow.php',
'ArcanistTestCase' => 'infrastructure/testing/ArcanistTestCase.php',
'ArcanistTextLinter' => 'lint/linter/ArcanistTextLinter.php',
'ArcanistTextLinterTestCase' => 'lint/linter/__tests__/ArcanistTextLinterTestCase.php',
'ArcanistTodoWorkflow' => 'workflow/ArcanistTodoWorkflow.php',
'ArcanistUncommittedChangesException' => 'exception/usage/ArcanistUncommittedChangesException.php',
+ 'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php',
+ 'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php',
'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php',
'ArcanistUnitWorkflow' => 'workflow/ArcanistUnitWorkflow.php',
'ArcanistUpgradeWorkflow' => 'workflow/ArcanistUpgradeWorkflow.php',
'ArcanistUploadWorkflow' => 'workflow/ArcanistUploadWorkflow.php',
'ArcanistUsageException' => 'exception/ArcanistUsageException.php',
'ArcanistUserAbortException' => 'exception/usage/ArcanistUserAbortException.php',
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php',
'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php',
'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php',
'ArcanistXHPASTLintSwitchHook' => 'lint/linter/xhpast/ArcanistXHPASTLintSwitchHook.php',
'ArcanistXHPASTLintTestSwitchHook' => 'lint/linter/__tests__/ArcanistXHPASTLintTestSwitchHook.php',
'ArcanistXHPASTLinter' => 'lint/linter/ArcanistXHPASTLinter.php',
'ArcanistXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php',
'ComprehensiveLintEngine' => 'lint/engine/ComprehensiveLintEngine.php',
'ExampleLintEngine' => 'lint/engine/ExampleLintEngine.php',
'GoTestResultParser' => 'unit/engine/GoTestResultParser.php',
'GoTestResultParserTestCase' => 'unit/engine/__tests__/GoTestResultParserTestCase.php',
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
'PHPUnitTestEngineTestCase' => 'unit/engine/__tests__/PHPUnitTestEngineTestCase.php',
'PhpunitResultParser' => 'unit/engine/PhpunitResultParser.php',
'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php',
'PhutilLintEngine' => 'lint/engine/PhutilLintEngine.php',
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php',
'UnitTestableArcanistLintEngine' => 'lint/engine/UnitTestableArcanistLintEngine.php',
),
'function' =>
array(
),
'xmap' =>
array(
'ArcanistAliasWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistAnoidWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter',
'ArcanistArcanistLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistBaseCommitParserTestCase' => 'ArcanistTestCase',
'ArcanistBaseWorkflow' => 'Phobject',
'ArcanistBaseXHPASTLinter' => 'ArcanistFutureLinter',
'ArcanistBookmarkWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBritishTestCase' => 'ArcanistTestCase',
'ArcanistBrowseWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistBundleTestCase' => 'ArcanistTestCase',
'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCapabilityNotSupportedException' => 'Exception',
'ArcanistChooseInvalidRevisionException' => 'Exception',
'ArcanistChooseNoRevisionsException' => 'Exception',
'ArcanistCloseRevisionWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCloseWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCommentRemoverTestCase' => 'ArcanistTestCase',
'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistConduitLinter' => 'ArcanistLinter',
'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCppcheckLinter' => 'ArcanistLinter',
'ArcanistCpplintLinter' => 'ArcanistLinter',
'ArcanistCpplintLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistDiffParserTestCase' => 'ArcanistTestCase',
'ArcanistDiffUtilsTestCase' => 'ArcanistTestCase',
'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDifferentialCommitMessageParserException' => 'Exception',
'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistEventType' => 'PhutilEventType',
'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFeatureWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistFlagWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFlake8Linter' => 'ArcanistLinter',
'ArcanistFlake8LinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistFutureLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinter' => 'ArcanistLinter',
'ArcanistGetConfigWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHelpWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHgClientChannel' => 'PhutilProtocolChannel',
'ArcanistHgServerChannel' => 'PhutilProtocolChannel',
'ArcanistInlinesWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistInstallCertificateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistJSHintLinter' => 'ArcanistLinter',
'ArcanistLandWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLicenseLinter' => 'ArcanistLinter',
'ArcanistLintConsoleRenderer' => 'ArcanistLintRenderer',
'ArcanistLintJSONRenderer' => 'ArcanistLintRenderer',
'ArcanistLintLikeCompilerRenderer' => 'ArcanistLintRenderer',
'ArcanistLintNoneRenderer' => 'ArcanistLintRenderer',
'ArcanistLintSummaryRenderer' => 'ArcanistLintRenderer',
'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistMercurialParserTestCase' => 'ArcanistTestCase',
'ArcanistMergeConflictLinter' => 'ArcanistLinter',
'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException',
'ArcanistNoLintLinter' => 'ArcanistLinter',
'ArcanistNoLintTestCaseMisnamed' => 'ArcanistLinterTestCase',
'ArcanistPEP8Linter' => 'ArcanistFutureLinter',
'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPhpcsLinter' => 'ArcanistLinter',
'ArcanistPhutilLibraryLinter' => 'ArcanistLinter',
'ArcanistPhutilTestCaseTestCase' => 'ArcanistPhutilTestCase',
'ArcanistPhutilTestSkippedException' => 'Exception',
'ArcanistPhutilTestTerminatedException' => 'Exception',
'ArcanistPhutilXHPASTLinter' => 'ArcanistBaseXHPASTLinter',
'ArcanistPhutilXHPASTLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistPyFlakesLinter' => 'ArcanistLinter',
'ArcanistPyLintLinter' => 'ArcanistLinter',
'ArcanistRepositoryAPIMiscTestCase' => 'ArcanistTestCase',
'ArcanistRepositoryAPIStateTestCase' => 'ArcanistTestCase',
'ArcanistRubyLinter' => 'ArcanistLinter',
'ArcanistRubyLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistScalaSBTLinter' => 'ArcanistLinter',
'ArcanistScriptAndRegexLinter' => 'ArcanistLinter',
'ArcanistSetConfigWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistShellCompleteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistSingleLintEngine' => 'ArcanistLintEngine',
'ArcanistSpellingLinter' => 'ArcanistLinter',
'ArcanistSpellingLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI',
'ArcanistSubversionHookAPI' => 'ArcanistHookAPI',
'ArcanistSvnHookPreCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTasksWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTestCase' => 'ArcanistPhutilTestCase',
'ArcanistTextLinter' => 'ArcanistLinter',
'ArcanistTextLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ArcanistTodoWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUncommittedChangesException' => 'ArcanistUsageException',
+ 'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer',
'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUpgradeWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUploadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUsageException' => 'Exception',
'ArcanistUserAbortException' => 'ArcanistUsageException',
'ArcanistWhichWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistXHPASTLintNamingHookTestCase' => 'ArcanistTestCase',
'ArcanistXHPASTLintTestSwitchHook' => 'ArcanistXHPASTLintSwitchHook',
'ArcanistXHPASTLinter' => 'ArcanistBaseXHPASTLinter',
'ArcanistXHPASTLinterTestCase' => 'ArcanistArcanistLinterTestCase',
'ComprehensiveLintEngine' => 'ArcanistLintEngine',
'ExampleLintEngine' => 'ArcanistLintEngine',
'GoTestResultParser' => 'ArcanistBaseTestResultParser',
'GoTestResultParserTestCase' => 'ArcanistTestCase',
'NoseTestEngine' => 'ArcanistBaseUnitTestEngine',
'PHPUnitTestEngineTestCase' => 'ArcanistTestCase',
'PhpunitResultParser' => 'ArcanistBaseTestResultParser',
'PhpunitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilLintEngine' => 'ArcanistLintEngine',
'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilUnitTestEngineTestCase' => 'ArcanistTestCase',
'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine',
),
));
diff --git a/src/unit/ArcanistUnitTestResult.php b/src/unit/ArcanistUnitTestResult.php
index 2690d6ee..6372efcc 100644
--- a/src/unit/ArcanistUnitTestResult.php
+++ b/src/unit/ArcanistUnitTestResult.php
@@ -1,140 +1,128 @@
<?php
/**
* Represents the outcome of running a unit test.
*
* @group unit
*/
final class ArcanistUnitTestResult {
const RESULT_PASS = 'pass';
const RESULT_FAIL = 'fail';
const RESULT_SKIP = 'skip';
const RESULT_BROKEN = 'broken';
const RESULT_UNSOUND = 'unsound';
const RESULT_POSTPONED = 'postponed';
private $namespace;
private $name;
private $link;
private $result;
private $duration;
private $userData;
private $extraData;
private $coverage;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setLink($link) {
$this->link = $link;
return $this;
}
public function getLink() {
return $this->link;
}
public function setResult($result) {
$this->result = $result;
return $this;
}
public function getResult() {
return $this->result;
}
- public function getConsoleFormattedResult() {
- static $status_codes = array(
- self::RESULT_PASS => '<bg:green>** PASS **</bg>',
- self::RESULT_FAIL => '<bg:red>** FAIL **</bg>',
- self::RESULT_SKIP => '<bg:yellow>** SKIP **</bg>',
- self::RESULT_BROKEN => '<bg:red>** BROKEN **</bg>',
- self::RESULT_UNSOUND => '<bg:yellow>** UNSOUND **</bg>',
- self::RESULT_POSTPONED => '<bg:yellow>** POSTPONED **</bg>',
- );
- return phutil_console_format($status_codes[$this->result]);
- }
-
public function setDuration($duration) {
$this->duration = $duration;
return $this;
}
public function getDuration() {
return $this->duration;
}
public function setUserData($user_data) {
$this->userData = $user_data;
return $this;
}
public function getUserData() {
return $this->userData;
}
/**
* "extra data" allows an implementation to store additional
* key/value metadata along with the result of the test run.
*/
public function setExtraData(array $extra_data = null) {
$this->extraData = $extra_data;
return $this;
}
public function getExtraData() {
return $this->extraData;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
public function getCoverage() {
return $this->coverage;
}
/**
* Merge several coverage reports into a comprehensive coverage report.
*
* @param list List of coverage report strings.
* @return string Cumulative coverage report.
*/
public static function mergeCoverage(array $coverage) {
if (empty($coverage)) {
return null;
}
$base = reset($coverage);
foreach ($coverage as $more_coverage) {
$len = min(strlen($base), strlen($more_coverage));
for ($ii = 0; $ii < $len; $ii++) {
if ($more_coverage[$ii] == 'C') {
$base[$ii] = 'C';
}
}
}
return $base;
}
public function toDictionary() {
return array(
'name' => $this->getName(),
'link' => $this->getLink(),
'result' => $this->getResult(),
'duration' => $this->getDuration(),
'extra' => $this->getExtraData(),
'userData' => $this->getUserData(),
'coverage' => $this->getCoverage(),
);
}
}
diff --git a/src/unit/engine/ArcanistBaseUnitTestEngine.php b/src/unit/engine/ArcanistBaseUnitTestEngine.php
index e6ea50f0..802cc15d 100644
--- a/src/unit/engine/ArcanistBaseUnitTestEngine.php
+++ b/src/unit/engine/ArcanistBaseUnitTestEngine.php
@@ -1,100 +1,106 @@
<?php
/**
* Manages unit test execution.
*
* @group unit
*/
abstract class ArcanistBaseUnitTestEngine {
private $workingCopy;
private $paths;
private $arguments = array();
protected $diffID;
private $enableAsyncTests;
private $enableCoverage;
private $runAllTests;
+ protected $renderer;
public function setRunAllTests($run_all_tests) {
if (!$this->supportsRunAllTests() && $run_all_tests) {
$class = get_class($this);
throw new Exception(
"Engine '{$class}' does not support --everything.");
}
$this->runAllTests = $run_all_tests;
return $this;
}
public function getRunAllTests() {
return $this->runAllTests;
}
protected function supportsRunAllTests() {
return false;
}
final public function __construct() {
}
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 setArguments(array $arguments) {
$this->arguments = $arguments;
return $this;
}
final public function getArgument($key, $default = null) {
return idx($this->arguments, $key, $default);
}
final public function setEnableAsyncTests($enable_async_tests) {
$this->enableAsyncTests = $enable_async_tests;
return $this;
}
final public function getEnableAsyncTests() {
return $this->enableAsyncTests;
}
final public function setEnableCoverage($enable_coverage) {
$this->enableCoverage = $enable_coverage;
return $this;
}
final public function getEnableCoverage() {
return $this->enableCoverage;
}
+ public function setRenderer(ArcanistUnitRenderer $renderer) {
+ $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;
}
}
diff --git a/src/unit/engine/PhutilUnitTestEngine.php b/src/unit/engine/PhutilUnitTestEngine.php
index 9f1cfcbb..2179d5ba 100644
--- a/src/unit/engine/PhutilUnitTestEngine.php
+++ b/src/unit/engine/PhutilUnitTestEngine.php
@@ -1,172 +1,179 @@
<?php
/**
* Very basic unit test engine which runs libphutil tests.
*
* @group unitrun
*/
final class PhutilUnitTestEngine extends ArcanistBaseUnitTestEngine {
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("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(
"You specified --coverage but xdebug is not available, so ".
"coverage can not be enabled for PhutilUnitTestEngine.");
}
} else {
$enable_coverage = true;
}
}
$project_root = $this->getWorkingCopy()->getProjectRoot();
$results = array();
foreach ($run_tests as $test_class) {
$test_case = newv($test_class, array());
$test_case->setEnableCoverage($enable_coverage);
$test_case->setProjectRoot($project_root);
if ($this->getPaths()) {
$test_case->setPaths($this->getPaths());
}
+ if ($this->renderer) {
+ $test_case->setRenderer($this->renderer);
+ }
$results[] = $test_case->run();
}
$results = array_mergev($results);
return $results;
}
private function getAllTests() {
$project_root = $this->getWorkingCopy()->getProjectRoot();
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass('ArcanistPhutilTestCase')
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
$in_working_copy = array();
$run_tests = array();
foreach ($symbols as $symbol) {
if (!preg_match('@/__tests__/@', $symbol['where'])) {
continue;
}
$library = $symbol['library'];
if (!isset($in_working_copy[$library])) {
$library_root = phutil_get_library_root($library);
$in_working_copy[$library] = Filesystem::isDescendant(
$library_root,
$project_root);
}
if ($in_working_copy[$library]) {
$run_tests[] = $symbol['name'];
}
}
return $run_tests;
}
private function getTestsForPaths() {
$project_root = $this->getWorkingCopy()->getProjectRoot();
$look_here = array();
foreach ($this->getPaths() as $path) {
$library_root = phutil_get_library_root_for_path($path);
if (!$library_root) {
continue;
}
$library_name = phutil_get_library_name_for_root($library_root);
if (!$library_name) {
throw new Exception(
"Attempting to run unit tests on a libphutil library which has not ".
"been loaded, at:\n\n".
" {$library_root}\n\n".
"This probably means one of two things:\n\n".
" - You may need to add this library to .arcconfig.\n".
" - You may be running tests on a copy of libphutil or arcanist\n".
" using a different copy of libphutil or arcanist. This\n".
" operation is not supported.");
}
$path = Filesystem::resolvePath($path, $project_root);
if (!is_dir($path)) {
$path = dirname($path);
}
if ($path == $library_root) {
continue;
}
if (!Filesystem::isDescendant($path, $library_root)) {
// We have encountered some kind of symlink maze -- for instance, $path
// is some symlink living outside the library that links into some file
// inside the library. Just ignore these cases, since the affected file
// does not actually lie within the library.
continue;
}
$library_path = Filesystem::readablePath($path, $library_root);
do {
$look_here[$library_name.':'.$library_path] = array(
'library' => $library_name,
'path' => $library_path,
);
$library_path = dirname($library_path);
} while ($library_path != '.');
}
// Look for any class that extends ArcanistPhutilTestCase inside a
// __tests__ directory in any parent directory of every affected file.
//
// The idea is that "infrastructure/__tests__/" tests defines general tests
// for all of "infrastructure/", and those tests run for any change in
// "infrastructure/". However, "infrastructure/concrete/rebar/__tests__/"
// defines more specific tests that run only when rebar/ (or some
// subdirectory) changes.
$run_tests = array();
foreach ($look_here as $path_info) {
$library = $path_info['library'];
$path = $path_info['path'];
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setLibrary($library)
->setPathPrefix($path.'/__tests__/')
->setAncestorClass('ArcanistPhutilTestCase')
->setConcreteOnly(true)
->selectAndLoadSymbols();
foreach ($symbols as $symbol) {
$run_tests[$symbol['name']] = true;
}
}
$run_tests = array_keys($run_tests);
return $run_tests;
}
+ public function shouldEchoTestResults() {
+ return !$this->renderer;
+ }
+
}
diff --git a/src/unit/engine/phutil/ArcanistPhutilTestCase.php b/src/unit/engine/phutil/ArcanistPhutilTestCase.php
index 2c9126a5..3f7d4507 100644
--- a/src/unit/engine/phutil/ArcanistPhutilTestCase.php
+++ b/src/unit/engine/phutil/ArcanistPhutilTestCase.php
@@ -1,533 +1,543 @@
<?php
/**
* Base test case for the very simple libphutil test framework.
*
* @task assert Making Test Assertions
* @task exceptions Exception Handling
* @task hook Hooks for Setup and Teardown
* @task internal Internals
*
* @group unitrun
*/
abstract class ArcanistPhutilTestCase {
private $assertions = 0;
private $runningTest;
private $testStartTime;
private $results = array();
private $enableCoverage;
private $coverage = array();
private $projectRoot;
private $paths;
+ private $renderer;
/* -( Making Test Assertions )--------------------------------------------- */
/**
* Assert that two values are equal. The test fails if they are not.
*
* NOTE: This method uses PHP's strict equality test operator ("===") to
* compare values. This means values and types must be equal, key order must
* be identical in arrays, and objects must be referentially identical.
*
* @param wild The theoretically expected value, generated by careful
* reasoning about the properties of the system.
* @param wild The empirically derived value, generated by executing the
* test.
* @param string A human-readable description of what these values represent,
* and particularly of what a discrepancy means.
*
* @return void
* @task assert
*/
final protected function assertEqual($expect, $result, $message = null) {
if ($expect === $result) {
$this->assertions++;
return;
}
$expect = PhutilReadableSerializer::printableValue($expect);
$result = PhutilReadableSerializer::printableValue($result);
$where = debug_backtrace();
$where = array_shift($where);
$line = idx($where, 'line');
$file = basename(idx($where, 'file'));
$output = "Assertion failed at line {$line} in {$file}";
if ($message) {
$output .= ": {$message}";
}
$output .= "\n";
if (strpos($expect, "\n") === false && strpos($result, "\n") === false) {
$output .= "Expected: {$expect}\n";
$output .= "Actual: {$result}";
} else {
$output .= "Expected vs Actual Output Diff\n";
$output .= ArcanistDiffUtils::renderDifferences(
$expect,
$result,
$lines = 0xFFFF);
}
$this->failTest($output);
throw new ArcanistPhutilTestTerminatedException($output);
}
/**
* Assert an unconditional failure. This is just a convenience method that
* better indicates intent than using dummy values with assertEqual(). This
* causes test failure.
*
* @param string Human-readable description of the reason for test failure.
* @return void
* @task assert
*/
final protected function assertFailure($message) {
$this->failTest($message);
throw new ArcanistPhutilTestTerminatedException($message);
}
/**
* End this test by asserting that the test should be skipped for some
* reason.
*
* @param string Reason for skipping this test.
* @return void
* @task assert
*/
final protected function assertSkipped($message) {
$this->skipTest($message);
throw new ArcanistPhutilTestSkippedException($message);
}
/* -( Exception Handling )------------------------------------------------- */
/**
* This simplest way to assert exceptions are thrown.
*
* @param exception The expected exception.
* @param callable The thing which throws the exception.
*
* @return void
* @task exceptions
*/
final protected function assertException($expected_exception_class,
$callable) {
$this->tryTestCases(
array('assertException' => array()),
array(false),
$callable,
$expected_exception_class);
}
/**
* Straightforward method for writing unit tests which check if some block of
* code throws an exception. For example, this allows you to test the
* exception behavior of ##is_a_fruit()## on various inputs:
*
* public function testFruit() {
* $this->tryTestCases(
* array(
* 'apple is a fruit' => new Apple(),
* 'rock is not a fruit' => new Rock(),
* ),
* array(
* true,
* false,
* ),
* array($this, 'tryIsAFruit'),
* 'NotAFruitException');
* }
*
* protected function tryIsAFruit($input) {
* is_a_fruit($input);
* }
*
* @param map Map of test case labels to test case inputs.
* @param list List of expected results, true to indicate that the case
* is expected to succeed and false to indicate that the case
* is expected to throw.
* @param callable Callback to invoke for each test case.
* @param string Optional exception class to catch, defaults to
* 'Exception'.
* @return void
* @task exceptions
*/
final protected function tryTestCases(
array $inputs,
array $expect,
$callable,
$exception_class = 'Exception') {
if (count($inputs) !== count($expect)) {
$this->assertFailure(
"Input and expectations must have the same number of values.");
}
$labels = array_keys($inputs);
$inputs = array_values($inputs);
$expecting = array_values($expect);
foreach ($inputs as $idx => $input) {
$expect = $expecting[$idx];
$label = $labels[$idx];
$caught = null;
try {
call_user_func($callable, $input);
} catch (Exception $ex) {
if ($ex instanceof ArcanistPhutilTestTerminatedException) {
throw $ex;
}
if (!($ex instanceof $exception_class)) {
throw $ex;
}
$caught = $ex;
}
$actual = !($caught instanceof Exception);
if ($expect === $actual) {
if ($expect) {
$message = "Test case '{$label}' did not throw, as expected.";
} else {
$message = "Test case '{$label}' threw, as expected.";
}
} else {
if ($expect) {
$message = "Test case '{$label}' was expected to succeed, but it ".
"raised an exception of class ".get_class($ex)." with ".
"message: ".$ex->getMessage();
} else {
$message = "Test case '{$label}' was expected to raise an ".
"exception, but it did not throw anything.";
}
}
$this->assertEqual($expect, $actual, $message);
}
}
/**
* Convenience wrapper around @{method:tryTestCases} for cases where your
* inputs are scalar. For example:
*
* public function testFruit() {
* $this->tryTestCaseMap(
* array(
* 'apple' => true,
* 'rock' => false,
* ),
* array($this, 'tryIsAFruit'),
* 'NotAFruitException');
* }
*
* protected function tryIsAFruit($input) {
* is_a_fruit($input);
* }
*
* For cases where your inputs are not scalar, use @{method:tryTestCases}.
*
* @param map Map of scalar test inputs to expected success (true
* expects success, false expects an exception).
* @param callable Callback to invoke for each test case.
* @param string Optional exception class to catch, defaults to
* 'Exception'.
* @return void
* @task exceptions
*/
final protected function tryTestCaseMap(
array $map,
$callable,
$exception_class = 'Exception') {
return $this->tryTestCases(
array_fuse(array_keys($map)),
array_values($map),
$callable,
$exception_class);
}
/* -( Hooks for Setup and Teardown )--------------------------------------- */
/**
* This hook is invoked once, before any tests in this class are run. It
* gives you an opportunity to perform setup steps for the entire class.
*
* @return void
* @task hook
*/
protected function willRunTests() {
return;
}
/**
* This hook is invoked once, after any tests in this class are run. It gives
* you an opportunity to perform teardown steps for the entire class.
*
* @return void
* @task hook
*/
protected function didRunTests() {
return;
}
/**
* This hook is invoked once per test, before the test method is invoked.
*
* @param string Method name of the test which will be invoked.
* @return void
* @task hook
*/
protected function willRunOneTest($test_method_name) {
return;
}
/**
* This hook is invoked once per test, after the test method is invoked.
*
* @param string Method name of the test which was invoked.
* @return void
* @task hook
*/
protected function didRunOneTest($test_method_name) {
return;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Construct a new test case. This method is ##final##, use willRunTests() to
* provide test-wide setup logic.
*
* @task internal
*/
final public function __construct() {
}
/**
* Mark the currently-running test as a failure.
*
* @param string Human-readable description of problems.
* @return void
*
* @task internal
*/
final private function failTest($reason) {
$this->resultTest(ArcanistUnitTestResult::RESULT_FAIL, $reason);
}
/**
* This was a triumph. I'm making a note here: HUGE SUCCESS.
*
* @param string Human-readable overstatement of satisfaction.
* @return void
*
* @task internal
*/
final private function passTest($reason) {
$this->resultTest(ArcanistUnitTestResult::RESULT_PASS, $reason);
}
/**
* Mark the current running test as skipped.
*
* @param string Description for why this test was skipped.
* @return void
* @task internal
*/
final private function skipTest($reason) {
$this->resultTest(ArcanistUnitTestResult::RESULT_SKIP, $reason);
}
final private function resultTest($test_result, $reason) {
$coverage = $this->endCoverage();
$result = new ArcanistUnitTestResult();
$result->setCoverage($coverage);
$result->setName($this->runningTest);
$result->setLink($this->getLink($this->runningTest));
$result->setResult($test_result);
$result->setDuration(microtime(true) - $this->testStartTime);
$result->setUserData($reason);
$this->results[] = $result;
+
+ if ($this->renderer) {
+ echo $this->renderer->renderUnitResult($result);
+ }
}
/**
* Execute the tests in this test case. You should not call this directly;
* use @{class:PhutilUnitTestEngine} to orchestrate test execution.
*
* @return void
* @task internal
*/
final public function run() {
$this->results = array();
$reflection = new ReflectionClass($this);
$methods = $reflection->getMethods();
// Try to ensure that poorly-written tests which depend on execution order
// (and are thus not properly isolated) will fail.
shuffle($methods);
$this->willRunTests();
foreach ($methods as $method) {
$name = $method->getName();
if (preg_match('/^test/', $name)) {
$this->runningTest = $name;
$this->assertions = 0;
$this->testStartTime = microtime(true);
try {
$this->willRunOneTest($name);
$this->beginCoverage();
$exceptions = array();
try {
call_user_func_array(
array($this, $name),
array());
$this->passTest(pht('%d assertion(s) passed.', $this->assertions));
} catch (Exception $ex) {
$exceptions['Execution'] = $ex;
}
try {
$this->didRunOneTest($name);
} catch (Exception $ex) {
$exceptions['Shutdown'] = $ex;
}
if ($exceptions) {
if (count($exceptions) == 1) {
throw head($exceptions);
} else {
throw new PhutilAggregateException(
"Multiple exceptions were raised during test execution.",
$exceptions);
}
}
} catch (ArcanistPhutilTestTerminatedException $ex) {
// Continue with the next test.
} catch (ArcanistPhutilTestSkippedException $ex) {
// Continue with the next test.
} catch (Exception $ex) {
$ex_class = get_class($ex);
$ex_message = $ex->getMessage();
$ex_trace = $ex->getTraceAsString();
$message = "EXCEPTION ({$ex_class}): {$ex_message}\n{$ex_trace}";
$this->failTest($message);
}
}
}
$this->didRunTests();
return $this->results;
}
final public function setEnableCoverage($enable_coverage) {
$this->enableCoverage = $enable_coverage;
return $this;
}
/**
* @phutil-external-symbol function xdebug_start_code_coverage
*/
final private function beginCoverage() {
if (!$this->enableCoverage) {
return;
}
$this->assertCoverageAvailable();
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
}
/**
* @phutil-external-symbol function xdebug_get_code_coverage
* @phutil-external-symbol function xdebug_stop_code_coverage
*/
final private function endCoverage() {
if (!$this->enableCoverage) {
return;
}
$result = xdebug_get_code_coverage();
xdebug_stop_code_coverage($cleanup = false);
$coverage = array();
foreach ($result as $file => $report) {
if (strncmp($file, $this->projectRoot, strlen($this->projectRoot))) {
continue;
}
$max = max(array_keys($report));
$str = '';
for ($ii = 1; $ii <= $max; $ii++) {
$c = idx($report, $ii);
if ($c === -1) {
$str .= 'U'; // Un-covered.
} else if ($c === -2) {
// TODO: This indicates "unreachable", but it flags the closing braces
// of functions which end in "return", which is super ridiculous. Just
// ignore it for now.
$str .= 'N'; // Not executable.
} else if ($c === 1) {
$str .= 'C'; // Covered.
} else {
$str .= 'N'; // Not executable.
}
}
$coverage[substr($file, strlen($this->projectRoot) + 1)] = $str;
}
// Only keep coverage information for files modified by the change.
$coverage = array_select_keys($coverage, $this->paths);
return $coverage;
}
final private function assertCoverageAvailable() {
if (!function_exists('xdebug_start_code_coverage')) {
throw new Exception(
"You've enabled code coverage but XDebug is not installed.");
}
}
final public function setProjectRoot($project_root) {
$this->projectRoot = $project_root;
return $this;
}
final public function setPaths(array $paths) {
$this->paths = $paths;
return $this;
}
protected function getLink($method) {
return null;
}
+ public function setRenderer(ArcanistUnitRenderer $renderer) {
+ $this->renderer = $renderer;
+ return $this;
+ }
+
}
diff --git a/src/unit/renderer/ArcanistUnitConsoleRenderer.php b/src/unit/renderer/ArcanistUnitConsoleRenderer.php
new file mode 100644
index 00000000..974a7979
--- /dev/null
+++ b/src/unit/renderer/ArcanistUnitConsoleRenderer.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @group unit
+ */
+final class ArcanistUnitConsoleRenderer extends ArcanistUnitRenderer {
+
+ public function renderUnitResult(ArcanistUnitTestResult $result) {
+ $result_code = $result->getResult();
+
+ $duration = '';
+ if ($result_code == ArcanistUnitTestResult::RESULT_PASS) {
+ $duration = ' '.$this->formatTestDuration($result->getDuration());
+ }
+ $return = sprintf(
+ " %s %s\n",
+ $this->getFormattedResult($result->getResult()).$duration,
+ $result->getName());
+
+ if ($result_code != ArcanistUnitTestResult::RESULT_PASS) {
+ $return .= $result->getUserData()."\n";
+ }
+
+ return $return;
+ }
+
+ public function renderPostponedResult($count) {
+ return sprintf(
+ "%s %s\n",
+ $this->getFormattedResult(ArcanistUnitTestResult::RESULT_POSTPONED),
+ pht('%d test(s)', $count));
+ }
+
+ private function getFormattedResult($result) {
+ static $status_codes = array(
+ ArcanistUnitTestResult::RESULT_PASS => '<bg:green>** PASS **</bg>',
+ ArcanistUnitTestResult::RESULT_FAIL => '<bg:red>** FAIL **</bg>',
+ ArcanistUnitTestResult::RESULT_SKIP => '<bg:yellow>** SKIP **</bg>',
+ ArcanistUnitTestResult::RESULT_BROKEN => '<bg:red>** BROKEN **</bg>',
+ ArcanistUnitTestResult::RESULT_UNSOUND => '<bg:yellow>** UNSOUND **</bg>',
+ ArcanistUnitTestResult::RESULT_POSTPONED =>
+ '<bg:yellow>** POSTPONED **</bg>',
+ );
+ return phutil_console_format($status_codes[$result]);
+ }
+
+ private function formatTestDuration($seconds) {
+ // Very carefully define inclusive upper bounds on acceptable unit test
+ // durations. Times are in milliseconds and are in increasing order.
+ $acceptableness = array(
+ 50 => "<fg:green>%s</fg><fg:yellow>\xE2\x98\x85</fg> ",
+ 200 => '<fg:green>%s</fg> ',
+ 500 => '<fg:yellow>%s</fg> ',
+ INF => '<fg:red>%s</fg> ',
+ );
+
+ $milliseconds = $seconds * 1000;
+ $duration = $this->formatTime($seconds);
+ foreach ($acceptableness as $upper_bound => $formatting) {
+ if ($milliseconds <= $upper_bound) {
+ return phutil_console_format($formatting, $duration);
+ }
+ }
+ return phutil_console_format(end($acceptableness), $duration);
+ }
+
+ private function formatTime($seconds) {
+ if ($seconds >= 60) {
+ $minutes = floor($seconds / 60);
+ return sprintf('%dm%02ds', $minutes, round($seconds % 60));
+ }
+
+ if ($seconds >= 1) {
+ return sprintf('%4.1fs', $seconds);
+ }
+
+ $milliseconds = $seconds * 1000;
+ if ($milliseconds >= 1) {
+ return sprintf('%3dms', round($milliseconds));
+ }
+
+ return ' <1ms';
+ }
+
+}
diff --git a/src/unit/renderer/ArcanistUnitRenderer.php b/src/unit/renderer/ArcanistUnitRenderer.php
new file mode 100644
index 00000000..c4fcd954
--- /dev/null
+++ b/src/unit/renderer/ArcanistUnitRenderer.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @group unit
+ */
+abstract class ArcanistUnitRenderer {
+
+ abstract public function renderUnitResult(ArcanistUnitTestResult $result);
+ abstract public function renderPostponedResult($count);
+
+}
diff --git a/src/workflow/ArcanistUnitWorkflow.php b/src/workflow/ArcanistUnitWorkflow.php
index 37c2e321..d5f7e7cc 100644
--- a/src/workflow/ArcanistUnitWorkflow.php
+++ b/src/workflow/ArcanistUnitWorkflow.php
@@ -1,367 +1,319 @@
<?php
/**
* Runs unit tests which cover your changes.
*
* @group workflow
*/
final class ArcanistUnitWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_UNSOUND = 1;
const RESULT_FAIL = 2;
const RESULT_SKIP = 3;
const RESULT_POSTPONED = 4;
private $unresolvedTests;
private $testResults;
private $engine;
public function getWorkflowName() {
return 'unit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**unit** [__options__] [__paths__]
**unit** [__options__] --rev [__rev__]
EOTEXT
);
}
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() {
return array(
'rev' => array(
'param' => 'revision',
'help' => "Run unit tests covering changes since a specific revision.",
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => "Arc unit does not currently support --rev in SVN.",
),
),
'engine' => array(
'param' => 'classname',
'help' =>
"Override configured unit engine for this project."
),
'coverage' => array(
'help' => 'Always enable coverage information.',
'conflicts' => array(
'no-coverage' => null,
),
),
'no-coverage' => array(
'help' => 'Always disable coverage information.',
),
'detailed-coverage' => array(
'help' => "Show a detailed coverage report on the CLI. Implies ".
"--coverage.",
),
'json' => array(
'help' => 'Report results in JSON format.',
),
'everything' => array(
'help' => 'Run every test.',
'conflicts' => array(
'rev' => '--everything runs all tests.',
),
),
'ugly' => array(
'help' => 'With --json, use uglier (but more efficient) formatting.',
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function getEngine() {
return $this->engine;
}
public function run() {
$working_copy = $this->getWorkingCopy();
$engine_class = $this->getArgument(
'engine',
$working_copy->getConfigFromAnySource('unit.engine'));
if (!$engine_class) {
throw new ArcanistNoEngineException(
"No unit test engine is configured for this project. Edit .arcconfig ".
"to specify a unit test engine.");
}
$paths = $this->getArgument('paths');
$rev = $this->getArgument('rev');
$everything = $this->getArgument('everything');
if ($everything && $paths) {
throw new ArcanistUsageException(
"You can not specify paths with --everything. The --everything ".
"flag runs every test.");
}
$paths = $this->selectPathsForWorkflow($paths, $rev);
if (!class_exists($engine_class) ||
!is_subclass_of($engine_class, 'ArcanistBaseUnitTestEngine')) {
throw new ArcanistUsageException(
"Configured unit test engine '{$engine_class}' is not a subclass of ".
"'ArcanistBaseUnitTestEngine'.");
}
$this->engine = newv($engine_class, array());
$this->engine->setWorkingCopy($working_copy);
if ($everything) {
$this->engine->setRunAllTests(true);
} else {
$this->engine->setPaths($paths);
}
$this->engine->setArguments($this->getPassthruArgumentsAsMap('unit'));
+ $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);
// Enable possible async tests only for 'arc diff' not 'arc unit'
if ($this->getParentWorkflow()) {
$this->engine->setEnableAsyncTests(true);
} else {
$this->engine->setEnableAsyncTests(false);
}
$results = $this->engine->run();
$this->testResults = $results;
$console = PhutilConsole::getConsole();
$json_output = $this->getArgument('json');
if ($json_output) {
$console->disableOut();
}
$unresolved = array();
$coverage = array();
$postponed_count = 0;
foreach ($results as $result) {
$result_code = $result->getResult();
if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED) {
$postponed_count++;
$unresolved[] = $result;
} else {
if ($this->engine->shouldEchoTestResults()) {
- $duration = '';
- if ($result_code == ArcanistUnitTestResult::RESULT_PASS) {
- $duration = ' '.self::formatTestDuration($result->getDuration());
- }
- $console->writeOut(
- " %s %s\n",
- $result->getConsoleFormattedResult().$duration,
- $result->getName());
+ $console->writeOut('%s', $renderer->renderUnitResult($result));
}
if ($result_code != ArcanistUnitTestResult::RESULT_PASS) {
- if ($this->engine->shouldEchoTestResults()) {
- $console->writeOut("%s\n", $result->getUserData());
- }
$unresolved[] = $result;
}
}
if ($result->getCoverage()) {
foreach ($result->getCoverage() as $file => $report) {
$coverage[$file][] = $report;
}
}
}
if ($postponed_count) {
- $postponed = id(new ArcanistUnitTestResult())
- ->setResult(ArcanistUnitTestResult::RESULT_POSTPONED);
$console->writeOut(
- "%s %s\n",
- $postponed->getConsoleFormattedResult(),
- pht('%d test(s)', $postponed_count));
+ '%s',
+ $renderer->renderPostponedResult($postponed_count));
}
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__COVERAGE REPORT__\n");
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)) {
$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;
} else if ($result_code == ArcanistUnitTestResult::RESULT_POSTPONED &&
$overall_result != self::RESULT_UNSOUND) {
$overall_result = self::RESULT_POSTPONED;
}
}
if ($json_output) {
$console->enableOut();
$data = array_values(mpull($results, 'toDictionary'));
if ($this->getArgument('ugly')) {
$console->writeOut('%s', json_encode($data));
} else {
$json = new PhutilJSON();
$console->writeOut('%s', $json->encodeFormatted($data));
}
}
return $overall_result;
}
public function getUnresolvedTests() {
return $this->unresolvedTests;
}
public function getTestResults() {
return $this->testResults;
}
- private static function formatTestDuration($seconds) {
- // Very carefully define inclusive upper bounds on acceptable unit test
- // durations. Times are in milliseconds and are in increasing order.
- $acceptableness = array(
- 50 => "<fg:green>%s</fg><fg:yellow>\xE2\x98\x85</fg> ",
- 200 => '<fg:green>%s</fg> ',
- 500 => '<fg:yellow>%s</fg> ',
- INF => '<fg:red>%s</fg> ',
- );
-
- $milliseconds = $seconds * 1000;
- $duration = self::formatTime($seconds);
- foreach ($acceptableness as $upper_bound => $formatting) {
- if ($milliseconds <= $upper_bound) {
- return phutil_console_format($formatting, $duration);
- }
- }
- return phutil_console_format(end($acceptableness), $duration);
- }
-
- private static function formatTime($seconds) {
- if ($seconds >= 60) {
- $minutes = floor($seconds / 60);
- return sprintf('%dm%02ds', $minutes, round($seconds % 60));
- }
-
- if ($seconds >= 1) {
- return sprintf('%4.1fs', $seconds);
- }
-
- $milliseconds = $seconds * 1000;
- if ($milliseconds >= 1) {
- return sprintf('%3dms', round($milliseconds));
- }
-
- return ' <1ms';
- }
-
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;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 22, 08:34 (39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
547654
Default Alt Text
(61 KB)

Event Timeline