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