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