FunctionalTestCase.php 9.17 KB
Newer Older
1 2 3 4 5 6 7
<?php

namespace MongoDB\Tests\SpecTests;

use ArrayIterator;
use IteratorIterator;
use LogicException;
8 9 10
use MongoDB\Collection;
use MongoDB\Driver\Server;
use MongoDB\Tests\FunctionalTestCase as BaseFunctionalTestCase;
11
use MultipleIterator;
12
use PHPUnit\Framework\SkippedTest;
13
use stdClass;
14
use Symfony\Bridge\PhpUnit\SetUpTearDownTrait;
15
use UnexpectedValueException;
16 17 18 19 20 21
use function in_array;
use function json_encode;
use function MongoDB\BSON\fromJSON;
use function MongoDB\BSON\toPHP;
use function sprintf;
use function version_compare;
22 23 24 25 26 27

/**
 * Base class for spec test runners.
 *
 * @see https://github.com/mongodb/specifications
 */
28
class FunctionalTestCase extends BaseFunctionalTestCase
29
{
30 31
    use SetUpTearDownTrait;

32 33 34 35
    const TOPOLOGY_SINGLE = 'single';
    const TOPOLOGY_REPLICASET = 'replicaset';
    const TOPOLOGY_SHARDED = 'sharded';

36
    /** @var Context|null */
37
    private $context;
38

39
    private function doSetUp()
40 41
    {
        parent::setUp();
42 43

        $this->context = null;
44 45
    }

46
    private function doTearDown()
47
    {
48
        $this->context = null;
49 50 51 52 53

        parent::tearDown();
    }

    /**
54
     * Assert that the expected and actual command documents match.
55
     *
56 57
     * Note: Spec tests that do not assert command started events may throw an
     * exception in lieu of implementing this method.
58
     *
59 60
     * @param stdClass $expectedCommand Expected command document
     * @param stdClass $actualCommand   Actual command document
61
     */
62 63
    public static function assertCommandMatches(stdClass $expected, stdClass $actual)
    {
64
        throw new LogicException(sprintf('%s does not assert CommandStartedEvents', static::class));
65
    }
66 67 68 69 70 71 72 73 74 75

    /**
     * Assert that the expected and actual command reply documents match.
     *
     * Note: Spec tests that do not assert command started events may throw an
     * exception in lieu of implementing this method.
     *
     * @param stdClass $expected Expected command reply document
     * @param stdClass $actual   Actual command reply document
     */
76 77
    public static function assertCommandReplyMatches(stdClass $expected, stdClass $actual)
    {
78
        throw new LogicException(sprintf('%s does not assert CommandSucceededEvents', static::class));
79
    }
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

    /**
     * Asserts that two given documents match.
     *
     * Extra keys in the actual value's document(s) will be ignored.
     *
     * @param array|object $expectedDocument
     * @param array|object $actualDocument
     * @param string       $message
     */
    protected static function assertDocumentsMatch($expectedDocument, $actualDocument, $message = '')
    {
        $constraint = new DocumentsMatchConstraint($expectedDocument, true, true);

        static::assertThat($actualDocument, $constraint, $message);
    }
96 97 98 99 100

    /**
     * Assert data within the outcome collection.
     *
     * @param array $expectedDocuments
101
     * @param int   $resultExpectation
102
     */
103
    protected function assertOutcomeCollectionData(array $expectedDocuments, $resultExpectation = ResultExpectation::ASSERT_SAME_DOCUMENT)
104
    {
105
        $outcomeCollection = $this->getOutcomeCollection($this->getContext()->outcomeReadOptions);
106

107 108
        $mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
        $mi->attachIterator(new ArrayIterator($expectedDocuments));
109
        $mi->attachIterator(new IteratorIterator($outcomeCollection->find()));
110 111 112

        foreach ($mi as $documents) {
            list($expectedDocument, $actualDocument) = $documents;
113 114
            $this->assertNotNull($expectedDocument);
            $this->assertNotNull($actualDocument);
115 116 117 118 119 120 121 122 123 124 125 126 127

            switch ($resultExpectation) {
                case ResultExpectation::ASSERT_SAME_DOCUMENT:
                    $this->assertSameDocument($expectedDocument, $actualDocument);
                    break;

                case ResultExpectation::ASSERT_DOCUMENTS_MATCH:
                    $this->assertDocumentsMatch($expectedDocument, $actualDocument);
                    break;

                default:
                    $this->fail(sprintf('Invalid result expectation "%d" for %s', $resultExpectation, __METHOD__));
            }
128 129 130 131 132 133 134 135 136 137 138 139
        }
    }

    /**
     * Checks server version and topology requirements.
     *
     * @param array $runOn
     * @throws SkippedTest if the server requirements are not met
     */
    protected function checkServerRequirements(array $runOn)
    {
        foreach ($runOn as $req) {
140 141 142
            $minServerVersion = $req->minServerVersion ?? null;
            $maxServerVersion = $req->maxServerVersion ?? null;
            $topologies = $req->topology ?? null;
143 144 145 146 147 148 149 150 151 152 153 154 155

            if ($this->isServerRequirementSatisifed($minServerVersion, $maxServerVersion, $topologies)) {
                return;
            }
        }

        $serverVersion = $this->getServerVersion();
        $topology = $this->getTopology();

        $this->markTestSkipped(sprintf('Server version "%s" and topology "%s" do not meet test requirements: %s', $serverVersion, $topology, json_encode($runOn)));
    }

    /**
156
     * Decode a JSON spec test.
157
     *
158 159 160 161 162
     * This decodes the file through the driver's extended JSON parser to ensure
     * proper handling of special types.
     *
     * @param string $json
     * @return array
163
     */
164
    protected function decodeJson($json)
165
    {
166
        return toPHP(fromJSON($json));
167 168 169 170 171 172 173 174 175 176
    }

    /**
     * Return the test context.
     *
     * @return Context
     * @throws LogicException if the context has not been set
     */
    protected function getContext()
    {
177
        if (! $this->context instanceof Context) {
178
            throw new LogicException('Context has not been set');
179 180
        }

181
        return $this->context;
182 183 184
    }

    /**
185
     * Set the test context.
186
     *
187 188 189 190 191 192 193 194 195
     * @param Context $context
     */
    protected function setContext(Context $context)
    {
        $this->context = $context;
    }

    /**
     * Drop the test and outcome collections by dropping them.
196
     */
197
    protected function dropTestAndOutcomeCollections()
198
    {
199
        $context = $this->getContext();
200

201 202 203 204
        if ($context->bucketName !== null) {
            $bucket = $context->getGridFSBucket($context->defaultWriteOptions);
            $bucket->drop();
        }
205

206 207
        $collection = null;
        if ($context->collectionName !== null) {
208 209
            $collection = $context->getCollection($context->defaultWriteOptions);
            $collection->drop();
210
        }
211

212
        if ($context->outcomeCollectionName !== null) {
213
            $outcomeCollection = $this->getOutcomeCollection($context->defaultWriteOptions);
214 215 216

            // Avoid redundant drop if the test and outcome collections are the same
            if ($collection === null || $outcomeCollection->getNamespace() !== $collection->getNamespace()) {
217
                $outcomeCollection->drop();
218
            }
219 220 221 222
        }
    }

    /**
223
     * Insert data fixtures into the test collection.
224
     *
225 226
     * @param array       $documents
     * @param string|null $collectionName
227
     */
228
    protected function insertDataFixtures(array $documents, $collectionName = null)
229
    {
230 231
        if (empty($documents)) {
            return;
232 233
        }

234
        $context = $this->getContext();
235
        $collection = $collectionName ? $context->selectCollection($context->databaseName, $collectionName) : $context->getCollection();
236

237
        $collection->insertMany($documents, $context->defaultWriteOptions);
238 239

        return;
240
    }
241

242
    private function getOutcomeCollection(array $collectionOptions = [])
243 244 245 246
    {
        $context = $this->getContext();

        // Outcome collection need not use the client under test
247
        return new Collection($this->manager, $context->databaseName, $context->outcomeCollectionName, $collectionOptions);
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
    }

    /**
     * Return the corresponding topology constants for the current topology.
     *
     * @return string
     * @throws UnexpectedValueException if topology is neither single nor RS nor sharded
     */
    private function getTopology()
    {
        $topologyTypeMap = [
            Server::TYPE_STANDALONE => self::TOPOLOGY_SINGLE,
            Server::TYPE_RS_PRIMARY => self::TOPOLOGY_REPLICASET,
            Server::TYPE_MONGOS => self::TOPOLOGY_SHARDED,
        ];

        $primaryType = $this->getPrimaryServer()->getType();

        if (isset($topologyTypeMap[$primaryType])) {
            return $topologyTypeMap[$primaryType];
        }

        throw new UnexpectedValueException('Toplogy is neither single nor RS nor sharded');
    }

    /**
     * Checks if server version and topology requirements are satifised.
     *
     * @param string|null $minServerVersion
     * @param string|null $maxServerVersion
     * @param array|null  $topologies
     * @return boolean
     */
    private function isServerRequirementSatisifed($minServerVersion, $maxServerVersion, array $topologies = null)
    {
        $serverVersion = $this->getServerVersion();

        if (isset($minServerVersion) && version_compare($serverVersion, $minServerVersion, '<')) {
            return false;
        }

        if (isset($maxServerVersion) && version_compare($serverVersion, $maxServerVersion, '>')) {
            return false;
        }

        $topology = $this->getTopology();

        if (isset($topologies) && ! in_array($topology, $topologies)) {
            return false;
        }

        return true;
    }
}