Skip to content
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
200193b
opcache_is_script_cached: file_cache option
iamacarpet Nov 28, 2024
798a07d
opcache_is_script_cached: file_cache: test
iamacarpet Nov 28, 2024
824e638
fix when file_cache_read_only
iamacarpet Nov 28, 2024
30641d6
remove file_cache_only
iamacarpet Nov 28, 2024
283844e
switch to zend_file_cache_script_validate(...)
iamacarpet Nov 29, 2024
8cc7293
fix build
iamacarpet Dec 2, 2024
fd93557
switch to opcache_is_script_cached_in_file_cache(...)
iamacarpet Dec 2, 2024
7e23285
remove double close(fd)
iamacarpet Dec 2, 2024
906b92d
zend_file_cache_open with struct
iamacarpet Dec 2, 2024
1cdf3fa
fix build
iamacarpet Dec 2, 2024
f620d9c
Unit Tests for GH-16551
iamacarpet Dec 3, 2024
6df1562
fix for file_cache_only
iamacarpet Dec 3, 2024
e282578
probe ARM test failure
iamacarpet Dec 4, 2024
17edbe6
remove unnecessary tests
iamacarpet Dec 4, 2024
cd0177f
Merge remote-tracking branch 'origin/master' into opcache/opcache_is_…
iamacarpet Feb 3, 2025
7cff70a
switch to validate_only method
iamacarpet Feb 3, 2025
59a4ecc
Merge remote-tracking branch 'origin/master' into opcache/opcache_is_…
iamacarpet Feb 20, 2025
b3b9501
zend_file_cache_script_load_ex
iamacarpet Feb 20, 2025
d1b74e9
Merge remote-tracking branch 'origin/master' into opcache/opcache_is_…
iamacarpet Apr 24, 2025
1b8311b
improve unit tests
iamacarpet Apr 24, 2025
3e573f3
Merge remote-tracking branch 'origin/master' into opcache/opcache_is_…
iamacarpet Jul 11, 2025
9346db9
reduce whitespace
iamacarpet Jul 11, 2025
f9c05a1
simplify tests & isolate to current change
iamacarpet Jul 11, 2025
d750ae0
Merge branch 'opcache/opcache_is_script_cached_file' of https://githu…
iamacarpet Jul 11, 2025
fbb4194
fix local test run
iamacarpet Jul 11, 2025
e36a92d
Simplify test
iluuu1994 Jul 14, 2025
4c99bd0
Drop unneeded include
iluuu1994 Jul 14, 2025
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
2 changes: 2 additions & 0 deletions ext/opcache/opcache.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ function opcache_jit_blacklist(Closure $closure): void {}
function opcache_get_configuration(): array|false {}

function opcache_is_script_cached(string $filename): bool {}

function opcache_is_script_cached_in_file_cache(string $filename): bool {}
6 changes: 5 additions & 1 deletion ext/opcache/opcache_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions ext/opcache/tests/gh16551_998.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

$a = 4+5;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this called 998? Also, why does it output 9, while 999 outputs 8. :D


echo $a . "\n";
5 changes: 5 additions & 0 deletions ext/opcache/tests/gh16551_999.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

$a = 3+5;

echo $a . "\n";
133 changes: 133 additions & 0 deletions ext/opcache/tests/gh16551_file_cache_only.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
--TEST--
GH-16551: Behavior with opcache.file_cache_only=1
--SKIPIF--
<?php
if (!extension_loaded('Zend OPcache')) die('skip Zend OPcache extension not available');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason for this, the --EXTENSIONS-- section already takes care of it.


// Ensure the cache directory exists BEFORE OPcache needs it
$cacheDir = __DIR__ . '/gh16551_fileonly_cache';
if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0777, true);
}
// Check if mkdir failed potentially due to permissions
if (!is_dir($cacheDir) || !is_writable($cacheDir)) {
die('skip Could not create or write to cache directory: ' . $cacheDir);
}
?>
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.jit=disable
opcache.jit_buffer_size=0
opcache.file_cache="{PWD}/gh16551_fileonly_cache"
opcache.file_cache_only=1
opcache.validate_timestamps=1
--EXTENSIONS--
opcache
--FILE--
<?php
$file = __DIR__ . '/gh16551_998.inc';
$uncached_file = __DIR__ . '/gh16551_999.inc';
$cacheDir = __DIR__ . '/gh16551_fileonly_cache';

