1package CHI::t::Driver; 2$CHI::t::Driver::VERSION = '0.60'; 3use strict; 4use warnings; 5use CHI::Test; 6use CHI::Test::Util 7 qw(activate_test_logger cmp_bool is_between random_string skip_until); 8use CHI::Util qw(can_load dump_one_line write_file); 9use Encode; 10use File::Spec::Functions qw(tmpdir); 11use File::Temp qw(tempdir); 12use List::Util qw(shuffle); 13use Scalar::Util qw(weaken); 14use Storable qw(dclone); 15use Test::Warn; 16use Time::HiRes qw(usleep); 17use base qw(CHI::Test::Class); 18 19# Flags indicating what each test driver supports 20sub supports_clear { 1 } 21sub supports_expires_on_backend { 0 } 22sub supports_get_namespaces { 1 } 23 24sub standard_keys_and_values : Test(startup) { 25 my ($self) = @_; 26 27 my ( $keys_ref, $values_ref ) = $self->set_standard_keys_and_values(); 28 $self->{keys} = $keys_ref; 29 $self->{values} = $values_ref; 30 $self->{keynames} = [ keys( %{$keys_ref} ) ]; 31 $self->{key_count} = scalar( @{ $self->{keynames} } ); 32 $self->{all_test_keys} = [ values(%$keys_ref), $self->extra_test_keys() ]; 33 my $cache = $self->new_cache(); 34 push( 35 @{ $self->{all_test_keys} }, 36 $self->process_keys( $cache, @{ $self->{all_test_keys} } ) 37 ); 38 $self->{all_test_keys_hash} = 39 { map { ( $_, 1 ) } @{ $self->{all_test_keys} } }; 40} 41 42sub kvpair { 43 my $self = shift; 44 my $count = shift || 1; 45 46 return map { 47 ( 48 $self->{keys}->{medium} . ( $_ == 1 ? '' : $_ ), 49 $self->{values}->{medium} . ( $_ == 1 ? '' : $_ ) 50 ) 51 } ( 1 .. $count ); 52} 53 54sub setup : Test(setup) { 55 my $self = shift; 56 57 $self->{cache} = $self->new_cache(); 58 $self->{cache}->clear() if $self->supports_clear(); 59} 60 61sub testing_driver_class { 62 my $self = shift; 63 my $class = ref($self); 64 65 # By default, take the last part of the classname and use it as driver 66 my $driver_class = 'CHI::Driver::' . ( split( '::', $class ) )[-1]; 67 return $driver_class; 68} 69 70sub testing_chi_root_class { 71 return 'CHI'; 72} 73 74sub new_cache { 75 my $self = shift; 76 77 return $self->testing_chi_root_class->new( $self->new_cache_options(), @_ ); 78} 79 80sub new_cleared_cache { 81 my $self = shift; 82 83 my $cache = $self->new_cache(@_); 84 $cache->clear(); 85 return $cache; 86} 87 88sub new_cache_options { 89 my $self = shift; 90 91 return ( 92 driver => '+' . $self->testing_driver_class(), 93 on_get_error => 'die', 94 on_set_error => 'die' 95 ); 96} 97 98sub set_standard_keys_and_values { 99 my $self = shift; 100 101 my ( %keys, %values ); 102 my @mixed_chars = ( 32 .. 48, 57 .. 65, 90 .. 97, 122 .. 126, 240 ); 103 104 %keys = ( 105 'space' => ' ', 106 'newline' => "\n", 107 'char' => 'a', 108 'zero' => 0, 109 'one' => 1, 110 'medium' => 'medium', 111 'mixed' => join( "", map { chr($_) } @mixed_chars ), 112 'binary' => join( "", map { chr($_) } ( 129 .. 255 ) ), 113 'large' => scalar( 'ab' x 256 ), 114 'empty' => 'empty', 115 'arrayref' => [ 1, 2 ], 116 'hashref' => { foo => [ 'bar', 'baz' ] }, 117 'utf8' => "Have \x{263a} a nice day", 118 ); 119 120 %values = map { 121 ( $_, ref( $keys{$_} ) ? $keys{$_} : scalar( reverse( $keys{$_} ) ) ) 122 } keys(%keys); 123 $values{empty} = ''; 124 125 return ( \%keys, \%values ); 126} 127 128# Extra keys (beyond the standard keys above) that we may use in these 129# tests. We need to adhere to this for the benefit of drivers that don't 130# support get_keys (like memcached) - they simulate get_keys(), clear(), 131# etc. by using this fixed list of keys. 132# 133sub extra_test_keys { 134 my ($class) = @_; 135 return ( 136 '', '2', 137 'medium2', 'foo', 138 'hashref', 'test_namespace_types', 139 "utf8", "encoded", 140 "binary", ( map { "done$_" } ( 0 .. 2 ) ), 141 ( map { "key$_" } ( 0 .. 20 ) ) 142 ); 143} 144 145sub set_some_keys { 146 my ( $self, $c ) = @_; 147 148 foreach my $keyname ( @{ $self->{keynames} } ) { 149 $c->set( $self->{keys}->{$keyname}, $self->{values}->{$keyname} ); 150 } 151} 152 153sub test_encode : Tests { 154 my $self = shift; 155 my $cache = $self->new_cleared_cache(); 156 157 my $utf8 = $self->{keys}->{utf8}; 158 my $encoded = encode( utf8 => $utf8 ); 159 my $binary_off = $self->{keys}->{binary}; 160 my $binary_on = substr( $binary_off . $utf8, 0, length($binary_off) ); 161 162 ok( $binary_off eq $binary_on, "binary_off eq binary_on" ); 163 ok( !Encode::is_utf8($binary_off), "!is_utf8(binary_off)" ); 164 ok( Encode::is_utf8($binary_on), "is_utf8(binary_on)" ); 165 166 # Key maps to same thing whether encoded or non-encoded 167 # 168 my $value = time; 169 $cache->set( $utf8, $value ); 170 is( $cache->get($utf8), $value, "get" ); 171 is( $cache->get($encoded), 172 $value, "encoded and non-encoded map to same value" ); 173 174 # Key maps to same thing whether utf8 flag is off or on 175 # 176 # Commenting out for now - this is broken on FastMmap and 177 # DBI drivers (at least), and not entirely sure whether or 178 # with what priority we should demand this behavior. 179 # 180 if (0) { 181 $cache->set( $binary_off, $value ); 182 is( $cache->get($binary_off), $value, "get binary_off" ); 183 is( $cache->get($binary_on), 184 $value, "binary_off and binary_on map to same value" ); 185 $cache->clear($binary_on); 186 ok( !$cache->get($binary_off), "cleared binary_off" ); # 187 } 188 189 # Value is maintained as a utf8 or binary string, in scalar or in arrayref 190 $cache->set( "utf8", $utf8 ); 191 is( $cache->get("utf8"), $utf8, "utf8 in scalar" ); 192 $cache->set( "utf8", [$utf8] ); 193 is( $cache->get("utf8")->[0], $utf8, "utf8 in arrayref" ); 194 195 $cache->set( "encoded", $encoded ); 196 is( $cache->get("encoded"), $encoded, "encoded in scalar" ); 197 $cache->set( "encoded", [$encoded] ); 198 is( $cache->get("encoded")->[0], $encoded, "encoded in arrayref" ); 199 200 # Value retrieves as same thing whether stored with utf8 flag off or on 201 # 202 $cache->set( "binary", $binary_off ); 203 is( $cache->get("binary"), $binary_on, "stored binary_off = binary_on" ); 204 $cache->set( "binary", $binary_on ); 205 is( $cache->get("binary"), $binary_off, "stored binary_on = binary_off" ); 206} 207 208sub test_simple : Tests { 209 my $self = shift; 210 my $cache = shift || $self->{cache}; 211 212 ok( $cache->set( $self->{keys}->{medium}, $self->{values}->{medium} ) ); 213 is( $cache->get( $self->{keys}->{medium} ), $self->{values}->{medium} ); 214} 215 216sub test_driver_class : Tests { 217 my $self = shift; 218 my $cache = $self->{cache}; 219 220 isa_ok( $cache, 'CHI::Driver' ); 221 isa_ok( $cache, $cache->driver_class ); 222 can_ok( $cache, 'get', 'set', 'remove', 'clear', 'expire' ); 223} 224 225sub test_key_types : Tests { 226 my $self = shift; 227 my $cache = $self->{cache}; 228 $self->num_tests( $self->{key_count} * 9 + 1 ); 229 230 my @keys_set; 231 my $check_keys_set = sub { 232 my $desc = shift; 233 cmp_set( [ $cache->get_keys ], \@keys_set, "checking keys $desc" ); 234 }; 235 236 $check_keys_set->("before sets"); 237 foreach my $keyname ( @{ $self->{keynames} } ) { 238 my $key = $self->{keys}->{$keyname}; 239 my $value = $self->{values}->{$keyname}; 240 ok( !defined $cache->get($key), "miss for key '$keyname'" ); 241 is( $cache->set( $key, $value ), $value, "set for key '$keyname'" ); 242 push( @keys_set, $self->process_keys( $cache, $key ) ); 243 $check_keys_set->("after set of key '$keyname'"); 244 cmp_deeply( $cache->get($key), $value, "hit for key '$keyname'" ); 245 } 246 247 foreach my $keyname ( reverse @{ $self->{keynames} } ) { 248 my $key = $self->{keys}->{$keyname}; 249 $cache->remove($key); 250 ok( !defined $cache->get($key), 251 "miss after remove for key '$keyname'" ); 252 pop(@keys_set); 253 $check_keys_set->("after removal of key '$keyname'"); 254 } 255 256 # Confirm that transform_key is idempotent 257 # 258 foreach my $keyname ( @{ $self->{keynames} } ) { 259 my $key = $self->{keys}->{$keyname}; 260 my $value = $self->{values}->{$keyname}; 261 is( 262 $cache->transform_key( $cache->transform_key($key) ), 263 $cache->transform_key($key), 264 "transform_key is idempotent for '$keyname'" 265 ); 266 $cache->clear(); 267 $cache->set( $key, $value ); 268 is( scalar( $cache->get_keys() ), 1, "exactly one key" ); 269 cmp_deeply( $cache->get( ( $cache->get_keys )[0] ), 270 $value, "get with get_keys[0] got same value" ); 271 } 272} 273 274sub test_deep_copy : Tests { 275 my $self = shift; 276 my $cache = $self->{cache}; 277 278 $self->set_some_keys($cache); 279 foreach my $keyname (qw(arrayref hashref)) { 280 my $key = $self->{keys}->{$keyname}; 281 my $value = $self->{values}->{$keyname}; 282 cmp_deeply( $cache->get($key), $value, 283 "get($key) returns original data structure" ); 284 cmp_deeply( $cache->get($key), $cache->get($key), 285 "multiple get($key) return same data structure" ); 286 isnt( $cache->get($key), $value, 287 "get($key) does not return original reference" ); 288 isnt( $cache->get($key), $cache->get($key), 289 "multiple get($key) do not return same reference" ); 290 } 291 292 my $struct = { a => [ 1, 2 ], b => [ 4, 5 ] }; 293 my $struct2 = dclone($struct); 294 $cache->set( 'hashref', $struct ); 295 push( @{ $struct->{a} }, 3 ); 296 delete( $struct->{b} ); 297 cmp_deeply( $cache->get('hashref'), 298 $struct2, 299 "altering original set structure does not affect cached copy" ); 300} 301 302sub test_expires_immediately : Tests { 303 my $self = shift; 304 305 return 'author testing only - timing is unreliable' 306 unless ( $ENV{AUTHOR_TESTING} ); 307 308 # expires_in default should be ignored 309 my $cache = $self->new_cache( expires_in => '1 hour' ); 310 311 # Expires immediately 312 my $test_expires_immediately = sub { 313 my ($set_option) = @_; 314 my ( $key, $value ) = $self->kvpair(); 315 my $desc = dump_one_line($set_option); 316 is( $cache->set( $key, $value, $set_option ), $value, "set ($desc)" ); 317 is_between( 318 $cache->get_expires_at($key), 319 time() - 4, 320 time(), "expires_at ($desc)" 321 ); 322 ok( $cache->exists_and_is_expired($key), "is_expired ($desc)" ); 323 ok( !defined $cache->get($key), "immediate miss ($desc)" ); 324 }; 325 $test_expires_immediately->(0); 326 $test_expires_immediately->(-1); 327 $test_expires_immediately->("0 seconds"); 328 $test_expires_immediately->("0 hours"); 329 $test_expires_immediately->("-1 seconds"); 330 $test_expires_immediately->( { expires_in => "0 seconds" } ); 331 $test_expires_immediately->( { expires_at => time - 1 } ); 332 $test_expires_immediately->("now"); 333} 334 335sub test_expires_shortly : Tests { 336 my $self = shift; 337 338 return 'author testing only - timing is unreliable' 339 unless ( $ENV{AUTHOR_TESTING} ); 340 341 # expires_in default should be ignored 342 my $cache = $self->new_cache( expires_in => '1 hour' ); 343 344 # Expires shortly (real time) 345 my $test_expires_shortly = sub { 346 my ($set_option) = @_; 347 my ( $key, $value ) = $self->kvpair(); 348 my $desc = "set_option = " . dump_one_line($set_option); 349 my $start_time = time(); 350 is( $cache->set( $key, $value, $set_option ), $value, "set ($desc)" ); 351 is( $cache->get($key), $value, "hit ($desc)" ); 352 is_between( 353 $cache->get_expires_at($key), 354 $start_time + 1, 355 $start_time + 8, 356 "expires_at ($desc)" 357 ); 358 ok( !$cache->exists_and_is_expired($key), "not expired ($desc)" ); 359 ok( $cache->is_valid($key), "valid ($desc)" ); 360 361 # Only bother sleeping and expiring for one of the variants 362 if ( $set_option eq "3 seconds" ) { 363 sleep(3); 364 ok( !defined $cache->get($key), "miss after 2 seconds ($desc)" ); 365 ok( $cache->exists_and_is_expired($key), "is_expired ($desc)" ); 366 ok( !$cache->is_valid($key), "invalid ($desc)" ); 367 } 368 }; 369 $test_expires_shortly->(3); 370 $test_expires_shortly->("3 seconds"); 371 $test_expires_shortly->( { expires_at => time + 3 } ); 372} 373 374sub test_expires_later : Tests { 375 my $self = shift; 376 377 return 'author testing only - timing is unreliable' 378 unless ( $ENV{AUTHOR_TESTING} ); 379 380 # expires_in default should be ignored 381 my $cache = $self->new_cache( expires_in => '1s' ); 382 383 # Expires later (test time) 384 my $test_expires_later = sub { 385 my ($set_option) = @_; 386 my ( $key, $value ) = $self->kvpair(); 387 my $desc = "set_option = " . dump_one_line($set_option); 388 is( $cache->set( $key, $value, $set_option ), $value, "set ($desc)" ); 389 is( $cache->get($key), $value, "hit ($desc)" ); 390 my $start_time = time(); 391 is_between( 392 $cache->get_expires_at($key), 393 $start_time + 3580, 394 $start_time + 3620, 395 "expires_at ($desc)" 396 ); 397 ok( !$cache->exists_and_is_expired($key), "not expired ($desc)" ); 398 ok( $cache->is_valid($key), "valid ($desc)" ); 399 local $CHI::Driver::Test_Time = $start_time + 3590; 400 ok( !$cache->exists_and_is_expired($key), "not expired ($desc)" ); 401 ok( $cache->is_valid($key), "valid ($desc)" ); 402 local $CHI::Driver::Test_Time = $start_time + 3610; 403 ok( !defined $cache->get($key), "miss after 1 hour ($desc)" ); 404 ok( $cache->exists_and_is_expired($key), "is_expired ($desc)" ); 405 ok( !$cache->is_valid($key), "invalid ($desc)" ); 406 }; 407 $test_expires_later->(3600); 408 $test_expires_later->("1 hour"); 409 $test_expires_later->( { expires_at => time + 3600 } ); 410} 411 412sub test_expires_never : Tests { 413 my $self = shift; 414 my $cache; 415 416 # Expires never (will fail in 2037) 417 my ( $key, $value ) = $self->kvpair(); 418 my $test_expires_never = sub { 419 my (@set_options) = @_; 420 $cache->set( $key, $value, @set_options ); 421 ok( 422 $cache->get_expires_at($key) > 423 time + Time::Duration::Parse::parse_duration('1 year'), 424 "expires never" 425 ); 426 ok( !$cache->exists_and_is_expired($key), "not expired" ); 427 ok( $cache->is_valid($key), "valid" ); 428 }; 429 430 # never is default 431 $cache = $self->new_cache(); 432 $test_expires_never->(); 433 434 # expires_in default should be ignored when never passed to set (RT #67970) 435 $cache = $self->new_cache( expires_in => '1s' ); 436 $test_expires_never->('never'); 437} 438 439sub test_expires_defaults : Tests { 440 my $self = shift; 441 442 my $start_time = time(); 443 local $CHI::Driver::Test_Time = $start_time; 444 my $cache; 445 446 my $set_and_confirm_expires_at = sub { 447 my ( $expected_expires_at, $desc ) = @_; 448 my ( $key, $value ) = $self->kvpair(); 449 $cache->set( $key, $value ); 450 is( $cache->get_expires_at($key), $expected_expires_at, $desc ); 451 $cache->clear(); 452 }; 453 454 $cache = $self->new_cache( expires_in => 10 ); 455 $set_and_confirm_expires_at->( 456 $start_time + 10, 457 "after expires_in constructor option" 458 ); 459 $cache->expires_in(20); 460 $set_and_confirm_expires_at->( $start_time + 20, 461 "after expires_in method" ); 462 463 $cache = $self->new_cache( expires_at => $start_time + 30 ); 464 $set_and_confirm_expires_at->( 465 $start_time + 30, 466 "after expires_at constructor option" 467 ); 468 $cache->expires_at( $start_time + 40 ); 469 $set_and_confirm_expires_at->( $start_time + 40, 470 "after expires_at method" ); 471} 472 473sub test_expires_manually : Tests { 474 my $self = shift; 475 my $cache = $self->{cache}; 476 477 my ( $key, $value ) = $self->kvpair(); 478 my $desc = "expires manually"; 479 $cache->set( $key, $value ); 480 is( $cache->get($key), $value, "hit ($desc)" ); 481 $cache->expire($key); 482 ok( !defined $cache->get($key), "miss after expire ($desc)" ); 483 ok( !$cache->is_valid($key), "invalid after expire ($desc)" ); 484} 485 486sub test_expires_conditionally : Tests { 487 my $self = shift; 488 my $cache = $self->{cache}; 489 490 # Expires conditionally 491 my $test_expires_conditionally = sub { 492 my ( $code, $cond_desc, $expect_expire ) = @_; 493 494 my ( $key, $value ) = $self->kvpair(); 495 my $desc = "expires conditionally ($cond_desc)"; 496 $cache->set( $key, $value ); 497 is( 498 $cache->get( $key, expire_if => $code ), 499 $expect_expire ? undef : $value, 500 "get result ($desc)" 501 ); 502 503 is( $cache->get($key), $value, "hit after expire_if ($desc)" ); 504 505 }; 506 my $time = time(); 507 $test_expires_conditionally->( sub { 1 }, 'true', 1 ); 508 $test_expires_conditionally->( sub { 0 }, 'false', 0 ); 509 $test_expires_conditionally->( 510 sub { $_[0]->created_at >= $time }, 511 'created_at >= now', 1 512 ); 513 $test_expires_conditionally->( 514 sub { $_[0]->created_at < $time }, 515 'created_at < now', 0 516 ); 517} 518 519sub test_expires_variance : Tests { 520 my $self = shift; 521 my $cache = $self->{cache}; 522 523 my $start_time = time(); 524 my $expires_at = $start_time + 10; 525 my ( $key, $value ) = $self->kvpair(); 526 $cache->set( $key, $value, 527 { expires_at => $expires_at, expires_variance => 0.5 } ); 528 is( $cache->get_object($key)->expires_at(), 529 $expires_at, "expires_at = $start_time" ); 530 is( 531 $cache->get_object($key)->early_expires_at(), 532 $start_time + 5, 533 "early_expires_at = $start_time + 5" 534 ); 535 536 my %expire_count; 537 for ( my $time = $start_time + 3 ; $time <= $expires_at + 1 ; $time++ ) { 538 local $CHI::Driver::Test_Time = $time; 539 for ( my $i = 0 ; $i < 100 ; $i++ ) { 540 if ( !defined $cache->get($key) ) { 541 $expire_count{$time}++; 542 } 543 } 544 } 545 for ( my $time = $start_time + 3 ; $time <= $start_time + 5 ; $time++ ) { 546 ok( !$expire_count{$time}, "got no expires at $time" ); 547 } 548 for ( my $time = $start_time + 7 ; $time <= $start_time + 8 ; $time++ ) { 549 ok( $expire_count{$time} > 0 && $expire_count{$time} < 100, 550 "got some expires at $time" ); 551 } 552 for ( my $time = $expires_at ; $time <= $expires_at + 1 ; $time++ ) { 553 ok( $expire_count{$time} == 100, "got all expires at $time" ); 554 } 555} 556 557sub test_not_in_cache : Tests { 558 my $self = shift; 559 my $cache = $self->{cache}; 560 561 ok( !defined $cache->get_object('not in cache') ); 562 ok( !defined $cache->get_expires_at('not in cache') ); 563 ok( !$cache->is_valid('not in cache') ); 564} 565 566sub test_serialize : Tests { 567 my $self = shift; 568 my $cache = $self->{cache}; 569 $self->num_tests( $self->{key_count} ); 570 571 $self->set_some_keys($cache); 572 foreach my $keyname ( @{ $self->{keynames} } ) { 573 my $expect_transformed = 574 ( $keyname eq 'arrayref' || $keyname eq 'hashref' ) ? 1 575 : ( $keyname eq 'utf8' ) ? 2 576 : 0; 577 is( 578 $cache->get_object( $self->{keys}->{$keyname} )->_is_transformed(), 579 $expect_transformed, 580 "is_transformed = $expect_transformed ($keyname)" 581 ); 582 } 583} 584 585{ 586 package DummySerializer; 587$DummySerializer::VERSION = '0.60'; 588sub serialize { } 589 sub deserialize { } 590} 591 592sub test_serializers : Tests { 593 my ($self) = @_; 594 595 unless ( can_load('Data::Serializer') ) { 596 $self->num_tests(1); 597 return 'Data::Serializer not installed'; 598 } 599 600 my @modes = (qw(string hash object)); 601 my @variants = (qw(Storable Data::Dumper YAML)); 602 @variants = grep { can_load($_) } @variants; 603 ok( scalar(@variants), "some variants ok" ); 604 605 my $initial_count = 5; 606 my $test_key_types_count = $self->{key_count}; 607 my $test_count = $initial_count + 608 scalar(@variants) * scalar(@modes) * ( 1 + $test_key_types_count ); 609 610 my $cache1 = $self->new_cache(); 611 isa_ok( $cache1->serializer, 'CHI::Serializer::Storable' ); 612 my $cache2 = $self->new_cache(); 613 is( $cache1->serializer, $cache2->serializer, 614 'same serializer returned from two objects' ); 615 616 throws_ok( 617 sub { 618 $self->new_cache( serializer => [1] ); 619 }, 620 qr/Validation failed for|isa check for ".*?" failed/, 621 "invalid serializer" 622 ); 623 lives_ok( 624 sub { $self->new_cache( serializer => bless( {}, 'DummySerializer' ) ) } 625 , 626 "valid dummy serializer" 627 ); 628 629 foreach my $mode (@modes) { 630 foreach my $variant (@variants) { 631 my $serializer_param = ( 632 $mode eq 'string' ? $variant 633 : $mode eq 'hash' ? { serializer => $variant } 634 : Data::Serializer->new( serializer => $variant ) 635 ); 636 my $cache = $self->new_cache( serializer => $serializer_param ); 637 is( $cache->serializer->serializer, 638 $variant, "serializer = $variant, mode = $mode" ); 639 $self->{cache} = $cache; 640 641 foreach my $keyname ( @{ $self->{keynames} } ) { 642 my $key = $self->{keys}->{$keyname}; 643 my $value = $self->{values}->{$keyname}; 644 $cache->set( $key, $value ); 645 cmp_deeply( $cache->get($key), $value, 646 "hit for key '$keyname'" ); 647 } 648 649 $self->num_tests($test_count); 650 } 651 } 652} 653 654sub test_namespaces : Tests { 655 my $self = shift; 656 my $cache = $self->{cache}; 657 658 my $cache0 = $self->new_cache(); 659 is( $cache0->namespace, 'Default', 'namespace defaults to "Default"' ); 660 661 my ( $ns1, $ns2, $ns3 ) = ( 'ns1', 'ns2', 'ns3' ); 662 my ( $cache1, $cache1a, $cache2, $cache3 ) = 663 map { $self->new_cache( namespace => $_ ) } ( $ns1, $ns1, $ns2, $ns3 ); 664 cmp_deeply( 665 [ map { $_->namespace } ( $cache1, $cache1a, $cache2, $cache3 ) ], 666 [ $ns1, $ns1, $ns2, $ns3 ], 667 'cache->namespace()' 668 ); 669 $self->set_some_keys($cache1); 670 cmp_deeply( 671 $cache1->dump_as_hash(), 672 $cache1a->dump_as_hash(), 673 'cache1 and cache1a are same cache' 674 ); 675 cmp_deeply( [ $cache2->get_keys() ], 676 [], 'cache2 empty after setting keys in cache1' ); 677 $cache3->set( $self->{keys}->{medium}, 'different' ); 678 is( 679 $cache1->get('medium'), 680 $self->{values}->{medium}, 681 'cache1{medium} = medium' 682 ); 683 is( $cache3->get('medium'), 'different', 'cache1{medium} = different' ); 684 685 if ( $self->supports_get_namespaces() ) { 686 687 # get_namespaces may or may not automatically include empty namespaces 688 cmp_deeply( 689 [ $cache1->get_namespaces() ], 690 supersetof( $ns1, $ns3 ), 691 "get_namespaces contains $ns1 and $ns3" 692 ); 693 694 foreach my $c ( $cache0, $cache1, $cache1a, $cache2, $cache3 ) { 695 cmp_set( 696 [ $cache->get_namespaces() ], 697 [ $c->get_namespaces() ], 698 'get_namespaces the same regardless of which cache asks' 699 ); 700 } 701 } 702 else { 703 throws_ok( 704 sub { $cache1->get_namespaces() }, 705 qr/not supported/, 706 "get_namespaces not supported" 707 ); 708 SKIP: { skip "get_namespaces not supported", 5 } 709 } 710} 711 712sub test_persist : Tests { 713 my $self = shift; 714 my $cache = $self->{cache}; 715 716 my $hash; 717 { 718 my $cache1 = $self->new_cache(); 719 $self->set_some_keys($cache1); 720 $hash = $cache1->dump_as_hash(); 721 } 722 my $cache2 = $self->new_cache(); 723 cmp_deeply( 724 $hash, 725 $cache2->dump_as_hash(), 726 'cache persisted between cache object creations' 727 ); 728} 729 730sub test_multi : Tests { 731 my $self = shift; 732 my $cache = $self->{cache}; 733 734 my ( $keys, $values, $keynames ) = 735 ( $self->{keys}, $self->{values}, $self->{keynames} ); 736 737 my @ordered_keys = map { $keys->{$_} } @{$keynames}; 738 my @ordered_values = 739 map { $values->{$_} } @{$keynames}; 740 my %ordered_scalar_key_values = 741 map { ( $keys->{$_}, $values->{$_} ) } 742 grep { !ref( $keys->{$_} ) } @{$keynames}; 743 744 cmp_deeply( $cache->get_multi_arrayref( ['foo'] ), 745 [undef], "get_multi_arrayref before set" ); 746 747 $cache->set_multi( \%ordered_scalar_key_values ); 748 $cache->set( $keys->{arrayref}, $values->{arrayref} ); 749 $cache->set( $keys->{hashref}, $values->{hashref} ); 750 751 cmp_deeply( $cache->get_multi_arrayref( \@ordered_keys ), 752 \@ordered_values, "get_multi_arrayref" ); 753 cmp_deeply( $cache->get( $ordered_keys[0] ), 754 $ordered_values[0], "get one after set_multi" ); 755 cmp_deeply( 756 $cache->get_multi_arrayref( [ reverse @ordered_keys ] ), 757 [ reverse @ordered_values ], 758 "get_multi_arrayref" 759 ); 760 cmp_deeply( 761 $cache->get_multi_hashref( [ grep { !ref($_) } @ordered_keys ] ), 762 \%ordered_scalar_key_values, "get_multi_hashref" ); 763 cmp_set( 764 [ $cache->get_keys ], 765 [ $self->process_keys( $cache, @ordered_keys ) ], 766 "get_keys after set_multi" 767 ); 768 769 $cache->remove_multi( \@ordered_keys ); 770 cmp_deeply( 771 $cache->get_multi_arrayref( \@ordered_keys ), 772 [ (undef) x scalar(@ordered_values) ], 773 "get_multi_arrayref after remove_multi" 774 ); 775 cmp_set( [ $cache->get_keys ], [], "get_keys after remove_multi" ); 776} 777 778sub test_multi_no_keys : Tests { 779 my $self = shift; 780 my $cache = $self->{cache}; 781 782 cmp_deeply( $cache->get_multi_arrayref( [] ), 783 [], "get_multi_arrayref (no args)" ); 784 cmp_deeply( $cache->get_multi_hashref( [] ), 785 {}, "get_multi_hashref (no args)" ); 786 lives_ok { $cache->set_multi( {} ) } "set_multi (no args)"; 787 lives_ok { $cache->remove_multi( [] ) } "remove_multi (no args)"; 788} 789 790sub test_l1_cache : Tests { 791 my $self = shift; 792 my @keys = map { "key$_" } ( 0 .. 2 ); 793 my @values = map { "value$_" } ( 0 .. 2 ); 794 my ( $cache, $l1_cache ); 795 796 return "skipping - no support for clear" unless $self->supports_clear(); 797 798 my $test_l1_cache = sub { 799 800 is( $l1_cache->subcache_type, "l1_cache", "subcache_type = l1_cache" ); 801 802 # Get on cache should populate l1 cache 803 # 804 $cache->set( $keys[0], $values[0] ); 805 $l1_cache->clear(); 806 ok( !$l1_cache->get( $keys[0] ), "l1 miss after clear" ); 807 is( $cache->get( $keys[0] ), 808 $values[0], "primary hit after primary set" ); 809 is( $l1_cache->get( $keys[0] ), $values[0], 810 "l1 hit after primary get" ); 811 812 # Primary cache should be reading l1 cache first 813 # 814 $l1_cache->set( $keys[0], $values[1] ); 815 is( $cache->get( $keys[0] ), 816 $values[1], "got new value set explicitly in l1 cache" ); 817 $l1_cache->remove( $keys[0] ); 818 is( $cache->get( $keys[0] ), $values[0], "got old value again" ); 819 820 $cache->clear(); 821 ok( !$cache->get( $keys[0] ), "miss after clear" ); 822 ok( !$l1_cache->get( $keys[0] ), "miss after clear" ); 823 824 # get_multi_* - one from l1 cache, one from primary cache, one miss 825 # 826 $cache->set( $keys[0], $values[0] ); 827 $cache->set( $keys[1], $values[1] ); 828 $l1_cache->remove( $keys[0] ); 829 $l1_cache->set( $keys[1], $values[2] ); 830 831 cmp_deeply( 832 $cache->get_multi_arrayref( [ $keys[0], $keys[1], $keys[2] ] ), 833 [ $values[0], $values[2], undef ], 834 "get_multi_arrayref" 835 ); 836 cmp_deeply( 837 $cache->get_multi_hashref( [ $keys[0], $keys[1], $keys[2] ] ), 838 { 839 $keys[0] => $values[0], 840 $keys[1] => $values[2], 841 $keys[2] => undef 842 }, 843 "get_multi_hashref" 844 ); 845 846 $self->_test_logging_with_l1_cache( $cache, $l1_cache ); 847 848 $self->_test_common_subcache_features( $cache, $l1_cache, 'l1_cache' ); 849 }; 850 851 # Test with current cache in primary position... 852 # 853 $cache = 854 $self->new_cache( l1_cache => { driver => 'Memory', datastore => {} } ); 855 $l1_cache = $cache->l1_cache; 856 isa_ok( $cache, $self->testing_driver_class, 'cache' ); 857 isa_ok( $l1_cache, 'CHI::Driver::Memory', 'l1_cache' ); 858 $test_l1_cache->(); 859 860 # and in l1 position 861 # 862 $cache = $self->testing_chi_root_class->new( 863 driver => 'Memory', 864 datastore => {}, 865 l1_cache => { $self->new_cache_options() } 866 ); 867 $l1_cache = $cache->l1_cache; 868 isa_ok( $cache, 'CHI::Driver::Memory', 'cache' ); 869 isa_ok( $l1_cache, $self->testing_driver_class, 'l1_cache' ); 870 $test_l1_cache->(); 871} 872 873sub test_mirror_cache : Tests { 874 my $self = shift; 875 my ( $cache, $mirror_cache ); 876 my ( $key, $value, $key2, $value2 ) = $self->kvpair(2); 877 878 return "skipping - no support for clear" unless $self->supports_clear(); 879 880 my $test_mirror_cache = sub { 881 882 is( $mirror_cache->subcache_type, "mirror_cache" ); 883 884 # Get on either cache should not populate the other, and should not be able to see 885 # mirror keys from regular cache 886 # 887 $cache->set( $key, $value ); 888 $mirror_cache->remove($key); 889 $cache->get($key); 890 ok( !$mirror_cache->get($key), "key not in mirror_cache" ); 891 892 $mirror_cache->set( $key2, $value2 ); 893 ok( !$cache->get($key2), "key2 not in cache" ); 894 895 $self->_test_logging_with_mirror_cache( $cache, $mirror_cache ); 896 897 $self->_test_common_subcache_features( $cache, $mirror_cache, 898 'mirror_cache' ); 899 }; 900 901 my $file_cache_options = sub { 902 my $root_dir = 903 tempdir( "chi-test-mirror-cache-XXXX", TMPDIR => 1, CLEANUP => 1 ); 904 return ( driver => 'File', root_dir => $root_dir, depth => 3 ); 905 }; 906 907 # Test with current cache in primary position... 908 # 909 $cache = $self->new_cache( mirror_cache => { $file_cache_options->() } ); 910 $mirror_cache = $cache->mirror_cache; 911 isa_ok( $cache, $self->testing_driver_class ); 912 isa_ok( $mirror_cache, 'CHI::Driver::File' ); 913 $test_mirror_cache->(); 914 915 # and in mirror position 916 # 917 $cache = 918 $self->testing_chi_root_class->new( $file_cache_options->(), 919 mirror_cache => { $self->new_cache_options() } ); 920 $mirror_cache = $cache->mirror_cache; 921 isa_ok( $cache, 'CHI::Driver::File' ); 922 isa_ok( $mirror_cache, $self->testing_driver_class ); 923 $test_mirror_cache->(); 924} 925 926sub test_subcache_overridable_params : Tests { 927 my ($self) = @_; 928 929 my $cache; 930 warning_like { 931 $cache = $self->new_cache( 932 l1_cache => { 933 driver => 'Memory', 934 on_get_error => 'log', 935 datastore => {}, 936 expires_variance => 0.5, 937 serializer => 'Foo' 938 } 939 ); 940 } 941 qr/cannot override these keys/, "non-overridable subcache keys"; 942 is( $cache->l1_cache->expires_variance, $cache->expires_variance ); 943 is( $cache->l1_cache->serializer, $cache->serializer ); 944 is( $cache->l1_cache->on_set_error, $cache->on_set_error ); 945 is( $cache->l1_cache->on_get_error, 'log' ); 946} 947 948# Run logging tests for a cache with an l1_cache 949# 950sub _test_logging_with_l1_cache { 951 my ( $self, $cache ) = @_; 952 953 $cache->clear(); 954 my $log = activate_test_logger(); 955 my ( $key, $value ) = $self->kvpair(); 956 957 my $driver = $cache->label; 958 959 my $miss_not_in_cache = 'MISS \(not in cache\)'; 960 my $miss_expired = 'MISS \(expired\)'; 961 962 my $start_time = time(); 963 964 $cache->get($key); 965 $log->contains_ok( 966 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_not_in_cache/ 967 ); 968 $log->contains_ok( 969 qr/cache get for .* key='$key', cache='.*l1.*', time='[-\d]+ms': $miss_not_in_cache/ 970 ); 971 $log->empty_ok(); 972 973 $cache->set( $key, $value, 81 ); 974 $log->contains_ok( 975 qr/cache set for .* key='$key', size=\d+, expires='1m2[012]s', cache='$driver', time='[-\d]+ms'/ 976 ); 977 978 $log->contains_ok( 979 qr/cache set for .* key='$key', size=\d+, expires='1m2[012]s', cache='.*l1.*', time='[-\d]+ms'/ 980 ); 981 $log->empty_ok(); 982 983 $cache->get($key); 984 $log->contains_ok( 985 qr/cache get for .* key='$key', cache='.*l1.*', time='[-\d]+ms': HIT/); 986 $log->empty_ok(); 987 988 local $CHI::Driver::Test_Time = $start_time + 120; 989 $cache->get($key); 990 $log->contains_ok( 991 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_expired/ 992 ); 993 $log->contains_ok( 994 qr/cache get for .* key='$key', cache='.*l1.*', time='[-\d]+ms': $miss_expired/ 995 ); 996 $log->empty_ok(); 997 998 $cache->remove($key); 999 $cache->get($key); 1000 $log->contains_ok( 1001 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_not_in_cache/ 1002 ); 1003 $log->contains_ok( 1004 qr/cache get for .* key='$key', cache='.*l1.*', time='[-\d]+ms': $miss_not_in_cache/ 1005 ); 1006 $log->empty_ok(); 1007} 1008 1009sub _test_logging_with_mirror_cache { 1010 my ( $self, $cache ) = @_; 1011 1012 $cache->clear(); 1013 my $log = activate_test_logger(); 1014 my ( $key, $value ) = $self->kvpair(); 1015 1016 my $driver = $cache->label; 1017 1018 my $miss_not_in_cache = 'MISS \(not in cache\)'; 1019 my $miss_expired = 'MISS \(expired\)'; 1020 1021 my $start_time = time(); 1022 1023 $cache->get($key); 1024 $log->contains_ok( 1025 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_not_in_cache/ 1026 ); 1027 $log->empty_ok(); 1028 1029 $cache->set( $key, $value, 81 ); 1030 $log->contains_ok( 1031 qr/cache set for .* key='$key', size=\d+, expires='1m2[012]s', cache='$driver', time='[-\d]+ms'/ 1032 ); 1033 1034 $log->contains_ok( 1035 qr/cache set for .* key='$key', size=\d+, expires='1m2[012]s', cache='.*mirror.*', time='[-\d]+ms'/ 1036 ); 1037 $log->empty_ok(); 1038 1039 $cache->get($key); 1040 $log->contains_ok( 1041 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': HIT/); 1042 $log->empty_ok(); 1043 1044 local $CHI::Driver::Test_Time = $start_time + 120; 1045 $cache->get($key); 1046 $log->contains_ok( 1047 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_expired/ 1048 ); 1049 $log->empty_ok(); 1050 1051 $cache->remove($key); 1052 $cache->get($key); 1053 $log->contains_ok( 1054 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_not_in_cache/ 1055 ); 1056 $log->empty_ok(); 1057} 1058 1059# Run tests common to l1_cache and mirror_cache 1060# 1061sub _test_common_subcache_features { 1062 my ( $self, $cache, $subcache, $subcache_type ) = @_; 1063 my ( $key, $value, $key2, $value2 ) = $self->kvpair(2); 1064 1065 for ( $cache, $subcache ) { $_->clear() } 1066 1067 # Test informational methods 1068 # 1069 ok( !$cache->is_subcache, "is_subcache - false" ); 1070 ok( $subcache->is_subcache, "is_subcache - true" ); 1071 ok( $cache->has_subcaches, "has_subcaches - true" ); 1072 ok( !$subcache->has_subcaches, "has_subcaches - false" ); 1073 ok( !$cache->can('parent_cache'), "parent_cache - cannot" ); 1074 is( $subcache->parent_cache, $cache, "parent_cache - defined" ); 1075 ok( !$cache->can('subcache_type'), "subcache_type - cannot" ); 1076 is( $subcache->subcache_type, $subcache_type, "subcache_type - defined" ); 1077 cmp_deeply( $cache->subcaches, [$subcache], "subcaches - defined" ); 1078 ok( !$subcache->can('subcaches'), "subcaches - cannot" ); 1079 is( $cache->$subcache_type, $subcache, "$subcache_type - defined" ); 1080 ok( !$subcache->can($subcache_type), "$subcache_type - cannot" ); 1081 1082 # Test that sets and various kinds of removals and expirations are distributed to both 1083 # the primary cache and the subcache 1084 # 1085 my ( $test_remove_method, $confirm_caches_empty, 1086 $confirm_caches_populated ); 1087 $test_remove_method = sub { 1088 my ( $desc, $remove_code ) = @_; 1089 $desc = "testing $desc"; 1090 1091 $confirm_caches_empty->("$desc: before set"); 1092 1093 $cache->set( $key, $value ); 1094 $cache->set( $key2, $value2 ); 1095 $confirm_caches_populated->("$desc: after set"); 1096 $remove_code->(); 1097 1098 $confirm_caches_empty->("$desc: before set_multi"); 1099 $cache->set_multi( { $key => $value, $key2 => $value2 } ); 1100 $confirm_caches_populated->("$desc: after set_multi"); 1101 $remove_code->(); 1102 1103 $confirm_caches_empty->("$desc: before return"); 1104 }; 1105 1106 $confirm_caches_empty = sub { 1107 my ($desc) = @_; 1108 ok( !defined( $cache->get($key) ), 1109 "primary cache is not populated with '$key' - $desc" ); 1110 ok( !defined( $subcache->get($key) ), 1111 "subcache is not populated with '$key' - $desc" ); 1112 ok( !defined( $cache->get($key2) ), 1113 "primary cache is not populated #2 with '$key2' - $desc" ); 1114 ok( !defined( $subcache->get($key2) ), 1115 "subcache is not populated #2 with '$key2' - $desc" ); 1116 }; 1117 1118 $confirm_caches_populated = sub { 1119 my ($desc) = @_; 1120 is( $cache->get($key), $value, 1121 "primary cache is populated with '$key' - $desc" ); 1122 is( $subcache->get($key), 1123 $value, "subcache is populated with '$key' - $desc" ); 1124 is( $cache->get($key2), $value2, 1125 "primary cache is populated with '$key2' - $desc" ); 1126 is( $subcache->get($key2), 1127 $value2, "subcache is populated with '$key2' - $desc" ); 1128 }; 1129 1130 $test_remove_method->( 1131 'remove', sub { $cache->remove($key); $cache->remove($key2) } 1132 ); 1133 $test_remove_method->( 1134 'expire', sub { $cache->expire($key); $cache->expire($key2) } 1135 ); 1136 $test_remove_method->( 'clear', sub { $cache->clear() } ); 1137} 1138 1139sub _verify_cache_is_cleared { 1140 my ( $self, $cache, $desc ) = @_; 1141 1142 cmp_deeply( [ $cache->get_keys ], [], "get_keys ($desc)" ); 1143 is( scalar( $cache->get_keys ), 0, "scalar(get_keys) = 0 ($desc)" ); 1144 while ( my ( $keyname, $key ) = each( %{ $self->{keys} } ) ) { 1145 ok( !defined $cache->get($key), 1146 "key '$keyname' no longer defined ($desc)" ); 1147 } 1148} 1149 1150sub process_keys { 1151 my ( $self, $cache, @keys ) = @_; 1152 $self->process_key( $cache, 'foo' ); 1153 return map { $self->process_key( $cache, $_ ) } @keys; 1154} 1155 1156sub process_key { 1157 my ( $self, $cache, $key ) = @_; 1158 return $cache->unescape_key( 1159 $cache->escape_key( $cache->transform_key($key) ) ); 1160} 1161 1162sub test_clear : Tests { 1163 my $self = shift; 1164 my $cache = $self->new_cache( namespace => 'name' ); 1165 my $cache2 = $self->new_cache( namespace => 'other' ); 1166 my $cache3 = $self->new_cache( namespace => 'name' ); 1167 $self->num_tests( $self->{key_count} * 2 + 5 ); 1168 1169 if ( $self->supports_clear() ) { 1170 $self->set_some_keys($cache); 1171 $self->set_some_keys($cache2); 1172 $cache->clear(); 1173 1174 $self->_verify_cache_is_cleared( $cache, 'cache after clear' ); 1175 $self->_verify_cache_is_cleared( $cache3, 'cache3 after clear' ); 1176 cmp_set( 1177 [ $cache2->get_keys ], 1178 [ $self->process_keys( $cache2, values( %{ $self->{keys} } ) ) ], 1179 'cache2 untouched by clear' 1180 ); 1181 } 1182 else { 1183 throws_ok( 1184 sub { $cache->clear() }, 1185 qr/not supported/, 1186 "clear not supported" 1187 ); 1188 SKIP: { skip "clear not supported", 9 } 1189 } 1190} 1191 1192sub test_logging : Tests { 1193 my $self = shift; 1194 my $cache = $self->{cache}; 1195 1196 my $log = activate_test_logger(); 1197 my ( $key, $value ) = $self->kvpair(); 1198 1199 my $driver = $cache->label; 1200 1201 my $miss_not_in_cache = 'MISS \(not in cache\)'; 1202 my $miss_expired = 'MISS \(expired\)'; 1203 1204 my $start_time = time(); 1205 1206 $cache->get($key); 1207 $log->contains_ok( 1208 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_not_in_cache/ 1209 ); 1210 $log->empty_ok(); 1211 1212 $cache->set( $key, $value ); 1213 $log->contains_ok( 1214 qr/cache set for .* key='$key', size=\d+, expires='never', cache='$driver', time='[-\d]+ms'/ 1215 ); 1216 $log->empty_ok(); 1217 $cache->set( $key, $value, 81 ); 1218 $log->contains_ok( 1219 qr/cache set for .* key='$key', size=\d+, expires='1m2[012]s', cache='$driver', time='[-\d]+ms'/ 1220 ); 1221 $log->empty_ok(); 1222 1223 $cache->get($key); 1224 $log->contains_ok( 1225 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': HIT/); 1226 $log->empty_ok(); 1227 1228 local $CHI::Driver::Test_Time = $start_time + 120; 1229 $cache->get($key); 1230 $log->contains_ok( 1231 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_expired/ 1232 ); 1233 $log->empty_ok(); 1234 1235 $cache->remove($key); 1236 $cache->get($key); 1237 $log->contains_ok( 1238 qr/cache get for .* key='$key', cache='$driver', time='[-\d]+ms': $miss_not_in_cache/ 1239 ); 1240 $log->empty_ok(); 1241} 1242 1243sub test_stats : Tests { 1244 my $self = shift; 1245 1246 return 'author testing only - possible differences between JSON versions' 1247 unless ( $ENV{AUTHOR_TESTING} ); 1248 1249 my $stats = $self->testing_chi_root_class->stats; 1250 $stats->enable(); 1251 1252 my ( $key, $value ) = $self->kvpair(); 1253 my $start_time = time(); 1254 1255 my $cache; 1256 $cache = $self->new_cache( namespace => 'Foo' ); 1257 $cache->get($key); 1258 $cache->set( $key, $value, 80 ); 1259 $cache->get($key); 1260 local $CHI::Driver::Test_Time = $start_time + 120; 1261 $cache->get($key); 1262 $cache->remove($key); 1263 $cache->get($key); 1264 1265 $cache = $self->new_cache( namespace => 'Bar' ); 1266 $cache->set( $key, scalar( $value x 3 ) ); 1267 $cache->set( $key, $value ); 1268 1269 $cache = $self->new_cache( namespace => 'Baz' ); 1270 my $code = sub { usleep(100000); scalar( $value x 5 ) }; 1271 $cache->compute( $key, undef, $code ); 1272 $cache->compute( $key, undef, $code ); 1273 $cache->compute( $key, undef, $code ); 1274 1275 my $log = activate_test_logger(); 1276 my $label = $cache->label; 1277 $log->empty_ok(); 1278 $stats->flush(); 1279 $log->contains_ok( 1280 qr/CHI stats: {"absent_misses":2,"end_time":\d+,"expired_misses":1,"get_time_ms":\d+,"hits":1,"label":"$label","namespace":"Foo","root_class":"CHI","set_key_size":6,"set_time_ms":\d+,"set_value_size":20,"sets":1,"start_time":\d+}/ 1281 ); 1282 $log->contains_ok( 1283 qr/CHI stats: {"end_time":\d+,"label":"$label","namespace":"Bar","root_class":"CHI","set_key_size":12,"set_time_ms":\d+,"set_value_size":52,"sets":2,"start_time":\d+}/ 1284 ); 1285 $log->contains_ok( 1286 qr/CHI stats: {"absent_misses":1,"compute_time_ms":\d+,"computes":1,"end_time":\d+,"get_time_ms":\d+,"hits":2,"label":"$label","namespace":"Baz","root_class":"CHI","set_key_size":6,"set_time_ms":\d+,"set_value_size":44,"sets":1,"start_time":\d+}/ 1287 ); 1288 $log->empty_ok(); 1289 1290 my @logs = ( 1291 'CHI stats: {"root_class":"CHI","namespace":"Foo","label":"File","start_time":1338404896,"end_time":1338404899,"hits":3,"sets":5,"set_time_ms":10}', 1292 'CHI stats: {"root_class":"CHI","namespace":"Foo","label":"File","start_time":1338404896,"end_time":1338404899,"hits":1,"sets":7,"set_time_ms":14}', 1293 'CHI stats: {"root_class":"CHI","namespace":"Bar","label":"File","start_time":1338404896,"end_time":1338404899,"hits":4,"sets":9,"set_time_ms":18}', 1294 'CHI stats: {"root_class":"CHI","namespace":"Foo","label":"File","start_time":1338404896,"end_time":1338404899,"sets":3,"set_time_ms":6}', 1295 'CHI stats: {"root_class":"CHI","namespace":"Foo","label":"File","start_time":1338404896,"end_time":1338404899,"hits":8}', 1296 'CHI stats: {"root_class":"CHI","namespace":"Foo","label":"Memory","start_time":1338404896,"end_time":1338404899,"sets":2,"set_time_ms":4}', 1297 'CHI stats: {"root_class":"CHI","namespace":"Bar","label":"File","start_time":1338404896,"end_time":1338404899,"hits":10,"sets":1,"set_time_ms":2}', 1298 'CHI stats: {"root_class":"CHI","namespace":"Bar","label":"File","start_time":1338404896,"end_time":1338404899,"hits":3,"set_errors":2}', 1299 ); 1300 my $log_dir = tempdir( "chi-test-stats-XXXX", TMPDIR => 1, CLEANUP => 1 ); 1301 write_file( "$log_dir/log1", join( "\n", splice( @logs, 0, 4 ) ) . "\n" ); 1302 write_file( "$log_dir/log2", join( "\n", @logs ) ); 1303 open( my $fh2, "<", "$log_dir/log2" ) or die "cannot open $log_dir/log2"; 1304 my $results = $stats->parse_stats_logs( "$log_dir/log1", $fh2 ); 1305 close($fh2); 1306 cmp_deeply( 1307 $results, 1308 Test::Deep::bag( 1309 { 1310 avg_set_time_ms => '2', 1311 gets => 12, 1312 hit_rate => '1', 1313 hits => 12, 1314 label => 'File', 1315 namespace => 'Foo', 1316 root_class => 'CHI', 1317 set_time_ms => 30, 1318 sets => 15 1319 }, 1320 { 1321 avg_set_time_ms => '2', 1322 gets => 17, 1323 hit_rate => '1', 1324 hits => 17, 1325 label => 'File', 1326 namespace => 'Bar', 1327 root_class => 'CHI', 1328 set_errors => 2, 1329 set_time_ms => 20, 1330 sets => 10 1331 }, 1332 { 1333 avg_set_time_ms => '2', 1334 label => 'Memory', 1335 namespace => 'Foo', 1336 root_class => 'CHI', 1337 set_time_ms => 4, 1338 sets => 2 1339 }, 1340 { 1341 avg_set_time_ms => '2', 1342 hits => '29', 1343 label => 'TOTALS', 1344 namespace => 'TOTALS', 1345 root_class => 'TOTALS', 1346 set_errors => '2', 1347 set_time_ms => 54, 1348 sets => 27 1349 } 1350 ), 1351 'parse_stats_logs' 1352 ); 1353} 1354 1355sub test_cache_object : Tests { 1356 my $self = shift; 1357 my $cache = $self->{cache}; 1358 my ( $key, $value ) = $self->kvpair(); 1359 my $start_time = time(); 1360 $cache->set( $key, $value, { expires_at => $start_time + 10 } ); 1361 is_between( $cache->get_object($key)->created_at, 1362 $start_time, $start_time + 2 ); 1363 is_between( $cache->get_object($key)->get_created_at, 1364 $start_time, $start_time + 2 ); 1365 is( $cache->get_object($key)->expires_at, $start_time + 10 ); 1366 is( $cache->get_object($key)->get_expires_at, $start_time + 10 ); 1367 1368 local $CHI::Driver::Test_Time = $start_time + 50; 1369 $cache->set( $key, $value ); 1370 is_between( 1371 $cache->get_object($key)->created_at, 1372 $start_time + 50, 1373 $start_time + 52 1374 ); 1375 is_between( 1376 $cache->get_object($key)->get_created_at, 1377 $start_time + 50, 1378 $start_time + 52 1379 ); 1380} 1381 1382sub test_size_awareness : Tests { 1383 my $self = shift; 1384 my ( $key, $value ) = $self->kvpair(); 1385 1386 ok( !$self->new_cleared_cache()->is_size_aware(), 1387 "not size aware by default" ); 1388 ok( $self->new_cleared_cache( is_size_aware => 1 )->is_size_aware(), 1389 "is_size_aware turns on size awareness" ); 1390 ok( $self->new_cleared_cache( max_size => 10 )->is_size_aware(), 1391 "max_size turns on size awareness" ); 1392 1393 my $cache = $self->new_cleared_cache( is_size_aware => 1 ); 1394 is( $cache->get_size(), 0, "size is 0 for empty" ); 1395 $cache->set( $key, $value ); 1396 is_about( $cache->get_size, 20, "size is about 20 with one value" ); 1397 $cache->set( $key, scalar( $value x 5 ) ); 1398 is_about( $cache->get_size, 45, "size is 45 after overwrite" ); 1399 $cache->set( $key, scalar( $value x 5 ) ); 1400 is_about( $cache->get_size, 45, "size is still 45 after same overwrite" ); 1401 $cache->set( $key, scalar( $value x 2 ) ); 1402 is_about( $cache->get_size, 26, "size is 26 after overwrite" ); 1403 $cache->remove($key); 1404 is( $cache->get_size, 0, "size is 0 again after removing key" ); 1405 $cache->set( $key, $value ); 1406 is_about( $cache->get_size, 20, "size is about 20 with one value" ); 1407 $cache->clear(); 1408 is( $cache->get_size, 0, "size is 0 again after clear" ); 1409 1410 my $time = time() + 10; 1411 $cache->set( $key, $value, { expires_at => $time } ); 1412 is( $cache->get_expires_at($key), 1413 $time, "set options respected by size aware cache" ); 1414} 1415 1416sub test_max_size : Tests { 1417 my $self = shift; 1418 1419 is( $self->new_cache( max_size => '30k' )->max_size, 1420 30 * 1024, 'max_size parsing' ); 1421 1422 my $cache = $self->new_cleared_cache( max_size => 99 ); 1423 ok( $cache->is_size_aware, "is size aware when max_size specified" ); 1424 my $value_20 = 'x' x 6; 1425 1426 for ( my $i = 0 ; $i < 5 ; $i++ ) { 1427 $cache->set( "key$i", $value_20 ); 1428 } 1429 for ( my $i = 0 ; $i < 10 ; $i++ ) { 1430 $cache->set( "key" . int( rand(10) ), $value_20 ); 1431 is_between( $cache->get_size, 60, 99, 1432 "after iteration $i, size = " . $cache->get_size ); 1433 is_between( scalar( $cache->get_keys ), 1434 3, 5, "after iteration $i, keys = " . scalar( $cache->get_keys ) ); 1435 } 1436} 1437 1438sub test_max_size_with_l1_cache : Tests { 1439 my $self = shift; 1440 1441 my $cache = $self->new_cleared_cache( 1442 l1_cache => { driver => 'Memory', datastore => {}, max_size => 99 } ); 1443 my $l1_cache = $cache->l1_cache; 1444 ok( $l1_cache->is_size_aware, "is size aware when max_size specified" ); 1445 my $value_20 = 'x' x 6; 1446 1447 my @keys = map { "key$_" } ( 0 .. 9 ); 1448 my @shuffle_keys = shuffle(@keys); 1449 for ( my $i = 0 ; $i < 5 ; $i++ ) { 1450 $cache->set( "key$i", $value_20 ); 1451 } 1452 for ( my $i = 0 ; $i < 10 ; $i++ ) { 1453 my $key = $shuffle_keys[$i]; 1454 $cache->set( $key, $value_20 ); 1455 is_between( $l1_cache->get_size, 60, 99, 1456 "after iteration $i, size = " . $l1_cache->get_size ); 1457 is_between( scalar( $l1_cache->get_keys ), 1458 3, 5, 1459 "after iteration $i, keys = " . scalar( $l1_cache->get_keys ) ); 1460 } 1461 cmp_deeply( [ sort $cache->get_keys ], 1462 \@keys, "primary cache still has all keys" ); 1463 1464 # Now test population by writeback 1465 $l1_cache->clear(); 1466 is( $l1_cache->get_size, 0, "l1 size is 0 after clear" ); 1467 for ( my $i = 0 ; $i < 5 ; $i++ ) { 1468 $cache->get("key$i"); 1469 } 1470 for ( my $i = 0 ; $i < 10 ; $i++ ) { 1471 my $key = $shuffle_keys[$i]; 1472 $cache->get($key); 1473 is_between( $l1_cache->get_size, 60, 99, 1474 "after iteration $i, size = " . $l1_cache->get_size ); 1475 is_between( scalar( $l1_cache->get_keys ), 1476 3, 5, 1477 "after iteration $i, keys = " . scalar( $l1_cache->get_keys ) ); 1478 } 1479} 1480 1481sub test_custom_discard_policy : Tests { 1482 my $self = shift; 1483 my $value_20 = 'x' x 6; 1484 my $highest_first = sub { 1485 my $c = shift; 1486 my @sorted_keys = sort( $c->get_keys ); 1487 return sub { pop(@sorted_keys) }; 1488 }; 1489 my $cache = $self->new_cleared_cache( 1490 is_size_aware => 1, 1491 discard_policy => $highest_first 1492 ); 1493 for ( my $j = 0 ; $j < 10 ; $j += 2 ) { 1494 $cache->clear(); 1495 for ( my $i = 0 ; $i < 10 ; $i++ ) { 1496 my $k = ( $i + $j ) % 10; 1497 $cache->set( "key$k", $value_20 ); 1498 } 1499 $cache->discard_to_size(100); 1500 cmp_set( 1501 [ $cache->get_keys ], 1502 [ map { "key$_" } ( 0 .. 4 ) ], 1503 "5 lowest" 1504 ); 1505 $cache->discard_to_size(20); 1506 cmp_set( [ $cache->get_keys ], ["key0"], "1 lowest" ); 1507 } 1508} 1509 1510sub test_discard_timeout : Tests { 1511 my $self = shift; 1512 return 'author testing only' unless ( $ENV{AUTHOR_TESTING} ); 1513 1514 my $bad_policy = sub { 1515 return sub { '1' }; 1516 }; 1517 my $cache = $self->new_cleared_cache( 1518 is_size_aware => 1, 1519 discard_policy => $bad_policy 1520 ); 1521 ok( defined( $cache->discard_timeout ) && $cache->discard_timeout > 1, 1522 "positive discard timeout" ); 1523 $cache->discard_timeout(1); 1524 is( $cache->discard_timeout, 1, "can set timeout" ); 1525 my $start_time = time; 1526 $cache->set( 2, 2 ); 1527 throws_ok { $cache->discard_to_size(0) } qr/discard timeout .* reached/; 1528 ok( 1529 time >= $start_time && time <= $start_time + 4, 1530 sprintf( 1531 "time (%d) is between %d and %d", 1532 time, $start_time, $start_time + 4 1533 ) 1534 ); 1535} 1536 1537sub test_size_awareness_with_subcaches : Tests { 1538 my $self = shift; 1539 1540 my ( $cache, $l1_cache ); 1541 my $set_values = sub { 1542 my $value_20 = 'x' x 6; 1543 for ( my $i = 0 ; $i < 20 ; $i++ ) { 1544 $cache->set( "key$i", $value_20 ); 1545 } 1546 $l1_cache = $cache->l1_cache; 1547 }; 1548 my $is_size_aware = sub { 1549 my $c = shift; 1550 my $label = $c->label; 1551 1552 ok( $c->is_size_aware, "$label is size aware" ); 1553 my $max_size = $c->max_size; 1554 ok( $max_size > 0, "$label has max size" ); 1555 is_between( $c->get_size, $max_size - 40, 1556 $max_size, "$label size = " . $c->get_size ); 1557 is_between( 1558 scalar( $c->get_keys ), 1559 ( $max_size + 1 ) / 20 - 2, 1560 ( $max_size + 1 ) / 20, 1561 "$label keys = " . scalar( $c->get_keys ) 1562 ); 1563 }; 1564 my $is_not_size_aware = sub { 1565 my $c = shift; 1566 my $label = $c->label; 1567 1568 ok( !$c->is_size_aware, "$label is not size aware" ); 1569 is( $c->get_keys, 20, "$label keys = 20" ); 1570 }; 1571 1572 $cache = $self->new_cleared_cache( 1573 l1_cache => { driver => 'Memory', datastore => {}, max_size => 99 } ); 1574 $set_values->(); 1575 $is_not_size_aware->($cache); 1576 $is_size_aware->($l1_cache); 1577 1578 $cache = $self->new_cleared_cache( 1579 l1_cache => { driver => 'Memory', datastore => {}, max_size => 99 }, 1580 max_size => 199 1581 ); 1582 $set_values->(); 1583 $is_size_aware->($cache); 1584 $is_size_aware->($l1_cache); 1585 1586 $cache = $self->new_cleared_cache( 1587 l1_cache => { driver => 'Memory', datastore => {} }, 1588 max_size => 199 1589 ); 1590 $set_values->(); 1591 $is_size_aware->($cache); 1592 1593 # Cannot call is_not_size_aware because the get_keys check will 1594 # fail. Keys will be removed from the l1_cache when they are removed 1595 # from the main cache, even though l1_cache does not have a max 1596 # size. Not sure if this is the correct behavior, but for now, we're not 1597 # going to test it. Normally, l1 caches will be more size limited than 1598 # their parent caches. 1599 # 1600 ok( !$l1_cache->is_size_aware, $l1_cache->label . " is not size aware" ); 1601} 1602 1603sub is_about { 1604 my ( $value, $expected, $msg ) = @_; 1605 1606 my $margin = int( $expected * 0.1 ); 1607 if ( abs( $value - $expected ) <= $margin ) { 1608 pass($msg); 1609 } 1610 else { 1611 fail("$msg - got $value, expected $expected"); 1612 } 1613} 1614 1615sub test_busy_lock : Tests { 1616 my $self = shift; 1617 my $cache = $self->{cache}; 1618 1619 my ( $key, $value ) = $self->kvpair(); 1620 my @bl = ( busy_lock => '30 sec' ); 1621 my $start_time = time(); 1622 1623 local $CHI::Driver::Test_Time = $start_time; 1624 $cache->set( $key, $value, 100 ); 1625 local $CHI::Driver::Test_Time = $start_time + 90; 1626 is( $cache->get( $key, @bl ), $value, "hit before expiration" ); 1627 is( 1628 $cache->get_expires_at($key), 1629 $start_time + 100, 1630 "expires_at before expiration" 1631 ); 1632 local $CHI::Driver::Test_Time = $start_time + 110; 1633 ok( !defined( $cache->get( $key, @bl ) ), "miss after expiration" ); 1634 is( 1635 $cache->get_expires_at($key), 1636 $start_time + 140, 1637 "expires_at after busy lock" 1638 ); 1639 is( $cache->get( $key, @bl ), $value, "hit after busy lock" ); 1640} 1641 1642sub test_obj_ref : Tests { 1643 my $self = shift; 1644 1645 # Make sure obj_ref works in conjunction with subcaches too 1646 my $cache = 1647 $self->new_cache( l1_cache => { driver => 'Memory', datastore => {} } ); 1648 my $obj; 1649 my ( $key, $value ) = ( 'medium', [ a => 5, b => 6 ] ); 1650 1651 my $validate_obj = sub { 1652 isa_ok( $obj, 'CHI::CacheObject' ); 1653 is( $obj->key, $key, "keys match" ); 1654 cmp_deeply( $obj->value, $value, "values match" ); 1655 }; 1656 1657 $cache->get( $key, obj_ref => \$obj ); 1658 ok( !defined($obj), "obj not defined on miss" ); 1659 $cache->set( $key, $value, { obj_ref => \$obj } ); 1660 $validate_obj->(); 1661 undef $obj; 1662 ok( !defined($obj), "obj not defined before get" ); 1663 $cache->get( $key, obj_ref => \$obj ); 1664 $validate_obj->(); 1665} 1666 1667sub test_metacache : Tests { 1668 my $self = shift; 1669 my $cache = $self->{cache}; 1670 1671 ok( !defined( $cache->{metacache} ), "metacache is lazy" ); 1672 $cache->metacache->set( 'foo', 5 ); 1673 ok( defined( $cache->{metacache} ), "metacache autovivified" ); 1674 is( $cache->metacache->get('foo'), 5 ); 1675} 1676 1677sub test_scalar_return_values : Tests { 1678 my $self = shift; 1679 my $cache = $self->{cache}; 1680 1681 my $check = sub { 1682 my ($code) = @_; 1683 my $scalar_result = $code->(); 1684 my @list = $code->(); 1685 cmp_deeply( \@list, [$scalar_result] ); 1686 }; 1687 1688 $check->( sub { $cache->fetch('a') } ); 1689 $check->( sub { $cache->get('a') } ); 1690 $check->( sub { $cache->set( 'a', 5 ) } ); 1691 $check->( sub { $cache->fetch('a') } ); 1692 $check->( sub { $cache->get('a') } ); 1693} 1694 1695sub test_no_leak : Tests { 1696 my ($self) = @_; 1697 1698 my $weakref; 1699 { 1700 my $cache = $self->new_cache(); 1701 $weakref = $cache; 1702 weaken($weakref); 1703 ok( defined($weakref) && $weakref->isa('CHI::Driver'), 1704 "weakref is defined" ); 1705 } 1706 ok( !defined($weakref), "weakref is no longer defined - cache was freed" ); 1707} 1708 1709{ 1710 package My::CHI; 1711$My::CHI::VERSION = '0.60'; 1712our @ISA = qw(CHI); 1713} 1714 1715sub test_driver_properties : Tests { 1716 my $self = shift; 1717 my $cache = $self->{cache}; 1718 1719 is( $cache->chi_root_class, 'CHI', 'chi_root_class=CHI' ); 1720 my $cache2 = My::CHI->new( $self->new_cache_options() ); 1721 is( $cache2->chi_root_class, 'My::CHI', 'chi_root_class=My::CHI' ); 1722} 1723 1724sub test_missing_params : Tests { 1725 my $self = shift; 1726 my $cache = $self->{cache}; 1727 1728 # These methods require a key 1729 foreach my $method ( 1730 qw(get get_object get_expires_at exists_and_is_expired is_valid set expire compute get_multi_arrayref get_multi_hashref set_multi remove_multi) 1731 ) 1732 { 1733 throws_ok( 1734 sub { $cache->$method() }, 1735 qr/must specify key/, 1736 "$method throws error when no key passed" 1737 ); 1738 } 1739} 1740 1741sub test_compute : Tests { 1742 my $self = shift; 1743 my $cache = $self->{cache}; 1744 1745 # Test current arg order and pre-0.40 arg order 1746 foreach my $iter ( 0 .. 1 ) { 1747 my $count = 5; 1748 my $expire_time = time + 10; 1749 my @args1 = ( { expires_at => $expire_time }, sub { $count++ } ); 1750 my @args2 = ( 1751 { 1752 expire_if => sub { 1 } 1753 }, 1754 sub { $count++ } 1755 ); 1756 if ($iter) { 1757 @args1 = reverse(@args1); 1758 @args2 = reverse(@args2); 1759 } 1760 $cache->clear; 1761 is( $cache->get('foo'), undef, "miss" ); 1762 is( $cache->compute( 'foo', @args1 ), 5, "compute - 5" ); 1763 is( $cache->get('foo'), 5, "hit - 5" ); 1764 is( $cache->get_object('foo')->expires_at, $expire_time, 1765 "expire time" ); 1766 is( $cache->compute( 'foo', @args2 ), 6, "compute - 6" ); 1767 is( $cache->get('foo'), 6, "hit - 6" ); 1768 } 1769 1770 # Test wantarray 1771 $cache->clear(); 1772 my $compute_list = sub { 1773 $cache->compute( 'foo', {}, sub { ( int( rand(10000) ) ) x 5 } ); 1774 }; 1775 my @list1 = $compute_list->(); 1776 my @list2 = $compute_list->(); 1777 is( scalar(@list1), 5, "list has 5 items" ); 1778 cmp_deeply( \@list1, \@list2, "lists are the same" ); 1779} 1780 1781sub test_compress_threshold : Tests { 1782 my $self = shift; 1783 my $cache = $self->{cache}; 1784 1785 my $s0 = 'x' x 180; 1786 my $s1 = 'x' x 200; 1787 $cache->set( 'key0', $s0 ); 1788 $cache->set( 'key1', $s1 ); 1789 is_between( $cache->get_object('key0')->size, 180, 220 ); 1790 is_between( $cache->get_object('key1')->size, 200, 240 ); 1791 1792 my $cache2 = $self->new_cache( compress_threshold => 190 ); 1793 $cache2->set( 'key0', $s0 ); 1794 $cache2->set( 'key1', $s1 ); 1795 is_between( $cache2->get_object('key0')->size, 180, 220 ); 1796 ok( $cache2->get_object('key1')->size < 100 ); 1797 is( $cache2->get('key0'), $s0 ); 1798 is( $cache2->get('key1'), $s1 ); 1799} 1800 1801sub test_expires_on_backend : Tests { 1802 my $self = shift; 1803 1804 return "skipping - no support for expires_on_backend" 1805 unless $self->supports_expires_on_backend(); 1806 foreach my $expires_on_backend ( 0, 1 ) { 1807 my $cache = 1808 $self->new_cache( expires_on_backend => $expires_on_backend ); 1809 $cache->set( 'key0', 5, '2s' ); 1810 $cache->set( 'key1', 6, { expires_at => time + 2 } ); 1811 is( $cache->get('key0'), 5, 'hit key0 before expire' ); 1812 is( $cache->get('key1'), 6, 'hit key1 before expire' ); 1813 sleep(3); 1814 ok( !defined( $cache->get('key0') ), 'miss key0 after expire' ); 1815 ok( !defined( $cache->get('key1') ), 'miss key1 after expire' ); 1816 1817 if ($expires_on_backend) { 1818 ok( 1819 !defined( $cache->get_object('key0') ), 1820 'cannot get_object(key0) after expire' 1821 ); 1822 ok( 1823 !defined( $cache->get_object('key1') ), 1824 'cannot get_object(key1) after expire' 1825 ); 1826 } 1827 else { 1828 ok( 1829 $cache->get_object('key0')->is_expired(), 1830 'can get_object(key0) after expire' 1831 ); 1832 ok( 1833 $cache->get_object('key1')->is_expired(), 1834 'can get_object(key1) after expire' 1835 ); 1836 } 1837 } 1838} 1839 1840sub test_append : Tests { 1841 my $self = shift; 1842 my $cache = $self->{cache}; 1843 my ( $key, $value ) = 1844 ( $self->{keys}->{arrayref}, $self->{values}->{medium} ); 1845 1846 # Appending to non-existent key has no effect 1847 # 1848 $cache->append( $key, $value ); 1849 ok( !$cache->get($key) ); 1850 1851 ok( $cache->set( $key, $value ) ); 1852 $cache->append( $key, $value ); 1853 is( $cache->get($key), $value . $value ); 1854 $cache->append( $key, $value ); 1855 is( $cache->get($key), $value . $value . $value ); 1856} 1857 1858sub test_add : Tests { 1859 my $self = shift; 1860 my $cache = $self->{cache}; 1861 my ( $key, $value ) = 1862 ( $self->{keys}->{arrayref}, $self->{values}->{medium} ); 1863 1864 my $t = time(); 1865 1866 $cache->add( $key, $value, { expires_at => $t + 100 } ); 1867 is( $cache->get($key), $value, "get" ); 1868 is( $cache->get_object($key)->expires_at, $t + 100, "expires_at" ); 1869 1870 $cache->add( $key, $value . $value, { expires_at => $t + 200 } ); 1871 is( $cache->get($key), $value, "get (after add)" ); 1872 is( $cache->get_object($key)->expires_at, 1873 $t + 100, "expires_at (after add)" ); 1874 1875 $cache->remove($key); 1876 $cache->add( $key, $value . $value, { expires_at => $t + 200 } ); 1877 is( $cache->get($key), $value . $value, "get (after expire and add)" ); 1878 is( $cache->get_object($key)->expires_at, 1879 $t + 200, "expires_at (after expire and add)" ); 1880} 1881 1882sub test_replace : Tests { 1883 my $self = shift; 1884 my $cache = $self->{cache}; 1885 my ( $key, $value ) = 1886 ( $self->{keys}->{arrayref}, $self->{values}->{medium} ); 1887 1888 my $t = time(); 1889 1890 $cache->replace( $key, $value, { expires_at => $t + 100 } ); 1891 ok( !$cache->get_object($key), "get" ); 1892 1893 $cache->set( $key, $value . $value, { expires_at => $t + 200 } ); 1894 $cache->replace( $key, $value, { expires_at => $t + 100 } ); 1895 is( $cache->get($key), $value, "get (after replace)" ); 1896 is( $cache->get_object($key)->expires_at, 1897 $t + 100, "expires_at (after replace)" ); 1898} 1899 1900sub test_max_key_length : Tests { 1901 my $self = shift; 1902 1903 # Test max_key_length and also that key does not get transformed twice in mirror_cache 1904 # 1905 my $mirror_store = {}; 1906 my $cache = $self->new_cleared_cache( 1907 max_key_length => 10, 1908 mirror_cache => { driver => 'Memory', datastore => $mirror_store } 1909 ); 1910 1911 foreach my $keyname ( 'medium', 'large' ) { 1912 my ( $key, $value ) = 1913 ( $self->{keys}->{$keyname}, $self->{values}->{$keyname} ); 1914 $cache->set( $key, $value ); 1915 is( $cache->get($key), $value, $keyname ); 1916 is( $cache->mirror_cache->get($key), $value, $keyname ); 1917 if ( $keyname eq 'medium' ) { 1918 is( $cache->get_object($key)->key(), $key, "medium key stored" ); 1919 } 1920 else { 1921 isnt( $cache->get_object($key)->key(), $key, "md5 key stored" ); 1922 is( length( $cache->get_object($key)->key() ), 1923 32, "md5 key stored" ); 1924 } 1925 } 1926} 1927 1928# Test that cache does not get corrupted with multiple concurrent processes writing 1929# 1930sub test_multiple_processes : Tests { 1931 my $self = shift; 1932 return "author test only" unless $ENV{AUTHOR_TESTING}; 1933 return "does not pass on file driver" 1934 if $self->new_cache->short_driver_name eq 'File'; 1935 1936 my ( @values, @pids, %valid_values ); 1937 my $shared_key = $self->{keys}->{medium}; 1938 my $num_procs = 4; 1939 1940 local $SIG{CHLD} = 'IGNORE'; 1941 1942 # Each child continuously writes a unique 10000 byte string to a single shared key 1943 # 1944 my $child_action = sub { 1945 my $p = shift; 1946 my $value = $values[$p]; 1947 my $child_cache = $self->new_cache(); 1948 1949 sleep(1); # Wait for parent to be ready 1950 my $child_end_time = time() + 5; 1951 while ( time < $child_end_time ) { 1952 $child_cache->set( $shared_key, $value ); 1953 } 1954 $child_cache->set( "done$p", 1 ); 1955 }; 1956 1957 foreach my $p ( 0 .. $num_procs ) { 1958 $values[$p] = random_string(10000); 1959 $valid_values{ $values[$p] } = $p; 1960 if ( my $pid = fork() ) { 1961 $pids[$p] = $pid; 1962 } 1963 else { 1964 $child_action->($p); 1965 exit; 1966 } 1967 } 1968 1969 # Parent continuously gets shared key, makes sure it is one of the valid values. 1970 # Loop until we see done flag for each child process, or until 10 secs pass. 1971 # At end make sure we saw each process's value once. 1972 # 1973 my ( %seen, $error ); 1974 my $parent_end_time = time() + 10; 1975 my $parent_cache = $self->new_cache(); 1976 while ( !$error ) { 1977 for ( my $i = 0 ; $i < 100 ; $i++ ) { 1978 my $value = $parent_cache->get($shared_key); 1979 if ( defined($value) ) { 1980 if ( defined( my $p = $valid_values{$value} ) ) { 1981 $seen{$p} = 1; 1982 } 1983 else { 1984 $error = "got invalid value '$value' from shared key"; 1985 last; 1986 } 1987 } 1988 } 1989 if ( !grep { !$parent_cache->get("done$_") } ( 0 .. $num_procs ) ) { 1990 last; 1991 } 1992 if ( time() >= $parent_end_time ) { 1993 $error = "did not see all done flags after 10 secs"; 1994 } 1995 } 1996 1997 if ( !$error ) { 1998 if ( my ($p) = grep { !$seen{$_} } ( 0 .. $num_procs ) ) { 1999 $error = "never saw value from process $p"; 2000 } 2001 } 2002 2003 if ($error) { 2004 ok( 0, $error ); 2005 } 2006 else { 2007 ok( 1, "passed" ); 2008 } 2009} 2010 20111; 2012