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 14     14   257862 use warnings;
  14         22  
  14         363  
4 14     14   46 use strict;
  14         17  
  14         213  
5              
6 14     14   52 use Carp;
  14         15  
  14         700  
7 14     14   56 use Config;
  14         14  
  14         419  
8 14     14   47 use Scalar::Util qw/blessed/;
  14         21  
  14         785  
9 14     14   6254 use Class::Method::Modifiers;
  14         15788  
  14         675  
10 14     14   5624 use Package::Stash;
  14         68015  
  14         367  
11 14     14   64 use Try::Tiny;
  14         16  
  14         627  
12 14     14   5789 use namespace::clean; # important: clean all subs imported above this line
  14         71568  
  14         44  
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.20
21              
22             =cut
23              
24             our $VERSION = '0.20';
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 365     365   608 tie my %hash, 'Test::Mock::Redis::PossiblyVolatile';
94 365         1026 return \%hash;
95             }
96              
97              
98             sub _defaults {
99 18     18   77 my @hex = (0..9, 'a'..'f');
100             return (
101             _quit => 0,
102             _shutdown => 0,
103 332         334 _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 18         50 _run_id => (join '', map { $hex[rand @hex] } 1..40), # E.G. '0e7e19fc45139fdb26ff3dd35ca6725d9882f1b7',
  720         1409  
109             );
110             }
111              
112              
113             my $instances;
114              
115             sub new {
116 23     23 1 675695 my $class = shift;
117 23         55 my %args = @_;
118              
119             my $server = defined $args{server}
120 23 100       82 ? $args{'server'}
121             : 'localhost:6379';
122              
123 23 100       83 if( $instances->{$server} ){
124 5 100       340 confess "Could not connect to Redis server at $server" if $instances->{$server}->{_shutdown};
125 4         7 $instances->{$server}->{_quit} = 0;
126 4         11 return $instances->{$server};
127             }
128              
129 18         51 my $self = bless {$class->_defaults, server => $server}, $class;
130              
131 18         87 $instances->{$server} = $self;
132              
133 18         77 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 308 my $self = shift;
153              
154 3         6 my $return = !$self->{_quit};
155              
156 3         4 $self->{_quit} = 1;
157 3         9 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 14     14   20713 return do { use bytes; length $self->_stash->{$key}; };
  14         113  
  14         55  
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 = $count < 0
533             ? ($#{ $self->_stash->{$key} }..0)
534             : (0..$#{ $self->_stash->{$key} })
535             ;
536             $count = abs $count;
537              
538             for my $index (@indicies){
539             if($self->_stash->{$key}->[$index] eq $value){
540             splice @{ $self->_stash->{$key} }, $index, 1;
541             last if $count && ++$removed >= $count;
542             }
543             }
544              
545             return $removed;
546             }
547              
548             sub lpop {
549             my ( $self, $key ) = @_;
550              
551             return undef unless $self->exists($key);
552              
553             return shift @{ $self->_stash->{$key} };
554             }
555              
556             sub rpop {
557             my ( $self, $key ) = @_;
558              
559             return undef unless $self->exists($key);
560              
561             return pop @{ $self->_stash->{$key} };
562             }
563              
564             sub select {
565             my ( $self, $index ) = @_;
566              
567             my $max_index = $#{ $self->{_stash} };
568             if ($index > $max_index ){
569             die "You called select($index), but max allowed is $max_index unless you configure more databases";
570             }
571              
572             $self->{_db_index} = $index;
573             return 'OK';
574             }
575              
576             sub _stash {
577 3329     3329   2553 my ( $self, $index ) = @_;
578 3329 100       4651 $index = $self->{_db_index} unless defined $index;
579              
580 3329         8488 return $self->{_stash}->[$index];
581             }
582              
583             sub sadd {
584             my ( $self, $key, $value ) = @_;
585              
586             $self->_make_set($key);
587             my $return = exists $self->_stash->{$key}->{$value}
588             ? 0
589             : 1;
590             $self->_stash->{$key}->{$value} = 1;
591             return $return;
592             }
593              
594             sub scard {
595             my ( $self, $key ) = @_;
596              
597             return $self->_is_set($key)
598             ? scalar $self->smembers($key)
599             : 0;
600             }
601              
602             sub sismember {
603             my ( $self, $key, $value ) = @_;
604              
605             return exists $self->_stash->{$key}->{$value}
606             ? 1
607             : 0;
608             }
609              
610             sub srem {
611             my ( $self, $key, $value ) = @_;
612              
613             return 0 unless exists $self->_stash->{$key}
614             && exists $self->_stash->{$key}->{$value};
615              
616             delete $self->_stash->{$key}->{$value};
617             return 1;
618             }
619              
620             sub spop {
621             my ( $self, $key ) = @_;
622              
623             return undef unless $self->_is_set($key);
624              
625             my $value = $self->srandmember($key);
626             delete $self->_stash->{$key}->{$value};
627             return $value;
628             }
629              
630             sub smove {
631             my ( $self, $source, $dest, $value ) = @_;
632              
633             confess "[smove] ERR Operation against a key holding the wrong kind of value"
634             if ( $self->exists($source) and not $self->_is_set($source) )
635             or ( $self->exists($dest) and not $self->_is_set($dest) );
636              
637             if( (delete $self->_stash->{$source}->{$value}) ){
638             $self->_make_set($dest) unless $self->_is_set($dest);
639             $self->_stash->{$dest}->{$value} = 1;
640             return 1;
641             }
642             return 0; # guess it wasn't in there
643             }
644              
645             sub srandmember {
646             my ( $self, $key ) = @_;
647              
648             return undef unless $self->_is_set($key);
649              
650             return ($self->smembers($key))[rand int $self->scard($key)];
651             }
652              
653             sub smembers {
654             my ( $self, $key ) = @_;
655              
656             return keys %{ $self->_stash->{$key} };
657             }
658              
659             sub sinter {
660             my ( $self, @keys ) = @_;
661              
662             my $r = {};
663              
664             foreach my $key (@keys){
665             $r->{$_}++ for keys %{ $self->_stash->{$key} };
666             }
667              
668             return grep { $r->{$_} >= @keys } keys %$r;
669             }
670              
671             sub sinterstore {
672             my ( $self, $dest, @keys ) = @_;
673              
674             $self->_stash->{$dest} = { map { $_ => 1 } $self->sinter(@keys) };
675             bless $self->_stash->{$dest}, 'Test::Mock::Redis::Set';
676             return $self->scard($dest);
677             }
678              
679             sub sunion {
680             my ( $self, @keys ) = @_;
681              
682             my $r = {};
683              
684             foreach my $key (@keys){
685             $r->{$_}++ for keys %{ $self->_stash->{$key} };
686             }
687              
688             return grep { $r->{$_} >= 1 } keys %$r;
689             }
690              
691             sub sunionstore {
692             my ( $self, $dest, @keys ) = @_;
693              
694             $self->_stash->{$dest} = { map { $_ => 1 } $self->sunion(@keys) };
695             bless $self->_stash->{$dest}, 'Test::Mock::Redis::Set';
696             return $self->scard($dest);
697             }
698              
699             sub sdiff {
700             my ( $self, $start, @keys ) = @_;
701              
702             my $r = { map { $_ => 0 } keys %{ $self->_stash->{$start} } };
703              
704             foreach my $key (@keys){
705             $r->{$_}++ for keys %{ $self->_stash->{$key} };
706             }
707              
708             return grep { $r->{$_} == 0 } keys %$r;
709             }
710              
711             sub sdiffstore {
712             my ( $self, $dest, $start, @keys ) = @_;
713              
714             $self->_stash->{$dest} = { map { $_ => 1 } $self->sdiff($start, @keys) };
715             bless $self->_stash->{$dest}, 'Test::Mock::Redis::Set';
716             return $self->scard($dest);
717             }
718              
719             sub hset {
720             my ( $self, $key, $hkey, $value ) = @_;
721              
722             confess '[hset] ERR Operation against a key holding the wrong kind of value'
723             if $self->exists($key) and !$self->_is_hash($key);
724              
725              
726             $self->_make_hash($key);
727              
728             my $ret = exists $self->_stash->{$key}->{$hkey}
729             ? 0
730             : 1;
731             $self->_stash->{$key}->{$hkey} = $value;
732             return $ret;
733             }
734              
735             sub hsetnx {
736             my ( $self, $key, $hkey, $value ) = @_;
737              
738             return 0 if exists $self->_stash->{$key}->{$hkey};
739              
740             $self->_make_hash($key);
741              
742             $self->_stash->{$key}->{$hkey} = "$value";
743             return 1;
744             }
745              
746             sub hmset {
747             my ( $self, $key, %hash ) = @_;
748              
749             $self->_make_hash($key);
750              
751             foreach my $hkey ( keys %hash ){
752             $self->hset($key, $hkey, $hash{$hkey});
753             }
754              
755             return 'OK';
756             }
757              
758             sub hget {
759             my ( $self, $key, $hkey ) = @_;
760              
761             return undef unless $self->_is_hash($key);
762              
763             return $self->_stash->{$key}->{$hkey};
764             }
765              
766             sub hmget {
767             my ( $self, $key, @hkeys ) = @_;
768              
769             return undef unless $self->_is_hash($key);
770              
771             return map { $self->_stash->{$key}->{$_} } @hkeys;
772             }
773              
774             sub hexists {
775             my ( $self, $key, $hkey ) = @_;
776              
777             confess '[hexists] ERR Operation against a key holding the wrong kind of value'
778             if $self->exists($key) and !$self->_is_hash($key);
779              
780             return exists $self->_stash->{$key}->{$hkey} ? 1 : 0;
781             }
782              
783             sub hdel {
784             my ( $self, $key, $hkey ) = @_;
785              
786             return 0 unless $self->_is_hash($key);
787              
788             my $ret = $self->hexists($key, $hkey);
789             delete $self->_stash->{$key}->{$hkey};
790             return $ret;
791             }
792              
793             sub hincrby {
794             confess "[hincrby] ERR wrong number of arguments for 'hincrby' command"
795             unless @_ == 4;
796              
797             my ( $self, $key, $hkey, $incr ) = @_;
798              
799             confess '[hexists] ERR Operation against a key holding the wrong kind of value'
800             if $self->exists($key) and !$self->_is_hash($key);
801              
802             confess "[hincrby] ERR hash value is not an integer"
803             if $self->hexists($key, $hkey) # it exists
804             and $self->hget($key, $hkey) !~ /^-?\d+$|^$/ # and it doesn't look like an integer (and it isn't empty)
805             ;
806              
807             $self->_make_hash($key) unless $self->_is_hash($key);
808              
809             $self->_stash->{$key}->{$hkey} ||= 0;
810              
811             return $self->_stash->{$key}->{$hkey} += $incr;
812             }
813              
814             sub hlen {
815             my ( $self, $key ) = @_;
816              
817             return 0 unless $self->_is_hash($key);
818              
819             return scalar values %{ $self->_stash->{$key} };
820             }
821              
822             sub hkeys {
823             my ( $self, $key ) = @_;
824              
825             confess '[hkeys] ERR Operation against a key holding the wrong kind of value'
826             if $self->exists($key) and !$self->_is_hash($key);
827              
828             return () unless $self->exists($key);
829              
830             return keys %{ $self->_stash->{$key} };
831             }
832              
833             sub hvals {
834             my ( $self, $key ) = @_;
835              
836             confess '[hvals] ERR Operation against a key holding the wrong kind of value'
837             if $self->exists($key) and !$self->_is_hash($key);
838              
839             return values %{ $self->_stash->{$key} };
840             }
841              
842             sub hgetall {
843             my ( $self, $key ) = @_;
844              
845             confess "[hgetall] ERR Operation against a key holding the wrong kind of value"
846             if $self->exists($key) and !$self->_is_hash($key);
847              
848             return $self->exists( $key )
849             ? %{ $self->_stash->{$key} }
850             : ();
851             }
852              
853             sub move {
854             my ( $self, $key, $to ) = @_;
855              
856             return 0 unless !exists $self->_stash($to)->{$key}
857             && exists $self->_stash->{$key}
858             ;
859              
860             $self->_stash($to)->{$key} = $self->_stash->{$key};
861             delete $self->_stash->{$key};
862             return 1;
863             }
864              
865             sub flushdb {
866             my $self = shift;
867              
868             $self->{_stash}->[$self->{_db_index}] = _new_db;
869             }
870              
871             sub flushall {
872             my $self = shift;
873              
874             $self->{_stash} = [ map { _new_db }(1..$NUM_DBS) ];
875             }
876              
877             sub sort {
878             my ( $self, $key, $how ) = @_;
879              
880             my $cmp = do
881 14     14   40610 { no warnings 'uninitialized';
  14         21  
  14         33624  
882             $how =~ /\bALPHA\b/
883             ? $how =~ /\bDESC\b/
884             ? sub { $b cmp $a }
885             : sub { $a cmp $b }
886             : $how =~ /\bDESC\b/
887             ? sub { $b <=> $a }
888             : sub { $a <=> $b }
889             ;
890             };
891              
892             return sort $cmp @{ $self->_stash->{$key} };
893             }
894              
895             sub save {
896             my $self = shift;
897             $self->{_last_save} = time;
898             return 'OK';
899             }
900              
901             sub bgsave {
902             my $self = shift;
903             return $self->save;
904             }
905              
906             sub lastsave {
907             my $self = shift;
908             return $self->{_last_save};
909             }
910              
911             sub info {
912             my $self = shift;
913              
914             return {
915             aof_current_rewrite_time_sec => '-1',
916             aof_enabled => '0',
917             aof_last_bgrewrite_status => 'ok',
918             aof_last_rewrite_time_sec => '-1',
919             aof_rewrite_in_progress => '0',
920             aof_rewrite_scheduled => '0',
921             arch_bits => $Config{use64bitint } ? '64' : '32',
922             blocked_clients => '0',
923             client_biggest_input_buf => '0',
924             client_longest_output_list => '0',
925             connected_clients => '1',
926             connected_slaves => '0',
927             evicted_keys => '0',
928             expired_keys => '0',
929             gcc_version => '4.2.1',
930             instantaneous_ops_per_sec => '568',
931             keyspace_hits => '272',
932             keyspace_misses => '0',
933             latest_fork_usec => '0',
934             loading => '0',
935             lru_clock => '1994309',
936             mem_allocator => 'libc',
937             mem_fragmentation_ratio => '1.61',
938             multiplexing_api => 'kqueue',
939             os => $Config{osname}.' '.$Config{osvers}, # should be like 'Darwin 12.2.1 x86_64', this is close
940             process_id => $$,
941             pubsub_channels => '0',
942             pubsub_patterns => '0',
943             rdb_bgsave_in_progress => '0',
944             rdb_changes_since_last_save => '0',
945             rdb_current_bgsave_time_sec => '-1',
946             rdb_last_bgsave_status => 'ok',
947             rdb_last_bgsave_time_sec => '-1',
948             rdb_last_save_time => '1362120372',
949             redis_git_dirty => '0',
950             redis_git_sha1 => '34b420db',
951             redis_mode => 'standalone',
952             redis_version => '2.6.10',
953             rejected_connections => '0',
954             role => 'master',
955             run_id => $self->{_run_id},
956             tcp_port => '11084',
957             total_commands_processed => '1401',
958             total_connections_received => '1',
959             uptime_in_days => (time - $self->{_up_since}) / 60 / 60 / 24,
960             uptime_in_seconds => time - $self->{_up_since},
961             used_cpu_sys => '0.04',
962             used_cpu_sys_children => '0.00',
963             used_cpu_user => '0.02',
964             used_cpu_user_children => '0.00',
965             used_memory => '1056288',
966             used_memory_human => '1.01M',
967             used_memory_lua => '31744',
968             used_memory_peak => '1055728',
969             used_memory_peak_human => '1.01M',
970             used_memory_rss => '1699840',
971             map { 'db'.$_ => sprintf('keys=%d,expires=%d',
972             scalar keys %{ $self->_stash($_) },
973             $self->_expires_count_for_db($_),
974             )
975             } grep { scalar keys %{ $self->_stash($_) } > 0 }
976             (0..15)
977             };
978             }
979              
980             sub _expires_count_for_db {
981 32     32   22 my ( $self, $db_index ) = @_;
982              
983 32         35 my $slot = $self->_stash($db_index);
984 32         29 my $tied = tied(%$slot);
985              
986 32         37 $tied->expire_count;
987             }
988              
989             sub zadd {
990             my ( $self, $key, $score, $value ) = @_;
991              
992             $self->_make_zset($key);
993              
994             my $ret = exists $self->_stash->{$key}->{$value}
995             ? 0
996             : 1;
997             $self->_stash->{$key}->{$value} = $score;
998             return $ret;
999             }
1000              
1001              
1002             sub zscore {
1003             my ( $self, $key, $value ) = @_;
1004             return $self->_stash->{$key}->{$value};
1005             }
1006              
1007             sub zincrby {
1008             my ( $self, $key, $score, $value ) = @_;
1009              
1010             $self->_stash->{$key}->{$value} ||= 0;
1011              
1012             return $self->_stash->{$key}->{$value} += $score;
1013             }
1014              
1015             sub zrank {
1016             my ( $self, $key, $value ) = @_;
1017             my $rank = 0;
1018             foreach my $elem ( $self->zrange($key, 0, $self->zcard($key)) ){
1019             return $rank if $value eq $elem;
1020             $rank++;
1021             }
1022             return undef;
1023             }
1024              
1025             sub zrevrank {
1026             my ( $self, $key, $value ) = @_;
1027             my $rank = 0;
1028             foreach my $elem ( $self->zrevrange($key, 0, $self->zcard($key)) ){
1029             return $rank if $value eq $elem;
1030             $rank++;
1031             }
1032             return undef;
1033             }
1034              
1035             sub zrange {
1036             my ( $self, $key, $start, $stop, $withscores ) = @_;
1037              
1038             my $length = $self->zcard($key);
1039             ($start,$stop) = _normalize_range($length,$start,$stop);
1040              
1041             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1042             ( map { $_->[0] }
1043             sort { $a->[1] <=> $b->[1] || $a->[0] cmp $b->[0] }
1044             map { [ $_, $self->_stash->{$key}->{$_} ] }
1045             keys %{ $self->_stash->{$key} }
1046             )[$start..$stop]
1047             ;
1048             }
1049              
1050             sub zrevrange {
1051             my ( $self, $key, $start, $stop, $withscores ) = @_;
1052              
1053             my $length = $self->zcard($key);
1054             ($start,$stop) = _normalize_range($length,$start,$stop);
1055              
1056             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1057             ( map { $_->[0] }
1058             sort { $b->[1] <=> $a->[1] || $b->[0] cmp $a->[0] }
1059             map { [ $_, $self->_stash->{$key}->{$_} ] }
1060             keys %{ $self->_stash->{$key} }
1061             )[$start..$stop]
1062             ;
1063             }
1064              
1065             sub zrangebyscore {
1066             my ( $self, $key, $min, $max, $withscores ) = @_;
1067              
1068             my $min_inc = !( $min =~ s/^\(// );
1069             my $max_inc = !( $max =~ s/^\(// );
1070              
1071             my $cmp = !$min_inc && !$max_inc
1072             ? sub { $self->zscore($key, $_[0]) > $min && $self->zscore($key, $_[0]) < $max }
1073             : !$min_inc
1074             ? sub { $self->zscore($key, $_[0]) > $min && $self->zscore($key, $_[0]) <= $max }
1075             : !$max_inc
1076             ? sub { $self->zscore($key, $_[0]) >= $min && $self->zscore($key, $_[0]) < $max }
1077             : sub { $self->zscore($key, $_[0]) >= $min && $self->zscore($key, $_[0]) <= $max }
1078             ;
1079              
1080             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1081             grep { $cmp->($_) } $self->zrange($key, 0, $self->zcard($key)-1);
1082             }
1083              
1084             # note max and min are reversed from zrangebyscore
1085             sub zrevrangebyscore {
1086             my ( $self, $key, $max, $min, $withscores ) = @_;
1087              
1088             my $not_with_scores = 0;
1089              
1090             return map { $withscores ? ( $_, $self->zscore($key, $_) ) : $_ }
1091             reverse $self->zrangebyscore($key, $min, $max, $not_with_scores);
1092             }
1093              
1094             sub zcount {
1095             my ( $self, $key, $min, $max ) = @_;
1096             return scalar $self->zrangebyscore($key, $min, $max);
1097             }
1098              
1099             sub zcard {
1100             my ( $self, $key ) = @_;
1101             return scalar values %{ $self->_stash->{$key} }
1102             }
1103              
1104             sub zremrangebyrank {
1105             my ( $self, $key, $start, $stop ) = @_;
1106              
1107             my @remove = $self->zrange($key, $start, $stop);
1108             delete $self->_stash->{$key}->{$_} for @remove;
1109             return scalar @remove;
1110             }
1111              
1112             sub zremrangebyscore {
1113             my ( $self, $key, $start, $stop ) = @_;
1114              
1115             my @remove = $self->zrangebyscore($key, $start, $stop);
1116             delete $self->_stash->{$key}->{$_} for @remove;
1117             return scalar @remove;
1118             }
1119              
1120             =head1 PIPELINING
1121              
1122             See L -- most methods support the use of a callback sub as
1123             the final argument. For this implementation, the callback sub will be called
1124             immediately (before the result of the original method is returned), and
1125             C does nothing. Combining pipelining with C/C
1126             is not supported.
1127              
1128             =head1 TODO
1129              
1130             Lots!
1131              
1132             Not all Redis functionality is implemented. The test files that output "TODO" are still to be done.
1133              
1134             The top of all test files [except 01-basic.t] has the list of commands tested or to-be tested in the file.
1135              
1136             Those marked with an "x" are pretty well-tested.
1137             Those marked with an "o" need help.
1138             Those that are unmarked have no tests, or are un-implemented. For example:
1139              
1140             x AUTH <--- has some tests
1141              
1142             o KEYS <--- only partially tested and/or implemented
1143              
1144             ZINTERSTORE <--- not tested (or maybe not implemented)
1145              
1146              
1147              
1148             Beyond that, it would be neat to add methods to inspect how often keys were accessed and get other information that
1149             allows the module user to confirm that their code interacted with redis (or Test::Mock::Redis) as they expected.
1150              
1151              
1152             =head1 AUTHOR
1153              
1154             Jeff Lavallee, C<< >>
1155              
1156             =head1 SEE ALSO
1157              
1158             The real Redis.pm client whose interface this module mimics: L
1159              
1160              
1161             =head1 BUGS
1162              
1163             Please report any bugs or feature requests to C, or through
1164             the web interface at L. I will be notified, and then you'll
1165             automatically be notified of progress on your bug as I make changes.
1166              
1167              
1168              
1169             =head1 SUPPORT
1170              
1171             You can find documentation for this module with the perldoc command.
1172              
1173             perldoc Test::Mock::Redis
1174              
1175              
1176             You can also look for information at:
1177              
1178             =over 4
1179              
1180             =item * RT: CPAN's request tracker
1181              
1182             L
1183              
1184             =item * AnnoCPAN: Annotated CPAN documentation
1185              
1186             L
1187              
1188             =item * CPAN Ratings
1189              
1190             L
1191              
1192             =item * Search CPAN
1193              
1194             L
1195              
1196             =back
1197              
1198              
1199             =head1 ACKNOWLEDGEMENTS
1200              
1201             Salvatore Sanfilippo for redis, of course!
1202              
1203             Dobrica Pavlinusic & Pedro Melo for Redis.pm
1204              
1205             The following people have contributed to I:
1206              
1207             =over
1208              
1209             =item * Chris Reinhardt
1210              
1211             =item * Ian Burrell
1212              
1213             =item * Gianni Ceccarelli
1214              
1215             =item * Karen Etheridge
1216              
1217             =item * Keith Broughton
1218              
1219             =item * Kevin Goess
1220              
1221             =item * Neil Bowers
1222              
1223             =item * Nigel Gregoire
1224              
1225             =item * Yaakov Shaul
1226              
1227             =back
1228              
1229             =head1 LICENSE AND COPYRIGHT
1230              
1231             Copyright 2015 Jeff Lavallee.
1232              
1233             This program is free software; you can redistribute it and/or modify it
1234             under the terms of either: the GNU General Public License as published
1235             by the Free Software Foundation; or the Artistic License.
1236              
1237             See L for more information.
1238              
1239              
1240             =cut
1241              
1242             sub _normalize_index {
1243 123     123   84 my ( $length, $index ) = @_;
1244              
1245 123 100       174 $index += $length if $index < 0;
1246 123         103 return $index;
1247             }
1248              
1249             sub _normalize_range {
1250 33     33   32 my ( $length, $start, $end ) = @_;
1251              
1252 33         37 $start = _normalize_index($length,$start);
1253 33         35 $end = _normalize_index($length,$end);
1254 33 100       48 $end = $length-1 if $end >= $length;
1255              
1256 33         38 return ($start,$end);
1257             }
1258              
1259             sub _is_list {
1260 77     77   53 my ( $self, $key ) = @_;
1261              
1262             return $self->exists($key)
1263             && blessed $self->_stash->{$key}
1264 77   66     1171 && $self->_stash->{$key}->isa('Test::Mock::Redis::List') ;
1265             }
1266              
1267             sub _make_list {
1268 55     55   62 my ( $self, $key ) = @_;
1269              
1270 55 100       60 $self->_stash->{$key} = Test::Mock::Redis::List->new
1271             unless $self->_is_list($key);
1272             }
1273              
1274             sub _is_hash {
1275 113     113   95 my ( $self, $key ) = @_;
1276              
1277             return $self->exists($key)
1278             && blessed $self->_stash->{$key}
1279 113   66     1758 && $self->_stash->{$key}->isa('Test::Mock::Redis::Hash') ;
1280             }
1281              
1282             sub _make_hash {
1283 25     25   22 my ( $self, $key ) = @_;
1284              
1285 25 100       34 $self->_stash->{$key} = Test::Mock::Redis::Hash->new
1286             unless $self->_is_hash($key);
1287             }
1288              
1289             sub _is_set {
1290 89     89   68 my ( $self, $key ) = @_;
1291              
1292             return $self->exists($key)
1293             && blessed $self->_stash->{$key}
1294 89   66     1339 && $self->_stash->{$key}->isa('Test::Mock::Redis::Set') ;
1295             }
1296              
1297             sub _make_set {
1298 32     32   22 my ( $self, $key ) = @_;
1299              
1300 32 100       42 $self->_stash->{$key} = Test::Mock::Redis::Set->new
1301             unless $self->_is_set($key);
1302             }
1303              
1304             sub _is_zset {
1305 20     20   11 my ( $self, $key ) = @_;
1306              
1307             return $self->exists($key)
1308             && blessed $self->_stash->{$key}
1309 20   66     278 && $self->_stash->{$key}->isa('Test::Mock::Redis::ZSet') ;
1310             }
1311              
1312             sub _make_zset {
1313 20     20   20 my ( $self, $key ) = @_;
1314              
1315 20 100       21 $self->_stash->{$key} = Test::Mock::Redis::ZSet->new
1316             unless $self->_is_zset($key);
1317             }
1318              
1319              
1320             # MULTI/EXEC/DISCARD: http://redis.io/topics/transactions
1321              
1322             sub multi {
1323 6     6 0 590 my ( $self ) = @_;
1324              
1325 6 100       94 confess '[multi] ERR MULTI calls can not be nested' if defined $self->{_multi_commands};
1326              
1327             # set up the list for storing commands sent between MULTI and EXEC/DISCARD
1328 5         7 $self->{_multi_commands} = [];
1329              
1330 5         11 return 'OK';
1331             }
1332              
1333             # methods that return a list, rather than a single value
1334             my @want_list = qw(mget keys lrange smembers sinter sunion sdiff hmget hkeys hvals hgetall sort zrange zrevrange zrangebyscore);
1335             my %want_list = map { $_ => 1 } @want_list;
1336              
1337             sub exec {
1338             my ( $self ) = @_;
1339              
1340             # we are going to commit all the changes we saved up;
1341             # replay them now and return all their output
1342              
1343             confess '[exec] ERR EXEC without MULTI' if not defined $self->{_multi_commands};
1344              
1345             my @commands = @{$self->{_multi_commands}};
1346             delete $self->{_multi_commands};
1347              
1348             # replay all the queries that were queued up
1349             # the returned result is a nested array of the results of all the commands
1350             my @exceptions;
1351             my @results = map {
1352             my ($method, @args) = @$_;
1353             my @result =
1354             try { $self->$method(@args) }
1355             catch { push @exceptions, $_; (); };
1356             $want_list{$method} ? \@result : $result[0];
1357             } @commands;
1358              
1359             s/^\[\w+\] // for @exceptions;
1360              
1361             confess('[exec] ', join('; ', @exceptions)) if @exceptions;
1362              
1363             return @results;
1364             }
1365              
1366             sub discard {
1367 3     3 0 1275 my ( $self ) = @_;
1368              
1369 3 100       87 confess '[discard] ERR DISCARD without MULTI' if not defined $self->{_multi_commands};
1370              
1371             # discard all the accumulated commands, without executing them
1372 2         3 delete $self->{_multi_commands};
1373              
1374 2         7 return 'OK';
1375             }
1376              
1377             sub watch {
1378             my ($self) = shift;
1379              
1380             confess '[watch] ERR wrong number of arguments for \'watch\' command' unless @_;
1381              
1382             return 'OK';
1383             }
1384              
1385             sub unwatch {
1386             my ($self) = shift;
1387              
1388             confess '[error] ERR wrong number of arguments for \'unwatch\' command' if @_;
1389              
1390             return 'OK';
1391             }
1392              
1393             # now that we've defined all our subs, we need to wrap them all in logic that
1394             # can check if we are in the middle of a MULTI, and if so, queue up the
1395             # commands for later replaying.
1396              
1397             my %no_transaction_wrap_methods = (
1398             new => 1,
1399             multi => 1,
1400             exec => 1,
1401             discard => 1,
1402             quit => 1,
1403             import => 1,
1404             change_num_databases => 1,
1405             );
1406              
1407             my @transaction_wrapped_methods =
1408             grep { !/^_/}
1409             grep { not $no_transaction_wrap_methods{$_} }
1410             Package::Stash->new(__PACKAGE__)->list_all_symbols('CODE');
1411              
1412             foreach my $method (@transaction_wrapped_methods)
1413             {
1414             around $method => sub {
1415             my $orig = shift;
1416             my $self = shift;
1417              
1418             # pass command through if we are not handling a MULTI
1419             return $self->$orig(@_) if not defined $self->{_multi_commands};
1420              
1421             push @{$self->{_multi_commands}}, [ $method, @_ ];
1422             return 'QUEUED';
1423             };
1424             }
1425              
1426              
1427             # PIPELINING SUPPORT
1428              
1429             # these method modifications must be done after (over top of) the modification
1430             # for transactions, as we need to check for/extract the $cb first.
1431              
1432             my %no_pipeline_wrap_methods = (
1433             new => 1,
1434             multi => 1,
1435             discard => 1,
1436             quit => 1,
1437             ping => 1,
1438             subscribe => 1,
1439             unsubscribe => 1,
1440             psubscribe => 1,
1441             punsubscribe => 1,
1442             wait_all_responses => 1,
1443             );
1444              
1445             my @pipeline_wrapped_methods =
1446             grep { !/^_/}
1447             grep { not $no_pipeline_wrap_methods{$_} }
1448             Package::Stash->new(__PACKAGE__)->list_all_symbols('CODE');
1449              
1450             # this is a bit messy, and the wantarray logic may not be quite right.
1451             # Alternatively, we could implement all this by reusing the logic in the real
1452             # Redis.pm -- subclass Redis, override new/multi/exec/discard (and probably
1453             # some other special functions), and have __run_cmd use a dispatch table to
1454             # call all our overridden implementations.
1455              
1456             foreach my $method (@pipeline_wrapped_methods)
1457             {
1458             around $method => sub {
1459             my $orig = shift;
1460             my $self = shift;
1461             my @args = @_;
1462              
1463             my $cb = @args && ref $args[-1] eq 'CODE' ? pop @args : undef;
1464              
1465             return $self->$orig(@args) if not $cb;
1466              
1467             # this may be officially supported eventually -- see
1468             # https://github.com/melo/perl-redis/issues/17
1469             # and "Pipeline management" in the Redis docs
1470             # To make this work, we just need to special-case exec, to collect all the
1471             # results and errors in tuples and send that to the $cb
1472             die 'cannot combine pipelining with MULTI' if $self->{_multi_commands};
1473              
1474             # We could also implement this with a queue, not bothering to process
1475             # the commands until wait_all_responses is called - but then we need to
1476             # make sure to call wait_all_responses explicitly as soon as a command
1477             # is issued without a $cb.
1478              
1479             my $error;
1480             my (@result) = try
1481             {
1482             $self->$orig(@args);
1483             }
1484             catch
1485             {
1486             $error = $_;
1487             ();
1488             };
1489              
1490             $cb->(
1491             # see notes above - this logic may not be quite right
1492             ( $want_list{$method} ? \@result : $result[0] ),
1493             $error,
1494             );
1495             return 1;
1496             };
1497             }
1498              
1499             # in a real Redis system, this will make all outstanding callbacks get called.
1500             sub wait_all_responses {}
1501              
1502              
1503             1; # End of Test::Mock::Redis
1504              
1505             package Test::Mock::Redis::List;
1506 8     8   47 sub new { return bless [], shift }
1507             1;
1508              
1509             package Test::Mock::Redis::Hash;
1510 5     5   15 sub new { return bless {}, shift }
1511             1;
1512              
1513             package Test::Mock::Redis::ZSet;
1514 3     3   13 sub new { return bless {}, shift }
1515             1;
1516              
1517             package Test::Mock::Redis::Set;
1518 7     7   16 sub new { return bless {}, shift }
1519             1;
1520              
1521             package Test::Mock::Redis::PossiblyVolatile;
1522              
1523 14     14   70 use strict; use warnings;
  14     14   17  
  14         248  
  14         41  
  14         13  
  14         351  
1524 14     14   6504 use Tie::Hash;
  14         9008  
  14         350  
1525 14     14   79 use base qw/Tie::StdHash/;
  14         14  
  14         6891  
1526              
1527             sub DELETE {
1528 21     21   19 my ( $self, $key ) = @_;
1529              
1530 21         87 delete $self->{$key};
1531             }
1532              
1533             my $expires;
1534              
1535             sub FETCH {
1536 1944     1944   1702 my ( $self, $key ) = @_;
1537              
1538             return $self->EXISTS($key)
1539 1944 100       1910 ? $self->{$key}
1540             : undef;
1541             }
1542              
1543             sub EXISTS {
1544 2809     2809   2056 my ( $self, $key ) = @_;
1545              
1546 2809         2780 $self->_delete_if_expired($key);
1547              
1548 2809         16460 return exists $self->{$key};
1549             }
1550              
1551             sub _delete_if_expired {
1552 2809     2809   1914 my ( $self, $key ) = @_;
1553 2809 100 100     6552 if(exists $expires->{$self}->{$key}
1554             && time >= $expires->{$self}->{$key}){
1555 3         10 delete $self->{$key};
1556 3         11 delete $expires->{$self}->{$key};
1557             }
1558             }
1559              
1560             sub expire {
1561 15     15   17 my ( $self, $key, $time ) = @_;
1562              
1563 15         35 $expires->{$self}->{$key} = $time;
1564             }
1565              
1566             sub expire_count {
1567 32     32   21 my ( $self ) = @_;
1568              
1569             # really, we should probably only count keys that haven't expired
1570 32         25 scalar keys %{ $expires->{$self} };
  32         105  
1571             }
1572              
1573             sub persist {
1574 1     1   2 my ( $self, $key, $time ) = @_;
1575              
1576 1         3 delete $expires->{$self}->{$key};
1577             }
1578              
1579             sub ttl {
1580 12     12   10 my ( $self, $key ) = @_;
1581              
1582 12 100       25 return -1 unless exists $expires->{$self}->{$key};
1583 11         47 return $expires->{$self}->{$key} - time;
1584             }
1585              
1586              
1587             1;