Completed
Push — in-memory-cache ( a24fa8 )
by André
26:08
created

testGetItemLastRemovedFromMemoryWhenReachingLimit()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 78

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 78
rs 8.48
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
8
9
namespace eZ\Publish\Core\Persistence\Cache\Adapter\Tests;
10
11
use eZ\Publish\Core\Persistence\Cache\Adapter\InMemoryCacheAdapter;
12
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
13
use Symfony\Component\Cache\CacheItem;
14
use PHPUnit\Framework\TestCase;
15
16
/**
17
 * Abstract test case for spi cache impl.
18
 */
19
class InMemoryCacheAdapterTest extends TestCase
20
{
21
    /**
22
     * @var \eZ\Publish\Core\Persistence\Cache\Adapter\InMemoryCacheAdapter
23
     */
24
    protected $cache;
25
26
    /**
27
     * @var \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface|\PHPUnit\Framework\MockObject\MockObject
28
     */
29
    protected $innerCacheMock;
30
31
    /**
32
     * @var \Closure
33
     */
34
    private $cacheItemsClosure;
35
36
    /**
37
     * Setup the HandlerTest.
38
     */
39
    final protected function setUp()
40
    {
41
        parent::setUp();
42
43
        $this->innerCacheMock = $this->createMock(TagAwareAdapterInterface::class);
44
45
        $this->cache = new InMemoryCacheAdapter(
46
            $this->innerCacheMock,
47
            12,
48
            3
49
        );
50
51
        $this->cacheItemsClosure = \Closure::bind(
52 View Code Duplication
            function ($key, $value, $isHit, $defaultLifetime = 0, $tags = []) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
53
                $item = new CacheItem();
54
                $item->key = $key;
0 ignored issues
show
Bug introduced by
The property key cannot be accessed from this context as it is declared protected in class Symfony\Component\Cache\CacheItem.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
55
                $item->value = $value;
0 ignored issues
show
Bug introduced by
The property value cannot be accessed from this context as it is declared protected in class Symfony\Component\Cache\CacheItem.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
56
                $item->isHit = $isHit;
0 ignored issues
show
Bug introduced by
The property isHit cannot be accessed from this context as it is declared protected in class Symfony\Component\Cache\CacheItem.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
57
                $item->prevTags = $tags;
0 ignored issues
show
Bug introduced by
The property prevTags cannot be accessed from this context as it is declared protected in class Symfony\Component\Cache\CacheItem.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
58
                $item->defaultLifetime = $defaultLifetime;
0 ignored issues
show
Bug introduced by
The property defaultLifetime cannot be accessed from this context as it is declared protected in class Symfony\Component\Cache\CacheItem.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
59
60
                return $item;
61
            },
62
            null,
63
            CacheItem::class
64
        );
65
    }
66
67
    /**
68
     * Tear down test (properties).
69
     */
70
    final protected function tearDown()
71
    {
72
        $this->cache->clear();
73
74
        unset($this->cache);
75
        unset($this->innerCacheMock);
76
        unset($this->cacheItemsClosure);
77
        unset($GLOBALS['override_time']);
78
        parent::tearDown();
79
    }
80
81
    public function testGetItemOnlyCalledOnce()
82
    {
83
        $item = $this->getCacheItem('first');
84
85
        $this->innerCacheMock
86
            ->expects($this->once())
87
            ->method('getItem')
88
            ->with('first')
89
            ->willReturn($item);
90
91
        $returnedItem = $this->cache->getItem('first');
92
        $this->assertSame($item, $returnedItem);
93
94
        $returnedItem = $this->cache->getItem('first');
95
        $this->assertSame($item, $returnedItem);
96
    }
97
98
    /**
99
     * @depends testGetItemOnlyCalledOnce
100
     */
101
    public function testGetItemTTL()
102
    {
103
        $item = $this->getCacheItem('first');
104
105
        $this->innerCacheMock
106
            ->expects($this->exactly(2))
107
            ->method('getItem')
108
            ->with('first')
109
            ->willReturn($item);
110
111
        $this->cache->getItem('first');
112
113
        $GLOBALS['override_time'] = time() + 4;
114
115
        $this->cache->getItem('first');
116
    }
117
118
    /**
119
     * @depends testGetItemOnlyCalledOnce
120
     */
121
    public function testGetItemLastRemovedFromMemoryWhenReachingLimit()
