packages/requirements/tests/Unit/Console/ReporterTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Console;
6
7use Override;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\Small;
11use PHPUnit\Framework\Attributes\UsesClass;
12use PHPUnit\Framework\TestCase;
13use Requirements\Console\CoverageTable;
14use Requirements\Console\Options;
15use Requirements\Console\Reporter;
16use Requirements\Console\SpecificationTable;
17use Requirements\Console\Text;
18use Requirements\Console\Verdict;
19use Requirements\Input\Fields;
20use Requirements\Input\InvalidInputException;
21use Symfony\Component\Console\Input\ArrayInput;
22use Symfony\Component\Console\Output\BufferedOutput;
23
24#[CoversClass(Reporter::class)]
25#[UsesClass(CoverageTable::class)]
26#[UsesClass(Options::class)]
27#[UsesClass(SpecificationTable::class)]
28#[UsesClass(Text::class)]
29#[UsesClass(Verdict::class)]
30#[UsesClass(Fields::class)]
31#[Small]
32final class ReporterTest extends TestCase
33{
34 #[Override]
35 protected function setUp(): void
36 {
37 putenv('COLUMNS=120');
38 }
39
40 #[Override]
41 protected function tearDown(): void
42 {
43 putenv('COLUMNS');
44 }
45
46 /**
47 * @param array<string, string|bool> $values
48 * @param array<string, mixed> $report
49 */
50 #[DataProvider('providerRender')]
51 public function testRenderWritesTheTitleTheCommandBodyAndTheVerdict(string $command, array $values, array $report, string $expected): void
52 {
53 $output = new BufferedOutput();
54 (new Reporter())->render($report, new Options($command, $values), new ArrayInput([]), $output);
55 self::assertSame($expected, preg_replace('/ +$/m', '', $output->fetch()));
56 }
57
58 /**
59 * @return array<string, array{string, array<string, string|bool>, array<string, mixed>, string}>
60 */
61 public static function providerRender(): array
62 {
63 return [
64 'lint' => ['lint', [], ['passed' => true, 'message' => '1 items validated.'], <<<'TEXT'
65
66Requirements · lint
67===================
68
69 [OK] 1 items validated.
70
71
72TEXT],
73 'format with changes' => ['format', [], ['passed' => true, 'changed' => ["/p/<info>definition.yaml\x07"]], <<<'TEXT'
74
75Requirements · format
76=====================
77
78 * /p/<info>definition.yaml
79
80 [OK] Documents formatted.
81
82
83TEXT],
84 'format without changes' => ['format', ['check' => true], ['passed' => true, 'changed' => []], <<<'TEXT'
85
86Requirements · format
87=====================
88
89 [OK] No formatting changes.
90
91
92TEXT],
93 'check' => ['check', [], ['passed' => false, 'mode' => 'configured', 'errors' => ['SPEC-001: quote not found.'], 'evidence' => []], <<<'TEXT'
94
95Requirements · check
96====================
97
98 [ERROR] SPEC-001: quote not found.
99
100
101TEXT],
102 'coverage' => ['coverage', [], ['passed' => true, 'overall' => ['total' => 3, 'accounted' => 1, 'supported' => 1, 'unsupported' => 0, 'uncovered' => 2, 'percentage' => 33.3333], 'sources' => [], 'diff' => null, 'errors' => []], <<<'TEXT'
103
104Requirements · coverage
105=======================
106
107 --------- ------- ----------- ----------- ------------- ----------- ----------
108 Scope Units Accounted Supported Unsupported Uncovered Coverage
109 --------- ------- ----------- ----------- ------------- ----------- ----------
110 Overall 3 1 1 0 2 33.33%
111 --------- ------- ----------- ----------- ------------- ----------- ----------
112
113 ! [NOTE] Coverage applies only to the declared source scopes. Unsupported units are accounted for; tests are verified
114 ! separately.
115
116 [OK] Coverage gates passed.
117
118
119TEXT],
120 'spec' => ['spec', ['no-test' => true], ['passed' => true, 'no_test' => true, 'specifications' => ['SPEC-001' => ['statement' => 'The reader shall read.', 'kind' => 'specification', 'source' => 'manual', 'origin' => 'sourced', 'category' => '', 'labels' => [], 'reason' => '', 'status' => 'not-run', 'passed_targets' => null, 'total_targets' => 0, 'message' => '']], 'errors' => []], <<<'TEXT'
121
122Requirements · spec
123===================
124
125+----------+------------------------+---------+-------+
126| ID | Statement | Result | Tests |
127+----------+------------------------+---------+-------+
128| SPEC-001 | The reader shall read. | not-run | -/0 |
129| | specification · manual | | |
130+----------+------------------------+---------+-------+
131 1 item(s). Tests: passed / linked targets. Use --json for execution counts and complete traceability records.
132
133 [OK] Records displayed; tests were not run.
134
135
136TEXT],
137 ];
138 }
139
140 /**
141 * @param array<string, mixed> $report
142 */
143 #[DataProvider('providerRenderRejected')]
144 public function testRenderRejectsAReportWithoutTheShapeOfItsCommand(string $command, array $report, string $message): void
145 {
146 $this->expectException(InvalidInputException::class);
147 $this->expectExceptionMessage($message);
148 (new Reporter())->render($report, new Options($command, []), new ArrayInput([]), new BufferedOutput());
149 }
150
151 /**
152 * @return array<string, array{string, array<string, mixed>, string}>
153 */
154 public static function providerRenderRejected(): array
155 {
156 return [
157 'format changes' => ['format', ['passed' => true, 'changed' => 'definition.yaml'], 'changed must be a list.'],
158 'format duplicate changes' => ['format', ['passed' => true, 'changed' => ['a.yaml', 'a.yaml']], 'changed contains duplicates.'],
159 'coverage sources' => ['coverage', ['passed' => true, 'overall' => [], 'sources' => 'manual', 'diff' => null], 'sources must be a mapping.'],
160 'spec rows' => ['spec', ['passed' => true, 'specifications' => 'SPEC-001'], 'specifications must be a mapping.'],
161 'check errors' => ['check', ['passed' => false, 'errors' => 'Broken.'], 'errors must be a list.'],
162 ];
163 }
164}
165