bztang-admin/vendor/mongodb/mongodb/tests/UnifiedSpecTests/UnifiedSpecTest.php

390 lines
12 KiB
PHP

<?php
namespace MongoDB\Tests\UnifiedSpecTests;
use MongoDB\Client;
use MongoDB\Collection;
use MongoDB\Driver\Exception\ServerException;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server;
use MongoDB\Operation\DatabaseCommand;
use MongoDB\Tests\FunctionalTestCase;
use stdClass;
use Symfony\Bridge\PhpUnit\SetUpTearDownTrait;
use Throwable;
use function file_get_contents;
use function gc_collect_cycles;
use function glob;
use function MongoDB\BSON\fromJSON;
use function MongoDB\BSON\toPHP;
use function PHPUnit\Framework\assertTrue;
use function sprintf;
use function version_compare;
/**
* Unified spec test runner.
*
* @see https://github.com/mongodb/specifications/pull/846
*/
class UnifiedSpecTest extends FunctionalTestCase
{
use SetUpTearDownTrait;
const SERVER_ERROR_INTERRUPTED = 11601;
const MIN_SCHEMA_VERSION = '1.0';
const MAX_SCHEMA_VERSION = '1.1';
const TOPOLOGY_SINGLE = 'single';
const TOPOLOGY_REPLICASET = 'replicaset';
const TOPOLOGY_SHARDED = 'sharded';
const TOPOLOGY_SHARDED_REPLICASET = 'sharded-replicaset';
/** @var MongoDB\Client */
private static $internalClient;
/** @var FailPointObserver */
private $failPointObserver;
private static function doSetUpBeforeClass()
{
parent::setUpBeforeClass();
/* Provide internal client unmodified URI, since it may need to execute
* commands on multiple mongoses (e.g. killAllSessions) */
self::$internalClient = new Client(static::getUri(true));
self::killAllSessions();
}
private function doSetUp()
{
parent::setUp();
/* The transactions spec advises calling killAllSessions only at the
* start of the test suite and after failed tests; however, the "unpin
* after transient error within a transaction" pinning test causes the
* subsequent transaction test to block. */
self::killAllSessions();
$this->failPointObserver = new FailPointObserver();
$this->failPointObserver->start();
}
private function doTearDown()
{
if ($this->hasFailed()) {
self::killAllSessions();
}
$this->failPointObserver->stop();
$this->failPointObserver->disableFailPoints();
/* Manually invoking garbage collection since each test is prone to
* create cycles (perhaps due to EntityMap), which can leak and prevent
* sessions from being released back into the pool. */
gc_collect_cycles();
parent::tearDown();
}
/**
* @dataProvider providePassingTests
*/
public function testPassingTests(stdClass $test, string $schemaVersion, array $runOnRequirements = null, array $createEntities = null, array $initialData = null)
{
if (! $this->isSchemaVersionSupported($schemaVersion)) {
$this->markTestIncomplete(sprintf('Test format schema version "%s" is not supported', $schemaVersion));
}
if (isset($runOnRequirements)) {
$this->checkRunOnRequirements($runOnRequirements);
}
if (isset($test->skipReason)) {
$this->assertIsString($test->skipReason);
$this->markTestSkipped($test->skipReason);
}
if (isset($test->runOnRequirements)) {
$this->assertIsArray($test->runOnRequirements);
$this->checkRunOnRequirements($test->runOnRequirements);
}
if (isset($initialData)) {
$this->prepareInitialData($initialData);
}
// Give Context unmodified URI so it can enforce useMultipleMongoses
$context = new Context(self::$internalClient, static::getUri(true));
if (isset($createEntities)) {
$context->createEntities($createEntities);
}
$this->assertIsArray($test->operations);
$this->preventStaleDbVersionError($test->operations, $context);
$context->startEventObservers();
foreach ($test->operations as $o) {
$operation = new Operation($o, $context);
$operation->assert();
}
$context->stopEventObservers();
if (isset($test->expectEvents)) {
$this->assertIsArray($test->expectEvents);
$context->assertExpectedEventsForClients($test->expectEvents);
}
if (isset($test->outcome)) {
$this->assertIsArray($test->outcome);
$this->assertOutcome($test->outcome);
}
}
public function providePassingTests()
{
return $this->provideTests(__DIR__ . '/valid-pass');
}
/**
* @dataProvider provideFailingTests
*/
public function testFailingTests(...$args)
{
// Cannot use expectException(), as it ignores PHPUnit Exceptions
$failed = false;
try {
$this->testCase(...$args);
} catch (Throwable $e) {
$failed = true;
}
assertTrue($failed, 'Expected test to throw an exception');
}
public function provideFailingTests()
{
return $this->provideTests(__DIR__ . '/valid-fail');
}
private function provideTests(string $dir)
{
$testArgs = [];
foreach (glob($dir . '/*.json') as $filename) {
/* Decode the file through the driver's extended JSON parser to
* ensure proper handling of special types. */
$json = toPHP(fromJSON(file_get_contents($filename)));
$description = $json->description;
$schemaVersion = $json->schemaVersion;
$runOnRequirements = $json->runOnRequirements ?? null;
$createEntities = $json->createEntities ?? null;
$initialData = $json->initialData ?? null;
$tests = $json->tests;
/* Assertions in data providers do not count towards test assertions
* but failures will interrupt the test suite with a warning. */
$message = 'Invalid test file: ' . $filename;
$this->assertIsString($description, $message);
$this->assertIsString($schemaVersion, $message);
$this->assertIsArray($tests, $message);
foreach ($json->tests as $test) {
$this->assertIsObject($test, $message);
$this->assertIsString($test->description, $message);
$name = $description . ': ' . $test->description;
$testArgs[$name] = [$test, $schemaVersion, $runOnRequirements, $createEntities, $initialData];
}
}
return $testArgs;
}
/**
* Checks server version and topology requirements.
*
* @param array $runOnRequirements
* @throws SkippedTest unless one or more runOnRequirements are met
*/
private function checkRunOnRequirements(array $runOnRequirements)
{
$this->assertNotEmpty($runOnRequirements);
$this->assertContainsOnly('object', $runOnRequirements);
$serverVersion = $this->getCachedServerVersion();
$topology = $this->getCachedTopology();
foreach ($runOnRequirements as $o) {
$runOnRequirement = new RunOnRequirement($o);
if ($runOnRequirement->isSatisfied($serverVersion, $topology)) {
return;
}
}
$this->markTestSkipped(sprintf('Server version "%s" and topology "%s" do not meet test requirements', $serverVersion, $topology));
}
/**
* Return the server version (cached for subsequent calls).
*
* @return string
*/
private function getCachedServerVersion()
{
static $cachedServerVersion;
if (isset($cachedServerVersion)) {
return $cachedServerVersion;
}
$cachedServerVersion = $this->getServerVersion();
return $cachedServerVersion;
}
/**
* Return the topology type (cached for subsequent calls).
*
* @return string
* @throws UnexpectedValueException if topology is neither single nor RS nor sharded
*/
private function getCachedTopology()
{
static $cachedTopology = null;
if (isset($cachedTopology)) {
return $cachedTopology;
}
switch ($this->getPrimaryServer()->getType()) {
case Server::TYPE_STANDALONE:
$cachedTopology = RunOnRequirement::TOPOLOGY_SINGLE;
break;
case Server::TYPE_RS_PRIMARY:
$cachedTopology = RunOnRequirement::TOPOLOGY_REPLICASET;
break;
case Server::TYPE_MONGOS:
$cachedTopology = $this->isShardedClusterUsingReplicasets()
? RunOnRequirement::TOPOLOGY_SHARDED_REPLICASET
: RunOnRequirement::TOPOLOGY_SHARDED;
break;
default:
throw new UnexpectedValueException('Toplogy is neither single nor RS nor sharded');
}
return $cachedTopology;
}
/**
* Checks is a test format schema version is supported.
*
* @param string $schemaVersion
* @return boolean
*/
private function isSchemaVersionSupported($schemaVersion)
{
return version_compare($schemaVersion, self::MIN_SCHEMA_VERSION, '>=') && version_compare($schemaVersion, self::MAX_SCHEMA_VERSION, '<');
}
/**
* Kill all sessions on the cluster.
*
* This will clean up any open transactions that may remain from a
* previously failed test. For sharded clusters, this command will be run
* on all mongos nodes.
*/
private static function killAllSessions()
{
$manager = self::$internalClient->getManager();
$primary = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
$servers = $primary->getType() === Server::TYPE_MONGOS ? $manager->getServers() : [$primary];
foreach ($servers as $server) {
try {
// Skip servers that do not support sessions
if (! isset($server->getInfo()['logicalSessionTimeoutMinutes'])) {
continue;
}
$command = new DatabaseCommand('admin', ['killAllSessions' => []]);
$command->execute($server);
} catch (ServerException $e) {
// Interrupted error is safe to ignore (see: SERVER-38335)
if ($e->getCode() != self::SERVER_ERROR_INTERRUPTED) {
throw $e;
}
}
}
}
private function assertOutcome(array $outcome)
{
$this->assertNotEmpty($outcome);
$this->assertContainsOnly('object', $outcome);
foreach ($outcome as $data) {
$collectionData = new CollectionData($data);
$collectionData->assertOutcome(self::$internalClient);
}
}
private function prepareInitialData(array $initialData)
{
$this->assertNotEmpty($initialData);
$this->assertContainsOnly('object', $initialData);
foreach ($initialData as $data) {
$collectionData = new CollectionData($data);
$collectionData->prepareInitialData(self::$internalClient);
}
}
/**
* Work around potential error executing distinct on sharded clusters.
*
* @see https://github.com/mongodb/specifications/tree/master/source/transactions/tests#why-do-tests-that-run-distinct-sometimes-fail-with-staledbversionts.
*/
private function preventStaleDbVersionError(array $operations, Context $context)
{
if (! $this->isShardedCluster()) {
return;
}
$hasStartTransaction = false;
$hasDistinct = false;
$collection = null;
foreach ($operations as $operation) {
switch ($operation->name) {
case 'distinct':
$hasDistinct = true;
$collection = $context->getEntityMap()[$operation->object];
break;
case 'startTransaction':
$hasStartTransaction = true;
break;
default:
continue 2;
}
if ($hasStartTransaction && $hasDistinct) {
$this->assertInstanceOf(Collection::class, $collection);
$collection->distinct('foo');
return;
}
}
}
}