test(phpunit): 添加PHP编译器相关单元测试

- 创建AST节点类型测试类AstNodeTypeTest,测试各种节点类型的识别方法
- 添加ClassDef扩展功能测试类ClassDefExtendedTest,验证类定义的各种属性和方法
- 实现CompilerBase API测试类CompilerBaseApiTest,测试类型转换、变量名生成等功能
- 创建FunctionContext扩展测试类Context/FunctionContextExtendedTest,测试作用域管理和变量操作
- 添加FunctionDef扩展测试类Entity/FunctionDefExtendedTest,测试函数定义相关功能
pull/1/head
韩天峰 3 months ago
parent b2d52d5d80
commit efd6beac87
  1. 436
      phpunit/src/AstNodeTypeTest.php
  2. 338
      phpunit/src/CompilerBaseApiTest.php
  3. 146
      phpunit/src/Context/FunctionContextExtendedTest.php
  4. 137
      phpunit/src/Entity/ClassDefExtendedTest.php
  5. 104
      phpunit/src/Entity/FunctionDefExtendedTest.php
  6. 69
      phpunit/src/Entity/MethodDefExtendedTest.php
  7. 320
      phpunit/src/Generator/UtilsTest.php
  8. 229
      phpunit/src/PreprocessorTest.php
  9. 139
      phpunit/src/ReflectionTest.php
  10. 311
      phpunit/src/TraitsTest.php

