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