122
    {
123
        $this->cache = new InMemoryCacheAdapter(
124
            $this->innerCacheMock,
0 ignored issues
show
Bug introduced by
It seems like $this->innerCacheMock can also be of type object<PHPUnit\Framework\MockObject\MockObject>; however, eZ\Publish\Core\Persiste...eAdapter::__construct() does only seem to accept object<Symfony\Component...gAwareAdapterInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
125
            6,
126
            3
127
        );
128
129
        $this->innerCacheMock
130
            ->expects($this->at(0))
131
            ->method('getItem')
132
            ->with('first')
133
            ->willReturn($this->getCacheItem('first'));
134
135
        $this->innerCacheMock
136
            ->expects($this->at(1))
137
            ->method('getItem')
138
            ->with('second')
139
            ->willReturn($this->getCacheItem('second'));
140
141
        $this->innerCacheMock
142
            ->expects($this->at(2))
143
            ->method('getItem')
144
            ->with('third')
145
            ->willReturn($this->getCacheItem('third'));
146
147
        $this->innerCacheMock
148
            ->expects($this->at(3))
149
            ->method('getItem')
150
            ->with('fourth')
151
            ->willReturn($this->getCacheItem('fourth'));
152
153
        $this->innerCacheMock
154
            ->expects($this->at(4))
155
            ->method('getItem')
156
            ->with('fifth')
157
            ->willReturn($this->getCacheItem('fifth'));
158
159
        // At this point cache should start clearing cache from the end of the list
160
        $this->innerCacheMock
161
            ->expects($this->at(5))
162
            ->method('getItem')
163
            ->with('sixth')
164
            ->willReturn($this->getCacheItem('sixth'));
165
166
        $this->innerCacheMock
167
            ->expects($this->once())
168
            ->method('getItems')
169
            ->with(['fifth'])
170
            ->willReturn([
171
                'fifth' => $this->getCacheItem('fifth'),
172
            ]);
173
174
        // On purpose these are called twice, they should not result in cache extra calls
175
        $this->cache->getItem('first');
176
        $this->cache->getItem('first');
177
178
        $this->cache->getItem('second');
179
        $this->cache->getItem('second');
180
181
        $this->cache->getItem('third');
182
        $this->cache->getItem('third');
183
184
        // Should not result in extra lookups at this point
185
        iterator_to_array($this->cache->getItems(['first', 'second', 'third']));
186
187
        $this->cache->getItem('fourth');
188
        $this->cache->getItem('fourth');
189
190
        $this->cache->getItem('fifth');
191
        $this->cache->getItem('fifth');
192
193
        $this->cache->getItem('sixth');
194
        $this->cache->getItem('sixth');
195
196
        // Should results in lookup for fifth
197
        iterator_to_array($this->cache->getItems(['first', 'second', 'third', 'fourth', 'fifth', 'sixth']));
198
    }
199
200
    public function testHasItem()
201
    {
202
        $this->innerCacheMock
203
            ->expects($this->once())
204
            ->method('getItem')
205
            ->with('first')
206
            ->willReturn($this->getCacheItem('first'));
207
208
        $this->innerCacheMock
209
            ->expects($this->once())
210
            ->method('hasItem')
211
            ->with('first')
212
            ->willReturn(true);
213
214
        $this->cache->hasItem('first');
215
216
        // populate cache
217
        $this->cache->getItem('first');
218
219
        $this->cache->hasItem('first');
220
    }
221
222 View Code Duplication
    public function testGetItemsOnlyCalledOnce()
223
    {
224
        $items = [
225
            'first' => $this->getCacheItem('first'),
226
            'second' => $this->getCacheItem('second'),
227
        ];
228
229
        $this->innerCacheMock
230
            ->expects($this->once())
231
            ->method('getItems')
232
            ->with(['first', 'second'])
233
            ->willReturn($items);
234
235
        $returnedItems = iterator_to_array($this->cache->getItems(['first', 'second']));
236
        $this->assertSame($items, $returnedItems);
237
238
        $returnedItems = iterator_to_array($this->cache->getItems(['first', 'second']));
239
        $this->assertSame($items, $returnedItems);
240
    }
241
242
    /**
243
     * Symfony uses generators with getItems() so we need to make sure we handle that.
244
     */
245 View Code Duplication
    public function testGetItemsWithGenerator()
246
    {
247
        $items = [
248
            'first' => $this->getCacheItem('first'),
249
            'second' => $this->getCacheItem('second'),
250
        ];
251
252
        $this->innerCacheMock
253
            ->expects($this->once())
254
            ->method('getItems')
255
            ->with(['first', 'second'])
256
            ->willReturn($this->arrayAsGenerator($items));
257
258
        $returnedItems = iterator_to_array($this->cache->getItems(['first', 'second']));
259
        $this->assertSame($items, $returnedItems);
260
261
        $returnedItems = iterator_to_array($this->cache->getItems(['first', 'second']));
262
        $this->assertSame($items, $returnedItems);
263
    }
