diff --git a/test/BaseTest.php b/test/BaseTest.php index 732b083cd3e..056d396b86f 100755 --- a/test/BaseTest.php +++ b/test/BaseTest.php @@ -195,6 +195,11 @@ public function testCases(string $name, string $code, array $sections): void { PhptWebSections::applyToEnv($env, $sections); // JIT/VM children must run llvm-env preload; PHPUnit parent skips it (#98, #2055). unset($env['PHP_COMPILER_SKIP_LLVM_PRELOAD']); + // php-src run-tests.php: honor --SKIPIF-- before executing FILE (#24888). + $skipReason = self::evaluatePhptSkipIf($sections, $env, $this->phpCommand(), $repoRoot); + if (null !== $skipReason) { + $this->markTestSkipped($skipReason); + } $runfile = isset($sections['RUNFILE']) ? trim($sections['RUNFILE']) : ''; if ('' !== $runfile) { $runPath = realpath(($sections['__phpt_dir'] ?? $repoRoot) . '/' . $runfile); @@ -357,6 +362,102 @@ protected static function applyEnvSection(array &$env, array $sections): void } } + /** + * Evaluate a PHPT --SKIPIF-- section under host PHP (php-src run-tests.php semantics). + * + * Returns the skip message when output begins with "skip" (case-insensitive), or null when + * the case should run. Prepends vendor/autoload.php so SKIPIF can call CompilerVersion / + * extension policies. Cwd is the PHPT directory. + * + * @param array $sections + * @param array $env + * @param list $phpCommand + */ + public static function evaluatePhptSkipIf( + array $sections, + array $env, + array $phpCommand, + string $repoRoot + ): ?string { + if (!isset($sections['SKIPIF'])) { + return null; + } + $skipif = $sections['SKIPIF']; + if ('' === trim($skipif)) { + return null; + } + $cwd = $sections['__phpt_dir'] ?? $repoRoot; + $autoload = $repoRoot . '/vendor/autoload.php'; + $tmp = tempnam(sys_get_temp_dir(), 'phpt-skipif-'); + if (false === $tmp) { + throw new \RuntimeException('Failed to allocate temp file for SKIPIF'); + } + $skipifPath = $tmp . '.php'; + rename($tmp, $skipifPath); + try { + // auto_prepend_file loads Composer so SKIPIF may reference PHPCompiler\* (#24888). + $body = $skipif; + if (!preg_match('/^\s*<\?php/i', $body)) { + $body = " ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $pipes = []; + $proc = proc_open($cmd, $descriptorSpec, $pipes, $cwd, $env); + if (!\is_resource($proc)) { + throw new \RuntimeException('Failed to spawn SKIPIF subprocess'); + } + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exitCode = proc_close($proc); + $out = is_string($stdout) ? $stdout : ''; + // php-src: SKIPIF may print to stdout; treat stderr-only when stdout empty. + if ('' === trim($out) && is_string($stderr) && '' !== trim($stderr)) { + $out = $stderr; + } + $trimmed = ltrim($out); + if (0 === strncasecmp($trimmed, 'skip', 4)) { + return trim($out); + } + if (0 !== $exitCode) { + $detail = trim($out); + if ('' === $detail && is_string($stderr)) { + $detail = trim($stderr); + } + if ('' === $detail) { + $detail = "(exit {$exitCode})"; + } + throw new \RuntimeException("SKIPIF bork for PHPT: {$detail}"); + } + // Non-empty output that is not a skip message → bork (php-src run-tests.php). + if ('' !== trim($out)) { + throw new \RuntimeException('SKIPIF bork for PHPT: ' . trim($out)); + } + + return null; + } finally { + @unlink($skipifPath); + } + } + /** * @param array $env */ diff --git a/test/unit/PhptSkipIfTest.php b/test/unit/PhptSkipIfTest.php new file mode 100644 index 00000000000..3a9387869d2 --- /dev/null +++ b/test/unit/PhptSkipIfTest.php @@ -0,0 +1,115 @@ + "assertNull($reason); + } + + public function testSkipPrefixSkips(): void + { + $reason = BaseTest::evaluatePhptSkipIf( + [ + 'SKIPIF' => " sys_get_temp_dir(), + ], + [], + [PHP_BINARY], + dirname(__DIR__, 2) + ); + $this->assertNotNull($reason); + $this->assertSame('skip no database', $reason); + } + + public function testSilentSkipIfRuns(): void + { + $reason = BaseTest::evaluatePhptSkipIf( + [ + 'SKIPIF' => " sys_get_temp_dir(), + ], + [], + [PHP_BINARY], + dirname(__DIR__, 2) + ); + $this->assertNull($reason); + } + + public function testEnvGateHonorsPhpCompilerProfile(): void + { + $sections = [ + 'SKIPIF' => <<<'PHP' + sys_get_temp_dir(), + ]; + $repo = dirname(__DIR__, 2); + $php = [PHP_BINARY]; + + $without = BaseTest::evaluatePhptSkipIf($sections, ['PHP_COMPILER_PROFILE' => ''], $php, $repo); + $this->assertNotNull($without); + $this->assertStringContainsString('skip requires PHP_COMPILER_PROFILE=8.4', (string) $without); + + $with = BaseTest::evaluatePhptSkipIf($sections, ['PHP_COMPILER_PROFILE' => '8.4'], $php, $repo); + $this->assertNull($with); + } + + public function testLazyGhostFixtureSkipsOnPhp82Host(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('host is PHP 8.4+; fixture SKIPIF would not skip'); + } + $path = dirname(__DIR__) . '/compliance/cases/language/lazy_ghost_basic.phpt'; + $this->assertFileExists($path); + $parsed = $this->parseSections($path); + $reason = BaseTest::evaluatePhptSkipIf( + $parsed, + [], + [PHP_BINARY], + dirname(__DIR__, 2) + ); + $this->assertNotNull($reason); + $this->assertStringContainsString('skip ReflectionClass::newLazyGhost requires PHP 8.4+', (string) $reason); + } + + /** + * @return array + */ + private function parseSections(string $filename): array + { + $sections = []; + $section = ''; + foreach (file($filename) as $line) { + if (preg_match('(^--([_A-Z]+)--)', $line, $result)) { + $section = $result[1]; + $sections[$section] = ''; + continue; + } + $sections[$section] .= $line; + } + $sections['__phpt_dir'] = dirname($filename); + + return $sections; + } +}