From efd6beac87653f8273bc7cf29081067b3b19aec5 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 19 May 2026 19:31:09 +0800 Subject: [PATCH] =?UTF-8?q?test(phpunit):=20=E6=B7=BB=E5=8A=A0PHP=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E5=99=A8=E7=9B=B8=E5=85=B3=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建AST节点类型测试类AstNodeTypeTest,测试各种节点类型的识别方法 - 添加ClassDef扩展功能测试类ClassDefExtendedTest,验证类定义的各种属性和方法 - 实现CompilerBase API测试类CompilerBaseApiTest,测试类型转换、变量名生成等功能 - 创建FunctionContext扩展测试类Context/FunctionContextExtendedTest,测试作用域管理和变量操作 - 添加FunctionDef扩展测试类Entity/FunctionDefExtendedTest,测试函数定义相关功能 --- phpunit/src/AstNodeTypeTest.php | 436 ++++++++++++++++++ phpunit/src/CompilerBaseApiTest.php | 338 ++++++++++++++ .../Context/FunctionContextExtendedTest.php | 146 ++++++ phpunit/src/Entity/ClassDefExtendedTest.php | 137 ++++++ .../src/Entity/FunctionDefExtendedTest.php | 104 +++++ phpunit/src/Entity/MethodDefExtendedTest.php | 69 +++ phpunit/src/Generator/UtilsTest.php | 320 +++++++++++++ phpunit/src/PreprocessorTest.php | 229 +++++++++ phpunit/src/ReflectionTest.php | 139 ++++++ phpunit/src/TraitsTest.php | 311 +++++++++++++ 10 files changed, 2229 insertions(+) create mode 100644 phpunit/src/AstNodeTypeTest.php create mode 100644 phpunit/src/CompilerBaseApiTest.php create mode 100644 phpunit/src/Context/FunctionContextExtendedTest.php create mode 100644 phpunit/src/Entity/ClassDefExtendedTest.php create mode 100644 phpunit/src/Entity/FunctionDefExtendedTest.php create mode 100644 phpunit/src/Entity/MethodDefExtendedTest.php create mode 100644 phpunit/src/Generator/UtilsTest.php create mode 100644 phpunit/src/PreprocessorTest.php create mode 100644 phpunit/src/ReflectionTest.php create mode 100644 phpunit/src/TraitsTest.php diff --git a/phpunit/src/AstNodeTypeTest.php b/phpunit/src/AstNodeTypeTest.php new file mode 100644 index 00000000..b196e0e0 --- /dev/null +++ b/phpunit/src/AstNodeTypeTest.php @@ -0,0 +1,436 @@ +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" + ); + } + } +} diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php new file mode 100644 index 00000000..98cb083d --- /dev/null +++ b/phpunit/src/CompilerBaseApiTest.php @@ -0,0 +1,338 @@ +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); + } +} diff --git a/phpunit/src/Context/FunctionContextExtendedTest.php b/phpunit/src/Context/FunctionContextExtendedTest.php new file mode 100644 index 00000000..47892de6 --- /dev/null +++ b/phpunit/src/Context/FunctionContextExtendedTest.php @@ -0,0 +1,146 @@ +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']; + $ctx->stdArrays['arr2'] = ['kind' => 'array', 'decl' => 'php::StdArray']; + + $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']; + $ctx->stdContainers['map'] = ['kind' => 'map', 'decl' => 'php::StdMap']; + + $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); + } +} diff --git a/phpunit/src/Entity/ClassDefExtendedTest.php b/phpunit/src/Entity/ClassDefExtendedTest.php new file mode 100644 index 00000000..b4ffb503 --- /dev/null +++ b/phpunit/src/Entity/ClassDefExtendedTest.php @@ -0,0 +1,137 @@ +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); + } +} diff --git a/phpunit/src/Entity/FunctionDefExtendedTest.php b/phpunit/src/Entity/FunctionDefExtendedTest.php new file mode 100644 index 00000000..e2c0a42e --- /dev/null +++ b/phpunit/src/Entity/FunctionDefExtendedTest.php @@ -0,0 +1,104 @@ +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); + } +} diff --git a/phpunit/src/Entity/MethodDefExtendedTest.php b/phpunit/src/Entity/MethodDefExtendedTest.php new file mode 100644 index 00000000..18efadcd --- /dev/null +++ b/phpunit/src/Entity/MethodDefExtendedTest.php @@ -0,0 +1,69 @@ +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()); + } +} diff --git a/phpunit/src/Generator/UtilsTest.php b/phpunit/src/Generator/UtilsTest.php new file mode 100644 index 00000000..0018497b --- /dev/null +++ b/phpunit/src/Generator/UtilsTest.php @@ -0,0 +1,320 @@ +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')); + } +} diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php new file mode 100644 index 00000000..4ddc0602 --- /dev/null +++ b/phpunit/src/PreprocessorTest.php @@ -0,0 +1,229 @@ +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); + } +} diff --git a/phpunit/src/ReflectionTest.php b/phpunit/src/ReflectionTest.php new file mode 100644 index 00000000..1158c2cf --- /dev/null +++ b/phpunit/src/ReflectionTest.php @@ -0,0 +1,139 @@ +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())); + } +} diff --git a/phpunit/src/TraitsTest.php b/phpunit/src/TraitsTest.php new file mode 100644 index 00000000..9e8b4878 --- /dev/null +++ b/phpunit/src/TraitsTest.php @@ -0,0 +1,311 @@ +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', + 'type' => 'php::Int', + 'class' => '', + ]; + $key = $this->invoke('getStdTypeKey', $info); + $this->assertStringContainsString('kind=vector', $key); + $this->assertStringContainsString('decl=php::StdVector', $key); + $this->assertStringContainsString('type=php::Int', $key); + $this->assertStringContainsString('class=', $key); + } + + public function testGetStdTypeKeyWithClass(): void + { + $info = [ + 'kind' => 'vector', + 'decl' => 'php::StdVector', + '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', + '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', + 'type' => 'php::Int', + 'class' => '', + ]; + $key = $this->invoke('getStdTypeKey', $info); + $this->assertStringNotContainsString('keyType', $key); + } + + public function testGetStdTypeKeyUnorderedMap(): void + { + $info = [ + 'kind' => 'unordered_map', + 'decl' => 'php::StdUnorderedMap', + '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', $result); + } + + public function testGetStdArrayDeclNested(): void + { + $result = $this->invoke('getStdArrayDecl', 'php::Int', [3, 4]); + $this->assertEquals('php::StdArray, 3>', $result); + } + + public function testGetStdArrayDeclFloat(): void + { + $result = $this->invoke('getStdArrayDecl', 'php::Float', [5]); + $this->assertEquals('php::StdArray', $result); + } + + // ======================================================================== + // StdContainerParser::getStdMapDecl + // ======================================================================== + + public function testGetStdMapDecl(): void + { + $result = $this->invoke('getStdMapDecl', 'std::map', 'php::Str', 'php::Int'); + $this->assertEquals('std::map', $result); + } + + public function testGetStdMapDeclUnordered(): void + { + $result = $this->invoke('getStdMapDecl', 'std::unordered_map', 'php::Int', 'php::Str'); + $this->assertEquals('std::unordered_map', $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)); + } +}