packages/requirements/tests/Unit/Console/CommandHandlerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Console;
6
7use JsonException;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\Small;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Config\CoverageThresholds;
13use Requirements\Config\DefinitionReader;
14use Requirements\Config\Definitions;
15use Requirements\Config\DocumentReader;
16use Requirements\Config\ExtensionClasses;
17use Requirements\Config\JsonSchemaFile;
18use Requirements\Config\LinkValidator;
19use Requirements\Config\Loader;
20use Requirements\Config\MarkdownDocument;
21use Requirements\Config\SchemaValidator;
22use Requirements\Console\CommandHandler;
23use Requirements\Console\Executor;
24use Requirements\Console\ItemRecord;
25use Requirements\Console\Options;
26use Requirements\Console\Reporter;
27use Requirements\Console\SpecificationReport;
28use Requirements\Console\SpecificationTable;
29use Requirements\Console\Text;
30use Requirements\Console\Verdict;
31use Requirements\Ears\ConditionOrder;
32use Requirements\Ears\LiteralMask;
33use Requirements\Ears\SystemResponse;
34use Requirements\Ears\Validator;
35use Requirements\Ears\Wording;
36use Requirements\Input\Fields;
37use Requirements\Input\InvalidInputException;
38use Requirements\Model\Excerpt;
39use Requirements\Model\Item;
40use Requirements\Model\ItemValidator;
41use Requirements\Model\Project;
42use Requirements\Model\Source;
43use Requirements\Report\Analysis;
44use Requirements\Report\Analyzer;
45use Requirements\Report\Claims;
46use Requirements\Report\Coverage;
47use Requirements\Report\EvidenceMatcher;
48use Requirements\Report\SourceUnit;
49use Requirements\Report\UnitCollector;
50use Requirements\Source\DomSource;
51use Requirements\Source\JsonSource;
52use Requirements\Source\LocalFile;
53use Requirements\Source\Registry as SourceRegistry;
54use Requirements\Source\ResourceLoader;
55use Requirements\Source\ResourceLocation;
56use Requirements\Source\TextFragment;
57use Requirements\Source\TextSource;
58use Requirements\Source\Unit;
59use Requirements\Test\Registry as TestRegistry;
60use Requirements\Verification\TargetResults;
61use Requirements\Verification\TestExecution;
62use Requirements\Verification\VerificationResult;
63use Requirements\Verification\Verifier;
64use Symfony\Component\Console\Input\ArrayInput;
65use Symfony\Component\Console\Input\InputDefinition;
66use Symfony\Component\Console\Input\InputOption;
67use Symfony\Component\Console\Output\BufferedOutput;
68use Tests\Fake\ProjectDirectory;
69
70#[CoversClass(CommandHandler::class)]
71#[UsesClass(CoverageThresholds::class)]
72#[UsesClass(DefinitionReader::class)]
73#[UsesClass(Definitions::class)]
74#[UsesClass(DocumentReader::class)]
75#[UsesClass(ExtensionClasses::class)]
76#[UsesClass(JsonSchemaFile::class)]
77#[UsesClass(LinkValidator::class)]
78#[UsesClass(Loader::class)]
79#[UsesClass(MarkdownDocument::class)]
80#[UsesClass(SchemaValidator::class)]
81#[UsesClass(Executor::class)]
82#[UsesClass(SpecificationReport::class)]
83#[UsesClass(Options::class)]
84#[UsesClass(Reporter::class)]
85#[UsesClass(SpecificationTable::class)]
86#[UsesClass(Text::class)]
87#[UsesClass(Verdict::class)]
88#[UsesClass(ConditionOrder::class)]
89#[UsesClass(LiteralMask::class)]
90#[UsesClass(SystemResponse::class)]
91#[UsesClass(Validator::class)]
92#[UsesClass(Wording::class)]
93#[UsesClass(Fields::class)]
94#[UsesClass(Excerpt::class)]
95#[UsesClass(Item::class)]
96#[UsesClass(ItemValidator::class)]
97#[UsesClass(Project::class)]
98#[UsesClass(Source::class)]
99#[UsesClass(Analysis::class)]
100#[UsesClass(Analyzer::class)]
101#[UsesClass(Claims::class)]
102#[UsesClass(Coverage::class)]
103#[UsesClass(EvidenceMatcher::class)]
104#[UsesClass(SourceUnit::class)]
105#[UsesClass(UnitCollector::class)]
106#[UsesClass(DomSource::class)]
107#[UsesClass(JsonSource::class)]
108#[UsesClass(LocalFile::class)]
109#[UsesClass(SourceRegistry::class)]
110#[UsesClass(ResourceLoader::class)]
111#[UsesClass(ResourceLocation::class)]
112#[UsesClass(TextFragment::class)]
113#[UsesClass(TextSource::class)]
114#[UsesClass(Unit::class)]
115#[UsesClass(TestRegistry::class)]
116#[UsesClass(Verifier::class)]
117#[UsesClass(TestExecution::class)]
118#[UsesClass(TargetResults::class)]
119#[UsesClass(ItemRecord::class)]
120#[UsesClass(VerificationResult::class)]
121#[Small]
122final class CommandHandlerTest extends TestCase
123{
124 /**
125 * @throws JsonException
126 */
127 public function testInvokeWritesThePrettyJsonReport(): void
128 {
129 $project = new ProjectDirectory();
130 $definition = new InputDefinition([new InputOption('config', null, InputOption::VALUE_REQUIRED, '', 'requirements.yaml'), new InputOption('json', null, InputOption::VALUE_NONE)]);
131 $output = new BufferedOutput();
132 self::assertSame(0, (new CommandHandler('lint'))(new ArrayInput(['--config' => $project->path('requirements.yaml'), '--json' => true], $definition), $output));
133 self::assertSame("{\n \"passed\": true,\n \"message\": \"1 items validated.\"\n}\n", $output->fetch());
134 }
135
136 /**
137 * @throws JsonException
138 */
139 public function testInvokeWritesUnescapedSlashesAndUnicode(): void
140 {
141 $project = new ProjectDirectory();
142 $project->write('definition.yaml', ['version' => 1, 'source' => ['id' => 'manual', 'uri' => 'source.html', 'format' => 'html', 'selector' => 'main p'], 'items' => [[...ProjectDirectory::item(), 'category' => 'Größe/Parser <b>']]]);
143 $definition = new InputDefinition([
144 new InputOption('config', null, InputOption::VALUE_REQUIRED, '', 'requirements.yaml'),
145 new InputOption('json', null, InputOption::VALUE_NONE),
146 ...array_map(static fn (array $option): InputOption => new InputOption($option[0], null, $option[1] ? InputOption::VALUE_NONE : InputOption::VALUE_REQUIRED), Options::definitions('spec')),
147 ]);
148 $output = new BufferedOutput();
149 self::assertSame(0, (new CommandHandler('spec'))(new ArrayInput(['--config' => $project->path('requirements.yaml'), '--json' => true, '--no-test' => true], $definition), $output));
150 self::assertStringContainsString("\n \"category\": \"Größe/Parser <b>\",\n", $output->fetch());
151 }
152
153 /**
154 * @throws JsonException
155 */
156 public function testInvokeRendersTheTerminalReport(): void
157 {
158 $project = new ProjectDirectory();
159 $definition = new InputDefinition([new InputOption('config', null, InputOption::VALUE_REQUIRED, '', 'requirements.yaml'), new InputOption('json', null, InputOption::VALUE_NONE)]);
160 $output = new BufferedOutput();
161 self::assertSame(0, (new CommandHandler('lint'))(new ArrayInput(['--config' => $project->path('requirements.yaml')], $definition), $output));
162 $text = $output->fetch();
163 self::assertStringContainsString("Requirements · lint\n===================\n", $text);
164 self::assertStringContainsString('[OK] 1 items validated.', $text);
165 self::assertStringNotContainsString('"passed"', $text);
166 }
167
168 /**
169 * @throws JsonException
170 */
171 public function testInvokeReturnsOneForAFailedJsonReport(): void
172 {
173 $project = new ProjectDirectory();
174 $definition = new InputDefinition([
175 new InputOption('config', null, InputOption::VALUE_REQUIRED, '', 'requirements.yaml'),
176 new InputOption('json', null, InputOption::VALUE_NONE),
177 ...array_map(static fn (array $option): InputOption => new InputOption($option[0], null, $option[1] ? InputOption::VALUE_NONE : InputOption::VALUE_REQUIRED), Options::definitions('coverage')),
178 ]);
179 $output = new BufferedOutput();
180 self::assertSame(1, (new CommandHandler('coverage'))(new ArrayInput(['--config' => $project->path('requirements.yaml'), '--json' => true, '--min-coverage' => '100'], $definition), $output));
181 self::assertStringContainsString('"passed": false', $output->fetch());
182 }
183
184 /**
185 * @throws JsonException
186 */
187 public function testInvokeReturnsOneForAFailedTerminalReport(): void
188 {
189 $project = new ProjectDirectory();
190 $definition = new InputDefinition([
191 new InputOption('config', null, InputOption::VALUE_REQUIRED, '', 'requirements.yaml'),
192 new InputOption('json', null, InputOption::VALUE_NONE),
193 ...array_map(static fn (array $option): InputOption => new InputOption($option[0], null, $option[1] ? InputOption::VALUE_NONE : InputOption::VALUE_REQUIRED), Options::definitions('spec')),
194 ]);
195 $output = new BufferedOutput();
196 self::assertSame(1, (new CommandHandler('spec'))(new ArrayInput(['--config' => $project->path('requirements.yaml'), '--id' => 'MISSING', '--strict' => true], $definition), $output));
197 self::assertStringContainsString('[ERROR] No specifications or requirements selected.', $output->fetch());
198 }
199
200 /**
201 * @throws JsonException
202 */
203 public function testInvokeRejectsAMissingConfiguration(): void
204 {
205 $project = new ProjectDirectory();
206 $definition = new InputDefinition([new InputOption('config', null, InputOption::VALUE_REQUIRED, '', 'requirements.yaml'), new InputOption('json', null, InputOption::VALUE_NONE)]);
207 $this->expectException(InvalidInputException::class);
208 $this->expectExceptionMessage('Configuration does not exist');
209 (new CommandHandler('lint'))(new ArrayInput(['--config' => $project->path('missing.yaml'), '--json' => true], $definition), new BufferedOutput());
210 }
211}
212