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