@ -0,0 +1,436 @@
<?php
namespace PhpAot\Tests;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\CompilerTest;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\VariadicPlaceholder;
class AstNodeTypeTest extends TestCase
{
private CompilerTest $compiler;
private \ReflectionClass $ref;
private string $tmpDir;
protected function setUp(): void
{
parent::setUp();
$this->tmpDir = sys_get_temp_dir() . '/ast_node_type_test_' . uniqid();
mkdir($this->tmpDir, 0777, true);
$this->compiler = CompilerTest::create($this->tmpDir);
$this->ref = new \ReflectionClass($this->compiler);
}
protected function tearDown(): void
{
parent::tearDown();
if (is_dir($this->tmpDir)) {
$this->removeDirectory($this->tmpDir);
}
}
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 invoke(string $method, ...$args): mixed
{
$m = $this->ref->getMethod($method);
$m->setAccessible(true);
return $m->invoke($this->compiler, ...$args);
}
// ========================================================================
// isArrayDimFetch
// ========================================================================
public function testIsArrayDimFetch(): void
{
$var = new Expr\Variable('arr');
$this->assertTrue($this->invoke('isArrayDimFetch', new Expr\ArrayDimFetch($var)));
$this->assertFalse($this->invoke('isArrayDimFetch', $var));
}
// ========================================================================
// isVarExpr
// ========================================================================
public function testIsVarExpr(): void
{
$this->assertTrue($this->invoke('isVarExpr', new Expr\Variable('foo')));
$this->assertFalse($this->invoke('isVarExpr', new Node\Scalar\Int_(42)));
}
// ========================================================================
// isIdExpr
// ========================================================================
public function testIsIdExpr(): void
{
$this->assertTrue($this->invoke('isIdExpr', new Node\Identifier('foo')));
$this->assertFalse($this->invoke('isIdExpr', new Expr\Variable('foo')));
}
// ========================================================================
// isPropertyFetch
// ========================================================================
public function testIsPropertyFetch(): void
{
$obj = new Expr\Variable('obj');
$this->assertTrue($this->invoke('isPropertyFetch', new Expr\PropertyFetch($obj, 'prop')));
$this->assertFalse($this->invoke('isPropertyFetch', $obj));
}
// ========================================================================
// isStaticPropertyFetch
// ========================================================================
public function testIsStaticPropertyFetch(): void
{
$class = new Node\Name('Foo');
$this->assertTrue($this->invoke('isStaticPropertyFetch', new Expr\StaticPropertyFetch($class, 'prop')));
$this->assertFalse($this->invoke('isStaticPropertyFetch', new Expr\Variable('a')));
}
// ========================================================================
// isClassConstFetch
// ========================================================================
public function testIsClassConstFetch(): void
{
$class = new Node\Name('Foo');
$this->assertTrue($this->invoke('isClassConstFetch', new Expr\ClassConstFetch($class, 'BAR')));
$this->assertFalse($this->invoke('isClassConstFetch', new Expr\Variable('a')));
}
// ========================================================================
// isNewExpr
// ========================================================================
public function testIsNewExpr(): void
{
$class = new Node\Name('Foo');
$this->assertTrue($this->invoke('isNewExpr', new Expr\New_($class)));
$this->assertFalse($this->invoke('isNewExpr', new Expr\Variable('a')));
}
// ========================================================================
// isNameExpr
// ========================================================================
public function testIsNameExpr(): void
{
$this->assertTrue($this->invoke('isNameExpr', new Node\Name('Foo')));
$this->assertFalse($this->invoke('isNameExpr', new Expr\Variable('Foo')));
}
// ========================================================================
// isFullNameExpr
// ========================================================================
public function testIsFullNameExpr(): void
{
$this->assertTrue($this->invoke('isFullNameExpr', new Node\Name\FullyQualified('Foo\\Bar')));
$this->assertFalse($this->invoke('isFullNameExpr', new Node\Name('Foo')));
}
// ========================================================================
// isNamedMethod
// ========================================================================
public function testIsNamedMethod(): void
{
$this->assertTrue($this->invoke('isNamedMethod', new Node\Identifier('methodName')));
$this->assertFalse($this->invoke('isNamedMethod', new Expr\Variable('a')));
}
// ========================================================================
// isScalarString
// ========================================================================
public function testIsScalarString(): void
{
$this->assertTrue($this->invoke('isScalarString', new Node\Scalar\String_('hello')));
$this->assertFalse($this->invoke('isScalarString', new Node\Scalar\Int_(1)));
}
// ========================================================================
// isFuncCallExpr
// ========================================================================
public function testIsFuncCallExpr(): void
{
$name = new Node\Name('foo');
$this->assertTrue($this->invoke('isFuncCallExpr', new Expr\FuncCall($name)));
$this->assertFalse($this->invoke('isFuncCallExpr', new Expr\Variable('a')));
}
// ========================================================================
// isRefvalCall
// ========================================================================
public function testIsRefvalCall(): void
{
$refvalCall = new Expr\FuncCall(new Node\Name('refval'));
$this->assertTrue($this->invoke('isRefvalCall', $refvalCall));
$otherCall = new Expr\FuncCall(new Node\Name('other'));
$this->assertFalse($this->invoke('isRefvalCall', $otherCall));
$this->assertFalse($this->invoke('isRefvalCall', new Expr\Variable('a')));
}
// ========================================================================
// isMethodCall
// ========================================================================
public function testIsMethodCall(): void
{
$obj = new Expr\Variable('obj');
$this->assertTrue($this->invoke('isMethodCall', new Expr\MethodCall($obj, 'method')));
$this->assertFalse($this->invoke('isMethodCall', $obj));
}
// ========================================================================
// isStaticCall
// ========================================================================
public function testIsStaticCall(): void
{
$class = new Node\Name('Foo');
$this->assertTrue($this->invoke('isStaticCall', new Expr\StaticCall($class, 'method')));
$this->assertFalse($this->invoke('isStaticCall', new Expr\Variable('a')));
}
// ========================================================================
// isScalar / isScalarInt / isScalarBool
// ========================================================================
public function testIsScalar(): void
{
$this->assertTrue($this->invoke('isScalar', new Node\Scalar\Int_(1)));
$this->assertTrue($this->invoke('isScalar', new Node\Scalar\String_('s')));
$this->assertTrue($this->invoke('isScalar', new Node\Scalar\Float_(1.0)));
$this->assertFalse($this->invoke('isScalar', new Expr\Variable('a')));
}
public function testIsScalarInt(): void
{
$this->assertTrue($this->invoke('isScalarInt', new Node\Scalar\Int_(42)));
$this->assertFalse($this->invoke('isScalarInt', new Node\Scalar\String_('42')));
}
public function testIsScalarBoolTrue(): void
{
$trueConst = new Expr\ConstFetch(new Node\Name('true'));
$this->assertTrue($this->invoke('isScalarBool', $trueConst));
$this->assertEquals('php::true_', $this->invoke('getBoolValue', $trueConst));
}
public function testIsScalarBoolFalse(): void
{
$falseConst = new Expr\ConstFetch(new Node\Name('false'));
$this->assertTrue($this->invoke('isScalarBool', $falseConst));
$this->assertEquals('php::false_', $this->invoke('getBoolValue', $falseConst));
}
public function testIsScalarBoolNotBool(): void
{
$nullConst = new Expr\ConstFetch(new Node\Name('null'));
$this->assertFalse($this->invoke('isScalarBool', $nullConst));
}
// ========================================================================
// isMatchExpr
// ========================================================================
public function testIsMatchExpr(): void
{
$cond = new Expr\Variable('x');
$this->assertTrue($this->invoke('isMatchExpr', new Expr\Match_($cond)));
$this->assertFalse($this->invoke('isMatchExpr', $cond));
}
// ========================================================================
// isConstFetch
// ========================================================================
public function testIsConstFetch(): void
{
$this->assertTrue($this->invoke('isConstFetch', new Expr\ConstFetch(new Node\Name('FOO'))));
$this->assertFalse($this->invoke('isConstFetch', new Expr\Variable('a')));
}
// ========================================================================
// isAssignOp / isAssignExpr
// ========================================================================
public function testIsAssignOp(): void
{
$var = new Expr\Variable('a');
$val = new Node\Scalar\Int_(1);
// Assign is also an AssignOp
$this->assertTrue($this->invoke('isAssignOp', new Expr\Assign($var, $val)));
$this->assertTrue($this->invoke('isAssignOp', new Expr\AssignOp\Plus($var, $val)));
$this->assertFalse($this->invoke('isAssignOp', $var));
}
public function testIsAssignExpr(): void
{
$var = new Expr\Variable('a');
$val = new Node\Scalar\Int_(1);
$this->assertTrue($this->invoke('isAssignExpr', new Expr\Assign($var, $val)));
$this->assertFalse($this->invoke('isAssignExpr', new Expr\AssignOp\Plus($var, $val)));
}
// ========================================================================
// isCallExpr
// ========================================================================
public function testIsCallExpr(): void
{
$name = new Node\Name('foo');
$obj = new Expr\Variable('obj');
$class = new Node\Name('Foo');
$this->assertTrue($this->invoke('isCallExpr', new Expr\FuncCall($name)));
$this->assertTrue($this->invoke('isCallExpr', new Expr\MethodCall($obj, 'bar')));
$this->assertTrue($this->invoke('isCallExpr', new Expr\StaticCall($class, 'baz')));
$this->assertFalse($this->invoke('isCallExpr', $obj));
}
// ========================================================================
// isPlaceholderExpr
// ========================================================================
public function testIsPlaceholderExpr(): void
{
$this->assertTrue($this->invoke('isPlaceholderExpr', new VariadicPlaceholder()));
$this->assertFalse($this->invoke('isPlaceholderExpr', new Expr\Variable('a')));
}
// ========================================================================
// isReturnExpr
// ========================================================================
public function testIsReturnExpr(): void
{
$this->assertTrue($this->invoke('isReturnExpr', new Node\Stmt\Return_(new Node\Scalar\Int_(1))));
$this->assertFalse($this->invoke('isReturnExpr', new Expr\Variable('a')));
}
// ========================================================================
// isBreakExpr
// ========================================================================
public function testIsBreakExpr(): void
{
$this->assertTrue($this->invoke('isBreakExpr', new Node\Stmt\Break_()));
$this->assertFalse($this->invoke('isBreakExpr', new Expr\Variable('a')));
}
// ========================================================================
// isThrowExpr
// ========================================================================
public function testIsThrowExprDirect(): void
{
$this->assertTrue($this->invoke('isThrowExpr', new Expr\Throw_(new Expr\Variable('e'))));
}
public function testIsThrowExprWrapped(): void
{
$throwExpr = new Expr\Throw_(new Expr\Variable('e'));
$wrapped = new Node\Stmt\Expression($throwExpr);
$this->assertTrue($this->invoke('isThrowExpr', $wrapped));
}
public function testIsThrowExprNotThrow(): void
{
$this->assertFalse($this->invoke('isThrowExpr', new Expr\Variable('a')));
}
// ========================================================================
// isExitExpr
// ========================================================================
public function testIsExitExprDirect(): void
{
$this->assertTrue($this->invoke('isExitExpr', new Expr\Exit_()));
}
public function testIsExitExprWrapped(): void
{
$exitExpr = new Expr\Exit_();
$wrapped = new Node\Stmt\Expression($exitExpr);
$this->assertTrue($this->invoke('isExitExpr', $wrapped));
}
public function testIsExitExprNotExit(): void
{
$this->assertFalse($this->invoke('isExitExpr', new Expr\Variable('a')));
}
// ========================================================================
// isEmptyArray
// ========================================================================
public function testIsEmptyArray(): void
{
$this->assertTrue($this->invoke('isEmptyArray', new Expr\Array_([])));
$this->assertFalse($this->invoke('isEmptyArray', new Expr\Array_([new Node\ArrayItem(new Node\Scalar\Int_(1))])));
}
public function testIsEmptyArrayNotArray(): void
{
$this->assertFalse($this->invoke('isEmptyArray', new Expr\Variable('a')));
}
// ========================================================================
// isNull
// ========================================================================
public function testIsNull(): void
{
$this->assertTrue($this->invoke('isNull', new Expr\ConstFetch(new Node\Name('null'))));
$this->assertFalse($this->invoke('isNull', new Expr\ConstFetch(new Node\Name('true'))));
$this->assertFalse($this->invoke('isNull', new Expr\Variable('a')));
}
// ========================================================================
// Cross-type verification: each is* is false for unrelated types
// ========================================================================
public function testNoCrossFalsePositives(): void
{
$var = new Expr\Variable('x');
$methods = [
'isArrayDimFetch', 'isPropertyFetch', 'isStaticPropertyFetch',
'isClassConstFetch', 'isNewExpr', 'isNameExpr', 'isFullNameExpr',
'isFuncCallExpr', 'isRefvalCall', 'isMethodCall', 'isStaticCall',
'isMatchExpr', 'isConstFetch', 'isAssignOp', 'isAssignExpr',
'isCallExpr', 'isPlaceholderExpr', 'isReturnExpr', 'isBreakExpr',
'isThrowExpr', 'isExitExpr', 'isEmptyArray', 'isNull',
];
foreach ($methods as $method) {
$this->assertFalse(
$this->invoke($method, $var),
"{$method} should return false for a plain Variable"
);
}
}
}

