packages/deriver/tests/Integration/CommandTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Integration;
6
7use Deriver\Command\Application;
8use org\bovigo\vfs\vfsStream;
9use Override;
10use phpmock\Mock;
11use phpmock\MockBuilder;
12use PHPUnit\Framework\Attributes\CoversNothing;
13use PHPUnit\Framework\Attributes\PreserveGlobalState;
14use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
15use PHPUnit\Framework\TestCase;
16use Symfony\Component\Console\Tester\ApplicationTester;
17
18#[CoversNothing]
19#[RunTestsInSeparateProcesses]
20#[PreserveGlobalState(false)]
21final class CommandTest extends TestCase
22{
23 /**
24 * @throws \phpmock\MockEnabledException
25 */
26 #[Override]
27 protected function setUp(): void
28 {
29 parent::setUp();
30 vfsStream::setup('project', null, [
31 'src' => ['Main.php' => '<?php throw new RuntimeException("Application files must not run"); function answer() { return 42; } function pending($id) { return $id; } function environment() { return getenv("APP_ENV"); }'],
32 'ignored' => ['Invalid.php' => '<?php invalid syntax !'],
33 'deliver.yaml' => "paths: [src]\ninclude: ['*.php']\nphpVersion: '8.3'\nformat: json\nenvironment: {APP_ENV: test}\n",
34 ]);
35 (new MockBuilder())->setNamespace('Deriver')->setName('is_dir')->setFunction(static fn (string $path): bool => is_dir('vfs:/' . $path))->build()->enable();
36 (new MockBuilder())->setNamespace('Deriver')->setName('is_readable')->setFunction(static fn (string $path): bool => is_readable('vfs:/' . $path))->build()->enable();
37 (new MockBuilder())->setNamespace('Deriver')->setName('realpath')->setFunction(static fn (string $path): string => 'vfs:/' . $path)->build()->enable();
38 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('str_contains')->setFunction(static fn (string $haystack, string $needle): bool => str_starts_with($haystack, 'vfs://') && $needle === '://' ? false : str_contains($haystack, $needle))->build()->enable();
39 (new MockBuilder())->setNamespace('Deriver\Configuration')->setName('is_file')->setFunction(static fn (string $path): bool => is_file('vfs:/' . $path))->build()->enable();
40 (new MockBuilder())->setNamespace('Deriver\Configuration')->setName('is_readable')->setFunction(static fn (string $path): bool => is_readable('vfs:/' . $path))->build()->enable();
41 (new MockBuilder())->setNamespace('Deriver\Configuration')->setName('file_get_contents')->setFunction(static fn (string $path): string|false => file_get_contents('vfs:/' . $path))->build()->enable();
42 }
43
44 public function testCommandSelectsFilesAndDoesNotExecuteApplicationCode(): void
45 {
46 $application = new Application();
47 $application->setAutoExit(false);
48 $tester = new ApplicationTester($application);
49 self::assertSame(0, $tester->run(['query' => 'answer#return', '--path' => ['/project'], '--exclude' => ['ignored']], ['capture_stderr_separately' => true]));
50 self::assertStringContainsString("[0] analyzed int\n42\n", $tester->getDisplay());
51 self::assertSame('', $tester->getErrorOutput());
52 }
53
54 public function testCommandLoadsConfigurationAndEnvironmentInputs(): void
55 {
56 $application = new Application();
57 $application->setAutoExit(false);
58 $tester = new ApplicationTester($application);
59 self::assertSame(0, $tester->run(['query' => 'environment#return', '--config' => '/project/deliver.yaml'], ['capture_stderr_separately' => true]));
60 self::assertStringContainsString('"result": "test"', $tester->getDisplay());
61 self::assertSame('', $tester->getErrorOutput());
62 }
63
64 public function testCommandOptionsOverrideConfiguredListsAndFormat(): void
65 {
66 file_put_contents('vfs://project/deliver.yaml', "paths: [ignored]\ninclude: ['Missing.php']\nexclude: ['Main.php']\nformat: json\n");
67 $application = new Application();
68 $application->setAutoExit(false);
69 $tester = new ApplicationTester($application);
70 self::assertSame(0, $tester->run(['query' => 'answer#return', '--config' => '/project/deliver.yaml', '--path' => ['/project/src'], '--include' => ['Main.php'], '--exclude' => ['Other.php'], '--format' => 'text']));
71 self::assertStringContainsString("[0] analyzed int\n42\n", $tester->getDisplay());
72 }
73
74 public function testCommandTreatsUnresolvedInputsAsCompletedPartialCandidates(): void
75 {
76 $application = new Application();
77 $application->setAutoExit(false);
78 $tester = new ApplicationTester($application);
79 self::assertSame(0, $tester->run(['query' => 'pending#return', '--config' => '/project/deliver.yaml']));
80 self::assertStringContainsString('"type": "partials"', $tester->getDisplay());
81 self::assertStringContainsString('"interrupted": false', $tester->getDisplay());
82 }
83
84 public function testCommandSeparatesParserFailuresFromReportOutput(): void
85 {
86 $application = new Application();
87 $application->setAutoExit(false);
88 $tester = new ApplicationTester($application);
89 self::assertSame(1, $tester->run(['query' => 'answer#return', '--path' => ['/project/ignored'], '--format' => 'json'], ['capture_stderr_separately' => true]));
90 self::assertSame('', $tester->getDisplay());
91 self::assertStringContainsString('Error:', $tester->getErrorOutput());
92 }
93
94 public function testCommandRejectsEmptyFileSelections(): void
95 {
96 $application = new Application();
97 $application->setAutoExit(false);
98 $tester = new ApplicationTester($application);
99 self::assertSame(2, $tester->run(['query' => 'answer#return', '--path' => ['/project/src'], '--include' => ['Missing.php']], ['capture_stderr_separately' => true]));
100 self::assertSame('', $tester->getDisplay());
101 self::assertStringContainsString('No source files matched', $tester->getErrorOutput());
102 }
103
104 public function testCommandKeepsConsoleFormattingTagsLiteral(): void
105 {
106 file_put_contents('vfs://project/src/Main.php', '<?php function answer() { return "<error>literal</error>"; }');
107 $application = new Application();
108 $application->setAutoExit(false);
109 $tester = new ApplicationTester($application);
110 self::assertSame(0, $tester->run(['query' => 'answer#return', '--path' => ['/project/src']], ['decorated' => true]));
111 self::assertStringContainsString("'<error>literal</error>'", $tester->getDisplay());
112 }
113
114 public function testCommandAppliesTheTargetPhpVersionOverride(): void
115 {
116 file_put_contents('vfs://project/src/Main.php', '<?php class Item { public int $value { get => 42; } } function answer() { return (new Item())->value; }');
117 $application = new Application();
118 $application->setAutoExit(false);
119 $tester = new ApplicationTester($application);
120 self::assertSame(0, $tester->run(['query' => 'answer#return', '--config' => '/project/deliver.yaml', '--php-version' => '8.4'], ['capture_stderr_separately' => true]));
121 self::assertStringContainsString('"result": 42', $tester->getDisplay());
122 self::assertSame('', $tester->getErrorOutput());
123 }
124
125 #[Override]
126 protected function tearDown(): void
127 {
128 Mock::disableAll();
129 parent::tearDown();
130 }
131}
132