generateMinitBody('enum-case-class-constant.php', 'ast_lifecycle_guard'); $guardPos = strpos($minit, 'if (type == MODULE_TEMPORARY) {'); self::assertIsInt($guardPos, 'MINIT must reject dl()-loaded temporary modules'); self::assertStringContainsString( 'registers enum-case class constants that must be released by MSHUTDOWN', $minit, ); self::assertStringContainsString('Load the extension from php.ini instead.', $minit); $handlersPos = strpos($minit, 'typephp_install_reflection_attribute_handlers()'); $firstRegisterPos = strpos($minit, 'register_class_'); self::assertIsInt($handlersPos); self::assertIsInt($firstRegisterPos); self::assertLessThan($handlersPos, $guardPos, 'the lifecycle guard must be the first MINIT statement'); self::assertLessThan($firstRegisterPos, $guardPos, 'the lifecycle guard must precede every class registration'); } public function testAstConstantRegistrationIsOrderedAfterEveryFallibleMinitStep(): void { $minit = $this->generateMinitBody('enum-case-class-constant.php', 'ast_lifecycle_order'); $firstRegisterPos = strpos($minit, 'register_class_'); $lastFailurePos = strrpos($minit, 'return FAILURE;'); self::assertIsInt($firstRegisterPos); self::assertIsInt($lastFailurePos); self::assertLessThan( $firstRegisterPos, $lastFailurePos, 'no MINIT step after the first class registration may return FAILURE: ' . 'a failed MINIT never reaches MSHUTDOWN, so the persistent class ' . 'table would keep an AST that destroy_zend_class() cannot handle', ); self::assertGreaterThan( strrpos($minit, 'register_class_'), strpos($minit, 'return SUCCESS;'), ); } public function testMshutdownReleasesAstConstantsBeforeAnyOtherTeardown(): void { $extension = $this->generateExtension('enum-case-class-constant.php', 'ast_lifecycle_shutdown'); $mshutdown = $this->sliceFunction($extension, 'PHP_MSHUTDOWN_FUNCTION', 'THREAD_LOCAL zval globals_array'); $releasePos = strpos($mshutdown, 'typephp_release_ast_constants_enum_case_class_constant();'); self::assertIsInt($releasePos, 'MSHUTDOWN must release the persistent AST constants'); // The release must run before anything else so the class table is // Zend-safe no matter what the rest of the teardown does. $firstStatementPos = strpos($mshutdown, ';'); self::assertSame($firstStatementPos, $releasePos + strlen('typephp_release_ast_constants_enum_case_class_constant();') - 1); } public function testModulesWithoutAstConstantsCarryNeitherGuardNorRelease(): void { $extension = $this->generateExtension('class-constant-codegen.php', 'ast_lifecycle_none'); self::assertStringNotContainsString('MODULE_TEMPORARY', $extension); self::assertStringNotContainsString('typephp_release_ast_constants_', $extension); } private function generateMinitBody(string $fixture, string $target): string { return $this->sliceFunction( $this->generateExtension($fixture, $target), 'PHP_MINIT_FUNCTION', 'PHP_MSHUTDOWN_FUNCTION', ); } private function generateExtension(string $fixture, string $target): string { global $translator; $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); $translator = $compiler; $compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); $compiler->setTargetName($target); $source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $fixture; $compiler->addFiles([$source]); $compiler->prepareFile($source); $compiler->convertFile($source); $extension = file_get_contents($compiler->genExtension()); self::assertIsString($extension); return $extension; } /** * The generated function bodies keep statements at column zero, so the * closing brace is not recognizable; slice up to the next known emission * instead. */ private function sliceFunction(string $extension, string $startMarker, string $endMarker): string { $start = strpos($extension, $startMarker); self::assertIsInt($start, "generated extension must contain {$startMarker}"); $end = strpos($extension, $endMarker, $start + strlen($startMarker)); self::assertIsInt($end, "generated extension must contain {$endMarker}"); return substr($extension, $start, $end - $start); } }