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.
 
 

938 lines
36 KiB

<?php
namespace PhpAot\Tests;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\CompilerTest;
use PhpAot\Php\CompilerBase;
use PhpAot\Php\Platform\Windows;
class CompilerBaseApiTest extends TestCase
{
private string $testDir;
private CompilerTest $compiler;
private \ReflectionClass $ref;
private array $originalArgv;
private string|false $originalPath;
protected function setUp(): void
{
parent::setUp();
global $argv;
$this->originalArgv = $argv ?? [];
$this->originalPath = getenv('PATH');
$this->testDir = sys_get_temp_dir() . '/compiler_api_test_' . uniqid();
mkdir($this->testDir, 0777, true);
$this->compiler = CompilerTest::create($this->testDir);
$this->ref = new \ReflectionClass($this->compiler);
}
protected function tearDown(): void
{
parent::tearDown();
global $argv;
$argv = $this->originalArgv;
if ($this->originalPath === false) {
putenv('PATH');
} else {
putenv('PATH=' . $this->originalPath);
}
// Recursively remove the test directory (compiler creates build/ subdir)
$this->removeDirectory($this->testDir);
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . DIRECTORY_SEPARATOR . $file;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
private function getPropertyValue(string $name): mixed
{
$prop = $this->ref->getProperty($name);
$prop->setAccessible(true);
return $prop->getValue($this->compiler);
}
private function setPropertyValue(string $name, mixed $value): void
{
$prop = $this->ref->getProperty($name);
$prop->setAccessible(true);
$prop->setValue($this->compiler, $value);
}
private function invokeMethod(string $method, ...$args): mixed
{
$m = $this->ref->getMethod($method);
$m->setAccessible(true);
return $m->invoke($this->compiler, ...$args);
}
private function fixturePath(string $file): string
{
return __DIR__ . '/../code/compiler_api/' . $file;
}
private function createProjectFile(string $yaml, string $filename = 'project.yml', string $baseDir = ''): string
{
$projectDir = $baseDir === '' ? $this->testDir : $this->testDir . '/' . trim($baseDir, '/');
if (!is_dir($projectDir)) {
mkdir($projectDir, 0777, true);
}
$sourceFile = $projectDir . '/main.php';
copy($this->fixturePath('main.php'), $sourceFile);
$projectFile = $projectDir . '/' . $filename;
file_put_contents($projectFile, $yaml);
return $projectFile;
}
private function createFakeClangFormat(string $binDir, string $logFile): void
{
mkdir($binDir, 0777, true);
file_put_contents($binDir . '/clang-format', "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then\n echo 'clang-format version test'\n exit 0\nfi\npwd > " . escapeshellarg($logFile) . "\nprintf '%s\\n' \"$@\" >> " . escapeshellarg($logFile) . "\n");
chmod($binDir . '/clang-format', 0755);
}
// ========================================================================
// getTypeFromZendType
// ========================================================================
public function testGetTypeFromZendTypeKnown(): void
{
$this->assertEquals(CompilerBase::TYPE_INT, $this->compiler->getTypeFromZendType('int'));
$this->assertEquals(CompilerBase::TYPE_FLOAT, $this->compiler->getTypeFromZendType('float'));
$this->assertEquals(CompilerBase::TYPE_BOOL, $this->compiler->getTypeFromZendType('bool'));
$this->assertEquals(CompilerBase::TYPE_BOOL, $this->compiler->getTypeFromZendType('true'));
$this->assertEquals(CompilerBase::TYPE_BOOL, $this->compiler->getTypeFromZendType('false'));
$this->assertEquals(CompilerBase::TYPE_VOID, $this->compiler->getTypeFromZendType('void'));
$this->assertEquals(CompilerBase::TYPE_VOID, $this->compiler->getTypeFromZendType('never'));
$this->assertEquals(CompilerBase::TYPE_STR, $this->compiler->getTypeFromZendType('string'));
$this->assertEquals(CompilerBase::TYPE_ARRAY, $this->compiler->getTypeFromZendType('array'));
$this->assertEquals(CompilerBase::TYPE_OBJECT, $this->compiler->getTypeFromZendType('object'));
$this->assertEquals(CompilerBase::TYPE_VAR, $this->compiler->getTypeFromZendType('mixed'));
$this->assertEquals(CompilerBase::TYPE_VAR, $this->compiler->getTypeFromZendType('null'));
$this->assertEquals(CompilerBase::TYPE_VAR, $this->compiler->getTypeFromZendType('callable'));
$this->assertEquals(CompilerBase::TYPE_VAR, $this->compiler->getTypeFromZendType('iterable'));
}
public function testGetTypeFromZendTypeUnknown(): void
{
$this->assertEquals(CompilerBase::TYPE_VAR, $this->compiler->getTypeFromZendType('unknown_type'));
$this->assertEquals(CompilerBase::TYPE_VAR, $this->compiler->getTypeFromZendType('SomeClass'));
}
// ========================================================================
// genTmpVarName
// ========================================================================
public function testGenTmpVarName(): void
{
// context must be initialized before genTmpVarName can be used
$this->invokeMethod('resetFunction');
$name1 = $this->compiler->genTmpVarName();
$name2 = $this->compiler->genTmpVarName();
$name3 = $this->compiler->genTmpVarName();
$this->assertStringStartsWith('tmp_var_', $name1);
$this->assertStringStartsWith('tmp_var_', $name2);
$this->assertStringStartsWith('tmp_var_', $name3);
// Must be sequential and unique
$this->assertNotEquals($name1, $name2);
$this->assertNotEquals($name2, $name3);
$this->assertNotEquals($name1, $name3);
}
public function testWindowsIntegerLiteralSuffixForGeneratedCValues(): void
{
$this->setPropertyValue('platform', new Windows());
$this->assertSame('42LL', $this->invokeMethod('genCValue', 42));
$this->assertSame('-42LL', $this->invokeMethod('genCValue', -42));
$this->assertSame('ZEND_LONG_MAX', $this->invokeMethod('genCValue', PHP_INT_MAX));
$this->assertSame('ZEND_LONG_MIN', $this->invokeMethod('genCValue', PHP_INT_MIN));
}
public function testWindowsIntegerLiteralSuffixForInternalConstants(): void
{
$this->setPropertyValue('platform', new Windows());
$this->assertSame(PHP_INT_SIZE . 'LL', $this->compiler->getConstValue('PHP_INT_SIZE'));
$this->assertSame('ZEND_LONG_MAX', $this->compiler->getConstValue('PHP_INT_MAX'));
$this->assertSame('ZEND_LONG_MIN', $this->compiler->getConstValue('PHP_INT_MIN'));
}
// ========================================================================
// genAnonClassName
// ========================================================================
public function testGenAnonClassName(): void
{
$name1 = $this->compiler->genAnonClassName();
$name2 = $this->compiler->genAnonClassName();
$this->assertStringStartsWith(CompilerBase::ANON_CLASS, $name1);
$this->assertStringStartsWith(CompilerBase::ANON_CLASS, $name2);
$this->assertNotEquals($name1, $name2);
}
// ========================================================================
// getIncludeDir / getBuildDir
// ========================================================================
public function testGetBuildDir(): void
{
$buildDir = $this->compiler->getBuildDir();
$this->assertStringEndsWith('/build', $buildDir);
$this->assertStringStartsWith($this->testDir, $buildDir);
}
public function testFormatCodeDisabledByDefault(): void
{
$this->assertFalse($this->getPropertyValue('formatCode'));
}
public function testGetIncludeDir(): void
{
$includeDir = $this->compiler->getIncludeDir();
$buildDir = $this->compiler->getBuildDir();
$this->assertEquals($buildDir . '/include', $includeDir);
}
public function testParseProjectYamlLoadsDocumentedCompilerOptions(): void
{
$binDir = $this->testDir . '/bin';
$formatLog = $this->testDir . '/format.log';
$this->createFakeClangFormat($binDir, $formatLog);
putenv('PATH=' . $binDir . ':' . ($this->originalPath ?: ''));
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
optimize: 2
job: 8
debug: true
profile: true
no-progress: true
no-console: true
no-literal-strings: true
sanitize: address
target-platform: aarch64-linux-gnu
build-dir: /tmp/project-build
include-paths:
- /opt/mylib/include
- ../shared/headers
defines:
- ENABLE_LOGGING=1
- DEBUG_LEVEL=3
lto: true
format: true
link-libs:
- curl
- ssl
link-paths:
- /usr/local/lib
- /opt/custom/lib
YAML);
$this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertSame(2, $this->getPropertyValue('optimizeLevel'));
$this->assertSame(8, $this->getPropertyValue('maxJob'));
$this->assertTrue($this->getPropertyValue('debug'));
$this->assertTrue($this->getPropertyValue('enableProfiler'));
$this->assertTrue($this->getPropertyValue('noProgress'));
$this->assertTrue($this->getPropertyValue('noConsole'));
$this->assertTrue($this->getPropertyValue('noLiteralStrings'));
$this->assertSame('address', $this->getPropertyValue('sanitize'));
$this->assertSame('aarch64-linux-gnu', $this->getPropertyValue('targetPlatform'));
$this->assertSame(['/opt/mylib/include', dirname($projectFile) . '/../shared/headers'], $this->compiler->getUserIncludePaths());
$this->assertSame(['ENABLE_LOGGING=1', 'DEBUG_LEVEL=3'], $this->compiler->getUserDefines());
$this->assertTrue($this->compiler->isLtoEnabled());
$this->assertSame(['curl', 'ssl'], $this->compiler->getLinkLibs());
$this->assertSame(['/usr/local/lib', '/opt/custom/lib'], $this->compiler->getLinkPaths());
$this->assertSame('/tmp/project-build', $this->compiler->getBuildDir());
$this->assertTrue($this->getPropertyValue('formatCode'));
}
public function testParseProjectYamlSupportsCustomFilenameAndRelativeBuildDir(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
build-dir: build/output
YAML, 'myproject.yml', 'nested/config');
$this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertSame(
realpath($this->testDir . '/nested/config/build/output'),
$this->compiler->getBuildDir()
);
}
public function testGetFilesAcceptsYamlExtension(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
YAML, 'project.yaml');
$files = $this->compiler->getFiles($projectFile);
$this->assertSame([realpath(dirname($projectFile) . '/main.php')], $files);
}
public function testParseProjectYamlSupportsCliStyleModeAndOutputAliases(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
mode: ext
output: out/custom-ext
dry: true
YAML, 'custom-name.yml', 'yaml-alias');
$this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertSame(CompilerBase::BUILD_MODE_EXT, $this->getPropertyValue('buildMode'));
$this->assertTrue($this->getPropertyValue('dryRun'));
$this->assertSame('custom_ext', $this->getPropertyValue('targetName'));
$this->assertSame(dirname($projectFile) . '/out', $this->getPropertyValue('outputDir'));
}
public function testParseProjectYamlNameDoesNotSetOutputDirectory(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
name: tetris
sources:
- main.php
YAML, 'myproject.yml', 'examples/tetris-sdl');
$this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertSame('tetris', $this->getPropertyValue('targetName'));
$this->assertSame('', $this->getPropertyValue('outputDir'));
$this->assertSame('tetris', $this->invokeMethod('getTargetFileName'));
}
public function testParseProjectYamlResolvesRelativePathOptionsAgainstYamlDirectory(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
include-paths:
- includes
link-paths:
- libs
output: bin/my-app
YAML, 'myproject.yml', 'nested/config');
$this->invokeMethod('parseProjectYaml', $projectFile);
$projectDir = dirname($projectFile);
$this->assertSame([$projectDir . '/includes'], $this->compiler->getUserIncludePaths());
$this->assertSame([$projectDir . '/libs'], $this->compiler->getLinkPaths());
$this->assertSame($projectDir . '/bin', $this->getPropertyValue('outputDir'));
$this->assertSame('my_app', $this->getPropertyValue('targetName'));
}
public function testCliOutputOverridesYamlOutputOnlyWhenCommandLineArgumentsAreApplied(): void
{
global $argv;
$argv = ['compiler.php', '--output', 'cli/out-file'];
$compiler = CompilerTest::create($this->testDir);
$ref = new \ReflectionClass($compiler);
$parseMethod = $ref->getMethod('parseProjectYaml');
$parseMethod->setAccessible(true);
$applyMethod = $ref->getMethod('applyCommandLineArguments');
$applyMethod->setAccessible(true);
$targetProp = $ref->getProperty('targetName');
$targetProp->setAccessible(true);
$outputProp = $ref->getProperty('outputDir');
$outputProp->setAccessible(true);
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
output: yaml/out-file
YAML, 'myproject.yml', 'cli-output');
$parseMethod->invoke($compiler, $projectFile);
$this->assertSame(dirname($projectFile) . '/yaml', $outputProp->getValue($compiler));
$this->assertSame('out_file', $targetProp->getValue($compiler));
$applyMethod->invoke($compiler);
$this->assertSame('cli', $outputProp->getValue($compiler));
$this->assertSame('out_file', $targetProp->getValue($compiler));
}
public function testApplyCommandLineArgumentsDoesNotClearYamlRepeatableOptionsWhenCliAbsent(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
include-paths:
- /yaml/include
defines:
- YAML_DEFINE=1
lto: true
link-libs:
- yamlssl
link-paths:
- /yaml/lib
YAML);
$this->invokeMethod('parseProjectYaml', $projectFile);
$this->invokeMethod('applyCommandLineArguments');
$this->assertSame(['/yaml/include'], $this->compiler->getUserIncludePaths());
$this->assertSame(['YAML_DEFINE=1'], $this->compiler->getUserDefines());
$this->assertTrue($this->compiler->isLtoEnabled());
$this->assertSame(['yamlssl'], $this->compiler->getLinkLibs());
$this->assertSame(['/yaml/lib'], $this->compiler->getLinkPaths());
}
public function testParseProjectYamlFiltersIgnoredFilesFromReturnedSources(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- .
ignore:
- ignored.php
- skipped
YAML);
$projectDir = dirname($projectFile);
mkdir($projectDir . '/skipped', 0777, true);
copy($this->fixturePath('ignored.php'), $projectDir . '/ignored.php');
copy($this->fixturePath('skipped_nested.php'), $projectDir . '/skipped/nested.php');
copy($this->fixturePath('kept.php'), $projectDir . '/kept.php');
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertContains(realpath($projectDir . '/main.php'), $files);
$this->assertContains(realpath($projectDir . '/kept.php'), $files);
$this->assertNotContains(realpath($projectDir . '/ignored.php'), $files);
$this->assertNotContains(realpath($projectDir . '/skipped/nested.php'), $files);
}
public function testParseProjectYamlSupportsConditionalSourcesByPhpVersion(): void
{
$futureVersion = PHP_VERSION_ID + 10000;
$projectFile = $this->createProjectFile(<<<YAML
sources:
- main.php
- path: php-current.php
if: PHP_VERSION_ID >= 80000
- path: php-id-reversed.php
if: 80000 <= PHP_VERSION_ID
- path: missing-future.php
if: PHP_VERSION_ID >= {$futureVersion}
YAML);
$projectDir = dirname($projectFile);
file_put_contents($projectDir . '/php-current.php', "<?php\nfunction php_current_source(): void {}\n");
file_put_contents($projectDir . '/php-id-reversed.php', "<?php\nfunction php_id_reversed_source(): void {}\n");
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertContains(realpath($projectDir . '/main.php'), $files);
$this->assertContains(realpath($projectDir . '/php-current.php'), $files);
$this->assertContains(realpath($projectDir . '/php-id-reversed.php'), $files);
$this->assertNotContains($projectDir . '/missing-future.php', $files);
}
public function testParseProjectYamlConditionalSourceAllowsIfBeforePath(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- if: PHP_VERSION_ID >= 80000
path: if-before-path.php
YAML);
$projectDir = dirname($projectFile);
file_put_contents($projectDir . '/if-before-path.php', "<?php\nfunction if_before_path_source(): void {}\n");
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertSame([realpath($projectDir . '/if-before-path.php')], $files);
}
public function testParseProjectYamlSupportsCompositeConditionalSources(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- path: composite.php
if: PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 90000
YAML);
$projectDir = dirname($projectFile);
file_put_contents($projectDir . '/composite.php', "<?php\nfunction composite_source(): void {}\n");
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertSame([realpath($projectDir . '/composite.php')], $files);
}
public function testParseProjectYamlSupportsPhpVersionStringConditionalSources(): void
{
$major = PHP_MAJOR_VERSION;
$nextMajor = PHP_MAJOR_VERSION + 1;
$projectFile = $this->createProjectFile(<<<YAML
sources:
- path: php-version-current.php
if: PHP_VERSION >= "{$major}.0.0"
- path: php-version-reversed.php
if: '"{$major}.0.0" <= PHP_VERSION'
- path: missing-next-major.php
if: PHP_VERSION >= "{$nextMajor}.0.0"
YAML);
$projectDir = dirname($projectFile);
file_put_contents($projectDir . '/php-version-current.php', "<?php\nfunction php_version_current_source(): void {}\n");
file_put_contents($projectDir . '/php-version-reversed.php', "<?php\nfunction php_version_reversed_source(): void {}\n");
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
$this->assertSame(
[
realpath($projectDir . '/php-version-current.php'),
realpath($projectDir . '/php-version-reversed.php'),
],
$files
);
}
public function testParseProjectYamlSupportsAllVersionCompareOperators(): void
{
$current = PHP_VERSION;
$projectFile = $this->createProjectFile(<<<YAML
sources:
- path: op-lt.php
if: PHP_VERSION lt "{$current}.1"
- path: op-le.php
if: PHP_VERSION le "{$current}"
- path: op-gt.php
if: PHP_VERSION gt "0.0.0"
- path: op-ge.php
if: PHP_VERSION ge "{$current}"
- path: op-eq.php
if: PHP_VERSION eq "{$current}"
- path: op-ne.php
if: PHP_VERSION ne "0.0.0"
- path: op-symbol-eq.php
if: PHP_VERSION = "{$current}"
- path: op-symbol-ne.php
if: PHP_VERSION <> "0.0.0"
- path: op-id-alias.php
if: PHP_VERSION_ID GE 80000
YAML);
$projectDir = dirname($projectFile);
foreach (['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'symbol-eq', 'symbol-ne', 'id-alias'] as $name) {
file_put_contents($projectDir . '/op-' . $name . '.php', "<?php\nfunction op_" . str_replace('-', '_', $name) . "(): void {}\n");
}
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
foreach (['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'symbol-eq', 'symbol-ne', 'id-alias'] as $name) {
$this->assertContains(realpath($projectDir . '/op-' . $name . '.php'), $files);
}
}
public function testParseProjectYamlSupportsPhpOsFamilyConditionalSources(): void
{
$osFamily = PHP_OS_FAMILY;
$otherFamily = $osFamily === 'Windows' ? 'Linux' : 'Windows';
$projectFile = $this->createProjectFile(<<<YAML
sources:
- path: os-current.php
if: PHP_OS_FAMILY == "{$osFamily}"
- path: os-not-windows.php
if: PHP_OS_FAMILY != "{$otherFamily}"
- path: os-reversed.php
if: '"{$osFamily}" == PHP_OS_FAMILY'
- path: os-composite.php
if: PHP_OS_FAMILY == "{$osFamily}" && PHP_VERSION_ID >= 80000
- path: os-or.php
if: PHP_OS_FAMILY == "{$otherFamily}" || PHP_OS_FAMILY == "{$osFamily}"
- path: missing-os.php
if: PHP_OS_FAMILY == "{$otherFamily}" && PHP_OS_FAMILY != "{$osFamily}"
YAML);
$projectDir = dirname($projectFile);
foreach (['current', 'not-windows', 'reversed', 'composite', 'or'] as $name) {
file_put_contents($projectDir . '/os-' . $name . '.php', "<?php\nfunction os_" . str_replace('-', '_', $name) . "(): void {}\n");
}
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
foreach (['current', 'not-windows', 'reversed', 'composite', 'or'] as $name) {
$this->assertContains(realpath($projectDir . '/os-' . $name . '.php'), $files);
}
$this->assertNotContains($projectDir . '/missing-os.php', $files);
}
public function testParseProjectYamlRejectsUnsupportedPhpOsFamilyOperator(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- path: main.php
if: PHP_OS_FAMILY >= "Linux"
YAML);
$this->expectException(\PhpAot\Php\Exception\TestError::class);
$this->expectExceptionMessage('Unsupported source condition');
$this->invokeMethod('parseProjectYaml', $projectFile);
}
public function testParseProjectYamlRejectsBarePhpVersionCondition(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- path: main.php
if: PHP_VERSION
YAML);
$this->expectException(\PhpAot\Php\Exception\TestError::class);
$this->expectExceptionMessage('Unsupported source condition');
$this->invokeMethod('parseProjectYaml', $projectFile);
}
public function testParseProjectYamlRejectsUnsafeConditionalSourceExpression(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- path: main.php
if: PHP_VERSION_ID >= getenv("MIN_PHP")
YAML);
$this->expectException(\PhpAot\Php\Exception\TestError::class);
$this->expectExceptionMessage('Unsupported source condition');
$this->invokeMethod('parseProjectYaml', $projectFile);
}
public function testCCompileCommandOptionsKeepCommonUserConfiguration(): void
{
$this->setPropertyValue('userIncludePaths', ['/user/include']);
$this->setPropertyValue('userDefines', ['FEATURE_X=1']);
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_EXT);
$this->setPropertyValue('enableProfiler', true);
$this->setPropertyValue('enableLto', true);
$this->setPropertyValue('sanitize', 'address');
$this->setPropertyValue('targetPlatform', 'aarch64-linux-gnu');
$this->setPropertyValue('march', 'native');
$options = $this->invokeMethod('getCCompileCommandOptions');
$this->assertContains('/user/include', $options['include_paths']);
$this->assertSame(['FEATURE_X=1'], $options['user_defines']);
$this->assertSame(CompilerBase::BUILD_MODE_EXT, $options['build_mode']);
$this->assertTrue($options['enable_profiler']);
$this->assertTrue($options['lto']);
$this->assertSame('address', $options['sanitize']);
$this->assertSame('aarch64-linux-gnu', $options['target_platform']);
$this->assertSame('native', $options['march']);
}
public function testNativeCompileCommandOptionsKeepCommonUserConfiguration(): void
{
$this->setPropertyValue('userIncludePaths', ['/native/include']);
$this->setPropertyValue('userDefines', ['NATIVE_FEATURE=1']);
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_EXT);
$this->setPropertyValue('enableProfiler', true);
$this->setPropertyValue('enableLto', true);
$options = $this->invokeMethod('getNativeCompileCommandOptions', 'objective-c');
$this->assertContains('/native/include', $options['include_paths']);
$this->assertSame(['NATIVE_FEATURE=1'], $options['user_defines']);
$this->assertSame(CompilerBase::BUILD_MODE_EXT, $options['build_mode']);
$this->assertTrue($options['enable_profiler']);
$this->assertTrue($options['lto']);
$this->assertArrayNotHasKey('cpp_std', $options);
$this->assertArrayNotHasKey('cxxflags', $options);
}
public function testExtensionCleanClearsRuntimeMapsWithValidCpp(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$ref = new \ReflectionClass($compiler);
$buildMode = $ref->getProperty('buildMode');
$buildMode->setAccessible(true);
$buildMode->setValue($compiler, CompilerBase::BUILD_MODE_EXT);
$testFile = ROOT_PATH . '/phpunit/code/compiler_api/extension_clean_maps.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
$extensionFile = $compiler->genExtension();
$code = file_get_contents($extensionFile);
$this->assertStringContainsString('#include <cstring>', $code);
$this->assertStringContainsString('std::memset(php_func_map, 0, sizeof(php_func_map));', $code);
$this->assertStringContainsString('std::memset(php_class_map, 0, sizeof(php_class_map));', $code);
$this->assertStringContainsString('std::memset(php_property_map, 0, sizeof(php_property_map));', $code);
$this->assertStringNotContainsString('func_map = {}', $code);
$this->assertStringNotContainsString('class_map = {}', $code);
$this->assertStringNotContainsString('property_map = {}', $code);
}
public function testObjectiveCppCompileCommandOptionsKeepCppOptions(): void
{
$this->setPropertyValue('cxxStd', 'c++20');
$this->setPropertyValue('cxxFlags', '-fobjc-arc');
$options = $this->invokeMethod('getNativeCompileCommandOptions', 'objective-c++');
$this->assertSame('c++20', $options['cpp_std']);
$this->assertSame('-fobjc-arc', $options['cxxflags']);
}
public function testLinkCommandOptionsPassUserLibrariesThroughBackendFields(): void
{
$this->setPropertyValue('linkLibs', ['curl', 'ssl']);
$this->setPropertyValue('linkPaths', ['/user/lib']);
$this->setPropertyValue('enableProfiler', true);
$this->setPropertyValue('ldflags', '-Wl,--as-needed');
$options = $this->invokeMethod('getLinkCommandOptions');
$this->assertContains('/user/lib', $options['library_paths']);
$this->assertContains('profiler', $options['libraries']);
$this->assertContains('curl', $options['libraries']);
$this->assertContains('ssl', $options['libraries']);
$this->assertSame('-Wl,--as-needed', $options['ldflags']);
}
public function testFormatCppCodeEscapesPathsWithSpaces(): void
{
$spaceDir = sys_get_temp_dir() . '/compiler api format ' . uniqid();
mkdir($spaceDir, 0777, true);
$binDir = $spaceDir . '/bin';
$logFile = $spaceDir . '/format.log';
$sourceFile = $spaceDir . '/hello world.cc';
copy($this->fixturePath('hello_world.cc'), $sourceFile);
$this->createFakeClangFormat($binDir, $logFile);
putenv('PATH=' . $binDir . ':' . ($this->originalPath ?: ''));
$compiler = CompilerTest::create($spaceDir);
$ref = new \ReflectionClass($compiler);
$formatProp = $ref->getProperty('formatCode');
$formatProp->setAccessible(true);
$formatProp->setValue($compiler, true);
$method = $ref->getMethod('formatCppCode');
$method->setAccessible(true);
$method->invoke($compiler, $sourceFile);
$this->assertFileExists($logFile);
$lines = file($logFile, FILE_IGNORE_NEW_LINES);
$this->assertSame($spaceDir, $lines[0]);
$this->assertSame('-i', $lines[1]);
$this->assertSame($sourceFile, $lines[2]);
$this->removeDirectory($spaceDir);
}
// ========================================================================
// isWindows / isLinux / isMacos
// ========================================================================
public function testPlatformDetectionMethods(): void
{
$isWin = $this->compiler->isWindows();
$isLin = $this->compiler->isLinux();
$isMac = $this->compiler->isMacos();
// Exactly one platform must be true
$sum = ($isWin ? 1 : 0) + ($isLin ? 1 : 0) + ($isMac ? 1 : 0);
$this->assertEquals(1, $sum, 'Exactly one platform must be detected');
// All return bool
$this->assertIsBool($isWin);
$this->assertIsBool($isLin);
$this->assertIsBool($isMac);
}
// ========================================================================
// isScalarInt - public method
// ========================================================================
public function testIsScalarIntTrue(): void
{
$this->assertTrue($this->compiler->isScalarInt(new \PhpParser\Node\Scalar\LNumber(42)));
}
public function testIsScalarIntFalse(): void
{
$this->assertFalse($this->compiler->isScalarInt(new \PhpParser\Node\Expr\Variable('a')));
}
// ========================================================================
// getNamespacedClassName - fully qualified
// ========================================================================
public function testGetNamespacedClassNameFullyQualified(): void
{
$this->assertEquals(
'App\\Entity\\User',
$this->compiler->getNamespacedClassName('\\App\\Entity\\User')
);
}
// ========================================================================
// getNamespacedClassName - with use alias
// ========================================================================
public function testGetNamespacedClassNameWithUseAlias(): void
{
$this->setPropertyValue('useAliases', ['User' => 'App\\Entity\\User']);
$this->assertEquals(
'App\\Entity\\User',
$this->compiler->getNamespacedClassName('User')
);
}
public function testGetNamespacedClassNameWithUseAliasSubNamespace(): void
{
$this->setPropertyValue('useAliases', ['Entity' => 'App\\Entity']);
$this->assertEquals(
'App\\Entity\\User',
$this->compiler->getNamespacedClassName('Entity\\User')
);
}
// ========================================================================
// getNamespacedClassName - with use namespace (partial match)
// ========================================================================
public function testGetNamespacedClassNameWithUseNamespace(): void
{
$this->setPropertyValue('useNamespaces', ['App\\Entity']);
// The last segment of 'App\Entity' is 'Entity', matching input 'Entity'
$this->assertEquals(
'App\\Entity',
$this->compiler->getNamespacedClassName('Entity')
);
}
public function testGetNamespacedClassNameWithUseNamespaceSub(): void
{
$this->setPropertyValue('useNamespaces', ['App\\Entity']);
// 'Entity\User' - first part 'Entity' matches the last part of 'App\Entity'
$this->assertEquals(
'App\\Entity\\User',
$this->compiler->getNamespacedClassName('Entity\\User')
);
}
// ========================================================================
// getNamespacedClassName - with current namespace
// ========================================================================
public function testGetNamespacedClassNameWithCurrentNamespace(): void
{
$this->setPropertyValue('namespace', 'App\\Service');
// No matching alias or use namespace
$this->setPropertyValue('useAliases', []);
$this->setPropertyValue('useNamespaces', []);
$this->assertEquals(
'App\\Service\\MyClass',
$this->compiler->getNamespacedClassName('MyClass')
);
}
public function testGetNamespacedClassNameNoNamespace(): void
{
$this->setPropertyValue('namespace', '');
$this->setPropertyValue('useAliases', []);
$this->setPropertyValue('useNamespaces', []);
$this->assertEquals(
'MyClass',
$this->compiler->getNamespacedClassName('MyClass')
);
}
// ========================================================================
// getNamespacedClassName - alias takes priority over use namespace
// ========================================================================
public function testGetNamespacedClassNameAliasPriority(): void
{
$this->setPropertyValue('useAliases', ['User' => 'App\\Models\\User']);
$this->setPropertyValue('useNamespaces', ['App\\Controllers']);
// Alias should be checked first
$this->assertEquals(
'App\\Models\\User',
$this->compiler->getNamespacedClassName('User')
);
}
// ========================================================================
// getNamespacedFuncName
// ========================================================================
public function testGetNamespacedFuncNameFullyQualified(): void
{
$this->assertEquals(
'App\\Lib\\helper_func',
$this->compiler->getNamespacedFuncName('\\App\\Lib\\helper_func')
);
}
public function testGetNamespacedFuncNameWithUseFunction(): void
{
$this->setPropertyValue('useFunctions', [
'helper_func' => 'App\\Lib\\helper_func',
]);
$this->assertEquals(
'App\\Lib\\helper_func',
$this->compiler->getNamespacedFuncName('helper_func')
);
}
public function testGetNamespacedFuncNameNoNamespace(): void
{
$this->setPropertyValue('useFunctions', []);
$this->assertEquals(
'helper_func',
$this->compiler->getNamespacedFuncName('helper_func')
);
}
// ========================================================================
// getNamespacedFuncName - not in useFunctions returns bare name
// ========================================================================
public function testGetNamespacedFuncNameNotInUseFunctions(): void
{
$this->setPropertyValue('useFunctions', ['other' => 'Some\\Ns\\other']);
$this->assertEquals(
'my_func',
$this->compiler->getNamespacedFuncName('my_func')
);
}
// ========================================================================
// getPhpDir
// ========================================================================
public function testGetPhpDir(): void
{
$phpDir = $this->compiler->getPhpDir();
$this->assertIsString($phpDir);
$this->assertNotEmpty($phpDir);
}
}