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