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