packages/deriver/tests/Integration/EnvironmentTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Integration;
6
7use Deriver\Deriver;
8use Deriver\Deriver\Candidate\CandidateStatus;
9use Deriver\Deriver\DeriverConfig;
10use Deriver\Deriver\Sources\Path;
11use org\bovigo\vfs\vfsStream;
12use Override;
13use phpmock\Mock;
14use phpmock\MockBuilder;
15use PHPUnit\Framework\Attributes\CoversNothing;
16use PHPUnit\Framework\Attributes\DataProvider;
17use PHPUnit\Framework\Attributes\PreserveGlobalState;
18use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
19use PHPUnit\Framework\TestCase;
20
21#[CoversNothing]
22#[RunTestsInSeparateProcesses]
23#[PreserveGlobalState(false)]
24final class EnvironmentTest extends TestCase
25{
26 /**
27 * @param array<string, scalar|array<array-key, mixed>|null> $environment Explicit external inputs
28 * @param scalar|array<array-key, mixed>|null $expected Selected return value
29 * @throws \phpmock\MockEnabledException
30 */
31 #[DataProvider('providerInputs')]
32 public function testDeriveUsesVariableAndEnvironmentNames(string $code, array $environment, mixed $expected): void
33 {
34 $workingDirectory = '/project';
35 $filesystem = vfsStream::setup('project');
36 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
37 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
38 })->build()->enable();
39 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
40 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
41 })->build()->enable();
42 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
43 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
44 })->build()->enable();
45 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
46 return $workingDirectory;
47 })->build()->enable();
48 $sources = $filesystem;
49 $name = 'app.php';
50 $contents = '<?php ' . $code;
51 $url = $sources->url() . '/' . $name;
52 file_put_contents($url, $contents);
53 $path = '/' . $sources->path() . '/' . $name;
54 $result = Deriver::prepare([$path], new DeriverConfig(environment: $environment))->derive('target#return');
55 self::assertSame([$expected], array_column(iterator_to_array($result, false), 'result'));
56 }
57 /**
58 * @return iterable<string, array{string, array<string, scalar|array<array-key, mixed>|null>, scalar|array<array-key, mixed>|null}>
59 */
60 public static function providerInputs(): iterable
61 {
62 yield 'superglobal' => ['function target(){return $_GET["id"];}', ['$_GET' => ['id' => 123]], 123];
63 yield 'global' => ['function target(){global $prefix;return $prefix."users";}', ['$prefix' => 'app_'], 'app_users'];
64 yield 'named environment' => ['function target(){return getenv("APP_ENV");}', ['APP_ENV' => 'test'], 'test'];
65 yield 'explicit absent environment' => ['function target(){return getenv("ABSENT");}', ['ABSENT' => false], false];
66 yield 'constant from source' => ['const APP_ENV="source";function target(){return APP_ENV;}', ['APP_ENV' => 'environment'], 'source'];
67 yield 'static from source' => ['function nextValue(){static $n=4;return ++$n;}function target(){return nextValue();}', ['n' => 99, '$n' => 99], 5];
68 yield 'local from source' => ['function target(){$name="source";return $name;}', ['$name' => 'environment'], 'source'];
69 }
70 /**
71 * @throws \phpmock\MockEnabledException
72 */
73 public function testDeriveIdentifiesEnvironmentContentsAndSourceChanges(): void
74 {
75 $workingDirectory = '/project';
76 $filesystem = vfsStream::setup('project');
77 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
78 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
79 })->build()->enable();
80 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
81 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
82 })->build()->enable();
83 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
84 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
85 })->build()->enable();
86 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
87 return $workingDirectory;
88 })->build()->enable();
89 $sources = $filesystem;
90 $name = 'app.php';
91 $contents = '<?php function target(){return getenv("APP_ENV");}';
92 $url = $sources->url() . '/' . $name;
93 file_put_contents($url, $contents);
94 $path = '/' . $sources->path() . '/' . $name;
95 $configuration = new DeriverConfig(environment: ['APP_ENV' => 'test']);
96 $deriver = Deriver::prepare([$path], $configuration);
97 self::assertSame('test', $deriver->derive('target#return')[0]->result);
98 self::assertSame('production', Deriver::prepare([$path], new DeriverConfig(environment: ['APP_ENV' => 'production']))->derive('target#return')[0]->result);
99 $name1 = 'app.php';
100 $contents1 = '<?php function target(){return "changed";}';
101 $url1 = $sources->url() . '/' . $name1;
102 file_put_contents($url1, $contents1);
103 self::assertSame('changed', Deriver::prepare([$path], $configuration)->derive('target#return')[0]->result);
104 }
105 /**
106 * @throws \phpmock\MockEnabledException
107 */
108 #[DataProvider('providerUnresolvedInputs')]
109 public function testDeriveKeepsUnspecifiedInputsAndEventsUnresolved(string $expression): void
110 {
111 $workingDirectory = '/project';
112 $filesystem = vfsStream::setup('project');
113 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_file')->setFunction(static function (string $path) use (&$workingDirectory): bool {
114 return is_file('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
115 })->build()->enable();
116 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('is_readable')->setFunction(static function (string $path) use (&$workingDirectory): bool {
117 return is_readable('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
118 })->build()->enable();
119 (new MockBuilder())->setNamespace('Deriver\Deriver\Sources')->setName('file_get_contents')->setFunction(static function (string $path) use (&$workingDirectory): string|false {
120 return file_get_contents('vfs:/' . Path::normalize(str_starts_with($path, '/') ? $path : $workingDirectory . '/' . $path));
121 })->build()->enable();
122 (new MockBuilder())->setNamespace('Deriver\Deriver\Selection')->setName('getcwd')->setFunction(static function () use (&$workingDirectory): string {
123 return $workingDirectory;
124 })->build()->enable();
125 $sources = $filesystem;
126 $name = 'app.php';
127 $contents = '<?php function target(){return ' . $expression . ';}';
128 $url = $sources->url() . '/' . $name;
129 file_put_contents($url, $contents);
130 $path = '/' . $sources->path() . '/' . $name;
131 $result = Deriver::prepare([$path], new DeriverConfig(environment: ['APP_ENV' => 'test', 'time' => 123, 'microtime' => 'captured']))->derive('target#return');
132 self::assertCount(1, $result);
133 self::assertSame(CandidateStatus::Partials, $result[0]->type);
134 }
135 /**
136 * @return iterable<string, array{string}>
137 */
138 public static function providerUnresolvedInputs(): iterable
139 {
140 yield 'missing name' => ['getenv("MISSING")'];
141 yield 'whole environment' => ['getenv()'];
142 yield 'clock event' => ['time()'];
143 yield 'microtime event' => ['microtime()'];
144 }
145 #[Override]
146 protected function tearDown(): void
147 {
148 Mock::disableAll();
149 parent::tearDown();
150 }
151}
152