diff --git a/bridge/symfony-console/bin/testo b/bridge/symfony-console/bin/testo index c1b6176e..f0f5f46c 100755 --- a/bridge/symfony-console/bin/testo +++ b/bridge/symfony-console/bin/testo @@ -23,7 +23,7 @@ use Testo\Common\Info; # As a dependency $s = DIRECTORY_SEPARATOR; - \str_ends_with(__DIR__, "{$s}vendor{$s}testo{$s}testo{$s}bin") and $paths[] = __DIR__ . '/../../../autoload.php'; + \str_ends_with(__DIR__, "{$s}vendor{$s}testo{$s}bridge-symfony-console{$s}bin") and $paths[] = __DIR__ . '/../../../autoload.php'; # Fallback to CWD $paths[] = 'vendor/autoload.php'; diff --git a/bridge/symfony-console/tests/Acceptance/EntrypointTest.php b/bridge/symfony-console/tests/Acceptance/EntrypointTest.php new file mode 100644 index 00000000..e61b79e7 --- /dev/null +++ b/bridge/symfony-console/tests/Acceptance/EntrypointTest.php @@ -0,0 +1,147 @@ +installEntrypointAsComposerDependency(); + + [$exitCode, $stdout, $stderr] = self::runBinary($entrypoint, '--version', $this->sandbox->path('run')); + + self::assertRuns($exitCode, $stdout, $stderr, 'the installed entrypoint must load vendor/autoload.php three levels above its bin directory'); + } + + public function loadsTheBridgeLocalAutoloaderDuringDevelopment(): void + { + $entrypoint = $this->installEntrypointForLocalDevelopment(); + + [$exitCode, $stdout, $stderr] = self::runBinary($entrypoint, '--version', $this->sandbox->path('run')); + + self::assertRuns($exitCode, $stdout, $stderr, 'the local-development entrypoint must load bridge/symfony-console/vendor/autoload.php'); + } + + public function loadsTheAutoloaderFromTheCurrentWorkingDirectoryAsFallback(): void + { + $entrypoint = $this->installEntrypointWithoutAdjacentAutoloaders(); + $workingDirectory = $this->sandbox->path('run'); + $this->installComposerAutoloader($workingDirectory . '/vendor'); + + [$exitCode, $stdout, $stderr] = self::runBinary($entrypoint, '--version', $workingDirectory); + + self::assertRuns($exitCode, $stdout, $stderr, 'the entrypoint must fall back to vendor/autoload.php in the current working directory'); + } + + #[BeforeTest] + public function createSandbox(): void + { + $this->sandbox = Sandbox::create(); + } + + #[AfterTest] + public function destroySandbox(): void + { + $this->sandbox->destroy(); + } + + private static function assertRuns(int $exitCode, string $stdout, string $stderr, string $expectation): void + { + Assert::same( + $exitCode, + 0, + $expectation . '; stdout: ' . $stdout . '; stderr: ' . $stderr, + ); + } + + /** + * @return array{int, string, string} + */ + private static function runBinary(string $entrypoint, string $argument, string $workingDirectory): array + { + $process = \proc_open( + [\PHP_BINARY, $entrypoint, $argument], + [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $workingDirectory, + ); + \assert(\is_resource($process)); + \fclose($pipes[0]); + + $stdout = (string) \stream_get_contents($pipes[1]); + $stderr = (string) \stream_get_contents($pipes[2]); + \fclose($pipes[1]); + \fclose($pipes[2]); + + return [\proc_close($process), $stdout, $stderr]; + } + + private function installEntrypointAsComposerDependency(): string + { + $entrypoint = $this->installEntrypoint('vendor/testo/bridge-symfony-console/bin/testo'); + $vendor = $this->sandbox->path('vendor'); + + $this->installComposerAutoloader($vendor); + $this->makeWorkingDirectory(); + + return $entrypoint; + } + + private function installEntrypointForLocalDevelopment(): string + { + $entrypoint = $this->installEntrypoint('bridge/symfony-console/bin/testo'); + + $this->installComposerAutoloader($this->sandbox->path('bridge/symfony-console/vendor')); + $this->makeWorkingDirectory(); + + return $entrypoint; + } + + private function installEntrypointWithoutAdjacentAutoloaders(): string + { + $entrypoint = $this->installEntrypoint('isolated/bin/testo'); + $this->makeWorkingDirectory(); + + return $entrypoint; + } + + private function installEntrypoint(string $relativePath): string + { + $root = \dirname(__DIR__, 4); + $entrypoint = $this->sandbox->path($relativePath); + $bin = \dirname($entrypoint); + + \is_dir($bin) || \mkdir($bin, 0o755, true) or throw new \RuntimeException("Cannot create {$bin}"); + \copy($root . '/bridge/symfony-console/bin/testo', $entrypoint) + or throw new \RuntimeException("Cannot install the testo entrypoint at {$relativePath}."); + + return $entrypoint; + } + + private function installComposerAutoloader(string $vendor): void + { + $root = \dirname(__DIR__, 4); + + \is_dir($vendor) || \mkdir($vendor, 0o755, true) or throw new \RuntimeException("Cannot create {$vendor}"); + \file_put_contents($vendor . '/autoload.php', "sandbox->path('run')) or throw new \RuntimeException('Cannot create isolated working directory.'); + } +}