diff --git a/phpunit/src/ArgInfoTest.php b/phpunit/src/ArgInfoTest.php new file mode 100644 index 00000000..0eed83d0 --- /dev/null +++ b/phpunit/src/ArgInfoTest.php @@ -0,0 +1,114 @@ +name = ''; + $arg->type = ''; + + // default values for implicitly-initialized fields + $this->assertEquals('', $arg->default); + $this->assertNull($arg->defaultValue); + $this->assertEquals('', $arg->class); + $this->assertFalse($arg->byRef); + $this->assertFalse($arg->variadic); + $this->assertFalse($arg->nullable); + $this->assertFalse($arg->property); + $this->assertFalse($arg->unsafePtr); + } + + public function testNameAndType(): void + { + $arg = new ArgInfo(); + $arg->name = 'value'; + $arg->type = 'php::Int'; + + $this->assertEquals('value', $arg->name); + $this->assertEquals('php::Int', $arg->type); + } + + public function testDefaultValues(): void + { + $arg = new ArgInfo(); + $arg->default = '0'; + + $this->assertEquals('0', $arg->default); + } + + public function testClass(): void + { + $arg = new ArgInfo(); + $arg->class = 'DateTime'; + + $this->assertEquals('DateTime', $arg->class); + } + + public function testByRef(): void + { + $arg = new ArgInfo(); + $this->assertFalse($arg->byRef); + + $arg->byRef = true; + $this->assertTrue($arg->byRef); + } + + public function testVariadic(): void + { + $arg = new ArgInfo(); + $this->assertFalse($arg->variadic); + + $arg->variadic = true; + $this->assertTrue($arg->variadic); + } + + public function testNullable(): void + { + $arg = new ArgInfo(); + $this->assertFalse($arg->nullable); + + $arg->nullable = true; + $this->assertTrue($arg->nullable); + } + + public function testProperty(): void + { + $arg = new ArgInfo(); + $this->assertFalse($arg->property); + + $arg->property = true; + $this->assertTrue($arg->property); + } + + public function testUnsafePtr(): void + { + $arg = new ArgInfo(); + $this->assertFalse($arg->unsafePtr); + + $arg->unsafePtr = true; + $this->assertTrue($arg->unsafePtr); + } + + public function testAllFlagsCombined(): void + { + $arg = new ArgInfo(); + $arg->name = 'args'; + $arg->type = 'php::Var'; + $arg->variadic = true; + $arg->byRef = true; + $arg->nullable = true; + + $this->assertEquals('args', $arg->name); + $this->assertEquals('php::Var', $arg->type); + $this->assertTrue($arg->variadic); + $this->assertTrue($arg->byRef); + $this->assertTrue($arg->nullable); + } +} diff --git a/phpunit/src/Context/FunctionContextTest.php b/phpunit/src/Context/FunctionContextTest.php new file mode 100644 index 00000000..420cd85c --- /dev/null +++ b/phpunit/src/Context/FunctionContextTest.php @@ -0,0 +1,113 @@ +assertEmpty($ctx->objects); + $this->assertEmpty($ctx->stdArrays); + $this->assertEmpty($ctx->stdContainers); + $this->assertEmpty($ctx->localVars); + $this->assertEmpty($ctx->staticVars); + $this->assertEmpty($ctx->globalVars); + $this->assertEmpty($ctx->ceWrappers); + $this->assertEmpty($ctx->arguments); + $this->assertEmpty($ctx->beforeStmtLines); + $this->assertEmpty($ctx->afterStmtLines); + $this->assertEmpty($ctx->objectProps); + $this->assertEmpty($ctx->scopeLayouts); + + $this->assertSame(0, $ctx->tmpVarIndex); + $this->assertSame(0, $ctx->scopeLevel); + $this->assertFalse($ctx->inLoop); + $this->assertFalse($ctx->inClosure); + $this->assertFalse($ctx->inAssignExpr); + } + + public function testEnterScopeIncrementsLevel(): void + { + $ctx = new FunctionContext(); + $this->assertSame(0, $ctx->scopeLevel); + + $ctx->enterScope(); + $this->assertSame(1, $ctx->scopeLevel); + $this->assertArrayHasKey(0, $ctx->scopeLayouts); + $this->assertInstanceOf(ScopeContext::class, $ctx->scopeLayouts[0]); + + $ctx->enterScope(); + $this->assertSame(2, $ctx->scopeLevel); + $this->assertArrayHasKey(0, $ctx->scopeLayouts); + $this->assertArrayHasKey(1, $ctx->scopeLayouts); + } + + public function testLeaveScopeDecrementsLevel(): void + { + $ctx = new FunctionContext(); + $ctx->enterScope(); + $ctx->enterScope(); + $this->assertSame(2, $ctx->scopeLevel); + + $ctx->leaveScope(); + $this->assertSame(1, $ctx->scopeLevel); + + $ctx->leaveScope(); + $this->assertSame(0, $ctx->scopeLevel); + } + + public function testMutableProperties(): void + { + $ctx = new FunctionContext(); + + $ctx->inLoop = true; + $this->assertTrue($ctx->inLoop); + + $ctx->inClosure = true; + $this->assertTrue($ctx->inClosure); + + $ctx->inAssignExpr = true; + $this->assertTrue($ctx->inAssignExpr); + + $ctx->tmpVarIndex = 5; + $this->assertSame(5, $ctx->tmpVarIndex); + } + + public function testLocalVarsManipulation(): void + { + $ctx = new FunctionContext(); + $ctx->localVars['x'] = 'php::Int'; + $ctx->localVars['name'] = 'php::Str'; + + $this->assertArrayHasKey('x', $ctx->localVars); + $this->assertArrayHasKey('name', $ctx->localVars); + $this->assertEquals('php::Int', $ctx->localVars['x']); + $this->assertEquals('php::Str', $ctx->localVars['name']); + } + + public function testObjectsMap(): void + { + $ctx = new FunctionContext(); + $ctx->objects['obj1'] = 'stdClass'; + $ctx->objects['obj2'] = 'DateTime'; + + $this->assertCount(2, $ctx->objects); + $this->assertEquals('stdClass', $ctx->objects['obj1']); + } + + public function testBeforeAfterStmtLines(): void + { + $ctx = new FunctionContext(); + $ctx->beforeStmtLines[] = 'int x = 1;'; + $ctx->afterStmtLines[] = 'x = 0;'; + + $this->assertCount(1, $ctx->beforeStmtLines); + $this->assertCount(1, $ctx->afterStmtLines); + } +} diff --git a/phpunit/src/Context/ScopeContextTest.php b/phpunit/src/Context/ScopeContextTest.php new file mode 100644 index 00000000..24356f0e --- /dev/null +++ b/phpunit/src/Context/ScopeContextTest.php @@ -0,0 +1,15 @@ +assertInstanceOf(ScopeContext::class, $scope); + } +} diff --git a/phpunit/src/Entity/ClassDefTest.php b/phpunit/src/Entity/ClassDefTest.php new file mode 100644 index 00000000..18edb767 --- /dev/null +++ b/phpunit/src/Entity/ClassDefTest.php @@ -0,0 +1,133 @@ +assertEquals('Foo', $class->name); + $this->assertEquals('', $class->namespace); + $this->assertSame(Modifiers::PUBLIC, $class->flags); + $this->assertEquals('', $class->extends); + $this->assertEmpty($class->implements); + $this->assertEmpty($class->methods); + $this->assertEmpty($class->properties); + $this->assertEmpty($class->constants); + $this->assertFalse($class->enum); + $this->assertFalse($class->requireCtor); + $this->assertFalse($class->inheritedFromInternalClass); + $this->assertNull($class->trait); + } + + public function testConstructWithNamespace(): void + { + $class = new ClassDef('Bar', Modifiers::PUBLIC, 'App\\Lib'); + $this->assertEquals('Bar', $class->name); + $this->assertEquals('App\\Lib', $class->namespace); + } + + public function testPropertyContextIsInitialized(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $this->assertNotNull($class->propertyContext); + $this->assertInstanceOf(\PhpAot\Php\Context\FunctionContext::class, $class->propertyContext); + } + + public function testGetNamespacedNameInheritedFromClassLikeDef(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC, 'A\\B'); + $this->assertEquals('A_B_Foo', $class->getNamespacedName(true)); + $this->assertEquals('A\\B\\Foo', $class->getNamespacedName(false)); + } + + public function testAddAndHasMethod(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $method = new MethodDef(Modifiers::PUBLIC, 'bar'); + + $this->assertFalse($class->hasMethod('bar')); + + $class->addMethod($method); + $this->assertTrue($class->hasMethod('bar')); + $this->assertTrue($class->hasMethod('BAR')); // case-insensitive + $this->assertSame($method, $class->getMethod('bar')); + } + + public function testGetMethodCaseInsensitive(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $method = new MethodDef(Modifiers::PUBLIC, 'MyMethod'); + + $class->addMethod($method); + $this->assertSame($method, $class->getMethod('mymethod')); + $this->assertSame($method, $class->getMethod('MYMETHOD')); + } + + public function testHasMethodNotFound(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $this->assertFalse($class->hasMethod('nonexistent')); + } + + public function testHasProperty(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $prop = new PropertyDef('name', Modifiers::PUBLIC, 'php::Str'); + $class->properties['name'] = $prop; + + $this->assertTrue($class->hasProperty('name')); + $this->assertFalse($class->hasProperty('missing')); + $this->assertSame($prop, $class->getProperty('name')); + } + + public function testHasConstant(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $const = new ConstantDef('VERSION', Modifiers::PUBLIC, 'php::Str', '"1.0"'); + $class->constants['VERSION'] = $const; + + $this->assertTrue($class->hasConstant('VERSION')); + $this->assertFalse($class->hasConstant('MISSING')); + $this->assertSame($const, $class->getConstant('VERSION')); + } + + public function testIsAbstract(): void + { + $abstractClass = new ClassDef('AbstractFoo', Modifiers::ABSTRACT); + $this->assertTrue($abstractClass->isAbstract()); + + $concreteClass = new ClassDef('ConcreteFoo', Modifiers::PUBLIC); + $this->assertFalse($concreteClass->isAbstract()); + } + + public function testRequireCtorAndEnumDefaults(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $this->assertFalse($class->requireCtor); + $this->assertFalse($class->enum); + } + + public function testTraitAliasesAndIgnoredDefaults(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $this->assertEmpty($class->traitAliases); + $this->assertEmpty($class->traitIgnored); + } + + public function testImplementsAndExtendsDefaults(): void + { + $class = new ClassDef('Foo', Modifiers::PUBLIC); + $this->assertEmpty($class->implements); + $this->assertEquals('', $class->extends); + } +} diff --git a/phpunit/src/Entity/ClassLikeDefTest.php b/phpunit/src/Entity/ClassLikeDefTest.php new file mode 100644 index 00000000..10c511f2 --- /dev/null +++ b/phpunit/src/Entity/ClassLikeDefTest.php @@ -0,0 +1,47 @@ +assertEquals('Foo', $def->name); + $this->assertEquals('', $def->namespace); + $this->assertEquals('', $def->extends); + } + + public function testConstructWithNamespace(): void + { + $def = new ClassLikeDef('Foo', 'App\\Bar'); + $this->assertEquals('Foo', $def->name); + $this->assertEquals('App\\Bar', $def->namespace); + } + + public function testGetNamespacedNameWithoutNamespace(): void + { + $def = new ClassLikeDef('Foo'); + $this->assertEquals('Foo', $def->getNamespacedName()); + $this->assertEquals('Foo', $def->getNamespacedName(false)); + } + + public function testGetNamespacedNameSymbolic(): void + { + $def = new ClassLikeDef('Foo', 'App\\Bar'); + // symbolic: backslashes → underscores → App_Bar_Foo + $this->assertEquals('App_Bar_Foo', $def->getNamespacedName(true)); + } + + public function testGetNamespacedNameNonSymbolic(): void + { + $def = new ClassLikeDef('Foo', 'App\\Bar'); + $this->assertEquals('App\\Bar\\Foo', $def->getNamespacedName(false)); + } +} diff --git a/phpunit/src/Entity/ConstantDefTest.php b/phpunit/src/Entity/ConstantDefTest.php new file mode 100644 index 00000000..b173b9cc --- /dev/null +++ b/phpunit/src/Entity/ConstantDefTest.php @@ -0,0 +1,68 @@ +assertEquals('MAX_SIZE', $const->name); + $this->assertSame(Modifiers::PUBLIC, $const->flags); + $this->assertEquals('php::Int', $const->type); + $this->assertEquals('1024L', $const->value); + $this->assertEquals('', $const->arrayExpr); + $this->assertEquals('', $const->class); + $this->assertNull($const->valueExpr); + } + + public function testConstructWithStringValue(): void + { + $const = new ConstantDef('APP_NAME', Modifiers::PUBLIC, 'php::Str', 'php::Str{"MyApp"}'); + + $this->assertEquals('APP_NAME', $const->name); + $this->assertEquals('php::Str', $const->type); + $this->assertEquals('php::Str{"MyApp"}', $const->value); + } + + public function testClassProperty(): void + { + $const = new ConstantDef('PI', Modifiers::PUBLIC, 'php::Float', '3.14'); + $this->assertEquals('', $const->class); + + $const->class = 'Math'; + $this->assertEquals('Math', $const->class); + } + + public function testArrayExprProperty(): void + { + $const = new ConstantDef('ITEMS', Modifiers::PUBLIC, 'php::Array', '[]'); + $this->assertEquals('', $const->arrayExpr); + + $const->arrayExpr = 'zval arr; array_init(&arr);'; + $this->assertEquals('zval arr; array_init(&arr);', $const->arrayExpr); + } + + public function testValueExprProperty(): void + { + $const = new ConstantDef('VALUE', Modifiers::PUBLIC, 'php::Int', '1L'); + $this->assertNull($const->valueExpr); + } + + public function testDifferentVisibilityFlags(): void + { + $publicConst = new ConstantDef('A', Modifiers::PUBLIC, 'php::Int', '1'); + $this->assertSame(Modifiers::PUBLIC, $publicConst->flags); + + $protectedConst = new ConstantDef('B', Modifiers::PROTECTED, 'php::Int', '2'); + $this->assertSame(Modifiers::PROTECTED, $protectedConst->flags); + + $privateConst = new ConstantDef('C', Modifiers::PRIVATE, 'php::Int', '3'); + $this->assertSame(Modifiers::PRIVATE, $privateConst->flags); + } +} diff --git a/phpunit/src/Entity/FunctionDefTest.php b/phpunit/src/Entity/FunctionDefTest.php new file mode 100644 index 00000000..7fcea0b3 --- /dev/null +++ b/phpunit/src/Entity/FunctionDefTest.php @@ -0,0 +1,115 @@ +assertEquals('test', $fn->name); + $this->assertEquals('void', $fn->returnType); + $this->assertEquals('', $fn->namespace); + $this->assertFalse($fn->method); + $this->assertFalse($fn->stub); + $this->assertEmpty($fn->argInfoList); + $this->assertEquals(0, $fn->argCountRequired); + $this->assertEquals('', $fn->params); + $this->assertEquals('', $fn->returnClass); + } + + public function testConstructWithNamespace(): void + { + $fn = new FunctionDef('run', 'php::Int', 'App\\Lib'); + + $this->assertEquals('run', $fn->name); + $this->assertEquals('php::Int', $fn->returnType); + $this->assertEquals('App\\Lib', $fn->namespace); + } + + public function testGetNamespacedNameWithoutNamespace(): void + { + $fn = new FunctionDef('main', 'void', ''); + $this->assertEquals('main', $fn->getNamespacedName()); + } + + public function testGetNamespacedNameWithNamespace(): void + { + $fn = new FunctionDef('run', 'void', 'App\\Lib'); + $this->assertEquals('App\\Lib\\run', $fn->getNamespacedName()); + } + + public function testHasVariadicArgEmptyList(): void + { + $fn = new FunctionDef('test', 'void', ''); + $this->assertFalse($fn->hasVariadicArg()); + } + + public function testHasVariadicArgFalse(): void + { + $fn = new FunctionDef('test', 'void', ''); + + $arg1 = new ArgInfo(); + $arg1->name = 'x'; + $arg1->type = 'php::Int'; + $arg1->variadic = false; + + $fn->argInfoList = [$arg1]; + $this->assertFalse($fn->hasVariadicArg()); + } + + public function testHasVariadicArgTrue(): void + { + $fn = new FunctionDef('test', 'void', ''); + + $arg1 = new ArgInfo(); + $arg1->name = 'args'; + $arg1->type = 'php::Var'; + $arg1->variadic = true; + + $fn->argInfoList = [$arg1]; + $this->assertTrue($fn->hasVariadicArg()); + } + + public function testHasVariadicArgLastOnly(): void + { + $fn = new FunctionDef('test', 'void', ''); + + $arg1 = new ArgInfo(); + $arg1->name = 'x'; + $arg1->type = 'php::Int'; + $arg1->variadic = false; + + $arg2 = new ArgInfo(); + $arg2->name = 'rest'; + $arg2->type = 'php::Var'; + $arg2->variadic = true; + + $fn->argInfoList = [$arg1, $arg2]; + $this->assertTrue($fn->hasVariadicArg()); + } + + public function testMethodFlag(): void + { + $fn = new FunctionDef('handle', 'void', ''); + $this->assertFalse($fn->method); + + $fn->method = true; + $this->assertTrue($fn->method); + } + + public function testParamsAndReturnClassSettable(): void + { + $fn = new FunctionDef('test', 'php::Object', ''); + $fn->params = 'php::Int x, php::Str y'; + $fn->returnClass = 'DateTime'; + + $this->assertEquals('php::Int x, php::Str y', $fn->params); + $this->assertEquals('DateTime', $fn->returnClass); + } +} diff --git a/phpunit/src/Entity/InterfaceDefTest.php b/phpunit/src/Entity/InterfaceDefTest.php new file mode 100644 index 00000000..645847ff --- /dev/null +++ b/phpunit/src/Entity/InterfaceDefTest.php @@ -0,0 +1,42 @@ +assertEquals('JsonSerializable', $iface->name); + $this->assertEquals('', $iface->namespace); + $this->assertEquals('', $iface->extends); + } + + public function testConstructWithNamespace(): void + { + $iface = new InterfaceDef('Renderable', 'App\\Contracts'); + + $this->assertEquals('Renderable', $iface->name); + $this->assertEquals('App\\Contracts', $iface->namespace); + } + + public function testGetNamespacedName(): void + { + $iface = new InterfaceDef('Logger', 'App\\Contracts'); + + $this->assertEquals('App_Contracts_Logger', $iface->getNamespacedName(true)); + $this->assertEquals('App\\Contracts\\Logger', $iface->getNamespacedName(false)); + } + + public function testExtendsCanBeSet(): void + { + $iface = new InterfaceDef('Child'); + $iface->extends = 'Parent'; + + $this->assertEquals('Parent', $iface->extends); + } +} diff --git a/phpunit/src/Entity/MethodDefTest.php b/phpunit/src/Entity/MethodDefTest.php new file mode 100644 index 00000000..b050cd0b --- /dev/null +++ b/phpunit/src/Entity/MethodDefTest.php @@ -0,0 +1,51 @@ +assertEquals('handle', $method->name); + $this->assertSame(Modifiers::PUBLIC, $method->flags); + $this->assertNull($method->functionDef); + $this->assertFalse($method->hasDynamicCall); + } + + public function testGetReturnType(): void + { + $method = new MethodDef(Modifiers::PUBLIC, 'getValue'); + $fn = new FunctionDef('getValue', 'php::Int', ''); + $method->functionDef = $fn; + + $this->assertEquals('php::Int', $method->getReturnType()); + } + + public function testGetReturnTypeVoid(): void + { + $method = new MethodDef(Modifiers::PROTECTED, 'execute'); + $fn = new FunctionDef('execute', 'void', ''); + $method->functionDef = $fn; + + $this->assertEquals('void', $method->getReturnType()); + } + + public function testFlags(): void + { + $publicMethod = new MethodDef(Modifiers::PUBLIC, 'pub'); + $this->assertSame(Modifiers::PUBLIC, $publicMethod->flags); + + $protectedMethod = new MethodDef(Modifiers::PROTECTED, 'prot'); + $this->assertSame(Modifiers::PROTECTED, $protectedMethod->flags); + + $privateMethod = new MethodDef(Modifiers::PRIVATE, 'priv'); + $this->assertSame(Modifiers::PRIVATE, $privateMethod->flags); + } +} diff --git a/phpunit/src/Entity/PropertyDefTest.php b/phpunit/src/Entity/PropertyDefTest.php new file mode 100644 index 00000000..093d1bc5 --- /dev/null +++ b/phpunit/src/Entity/PropertyDefTest.php @@ -0,0 +1,82 @@ +assertEquals('count', $prop->name); + $this->assertSame(Modifiers::PUBLIC, $prop->flags); + $this->assertEquals('php::Int', $prop->type); + $this->assertNull($prop->default); + $this->assertFalse($prop->nullable); + $this->assertEquals('', $prop->class); + } + + public function testConstructWithDefaultValue(): void + { + $prop = new PropertyDef('count', Modifiers::PUBLIC, 'php::Int', '0'); + + $this->assertEquals('0', $prop->default); + } + + public function testConstructWithNullable(): void + { + $prop = new PropertyDef('name', Modifiers::PUBLIC, 'php::Str', null, true); + + $this->assertTrue($prop->nullable); + } + + public function testIsPrivate(): void + { + $privateProp = new PropertyDef('secret', Modifiers::PRIVATE, 'php::Str'); + $this->assertTrue($privateProp->isPrivate()); + $this->assertFalse($privateProp->isProtected()); + $this->assertFalse($privateProp->isPublic()); + } + + public function testIsProtected(): void + { + $protectedProp = new PropertyDef('value', Modifiers::PROTECTED, 'php::Int'); + $this->assertFalse($protectedProp->isPrivate()); + $this->assertTrue($protectedProp->isProtected()); + $this->assertFalse($protectedProp->isPublic()); + } + + public function testIsPublic(): void + { + $publicProp = new PropertyDef('name', Modifiers::PUBLIC, 'php::Str'); + $this->assertFalse($publicProp->isPrivate()); + $this->assertFalse($publicProp->isProtected()); + $this->assertTrue($publicProp->isPublic()); + + // PUBLIC flag is 0, so no-flag properties are also public + $impliedPublic = new PropertyDef('name', 0, 'php::Str'); + $this->assertTrue($impliedPublic->isPublic()); + } + + public function testIsStatic(): void + { + $staticProp = new PropertyDef('counter', Modifiers::PUBLIC | Modifiers::STATIC, 'php::Int'); + $this->assertTrue($staticProp->isStatic()); + + $instanceProp = new PropertyDef('counter', Modifiers::PUBLIC, 'php::Int'); + $this->assertFalse($instanceProp->isStatic()); + } + + public function testClassProperty(): void + { + $prop = new PropertyDef('data', Modifiers::PUBLIC, 'php::Var'); + $this->assertEquals('', $prop->class); + + $prop->class = 'App\\Entity\\User'; + $this->assertEquals('App\\Entity\\User', $prop->class); + } +} diff --git a/phpunit/src/ExceptionTest.php b/phpunit/src/ExceptionTest.php new file mode 100644 index 00000000..80d1b28d --- /dev/null +++ b/phpunit/src/ExceptionTest.php @@ -0,0 +1,151 @@ +assertInstanceOf(\RuntimeException::class, $e); + $this->assertEquals('Dynamic call error', $e->getMessage()); + } + + public function testPlaceHolder(): void + { + $e = new PlaceHolder('Placeholder error'); + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertEquals('Placeholder error', $e->getMessage()); + } + + public function testRedo(): void + { + $e = new Redo('Redo compilation'); + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertStringContainsString('Redo', $e->getMessage()); + } + + public function testSkip(): void + { + $e = new Skip('Skip this file'); + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertStringContainsString('Skip', $e->getMessage()); + } + + public function testSyntaxError(): void + { + $e = new SyntaxError('Unexpected token'); + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertStringContainsString('Unexpected', $e->getMessage()); + } + + public function testTestError(): void + { + $e = new TestError('Test assertion failed'); + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertStringContainsString('Test assertion', $e->getMessage()); + } + + public function testUnsupported(): void + { + $e = new Unsupported('Unsupported syntax'); + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertStringContainsString('Unsupported', $e->getMessage()); + } + + public function testCustomErrorCodes(): void + { + $e = new TestError('Error 42', 42); + $this->assertEquals(42, $e->getCode()); + } + + public function testCatchByBaseRuntimeException(): void + { + // All exceptions can be caught by their parent RuntimeException + $errors = [ + new DynamicCall('test'), + new PlaceHolder('test'), + new Redo('test'), + new Skip('test'), + new SyntaxError('test'), + new TestError('test'), + new Unsupported('test'), + ]; + + foreach ($errors as $e) { + try { + throw $e; + } catch (\RuntimeException $caught) { + $this->assertSame($e, $caught); + } + } + } + + public function testConstantsReservedNames(): void + { + $this->assertIsArray(Constants::CPP_RESERVED_NAMES); + $this->assertContains('class', Constants::CPP_RESERVED_NAMES); + $this->assertContains('namespace', Constants::CPP_RESERVED_NAMES); + $this->assertContains('int', Constants::CPP_RESERVED_NAMES); + $this->assertContains('void', Constants::CPP_RESERVED_NAMES); + } + + public function testConstantsUnsupportedFunctions(): void + { + $this->assertIsArray(Constants::UNSUPPORTED_FUNCTIONS); + $this->assertContains('extract', Constants::UNSUPPORTED_FUNCTIONS); + } + + public function testConstantsCompilerOptions(): void + { + $options = Constants::COMPILER_OPTIONS; + $this->assertIsArray($options); + $this->assertArrayHasKey('optimize', $options); + $this->assertArrayHasKey('output', $options); + $this->assertArrayHasKey('help', $options); + $this->assertArrayHasKey('debug', $options); + $this->assertArrayHasKey('job', $options); + $this->assertArrayHasKey('mode', $options); + } + + public function testCompilerOptionOptimizeDefaults(): void + { + $opt = Constants::COMPILER_OPTIONS['optimize']; + $this->assertEquals('O', $opt['prefix']); + $this->assertIsInt($opt['defaultValue']); + $this->assertEquals(0, $opt['defaultValue']); + } + + public function testCompilerOptionModeDefaults(): void + { + $opt = Constants::COMPILER_OPTIONS['mode']; + $this->assertEquals('bin', $opt['defaultValue']); + } + + public function testCompilerOptionNoValueFlags(): void + { + $noValueFlags = ['help', 'version', 'no-literal-strings', 'force', 'debug', 'no-console']; + foreach ($noValueFlags as $flag) { + $this->assertArrayHasKey($flag, Constants::COMPILER_OPTIONS); + $this->assertTrue(Constants::COMPILER_OPTIONS[$flag]['noValue'] ?? false); + } + } + + public function testMsvcSuppressedWarnings(): void + { + $warnings = Constants::MSVC_SUPPRESSED_WARNINGS; + $this->assertIsArray($warnings); + $this->assertArrayHasKey('4244', $warnings); + $this->assertArrayHasKey('4127', $warnings); + } +} diff --git a/phpunit/src/FileScannerTest.php b/phpunit/src/FileScannerTest.php new file mode 100644 index 00000000..4f6a03e2 --- /dev/null +++ b/phpunit/src/FileScannerTest.php @@ -0,0 +1,183 @@ +assertTrue(FileScanner::isPhpFile('/path/to/file.php')); + $this->assertFalse(FileScanner::isPhpFile('/path/to/file.cc')); + $this->assertFalse(FileScanner::isPhpFile('/path/to/file.c')); + $this->assertFalse(FileScanner::isPhpFile('/path/to/file.py')); + } + + public function testIsCppFile(): void + { + $this->assertTrue(FileScanner::isCppFile('/path/to/file.cc')); + $this->assertTrue(FileScanner::isCppFile('/path/to/file.cpp')); + $this->assertTrue(FileScanner::isCppFile('/path/to/file.cxx')); + $this->assertFalse(FileScanner::isCppFile('/path/to/file.c')); + $this->assertFalse(FileScanner::isCppFile('/path/to/file.php')); + } + + public function testIsNativeSourceFile(): void + { + // C++ files + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.cc')); + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.cpp')); + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.cxx')); + // C files + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.c')); + // Assembly files + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.s')); + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.S')); + // Objective-C files + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.m')); + $this->assertTrue(FileScanner::isNativeSourceFile('/path/to/file.mm')); + // Not native source + $this->assertFalse(FileScanner::isNativeSourceFile('/path/to/file.php')); + $this->assertFalse(FileScanner::isNativeSourceFile('/path/to/file.py')); + $this->assertFalse(FileScanner::isNativeSourceFile('/path/to/file.h')); + $this->assertFalse(FileScanner::isNativeSourceFile('/path/to/file.hpp')); + } + + // ======================================================================== + // getFileName / getFileExt + // ======================================================================== + + public function testGetFileName(): void + { + $this->assertEquals('hello', FileScanner::getFileName('/path/to/hello.php')); + $this->assertEquals('helper', FileScanner::getFileName('/path/to/helper.c')); + $this->assertEquals('main', FileScanner::getFileName('/path/to/main.cc')); + } + + public function testGetFileNameWithMultipleDots(): void + { + $this->assertEquals('my.test', FileScanner::getFileName('/path/to/my.test.php')); + } + + public function testGetFileExt(): void + { + $this->assertEquals('php', FileScanner::getFileExt('/path/to/file.php')); + $this->assertEquals('c', FileScanner::getFileExt('/path/to/file.c')); + $this->assertEquals('cc', FileScanner::getFileExt('/path/to/file.cc')); + $this->assertEquals('S', FileScanner::getFileExt('/path/to/file.S')); + } + + // ======================================================================== + // Constants + // ======================================================================== + + public function testPhpExtConstant(): void + { + $this->assertEquals(['php'], FileScanner::PHP_EXT); + } + + public function testCppExtConstant(): void + { + $this->assertContains('cc', FileScanner::CPP_EXT); + $this->assertContains('cpp', FileScanner::CPP_EXT); + $this->assertContains('cxx', FileScanner::CPP_EXT); + } + + public function testCextConstant(): void + { + $this->assertEquals(['c'], FileScanner::C_EXT); + } + + public function testAsmExtConstant(): void + { + $this->assertContains('s', FileScanner::ASM_EXT); + $this->assertContains('S', FileScanner::ASM_EXT); + } + + public function testObjcExtConstant(): void + { + $this->assertEquals(['m'], FileScanner::OBJC_EXT); + } + + public function testObjcxxExtConstant(): void + { + $this->assertEquals(['mm'], FileScanner::OBJCXX_EXT); + } + + public function testNativeSrcExtConstant(): void + { + $expected = ['cpp', 'cxx', 'cc', 'c', 's', 'S', 'm', 'mm']; + sort($expected); + $actual = FileScanner::NATIVE_SRC_EXT; + sort($actual); + $this->assertEquals($expected, $actual); + } + + // ======================================================================== + // Scan method + // ======================================================================== + + public function testScanDirectory(): void + { + $tmpDir = sys_get_temp_dir() . '/file_scanner_test_' . uniqid(); + mkdir($tmpDir, 0777, true); + mkdir($tmpDir . '/sub', 0777, true); + + touch($tmpDir . '/main.php'); + touch($tmpDir . '/helper.c'); + touch($tmpDir . '/math.S'); + touch($tmpDir . '/module.cc'); + touch($tmpDir . '/lib.m'); + touch($tmpDir . '/ignored.py'); + touch($tmpDir . '/README.md'); + touch($tmpDir . '/sub/nested.cpp'); + + $scanner = new FileScanner($tmpDir); + $files = $scanner->scan(); + + // Python and Markdown should not be included + $this->assertContains($tmpDir . '/main.php', $files); + $this->assertContains($tmpDir . '/helper.c', $files); + $this->assertContains($tmpDir . '/math.S', $files); + $this->assertContains($tmpDir . '/module.cc', $files); + $this->assertContains($tmpDir . '/lib.m', $files); + $this->assertContains($tmpDir . '/sub/nested.cpp', $files); + $this->assertNotContains($tmpDir . '/ignored.py', $files); + $this->assertNotContains($tmpDir . '/README.md', $files); + + // Clean up + unlink($tmpDir . '/main.php'); + unlink($tmpDir . '/helper.c'); + unlink($tmpDir . '/math.S'); + unlink($tmpDir . '/module.cc'); + unlink($tmpDir . '/lib.m'); + unlink($tmpDir . '/ignored.py'); + unlink($tmpDir . '/README.md'); + unlink($tmpDir . '/sub/nested.cpp'); + rmdir($tmpDir . '/sub'); + rmdir($tmpDir); + } + + public function testConstructorRejectsNonexistentDir(): void + { + $this->expectException(\InvalidArgumentException::class); + new FileScanner('/nonexistent/path'); + } + + public function testGetDirectory(): void + { + $tmpDir = sys_get_temp_dir() . '/file_scanner_getdir_' . uniqid(); + mkdir($tmpDir, 0777, true); + + $scanner = new FileScanner($tmpDir); + $this->assertEquals($tmpDir, $scanner->getDirectory()); + + rmdir($tmpDir); + } +} diff --git a/phpunit/src/SymbolTest.php b/phpunit/src/SymbolTest.php new file mode 100644 index 00000000..99b3852d --- /dev/null +++ b/phpunit/src/SymbolTest.php @@ -0,0 +1,71 @@ +assertEquals('php::getStaticProperty', Symbol::getStaticProperty()); + } + + public function testSetStaticProperty(): void + { + $this->assertEquals('php::setStaticProperty', Symbol::setStaticProperty()); + } + + public function testInstanceOf(): void + { + $this->assertEquals('php::instanceOf', Symbol::instanceOf()); + } + + public function testConcat(): void + { + $this->assertEquals('php::concat', Symbol::concat()); + } + + public function testConstant(): void + { + $this->assertEquals('php::constant', Symbol::constant()); + } + + public function testGetClassEntrySafe(): void + { + $this->assertEquals('php::getClassEntrySafe', Symbol::getClassEntrySafe()); + } + + public function testArgList(): void + { + $this->assertEquals('php::ArgList', Symbol::argList()); + } + + public function testGetCalledCe(): void + { + $result = Symbol::getCalledCe(); + $this->assertStringContainsString(CompilerBase::PREFIX, $result); + $this->assertStringContainsString('get_called_ce', $result); + } + + public function testGetCalledClass(): void + { + $result = Symbol::getCalledClass(); + $this->assertStringContainsString(CompilerBase::PREFIX, $result); + $this->assertStringContainsString('get_called_class', $result); + } + + public function testSafeIndex(): void + { + $result = Symbol::safeIndex('0', '10'); + $this->assertEquals('php::safeIndex(0, 10)', $result); + } + + public function testSafeIndexWithVariables(): void + { + $result = Symbol::safeIndex('i', 'count'); + $this->assertEquals('php::safeIndex(i, count)', $result); + } +}