diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 52e76d95..fdb205d5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,318 +1,322 @@ 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', 'ArcanistBackoutWorkflow' => 'workflow/ArcanistBackoutWorkflow.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', 'ArcanistCSSLintLinter' => 'lint/linter/ArcanistCSSLintLinter.php', 'ArcanistCSSLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCSSLintLinterTestCase.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', 'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.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', 'ArcanistExternalLinter' => 'lint/linter/ArcanistExternalLinter.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', 'ArcanistPEP8LinterTestCase' => 'lint/linter/__tests__/ArcanistPEP8LinterTestCase.php', 'ArcanistPHPCSLinterTestCase' => 'lint/linter/__tests__/ArcanistPHPCSLinterTestCase.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', 'ArcanistRevertWorkflow' => 'workflow/ArcanistRevertWorkflow.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', + 'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.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', 'PytestTestEngine' => 'unit/engine/PytestTestEngine.php', 'UnitTestableArcanistLintEngine' => 'lint/engine/UnitTestableArcanistLintEngine.php', + 'XUnitTestEngine' => 'unit/engine/XUnitTestEngine.php', ), 'function' => array( ), 'xmap' => array( 'ArcanistAliasWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistAnoidWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter', 'ArcanistArcanistLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistBackoutWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistBaseCommitParserTestCase' => 'ArcanistTestCase', 'ArcanistBaseWorkflow' => 'Phobject', 'ArcanistBaseXHPASTLinter' => 'ArcanistFutureLinter', 'ArcanistBookmarkWorkflow' => 'ArcanistFeatureWorkflow', 'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow', 'ArcanistBritishTestCase' => 'ArcanistTestCase', 'ArcanistBrowseWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistBundleTestCase' => 'ArcanistTestCase', 'ArcanistCSSLintLinter' => 'ArcanistExternalLinter', 'ArcanistCSSLintLinterTestCase' => 'ArcanistArcanistLinterTestCase', 'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCapabilityNotSupportedException' => 'Exception', 'ArcanistChooseInvalidRevisionException' => 'Exception', 'ArcanistChooseNoRevisionsException' => 'Exception', 'ArcanistCloseRevisionWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCloseWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCommentRemoverTestCase' => 'ArcanistTestCase', 'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistConduitLinter' => 'ArcanistLinter', 'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine', 'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCppcheckLinter' => 'ArcanistLinter', 'ArcanistCpplintLinter' => 'ArcanistLinter', 'ArcanistCpplintLinterTestCase' => 'ArcanistArcanistLinterTestCase', 'ArcanistDiffParserTestCase' => 'ArcanistTestCase', 'ArcanistDiffUtilsTestCase' => 'ArcanistTestCase', 'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistDifferentialCommitMessageParserException' => 'Exception', 'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistEventType' => 'PhutilEventType', 'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistExternalLinter' => 'ArcanistFutureLinter', 'ArcanistFeatureWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistFilenameLinter' => 'ArcanistLinter', 'ArcanistFlagWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistFlake8Linter' => 'ArcanistExternalLinter', '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' => 'ArcanistExternalLinter', 'ArcanistPEP8LinterTestCase' => 'ArcanistArcanistLinterTestCase', 'ArcanistPHPCSLinterTestCase' => 'ArcanistArcanistLinterTestCase', 'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPhpcsLinter' => 'ArcanistExternalLinter', 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', 'ArcanistPhutilTestCaseTestCase' => 'ArcanistPhutilTestCase', 'ArcanistPhutilTestSkippedException' => 'Exception', 'ArcanistPhutilTestTerminatedException' => 'Exception', 'ArcanistPhutilXHPASTLinter' => 'ArcanistBaseXHPASTLinter', 'ArcanistPhutilXHPASTLinterTestCase' => 'ArcanistArcanistLinterTestCase', 'ArcanistPyFlakesLinter' => 'ArcanistLinter', 'ArcanistPyLintLinter' => 'ArcanistLinter', 'ArcanistRepositoryAPIMiscTestCase' => 'ArcanistTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'ArcanistTestCase', 'ArcanistRevertWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistRubyLinter' => 'ArcanistExternalLinter', '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', + 'CSharpToolsTestEngine' => 'XUnitTestEngine', 'ComprehensiveLintEngine' => 'ArcanistLintEngine', 'ExampleLintEngine' => 'ArcanistLintEngine', 'GoTestResultParser' => 'ArcanistBaseTestResultParser', 'GoTestResultParserTestCase' => 'ArcanistTestCase', 'NoseTestEngine' => 'ArcanistBaseUnitTestEngine', 'PHPUnitTestEngineTestCase' => 'ArcanistTestCase', 'PhpunitResultParser' => 'ArcanistBaseTestResultParser', 'PhpunitTestEngine' => 'ArcanistBaseUnitTestEngine', 'PhutilLintEngine' => 'ArcanistLintEngine', 'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine', 'PhutilUnitTestEngineTestCase' => 'ArcanistTestCase', 'PytestTestEngine' => 'ArcanistBaseUnitTestEngine', 'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine', + 'XUnitTestEngine' => 'ArcanistBaseUnitTestEngine', ), )); diff --git a/src/unit/engine/CSharpToolsTestEngine.php b/src/unit/engine/CSharpToolsTestEngine.php new file mode 100644 index 00000000..95e6d12d --- /dev/null +++ b/src/unit/engine/CSharpToolsTestEngine.php @@ -0,0 +1,281 @@ +getWorkingCopy(); + $this->xunitHintPath = $working->getConfig('unit.csharp.xunit.binary'); + $this->cscoverHintPath = $working->getConfig('unit.csharp.cscover.binary'); + $this->matchRegex = $working->getConfig('unit.csharp.coverage.match'); + $this->excludedFiles = $working->getConfig('unit.csharp.coverage.excluded'); + + parent::loadEnvironment('unit.csharp.xunit.binary'); + + if ($this->getEnableCoverage() === false) { + return; + } + + // Determine coverage path. + if ($this->cscoverHintPath === null) { + throw new Exception( + "Unable to locate cscover. Configure it with ". + "the `unit.csharp.coverage.binary' option in .arcconfig"); + } + $cscover = $this->projectRoot."/".$this->cscoverHintPath; + if (file_exists($cscover)) { + $this->coverEngine = Filesystem::resolvePath($cscover); + } else { + throw new Exception( + "Unable to locate cscover coverage runner ". + "(have you built yet?)"); + } + + } + + /** + * Returns whether the specified assembly should be instrumented for + * code coverage reporting. Checks the excluded file list and the + * matching regex if they are configured. + * + * @return boolean Whether the assembly should be instrumented. + */ + private function assemblyShouldBeInstrumented($file) { + if ($this->excludedFiles !== null) { + if (array_key_exists((string)$file, $this->excludedFiles)) { + return false; + } + } + if ($this->matchRegex !== null) { + if (preg_match($this->matchRegex, $file) === 1) { + return true; + } else { + return false; + } + } + return true; + } + + /** + * Overridden version of `buildTestFuture` so that the unit test can be run + * via `cscover`, which instruments assemblies and reports on code coverage. + * + * @param string Name of the test assembly. + * @return array The future, output filename and coverage filename + * stored in an array. + */ + protected function buildTestFuture($test_assembly) { + if ($this->getEnableCoverage() === false) { + return parent::buildTestFuture($test_assembly); + } + + // FIXME: Can't use TempFile here as xUnit doesn't like + // UNIX-style full paths. It sees the leading / as the + // start of an option flag, even when quoted. + $xunit_temp = $test_assembly.".results.xml"; + if (file_exists($xunit_temp)) { + unlink($xunit_temp); + } + $cover_temp = new TempFile(); + $cover_temp->setPreserveFile(true); + $xunit_cmd = $this->runtimeEngine; + $xunit_args = null; + if ($xunit_cmd === "") { + $xunit_cmd = $this->testEngine; + $xunit_args = csprintf( + "%s /xml %s /silent", + $test_assembly.".dll", + $xunit_temp); + } else { + $xunit_args = csprintf( + "%s %s /xml %s /silent", + $this->testEngine, + $test_assembly.".dll", + $xunit_temp); + } + $assembly_dir = $test_assembly."/bin/Debug/"; + $assemblies_to_instrument = array(); + foreach (Filesystem::listDirectory($assembly_dir) as $file) { + if (substr($file, -4) == ".dll" || substr($file, -4) == ".exe") { + if ($this->assemblyShouldBeInstrumented($file)) { + $assemblies_to_instrument[] = $assembly_dir.$file; + } + } + } + $future = new ExecFuture( + "%C -o %s -c %s -a %s -w %s %Ls", + trim($this->runtimeEngine." ".$this->coverEngine), + $cover_temp, + $xunit_cmd, + $xunit_args, + $assembly_dir, + $assemblies_to_instrument); + $future->setCWD(Filesystem::resolvePath($this->projectRoot)); + return array( + $future, + $this->projectRoot."/".$assembly_dir.$xunit_temp, + $cover_temp); + } + + /** + * Returns coverage results for the unit tests. + * + * @param string The name of the coverage file if one was provided by + * `buildTestFuture`. + * @return array Code coverage results, or null. + */ + protected function parseCoverageResult($cover_file) { + if ($this->getEnableCoverage() === false) { + return parent::parseCoverageResult($cover_file); + } + return $this->readCoverage($cover_file); + } + + /** + * Retrieves the cached results for a coverage result file. The coverage + * result file is XML and can be large depending on what has been instrumented + * so we cache it in case it's requested again. + * + * @param string The name of the coverage file. + * @return array Code coverage results, or null if not cached. + */ + private function getCachedResultsIfPossible($cover_file) { + if ($this->cachedResults == null) { + $this->cachedResults = array(); + } + if (array_key_exists((string)$cover_file, $this->cachedResults)) { + return $this->cachedResults[(string)$cover_file]; + } + return null; + } + + /** + * Stores the code coverage results in the cache. + * + * @param string The name of the coverage file. + * @param array The results to cache. + */ + private function addCachedResults($cover_file, array $results) { + if ($this->cachedResults == null) { + $this->cachedResults = array(); + } + $this->cachedResults[(string)$cover_file] = $results; + } + + /** + * Processes a set of XML tags as code coverage results. We parse + * the `instrumented` and `executed` tags with this method so that + * we can access the data multiple times without a performance hit. + * + * @param array The array of XML tags to parse. + * @return array A PHP array containing the data. + */ + private function processTags($tags) { + $results = array(); + foreach ($tags as $tag) { + $results[] = array( + "file" => $tag->getAttribute("file"), + "start" => $tag->getAttribute("start"), + "end" => $tag->getAttribute("end")); + } + return $results; + } + + /** + * Reads the code coverage results from the cscover results file. + * + * @param string The path to the code coverage file. + * @return array The code coverage results. + */ + public function readCoverage($cover_file) { + $cached = $this->getCachedResultsIfPossible($cover_file); + if ($cached !== null) { + return $cached; + } + + $coverage_dom = new DOMDocument(); + $coverage_dom->loadXML(Filesystem::readFile($cover_file)); + + $modified = $this->getPaths(); + $files = array(); + $reports = array(); + $instrumented = array(); + $executed = array(); + + $instrumented = $this->processTags( + $coverage_dom->getElementsByTagName("instrumented")); + $executed = $this->processTags( + $coverage_dom->getElementsByTagName("executed")); + + foreach ($instrumented as $instrument) { + $absolute_file = $instrument["file"]; + $relative_file = substr($absolute_file, strlen($this->projectRoot) + 1); + if (!in_array($relative_file, $files)) { + $files[] = $relative_file; + } + } + + foreach ($files as $file) { + $absolute_file = $this->projectRoot."/".$file; + + // get total line count in file + $line_count = count(file($absolute_file)); + + $coverage = array(); + for ($i = 0; $i < $line_count; $i++) { + $coverage[$i] = 'N'; + } + + foreach ($instrumented as $instrument) { + if ($instrument["file"] !== $absolute_file) { + continue; + } + for ( + $i = $instrument["start"]; + $i <= $instrument["end"]; + $i++) { + $coverage[$i - 1] = 'U'; + } + } + + foreach ($executed as $execute) { + if ($execute["file"] !== $absolute_file) { + continue; + } + for ( + $i = $execute["start"]; + $i <= $execute["end"]; + $i++) { + $coverage[$i - 1] = 'C'; + } + } + + $reports[$file] = implode($coverage); + } + + $this->addCachedResults($cover_file, $reports); + return $reports; + } +} + + diff --git a/src/unit/engine/XUnitTestEngine.php b/src/unit/engine/XUnitTestEngine.php new file mode 100644 index 00000000..79887f5b --- /dev/null +++ b/src/unit/engine/XUnitTestEngine.php @@ -0,0 +1,451 @@ +projectRoot = $this->getWorkingCopy()->getProjectRoot(); + + // Determine build engine. + if (Filesystem::binaryExists("msbuild")) { + $this->buildEngine = "msbuild"; + } else if (Filesystem::binaryExists("xbuild")) { + $this->buildEngine = "xbuild"; + } else { + throw new Exception("Unable to find msbuild or xbuild in PATH!"); + } + + // Determine runtime engine (.NET or Mono). + if (phutil_is_windows()) { + $this->runtimeEngine = ""; + } else if (Filesystem::binaryExists("mono")) { + $this->runtimeEngine = Filesystem::resolveBinary("mono"); + } else { + throw new Exception("Unable to find Mono and you are not on Windows!"); + } + + // Determine xUnit test runner path. + if ($this->xunitHintPath === null) { + $this->xunitHintPath = $this->getWorkingCopy()->getConfig( + 'unit.xunit.binary'); + } + if ($this->xunitHintPath === null) { + } + $xunit = $this->projectRoot."/".$this->xunitHintPath; + if (file_exists($xunit)) { + $this->testEngine = Filesystem::resolvePath($xunit); + } else if (Filesystem::binaryExists("xunit.console.clr4.exe")) { + $this->testEngine = "xunit.console.clr4.exe"; + } else { + throw new Exception( + "Unable to locate xUnit console runner. Configure ". + "it with the `$config_item' option in .arcconfig"); + } + } + + /** + * Returns all available tests and related projects. Recurses into + * Protobuild submodules if they are present. + * + * @return array Mappings of test project to project being tested. + */ + public function getAllAvailableTestsAndRelatedProjects($path = null) { + if ($path == null) { + $path = $this->projectRoot; + } + $entries = Filesystem::listDirectory($path); + $mappings = array(); + foreach ($entries as $entry) { + if (substr($entry, -6) === ".Tests") { + if (is_dir($path."/".$entry)) { + $mappings[$path."/".$entry] = $path."/". + substr($entry, 0, strlen($entry) - 6); + } + } elseif (is_dir($path."/".$entry."/Build")) { + if (file_exists($path."/".$entry."/Build/Module.xml")) { + // The entry is a Protobuild submodule, which we should + // also recurse into. + $submappings = + $this->getAllAvailableTestsAndRelatedProjects($path."/".$entry); + foreach ($submappings as $key => $value) { + $mappings[$key] = $value; + } + } + } + } + return $mappings; + } + + /** + * Main entry point for the test engine. Determines what assemblies to + * build and test based on the files that have changed. + * + * @return array Array of test results. + */ + public function run() { + + $this->loadEnvironment(); + + $affected_tests = array(); + if ($this->getRunAllTests()) { + echo "Loading tests..."."\n"; + $entries = $this->getAllAvailableTestsAndRelatedProjects(); + foreach ($entries as $key => $value) { + echo "Test: ".substr($key, strlen($this->projectRoot) + 1)."\n"; + $affected_tests[] = substr($key, strlen($this->projectRoot) + 1); + } + } else { + $paths = $this->getPaths(); + + foreach ($paths as $path) { + if (substr($path, -4) == ".dll" || + substr($path, -4) == ".mdb") { + continue; + } + if (substr_count($path, "/") > 0) { + $components = explode("/", $path); + $affected_assembly = $components[0]; + + // If the change is made inside an assembly that has a `.Tests` + // extension, then the developer has changed the actual tests. + if (substr($affected_assembly, -6) === ".Tests") { + $affected_assembly_path = Filesystem::resolvePath( + $affected_assembly); + $test_assembly = $affected_assembly; + } else { + $affected_assembly_path = Filesystem::resolvePath( + $affected_assembly.".Tests"); + $test_assembly = $affected_assembly.".Tests"; + } + if (is_dir($affected_assembly_path) && + !in_array($test_assembly, $affected_tests)) { + $affected_tests[] = $test_assembly; + } + } + } + } + + return $this->runAllTests($affected_tests); + } + + /** + * Builds and runs the specified test assemblies. + * + * @param array Array of test assemblies. + * @return array Array of test results. + */ + public function runAllTests(array $test_assemblies) { + if (empty($test_assemblies)) { + return array(); + } + + $results = array(); + $results[] = $this->generateProjects(); + if ($this->resultsContainFailures($results)) { + return array_mergev($results); + } + $results[] = $this->buildProjects($test_assemblies); + if ($this->resultsContainFailures($results)) { + return array_mergev($results); + } + $results[] = $this->testAssemblies($test_assemblies); + + return array_mergev($results); + } + + /** + * Determine whether or not a current set of results contains any failures. + * This is needed since we build the assemblies as part of the unit tests, but + * we can't run any of the unit tests if the build fails. + * + * @param array Array of results to check. + * @return bool If there are any failures in the results. + */ + private function resultsContainFailures(array $results) { + $results = array_mergev($results); + foreach ($results as $result) { + if ($result->getResult() != ArcanistUnitTestResult::RESULT_PASS) { + return true; + } + } + return false; + } + + /** + * If the `Build` directory exists, we assume that this is a multi-platform + * project that requires generation of C# project files. Because we want to + * test that the generation and subsequent build is whole, we need to + * regenerate any projects in case the developer has added files through an + * IDE and then forgotten to add them to the respective `.definitions` file. + * By regenerating the projects we ensure that any missing definition entries + * will cause the build to fail. + * + * @return array Array of test results. + */ + private function generateProjects() { + + // No "Build" directory; so skip generation of projects. + if (!is_dir(Filesystem::resolvePath($this->projectRoot."/Build"))) { + return array(); + } + + // Work out what platform the user is building for already. + $platform = phutil_is_windows() ? "Windows" : "Linux"; + $files = Filesystem::listDirectory($this->projectRoot); + foreach ($files as $file) { + if (strtolower(substr($file, -4)) == ".sln") { + $parts = explode(".", $file); + $platform = $parts[count($parts) - 2]; + break; + } + } + + $regenerate_start = microtime(true); + $regenerate_future = new ExecFuture( + "%C Protobuild.exe --resync %s", + $this->runtimeEngine, + $platform); + $regenerate_future->setCWD(Filesystem::resolvePath( + $this->projectRoot)); + $results = array(); + $result = new ArcanistUnitTestResult(); + $result->setName("(regenerate projects for $platform)"); + + try { + $regenerate_future->resolvex(); + $result->setResult(ArcanistUnitTestResult::RESULT_PASS); + } catch(CommandException $exc) { + if ($exc->getError() > 1) { + throw $exc; + } + $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); + $result->setUserdata($exc->getStdout()); + } + + $result->setDuration(microtime(true) - $regenerate_start); + $results[] = $result; + return $results; + } + + /** + * Build the projects relevant for the specified test assemblies and return + * the results of the builds as test results. This build also passes the + * "SkipTestsOnBuild" parameter when building the projects, so that MSBuild + * conditionals can be used to prevent any tests running as part of the + * build itself (since the unit tester is about to run each of the tests + * individually). + * + * @param array Array of test assemblies. + * @return array Array of test results. + */ + private function buildProjects(array $test_assemblies) { + $build_futures = array(); + $build_failed = false; + $build_start = microtime(true); + $results = array(); + foreach ($test_assemblies as $test_assembly) { + $build_future = new ExecFuture( + "%C %s", + $this->buildEngine, + "/p:SkipTestsOnBuild=True"); + $build_future->setCWD(Filesystem::resolvePath( + $this->projectRoot."/".$test_assembly)); + $build_futures[$test_assembly] = $build_future; + } + $iterator = Futures($build_futures)->limit(1); + foreach ($iterator as $test_assembly => $future) { + $result = new ArcanistUnitTestResult(); + $result->setName("(build) ".$test_assembly); + + try { + $future->resolvex(); + $result->setResult(ArcanistUnitTestResult::RESULT_PASS); + } catch(CommandException $exc) { + if ($exc->getError() > 1) { + throw $exc; + } + $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); + $result->setUserdata($exc->getStdout()); + $build_failed = true; + } + + $result->setDuration(microtime(true) - $build_start); + $results[] = $result; + } + return $results; + } + + /** + * Build the future for running a unit test. This can be + * overridden to enable support for code coverage via + * another tool + * + * @param string Name of the test assembly. + * @return array The future, output filename and coverage filename + * stored in an array. + */ + protected function buildTestFuture($test_assembly) { + // FIXME: Can't use TempFile here as xUnit doesn't like + // UNIX-style full paths. It sees the leading / as the + // start of an option flag, even when quoted. + $xunit_temp = $test_assembly.".results.xml"; + if (file_exists($xunit_temp)) { + unlink($xunit_temp); + } + $future = new ExecFuture( + "%C %s /xml %s /silent", + trim($this->runtimeEngine." ".$this->testEngine), + $test_assembly."/bin/Debug/".$test_assembly.".dll", + $xunit_temp); + $future->setCWD(Filesystem::resolvePath($this->projectRoot)); + return array($future, $xunit_temp, null); + } + + /** + * Run the xUnit test runner on each of the assemblies and parse the + * resulting XML. + * + * @param array Array of test assemblies. + * @return array Array of test results. + */ + private function testAssemblies(array $test_assemblies) { + + $results = array(); + + // Build the futures for running the tests. + $futures = array(); + $outputs = array(); + $coverages = array(); + foreach ($test_assemblies as $test_assembly) { + list($future, $xunit_temp, $coverage) = + $this->buildTestFuture($test_assembly); + $futures[$test_assembly] = $future; + $outputs[$test_assembly] = $xunit_temp; + $coverages[$test_assembly] = $coverage; + } + + // Run all of the tests. + foreach (Futures($futures) as $test_assembly => $future) { + $future->resolve(); + + if (file_exists($outputs[$test_assembly])) { + $result = $this->parseTestResult( + $outputs[$test_assembly], + $coverages[$test_assembly]); + $results[] = $result; + unlink($outputs[$test_assembly]); + } else { + $result = new ArcanistUnitTestResult(); + $result->setName("(execute) ".$test_assembly); + $result->setResult(ArcanistUnitTestResult::RESULT_BROKEN); + $result->setUserData($outputs[$test_assembly]." not found on disk."); + $results[] = array($result); + } + } + + return array_mergev($results); + } + + /** + * Returns null for this implementation as xUnit does not support code + * coverage directly. Override this method in another class to provide + * code coverage information (also see `CSharpToolsUnitEngine`). + * + * @param string The name of the coverage file if one was provided by + * `buildTestFuture`. + * @return array Code coverage results, or null. + */ + protected function parseCoverageResult($coverage) { + return null; + } + + /** + * Parses the test results from xUnit. + * + * @param string The name of the xUnit results file. + * @param string The name of the coverage file if one was provided by + * `buildTestFuture`. This is passed through to + * `parseCoverageResult`. + * @return array Test results. + */ + private function parseTestResult($xunit_tmp, $coverage) { + $xunit_dom = new DOMDocument(); + $xunit_dom->loadXML(Filesystem::readFile($xunit_tmp)); + + $results = array(); + $tests = $xunit_dom->getElementsByTagName("test"); + foreach ($tests as $test) { + $name = $test->getAttribute("name"); + $time = $test->getAttribute("time"); + $status = ArcanistUnitTestResult::RESULT_UNSOUND; + switch ($test->getAttribute("result")) { + case "Pass": + $status = ArcanistUnitTestResult::RESULT_PASS; + break; + case "Fail": + $status = ArcanistUnitTestResult::RESULT_FAIL; + break; + case "Skip": + $status = ArcanistUnitTestResult::RESULT_SKIP; + break; + } + $userdata = ""; + $reason = $test->getElementsByTagName("reason"); + $failure = $test->getElementsByTagName("failure"); + if ($reason->length > 0 || $failure->length > 0) { + $node = ($reason->length > 0) ? $reason : $failure; + $message = $node->item(0)->getElementsByTagName("message"); + if ($message->length > 0) { + $userdata = $message->item(0)->nodeValue; + } + $stacktrace = $node->item(0)->getElementsByTagName("stack-trace"); + if ($stacktrace->length > 0) { + $userdata .= "\n".$stacktrace->item(0)->nodeValue; + } + } + + $result = new ArcanistUnitTestResult(); + $result->setName($name); + $result->setResult($status); + $result->setDuration($time); + $result->setUserData($userdata); + if ($coverage != null) { + $result->setCoverage($this->parseCoverageResult($coverage)); + } + $results[] = $result; + } + + return $results; + } + +}