Skip to content

Commit 932a341

Browse files
smuufdg
andauthored
Implemented console-lines mode which prints each test on separate line. (#443)
This is handy for environments with non-standard (buffered) handling of standard output, for example Github Actions (where a progress of tests cannot be seen until until end-of-line appears, which in standard `console` mode happens only when all tests finish) or Docker Compose logging output, where in standard `console` mode each finished test's dot is printed alone on separate line. Or the console-lines mode can be handy just to see a more detailed progress of tests in all environments, because it outputs something like this: ``` · 1/85 Framework/Assert.contains.phpt OK in 0.14 s · 2/85 CodeCoverage/PhpParser.parse.edge.phpt OK in 0.17 s · 3/85 CodeCoverage/PhpParser.parse.lines-of-code.phpt SKIPPED in 0.18 s · 4/85 CodeCoverage/PhpParser.parse.lines.phpt FAILED in 0.19 s ... ``` Also, "cider mode" now shows a lemon emoji for skipped tests. Co-authored-by: David Grudl <[email protected]>
1 parent 0d70858 commit 932a341

File tree

7 files changed

+225
-35
lines changed

7 files changed

+225
-35
lines changed

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ parameters:
44

55
paths:
66
- src
7+
typeAliases:
8+
Alias_TestResultState: 'Tester\Runner\Test::Passed|Tester\Runner\Test::Skipped|Tester\Runner\Test::Failed|Tester\Runner\Test::Prepared'

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ Options:
223223
-s Show information about skipped tests.
224224
--stop-on-fail Stop execution upon the first failure.
225225
-j <num> Run <num> jobs in parallel (default: 8).
226-
-o <console|tap|junit|none> Specify output format.
226+
-o <console|console-lines|tap|junit|none>
227+
Specify output format.
227228
-w | --watch <path> Watch directory.
228229
-i | --info Show tests environment info and exit.
229230
--setup <path> Script for runner setup.

src/Runner/CliTester.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use Tester\Dumper;
1414
use Tester\Environment;
1515
use Tester\Helpers;
16-
16+
use Tester\Runner\Output\ConsolePrinter;
1717

1818
/**
1919
* CLI Tester.
@@ -112,7 +112,7 @@ private function loadOptions(): CommandLine
112112
-s Show information about skipped tests.
113113
--stop-on-fail Stop execution upon the first failure.
114114
-j <num> Run <num> jobs in parallel (default: 8).
115-
-o <console|tap|junit|log|none> (e.g. -o junit:output.xml)
115+
-o <console|console-lines|tap|junit|log|none> (e.g. -o junit:output.xml)
116116
Specify one or more output formats with optional file name.
117117
-w | --watch <path> Watch directory.
118118
-i | --info Show tests environment info and exit.
@@ -219,18 +219,14 @@ private function createRunner(): Runner
219219
$runner->setTempDirectory($this->options['--temp']);
220220

221221
if ($this->stdoutFormat === null) {
222-
$runner->outputHandlers[] = new Output\ConsolePrinter(
223-
$runner,
224-
(bool) $this->options['-s'],
225-
'php://output',
226-
(bool) $this->options['--cider'],
227-
);
222+
$runner->outputHandlers[] = $this->buildConsolePrinter($runner, 'php://output', false);
228223
}
229224

230225
foreach ($this->options['-o'] as $output) {
231226
[$format, $file] = $output;
232227
match ($format) {
233-
'console' => $runner->outputHandlers[] = new Output\ConsolePrinter($runner, (bool) $this->options['-s'], $file, (bool) $this->options['--cider']),
228+
'console' => $runner->outputHandlers[] = $this->buildConsolePrinter($runner, $file, false),
229+
'console-lines' => $runner->outputHandlers[] = $this->buildConsolePrinter($runner, $file, true),
234230
'tap' => $runner->outputHandlers[] = new Output\TapPrinter($file),
235231
'junit' => $runner->outputHandlers[] = new Output\JUnitPrinter($file),
236232
'log' => $runner->outputHandlers[] = new Output\Logger($runner, $file),
@@ -248,6 +244,25 @@ private function createRunner(): Runner
248244
return $runner;
249245
}
250246

247+
/**
248+
* Builds and returns a new `ConsolePrinter`.
249+
* @param bool $lineMode If `true`, reports each finished test on separate line.
250+
*/
251+
private function buildConsolePrinter(
252+
Runner $runner,
253+
?string $file,
254+
bool $lineMode,
255+
): ConsolePrinter
256+
{
257+
return new Output\ConsolePrinter(
258+
$runner,
259+
(bool) $this->options['-s'],
260+
$file,
261+
(bool) $this->options['--cider'],
262+
$lineMode,
263+
);
264+
}
265+
251266

252267
private function prepareCodeCoverage(Runner $runner): string
253268
{

src/Runner/Output/ConsolePrinter.php

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,46 +20,66 @@
2020
*/
2121
class ConsolePrinter implements Tester\Runner\OutputHandler
2222
{
23-
private Runner $runner;
24-
2523
/** @var resource */
2624
private $file;
27-
private bool $displaySkipped = false;
28-
private string $buffer;
25+
26+
/** @var list<string> */
27+
private array $buffer;
28+
29+
/**
30+
* @phpstan-var array<Alias_TestResultState, string>
31+
* @var array<int, string>
32+
*/
33+
private array $symbols;
34+
35+
/**
36+
* @phpstan-var array<Alias_TestResultState, int>
37+
* @var array<int, string>
38+
*/
39+
private array $results = [
40+
Test::Passed => 0,
41+
Test::Skipped => 0,
42+
Test::Failed => 0,
43+
];
44+
2945
private float $time;
3046
private int $count;
31-
private array $results;
3247
private ?string $baseDir;
33-
private array $symbols;
34-
48+
private int $resultsCount = 0;
3549

50+
/**
51+
* @param bool $lineMode If `true`, reports each finished test on separate line.
52+
*/
3653
public function __construct(
37-
Runner $runner,
38-
bool $displaySkipped = false,
54+
private Runner $runner,
55+
private bool $displaySkipped = false,
3956
?string $file = null,
4057
bool $ciderMode = false,
58+
private bool $lineMode = false,
4159
) {
4260
$this->runner = $runner;
4361
$this->displaySkipped = $displaySkipped;
4462
$this->file = fopen($file ?: 'php://output', 'w');
63+
4564
$this->symbols = [
46-
Test::Passed => $ciderMode ? Dumper::color('green', '🍎') : '.',
47-
Test::Skipped => 's',
48-
Test::Failed => $ciderMode ? Dumper::color('red', '🍎') : Dumper::color('white/red', 'F'),
65+
Test::Passed => $this->lineMode ? Dumper::color('lime', 'OK') : '.',
66+
Test::Skipped => $this->lineMode ? Dumper::color('yellow', 'SKIP') : 's',
67+
Test::Failed => $this->lineMode ? Dumper::color('white/red', 'FAIL') : Dumper::color('white/red', 'F'),
4968
];
69+
70+
if ($ciderMode) {
71+
$this->symbols[Test::Passed] = '🍏';
72+
$this->symbols[Test::Skipped] = '🍋';
73+
$this->symbols[Test::Failed] = '🍎';
74+
}
5075
}
5176

5277

5378
public function begin(): void
5479
{
5580
$this->count = 0;
56-
$this->buffer = '';
81+
$this->buffer = [];
5782
$this->baseDir = null;
58-
$this->results = [
59-
Test::Passed => 0,
60-
Test::Skipped => 0,
61-
Test::Failed => 0,
62-
];
6383
$this->time = -microtime(true);
6484
fwrite($this->file, $this->runner->getInterpreter()->getShortInfo()
6585
. ' | ' . $this->runner->getInterpreter()->getCommandLine()
@@ -94,15 +114,17 @@ public function prepare(Test $test): void
94114
public function finish(Test $test): void
95115
{
96116
$this->results[$test->getResult()]++;
97-
fwrite($this->file, $this->symbols[$test->getResult()]);
117+
$this->lineMode
118+
? $this->printFinishedTestLine($test)
119+
: $this->printFinishedTestDot($test);
98120

99121
$title = ($test->title ? "$test->title | " : '') . substr($test->getSignature(), strlen($this->baseDir));
100122
$message = ' ' . str_replace("\n", "\n ", trim((string) $test->message)) . "\n\n";
101123
$message = preg_replace('/^ $/m', '', $message);
102124
if ($test->getResult() === Test::Failed) {
103-
$this->buffer .= Dumper::color('red', "-- FAILED: $title") . "\n$message";
125+
$this->buffer[] = Dumper::color('red', "-- FAILED: $title") . "\n$message";
104126
} elseif ($test->getResult() === Test::Skipped && $this->displaySkipped) {
105-
$this->buffer .= "-- Skipped: $title\n$message";
127+
$this->buffer[] = "-- Skipped: $title\n$message";
106128
}
107129
}
108130

@@ -111,14 +133,73 @@ public function end(): void
111133
{
112134
$run = array_sum($this->results);
113135
fwrite($this->file, !$this->count ? "No tests found\n" :
114-
"\n\n" . $this->buffer . "\n"
136+
"\n\n" . implode('', $this->buffer) . "\n"
115137
. ($this->results[Test::Failed] ? Dumper::color('white/red') . 'FAILURES!' : Dumper::color('white/green') . 'OK')
116138
. " ($this->count test" . ($this->count > 1 ? 's' : '') . ', '
117139
. ($this->results[Test::Failed] ? $this->results[Test::Failed] . ' failure' . ($this->results[Test::Failed] > 1 ? 's' : '') . ', ' : '')
118140
. ($this->results[Test::Skipped] ? $this->results[Test::Skipped] . ' skipped, ' : '')
119141
. ($this->count !== $run ? ($this->count - $run) . ' not run, ' : '')
120142
. sprintf('%0.1f', $this->time + microtime(true)) . ' seconds)' . Dumper::color() . "\n");
121143

122-
$this->buffer = '';
144+
$this->buffer = [];
145+
$this->resultsCount = 0;
146+
}
147+
148+
149+
private function printFinishedTestDot(Test $test): void
150+
{
151+
fwrite($this->file, $this->symbols[$test->getResult()]);
152+
}
153+
154+
155+
private function printFinishedTestLine(Test $test): void
156+
{
157+
$this->resultsCount++;
158+
$result = $test->getResult();
159+
160+
$shortFilePath = str_replace($this->baseDir, '', $test->getFile());
161+
$shortDirPath = dirname($shortFilePath) . DIRECTORY_SEPARATOR;
162+
$basename = basename($shortFilePath);
163+
164+
// Filename.
165+
if ($result === Test::Failed) {
166+
$fileText = Dumper::color('red', $shortDirPath) . Dumper::color('white/red', $basename);
167+
} else {
168+
$fileText = Dumper::color('gray', $shortDirPath) . Dumper::color('silver', $basename);
169+
}
170+
171+
// Line header.
172+
$header = "· ";
173+
// Test's title.
174+
$titleText = $test->title
175+
? Dumper::color('fuchsia', " [$test->title]")
176+
: '';
177+
178+
// Print test's message only if it's not failed (those will be printed
179+
// after all tests are finished and will contain detailed info about the
180+
// failed test).
181+
$message = '';
182+
if ($result !== Test::Failed && $test->message) {
183+
$message = $test->message;
184+
$indent = str_repeat(' ', mb_strlen($header));
185+
186+
if (preg_match('#\n#', $message)) {
187+
$message = "\n$indent" . preg_replace('#\r?\n#', '\0' . $indent, $message);
188+
} else {
189+
$message = Dumper::color('olive', "[$message]");
190+
}
191+
}
192+
193+
$output = sprintf(
194+
"%s%s %s %s %s %s\n",
195+
$header,
196+
"{$this->resultsCount}/{$this->count}",
197+
"$fileText{$titleText}",
198+
$this->symbols[$result],
199+
Dumper::color('gray', sprintf("in %.2f s", $test->getDuration())),
200+
$message,
201+
);
202+
203+
fwrite($this->file, $output);
123204
}
124205
}

src/Runner/Test.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,23 @@ class Test
2828
PASSED = self::Passed,
2929
SKIPPED = self::Skipped;
3030

31+
private const PossibleResults = [
32+
self::Prepared,
33+
self::Failed,
34+
self::Passed,
35+
self::Skipped,
36+
];
37+
3138
public ?string $title;
3239
public ?string $message = null;
3340
public string $stdout = '';
3441
public string $stderr = '';
3542
private string $file;
36-
private int $result = self::Prepared;
3743
private ?float $duration = null;
3844

45+
/** @phpstan-var Alias_TestResultState */
46+
private int $result = self::Prepared;
47+
3948
/** @var string[]|string[][] */
4049
private $args = [];
4150

@@ -70,6 +79,9 @@ public function getSignature(): string
7079
}
7180

7281

82+
/**
83+
* @phpstan-return Alias_TestResultState
84+
*/
7385
public function getResult(): int
7486
{
7587
return $this->result;
@@ -123,6 +135,7 @@ public function withArguments(array $args): self
123135

124136

125137
/**
138+
* @phpstan-param Alias_TestResultState $result
126139
* @return static
127140
*/
128141
public function withResult(int $result, ?string $message, ?float $duration = null): self
@@ -131,6 +144,10 @@ public function withResult(int $result, ?string $message, ?float $duration = nul
131144
throw new \LogicException("Result of test is already set to $this->result with message '$this->message'.");
132145
}
133146

147+
if (!in_array($result, self::PossibleResults, true)) {
148+
throw new \LogicException("Invalid test result $result");
149+
}
150+
134151
$me = clone $this;
135152
$me->result = $result;
136153
$me->message = $message;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
%a% | %a% | 1 thread
2+
3+
· 1/%d% ./01-basic.fail.phptx FAIL in %f% s
4+
· 2/%d% ./01-basic.pass.phptx OK in %f% s
5+
· 3/%d% ./01-basic.skip.phptx SKIP in %f% s
6+
· 4/%d% ./02-title.fail.phptx [Title for output handlers] FAIL in %f% s
7+
· 5/%d% ./02-title.pass.phptx [Title for output handlers] OK in %f% s
8+
· 6/%d% ./02-title.skip.phptx [Title for output handlers] SKIP in %f% s
9+
· 7/%d% ./03-message.fail.phptx FAIL in %f% s
10+
· 8/%d% ./03-message.skip.phptx SKIP in %f% s
11+
Multi
12+
line
13+
message.
14+
· 9/%d% ./04-args.fail.phptx FAIL in %f% s
15+
· 10/%d% ./04-args.pass.phptx OK in %f% s
16+
· 11/%d% ./04-args.skip.phptx SKIP in %f% s
17+
Multi
18+
line
19+
message.
20+
21+
22+
-- FAILED: 01-basic.fail.phptx
23+
Multi
24+
line
25+
stdout.Failed:
26+
27+
in %a%01-basic.fail.phptx(%d%) Tester\Assert::fail('');
28+
29+
STDERR:
30+
Multi
31+
line
32+
stderr.
33+
34+
-- FAILED: Title for output handlers | 02-title.fail.phptx
35+
Multi
36+
line
37+
stdout.Failed:
38+
39+
in %a%02-title.fail.phptx(%d%) Tester\Assert::fail('');
40+
41+
STDERR:
42+
Multi
43+
line
44+
stderr.
45+
46+
-- FAILED: 03-message.fail.phptx
47+
Multi
48+
line
49+
stdout.Failed: Multi
50+
line
51+
message.
52+
53+
in %a%03-message.fail.phptx(%d%) Tester\Assert::fail("Multi\nline\nmessage.");
54+
55+
STDERR:
56+
Multi
57+
line
58+
stderr.
59+
60+
-- FAILED: 04-args.fail.phptx dataprovider=thisIsAVeryVeryVeryLongArgumentNameToTestHowOutputHandlersDealWithItsLengthInTheirOutputFormatting|%a%provider.ini
61+
Multi
62+
line
63+
stdout.Failed:
64+
65+
in %a%04-args.fail.phptx(%d%) Tester\Assert::fail('');
66+
67+
STDERR:
68+
Multi
69+
line
70+
stderr.
71+
72+
73+
FAILURES! (11 tests, 4 failures, 4 skipped, %a% seconds)

0 commit comments

Comments
 (0)