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 @@ getMessage(); + } + }); + + if (is_string($reason)) { + throw $exception ?? new PDOException($reason); } - // 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; + return $database; } static function skip() { try { - $db = PDOTest::factory(); + self::factory(); } catch (PDOException $e) { die("skip " . $e->getMessage()); } @@ -95,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_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..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; diff --git a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc index 75f299ff9e19..4e0c705f8e44 100644 --- a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc +++ b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc @@ -20,20 +20,29 @@ class MySQLPDOTest extends PDOTest { $attr = is_string($attr) && strlen($attr) ? unserialize($attr) : null; } - if ($useConnectMethod) { - $db = $classname::connect($dsn, $user, $pass, $attr); - } else { - $db = new $classname($dsn, $user, $pass, $attr); + if (getenv('TEST_PHP_EVALUATING_SKIPIF') !== '1') { + return self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); } - if (!$db) { - die("Could not create PDO object (DSN=$dsn, user=$user)\n"); - } + $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 { + try { + $database = self::createConnection($classname, $useConnectMethod, $dsn, $user, $pass, $attr); + return null; + } catch (PDOException $e) { + $exception = $e; + return $e->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 0ae4ee16e5b8..43cc75f36b73 100644 --- a/ext/snmp/tests/skipif.inc +++ b/ext/snmp/tests/skipif.inc @@ -1,10 +1,27 @@ + @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 ]] ) -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/run-tests.php b/run-tests.php index 998e7e24c337..7951ec7ae673 100755 --- a/run-tests.php +++ b/run-tests.php @@ -232,6 +232,8 @@ function main(): void } } + SharedProbeCache::setUp($environment); + if (IS_WINDOWS && empty($environment["SystemRoot"])) { $environment["SystemRoot"] = getenv("SystemRoot"); } @@ -1065,13 +1067,68 @@ function get_file_cache_dir(): string return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-run-tests-file-cache'; } +final class SharedProbeCache +{ + 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(); + }); + } + + 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; + } + + private function cleanup(): void + { + if (is_dir($this->directory)) { + rmdir_recursive($this->directory); + } + } +} + 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; } @@ -3628,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)