packages/requirements/tests/Unit/Test/JUnitTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Test;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Small;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Test\JUnit;
13use Requirements\Test\TestResult;
14use Tests\Fake\ProjectDirectory;
15
16#[CoversClass(JUnit::class)]
17#[UsesClass(TestResult::class)]
18#[Small]
19final class JUnitTest extends TestCase
20{
21 #[DataProvider('providerReadCannotPassWithoutExecutedPassingCases')]
22 public function testReadCannotPassWithoutExecutedPassingCases(string $xml, string $status): void
23 {
24 $project = new ProjectDirectory();
25 $file = $project->put('report.xml', $xml);
26 self::assertSame($status, (new JUnit())->read([$file], 0, '')->status);
27 }
28
29 /**
30 * @return list<array{string, string}>
31 */
32 public static function providerReadCannotPassWithoutExecutedPassingCases(): array
33 {
34 return [
35 ['<testsuite/>', 'error'],
36 ['<testsuite><testcase/></testsuite>', 'passed'],
37 ['<testsuite><testcase><skipped/></testcase></testsuite>', 'failed'],
38 ['<testsuite><testcase status="pending"/></testsuite>', 'failed'],
39 ['<testsuite><testcase><error/></testcase></testsuite>', 'failed'],
40 ['<testsuite><testcase>', 'error'],
41 ['<!DOCTYPE a><testsuite><testcase/></testsuite>', 'error'],
42 ];
43 }
44
45 #[DataProvider('providerReadCountsPassingCases')]
46 public function testReadCountsPassingCases(string $xml, int $tests): void
47 {
48 $project = new ProjectDirectory();
49 $file = $project->put('report.xml', $xml);
50 self::assertSame(['status' => 'passed', 'tests' => $tests, 'message' => ''], (new JUnit())->read([$file], 0, "output\n")->toArray());
51 }
52
53 /**
54 * @return array<string, array{string, int}>
55 */
56 public static function providerReadCountsPassingCases(): array
57 {
58 return [
59 'testsuite root' => ['<testsuite><testcase/></testsuite>', 1],
60 'testsuites root' => ['<?xml version="1.0"?><testsuites><testsuite><testcase name="a"/><testcase name="b"/></testsuite></testsuites>', 2],
61 'nested suites' => ['<testsuites><testsuite><testsuite><testcase/></testsuite><testcase/></testsuite></testsuites>', 2],
62 'passed status' => ['<testsuite><testcase status="passed"/></testsuite>', 1],
63 'success status' => ['<testsuite><testcase status="success"/></testsuite>', 1],
64 'empty status' => ['<testsuite><testcase status=""/></testsuite>', 1],
65 'system output' => ['<testsuite><testcase><system-out>text</system-out></testcase></testsuite>', 1],
66 ];
67 }
68
69 #[DataProvider('providerReadFailsOnDefects')]
70 public function testReadFailsOnDefects(string $xml, int $tests): void
71 {
72 $project = new ProjectDirectory();
73 $file = $project->put('report.xml', $xml);
74 self::assertSame(['status' => 'failed', 'tests' => $tests, 'message' => 'Expected failure.'], (new JUnit())->read([$file], 0, "\n Expected failure.\n")->toArray());
75 }
76
77 /**
78 * @return array<string, array{string, int}>
79 */
80 public static function providerReadFailsOnDefects(): array
81 {
82 return [
83 'failure' => ['<testsuite><testcase><failure>boom</failure></testcase></testsuite>', 1],
84 'failed status before a passing case' => ['<testsuite><testcase status="failed"/><testcase/></testsuite>', 2],
85 'uppercase status' => ['<testsuite><testcase status="PASSED"/></testsuite>', 1],
86 'error outside cases' => ['<testsuites><testsuite><testcase/><error/></testsuite></testsuites>', 1],
87 ];
88 }
89
90 public function testReadFailsOnANonzeroExitCode(): void
91 {
92 $project = new ProjectDirectory();
93 $file = $project->put('report.xml', '<testsuite><testcase/></testsuite>');
94 self::assertSame(['status' => 'failed', 'tests' => 1, 'message' => 'Exit 1.'], (new JUnit())->read([$file], 1, " Exit 1.\n")->toArray());
95 }
96
97 public function testReadSumsCasesOfEveryReport(): void
98 {
99 $project = new ProjectDirectory();
100 $first = $project->put('first.xml', '<testsuite><testcase/></testsuite>');
101 $second = $project->put('second.xml', '<testsuite><testcase/><testcase/></testsuite>');
102 self::assertSame(['status' => 'passed', 'tests' => 3, 'message' => ''], (new JUnit())->read([$first, $second], 0, '')->toArray());
103 }
104
105 public function testReadFailsWhenAnyReportFails(): void
106 {
107 $project = new ProjectDirectory();
108 $first = $project->put('first.xml', '<testsuite><testcase><failure/></testcase></testsuite>');
109 $second = $project->put('second.xml', '<testsuite><testcase/></testsuite>');
110 self::assertSame(['status' => 'failed', 'tests' => 2, 'message' => 'out'], (new JUnit())->read([$first, $second], 0, 'out')->toArray());
111 }
112
113 public function testReadRejectsAnUnsafeLaterReport(): void
114 {
115 $project = new ProjectDirectory();
116 $first = $project->put('first.xml', '<testsuite><testcase/></testsuite>');
117 $second = $project->put('second.xml', '<testsuite><testcase/></testsuite><!ENTITY a "b">');
118 self::assertSame(['status' => 'error', 'tests' => 0, 'message' => 'Unsafe or unreadable JUnit report.'], (new JUnit())->read([$first, $second], 0, '')->toArray());
119 }
120
121 #[DataProvider('providerReadRejectsUnsafeReports')]
122 public function testReadRejectsUnsafeReports(string $xml): void
123 {
124 $project = new ProjectDirectory();
125 $file = $project->put('report.xml', $xml);
126 self::assertSame(['status' => 'error', 'tests' => 0, 'message' => 'Unsafe or unreadable JUnit report.'], (new JUnit())->read([$file], 0, '')->toArray());
127 }
128
129 /**
130 * @return array<string, array{string}>
131 */
132 public static function providerReadRejectsUnsafeReports(): array
133 {
134 return [
135 'doctype' => ['<!DOCTYPE a><testsuite><testcase/></testsuite>'],
136 'lowercase doctype' => ['<!doctype a><testsuite><testcase/></testsuite>'],
137 'entity' => ['<testsuite><!ENTITY a "b"><testcase/></testsuite>'],
138 'lowercase entity' => ['<testsuite><!entity a "b"><testcase/></testsuite>'],
139 ];
140 }
141
142 #[DataProvider('providerReadRejectsMalformedReports')]
143 public function testReadRejectsMalformedReports(string $xml): void
144 {
145 $project = new ProjectDirectory();
146 $file = $project->put('report.xml', $xml);
147 self::assertSame(['status' => 'error', 'tests' => 0, 'message' => 'Malformed JUnit report.'], (new JUnit())->read([$file], 0, '')->toArray());
148 }
149
150 /**
151 * @return array<string, array{string}>
152 */
153 public static function providerReadRejectsMalformedReports(): array
154 {
155 return [
156 'unclosed' => ['<testsuite><testcase>'],
157 'text' => ['passed'],
158 'other root' => ['<results><testcase/></results>'],
159 'testcase root' => ['<testcase/>'],
160 'empty' => [''],
161 'blank' => [" \n"],
162 ];
163 }
164
165 public function testReadLeavesNoLibxmlErrorsBehind(): void
166 {
167 $project = new ProjectDirectory();
168 $file = $project->put('report.xml', '<testsuite><testcase>');
169 (new JUnit())->read([$file], 0, '');
170 self::assertSame([], libxml_get_errors());
171 self::assertFalse(libxml_use_internal_errors(false));
172 }
173
174 public function testReadReportsNoExecutedTestsWithTheOutput(): void
175 {
176 $project = new ProjectDirectory();
177 $file = $project->put('report.xml', '<testsuites><testsuite name="empty"/></testsuites>');
178 self::assertSame(['status' => 'error', 'tests' => 0, 'message' => 'No executed tests in fresh JUnit reports. No tests executed!'], (new JUnit())->read([$file], 0, "\nNo tests executed!\n")->toArray());
179 }
180
181 public function testReadReportsNoExecutedTestsWithoutReports(): void
182 {
183 self::assertSame(['status' => 'error', 'tests' => 0, 'message' => 'No executed tests in fresh JUnit reports. '], (new JUnit())->read([], 0, '')->toArray());
184 }
185}
186