feat(integration): add peer library and extension for symbol isolation testing

- Add peer-provider library with PrivateSupport namespace and adjust function
- Modify provider library to use PrivateSupport adjust function with different impl
- Add peer extension with probe function and isolated request cache handling
- Update integration tests to build and load both primary and peer modules
- Configure test runner to verify both load orders in CLI, php -S and PHP-FPM
- Extend consumer to use both provider libraries with scale and label functions
- Add
master
韩天峰 2 days ago
parent 718c3dad25
commit 1061848f97
  1. 19
      .github/integration/README.md
  2. 8
      .github/integration/ext/lifecycle/host/request.php
  3. 24
      .github/integration/ext/lifecycle/src/peer-extension.php
  4. 5
      .github/integration/lib/consumer/main.php
  5. 5
      .github/integration/lib/peer-provider/project.yml
  6. 40
      .github/integration/lib/peer-provider/src/library.php
  7. 12
      .github/integration/lib/provider/src/library.php
  8. 165
      bin/run-integration-tests.php

@ -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 suffix are intentionally ignored below `tests/`. It protects build-mode
boundaries rather than duplicating the PHP syntax coverage in `tests/compiler`. boundaries rather than duplicating the PHP syntax coverage in `tests/compiler`.
- `ext/lifecycle` builds a real Zend extension and loads it through CLI, - `ext/lifecycle` builds two real Zend extensions, loads both orders through
`php -S`, and PHP-FPM. The long-running hosts alternate implementations of CLI, and uses opposite orders for `php -S` and PHP-FPM. The long-running hosts
the same request-local class and function, while the extension also calls an alternate implementations of the same request-local class and function,
internal class and method. This protects request cache cleanup and persistent while both extensions also call an internal class and method. This protects
cache reuse across repeated RINIT/RSHUTDOWN cycles. per-module cache isolation, shared PHPX lifecycle handling, request cache
- `lib` builds a provider library, consumes its generated `@import-library` cleanup, and persistent cache reuse across repeated RINIT/RSHUTDOWN cycles.
stub from a second TypePHP binary, links the two artifacts, and runs the - `lib` builds two provider libraries, consumes both generated
consumer. Both modes include a throwing `main()` declaration to verify that `@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. only bin mode executes the entrypoint.
Run from the repository root: Run from the repository root:

@ -52,6 +52,14 @@ echo json_encode([
typephp_integration_probe($request), typephp_integration_probe($request),
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'), 'main_registered' => function_exists('main'),
'pid' => getmypid(), 'pid' => getmypid(),
], JSON_THROW_ON_ERROR); ], JSON_THROW_ON_ERROR);

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
function typephp_integration_peer_probe(int $request): string
{
static $calls = 0;
++$calls;
// Resolve the same request-local symbols as the primary extension. Each
// module must keep its own cache slots while sharing the PHPX request.
$date = new DateTimeImmutable('@' . $request);
$value = new TypePhpIntegrationRequestValue($request);
return 'peer-' . $calls . '@' . $date->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()');
}

@ -3,7 +3,9 @@
declare(strict_types=1); declare(strict_types=1);
use TypePhpIntegration\Library\Counter; use TypePhpIntegration\Library\Counter;
use TypePhpIntegration\PeerLibrary\Label;
use function TypePhpIntegration\Library\add; use function TypePhpIntegration\Library\add;
use function TypePhpIntegration\PeerLibrary\scale;
function main(): void function main(): void
{ {
@ -13,4 +15,7 @@ function main(): void
$counter->add(3); $counter->add(3);
$counter->add(4); $counter->add(4);
echo 'counter=', $counter->value, "\n"; echo 'counter=', $counter->value, "\n";
echo 'scaled=', scale(7), "\n";
echo 'label=', (new Label('peer'))->render(), "\n";
} }

@ -0,0 +1,5 @@
name: integration_peer
mode: lib
cxx-std: c++17
sources:
- src

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace {
function main(): void
{
throw new RuntimeException('peer lib mode invoked bin main()');
}
}
namespace TypePhpIntegration\PrivateSupport {
// The primary provider deliberately defines the same hidden PHP/C++ symbol.
// Linking both libraries verifies that private implementation symbols bind
// locally instead of being interposed by the other provider.
#[\NoExport]
function adjust(int $value): int
{
return $value * 3;
}
}
namespace TypePhpIntegration\PeerLibrary {
function scale(int $value): int
{
return \TypePhpIntegration\PrivateSupport\adjust($value);
}
final class Label
{
public function __construct(private string $value)
{
}
public function render(): string
{
return '[' . $this->value . ']';
}
}
}

@ -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 { namespace TypePhpIntegration\Library {
function add(int $left, int $right): int function add(int $left, int $right): int
{ {
return $left + $right; return \TypePhpIntegration\PrivateSupport\adjust($left + $right - 1);
} }
final class Counter final class Counter

@ -222,6 +222,11 @@ function assertLifecycleBody(string $body, int $request, ?int &$expectedPid, str
"1@{$request}|{$kind}-handler[{$kind}:{$request}]", "1@{$request}|{$kind}-handler[{$kind}:{$request}]",
"2@{$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, 'main_registered' => false,
]; ];
if ($actual !== $expected) { if ($actual !== $expected) {
@ -442,38 +447,63 @@ function requestFastCgi(int $port, string $script, int $request, float $timeout
function runExtIntegration(array $options, string $temporaryRoot): void function runExtIntegration(array $options, string $temporaryRoot): void
{ {
fwrite(STDOUT, "\n[EXT] build and Zend host lifecycle\n"); fwrite(STDOUT, "\n[EXT] build two modules and verify shared Zend host lifecycle\n");
$extension = $temporaryRoot . '/typephp_integration_ext.' . PHP_SHLIB_SUFFIX; $extensions = [];
$buildDirectory = $temporaryRoot . '/ext-build'; foreach ([
runIntegrationCommand([ 'primary' => 'extension.php',
$options['compiler'], 'peer' => 'peer-extension.php',
TYPEPHP_INTEGRATION_TEST_ROOT . '/ext/lifecycle/src/extension.php', ] as $name => $source) {
'--mode', 'ext', $extension = $temporaryRoot . '/integration_ext_' . $name . '.' . PHP_SHLIB_SUFFIX;
'--output', $extension, runIntegrationCommand([
'--build-dir', $buildDirectory, $options['compiler'],
'--job', '1', TYPEPHP_INTEGRATION_TEST_ROOT . '/ext/lifecycle/src/' . $source,
'--no-progress', '--mode', 'ext',
]); '--output', $extension,
assertIntegrationTrue(is_file($extension), 'Extension artifact was not generated: ' . $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'); $hostScript = realpath(TYPEPHP_INTEGRATION_TEST_ROOT . '/ext/lifecycle/host/request.php');
if ($hostScript === false) { if ($hostScript === false) {
throw new IntegrationFailure('Extension host script is missing'); throw new IntegrationFailure('Extension host script is missing');
} }
for ($request = 1; $request <= 2; ++$request) { foreach ([$extensions, array_reverse($extensions)] as $orderIndex => $extensionOrder) {
$result = runIntegrationCommand([ for ($request = 1; $request <= 2; ++$request) {
$options['php'], '-n', '-d', 'extension=' . $extension, $hostScript, $command = [$options['php'], '-n'];
], null, ['TYPEPHP_INTEGRATION_REQUEST' => (string) $request]); foreach ($extensionOrder as $extension) {
$cliPid = null; array_push($command, '-d', 'extension=' . $extension);
assertLifecycleBody(trim($result['stdout']), $request, $cliPid, 'CLI 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(); $serverPort = reserveIntegrationPort();
$server = startIntegrationProcess([ $serverCommand = [$options['php'], '-n'];
$options['php'], '-n', '-d', 'extension=' . $extension, foreach ($extensions as $extension) {
array_push($serverCommand, '-d', 'extension=' . $extension);
}
array_push(
$serverCommand,
'-d', 'display_errors=1', '-S', "127.0.0.1:{$serverPort}", '-d', 'display_errors=1', '-S', "127.0.0.1:{$serverPort}",
'-t', dirname($hostScript), '-t', dirname($hostScript),
]); );
$server = startIntegrationProcess($serverCommand);
$serverLogs = ''; $serverLogs = '';
try { try {
$first = waitForIntegrationServer($server, fn(): string => requestHttp($serverPort, 1)); $first = waitForIntegrationServer($server, fn(): string => requestHttp($serverPort, 1));
@ -507,11 +537,16 @@ clear_env = no
catch_workers_output = yes catch_workers_output = yes
INI); INI);
$fpm = startIntegrationProcess([ $fpmCommand = [$options['php_fpm'], '-n'];
$options['php_fpm'], '-n', '-d', 'extension=' . $extension, foreach (array_reverse($extensions) as $extension) {
array_push($fpmCommand, '-d', 'extension=' . $extension);
}
array_push(
$fpmCommand,
'-d', 'display_errors=1', '-d', 'log_errors=0', '-d', 'display_errors=1', '-d', 'log_errors=0',
'-y', $fpmConfig, '-F', '-O', '-y', $fpmConfig, '-F', '-O',
]); );
$fpm = startIntegrationProcess($fpmCommand);
$fpmLogs = ''; $fpmLogs = '';
try { try {
$first = waitForIntegrationServer( $first = waitForIntegrationServer(
@ -555,48 +590,76 @@ function copyIntegrationTree(string $source, string $destination): void
function runLibIntegration(array $options, string $temporaryRoot): void function runLibIntegration(array $options, string $temporaryRoot): void
{ {
fwrite(STDOUT, "\n[LIB] provider/import stub/consumer boundary\n"); fwrite(STDOUT, "\n[LIB] two providers/import stubs/one consumer boundary\n");
$providerRoot = $temporaryRoot . '/provider'; $providers = [];
copyIntegrationTree(TYPEPHP_INTEGRATION_TEST_ROOT . '/lib/provider', $providerRoot); foreach ([
runIntegrationCommand([ 'integration_provider' => 'provider',
$options['compiler'], $providerRoot . '/project.yml', 'integration_peer' => 'peer-provider',
'--output', $providerRoot . '/integration_provider.' . PHP_SHLIB_SUFFIX, ] as $target => $fixture) {
'--build-dir', $providerRoot . '/build', '--job', '1', '--no-progress', $providerRoot = $temporaryRoot . '/' . $fixture;
]); copyIntegrationTree(TYPEPHP_INTEGRATION_TEST_ROOT . '/lib/' . $fixture, $providerRoot);
runIntegrationCommand([
$library = $providerRoot . '/integration_provider.' . PHP_SHLIB_SUFFIX; $options['compiler'], $providerRoot . '/project.yml',
$stub = $providerRoot . '/integration_provider.stub.php'; '--output', $providerRoot . '/' . $target . '.' . PHP_SHLIB_SUFFIX,
assertIntegrationTrue(is_file($library), 'Library artifact was not generated: ' . $library); '--build-dir', $providerRoot . '/build', '--job', '1', '--no-progress',
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'); $library = $providerRoot . '/' . $target . '.' . PHP_SHLIB_SUFFIX;
assertIntegrationTrue(!str_contains($stubCode, 'function main('), 'Library stub must not export bin main()'); $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 // Import stubs automatically add both -l<target> options. Place both
// consumer. Keep the provider target name (and therefore its stub ABI name) // linker-visible names in one directory so the consumer exercises a real
// independent from the Unix lib prefix used by the linker. // multi-library link rather than two independent executions.
$linkLibrary = $providerRoot . '/libintegration_provider.' . PHP_SHLIB_SUFFIX; $linkRoot = $temporaryRoot . '/lib-link';
if (!copy($library, $linkLibrary)) { if (!mkdir($linkRoot, 0777, true) && !is_dir($linkRoot)) {
throw new IntegrationFailure('Cannot prepare linker-visible provider library'); 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'; $consumerRoot = $temporaryRoot . '/consumer';
copyIntegrationTree(TYPEPHP_INTEGRATION_TEST_ROOT . '/lib/consumer', $consumerRoot); 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'; $consumer = $temporaryRoot . '/integration_consumer';
runIntegrationCommand([ runIntegrationCommand([
$options['compiler'], $consumerRoot, $options['compiler'], $consumerRoot,
'--mode', 'bin', '--output', $consumer, '--mode', 'bin', '--output', $consumer,
'--build-dir', $consumerRoot . '/build', '--build-dir', $consumerRoot . '/build',
'--link-path', $providerRoot, '--link-path', $linkRoot,
'--job', '1', '--no-progress', '--job', '1', '--no-progress',
]); ]);
$libraryPath = $providerRoot; $libraryPath = $linkRoot;
$environment = PHP_OS_FAMILY === 'Darwin' $environment = PHP_OS_FAMILY === 'Darwin'
? ['DYLD_LIBRARY_PATH' => $libraryPath . ':' . (getenv('DYLD_LIBRARY_PATH') ?: '')] ? ['DYLD_LIBRARY_PATH' => $libraryPath . ':' . (getenv('DYLD_LIBRARY_PATH') ?: '')]
: ['LD_LIBRARY_PATH' => $libraryPath . ':' . (getenv('LD_LIBRARY_PATH') ?: '')]; : ['LD_LIBRARY_PATH' => $libraryPath . ':' . (getenv('LD_LIBRARY_PATH') ?: '')];
$result = runIntegrationCommand([$consumer], null, $environment); $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 function removeIntegrationTree(string $path): void

Loading…
Cancel
Save