echo "Initial state (file_cache_only mode):\n";
// SHM is always false, File Cache might be true in Pass 2
var_dump(opcache_is_script_cached($file));
var_dump(opcache_is_script_cached_in_file_cache($file));

echo "\nAttempting opcache_compile_file():\n";
opcache_compile_file($file);

echo "\nState after compile attempt:\n";
// SHM remains false, File Cache becomes true
var_dump(opcache_is_script_cached($file));
var_dump(opcache_is_script_cached_in_file_cache($file));

// Check file existence via glob
echo "\nChecking file system for compiled file:\n";
if (substr(PHP_OS, 0, 3) === 'WIN') {
$sanitizedDir = str_replace(':', '', __DIR__);
$pattern = $cacheDir . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . $sanitizedDir . DIRECTORY_SEPARATOR . 'gh16551_998.inc.bin';
} else {
$pattern = $cacheDir . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . __DIR__ . DIRECTORY_SEPARATOR . 'gh16551_998.inc.bin';
}
$found = glob($pattern);
var_dump(count($found) > 0); // Expect true after compile

echo "\nAttempting require:\n";
require $file; // Outputs 9, should execute from file cache

echo "\nState after require:\n";
// State remains unchanged
var_dump(opcache_is_script_cached($file));
var_dump(opcache_is_script_cached_in_file_cache($file));

echo "\nChecking uncached file initial state:\n";
// SHM false, File Cache might be true in Pass 2 for this file too
var_dump(opcache_is_script_cached($uncached_file));
var_dump(opcache_is_script_cached_in_file_cache($uncached_file));

echo "\nRequiring uncached file:\n";
require $uncached_file; // Outputs 8, should compile to file cache now

echo "\nState after requiring uncached file:\n";
// SHM remains false, File cache becomes true for this file
var_dump(opcache_is_script_cached($uncached_file));
var_dump(opcache_is_script_cached_in_file_cache($uncached_file));

?>
--CLEAN--
<?php
$baseCacheDir = __DIR__ . '/gh16551_fileonly_cache';

function removeDirRecursive($dir) {
if (!is_dir($dir)) return;
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $fileinfo) {
if ($fileinfo->isDir()) {
@rmdir($fileinfo->getRealPath());
} else {
@unlink($fileinfo->getRealPath());
}
}
@rmdir($dir);
} catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); }
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to some common helper file.


removeDirRecursive($baseCacheDir);
?>
--EXPECTF--
Initial state (file_cache_only mode):
bool(false)
bool(%s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if you had the setup of the test remove the cached file, and a separate test that includes the file. This way, you can test both conditions rather than assuming this will be correct.


Attempting opcache_compile_file():

State after compile attempt:
bool(false)
bool(true)

Checking file system for compiled file:
bool(true)

Attempting require:
9

State after require:
bool(false)
bool(true)

Checking uncached file initial state:
bool(false)
bool(%s)

Requiring uncached file:
8

State after requiring uncached file:
bool(false)
bool(true)
124 changes: 124 additions & 0 deletions ext/opcache/tests/gh16551_invalidate_file_cache_only.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
--TEST--
GH-16551: Verify opcache_invalidate fails or is ineffective when opcache.file_cache_only=1
--SKIPIF--
<?php
if (!extension_loaded('Zend OPcache')) die('skip Zend OPcache extension not available');
if (substr(PHP_OS, 0, 3) == 'WIN') die('skip Test relies on shell_exec and specific helper script pathing, skipping on Windows');
$php_binary = getenv('TEST_PHP_EXECUTABLE');
if (!$php_binary) die('skip TEST_PHP_EXECUTABLE environment variable not set');
if (!is_executable($php_binary)) die("skip $php_binary is not executable");

// Ensure the cache directory exists BEFORE OPcache needs it
$cacheDir = __DIR__ . '/gh16551_invalidate_fco_cache';
if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0777, true);
}
// Check if mkdir failed potentially due to permissions
if (!is_dir($cacheDir) || !is_writable($cacheDir)) {
die('skip Could not create or write to cache directory: ' . $cacheDir);
}
?>
--INI--
; Main test runs with file_cache_only=1
opcache.enable=1
opcache.enable_cli=1
opcache.jit=disable
opcache.jit_buffer_size=0
opcache.file_cache="{PWD}/gh16551_invalidate_fco_cache"
opcache.file_cache_only=1
opcache.validate_timestamps=0
--EXTENSIONS--
opcache
--FILE--
<?php
$phpBinary = getenv('TEST_PHP_EXECUTABLE');
$helperScript = __DIR__ . '/gh16551_populate_cache_helper.inc';
$fileToCache = __DIR__ . '/gh16551_998.inc';
$cacheDir = __DIR__ . '/gh16551_invalidate_fco_cache';
$helperOutputFile = $cacheDir . '/helper_output.txt';