@ -0,0 +1,338 @@
<?php
namespace PhpAot\Tests;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\CompilerTest;
use PhpAot\Php\CompilerBase;
class CompilerBaseApiTest extends TestCase
{
private string $testDir;
private CompilerTest $compiler;
private \ReflectionClass $ref;
protected function setUp(): void
{
parent::setUp();
$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();
// 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);
}
// ========================================================================
// 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'));
$this->assertEquals(CompilerBase::TYPE_VAR, $this->compiler->getTypeFromZendType('UnsafePtr'));
}
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);
}
// ========================================================================
// 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 testGetIncludeDir(): void
{
$includeDir = $this->compiler->getIncludeDir();
$buildDir = $this->compiler->getBuildDir();
$this->assertEquals($buildDir . '/include', $includeDir);
}
// ========================================================================
// 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',
]);
$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']);
$this->assertEquals(
'my_func',
$this->compiler->getNamespacedFuncName('my_func')
);
}
// ========================================================================
// getPhpDir
// ========================================================================
public function testGetPhpDir(): void
{
$phpDir = $this->compiler->getPhpDir();
$this->assertIsString($phpDir);
$this->assertNotEmpty($phpDir);
}
}

@ -0,0 +1,146 @@
<?php
namespace PhpAot\Tests\Context;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\Context\FunctionContext;
use PhpAot\Php\Context\ScopeContext;
class FunctionContextExtendedTest extends TestCase
{
public function testMultipleEnterLeaveScopeCycles(): void
{
$ctx = new FunctionContext();
// First cycle
$ctx->enterScope();
$this->assertSame(1, $ctx->scopeLevel);
$ctx->leaveScope();
$this->assertSame(0, $ctx->scopeLevel);
// Second cycle - should still work
$ctx->enterScope();
$this->assertSame(1, $ctx->scopeLevel);
$ctx->leaveScope();
$this->assertSame(0, $ctx->scopeLevel);
}
public function testDeepScopeNesting(): void
{
$ctx = new FunctionContext();
for ($i = 1; $i <= 5; $i++) {
$ctx->enterScope();
$this->assertSame($i, $ctx->scopeLevel);
$this->assertCount($i, $ctx->scopeLayouts);
$this->assertInstanceOf(ScopeContext::class, $ctx->scopeLayouts[$i - 1]);
}
for ($i = 4; $i >= 0; $i--) {
$ctx->leaveScope();
$this->assertSame($i, $ctx->scopeLevel);
}
}
public function testStaticVarsManipulation(): void
{
$ctx = new FunctionContext();
$ctx->staticVars['counter'] = 'php::Int';
$ctx->staticVars['cache'] = 'php::Array';
$this->assertArrayHasKey('counter', $ctx->staticVars);
$this->assertArrayHasKey('cache', $ctx->staticVars);
$this->assertEquals('php::Int', $ctx->staticVars['counter']);
}
public function testGlobalVarsManipulation(): void
{
$ctx = new FunctionContext();
$ctx->globalVars['_SESSION'] = 'php::Array';
$ctx->globalVars['_ENV'] = 'php::Array';
$this->assertArrayHasKey('_SESSION', $ctx->globalVars);
$this->assertArrayHasKey('_ENV', $ctx->globalVars);
}
public function testStdArraysManipulation(): void
{
$ctx = new FunctionContext();
$ctx->stdArrays['arr1'] = ['kind' => 'array', 'decl' => 'php::StdArray<php::Int, 10>'];
$ctx->stdArrays['arr2'] = ['kind' => 'array', 'decl' => 'php::StdArray<php::Float, 5>'];
$this->assertArrayHasKey('arr1', $ctx->stdArrays);
$this->assertArrayHasKey('arr2', $ctx->stdArrays);
$this->assertCount(2, $ctx->stdArrays);
}
public function testStdContainersManipulation(): void
{
$ctx = new FunctionContext();
$ctx->stdContainers['vec'] = ['kind' => 'vector', 'decl' => 'php::StdVector<php::Int>'];
$ctx->stdContainers['map'] = ['kind' => 'map', 'decl' => 'php::StdMap<php::Str, php::Int>'];
$this->assertArrayHasKey('vec', $ctx->stdContainers);
$this->assertArrayHasKey('map', $ctx->stdContainers);
$this->assertEquals('vector', $ctx->stdContainers['vec']['kind']);
$this->assertEquals('map', $ctx->stdContainers['map']['kind']);
}
public function testArgumentsManipulation(): void
{
$ctx = new FunctionContext();
$ctx->arguments['arg1'] = 'php::Int';
$ctx->arguments['arg2'] = 'php::Str';
$this->assertArrayHasKey('arg1', $ctx->arguments);
$this->assertArrayHasKey('arg2', $ctx->arguments);
}
public function testCeWrappersManipulation(): void
{
$ctx = new FunctionContext();
$ctx->ceWrappers['stdClass'] = 'ce_wrapper_0';
$ctx->ceWrappers['Exception'] = 'ce_wrapper_1';
$this->assertArrayHasKey('stdClass', $ctx->ceWrappers);
$this->assertArrayHasKey('Exception', $ctx->ceWrappers);
}
public function testObjectPropsManipulation(): void
{
$ctx = new FunctionContext();
$ctx->objectProps['_object_prop_obj__name'] = ['type' => 'php::Str', 'class' => ''];
$ctx->objectProps['_object_prop_obj__age'] = ['type' => 'php::Int', 'class' => ''];
$this->assertArrayHasKey('_object_prop_obj__name', $ctx->objectProps);
$this->assertArrayHasKey('_object_prop_obj__age', $ctx->objectProps);
}
public function testScopeLayoutsEntriesAreUniqueInstances(): void
{
$ctx = new FunctionContext();
$ctx->enterScope();
$ctx->enterScope();
$this->assertNotSame($ctx->scopeLayouts[0], $ctx->scopeLayouts[1]);
}
public function testInLoopToggleWithinScope(): void
{
$ctx = new FunctionContext();
$ctx->enterScope();
$ctx->inLoop = true;
$this->assertTrue($ctx->inLoop);
$ctx->leaveScope();
// inLoop is not affected by scope leave
$this->assertTrue($ctx->inLoop);
}
public function testTmpVarIndexIncrementsNormally(): void
{
$ctx = new FunctionContext();
$indices = [];
for ($i = 0; $i < 10; $i++) {
$indices[] = $ctx->tmpVarIndex++;
}
$this->assertEquals(range(0, 9), $indices);
}
}

