diff --git a/.github/integration/README.md b/.github/integration/README.md index 599bf906..50774379 100644 --- a/.github/integration/README.md +++ b/.github/integration/README.md @@ -4,14 +4,17 @@ This suite lives below `.github` because repository test fixtures with a `.php` suffix are intentionally ignored below `tests/`. It protects build-mode boundaries rather than duplicating the PHP syntax coverage in `tests/compiler`. -- `ext/lifecycle` builds a real Zend extension and loads it through CLI, - `php -S`, and PHP-FPM. The long-running hosts alternate implementations of - the same request-local class and function, while the extension also calls an - internal class and method. This protects request cache cleanup and persistent - cache reuse across repeated RINIT/RSHUTDOWN cycles. -- `lib` builds a provider library, consumes its generated `@import-library` - stub from a second TypePHP binary, links the two artifacts, and runs the - consumer. Both modes include a throwing `main()` declaration to verify that +- `ext/lifecycle` builds two real Zend extensions, loads both orders through + CLI, and uses opposite orders for `php -S` and PHP-FPM. The long-running hosts + alternate implementations of the same request-local class and function, + while both extensions also call an internal class and method. This protects + per-module cache isolation, shared PHPX lifecycle handling, request cache + cleanup, and persistent cache reuse across repeated RINIT/RSHUTDOWN cycles. +- `lib` builds two provider libraries, consumes both generated + `@import-library` stubs from one TypePHP binary, links all three artifacts, + and runs the consumer. The providers deliberately contain an identically + named private helper with different implementations to protect hidden-symbol + isolation. Both modes include throwing `main()` declarations to verify that only bin mode executes the entrypoint. Run from the repository root: diff --git a/.github/integration/ext/lifecycle/host/request.php b/.github/integration/ext/lifecycle/host/request.php index 79aec680..74855fde 100644 --- a/.github/integration/ext/lifecycle/host/request.php +++ b/.github/integration/ext/lifecycle/host/request.php @@ -52,6 +52,14 @@ echo json_encode([ typephp_integration_probe($request), typephp_integration_probe($request), ], + 'peer_results' => [ + typephp_integration_peer_probe($request), + typephp_integration_peer_probe($request), + ], + 'extensions_loaded' => [ + extension_loaded('typephp_integration_ext_primary'), + extension_loaded('typephp_integration_ext_peer'), + ], 'main_registered' => function_exists('main'), 'pid' => getmypid(), ], JSON_THROW_ON_ERROR); diff --git a/.github/integration/ext/lifecycle/src/peer-extension.php b/.github/integration/ext/lifecycle/src/peer-extension.php new file mode 100644 index 00000000..472f1ca5 --- /dev/null +++ b/.github/integration/ext/lifecycle/src/peer-extension.php @@ -0,0 +1,24 @@ +format('U') . '|' + . typephp_integration_request_transform($value->render()); +} + +// Both shared objects contain the same hidden generated php_main symbol. Only +// get_module and the module-specific Zend entry points may be visible outside +// their respective DSO. +function main(): void +{ + throw new RuntimeException('peer ext mode invoked bin main()'); +} diff --git a/.github/integration/lib/consumer/main.php b/.github/integration/lib/consumer/main.php index a8ef391a..ac51d30d 100644 --- a/.github/integration/lib/consumer/main.php +++ b/.github/integration/lib/consumer/main.php @@ -3,7 +3,9 @@ declare(strict_types=1); use TypePhpIntegration\Library\Counter; +use TypePhpIntegration\PeerLibrary\Label; use function TypePhpIntegration\Library\add; +use function TypePhpIntegration\PeerLibrary\scale; function main(): void { @@ -13,4 +15,7 @@ function main(): void $counter->add(3); $counter->add(4); echo 'counter=', $counter->value, "\n"; + + echo 'scaled=', scale(7), "\n"; + echo 'label=', (new Label('peer'))->render(), "\n"; } diff --git a/.github/integration/lib/peer-provider/project.yml b/.github/integration/lib/peer-provider/project.yml new file mode 100644 index 00000000..19b3c15d --- /dev/null +++ b/.github/integration/lib/peer-provider/project.yml @@ -0,0 +1,5 @@ +name: integration_peer +mode: lib +cxx-std: c++17 +sources: + - src diff --git a/.github/integration/lib/peer-provider/src/library.php b/.github/integration/lib/peer-provider/src/library.php new file mode 100644 index 00000000..532e8927 --- /dev/null +++ b/.github/integration/lib/peer-provider/src/library.php @@ -0,0 +1,40 @@ +value . ']'; + } + } +} diff --git a/.github/integration/lib/provider/src/library.php b/.github/integration/lib/provider/src/library.php index 62c7ce66..bf5b41b2 100644 --- a/.github/integration/lib/provider/src/library.php +++ b/.github/integration/lib/provider/src/library.php @@ -11,10 +11,20 @@ namespace { } } +namespace TypePhpIntegration\PrivateSupport { + // The peer provider defines the same non-exported symbol with a different + // implementation. Both DSOs must retain their own hidden copy. + #[\NoExport] + function adjust(int $value): int + { + return $value + 1; + } +} + namespace TypePhpIntegration\Library { function add(int $left, int $right): int { - return $left + $right; + return \TypePhpIntegration\PrivateSupport\adjust($left + $right - 1); } final class Counter diff --git a/bin/run-integration-tests.php b/bin/run-integration-tests.php index 24673048..21fbc2ea 100755 --- a/bin/run-integration-tests.php +++ b/bin/run-integration-tests.php @@ -222,6 +222,11 @@ function assertLifecycleBody(string $body, int $request, ?int &$expectedPid, str "1@{$request}|{$kind}-handler[{$kind}:{$request}]", "2@{$request}|{$kind}-handler[{$kind}:{$request}]", ], + 'peer_results' => [ + "peer-1@{$request}|{$kind}-handler[{$kind}:{$request}]", + "peer-2@{$request}|{$kind}-handler[{$kind}:{$request}]", + ], + 'extensions_loaded' => [true, true], 'main_registered' => false, ]; if ($actual !== $expected) { @@ -442,38 +447,63 @@ function requestFastCgi(int $port, string $script, int $request, float $timeout function runExtIntegration(array $options, string $temporaryRoot): void { - fwrite(STDOUT, "\n[EXT] build and Zend host lifecycle\n"); - $extension = $temporaryRoot . '/typephp_integration_ext.' . PHP_SHLIB_SUFFIX; - $buildDirectory = $temporaryRoot . '/ext-build'; - runIntegrationCommand([ - $options['compiler'], - TYPEPHP_INTEGRATION_TEST_ROOT . '/ext/lifecycle/src/extension.php', - '--mode', 'ext', - '--output', $extension, - '--build-dir', $buildDirectory, - '--job', '1', - '--no-progress', - ]); - assertIntegrationTrue(is_file($extension), 'Extension artifact was not generated: ' . $extension); + fwrite(STDOUT, "\n[EXT] build two modules and verify shared Zend host lifecycle\n"); + $extensions = []; + foreach ([ + 'primary' => 'extension.php', + 'peer' => 'peer-extension.php', + ] as $name => $source) { + $extension = $temporaryRoot . '/integration_ext_' . $name . '.' . PHP_SHLIB_SUFFIX; + runIntegrationCommand([ + $options['compiler'], + TYPEPHP_INTEGRATION_TEST_ROOT . '/ext/lifecycle/src/' . $source, + '--mode', 'ext', + '--output', $extension, + '--build-dir', $temporaryRoot . '/ext-build-' . $name, + '--job', '1', + '--no-progress', + ]); + assertIntegrationTrue(is_file($extension), 'Extension artifact was not generated: ' . $extension); + $extensions[$name] = $extension; + } $hostScript = realpath(TYPEPHP_INTEGRATION_TEST_ROOT . '/ext/lifecycle/host/request.php'); if ($hostScript === false) { throw new IntegrationFailure('Extension host script is missing'); } - for ($request = 1; $request <= 2; ++$request) { - $result = runIntegrationCommand([ - $options['php'], '-n', '-d', 'extension=' . $extension, $hostScript, - ], null, ['TYPEPHP_INTEGRATION_REQUEST' => (string) $request]); - $cliPid = null; - assertLifecycleBody(trim($result['stdout']), $request, $cliPid, 'CLI extension'); + foreach ([$extensions, array_reverse($extensions)] as $orderIndex => $extensionOrder) { + for ($request = 1; $request <= 2; ++$request) { + $command = [$options['php'], '-n']; + foreach ($extensionOrder as $extension) { + array_push($command, '-d', 'extension=' . $extension); + } + $command[] = $hostScript; + $result = runIntegrationCommand( + $command, + null, + ['TYPEPHP_INTEGRATION_REQUEST' => (string) $request], + ); + $cliPid = null; + assertLifecycleBody( + trim($result['stdout']), + $request, + $cliPid, + 'CLI extensions, load order ' . ($orderIndex + 1), + ); + } } $serverPort = reserveIntegrationPort(); - $server = startIntegrationProcess([ - $options['php'], '-n', '-d', 'extension=' . $extension, + $serverCommand = [$options['php'], '-n']; + foreach ($extensions as $extension) { + array_push($serverCommand, '-d', 'extension=' . $extension); + } + array_push( + $serverCommand, '-d', 'display_errors=1', '-S', "127.0.0.1:{$serverPort}", '-t', dirname($hostScript), - ]); + ); + $server = startIntegrationProcess($serverCommand); $serverLogs = ''; try { $first = waitForIntegrationServer($server, fn(): string => requestHttp($serverPort, 1)); @@ -507,11 +537,16 @@ clear_env = no catch_workers_output = yes INI); - $fpm = startIntegrationProcess([ - $options['php_fpm'], '-n', '-d', 'extension=' . $extension, + $fpmCommand = [$options['php_fpm'], '-n']; + foreach (array_reverse($extensions) as $extension) { + array_push($fpmCommand, '-d', 'extension=' . $extension); + } + array_push( + $fpmCommand, '-d', 'display_errors=1', '-d', 'log_errors=0', '-y', $fpmConfig, '-F', '-O', - ]); + ); + $fpm = startIntegrationProcess($fpmCommand); $fpmLogs = ''; try { $first = waitForIntegrationServer( @@ -555,48 +590,76 @@ function copyIntegrationTree(string $source, string $destination): void function runLibIntegration(array $options, string $temporaryRoot): void { - fwrite(STDOUT, "\n[LIB] provider/import stub/consumer boundary\n"); - $providerRoot = $temporaryRoot . '/provider'; - copyIntegrationTree(TYPEPHP_INTEGRATION_TEST_ROOT . '/lib/provider', $providerRoot); - runIntegrationCommand([ - $options['compiler'], $providerRoot . '/project.yml', - '--output', $providerRoot . '/integration_provider.' . PHP_SHLIB_SUFFIX, - '--build-dir', $providerRoot . '/build', '--job', '1', '--no-progress', - ]); - - $library = $providerRoot . '/integration_provider.' . PHP_SHLIB_SUFFIX; - $stub = $providerRoot . '/integration_provider.stub.php'; - assertIntegrationTrue(is_file($library), 'Library artifact was not generated: ' . $library); - assertIntegrationTrue(is_file($stub), 'Library import stub was not generated: ' . $stub); - $stubCode = file_get_contents($stub); - assertIntegrationTrue(is_string($stubCode) && str_contains($stubCode, '@import-library'), 'Invalid library stub'); - assertIntegrationTrue(!str_contains($stubCode, 'function main('), 'Library stub must not export bin main()'); + fwrite(STDOUT, "\n[LIB] two providers/import stubs/one consumer boundary\n"); + $providers = []; + foreach ([ + 'integration_provider' => 'provider', + 'integration_peer' => 'peer-provider', + ] as $target => $fixture) { + $providerRoot = $temporaryRoot . '/' . $fixture; + copyIntegrationTree(TYPEPHP_INTEGRATION_TEST_ROOT . '/lib/' . $fixture, $providerRoot); + runIntegrationCommand([ + $options['compiler'], $providerRoot . '/project.yml', + '--output', $providerRoot . '/' . $target . '.' . PHP_SHLIB_SUFFIX, + '--build-dir', $providerRoot . '/build', '--job', '1', '--no-progress', + ]); + + $library = $providerRoot . '/' . $target . '.' . PHP_SHLIB_SUFFIX; + $stub = $providerRoot . '/' . $target . '.stub.php'; + assertIntegrationTrue(is_file($library), 'Library artifact was not generated: ' . $library); + assertIntegrationTrue(is_file($stub), 'Library import stub was not generated: ' . $stub); + $stubCode = file_get_contents($stub); + assertIntegrationTrue( + is_string($stubCode) && str_contains($stubCode, '@import-library'), + 'Invalid library stub: ' . $stub, + ); + assertIntegrationTrue(!str_contains($stubCode, 'function main('), 'Library stub must not export bin main()'); + assertIntegrationTrue( + !str_contains($stubCode, 'PrivateSupport'), + 'Library stub exported a #[NoExport] private helper: ' . $stub, + ); + $providers[$target] = ['library' => $library, 'stub' => $stub]; + } - // The published stub automatically adds -lintegration_provider to its - // consumer. Keep the provider target name (and therefore its stub ABI name) - // independent from the Unix lib prefix used by the linker. - $linkLibrary = $providerRoot . '/libintegration_provider.' . PHP_SHLIB_SUFFIX; - if (!copy($library, $linkLibrary)) { - throw new IntegrationFailure('Cannot prepare linker-visible provider library'); + // Import stubs automatically add both -l options. Place both + // linker-visible names in one directory so the consumer exercises a real + // multi-library link rather than two independent executions. + $linkRoot = $temporaryRoot . '/lib-link'; + if (!mkdir($linkRoot, 0777, true) && !is_dir($linkRoot)) { + throw new IntegrationFailure('Cannot create library link directory: ' . $linkRoot); + } + foreach ($providers as $target => $provider) { + $linkLibrary = $linkRoot . '/lib' . $target . '.' . PHP_SHLIB_SUFFIX; + if (!copy($provider['library'], $linkLibrary)) { + throw new IntegrationFailure('Cannot prepare linker-visible provider library: ' . $target); + } } $consumerRoot = $temporaryRoot . '/consumer'; copyIntegrationTree(TYPEPHP_INTEGRATION_TEST_ROOT . '/lib/consumer', $consumerRoot); - copy($stub, $consumerRoot . '/integration_provider.stub.php'); + foreach ($providers as $target => $provider) { + if (!copy($provider['stub'], $consumerRoot . '/' . $target . '.stub.php')) { + throw new IntegrationFailure('Cannot prepare provider import stub: ' . $target); + } + } $consumer = $temporaryRoot . '/integration_consumer'; runIntegrationCommand([ $options['compiler'], $consumerRoot, '--mode', 'bin', '--output', $consumer, '--build-dir', $consumerRoot . '/build', - '--link-path', $providerRoot, + '--link-path', $linkRoot, '--job', '1', '--no-progress', ]); - $libraryPath = $providerRoot; + $libraryPath = $linkRoot; $environment = PHP_OS_FAMILY === 'Darwin' ? ['DYLD_LIBRARY_PATH' => $libraryPath . ':' . (getenv('DYLD_LIBRARY_PATH') ?: '')] : ['LD_LIBRARY_PATH' => $libraryPath . ':' . (getenv('LD_LIBRARY_PATH') ?: '')]; $result = runIntegrationCommand([$consumer], null, $environment); - assertIntegrationSame("42\ncounter=7\n", $result['stdout'], 'TypePHP library consumer returned unexpected output'); + assertIntegrationSame( + "42\ncounter=7\nscaled=21\nlabel=[peer]\n", + $result['stdout'], + 'TypePHP multi-library consumer returned unexpected output', + ); } function removeIntegrationTree(string $path): void