ErrorExpectation.php 6.76 KB
Newer Older
1 2 3 4
<?php

namespace MongoDB\Tests\SpecTests;

5
use Exception;
6 7
use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\CommandException;
8
use MongoDB\Driver\Exception\ExecutionTimeoutException;
9 10 11 12
use MongoDB\Driver\Exception\RuntimeException;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Tests\TestCase;
use stdClass;
13 14 15 16
use function get_class;
use function is_array;
use function is_string;
use function sprintf;
17 18 19 20 21 22 23 24

/**
 * Spec test operation error expectation.
 */
final class ErrorExpectation
{
    /**
     * @see https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.err
25 26
     *
     * @var array
27 28 29
     */
    private static $codeNameMap = [
        'Interrupted' => 11601,
30
        'MaxTimeMSExpired' => 50,
31 32
        'NoSuchTransaction' => 251,
        'OperationNotSupportedInTransaction' => 263,
33
        'WriteConflict' => 112,
34 35
    ];

36
    /** @var integer */
37
    private $code;
38 39

    /** @var string */
40
    private $codeName;
41 42

    /** @var boolean */
43
    private $isExpected = false;
44 45

    /** @var string[] */
46
    private $excludedLabels = [];
47 48

    /** @var string[] */
49
    private $includedLabels = [];
50 51

    /** @var string */
52 53 54 55 56 57
    private $messageContains;

    private function __construct()
    {
    }

58 59
    public static function fromChangeStreams(stdClass $result)
    {
60
        $o = new self();
61 62 63 64 65 66 67

        if (isset($result->error->code)) {
            $o->code = $result->error->code;
            $o->isExpected = true;
        }

        if (isset($result->error->errorLabels)) {
68
            if (! self::isArrayOfStrings($result->error->errorLabels)) {
69 70 71 72 73 74 75 76 77
                throw InvalidArgumentException::invalidType('errorLabels', $result->error->errorLabels, 'string[]');
            }
            $o->includedLabels = $result->error->errorLabels;
            $o->isExpected = true;
        }

        return $o;
    }

78 79 80 81 82 83 84 85 86 87 88
    public static function fromRetryableReads(stdClass $operation)
    {
        $o = new self();

        if (isset($operation->error)) {
            $o->isExpected = $operation->error;
        }

        return $o;
    }

89 90
    public static function fromRetryableWrites(stdClass $outcome)
    {
91
        $o = new self();
92 93 94 95 96 97 98 99 100 101 102 103 104

        if (isset($outcome->error)) {
            $o->isExpected = $outcome->error;
        }

        return $o;
    }

    /**
     * @throws InvalidArgumentException
     */
    public static function fromTransactions(stdClass $operation)
    {
105
        $o = new self();
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

        if (isset($operation->error)) {
            $o->isExpected = $operation->error;
        }

        $result = isset($operation->result) ? $operation->result : null;

        if (isset($result->errorContains)) {
            $o->messageContains = $result->errorContains;
            $o->isExpected = true;
        }

        if (isset($result->errorCodeName)) {
            $o->codeName = $result->errorCodeName;
            $o->isExpected = true;
        }

        if (isset($result->errorLabelsContain)) {
124
            if (! self::isArrayOfStrings($result->errorLabelsContain)) {
125 126 127 128 129 130 131
                throw InvalidArgumentException::invalidType('errorLabelsContain', $result->errorLabelsContain, 'string[]');
            }
            $o->includedLabels = $result->errorLabelsContain;
            $o->isExpected = true;
        }

        if (isset($result->errorLabelsOmit)) {
132
            if (! self::isArrayOfStrings($result->errorLabelsOmit)) {
133 134 135 136 137 138 139 140 141
                throw InvalidArgumentException::invalidType('errorLabelsOmit', $result->errorLabelsOmit, 'string[]');
            }
            $o->excludedLabels = $result->errorLabelsOmit;
            $o->isExpected = true;
        }

        return $o;
    }

142 143 144 145 146
    public static function noError()
    {
        return new self();
    }

147 148 149 150 151 152 153 154
    /**
     * Assert that the error expectation matches the actual outcome.
     *
     * @param TestCase       $test   Test instance for performing assertions
     * @param Exception|null $actual Exception (if any) from the actual outcome
     */
    public function assert(TestCase $test, Exception $actual = null)
    {
155
        if (! $this->isExpected) {
156 157 158
            if ($actual !== null) {
                $test->fail(sprintf("Operation threw unexpected %s: %s\n%s", get_class($actual), $actual->getMessage(), $actual->getTraceAsString()));
            }
159

160 161 162 163 164 165
            return;
        }

        $test->assertNotNull($actual);

        if (isset($this->messageContains)) {
166
            $test->assertStringContainsStringIgnoringCase($this->messageContains, $actual->getMessage());
167 168 169 170 171 172
        }

        if (isset($this->codeName)) {
            $this->assertCodeName($test, $actual);
        }

173
        if (! empty($this->excludedLabels) || ! empty($this->includedLabels)) {
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
            $test->assertInstanceOf(RuntimeException::class, $actual);

            foreach ($this->excludedLabels as $label) {
                $test->assertFalse($actual->hasErrorLabel($label), 'Exception should not have error label: ' . $label);
            }

            foreach ($this->includedLabels as $label) {
                $test->assertTrue($actual->hasErrorLabel($label), 'Exception should have error label: ' . $label);
            }
        }
    }

    public function isExpected()
    {
        return $this->isExpected;
    }

    /**
     * Assert that the error code name expectation matches the actual outcome.
     *
     * @param TestCase       $test   Test instance for performing assertions
     * @param Exception|null $actual Exception (if any) from the actual outcome
     */
    private function assertCodeName(TestCase $test, Exception $actual = null)
    {
        /* BulkWriteException does not expose codeName for server errors. Work
         * around this be comparing the error code against a map.
         *
         * TODO: Remove this once PHPC-1386 is resolved. */
203
        if ($actual instanceof BulkWriteException || $actual instanceof ExecutionTimeoutException) {
204 205
            $test->assertArrayHasKey($this->codeName, self::$codeNameMap);
            $test->assertSame(self::$codeNameMap[$this->codeName], $actual->getCode());
206

207 208 209 210 211
            return;
        }

        $test->assertInstanceOf(CommandException::class, $actual);
        $result = $actual->getResultDocument();
212 213 214 215 216 217 218 219

        if (isset($result->writeConcernError)) {
            $test->assertObjectHasAttribute('codeName', $result->writeConcernError);
            $test->assertSame($this->codeName, $result->writeConcernError->codeName);

            return;
        }

220
        $test->assertObjectHasAttribute('codeName', $result);
221
        $test->assertSame($this->codeName, $result->codeName);
222 223 224 225
    }

    private static function isArrayOfStrings($array)
    {
226
        if (! is_array($array)) {
227 228 229 230
            return false;
        }

        foreach ($array as $string) {
231
            if (! is_string($string)) {
232 233 234 235 236 237 238
                return false;
            }
        }

        return true;
    }
}