264
265
    /**
266
     * @depends testGetItemsOnlyCalledOnce
267
     */
268
    public function testGetItemsTTL()
269
    {
270
        $items = [
271
            'first' => $this->getCacheItem('first'),
272
            'second' => $this->getCacheItem('second'),
273
        ];
274
275
        $this->innerCacheMock
276
            ->expects($this->exactly(2))
277
            ->method('getItems')
278
            ->with(['first', 'second'])
279
            ->willReturn($items);
280
281
        iterator_to_array($this->cache->getItems(['first', 'second']));
282
283
        $GLOBALS['override_time'] = time() + 4;
284
285
        iterator_to_array($item = $this->cache->getItems(['first', 'second']));
286
    }
287
288
    /**
289
     * @depends testGetItemsOnlyCalledOnce
290
     */
291
    public function testGetItemsNotPlacedInMemoryIfEmpty()
292
    {
293
        $items = [];
294
295
        $this->innerCacheMock
296
            ->expects($this->exactly(2))
297
            ->method('getItems')
298
            ->with(['first', 'second', 'third'])
299
            ->willReturn($items);
300
301
        iterator_to_array($this->cache->getItems(['first', 'second', 'third']));
302
        iterator_to_array($this->cache->getItems(['first', 'second', 'third']));
303
    }
304
305
    /**
306
     * @depends testGetItemsOnlyCalledOnce
307
     */
308
    public function testGetItemsNotPlacedInMemoryIfLargerList()
309
    {
310
        $items = [
311
            'first' => $this->getCacheItem('first'),
312
            'second' => $this->getCacheItem('second'),
313
            'third' => $this->getCacheItem('third'),
314
        ];
315
316
        $this->innerCacheMock
317
            ->expects($this->exactly(2))
318
            ->method('getItems')
319
            ->with(['first', 'second', 'third'])
320
            ->willReturn($items);
321
322
        iterator_to_array($this->cache->getItems(['first', 'second', 'third']));
323
        iterator_to_array($this->cache->getItems(['first', 'second', 'third']));
324
    }
325
326
    /**
327
     * @dataProvider providerForInvalidation
328
     */
329
    public function testCacheClearing(string $method, $argument, int $expectedCount)
330
    {
331
        $this->innerCacheMock
332
            ->expects($this->exactly($expectedCount))
333
            ->method('getItem')
334
            ->with('first')
335
            ->willReturn($this->getCacheItem('first', ['my_tag']));
336
337
        $this->innerCacheMock
338
            ->expects($this->never())
339
            ->method('getItems')
340
            ->with(['first']);
341
342
        // should only lookup once
343
        $this->cache->getItem('first');
344
        $this->cache->getItem('first');
345
        iterator_to_array($this->cache->getItems(['first']));
346
347
        // invalidate it
348
        $this->cache->$method($argument);
349
350
        // again, should only lookup once
351
        $this->cache->getItem('first');
352
        $this->cache->getItem('first');
353
        iterator_to_array($this->cache->getItems(['first']));
354
    }
355
356
    public function providerForInvalidation(): array
357
    {
358
        return [
359
            ['deleteItem', 'first', 2],
360
            ['deleteItems', ['first'], 2],
361
            ['invalidateTags', ['my_tag'], 2],
362
            ['clear', null, 2],
363
            // negative cases
364
            ['deleteItem', 'second', 1],
365
            ['deleteItems', ['second'], 1],
366
            ['invalidateTags', ['some_other_tag'], 1],
367
        ];
368
    }
369
370
    /**
371
     * @param $key
372
     * @param null $value If null the cache item will be assumed to be a cache miss here.
373
     * @param int $defaultLifetime
0 ignored issues
show
Bug introduced by
There is no parameter named $defaultLifetime. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
374
     *
375
     * @return CacheItem
376
     */
377
    private function getCacheItem($key, $tags = [], $value = true)
378
    {
379
        $cacheItemsClosure = $this->cacheItemsClosure;
380
381
        return $cacheItemsClosure($key, $value, (bool) $value, 0, $tags);
382
    }
383
384
    private function arrayAsGenerator(array $array)
385
    {
386
        foreach ($array as $key => $item) {
387
            yield $key => $item;
388
        }
389
    }
390
}
391
392
namespace eZ\Publish\Core\Persistence\Cache\Adapter;
393
394
/**
395
 * Overload time call used in InMemoryCacheAdapter in order to be able to test expiry.
396
 *
397
 * @return mixed
398
 */
399
function time()
400
{
401
    if (isset($GLOBALS['override_time'])) {
402
        return $GLOBALS['override_time'];
403
    }
404
405
    return \time();
406
}
407