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..09d333ba290b --- /dev/null +++ b/ext/pdo/tests/pdo_test_skip_cache.phpt @@ -0,0 +1,74 @@ +--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 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-- + @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..9118d22b1fbf --- /dev/null +++ b/ext/snmp/tests/snmp_skip_cache.phpt @@ -0,0 +1,74 @@ +--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 998e7e24c337..2703c0f4209d 100755 --- a/run-tests.php +++ b/run-tests.php @@ -232,6 +232,20 @@ 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); + } + }); + } + if (IS_WINDOWS && empty($environment["SystemRoot"])) { $environment["SystemRoot"] = getenv("SystemRoot"); } @@ -1065,13 +1079,33 @@ 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 +{ + $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 $directory; + } + } + + return null; +} + function rmdir_recursive($dir) { - if (!file_exists($dir)) { + if (!file_exists($dir) && !is_link($dir)) { return; } - if (!is_dir($dir)) { - unlink($dir); + if (is_link($dir) || !is_dir($dir)) { + @unlink($dir); return; }