Passed
Pull Request — master (#251)
by Gabriel
10:21
created

CacheTest::provideCacheIds()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 31
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
c 0
b 0
f 0
rs 8.8571
cc 1
eloc 28
nc 1
nop 0
1
<?php
2
3
namespace Doctrine\Tests\Common\Cache;
4
5
use ArrayObject;
6
use Doctrine\Common\Cache\Cache;
7
use Doctrine\Common\Cache\CacheProvider;
8
use Doctrine\Tests\DoctrineTestCase;
9
use function array_keys;
10
use function array_map;
11
use function array_slice;
12
use function is_object;
13
use function restore_error_handler;
14
use function set_error_handler;
15
use function sleep;
16
use function sprintf;
17
use function str_repeat;
18
19
abstract class CacheTest extends DoctrineTestCase
20
{
21
    /**
22
     * @dataProvider provideDataToCache
23
     */
24
    public function testSetContainsFetchDelete($value) : void
25
    {
26
        $cache = $this->_getCacheDriver();
27
28
        // Test saving a value, checking if it exists, and fetching it back
29
        self::assertTrue($cache->save('key', $value));
30
        self::assertTrue($cache->contains('key'));
31
        if (is_object($value)) {
32
            self::assertEquals($value, $cache->fetch('key'), 'Objects retrieved from the cache must be equal but not necessarily the same reference');
33
        } else {
34
            self::assertSame($value, $cache->fetch('key'), 'Scalar and array data retrieved from the cache must be the same as the original, e.g. same type');
35
        }
36
37
        // Test deleting a value
38
        self::assertTrue($cache->delete('key'));
39
        self::assertFalse($cache->contains('key'));
40
        self::assertFalse($cache->fetch('key'));
41
    }
42
43
    /**
44
     * @dataProvider provideDataToCache
45
     */
46
    public function testUpdateExistingEntry($value) : void
47
    {
48
        $cache = $this->_getCacheDriver();
49
50
        self::assertTrue($cache->save('key', 'old-value'));
51
        self::assertTrue($cache->contains('key'));
52
53
        self::assertTrue($cache->save('key', $value));
54
        self::assertTrue($cache->contains('key'));
55
        if (is_object($value)) {
56
            self::assertEquals($value, $cache->fetch('key'), 'Objects retrieved from the cache must be equal but not necessarily the same reference');
57
        } else {
58
            self::assertSame($value, $cache->fetch('key'), 'Scalar and array data retrieved from the cache must be the same as the original, e.g. same type');
59
        }
60
    }
61
62
    public function testCacheKeyIsCaseSensitive() : void
63
    {
64
        $cache = $this->_getCacheDriver();
65
66
        self::assertTrue($cache->save('key', 'value'));
67
        self::assertTrue($cache->contains('key'));
68
        self::assertSame('value', $cache->fetch('key'));
69
70
        self::assertFalse($cache->contains('KEY'));
71
        self::assertFalse($cache->fetch('KEY'));
72
73
        $cache->delete('KEY');
74
        self::assertTrue($cache->contains('key'), 'Deleting cache item with different case must not affect other cache item');
75
    }
76
77
    public function testFetchMultiple() : void
78
    {
79
        $cache  = $this->_getCacheDriver();
80
        $values = $this->provideDataToCache();
81
        $saved  = [];
82
83
        foreach ($values as $key => $value) {
84
            $cache->save($key, $value[0]);
85
86
            $saved[$key] = $value[0];
87
        }
88
89
        $keys = array_keys($saved);
90
91
        self::assertEquals(
92
            $saved,
93
            $cache->fetchMultiple($keys),
94
            'Testing fetchMultiple with different data types'
95
        );
96
        self::assertEquals(
97
            array_slice($saved, 0, 1),
98
            $cache->fetchMultiple(array_slice($keys, 0, 1)),
99
            'Testing fetchMultiple with a single key'
100
        );
101
102
        $keysWithNonExisting   = [];
103
        $keysWithNonExisting[] = 'non_existing1';
104
        $keysWithNonExisting[] = $keys[0];
105
        $keysWithNonExisting[] = 'non_existing2';
106
        $keysWithNonExisting[] = $keys[1];
107
        $keysWithNonExisting[] = 'non_existing3';
108
109
        self::assertEquals(
110
            array_slice($saved, 0, 2),
111
            $cache->fetchMultiple($keysWithNonExisting),
112
            'Testing fetchMultiple with a subset of keys and mixed with non-existing ones'
113
        );
114
    }
115
116
    public function testFetchMultipleWithNoKeys() : void
117
    {
118
        $cache = $this->_getCacheDriver();
119
120
        self::assertSame([], $cache->fetchMultiple([]));
121
    }
122
123
    public function testSaveMultiple() : void
124
    {
125
        $cache = $this->_getCacheDriver();
126
        $cache->deleteAll();
127
128
        $data = array_map(function ($value) {
129
            return $value[0];
130
        }, $this->provideDataToCache());
131
132
        self::assertTrue($cache->saveMultiple($data));
133
134
        $keys = array_keys($data);
135
136
        self::assertEquals($data, $cache->fetchMultiple($keys));
137
    }
138
139
    public function provideDataToCache() : array
140
    {
141
        $obj       = new \stdClass();
142
        $obj->foo  = 'bar';
143
        $obj2      = new \stdClass();
144
        $obj2->bar = 'foo';
145
        $obj2->obj = $obj;
146
        $obj->obj2 = $obj2;
147
148
        return [
149
            'array' => [['one', 2, 3.01]],
150
            'string' => ['value'],
151
            'string_invalid_utf8' => ["\xc3\x28"],
152
            'string_null_byte' => ['with' . "\0" . 'null char'],
153
            'integer' => [1],
154
            'float' => [1.5],
155
            'object' => [new ArrayObject(['one', 2, 3.01])],
156
            'object_recursive' => [$obj],
157
            'true' => [true],
158
            // the following are considered FALSE in boolean context, but caches should still recognize their existence
159
            'null' => [null],
160
            'false' => [false],
161
            'array_empty' => [[]],
162
            'string_zero' => ['0'],
163
            'integer_zero' => [0],
164
            'float_zero' => [0.0],
165
            'string_empty' => [''],
166
        ];
167
    }
168
169
    public function testDeleteIsSuccessfulWhenKeyDoesNotExist() : void
170
    {
171
        $cache = $this->_getCacheDriver();
172
173
        $cache->delete('key');
174
        self::assertFalse($cache->contains('key'));
175
        self::assertTrue($cache->delete('key'));
176
    }
177
178
    public function testDeleteAll() : void
179
    {
180
        $cache = $this->_getCacheDriver();
181
182
        self::assertTrue($cache->save('key1', 1));
183
        self::assertTrue($cache->save('key2', 2));
184
        self::assertTrue($cache->deleteAll());
185
        self::assertFalse($cache->contains('key1'));
186
        self::assertFalse($cache->contains('key2'));
187
    }
188
189
    public function testDeleteMulti() : void
190
    {
191
        $cache = $this->_getCacheDriver();
192
193
        self::assertTrue($cache->save('key1', 1));
194
        self::assertTrue($cache->save('key2', 1));
195
        self::assertTrue($cache->deleteMultiple(['key1', 'key2', 'key3']));
196
        self::assertFalse($cache->contains('key1'));
197
        self::assertFalse($cache->contains('key2'));
198
        self::assertFalse($cache->contains('key3'));
199
    }
200
201
    /**
202
     * @dataProvider provideCacheIds
203
     */
204
    public function testCanHandleSpecialCacheIds($id) : void
205
    {
206
        $cache = $this->_getCacheDriver();
207
208
        self::assertTrue($cache->save($id, 'value'));
209
        self::assertTrue($cache->contains($id));
210
        self::assertEquals('value', $cache->fetch($id));
211
212
        self::assertTrue($cache->delete($id));
213
        self::assertFalse($cache->contains($id));
214
        self::assertFalse($cache->fetch($id));
215
    }
216
217
    public function testNoCacheIdCollisions() : void
218
    {
219
        $cache = $this->_getCacheDriver();
220
221
        $ids = $this->provideCacheIds();
222
223
        // fill cache with each id having a different value
224
        foreach ($ids as $index => $id) {
225
            $cache->save($id[0], $index);
226
        }
227
228
        // then check value of each cache id
229
        foreach ($ids as $index => $id) {
230
            $value = $cache->fetch($id[0]);
231
            self::assertNotFalse($value, sprintf('Failed to retrieve data for cache id "%s".', $id[0]));
232
            if ($index === $value) {
233
                continue;
234
            }
235
236
            $this->fail(sprintf('Cache id "%s" collides with id "%s".', $id[0], $ids[$value][0]));
237
        }
238
    }
239
240
    /**
241
     * Returns cache ids with special characters that should still work.
242
     *
243
     * For example, the characters :\/<>"*?| are not valid in Windows filenames. So they must be encoded properly.
244
     * Each cache id should be considered different from the others.
245
     */
246
    public function provideCacheIds() : array
247
    {
248
        return [
249
            [':'],
250
            ['\\'],
251
            ['/'],
252
            ['<'],
253
            ['>'],
254
            ['"'],
255
            ['*'],
256
            ['?'],
257
            ['|'],
258
            ['['],
259
            [']'],
260
            ['ä'],
261
            ['a'],
262
            ['é'],
263
            ['e'],
264
            ['.'], // directory traversal
265
            ['..'], // directory traversal
266
            ['-'],
267
            ['_'],
268
            ['$'],
269
            ['%'],
270
            [' '],
271
            ["\0"],
272
            [''],
273
            [str_repeat('a', 300)], // long key
274
            [str_repeat('a', 113)],
275
        ];
276
    }
277
278
    public function testLifetime() : void
279
    {
280
        $cache = $this->_getCacheDriver();
281
        $cache->save('expire', 'value', 1);
282
        self::assertTrue($cache->contains('expire'), 'Data should not be expired yet');
283
        // @TODO should more TTL-based tests pop up, so then we should mock the `time` API instead
284
        sleep(2);
285
        self::assertFalse($cache->contains('expire'), 'Data should be expired');
286
    }
287
288
    public function testNoExpire() : void
289
    {
290
        $cache = $this->_getCacheDriver();
291
        $cache->save('noexpire', 'value', 0);
292
        // @TODO should more TTL-based tests pop up, so then we should mock the `time` API instead
293
        sleep(1);
294
        self::assertTrue($cache->contains('noexpire'), 'Data with lifetime of zero should not expire');
295
    }
296
297
    public function testLongLifetime() : void
298
    {
299
        $cache = $this->_getCacheDriver();
300
        $cache->save('longlifetime', 'value', 30 * 24 * 3600 + 1);
301
        self::assertTrue($cache->contains('longlifetime'), 'Data with lifetime > 30 days should be accepted');
302
    }
303
304
    public function testDeleteAllAndNamespaceVersioningBetweenCaches() : void
305
    {
306
        if (! $this->isSharedStorage()) {
307
            $this->markTestSkipped('The cache storage needs to be shared.');
308
        }
309
310
        $cache1 = $this->_getCacheDriver();
311
        $cache2 = $this->_getCacheDriver();
312
313
        self::assertTrue($cache1->save('key1', 1));
314
        self::assertTrue($cache2->save('key2', 2));
315
316
        /* Both providers are initialized with the same namespace version, so
317
         * they can see entries set by each other.
318
         */
319
        self::assertTrue($cache1->contains('key1'));
320
        self::assertTrue($cache1->contains('key2'));
321
        self::assertTrue($cache2->contains('key1'));
322
        self::assertTrue($cache2->contains('key2'));
323
324
        /* Deleting all entries through one provider will only increment the
325
         * namespace version on that object (and in the cache itself, which new
326
         * instances will use to initialize). The second provider will retain
327
         * its original version and still see stale data.
328
         */
329
        self::assertTrue($cache1->deleteAll());
330
        self::assertFalse($cache1->contains('key1'));
331
        self::assertFalse($cache1->contains('key2'));
332
        self::assertTrue($cache2->contains('key1'));
333
        self::assertTrue($cache2->contains('key2'));
334
335
        /* A new cache provider should not see the deleted entries, since its
336
         * namespace version will be initialized.
337
         */
338
        $cache3 = $this->_getCacheDriver();
339
        self::assertFalse($cache3->contains('key1'));
340
        self::assertFalse($cache3->contains('key2'));
341
    }
342
343
    public function testFlushAll() : void
344
    {
345
        $cache = $this->_getCacheDriver();
346
347
        self::assertTrue($cache->save('key1', 1));
348
        self::assertTrue($cache->save('key2', 2));
349
        self::assertTrue($cache->flushAll());
350
        self::assertFalse($cache->contains('key1'));
351
        self::assertFalse($cache->contains('key2'));
352
    }
353
354
    public function testFlushAllAndNamespaceVersioningBetweenCaches() : void
355
    {
356
        if (! $this->isSharedStorage()) {
357
            $this->markTestSkipped('The cache storage needs to be shared.');
358
        }
359
360
        $cache1 = $this->_getCacheDriver();
361
        $cache2 = $this->_getCacheDriver();
362
363
        /* Deleting all elements from the first provider should increment its
364
         * namespace version before saving the first entry.
365
         */
366
        $cache1->deleteAll();
367
        self::assertTrue($cache1->save('key1', 1));
368
369
        /* The second provider will be initialized with the same namespace
370
         * version upon its first save operation.
371
         */
372
        self::assertTrue($cache2->save('key2', 2));
373
374
        /* Both providers have the same namespace version and can see entries
375
         * set by each other.
376
         */
377
        self::assertTrue($cache1->contains('key1'));
378
        self::assertTrue($cache1->contains('key2'));
379
        self::assertTrue($cache2->contains('key1'));
380
        self::assertTrue($cache2->contains('key2'));
381
382
        /* Flushing all entries through one cache will remove all entries from
383
         * the cache but leave their namespace version as-is.
384
         */
385
        self::assertTrue($cache1->flushAll());
386
        self::assertFalse($cache1->contains('key1'));
387
        self::assertFalse($cache1->contains('key2'));
388
        self::assertFalse($cache2->contains('key1'));
389
        self::assertFalse($cache2->contains('key2'));
390
391
        /* Inserting a new entry will use the same, incremented namespace
392
         * version, and it will be visible to both providers.
393
         */
394
        self::assertTrue($cache1->save('key1', 1));
395
        self::assertTrue($cache1->contains('key1'));
396
        self::assertTrue($cache2->contains('key1'));
397
398
        /* A new cache provider will be initialized with the original namespace
399
         * version and not share any visibility with the first two providers.
400
         */
401
        $cache3 = $this->_getCacheDriver();
402
        self::assertFalse($cache3->contains('key1'));
403
        self::assertFalse($cache3->contains('key2'));
404
        self::assertTrue($cache3->save('key3', 3));
405
        self::assertTrue($cache3->contains('key3'));
406
    }
407
408
    public function testNamespace() : void
409
    {
410
        $cache = $this->_getCacheDriver();
411
412
        $cache->setNamespace('ns1_');
413
414
        self::assertTrue($cache->save('key1', 1));
415
        self::assertTrue($cache->contains('key1'));
416
417
        $cache->setNamespace('ns2_');
418
419
        self::assertFalse($cache->contains('key1'));
420
    }
421
422
    public function testDeleteAllNamespace() : void
423
    {
424
        $cache = $this->_getCacheDriver();
425
426
        $cache->setNamespace('ns1');
427
        self::assertFalse($cache->contains('key1'));
428
        $cache->save('key1', 'test');
429
        self::assertTrue($cache->contains('key1'));
430
431
        $cache->setNamespace('ns2');
432
        self::assertFalse($cache->contains('key1'));
433
        $cache->save('key1', 'test');
434
        self::assertTrue($cache->contains('key1'));
435
436
        $cache->setNamespace('ns1');
437
        self::assertTrue($cache->contains('key1'));
438
        $cache->deleteAll();
439
        self::assertFalse($cache->contains('key1'));
440
441
        $cache->setNamespace('ns2');
442
        self::assertTrue($cache->contains('key1'));
443
        $cache->deleteAll();
444
        self::assertFalse($cache->contains('key1'));
445
    }
446
447
    /**
448
     * @group DCOM-43
449
     */
450
    public function testGetStats() : void
451
    {
452
        $cache = $this->_getCacheDriver();
453
        $stats = $cache->getStats();
454
455
        self::assertArrayHasKey(Cache::STATS_HITS, $stats);
456
        self::assertArrayHasKey(Cache::STATS_MISSES, $stats);
457
        self::assertArrayHasKey(Cache::STATS_UPTIME, $stats);
458
        self::assertArrayHasKey(Cache::STATS_MEMORY_USAGE, $stats);
459
        self::assertArrayHasKey(Cache::STATS_MEMORY_AVAILABLE, $stats);
460
    }
461
462
    public function testSaveReturnsTrueWithAndWithoutTTlSet() : void
463
    {
464
        $cache = $this->_getCacheDriver();
465
        $cache->deleteAll();
466
        self::assertTrue($cache->save('without_ttl', 'without_ttl'));
467
        self::assertTrue($cache->save('with_ttl', 'with_ttl', 3600));
468
    }
469
470
    public function testValueThatIsFalseBooleanIsProperlyRetrieved()
471
    {
472
        $cache = $this->_getCacheDriver();
473
        $cache->deleteAll();
474
475
        self::assertTrue($cache->save('key1', false));
476
        self::assertTrue($cache->contains('key1'));
477
        self::assertFalse($cache->fetch('key1'));
478
    }
479
480
    /**
481
     * @group 147
482
     * @group 152
483
     */
484
    public function testFetchingANonExistingKeyShouldNeverCauseANoticeOrWarning() : void
485
    {
486
        $cache = $this->_getCacheDriver();
487
488
        $errorHandler = function () {
489
            restore_error_handler();
490
491
            $this->fail('include failure captured');
492
        };
493
494
        set_error_handler($errorHandler);
495
496
        $cache->fetch('key');
497
498
        self::assertSame(
499
            $errorHandler,
500
            set_error_handler(function () {
501
            }),
502
            'The error handler is the one set by this test, and wasn\'t replaced'
503
        );
504
505
        restore_error_handler();
506
        restore_error_handler();
507
    }
508
509
    /**
510
     * Return whether multiple cache providers share the same storage.
511
     *
512
     * This is used for skipping certain tests for shared storage behavior.
513
     */
514
    protected function isSharedStorage() : bool
515
    {
516
        return true;
517
    }
518
519
    abstract protected function _getCacheDriver() : CacheProvider;
520
}
521