@ -0,0 +1,137 @@
<?php
namespace PhpAot\Tests\Entity;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\Entity\ClassDef;
use PhpAot\Php\Entity\MethodDef;
use PhpParser\Modifiers;
use PhpParser\Node\Stmt\Trait_;
class ClassDefExtendedTest extends TestCase
{
public function testEnumFlag(): void
{
$class = new ClassDef('Status', Modifiers::PUBLIC);
$this->assertFalse($class->enum);
$class->enum = true;
$this->assertTrue($class->enum);
}
public function testInheritedFromInternalClass(): void
{
$class = new ClassDef('MyClass', Modifiers::PUBLIC);
$this->assertFalse($class->inheritedFromInternalClass);
$class->inheritedFromInternalClass = true;
$this->assertTrue($class->inheritedFromInternalClass);
}
public function testCtorInitAndClean(): void
{
$class = new ClassDef('Service', Modifiers::PUBLIC);
$this->assertEquals('', $class->ctorInit);
$this->assertEquals('', $class->ctorClean);
$class->ctorInit = 'property_1 = 0;';
$class->ctorClean = 'property_1 = 0;';
$this->assertEquals('property_1 = 0;', $class->ctorInit);
$this->assertEquals('property_1 = 0;', $class->ctorClean);
}
public function testRequireCtorFlag(): void
{
$class = new ClassDef('Entity', Modifiers::PUBLIC);
$this->assertFalse($class->requireCtor);
$class->requireCtor = true;
$this->assertTrue($class->requireCtor);
}
public function testTraitAssociation(): void
{
$class = new ClassDef('User', Modifiers::PUBLIC);
$this->assertNull($class->trait);
$traitStmt = new Trait_('SomeTrait');
$class->trait = $traitStmt;
$this->assertSame($traitStmt, $class->trait);
}
public function testTraitAliasesAndIgnoredCanBeSet(): void
{
$class = new ClassDef('User', Modifiers::PUBLIC);
$class->traitAliases['Full\\Trait::method'] = ['alias' => 'newName'];
$class->traitIgnored['Full\\Trait::other'] = true;
$this->assertArrayHasKey('Full\\Trait::method', $class->traitAliases);
$this->assertArrayHasKey('Full\\Trait::other', $class->traitIgnored);
$this->assertTrue($class->traitIgnored['Full\\Trait::other']);
}
public function testExtendsCanBeSet(): void
{
$class = new ClassDef('Derived', Modifiers::PUBLIC);
$class->extends = 'App\\Entity\\Base';
$this->assertEquals('App\\Entity\\Base', $class->extends);
}
public function testImplementsCanBeSet(): void
{
$class = new ClassDef('Service', Modifiers::PUBLIC);
$class->implements = ['Serializable', 'JsonSerializable'];
$this->assertContains('Serializable', $class->implements);
$this->assertContains('JsonSerializable', $class->implements);
}
public function testMultipleMethodsWithSameNameCaseInsensitive(): void
{
$class = new ClassDef('Foo', Modifiers::PUBLIC);
$method = new MethodDef(Modifiers::PUBLIC, 'MyMethod');
$class->addMethod($method);
// Adding same method name (case-insensitive) overwrites
$this->assertTrue($class->hasMethod('MYMETHOD'));
$this->assertTrue($class->hasMethod('mymethod'));
$this->assertTrue($class->hasMethod('MyMethod'));
}
public function testFinalClassModifier(): void
{
$class = new ClassDef('FinalClass', Modifiers::PUBLIC | Modifiers::FINAL);
$this->assertTrue((bool) ($class->flags & Modifiers::FINAL));
$this->assertFalse($class->isAbstract());
}
public function testReadonlyClassModifier(): void
{
$class = new ClassDef('ReadonlyClass', Modifiers::PUBLIC | Modifiers::READONLY);
$this->assertTrue((bool) ($class->flags & Modifiers::READONLY));
}
public function testFlagsPropertyIsAccessible(): void
{
$class = new ClassDef('Foo', Modifiers::PUBLIC);
$this->assertSame(Modifiers::PUBLIC, $class->flags);
$class->flags = Modifiers::PROTECTED;
$this->assertSame(Modifiers::PROTECTED, $class->flags);
}
public function testPropertiesDefaultEmptyArray(): void
{
$class = new ClassDef('Foo', Modifiers::PUBLIC);
$this->assertIsArray($class->properties);
$this->assertEmpty($class->properties);
$this->assertIsArray($class->constants);
$this->assertEmpty($class->constants);
$this->assertIsArray($class->methods);
$this->assertEmpty($class->methods);
}
public function testPropertyContextIsUniquePerInstance(): void
{
$class1 = new ClassDef('Foo', Modifiers::PUBLIC);
$class2 = new ClassDef('Bar', Modifiers::PUBLIC);
$this->assertNotSame($class1->propertyContext, $class2->propertyContext);
}
}

@ -0,0 +1,104 @@
<?php
namespace PhpAot\Tests\Entity;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\Entity\FunctionDef;
use PhpAot\Php\ArgInfo;
class FunctionDefExtendedTest extends TestCase
{
public function testStubFlagDefaultsToFalse(): void
{
$fn = new FunctionDef('test', 'php::Int', '');
$this->assertFalse($fn->stub);
}
public function testStubFlagCanBeSet(): void
{
$fn = new FunctionDef('test', 'php::Int', '');
$fn->stub = true;
$this->assertTrue($fn->stub);
}
public function testArgCountRequiredDefault(): void
{
$fn = new FunctionDef('test', 'php::Int', '');
$this->assertSame(0, $fn->argCountRequired);
}
public function testArgCountRequiredCanBeSet(): void
{
$fn = new FunctionDef('test', 'php::Int', '');
$fn->argCountRequired = 3;
$this->assertSame(3, $fn->argCountRequired);
}
public function testParamsDefault(): void
{
$fn = new FunctionDef('test', 'php::Int', '');
$this->assertEquals('', $fn->params);
}
public function testParamsCanBeSet(): void
{
$fn = new FunctionDef('test', 'php::Int', '');
$fn->params = 'php::Str a, php::Int b';
$this->assertEquals('php::Str a, php::Int b', $fn->params);
}
public function testReturnClassDefault(): void
{
$fn = new FunctionDef('test', 'php::Object', '');
$this->assertEquals('', $fn->returnClass);
}
public function testReturnClassCanBeSet(): void
{
$fn = new FunctionDef('create', 'php::Object', 'App\\Factory');
$fn->returnClass = 'App\\Entity\\Product';
$this->assertEquals('App\\Entity\\Product', $fn->returnClass);
}
public function testHasVariadicArgWithMultipleNonVariadicArgs(): void
{
$fn = new FunctionDef('test', 'void', '');
$fn->argInfoList = [
new ArgInfo('a', 'php::Int'),
new ArgInfo('b', 'php::Str'),
new ArgInfo('c', 'php::Float'),
];
$this->assertFalse($fn->hasVariadicArg());
}
public function testHasVariadicArgWithSingleNonVariadicArg(): void
{
$fn = new FunctionDef('test', 'void', '');
$fn->argInfoList = [
new ArgInfo('a', 'php::Int'),
];
$this->assertFalse($fn->hasVariadicArg());
}
public function testGetNamespacedNameWithoutNamespaceWithParams(): void
{
$fn = new FunctionDef('run', 'void', '');
$fn->argCountRequired = 1;
$fn->params = 'php::Int n';
$this->assertEquals('run', $fn->getNamespacedName());
}
public function testMethodFlagWithFullSetup(): void
{
$fn = new FunctionDef('handle', 'php::Var', 'App\\Controller');
$fn->method = true;
$fn->argInfoList = [
new ArgInfo('request', 'php::Object'),
];
$fn->returnClass = 'App\\Entity\\Response';
$this->assertTrue($fn->method);
$this->assertEquals('App\\Controller\\handle', $fn->getNamespacedName());
$this->assertEquals('App\\Entity\\Response', $fn->returnClass);
}
}

@ -0,0 +1,69 @@
<?php
namespace PhpAot\Tests\Entity;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\Entity\MethodDef;
use PhpAot\Php\Entity\FunctionDef;
use PhpAot\Php\ArgInfo;
use PhpParser\Modifiers;
class MethodDefExtendedTest extends TestCase
{
public function testHasDynamicCallDefaultsToFalse(): void
{
$method = new MethodDef(Modifiers::PUBLIC, 'test');
$this->assertFalse($method->hasDynamicCall);
}
public function testHasDynamicCallCanBeSet(): void
{
$method = new MethodDef(Modifiers::PUBLIC, 'test');
$method->hasDynamicCall = true;
$this->assertTrue($method->hasDynamicCall);
}
public function testStaticMethod(): void
{
$method = new MethodDef(Modifiers::PUBLIC | Modifiers::STATIC, 'factory');
$this->assertTrue((bool) ($method->flags & Modifiers::STATIC));
}
public function testAbstractMethod(): void
{
$method = new MethodDef(Modifiers::PUBLIC | Modifiers::ABSTRACT, 'handle');
$this->assertTrue((bool) ($method->flags & Modifiers::ABSTRACT));
}
public function testFinalMethod(): void
{
$method = new MethodDef(Modifiers::PUBLIC | Modifiers::FINAL, 'lock');
$this->assertTrue((bool) ($method->flags & Modifiers::FINAL));
}
public function testCombinedFlags(): void
{
$method = new MethodDef(
Modifiers::PUBLIC | Modifiers::STATIC | Modifiers::FINAL,
'combined'
);
$this->assertTrue((bool) ($method->flags & Modifiers::PUBLIC));
$this->assertTrue((bool) ($method->flags & Modifiers::STATIC));
$this->assertTrue((bool) ($method->flags & Modifiers::FINAL));
}
public function testFunctionDefLinkRoundTrip(): void
{
$method = new MethodDef(Modifiers::PUBLIC, 'getValue');
$fn = new FunctionDef('getValue', 'php::Int', 'App\\Service');
$fn->argInfoList = [
new ArgInfo('arg1', 'php::Str'),
];
$fn->params = 'php::Str arg1';
$method->functionDef = $fn;
$this->assertSame($fn, $method->functionDef);
$this->assertEquals('php::Int', $method->getReturnType());
$this->assertEquals('App\\Service\\getValue', $fn->getNamespacedName());
}
}

