packages/requirements/tests/Integration/Console/CommandsTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Integration\Console;
6
7use JsonException;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\Large;
11use PHPUnit\Framework\Attributes\UsesClass;
12use PHPUnit\Framework\TestCase;
13use Requirements\Console\Application;
14use Requirements\Console\CommandHandler;
15use Requirements\Console\CommandLine;
16use Requirements\Console\Executor;
17use Requirements\Console\Overview;
18use Requirements\Console\Reporter;
19use Requirements\Console\SpecificationReport;
20use Tests\Fake\CommandLine as Cli;
21use Tests\Fake\ProjectDirectory;
22
23#[CoversClass(Application::class)]
24#[UsesClass(CommandLine::class)]
25#[UsesClass(Overview::class)]
26#[UsesClass(CommandHandler::class)]
27#[UsesClass(Executor::class)]
28#[UsesClass(SpecificationReport::class)]
29#[UsesClass(Reporter::class)]
30#[Large]
31final class CommandsTest extends TestCase
32{
33 #[DataProvider('providerCommands')]
34 public function testRunExitsZeroForEachCommandAsJson(string $command): void
35 {
36 $project = new ProjectDirectory();
37 $process = Cli::run([$command, '--json'], $project->directory);
38 self::assertSame(0, $process->getExitCode(), $process->getOutput() . $process->getErrorOutput());
39 }
40
41 /**
42 * @return array<string, array{string}>
43 */
44 public static function providerCommands(): array
45 {
46 return [
47 'lint' => ['lint'],
48 'check' => ['check'],
49 'coverage' => ['coverage'],
50 'format' => ['format'],
51 ];
52 }
53
54 public function testRunChecksCoverageFormatsAndFiltersIndependentSpecifications(): void
55 {
56 $project = new ProjectDirectory();
57 $format = Cli::run(['format', '--check'], $project->directory);
58 self::assertSame(0, $format->getExitCode(), $format->getOutput());
59 $gate = Cli::run(['coverage', '--min-coverage=100'], $project->directory);
60 self::assertSame(1, $gate->getExitCode());
61 $spec = Cli::run(['spec'], $project->directory);
62 self::assertSame(0, $spec->getExitCode());
63 self::assertStringContainsString('unverified', $spec->getOutput());
64 $project->write('definition.yaml', ['version' => 1, 'source' => null, 'items' => [['id' => 'ORIGINAL-001', 'statement' => 'The parser shall reject truncated input.', 'origin' => 'original', 'reason' => 'Avoid silent data loss.', 'labels' => ['strictness']]]]);
65 $list = Cli::run(['spec', '--no-test', '--without-source', '--label=strictness', '--json'], $project->directory);
66 self::assertSame(0, $list->getExitCode());
67 self::assertStringContainsString('ORIGINAL-001', $list->getOutput());
68 $empty = Cli::run(['spec', '--id=MISSING'], $project->directory);
69 self::assertSame(0, $empty->getExitCode());
70 }
71
72 public function testRunExitsTwoForAnInvalidPercentageAndAnUnknownOption(): void
73 {
74 $project = new ProjectDirectory();
75 $process = Cli::run(['coverage', '--min-coverage=not-a-number', '--json'], $project->directory);
76 self::assertSame(2, $process->getExitCode());
77 self::assertStringContainsString('percentage', $process->getOutput());
78 $process = Cli::run(['spec', '--unknown'], $project->directory);
79 self::assertSame(2, $process->getExitCode());
80 }
81
82 /**
83 * @param list<string> $arguments
84 */
85 #[DataProvider('providerHelp')]
86 public function testRunShowsHelpWithoutAProject(array $arguments): void
87 {
88 $project = new ProjectDirectory();
89 unlink($project->path('requirements.yaml'));
90 $process = Cli::run($arguments, $project->directory);
91 self::assertSame(0, $process->getExitCode(), $process->getErrorOutput());
92 self::assertStringContainsString('Usage:', $process->getOutput());
93 self::assertStringNotContainsString("\x1b", $process->getOutput());
94 }
95
96 /**
97 * @return array<string, array{list<string>}>
98 */
99 public static function providerHelp(): array
100 {
101 return [
102 'nothing' => [[]],
103 'help option' => [['--help']],
104 'help shortcut' => [['-h']],
105 'help without decoration' => [['--help', '--no-ansi']],
106 'help with a missing configuration' => [['--config', 'missing.yaml', '--help']],
107 'help with json' => [['--json', '--help']],
108 'help command for a command' => [['help', 'coverage']],
109 'command help' => [['coverage', '--help']],
110 'command help shortcut' => [['spec', '-h']],
111 'list' => [['list']],
112 ];
113 }
114
115 public function testRunShowsTheOverviewAndTheOptionsOfEachCommandWithoutAProject(): void
116 {
117 $project = new ProjectDirectory();
118 unlink($project->path('requirements.yaml'));
119 $overview = Cli::run(['--config', 'missing.yaml', '--help'], $project->directory);
120 self::assertSame(0, $overview->getExitCode());
121 self::assertStringContainsString('Available commands:', $overview->getOutput());
122 $help = Cli::run(['coverage', '--help'], $project->directory);
123 self::assertSame(0, $help->getExitCode());
124 self::assertStringContainsString('--min-diff-coverage', $help->getOutput());
125 self::assertStringContainsString('--snapshot', $help->getOutput());
126 self::assertStringContainsString('--write-snapshot', $help->getOutput());
127 self::assertStringNotContainsString('baseline', $help->getOutput());
128 self::assertStringNotContainsString('--without-source', $help->getOutput());
129 $help = Cli::run(['spec', '--help'], $project->directory);
130 self::assertSame(0, $help->getExitCode());
131 self::assertStringContainsString('--no-test', $help->getOutput());
132 self::assertStringContainsString('--strict', $help->getOutput());
133 self::assertStringContainsString('--all', $help->getOutput());
134 self::assertStringContainsString('--without-source', $help->getOutput());
135 }
136
137 public function testRunAcceptsGlobalOptionsBeforeTheCommandAndRendersTables(): void
138 {
139 $project = new ProjectDirectory();
140 $process = Cli::run(['--config', 'requirements.yaml', 'coverage', '--no-ansi'], $project->directory);
141 self::assertSame(0, $process->getExitCode(), $process->getErrorOutput());
142 self::assertStringContainsString('33.33%', $process->getOutput());
143 self::assertStringContainsString('Uncovered', $process->getOutput());
144 self::assertStringNotContainsString('passed:', $process->getOutput());
145 self::assertStringNotContainsString('{ }', $process->getOutput());
146 $list = Cli::run(['spec', '--no-test', '--no-ansi'], $project->directory);
147 self::assertSame(0, $list->getExitCode());
148 self::assertStringContainsString('SPEC-001', $list->getOutput());
149 self::assertStringContainsString('Statement', $list->getOutput());
150 }
151
152 /**
153 * @throws JsonException
154 */
155 public function testRunWritesAndComparesCoverageSnapshots(): void
156 {
157 $project = new ProjectDirectory();
158 $write = Cli::run(['coverage', '--write-snapshot=snapshot.json', '--json'], $project->directory);
159 self::assertSame(0, $write->getExitCode(), $write->getOutput() . $write->getErrorOutput());
160 $snapshot = json_decode($project->read('snapshot.json'), true, 512, JSON_THROW_ON_ERROR);
161 self::assertIsArray($snapshot);
162 self::assertSame('requirements-snapshot', $snapshot['type']);
163 self::assertSame(['version', 'type', 'units'], array_keys($snapshot));
164 $compare = Cli::run(['coverage', '--snapshot=snapshot.json', '--min-diff-coverage=100', '--json'], $project->directory);
165 self::assertSame(0, $compare->getExitCode(), $compare->getOutput() . $compare->getErrorOutput());
166 $report = json_decode($compare->getOutput(), true, 512, JSON_THROW_ON_ERROR);
167 self::assertIsArray($report);
168 self::assertSame(['total' => 0, 'accounted' => 0, 'supported' => 0, 'unsupported' => 0, 'uncovered' => 0, 'percentage' => null], $report['diff']);
169 self::assertSame([], $report['removed']);
170 $missing = Cli::run(['coverage', '--min-diff-coverage=100', '--no-ansi'], $project->directory);
171 self::assertSame(1, $missing->getExitCode());
172 self::assertStringContainsString('requires --snapshot', $missing->getOutput());
173 }
174
175 /**
176 * @param list<string> $arguments
177 *
178 * @throws JsonException
179 */
180 #[DataProvider('providerInvalidInput')]
181 public function testRunKeepsJsonMachineReadableForInvalidInput(array $arguments): void
182 {
183 $project = new ProjectDirectory();
184 $process = Cli::run(['--json', ...$arguments], $project->directory);
185 self::assertSame(2, $process->getExitCode());
186 $report = json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR);
187 self::assertIsArray($report);
188 self::assertFalse($report['passed']);
189 self::assertSame('', $process->getErrorOutput());
190 }
191
192 /**
193 * @return array<string, array{list<string>}>
194 */
195 public static function providerInvalidInput(): array
196 {
197 return [
198 'unknown option' => [['coverage', '--unknown']],
199 'removed baseline option' => [['coverage', '--baseline=snapshot.json']],
200 'removed write-baseline option' => [['coverage', '--write-baseline=snapshot.json']],
201 'option of another command' => [['spec', '--live']],
202 'unknown command' => [['unknown']],
203 'missing configuration' => [['--config', 'missing.yaml', 'lint']],
204 ];
205 }
206
207 public function testRunReportsAMissingConfigurationOnStandardError(): void
208 {
209 $project = new ProjectDirectory();
210 $process = Cli::run(['lint', '--config', 'missing.yaml', '--no-ansi'], $project->directory);
211 self::assertSame(2, $process->getExitCode());
212 self::assertSame('', $process->getOutput());
213 self::assertStringContainsString('[ERROR]', $process->getErrorOutput());
214 self::assertStringContainsString('Configuration does not exist', $process->getErrorOutput());
215 }
216}
217