File Coverage

blib/lib/Test/Mock/Redis.pm
Criterion Covered Total %
statement 129 129 100.0
branch 30 30 100.0
condition 11 15 73.3
subroutine 45 45 100.0
pod 1 4 25.0
total 216 223 96.8


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