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