From 6349ea5d62ea8296a579849d672b8f8baeb965f4 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 3 Jul 2026 14:33:43 +0400 Subject: [PATCH] feat(terminal): show a data provider's description once at the batch node A test's description belongs to the method, not to each data set. The terminal renderer copied it from every data set result, so a DataProvider test repeated the same description under each data set. Print it once under the batch node (the root of the data set tree) and suppress it on the individual data sets. Co-Authored-By: Claude Opus 4.8 (1M context) --- core/Output/Terminal/Renderer/Formatter.php | 30 ++++++--- .../Terminal/Renderer/TerminalLogger.php | 22 ++++++- tests/Output/Stub/JUnit/SampleTestClass.php | 5 ++ .../Unit/Terminal/TerminalLoggerTest.php | 63 +++++++++++++++++++ 4 files changed, 110 insertions(+), 10 deletions(-) diff --git a/core/Output/Terminal/Renderer/Formatter.php b/core/Output/Terminal/Renderer/Formatter.php index 38ff73ee..12a8389d 100644 --- a/core/Output/Terminal/Renderer/Formatter.php +++ b/core/Output/Terminal/Renderer/Formatter.php @@ -324,6 +324,27 @@ public static function comparisonBlock(ComparisonFailure $failure): string return \implode("\n", $lines); } + /** + * Formats a description block indented under an item at the given nesting level. Returns an empty + * string when there is no description. Used both for plain test runs and for the DataProvider + * batch node, so the description shows once at the root of the dataset tree instead of repeating + * under every dataset. + */ + public static function description(string $description, int $indentLevel, OutputFormat $format): string + { + if ($description === '' || $format === OutputFormat::Dots) { + return ''; + } + + $baseIndent = $format === OutputFormat::Verbose ? self::INDENT_VERBOSE : self::INDENT_COMPACT; + $descriptionPadding = $baseIndent . \str_repeat(self::INDENT_STEP, $indentLevel) . self::INDENT_STEP; + $descriptionStr = Style::dim( + \str_replace("\n", "\n{$descriptionPadding}", $description), + ); + + return "{$descriptionPadding}{$descriptionStr}\n"; + } + /** * Renders one row of a summary block: a dim, fixed-width label followed by its (already styled) * value. An empty label produces a continuation row aligned under the value column, so the parts @@ -369,14 +390,7 @@ private static function formatCompactRun(FormattedItem $item, OutputFormat $form : ''; $result = "{$indent}{$symbol} {$item->name}{$durationStr}\n"; - - if ($item->description !== '') { - $descriptionPadding = "{$indent} "; - $descriptionStr = Style::dim( - \str_replace("\n", "\n{$descriptionPadding}", $item->description), - ); - $result .= "{$descriptionPadding}{$descriptionStr}\n"; - } + $result .= self::description($item->description, $item->indentLevel, $format); return $result; } diff --git a/core/Output/Terminal/Renderer/TerminalLogger.php b/core/Output/Terminal/Renderer/TerminalLogger.php index ced42477..9718d4a4 100644 --- a/core/Output/Terminal/Renderer/TerminalLogger.php +++ b/core/Output/Terminal/Renderer/TerminalLogger.php @@ -123,6 +123,14 @@ public function batchStartedFromInfo(TestInfo $info): void $indent = $this->format === OutputFormat::Verbose ? ' ' : ' '; $symbol = Style::dim(Symbol::DataProvider->value); $this->write("{$indent}{$symbol} {$info->name}\n"); + + // The description belongs to the test, not to each dataset — print it once here, at the root + // of the dataset tree, so it is not repeated under every dataset (see handle*Test). + $this->write(Formatter::description( + (string) $info->testDefinition->getDescription(), + 0, + $this->format, + )); } /** @@ -326,7 +334,7 @@ private function handlePassedTest(TestResult $result, ?int $duration): void status: $result->status, duration: $duration, indentLevel: $this->currentIndentLevel, - description: (string) $result->getAttribute('description'), + description: $this->resultDescription($result), ); $this->write(Formatter::formatRun($item, $this->format)); @@ -353,7 +361,7 @@ private function handleFailedTest(TestResult $result, ?int $duration): void status: $result->status, duration: $duration, indentLevel: $this->currentIndentLevel, - description: (string) $result->getAttribute('description'), + description: $this->resultDescription($result), ); $this->write(Formatter::formatRun($item, $this->format)); @@ -361,6 +369,16 @@ private function handleFailedTest(TestResult $result, ?int $duration): void $this->currentTestName = null; } + /** + * The description to show under a test run. Inside a DataProvider batch it is already printed once + * at the batch node ({@see batchStartedFromInfo}), so it is suppressed here to avoid repeating the + * same description under every dataset. + */ + private function resultDescription(TestResult $result): string + { + return $this->currentIndentLevel > 0 ? '' : (string) $result->getAttribute('description'); + } + /** * Prints multiple test runs if available. */ diff --git a/tests/Output/Stub/JUnit/SampleTestClass.php b/tests/Output/Stub/JUnit/SampleTestClass.php index 0c993a87..6e89c913 100644 --- a/tests/Output/Stub/JUnit/SampleTestClass.php +++ b/tests/Output/Stub/JUnit/SampleTestClass.php @@ -12,4 +12,9 @@ final class SampleTestClass public function passingTest(): void {} public function failingTest(): void {} + + /** + * Sample description line. + */ + public function describedTest(): void {} } diff --git a/tests/Output/Unit/Terminal/TerminalLoggerTest.php b/tests/Output/Unit/Terminal/TerminalLoggerTest.php index 45e5c4e9..46740728 100644 --- a/tests/Output/Unit/Terminal/TerminalLoggerTest.php +++ b/tests/Output/Unit/Terminal/TerminalLoggerTest.php @@ -99,6 +99,35 @@ public function dataSetIsTreatedAsAnOrdinarySingleTest(): void Assert::string($output)->contains('single dataset output'); } + public function dataProviderDescriptionIsPrintedOnceAtTheBatchNode(): void + { + // A DataProvider test's description belongs to the method, not to each dataset — the datasets + // carry the same 'description' attribute the runner copies onto every result. It must appear + // once under the batch node and never repeat under the individual datasets. + $description = 'Sample description line.'; + $first = self::test('describedTest', Status::Passed, attributes: ['description' => $description]); + $second = self::test('describedTest', Status::Passed, attributes: ['description' => $description]); + + $output = self::renderBatch($first->info, [ + 'Dataset #0 [0]' => $first, + 'Dataset #1 [1]' => $second, + ]); + + Assert::same(\substr_count($output, $description), 1); + } + + public function regularTestStillPrintsItsDescription(): void + { + // A test without a DataProvider has no batch node, so its description must still print under + // the test itself. + $description = 'Sample description line.'; + $test = self::test('describedTest', Status::Passed, attributes: ['description' => $description]); + + $output = self::render(self::run([$test], Status::Passed), handled: [$test]); + + Assert::string($output)->contains($description); + } + protected function setUp(): void { // Strip ANSI styling so assertions match raw text regardless of TTY config. @@ -140,6 +169,35 @@ private static function render( return $output === false ? '' : $output; } + /** + * Drives the logger through a DataProvider batch the way {@see \Testo\Output\Terminal\TerminalPlugin} + * does — a batch node followed by each dataset keyed by its display name — and returns what was + * written. + * + * @param array $datasets Dataset display name => result, in order. + */ + private static function renderBatch(TestInfo $info, array $datasets): string + { + $stream = \fopen('php://memory', 'rb+'); + \assert($stream !== false); + + try { + $logger = new TerminalLogger(OutputFormat::Compact, Verbosity::Normal, $stream); + $logger->batchStartedFromInfo($info); + foreach ($datasets as $name => $result) { + $logger->testStartedFromInfo($info, $name); + $logger->handleTestResult($result, 0); + } + $logger->batchFinishedFromInfo($info); + \rewind($stream); + $output = \stream_get_contents($stream); + } finally { + \fclose($stream); + } + + return $output === false ? '' : $output; + } + /** * Wraps the given test results in a single suite/case. * @@ -154,11 +212,15 @@ private static function run(array $results, Status $status, ?Summary $summary = return new RunResult([$suite], $status, 0.0, $summary); } + /** + * @param array $attributes + */ private static function test( string $method, Status $status, ?\Throwable $failure = null, ?MessageLog $messages = null, + array $attributes = [], ): TestResult { $info = new TestInfo( name: $method, @@ -176,6 +238,7 @@ private static function test( info: $info, status: $status, failure: $failure, + attributes: $attributes, messages: $messages ?? new MessageLog(), ); }