File Coverage

blib/lib/Test/Mock/Redis.pm
Criterion Covered Total %
statement 161 165 97.5
branch 42 44 95.4
condition 13 18 72.2
subroutine 49 50 98.0
pod 1 5 20.0
total 266 282 94.3


line stmt bran cond sub pod time code
1             package Test::Mock::Redis;
2              
3 15     15   1029182 use warnings;
  15         159  
  15         470  
4 15     15   80 use strict;
  15         26  
  15         346  
5              
6 15     15   72 use Carp 'confess';
  15         31  
  15         622  
7 15     15   77 use Config;
  15         29  
  15         669  
8 15     15   84 use Scalar::Util qw/blessed/;
  15         43  
  15         700  
9 15     15   6912 use Class::Method::Modifiers;
  15         22650  
  15         787  
10 15     15   6334 use Package::Stash ();
  15         94175  
  15         348  
11 15     15   99 use Try::Tiny;
  15         27  
  15         792  
12 15     15   6363 use namespace::clean; # important: clean all subs imported above this line
  15         102315  
  15         103  
13              
14             =head1 NAME
15              
16             Test::Mock::Redis - use in place of Redis for unit testing
17              
18             =head1 VERSION
19              
20             Version 0.22
21              
22             =cut
23              
24             our $VERSION = '0.22';
25              
26             =head1 SYNOPSIS
27              
28             Test::Mock::Redis can be used in place of Redis for running
29             tests without needing a running redis instance.
30              
31             use Test::Mock::Redis;
32              
33             my $redis = Test::Mock::Redis->new(server => 'whatever');
34              
35             $redis->set($key, 'some value');
36              
37             $redis->get($key);
38              
39             ...
40              
41             This module is designed to function as a drop in replacement for
42             Redis.pm for testing purposes.
43              
44             See perldoc Redis and the redis documentation at L
45              
46             =head1 PERSISTENCE
47              
48             The "connection" to the mocked server (and its stored data) will persist beyond
49             the object instance, just like a real Redis server. This means that you do not
50             need to save the instance to this object in order to preserve your data; simply
51             call C with the same server parameter and the same instance will be
52             returned, with all data preserved.
53              
54             =head1 SUBROUTINES/METHODS
55              
56             =head2 new
57              
58             Create a new Test::Mock::Redis object.
59              
60             It can be used in place of a Redis object for unit testing.
61              
62             It accepts the "server" argument, just like Redis.pm's new.
63              
64             =head2 num_databases
65              
66             Redis ships with a default of 16 databases, and that's what this module
67             handles by default. If you need to change that, do
68              
69             use Test::Mock::Redis num_databases => 21;
70              
71             or at run-time
72              
73             Test::Mock::Redis::change_num_databases(21);
74              
75             =cut
76              
77             my $NUM_DBS = 16;
78              
79             sub import {
80             my ($class, %args) = @_;
81              
82             if ($args{num_databases}){
83             change_num_databases($args{num_databases});
84             }
85             }
86              
87             sub change_num_databases {
88             $NUM_DBS = shift;
89             }
90              
91              
92             sub _new_db {
93 381     381   885 tie my %hash, 'Test::Mock::Redis::PossiblyVolatile';
94 381         1402 return \%hash;
95             }
96              
97              
98             sub _defaults {
99 19     19   102 my @hex = (0..9, 'a'..'f');
100             return (
101             _quit => 0,
102             _shutdown => 0,
103 348         499 _stash => [ map { _new_db } (1..$NUM_DBS) ],
104             _num_dbs => $NUM_DBS,
105             _db_index => 0,
106             _up_since => time,
107             _last_save => time,
108 19         81 _run_id => (join '', map { $hex[rand @hex] } 1..40), # E.G. '0e7e19fc45139fdb26ff3dd35ca6725d9882f1b7',
  760         2122  
109             );
110             }
111              
112              
113             my $instances;
114              
115             sub new {
116 24     24 1 2632 my $class = shift;
117 24         78 my %args = @_;
118              
119             my $server = defined $args{server}
120 24 100       113 ? $args{'server'}
121             : 'localhost:6379';
122              
123 24 100       92 if( $instances->{$server} ){
124 5 100       348 confess "Could not connect to Redis server at $server" if $instances->{$server}->{_shutdown};
125 4         9 $instances->{$server}->{_quit} = 0;
126 4         17 return $instances->{$server};
127             }
128              
129 19         70 my $self = bless {$class->_defaults, server => $server}, $class;
130              
131 19         89 $instances->{$server} = $self;
132              
133 19         108 return $self;
134             }
135              
136             sub ping {
137             my $self = shift;
138              
139             return !$self->{_shutdown}
140             && !$self->{_quit};
141             }
142              
143             sub auth {
144             my $self = shift;
145              
146             confess '[auth] ERR wrong number of arguments for \'auth\' command' unless @_;
147              
148             return 'OK';
149             }
150              
151             sub quit {
152 3     3 0 646 my $self = shift;
153              
154 3         9 my $return = !$self->{_quit};
155              
156 3         6 $self->{_quit} = 1;
157 3         13 return $return;
158             }
159              
160             sub shutdown {
161             my $self = shift;
162              
163             $self->{_shutdown} = 1;
164             }
165              
166             sub set {
167             my ( $self, $key, $value, @args ) = @_;
168              
169             my ( $expires, $expire_cmd, $cond_cmd );
170             while (my $option = shift @args) {
171             $option = lc $option;
172              
173             if ($option eq 'nx' || $option eq 'xx') { # the same condition can be repeated but mix isn't allowed
174             confess '[set] ERR syntax error'
175             if defined $cond_cmd && $cond_cmd ne $option;
176              
177             $cond_cmd = $option;
178             } elsif ($option eq 'ex' || $option eq 'px') { # same units can be repeated but mix isn't allowed
179             confess '[set] ERR syntax error'
180             if defined $expire_cmd && $expire_cmd ne $option;
181              
182             $expire_cmd = $option;
183              
184             $expires = shift @args; # do we need a validation here?
185              
186             $expires /= 1000 # milliseconds to seconds
187             if $expire_cmd eq 'px';
188             } else {
189             confess '[set] ERR syntax error';
190             }
191             }
192              
193             if ( defined $cond_cmd ) {
194             # Only set if key exists
195             return
196             if $cond_cmd eq 'xx'
197             && ! $self->exists($key);
198              
199             # Only set if key doesn't exist
200             return
201             if $cond_cmd eq 'nx'
202             && $self->exists($key);
203             }
204              
205             $self->_stash->{$key} = "$value";
206             $self->expire($key, $expires)
207             if defined $expires;
208              
209             return 'OK';
210             }
211              
212             sub setnx {
213             my ( $self, $key, $value ) = @_;
214              
215             return 0 if $self->exists($key);
216              
217             $self->_stash->{$key} = "$value";
218              
219             return 1;
220             }
221              
222             sub setex {
223             my ( $self, $key, $ttl, $value ) = @_;
224             $self->set($key, $value);
225             $self->expire($key, $ttl);
226             return 'OK';
227             }
228              
229             sub expire {
230             my ( $self, $key, $ttl ) = @_;
231              
232             return $self->expireat($key, time + $ttl);
233             }
234              
235             sub expireat {
236             my ( $self, $key, $when ) = @_;
237              
238             return 0 unless exists $self->_stash->{$key};
239              
240             my $slot = $self->_stash;
241             my $tied = tied(%$slot);
242              
243             $tied->expire($key, $when);
244              
245             return 1;
246             }
247              
248             sub persist {
249             my ( $self, $key, $ttl ) = @_;
250              
251             return 0 unless exists $self->_stash->{$key};
252              
253             my $slot = $self->_stash;
254             my $tied = tied(%$slot);
255              
256             $tied->persist($key);
257              
258             return 1;
259             }
260              
261             sub ttl {
262             my ( $self, $key, $ttl ) = @_;
263              
264             return -2 unless exists $self->_stash->{$key};
265              
266             my $slot = $self->_stash;
267             my $tied = tied(%$slot);
268              
269             return $tied->ttl($key);
270             }
271              
272             sub exists :method {
273             my ( $self, $key ) = @_;
274             return exists $self->_stash->{$key} ? 1 : 0;
275             }
276              
277             sub get {
278             my ( $self, $key ) = @_;
279              
280             return $self->_stash->{$key};
281             }
282              
283             sub append {
284             my ( $self, $key, $value ) = @_;
285              
286             $self->_stash->{$key} .= $value;
287              
288             return $self->strlen($key);
289             }
290              
291             sub strlen {
292             my ( $self, $key ) = @_;
293 15     15   30785 return do { use bytes; length $self->_stash->{$key}; };
  15         218  
  15         82  
294             }
295              
296             sub getset {
297             my ( $self, $key, $value ) = @_;
298              
299             #TODO: should return error when original value isn't a string
300             my $old_value = $self->_stash->{$key};
301              
302             $self->set($key, $value);
303              
304             return $old_value;
305             }
306              
307             sub incr {
308             my ( $self, $key ) = @_;
309              
310             $self->_stash->{$key} ||= 0;
311              
312             return ++$self->_stash->{$key};
313             }
314              
315             sub incrby {
316             my ( $self, $key, $incr ) = @_;
317              
318             $self->_stash->{$key} ||= 0;
319              
320             return $self->_stash->{$key} += $incr;
321             }
322              
323             sub decr {
324             my ( $self, $key ) = @_;
325              
326             return --$self->_stash->{$key};
327             }
328              
329             sub decrby {
330             my ( $self, $key, $decr ) = @_;
331              
332             $self->_stash->{$key} ||= 0;
333              
334             return $self->_stash->{$key} -= $decr;
335             }
336              
337             sub mget {
338             my ( $self, @keys ) = @_;
339              
340             return map { $self->_stash->{$_} } @keys;
341             }
342              
343             sub mset {
344             my ( $self, %things ) = @_;
345              
346             @{ $self->_stash }{keys %things} = (values %things);
347              
348             return 'OK';
349             }
350              
351             sub msetnx {
352             my ( $self, %things ) = @_;
353              
354             $self->exists($_) && return 0 for keys %things;
355              
356             @{ $self->_stash }{keys %things} = (values %things);
357              
358             return 1;
359             }
360              
361             sub del {
362             my ( $self, @keys ) = @_;
363              
364             my $ret = 0;
365              
366             for my $key (@keys) {
367             $ret++ if $self->exists($key);
368             delete $self->_stash->{$key};
369             }
370              
371             return $ret;
372             }
373              
374             sub type {
375             my ( $self, $key ) = @_;
376             # types are string, list, set, zset and hash
377              
378             return 'none' unless $self->exists($key);
379              
380             my $type = ref $self->_stash->{$key};
381              
382             return !$type
383             ? 'string'
384             : $type eq 'Test::Mock::Redis::Hash'
385             ? 'hash'
386             : $type eq 'Test::Mock::Redis::Set'
387             ? 'set'
388             : $type eq 'Test::Mock::Redis::ZSet'
389             ? 'zset'
390             : $type eq 'Test::Mock::Redis::List'
391             ? 'list'
392             : 'unknown'
393             ;
394             }
395              
396             sub keys :method {
397             my ( $self, $match ) = @_;
398              
399             confess q{[KEYS] ERR wrong number of arguments for 'keys' command} unless defined $match;
400              
401             # TODO: we're not escaping other meta-characters
402             $match =~ s/(?
403             $match =~ s/(?
404              
405             return @{[ sort { $a cmp $b }
406             grep { exists $self->_stash->{$_} }
407             grep { /^$match/ }
408             keys %{ $self->_stash }]};
409             }
410              
411             sub randomkey {
412             my $self = shift;
413              
414             return ( keys %{ $self->_stash } )[
415             int(rand( scalar keys %{ $self->_stash } ))
416             ]
417             ;
418             }
419              
420             sub rename {
421             my ( $self, $from, $to, $whine ) = @_;
422              
423             confess '[rename] ERR source and destination objects are the same' if $from eq $to;
424             confess '[rename] ERR no such key' unless $self->exists($from);
425             confess 'rename to existing key' if $whine && $self->_stash->{$to};
426              
427             $self->_stash->{$to} = $self->_stash->{$from};
428             delete $self->_stash->{$from};
429             return 'OK';
430             }
431              
432             sub renamenx {
433             my ( $self, $from, $to ) = @_;
434              
435             return 0 if $self->exists($to);
436             return $self->rename($from, $to);
437             }
438              
439             sub dbsize {
440             my $self = shift;
441              
442             return scalar keys %{ $self->_stash };
443             }
444              
445             sub rpush {
446             my ( $self, $key, @values ) = @_;
447              
448             confess "[rpush] ERR wrong number of arguments for 'rpush' command"
449             unless @values;
450              
451             confess "[rpush] WRONGTYPE Operation against a key holding the wrong kind of value"
452             unless !$self->exists($key) or $self->_is_list($key);
453              
454             $self->_make_list($key);
455              
456             push @{ $self->_stash->{$key} }, map "$_", @values;
457             return scalar @{ $self->_stash->{$key} };
458             }
459              
460             sub lpush {
461             my ( $self, $key, @values ) = @_;
462              
463             confess "[lpush] ERR wrong number of arguments for 'lpush' command"
464             unless @values;
465              
466             confess "[lpush] WRONGTYPE Operation against a key holding the wrong kind of value"
467             unless !$self->exists($key) or $self->_is_list($key);
468              
469             $self->_make_list($key);
470              
471             unshift @{ $self->_stash->{$key} }, map "$_", reverse @values;
472             return scalar @{ $self->_stash->{$key} };
473             }
474              
475             sub rpushx {
476             my ( $self, $key, $value ) = @_;
477              
478             return unless $self->_is_list($key);
479              
480             push @{ $self->_stash->{$key} }, "$value";
481             return scalar @{ $self->_stash->{$key} };
482             }
483              
484             sub lpushx {
485             my ( $self, $key, $value ) = @_;
486              
487             return unless $self->_is_list($key);
488              
489             unshift @{ $self->_stash->{$key} }, "$value";
490             return scalar @{ $self->_stash->{$key} };
491             }
492              
493             sub rpoplpush {
494             my ( $self, $source_key, $destination_key ) = @_;
495              
496             my $popped_element = $self->rpop( $source_key ) or return;
497              
498             $self->lpush( $destination_key, $popped_element );
499              
500             return $popped_element;
501             }
502              
503             sub llen {
504             my ( $self, $key ) = @_;
505              
506             return 0 unless $self->exists($key);
507              
508             return scalar @{ $self->_stash->{$key} };
509             }
510              
511             sub lrange {
512             my ( $self, $key, $start, $end ) = @_;
513              
514             my @result;
515              
516             if ( my $array = $self->_stash->{$key} ) {
517             ($start,$end) = _normalize_range(scalar(@$array),$start,$end);
518             @result = @{ $array }[$start..$end];
519             }
520              
521             return wantarray ? @result : \ @result;
522             }
523              
524             sub ltrim {
525             my ( $self, $key, $start, $end ) = @_;
526              
527             my $array = $self->_stash->{$key};
528             ($start,$end) = _normalize_range(scalar(@$array),$start,$end);
529             $self->_stash->{$key} = [ @{ $array }[$start..$end] ];
530             return 'OK';
531             }
532              
533             sub lindex {
534             my ( $self, $key, $index ) = @_;
535              
536             my $array = $self->_stash->{$key};
537             $index = _normalize_index(scalar(@$array),$index);
538             return $array->[$index];
539             }
540              
541             sub lset {
542             my ( $self, $key, $index, $value ) = @_;
543              
544             my $array = $self->_stash->{$key};
545             $index = _normalize_index(scalar(@$array),$index);
546             $array->[$index] = "$value";
547             return 'OK';
548             }
549              
550             sub lrem {
551             my ( $self, $key, $count, $value ) = @_;
552             my $removed;
553             my @indicies = (0..$#{ $self->_stash->{$key} });
554             @indicies = reverse @indicies if $count < 0;
555             $count = abs $count;
556              
557             my @to_remove;
558             for my $index (@indicies){
559             if($self->_stash->{$key}->[$index] eq $value){
560             push @to_remove, $index;
561             $removed++;
562             last if $count && $removed >= $count;
563             }
564             }
565              
566             # reverse sort so that the higher indecies are removed first
567             for my $rm_idx (sort { $b <=> $a } @to_remove){
568             splice @{ $self->_stash->{$key} }, $rm_idx, 1;
569             }
570              
571             return $removed;
572             }
573              
574             sub lpop {
575             my ( $self, $key ) = @_;
576              
577             return undef unless $self->exists($key);
578              
579             return shift @{ $self->_stash->{$key} };
580             }
581              
582             sub rpop {
583             my ( $self, $key ) = @_;
584              
585             return undef unless $self->exists($key);
586              
587             return pop @{ $self->_stash->{$key} };
588             }
589              
590             sub select {
591             my ( $self, $index ) = @_;
592              
593             my $max_index = $#{ $self->{_stash} };
594             if ($index > $max_index ){
595             die "You called select($index), but max allowed is $max_index unless you configure more databases";
596             }
597              
598             $self->{_db_index} = $index;
599             return 'OK';
600             }
601              
602             sub _stash {
603 3593     3593   5958 my ( $self, $index ) = @_;
604 3593 100       7030 $index = $self->{_db_index} unless defined $index;
605              
606 3593         12923 return $self->{_stash}->[$index];
607             }
608              
609             sub sadd {
610             my ( $self, $key, $value ) = @_;
611              
612             $self->_make_set($key);
613             my $return = exists $self->_stash->{$key}->{$value}
614             ? 0
615             : 1;
616             $self->_stash->{$key}->{$value} = 1;
617             return $return;
618             }
619              
620             sub scard {
621             my ( $self, $key ) = @_;
622              
623             return $self->_is_set($key)
624             ? scalar $self->smembers($key)
625             : 0;
626             }
627              
628             sub sismember {
629             my ( $self, $key, $value ) = @_;
630              
631             return exists $self->_stash->{$key}->{$value}
632             ? 1
633             : 0;
634             }
635              
636             sub srem {
637             my ( $self, $key, $value ) = @_;
638              
639             return 0 unless exists $self->_stash->{$key}
640             && exists $self->_stash->{$key}->{$value};
641              
642             delete $self->_stash->{$key}->{$value};
643             return 1;
644             }
645              
646             sub spop {
647             my ( $self, $key ) = @_;
648              
649             return undef unless $self->_is_set($key);
650              
651             my $value = $self->srandmember($key);
652             delete $self->_stash->{$key}->{$value};
653             return $value;
654             }
655              
656             sub smove {
657             my ( $self, $source, $dest, $value ) = @_;
658              
659             confess "[smove] WRONGTYPE Operation against a key holding the wrong kind of value"
660             if ( $self->exists($source) and not $self->_is_set($source) )
661             or ( $self->exists($dest) and not $self->_is_set($dest) );
662              
663             if( (delete $self->_stash->{$source}->{$value}) ){
664             $self->_make_set($dest) unless $self->_is_set($dest);
665             $self->_stash->{$dest}->{$value} = 1;
666             return 1;
667             }
668             return 0; # guess it wasn't in there
669             }
670              
671             sub srandmember {
672             my ( $self, $key ) = @_;
673              
674             return undef unless $self->_is_set($key);
675              
676             return ($self->smembers($key))[rand int $self->scard($key)];
677             }
678              
679             sub smembers {
680             my ( $self, $key ) = @_;
681              
682             return keys %{ $self->_stash->{$key} };
683             }
684              
685             sub sinter {
686             my ( $self, @keys ) = @_;
687              
688             my $r = {};
689              
690             foreach my $key (@keys){
691             $r->{$_}++ for keys %{ $self->_stash->{$key} };
692             }
693              
694             return grep { $r->{$_} >= @keys } keys %$r;
695             }
696              
697             sub sinterstore {
698             my ( $self, $dest, @keys ) = @_;
699              
700             $self->_stash->{$dest} = { map { $_ => 1 } $self->sinter(@keys) };
701             bless $self->_stash->{$dest}, 'Test::Mock::Redis::Set';
702             return $self->scard($dest);
703             }
704              
705             sub sunion {
706             my ( $self, @keys ) = @_;
707              
708             my $r = {};
709              
710             foreach my $key (@keys){
711             $r->{$_}++ for keys %{ $self->_stash->{$key} };
712             }
713              
714             return grep { $r->{$_} >= 1 } keys %$r;
715             }
716              
717             sub sunionstore {
718             my ( $self, $dest, @keys ) = @_;
719              
720             $self->_stash->{$dest} = { map { $_ => 1 } $self->sunion(@keys) };
721             bless $self->_stash->{$dest}, 'Test::Mock::Redis::Set';
722             return $self->scard($dest);
723             }
724              
725             sub sdiff {
726             my ( $self, $start, @keys ) = @_;
727              
728             my $r = { map { $_ => 0 } keys %{ $self->_stash->{$start} } };
729              
730             foreach my $key (@keys){
731             $r->{$_}++ for keys %{ $self->_stash->{$key} };
732             }
733              
734             return grep { $r->{$_} == 0 } keys %$r;
735             }
736              
737             sub sdiffstore {
738             my ( $self, $dest, $start, @keys ) = @_;
739              
740             $self->_stash->{$dest} = { map { $_ => 1 } $self->sdiff($start, @keys) };
741             bless $self->_stash->{$dest}, 'Test::Mock::Redis::Set';
742             return $self->scard($dest);
743             }
744              
745             sub hset {
746             my ( $self, $key, $hkey, $value ) = @_;
747              
748             confess '[hset] WRONGTYPE Operation against a key holding the wrong kind of value'
749             if $self->exists($key) and !$self->_is_hash($key);
750              
751              
752             $self->_make_hash($key);
753              
754             my $ret = exists $self->_stash->{$key}->{$hkey}
755             ? 0
756             : 1;
757             $self->_stash->{$key}->{$hkey} = $value;
758             return $ret;
759             }
760              
761             sub hsetnx {
762             my ( $self, $key, $hkey, $value ) = @_;
763              
764             return 0 if exists $self->_stash->{$key}->{$hkey};
765              
766             $self->_make_hash($key);
767              
768             $self->_stash->{$key}->{$hkey} = "$value";
769             return 1;
770             }
771              
772             sub hmset {
773             my ( $self, $key, %hash ) = @_;
774              
775             $self->_make_hash($key);
776              
777             foreach my $hkey ( keys %hash ){
778             $self->hset($key, $hkey, $hash{$hkey});
779             }
780              
781             return 'OK';
782             }
783              
784             sub hget {
785             my ( $self, $key, $hkey ) = @_;
786              
787             return undef unless $self->_is_hash($key);
788              
789             return $self->_stash->{$key}->{$hkey};
790             }
791              
792             sub hmget {
793             my ( $self, $key, @hkeys ) = @_;
794              
795             return undef unless $self->_is_hash($key);
796              
797             return map { $self->_stash->{$key}->{$_} } @hkeys;
798             }
799              
800             sub hexists {
801             my ( $self, $key, $hkey ) = @_;
802              
803             confess '[hexists] WRONGTYPE Operation against a key holding the wrong kind of value'
804             if $self->exists($key) and !$self->_is_hash($key);
805              
806             return $self->exists($key) && exists $self->_stash->{$key}->{$hkey} ? 1 : 0;
807             }
808              
809             sub hdel {
810             my ( $self, $key, $hkey ) = @_;
811              
812             return 0 unless $self->_is_hash($key);
813              
814             my $ret = $self->hexists($key, $hkey);
815             delete $self->_stash->{$key}->{$hkey};
816             return $ret;
817             }
818              
819             sub hincrby {
820             confess "[hincrby] ERR wrong number of arguments for 'hincrby' command"
821             unless @_ == 4;
822              
823             my ( $self, $key, $hkey, $incr ) = @_;
824              
825             confess '[hexists] ERR Operation against a key holding the wrong kind of value'
826             if $self->exists($key) and !$self->_is_hash($key);
827              
828             confess "[hincrby] ERR hash value is not an integer"
829             if $self->hexists($key, $hkey) # it exists
830             and $self->hget($key, $hkey) !~ /^-?\d+$/ # and it doesn't look like an integer (and it isn't empty)
831             ;
832              
833             $self->_make_hash($key) unless $self->_is_hash($key);
834              
835             $self->_stash->{$key}->{$hkey} ||= 0;
836              
837             return $self->_stash->{$key}->{$hkey} += $incr;
838             }
839              
840             sub hlen {
841             my ( $self, $key ) = @_;
842              
843             return 0 unless $self->_is_hash($key);
844              
845             return scalar values %{ $self->_stash->{$key} };
846             }
847              
848             sub hkeys {
849             my ( $self, $key ) = @_;
850              
851             confess '[hkeys] WRONGTYPE Operation against a key holding the wrong kind of value'
852             if $self->exists($key) and !$self->_is_hash($key);
853              
854             return () unless $self->exists($key);
855              
856             return keys %{ $self->_stash->{$key} };
857             }
858              
859             sub hvals {
860             my ( $self, $key ) = @_;
861              
862             confess '[hvals] WRONGTYPE Operation against a key holding the wrong kind of value'
863             if $self->exists($key) and !$self->_is_hash($key);
864              
865             return values %{ $self->_stash->{$key} };
866             }
867              
868             sub hgetall {
869             my ( $self, $key ) = @_;
870              
871             confess "[hgetall] WRONGTYPE Operation against a key holding the wrong kind of value"
872             if $self->exists($key) and !$self->_is_hash($key);
873              
874             return $self->exists( $key )
875             ? %{ $self->_stash->{$key} }
876             : ();
877             }
878              
879             sub move {
880             my ( $self, $key, $to ) = @_;
881              
882             return 0 unless !exists $self->_stash($to)->{$key}
883             && exists $self->_stash->{$key}
884             ;
885              
886             $self->_stash($to)->{$key} = $self->_stash->{$key};
887             delete $self->_stash->{$key};
888             return 1;
889             }
890              
891             sub flushdb {
892             my $self = shift;
893              
894             $self->{_stash}->[$self->{_db_index}] = _new_db;
895             }
896              
897             sub flushall {
898             my $self = shift;
899              
900             $self->{_stash} = [ map { _new_db }(1..$NUM_DBS) ];
901             }
902              
903             sub sort {
904             my ( $self, $key, $how ) = @_;
905              
906             my $cmp = do
907 15     15   62852 { no warnings 'uninitialized';
  15         33  
  15         51329  
908             $how =~ /\bALPHA\b/
909             ? $how =~ /\bDESC\b/
910             ? sub { $b cmp $a }
911             : sub { $a cmp $b }
912             : $how =~ /\bDESC\b/
913             ? sub { $b <=> $a }
914             : sub { $a <=> $b }
915             ;
916             };
917              
918             return sort $cmp @{ $self->_stash->{$key} };
919             }
920              
921             sub save {
922             my $self = shift;
923             $self->{_last_save} = time;
924             return 'OK';
925             }
926              
927             sub bgsave {
928             my $self = shift;
929             return $self->save;
930             }
931              
932             sub lastsave {
933             my $self = shift;
934             return $self->{_last_save};
935             }
936              
937             sub info {
938             my $self = shift;
939              
940             return {
941             aof_current_rewrite_time_sec => '-1',
942             aof_enabled => '0',
943             aof_last_bgrewrite_status => 'ok',
944             aof_last_rewrite_time_sec => '-1',
945             aof_rewrite_in_progress => '0',
946             aof_rewrite_scheduled => '0',
947             arch_bits => $Config{use64bitint } ? '64' : '32',
948             blocked_clients => '0',
949             client_biggest_input_buf => '0',
950             client_longest_output_list => '0',
951             connected_clients => '1',
952             connected_slaves => '0',
953             evicted_keys => '0',
954             expired_keys => '0',
955             gcc_version => '4.2.1',
956             instantaneous_ops_per_sec => '568',
957             keyspace_hits => '272',
958             keyspace_misses => '0',
959             latest_fork_usec => '0',
960             loading => '0',
961             lru_clock => '1994309',
962             mem_allocator => 'libc',
963             mem_fragmentation_ratio => '1.61',
964             multiplexing_api => 'kqueue',
965             os => $Config{osname}.' '.$Config{osvers}, # should be like 'Darwin 12.2.1 x86_64', this is close
966             process_id => $$,
967             pubsub_channels => '0',
968             pubsub_patterns => '0',
969             rdb_bgsave_in_progress => '0',
970             rdb_changes_since_last_save => '0',
971             rdb_current_bgsave_time_sec => '-1',
972             rdb_last_bgsave_status => 'ok',
973             rdb_last_bgsave_time_sec => '-1',
974             rdb_last_save_time => '1362120372',
975             redis_git_dirty => '0',
976             redis_git_sha1 => '34b420db',
977             redis_mode => 'standalone',
978             redis_version => '2.6.10',
979             rejected_connections => '0',
980             role => 'master',
981             run_id => $self->{_run_id},
982             tcp_port => '11084',
983             total_commands_processed => '1401',
984             total_connections_received => '1',
985             uptime_in_days => (time - $self->{_up_since}) / 60 / 60 / 24,
986             uptime_in_seconds => time - $self->{_up_since},
987             used_cpu_sys => '0.04',
988             used_cpu_sys_children => '0.00',
989             used_cpu_user => '0.02',
990             used_cpu_user_children => '0.00',
991             used_memory => '1056288',
992             used_memory_human => '1.01M',
993             used_memory_lua => '31744',
994             used_memory_peak => '1055728',
995             used_memory_peak_human => '1.01M',
996             used_memory_rss => '1699840',
997             map { 'db'.$_ => sprintf('keys=%d,expires=%d,avg_ttl=%d',
998             scalar keys %{ $self->_stash($_) },
999             $self->_expires_count_and_avg_ttl_for_db($_),
1000             )
1001             } grep { scalar keys %{ $self->_stash($_) } > 0 }
1002             (0..15)
1003             };
1004             }
1005              
1006             sub _expires_count_and_avg_ttl_for_db {
1007 32     32   54 my ( $self, $db_index ) = @_;
1008              
1009 32         50 my $slot = $self->_stash($db_index);
1010 32         49 my $tied = tied(%$slot);
1011              
1012 32         58 $tied->expire_count_and_avg_ttl;
1013             }
1014              
1015             sub zadd {
1016             my ( $self, $key, $score, $value ) = @_;
1017              
1018             $self->_make_zset($key);
1019              
1020             my $ret = exists $self->_stash->{$key}->{$value}
1021             ? 0
1022             : 1;
1023             $self->_stash->{$key}->{$value} = $score;
1024             return $ret;
1025             }
1026              
1027              
1028             sub zscore {
1029             my ( $self, $key, $value ) = @_;
1030             return $self->_stash->{$key}->{$value};
1031             }
1032              
1033             sub zincrby {
1034             my ( $self, $key, $score, $value ) = @_;
1035              
1036             $self->_stash->{$key}->{$value} ||= 0;
1037              
1038             return $self->_stash->{$key}->{$value} += $score;
1039             }
1040              
1041             sub zrank {
1042             my ( $self, $key, $value ) = @_;
1043             my $rank = 0;
1044             foreach my $elem ( $self->zrange($key, 0, $self->zcard($key)) ){
1045             return $rank if $value eq $elem;
1046             $rank++;
1047             }
1048             return undef;
1049             }
1050              
1051             sub zrevrank {
1052             my ( $self, $key, $value ) = @_;
1053             my $rank = 0;
1054             foreach my $elem ( $self->zrevrange($key, 0, $self->zcard($key)) ){
1055             return $rank if $value eq $elem;
1056             $rank++;
1057             }
1058             return undef;
1059             }
1060              
1061             sub zrange {
1062             my ( $self, $key, $start, $stop, $withscores ) = @_;
1063              
1064             my $length = $self->zcard($key);
1065             ($start,$stop) = _normalize_range($length,$start,$stop);
1066              
1067             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1068             ( map { $_->[0] }
1069             sort { $a->[1] <=> $b->[1] || $a->[0] cmp $b->[0] }
1070             map { [ $_, $self->_stash->{$key}->{$_} ] }
1071             keys %{ $self->_stash->{$key} }
1072             )[$start..$stop]
1073             ;
1074             }
1075              
1076             sub zrevrange {
1077             my ( $self, $key, $start, $stop, $withscores ) = @_;
1078              
1079             my $length = $self->zcard($key);
1080             ($start,$stop) = _normalize_range($length,$start,$stop);
1081              
1082             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1083             ( map { $_->[0] }
1084             sort { $b->[1] <=> $a->[1] || $b->[0] cmp $a->[0] }
1085             map { [ $_, $self->_stash->{$key}->{$_} ] }
1086             keys %{ $self->_stash->{$key} }
1087             )[$start..$stop]
1088             ;
1089             }
1090              
1091             sub zrangebyscore {
1092             my ( $self, $key, $min, $max, $withscores ) = @_;
1093              
1094             my $min_inc = !( $min =~ s/^\(// );
1095             my $max_inc = !( $max =~ s/^\(// );
1096              
1097             my $cmp = !$min_inc && !$max_inc
1098             ? sub { $self->zscore($key, $_[0]) > $min && $self->zscore($key, $_[0]) < $max }
1099             : !$min_inc
1100             ? sub { $self->zscore($key, $_[0]) > $min && $self->zscore($key, $_[0]) <= $max }
1101             : !$max_inc
1102             ? sub { $self->zscore($key, $_[0]) >= $min && $self->zscore($key, $_[0]) < $max }
1103             : sub { $self->zscore($key, $_[0]) >= $min && $self->zscore($key, $_[0]) <= $max }
1104             ;
1105              
1106             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1107             grep { $cmp->($_) } $self->zrange($key, 0, $self->zcard($key)-1);
1108             }
1109              
1110             # note max and min are reversed from zrangebyscore
1111             sub zrevrangebyscore {
1112             my ( $self, $key, $max, $min, $withscores ) = @_;
1113              
1114             my $not_with_scores = 0;
1115              
1116             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1117             reverse $self->zrangebyscore($key, $min, $max, $not_with_scores);
1118             }
1119              
1120             sub zcount {
1121             my ( $self, $key, $min, $max ) = @_;
1122             return scalar $self->zrangebyscore($key, $min, $max);
1123             }
1124              
1125             sub zcard {
1126             my ( $self, $key ) = @_;
1127             return scalar values %{ $self->_stash->{$key} }
1128             }
1129              
1130             sub zremrangebyrank {
1131             my ( $self, $key, $start, $stop ) = @_;
1132              
1133             my @remove = $self->zrange($key, $start, $stop);
1134             delete $self->_stash->{$key}->{$_} for @remove;
1135             return scalar @remove;
1136             }
1137              
1138             sub zremrangebyscore {
1139             my ( $self, $key, $start, $stop ) = @_;
1140              
1141             my @remove = $self->zrangebyscore($key, $start, $stop);
1142             delete $self->_stash->{$key}->{$_} for @remove;
1143             return scalar @remove;
1144             }
1145              
1146             =head1 PIPELINING
1147              
1148             See L -- most methods support the use of a callback sub as
1149             the final argument. For this implementation, the callback sub will be called
1150             immediately (before the result of the original method is returned), and
1151             C does nothing. Combining pipelining with C/C
1152             is not supported.
1153              
1154             =head1 TODO
1155              
1156             Lots!
1157              
1158             Not all Redis functionality is implemented. The test files that output "TODO" are still to be done.
1159              
1160             The top of all test files [except 01-basic.t] has the list of commands tested or to-be tested in the file.
1161              
1162             Those marked with an "x" are pretty well-tested.
1163             Those marked with an "o" need help.
1164             Those that are unmarked have no tests, or are un-implemented. For example:
1165              
1166             x AUTH <--- has some tests
1167              
1168             o KEYS <--- only partially tested and/or implemented
1169              
1170             ZINTERSTORE <--- not tested (or maybe not implemented)
1171              
1172              
1173              
1174             Beyond that, it would be neat to add methods to inspect how often keys were accessed and get other information that
1175             allows the module user to confirm that their code interacted with redis (or Test::Mock::Redis) as they expected.
1176              
1177              
1178             =head1 AUTHOR
1179              
1180             Jeff Lavallee, C<< >>
1181              
1182             =head1 SEE ALSO
1183              
1184             The real Redis.pm client whose interface this module mimics: L
1185              
1186              
1187             =head1 BUGS
1188              
1189             Please report any bugs or feature requests to C, or through
1190             the web interface at L. I will be notified, and then you'll
1191             automatically be notified of progress on your bug as I make changes.
1192              
1193              
1194              
1195             =head1 SUPPORT
1196              
1197             You can find documentation for this module with the perldoc command.
1198              
1199             perldoc Test::Mock::Redis
1200              
1201              
1202             You can also look for information at:
1203              
1204             =over 4
1205              
1206             =item * RT: CPAN's request tracker
1207              
1208             L
1209              
1210             =item * AnnoCPAN: Annotated CPAN documentation
1211              
1212             L
1213              
1214             =item * CPAN Ratings
1215              
1216             L
1217              
1218             =item * Search CPAN
1219              
1220             L
1221              
1222             =back
1223              
1224              
1225             =head1 ACKNOWLEDGEMENTS
1226              
1227             Salvatore Sanfilippo for redis, of course!
1228              
1229             Dobrica Pavlinusic & Pedro Melo for Redis.pm
1230              
1231             The following people have contributed to I:
1232              
1233             =over
1234              
1235             =item * Chris Reinhardt
1236              
1237             =item * Ian Burrell
1238              
1239             =item * Gianni Ceccarelli
1240              
1241             =item * Karen Etheridge
1242              
1243             =item * Keith Broughton
1244              
1245             =item * Kevin Goess
1246              
1247             =item * Neil Bowers
1248              
1249             =item * Nigel Gregoire
1250              
1251             =item * Thomas Bloor
1252              
1253             =item * Valery Kalesnik
1254              
1255             =item * Yaakov Shaul
1256              
1257             =back
1258              
1259             =head1 LICENSE AND COPYRIGHT
1260              
1261             Copyright 2018 Jeff Lavallee.
1262              
1263             This program is free software; you can redistribute it and/or modify it
1264             under the terms of either: the GNU General Public License as published
1265             by the Free Software Foundation; or the Artistic License.
1266              
1267             See L for more information.
1268              
1269              
1270             =cut
1271              
1272             sub _normalize_index {
1273 151     151   235 my ( $length, $index ) = @_;
1274              
1275 151 100       327 $index += $length if $index < 0;
1276 151         262 return $index;
1277             }
1278              
1279             sub _normalize_range {
1280 47     47   88 my ( $length, $start, $end ) = @_;
1281              
1282 47         99 $start = _normalize_index($length,$start);
1283 47         89 $end = _normalize_index($length,$end);
1284 47 100       119 $end = $length-1 if $end >= $length;
1285              
1286 47         92 return ($start,$end);
1287             }
1288              
1289             sub _is_list {
1290 129     129   211 my ( $self, $key ) = @_;
1291              
1292             return $self->exists($key)
1293             && blessed $self->_stash->{$key}
1294 129   66     2385 && $self->_stash->{$key}->isa('Test::Mock::Redis::List') ;
1295             }
1296              
1297             sub _make_list {
1298 69     69   117 my ( $self, $key ) = @_;
1299              
1300 69 100       132 $self->_stash->{$key} = Test::Mock::Redis::List->new
1301             unless $self->_is_list($key);
1302             }
1303              
1304             sub _is_hash {
1305 112     112   252 my ( $self, $key ) = @_;
1306              
1307             return $self->exists($key)
1308             && blessed $self->_stash->{$key}
1309 112   66     2074 && $self->_stash->{$key}->isa('Test::Mock::Redis::Hash') ;
1310             }
1311              
1312             sub _make_hash {
1313 25     25   49 my ( $self, $key ) = @_;
1314              
1315 25 100       70 $self->_stash->{$key} = Test::Mock::Redis::Hash->new
1316             unless $self->_is_hash($key);
1317             }
1318              
1319             sub _is_set {
1320 89     89   146 my ( $self, $key ) = @_;
1321              
1322             return $self->exists($key)
1323             && blessed $self->_stash->{$key}
1324 89   66     1653 && $self->_stash->{$key}->isa('Test::Mock::Redis::Set') ;
1325             }
1326              
1327             sub _make_set {
1328 32     32   53 my ( $self, $key ) = @_;
1329              
1330 32 100       64 $self->_stash->{$key} = Test::Mock::Redis::Set->new
1331             unless $self->_is_set($key);
1332             }
1333              
1334             sub _is_zset {
1335 20     20   31 my ( $self, $key ) = @_;
1336              
1337             return $self->exists($key)
1338             && blessed $self->_stash->{$key}
1339 20   66     356 && $self->_stash->{$key}->isa('Test::Mock::Redis::ZSet') ;
1340             }
1341              
1342             sub _make_zset {
1343 20     20   38 my ( $self, $key ) = @_;
1344              
1345 20 100       39 $self->_stash->{$key} = Test::Mock::Redis::ZSet->new
1346             unless $self->_is_zset($key);
1347             }
1348              
1349              
1350             # MULTI/EXEC/DISCARD: http://redis.io/topics/transactions
1351              
1352             sub multi {
1353 6     6 0 886 my ( $self ) = @_;
1354              
1355 6 100       114 confess '[multi] ERR MULTI calls can not be nested' if defined $self->{_multi_commands};
1356              
1357             # set up the list for storing commands sent between MULTI and EXEC/DISCARD
1358 5         14 $self->{_multi_commands} = [];
1359              
1360 5         17 return 'OK';
1361             }
1362              
1363             # methods that return a list, rather than a single value
1364             my @want_list = qw(mget keys lrange smembers sinter sunion sdiff hmget hkeys hvals hgetall sort zrange zrevrange zrangebyscore);
1365             my %want_list = map { $_ => 1 } @want_list;
1366              
1367             sub exec {
1368 4     4 0 147 my ( $self, $cb ) = @_;
1369              
1370             # we are going to commit all the changes we saved up;
1371             # replay them now and return all their output
1372              
1373 4 100       213 confess '[exec] ERR EXEC without MULTI' if not defined $self->{_multi_commands};
1374              
1375 3         6 my @commands = @{$self->{_multi_commands}};
  3         10  
1376 3         7 delete $self->{_multi_commands};
1377              
1378             # replay all the queries that were queued up
1379             # exec has special behaviour when run in a pipeline:
1380             # the $reply argument to the pipeline callback is an array ref whose elements are themselves [$reply, $error] pairs.
1381 3 100 66     18 if ( $cb && 'CODE' eq ref $cb ) {
1382             my @reply = map {
1383 1         3 my ( $method, @args ) = @$_;
  1         4  
1384             try {
1385 1     1   73 my @result = $self->$method( @args );
1386 1 50       8 [ $want_list{ $method } ? \ @result : $result[ 0 ],
1387             undef,
1388             ];
1389             } catch {
1390 0     0   0 s/^\[\w+\] //;
1391 0         0 [ undef, $_ ];
1392 1         9 };
1393             } @commands;
1394              
1395 1         21 $cb->( \ @reply, undef );
1396              
1397 1         3320 return 1;
1398             }
1399              
1400             # the returned result is a nested array of the results of all the commands
1401 2         3 my @exceptions;
1402             my @results = map {
1403 2         6 my ($method, @args) = @$_;
  7         18  
1404             my @result =
1405 7     7   443 try { $self->$method(@args) }
1406 7     1   43 catch { push @exceptions, $_; (); };
  1         595  
  1         4  
1407 7 100       120 $want_list{$method} ? \@result : $result[0];
1408             } @commands;
1409              
1410 2         12 s/^\[\w+\] // for @exceptions;
1411              
1412 2 100       132 confess('[exec] ', join('; ', @exceptions)) if @exceptions;
1413              
1414 1         10 return @results;
1415             }
1416              
1417             sub discard {
1418 3     3 0 1667 my ( $self ) = @_;
1419              
1420 3 100       103 confess '[discard] ERR DISCARD without MULTI' if not defined $self->{_multi_commands};
1421              
1422             # discard all the accumulated commands, without executing them
1423 2         4 delete $self->{_multi_commands};
1424              
1425 2         12 return 'OK';
1426             }
1427              
1428             sub watch {
1429             my ($self) = shift;
1430              
1431             confess '[watch] ERR wrong number of arguments for \'watch\' command' unless @_;
1432              
1433             return 'OK';
1434             }
1435              
1436             sub unwatch {
1437             my ($self) = shift;
1438              
1439             confess '[error] ERR wrong number of arguments for \'unwatch\' command' if @_;
1440              
1441             return 'OK';
1442             }
1443              
1444             # now that we've defined all our subs, we need to wrap them all in logic that
1445             # can check if we are in the middle of a MULTI, and if so, queue up the
1446             # commands for later replaying.
1447              
1448             my %no_transaction_wrap_methods = (
1449             new => 1,
1450             multi => 1,
1451             exec => 1,
1452             discard => 1,
1453             quit => 1,
1454             import => 1,
1455             change_num_databases => 1,
1456             );
1457              
1458             my @transaction_wrapped_methods =
1459             grep { !/^_/}
1460             grep { not $no_transaction_wrap_methods{$_} }
1461             Package::Stash->new(__PACKAGE__)->list_all_symbols('CODE');
1462              
1463             foreach my $method (@transaction_wrapped_methods)
1464             {
1465             around $method => sub {
1466             my $orig = shift;
1467             my $self = shift;
1468              
1469             # pass command through if we are not handling a MULTI
1470             return $self->$orig(@_) if not defined $self->{_multi_commands};
1471              
1472             push @{$self->{_multi_commands}}, [ $method, @_ ];
1473             return 'QUEUED';
1474             };
1475             }
1476              
1477              
1478             # PIPELINING SUPPORT
1479              
1480             # these method modifications must be done after (over top of) the modification
1481             # for transactions, as we need to check for/extract the $cb first.
1482              
1483             my %no_pipeline_wrap_methods = (
1484             new => 1,
1485             multi => 1,
1486             discard => 1,
1487             quit => 1,
1488             ping => 1,
1489             subscribe => 1,
1490             unsubscribe => 1,
1491             psubscribe => 1,
1492             punsubscribe => 1,
1493             wait_all_responses => 1,
1494             exec => 1, # doc: 'exec has special behaviour when run in a pipeline'. covered in the method
1495             );
1496              
1497             my @pipeline_wrapped_methods =
1498             grep { !/^_/}
1499             grep { not $no_pipeline_wrap_methods{$_} }
1500             Package::Stash->new(__PACKAGE__)->list_all_symbols('CODE');
1501              
1502             # this is a bit messy, and the wantarray logic may not be quite right.
1503             # Alternatively, we could implement all this by reusing the logic in the real
1504             # Redis.pm -- subclass Redis, override new/multi/exec/discard (and probably
1505             # some other special functions), and have __run_cmd use a dispatch table to
1506             # call all our overridden implementations.
1507              
1508             foreach my $method (@pipeline_wrapped_methods)
1509             {
1510             around $method => sub {
1511             my $orig = shift;
1512             my $self = shift;
1513             my @args = @_;
1514              
1515             my $cb = @args && ref $args[-1] eq 'CODE' ? pop @args : undef;
1516              
1517             return $self->$orig(@args) if not $cb;
1518              
1519             # this may be officially supported eventually -- see
1520             # https://github.com/melo/perl-redis/issues/17
1521             # and "Pipeline management" in the Redis docs
1522             # To make this work, we just need to special-case exec, to collect all the
1523             # results and errors in tuples and send that to the $cb
1524             # die 'cannot combine pipelining with MULTI' if $self->{_multi_commands};
1525              
1526             # We could also implement this with a queue, not bothering to process
1527             # the commands until wait_all_responses is called - but then we need to
1528             # make sure to call wait_all_responses explicitly as soon as a command
1529             # is issued without a $cb.
1530              
1531             my $error;
1532             my (@result) = try
1533             {
1534             $self->$orig(@args);
1535             }
1536             catch
1537             {
1538             $error = $_;
1539             ();
1540             };
1541              
1542             $cb->(
1543             # see notes above - this logic may not be quite right
1544             ( $want_list{$method} ? \@result : $result[0] ),
1545             $error,
1546             );
1547             return 1;
1548             };
1549             }
1550              
1551             # in a real Redis system, this will make all outstanding callbacks get called.
1552             sub wait_all_responses {}
1553              
1554              
1555             1; # End of Test::Mock::Redis
1556              
1557             package Test::Mock::Redis::List;
1558 11     11   37 sub new { return bless [], shift }
1559             1;
1560              
1561             package Test::Mock::Redis::Hash;
1562 5     5   22 sub new { return bless {}, shift }
1563             1;
1564              
1565             package Test::Mock::Redis::ZSet;
1566 3     3   9 sub new { return bless {}, shift }
1567             1;
1568              
1569             package Test::Mock::Redis::Set;
1570 7     7   26 sub new { return bless {}, shift }
1571             1;
1572              
1573             package Test::Mock::Redis::PossiblyVolatile;
1574              
1575 15     15   151 use strict; use warnings;
  15     15   41  
  15         473  
  15         94  
  15         30  
  15         561  
1576 15     15   7589 use Tie::Hash;
  15         13816  
  15         479  
1577 15     15   98 use base qw/Tie::StdHash/;
  15         33  
  15         10606  
1578              
1579             sub DELETE {
1580 21     21   45 my ( $self, $key ) = @_;
1581              
1582 21         105 delete $self->{$key};
1583             }
1584              
1585             my $expires;
1586              
1587             sub FETCH {
1588 2098     2098   3902 my ( $self, $key ) = @_;
1589              
1590             return $self->EXISTS($key)
1591 2098 100       3619 ? $self->{$key}
1592             : undef;
1593             }
1594              
1595             sub EXISTS {
1596 3076     3076   4960 my ( $self, $key ) = @_;
1597              
1598 3076         6436 $self->_delete_if_expired($key);
1599              
1600 3076         23171 return exists $self->{$key};
1601             }
1602              
1603             sub _delete_if_expired {
1604 3076     3076   4487 my ( $self, $key ) = @_;
1605 3076 100 100     8628 if(exists $expires->{$self}->{$key}
1606             && time >= $expires->{$self}->{$key}){
1607 3         12 delete $self->{$key};
1608 3         12 delete $expires->{$self}->{$key};
1609             }
1610             }
1611              
1612             sub expire {
1613 13     13   28 my ( $self, $key, $time ) = @_;
1614              
1615 13         38 $expires->{$self}->{$key} = $time;
1616             }
1617              
1618             sub expire_count_and_avg_ttl {
1619 32     32   47 my ( $self ) = @_;
1620              
1621 32         44 my $now = time();
1622 32         55 my $count = 0;
1623 32         42 my $ttl = 0; # looks like actual redis uses more complicated calculations here. let's do something simple to start with
1624 32         59 for my $key ( keys %{ $expires->{$self} } ) {
  32         96  
1625 5 50       12 if ( $now >= $expires->{$self}->{$key} ) {
1626 0         0 delete $self->{$key};
1627 0         0 delete $expires->{$self}->{$key};
1628             } else {
1629 5         7 ++ $count;
1630 5         13 $ttl += $expires->{$self}->{$key} - $now;
1631             }
1632             }
1633              
1634 32 100       74 $ttl = int( $ttl / $count * 1_000 )
1635             if $count;
1636              
1637 32         130 ( $count, $ttl );
1638             }
1639              
1640             sub persist {
1641 1     1   3 my ( $self, $key, $time ) = @_;
1642              
1643 1         3 delete $expires->{$self}->{$key};
1644             }
1645              
1646             sub ttl {
1647 8     8   18 my ( $self, $key ) = @_;
1648              
1649 8 100       27 return -1 unless exists $expires->{$self}->{$key};
1650 7         40 return $expires->{$self}->{$key} - time;
1651             }
1652              
1653              
1654             1;