packages/requirements/tests/Unit/Console/VerdictTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Console;
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\Console\Options;
13use Requirements\Console\Text;
14use Requirements\Console\Verdict;
15use Requirements\Input\Fields;
16use Requirements\Input\InvalidInputException;
17use Symfony\Component\Console\Input\ArrayInput;
18use Symfony\Component\Console\Output\BufferedOutput;
19use Symfony\Component\Console\Style\SymfonyStyle;
20
21#[CoversClass(Verdict::class)]
22#[UsesClass(Options::class)]
23#[UsesClass(Text::class)]
24#[UsesClass(Fields::class)]
25#[Small]
26final class VerdictTest extends TestCase
27{
28    /**
29     * @param array<string, mixed> $report
30     * @param array<string, string|bool> $values
31     */
32    #[DataProvider('providerRenderPassed')]
33    public function testRenderClosesAPassedReportWithTheCommandMessage(string $command, array $values, array $report, string $expected): void
34    {
35        $output = new BufferedOutput();
36        (new Verdict())->render(new SymfonyStyle(new ArrayInput([]), $output), $report, new Options($command, $values));
37        self::assertSame($expected, preg_replace('/ +$/m', '', $output->fetch()));
38    }
39
40    /**
41     * @return array<string, array{string, array<string, string|bool>, array<string, mixed>, string}>
42     */
43    public static function providerRenderPassed(): array
44    {
45        return [
46            'lint' => ['lint', [], ['passed' => true, 'message' => '3 items validated.'], "\n [OK] 3 items validated.\n\n"],
47            'lint escapes markup' => ['lint', [], ['passed' => true, 'message' => "<info>3</info> items\x07 validated."], "\n [OK] \\<info\\>3\\</info\\> items validated.\n\n"],
48            'check' => ['check', [], ['passed' => true, 'errors' => []], "\n [OK] Source evidence is valid.\n\n"],
49            'coverage' => ['coverage', [], ['passed' => true, 'errors' => []], "\n [OK] Coverage gates passed.\n\n"],
50            'spec' => ['spec', [], ['passed' => true, 'errors' => []], "\n [OK] Specification verification completed.\n\n"],
51            'spec without tests' => ['spec', ['no-test' => true], ['passed' => true, 'errors' => []], "\n [OK] Records displayed; tests were not run.\n\n"],
52            'format unchanged' => ['format', [], ['passed' => true, 'changed' => []], "\n [OK] No formatting changes.\n\n"],
53            'format changed' => ['format', [], ['passed' => true, 'changed' => ['definition.yaml']], "\n [OK] Documents formatted.\n\n"],
54            'passed missing' => ['check', [], [], "\n [OK] Source evidence is valid.\n\n"],
55            'unknown command' => ['unknown', [], ['passed' => true], ''],
56        ];
57    }
58
59    /**
60     * @param array<string, mixed> $report
61     */
62    #[DataProvider('providerRenderFailed')]
63    public function testRenderReportsAFailedReport(string $command, array $report, string $expected): void
64    {
65        $output = new BufferedOutput();
66        (new Verdict())->render(new SymfonyStyle(new ArrayInput([]), $output), $report, new Options($command, []));
67        self::assertSame($expected, preg_replace('/ +$/m', '', $output->fetch()));
68    }
69
70    /**
71     * @return array<string, array{string, array<string, mixed>, string}>
72     */
73    public static function providerRenderFailed(): array
74    {
75        return [
76            'format without errors' => ['format', ['passed' => false, 'changed' => ['definition.yaml']], "\n [ERROR] Documents need formatting. Run requirements format.\n\n"],
77            'spec without errors' => ['spec', ['passed' => false, 'errors' => []], "\n [ERROR] Specification verification failed.\n\n"],
78            'unknown without errors' => ['unknown', ['passed' => false], "\n [ERROR] Specification verification failed.\n\n"],
79            'passed not true' => ['spec', ['passed' => 'yes'], "\n [ERROR] Specification verification failed.\n\n"],
80            'spec with an error' => ['spec', ['passed' => false, 'errors' => ['No specifications or requirements selected.']], "\n [ERROR] No specifications or requirements selected.\n\n"],
81            'format with an error' => ['format', ['passed' => false, 'errors' => ['Broken.'], 'changed' => []], "\n [ERROR] Broken.\n\n"],
82            'repeated errors' => ['check', ['passed' => false, 'errors' => ['Same error.', 'Same error.']], "\n [ERROR] Same error.\n\n         Same error.\n\n"],
83            'escaped errors' => ['coverage', ['passed' => false, 'errors' => ["<error>bad</error>\x1b[0m"]], "\n [ERROR] \\<error\\>bad\\</error\\>[0m\n\n"],
84        ];
85    }
86
87    public function testRenderShowsErrorsBeforeTheSuccessOfAPassedReport(): void
88    {
89        $output = new BufferedOutput();
90        (new Verdict())->render(new SymfonyStyle(new ArrayInput([]), $output), ['passed' => true, 'errors' => ['A warning.']], new Options('check', []));
91        self::assertSame("\n [ERROR] A warning.\n\n [OK] Source evidence is valid.\n\n", preg_replace('/ +$/m', '', $output->fetch()));
92    }
93
94    /**
95     * @param array<string, mixed> $report
96     */
97    #[DataProvider('providerRenderRejected')]
98    public function testRenderRejectsErrorsThatAreNotAListOfStrings(array $report, string $message): void
99    {
100        $this->expectException(InvalidInputException::class);
101        $this->expectExceptionMessage($message);
102        (new Verdict())->render(new SymfonyStyle(new ArrayInput([]), new BufferedOutput()), $report, new Options('check', []));
103    }
104
105    /**
106     * @return array<string, array{array<string, mixed>, string}>
107     */
108    public static function providerRenderRejected(): array
109    {
110        return [
111            'mapping' => [['passed' => false, 'errors' => ['a' => 'Error.']], 'errors must be a list.'],
112            'text' => [['passed' => false, 'errors' => 'Error.'], 'errors must be a list.'],
113            'number' => [['passed' => false, 'errors' => [1]], 'errors must contain nonempty strings.'],
114            'blank' => [['passed' => false, 'errors' => [' ']], 'errors must contain nonempty strings.'],
115        ];
116    }
117}
118