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