packages/requirements/tests/Integration/Console/SpecCommandTest.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\Large;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Console\Application;
13use Requirements\Console\CommandHandler;
14use Requirements\Console\CommandLine;
15use Requirements\Console\Executor;
16use Requirements\Console\ItemRecord;
17use Requirements\Console\Reporter;
18use Requirements\Console\SpecificationReport;
19use Requirements\Console\SpecificationTable;
20use Requirements\Input\Fields;
21use Requirements\Verification\TargetResults;
22use Requirements\Verification\TestExecution;
23use Requirements\Verification\Verifier;
24use Tests\Fake\CommandLine as Cli;
25use Tests\Fake\PhpUnitSuite;
26use Tests\Fake\ProjectDirectory;
27
28#[CoversClass(Application::class)]
29#[UsesClass(CommandLine::class)]
30#[UsesClass(CommandHandler::class)]
31#[UsesClass(Executor::class)]
32#[UsesClass(SpecificationReport::class)]
33#[UsesClass(ItemRecord::class)]
34#[UsesClass(Reporter::class)]
35#[UsesClass(SpecificationTable::class)]
36#[UsesClass(Verifier::class)]
37#[UsesClass(TestExecution::class)]
38#[UsesClass(TargetResults::class)]
39#[UsesClass(Fields::class)]
40#[Large]
41final class SpecCommandTest extends TestCase
42{
43    /**
44     * @throws JsonException
45     */
46    public function testRunCountsPassingTargetsPerSpecificationSeparatelyFromDataSets(): void
47    {
48        $project = new ProjectDirectory();
49        $project->write('requirements.yaml', [
50            'version' => 1,
51            'definitions' => ['definition.yaml'],
52            'runners' => ['unit' => ['extension' => 'phpunit', 'command' => PhpUnitSuite::write($project->directory)]],
53        ]);
54        $item = [...ProjectDirectory::item(), 'kind' => 'specification', 'status' => 'supported', 'reason' => 'A documented decision.'];
55        $items = [
56            [...$item, 'id' => 'PASS', 'tests' => [['runner' => 'unit', 'target' => 'Sample\\PassingTest::testPass'], ['runner' => 'unit', 'target' => 'Sample\\PassingTest::testData']]],
57            [...$item, 'id' => 'PARTIAL', 'tests' => [['runner' => 'unit', 'target' => 'Sample\\PassingTest::testPass'], ['runner' => 'unit', 'target' => 'Sample\\PassingTest::testFailure'], ['runner' => 'unit', 'target' => 'Sample\\PassingTest::testSkip']]],
58            [...$item, 'id' => 'MISSING', 'tests' => [['runner' => 'unit', 'target' => 'Sample\\PassingTest::testMissing']]],
59            [...$item, 'id' => 'SHARED', 'tests' => [['runner' => 'unit', 'target' => 'Sample\\PassingTest::testData'], ['runner' => 'unit', 'target' => 'Sample\\PassingTest::testData']]],
60            [...$item, 'id' => 'UNVERIFIED', 'tests' => []],
61            [...$item, 'id' => 'UNSUPPORTED', 'status' => 'unsupported', 'tests' => [['runner' => 'unit', 'target' => 'Sample\\PassingTest::testFailure']]],
62            [...$item, 'id' => 'REQ', 'kind' => 'requirement', 'tests' => []],
63        ];
64        $project->write('definition.yaml', ['version' => 1, 'source' => ['id' => 'manual', 'uri' => 'source.html', 'format' => 'html', 'selector' => 'main p'], 'items' => $items]);
65        $process = Cli::run(['spec', '--json'], $project->directory);
66        self::assertSame(1, $process->getExitCode(), $process->getErrorOutput());
67        $report = Fields::mapping(json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR), 'report');
68        self::assertFalse($report['passed']);
69        self::assertFalse($report['no_test']);
70        $rows = Fields::mapping($report['specifications'], 'specifications');
71        self::assertSame(['PASS' => 'passed', 'PARTIAL' => 'failed', 'MISSING' => 'failed', 'SHARED' => 'passed', 'UNVERIFIED' => 'unverified', 'UNSUPPORTED' => 'unsupported', 'REQ' => 'not-applicable'], array_column($rows, 'status', 'id'));
72        self::assertSame(['PASS' => 2, 'PARTIAL' => 1, 'MISSING' => 0, 'SHARED' => 1, 'UNVERIFIED' => 0, 'UNSUPPORTED' => null, 'REQ' => null], array_column($rows, 'passed_targets', 'id'));
73        self::assertSame(['PASS' => 2, 'PARTIAL' => 3, 'MISSING' => 1, 'SHARED' => 1, 'UNVERIFIED' => 0, 'UNSUPPORTED' => 1, 'REQ' => 0], array_column($rows, 'total_targets', 'id'));
74        self::assertSame(['PASS' => 3, 'PARTIAL' => 3, 'MISSING' => 0, 'SHARED' => 2, 'UNVERIFIED' => 0, 'UNSUPPORTED' => 0, 'REQ' => 0], array_column($rows, 'tests', 'id'));
75        self::assertSame(['PASS' => 'manual', 'PARTIAL' => 'manual', 'MISSING' => 'manual', 'SHARED' => 'manual', 'UNVERIFIED' => 'manual', 'UNSUPPORTED' => 'manual', 'REQ' => 'manual'], array_column($rows, 'source', 'id'));
76        self::assertSame(['PASS' => 'supported', 'PARTIAL' => 'supported', 'MISSING' => 'supported', 'SHARED' => 'supported', 'UNVERIFIED' => 'supported', 'UNSUPPORTED' => 'unsupported', 'REQ' => 'supported'], array_column($rows, 'support', 'id'));
77        $table = Cli::run(['spec', '--id=PARTIAL', '--no-ansi'], $project->directory);
78        self::assertSame(1, $table->getExitCode());
79        self::assertStringContainsString('1/3', $table->getOutput());
80        self::assertStringNotContainsString('2/2', $table->getOutput());
81        $passing = Cli::run(['spec', '--id=PASS', '--no-ansi'], $project->directory);
82        self::assertSame(0, $passing->getExitCode());
83        self::assertStringContainsString('2/2', $passing->getOutput());
84        $requirements = Cli::run(['spec', '--kind=requirement', '--no-ansi'], $project->directory);
85        self::assertSame(0, $requirements->getExitCode());
86        self::assertStringContainsString('REQ', $requirements->getOutput());
87        self::assertStringContainsString('-/0', $requirements->getOutput());
88    }
89
90    /**
91     * @throws JsonException
92     */
93    public function testRunBrowsesCompleteFilteredRecordsWithoutExecutingOrFetching(): void
94    {
95        $project = new ProjectDirectory();
96        unlink($project->path('source.html'));
97        $project->write('requirements.yaml', [
98            'version' => 1,
99            'definitions' => ['definition.yaml'],
100            'runners' => ['unit' => ['extension' => 'phpunit', 'command' => [PHP_BINARY, '-r', 'file_put_contents("executed.txt", "executed"); exit(1);']]],
101        ]);
102        $record = [
103            'id' => 'ORIGINAL', 'statement' => 'The reader shall preserve positions.', 'origin' => 'original', 'reason' => 'Support editor diagnostics.',
104            'labels' => ['diagnostics'], 'category' => 'reader', 'design' => [['text' => 'Preserve offsets.']], 'metadata' => ['owner' => 'parser'],
105            'tests' => [['runner' => 'unit', 'target' => 'ReaderTest::testA'], ['runner' => 'unit', 'target' => 'ReaderTest::testB'], ['runner' => 'unit', 'target' => 'ReaderTest::testC']],
106        ];
107        $project->write('definition.yaml', ['version' => 1, 'source' => null, 'items' => [
108            $record,
109            [...$record, 'id' => 'UNSUPPORTED', 'status' => 'unsupported', 'related' => ['ORIGINAL']],
110            [...$record, 'id' => 'UNLINKED', 'tests' => []],
111        ]]);
112        $process = Cli::run(['spec', '--no-test', '--json'], $project->directory);
113        self::assertSame(0, $process->getExitCode(), $process->getErrorOutput());
114        $report = Fields::mapping(json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR), 'report');
115        self::assertTrue($report['passed']);
116        self::assertTrue($report['no_test']);
117        $rows = Fields::mapping($report['specifications'], 'specifications');
118        self::assertSame(['ORIGINAL' => 'not-run', 'UNSUPPORTED' => 'unsupported', 'UNLINKED' => 'not-run'], array_column($rows, 'status', 'id'));
119        self::assertSame(['ORIGINAL' => 3, 'UNSUPPORTED' => 3, 'UNLINKED' => 0], array_column($rows, 'total_targets', 'id'));
120        self::assertSame(['ORIGINAL' => null, 'UNSUPPORTED' => null, 'UNLINKED' => null], array_column($rows, 'passed_targets', 'id'));
121        self::assertSame(['ORIGINAL' => 0, 'UNSUPPORTED' => 0, 'UNLINKED' => 0], array_column($rows, 'tests', 'id'));
122        self::assertSame(['ORIGINAL' => $record['design'], 'UNSUPPORTED' => $record['design'], 'UNLINKED' => $record['design']], array_column($rows, 'design', 'id'));
123        self::assertSame(['ORIGINAL' => $record['metadata'], 'UNSUPPORTED' => $record['metadata'], 'UNLINKED' => $record['metadata']], array_column($rows, 'metadata', 'id'));
124        self::assertSame(['ORIGINAL' => $record['reason'], 'UNSUPPORTED' => $record['reason'], 'UNLINKED' => $record['reason']], array_column($rows, 'reason', 'id'));
125        $row = Fields::mapping($rows['UNSUPPORTED'], 'unsupported');
126        self::assertSame(['ORIGINAL'], $row['related']);
127        self::assertSame($record['tests'], $row['test_references']);
128        $table = Cli::run(['spec', '--no-test', '--without-source', '--label=diagnostics', '--category=reader', '--status=supported', '--origin=original', '--kind=specification', '--id=ORIGINAL', '--no-ansi'], $project->directory);
129        self::assertSame(0, $table->getExitCode());
130        self::assertStringContainsString('-/3', $table->getOutput());
131        self::assertStringContainsString('not-run', $table->getOutput());
132        self::assertStringContainsString('diagnostics', $table->getOutput());
133        self::assertStringContainsString('Reason: Support editor', $table->getOutput());
134        self::assertStringContainsString('tests were not run', $table->getOutput());
135        self::assertStringNotContainsString('UNSUPPORTED', $table->getOutput());
136        self::assertFileDoesNotExist($project->path('executed.txt'));
137        $empty = Cli::run(['spec', '--no-test', '--source=missing'], $project->directory);
138        self::assertSame(0, $empty->getExitCode());
139    }
140}
141