@ -0,0 +1,320 @@
<?php
namespace PhpAot\Tests\Generator;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\CompilerTest;
use PhpAot\Php\CompilerBase;
class UtilsTest extends TestCase
{
private string $testDir;
private CompilerTest $compiler;
protected function setUp(): void
{
parent::setUp();
$this->testDir = sys_get_temp_dir() . '/utils_test_' . uniqid();
mkdir($this->testDir, 0777, true);
$this->compiler = CompilerTest::create($this->testDir);
}
protected function tearDown(): void
{
parent::tearDown();
array_map('unlink', glob($this->testDir . '/*'));
rmdir($this->testDir);
}
private function invokeMethod(string $method, ...$args): mixed
{
$ref = new \ReflectionClass($this->compiler);
$meth = $ref->getMethod($method);
$meth->setAccessible(true);
return $meth->invoke($this->compiler, ...$args);
}
// ========================================================================
// genCValue
// ========================================================================
public function testGenCValueInt(): void
{
$this->assertSame(42, $this->invokeMethod('genCValue', 42));
$this->assertSame(-1, $this->invokeMethod('genCValue', -1));
$this->assertSame(0, $this->invokeMethod('genCValue', 0));
}
public function testGenCValueFloat(): void
{
$result = $this->invokeMethod('genCValue', 3.14);
$this->assertSame(3.14, $result);
}
public function testGenCValueBool(): void
{
$this->assertSame(1, $this->invokeMethod('genCValue', true));
$this->assertSame(0, $this->invokeMethod('genCValue', false));
}
public function testGenCValueString(): void
{
$result = $this->invokeMethod('genCValue', 'hello');
$this->assertEquals('"hello"', $result);
}
// ========================================================================
// genCharPtr
// ========================================================================
public function testGenCharPtr(): void
{
$this->assertEquals('"hello"', $this->invokeMethod('genCharPtr', 'hello'));
$this->assertEquals('""', $this->invokeMethod('genCharPtr', ''));
}
public function testGenCharPtrEscape(): void
{
$result = $this->invokeMethod('genCharPtr', 'hello "world"', true);
$this->assertStringContainsString('\\"', $result);
}
// ========================================================================
// genZendStrl
// ========================================================================
public function testGenZendStrl(): void
{
$result = $this->invokeMethod('genZendStrl', 'name');
$this->assertStringStartsWith('ZEND_STRL("', $result);
$this->assertStringContainsString('name', $result);
}
// ========================================================================
// genArray
// ========================================================================
public function testGenArray(): void
{
$result = $this->invokeMethod('genArray', ['1', '2', '3']);
$this->assertStringStartsWith(CompilerBase::TYPE_ARRAY . '{', $result);
$this->assertStringContainsString('1, 2, 3', $result);
}
public function testGenArrayEmpty(): void
{
$result = $this->invokeMethod('genArray', []);
$this->assertStringStartsWith(CompilerBase::TYPE_ARRAY . '{', $result);
}
// ========================================================================
// genRawStr
// ========================================================================
public function testGenRawStr(): void
{
$result = $this->invokeMethod('genRawStr', 'hello');
$this->assertEquals('R"(hello)"', $result);
}
public function testGenRawStrMultiLine(): void
{
$result = $this->invokeMethod('genRawStr', "line1\nline2");
$this->assertStringStartsWith('R"(', $result);
$this->assertStringEndsWith(')"', $result);
}
// ========================================================================
// escapeString
// ========================================================================
public function testEscapeStringSimple(): void
{
$this->assertEquals('hello', $this->invokeMethod('escapeString', 'hello'));
}
public function testEscapeStringQuotes(): void
{
$result = $this->invokeMethod('escapeString', 'he"llo');
$this->assertStringContainsString('\\"', $result);
}
public function testEscapeStringBackslash(): void
{
$result = $this->invokeMethod('escapeString', 'a\\b');
$this->assertStringContainsString('\\\\', $result);
}
public function testEscapeStringNewline(): void
{
$result = $this->invokeMethod('escapeString', "a\nb");
// newline may be escaped as \n or literal depending on addcslashes behavior
$this->assertNotEquals("a\nb", $result);
}
// ========================================================================
// escapeBool
// ========================================================================
public function testEscapeBool(): void
{
$this->assertEquals('true', $this->invokeMethod('escapeBool', true));
$this->assertEquals('false', $this->invokeMethod('escapeBool', false));
}
// ========================================================================
// escapeVarName / unescapeVarName
// ========================================================================
public function testEscapeVarNameNormal(): void
{
$this->assertEquals('foo', $this->invokeMethod('escapeVarName', 'foo'));
$this->assertEquals('bar', $this->invokeMethod('escapeVarName', 'bar'));
}
public function testEscapeVarNameThis(): void
{
$this->assertEquals('this_', $this->invokeMethod('escapeVarName', 'this'));
}
public function testEscapeVarNameReservedKeyword(): void
{
// 'class' is in CPP_RESERVED_NAMES
$result = $this->invokeMethod('escapeVarName', 'class');
$this->assertStringStartsWith('_php__var__', $result);
}
public function testUnescapeVarName(): void
{
$this->assertEquals('foo', $this->invokeMethod('unescapeVarName', '_php__var__foo'));
$this->assertEquals('bar', $this->invokeMethod('unescapeVarName', 'bar'));
}
// ========================================================================
// escapeNamespace
// ========================================================================
public function testEscapeNamespace(): void
{
$result = $this->invokeMethod('escapeNamespace', 'App\\Lib\\Module');
$this->assertEquals('app__lib__module', $result);
}
public function testEscapeNamespaceNoBackslash(): void
{
$result = $this->invokeMethod('escapeNamespace', 'app');
$this->assertEquals('app', $result);
}
// ========================================================================
// escapeZendFnName / escapeCeName
// ========================================================================
public function testEscapeZendFnNameLower(): void
{
$result = $this->invokeMethod('escapeZendFnName', 'App\\Foo\\bar', true);
$this->assertEquals('app_foo_bar', $result);
}
public function testEscapeZendFnNameNoLower(): void
{
$result = $this->invokeMethod('escapeZendFnName', 'App\\Foo\\bar', false);
$this->assertEquals('App_Foo_bar', $result);
}
public function testEscapeCeName(): void
{
$result = $this->invokeMethod('escapeCeName', 'App\\Entity\\User');
$this->assertEquals('App_Entity_User', $result);
}
// ========================================================================
// escapeName
// ========================================================================
public function testEscapeName(): void
{
$this->assertEquals('foo', $this->invokeMethod('escapeName', 'FOO'));
$this->assertEquals('bar', $this->invokeMethod('escapeName', 'BAR'));
}
// ========================================================================
// escapeClass / escapeFunction
// ========================================================================
public function testEscapeClass(): void
{
$result = $this->invokeMethod('escapeClass', '\\App\\Lib\\MyClass');
$this->assertEquals('app_lib_myclass', $result);
}
public function testEscapeFunction(): void
{
$result = $this->invokeMethod('escapeFunction', 'App\\Foo\\run');
$this->assertEquals('app_foo_run', $result);
}
// ========================================================================
// escapeFileName
// ========================================================================
public function testEscapeFileName(): void
{
$this->assertEquals('my_file', $this->invokeMethod('escapeFileName', 'my-file'));
$this->assertEquals('a_b_c', $this->invokeMethod('escapeFileName', 'a-b-c'));
}
public function testEscapeFileNameNoDash(): void
{
$this->assertEquals('already_good', $this->invokeMethod('escapeFileName', 'already_good'));
}
// ========================================================================
// escapeGlobalVar
// ========================================================================
public function testEscapeGlobalVar(): void
{
$result = $this->invokeMethod('escapeGlobalVar', 'myvar');
$this->assertStringStartsWith('_global_var_', $result);
$this->assertStringEndsWith('myvar', $result);
}
// ========================================================================
// isClosedExpr
// ========================================================================
public function testIsClosedExprSimple(): void
{
$this->assertTrue($this->invokeMethod('isClosedExpr', '(a + b)', ''));
$this->assertTrue($this->invokeMethod('isClosedExpr', '(1)', ''));
}
public function testIsClosedExprNotClosed(): void
{
$this->assertFalse($this->invokeMethod('isClosedExpr', 'a + b', ''));
$this->assertFalse($this->invokeMethod('isClosedExpr', '(a + b', ''));
}
public function testIsClosedExprNested(): void
{
$this->assertTrue($this->invokeMethod('isClosedExpr', '((a + b) * c)', ''));
}
public function testIsClosedExprWithCall(): void
{
$this->assertTrue($this->invokeMethod('isClosedExpr', 'foo(1, 2)', 'foo'));
$this->assertFalse($this->invokeMethod('isClosedExpr', 'bar(1, 2)', 'foo'));
}
// ========================================================================
// trimBrackets
// ========================================================================
public function testTrimBrackets(): void
{
$this->assertEquals('a + b', $this->invokeMethod('trimBrackets', '(a + b)'));
$this->assertEquals('not wrapped', $this->invokeMethod('trimBrackets', 'not wrapped'));
}
}

