diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -165,7 +165,7 @@ 'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase.php', 'ArcanistDefaultParametersXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDefaultParametersXHPASTLinterRule.php', 'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDefaultParametersXHPASTLinterRuleTestCase.php', - 'ArcanistDefaultUnitFormatter' => 'unit/formatter/ArcanistDefaultUnitFormatter.php', + 'ArcanistDefaultUnitSink' => 'unit/sink/ArcanistDefaultUnitSink.php', 'ArcanistDefaultsConfigurationSource' => 'config/source/ArcanistDefaultsConfigurationSource.php', 'ArcanistDeprecationXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeprecationXHPASTLinterRule.php', 'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeprecationXHPASTLinterRuleTestCase.php', @@ -279,7 +279,7 @@ 'ArcanistJSONLintRenderer' => 'lint/renderer/ArcanistJSONLintRenderer.php', 'ArcanistJSONLinter' => 'lint/linter/ArcanistJSONLinter.php', 'ArcanistJSONLinterTestCase' => 'lint/linter/__tests__/ArcanistJSONLinterTestCase.php', - 'ArcanistJSONUnitFormatter' => 'unit/formatter/ArcanistJSONUnitFormatter.php', + 'ArcanistJSONUnitSink' => 'unit/sink/ArcanistJSONUnitSink.php', 'ArcanistJscsLinter' => 'lint/linter/ArcanistJscsLinter.php', 'ArcanistJscsLinterTestCase' => 'lint/linter/__tests__/ArcanistJscsLinterTestCase.php', 'ArcanistKeywordCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistKeywordCasingXHPASTLinterRule.php', @@ -473,9 +473,9 @@ 'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase.php', 'ArcanistUnitConsoleRenderer' => 'unit/renderer/ArcanistUnitConsoleRenderer.php', 'ArcanistUnitEngine' => 'unit/engine/ArcanistUnitEngine.php', - 'ArcanistUnitFormatter' => 'unit/formatter/ArcanistUnitFormatter.php', 'ArcanistUnitOverseer' => 'unit/overseer/ArcanistUnitOverseer.php', 'ArcanistUnitRenderer' => 'unit/renderer/ArcanistUnitRenderer.php', + 'ArcanistUnitSink' => 'unit/sink/ArcanistUnitSink.php', 'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php', 'ArcanistUnitTestResultTestCase' => 'unit/__tests__/ArcanistUnitTestResultTestCase.php', 'ArcanistUnitTestableLintEngine' => 'lint/engine/ArcanistUnitTestableLintEngine.php', @@ -1276,7 +1276,7 @@ 'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistDefaultUnitFormatter' => 'ArcanistUnitFormatter', + 'ArcanistDefaultUnitSink' => 'ArcanistUnitSink', 'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1390,7 +1390,7 @@ 'ArcanistJSONLintRenderer' => 'ArcanistLintRenderer', 'ArcanistJSONLinter' => 'ArcanistLinter', 'ArcanistJSONLinterTestCase' => 'ArcanistLinterTestCase', - 'ArcanistJSONUnitFormatter' => 'ArcanistUnitFormatter', + 'ArcanistJSONUnitSink' => 'ArcanistUnitSink', 'ArcanistJscsLinter' => 'ArcanistExternalLinter', 'ArcanistJscsLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistKeywordCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1584,9 +1584,9 @@ 'ArcanistUnexpectedReturnValueXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUnitConsoleRenderer' => 'ArcanistUnitRenderer', 'ArcanistUnitEngine' => 'Phobject', - 'ArcanistUnitFormatter' => 'Phobject', 'ArcanistUnitOverseer' => 'Phobject', 'ArcanistUnitRenderer' => 'Phobject', + 'ArcanistUnitSink' => 'Phobject', 'ArcanistUnitTestResult' => 'Phobject', 'ArcanistUnitTestResultTestCase' => 'PhutilTestCase', 'ArcanistUnitTestableLintEngine' => 'ArcanistLintEngine', diff --git a/src/unit/ArcanistUnitTestResult.php b/src/unit/ArcanistUnitTestResult.php --- a/src/unit/ArcanistUnitTestResult.php +++ b/src/unit/ArcanistUnitTestResult.php @@ -175,13 +175,8 @@ } public static function getAllResultCodes() { - return array( - self::RESULT_PASS, - self::RESULT_FAIL, - self::RESULT_SKIP, - self::RESULT_BROKEN, - self::RESULT_UNSOUND, - ); + $map = self::getResultCodeSpecs(); + return array_keys($map); } public static function getResultCodeName($result_code) { @@ -205,31 +200,56 @@ return idx($specs, $result_code); } + public function getANSIColor() { + $spec = $this->getResultMap(); + return idx($spec, 'color.ansi', 'red'); + } + + public function getResultLabel() { + $spec = $this->getResultMap(); + return idx($spec, 'label', $this->getResult()); + } + + private function getResultMap() { + $map = self::getResultCodeSpecs(); + return idx($map, $this->getResult(), array()); + } + private static function getResultCodeSpecs() { return array( self::RESULT_PASS => array( 'name' => pht('Pass'), + 'label' => pht('PASS'), + 'color.ansi' => 'green', 'description' => pht( 'The test passed.'), ), self::RESULT_FAIL => array( 'name' => pht('Fail'), + 'label' => pht('FAIL'), + 'color.ansi' => 'red', 'description' => pht( 'The test failed.'), ), self::RESULT_SKIP => array( 'name' => pht('Skip'), + 'label' => pht('SKIP'), + 'color.ansi' => 'cyan', 'description' => pht( 'The test was not executed.'), ), self::RESULT_BROKEN => array( 'name' => pht('Broken'), + 'label' => pht('BROKEN'), + 'color.ansi' => 'red', 'description' => pht( 'The test failed in an abnormal or severe way. For example, the '. 'harness crashed instead of reporting a failure.'), ), self::RESULT_UNSOUND => array( 'name' => pht('Unsound'), + 'label' => pht('UNSOUND'), + 'color.ansi' => 'yellow', 'description' => pht( 'The test failed, but this change is probably not what broke it. '. 'For example, it might have already been failing.'), @@ -237,5 +257,15 @@ ); } + public function getDisplayName() { + $name = $this->getName(); + + $namespace = $this->getNamespace(); + if (strlen($namespace)) { + $name = $namespace.'::'.$name; + } + + return $name; + } } diff --git a/src/unit/engine/ArcanistUnitEngine.php b/src/unit/engine/ArcanistUnitEngine.php --- a/src/unit/engine/ArcanistUnitEngine.php +++ b/src/unit/engine/ArcanistUnitEngine.php @@ -56,14 +56,7 @@ abstract public function runTests(); final protected function didRunTests(array $tests) { - assert_instances_of($tests, 'ArcanistUnitTestResult'); - - // TOOLSETS: Pass this stuff to result output so it can print progress or - // stream results. - - foreach ($tests as $test) { - echo "Ran Test: ".$test->getNamespace().'::'.$test->getName()."\n"; - } + return $this->getOverseer()->didRunTests($tests); } } \ No newline at end of file diff --git a/src/unit/formatter/ArcanistDefaultUnitFormatter.php b/src/unit/formatter/ArcanistDefaultUnitFormatter.php deleted file mode 100644 --- a/src/unit/formatter/ArcanistDefaultUnitFormatter.php +++ /dev/null @@ -1,9 +0,0 @@ -getPhobjectClassConstant('FORMATTER_KEY'); - } - - public static function getAllUnitFormatters() { - return id(new PhutilClassMapQuery()) - ->setAncestorClass(__CLASS__) - ->setUniqueMethod('getUnitFormatterKey') - ->execute(); - } - -} diff --git a/src/unit/overseer/ArcanistUnitOverseer.php b/src/unit/overseer/ArcanistUnitOverseer.php --- a/src/unit/overseer/ArcanistUnitOverseer.php +++ b/src/unit/overseer/ArcanistUnitOverseer.php @@ -5,9 +5,9 @@ private $directory; private $paths = array(); - private $formatter; + private $sinks = array(); - public function setPaths($paths) { + public function setPaths(array $paths) { $this->paths = $paths; return $this; } @@ -16,13 +16,14 @@ return $this->paths; } - public function setFormatter(ArcanistUnitFormatter $formatter) { - $this->formatter = $formatter; + public function setSinks(array $sinks) { + assert_instances_of($sinks, 'ArcanistUnitSink'); + $this->sinks = $sinks; return $this; } - public function getFormatter() { - return $this->formatter; + public function getSinks() { + return $this->sinks; } public function setDirectory($directory) { @@ -50,9 +51,27 @@ } } + $this->didCompleteTests($results); + return $results; } + public function didRunTests(array $tests) { + assert_instances_of($tests, 'ArcanistUnitTestResult'); + + foreach ($this->getSinks() as $sink) { + $sink->sinkPartialResults($tests); + } + } + + private function didCompleteTests(array $tests) { + assert_instances_of($tests, 'ArcanistUnitTestResult'); + + foreach ($this->getSinks() as $sink) { + $sink->sinkFinalResults($tests); + } + } + private function loadEngines() { $root = $this->getDirectory(); diff --git a/src/unit/sink/ArcanistDefaultUnitSink.php b/src/unit/sink/ArcanistDefaultUnitSink.php new file mode 100644 --- /dev/null +++ b/src/unit/sink/ArcanistDefaultUnitSink.php @@ -0,0 +1,169 @@ +getResult(); + switch ($result_code) { + case ArcanistUnitTestResult::RESULT_PASS: + case ArcanistUnitTestResult::RESULT_SKIP: + break; + default: + $failed[] = $result; + break; + } + } + + $now = microtime(true); + + if (!$failed) { + if ($this->lastUpdateTime) { + $delay = 1; + if (($now - $this->lastUpdateTime) < $delay) { + $this->buffer[] = $results; + return; + } + } + } + + $this->buffer[] = $results; + $results = array_mergev($this->buffer); + $this->buffer = array(); + + $pass_count = 0; + $skip_count = 0; + $failed = array(); + foreach ($results as $result) { + $result_code = $result->getResult(); + switch ($result_code) { + case ArcanistUnitTestResult::RESULT_PASS: + $pass_count++; + break; + case ArcanistUnitTestResult::RESULT_SKIP: + $skip_count++; + break; + default: + $failed[] = $result; + break; + } + } + + if ($pass_count) { + echo tsprintf( + "%s\n", + pht('%s tests passed.', $pass_count)); + } + + if ($skip_count) { + echo tsprintf( + "%s\n", + pht('%s tests skipped.', $skip_count)); + } + + foreach ($failed as $result) { + echo $this->getDisplayForTest($result, false); + } + + $this->lastUpdateTime = $now; + + return $this; + } + + public function sinkFinalResults(array $results) { + + $passed = array(); + $skipped = array(); + $failed = array(); + foreach ($results as $result) { + $result_code = $result->getResult(); + switch ($result_code) { + case ArcanistUnitTestResult::RESULT_PASS: + $passed[] = $result; + break; + case ArcanistUnitTestResult::RESULT_SKIP: + $skipped[] = $result; + break; + default: + $failed[] = $result; + break; + } + } + + echo tsprintf( + "%s\n", + pht('RESULT SUMMARY')); + + + if ($skipped) { + echo tsprintf( + "%s\n", + pht('SKIPPED TESTS')); + + foreach ($skipped as $result) { + echo $this->getDisplayForTest($result); + } + } + + if ($failed) { + echo tsprintf( + "%s\n", + pht('FAILED TESTS')); + + foreach ($failed as $result) { + echo $this->getDisplayForTest($result); + } + } + + + echo tsprintf( + "** ~~~ %s **\n", + pht( + "%s PASSED * %s SKIPPED * %s FAILED/BROKEN/UNSTABLE", + phutil_count($passed), + phutil_count($skipped), + phutil_count($failed))); + } + + private function getDisplayForTest(ArcanistUnitTestResult $result) { + + $color = $result->getANSIColor(); + $status = $result->getResultLabel(); + $name = $result->getDisplayName(); + + // TOOLSETS: Restore timing information. + $timing = ' '; + + $output = tsprintf( + "** %s ** %s %s\n", + $status, + $timing, + $name); + + $user_data = $result->getUserData(); + if (strlen($user_data)) { + $output = tsprintf( + "%s%B\n", + $output, + $user_data); + } + + return $output; + } + +} diff --git a/src/unit/sink/ArcanistJSONUnitSink.php b/src/unit/sink/ArcanistJSONUnitSink.php new file mode 100644 --- /dev/null +++ b/src/unit/sink/ArcanistJSONUnitSink.php @@ -0,0 +1,9 @@ +getPhobjectClassConstant('SINKKEY'); + } + + public static function getAllUnitSinks() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getUnitSinkKey') + ->execute(); + } + + public function sinkPartialResults(array $results) { + return $this; + } + + public function sinkFinalResults(array $results) { + return $this; + } + + public function getOutput() { + return null; + } + +} diff --git a/src/workflow/ArcanistUnitWorkflow.php b/src/workflow/ArcanistUnitWorkflow.php --- a/src/workflow/ArcanistUnitWorkflow.php +++ b/src/workflow/ArcanistUnitWorkflow.php @@ -23,7 +23,7 @@ return array( $this->newWorkflowArgument('commit') ->setParameter('commit'), - $this->newWorkflowArgument('format') + $this->newWorkflowArgument('sink') ->setParameter('format'), $this->newWorkflowArgument('everything'), $this->newWorkflowArgument('paths') @@ -51,32 +51,40 @@ // though it is "arc unit --everything", and ignoring the "--commit" flag // and "paths" arguments. - $formatter = $this->newUnitFormatter(); - $overseer->setFormatter($formatter); + $sinks = array(); + $sinks[] = $this->newUnitSink(); + $overseer->setSinks($sinks); $overseer->execute(); + foreach ($sinks as $sink) { + $result = $sink->getOutput(); + if ($result !== null) { + echo $result; + } + } + return 0; } - private function newUnitFormatter() { - $formatters = ArcanistUnitFormatter::getAllUnitFormatters(); - $format_key = $this->getArgument('format'); - if (!strlen($format_key)) { - $format_key = ArcanistDefaultUnitFormatter::FORMATTER_KEY; + private function newUnitSink() { + $sinks = ArcanistUnitSink::getAllUnitSinks(); + $sink_key = $this->getArgument('sink'); + if (!strlen($sink_key)) { + $sink_key = ArcanistDefaultUnitSink::SINKKEY; } - $formatter = idx($formatters, $format_key); - if (!$formatter) { + $sink = idx($sinks, $sink_key); + if (!$sink) { throw new ArcanistUsageException( pht( - 'Unit test output format ("%s") is unknown. Supported formats '. + 'Unit test output sink ("%s") is unknown. Supported sinks '. 'are: %s.', - $format_key, - implode(', ', array_keys($formatters)))); + $sink_key, + implode(', ', array_keys($sinks)))); } - return $formatter; + return $sink; } }