From 9bd6ffe5d65447847291858673d3f4318c359057 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 01/10] Cache and bound failed PDO connection probes --- ext/pdo/tests/bug_73234.phpt | 2 +- ext/pdo/tests/bug_79106.phpt | 2 +- ext/pdo/tests/bug_79106_collision.phpt | 2 +- ext/pdo/tests/debug_emulated_prepares.phpt | 2 +- ext/pdo/tests/gh8626.phpt | 2 +- ext/pdo/tests/pdo_017.phpt | 2 +- ext/pdo/tests/pdo_test.inc | 88 ++++++++++++++++--- ext/pdo/tests/pdo_test_skip_cache.phpt | 72 +++++++++++++++ ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt | 2 +- .../tests/batch_stmt_ins_sel_up_del.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_ins_up.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_rowcount.phpt | 2 +- .../tests/batch_stmt_transaction.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_try.phpt | 2 +- ext/pdo_dblib/tests/bug_38955.phpt | 2 +- ext/pdo_dblib/tests/bug_45876.phpt | 2 +- ext/pdo_dblib/tests/bug_47588.phpt | 2 +- ext/pdo_dblib/tests/bug_50755.phpt | 2 +- ext/pdo_dblib/tests/bug_54648.phpt | 2 +- ext/pdo_dblib/tests/bug_67130.phpt | 2 +- ext/pdo_dblib/tests/bug_68957.phpt | 2 +- ext/pdo_dblib/tests/bug_69592.phpt | 2 +- ext/pdo_dblib/tests/bug_69757.phpt | 2 +- ext/pdo_dblib/tests/bug_71667.phpt | 2 +- ext/pdo_dblib/tests/bug_73396.phpt | 2 +- ext/pdo_dblib/tests/common.phpt | 10 ++- ext/pdo_dblib/tests/config.inc | 7 ++ ext/pdo_dblib/tests/datetime2.phpt | 2 +- ext/pdo_dblib/tests/datetime_convert.phpt | 2 +- ext/pdo_dblib/tests/dbtds.phpt | 2 +- ext/pdo_dblib/tests/dbversion.phpt | 2 +- .../tests/pdo_dblib_param_str_natl.phpt | 2 +- ext/pdo_dblib/tests/pdo_dblib_quote.phpt | 2 +- ext/pdo_dblib/tests/pdodblib_001.phpt | 2 +- ext/pdo_dblib/tests/pdodblib_002.phpt | 2 +- .../tests/stringify_uniqueidentifier.phpt | 2 +- ext/pdo_dblib/tests/timeout.phpt | 2 +- ext/pdo_dblib/tests/types.phpt | 2 +- run-tests.php | 40 ++++++++- 40 files changed, 238 insertions(+), 49 deletions(-) create mode 100644 ext/pdo/tests/pdo_test_skip_cache.phpt diff --git a/ext/pdo/tests/bug_73234.phpt b/ext/pdo/tests/bug_73234.phpt index f291df704bbc..628feda50f42 100644 --- a/ext/pdo/tests/bug_73234.phpt +++ b/ext/pdo/tests/bug_73234.phpt @@ -10,7 +10,7 @@ if (str_starts_with(getenv('PDOTEST_DSN'), "firebird")) die('xfail firebird driv require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'oci') { die("xfail PDO::PARAM_NULL is not honored by OCI driver, related with bug #81586"); } diff --git a/ext/pdo/tests/bug_79106.phpt b/ext/pdo/tests/bug_79106.phpt index c3d13914e373..7e756a35ff25 100644 --- a/ext/pdo/tests/bug_79106.phpt +++ b/ext/pdo/tests/bug_79106.phpt @@ -8,7 +8,7 @@ $dir = getenv('REDIR_TEST_DIR'); if (!$dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; try { - $db = PDOTest::factory(); + $db = PDOTest::factoryForSkip(); } catch (PDOException $e) { die('skip ' . $e->getMessage()); } diff --git a/ext/pdo/tests/bug_79106_collision.phpt b/ext/pdo/tests/bug_79106_collision.phpt index dc895f017016..403b76ff512f 100644 --- a/ext/pdo/tests/bug_79106_collision.phpt +++ b/ext/pdo/tests/bug_79106_collision.phpt @@ -8,7 +8,7 @@ $dir = getenv('REDIR_TEST_DIR'); if (!$dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; try { - $db = PDOTest::factory(); + $db = PDOTest::factoryForSkip(); } catch (PDOException $e) { die('skip ' . $e->getMessage()); } diff --git a/ext/pdo/tests/debug_emulated_prepares.phpt b/ext/pdo/tests/debug_emulated_prepares.phpt index fba878eeed8b..7c7056323b96 100644 --- a/ext/pdo/tests/debug_emulated_prepares.phpt +++ b/ext/pdo/tests/debug_emulated_prepares.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') die('skip pgsql has its own test for this feature'); if (!@$db->getAttribute(PDO::ATTR_EMULATE_PREPARES) && !@$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true)) die('skip driver cannot emulate prepared statements'); ?> diff --git a/ext/pdo/tests/gh8626.phpt b/ext/pdo/tests/gh8626.phpt index 3515a30d77fb..b39ff30e40f1 100644 --- a/ext/pdo/tests/gh8626.phpt +++ b/ext/pdo/tests/gh8626.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'oci') { die("xfail OCI driver errorInfo is inconsistent with other PDO drivers"); } diff --git a/ext/pdo/tests/pdo_017.phpt b/ext/pdo/tests/pdo_017.phpt index b9171ce0cd58..fd4b8674669d 100644 --- a/ext/pdo/tests/pdo_017.phpt +++ b/ext/pdo/tests/pdo_017.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); try { $db->beginTransaction(); $db->rollback(); diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index b44d0b88e77b..7b88184da1ad 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -16,26 +16,31 @@ if (getenv('PDOTEST_DSN') === false) { } class PDOTest { + private static function getAttributes(string $environmentVariable): ?array { + $attributes = getenv($environmentVariable); + if (is_string($attributes) && strlen($attributes)) { + return unserialize($attributes); + } + return null; + } + // create an instance of the PDO driver, based on // the current environment - static function factory($classname = PDO::class, bool $useConnectMethod = false) { + static function factory($classname = PDO::class, bool $useConnectMethod = false, ?array $attributes = null) { $dsn = getenv('PDOTEST_DSN'); $user = getenv('PDOTEST_USER'); $pass = getenv('PDOTEST_PASS'); - $attr = getenv('PDOTEST_ATTR'); - if (is_string($attr) && strlen($attr)) { - $attr = unserialize($attr); - } else { - $attr = null; + if ($attributes === null) { + $attributes = self::getAttributes('PDOTEST_ATTR'); } if ($user === false) $user = NULL; if ($pass === false) $pass = NULL; if ($useConnectMethod) { - $db = $classname::connect($dsn, $user, $pass, $attr); + $db = $classname::connect($dsn, $user, $pass, $attributes); } else { - $db = new $classname($dsn, $user, $pass, $attr); + $db = new $classname($dsn, $user, $pass, $attributes); } if (!$db) { @@ -50,14 +55,77 @@ class PDOTest { return $db; } - static function skip() { + private static function getSkipCacheFile(): ?string { + $directory = getenv('TEST_PHP_SHARED_CACHE_DIR'); + if (!is_string($directory) || !is_dir($directory)) { + return null; + } + + $configuration = [ + getenv('PDOTEST_DSN'), + getenv('PDOTEST_USER'), + getenv('PDOTEST_PASS'), + getenv('PDOTEST_ATTR'), + getenv('PDOTEST_SKIP_ATTR'), + ]; + return $directory . DIRECTORY_SEPARATOR . 'pdo-' . hash('sha256', serialize($configuration)); + } + + static function factoryForSkip() { + $attributes = self::getAttributes('PDOTEST_ATTR'); + $skipAttributes = self::getAttributes('PDOTEST_SKIP_ATTR'); + if ($skipAttributes !== null) { + $attributes = $skipAttributes + ($attributes ?? []); + } + return PDOTest::factory(PDO::class, false, $attributes); + } + + private static function connectOrSkip(): void { try { - $db = PDOTest::factory(); + self::factoryForSkip(); } catch (PDOException $e) { die("skip " . $e->getMessage()); } } + static function skip() { + $cacheFile = self::getSkipCacheFile(); + if ($cacheFile === null) { + self::connectOrSkip(); + return; + } + + $cache = @fopen($cacheFile, 'c+'); + if ($cache === false || !flock($cache, LOCK_EX)) { + if (is_resource($cache)) { + fclose($cache); + } + self::connectOrSkip(); + return; + } + + $cached = stream_get_contents($cache); + $reason = $cached !== '' ? $cached : null; + if ($reason === null) { + // Only failures are shared; successful checks still create their own connection. + try { + self::factoryForSkip(); + } catch (PDOException $e) { + $reason = $e->getMessage(); + rewind($cache); + ftruncate($cache, 0); + fwrite($cache, $reason); + fflush($cache); + } + } + + flock($cache, LOCK_UN); + fclose($cache); + if (is_string($reason)) { + die("skip $reason"); + } + } + static function test_factory($file, $classname = PDO::class, bool $useConnectMethod = false) { $config = self::get_config($file); foreach ($config['ENV'] as $k => $v) { diff --git a/ext/pdo/tests/pdo_test_skip_cache.phpt b/ext/pdo/tests/pdo_test_skip_cache.phpt new file mode 100644 index 000000000000..1f44a3b50c2f --- /dev/null +++ b/ext/pdo/tests/pdo_test_skip_cache.phpt @@ -0,0 +1,72 @@ +--TEST-- +PDO test helper caches connection failures for one test run +--EXTENSIONS-- +pdo +--FILE-- + ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, + ['bypass_shell' => true], + ); + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + if (0 !== $exitCode = proc_close($process)) { + throw new Exception("PHP subprocess exited with code $exitCode: $output"); + } + + return $output; +} + +$dsn = 'missing_' . getmypid() . ':'; +$cacheDirectory = __DIR__ . '/pdo_test_skip_cache_' . getmypid(); +mkdir($cacheDirectory); + +$environment = getenv(); +$environment['PDOTEST_DSN'] = $dsn; +$environment['PDOTEST_USER'] = 'test'; +$environment['PDOTEST_PASS'] = 'test'; +$environment['TEST_PHP_SHARED_CACHE_DIR'] = $cacheDirectory; +unset($environment['PDOTEST_ATTR']); + +$helperDirectory = getenv('REDIR_TEST_DIR') ?: __DIR__; +$helper = var_export($helperDirectory . '/pdo_test.inc', true); +$code = "require $helper; PDOTest::skip();"; +try { + $first = run_pdo_skip_check($code, $environment); + + $cacheFiles = glob($cacheDirectory . '/pdo-*'); + if (count($cacheFiles) !== 1) { + throw new Exception('Expected exactly one cache file'); + } + $cacheFile = $cacheFiles[0]; + $cachedReason = file_get_contents($cacheFile); + file_put_contents($cacheFile, 'cached connection failure'); + + $second = run_pdo_skip_check($code, $environment); + echo "$first\n$cachedReason\n$second\n"; +} finally { + foreach (glob($cacheDirectory . '/*') as $file) { + unlink($file); + } + rmdir($cacheDirectory); +} +?> +--EXPECT-- +skip could not find driver +could not find driver +skip cached connection failure diff --git a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt index 95b33ddb7f3c..812a715177e7 100644 --- a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt +++ b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt @@ -9,7 +9,7 @@ if (PHP_INT_SIZE != 4) die("skip for 32bit platforms only"); if (PHP_OS_FAMILY === "Windows") die("skip not for Windows because the virtual address space for application is only 2GiB"); if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); require __DIR__ . '/config.inc'; -getDbConnection(); +skipIfNoDbConnection(); ?> --INI-- memory_limit=-1 diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt index 5b5c35252b0f..df9e0ac12d7a 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt index 80bc8ab533f0..c94b560dbed0 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt index afc87dd05d7c..3eda5b6733d6 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt b/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt index 03f8456c7876..126bb5a20746 100644 --- a/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_transaction.phpt b/ext/pdo_dblib/tests/batch_stmt_transaction.phpt index 2fab8dcda742..a9ed9762ea45 100644 --- a/ext/pdo_dblib/tests/batch_stmt_transaction.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_transaction.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_try.phpt b/ext/pdo_dblib/tests/batch_stmt_try.phpt index 9e735ff87715..0383e2feb00c 100644 --- a/ext/pdo_dblib/tests/batch_stmt_try.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_try.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/bug_38955.phpt b/ext/pdo_dblib/tests/bug_38955.phpt index cd244ba1ec33..256b11f590f4 100644 --- a/ext/pdo_dblib/tests/bug_38955.phpt +++ b/ext/pdo_dblib/tests/bug_38955.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --CONFLICTS-- all diff --git a/ext/pdo_dblib/tests/bug_47588.phpt b/ext/pdo_dblib/tests/bug_47588.phpt index 218088b1e11f..3f58844b6a7f 100644 --- a/ext/pdo_dblib/tests/bug_47588.phpt +++ b/ext/pdo_dblib/tests/bug_47588.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --CONFLICTS-- all diff --git a/ext/pdo_dblib/tests/bug_54648.phpt b/ext/pdo_dblib/tests/bug_54648.phpt index 9128b8c25404..111d79692c84 100644 --- a/ext/pdo_dblib/tests/bug_54648.phpt +++ b/ext/pdo_dblib/tests/bug_54648.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6', '5.0', '6.0', '7.0'])) die('skip bigint type is unsupported by active TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/common.phpt b/ext/pdo_dblib/tests/common.phpt index 293597b623e3..f3a963ef9b63 100644 --- a/ext/pdo_dblib/tests/common.phpt +++ b/ext/pdo_dblib/tests/common.phpt @@ -5,7 +5,7 @@ pdo_dblib --REDIRECTTEST-- # magic auto-configuration -return [ +$config = [ 'ENV' => [ 'PDOTEST_DSN' => getenv('PDO_DBLIB_TEST_DSN') ?: 'dblib:host=localhost;dbname=test', 'PDOTEST_USER' => getenv('PDO_DBLIB_TEST_USER') ?: 'php', @@ -13,3 +13,11 @@ return [ ], 'TESTS' => __DIR__ . '/ext/pdo/tests', ]; + +if (getenv('PDO_DBLIB_TEST_DSN') === false) { + $config['ENV']['PDOTEST_SKIP_ATTR'] = serialize([ + Pdo\Dblib::ATTR_CONNECTION_TIMEOUT => 1, + ]); +} + +return $config; diff --git a/ext/pdo_dblib/tests/config.inc b/ext/pdo_dblib/tests/config.inc index 1612a80a9336..4f9605e1ba5c 100644 --- a/ext/pdo_dblib/tests/config.inc +++ b/ext/pdo_dblib/tests/config.inc @@ -55,6 +55,13 @@ function getDbConnection(string $class = PDO::class, ?array $attributes = null) return $db; } +function skipIfNoDbConnection(): PDO { + $attributes = getenv('PDO_DBLIB_TEST_DSN') === false + ? [Pdo\Dblib::ATTR_CONNECTION_TIMEOUT => 1] + : null; + return getDbConnection(PDO::class, $attributes); +} + function connectToDb() { [$dsn, $user, $pass] = getCredentials(); diff --git a/ext/pdo_dblib/tests/datetime2.phpt b/ext/pdo_dblib/tests/datetime2.phpt index 2b54361a30e5..6564b398513c 100644 --- a/ext/pdo_dblib/tests/datetime2.phpt +++ b/ext/pdo_dblib/tests/datetime2.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6', '5.0', '6.0', '7.0', '7.1', '7.2'])) die('skip feature unsupported by this TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/datetime_convert.phpt b/ext/pdo_dblib/tests/datetime_convert.phpt index 0934dd7f83e7..18244a181957 100644 --- a/ext/pdo_dblib/tests/datetime_convert.phpt +++ b/ext/pdo_dblib/tests/datetime_convert.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6'])) die('skip feature unsupported by this TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/timeout.phpt b/ext/pdo_dblib/tests/timeout.phpt index 5f935a3d01b8..9bc5eb9bd5c3 100644 --- a/ext/pdo_dblib/tests/timeout.phpt +++ b/ext/pdo_dblib/tests/timeout.phpt @@ -6,7 +6,7 @@ pdo_dblib --FILE-- --FILE-- Date: Tue, 28 Jul 2026 20:29:54 +0700 Subject: [PATCH 02/10] Cache failed SNMP agent probes --- ext/snmp/tests/skipif.inc | 51 +++++++++++++++++++- ext/snmp/tests/snmp_skip_cache.phpt | 72 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 ext/snmp/tests/snmp_skip_cache.phpt diff --git a/ext/snmp/tests/skipif.inc b/ext/snmp/tests/skipif.inc index 0ae4ee16e5b8..4beb3b28080d 100644 --- a/ext/snmp/tests/skipif.inc +++ b/ext/snmp/tests/skipif.inc @@ -1,10 +1,57 @@ + @snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout) === false + ? 'NO SNMPD on this host or community invalid' + : null; + + $directory = getenv('TEST_PHP_SHARED_CACHE_DIR'); + if (!is_string($directory) || !is_dir($directory)) { + return $probe(); + } + + $configuration = [$hostname, $community, $timeout]; + $cacheFile = $directory + . DIRECTORY_SEPARATOR + . 'snmp-' + . hash('sha256', serialize($configuration)); + $cache = @fopen($cacheFile, 'c+'); + if ($cache === false || !flock($cache, LOCK_EX)) { + if (is_resource($cache)) { + fclose($cache); + } + return $probe(); + } + + $cached = stream_get_contents($cache); + $reason = $cached !== '' ? $cached : null; + if ($reason === null) { + // Only failures are shared; successful checks still probe the configured agent. + $reason = $probe(); + if (is_string($reason)) { + rewind($cache); + ftruncate($cache, 0); + fwrite($cache, $reason); + fflush($cache); + } + } + + flock($cache, LOCK_UN); + fclose($cache); + return $reason; +} + //test server is available // this require snmpget to work ... //snmpget ( string $hostname , string $community , //string $object_id [, int $timeout [, int $retries ]] ) -if (@snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout) === false) - die('skip NO SNMPD on this host or community invalid'); +$reason = get_snmp_test_agent_unavailable_reason(); +if (is_string($reason)) { + die("skip $reason"); +} diff --git a/ext/snmp/tests/snmp_skip_cache.phpt b/ext/snmp/tests/snmp_skip_cache.phpt new file mode 100644 index 000000000000..16f9f9c31674 --- /dev/null +++ b/ext/snmp/tests/snmp_skip_cache.phpt @@ -0,0 +1,72 @@ +--TEST-- +SNMP test helper caches agent availability for one test run +--EXTENSIONS-- +snmp +--FILE-- + ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, + ['bypass_shell' => true], + ); + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + if (0 !== $exitCode = proc_close($process)) { + throw new Exception("PHP subprocess exited with code $exitCode: $output"); + } + + return $output; +} + +$cacheDirectory = __DIR__ . '/snmp_skip_cache_' . getmypid(); +mkdir($cacheDirectory); + +$environment = getenv(); +$environment['SNMP_HOSTNAME'] = '127.0.0.1'; +$environment['SNMP_COMMUNITY'] = 'php_test_cache'; +$environment['SNMP_TIMEOUT'] = '100000'; +$environment['SNMP_RETRIES'] = '0'; +$environment['TEST_PHP_SHARED_CACHE_DIR'] = $cacheDirectory; + +$helper = var_export(__DIR__ . '/skipif.inc', true); +$code = "require $helper; echo \"available\\n\";"; +try { + $first = run_snmp_skip_check($code, $environment); + + $cacheFiles = glob($cacheDirectory . '/snmp-*'); + if (count($cacheFiles) !== 1) { + throw new Exception('Expected exactly one cache file'); + } + $cacheFile = $cacheFiles[0]; + $cachedReason = file_get_contents($cacheFile); + file_put_contents($cacheFile, 'cached agent failure'); + + $second = run_snmp_skip_check($code, $environment); + echo "$first\n"; + echo "$cachedReason\n"; + echo $second; +} finally { + foreach (glob($cacheDirectory . '/*') as $file) { + unlink($file); + } + rmdir($cacheDirectory); +} +?> +--EXPECT-- +skip NO SNMPD on this host or community invalid +NO SNMPD on this host or community invalid +skip cached agent failure From 86347edf536bb6d005bcd16edc34b38ed5406c91 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Thu, 30 Jul 2026 22:52:02 +0700 Subject: [PATCH 03/10] review: switched service probe fixture cleanup to --CLEAN-- --- ext/pdo/tests/pdo_test_skip_cache.phpt | 32 ++++++++++++----------- ext/snmp/tests/snmp_skip_cache.phpt | 36 ++++++++++++++------------ 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/ext/pdo/tests/pdo_test_skip_cache.phpt b/ext/pdo/tests/pdo_test_skip_cache.phpt index 1f44a3b50c2f..09d333ba290b 100644 --- a/ext/pdo/tests/pdo_test_skip_cache.phpt +++ b/ext/pdo/tests/pdo_test_skip_cache.phpt @@ -46,24 +46,26 @@ unset($environment['PDOTEST_ATTR']); $helperDirectory = getenv('REDIR_TEST_DIR') ?: __DIR__; $helper = var_export($helperDirectory . '/pdo_test.inc', true); $code = "require $helper; PDOTest::skip();"; -try { - $first = run_pdo_skip_check($code, $environment); +$first = run_pdo_skip_check($code, $environment); - $cacheFiles = glob($cacheDirectory . '/pdo-*'); - if (count($cacheFiles) !== 1) { - throw new Exception('Expected exactly one cache file'); - } - $cacheFile = $cacheFiles[0]; - $cachedReason = file_get_contents($cacheFile); - file_put_contents($cacheFile, 'cached connection failure'); +$cacheFiles = glob($cacheDirectory . '/pdo-*'); +if (count($cacheFiles) !== 1) { + throw new Exception('Expected exactly one cache file'); +} +$cacheFile = $cacheFiles[0]; +$cachedReason = file_get_contents($cacheFile); +file_put_contents($cacheFile, 'cached connection failure'); - $second = run_pdo_skip_check($code, $environment); - echo "$first\n$cachedReason\n$second\n"; -} finally { - foreach (glob($cacheDirectory . '/*') as $file) { - unlink($file); +$second = run_pdo_skip_check($code, $environment); +echo "$first\n$cachedReason\n$second\n"; +?> +--CLEAN-- + --EXPECT-- diff --git a/ext/snmp/tests/snmp_skip_cache.phpt b/ext/snmp/tests/snmp_skip_cache.phpt index 16f9f9c31674..9118d22b1fbf 100644 --- a/ext/snmp/tests/snmp_skip_cache.phpt +++ b/ext/snmp/tests/snmp_skip_cache.phpt @@ -44,26 +44,28 @@ $environment['TEST_PHP_SHARED_CACHE_DIR'] = $cacheDirectory; $helper = var_export(__DIR__ . '/skipif.inc', true); $code = "require $helper; echo \"available\\n\";"; -try { - $first = run_snmp_skip_check($code, $environment); +$first = run_snmp_skip_check($code, $environment); - $cacheFiles = glob($cacheDirectory . '/snmp-*'); - if (count($cacheFiles) !== 1) { - throw new Exception('Expected exactly one cache file'); - } - $cacheFile = $cacheFiles[0]; - $cachedReason = file_get_contents($cacheFile); - file_put_contents($cacheFile, 'cached agent failure'); +$cacheFiles = glob($cacheDirectory . '/snmp-*'); +if (count($cacheFiles) !== 1) { + throw new Exception('Expected exactly one cache file'); +} +$cacheFile = $cacheFiles[0]; +$cachedReason = file_get_contents($cacheFile); +file_put_contents($cacheFile, 'cached agent failure'); - $second = run_snmp_skip_check($code, $environment); - echo "$first\n"; - echo "$cachedReason\n"; - echo $second; -} finally { - foreach (glob($cacheDirectory . '/*') as $file) { - unlink($file); +$second = run_snmp_skip_check($code, $environment); +echo "$first\n"; +echo "$cachedReason\n"; +echo $second; +?> +--CLEAN-- + --EXPECT-- From 24eb85e5940bd797d06677433f9e00f9cfba7a0c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sun, 2 Aug 2026 03:31:33 +0700 Subject: [PATCH 04/10] Share failed service probes across tests --- ext/ldap/tests/skipifbindfailure.inc | 20 ++- ext/mysqli/tests/skipifconnectfailure.inc | 21 ++- ext/mysqli/tests/test_setup/test_helpers.inc | 39 +++-- ext/odbc/tests/skipif.inc | 16 +- ext/pdo/tests/bug_73234.phpt | 2 +- ext/pdo/tests/bug_79106.phpt | 2 +- ext/pdo/tests/bug_79106_collision.phpt | 2 +- ext/pdo/tests/debug_emulated_prepares.phpt | 2 +- ext/pdo/tests/gh8626.phpt | 2 +- ext/pdo/tests/pdo_017.phpt | 2 +- ext/pdo/tests/pdo_test.inc | 137 +++++++----------- ext/pdo/tests/pdo_test_skip_cache.phpt | 74 ---------- ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt | 2 +- .../tests/batch_stmt_ins_sel_up_del.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_ins_up.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_rowcount.phpt | 2 +- .../tests/batch_stmt_transaction.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_try.phpt | 2 +- ext/pdo_dblib/tests/bug_38955.phpt | 2 +- ext/pdo_dblib/tests/bug_45876.phpt | 2 +- ext/pdo_dblib/tests/bug_47588.phpt | 2 +- ext/pdo_dblib/tests/bug_50755.phpt | 2 +- ext/pdo_dblib/tests/bug_54648.phpt | 2 +- ext/pdo_dblib/tests/bug_67130.phpt | 2 +- ext/pdo_dblib/tests/bug_68957.phpt | 2 +- ext/pdo_dblib/tests/bug_69592.phpt | 2 +- ext/pdo_dblib/tests/bug_69757.phpt | 2 +- ext/pdo_dblib/tests/bug_71667.phpt | 2 +- ext/pdo_dblib/tests/bug_73396.phpt | 2 +- ext/pdo_dblib/tests/config.inc | 38 +++-- ext/pdo_dblib/tests/datetime2.phpt | 2 +- ext/pdo_dblib/tests/datetime_convert.phpt | 2 +- ext/pdo_dblib/tests/dbtds.phpt | 2 +- ext/pdo_dblib/tests/dbversion.phpt | 2 +- .../tests/pdo_dblib_param_str_natl.phpt | 2 +- ext/pdo_dblib/tests/pdo_dblib_quote.phpt | 2 +- ext/pdo_dblib/tests/pdodblib_001.phpt | 2 +- ext/pdo_dblib/tests/pdodblib_002.phpt | 2 +- .../tests/stringify_uniqueidentifier.phpt | 2 +- ext/pdo_dblib/tests/timeout.phpt | 2 +- ext/pdo_dblib/tests/types.phpt | 2 +- ext/pdo_mysql/tests/inc/mysql_pdo_test.inc | 48 ++++-- ext/pgsql/tests/inc/skipif.inc | 13 +- ext/snmp/tests/skipif.inc | 38 +---- ext/snmp/tests/snmp_skip_cache.phpt | 74 ---------- run-tests.php | 74 ++++++---- tests/probe_cache.inc | 52 +++++++ tests/run-test/test_probe_cache.phpt | 128 ++++++++++++++++ tests/run-test/test_skipif_environment.phpt | 14 ++ 50 files changed, 482 insertions(+), 374 deletions(-) delete mode 100644 ext/pdo/tests/pdo_test_skip_cache.phpt delete mode 100644 ext/snmp/tests/snmp_skip_cache.phpt create mode 100644 tests/probe_cache.inc create mode 100644 tests/run-test/test_probe_cache.phpt create mode 100644 tests/run-test/test_skipif_environment.phpt diff --git a/ext/ldap/tests/skipifbindfailure.inc b/ext/ldap/tests/skipifbindfailure.inc index 81c7998cfbb5..feb8faefbf3d 100644 --- a/ext/ldap/tests/skipifbindfailure.inc +++ b/ext/ldap/tests/skipifbindfailure.inc @@ -1,14 +1,24 @@ diff --git a/ext/mysqli/tests/test_setup/test_helpers.inc b/ext/mysqli/tests/test_setup/test_helpers.inc index c9ab401e7568..e980044e635d 100644 --- a/ext/mysqli/tests/test_setup/test_helpers.inc +++ b/ext/mysqli/tests/test_setup/test_helpers.inc @@ -1,5 +1,7 @@ query("SELECT SUPPORT FROM INFORMATION_SCHEMA.ENGINES WHERE ENGINE = 'InnoDB'"); @@ -123,11 +146,7 @@ function have_innodb(mysqli $link): bool { return $supported === 'YES' || $supported === 'DEFAULT'; } function mysqli_check_innodb_support_skip_test(): void { - try { - $link = default_mysqli_connect(); - } catch (\mysqli_sql_exception) { - die(sprintf("skip Can't connect to MySQL Server - [%d] %s", mysqli_connect_errno(), mysqli_connect_error())); - } + $link = mysqli_connect_or_skip(); if (! have_innodb($link)) { die(sprintf("skip Needs InnoDB support")); } diff --git a/ext/odbc/tests/skipif.inc b/ext/odbc/tests/skipif.inc index 9785f5843a77..be05da96f0a2 100644 --- a/ext/odbc/tests/skipif.inc +++ b/ext/odbc/tests/skipif.inc @@ -1,8 +1,18 @@ getAttribute(PDO::ATTR_DRIVER_NAME) == 'oci') { die("xfail PDO::PARAM_NULL is not honored by OCI driver, related with bug #81586"); } diff --git a/ext/pdo/tests/bug_79106.phpt b/ext/pdo/tests/bug_79106.phpt index 7e756a35ff25..c3d13914e373 100644 --- a/ext/pdo/tests/bug_79106.phpt +++ b/ext/pdo/tests/bug_79106.phpt @@ -8,7 +8,7 @@ $dir = getenv('REDIR_TEST_DIR'); if (!$dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; try { - $db = PDOTest::factoryForSkip(); + $db = PDOTest::factory(); } catch (PDOException $e) { die('skip ' . $e->getMessage()); } diff --git a/ext/pdo/tests/bug_79106_collision.phpt b/ext/pdo/tests/bug_79106_collision.phpt index 403b76ff512f..dc895f017016 100644 --- a/ext/pdo/tests/bug_79106_collision.phpt +++ b/ext/pdo/tests/bug_79106_collision.phpt @@ -8,7 +8,7 @@ $dir = getenv('REDIR_TEST_DIR'); if (!$dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; try { - $db = PDOTest::factoryForSkip(); + $db = PDOTest::factory(); } catch (PDOException $e) { die('skip ' . $e->getMessage()); } diff --git a/ext/pdo/tests/debug_emulated_prepares.phpt b/ext/pdo/tests/debug_emulated_prepares.phpt index 7c7056323b96..fba878eeed8b 100644 --- a/ext/pdo/tests/debug_emulated_prepares.phpt +++ b/ext/pdo/tests/debug_emulated_prepares.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factoryForSkip(); +$db = PDOTest::factory(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') die('skip pgsql has its own test for this feature'); if (!@$db->getAttribute(PDO::ATTR_EMULATE_PREPARES) && !@$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true)) die('skip driver cannot emulate prepared statements'); ?> diff --git a/ext/pdo/tests/gh8626.phpt b/ext/pdo/tests/gh8626.phpt index b39ff30e40f1..3515a30d77fb 100644 --- a/ext/pdo/tests/gh8626.phpt +++ b/ext/pdo/tests/gh8626.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factoryForSkip(); +$db = PDOTest::factory(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'oci') { die("xfail OCI driver errorInfo is inconsistent with other PDO drivers"); } diff --git a/ext/pdo/tests/pdo_017.phpt b/ext/pdo/tests/pdo_017.phpt index fd4b8674669d..b9171ce0cd58 100644 --- a/ext/pdo/tests/pdo_017.phpt +++ b/ext/pdo/tests/pdo_017.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factoryForSkip(); +$db = PDOTest::factory(); try { $db->beginTransaction(); $db->rollback(); diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index 7b88184da1ad..2776c9811ec3 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -1,6 +1,8 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + $database = null; + $exception = null; + $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attributes]; - $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); - $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); - $db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); - return $db; - } + $reason = ProbeCache::getFailure('pdo', $configuration, static function () use (&$database, &$exception, $classname, $useConnectMethod, $dsn, $user, $pass, $attributes): ?string { + try { + $database = self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); + return null; + } catch (PDOException $e) { + $exception = $e; + return $e->getMessage(); + } + }); - private static function getSkipCacheFile(): ?string { - $directory = getenv('TEST_PHP_SHARED_CACHE_DIR'); - if (!is_string($directory) || !is_dir($directory)) { - return null; + if (is_string($reason)) { + throw $exception ?? new PDOException($reason); } - $configuration = [ - getenv('PDOTEST_DSN'), - getenv('PDOTEST_USER'), - getenv('PDOTEST_PASS'), - getenv('PDOTEST_ATTR'), - getenv('PDOTEST_SKIP_ATTR'), - ]; - return $directory . DIRECTORY_SEPARATOR . 'pdo-' . hash('sha256', serialize($configuration)); + return $database; } - static function factoryForSkip() { - $attributes = self::getAttributes('PDOTEST_ATTR'); - $skipAttributes = self::getAttributes('PDOTEST_SKIP_ATTR'); - if ($skipAttributes !== null) { - $attributes = $skipAttributes + ($attributes ?? []); - } - return PDOTest::factory(PDO::class, false, $attributes); - } - - private static function connectOrSkip(): void { + static function skip() { try { - self::factoryForSkip(); + self::factory(); } catch (PDOException $e) { die("skip " . $e->getMessage()); } } - static function skip() { - $cacheFile = self::getSkipCacheFile(); - if ($cacheFile === null) { - self::connectOrSkip(); - return; - } - - $cache = @fopen($cacheFile, 'c+'); - if ($cache === false || !flock($cache, LOCK_EX)) { - if (is_resource($cache)) { - fclose($cache); - } - self::connectOrSkip(); - return; - } - - $cached = stream_get_contents($cache); - $reason = $cached !== '' ? $cached : null; - if ($reason === null) { - // Only failures are shared; successful checks still create their own connection. - try { - self::factoryForSkip(); - } catch (PDOException $e) { - $reason = $e->getMessage(); - rewind($cache); - ftruncate($cache, 0); - fwrite($cache, $reason); - fflush($cache); - } - } - - flock($cache, LOCK_UN); - fclose($cache); - if (is_string($reason)) { - die("skip $reason"); - } - } - static function test_factory($file, $classname = PDO::class, bool $useConnectMethod = false) { $config = self::get_config($file); foreach ($config['ENV'] as $k => $v) { @@ -163,6 +107,33 @@ class PDOTest { default => $db->exec("DROP TABLE IF EXISTS $tableName"), }; } + + private static function createConnection($classname, bool $useConnectMethod, $dsn, $user, $pass, ?array $attributes) { + if ($useConnectMethod) { + $db = $classname::connect($dsn, $user, $pass, $attributes); + } else { + $db = new $classname($dsn, $user, $pass, $attributes); + } + + if (!$db) { + die("Could not create PDO object (DSN=$dsn, user=$user)\n"); + } + // Ignore errors about non-existent tables + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); + $db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); + return $db; + } + + private static function getAttributes(string $environmentVariable): ?array { + $attributes = getenv($environmentVariable); + if (is_string($attributes) && strlen($attributes)) { + return unserialize($attributes); + } + return null; + } } /** See https://stackoverflow.com/a/3732466 */ function get_dummy_sql_request(): string diff --git a/ext/pdo/tests/pdo_test_skip_cache.phpt b/ext/pdo/tests/pdo_test_skip_cache.phpt deleted file mode 100644 index 09d333ba290b..000000000000 --- a/ext/pdo/tests/pdo_test_skip_cache.phpt +++ /dev/null @@ -1,74 +0,0 @@ ---TEST-- -PDO test helper caches connection failures for one test run ---EXTENSIONS-- -pdo ---FILE-- - ['pipe', 'w'], - 2 => ['redirect', 1], - ], - $pipes, - null, - $environment, - ['bypass_shell' => true], - ); - $output = stream_get_contents($pipes[1]); - fclose($pipes[1]); - - if (0 !== $exitCode = proc_close($process)) { - throw new Exception("PHP subprocess exited with code $exitCode: $output"); - } - - return $output; -} - -$dsn = 'missing_' . getmypid() . ':'; -$cacheDirectory = __DIR__ . '/pdo_test_skip_cache_' . getmypid(); -mkdir($cacheDirectory); - -$environment = getenv(); -$environment['PDOTEST_DSN'] = $dsn; -$environment['PDOTEST_USER'] = 'test'; -$environment['PDOTEST_PASS'] = 'test'; -$environment['TEST_PHP_SHARED_CACHE_DIR'] = $cacheDirectory; -unset($environment['PDOTEST_ATTR']); - -$helperDirectory = getenv('REDIR_TEST_DIR') ?: __DIR__; -$helper = var_export($helperDirectory . '/pdo_test.inc', true); -$code = "require $helper; PDOTest::skip();"; -$first = run_pdo_skip_check($code, $environment); - -$cacheFiles = glob($cacheDirectory . '/pdo-*'); -if (count($cacheFiles) !== 1) { - throw new Exception('Expected exactly one cache file'); -} -$cacheFile = $cacheFiles[0]; -$cachedReason = file_get_contents($cacheFile); -file_put_contents($cacheFile, 'cached connection failure'); - -$second = run_pdo_skip_check($code, $environment); -echo "$first\n$cachedReason\n$second\n"; -?> ---CLEAN-- - ---EXPECT-- -skip could not find driver -could not find driver -skip cached connection failure diff --git a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt index 812a715177e7..95b33ddb7f3c 100644 --- a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt +++ b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt @@ -9,7 +9,7 @@ if (PHP_INT_SIZE != 4) die("skip for 32bit platforms only"); if (PHP_OS_FAMILY === "Windows") die("skip not for Windows because the virtual address space for application is only 2GiB"); if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); require __DIR__ . '/config.inc'; -skipIfNoDbConnection(); +getDbConnection(); ?> --INI-- memory_limit=-1 diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt index df9e0ac12d7a..5b5c35252b0f 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt index c94b560dbed0..80bc8ab533f0 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt index 3eda5b6733d6..afc87dd05d7c 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt b/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt index 126bb5a20746..03f8456c7876 100644 --- a/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_transaction.phpt b/ext/pdo_dblib/tests/batch_stmt_transaction.phpt index a9ed9762ea45..2fab8dcda742 100644 --- a/ext/pdo_dblib/tests/batch_stmt_transaction.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_transaction.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_try.phpt b/ext/pdo_dblib/tests/batch_stmt_try.phpt index 0383e2feb00c..9e735ff87715 100644 --- a/ext/pdo_dblib/tests/batch_stmt_try.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_try.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/bug_38955.phpt b/ext/pdo_dblib/tests/bug_38955.phpt index 256b11f590f4..cd244ba1ec33 100644 --- a/ext/pdo_dblib/tests/bug_38955.phpt +++ b/ext/pdo_dblib/tests/bug_38955.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --CONFLICTS-- all diff --git a/ext/pdo_dblib/tests/bug_47588.phpt b/ext/pdo_dblib/tests/bug_47588.phpt index 3f58844b6a7f..218088b1e11f 100644 --- a/ext/pdo_dblib/tests/bug_47588.phpt +++ b/ext/pdo_dblib/tests/bug_47588.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --CONFLICTS-- all diff --git a/ext/pdo_dblib/tests/bug_54648.phpt b/ext/pdo_dblib/tests/bug_54648.phpt index 111d79692c84..9128b8c25404 100644 --- a/ext/pdo_dblib/tests/bug_54648.phpt +++ b/ext/pdo_dblib/tests/bug_54648.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6', '5.0', '6.0', '7.0'])) die('skip bigint type is unsupported by active TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/config.inc b/ext/pdo_dblib/tests/config.inc index 4f9605e1ba5c..d33a71b4245b 100644 --- a/ext/pdo_dblib/tests/config.inc +++ b/ext/pdo_dblib/tests/config.inc @@ -1,5 +1,7 @@ 1]; + } [$dsn, $user, $pass] = getCredentials(); - try { - $db = new $class($dsn, $user, $pass, $attributes); - if ($attributes === null) { - setAttributes($db); + $db = null; + $probe = static function () use (&$db, $class, $dsn, $user, $pass, $attributes, $connectionAttributes): ?string { + try { + $db = new $class($dsn, $user, $pass, $connectionAttributes); + if ($attributes === null) { + setAttributes($db); + } + return null; + } catch (PDOException $e) { + return $e->getMessage(); } - } catch (PDOException $e) { - die('skip ' . $e->getMessage()); + }; + + $reason = $evaluatingSkipif + ? ProbeCache::getFailure('pdo', [$class, false, $dsn, $user, $pass, $connectionAttributes], $probe) + : $probe(); + + if (is_string($reason)) { + die('skip ' . $reason); } return $db; } -function skipIfNoDbConnection(): PDO { - $attributes = getenv('PDO_DBLIB_TEST_DSN') === false - ? [Pdo\Dblib::ATTR_CONNECTION_TIMEOUT => 1] - : null; - return getDbConnection(PDO::class, $attributes); -} - function connectToDb() { [$dsn, $user, $pass] = getCredentials(); diff --git a/ext/pdo_dblib/tests/datetime2.phpt b/ext/pdo_dblib/tests/datetime2.phpt index 6564b398513c..2b54361a30e5 100644 --- a/ext/pdo_dblib/tests/datetime2.phpt +++ b/ext/pdo_dblib/tests/datetime2.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6', '5.0', '6.0', '7.0', '7.1', '7.2'])) die('skip feature unsupported by this TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/datetime_convert.phpt b/ext/pdo_dblib/tests/datetime_convert.phpt index 18244a181957..0934dd7f83e7 100644 --- a/ext/pdo_dblib/tests/datetime_convert.phpt +++ b/ext/pdo_dblib/tests/datetime_convert.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6'])) die('skip feature unsupported by this TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/timeout.phpt b/ext/pdo_dblib/tests/timeout.phpt index 9bc5eb9bd5c3..5f935a3d01b8 100644 --- a/ext/pdo_dblib/tests/timeout.phpt +++ b/ext/pdo_dblib/tests/timeout.phpt @@ -6,7 +6,7 @@ pdo_dblib --FILE-- --FILE-- getMessage(); + } + }); - $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); - $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); + if (is_string($reason)) { + throw $exception ?? new PDOException($reason); + } - return $db; + return $database; } static function factoryWithAttr($attr) { @@ -168,7 +177,7 @@ class MySQLPDOTest extends PDOTest { static function skip() { try { - $db = self::factory(); + self::factory(); } catch (PDOException $e) { die('skip could not connect'); } @@ -212,5 +221,22 @@ class MySQLPDOTest extends PDOTest { $message = $message ?? 'skip Transactional engine not found'; if (false == self::detect_transactional_mysql_engine($db)) die($message); } + + private static function createConnection($classname, bool $useConnectMethod, $dsn, $user, $pass, $attr) { + if ($useConnectMethod) { + $db = $classname::connect($dsn, $user, $pass, $attr); + } else { + $db = new $classname($dsn, $user, $pass, $attr); + } + + if (!$db) { + die("Could not create PDO object (DSN=$dsn, user=$user)\n"); + } + + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); + + return $db; + } } ?> diff --git a/ext/pgsql/tests/inc/skipif.inc b/ext/pgsql/tests/inc/skipif.inc index 2ce5f46e7787..bbe2a3169dd1 100644 --- a/ext/pgsql/tests/inc/skipif.inc +++ b/ext/pgsql/tests/inc/skipif.inc @@ -8,15 +8,22 @@ include("config.inc"); include("lcmess.inc"); +require_once dirname(__DIR__, 4) . '/tests/probe_cache.inc'; if (getenv("SKIP_REPEAT")) { // pgsql tests are order-dependent. // We should probably change that, but in the meantime do not allow repetition. die("skip Cannot repeat pgsql tests"); } -$conn = @pg_connect($conn_str); -if (!$conn) { - die("skip could not connect\n"); +$conn = null; + +$reason = ProbeCache::getFailure('pgsql', [$conn_str], static function () use (&$conn, $conn_str): ?string { + $conn = @pg_connect($conn_str); + return $conn ? null : 'could not connect'; +}); + +if (is_string($reason)) { + die("skip $reason\n"); } function skip_server_version($version, $op = '<') diff --git a/ext/snmp/tests/skipif.inc b/ext/snmp/tests/skipif.inc index 4beb3b28080d..43cc75f36b73 100644 --- a/ext/snmp/tests/skipif.inc +++ b/ext/snmp/tests/skipif.inc @@ -1,49 +1,19 @@ - @snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout) === false + @snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout, $retries) === false ? 'NO SNMPD on this host or community invalid' : null; - $directory = getenv('TEST_PHP_SHARED_CACHE_DIR'); - if (!is_string($directory) || !is_dir($directory)) { - return $probe(); - } - $configuration = [$hostname, $community, $timeout]; - $cacheFile = $directory - . DIRECTORY_SEPARATOR - . 'snmp-' - . hash('sha256', serialize($configuration)); - $cache = @fopen($cacheFile, 'c+'); - if ($cache === false || !flock($cache, LOCK_EX)) { - if (is_resource($cache)) { - fclose($cache); - } - return $probe(); - } - - $cached = stream_get_contents($cache); - $reason = $cached !== '' ? $cached : null; - if ($reason === null) { - // Only failures are shared; successful checks still probe the configured agent. - $reason = $probe(); - if (is_string($reason)) { - rewind($cache); - ftruncate($cache, 0); - fwrite($cache, $reason); - fflush($cache); - } - } - flock($cache, LOCK_UN); - fclose($cache); - return $reason; + return ProbeCache::getFailure('snmp', $configuration, $probe); } //test server is available diff --git a/ext/snmp/tests/snmp_skip_cache.phpt b/ext/snmp/tests/snmp_skip_cache.phpt deleted file mode 100644 index 9118d22b1fbf..000000000000 --- a/ext/snmp/tests/snmp_skip_cache.phpt +++ /dev/null @@ -1,74 +0,0 @@ ---TEST-- -SNMP test helper caches agent availability for one test run ---EXTENSIONS-- -snmp ---FILE-- - ['pipe', 'w'], - 2 => ['redirect', 1], - ], - $pipes, - null, - $environment, - ['bypass_shell' => true], - ); - $output = stream_get_contents($pipes[1]); - fclose($pipes[1]); - - if (0 !== $exitCode = proc_close($process)) { - throw new Exception("PHP subprocess exited with code $exitCode: $output"); - } - - return $output; -} - -$cacheDirectory = __DIR__ . '/snmp_skip_cache_' . getmypid(); -mkdir($cacheDirectory); - -$environment = getenv(); -$environment['SNMP_HOSTNAME'] = '127.0.0.1'; -$environment['SNMP_COMMUNITY'] = 'php_test_cache'; -$environment['SNMP_TIMEOUT'] = '100000'; -$environment['SNMP_RETRIES'] = '0'; -$environment['TEST_PHP_SHARED_CACHE_DIR'] = $cacheDirectory; - -$helper = var_export(__DIR__ . '/skipif.inc', true); -$code = "require $helper; echo \"available\\n\";"; -$first = run_snmp_skip_check($code, $environment); - -$cacheFiles = glob($cacheDirectory . '/snmp-*'); -if (count($cacheFiles) !== 1) { - throw new Exception('Expected exactly one cache file'); -} -$cacheFile = $cacheFiles[0]; -$cachedReason = file_get_contents($cacheFile); -file_put_contents($cacheFile, 'cached agent failure'); - -$second = run_snmp_skip_check($code, $environment); -echo "$first\n"; -echo "$cachedReason\n"; -echo $second; -?> ---CLEAN-- - ---EXPECT-- -skip NO SNMPD on this host or community invalid -NO SNMPD on this host or community invalid -skip cached agent failure diff --git a/run-tests.php b/run-tests.php index ac1d02e5408a..e092e0453c6e 100755 --- a/run-tests.php +++ b/run-tests.php @@ -232,19 +232,7 @@ function main(): void } } - // Tests may use this private directory to share results within this run. - unset($environment['TEST_PHP_SHARED_CACHE_DIR']); - $sharedCacheDirectory = getenv('TEST_PHP_SHARED_CACHE') !== '0' - ? create_shared_test_cache_directory() - : null; - if ($sharedCacheDirectory !== null) { - $environment['TEST_PHP_SHARED_CACHE_DIR'] = $sharedCacheDirectory; - register_shutdown_function(static function () use ($sharedCacheDirectory): void { - if (is_dir($sharedCacheDirectory)) { - rmdir_recursive($sharedCacheDirectory); - } - }); - } + SharedProbeCache::setUp($environment); if (IS_WINDOWS && empty($environment["SystemRoot"])) { $environment["SystemRoot"] = getenv("SystemRoot"); @@ -1087,24 +1075,59 @@ function get_file_cache_dir(): string return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-run-tests-file-cache'; } -function create_shared_test_cache_directory(): ?string +final class SharedProbeCache { - $temporaryDirectory = sys_get_temp_dir(); - if ($temporaryDirectory === '') { - return null; + private string $directory; + + private function __construct(string $directory) + { + $this->directory = $directory; + } + + public static function setUp(array &$environment): void + { + unset($environment['TEST_PHP_SHARED_CACHE_DIR']); + if (getenv('TEST_PHP_SHARED_CACHE') === '0') { + return; + } + + $cache = self::create(); + if ($cache === null) { + return; + } + + $environment['TEST_PHP_SHARED_CACHE_DIR'] = $cache->directory; + register_shutdown_function(static function () use ($cache): void { + $cache->cleanup(); + }); } - for ($attempt = 0; $attempt < 3; $attempt++) { - $directory = $temporaryDirectory - . DIRECTORY_SEPARATOR - . 'php-run-tests-' - . bin2hex(random_bytes(8)); - if (@mkdir($directory, 0700)) { - return $directory; + private static function create(): ?self + { + $temporaryDirectory = sys_get_temp_dir(); + if ($temporaryDirectory === '') { + return null; + } + + for ($attempt = 0; $attempt < 3; $attempt++) { + $directory = $temporaryDirectory + . DIRECTORY_SEPARATOR + . 'php-run-tests-' + . bin2hex(random_bytes(8)); + if (@mkdir($directory, 0700)) { + return new self($directory); + } } + + return null; } - return null; + private function cleanup(): void + { + if (is_dir($this->directory)) { + rmdir_recursive($this->directory); + } + } } function rmdir_recursive($dir) @@ -3803,6 +3826,7 @@ public function checkSkip(array $command, string $code, string $checkFile, strin } save_text($checkFile, $code, $tempFile); + $env['TEST_PHP_EVALUATING_SKIPIF'] = '1'; $command[] = $checkFile; $result = trim(system_with_timeout($command, $env)); if (strpos($result, 'nocache') === 0) { diff --git a/tests/probe_cache.inc b/tests/probe_cache.inc new file mode 100644 index 000000000000..600308bd6994 --- /dev/null +++ b/tests/probe_cache.inc @@ -0,0 +1,52 @@ + ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, + ['bypass_shell' => true], + ); + + return [$process, $pipes]; +} + +function finish_probe_cache_process($process, array $pipes): string +{ + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + if (0 !== $exitCode = proc_close($process)) { + throw new Exception("PHP subprocess exited with code $exitCode: $output"); + } + + return $output; +} + +function run_probe_cache_process(string $code, array $environment): string +{ + [$process, $pipes] = start_probe_cache_process($code, $environment); + return finish_probe_cache_process($process, $pipes); +} + +$cacheDirectory = getenv('TEST_PHP_SHARED_CACHE_DIR'); +if (!is_string($cacheDirectory)) { + throw new Exception('Missing shared test cache directory'); +} + +$environment = getenv(); + +$helper = var_export(dirname(__DIR__) . '/probe_cache.inc', true); +$namespace = 'probe-cache-test-' . bin2hex(random_bytes(8)); +$namespaceCode = var_export($namespace, true); +$first = run_probe_cache_process( + "require $helper; echo ProbeCache::getFailure($namespaceCode, ['shared'], static fn(): ?string => 'shared failure');", + $environment, +); +$second = run_probe_cache_process( + "require $helper; echo ProbeCache::getFailure($namespaceCode, ['shared'], static function (): ?string { throw new Exception('Probe should not run'); });", + $environment, +); +echo "$first\n$second\n"; + +$probeStarted = $cacheDirectory . '/probe_started'; +$probeStartedCode = var_export($probeStarted, true); +@unlink($probeStarted); +[$firstProcess, $firstPipes] = start_probe_cache_process( + "require $helper; echo ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): ?string { file_put_contents($probeStartedCode, 'started'); usleep(1000000); return 'concurrent failure'; });", + $environment, +); + +$deadline = microtime(true) + 5; +while (!file_exists($probeStarted) && microtime(true) < $deadline) { + usleep(1000); +} +if (!file_exists($probeStarted)) { + $output = finish_probe_cache_process($firstProcess, $firstPipes); + throw new Exception("Concurrent probe did not start: $output"); +} + +[$secondProcess, $secondPipes] = start_probe_cache_process( + "require $helper; echo ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): ?string { throw new Exception('Concurrent probe should not run'); });", + $environment, +); +$first = finish_probe_cache_process($firstProcess, $firstPipes); +$second = finish_probe_cache_process($secondProcess, $secondPipes); +echo "$first\n$second\n"; + +putenv("TEST_PHP_SHARED_CACHE_DIR=$cacheDirectory"); + +$failureCalls = 0; +$failureProbe = static function () use (&$failureCalls): ?string { + $failureCalls++; + return "failure $failureCalls"; +}; + +var_dump(ProbeCache::getFailure($namespace, ['first'], $failureProbe)); +var_dump(ProbeCache::getFailure($namespace, ['first'], $failureProbe)); +var_dump(ProbeCache::getFailure($namespace, ['second'], $failureProbe)); +var_dump($failureCalls); + +$successCalls = 0; +$successProbe = static function () use (&$successCalls): ?string { + $successCalls++; + return null; +}; + +var_dump(ProbeCache::getFailure($namespace, ['available'], $successProbe)); +var_dump(ProbeCache::getFailure($namespace, ['available'], $successProbe)); +var_dump($successCalls); + +putenv('TEST_PHP_SHARED_CACHE_DIR'); +var_dump(ProbeCache::getFailure($namespace, ['uncached'], static fn(): ?string => 'uncached failure')); +?> +--EXPECT-- +shared failure +shared failure +concurrent failure +concurrent failure +string(9) "failure 1" +string(9) "failure 1" +string(9) "failure 2" +int(2) +NULL +NULL +int(2) +string(16) "uncached failure" diff --git a/tests/run-test/test_skipif_environment.phpt b/tests/run-test/test_skipif_environment.phpt new file mode 100644 index 000000000000..8ccb69b0e508 --- /dev/null +++ b/tests/run-test/test_skipif_environment.phpt @@ -0,0 +1,14 @@ +--TEST-- +SKIPIF evaluation environment +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(false) From 9735f1309aa2e1e54982746b21ef57aeda08d506 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 4 Aug 2026 21:42:04 +0700 Subject: [PATCH 05/10] review: replaced `is_resource()` check --- tests/probe_cache.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/probe_cache.inc b/tests/probe_cache.inc index 600308bd6994..d32f830a6dbc 100644 --- a/tests/probe_cache.inc +++ b/tests/probe_cache.inc @@ -23,7 +23,7 @@ final class ProbeCache $cache = @fopen($cacheFile, 'c+'); if ($cache === false || !flock($cache, LOCK_EX)) { - if (is_resource($cache)) { + if ($cache !== false) { fclose($cache); } return $probe(); From 1ad2bdae52f92ce79318d82705a2d142677d6acb Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 4 Aug 2026 21:57:08 +0700 Subject: [PATCH 06/10] review: always throw new exception --- ext/pdo/tests/pdo_test.inc | 6 ++---- ext/pdo_mysql/tests/inc/mysql_pdo_test.inc | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index 2776c9811ec3..82bf385a7931 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -42,21 +42,19 @@ class PDOTest { } $database = null; - $exception = null; $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attributes]; - $reason = ProbeCache::getFailure('pdo', $configuration, static function () use (&$database, &$exception, $classname, $useConnectMethod, $dsn, $user, $pass, $attributes): ?string { + $reason = ProbeCache::getFailure('pdo', $configuration, static function () use (&$database, $classname, $useConnectMethod, $dsn, $user, $pass, $attributes): ?string { try { $database = self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); return null; } catch (PDOException $e) { - $exception = $e; return $e->getMessage(); } }); if (is_string($reason)) { - throw $exception ?? new PDOException($reason); + throw new PDOException($reason); } return $database; diff --git a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc index 4e0c705f8e44..9fc50a712173 100644 --- a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc +++ b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc @@ -25,21 +25,19 @@ class MySQLPDOTest extends PDOTest { } $database = null; - $exception = null; $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attr]; - $reason = ProbeCache::getFailure('pdo', $configuration, static function () use (&$database, &$exception, $classname, $useConnectMethod, $dsn, $user, $pass, $attr): ?string { + $reason = ProbeCache::getFailure('pdo', $configuration, static function () use (&$database, $classname, $useConnectMethod, $dsn, $user, $pass, $attr): ?string { try { $database = self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); return null; } catch (PDOException $e) { - $exception = $e; return $e->getMessage(); } }); if (is_string($reason)) { - throw $exception ?? new PDOException($reason); + throw new PDOException($reason); } return $database; From 9678f91aa513929e51e80122b7e0dc3b00263e45 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 5 Aug 2026 00:10:48 +0700 Subject: [PATCH 07/10] review: introduced `ProbeFailureException` --- ext/ldap/tests/skipifbindfailure.inc | 23 ++++--- ext/mysqli/tests/skipifconnectfailure.inc | 21 ++++--- ext/mysqli/tests/test_setup/test_helpers.inc | 27 ++++----- ext/odbc/tests/skipif.inc | 21 ++++--- ext/pdo/tests/pdo_test.inc | 28 ++++----- ext/pdo_dblib/tests/config.inc | 33 ++++------ ext/pdo_mysql/tests/inc/mysql_pdo_test.inc | 28 ++++----- ext/pgsql/tests/inc/skipif.inc | 18 +++--- ext/snmp/tests/skipif.inc | 25 +++----- tests/probe_cache.inc | 39 +++++++++--- tests/run-test/test_probe_cache.phpt | 63 +++++++++++++++----- 11 files changed, 174 insertions(+), 152 deletions(-) diff --git a/ext/ldap/tests/skipifbindfailure.inc b/ext/ldap/tests/skipifbindfailure.inc index feb8faefbf3d..0b421ef1bd71 100644 --- a/ext/ldap/tests/skipifbindfailure.inc +++ b/ext/ldap/tests/skipifbindfailure.inc @@ -5,19 +5,18 @@ require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc'; if ($skip_on_bind_failure) { $configuration = [$uri, $user, $passwd, $protocol_version]; - $reason = ProbeCache::getFailure('ldap.bind', $configuration, static function () use ($uri, $user, $passwd, $protocol_version): ?string { - $link = ldap_connect($uri); - ldap_set_option($link, LDAP_OPT_PROTOCOL_VERSION, $protocol_version); - if (!@ldap_bind($link, $user, $passwd)) { - return sprintf("Can't bind to LDAP Server - [%d] %s", ldap_errno($link), ldap_error($link)); - } - - ldap_unbind($link); - return null; - }); + try { + ProbeCache::getFailure('ldap.bind', $configuration, static function () use ($uri, $user, $passwd, $protocol_version): void { + $link = ldap_connect($uri); + ldap_set_option($link, LDAP_OPT_PROTOCOL_VERSION, $protocol_version); + if (!@ldap_bind($link, $user, $passwd)) { + throw new ProbeFailureException(sprintf("Can't bind to LDAP Server - [%d] %s", ldap_errno($link), ldap_error($link))); + } - if (is_string($reason)) { - die("skip $reason"); + ldap_unbind($link); + }); + } catch (ProbeFailureException $e) { + die("skip {$e->getMessage()}"); } } diff --git a/ext/mysqli/tests/skipifconnectfailure.inc b/ext/mysqli/tests/skipifconnectfailure.inc index eb7dee5f60bf..f5cd763af140 100644 --- a/ext/mysqli/tests/skipifconnectfailure.inc +++ b/ext/mysqli/tests/skipifconnectfailure.inc @@ -4,17 +4,16 @@ require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc'; $configuration = [$host, $port, $user, $passwd, $db, $socket, get_environment_connection_flags()]; -$reason = ProbeCache::getFailure('mysqli', $configuration, static function () use ($host, $user, $passwd, $db, $port, $socket): ?string { - $link = @my_mysqli_connect($host, $user, $passwd, $db, $port, $socket); - if (!is_object($link)) { - return sprintf("Can't connect to MySQL Server - [%d] %s", mysqli_connect_errno(), mysqli_connect_error()); - } +try { + ProbeCache::getFailure('mysqli', $configuration, static function () use ($host, $user, $passwd, $db, $port, $socket): void { + $link = @my_mysqli_connect($host, $user, $passwd, $db, $port, $socket); + if (!is_object($link)) { + throw new ProbeFailureException(sprintf("Can't connect to MySQL Server - [%d] %s", mysqli_connect_errno(), mysqli_connect_error())); + } - mysqli_close($link); - return null; -}); - -if (is_string($reason)) { - die("skip $reason"); + mysqli_close($link); + }); +} catch (ProbeFailureException $e) { + die("skip {$e->getMessage()}"); } ?> diff --git a/ext/mysqli/tests/test_setup/test_helpers.inc b/ext/mysqli/tests/test_setup/test_helpers.inc index e980044e635d..d32697aebf82 100644 --- a/ext/mysqli/tests/test_setup/test_helpers.inc +++ b/ext/mysqli/tests/test_setup/test_helpers.inc @@ -114,7 +114,6 @@ function mysqli_check_skip_test(): void { } function mysqli_connect_or_skip(): mysqli { - $connection = null; $configuration = [ get_default_host(), get_default_port(), @@ -125,20 +124,20 @@ function mysqli_connect_or_skip(): mysqli { get_environment_connection_flags(), ]; - $reason = ProbeCache::getFailure('mysqli', $configuration, static function () use (&$connection): ?string { - try { - $connection = default_mysqli_connect(); - return null; - } catch (mysqli_sql_exception $e) { - return sprintf("Can't connect to MySQL Server - [%d] %s", mysqli_connect_errno(), mysqli_connect_error()); - } - }); - - if (is_string($reason)) { - die("skip $reason"); + try { + return ProbeCache::getFailure('mysqli', $configuration, static function (): mysqli { + try { + return default_mysqli_connect(); + } catch (mysqli_sql_exception $e) { + throw new ProbeFailureException( + sprintf("Can't connect to MySQL Server - [%d] %s", mysqli_connect_errno(), mysqli_connect_error()), + $e, + ); + } + }); + } catch (ProbeFailureException $e) { + die("skip {$e->getMessage()}"); } - - return $connection; } function have_innodb(mysqli $link): bool { $res = $link->query("SELECT SUPPORT FROM INFORMATION_SCHEMA.ENGINES WHERE ENGINE = 'InnoDB'"); diff --git a/ext/odbc/tests/skipif.inc b/ext/odbc/tests/skipif.inc index be05da96f0a2..a602debe2271 100644 --- a/ext/odbc/tests/skipif.inc +++ b/ext/odbc/tests/skipif.inc @@ -3,16 +3,15 @@ include 'config.inc'; require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc'; -$conn = null; -$reason = ProbeCache::getFailure('odbc', [$dsn, $user, $pass], static function () use ($dsn, $user, $pass, &$conn): ?string { - $conn = @odbc_connect($dsn, $user, $pass); - if (!$conn) { - return 'could not connect'; - } +try { + $conn = ProbeCache::getFailure('odbc', [$dsn, $user, $pass], static function () use ($dsn, $user, $pass): Odbc\Connection { + $conn = @odbc_connect($dsn, $user, $pass); + if (!$conn) { + throw new ProbeFailureException('could not connect'); + } - return null; -}); - -if (is_string($reason)) { - die("skip $reason"); + return $conn; + }); +} catch (ProbeFailureException $e) { + die("skip {$e->getMessage()}"); } diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index 82bf385a7931..817eaa7bf798 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -37,27 +37,19 @@ class PDOTest { if ($user === false) $user = NULL; if ($pass === false) $pass = NULL; - if (getenv('TEST_PHP_EVALUATING_SKIPIF') !== '1') { - return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); - } - - $database = null; $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attributes]; - $reason = ProbeCache::getFailure('pdo', $configuration, static function () use (&$database, $classname, $useConnectMethod, $dsn, $user, $pass, $attributes): ?string { - try { - $database = self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); - return null; - } catch (PDOException $e) { - return $e->getMessage(); - } - }); - - if (is_string($reason)) { - throw new PDOException($reason); + try { + return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attributes): PDO { + try { + return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); + } catch (PDOException $e) { + throw new ProbeFailureException($e); + } + }); + } catch (ProbeFailureException $e) { + throw new PDOException($e->getMessage()); } - - return $database; } static function skip() { diff --git a/ext/pdo_dblib/tests/config.inc b/ext/pdo_dblib/tests/config.inc index d33a71b4245b..78bf0804890d 100644 --- a/ext/pdo_dblib/tests/config.inc +++ b/ext/pdo_dblib/tests/config.inc @@ -50,28 +50,21 @@ function getDbConnection(string $class = PDO::class, ?array $attributes = null) } [$dsn, $user, $pass] = getCredentials(); - $db = null; - $probe = static function () use (&$db, $class, $dsn, $user, $pass, $attributes, $connectionAttributes): ?string { - try { - $db = new $class($dsn, $user, $pass, $connectionAttributes); - if ($attributes === null) { - setAttributes($db); + try { + return ProbeCache::getFailure('pdo', [$class, false, $dsn, $user, $pass, $connectionAttributes], static function () use ($class, $dsn, $user, $pass, $attributes, $connectionAttributes): PDO { + try { + $db = new $class($dsn, $user, $pass, $connectionAttributes); + if ($attributes === null) { + setAttributes($db); + } + return $db; + } catch (PDOException $e) { + throw new ProbeFailureException($e); } - return null; - } catch (PDOException $e) { - return $e->getMessage(); - } - }; - - $reason = $evaluatingSkipif - ? ProbeCache::getFailure('pdo', [$class, false, $dsn, $user, $pass, $connectionAttributes], $probe) - : $probe(); - - if (is_string($reason)) { - die('skip ' . $reason); + }); + } catch (ProbeFailureException $e) { + die('skip ' . $e->getMessage()); } - - return $db; } function connectToDb() { diff --git a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc index 9fc50a712173..d840e66b6ff5 100644 --- a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc +++ b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc @@ -20,27 +20,19 @@ class MySQLPDOTest extends PDOTest { $attr = is_string($attr) && strlen($attr) ? unserialize($attr) : null; } - if (getenv('TEST_PHP_EVALUATING_SKIPIF') !== '1') { - return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); - } - - $database = null; $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attr]; - $reason = ProbeCache::getFailure('pdo', $configuration, static function () use (&$database, $classname, $useConnectMethod, $dsn, $user, $pass, $attr): ?string { - try { - $database = self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); - return null; - } catch (PDOException $e) { - return $e->getMessage(); - } - }); - - if (is_string($reason)) { - throw new PDOException($reason); + try { + return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attr): PDO { + try { + return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); + } catch (PDOException $e) { + throw new ProbeFailureException($e); + } + }); + } catch (ProbeFailureException $e) { + throw new PDOException($e->getMessage()); } - - return $database; } static function factoryWithAttr($attr) { diff --git a/ext/pgsql/tests/inc/skipif.inc b/ext/pgsql/tests/inc/skipif.inc index bbe2a3169dd1..03a00b0cde7b 100644 --- a/ext/pgsql/tests/inc/skipif.inc +++ b/ext/pgsql/tests/inc/skipif.inc @@ -15,15 +15,17 @@ if (getenv("SKIP_REPEAT")) { // We should probably change that, but in the meantime do not allow repetition. die("skip Cannot repeat pgsql tests"); } -$conn = null; +try { + $conn = ProbeCache::getFailure('pgsql', [$conn_str], static function () use ($conn_str): PgSql\Connection { + $conn = @pg_connect($conn_str); + if (!$conn) { + throw new ProbeFailureException('could not connect'); + } -$reason = ProbeCache::getFailure('pgsql', [$conn_str], static function () use (&$conn, $conn_str): ?string { - $conn = @pg_connect($conn_str); - return $conn ? null : 'could not connect'; -}); - -if (is_string($reason)) { - die("skip $reason\n"); + return $conn; + }); +} catch (ProbeFailureException $e) { + die("skip {$e->getMessage()}\n"); } function skip_server_version($version, $op = '<') diff --git a/ext/snmp/tests/skipif.inc b/ext/snmp/tests/skipif.inc index 43cc75f36b73..283cfaa5357d 100644 --- a/ext/snmp/tests/skipif.inc +++ b/ext/snmp/tests/skipif.inc @@ -2,26 +2,17 @@ require_once (dirname(__FILE__).'/snmp_include.inc'); require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc'; -function get_snmp_test_agent_unavailable_reason(): ?string -{ - global $hostname, $community, $timeout, $retries; - - $probe = static fn(): ?string => - @snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout, $retries) === false - ? 'NO SNMPD on this host or community invalid' - : null; - - $configuration = [$hostname, $community, $timeout]; - - return ProbeCache::getFailure('snmp', $configuration, $probe); -} - //test server is available // this require snmpget to work ... //snmpget ( string $hostname , string $community , //string $object_id [, int $timeout [, int $retries ]] ) -$reason = get_snmp_test_agent_unavailable_reason(); -if (is_string($reason)) { - die("skip $reason"); +try { + ProbeCache::getFailure('snmp', [$hostname, $community, $timeout], static function () use ($hostname, $community, $timeout, $retries): void { + if (@snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout, $retries) === false) { + throw new ProbeFailureException('NO SNMPD on this host or community invalid'); + } + }); +} catch (ProbeFailureException $e) { + die("skip {$e->getMessage()}"); } diff --git a/tests/probe_cache.inc b/tests/probe_cache.inc index d32f830a6dbc..01ba1bec5564 100644 --- a/tests/probe_cache.inc +++ b/tests/probe_cache.inc @@ -1,15 +1,36 @@ getMessage(); + } + + parent::__construct($failure, 0, $previous); + } +} + final class ProbeCache { private const FAILURE_PREFIX = 'failure:'; /** - * Runs a probe once per configuration for the test run, caching only a - * failure. The callback returns its skip reason, or null on success. + * Runs a probe once per configuration during SKIPIF, caching only failures. + * Outside SKIPIF, the cache is bypassed and wrapped exceptions are rethrown. */ - public static function getFailure(string $namespace, array $configuration, callable $probe): ?string + public static function getFailure(string $namespace, array $configuration, callable $probe): mixed { + if (getenv('TEST_PHP_EVALUATING_SKIPIF') !== '1') { + try { + return $probe(); + } catch (ProbeFailureException $e) { + throw $e->getPrevious() ?? $e; + } + } + $directory = getenv('TEST_PHP_SHARED_CACHE_DIR'); if (!is_string($directory) || !is_dir($directory)) { return $probe(); @@ -32,18 +53,18 @@ final class ProbeCache try { $cached = stream_get_contents($cache); if (is_string($cached) && str_starts_with($cached, self::FAILURE_PREFIX)) { - return substr($cached, strlen(self::FAILURE_PREFIX)); + throw new ProbeFailureException(substr($cached, strlen(self::FAILURE_PREFIX))); } - $failure = $probe(); - if (is_string($failure)) { + try { + return $probe(); + } catch (ProbeFailureException $e) { rewind($cache); ftruncate($cache, 0); - fwrite($cache, self::FAILURE_PREFIX . $failure); + fwrite($cache, self::FAILURE_PREFIX . $e->getMessage()); fflush($cache); + throw $e; } - - return $failure; } finally { flock($cache, LOCK_UN); fclose($cache); diff --git a/tests/run-test/test_probe_cache.phpt b/tests/run-test/test_probe_cache.phpt index 70ad9ef7153e..ef703ed8f637 100644 --- a/tests/run-test/test_probe_cache.phpt +++ b/tests/run-test/test_probe_cache.phpt @@ -48,16 +48,17 @@ if (!is_string($cacheDirectory)) { } $environment = getenv(); +$environment['TEST_PHP_EVALUATING_SKIPIF'] = '1'; $helper = var_export(dirname(__DIR__) . '/probe_cache.inc', true); $namespace = 'probe-cache-test-' . bin2hex(random_bytes(8)); $namespaceCode = var_export($namespace, true); $first = run_probe_cache_process( - "require $helper; echo ProbeCache::getFailure($namespaceCode, ['shared'], static fn(): ?string => 'shared failure');", + "require $helper; try { ProbeCache::getFailure($namespaceCode, ['shared'], static function (): never { throw new ProbeFailureException('shared failure'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", $environment, ); $second = run_probe_cache_process( - "require $helper; echo ProbeCache::getFailure($namespaceCode, ['shared'], static function (): ?string { throw new Exception('Probe should not run'); });", + "require $helper; try { ProbeCache::getFailure($namespaceCode, ['shared'], static function (): never { throw new Exception('Probe should not run'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", $environment, ); echo "$first\n$second\n"; @@ -66,7 +67,7 @@ $probeStarted = $cacheDirectory . '/probe_started'; $probeStartedCode = var_export($probeStarted, true); @unlink($probeStarted); [$firstProcess, $firstPipes] = start_probe_cache_process( - "require $helper; echo ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): ?string { file_put_contents($probeStartedCode, 'started'); usleep(1000000); return 'concurrent failure'; });", + "require $helper; try { ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): never { file_put_contents($probeStartedCode, 'started'); usleep(1000000); throw new ProbeFailureException('concurrent failure'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", $environment, ); @@ -80,7 +81,7 @@ if (!file_exists($probeStarted)) { } [$secondProcess, $secondPipes] = start_probe_cache_process( - "require $helper; echo ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): ?string { throw new Exception('Concurrent probe should not run'); });", + "require $helper; try { ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): never { throw new Exception('Concurrent probe should not run'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", $environment, ); $first = finish_probe_cache_process($firstProcess, $firstPipes); @@ -88,30 +89,62 @@ $second = finish_probe_cache_process($secondProcess, $secondPipes); echo "$first\n$second\n"; putenv("TEST_PHP_SHARED_CACHE_DIR=$cacheDirectory"); +putenv('TEST_PHP_EVALUATING_SKIPIF=1'); $failureCalls = 0; -$failureProbe = static function () use (&$failureCalls): ?string { +$failureProbe = static function () use (&$failureCalls): never { $failureCalls++; - return "failure $failureCalls"; + throw new ProbeFailureException("failure $failureCalls"); }; -var_dump(ProbeCache::getFailure($namespace, ['first'], $failureProbe)); -var_dump(ProbeCache::getFailure($namespace, ['first'], $failureProbe)); -var_dump(ProbeCache::getFailure($namespace, ['second'], $failureProbe)); +try { + ProbeCache::getFailure($namespace, ['first'], $failureProbe); +} catch (ProbeFailureException $e) { + var_dump($e->getMessage()); +} +try { + ProbeCache::getFailure($namespace, ['first'], $failureProbe); +} catch (ProbeFailureException $e) { + var_dump($e->getMessage()); +} +try { + ProbeCache::getFailure($namespace, ['second'], $failureProbe); +} catch (ProbeFailureException $e) { + var_dump($e->getMessage()); +} var_dump($failureCalls); $successCalls = 0; -$successProbe = static function () use (&$successCalls): ?string { +$successProbe = static function () use (&$successCalls): string { $successCalls++; - return null; + return "success $successCalls"; }; var_dump(ProbeCache::getFailure($namespace, ['available'], $successProbe)); var_dump(ProbeCache::getFailure($namespace, ['available'], $successProbe)); var_dump($successCalls); +putenv('TEST_PHP_EVALUATING_SKIPIF'); +var_dump(ProbeCache::getFailure($namespace, ['first'], static fn(): string => 'uncached success')); + +$previous = new Exception('original failure'); +try { + ProbeCache::getFailure($namespace, ['wrapped'], static function () use ($previous): never { + throw new ProbeFailureException($previous); + }); +} catch (Throwable $e) { + var_dump($e === $previous); +} + +putenv('TEST_PHP_EVALUATING_SKIPIF=1'); putenv('TEST_PHP_SHARED_CACHE_DIR'); -var_dump(ProbeCache::getFailure($namespace, ['uncached'], static fn(): ?string => 'uncached failure')); +try { + ProbeCache::getFailure($namespace, ['uncached'], static function (): never { + throw new ProbeFailureException('uncached failure'); + }); +} catch (ProbeFailureException $e) { + var_dump($e->getMessage()); +} ?> --EXPECT-- shared failure @@ -122,7 +155,9 @@ string(9) "failure 1" string(9) "failure 1" string(9) "failure 2" int(2) -NULL -NULL +string(9) "success 1" +string(9) "success 2" int(2) +string(16) "uncached success" +bool(true) string(16) "uncached failure" From 03845a0f4d01e1da46d5b78824ab71dfd41a4930 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 5 Aug 2026 01:56:36 +0700 Subject: [PATCH 08/10] fix: adjusted test assertions --- .../pdo_ATTR_STATEMENT_CLASS_basic.phpt | 6 +++--- .../pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt | 4 ++-- .../pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt | 8 ++++---- .../pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt | 4 ++-- ext/pdo/tests/pdo_027.phpt | 8 ++++---- ext/pdo/tests/pdo_query_fetch_lazy001.phpt | 4 ++-- ext/pdo_pgsql/tests/bug75402.phpt | 6 +++--- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_basic.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_basic.phpt index dfef3bb4839f..bd510b6ac636 100644 --- a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_basic.phpt +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_basic.phpt @@ -78,7 +78,7 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_attr_statement_class_basic"); ?> ---EXPECT-- +--EXPECTF-- array(1) { [0]=> string(12) "PDOStatement" @@ -89,7 +89,7 @@ StatementWithPublicDestructor::__destruct Class derived from PDOStatement, with private constructor: bool(true) StatementWithPrivateConstructor::__construct -object(StatementWithPrivateConstructor)#2 (1) { +object(StatementWithPrivateConstructor)#%d (1) { ["queryString"]=> string(68) "SELECT id, label FROM pdo_attr_statement_class_basic ORDER BY id ASC" } @@ -97,7 +97,7 @@ string(6) "param1" Class derived from a child of PDOStatement: bool(true) StatementWithPrivateConstructor::__construct -object(StatementDerivedFromChild)#2 (1) { +object(StatementDerivedFromChild)#%d (1) { ["queryString"]=> string(68) "SELECT id, label FROM pdo_attr_statement_class_basic ORDER BY id ASC" } diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt index 301a8835eee2..aebf969b5832 100644 --- a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt @@ -49,8 +49,8 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_attr_statement_class_ctor_arg_gc"); ?> ---EXPECT-- -object(Bar)#1 (1) { +--EXPECTF-- +object(Bar)#%d (1) { ["statementClass"]=> string(3) "Foo" } diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt index 61cfe56f779d..672085441c95 100644 --- a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt @@ -42,18 +42,18 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_attr_statement_class_cyclic_ctor_args"); ?> ---EXPECT-- +--EXPECTF-- array(1) { [0]=> string(12) "PDOStatement" } bool(true) -object(PDO)#1 (0) { +object(PDO)#%d (0) { } -object(HoldPdo)#2 (2) { +object(HoldPdo)#%d (2) { ["queryString"]=> string(79) "SELECT id, label FROM pdo_attr_statement_class_cyclic_ctor_args ORDER BY id ASC" ["v"]=> - object(PDO)#1 (0) { + object(PDO)#%d (0) { } } diff --git a/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt b/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt index 86d2cfcefd5f..bc825d4f8523 100644 --- a/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt +++ b/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt @@ -51,8 +51,8 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_prepare_attr_statement_class_ctor_arg_gc"); ?> ---EXPECT-- -object(Bar)#1 (1) { +--EXPECTF-- +object(Bar)#%d (1) { ["statementClass"]=> string(3) "Foo" } diff --git a/ext/pdo/tests/pdo_027.phpt b/ext/pdo/tests/pdo_027.phpt index 6bb350f0c133..969ec9462139 100644 --- a/ext/pdo/tests/pdo_027.phpt +++ b/ext/pdo/tests/pdo_027.phpt @@ -36,13 +36,13 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "test027"); ?> ---EXPECT-- -object(PDOStatement)#2 (1) { +--EXPECTF-- +object(PDOStatement)#%d (1) { ["queryString"]=> string(21) "SELECT * FROM test027" } bool(false) -object(PDORow)#4 (3) { +object(PDORow)#%d (3) { ["queryString"]=> string(21) "SELECT * FROM test027" ["id"]=> @@ -52,7 +52,7 @@ object(PDORow)#4 (3) { } lazy: 1test1 bool(true) -object(PDORow)#4 (3) { +object(PDORow)#%d (3) { ["queryString"]=> string(21) "SELECT * FROM test027" ["id"]=> diff --git a/ext/pdo/tests/pdo_query_fetch_lazy001.phpt b/ext/pdo/tests/pdo_query_fetch_lazy001.phpt index cec5e9f6d6c9..7d9538513045 100644 --- a/ext/pdo/tests/pdo_query_fetch_lazy001.phpt +++ b/ext/pdo/tests/pdo_query_fetch_lazy001.phpt @@ -29,8 +29,8 @@ require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc'; $db = PDOTest::factory(); PDOTest::dropTableIfExists($db, "pdo_query_fetch_lazy_001"); ?> ---EXPECT-- -object(PDOStatement)#2 (1) { +--EXPECTF-- +object(PDOStatement)#%d (1) { ["queryString"]=> string(38) "SELECT * FROM pdo_query_fetch_lazy_001" } diff --git a/ext/pdo_pgsql/tests/bug75402.phpt b/ext/pdo_pgsql/tests/bug75402.phpt index be0fbd3f0bee..909c315f8004 100644 --- a/ext/pdo_pgsql/tests/bug75402.phpt +++ b/ext/pdo_pgsql/tests/bug75402.phpt @@ -88,12 +88,12 @@ require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc'; $db = PDOTest::test_factory(__DIR__ . '/common.phpt'); $db->exec('DROP TABLE IF EXISTS bug75402'); ?> ---EXPECT-- -object(stdClass)#2 (1) { +--EXPECTF-- +object(stdClass)#%d (1) { ["entries"]=> array(1) { [0]=> - object(stdClass)#4 (10) { + object(stdClass)#%d (10) { ["sid"]=> string(19) "20171016083645_5337" ["sgroupid"]=> From 355c0ea05cf6e9f7248b09f4ba67e7111ce3ae04 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Thu, 6 Aug 2026 00:47:58 +0700 Subject: [PATCH 09/10] review: cleaned up exceptions --- ext/pdo/tests/pdo_test.inc | 20 ++--- ext/pdo_mysql/tests/inc/mysql_pdo_test.inc | 20 ++--- tests/run-test/test_probe_cache.phpt | 92 ++++++++++++++++++---- 3 files changed, 92 insertions(+), 40 deletions(-) diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index 817eaa7bf798..fa10cc5fd5bb 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -39,23 +39,19 @@ class PDOTest { $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attributes]; - try { - return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attributes): PDO { - try { - return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); - } catch (PDOException $e) { - throw new ProbeFailureException($e); - } - }); - } catch (ProbeFailureException $e) { - throw new PDOException($e->getMessage()); - } + return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attributes): PDO { + try { + return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); + } catch (PDOException $e) { + throw new ProbeFailureException($e); + } + }); } static function skip() { try { self::factory(); - } catch (PDOException $e) { + } catch (ProbeFailureException $e) { die("skip " . $e->getMessage()); } } diff --git a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc index d840e66b6ff5..8aabe47ef855 100644 --- a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc +++ b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc @@ -22,17 +22,13 @@ class MySQLPDOTest extends PDOTest { $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attr]; - try { - return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attr): PDO { - try { - return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); - } catch (PDOException $e) { - throw new ProbeFailureException($e); - } - }); - } catch (ProbeFailureException $e) { - throw new PDOException($e->getMessage()); - } + return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attr): PDO { + try { + return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); + } catch (PDOException $e) { + throw new ProbeFailureException($e); + } + }); } static function factoryWithAttr($attr) { @@ -168,7 +164,7 @@ class MySQLPDOTest extends PDOTest { static function skip() { try { self::factory(); - } catch (PDOException $e) { + } catch (ProbeFailureException $e) { die('skip could not connect'); } } diff --git a/tests/run-test/test_probe_cache.phpt b/tests/run-test/test_probe_cache.phpt index ef703ed8f637..6bc94ba00a91 100644 --- a/tests/run-test/test_probe_cache.phpt +++ b/tests/run-test/test_probe_cache.phpt @@ -54,11 +54,39 @@ $helper = var_export(dirname(__DIR__) . '/probe_cache.inc', true); $namespace = 'probe-cache-test-' . bin2hex(random_bytes(8)); $namespaceCode = var_export($namespace, true); $first = run_probe_cache_process( - "require $helper; try { ProbeCache::getFailure($namespaceCode, ['shared'], static function (): never { throw new ProbeFailureException('shared failure'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", + <<getMessage(); + } + PHP, $environment, ); $second = run_probe_cache_process( - "require $helper; try { ProbeCache::getFailure($namespaceCode, ['shared'], static function (): never { throw new Exception('Probe should not run'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", + <<getMessage(); + } + PHP, $environment, ); echo "$first\n$second\n"; @@ -67,7 +95,23 @@ $probeStarted = $cacheDirectory . '/probe_started'; $probeStartedCode = var_export($probeStarted, true); @unlink($probeStarted); [$firstProcess, $firstPipes] = start_probe_cache_process( - "require $helper; try { ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): never { file_put_contents($probeStartedCode, 'started'); usleep(1000000); throw new ProbeFailureException('concurrent failure'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", + <<getMessage(); + } + PHP, $environment, ); @@ -81,7 +125,21 @@ if (!file_exists($probeStarted)) { } [$secondProcess, $secondPipes] = start_probe_cache_process( - "require $helper; try { ProbeCache::getFailure($namespaceCode, ['concurrent'], static function (): never { throw new Exception('Concurrent probe should not run'); }); } catch (ProbeFailureException \$e) { echo \$e->getMessage(); }", + <<getMessage(); + } + PHP, $environment, ); $first = finish_probe_cache_process($firstProcess, $firstPipes); @@ -100,17 +158,17 @@ $failureProbe = static function () use (&$failureCalls): never { try { ProbeCache::getFailure($namespace, ['first'], $failureProbe); } catch (ProbeFailureException $e) { - var_dump($e->getMessage()); + echo $e::class, ': ', $e->getMessage(), "\n"; } try { ProbeCache::getFailure($namespace, ['first'], $failureProbe); } catch (ProbeFailureException $e) { - var_dump($e->getMessage()); + echo $e::class, ': ', $e->getMessage(), "\n"; } try { ProbeCache::getFailure($namespace, ['second'], $failureProbe); } catch (ProbeFailureException $e) { - var_dump($e->getMessage()); + echo $e::class, ': ', $e->getMessage(), "\n"; } var_dump($failureCalls); @@ -133,6 +191,7 @@ try { throw new ProbeFailureException($previous); }); } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; var_dump($e === $previous); } @@ -143,21 +202,22 @@ try { throw new ProbeFailureException('uncached failure'); }); } catch (ProbeFailureException $e) { - var_dump($e->getMessage()); + echo $e::class, ': ', $e->getMessage(), "\n"; } ?> --EXPECT-- -shared failure -shared failure -concurrent failure -concurrent failure -string(9) "failure 1" -string(9) "failure 1" -string(9) "failure 2" +ProbeFailureException: shared failure +ProbeFailureException: shared failure +ProbeFailureException: concurrent failure +ProbeFailureException: concurrent failure +ProbeFailureException: failure 1 +ProbeFailureException: failure 1 +ProbeFailureException: failure 2 int(2) string(9) "success 1" string(9) "success 2" int(2) string(16) "uncached success" +Exception: original failure bool(true) -string(16) "uncached failure" +ProbeFailureException: uncached failure From c877c9aa9678a42d126d13ce0a114bb1ee5318c6 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Thu, 6 Aug 2026 01:24:31 +0700 Subject: [PATCH 10/10] review: cleaned up exceptions --- ext/pdo/tests/pdo_test.inc | 20 ++++++++++++-------- ext/pdo_mysql/tests/inc/mysql_pdo_test.inc | 20 ++++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index fa10cc5fd5bb..817eaa7bf798 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -39,19 +39,23 @@ class PDOTest { $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attributes]; - return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attributes): PDO { - try { - return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); - } catch (PDOException $e) { - throw new ProbeFailureException($e); - } - }); + try { + return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attributes): PDO { + try { + return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attributes); + } catch (PDOException $e) { + throw new ProbeFailureException($e); + } + }); + } catch (ProbeFailureException $e) { + throw new PDOException($e->getMessage()); + } } static function skip() { try { self::factory(); - } catch (ProbeFailureException $e) { + } catch (PDOException $e) { die("skip " . $e->getMessage()); } } diff --git a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc index 8aabe47ef855..d840e66b6ff5 100644 --- a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc +++ b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc @@ -22,13 +22,17 @@ class MySQLPDOTest extends PDOTest { $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attr]; - return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attr): PDO { - try { - return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); - } catch (PDOException $e) { - throw new ProbeFailureException($e); - } - }); + try { + return ProbeCache::getFailure('pdo', $configuration, static function () use ($classname, $useConnectMethod, $dsn, $user, $pass, $attr): PDO { + try { + return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); + } catch (PDOException $e) { + throw new ProbeFailureException($e); + } + }); + } catch (ProbeFailureException $e) { + throw new PDOException($e->getMessage()); + } } static function factoryWithAttr($attr) { @@ -164,7 +168,7 @@ class MySQLPDOTest extends PDOTest { static function skip() { try { self::factory(); - } catch (ProbeFailureException $e) { + } catch (PDOException $e) { die('skip could not connect'); } }