From 902f7d0fec8b493769af236a35143a3a83d95046 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 1/4] 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 2/4] 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 6e04a2283aea1b4fa1d8a067c2b9583d231e11e2 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Thu, 30 Jul 2026 22:52:02 +0700 Subject: [PATCH 3/4] 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 300a315faa6894e583074934dcf9e74f396ebf61 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sun, 2 Aug 2026 03:31:33 +0700 Subject: [PATCH 4/4] 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 2703c0f4209d..7951ec7ae673 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"); @@ -1079,24 +1067,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) @@ -3662,6 +3685,7 @@ public function checkSkip(string $php, string $code, string $checkFile, string $ } save_text($checkFile, $code, $tempFile); + $env['TEST_PHP_EVALUATING_SKIPIF'] = '1'; $result = trim(system_with_timeout("$php \"$checkFile\"", $env)); if (strpos($result, 'nocache') === 0) { $result = ''; 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)