// 1. Populate cache using helper (runs WITHOUT file_cache_only=1)
$opcache_extension = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_opcache.dll' : 'opcache.so';
$cmd = escapeshellarg($phpBinary) . ' ' .
'-n ' . // <-- Tell PHP *not* to load any php.ini files
'-d zend_extension=' . escapeshellarg($opcache_extension) . ' '. // <-- Explicitly load OPcache
'-d opcache.enable=1 -d opcache.enable_cli=1 ' .
'-d opcache.file_cache=' . escapeshellarg($cacheDir) . ' ' .
'-d opcache.file_cache_only=0 ' . // <-- Helper runs normally
'-d opcache.validate_timestamps=1 ' .
'-d opcache.jit_buffer_size=0 ' . // Ensure helper doesn't try JIT
escapeshellarg($helperScript) . ' ' .
escapeshellarg($fileToCache) . ' ' .
escapeshellarg($cacheDir) . ' ' . // Pass path for verification/output
escapeshellarg($helperOutputFile);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand why this needs to happen in a separate process. Can you not just include the file from skipif via an include/function call?


echo "Running helper script to populate cache...\n";
$helperResult = shell_exec($cmd); // Execute the command

// 3. Check if helper script succeeded via output file
if (!file_exists($helperOutputFile) || trim(file_get_contents($helperOutputFile)) !== 'SUCCESS') {
echo "Helper script failed:\n";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I think tests should be minimal rather than produce the maximal amount of output. If they break, it's easy to extend them and see what's wrong. Otherwise, the size is probably tripple of what it needs to be.

if (file_exists($helperOutputFile)) {
readfile($helperOutputFile); // Show helper error message
} else {
echo "Helper output file '$helperOutputFile' not found.";
}
echo "\nShell Exec Output: " . $helperResult;
exit(1); // Abort test
}
echo "Helper script successful.\n";

// 2. Verify cache exists in main process (running file_cache_only=1)
echo "\nVerifying initial state (file_cache_only=1, cache populated):\n";
var_dump(opcache_is_script_cached($fileToCache)); // Should be false (SHM not used)
var_dump(opcache_is_script_cached_in_file_cache($fileToCache)); // Should be true

// 3. Attempt to invalidate (should fail or do nothing due to file_cache_only=1)
echo "\nAttempting opcache_invalidate() with file_cache_only=1:\n";
$invalidate_result = opcache_invalidate($fileToCache, true); // force=true
var_dump($invalidate_result); // Expect bool(false) as the function exits early

// 4. Verify cache state *after* invalidate attempt (should be unchanged)
echo "\nVerifying state after invalidate attempt:\n";
var_dump(opcache_is_script_cached($fileToCache)); // Still false
var_dump(opcache_is_script_cached_in_file_cache($fileToCache)); // Should STILL be true

?>
--CLEAN--
<?php
$baseCacheDir = __DIR__ . '/gh16551_invalidate_fco_cache';

function removeDirRecursive($dir) {
if (!is_dir($dir)) return;
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $fileinfo) {
if ($fileinfo->isDir()) {
@rmdir($fileinfo->getRealPath());
} else {
@unlink($fileinfo->getRealPath());
}
}
@rmdir($dir);
} catch (UnexpectedValueException $e) { @rmdir($dir); } catch (Exception $e) { @rmdir($dir); }
}

removeDirRecursive($baseCacheDir);
?>
--EXPECTF--
Running helper script to populate cache...
Helper script successful.

Verifying initial state (file_cache_only=1, cache populated):
bool(false)
bool(true)

Attempting opcache_invalidate() with file_cache_only=1:
bool(false)

Verifying state after invalidate attempt:
bool(false)
bool(true)
Loading
Loading