Commit 5d588761922 for php.net
commit 5d5887619228914abe843657be56ddfc98528cfc
Author: NickSdot <32384907+NickSdot@users.noreply.github.com>
Date: Thu Aug 6 16:42:56 2026 +0700
tests: Cache failed service probes (#22944)
Prevents unavailable optional test services from slowing the suite through repeated connection timeouts. PDO and SNMP availability probes cache the first failure for the duration of one test run, so later SKIPIF checks return immediately.
Only failures are cached; successful probes and test connections are unchanged. PDOs one second timeout applies only to the default availability probe. The caches are private, run-scoped and removed afterwards. A service becoming available mid run is detected on the next test-suite run.
diff --git a/ext/ldap/tests/skipifbindfailure.inc b/ext/ldap/tests/skipifbindfailure.inc
index 81c7998cfbb..0b421ef1bd7 100644
--- a/ext/ldap/tests/skipifbindfailure.inc
+++ b/ext/ldap/tests/skipifbindfailure.inc
@@ -1,14 +1,23 @@
<?php
require_once 'connect.inc';
+require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc';
if ($skip_on_bind_failure) {
+ $configuration = [$uri, $user, $passwd, $protocol_version];
+
+ 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)));
+ }
- $link = ldap_connect($uri);
- ldap_set_option($link, LDAP_OPT_PROTOCOL_VERSION, $protocol_version);
- if (!@ldap_bind($link, $user, $passwd))
- die(sprintf("skip Can't bind to LDAP Server - [%d] %s", ldap_errno($link), ldap_error($link)));
-
- ldap_unbind($link);
+ ldap_unbind($link);
+ });
+ } catch (ProbeFailureException $e) {
+ die("skip {$e->getMessage()}");
+ }
}
if (isset($require_vendor)) {
diff --git a/ext/mysqli/tests/skipifconnectfailure.inc b/ext/mysqli/tests/skipifconnectfailure.inc
index 986f646f8bb..f5cd763af14 100644
--- a/ext/mysqli/tests/skipifconnectfailure.inc
+++ b/ext/mysqli/tests/skipifconnectfailure.inc
@@ -1,7 +1,19 @@
<?php
require_once 'connect.inc';
-$link = @my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
-if (!is_object($link))
- die(sprintf("skip Can't connect to MySQL Server - [%d] %s", mysqli_connect_errno(), mysqli_connect_error()));
-mysqli_close($link);
+require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc';
+
+$configuration = [$host, $port, $user, $passwd, $db, $socket, get_environment_connection_flags()];
+
+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);
+ });
+} 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 c9ab401e756..d32697aebf8 100644
--- a/ext/mysqli/tests/test_setup/test_helpers.inc
+++ b/ext/mysqli/tests/test_setup/test_helpers.inc
@@ -1,5 +1,7 @@
<?php
+require_once dirname(__DIR__, 4) . '/tests/probe_cache.inc';
+
function get_default_host(): string {
static $host = null;
if ($host === null) {
@@ -110,11 +112,31 @@ function default_mysqli_connect(): \mysqli{
function mysqli_check_skip_test(): void {
mysqli_connect_or_skip();
}
-function mysqli_connect_or_skip() {
+
+function mysqli_connect_or_skip(): mysqli {
+ $configuration = [
+ get_default_host(),
+ get_default_port(),
+ get_default_user(),
+ get_default_password(),
+ get_default_database(),
+ get_default_socket(),
+ get_environment_connection_flags(),
+ ];
+
try {
- return default_mysqli_connect();
- } catch (\mysqli_sql_exception) {
- die(sprintf("skip Can't connect to MySQL Server - [%d] %s", mysqli_connect_errno(), mysqli_connect_error()));
+ 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()}");
}
}
function have_innodb(mysqli $link): bool {
@@ -123,11 +145,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 9785f5843a7..a602debe227 100644
--- a/ext/odbc/tests/skipif.inc
+++ b/ext/odbc/tests/skipif.inc
@@ -1,8 +1,17 @@
<?php
include 'config.inc';
+require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc';
-$conn = @odbc_connect($dsn, $user, $pass);
-if (!$conn) {
- die('skip 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 $conn;
+ });
+} catch (ProbeFailureException $e) {
+ die("skip {$e->getMessage()}");
}
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 dfef3bb4839..bd510b6ac63 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 @@ public function fetchAll($fetch_style = 1, ...$fetch_args): array {
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_attr_statement_class_basic");
?>
---EXPECT--
+--EXPECTF--
array(1) {
[0]=>
string(12) "PDOStatement"
@@ -89,7 +89,7 @@ public function fetchAll($fetch_style = 1, ...$fetch_args): array {
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 @@ public function fetchAll($fetch_style = 1, ...$fetch_args): array {
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 301a8835eee..aebf969b583 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 @@ function __construct($dsn, $username, $password, $driver_options = []) {
$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 61cfe56f779..672085441c9 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 @@ private function __construct(public PDO $v) {
$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 86d2cfcefd5..bc825d4f852 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 @@ function __construct($dsn, $username, $password, $driver_options = []) {
$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 6bb350f0c13..969ec946213 100644
--- a/ext/pdo/tests/pdo_027.phpt
+++ b/ext/pdo/tests/pdo_027.phpt
@@ -36,13 +36,13 @@
$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 @@
}
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 cec5e9f6d6c..7d953851304 100644
--- a/ext/pdo/tests/pdo_query_fetch_lazy001.phpt
+++ b/ext/pdo/tests/pdo_query_fetch_lazy001.phpt
@@ -29,8 +29,8 @@
$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/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc
index b44d0b88e77..817eaa7bf79 100644
--- a/ext/pdo/tests/pdo_test.inc
+++ b/ext/pdo/tests/pdo_test.inc
@@ -1,6 +1,8 @@
<?php
# PDO test framework utilities
+require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc';
+
if (getenv('PDOTEST_DSN') === false) {
$common = '';
$append = false;
@@ -18,41 +20,41 @@ if (getenv('PDOTEST_DSN') === false) {
class PDOTest {
// 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 (getenv('TEST_PHP_EVALUATING_SKIPIF') === '1') {
+ $skipAttributes = self::getAttributes('PDOTEST_SKIP_ATTR');
+ if ($skipAttributes !== null) {
+ $attributes = $skipAttributes + ($attributes ?? []);
+ }
+ }
}
if ($user === false) $user = NULL;
if ($pass === false) $pass = NULL;
- if ($useConnectMethod) {
- $db = $classname::connect($dsn, $user, $pass, $attr);
- } else {
- $db = new $classname($dsn, $user, $pass, $attr);
- }
+ $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attributes];
- if (!$db) {
- die("Could not create PDO object (DSN=$dsn, user=$user)\n");
+ 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());
}
- // 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;
}
static function skip() {
try {
- $db = PDOTest::factory();
+ self::factory();
} catch (PDOException $e) {
die("skip " . $e->getMessage());
}
@@ -95,6 +97,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 293597b623e..f3a963ef9b6 100644
--- a/ext/pdo_dblib/tests/common.phpt
+++ b/ext/pdo_dblib/tests/common.phpt
@@ -5,7 +5,7 @@
--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 @@
],
'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 1612a80a933..78bf0804890 100644
--- a/ext/pdo_dblib/tests/config.inc
+++ b/ext/pdo_dblib/tests/config.inc
@@ -1,5 +1,7 @@
<?php
+require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc';
+
// bug #72969 reflects a bug with FreeTDS, not with pdo_dblib
// this function will version detect so the relevant tests can XFAILIF
// assume this bug isn't present if not using FreeTDS
@@ -41,18 +43,28 @@ function setAttributes(PDO $db) {
}
function getDbConnection(string $class = PDO::class, ?array $attributes = null) {
+ $connectionAttributes = $attributes;
+ $evaluatingSkipif = getenv('TEST_PHP_EVALUATING_SKIPIF') === '1';
+ if ($evaluatingSkipif && $connectionAttributes === null && getenv('PDO_DBLIB_TEST_DSN') === false) {
+ $connectionAttributes = [Pdo\Dblib::ATTR_CONNECTION_TIMEOUT => 1];
+ }
[$dsn, $user, $pass] = getCredentials();
try {
- $db = new $class($dsn, $user, $pass, $attributes);
- if ($attributes === null) {
- setAttributes($db);
- }
- } catch (PDOException $e) {
+ 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);
+ }
+ });
+ } 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 75f299ff9e1..d840e66b6ff 100644
--- a/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc
+++ b/ext/pdo_mysql/tests/inc/mysql_pdo_test.inc
@@ -20,20 +20,19 @@ 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);
- }
+ $configuration = [$classname, $useConnectMethod, $dsn, $user, $pass, $attr];
- if (!$db) {
- die("Could not create PDO object (DSN=$dsn, user=$user)\n");
+ 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());
}
-
- $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
- $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
-
- return $db;
}
static function factoryWithAttr($attr) {
@@ -168,7 +167,7 @@ class MySQLPDOTest extends PDOTest {
static function skip() {
try {
- $db = self::factory();
+ self::factory();
} catch (PDOException $e) {
die('skip could not connect');
}
@@ -212,5 +211,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/pdo_pgsql/tests/bug75402.phpt b/ext/pdo_pgsql/tests/bug75402.phpt
index be0fbd3f0be..909c315f800 100644
--- a/ext/pdo_pgsql/tests/bug75402.phpt
+++ b/ext/pdo_pgsql/tests/bug75402.phpt
@@ -88,12 +88,12 @@
$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"]=>
diff --git a/ext/pgsql/tests/inc/skipif.inc b/ext/pgsql/tests/inc/skipif.inc
index 2ce5f46e778..03a00b0cde7 100644
--- a/ext/pgsql/tests/inc/skipif.inc
+++ b/ext/pgsql/tests/inc/skipif.inc
@@ -8,15 +8,24 @@
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");
+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');
+ }
+
+ 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 0ae4ee16e5b..283cfaa5357 100644
--- a/ext/snmp/tests/skipif.inc
+++ b/ext/snmp/tests/skipif.inc
@@ -1,10 +1,18 @@
<?php
require_once (dirname(__FILE__).'/snmp_include.inc');
+require_once dirname(__DIR__, 3) . '/tests/probe_cache.inc';
//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');
+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/run-tests.php b/run-tests.php
index 89b5882f667..6eed6649aec 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");
}
@@ -1073,13 +1075,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;
}
@@ -3777,6 +3834,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 00000000000..01ba1bec556
--- /dev/null
+++ b/tests/probe_cache.inc
@@ -0,0 +1,73 @@
+<?php
+
+final class ProbeFailureException extends Exception
+{
+ public function __construct(string|Throwable $failure, ?Throwable $previous = null)
+ {
+ if ($failure instanceof Throwable) {
+ $previous = $failure;
+ $failure = $failure->getMessage();
+ }
+
+ parent::__construct($failure, 0, $previous);
+ }
+}
+
+final class ProbeCache
+{
+ private const FAILURE_PREFIX = 'failure:';
+
+ /**
+ * 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): 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();
+ }
+
+ $cacheFile = $directory
+ . DIRECTORY_SEPARATOR
+ . 'probe-'
+ . hash('sha256', serialize([$namespace, $configuration]));
+
+ $cache = @fopen($cacheFile, 'c+');
+
+ if ($cache === false || !flock($cache, LOCK_EX)) {
+ if ($cache !== false) {
+ fclose($cache);
+ }
+ return $probe();
+ }
+
+ try {
+ $cached = stream_get_contents($cache);
+ if (is_string($cached) && str_starts_with($cached, self::FAILURE_PREFIX)) {
+ throw new ProbeFailureException(substr($cached, strlen(self::FAILURE_PREFIX)));
+ }
+
+ try {
+ return $probe();
+ } catch (ProbeFailureException $e) {
+ rewind($cache);
+ ftruncate($cache, 0);
+ fwrite($cache, self::FAILURE_PREFIX . $e->getMessage());
+ fflush($cache);
+ throw $e;
+ }
+ } 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
new file mode 100644
index 00000000000..6bc94ba00a9
--- /dev/null
+++ b/tests/run-test/test_probe_cache.phpt
@@ -0,0 +1,223 @@
+--TEST--
+Shared test probe cache caches failures across processes
+--FILE--
+<?php
+require dirname(__DIR__) . '/probe_cache.inc';
+
+function start_probe_cache_process(string $code, array $environment): array
+{
+ $command = getenv('TEST_PHP_EXECUTABLE_ESCAPED')
+ . ' -n -r '
+ . escapeshellarg($code);
+ $process = proc_open(
+ $command,
+ [
+ 1 => ['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();
+$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(
+ <<<PHP
+ require $helper;
+
+ try {
+ ProbeCache::getFailure(
+ $namespaceCode,
+ ['shared'],
+ static function (): never {
+ throw new ProbeFailureException('shared failure');
+ },
+ );
+ } catch (ProbeFailureException \$e) {
+ echo \$e::class, ': ', \$e->getMessage();
+ }
+ PHP,
+ $environment,
+);
+$second = run_probe_cache_process(
+ <<<PHP
+ require $helper;
+
+ try {
+ ProbeCache::getFailure(
+ $namespaceCode,
+ ['shared'],
+ static function (): never {
+ throw new Exception('Probe should not run');
+ },
+ );
+ } catch (ProbeFailureException \$e) {
+ echo \$e::class, ': ', \$e->getMessage();
+ }
+ PHP,
+ $environment,
+);
+echo "$first\n$second\n";
+
+$probeStarted = $cacheDirectory . '/probe_started';
+$probeStartedCode = var_export($probeStarted, true);
+@unlink($probeStarted);
+[$firstProcess, $firstPipes] = start_probe_cache_process(
+ <<<PHP
+ 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::class, ': ', \$e->getMessage();
+ }
+ PHP,
+ $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(
+ <<<PHP
+ require $helper;
+
+ try {
+ ProbeCache::getFailure(
+ $namespaceCode,
+ ['concurrent'],
+ static function (): never {
+ throw new Exception('Concurrent probe should not run');
+ },
+ );
+ } catch (ProbeFailureException \$e) {
+ echo \$e::class, ': ', \$e->getMessage();
+ }
+ PHP,
+ $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");
+putenv('TEST_PHP_EVALUATING_SKIPIF=1');
+
+$failureCalls = 0;
+$failureProbe = static function () use (&$failureCalls): never {
+ $failureCalls++;
+ throw new ProbeFailureException("failure $failureCalls");
+};
+
+try {
+ ProbeCache::getFailure($namespace, ['first'], $failureProbe);
+} catch (ProbeFailureException $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+try {
+ ProbeCache::getFailure($namespace, ['first'], $failureProbe);
+} catch (ProbeFailureException $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+try {
+ ProbeCache::getFailure($namespace, ['second'], $failureProbe);
+} catch (ProbeFailureException $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+var_dump($failureCalls);
+
+$successCalls = 0;
+$successProbe = static function () use (&$successCalls): string {
+ $successCalls++;
+ 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) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+ var_dump($e === $previous);
+}
+
+putenv('TEST_PHP_EVALUATING_SKIPIF=1');
+putenv('TEST_PHP_SHARED_CACHE_DIR');
+try {
+ ProbeCache::getFailure($namespace, ['uncached'], static function (): never {
+ throw new ProbeFailureException('uncached failure');
+ });
+} catch (ProbeFailureException $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+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)
+ProbeFailureException: 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 00000000000..8ccb69b0e50
--- /dev/null
+++ b/tests/run-test/test_skipif_environment.phpt
@@ -0,0 +1,14 @@
+--TEST--
+SKIPIF evaluation environment
+--SKIPIF--
+<?php
+if (getenv('TEST_PHP_EVALUATING_SKIPIF') !== '1') {
+ echo 'missing SKIPIF environment marker';
+}
+?>
+--FILE--
+<?php
+var_dump(getenv('TEST_PHP_EVALUATING_SKIPIF'));
+?>
+--EXPECT--
+bool(false)