TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

540 lines
22 KiB

<?php
namespace TypePhp\Tests\Build;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use TypePhp\CompilerTest;
final class IncrementalDeclarationTest extends TestCase
{
private string $directory;
private string $buildDirectory;
private string $provider;
private string $consumer;
private string $independent;
protected function setUp(): void
{
$this->directory = sys_get_temp_dir() . '/typephp_incremental_' . bin2hex(random_bytes(8));
$this->buildDirectory = $this->directory . '/build';
mkdir($this->directory, 0777, true);
$this->provider = $this->directory . '/provider.php';
$this->consumer = $this->directory . '/consumer.php';
$this->independent = $this->directory . '/independent.php';
file_put_contents($this->provider, <<<'PHP'
<?php
namespace Incremental;
const LIMIT = 42;
function answer(): int
{
global $shared;
$shared = LIMIT;
return LIMIT;
}
PHP);
file_put_contents($this->independent, <<<'PHP'
<?php
function independent(): string
{
return 'stable literal';
}
PHP);
file_put_contents($this->consumer, <<<'PHP'
<?php
function main(): int
{
global $shared;
return \Incremental\answer() + \Incremental\LIMIT;
}
PHP);
}
protected function tearDown(): void
{
$this->removeDirectory($this->directory);
}
public function testDeclarationsAreSplitBySourceAndDependenciesAreIncluded(): void
{
$compiler = $this->convertProject();
$providerHeader = $compiler->getDeclarationHeaderFile($this->provider);
$consumerHeader = $compiler->getDeclarationHeaderFile($this->consumer);
$consumerCpp = $this->invoke($compiler, 'getCppFile', $this->consumer);
self::assertFileExists($providerHeader);
self::assertFileExists($consumerHeader);
self::assertFileExists($this->buildDirectory . '/include/php_incremental_runtime_decl.h');
self::assertFileExists($this->buildDirectory . '/include/php_incremental_all_decl.h');
self::assertFileDoesNotExist($this->buildDirectory . '/include/php_incremental_func_decl.h');
self::assertFileDoesNotExist($this->buildDirectory . '/include/php_incremental_data_decl.h');
$providerDeclarations = (string) file_get_contents($providerHeader);
$consumerDeclarations = (string) file_get_contents($consumerHeader);
$runtimeDeclarations = (string) file_get_contents(
$this->buildDirectory . '/include/php_incremental_runtime_decl.h',
);
$consumerCode = (string) file_get_contents($consumerCpp);
$extensionCode = (string) file_get_contents(
$this->buildDirectory . '/extension-incremental.cc',
);
self::assertStringContainsString('php_incremental__answer(', $providerDeclarations);
self::assertStringContainsString('_const_var_Incremental__LIMIT', $providerDeclarations);
self::assertStringContainsString('_global_var_shared', $providerDeclarations);
self::assertStringNotContainsString('_global_var_shared', $consumerDeclarations);
self::assertStringNotContainsString('_global_var_shared', $runtimeDeclarations);
self::assertStringNotContainsString('php_main(', $providerDeclarations);
self::assertStringContainsString('php_main(', $consumerDeclarations);
self::assertStringContainsString(
'#include <' . basename($providerHeader) . '>',
$consumerDeclarations,
);
self::assertStringContainsString(
'#include <' . basename($consumerHeader) . '>',
$consumerCode,
);
self::assertStringContainsString(
'#include <php_incremental_runtime_decl.h>',
$extensionCode,
);
self::assertStringContainsString(
'#include <' . basename($compiler->getArgInfoHeaderFile($this->provider)) . '>',
$extensionCode,
);
self::assertStringNotContainsString(
'#include <' . basename($providerHeader) . '>',
$extensionCode,
);
self::assertStringNotContainsString(
'#include <' . basename($consumerHeader) . '>',
$extensionCode,
);
$symbols = $this->property($compiler, 'symbolDeclInFile');
self::assertSame($this->provider, $symbols['function:incremental\\answer']);
self::assertSame($this->provider, $symbols['constant:Incremental\\LIMIT']);
}
public function testStdContainerParameterContractsSurviveWarmConversionAndInvalidateCallers(): void
{
file_put_contents($this->provider, <<<'PHP'
<?php
namespace Incremental;
function answer(#[\StdVector(\Type::Int)] $vec): int { return $vec[0]; }
PHP);
file_put_contents($this->consumer, <<<'PHP'
<?php
function main(): void {
$vec = \std::vector(\Type::Int);
$vec[] = 7;
var_dump(\Incremental\answer($vec));
}
PHP);
$first = $this->convertProject();
$providerCpp = $this->invoke($first, 'getCppFile', $this->provider);
$coldCode = file_get_contents($providerCpp);
self::assertStringContainsString('auto &vec_ref = php::toStdContainer<php::StdVector<php::Int>>', $coldCode);
$second = $this->convertProject();
self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider));
self::assertSame($coldCode, file_get_contents($providerCpp));
$function = $this->invoke($second, 'getFunction', 'incremental__answer');
self::assertSame('vector', $function->argInfoList[0]->stdContainer['kind']);
file_put_contents($this->provider, str_replace('Type::Int', 'Type::Float', file_get_contents($this->provider)));
$third = $this->convertProject();
self::assertTrue($this->invoke($third, 'shouldRegeneratePhpFile', $this->consumer));
self::assertStringContainsString('php::StdVector<php::Float>', file_get_contents($providerCpp));
}
public function testUnchangedGeneratedCppKeepsItsTimestamp(): void
{
$first = $this->convertProject();
$consumerCpp = $this->invoke($first, 'getCppFile', $this->consumer);
$oldTimestamp = 1_600_000_000;
touch($consumerCpp, $oldTimestamp);
clearstatcache(true, $consumerCpp);
$this->convertProject();
clearstatcache(true, $consumerCpp);
self::assertSame($oldTimestamp, filemtime($consumerCpp));
self::assertStringContainsString(
'get_str(uint32_t index)',
(string) file_get_contents($this->buildDirectory . '/include/php_incremental_runtime_decl.h'),
);
}
public function testGlobalArrayConstantInitializersSurviveColdAndWarmConversion(): void
{
file_put_contents($this->provider, <<<'PHP'
<?php
namespace Incremental;
const LIMIT = 42;
const FIRST = [[[LIMIT, 1]], [[2, 3]]];
const ENABLED = true;
const SECOND = [0 => ['left' => LIMIT], 'tail' => [2, [3]]];
function answer(): int { return FIRST[0][0][0] + SECOND[0]['left']; }
PHP);
$first = $this->convertProject();
$extension = $this->buildDirectory . '/extension-incremental.cc';
$coldCode = file_get_contents($extension);
self::assertStringContainsString('php::Array tmp_var_', $coldCode);
$coldConstants = $this->property($first, 'constants');
self::assertSame(\TypePhp\Type::BOOL, $coldConstants['_const_var_Incremental__ENABLED']->type);
$second = $this->convertProject();
self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider));
self::assertSame($coldCode, file_get_contents($extension));
$warmConstants = $this->property($second, 'constants');
self::assertSame($coldConstants['_const_var_Incremental__LIMIT']->type, $warmConstants['_const_var_Incremental__LIMIT']->type);
self::assertSame(\TypePhp\Type::BOOL, $warmConstants['_const_var_Incremental__ENABLED']->type);
}
public function testGeneratorFingerprintChangeKeepsIdenticalCppTimestamps(): void
{
$first = $this->convertProject();
$generatedFiles = [
$this->invoke($first, 'getCppFile', $this->provider),
$this->invoke($first, 'getCppFile', $this->consumer),
$this->invoke($first, 'getCppFile', $this->independent),
];
$oldTimestamp = 1_600_000_000;
foreach ($generatedFiles as $generatedFile) {
touch($generatedFile, $oldTimestamp);
}
$stateFile = $this->buildDirectory . '/cache/incremental/incremental/build-state.json';
$state = json_decode((string) file_get_contents($stateFile), true, flags: JSON_THROW_ON_ERROR);
$state['generatorFingerprint'] = str_repeat('0', 64);
file_put_contents(
$stateFile,
json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL,
);
clearstatcache();
$this->convertProject();
clearstatcache();
foreach ($generatedFiles as $generatedFile) {
self::assertSame($oldTimestamp, filemtime($generatedFile));
}
}
public function testRegenerationKeepsTimestampsForIdenticalGeneratedContents(): void
{
$first = $this->convertProject();
$providerCpp = $this->invoke($first, 'getCppFile', $this->provider);
$providerHeader = $first->getDeclarationHeaderFile($this->provider);
$consumerCpp = $this->invoke($first, 'getCppFile', $this->consumer);
$independentCpp = $this->invoke($first, 'getCppFile', $this->independent);
$oldTimestamp = 1_600_000_000;
foreach ([$providerCpp, $providerHeader, $consumerCpp, $independentCpp] as $generatedFile) {
touch($generatedFile, $oldTimestamp);
}
$providerSource = (string) file_get_contents($this->provider);
file_put_contents($this->provider, str_replace('const LIMIT = 42;', 'const LIMIT = 43;', $providerSource));
clearstatcache();
$this->convertProject();
clearstatcache();
self::assertGreaterThan($oldTimestamp, filemtime($providerHeader));
self::assertSame($oldTimestamp, filemtime($providerCpp));
// The dependency graph still regenerates the consumer, but its own C++
// bytes only include the provider header by its stable name. Preserve
// the timestamp; the changed header content invalidates its object key.
self::assertSame($oldTimestamp, filemtime($consumerCpp));
self::assertSame($oldTimestamp, filemtime($independentCpp));
}
public function testCachedClassKeepsRuntimeArrayPropertyDefaultAllocator(): void
{
file_put_contents($this->independent, <<<'PHP'
<?php
final class IncrementalDefaults
{
protected array $values = ['ready'];
}
PHP);
$this->convertProject();
$this->convertProject();
$extensionCode = (string) file_get_contents(
$this->buildDirectory . '/extension-incremental.cc',
);
$initializer = 'typephp_ensure_request_array_defaults_IncrementalDefaults()';
self::assertSame(2, substr_count($extensionCode, $initializer));
self::assertStringContainsString(
'php_class_entry_IncrementalDefaults->create_object =',
$extensionCode,
);
}
public function testGeneratedObjectCacheUsesContentRatherThanSourceTimestamp(): void
{
$compiler = $this->convertProject();
$consumerCpp = $this->invoke($compiler, 'getCppFile', $this->consumer);
$consumerObject = $compiler->getObjectFile($consumerCpp);
$sourceTimestamp = 1_600_000_000;
$objectTimestamp = $sourceTimestamp + 10;
file_put_contents($consumerObject, 'object');
touch($consumerCpp, $sourceTimestamp);
touch($consumerObject, $objectTimestamp);
clearstatcache();
$this->invoke($compiler, 'writeGeneratedObjectCacheMetadata', $consumerCpp, $consumerObject);
self::assertTrue(
$this->invoke($compiler, 'hasGeneratedObjectFileCache', $consumerCpp, $consumerObject),
);
touch($consumerCpp, $objectTimestamp);
clearstatcache();
self::assertTrue(
$this->invoke($compiler, 'hasGeneratedObjectFileCache', $consumerCpp, $consumerObject),
);
touch($consumerCpp, $objectTimestamp + 1);
clearstatcache();
self::assertTrue(
$this->invoke($compiler, 'hasGeneratedObjectFileCache', $consumerCpp, $consumerObject),
);
file_put_contents($consumerCpp, "\n// changed generated input\n", FILE_APPEND);
clearstatcache();
self::assertFalse(
$this->invoke($compiler, 'hasGeneratedObjectFileCache', $consumerCpp, $consumerObject),
);
unlink($consumerObject);
clearstatcache();
self::assertFalse(
$this->invoke($compiler, 'hasGeneratedObjectFileCache', $consumerCpp, $consumerObject),
);
file_put_contents($consumerObject, 'object');
touch($consumerObject, $objectTimestamp);
unlink($consumerCpp);
clearstatcache();
self::assertFalse(
$this->invoke($compiler, 'hasGeneratedObjectFileCache', $consumerCpp, $consumerObject),
);
}
public function testLinkCacheRequiresEveryObjectAndAnUpToDateTarget(): void
{
$compiler = $this->newCompiler();
$target = $this->directory . '/incremental-bin';
$firstObject = $this->directory . '/first.o';
$secondObject = $this->directory . '/second.o';
$timestamp = 1_600_000_000;
foreach ([$target, $firstObject, $secondObject] as $file) {
file_put_contents($file, 'artifact');
touch($file, $timestamp);
}
clearstatcache();
$objects = [$firstObject, $secondObject];
$this->invoke($compiler, 'writeLinkCache', $objects, $target);
self::assertFileExists(
$this->buildDirectory . '/incremental-bin.typephp-link-cache',
);
self::assertFileDoesNotExist($target . '.typephp-link-cache');
self::assertTrue($this->invoke($compiler, 'hasLinkCache', $objects, $target));
touch($secondObject, $timestamp + 1);
clearstatcache();
self::assertFalse($this->invoke($compiler, 'hasLinkCache', $objects, $target));
unlink($secondObject);
clearstatcache();
self::assertFalse($this->invoke($compiler, 'hasLinkCache', $objects, $target));
}
public function testForceCacheClearRemovesAstAndTargetIncrementalState(): void
{
$compiler = $this->newCompiler();
$astDirectory = $this->buildDirectory . '/cache/ast';
$incrementalDirectory = $this->buildDirectory . '/cache/incremental/incremental';
mkdir($astDirectory, 0777, true);
mkdir($incrementalDirectory, 0777, true);
file_put_contents($astDirectory . '/entry.ast', 'ast');
file_put_contents($incrementalDirectory . '/stable-ids.json', '{}');
$this->invoke($compiler, 'clearIncrementalBuildCache');
self::assertDirectoryDoesNotExist($astDirectory);
self::assertDirectoryDoesNotExist($incrementalDirectory);
}
private function prepareNativeStub(): void
{
$stub = $this->directory . '/provider.stub.php';
rename($this->provider, $stub);
$this->provider = $stub;
file_put_contents($stub, '<?php namespace Incremental; function answer(int $value = 42): int {}');
file_put_contents($this->consumer, '<?php function main(): int { return \\Incremental\\answer(); }');
}
public function testStubDeclarationsAreGeneratedWithoutConvertingStubBodies(): void
{
$this->prepareNativeStub();
$first = $this->convertProject();
$header = $first->getDeclarationHeaderFile($this->provider);
self::assertFileExists($header);
self::assertStringContainsString('php_incremental__answer(', file_get_contents($header));
self::assertStringContainsString('#include <' . basename($header) . '>',
file_get_contents($first->getDeclarationHeaderFile($this->consumer)));
self::assertFileDoesNotExist($this->invoke($first, 'getCppFile', $this->provider));
self::assertFileExists($first->getArgInfoHeaderFile($this->provider));
$extension = $this->buildDirectory . '/extension-incremental.cc';
$extensionCode = file_get_contents($extension);
self::assertStringContainsString('ZEND_FUNCTION(incremental_answer)', $extensionCode);
self::assertStringContainsString('#include <' . basename($first->getArgInfoHeaderFile($this->provider)) . '>', $extensionCode);
$state = json_decode(file_get_contents($this->buildDirectory . '/cache/incremental/incremental/build-state.json'), true);
self::assertFalse($state['files'][$this->provider]['emitsTranslationUnit']);
self::assertContains($this->provider, $state['files'][$this->consumer]['dependencies']);
$second = $this->convertProject();
self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider));
self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
self::assertSame($extensionCode, file_get_contents($extension));
}
public function testStubChangePreservingMtimeInvalidatesTheConsumer(): void
{
$this->prepareNativeStub();
$this->convertProject();
$extension = $this->buildDirectory . '/extension-incremental.cc';
$oldCode = file_get_contents($extension);
$mtime = filemtime($this->provider);
file_put_contents($this->provider, str_replace('42', '43', file_get_contents($this->provider)));
touch($this->provider, $mtime);
clearstatcache();
$second = $this->convertProject();
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider));
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
self::assertNotSame($oldCode, file_get_contents($extension));
self::assertStringContainsString('43', file_get_contents($extension));
}
public function testMissingStubHeaderIsRegeneratedAndInvalidatesConsumers(): void
{
$this->prepareNativeStub();
$first = $this->convertProject();
unlink($first->getDeclarationHeaderFile($this->provider));
$second = $this->convertProject();
self::assertFileExists($second->getDeclarationHeaderFile($this->provider));
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
}
public function testMissingStubArginfoIsRegeneratedAndInvalidatesConsumers(): void
{
$this->prepareNativeStub();
$first = $this->convertProject();
unlink($first->getArgInfoHeaderFile($this->provider));
$second = $this->convertProject();
self::assertFileExists($second->getArgInfoHeaderFile($this->provider));
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
}
public function testImportedClassMetadataAndCallbacksStayInTheExtension(): void
{
$this->prepareNativeStub();
file_put_contents($this->provider, <<<'PHP'
<?php
/** @import-library */
namespace Incremental;
function answer(int $value = 42): int {}
final class Counter
{
public int $value = 0;
public function add(int $delta): int {}
}
PHP);
file_put_contents($this->consumer, <<<'PHP'
<?php
function main(): int
{
$counter = new \Incremental\Counter();
return $counter->add(1) + \Incremental\answer();
}
PHP);
$first = $this->convertProject();
$arginfo = $first->getArgInfoHeaderFile($this->provider);
$extension = $this->buildDirectory . '/extension-incremental.cc';
$extensionCode = file_get_contents($extension);
self::assertStringContainsString('php_register_class_Incremental_Counter', file_get_contents($arginfo));
self::assertStringContainsString('ZEND_METHOD(Incremental_Counter, add)', $extensionCode);
self::assertStringContainsString('php_incremental__counter__add(this_, arg_delta)', $extensionCode);
self::assertStringContainsString('#include <' . basename($arginfo) . '>', $extensionCode);
self::assertStringNotContainsString(basename($arginfo), file_get_contents($this->invoke($first, 'getCppFile', $this->consumer)));
self::assertFileDoesNotExist($this->invoke($first, 'getCppFile', $this->provider));
$this->convertProject();
self::assertSame($extensionCode, file_get_contents($extension));
}
private function convertProject(): CompilerTest
{
global $translator;
$compiler = $this->newCompiler();
$translator = $compiler;
$files = [$this->provider, $this->consumer, $this->independent];
$compiler->addFiles([$this->directory]);
foreach ($files as $file) {
$compiler->prepareFile($file);
}
$files = $compiler->getSortedFiles($files);
$this->invoke($compiler, 'initializeIncrementalCompilation', $files);
$compiler->convert($files);
return $compiler;
}
private function newCompiler(): CompilerTest
{
$compiler = CompilerTest::create($this->directory);
// Exercise the same on-disk stable-ID registry used by real builds.
$reflection = new ReflectionClass($compiler);
$forTest = $reflection->getProperty('forTest');
$forTest->setAccessible(true);
$forTest->setValue($compiler, false);
$compiler->setTargetName('incremental');
$this->invoke($compiler, 'setBuildDir', $this->buildDirectory);
return $compiler;
}
private function invoke(CompilerTest $compiler, string $method, mixed ...$arguments): mixed
{
$reflection = new ReflectionClass($compiler);
$methodReflection = $reflection->getMethod($method);
$methodReflection->setAccessible(true);
return $methodReflection->invoke($compiler, ...$arguments);
}
private function property(CompilerTest $compiler, string $name): mixed
{
$reflection = new ReflectionClass($compiler);
$property = $reflection->getProperty($name);
$property->setAccessible(true);
return $property->getValue($compiler);
}
private function removeDirectory(string $directory): void
{
if (!is_dir($directory)) {
return;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($iterator as $entry) {
$entry->isDir() ? rmdir($entry->getPathname()) : unlink($entry->getPathname());
}
rmdir($directory);
}
}