1<?php
2
3/*
4 * This file is part of the Stash package.
5 *
6 * (c) Robert Hafner <tedivm@tedivm.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Stash\Test;
13
14use Stash\Item;
15use Stash\Invalidation;
16use Stash\Utilities;
17use Stash\Driver\Ephemeral;
18use Stash\Test\Stubs\PoolGetDriverStub;
19
20/**
21 * @package Stash
22 * @author  Robert Hafner <tedivm@tedivm.com>
23 *
24 * @todo find out why this has to be abstract to work (see https://github.com/tedivm/Stash/pull/10)
25 */
26abstract class AbstractItemTest extends \PHPUnit\Framework\TestCase
27{
28    protected $data = array('string' => 'Hello world!',
29                            'complexString' => "\t\t\t\tHello\r\n\rWorld!",
30                            'int' => 4234,
31                            'negint' => -6534,
32                            'float' => 1.8358023545,
33                            'negfloat' => -5.7003249023,
34                            'false' => false,
35                            'true' => true,
36                            'null' => null,
37                            'array' => array(3, 5, 7),
38                            'hashmap' => array('one' => 1, 'two' => 2),
39                            'multidemensional array' => array(array(5345),
40                                                              array(3, 'hello', false, array('one' => 1, 'two' => 2))
41                            )
42    );
43
44    protected $expiration;
45    protected $startTime;
46    private $setup = false;
47    protected $driver;
48
49    protected $itemClass = '\Stash\Item';
50
51    public static function tearDownAfterClass()
52    {
53        Utilities::deleteRecursive(Utilities::getBaseDirectory());
54    }
55
56    protected function setUp()
57    {
58        if (!$this->setup) {
59            $this->startTime = time();
60            $this->expiration = $this->startTime + 3600;
61            $this->data['object'] = new \stdClass();
62        }
63    }
64
65    /**
66     * This just makes it slightly easier to extend AbstractCacheTest to
67     * other Item types.
68     *
69     * @return \Stash\Interfaces\ItemInterface
70     */
71    protected function getItem()
72    {
73        return new $this->itemClass();
74    }
75
76    public function testConstruct($key = array())
77    {
78        if (!isset($this->driver)) {
79            $this->driver = new Ephemeral(array());
80        }
81
82        $item = $this->getItem();
83        $this->assertTrue(is_a($item, 'Stash\Item'), 'Test object is an instance of Stash');
84
85        $poolStub = new PoolGetDriverStub();
86        $poolStub->setDriver($this->driver);
87        $item->setPool($poolStub);
88
89        $item->setKey($key);
90
91        return $item;
92    }
93
94    public function testSetupKey()
95    {
96        $keyString = 'this/is/the/key';
97        $keyArray = array('this', 'is', 'the', 'key');
98        $keyNormalized = array('cache', 'this', 'is', 'the', 'key');
99
100        $stashArray = $this->testConstruct($keyArray);
101        $this->assertAttributeInternalType('string', 'keyString', $stashArray, 'Argument based keys setup keystring');
102        $this->assertAttributeInternalType('array', 'key', $stashArray, 'Array based keys setup key');
103
104        $returnedKey = $stashArray->getKey();
105        $this->assertEquals($keyString, $returnedKey, 'getKey returns properly normalized key from array argument.');
106    }
107
108    public function testSet()
109    {
110        foreach ($this->data as $type => $value) {
111            $key = array('base', $type);
112            $stash = $this->testConstruct($key);
113            $this->assertAttributeInternalType('string', 'keyString', $stash, 'Argument based keys setup keystring');
114            $this->assertAttributeInternalType('array', 'key', $stash, 'Argument based keys setup key');
115
116            $this->assertTrue($stash->set($value)->save(), 'Driver class able to store data type ' . $type);
117        }
118
119        $item = $this->getItem();
120        $poolStub = new PoolGetDriverStub();
121        $poolStub->setDriver(new Ephemeral(array()));
122        $item->setPool($poolStub);
123        $this->assertFalse($item->set($this->data), 'Item without key returns false for set.');
124    }
125
126    /**
127     * @depends testSet
128     */
129    public function testGet()
130    {
131        foreach ($this->data as $type => $value) {
132            $key = array('base', $type);
133            $stash = $this->testConstruct($key);
134            $stash->set($value)->save();
135
136            // new object, but same backend
137            $stash = $this->testConstruct($key);
138            $data = $stash->get();
139            $this->assertEquals($value, $data, 'getData ' . $type . ' returns same item as stored');
140        }
141
142        if (!isset($this->driver)) {
143            $this->driver = new Ephemeral();
144        }
145
146        $item = $this->getItem();
147
148        $poolStub = new PoolGetDriverStub();
149        $poolStub->setDriver(new Ephemeral());
150        $item->setPool($poolStub);
151
152        $this->assertEquals(null, $item->get(), 'Item without key returns null for get.');
153    }
154
155    public function testGetItemInvalidKey()
156    {
157        try {
158            $item = $this->getItem();
159            $poolStub = new PoolGetDriverStub();
160            $poolStub->setDriver(new Ephemeral(array()));
161            $item->setPool($poolStub);
162            $item->setKey('This is not an array');
163        } catch (\Throwable $t) {
164            return;
165        } catch (\Exception $expected) {
166            return;
167        }
168
169        $this->fail('An expected exception has not been raised.');
170    }
171
172    public function testLock()
173    {
174        $item = $this->getItem();
175        $poolStub = new PoolGetDriverStub();
176        $poolStub->setDriver(new Ephemeral());
177        $item->setPool($poolStub);
178        $this->assertFalse($item->lock(), 'Item without key returns false for lock.');
179    }
180
181    public function testInvalidation()
182    {
183        $key = array('path', 'to', 'item');
184        $oldValue = 'oldValue';
185        $newValue = 'newValue';
186
187        $runningStash = $this->testConstruct($key);
188        $runningStash->set($oldValue)->expiresAfter(-300)->save();
189
190        // Test without stampede
191        $controlStash = $this->testConstruct($key);
192        $controlStash->setInvalidationMethod(Invalidation::VALUE, $newValue);
193        $return = $controlStash->get();
194        $this->assertNull($return, 'NULL is returned when isHit is false');
195        $this->assertFalse($controlStash->isHit());
196        unset($controlStash);
197
198        // Enable stampede control
199        $runningStash->lock();
200        $this->assertAttributeEquals(true, 'stampedeRunning', $runningStash, 'Stampede flag is set.');
201
202        // Old
203        $oldStash = $this->testConstruct($key);
204        $oldStash->setInvalidationMethod(Invalidation::OLD);
205        $return = $oldStash->get(Invalidation::OLD);
206        $this->assertEquals($oldValue, $return, 'Old value is returned');
207        $this->assertFalse($oldStash->isMiss());
208        unset($oldStash);
209
210        // Value
211        $valueStash = $this->testConstruct($key);
212        $valueStash->setInvalidationMethod(Invalidation::VALUE, $newValue);
213        $return = $valueStash->get(Invalidation::VALUE, $newValue);
214        $this->assertEquals($newValue, $return, 'New value is returned');
215        $this->assertFalse($valueStash->isMiss());
216        unset($valueStash);
217
218        // Sleep
219        $sleepStash = $this->testConstruct($key);
220        $sleepStash->setInvalidationMethod(Invalidation::SLEEP, 250, 2);
221        $start = microtime(true);
222        $return = $sleepStash->get();
223        $end = microtime(true);
224
225        $this->assertTrue($sleepStash->isMiss());
226        $sleepTime = ($end - $start) * 1000;
227
228        $this->assertGreaterThan(500, $sleepTime, 'Sleep method sleeps for required time.');
229        $this->assertLessThan(550, $sleepTime, 'Sleep method does not oversleep.');
230
231        unset($sleepStash);
232
233        // Unknown - if a random, unknown method is passed for invalidation we should rely on the default method
234        $unknownStash = $this->testConstruct($key);
235
236        $return = $unknownStash->get(78);
237        $this->assertNull($return, 'NULL is returned when isHit is false');
238        $this->assertFalse($unknownStash->isHit(), 'Cache is marked as miss');
239        unset($unknownStash);
240
241        // Test that storing the cache turns off stampede mode.
242        $runningStash->set($newValue)->expiresAfter(30)->save();
243        $this->assertAttributeEquals(false, 'stampedeRunning', $runningStash, 'Stampede flag is off.');
244        unset($runningStash);
245
246        // Precompute - test outside limit
247        $precomputeStash = $this->testConstruct($key);
248        $precomputeStash->setInvalidationMethod(Invalidation::PRECOMPUTE, 10);
249        $return = $precomputeStash->get(Invalidation::PRECOMPUTE, 10);
250        $this->assertFalse($precomputeStash->isMiss(), 'Cache is marked as hit');
251        unset($precomputeStash);
252
253        // Precompute - test inside limit
254        $precomputeStash = $this->testConstruct($key);
255        $precomputeStash->setInvalidationMethod(Invalidation::PRECOMPUTE, 35);
256        $return = $precomputeStash->get();
257        $this->assertTrue($precomputeStash->isMiss(), 'Cache is marked as miss');
258        unset($precomputeStash);
259
260        // Test Stampede Flag Expiration
261        $key = array('stampede', 'expire');
262        $Item_SPtest = $this->testConstruct($key);
263        $Item_SPtest->setInvalidationMethod(Invalidation::VALUE, $newValue);
264        $Item_SPtest->set($oldValue)->expiresAfter(300)->save();
265        $Item_SPtest->lock(-5);
266        $Item_SPtest = $this->testConstruct($key);
267        $this->assertEquals($oldValue, $Item_SPtest->get(), 'Expired lock is ignored');
268    }
269
270    public function testSetTTLDatetime()
271    {
272        $expiration = new \DateTime('now');
273        $expiration->add(new \DateInterval('P1D'));
274
275        $key = array('ttl', 'expiration', 'test');
276        $stash = $this->testConstruct($key);
277
278        $stash->set(array(1, 2, 3, 'apples'))
279          ->setTTL($expiration)
280          ->save();
281        $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp());
282
283        $stash = $this->testConstruct($key);
284        $data = $stash->get();
285        $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration');
286        $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp());
287    }
288
289    public function testSetTTLDateInterval()
290    {
291        $interval = new \DateInterval('P1D');
292        $expiration = new \DateTime('now');
293        $expiration->add($interval);
294
295        $key = array('ttl', 'expiration', 'test');
296        $stash = $this->testConstruct($key);
297        $stash->set(array(1, 2, 3, 'apples'))
298          ->setTTL($interval)
299          ->save();
300
301        $stash = $this->testConstruct($key);
302        $data = $stash->get();
303        $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration');
304        $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp());
305    }
306
307    public function testSetTTLNulll()
308    {
309        $key = array('ttl', 'expiration', 'test');
310        $stash = $this->testConstruct($key);
311        $stash->set(array(1, 2, 3, 'apples'))
312          ->setTTL(null)
313          ->save();
314
315        $this->assertAttributeEquals(null, 'expiration', $stash);
316    }
317
318
319    public function testExpiresAt()
320    {
321        $expiration = new \DateTime('now');
322        $expiration->add(new \DateInterval('P1D'));
323
324        $key = array('base', 'expiration', 'test');
325        $stash = $this->testConstruct($key);
326
327        $stash->set(array(1, 2, 3, 'apples'))
328          ->expiresAt($expiration)
329          ->save();
330
331        $stash = $this->testConstruct($key);
332        $data = $stash->get();
333        $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration');
334        $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp());
335    }
336
337    /**
338     * @expectedException Stash\Exception\InvalidArgumentException
339     * @expectedExceptionMessage expiresAt requires \DateTimeInterface or null
340     */
341    public function testExpiresAtException()
342    {
343        $stash = $this->testConstruct(array('base', 'expiration', 'test'));
344        $stash->expiresAt(false);
345    }
346
347    public function testExpiresAfterWithDateTimeInterval()
348    {
349        $key = array('base', 'expiration', 'test');
350        $stash = $this->testConstruct($key);
351
352        $stash->set(array(1, 2, 3, 'apples'))
353          ->expiresAfter(new \DateInterval('P1D'))
354          ->save();
355
356        $stash = $this->testConstruct($key);
357        $data = $stash->get();
358        $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration');
359    }
360
361
362    public function testGetCreation()
363    {
364        $creation = new \DateTime('now');
365        $creation->add(new \DateInterval('PT10S')); // expire 10 seconds after createdOn
366        $creationTS = $creation->getTimestamp();
367
368        $key = array('getCreation', 'test');
369        $stash = $this->testConstruct($key);
370
371        $this->assertFalse($stash->getCreation(), 'no record exists yet, return null');
372
373        $stash->set(array('stuff'), $creation)->save();
374
375        $stash = $this->testConstruct($key);
376        $createdOn = $stash->getCreation();
377        $this->assertInstanceOf('\DateTime', $createdOn, 'getCreation returns DateTime');
378        $itemCreationTimestamp = $createdOn->getTimestamp();
379        $this->assertEquals($creationTS - 10, $itemCreationTimestamp, 'createdOn is 10 seconds before expiration');
380    }
381
382    public function testGetExpiration()
383    {
384        $expiration = new \DateTime('now');
385        $expiration->add(new \DateInterval('P1D'));
386        $expirationTS = $expiration->getTimestamp();
387
388        $key = array('getExpiration', 'test');
389        $stash = $this->testConstruct($key);
390
391
392        $currentDate = new \DateTime();
393        $returnedDate = $stash->getExpiration();
394
395        $this->assertLessThanOrEqual(2, $currentDate->getTimestamp() -  $returnedDate->getTimestamp(), 'No record set, return as expired.');
396        $this->assertLessThanOrEqual(2, $returnedDate->getTimestamp() -  $currentDate->getTimestamp(), 'No record set, return as expired.');
397
398        #$this->assertFalse($stash->getExpiration(), 'no record exists yet, return null');
399
400        $stash->set(array('stuff'))->expiresAt($expiration)->save();
401
402        $stash = $this->testConstruct($key);
403        $itemExpiration = $stash->getExpiration();
404        $this->assertInstanceOf('\DateTime', $itemExpiration, 'getExpiration returns DateTime');
405        $itemExpirationTimestamp = $itemExpiration->getTimestamp();
406        $this->assertLessThanOrEqual($expirationTS, $itemExpirationTimestamp, 'sometime before explicitly set expiration');
407    }
408
409    public function testIsMiss()
410    {
411        $stash = $this->testConstruct(array('This', 'Should', 'Fail'));
412        $this->assertTrue($stash->isMiss(), 'isMiss returns true for missing data');
413        $data = $stash->get();
414        $this->assertNull($data, 'getData returns null for missing data');
415
416        $key = array('isMiss', 'test');
417
418        $stash = $this->testConstruct($key);
419        $stash->set('testString')->save();
420
421        $stash = $this->testConstruct($key);
422        $this->assertTrue(!$stash->isMiss(), 'isMiss returns false for valid data');
423    }
424
425    public function testIsHit()
426    {
427        $stash = $this->testConstruct(array('This', 'Should', 'Fail'));
428        $this->assertFalse($stash->isHit(), 'isHit returns false for missing data');
429        $data = $stash->get();
430        $this->assertNull($data, 'getData returns null for missing data');
431
432        $key = array('isHit', 'test');
433
434        $stash = $this->testConstruct($key);
435        $stash->set('testString')->save();
436
437        $stash = $this->testConstruct($key);
438        $this->assertTrue($stash->isHit(), 'isHit returns true for valid data');
439    }
440
441    public function testClear()
442    {
443        // repopulate
444        foreach ($this->data as $type => $value) {
445            $key = array('base', $type);
446            $stash = $this->testConstruct($key);
447            $stash->set($value)->save();
448            $this->assertAttributeInternalType('string', 'keyString', $stash, 'Argument based keys setup keystring');
449            $this->assertAttributeInternalType('array', 'key', $stash, 'Argument based keys setup key');
450
451            $this->assertTrue($stash->set($value)->save(), 'Driver class able to store data type ' . $type);
452        }
453
454        foreach ($this->data as $type => $value) {
455            $key = array('base', $type);
456
457            // Make sure its actually populated. This has the added bonus of making sure one clear doesn't empty the
458            // entire cache.
459            $stash = $this->testConstruct($key);
460            $data = $stash->get();
461            $this->assertEquals($value, $data, 'getData ' . $type . ' returns same item as stored after other data is cleared');
462
463
464            // Run the clear, make sure it says it works.
465            $stash = $this->testConstruct($key);
466            $this->assertTrue($stash->clear(), 'clear returns true');
467
468
469            // Finally verify that the data has actually been removed.
470            $stash = $this->testConstruct($key);
471            $data = $stash->get();
472            $this->assertNull($data, 'getData ' . $type . ' returns null once deleted');
473            $this->assertTrue($stash->isMiss(), 'isMiss returns true for deleted data');
474        }
475
476        // repopulate
477        foreach ($this->data as $type => $value) {
478            $key = array('base', $type);
479            $stash = $this->testConstruct($key);
480            $stash->set($value)->save();
481        }
482
483        // clear
484        $stash = $this->testConstruct();
485        $this->assertTrue($stash->clear(), 'clear returns true');
486
487        // make sure all the keys are gone.
488        foreach ($this->data as $type => $value) {
489            $key = array('base', $type);
490
491            // Finally verify that the data has actually been removed.
492            $stash = $this->testConstruct($key);
493            $data = $stash->get();
494            $this->assertNull($data, 'getData ' . $type . ' returns null once deleted');
495            $this->assertTrue($stash->isMiss(), 'isMiss returns true for deleted data');
496        }
497    }
498
499    public function testExtend()
500    {
501        $this->driver = null;
502        foreach ($this->data as $type => $value) {
503            $key = array('base', $type);
504
505            $stash = $this->testConstruct();
506            $stash->clear();
507
508
509            $stash = $this->testConstruct($key);
510            $stash->set($value, -600)->save();
511
512            $stash = $this->testConstruct($key);
513            $this->assertEquals($stash->extend(), $stash, 'extend returns item object');
514            $stash->save();
515
516            $stash = $this->testConstruct($key);
517            $data = $stash->get();
518            $this->assertEquals($value, $data, 'getData ' . $type . ' returns same item as stored and extended');
519            $this->assertFalse($stash->isMiss(), 'getData ' . $type . ' returns false for isMiss');
520        }
521    }
522
523
524    public function testDisable()
525    {
526        $stash = $this->testConstruct(array('path', 'to', 'key'));
527        $stash->disable();
528        $this->assertDisabledStash($stash);
529    }
530
531    public function testDisableCacheWillNeverCallDriver()
532    {
533        $item = $this->getItem();
534        $poolStub = new PoolGetDriverStub();
535        $poolStub->setDriver($this->getMockedDriver());
536        $item->setPool($poolStub);
537        $item->setKey(array('test', 'key'));
538        $item->disable();
539
540        $this->assertTrue($item->isDisabled());
541        $this->assertDisabledStash($item);
542    }
543
544    public function testDisableCacheGlobally()
545    {
546        Item::$runtimeDisable = true;
547        $testDriver = $this->getMockedDriver();
548
549        $item = $this->getItem();
550        $poolStub = new PoolGetDriverStub();
551        $poolStub->setDriver($this->getMockedDriver());
552        $item->setPool($poolStub);
553        $item->setKey(array('test', 'key'));
554
555        $this->assertDisabledStash($item);
556        $this->assertTrue($item->isDisabled());
557        $this->assertFalse($testDriver->wasCalled(), 'Driver was not called after Item was disabled.');
558        Item::$runtimeDisable = false;
559    }
560
561    private function getMockedDriver()
562    {
563        return new \Stash\Test\Stubs\DriverCallCheckStub();
564    }
565
566    private function assertDisabledStash(\Stash\Interfaces\ItemInterface $item)
567    {
568        $this->assertEquals($item, $item->set('true'), 'storeData returns self for disabled cache');
569        $this->assertNull($item->get(), 'getData returns null for disabled cache');
570        $this->assertFalse($item->clear(), 'clear returns false for disabled cache');
571        $this->assertTrue($item->isMiss(), 'isMiss returns true for disabled cache');
572        $this->assertFalse($item->extend(), 'extend returns false for disabled cache');
573        $this->assertTrue($item->lock(100), 'lock returns true for disabled cache');
574    }
575}
576