@ -0,0 +1,229 @@
<?php
namespace PhpAot\Tests;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\CompilerTest;
use PhpAot\Php\ArgInfo;
use PhpParser\Node;
class PreprocessorTest extends TestCase
{
private string $testDir;
private CompilerTest $compiler;
private \ReflectionClass $ref;
protected function setUp(): void
{
parent::setUp();
$this->testDir = sys_get_temp_dir() . '/preprocessor_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();
if (is_dir($this->testDir)) {
$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 invokeMethod(string $method, ...$args): mixed
{
$m = $this->ref->getMethod($method);
$m->setAccessible(true);
return $m->invoke($this->compiler, ...$args);
}
private function setProperty(string $name, mixed $value): void
{
$prop = $this->ref->getProperty($name);
$prop->setAccessible(true);
$prop->setValue($this->compiler, $value);
}
// ========================================================================
// genArgumentDeclaration
// ========================================================================
public function testGenArgumentDeclarationSimple(): void
{
$arg = new ArgInfo();
$arg->name = 'count';
$arg->type = 'php::Int';
$result = $this->invokeMethod('genArgumentDeclaration', $arg);
$this->assertEquals('php::Int count', $result);
}
public function testGenArgumentDeclarationString(): void
{
$arg = new ArgInfo();
$arg->name = 'name';
$arg->type = 'php::Str';
$result = $this->invokeMethod('genArgumentDeclaration', $arg);
$this->assertEquals('php::Str name', $result);
}
public function testGenArgumentDeclarationObject(): void
{
$arg = new ArgInfo();
$arg->name = 'obj';
$arg->type = 'php::Object';
$result = $this->invokeMethod('genArgumentDeclaration', $arg);
$this->assertEquals('php::Object obj', $result);
}
public function testGenArgumentDeclarationUnsafePtr(): void
{
$arg = new ArgInfo();
$arg->name = 'container';
$arg->type = 'php::Var';
$arg->unsafePtr = true;
$result = $this->invokeMethod('genArgumentDeclaration', $arg);
$this->assertEquals('php::Var container', $result);
}
// ========================================================================
// getCppFile
// ========================================================================
public function testGetCppFile(): void
{
$phpFile = '/home/user/project/src/app.php';
$result = $this->compiler->getCppFile($phpFile);
$this->assertStringEndsWith('.cc', $result);
$this->assertStringStartsWith($this->compiler->getBuildDir(), $result);
$this->assertStringContainsString('app', $result);
}
public function testGetCppFilePreservesRelativePath(): void
{
$phpFile = '/var/www/myapp/controllers/UserController.php';
$result = $this->compiler->getCppFile($phpFile);
$this->assertStringEndsWith('UserController.cc', $result);
$this->assertStringStartsWith($this->compiler->getBuildDir(), $result);
}
public function testGetCppFileDotPhpReplaced(): void
{
$phpFile = '/tmp/test_file.php';
$result = $this->compiler->getCppFile($phpFile);
$this->assertStringEndsWith('test_file.cc', $result);
$this->assertStringNotContainsString('.php', basename($result));
}
// ========================================================================
// getObjectFile
// ========================================================================
public function testGetObjectFile(): void
{
$cppFile = $this->compiler->getBuildDir() . '/include/test.cc';
$result = $this->compiler->getObjectFile($cppFile);
$this->assertStringEndsWith('.o', $result);
$this->assertStringContainsString('test', $result);
}
public function testGetObjectFileDifferentObjectExtension(): void
{
// On Linux the object extension is .o
$path = '/some/path/file.cc';
$result = $this->compiler->getObjectFile($path);
$this->assertStringEndsWith('file.o', $result);
}
// ========================================================================
// getMethodName
// ========================================================================
public function testGetMethodName(): void
{
$method = new Node\Stmt\ClassMethod('handle');
$result = $this->invokeMethod('getMethodName', $method);
$this->assertEquals('handle', $result);
}
public function testGetMethodNameConstructor(): void
{
$method = new Node\Stmt\ClassMethod('__construct');
$result = $this->invokeMethod('getMethodName', $method);
$this->assertEquals('__construct', $result);
}
// ========================================================================
// getParentClass
// ========================================================================
public function testGetParentClassWithNamespace(): void
{
$extends = new Node\Name('BaseController');
$this->setProperty('namespace', 'App\\Controllers');
$result = $this->invokeMethod('getParentClass', $extends);
$this->assertEquals('App\\Controllers\\BaseController', $result);
}
public function testGetParentClassFullyQualified(): void
{
$extends = new Node\Name\FullyQualified('App\\Entity\\Base');
$result = $this->invokeMethod('getParentClass', $extends);
$this->assertEquals('App\\Entity\\Base', $result);
}
// ========================================================================
// hasCppFileCache
// ========================================================================
public function testHasCppFileCacheWhenDisabled(): void
{
// enableCache defaults to false, so cache should always return false
$this->assertFalse($this->compiler->hasCppFileCache('/tmp/test.php'));
}
public function testHasCppFileCacheNonexistentFile(): void
{
$this->assertFalse($this->compiler->hasCppFileCache('/nonexistent/path/test.php'));
}
// ========================================================================
// sortFiles
// ========================================================================
public function testSortFilesPreservesOrderForUnrelatedFiles(): void
{
$files = ['/a/file1.php', '/a/file2.php', '/a/file3.php'];
$this->compiler->sortFiles($files);
// All original files must still be present
$this->assertContains('/a/file1.php', $files);
$this->assertContains('/a/file2.php', $files);
$this->assertContains('/a/file3.php', $files);
// Original files are preserved (sortFiles may append, not remove)
$this->assertGreaterThanOrEqual(3, count($files));
}
public function testSortFilesEmpty(): void
{
$files = [];
$this->compiler->sortFiles($files);
// Empty array stays empty or nearly empty
$this->assertIsArray($files);
}
}

@ -0,0 +1,139 @@
<?php
namespace PhpAot\Tests;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\Reflection;
class ReflectionTest extends TestCase
{
public function testIsInternalClass(): void
{
// Standard PHP internal classes
$this->assertTrue(Reflection::isInternalClass('stdClass'));
$this->assertTrue(Reflection::isInternalClass('Exception'));
$this->assertTrue(Reflection::isInternalClass('DateTime'));
}
public function testIsInternalClassCaseInsensitive(): void
{
$this->assertTrue(Reflection::isInternalClass('stdclass'));
$this->assertTrue(Reflection::isInternalClass('EXCEPTION'));
}
public function testIsInternalClassNonexistent(): void
{
$this->assertFalse(Reflection::isInternalClass('NonExistentClass_' . uniqid()));
}
public function testIsInternalInterface(): void
{
$this->assertTrue(Reflection::isInternalInterface('Iterator'));
$this->assertTrue(Reflection::isInternalInterface('ArrayAccess'));
$this->assertTrue(Reflection::isInternalInterface('JsonSerializable'));
}
public function testIsInternalInterfaceNonexistent(): void
{
$this->assertFalse(Reflection::isInternalInterface('NonExistentIface_' . uniqid()));
}
public function testGetFunction(): void
{
$ref = Reflection::getFunction('strlen');
$this->assertNotNull($ref);
$this->assertInstanceOf(\ReflectionFunction::class, $ref);
$this->assertEquals('strlen', $ref->getName());
}
public function testGetFunctionNonexistent(): void
{
$ref = Reflection::getFunction('nonexistent_func_' . uniqid());
$this->assertNull($ref);
}
public function testGetFunctionCaches(): void
{
$ref1 = Reflection::getFunction('strtolower');
$ref2 = Reflection::getFunction('strtolower');
$this->assertSame($ref1, $ref2);
}
public function testGetClass(): void
{
$ref = Reflection::getClass('stdClass');
$this->assertNotNull($ref);
$this->assertInstanceOf(\ReflectionClass::class, $ref);
}
public function testGetClassNonexistent(): void
{
$ref = Reflection::getClass('NonExistent_' . uniqid());
$this->assertNull($ref);
}
public function testGetFunctionReturnType(): void
{
$type = Reflection::getFunctionReturnType('strlen');
$this->assertEquals('int', $type);
}
public function testGetFunctionReturnTypeNonexistent(): void
{
$type = Reflection::getFunctionReturnType('nonexistent_' . uniqid());
$this->assertNull($type);
}
public function testGetFunctionParameter(): void
{
$param = Reflection::getFunctionParameter('strlen', 0);
$this->assertNotNull($param);
$this->assertInstanceOf(\ReflectionParameter::class, $param);
}
public function testGetFunctionParameterOutOfRange(): void
{
$param = Reflection::getFunctionParameter('strlen', 999);
$this->assertNull($param);
}
public function testHasMethod(): void
{
$this->assertTrue(Reflection::hasMethod('Exception', 'getMessage'));
$this->assertTrue(Reflection::hasMethod('Exception', '__construct'));
// Method that doesn't exist
$this->assertFalse(Reflection::hasMethod('Exception', 'nonexistent_' . uniqid()));
}
public function testGetClassMethodModifiers(): void
{
$modifiers = Reflection::getClassMethodModifiers('DateTime', 'format');
$this->assertNotNull($modifiers);
$this->assertIsInt($modifiers);
}
public function testGetClassMethodModifiersNonexistent(): void
{
$modifiers = Reflection::getClassMethodModifiers('NonExistent_' . uniqid(), 'test');
$this->assertNull($modifiers);
}
public function testGetMethodReturnType(): void
{
$type = Reflection::getMethodReturnType('Exception', 'getMessage');
$this->assertEquals('string', $type);
}
public function testGetMethodReturnTypeNonexistent(): void
{
$type = Reflection::getMethodReturnType('NonExistent_' . uniqid(), 'test');
$this->assertNull($type);
}
public function testIsAbstractClass(): void
{
$this->assertFalse(Reflection::isAbstractClass('stdClass'));
$this->assertFalse(Reflection::isAbstractClass('NonExistent_' . uniqid()));
}
}

@ -0,0 +1,311 @@
<?php
namespace PhpAot\Tests;
use PHPUnit\Framework\TestCase;
use PhpAot\Php\CompilerTest;
use PhpAot\Php\CompilerBase;
use PhpAot\Php\ArgInfo;
class TraitsTest extends TestCase
{
private string $testDir;
private CompilerTest $compiler;
private \ReflectionClass $ref;
protected function setUp(): void
{
parent::setUp();
$this->testDir = sys_get_temp_dir() . '/traits_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();
if (is_dir($this->testDir)) {
$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 invoke(string $method, ...$args): mixed
{
$m = $this->ref->getMethod($method);
$m->setAccessible(true);
return $m->invoke($this->compiler, ...$args);
}
// ========================================================================
// MagicMethodDetector::checkArgType
// ========================================================================
public function testCheckArgTypeExactMatch(): void
{
$this->assertTrue($this->invoke('checkArgType', 'php::Int', 'php::Int'));
$this->assertTrue($this->invoke('checkArgType', 'php::Str', 'php::Str'));
$this->assertTrue($this->invoke('checkArgType', 'php::Array', 'php::Array'));
}
public function testCheckArgTypeMismatch(): void
{
$this->assertFalse($this->invoke('checkArgType', 'php::Int', 'php::Str'));
$this->assertFalse($this->invoke('checkArgType', 'php::Float', 'php::Int'));
}
public function testCheckArgTypeVarMatchesAny(): void
{
// TYPE_VAR matches any expected type when canBeVar is true (default)
$this->assertTrue($this->invoke('checkArgType', 'php::Var', 'php::Int'));
$this->assertTrue($this->invoke('checkArgType', 'php::Var', 'php::Str'));
$this->assertTrue($this->invoke('checkArgType', 'php::Var', 'php::Array'));
}
public function testCheckArgTypeVarDoesNotMatchWhenCannotBeVar(): void
{
// TYPE_VAR does NOT match when canBeVar is false
$this->assertFalse($this->invoke('checkArgType', 'php::Var', 'php::Str', false));
$this->assertFalse($this->invoke('checkArgType', 'php::Var', 'php::Int', false));
}
public function testCheckArgTypeExactMatchWithCannotBeVar(): void
{
$this->assertTrue($this->invoke('checkArgType', 'php::Str', 'php::Str', false));
$this->assertTrue($this->invoke('checkArgType', 'php::Array', 'php::Array', false));
}
public function testCheckArgTypeVoid(): void
{
$this->assertTrue($this->invoke('checkArgType', 'void', 'void'));
$this->assertFalse($this->invoke('checkArgType', 'void', 'php::Str'));
}
// ========================================================================
// FuncCallOptimizer::isValidDefineName
// ========================================================================
public function testIsValidDefineNameValid(): void
{
$this->assertTrue($this->invoke('isValidDefineName', 'MY_CONSTANT'));
$this->assertTrue($this->invoke('isValidDefineName', 'APP_NAME'));
$this->assertTrue($this->invoke('isValidDefineName', '_PRIVATE'));
$this->assertTrue($this->invoke('isValidDefineName', 'camelCase'));
$this->assertTrue($this->invoke('isValidDefineName', 'Test123'));
$this->assertTrue($this->invoke('isValidDefineName', '_'));
}
public function testIsValidDefineNameInvalid(): void
{
$this->assertFalse($this->invoke('isValidDefineName', '123abc')); // starts with digit
$this->assertFalse($this->invoke('isValidDefineName', 'has space')); // contains space
$this->assertFalse($this->invoke('isValidDefineName', 'has-dash')); // contains dash
$this->assertFalse($this->invoke('isValidDefineName', '')); // empty
$this->assertFalse($this->invoke('isValidDefineName', '0abc')); // starts with zero
}
// ========================================================================
// StdContainerParser::isStdContainerType
// ========================================================================
public function testIsStdContainerTypeTrue(): void
{
$this->assertTrue($this->invoke('isStdContainerType', CompilerBase::TYPE_STD_ARRAY));
$this->assertTrue($this->invoke('isStdContainerType', CompilerBase::TYPE_STD_VECTOR));
$this->assertTrue($this->invoke('isStdContainerType', CompilerBase::TYPE_STD_MAP));
$this->assertTrue($this->invoke('isStdContainerType', CompilerBase::TYPE_STD_UNORDERED_MAP));
}
public function testIsStdContainerTypeFalse(): void
{
$this->assertFalse($this->invoke('isStdContainerType', CompilerBase::TYPE_ARRAY));
$this->assertFalse($this->invoke('isStdContainerType', CompilerBase::TYPE_INT));
$this->assertFalse($this->invoke('isStdContainerType', CompilerBase::TYPE_VAR));
$this->assertFalse($this->invoke('isStdContainerType', CompilerBase::TYPE_OBJECT));
}
// ========================================================================
// StdContainerParser::getStdTypeKey
// ========================================================================
public function testGetStdTypeKeyBasic(): void
{
$info = [
'kind' => 'vector',
'decl' => 'php::StdVector<php::Int>',
'type' => 'php::Int',
'class' => '',
];
$key = $this->invoke('getStdTypeKey', $info);
$this->assertStringContainsString('kind=vector', $key);
$this->assertStringContainsString('decl=php::StdVector<php::Int>', $key);
$this->assertStringContainsString('type=php::Int', $key);
$this->assertStringContainsString('class=', $key);
}
public function testGetStdTypeKeyWithClass(): void
{
$info = [
'kind' => 'vector',
'decl' => 'php::StdVector<php::Object>',
'type' => 'php::Object',
'class' => 'App\\Entity\\User',
];
$key = $this->invoke('getStdTypeKey', $info);
$this->assertStringContainsString('class=App\\Entity\\User', $key);
}
public function testGetStdTypeKeyWithKeyType(): void
{
$info = [
'kind' => 'map',
'decl' => 'php::StdMap<php::Str, php::Int>',
'type' => 'php::Int',
'class' => '',
'keyType' => 'php::Str',
];
$key = $this->invoke('getStdTypeKey', $info);
$this->assertStringContainsString('keyType=php::Str', $key);
}
public function testGetStdTypeKeyWithoutKeyType(): void
{
$info = [
'kind' => 'array',
'decl' => 'php::StdArray<php::Int, 10>',
'type' => 'php::Int',
'class' => '',
];
$key = $this->invoke('getStdTypeKey', $info);
$this->assertStringNotContainsString('keyType', $key);
}
public function testGetStdTypeKeyUnorderedMap(): void
{
$info = [
'kind' => 'unordered_map',
'decl' => 'php::StdUnorderedMap<php::Str, php::Int>',
'type' => 'php::Int',
'class' => '',
'keyType' => 'php::Str',
];
$key = $this->invoke('getStdTypeKey', $info);
$this->assertStringContainsString('kind=unordered_map', $key);
}
// ========================================================================
// PropertyPromotion::genPropertyPromotion
// ========================================================================
public function testGenPropertyPromotion(): void
{
// Need context initialized for genCharPtr
$this->invoke('resetFunction');
$argInfo = new ArgInfo();
$argInfo->name = 'title';
$argInfo->type = 'php::Str';
$result = $this->invoke('genPropertyPromotion', $argInfo);
$this->assertStringContainsString('this_.setProperty', $result);
$this->assertStringContainsString('title', $result);
}
public function testGenPropertyPromotionWithNumber(): void
{
$this->invoke('resetFunction');
$argInfo = new ArgInfo();
$argInfo->name = 'count';
$argInfo->type = 'php::Int';
$result = $this->invoke('genPropertyPromotion', $argInfo);
$this->assertStringContainsString('count', $result);
}
// ========================================================================
// ClosureGenerator::genScopeSwitchCode
// ========================================================================
public function testGenScopeSwitchCode(): void
{
$this->invoke('resetFunction');
$result = $this->invoke('genScopeSwitchCode');
$this->assertStringContainsString('php_switch_scope', $result);
$this->assertStringContainsString('ON_SCOPE_EXIT', $result);
$this->assertStringContainsString('php_restore_scope', $result);
}
public function testGenScopeSwitchCodeTemplate(): void
{
$this->invoke('resetFunction');
$result = $this->invoke('genScopeSwitchCode');
// Should have the pattern: auto tmp_var_X = php_switch_scope(this_);
$this->assertStringContainsString('auto', $result);
$this->assertStringContainsString('= php_switch_scope(this_)', $result);
}
// ========================================================================
// StdContainerParser::getStdArrayDecl
// ========================================================================
public function testGetStdArrayDecl(): void
{
$result = $this->invoke('getStdArrayDecl', 'php::Int', [10]);
$this->assertEquals('php::StdArray<php::Int, 10>', $result);
}
public function testGetStdArrayDeclNested(): void
{
$result = $this->invoke('getStdArrayDecl', 'php::Int', [3, 4]);
$this->assertEquals('php::StdArray<php::StdArray<php::Int, 4>, 3>', $result);
}
public function testGetStdArrayDeclFloat(): void
{
$result = $this->invoke('getStdArrayDecl', 'php::Float', [5]);
$this->assertEquals('php::StdArray<php::Float, 5>', $result);
}
// ========================================================================
// StdContainerParser::getStdMapDecl
// ========================================================================
public function testGetStdMapDecl(): void
{
$result = $this->invoke('getStdMapDecl', 'std::map', 'php::Str', 'php::Int');
$this->assertEquals('std::map<php::Str, php::Int>', $result);
}
public function testGetStdMapDeclUnordered(): void
{
$result = $this->invoke('getStdMapDecl', 'std::unordered_map', 'php::Int', 'php::Str');
$this->assertEquals('std::unordered_map<php::Int, php::Str>', $result);
}
// ========================================================================
// StdContainerParser::getStdValueTypeBytes
// ========================================================================
public function testGetStdValueTypeBytes(): void
{
$this->assertGreaterThan(0, $this->invoke('getStdValueTypeBytes', CompilerBase::TYPE_INT));
$this->assertGreaterThan(0, $this->invoke('getStdValueTypeBytes', CompilerBase::TYPE_FLOAT));
$this->assertGreaterThan(0, $this->invoke('getStdValueTypeBytes', CompilerBase::TYPE_BOOL));
}
}
Loading…
Cancel
Save