$collectionName]); try { /* Unless we are dropping a collection within the "local" database, * which does not support a write concern, we need to use w:majority due * to the issue explained in SERVER-35613: "drop" uses a two phase * commit, and due to that, it is possible that a lock can't be acquired * for a transaction that gets quickly started as the "drop" reaper * hasn't completed yet. */ $wc = $databaseName === 'local' ? new WriteConcern(1) : new WriteConcern(WriteConcern::MAJORITY); $server->executeCommand( $databaseName, $command, ['writeConcern' => $wc] ); } catch (RuntimeException $e) { if ($e->getMessage() !== 'ns not found') { throw $e; } } } /** * Returns the value of a module row from phpinfo(), or null if it's not found. * * @param string $row * @return string|null */ function get_module_info($row) { ob_start(); phpinfo(INFO_MODULES); $info = ob_get_clean(); $pattern = sprintf('/^%s(.*)$/m', preg_quote($row . ' => ')); if (preg_match($pattern, $info, $matches) !== 1) { return null; } return $matches[1]; } function create_test_manager(string $uri = null, array $options = [], array $driverOptions = []) { if (getenv('API_VERSION') && ! isset($driverOptions['serverApi'])) { $driverOptions['serverApi'] = new ServerApi(getenv('API_VERSION')); } if (getenv('CRYPT_SHARED_LIB_PATH') && isset($driverOptions['autoEncryption'])) { if (is_array($driverOptions['autoEncryption']) && (! isset($driverOptions['autoEncryption']['extraOptions']) || is_array($driverOptions['autoEncryption']['extraOptions'])) && ! isset($driverOptions['autoEncryption']['extraOptions']['cryptSharedLibPath'])) { $driverOptions['autoEncryption']['extraOptions']['cryptSharedLibPath'] = getenv('CRYPT_SHARED_LIB_PATH'); } } return new Manager($uri ?? URI, $options, $driverOptions); } /** * Returns the primary server. * * @param string $uri Connection string * @return Server * @throws ConnectionException */ function get_primary_server($uri) { return create_test_manager($uri)->selectServer(new ReadPreference('primary')); } /** * Returns a secondary server. * * @param string $uri Connection string * @return Server * @throws ConnectionException */ function get_secondary_server($uri) { return create_test_manager($uri)->selectServer(new ReadPreference('secondary')); } /** * Runs a command and returns whether an exception was thrown or not * * @param string $uri Connection string * @param array|object $commandSpec * @return bool * @throws RuntimeException */ function command_works($uri, $commandSpec) { $command = new Command($commandSpec); $server = get_primary_server($uri); try { $cursor = $server->executeCommand('admin', $command); return true; } catch (Exception $e) { return false; } } /** * Returns a parameter of the primary server. * * @param string $uri Connection string * @return mixed * @throws RuntimeException */ function get_server_parameter($uri, $parameter) { $server = get_primary_server($uri); $command = new Command(['getParameter' => 1, $parameter => 1]); $cursor = $server->executeCommand('admin', $command); return current($cursor->toArray())->$parameter; } /** * Returns the storage engine of the primary server. * * @param string $uri Connection string * @return string * @throws RuntimeException */ function get_server_storage_engine($uri) { $server = get_primary_server($uri); $command = new Command(['serverStatus' => 1]); $cursor = $server->executeCommand('admin', $command); return current($cursor->toArray())->storageEngine->name; } /** * Helper to return the version of a specific server. * * @param Server $server * @return string * @throws RuntimeException */ function get_server_version_from_server(Server $server) { $command = new Command(['buildInfo' => 1]); $cursor = $server->executeCommand('admin', $command); return current($cursor->toArray())->version; } /** * Returns the version of the primary server. * * @param string $uri Connection string * @return string * @throws RuntimeException */ function get_server_version($uri) { $server = get_primary_server($uri); return get_server_version_from_server($server); } /** * Returns the value of a URI option, or null if it's not found. * * @param string $uri * @return string|null */ function get_uri_option($uri, $option) { $pattern = sprintf('/[?&]%s=([^&]+)/i', preg_quote($option)); if (preg_match($pattern, $uri, $matches) !== 1) { return null; } return $matches[1]; } /** * Checks that the topology is load balanced. * * @param string $uri * @return boolean */ function is_load_balanced($uri) { return get_primary_server($uri)->getType() === Server::TYPE_LOAD_BALANCER; } /** * Checks that the topology is a sharded cluster. * * @param string $uri * @return boolean */ function is_mongos($uri) { return get_primary_server($uri)->getType() === Server::TYPE_MONGOS; } /** * Checks that the topology is a sharded cluster using a replica set. * * Note: only the first shard is checked. */ function is_sharded_cluster_with_replica_set($uri) { $server = get_primary_server($uri); if ($server->getType() !== Server::TYPE_MONGOS && $server->getType() !== Server::TYPE_LOAD_BALANCER) { return false; } $cursor = $server->executeQuery('config.shards', new \MongoDB\Driver\Query([], ['limit' => 1])); $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); $document = current($cursor->toArray()); if (! $document) { return false; } /** * Use regular expression to distinguish between standalone or replicaset: * Without a replicaset: "host" : "localhost:4100" * With a replicaset: "host" : "dec6d8a7-9bc1-4c0e-960c-615f860b956f/localhost:4400,localhost:4401" */ return preg_match('@^.*/.*:\d+@', $document['host']); } /** * Checks that the topology is a replica set. * * @param string $uri * @return boolean */ function is_replica_set($uri) { if (get_primary_server($uri)->getType() !== Server::TYPE_RS_PRIMARY) { return false; } /* Note: this may return a false negative if replicaSet is specified through * a TXT record for a mongodb+srv connection string. */ if (get_uri_option($uri, 'replicaSet') === NULL) { return false; } return true; } /** * Checks if the connection string uses authentication. * * @param string $uri * @return boolean */ function is_auth($uri) { if (stripos($uri, 'authmechanism=') !== false) { return true; } if (strpos($uri, ':') !== false && strpos($uri, '@') !== false) { return true; } return false; } /** * Checks if the connection string uses SSL. * * @param string $uri * @return boolean */ function is_ssl($uri) { return stripos($uri, 'ssl=true') !== false || stripos($uri, 'tls=true') !== false; } /** * Checks that the topology is a standalone. * * @param string $uri * @return boolean */ function is_standalone($uri) { return get_primary_server($uri)->getType() === Server::TYPE_STANDALONE; } /** * Converts the server type constant to a string. * * @see http://php.net/manual/en/class.mongodb-driver-server.php * @param integer $type * @return string */ function server_type_as_string($type) { switch ($type) { case Server::TYPE_STANDALONE: return 'Standalone'; case Server::TYPE_MONGOS: return 'Mongos'; case Server::TYPE_POSSIBLE_PRIMARY: return 'PossiblePrimary'; case Server::TYPE_RS_PRIMARY: return 'RSPrimary'; case Server::TYPE_RS_SECONDARY: return 'RSSecondary'; case Server::TYPE_RS_ARBITER: return 'RSArbiter'; case Server::TYPE_RS_OTHER: return 'RSOther'; case Server::TYPE_RS_GHOST: return 'RSGhost'; default: return 'Unknown'; } } /** * Converts an errno number to a string. * * @see http://php.net/manual/en/errorfunc.constants.php * @param integer $errno * @param string */ function errno_as_string($errno) { $errors = [ 'E_ERROR', 'E_WARNING', 'E_PARSE', 'E_NOTICE', 'E_CORE_ERROR', 'E_CORE_WARNING', 'E_COMPILE_ERROR', 'E_COMPILE_WARNING', 'E_USER_ERROR', 'E_USER_WARNING', 'E_USER_NOTICE', 'E_STRICT', 'E_RECOVERABLE_ERROR', 'E_DEPRECATED', 'E_USER_DEPRECATED', 'E_ALL', ]; foreach ($errors as $error) { if ($errno === constant($error)) { return $error; } } return 'Unknown'; } /** * Prints a traditional hex dump of byte values and printable characters. * * @see http://stackoverflow.com/a/4225813/162228 * @param string $data Binary data * @param integer $width Bytes displayed per line */ function hex_dump($data, $width = 16) { static $pad = '.'; // Placeholder for non-printable characters static $from = ''; static $to = ''; if ($from === '') { for ($i = 0; $i <= 0xFF; $i++) { $from .= chr($i); $to .= ($i >= 0x20 && $i <= 0x7E) ? chr($i) : $pad; } } $hex = str_split(bin2hex($data), $width * 2); $chars = str_split(strtr($data, $from, $to), $width); $offset = 0; $length = $width * 3; foreach ($hex as $i => $line) { printf("%6X : %-{$length}s [%s]\n", $offset, implode(' ', str_split($line, 2)), $chars[$i]); $offset += $width; } } /** * Canonicalizes a JSON string. * * @param string $json * @return string */ function json_canonicalize($json) { $json = json_encode(json_decode($json)); /* Canonicalize string values for $numberDouble to ensure they are converted * the same as number literals in legacy and relaxed output. This is needed * because the printf format in _bson_as_json_visit_double uses a high level * of precision and may not produce the exponent notation expected by the * BSON corpus tests. */ $json = preg_replace_callback( '/{"\$numberDouble":"(-?\d+(\.\d+([eE]\+\d+)?)?)"}/', function ($matches) { return '{"$numberDouble":"' . json_encode(json_decode($matches[1])) . '"}'; }, $json ); return $json; } /** * Return a collection name to use for the test file. * * The filename will be stripped of the base path to the test suite (prefix) as * well as the PHP file extension (suffix). Special characters (including hyphen * for shell compatibility) will be replaced with underscores. * * @param string $filename * @return string */ function makeCollectionNameFromFilename($filename) { $filename = realpath($filename); $prefix = realpath(dirname(__FILE__) . '/..') . DIRECTORY_SEPARATOR; $replacements = array( // Strip test path prefix sprintf('/^%s/', preg_quote($prefix, '/')) => '', // Strip file extension suffix '/\.php$/' => '', // SKIPIFs add ".skip" between base name and extension '/\.skip$/' => '', // Replace special characters with underscores sprintf('/[%s]/', preg_quote('-$/\\', '/')) => '_', ); return preg_replace(array_keys($replacements), array_values($replacements), $filename); } function NEEDS($configuration) { if (!constant($configuration)) { exit("skip -- need '$configuration' defined"); } } function SLOW() { if (getenv("SKIP_SLOW_TESTS")) { exit("skip SKIP_SLOW_TESTS"); } } function loadFixtures(Manager $manager, $dbname = DATABASE_NAME, $collname = COLLECTION_NAME, $filename = null) { if (!$filename) { $filename = "compress.zlib://" . __DIR__ . "/" . "PHONGO-FIXTURES.json.gz"; } $bulk = new BulkWrite(['ordered' => false]); $server = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); $data = file_get_contents($filename); $array = json_decode($data); foreach($array as $document) { $bulk->insert($document); } $retval = $server->executeBulkWrite("$dbname.$collname", $bulk); if ($retval->getInsertedCount() !== count($array)) { exit(sprintf('skip Fixtures were not loaded (expected: %d, actual: %d)', $total, $retval->getInsertedCount())); } } function createTemporaryMongoInstance(array $options = []) { $id = 'mo_' . COLLECTION_NAME; $options += [ "name" => "mongod", "id" => $id, 'procParams' => [ 'logpath' => "/tmp/MO/phongo/{$id}.log", 'ipv6' => true, 'setParameter' => [ 'enableTestCommands' => 1 ], ], ]; $opts = array( "http" => array( "timeout" => 60, "method" => "PUT", "header" => "Accept: application/json\r\n" . "Content-type: application/x-www-form-urlencoded", "content" => json_encode($options), "ignore_errors" => true, ), ); $ctx = stream_context_create($opts); $json = file_get_contents(MONGO_ORCHESTRATION_URI . "/servers/$id", false, $ctx); $result = json_decode($json, true); /* Failed -- or was already started */ if (!isset($result["mongodb_uri"])) { destroyTemporaryMongoInstance($id); throw new Exception("Could not start temporary server instance\n"); } else { return $result['mongodb_uri']; } } function destroyTemporaryMongoInstance($id = NULL) { if ($id == NULL) { $id = 'mo_' . COLLECTION_NAME; } $opts = array( "http" => array( "timeout" => 60, "method" => "DELETE", "header" => "Accept: application/json\r\n", "ignore_errors" => true, ), ); $ctx = stream_context_create($opts); $json = file_get_contents(MONGO_ORCHESTRATION_URI . "/servers/$id", false, $ctx); } /** * Converts an error level (constant or bitmask) to a string description. */ function severityToString(int $severity): string { static $constants = [ 'E_ERROR' => E_ERROR, 'E_WARNING' => E_WARNING, 'E_PARSE' => E_PARSE, 'E_NOTICE' => E_NOTICE, 'E_CORE_ERROR' => E_CORE_ERROR, 'E_CORE_WARNING' => E_CORE_WARNING, 'E_COMPILE_ERROR' => E_COMPILE_ERROR, 'E_COMPILE_WARNING' => E_COMPILE_WARNING, 'E_USER_ERROR' => E_USER_ERROR, 'E_USER_WARNING' => E_USER_WARNING, 'E_USER_NOTICE' => E_USER_NOTICE, 'E_STRICT' => E_STRICT, 'E_RECOVERABLE_ERROR' => E_RECOVERABLE_ERROR, 'E_DEPRECATED' => E_DEPRECATED, 'E_USER_DEPRECATED' => E_USER_DEPRECATED, // E_ALL is handled separately ]; if ($severity === E_ALL) { return 'E_ALL'; } foreach ($constants as $constant => $value) { if ($severity & $value) { $matches[] = $constant; } } return empty($matches) ? 'UNKNOWN' : implode('|', $matches); } /** * Expects the callable to raise an error matching the expected severity, which * may be a constant or bitmask. May optionally expect the error to be raised * from a particular function. Returns the message from the raised error or * exception, or an empty string if neither was thrown. */ function raises(callable $callable, int $expectedSeverity, string $expectedFromFunction = null): string { set_error_handler(function(int $severity, string $message, string $file, int $line) { throw new ErrorException($message, 0, $severity, $file, $line); }); try { call_user_func($callable); } catch (ErrorException $e) { if (!($e->getSeverity() & $expectedSeverity)) { printf("ALMOST: Got %s - expected %s\n", severityToString($e->getSeverity()), severityToString($expectedSeverity)); return $e->getMessage(); } if ($expectedFromFunction === null) { printf("OK: Got %s\n", severityToString($e->getSeverity())); return $e->getMessage(); } $fromFunction = $e->getTrace()[0]['function']; if (strcasecmp($fromFunction, $expectedFromFunction) !== 0) { printf("ALMOST: Got %s - but was raised from %s, not %s\n", errorLevelToString($e->getSeverity()), $fromFunction, $expectedFromFunction); return $e->getMessage(); } printf("OK: Got %s raised from %s\n", severityToString($e->getSeverity()), $fromFunction); return $e->getMessage(); } catch (Throwable $e) { printf("ALMOST: Got %s - expected %s\n", get_class($e), ErrorException::class); return $e->getMessage(); } finally { restore_error_handler(); } printf("FAILED: Expected %s, but no error raised!\n", ErrorException::class); return ''; } /** * Expects the callable to throw an expected exception. May optionally expect * the exception to be thrown from a particular function. Returns the message * from the thrown exception, or an empty string if one was not thrown. */ function throws(callable $callable, string $expectedException, string $expectedFromFunction = null): string { try { call_user_func($callable); } catch (Throwable $e) { if (!($e instanceof $expectedException)) { printf("ALMOST: Got %s - expected %s\n", get_class($e), $expectedException); return $e->getMessage(); } if ($expectedFromFunction === null) { printf("OK: Got %s\n", $expectedException); return $e->getMessage(); } $fromFunction = $e->getTrace()[0]['function']; if (strcasecmp($fromFunction, $expectedFromFunction) !== 0) { printf("ALMOST: Got %s - but was thrown from %s, not %s\n", $expectedException, $fromFunction, $expectedFromFunction); return $e->getMessage(); } printf("OK: Got %s thrown from %s\n", $expectedException, $fromFunction); return $e->getMessage(); } printf("FAILED: Expected %s, but no exception thrown!\n", $expectedException); return ''; } function printServer(Server $server) { printf("server: %s:%d\n", $server->getHost(), $server->getPort()); } function printWriteResult(WriteResult $result, $details = true) { printServer($result->getServer()); printf("insertedCount: %d\n", $result->getInsertedCount()); printf("matchedCount: %d\n", $result->getMatchedCount()); printf("modifiedCount: %d\n", $result->getModifiedCount()); printf("upsertedCount: %d\n", $result->getUpsertedCount()); printf("deletedCount: %d\n", $result->getDeletedCount()); foreach ($result->getUpsertedIds() as $index => $id) { printf("upsertedId[%d]: ", $index); var_dump($id); } $writeConcernError = $result->getWriteConcernError(); printWriteConcernError($writeConcernError ? $writeConcernError : null, $details); foreach ($result->getWriteErrors() as $writeError) { printWriteError($writeError); } } function printWriteConcernError(WriteConcernError $error = null, $details) { if ($error) { /* This stuff is generated by the server, no need for us to test it */ if (!$details) { printf("writeConcernError: %s (%d)\n", $error->getMessage(), $error->getCode()); return; } var_dump($error); printf("writeConcernError.message: %s\n", $error->getMessage()); printf("writeConcernError.code: %d\n", $error->getCode()); printf("writeConcernError.info: "); var_dump($error->getInfo()); } } function printWriteError(WriteError $error) { var_dump($error); printf("writeError[%d].message: %s\n", $error->getIndex(), $error->getMessage()); printf("writeError[%d].code: %d\n", $error->getIndex(), $error->getCode()); } function getInsertCount($retval) { return $retval->getInsertedCount(); } function getModifiedCount($retval) { return $retval->getModifiedCount(); } function getDeletedCount($retval) { return $retval->getDeletedCount(); } function getUpsertedCount($retval) { return $retval->getUpsertedCount(); } function getWriteErrors($retval) { return (array)$retval->getWriteErrors(); } function def($arr) { foreach($arr as $const => $value) { define($const, getenv("PHONGO_TEST_$const") ?: $value); } } function configureFailPoint(Manager $manager, $failPoint, $mode, array $data = []) { $doc = [ 'configureFailPoint' => $failPoint, 'mode' => $mode, ]; if ($data) { $doc['data'] = $data; } $cmd = new Command($doc); $manager->executeCommand('admin', $cmd); } function configureTargetedFailPoint(Server $server, $failPoint, $mode, array $data = []) { $doc = array( 'configureFailPoint' => $failPoint, 'mode' => $mode, ); if ($data) { $doc['data'] = $data; } $cmd = new Command($doc); $server->executeCommand('admin', $cmd); } function failMaxTimeMS(Server $server) { configureTargetedFailPoint($server, 'maxTimeAlwaysTimeOut', [ 'times' => 1 ]); } function toPHP($var, $typemap = array()) { return MongoDB\BSON\toPHP($var, $typemap); } function fromPHP($var) { return MongoDB\BSON\fromPHP($var); } function toJSON($var) { return MongoDB\BSON\toJSON($var); } function toCanonicalExtendedJSON($var) { return MongoDB\BSON\toCanonicalExtendedJSON($var); } function toRelaxedExtendedJSON($var) { return MongoDB\BSON\toRelaxedExtendedJSON($var); } function fromJSON($var) { return MongoDB\BSON\fromJSON($var); } /* Note: this fail point may terminate the mongod process, so you may want to * use this in conjunction with a throwaway server. */ function failGetMore(Manager $manager) { /* We need to do version detection here */ $primary = $manager->selectServer(new ReadPreference('primary')); $version = get_server_version_from_server($primary); if (version_compare($version, "4.0", ">=")) { /* We use 237 here, as that's the same original code that MongoD would * throw if a cursor had already gone by the time we call getMore. This * allows us to make things consistent with the getMore OP behaviour * from previous mongod versions. An errorCode is required here for the * failPoint to work. */ configureFailPoint($manager, 'failCommand', 'alwaysOn', [ 'errorCode' => 237, 'failCommands' => ['getMore'] ]); return; } throw new Exception("Trying to configure a getMore fail point for a server version ($version) that doesn't support it"); } function getAtlasConnectivityUrls(): array { $atlasUriString = getenv('ATLAS_CONNECTIVITY_URIS') ?: ''; if (!$atlasUriString) { return []; } $rawUrls = explode("\n", $atlasUriString); $urls = []; foreach ($rawUrls as $url) { $url = trim($url); if ($url == '') { continue; } $urls[] = $url; } return $urls; }