1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace Terah\Assert; |
4
|
|
|
|
5
|
|
|
use Closure; |
6
|
|
|
|
7
|
|
|
class Tester |
8
|
|
|
{ |
9
|
|
|
const DEFAULT_SUITE = 'default'; |
10
|
|
|
|
11
|
|
|
/** @var string */ |
12
|
|
|
protected static $currentSuite = self::DEFAULT_SUITE; |
13
|
|
|
|
14
|
|
|
/** @var Suite[] */ |
15
|
|
|
protected static $suites = []; |
16
|
|
|
|
17
|
|
|
/** @var Logger $logger */ |
18
|
|
|
public static $logger = null; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @return bool |
22
|
|
|
*/ |
23
|
|
|
public static function init() : bool |
24
|
|
|
{ |
25
|
|
|
return true; |
26
|
|
|
} |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @param string $suiteName |
30
|
|
|
* @return Suite |
31
|
|
|
*/ |
32
|
|
|
public static function suite(string $suiteName='') : Suite |
33
|
|
|
{ |
34
|
|
|
$suiteName = $suiteName ?: static::$currentSuite; |
35
|
|
|
static::$suites[$suiteName] = new Suite(); |
36
|
|
|
|
37
|
|
|
return static::$suites[$suiteName]; |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @param string $testName |
42
|
|
|
* @param Closure $test |
43
|
|
|
* @param string $suiteName |
44
|
|
|
* @param string $successMessage |
45
|
|
|
* @param int|null $exceptionCode |
46
|
|
|
* @param string $exceptionClass |
47
|
|
|
* @return Suite |
48
|
|
|
* @throws AssertionFailedException |
49
|
|
|
*/ |
50
|
|
|
public static function test(string $testName, Closure $test, string $suiteName='', string $successMessage='', int $exceptionCode=0, string $exceptionClass='') : Suite |
51
|
|
|
{ |
52
|
|
|
Assert::that($successMessage)->notEmpty(); |
53
|
|
|
Assert::that($test)->isCallable(); |
54
|
|
|
Assert::that($suiteName)->notEmpty(); |
55
|
|
|
|
56
|
|
|
return static::suite($suiteName)->test($testName, $test, $successMessage, $exceptionCode, $exceptionClass); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @param string $suiteName |
61
|
|
|
* @param string $testName |
62
|
|
|
*/ |
63
|
|
|
public static function run(string $suiteName='', string $testName='') |
64
|
|
|
{ |
65
|
|
|
$suites = static::$suites; |
66
|
|
|
if ( ! empty($suiteName) ) |
67
|
|
|
{ |
68
|
|
|
Assert::that($suites)->keyExists($suiteName, "The test suite ({$suiteName}) has not been loaded"); |
69
|
|
|
$suites = [$suites[$suiteName]]; |
70
|
|
|
} |
71
|
|
|
foreach ( $suites as $suite ) |
72
|
|
|
{ |
73
|
|
|
$suite->run($testName); |
74
|
|
|
} |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @return Logger |
79
|
|
|
*/ |
80
|
|
|
public static function getLogger() : Logger |
81
|
|
|
{ |
82
|
|
|
if ( ! static::$logger ) |
83
|
|
|
{ |
84
|
|
|
static::$logger = new Logger(); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
return static::$logger; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* @param string $suiteName |
92
|
|
|
* @return Suite |
93
|
|
|
*/ |
94
|
|
|
protected static function getSuite(string $suiteName='') : Suite |
95
|
|
|
{ |
96
|
|
|
$suiteName = $suiteName ?: static::$currentSuite; |
97
|
|
|
if ( ! array_key_exists($suiteName, static::$suites) ) |
98
|
|
|
{ |
99
|
|
|
return static::suite($suiteName); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
return static::$suites[$suiteName]; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @param string $inputFile |
108
|
|
|
* @param string $outputPath |
109
|
|
|
* @return bool |
110
|
|
|
*/ |
111
|
|
|
public static function generateTest(string $inputFile, string $outputPath) : bool |
112
|
|
|
{ |
113
|
|
|
//Assert::that($inputFile)->classExists(); |
|
|
|
|
114
|
|
|
$declaredClasses = get_declared_classes(); |
115
|
|
|
require $inputFile; //one or more classes in file, contains class class1, class2, etc... |
116
|
|
|
|
117
|
|
|
$className = array_values(array_diff_key(get_declared_classes(), $declaredClasses)); |
118
|
|
|
|
119
|
|
|
$reflectionClass = new \ReflectionClass($className[0]); |
120
|
|
|
$publicMethods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); |
121
|
|
|
$fullClassName = $reflectionClass->getName(); |
|
|
|
|
122
|
|
|
$className = $reflectionClass->getShortName(); |
123
|
|
|
$namespace = $reflectionClass->getNamespaceName(); |
124
|
|
|
$constructorParams = ''; |
125
|
|
|
foreach ( $publicMethods as $method ) |
126
|
|
|
{ |
127
|
|
|
if ( $method->isConstructor() ) |
128
|
|
|
{ |
129
|
|
|
$constructorParams = static::getMethodParams($method); |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
$objectInit = "new {$fullClassName}({$constructorParams})"; |
133
|
|
|
$output = []; |
134
|
|
|
$output[] = <<<PHP |
135
|
|
|
<?php declare(strict_types=1); |
136
|
|
|
|
137
|
|
|
namespace {$namespace}\Test; |
138
|
|
|
|
139
|
|
|
use Terah\Assert\Assert; |
140
|
|
|
use Terah\Assert\Tester; |
141
|
|
|
use Terah\Assert\Suite; |
142
|
|
|
|
143
|
|
|
Tester::suite('AssertSuite') |
144
|
|
|
|
145
|
|
|
->fixture('testSubject', {$objectInit}) |
146
|
|
|
PHP; |
147
|
|
|
|
148
|
|
|
foreach ( $publicMethods as $method ) |
149
|
|
|
{ |
150
|
|
|
$methodName = $method->getName(); |
|
|
|
|
151
|
|
|
$methodParams = static::getMethodParams($method); |
152
|
|
|
$testName = 'test' . ucfirst($methodName); |
153
|
|
|
$successArgs = static::getMethodArgs($method); |
154
|
|
|
$failArgs = static::getMethodArgs($method, ' '); |
155
|
|
|
$returnVal = static::getReturnVal($method); |
156
|
|
|
$methodSignature = "\$suite->getFixture('testSubject')->{$methodName}({$methodParams})"; |
157
|
|
|
|
158
|
|
|
if ( $method->isStatic() ) |
159
|
|
|
{ |
160
|
|
|
$methodSignature = "{$className}::{$methodName}({$methodParams})"; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
$output[] = <<<PHP |
164
|
|
|
|
165
|
|
|
->test('{$testName}Success', function(Suite \$suite) { |
166
|
|
|
|
167
|
|
|
{$successArgs} |
168
|
|
|
\$actual = {$methodSignature}; |
169
|
|
|
\$expected = {$returnVal}; |
170
|
|
|
|
171
|
|
|
Assert::that(\$actual))->eq(\$expected, 'The method ({$methodName}) did not produce the correct output'); |
172
|
|
|
}) |
173
|
|
|
|
174
|
|
|
->test('{$testName}Failure', function(Suite \$suite) { |
175
|
|
|
|
176
|
|
|
{$failArgs} |
177
|
|
|
\$actual = {$methodSignature}; |
178
|
|
|
\$expected = {$returnVal}; |
179
|
|
|
|
180
|
|
|
Assert::that(\$actual))->eq(\$expected, 'The method ({$methodName}) did not produce the correct output'); |
181
|
|
|
|
182
|
|
|
}, '', Assert::INVALID_INTEGER, AssertionFailedException::class) |
183
|
|
|
PHP; |
184
|
|
|
|
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$output[] = " ;"; |
188
|
|
|
|
189
|
|
|
return static::createDirectoriesAndSaveFile($outputPath, implode("\n", $output)); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @param string $filePath |
195
|
|
|
* @param string $data |
196
|
|
|
* @param int $flags |
197
|
|
|
* @param int $dirMode |
198
|
|
|
* @return bool |
199
|
|
|
*/ |
200
|
|
|
protected static function createDirectoriesAndSaveFile(string $filePath, $data, $flags=0, $dirMode=0755) : bool |
201
|
|
|
{ |
202
|
|
|
static::createParentDirectories($filePath, $dirMode); |
203
|
|
|
Assert::that(file_put_contents($filePath, $data, $flags))->notFalse("Failed to put contents in file ({$filePath})"); |
204
|
|
|
|
205
|
|
|
return true; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* @param string $filePath |
210
|
|
|
* @param int $mode |
211
|
|
|
* @return bool |
212
|
|
|
*/ |
213
|
|
|
protected static function createParentDirectories(string $filePath, $mode=0755) : bool |
214
|
|
|
{ |
215
|
|
|
$directoryPath = preg_match('/.*\//', $filePath); |
216
|
|
|
Assert::that($filePath) |
217
|
|
|
->notEmpty("Failed to identify path ({$directoryPath}) to create") |
218
|
|
|
->notEq(DIRECTORY_SEPARATOR, "Failed to identify path ({$directoryPath}) to create"); |
219
|
|
|
if ( file_exists($directoryPath) ) |
220
|
|
|
{ |
221
|
|
|
Assert::that(is_dir($directoryPath))->notFalse("Failed to create parent directories.. files exists and is not a directory({$directoryPath})"); |
222
|
|
|
|
223
|
|
|
return true; |
224
|
|
|
} |
225
|
|
|
Assert::that(mkdir($directoryPath, $mode, true))->notFalse("Failed to create parent directories ({$directoryPath})"); |
226
|
|
|
Assert::that($directoryPath)->directory(); |
227
|
|
|
|
228
|
|
|
return true; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* @param \ReflectionMethod $method |
233
|
|
|
* @return string |
234
|
|
|
*/ |
235
|
|
|
protected static function getMethodParams(\ReflectionMethod $method) : string |
236
|
|
|
{ |
237
|
|
|
$output = []; |
238
|
|
|
foreach ( $method->getParameters() as $param ) |
239
|
|
|
{ |
240
|
|
|
$output[] = '$' . $param->getName(); |
|
|
|
|
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
return implode(', ', $output); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* @param \ReflectionMethod $method |
248
|
|
|
* @param string $extraPadding |
249
|
|
|
* @return string |
250
|
|
|
*/ |
251
|
|
|
protected static function getMethodArgs(\ReflectionMethod $method, string $extraPadding='') : string |
252
|
|
|
{ |
253
|
|
|
$output = []; |
254
|
|
|
$params = $method->getParameters(); |
255
|
|
|
foreach ( $params as $param ) |
256
|
|
|
{ |
257
|
|
|
$type = $param->hasType() ? $param->getType()->_toString() : ''; |
258
|
|
|
$paramDef = str_pad('$' . $param->getName(), 32, ' ') . '= '; |
|
|
|
|
259
|
|
|
$paramDef .= static::getDefaultValue($type); |
260
|
|
|
$output[] = $paramDef . ';'; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return implode("\n {$extraPadding}", $output); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param \ReflectionMethod $method |
268
|
|
|
* @return string |
269
|
|
|
*/ |
270
|
|
|
protected static function getReturnVal(\ReflectionMethod $method) : string |
271
|
|
|
{ |
272
|
|
|
|
273
|
|
|
$returnType = $method->hasReturnType() ? $method->getReturnType()->_toString() : ''; |
274
|
|
|
|
275
|
|
|
return static::getDefaultValue($returnType); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* @param string $type |
280
|
|
|
* @param string $default |
281
|
|
|
* @return string |
282
|
|
|
*/ |
283
|
|
|
protected static function getDefaultValue(string $type='', string $default='null') : string |
284
|
|
|
{ |
285
|
|
|
$typeMap = [ |
286
|
|
|
'int' => "0", |
287
|
|
|
'float' => "0.0", |
288
|
|
|
'string' => "''", |
289
|
|
|
'bool' => "false", |
290
|
|
|
'stdClass' => "new stdClass", |
291
|
|
|
'array' => "[]", |
292
|
|
|
]; |
293
|
|
|
|
294
|
|
|
return $typeMap[$type] ?? $default; |
295
|
|
|
} |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
|
299
|
|
|
class Suite |
|
|
|
|
300
|
|
|
{ |
301
|
|
|
/** @var Test[] */ |
302
|
|
|
protected $tests = []; |
303
|
|
|
|
304
|
|
|
/** @var mixed[] */ |
305
|
|
|
protected $fixtures = []; |
306
|
|
|
|
307
|
|
|
/** @var Logger */ |
308
|
|
|
protected $logger = null; |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* @param string $filter |
312
|
|
|
*/ |
313
|
|
|
public function run(string $filter='') |
314
|
|
|
{ |
315
|
|
|
foreach ( $this->tests as $test => $testCase ) |
316
|
|
|
{ |
317
|
|
|
$testName = $testCase->getTestName(); |
318
|
|
|
if ( $filter && $test !== $filter ) |
|
|
|
|
319
|
|
|
{ |
320
|
|
|
continue; |
321
|
|
|
} |
322
|
|
|
try |
323
|
|
|
{ |
324
|
|
|
$this->getLogger()->info("[{$testName}] - Starting..."); |
325
|
|
|
$testCase->runTest($this); |
326
|
|
|
$this->getLogger()->info("[{$testName}] - " . $testCase->getSuccessMessage()); |
327
|
|
|
} |
328
|
|
|
catch ( \Exception $e ) |
329
|
|
|
{ |
330
|
|
|
$expectedCode = $testCase->getExceptionCode(); |
331
|
|
|
$expectedClass = $testCase->getExceptionType(); |
332
|
|
|
$code = $e->getCode(); |
333
|
|
|
$exception = get_class($e); |
334
|
|
|
if ( ! $expectedClass && ! $expectedCode ) |
335
|
|
|
{ |
336
|
|
|
$this->getLogger()->error($e->getMessage(), [compact('testName'), $e]); |
337
|
|
|
|
338
|
|
|
continue; |
339
|
|
|
} |
340
|
|
|
if ( $expectedCode && $expectedCode !== $code ) |
341
|
|
|
{ |
342
|
|
|
$this->getLogger()->error("Exception code({$code}) was expected to be ({$expectedCode})", [compact('testName'), $e]); |
343
|
|
|
|
344
|
|
|
continue; |
345
|
|
|
} |
346
|
|
|
if ( $expectedClass && $expectedClass !== $exception ) |
347
|
|
|
{ |
348
|
|
|
$this->getLogger()->error("Exception class({$exception}) was expected to be ({$expectedClass})", [compact('testName'), $e]); |
349
|
|
|
|
350
|
|
|
continue; |
351
|
|
|
} |
352
|
|
|
$this->getLogger()->info("[{$test}] - " . $testCase->getSuccessMessage()); |
353
|
|
|
} |
354
|
|
|
} |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @param string $testName |
359
|
|
|
* @param Closure $test |
360
|
|
|
* @param string $successMessage |
361
|
|
|
* @param int|null $exceptionCode |
362
|
|
|
* @param string $exceptionClass |
363
|
|
|
* @return Suite |
364
|
|
|
* @throws AssertionFailedException |
365
|
|
|
*/ |
366
|
|
|
public function test(string $testName, Closure $test, string $successMessage='', int $exceptionCode=0, string $exceptionClass='') : Suite |
367
|
|
|
{ |
368
|
|
|
$this->tests[] = new Test($testName, $test, $successMessage, $exceptionCode, $exceptionClass); |
369
|
|
|
|
370
|
|
|
return $this; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* @param string $fixtureName |
375
|
|
|
* @param $value |
376
|
|
|
* @return Suite |
377
|
|
|
*/ |
378
|
|
|
public function fixture(string $fixtureName, $value) : Suite |
379
|
|
|
{ |
380
|
|
|
$this->fixtures[$fixtureName] = $value; |
381
|
|
|
|
382
|
|
|
return $this; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* @param string $fixtureName |
387
|
|
|
* @return mixed |
388
|
|
|
* @throws AssertionFailedException |
389
|
|
|
*/ |
390
|
|
|
public function getFixture(string $fixtureName) |
391
|
|
|
{ |
392
|
|
|
Assert::that($this->fixtures)->keyExists($fixtureName, "The fixture ({$fixtureName}) does not exist."); |
393
|
|
|
|
394
|
|
|
return $this->fixtures[$fixtureName]; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* @param Logger $logger |
400
|
|
|
* @return $this |
401
|
|
|
*/ |
402
|
|
|
public function setLogger(Logger $logger) : Suite |
403
|
|
|
{ |
404
|
|
|
$this->logger = $logger; |
405
|
|
|
|
406
|
|
|
return $this; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* @return Logger |
411
|
|
|
*/ |
412
|
|
|
public function getLogger() : Logger |
413
|
|
|
{ |
414
|
|
|
if ( ! $this->logger ) |
415
|
|
|
{ |
416
|
|
|
$this->logger = new Logger(); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
return $this->logger; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
class Test |
|
|
|
|
425
|
|
|
{ |
426
|
|
|
/** @var string */ |
427
|
|
|
public $testName = ''; |
428
|
|
|
|
429
|
|
|
/** @var string */ |
430
|
|
|
public $successMessage = ''; |
431
|
|
|
|
432
|
|
|
/** @var Closure */ |
433
|
|
|
public $test = null; |
434
|
|
|
|
435
|
|
|
/** @var string */ |
436
|
|
|
public $exceptionType = null; |
437
|
|
|
|
438
|
|
|
/** @var int */ |
439
|
|
|
public $exceptionCode = null; |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Test constructor. |
443
|
|
|
* |
444
|
|
|
* @param string $testName |
445
|
|
|
* @param Closure $test |
446
|
|
|
* @param string $successMessage |
447
|
|
|
* @param int|null $exceptionCode |
448
|
|
|
* @param string $exceptionClass |
449
|
|
|
* @throws AssertionFailedException |
450
|
|
|
*/ |
451
|
|
|
public function __construct(string $testName, Closure $test, string $successMessage='', int $exceptionCode=0, string $exceptionClass='') |
452
|
|
|
{ |
453
|
|
|
$this->setTestName($testName); |
454
|
|
|
$this->setTest($test); |
455
|
|
|
$this->setSuccessMessage($successMessage); |
456
|
|
|
$this->setExceptionCode($exceptionCode); |
457
|
|
|
$this->setExceptionType($exceptionClass); |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
/** |
461
|
|
|
* @return string |
462
|
|
|
*/ |
463
|
|
|
public function getTestName() : string |
464
|
|
|
{ |
465
|
|
|
return $this->testName; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
/** |
469
|
|
|
* @param string $testName |
470
|
|
|
* @return Test |
471
|
|
|
*/ |
472
|
|
|
public function setTestName(string $testName) : Test |
473
|
|
|
{ |
474
|
|
|
Assert::that($testName)->notEmpty(); |
475
|
|
|
|
476
|
|
|
$this->testName = $testName; |
477
|
|
|
|
478
|
|
|
return $this; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* @return string |
483
|
|
|
*/ |
484
|
|
|
public function getSuccessMessage() : string |
485
|
|
|
{ |
486
|
|
|
if ( ! $this->successMessage ) |
487
|
|
|
{ |
488
|
|
|
return "Successfully run {$this->testName}"; |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
return $this->successMessage; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
/** |
495
|
|
|
* @param string $successMessage |
496
|
|
|
* @return Test |
497
|
|
|
* @throws AssertionFailedException |
498
|
|
|
*/ |
499
|
|
|
public function setSuccessMessage(string $successMessage) : Test |
500
|
|
|
{ |
501
|
|
|
$this->successMessage = $successMessage; |
502
|
|
|
|
503
|
|
|
return $this; |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
/** |
507
|
|
|
* @return Closure |
508
|
|
|
*/ |
509
|
|
|
public function getTest() : Closure |
510
|
|
|
{ |
511
|
|
|
return $this->test; |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
/** |
515
|
|
|
* @param Closure $test |
516
|
|
|
* @return Test |
517
|
|
|
*/ |
518
|
|
|
public function setTest(Closure $test) : Test |
519
|
|
|
{ |
520
|
|
|
$this->test = $test; |
521
|
|
|
|
522
|
|
|
return $this; |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
/** |
526
|
|
|
* @return string |
527
|
|
|
*/ |
528
|
|
|
public function getExceptionType() : string |
529
|
|
|
{ |
530
|
|
|
return $this->exceptionType; |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
/** |
534
|
|
|
* @param string $exceptionType |
535
|
|
|
* @return Test |
536
|
|
|
*/ |
537
|
|
|
public function setExceptionType(string $exceptionType) : Test |
538
|
|
|
{ |
539
|
|
|
$this->exceptionType = $exceptionType; |
540
|
|
|
|
541
|
|
|
return $this; |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
/** |
545
|
|
|
* @return int |
546
|
|
|
*/ |
547
|
|
|
public function getExceptionCode() : int |
548
|
|
|
{ |
549
|
|
|
return $this->exceptionCode; |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
/** |
553
|
|
|
* @param int $exceptionCode |
554
|
|
|
* @return Test |
555
|
|
|
*/ |
556
|
|
|
public function setExceptionCode(int $exceptionCode) : Test |
557
|
|
|
{ |
558
|
|
|
$this->exceptionCode = $exceptionCode; |
559
|
|
|
|
560
|
|
|
return $this; |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
/** |
564
|
|
|
* @param Suite $suite |
565
|
|
|
* @return mixed |
566
|
|
|
*/ |
567
|
|
|
public function runTest(Suite $suite) |
568
|
|
|
{ |
569
|
|
|
return $this->getTest()->__invoke($suite); |
570
|
|
|
} |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
/** |
574
|
|
|
* Class Logger |
575
|
|
|
* |
576
|
|
|
* @package Terah\Assert |
577
|
|
|
*/ |
578
|
|
|
class Logger |
|
|
|
|
579
|
|
|
{ |
580
|
|
|
const EMERGENCY = 'emergency'; |
581
|
|
|
const ALERT = 'alert'; |
582
|
|
|
const CRITICAL = 'critical'; |
583
|
|
|
const ERROR = 'error'; |
584
|
|
|
const WARNING = 'warning'; |
585
|
|
|
const NOTICE = 'notice'; |
586
|
|
|
const INFO = 'info'; |
587
|
|
|
const DEBUG = 'debug'; |
588
|
|
|
|
589
|
|
|
const BLACK = 'black'; |
590
|
|
|
const DARK_GRAY = 'dark_gray'; |
591
|
|
|
const BLUE = 'blue'; |
592
|
|
|
const LIGHT_BLUE = 'light_blue'; |
593
|
|
|
const GREEN = 'green'; |
594
|
|
|
const LIGHT_GREEN = 'light_green'; |
595
|
|
|
const CYAN = 'cyan'; |
596
|
|
|
const LIGHT_CYAN = 'light_cyan'; |
597
|
|
|
const RED = 'red'; |
598
|
|
|
const LIGHT_RED = 'light_red'; |
599
|
|
|
const PURPLE = 'purple'; |
600
|
|
|
const LIGHT_PURPLE = 'light_purple'; |
601
|
|
|
const BROWN = 'brown'; |
602
|
|
|
const YELLOW = 'yellow'; |
603
|
|
|
const MAGENTA = 'magenta'; |
604
|
|
|
const LIGHT_GRAY = 'light_gray'; |
605
|
|
|
const WHITE = 'white'; |
606
|
|
|
const DEFAULT = 'default'; |
607
|
|
|
const BOLD = 'bold'; |
608
|
|
|
|
609
|
|
|
/** @var resource $resource The file handle */ |
610
|
|
|
protected $resource = null; |
611
|
|
|
|
612
|
|
|
/** @var string $level */ |
613
|
|
|
protected $level = self::INFO; |
614
|
|
|
|
615
|
|
|
/** @var bool $closeLocally */ |
616
|
|
|
protected $closeLocally = false; |
617
|
|
|
|
618
|
|
|
/** @var bool */ |
619
|
|
|
protected $addDate = true; |
620
|
|
|
|
621
|
|
|
/** @var string */ |
622
|
|
|
protected $separator = ' | '; |
623
|
|
|
|
624
|
|
|
/** @var \Closure */ |
625
|
|
|
protected $formatter = null; |
626
|
|
|
|
627
|
|
|
/** @var string */ |
628
|
|
|
protected $lastLogEntry = ''; |
629
|
|
|
|
630
|
|
|
/** @var bool|null */ |
631
|
|
|
protected $gzipFile = null; |
632
|
|
|
|
633
|
|
|
/** @var bool */ |
634
|
|
|
protected $useLocking = false; |
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* @var array $logLevels List of supported levels |
638
|
|
|
*/ |
639
|
|
|
static protected $logLevels = [ |
640
|
|
|
self::EMERGENCY => [1, self::WHITE, self::RED, self::DEFAULT, 'EMERG'], |
641
|
|
|
self::ALERT => [2, self::WHITE, self::YELLOW, self::DEFAULT, 'ALERT'], |
642
|
|
|
self::CRITICAL => [3, self::RED, self::DEFAULT, self::BOLD , 'CRIT'], |
643
|
|
|
self::ERROR => [4, self::RED, self::DEFAULT, self::DEFAULT, 'ERROR'], |
644
|
|
|
self::WARNING => [5, self::YELLOW, self::DEFAULT, self::DEFAULT, 'WARN'], |
645
|
|
|
self::NOTICE => [6, self::CYAN, self::DEFAULT, self::DEFAULT, 'NOTE'], |
646
|
|
|
self::INFO => [7, self::GREEN, self::DEFAULT, self::DEFAULT, 'INFO'], |
647
|
|
|
self::DEBUG => [8, self::LIGHT_GRAY, self::DEFAULT, self::DEFAULT, 'DEBUG'], |
648
|
|
|
]; |
649
|
|
|
|
650
|
|
|
/** |
651
|
|
|
* @var array |
652
|
|
|
*/ |
653
|
|
|
static protected $colours = [ |
654
|
|
|
'fore' => [ |
655
|
|
|
self::BLACK => '0;30', |
656
|
|
|
self::DARK_GRAY => '1;30', |
657
|
|
|
self::BLUE => '0;34', |
658
|
|
|
self::LIGHT_BLUE => '1;34', |
659
|
|
|
self::GREEN => '0;32', |
660
|
|
|
self::LIGHT_GREEN => '1;32', |
661
|
|
|
self::CYAN => '0;36', |
662
|
|
|
self::LIGHT_CYAN => '1;36', |
663
|
|
|
self::RED => '0;31', |
664
|
|
|
self::LIGHT_RED => '1;31', |
665
|
|
|
self::PURPLE => '0;35', |
666
|
|
|
self::LIGHT_PURPLE => '1;35', |
667
|
|
|
self::BROWN => '0;33', |
668
|
|
|
self::YELLOW => '1;33', |
669
|
|
|
self::MAGENTA => '0;35', |
670
|
|
|
self::LIGHT_GRAY => '0;37', |
671
|
|
|
self::WHITE => '1;37', |
672
|
|
|
], |
673
|
|
|
'back' => [ |
674
|
|
|
self::DEFAULT => '49', |
675
|
|
|
self::BLACK => '40', |
676
|
|
|
self::RED => '41', |
677
|
|
|
self::GREEN => '42', |
678
|
|
|
self::YELLOW => '43', |
679
|
|
|
self::BLUE => '44', |
680
|
|
|
self::MAGENTA => '45', |
681
|
|
|
self::CYAN => '46', |
682
|
|
|
self::LIGHT_GRAY => '47', |
683
|
|
|
], |
684
|
|
|
self::BOLD => [], |
685
|
|
|
]; |
686
|
|
|
|
687
|
|
|
/** |
688
|
|
|
* @param mixed $resource |
689
|
|
|
* @param string $level |
690
|
|
|
* @param bool $useLocking |
691
|
|
|
* @param bool $gzipFile |
692
|
|
|
* @param bool $addDate |
693
|
|
|
*/ |
694
|
|
|
public function __construct($resource=STDOUT, string $level=self::INFO, bool $useLocking=false, bool $gzipFile=false, bool $addDate=true) |
695
|
|
|
{ |
696
|
|
|
$this->resource = $resource; |
697
|
|
|
$this->setLogLevel($level); |
698
|
|
|
$this->useLocking = $useLocking; |
699
|
|
|
$this->gzipFile = $gzipFile; |
700
|
|
|
$this->addDate = $addDate; |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
/** |
704
|
|
|
* System is unusable. |
705
|
|
|
* |
706
|
|
|
* @param string $message |
707
|
|
|
* @param array $context |
708
|
|
|
*/ |
709
|
|
|
public function emergency(string $message, array $context=[]) |
710
|
|
|
{ |
711
|
|
|
$this->log(self::EMERGENCY, $message, $context); |
712
|
|
|
} |
713
|
|
|
|
714
|
|
|
/** |
715
|
|
|
* Action must be taken immediately. |
716
|
|
|
* |
717
|
|
|
* Example: Entire website down, database unavailable, etc. This should |
718
|
|
|
* trigger the SMS alerts and wake you up. |
719
|
|
|
* |
720
|
|
|
* @param string $message |
721
|
|
|
* @param array $context |
722
|
|
|
*/ |
723
|
|
|
public function alert(string $message, array $context=[]) |
724
|
|
|
{ |
725
|
|
|
$this->log(self::ALERT, $message, $context); |
726
|
|
|
} |
727
|
|
|
|
728
|
|
|
/** |
729
|
|
|
* Critical conditions. |
730
|
|
|
* |
731
|
|
|
* Example: Application component unavailable, unexpected exception. |
732
|
|
|
* |
733
|
|
|
* @param string $message |
734
|
|
|
* @param array $context |
735
|
|
|
*/ |
736
|
|
|
public function critical(string $message, array $context=[]) |
737
|
|
|
{ |
738
|
|
|
$this->log(self::CRITICAL, $message, $context); |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
/** |
742
|
|
|
* Runtime errors that do not require immediate action but should typically |
743
|
|
|
* be logged and monitored. |
744
|
|
|
* |
745
|
|
|
* @param string $message |
746
|
|
|
* @param array $context |
747
|
|
|
*/ |
748
|
|
|
public function error(string $message, array $context=[]) |
749
|
|
|
{ |
750
|
|
|
$this->log(self::ERROR, $message, $context); |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* Exceptional occurrences that are not errors. |
755
|
|
|
* |
756
|
|
|
* Example: Use of deprecated APIs, poor use of an API, undesirable things |
757
|
|
|
* that are not necessarily wrong. |
758
|
|
|
* |
759
|
|
|
* @param string $message |
760
|
|
|
* @param array $context |
761
|
|
|
*/ |
762
|
|
|
public function warning(string $message, array $context=[]) |
763
|
|
|
{ |
764
|
|
|
$this->log(self::WARNING, $message, $context); |
765
|
|
|
} |
766
|
|
|
|
767
|
|
|
/** |
768
|
|
|
* Normal but significant events. |
769
|
|
|
* |
770
|
|
|
* @param string $message |
771
|
|
|
* @param array $context |
772
|
|
|
*/ |
773
|
|
|
public function notice(string $message, array $context=[]) |
774
|
|
|
{ |
775
|
|
|
$this->log(self::NOTICE, $message, $context); |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
/** |
779
|
|
|
* Interesting events. |
780
|
|
|
* |
781
|
|
|
* Example: User logs in, SQL logs. |
782
|
|
|
* |
783
|
|
|
* @param string $message |
784
|
|
|
* @param array $context |
785
|
|
|
*/ |
786
|
|
|
public function info(string $message, array $context=[]) |
787
|
|
|
{ |
788
|
|
|
$this->log(self::INFO, $message, $context); |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
/** |
792
|
|
|
* Detailed debug information. |
793
|
|
|
* |
794
|
|
|
* @param string $message |
795
|
|
|
* @param array $context |
796
|
|
|
*/ |
797
|
|
|
public function debug(string $message, array $context=[]) |
798
|
|
|
{ |
799
|
|
|
$this->log(self::DEBUG, $message, $context); |
800
|
|
|
} |
801
|
|
|
|
802
|
|
|
/** |
803
|
|
|
* @param $resource |
804
|
|
|
* @return Logger |
805
|
|
|
*/ |
806
|
|
|
public function setLogFile($resource) : Logger |
807
|
|
|
{ |
808
|
|
|
$this->resource = $resource; |
809
|
|
|
|
810
|
|
|
return $this; |
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
/** |
814
|
|
|
* @param string $string |
815
|
|
|
* @param string $foregroundColor |
816
|
|
|
* @param string $backgroundColor |
817
|
|
|
* @param bool $bold |
818
|
|
|
* @return string |
819
|
|
|
*/ |
820
|
|
|
public static function addColour(string $string, string $foregroundColor='', string $backgroundColor='', bool $bold=false) : string |
821
|
|
|
{ |
822
|
|
|
// todo: support bold |
823
|
|
|
unset($bold); |
824
|
|
|
$coloredString = ''; |
825
|
|
|
// Check if given foreground color found |
826
|
|
View Code Duplication |
if ( isset(static::$colours['fore'][$foregroundColor]) ) |
|
|
|
|
827
|
|
|
{ |
828
|
|
|
$coloredString .= "\033[" . static::$colours['fore'][$foregroundColor] . "m"; |
829
|
|
|
} |
830
|
|
|
// Check if given background color found |
831
|
|
View Code Duplication |
if ( isset(static::$colours['back'][$backgroundColor]) ) |
|
|
|
|
832
|
|
|
{ |
833
|
|
|
$coloredString .= "\033[" . static::$colours['back'][$backgroundColor] . "m"; |
834
|
|
|
} |
835
|
|
|
// Add string and end coloring |
836
|
|
|
$coloredString .= $string . "\033[0m"; |
837
|
|
|
|
838
|
|
|
return $coloredString; |
839
|
|
|
} |
840
|
|
|
|
841
|
|
|
/** |
842
|
|
|
* @param string $string |
843
|
|
|
* @param string $foregroundColor |
844
|
|
|
* @param string $backgroundColor |
845
|
|
|
* @param bool $bold |
846
|
|
|
* @return string |
847
|
|
|
*/ |
848
|
|
|
public function colourize(string $string, string $foregroundColor='', string $backgroundColor='', bool $bold=false) : string |
849
|
|
|
{ |
850
|
|
|
return static::addColour($string, $foregroundColor, $backgroundColor, $bold); |
851
|
|
|
} |
852
|
|
|
|
853
|
|
|
/** |
854
|
|
|
* @param string $level Ignore logging attempts at a level less the $level |
855
|
|
|
* @return Logger |
856
|
|
|
*/ |
857
|
|
|
public function setLogLevel(string $level) : Logger |
858
|
|
|
{ |
859
|
|
|
if ( ! isset(static::$logLevels[$level]) ) |
860
|
|
|
{ |
861
|
|
|
throw new \InvalidArgumentException("Log level is invalid"); |
862
|
|
|
} |
863
|
|
|
$this->level = static::$logLevels[$level][0]; |
864
|
|
|
|
865
|
|
|
return $this; |
866
|
|
|
} |
867
|
|
|
|
868
|
|
|
/** |
869
|
|
|
* @return Logger |
870
|
|
|
*/ |
871
|
|
|
public function lock() : Logger |
872
|
|
|
{ |
873
|
|
|
$this->useLocking = true; |
874
|
|
|
|
875
|
|
|
return $this; |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
/** |
879
|
|
|
* @return Logger |
880
|
|
|
*/ |
881
|
|
|
public function gzipped() : Logger |
882
|
|
|
{ |
883
|
|
|
$this->gzipFile = true; |
884
|
|
|
|
885
|
|
|
return $this; |
886
|
|
|
} |
887
|
|
|
|
888
|
|
|
/** |
889
|
|
|
* @param callable $fnFormatter |
890
|
|
|
* |
891
|
|
|
* @return Logger |
892
|
|
|
*/ |
893
|
|
|
public function formatter(callable $fnFormatter) : Logger |
894
|
|
|
{ |
895
|
|
|
$this->formatter = $fnFormatter; |
|
|
|
|
896
|
|
|
|
897
|
|
|
return $this; |
898
|
|
|
} |
899
|
|
|
|
900
|
|
|
/** |
901
|
|
|
* Log messages to resource |
902
|
|
|
* |
903
|
|
|
* @param mixed $level The level of the log message |
904
|
|
|
* @param string|object $message If an object is passed it must implement __toString() |
905
|
|
|
* @param array $context Placeholders to be substituted in the message |
906
|
|
|
*/ |
907
|
|
|
public function log($level, $message, array $context=[]) |
908
|
|
|
{ |
909
|
|
|
$level = isset(static::$logLevels[$level]) ? $level : self::INFO; |
910
|
|
|
list($logLevel, $fore, $back, $style) = static::$logLevels[$level]; |
911
|
|
|
unset($style); |
912
|
|
|
if ( $logLevel > $this->level ) |
913
|
|
|
{ |
914
|
|
|
return ; |
915
|
|
|
} |
916
|
|
|
if ( is_callable($this->formatter) ) |
917
|
|
|
{ |
918
|
|
|
$message = $this->formatter->__invoke(static::$logLevels[$level][4], $message, $context); |
919
|
|
|
} |
920
|
|
|
else |
921
|
|
|
{ |
922
|
|
|
$message = $this->formatMessage($level, $message, $context); |
|
|
|
|
923
|
|
|
} |
924
|
|
|
$this->lastLogEntry = $message; |
925
|
|
|
$this->write($this->colourize($message, $fore, $back) . PHP_EOL); |
926
|
|
|
} |
927
|
|
|
|
928
|
|
|
/** |
929
|
|
|
* @param string $style |
930
|
|
|
* @param string $message |
931
|
|
|
* @return string |
932
|
|
|
*/ |
933
|
|
|
public static function style(string $style, string $message) : string |
934
|
|
|
{ |
935
|
|
|
$style = isset(static::$logLevels[$style]) ? $style : self::INFO; |
936
|
|
|
list($logLevel, $fore, $back, $style) = static::$logLevels[$style]; |
937
|
|
|
unset($logLevel, $style); |
938
|
|
|
|
939
|
|
|
return static::addColour($message, $fore, $back); |
940
|
|
|
} |
941
|
|
|
|
942
|
|
|
/** |
943
|
|
|
* @param string $level |
944
|
|
|
* @param string $message |
945
|
|
|
* @param array $context |
946
|
|
|
* @return string |
947
|
|
|
*/ |
948
|
|
|
protected function formatMessage(string $level, string $message, array $context=[]) : string |
949
|
|
|
{ |
950
|
|
|
# Handle objects implementing __toString |
951
|
|
|
$message = (string) $message; |
952
|
|
|
$message .= empty($context) ? '' : PHP_EOL . json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); |
953
|
|
|
$data = $this->addDate ? ['date' => date('Y-m-d H:i:s')] : []; |
954
|
|
|
$data['level'] = strtoupper(str_pad(static::$logLevels[$level][4], 5, ' ', STR_PAD_RIGHT)); |
955
|
|
|
$data['message'] = $message; |
956
|
|
|
|
957
|
|
|
return implode($this->separator, $data); |
958
|
|
|
} |
959
|
|
|
|
960
|
|
|
/** |
961
|
|
|
* Write the content to the stream |
962
|
|
|
* |
963
|
|
|
* @param string $content |
964
|
|
|
*/ |
965
|
|
|
public function write(string $content) |
966
|
|
|
{ |
967
|
|
|
$resource = $this->getResource(); |
968
|
|
|
if ( $this->useLocking ) |
969
|
|
|
{ |
970
|
|
|
flock($resource, LOCK_EX); |
971
|
|
|
} |
972
|
|
|
gzwrite($resource, $content); |
973
|
|
|
if ( $this->useLocking ) |
974
|
|
|
{ |
975
|
|
|
flock($resource, LOCK_UN); |
976
|
|
|
} |
977
|
|
|
} |
978
|
|
|
|
979
|
|
|
/** |
980
|
|
|
* @return mixed|resource |
981
|
|
|
* @throws \Exception |
982
|
|
|
*/ |
983
|
|
|
protected function getResource() |
984
|
|
|
{ |
985
|
|
|
if ( is_resource($this->resource) ) |
986
|
|
|
{ |
987
|
|
|
return $this->resource; |
988
|
|
|
} |
989
|
|
|
$fileName = $this->resource; |
990
|
|
|
$this->closeLocally = true; |
991
|
|
|
$this->resource = $this->openResource(); |
992
|
|
|
if ( ! is_resource($this->resource) ) |
993
|
|
|
{ |
994
|
|
|
throw new \Exception("The resource ({$fileName}) could not be opened"); |
995
|
|
|
} |
996
|
|
|
|
997
|
|
|
return $this->resource; |
998
|
|
|
} |
999
|
|
|
|
1000
|
|
|
/** |
1001
|
|
|
* @return string |
1002
|
|
|
*/ |
1003
|
|
|
public function getLastLogEntry() : string |
1004
|
|
|
{ |
1005
|
|
|
return $this->lastLogEntry; |
1006
|
|
|
} |
1007
|
|
|
|
1008
|
|
|
/** |
1009
|
|
|
* @return resource |
1010
|
|
|
*/ |
1011
|
|
|
protected function openResource() |
1012
|
|
|
{ |
1013
|
|
|
if ( $this->gzipFile ) |
1014
|
|
|
{ |
1015
|
|
|
return gzopen($this->resource, 'a'); |
1016
|
|
|
} |
1017
|
|
|
|
1018
|
|
|
return fopen($this->resource, 'a'); |
1019
|
|
|
} |
1020
|
|
|
|
1021
|
|
|
public function __destruct() |
1022
|
|
|
{ |
1023
|
|
|
if ($this->closeLocally) |
1024
|
|
|
{ |
1025
|
|
|
gzclose($this->getResource()); |
1026
|
|
|
} |
1027
|
|
|
} |
1028
|
|
|
} |
1029
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.