Passed
Push — master ( 5adaea...ab4b97 )
by Alexander
01:37
created

ConnectionTest::testMastersShuffled()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 14
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 24
rs 9.7998
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\Tests;
6
7
use Yiisoft\Db\Drivers\Connection;
8
use Yiisoft\Db\Exceptions\InvalidConfigException;
9
use Yiisoft\Db\Transactions\Transaction;
10
use Yiisoft\Db\Tests\ConnectionTest as AbstractConnectionTest;
11
12
class ConnectionTest extends AbstractConnectionTest
13
{
14
    protected ?string $driverName = 'sqlite';
15
16
    public function testConstruct(): void
17
    {
18
        $connection = $this->getConnection(false);
19
20
        $this->assertEquals($this->databases['dsn'], $connection->getDsn());
21
    }
22
23
    /**
24
     * Test whether slave connection is recovered when call getSlavePdo() after close().
25
     *
26
     * @see https://github.com/yiisoft/yii2/issues/14165
27
     */
28
    public function testGetPdoAfterClose(): void
29
    {
30
        $connection = $this->getConnection();
31
32
        $connection->slaves[] = [
33
            'dsn' => $this->databases['dsn'],
34
        ];
35
36
        $this->assertNotNull($connection->getSlavePdo(false));
37
38
        $connection->close();
39
40
        $masterPdo = $connection->getMasterPdo();
41
        $this->assertNotFalse($masterPdo);
42
        $this->assertNotNull($masterPdo);
43
44
        $slavePdo = $connection->getSlavePdo(false);
45
        $this->assertNotFalse($slavePdo);
46
        $this->assertNotNull($slavePdo);
47
        $this->assertNotSame($masterPdo, $slavePdo);
48
    }
49
50
    public function testServerStatusCacheWorks(): void
51
    {
52
        $connection = $this->getConnection(true, false);
53
54
        $connection->masters[] = [
55
            'dsn' => $this->databases['dsn'],
56
        ];
57
58
        $connection->shuffleMasters = false;
59
60
        $cacheKey = ['Yiisoft\Db\Drivers\Connection::openFromPoolSequentially', $connection->getDsn()];
61
62
        $this->assertFalse($this->cache->has($cacheKey));
0 ignored issues
show
Bug introduced by
$cacheKey of type array<integer,null|string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::has(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

62
        $this->assertFalse($this->cache->has(/** @scrutinizer ignore-type */ $cacheKey));
Loading history...
63
64
        $connection->open();
65
66
        $this->assertFalse(
67
            $this->cache->has($cacheKey),
68
            'Connection was successful – cache must not contain information about this DSN'
69
        );
70
71
        $connection->close();
72
73
        $connection = $this->getConnection(true, false);
74
75
        $cacheKey = ['Yiisoft\Db\Drivers\Connection::openFromPoolSequentially', 'host:invalid'];
76
77
        $connection->masters[] = [
78
            'dsn' => 'host:invalid',
79
        ];
80
81
        $connection->shuffleMasters = true;
82
83
        try {
84
            $connection->open();
85
        } catch (InvalidConfigException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
86
        }
87
88
        $this->assertTrue(
89
            $this->cache->has($cacheKey),
0 ignored issues
show
Bug introduced by
$cacheKey of type array<integer,string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::has(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
            $this->cache->has(/** @scrutinizer ignore-type */ $cacheKey),
Loading history...
90
            'Connection was not successful – cache must contain information about this DSN'
91
        );
92
93
        $connection->close();
94
    }
95
96
    public function testServerStatusCacheCanBeDisabled(): void
97
    {
98
        $this->cache->clear();
99
100
        $connection = $this->getConnection(true, false);
101
102
        $connection->masters[] = [
103
            'dsn' => $this->databases['dsn'],
104
        ];
105
106
        $connection->setSchemaCache(null);
107
108
        $connection->shuffleMasters = false;
109
110
        $cacheKey = ['Yiisoft\Db\Drivers\Connection::openFromPoolSequentially', $connection->getDsn()];
111
112
        $this->assertFalse($this->cache->has($cacheKey));
0 ignored issues
show
Bug introduced by
$cacheKey of type array<integer,null|string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::has(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
        $this->assertFalse($this->cache->has(/** @scrutinizer ignore-type */ $cacheKey));
Loading history...
113
114
        $connection->open();
115
116
        $this->assertFalse($this->cache->has($cacheKey), 'Caching is disabled');
117
118
        $connection->close();
119
120
        $cacheKey = ['Yiisoft\Db\Drivers\Connection::openFromPoolSequentially', 'host:invalid'];
121
122
        $connection->masters[] = [
123
            'dsn' => 'host:invalid',
124
        ];
125
126
        try {
127
            $connection->open();
128
        } catch (InvalidConfigException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
129
        }
130
131
        $this->assertFalse($this->cache->has($cacheKey), 'Caching is disabled');
0 ignored issues
show
Bug introduced by
$cacheKey of type array<integer,string> is incompatible with the type string expected by parameter $key of Psr\SimpleCache\CacheInterface::has(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

131
        $this->assertFalse($this->cache->has(/** @scrutinizer ignore-type */ $cacheKey), 'Caching is disabled');
Loading history...
132
133
        $connection->close();
134
    }
135
136
    public function testQuoteValue(): void
137
    {
138
        $connection = $this->getConnection(false);
139
        $this->assertEquals(123, $connection->quoteValue(123));
140
        $this->assertEquals("'string'", $connection->quoteValue('string'));
141
        $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting"));
142
    }
143
144
    public function testTransactionIsolation(): void
145
    {
146
        $connection = $this->getConnection(true);
147
148
        $transaction = $connection->beginTransaction(Transaction::READ_UNCOMMITTED);
149
        $transaction->rollBack();
150
151
        $transaction = $connection->beginTransaction(Transaction::SERIALIZABLE);
152
        $transaction->rollBack();
153
154
        $this->assertTrue(true); // No exceptions means test is passed.
155
    }
156
157
    public function testMasterSlave(): void
158
    {
159
        $counts = [[0, 2], [1, 2], [2, 2]];
160
161
        foreach ($counts as $count) {
162
            [$masterCount, $slaveCount] = $count;
163
164
            $db = $this->prepareMasterSlave($masterCount, $slaveCount);
165
166
            $this->assertInstanceOf(Connection::class, $db->getSlave());
167
            $this->assertTrue($db->getSlave()->getIsActive());
168
            $this->assertFalse($db->getIsActive());
169
170
            // test SELECT uses slave
171
            $this->assertEquals(2, $db->createCommand('SELECT COUNT(*) FROM profile')->queryScalar());
172
            $this->assertFalse($db->getIsActive());
173
174
            // test UPDATE uses master
175
            $db->createCommand("UPDATE profile SET description='test' WHERE id=1")->execute();
176
            $this->assertTrue($db->getIsActive());
177
178
            if ($masterCount > 0) {
179
                $this->assertInstanceOf(Connection::class, $db->getMaster());
180
                $this->assertTrue($db->getMaster()->getIsActive());
181
            } else {
182
                $this->assertNull($db->getMaster());
183
            }
184
185
            $this->assertNotEquals('test', $db->createCommand('SELECT description FROM profile WHERE id=1')->queryScalar());
186
187
            $result = $db->useMaster(static function (Connection $db) {
188
                return $db->createCommand('SELECT description FROM profile WHERE id=1')->queryScalar();
189
            });
190
191
            $this->assertEquals('test', $result);
192
        }
193
    }
194
195
    public function testMastersShuffled(): void
196
    {
197
        $mastersCount = 2;
198
        $slavesCount = 2;
199
        $retryPerNode = 10;
200
201
        $nodesCount = $mastersCount + $slavesCount;
202
203
        $hit_slaves = $hit_masters = [];
204
205
        for ($i = $nodesCount * $retryPerNode; $i-- > 0;) {
206
            $db = $this->prepareMasterSlave($mastersCount, $slavesCount);
207
            $db->shuffleMasters = true;
208
209
            $hit_slaves[$db->getSlave()->getDsn()] = true;
210
            $hit_masters[$db->getMaster()->getDsn()] = true;
211
212
            if (\count($hit_slaves) === $slavesCount && \count($hit_masters) === $mastersCount) {
213
                break;
214
            }
215
        }
216
217
        $this->assertCount($mastersCount, $hit_masters, 'all masters hit');
218
        $this->assertCount($slavesCount, $hit_slaves, 'all slaves hit');
219
    }
220
221
    public function testMastersSequential(): void
222
    {
223
        $mastersCount = 2;
224
        $slavesCount = 2;
225
        $retryPerNode = 10;
226
227
        $nodesCount = $mastersCount + $slavesCount;
228
229
        $hit_slaves = $hit_masters = [];
230
231
        for ($i = $nodesCount * $retryPerNode; $i-- > 0;) {
232
            $db = $this->prepareMasterSlave($mastersCount, $slavesCount);
233
            $db->shuffleMasters = false;
234
235
            $hit_slaves[$db->getSlave()->getDsn()] = true;
236
            $hit_masters[$db->getMaster()->getDsn()] = true;
237
238
            if (\count($hit_slaves) === $slavesCount) {
239
                break;
240
            }
241
        }
242
243
        $this->assertCount(1, $hit_masters, 'same master hit');
244
245
        // slaves are always random
246
        $this->assertCount($slavesCount, $hit_slaves, 'all slaves hit');
247
    }
248
249
    public function testRestoreMasterAfterException(): void
250
    {
251
        $db = $this->prepareMasterSlave(1, 1);
252
        $this->assertTrue($db->enableSlaves);
253
254
        try {
255
            $db->useMaster(static function (Connection $db) {
0 ignored issues
show
Unused Code introduced by
The parameter $db is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

255
            $db->useMaster(static function (/** @scrutinizer ignore-unused */ Connection $db) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
256
                throw new \Exception('fail');
257
            });
258
            $this->fail('Exceptions was caught somewhere');
259
        } catch (\Exception $e) {
260
            // ok
261
        }
262
263
        $this->assertTrue($db->enableSlaves);
264
    }
265
266
    public function testExceptionContainsRawQuery(): void
267
    {
268
        $this->markTestSkipped('This test does not work on sqlite because preparing the failing query fails');
269
    }
270
271
    protected function prepareMasterSlave($masterCount, $slaveCount)
272
    {
273
        $db = $this->getConnection(true, true, true);
274
275
        for ($i = 0; $i < $masterCount; ++$i) {
276
            $this->prepareDatabase(true, true, [
277
                'dsn' => 'sqlite:' .  __DIR__ . "/data/yii_test_master{$i}.sq3",
278
            ]);
279
            $db->masters[] = [
280
                'dsn' => 'sqlite:' .  __DIR__ . "/data/yii_test_master{$i}.sq3",
281
            ];
282
        }
283
284
        for ($i = 0; $i < $slaveCount; ++$i) {
285
            $this->prepareDatabase(true, true, [
286
                'dsn' =>  'sqlite:' .  __DIR__ . "/data/yii_test_slave{$i}.sq3",
287
            ]);
288
            $db->slaves[] = [
289
                'dsn' => 'sqlite:' .  __DIR__ . "/data/yii_test_slave{$i}.sq3",
290
            ];
291
        }
292
293
        $db->close();
294
295
        return $db;
296
    }
297
}
298