* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Stash\Test; use Stash\Item; use Stash\Invalidation; use Stash\Utilities; use Stash\Driver\Ephemeral; use Stash\Test\Stubs\PoolGetDriverStub; /** * @package Stash * @author Robert Hafner * * @todo find out why this has to be abstract to work (see https://github.com/tedivm/Stash/pull/10) */ abstract class AbstractItemTest extends \PHPUnit_Framework_TestCase { protected $data = array('string' => 'Hello world!', 'complexString' => "\t\t\t\tHello\r\n\rWorld!", 'int' => 4234, 'negint' => -6534, 'float' => 1.8358023545, 'negfloat' => -5.7003249023, 'false' => false, 'true' => true, 'null' => null, 'array' => array(3, 5, 7), 'hashmap' => array('one' => 1, 'two' => 2), 'multidemensional array' => array(array(5345), array(3, 'hello', false, array('one' => 1, 'two' => 2)) ) ); protected $expiration; protected $startTime; private $setup = false; protected $driver; protected $itemClass = '\Stash\Item'; public static function tearDownAfterClass() { Utilities::deleteRecursive(Utilities::getBaseDirectory()); } protected function setUp() { if (!$this->setup) { $this->startTime = time(); $this->expiration = $this->startTime + 3600; $this->data['object'] = new \stdClass(); } } /** * This just makes it slightly easier to extend AbstractCacheTest to * other Item types. * * @return \Stash\Interfaces\ItemInterface */ protected function getItem() { return new $this->itemClass(); } public function testConstruct($key = array()) { if (!isset($this->driver)) { $this->driver = new Ephemeral(array()); } $item = $this->getItem(); $this->assertTrue(is_a($item, 'Stash\Item'), 'Test object is an instance of Stash'); $poolStub = new PoolGetDriverStub(); $poolStub->setDriver($this->driver); $item->setPool($poolStub); $item->setKey($key); return $item; } public function testSetupKey() { $keyString = 'this/is/the/key'; $keyArray = array('this', 'is', 'the', 'key'); $keyNormalized = array('cache', 'this', 'is', 'the', 'key'); $stashArray = $this->testConstruct($keyArray); $this->assertAttributeInternalType('string', 'keyString', $stashArray, 'Argument based keys setup keystring'); $this->assertAttributeInternalType('array', 'key', $stashArray, 'Array based keys setup key'); $returnedKey = $stashArray->getKey(); $this->assertEquals($keyString, $returnedKey, 'getKey returns properly normalized key from array argument.'); } public function testSet() { foreach ($this->data as $type => $value) { $key = array('base', $type); $stash = $this->testConstruct($key); $this->assertAttributeInternalType('string', 'keyString', $stash, 'Argument based keys setup keystring'); $this->assertAttributeInternalType('array', 'key', $stash, 'Argument based keys setup key'); $this->assertTrue($stash->set($value)->save(), 'Driver class able to store data type ' . $type); } $item = $this->getItem(); $poolStub = new PoolGetDriverStub(); $poolStub->setDriver(new Ephemeral(array())); $item->setPool($poolStub); $this->assertFalse($item->set($this->data), 'Item without key returns false for set.'); } /** * @depends testSet */ public function testGet() { foreach ($this->data as $type => $value) { $key = array('base', $type); $stash = $this->testConstruct($key); $stash->set($value)->save(); // new object, but same backend $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertEquals($value, $data, 'getData ' . $type . ' returns same item as stored'); } if (!isset($this->driver)) { $this->driver = new Ephemeral(); } $item = $this->getItem(); $poolStub = new PoolGetDriverStub(); $poolStub->setDriver(new Ephemeral()); $item->setPool($poolStub); $this->assertEquals(null, $item->get(), 'Item without key returns null for get.'); } public function testGetItemInvalidKey() { try { $item = $this->getItem(); $poolStub = new PoolGetDriverStub(); $poolStub->setDriver(new Ephemeral(array())); $item->setPool($poolStub); $item->setKey('This is not an array'); } catch (\Throwable $t) { return; } catch (\Exception $expected) { return; } $this->fail('An expected exception has not been raised.'); } public function testLock() { $item = $this->getItem(); $poolStub = new PoolGetDriverStub(); $poolStub->setDriver(new Ephemeral()); $item->setPool($poolStub); $this->assertFalse($item->lock(), 'Item without key returns false for lock.'); } public function testInvalidation() { $key = array('path', 'to', 'item'); $oldValue = 'oldValue'; $newValue = 'newValue'; $runningStash = $this->testConstruct($key); $runningStash->set($oldValue)->expiresAfter(-300)->save(); // Test without stampede $controlStash = $this->testConstruct($key); $controlStash->setInvalidationMethod(Invalidation::VALUE, $newValue); $return = $controlStash->get(); $this->assertEquals($oldValue, $return, 'Old value is returned'); $this->assertTrue($controlStash->isMiss()); unset($controlStash); // Enable stampede control $runningStash->lock(); $this->assertAttributeEquals(true, 'stampedeRunning', $runningStash, 'Stampede flag is set.'); // Old $oldStash = $this->testConstruct($key); $oldStash->setInvalidationMethod(Invalidation::OLD); $return = $oldStash->get(Invalidation::OLD); $this->assertEquals($oldValue, $return, 'Old value is returned'); $this->assertFalse($oldStash->isMiss()); unset($oldStash); // Value $valueStash = $this->testConstruct($key); $valueStash->setInvalidationMethod(Invalidation::VALUE, $newValue); $return = $valueStash->get(Invalidation::VALUE, $newValue); $this->assertEquals($newValue, $return, 'New value is returned'); $this->assertFalse($valueStash->isMiss()); unset($valueStash); // Sleep $sleepStash = $this->testConstruct($key); $sleepStash->setInvalidationMethod(Invalidation::SLEEP, 250, 2); $start = microtime(true); $return = $sleepStash->get(); $end = microtime(true); $this->assertTrue($sleepStash->isMiss()); $sleepTime = ($end - $start) * 1000; $this->assertGreaterThan(500, $sleepTime, 'Sleep method sleeps for required time.'); $this->assertLessThan(520, $sleepTime, 'Sleep method does not oversleep.'); unset($sleepStash); // Unknown - if a random, unknown method is passed for invalidation we should rely on the default method $unknownStash = $this->testConstruct($key); $return = $unknownStash->get(78); $this->assertEquals($oldValue, $return, 'Old value is returned'); $this->assertTrue($unknownStash->isMiss(), 'Cache is marked as miss'); unset($unknownStash); // Test that storing the cache turns off stampede mode. $runningStash->set($newValue)->expiresAfter(30)->save(); $this->assertAttributeEquals(false, 'stampedeRunning', $runningStash, 'Stampede flag is off.'); unset($runningStash); // Precompute - test outside limit $precomputeStash = $this->testConstruct($key); $precomputeStash->setInvalidationMethod(Invalidation::PRECOMPUTE, 10); $return = $precomputeStash->get(Invalidation::PRECOMPUTE, 10); $this->assertFalse($precomputeStash->isMiss(), 'Cache is marked as hit'); unset($precomputeStash); // Precompute - test inside limit $precomputeStash = $this->testConstruct($key); $precomputeStash->setInvalidationMethod(Invalidation::PRECOMPUTE, 35); $return = $precomputeStash->get(); $this->assertTrue($precomputeStash->isMiss(), 'Cache is marked as miss'); unset($precomputeStash); // Test Stampede Flag Expiration $key = array('stampede', 'expire'); $Item_SPtest = $this->testConstruct($key); $Item_SPtest->setInvalidationMethod(Invalidation::VALUE, $newValue); $Item_SPtest->set($oldValue)->expiresAfter(-300)->save(); $Item_SPtest->lock(-5); $this->assertEquals($oldValue, $Item_SPtest->get(), 'Expired lock is ignored'); } public function testSetTTLDatetime() { $expiration = new \DateTime('now'); $expiration->add(new \DateInterval('P1D')); $key = array('ttl', 'expiration', 'test'); $stash = $this->testConstruct($key); $stash->set(array(1, 2, 3, 'apples')) ->setTTL($expiration) ->save(); $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp()); $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration'); $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp()); } public function testSetTTLDateInterval() { $interval = new \DateInterval('P1D'); $expiration = new \DateTime('now'); $expiration->add($interval); $key = array('ttl', 'expiration', 'test'); $stash = $this->testConstruct($key); $stash->set(array(1, 2, 3, 'apples')) ->setTTL($interval) ->save(); $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration'); $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp()); } public function testSetTTLNulll() { $key = array('ttl', 'expiration', 'test'); $stash = $this->testConstruct($key); $stash->set(array(1, 2, 3, 'apples')) ->setTTL(null) ->save(); $this->assertAttributeEquals(null, 'expiration', $stash); } public function testExpiresAt() { $expiration = new \DateTime('now'); $expiration->add(new \DateInterval('P1D')); $key = array('base', 'expiration', 'test'); $stash = $this->testConstruct($key); $stash->set(array(1, 2, 3, 'apples')) ->expiresAt($expiration) ->save(); $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration'); $this->assertLessThanOrEqual($expiration->getTimestamp(), $stash->getExpiration()->getTimestamp()); } /** * @expectedException Stash\Exception\InvalidArgumentException * @expectedExceptionMessage expiresAt requires \DateTimeInterface or null */ public function testExpiresAtException() { $stash = $this->testConstruct(array('base', 'expiration', 'test')); $stash->expiresAt(false); } public function testExpiresAfterWithDateTimeInterval() { $key = array('base', 'expiration', 'test'); $stash = $this->testConstruct($key); $stash->set(array(1, 2, 3, 'apples')) ->expiresAfter(new \DateInterval('P1D')) ->save(); $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertEquals(array(1, 2, 3, 'apples'), $data, 'getData returns data stores using a datetime expiration'); } public function testGetCreation() { $creation = new \DateTime('now'); $creation->add(new \DateInterval('PT10S')); // expire 10 seconds after createdOn $creationTS = $creation->getTimestamp(); $key = array('getCreation', 'test'); $stash = $this->testConstruct($key); $this->assertFalse($stash->getCreation(), 'no record exists yet, return null'); $stash->set(array('stuff'), $creation)->save(); $stash = $this->testConstruct($key); $createdOn = $stash->getCreation(); $this->assertInstanceOf('\DateTime', $createdOn, 'getCreation returns DateTime'); $itemCreationTimestamp = $createdOn->getTimestamp(); $this->assertEquals($creationTS - 10, $itemCreationTimestamp, 'createdOn is 10 seconds before expiration'); } public function testGetExpiration() { $expiration = new \DateTime('now'); $expiration->add(new \DateInterval('P1D')); $expirationTS = $expiration->getTimestamp(); $key = array('getExpiration', 'test'); $stash = $this->testConstruct($key); $currentDate = new \DateTime(); $returnedDate = $stash->getExpiration(); $this->assertLessThanOrEqual(2, $currentDate->getTimestamp() - $returnedDate->getTimestamp(), 'No record set, return as expired.'); $this->assertLessThanOrEqual(2, $returnedDate->getTimestamp() - $currentDate->getTimestamp(), 'No record set, return as expired.'); #$this->assertFalse($stash->getExpiration(), 'no record exists yet, return null'); $stash->set(array('stuff'))->expiresAt($expiration)->save(); $stash = $this->testConstruct($key); $itemExpiration = $stash->getExpiration(); $this->assertInstanceOf('\DateTime', $itemExpiration, 'getExpiration returns DateTime'); $itemExpirationTimestamp = $itemExpiration->getTimestamp(); $this->assertLessThanOrEqual($expirationTS, $itemExpirationTimestamp, 'sometime before explicitly set expiration'); } public function testIsMiss() { $stash = $this->testConstruct(array('This', 'Should', 'Fail')); $this->assertTrue($stash->isMiss(), 'isMiss returns true for missing data'); $data = $stash->get(); $this->assertNull($data, 'getData returns null for missing data'); $key = array('isMiss', 'test'); $stash = $this->testConstruct($key); $stash->set('testString')->save(); $stash = $this->testConstruct($key); $this->assertTrue(!$stash->isMiss(), 'isMiss returns false for valid data'); } public function testIsHit() { $stash = $this->testConstruct(array('This', 'Should', 'Fail')); $this->assertFalse($stash->isHit(), 'isHit returns false for missing data'); $data = $stash->get(); $this->assertNull($data, 'getData returns null for missing data'); $key = array('isHit', 'test'); $stash = $this->testConstruct($key); $stash->set('testString')->save(); $stash = $this->testConstruct($key); $this->assertTrue($stash->isHit(), 'isHit returns true for valid data'); } public function testClear() { // repopulate foreach ($this->data as $type => $value) { $key = array('base', $type); $stash = $this->testConstruct($key); $stash->set($value)->save(); $this->assertAttributeInternalType('string', 'keyString', $stash, 'Argument based keys setup keystring'); $this->assertAttributeInternalType('array', 'key', $stash, 'Argument based keys setup key'); $this->assertTrue($stash->set($value)->save(), 'Driver class able to store data type ' . $type); } foreach ($this->data as $type => $value) { $key = array('base', $type); // Make sure its actually populated. This has the added bonus of making sure one clear doesn't empty the // entire cache. $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertEquals($value, $data, 'getData ' . $type . ' returns same item as stored after other data is cleared'); // Run the clear, make sure it says it works. $stash = $this->testConstruct($key); $this->assertTrue($stash->clear(), 'clear returns true'); // Finally verify that the data has actually been removed. $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertNull($data, 'getData ' . $type . ' returns null once deleted'); $this->assertTrue($stash->isMiss(), 'isMiss returns true for deleted data'); } // repopulate foreach ($this->data as $type => $value) { $key = array('base', $type); $stash = $this->testConstruct($key); $stash->set($value)->save(); } // clear $stash = $this->testConstruct(); $this->assertTrue($stash->clear(), 'clear returns true'); // make sure all the keys are gone. foreach ($this->data as $type => $value) { $key = array('base', $type); // Finally verify that the data has actually been removed. $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertNull($data, 'getData ' . $type . ' returns null once deleted'); $this->assertTrue($stash->isMiss(), 'isMiss returns true for deleted data'); } } public function testExtend() { $this->driver = null; foreach ($this->data as $type => $value) { $key = array('base', $type); $stash = $this->testConstruct(); $stash->clear(); $stash = $this->testConstruct($key); $stash->set($value, -600)->save(); $stash = $this->testConstruct($key); $this->assertEquals($stash->extend(), $stash, 'extend returns item object'); $stash->save(); $stash = $this->testConstruct($key); $data = $stash->get(); $this->assertEquals($value, $data, 'getData ' . $type . ' returns same item as stored and extended'); $this->assertFalse($stash->isMiss(), 'getData ' . $type . ' returns false for isMiss'); } } public function testDisable() { $stash = $this->testConstruct(array('path', 'to', 'key')); $stash->disable(); $this->assertDisabledStash($stash); } public function testDisableCacheWillNeverCallDriver() { $item = $this->getItem(); $poolStub = new PoolGetDriverStub(); $poolStub->setDriver($this->getMockedDriver()); $item->setPool($poolStub); $item->setKey(array('test', 'key')); $item->disable(); $this->assertTrue($item->isDisabled()); $this->assertDisabledStash($item); } public function testDisableCacheGlobally() { Item::$runtimeDisable = true; $testDriver = $this->getMockedDriver(); $item = $this->getItem(); $poolStub = new PoolGetDriverStub(); $poolStub->setDriver($this->getMockedDriver()); $item->setPool($poolStub); $item->setKey(array('test', 'key')); $this->assertDisabledStash($item); $this->assertTrue($item->isDisabled()); $this->assertFalse($testDriver->wasCalled(), 'Driver was not called after Item was disabled.'); Item::$runtimeDisable = false; } private function getMockedDriver() { return new \Stash\Test\Stubs\DriverCallCheckStub(); } private function assertDisabledStash(\Stash\Interfaces\ItemInterface $item) { $this->assertEquals($item, $item->set('true'), 'storeData returns self for disabled cache'); $this->assertNull($item->get(), 'getData returns null for disabled cache'); $this->assertFalse($item->clear(), 'clear returns false for disabled cache'); $this->assertTrue($item->isMiss(), 'isMiss returns true for disabled cache'); $this->assertFalse($item->extend(), 'extend returns false for disabled cache'); $this->assertTrue($item->lock(100), 'lock returns true for disabled cache'); } }