-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRunner.php
More file actions
234 lines (196 loc) · 6.57 KB
/
Copy pathRunner.php
File metadata and controls
234 lines (196 loc) · 6.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/**
* Italix Testing - Runner
*
* @package Italix\Testing
*/
declare(strict_types=1);
namespace Italix\Testing;
/**
* Counters, output and an exit code. That is the whole runner.
*
* The existing Italix suites (`Rules/tests/ChecksTest.php`,
* `Encode/tests/HtmlTest.php`) each declare their own global `test()` function
* with identical bodies. This class is that function, written once, so the
* output format and the exit code stop drifting between libraries.
*
* There is no assertion DSL and no mocking. A test is a name and a boolean:
* whatever computes the boolean is ordinary PHP, which keeps the failure
* message the responsibility of the person who knows what the test meant.
*
* @example
* use function Italix\Testing\{suite, test, summary};
*
* suite('Italix Encode - Html');
* test('e() escapes angle brackets', (string) Html::e('<b>') === '<b>');
* exit(summary());
*/
final class Runner
{
private static int $passed_n = 0;
private static int $failed_n = 0;
/** @var string[] Names of failed tests, for the summary block */
private static array $failures = [];
/** @var string The suite title, for the machine-readable report */
private static string $suite_c = '';
/** @var string The section() heading currently in effect */
private static string $section_c = '';
/** @var array<int, array<string, mixed>> One entry per assertion */
private static array $cases = [];
private static float $suite_started_t = 0.0;
private static float $case_started_t = 0.0;
/** @var string|null An explicit JUnit destination, overriding the environment */
private static ?string $junit_c = null;
private function __construct()
{
}
/**
* Write a JUnit XML report as well as the human output.
*
* Normally set through the environment instead — `ITALIX_JUNIT=build/junit`
* — so that a parent runner starting thirty suites configures it once and
* no suite file has to know it is running under CI. Call this when a single
* suite wants its own destination regardless.
*
* Passing null goes back to reading the environment.
*
* @see Junit
*/
public static function junit_to(?string $path): void
{
self::$junit_c = $path;
}
/**
* Print a suite header and reset the counters.
*/
public static function suite(string $title): void
{
self::reset();
self::$suite_c = $title;
self::$suite_started_t = microtime(true);
self::$case_started_t = self::$suite_started_t;
// Before the first byte of output: CLI marks headers as sent on it, and
// a session cannot start afterwards. Doing it here means application
// middleware finds a session already active instead of warning into the
// middle of the results. See TestSession.
TestSession::ensure_started();
$rule = str_repeat('=', 59);
echo "{$rule}\n {$title}\n{$rule}\n\n";
}
/**
* Print a section heading inside a suite. Does not touch the counters.
*/
public static function section(string $title): void
{
self::$section_c = $title;
echo "--- {$title}\n";
}
/**
* Record one assertion.
*
* `$details` is printed only on failure, so it can afford to be expensive
* to read: the actual value, the offending input, the query that ran.
*/
public static function test(string $name, bool $condition, string $details = ''): void
{
$now_t = microtime(true);
self::$cases[] = [
'name' => $name,
'section' => self::$section_c,
'ok' => $condition,
'details' => $condition ? '' : $details,
'time' => $now_t - self::$case_started_t,
];
self::$case_started_t = $now_t;
if ($condition) {
echo "✓ PASS: {$name}\n";
self::$passed_n++;
return;
}
echo "✗ FAIL: {$name}\n";
if ($details !== '') {
echo " {$details}\n";
}
self::$failed_n++;
self::$failures[] = $name;
}
/**
* Print the totals and return the process exit code: 0 clean, 1 failures.
*/
public static function summary(): int
{
$total_n = self::$passed_n + self::$failed_n;
self::write_junit();
echo "\n" . str_repeat('=', 59) . "\n";
echo " {$total_n} assertions — " . self::$passed_n . ' passed, ' . self::$failed_n . " failed\n";
echo str_repeat('=', 59) . "\n";
if (self::$failed_n > 0) {
echo "\nFailed:\n";
foreach (self::$failures as $name) {
echo " - {$name}\n";
}
return 1;
}
return 0;
}
public static function reset(): void
{
self::$passed_n = 0;
self::$failed_n = 0;
self::$failures = [];
self::$cases = [];
self::$section_c = '';
}
/**
* The assertions as recorded, for a caller that wants to report them
* somewhere this class does not know about.
*
* @return array<int, array<string, mixed>>
*/
public static function cases(): array
{
return self::$cases;
}
/**
* Emit the machine-readable report, if anything asked for one.
*
* A suite with no assertions writes nothing: those are the ones that
* printed SKIPPED and exited, and an empty `<testsuite>` in the report
* reads as a suite that ran and found nothing to check.
*/
private static function write_junit(): void
{
if (self::$cases === []) {
return;
}
$path = Junit::destination(self::$suite_c, self::$junit_c);
if ($path === null) {
return;
}
$file_c = '';
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$entry = end($trace);
if (is_array($entry) && isset($entry['file'])) {
$file_c = (string) $entry['file'];
}
Junit::write($path, Junit::document(
self::$suite_c,
self::$cases,
microtime(true) - self::$suite_started_t,
$file_c
));
}
public static function passed_n(): int
{
return self::$passed_n;
}
public static function failed_n(): int
{
return self::$failed_n;
}
}