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