1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Paraunit\Parser\JSON; |
6
|
|
|
|
7
|
|
|
use Paraunit\Configuration\EnvVariables; |
8
|
|
|
use PHPUnit\Framework\AssertionFailedError; |
9
|
|
|
use PHPUnit\Framework\SelfDescribing; |
10
|
|
|
use PHPUnit\Framework\Test; |
11
|
|
|
use PHPUnit\Framework\TestCase; |
12
|
|
|
use PHPUnit\Framework\TestFailure; |
13
|
|
|
use PHPUnit\Framework\TestListener; |
14
|
|
|
use PHPUnit\Framework\TestSuite; |
15
|
|
|
use PHPUnit\Framework\Warning; |
16
|
|
|
use PHPUnit\Runner\Version; |
17
|
|
|
use PHPUnit\Util; |
18
|
|
|
|
19
|
1 |
|
if (version_compare(Version::id(), '7.0.0', '<')) { |
20
|
|
|
class_alias('Paraunit\Parser\JSON\LogPrinterV6', 'Paraunit\Parser\JSON\LogPrinter'); |
21
|
|
|
// Using an early return instead of a else does not work when using the PHPUnit phar due to some weird PHP behavior |
22
|
|
|
// (the class gets defined without executing the code before it and so the definition is not properly conditional) |
23
|
|
|
} else { |
24
|
|
|
/** |
25
|
|
|
* This class comes from Util\Log_JSON. |
26
|
|
|
* It's copied and refactored here because it's deprecated in PHPUnit 5.7 and it will be dropped in PHPUnit 6 |
27
|
|
|
* |
28
|
|
|
* Class LogPrinter |
29
|
|
|
* @package Paraunit\Parser\JSON |
30
|
|
|
*/ |
31
|
|
|
class LogPrinter extends Util\Printer implements TestListener |
32
|
|
|
{ |
33
|
|
|
const STATUS_ERROR = 'error'; |
34
|
|
|
const STATUS_WARNING = 'warning'; |
35
|
|
|
const STATUS_FAIL = 'fail'; |
36
|
|
|
const STATUS_PASS = 'pass'; |
37
|
|
|
|
38
|
|
|
const MESSAGE_INCOMPLETE_TEST = 'Incomplete Test: '; |
39
|
|
|
const MESSAGE_RISKY_TEST = 'Risky Test: '; |
40
|
|
|
const MESSAGE_SKIPPED_TEST = 'Skipped Test: '; |
41
|
|
|
|
42
|
|
|
/** @var resource */ |
43
|
|
|
private $logFile; |
44
|
|
|
|
45
|
|
|
/** @var string */ |
46
|
|
|
private $currentTestSuiteName; |
47
|
|
|
|
48
|
|
|
/** @var string */ |
49
|
|
|
private $currentTestName; |
50
|
|
|
|
51
|
|
|
/** @var bool */ |
52
|
|
|
private $currentTestPass; |
53
|
|
|
|
54
|
9 |
|
public function __construct() |
55
|
|
|
{ |
56
|
9 |
|
$file = fopen($this->getLogFilename(), 'wt'); |
57
|
9 |
|
if (! \is_resource($file)) { |
58
|
|
|
throw new \RuntimeException('Unable to create log file'); |
59
|
|
|
} |
60
|
9 |
|
$this->logFile = $file; |
61
|
9 |
|
$this->autoFlush = true; |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* An error occurred. |
66
|
|
|
* |
67
|
|
|
* @param Test $test |
68
|
|
|
* @param \Throwable $exception |
69
|
|
|
* @param float $time |
70
|
|
|
*/ |
71
|
1 |
|
public function addError(Test $test, \Throwable $exception, float $time): void |
72
|
|
|
{ |
73
|
1 |
|
$this->writeCase( |
74
|
1 |
|
self::STATUS_ERROR, |
75
|
1 |
|
$time, |
76
|
1 |
|
$this->getStackTrace($exception), |
77
|
1 |
|
TestFailure::exceptionToString($exception), |
78
|
1 |
|
$test |
79
|
|
|
); |
80
|
|
|
|
81
|
1 |
|
$this->currentTestPass = false; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* A warning occurred. |
86
|
|
|
* |
87
|
|
|
* @param Test $test |
88
|
|
|
* @param Warning $warning |
89
|
|
|
* @param float $time |
90
|
|
|
*/ |
91
|
1 |
|
public function addWarning(Test $test, Warning $warning, float $time): void |
92
|
|
|
{ |
93
|
1 |
|
$this->writeCase( |
94
|
1 |
|
self::STATUS_WARNING, |
95
|
1 |
|
$time, |
96
|
1 |
|
$this->getStackTrace($warning), |
97
|
1 |
|
TestFailure::exceptionToString($warning), |
98
|
1 |
|
$test |
99
|
|
|
); |
100
|
|
|
|
101
|
1 |
|
$this->currentTestPass = false; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* A failure occurred. |
106
|
|
|
* |
107
|
|
|
* @param Test $test |
108
|
|
|
* @param AssertionFailedError $error |
109
|
|
|
* @param float $time |
110
|
|
|
*/ |
111
|
1 |
|
public function addFailure(Test $test, AssertionFailedError $error, float $time): void |
112
|
|
|
{ |
113
|
1 |
|
$this->writeCase( |
114
|
1 |
|
self::STATUS_FAIL, |
115
|
1 |
|
$time, |
116
|
1 |
|
$this->getStackTrace($error), |
117
|
1 |
|
TestFailure::exceptionToString($error), |
118
|
1 |
|
$test |
119
|
|
|
); |
120
|
|
|
|
121
|
1 |
|
$this->currentTestPass = false; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Incomplete test. |
126
|
|
|
* |
127
|
|
|
* @param Test $test |
128
|
|
|
* @param \Throwable $error |
129
|
|
|
* @param float $time |
130
|
|
|
*/ |
131
|
1 |
View Code Duplication |
public function addIncompleteTest(Test $test, \Throwable $error, float $time): void |
|
|
|
|
132
|
|
|
{ |
133
|
1 |
|
$this->writeCase( |
134
|
1 |
|
self::STATUS_ERROR, |
135
|
1 |
|
$time, |
136
|
1 |
|
$this->getStackTrace($error), |
137
|
1 |
|
self::MESSAGE_INCOMPLETE_TEST . $error->getMessage(), |
138
|
1 |
|
$test |
139
|
|
|
); |
140
|
|
|
|
141
|
1 |
|
$this->currentTestPass = false; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Risky test. |
146
|
|
|
* |
147
|
|
|
* @param Test $test |
148
|
|
|
* @param \Throwable $exception |
149
|
|
|
* @param float $time |
150
|
|
|
*/ |
151
|
1 |
View Code Duplication |
public function addRiskyTest(Test $test, \Throwable $exception, float $time): void |
|
|
|
|
152
|
|
|
{ |
153
|
1 |
|
$this->writeCase( |
154
|
1 |
|
self::STATUS_ERROR, |
155
|
1 |
|
$time, |
156
|
1 |
|
$this->getStackTrace($exception), |
157
|
1 |
|
self::MESSAGE_RISKY_TEST . $exception->getMessage(), |
158
|
1 |
|
$test |
159
|
|
|
); |
160
|
|
|
|
161
|
1 |
|
$this->currentTestPass = false; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Skipped test. |
166
|
|
|
* |
167
|
|
|
* @param Test $test |
168
|
|
|
* @param \Throwable $exception |
169
|
|
|
* @param float $time |
170
|
|
|
*/ |
171
|
1 |
View Code Duplication |
public function addSkippedTest(Test $test, \Throwable $exception, float $time): void |
|
|
|
|
172
|
|
|
{ |
173
|
1 |
|
$this->writeCase( |
174
|
1 |
|
self::STATUS_ERROR, |
175
|
1 |
|
$time, |
176
|
1 |
|
$this->getStackTrace($exception), |
177
|
1 |
|
self::MESSAGE_SKIPPED_TEST . $exception->getMessage(), |
178
|
1 |
|
$test |
179
|
|
|
); |
180
|
|
|
|
181
|
1 |
|
$this->currentTestPass = false; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* A testsuite started. |
186
|
|
|
* |
187
|
|
|
* @param TestSuite $suite |
188
|
|
|
* @throws \RuntimeException |
189
|
|
|
*/ |
190
|
9 |
View Code Duplication |
public function startTestSuite(TestSuite $suite): void |
|
|
|
|
191
|
|
|
{ |
192
|
9 |
|
$this->currentTestSuiteName = $suite->getName(); |
193
|
9 |
|
$this->currentTestName = ''; |
194
|
|
|
|
195
|
9 |
|
$this->writeArray([ |
196
|
9 |
|
'event' => 'suiteStart', |
197
|
9 |
|
'suite' => $this->currentTestSuiteName, |
198
|
9 |
|
'tests' => count($suite), |
199
|
|
|
]); |
200
|
|
|
} |
201
|
|
|
|
202
|
1 |
|
public function endTestSuite(TestSuite $suite): void |
203
|
|
|
{ |
204
|
1 |
|
$this->currentTestSuiteName = ''; |
205
|
1 |
|
$this->currentTestName = ''; |
206
|
|
|
} |
207
|
|
|
|
208
|
7 |
View Code Duplication |
public function startTest(Test $test): void |
|
|
|
|
209
|
|
|
{ |
210
|
7 |
|
$this->currentTestName = $test instanceof SelfDescribing ? $test->toString() : \get_class($test); |
211
|
7 |
|
$this->currentTestPass = true; |
212
|
|
|
|
213
|
7 |
|
$this->writeArray([ |
214
|
7 |
|
'event' => 'testStart', |
215
|
7 |
|
'suite' => $this->currentTestSuiteName, |
216
|
7 |
|
'test' => $this->currentTestName, |
217
|
|
|
]); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* A test ended. |
222
|
|
|
* |
223
|
|
|
* @param Test $test |
224
|
|
|
* @param float $time |
225
|
|
|
*/ |
226
|
1 |
|
public function endTest(Test $test, float $time): void |
227
|
|
|
{ |
228
|
1 |
|
if ($this->currentTestPass) { |
229
|
1 |
|
$this->writeCase(self::STATUS_PASS, $time, '', '', $test); |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @param string $status |
235
|
|
|
* @param float $time |
236
|
|
|
* @param string $trace |
237
|
|
|
* @param string $message |
238
|
|
|
* @param Test|TestCase|null $test |
239
|
|
|
*/ |
240
|
7 |
View Code Duplication |
private function writeCase(string $status, float $time, string $trace, $message = '', $test = null) |
|
|
|
|
241
|
|
|
{ |
242
|
7 |
|
$output = ''; |
243
|
7 |
|
if ($test instanceof TestCase) { |
244
|
|
|
$output = $test->getActualOutput(); |
245
|
|
|
} |
246
|
|
|
|
247
|
7 |
|
$this->writeArray([ |
248
|
7 |
|
'event' => 'test', |
249
|
7 |
|
'suite' => $this->currentTestSuiteName, |
250
|
7 |
|
'test' => $this->currentTestName, |
251
|
7 |
|
'status' => $status, |
252
|
7 |
|
'time' => $time, |
253
|
7 |
|
'trace' => $trace, |
254
|
7 |
|
'message' => $this->convertToUtf8($message), |
255
|
7 |
|
'output' => $output, |
256
|
|
|
]); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @param array $buffer |
261
|
|
|
*/ |
262
|
9 |
View Code Duplication |
private function writeArray($buffer) |
|
|
|
|
263
|
|
|
{ |
264
|
|
|
array_walk_recursive($buffer, function (&$input) { |
265
|
9 |
|
if (is_string($input)) { |
266
|
9 |
|
$input = $this->convertToUtf8($input); |
267
|
|
|
} |
268
|
9 |
|
}); |
269
|
|
|
|
270
|
9 |
|
$this->writeToLog(json_encode($buffer, JSON_PRETTY_PRINT)); |
271
|
|
|
} |
272
|
|
|
|
273
|
9 |
View Code Duplication |
private function writeToLog($buffer) |
|
|
|
|
274
|
|
|
{ |
275
|
|
|
// ignore everything that is not a JSON object |
276
|
9 |
|
if ($buffer != '' && $buffer[0] === '{') { |
277
|
9 |
|
\fwrite($this->logFile, $buffer); |
278
|
9 |
|
\fflush($this->logFile); |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* @return string |
284
|
|
|
* @throws \RuntimeException |
285
|
|
|
* @throws \InvalidArgumentException |
286
|
|
|
*/ |
287
|
9 |
View Code Duplication |
private function getLogFilename(): string |
|
|
|
|
288
|
|
|
{ |
289
|
9 |
|
$logDir = $this->getLogDirectory(); |
290
|
9 |
|
if (! @mkdir($logDir, 0777, true) && ! is_dir($logDir)) { |
291
|
|
|
throw new \RuntimeException('Cannot create folder for JSON logs'); |
292
|
|
|
} |
293
|
|
|
|
294
|
9 |
|
$logFilename = getenv(EnvVariables::PROCESS_UNIQUE_ID); |
295
|
9 |
|
if ($logFilename === false) { |
296
|
|
|
throw new \InvalidArgumentException('Log filename not received: environment variable not set'); |
297
|
|
|
} |
298
|
|
|
|
299
|
9 |
|
return $logDir . $logFilename . '.json.log'; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* @return string |
304
|
|
|
* @throws \InvalidArgumentException |
305
|
|
|
*/ |
306
|
9 |
View Code Duplication |
private function getLogDirectory(): string |
|
|
|
|
307
|
|
|
{ |
308
|
9 |
|
$logDirectory = getenv(EnvVariables::LOG_DIR); |
309
|
|
|
|
310
|
9 |
|
if ($logDirectory === false) { |
311
|
|
|
throw new \InvalidArgumentException('Log directory not received: environment variable not set'); |
312
|
|
|
} |
313
|
|
|
|
314
|
9 |
|
if (substr($logDirectory, -1) !== DIRECTORY_SEPARATOR) { |
315
|
|
|
$logDirectory .= DIRECTORY_SEPARATOR; |
316
|
|
|
} |
317
|
|
|
|
318
|
9 |
|
return $logDirectory; |
319
|
|
|
} |
320
|
|
|
|
321
|
9 |
|
private function convertToUtf8($string): string |
322
|
|
|
{ |
323
|
9 |
|
if (! \mb_detect_encoding($string, 'UTF-8', true)) { |
324
|
|
|
return \mb_convert_encoding($string, 'UTF-8'); |
325
|
|
|
} |
326
|
|
|
|
327
|
9 |
|
return $string; |
328
|
|
|
} |
329
|
|
|
|
330
|
6 |
|
private function getStackTrace($error): string |
331
|
|
|
{ |
332
|
6 |
|
return Util\Filter::getFilteredStacktrace($error); |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
} |
336
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.