Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions test/BaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string, string> $sections
* @param array<string, string> $env
* @param list<string> $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 = "<?php\n" . $body;
}
if (!file_put_contents($skipifPath, $body)) {
throw new \RuntimeException("Failed to write SKIPIF temp file: {$skipifPath}");
}
$cmd = $phpCommand;
if (is_file($autoload)) {
$cmd[] = '-d';
$cmd[] = 'auto_prepend_file=' . $autoload;
}
foreach (self::phptIniArgvFlags($sections) as $flag) {
$cmd[] = $flag;
}
$cmd[] = $skipifPath;
$descriptorSpec = [
0 => ['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<string, string> $env
*/
Expand Down
115 changes: 115 additions & 0 deletions test/unit/PhptSkipIfTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../BaseTest.php';

/**
* --SKIPIF-- must be evaluated (php-src run-tests.php semantics) — #24888.
*/
final class PhptSkipIfTest extends TestCase
{
public function testMissingSkipIfRuns(): void
{
$reason = BaseTest::evaluatePhptSkipIf(
['FILE' => "<?php\n"],
[],
[PHP_BINARY],
dirname(__DIR__, 2)
);
$this->assertNull($reason);
}

public function testSkipPrefixSkips(): void
{
$reason = BaseTest::evaluatePhptSkipIf(
[
'SKIPIF' => "<?php\necho 'skip no database';\n",
'__phpt_dir' => 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' => "<?php\n// ready\n",
'__phpt_dir' => sys_get_temp_dir(),
],
[],
[PHP_BINARY],
dirname(__DIR__, 2)
);
$this->assertNull($reason);
}

public function testEnvGateHonorsPhpCompilerProfile(): void
{
$sections = [
'SKIPIF' => <<<'PHP'
<?php
if (!getenv('PHP_COMPILER_PROFILE') || '8.4' !== getenv('PHP_COMPILER_PROFILE')) {
die('skip requires PHP_COMPILER_PROFILE=8.4');
}
PHP,
'__phpt_dir' => 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<string, string>
*/
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;
}
}