Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions core/Output/Terminal/Renderer/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
22 changes: 20 additions & 2 deletions core/Output/Terminal/Renderer/TerminalLogger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
));
}

/**
Expand Down Expand Up @@ -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));
Expand All @@ -353,14 +361,24 @@ 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));
$this->printMultipleRuns($result);
$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.
*/
Expand Down
5 changes: 5 additions & 0 deletions tests/Output/Stub/JUnit/SampleTestClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ final class SampleTestClass
public function passingTest(): void {}

public function failingTest(): void {}

/**
* Sample description line.
*/
public function describedTest(): void {}
}
63 changes: 63 additions & 0 deletions tests/Output/Unit/Terminal/TerminalLoggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<non-empty-string, TestResult> $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.
*
Expand All @@ -154,11 +212,15 @@ private static function run(array $results, Status $status, ?Summary $summary =
return new RunResult([$suite], $status, 0.0, $summary);
}

/**
* @param array<non-empty-string, mixed> $attributes
*/
private static function test(
string $method,
Status $status,
?\Throwable $failure = null,
?MessageLog $messages = null,
array $attributes = [],
): TestResult {
$info = new TestInfo(
name: $method,
Expand All @@ -176,6 +238,7 @@ private static function test(
info: $info,
status: $status,
failure: $failure,
attributes: $attributes,
messages: $messages ?? new MessageLog(),
);
}
Expand Down
Loading