Skip to content

Commit 896af4b

Browse files
committed
Add CodewarsResultPrinter
1 parent 11a1131 commit 896af4b

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Qualified Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## codewars/phpunit-codewars
2+
3+
Provides `CodewarsResultPrinter`.
4+
5+
### Usage
6+
7+
CLI:
8+
```
9+
phpunit --printer codewars\PHPUnitCodewars\CodewarsResultPrinter
10+
```
11+
12+
With `phpunit.xml`:
13+
```xml
14+
<phpunit printerClass="codewars\PHPUnitCodewars\CodewarsResultPrinter">
15+
</phpunit>
16+
```

composer.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "codewars/phpunit-codewars",
3+
"description": "Codewars result printer for PHPUnit",
4+
"type": "library",
5+
"license": "MIT",
6+
"keywords": ["codewars", "test", "phpunit", "phpunit reporter"],
7+
"homepage": "https://github.com/codewars/phpunit-codewars",
8+
"autoload": {
9+
"psr-4": {
10+
"codewars\\PHPUnitCodewars\\": "src"
11+
}
12+
},
13+
"require": {
14+
"php": ">=7.4",
15+
"phpunit/phpunit": "^9"
16+
}
17+
}

src/CodewarsResultPrinter.php

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php
2+
use PHPUnit\Framework\AssertionFailedError;
3+
use PHPUnit\Framework\ExceptionWrapper;
4+
use PHPUnit\Framework\ExpectationFailedException;
5+
use PHPUnit\Framework\Test;
6+
use PHPUnit\Framework\TestCase;
7+
use PHPUnit\Framework\TestFailure;
8+
use PHPUnit\Framework\TestListener;
9+
use PHPUnit\Framework\TestResult;
10+
use PHPUnit\Framework\TestSuite;
11+
use PHPUnit\Framework\Warning;
12+
use PHPUnit\TextUI\DefaultResultPrinter;
13+
use PHPUnit\Util\Filter;
14+
use SebastianBergmann\Comparator\ComparisonFailure;
15+
16+
/**
17+
* Outputs events in Codewars format. Based on TeamCity class.
18+
*/
19+
class CodewarsResultPrinter extends DefaultResultPrinter
20+
{
21+
/**
22+
* @var TestSuite
23+
*/
24+
private $wrapperSuite = null;
25+
/**
26+
* @var bool
27+
*/
28+
protected $lastTestFailed = false;
29+
30+
31+
/**
32+
* An error occurred.
33+
*/
34+
public function addError(Test $test, \Throwable $t, float $time): void
35+
{
36+
$this->lastTestFailed = true;
37+
$this->write(sprintf("\n<ERROR::>%s\n", self::getMessage($t)));
38+
$this->write(sprintf("\n<LOG::-Details>%s\n", self::escapeLF(self::getDetails($t))));
39+
}
40+
41+
/**
42+
* A warning occurred.
43+
*/
44+
public function addWarning(Test $test, Warning $e, float $time): void
45+
{
46+
$this->lastTestFailed = true;
47+
$this->write(sprintf("\n<ERROR::>%s\n", self::getMessage($e)));
48+
$this->write(sprintf("\n<LOG::-Details>%s\n", self::escapeLF(self::getDetails($e))));
49+
}
50+
51+
/**
52+
* A failure occurred.
53+
*/
54+
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
55+
{
56+
$this->lastTestFailed = true;
57+
$msg = self::getMessage($e);
58+
if ($e instanceof ExpectationFailedException) {
59+
$msg .= self::getAssertionDetails($e);
60+
}
61+
$this->write(sprintf("\n<FAILED::>%s\n", self::escapeLF($msg)));
62+
$this->write(sprintf("\n<LOG::-Details>%s\n", self::escapeLF(self::getDetails($e))));
63+
}
64+
65+
/**
66+
* Incomplete test.
67+
*/
68+
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
69+
{
70+
$this->write("\n<LOG::>Test Incomplete\n");
71+
}
72+
73+
/**
74+
* Risky test.
75+
*/
76+
public function addRiskyTest(Test $test, \Throwable $t, float $time): void
77+
{
78+
$this->addError($test, $t, $time);
79+
}
80+
81+
/**
82+
* Skipped test.
83+
*/
84+
public function addSkippedTest(Test $test, \Throwable $t, float $time): void
85+
{
86+
$this->write("\n<LOG::>Test Ignored\n");
87+
}
88+
89+
/**
90+
* A testsuite started.
91+
*/
92+
public function startTestSuite(TestSuite $suite): void
93+
{
94+
// The first suite is just a wrapper around the actual suites.
95+
// Remember this so that this can be used again in `endTestSuite`.
96+
if ($this->wrapperSuite == null) {
97+
$this->wrapperSuite = $suite;
98+
return;
99+
}
100+
101+
$suiteName = $suite->getName();
102+
if (empty($suiteName)) {
103+
return;
104+
}
105+
$this->write(sprintf("\n<DESCRIBE::>%s\n", $suiteName));
106+
}
107+
108+
/**
109+
* A testsuite ended.
110+
*/
111+
public function endTestSuite(TestSuite $suite): void
112+
{
113+
if ($this->wrapperSuite == $suite) {
114+
$this->wrapperSuite = null;
115+
return;
116+
}
117+
118+
if (empty($suite->getName())) {
119+
return;
120+
}
121+
122+
$this->write("\n<COMPLETEDIN::>\n");
123+
}
124+
125+
/**
126+
* A test started.
127+
*/
128+
public function startTest(Test $test): void
129+
{
130+
$this->write(sprintf("\n<IT::>%s\n", $test->getName()));
131+
}
132+
133+
/**
134+
* A test ended.
135+
*/
136+
public function endTest(Test $test, float $time): void
137+
{
138+
if (!$this->lastTestFailed) {
139+
$this->write("\n<PASSED::>Test Passed\n");
140+
$this->lastTestFailed = false;
141+
}
142+
$this->write(sprintf("\n<COMPLETEDIN::>%.4f\n", $time * 1000));
143+
}
144+
145+
public function printResult(TestResult $result): void
146+
{
147+
}
148+
149+
private static function getMessage(\Throwable $t): string
150+
{
151+
$message = '';
152+
153+
if ($t instanceof ExceptionWrapper) {
154+
if ($t->getClassName() !== '') {
155+
$message .= $t->getClassName();
156+
}
157+
158+
if ($message !== '' && $t->getMessage() !== '') {
159+
$message .= ' : ';
160+
}
161+
}
162+
163+
return $message . $t->getMessage();
164+
}
165+
166+
/**
167+
* @throws \InvalidArgumentException
168+
*/
169+
private static function getDetails(\Throwable $t): string
170+
{
171+
$stackTrace = Filter::getFilteredStacktrace($t);
172+
$previous = $t instanceof ExceptionWrapper ? $t->getPreviousWrapped() : $t->getPrevious();
173+
174+
while ($previous) {
175+
$stackTrace .= "\nCaused by\n" .
176+
TestFailure::exceptionToString($previous) . "\n" .
177+
Filter::getFilteredStacktrace($previous);
178+
179+
$previous = $previous instanceof ExceptionWrapper ?
180+
$previous->getPreviousWrapped() : $previous->getPrevious();
181+
}
182+
183+
return ' ' . \str_replace("\n", "\n ", $stackTrace);
184+
}
185+
186+
private static function getPrimitiveValueAsString($value): ?string
187+
{
188+
if ($value === null) {
189+
return 'null';
190+
}
191+
192+
if (\is_bool($value)) {
193+
return $value === true ? 'true' : 'false';
194+
}
195+
196+
if (\is_scalar($value)) {
197+
return \print_r($value, true);
198+
}
199+
200+
return null;
201+
}
202+
203+
private static function escapeLF(string $text): string
204+
{
205+
return \str_replace("\n", "<:LF:>", $text);
206+
}
207+
208+
private static function getAssertionDetails(ExpectationFailedException $e): string
209+
{
210+
$comparisonFailure = $e->getComparisonFailure();
211+
if (!($comparisonFailure instanceof ComparisonFailure)) {
212+
return "";
213+
}
214+
215+
$expectedString = $comparisonFailure->getExpectedAsString();
216+
if ($expectedString === null || empty($expectedString)) {
217+
$expectedString = self::getPrimitiveValueAsString($comparisonFailure->getExpected());
218+
}
219+
220+
$actualString = $comparisonFailure->getActualAsString();
221+
if ($actualString === null || empty($actualString)) {
222+
$actualString = self::getPrimitiveValueAsString($comparisonFailure->getActual());
223+
}
224+
225+
if ($actualString !== null && $expectedString !== null) {
226+
return sprintf("\nExpected: %s\nActual : %s", $expectedString, $actualString);
227+
}
228+
229+
return "";
230+
}
231+
}

0 commit comments

Comments
 (0)