Completed
Push — master ( f56795...5a873a )
by Marco
03:36
created

testFetchingANonExistingKeyShouldNeverCauseANoticeOrWarning()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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