From 8284306826392864f4a34c0f04db11407663653b Mon Sep 17 00:00:00 2001 From: batyrmastyr Date: Sat, 19 Sep 2026 23:31:55 +0400 Subject: [PATCH 1/2] fix(bridge-symfony-console): vendor path detection in composer installations --- bridge/symfony-console/bin/testo | 2 +- .../tests/Acceptance/EntrypointTest.php | 149 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 bridge/symfony-console/tests/Acceptance/EntrypointTest.php 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..2afe3e52 --- /dev/null +++ b/bridge/symfony-console/tests/Acceptance/EntrypointTest.php @@ -0,0 +1,149 @@ +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->linkComposerAutoloader($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 function installEntrypointAsComposerDependency(): string + { + $entrypoint = $this->installEntrypoint('vendor/testo/bridge-symfony-console/bin/testo'); + $vendor = $this->sandbox->path('vendor'); + + $this->linkComposerAutoloader($vendor); + $this->makeWorkingDirectory(); + + return $entrypoint; + } + + private function installEntrypointForLocalDevelopment(): string + { + $entrypoint = $this->installEntrypoint('bridge/symfony-console/bin/testo'); + + $this->linkComposerAutoloader($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 linkComposerAutoloader(string $vendor): void + { + $root = \dirname(__DIR__, 4); + + \is_dir($vendor) || \mkdir($vendor, 0o755, true) or throw new \RuntimeException("Cannot create {$vendor}"); + \symlink($root . '/vendor/autoload.php', $vendor . '/autoload.php') + or throw new \RuntimeException('Cannot link Composer autoloader.'); + \symlink($root . '/vendor/composer', $vendor . '/composer') + or throw new \RuntimeException('Cannot link Composer metadata.'); + } + + private function makeWorkingDirectory(): void + { + \mkdir($this->sandbox->path('run')) or throw new \RuntimeException('Cannot create isolated working directory.'); + } + + 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]; + } +} From 6ac37fe96b2b7556f38b8a02517cc29ff5953933 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 20 Sep 2026 01:46:24 +0400 Subject: [PATCH 2/2] test(bridge-symfony-console): install the autoloader stub instead of symlinking vendor symlink() needs elevated rights or Developer Mode on Windows, so the entrypoint tests errored there on every path. A one-line PHP file that requires the repository autoloader gives the entrypoint the same vendor/autoload.php, and the real autoloader finds its composer/ directory itself, so the second link was not needed either. Assisted-By: Claude Fable 5.1 --- .../tests/Acceptance/EntrypointTest.php | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/bridge/symfony-console/tests/Acceptance/EntrypointTest.php b/bridge/symfony-console/tests/Acceptance/EntrypointTest.php index 2afe3e52..e61b79e7 100644 --- a/bridge/symfony-console/tests/Acceptance/EntrypointTest.php +++ b/bridge/symfony-console/tests/Acceptance/EntrypointTest.php @@ -39,7 +39,7 @@ public function loadsTheAutoloaderFromTheCurrentWorkingDirectoryAsFallback(): vo { $entrypoint = $this->installEntrypointWithoutAdjacentAutoloaders(); $workingDirectory = $this->sandbox->path('run'); - $this->linkComposerAutoloader($workingDirectory . '/vendor'); + $this->installComposerAutoloader($workingDirectory . '/vendor'); [$exitCode, $stdout, $stderr] = self::runBinary($entrypoint, '--version', $workingDirectory); @@ -58,12 +58,43 @@ 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->linkComposerAutoloader($vendor); + $this->installComposerAutoloader($vendor); $this->makeWorkingDirectory(); return $entrypoint; @@ -73,7 +104,7 @@ private function installEntrypointForLocalDevelopment(): string { $entrypoint = $this->installEntrypoint('bridge/symfony-console/bin/testo'); - $this->linkComposerAutoloader($this->sandbox->path('bridge/symfony-console/vendor')); + $this->installComposerAutoloader($this->sandbox->path('bridge/symfony-console/vendor')); $this->makeWorkingDirectory(); return $entrypoint; @@ -100,50 +131,17 @@ private function installEntrypoint(string $relativePath): string return $entrypoint; } - private function linkComposerAutoloader(string $vendor): void + private function installComposerAutoloader(string $vendor): void { $root = \dirname(__DIR__, 4); \is_dir($vendor) || \mkdir($vendor, 0o755, true) or throw new \RuntimeException("Cannot create {$vendor}"); - \symlink($root . '/vendor/autoload.php', $vendor . '/autoload.php') - or throw new \RuntimeException('Cannot link Composer autoloader.'); - \symlink($root . '/vendor/composer', $vendor . '/composer') - or throw new \RuntimeException('Cannot link Composer metadata.'); + \file_put_contents($vendor . '/autoload.php', "sandbox->path('run')) or throw new \RuntimeException('Cannot create isolated working directory.'); } - - 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]; - } }