File Coverage

blib/lib/Test/RedisServer.pm
Criterion Covered Total %
statement 97 132 73.4
branch 23 48 47.9
condition 3 16 18.7
subroutine 16 19 84.2
pod 7 7 100.0
total 146 222 65.7


line stmt bran cond sub pod time code
1             package Test::RedisServer;
2 14     14   1790982 use strict;
  14         94  
  14         384  
3 14     14   62 use warnings;
  14         30  
  14         312  
4 14     14   5990 use Mouse;
  14         336108  
  14         52  
5              
6             our $VERSION = '0.23';
7              
8 14     14   4792 use Carp;
  14         48  
  14         818  
9 14     14   6410 use File::Temp;
  14         148572  
  14         940  
10 14     14   2464 use POSIX qw(SIGTERM WNOHANG);
  14         30638  
  14         90  
11 14     14   10436 use Time::HiRes qw(sleep);
  14         6444  
  14         96  
12 14     14   2472 use Errno ();
  14         28  
  14         228  
13 14     14   4686 use Redis;
  14         587610  
  14         1292  
14              
15             has auto_start => (
16             is => 'rw',
17             default => 1,
18             );
19              
20             has [qw/pid _owner_pid/] => (
21             is => 'rw',
22             );
23              
24             has conf => (
25             is => 'rw',
26             isa => 'HashRef',
27             default => sub { {} },
28             );
29              
30             has timeout => (
31             is => 'rw',
32             default => 3,
33             );
34              
35             has tmpdir => (
36             is => 'rw',
37             lazy_build => 1,
38             );
39              
40             has _redis => (
41             is => 'rw',
42             isa => 'Redis',
43             );
44              
45 14     14   94 no Mouse;
  14         28  
  14         88  
46              
47             sub BUILD {
48 14     14 1 38 my ($self) = @_;
49              
50 14         74 $self->_owner_pid($$);
51              
52 14         68 my $tmpdir = $self->tmpdir;
53 14 50 33     7314 unless (defined $self->conf->{port} or defined $self->conf->{unixsocket}) {
54 14         360 $self->conf->{unixsocket} = "$tmpdir/redis.sock";
55 14         182 $self->conf->{port} = '0';
56             }
57              
58 14 50       66 unless (defined $self->conf->{dir}) {
59 14         38 $self->conf->{dir} = "$tmpdir/";
60             }
61              
62 14 50 33     156 if ($self->conf->{loglevel} and $self->conf->{loglevel} eq 'warning') {
63 0         0 warn "Test::RedisServer does not support \"loglevel warning\", using \"notice\" instead.\n";
64 0         0 $self->conf->{loglevel} = 'notice';
65             }
66              
67 14 50       54 if ($self->auto_start) {
68 14         44 $self->start;
69             }
70             }
71              
72             sub DEMOLISH {
73 14     14 1 15950 my ($self) = @_;
74 14 50 33     1031 $self->stop if defined $self->pid && $$ == $self->_owner_pid;
75             }
76              
77             sub start {
78 14     14 1 28 my ($self) = @_;
79              
80 14 50       64 return if defined $self->pid;
81              
82 14         38 my $tmpdir = $self->tmpdir;
83 14 50       74 open my $logfh, '>>', "$tmpdir/redis-server.log"
84             or croak "failed to create log file: $tmpdir/redis-server.log";
85              
86 14         26140 my $pid = fork;
87 14 50       1242 croak "fork(2) failed:$!" unless defined $pid;
88              
89 14 100       429 if ($pid == 0) {
90 7 50       1359 open STDOUT, '>&', $logfh or croak "dup(2) failed:$!";
91 7 50       357 open STDERR, '>&', $logfh or croak "dup(2) failed:$!";
92 7         386 $self->exec;
93             }
94 7         225 close $logfh;
95              
96 7         85 my $ready;
97 7         72 my $elapsed = 0;
98 7         326 $self->pid($pid);
99              
100 7         222 while ($elapsed <= $self->timeout) {
101 21 100       929 if (waitpid($pid, WNOHANG) > 0) {
102 7         343 $self->pid(undef);
103 7         73 last;
104             }
105             else {
106 14         175 my $log = q[];
107 14 50       503 if (open $logfh, '<', "$tmpdir/redis-server.log") {
108 14         1278 $log = do { local $/; <$logfh> };
  14         287  
  14         849  
109 14         237 close $logfh;
110             }
111              
112             # confirmed this message is included from v1.3.6 (older version in git repo)
113             # to current HEAD (2012-07-30)
114             # The message has changed a bit with Redis 4.x, make regexp a bit more flexible
115 14 50       238 if ( $log =~ /[Rr]eady to accept connections/ ) {
116 0         0 $ready = 1;
117 0         0 last;
118             }
119             }
120              
121 14         2103012 sleep $elapsed += 0.1;
122             }
123              
124 7 50       111 unless ($ready) {
125 7 50       165 if ($self->pid) {
126 0         0 $self->pid(undef);
127 0         0 kill SIGTERM, $pid;
128 0         0 while (waitpid($pid, WNOHANG) >= 0) {
129             }
130             }
131              
132 7         39 croak "*** failed to launch redis-server ***\n" . do {
133 7         92 my $log = q[];
134 7 50       155 if (open $logfh, '<', "$tmpdir/redis-server.log") {
135 7         1087 $log = do { local $/; <$logfh> };
  7         109  
  7         476  
136 7         206 close $logfh;
137             }
138 7         4729 $log;
139             };
140             }
141              
142             # This is sometimes needed to send commands to RedisServer during the stop process.
143             # Generally, we would like to generate it lazily and not have it as a property
144             # of the object. However, if you try to create the object at the stop,
145             # the object generation may fail, such as missing the socket file. Therefore,
146             # we will make the object and store it as property here.
147 0         0 $self->_redis( Redis->new($self->connect_info) );
148              
149 0         0 $self->pid($pid);
150             }
151              
152             sub exec {
153 7     7 1 97 my ($self) = @_;
154              
155 7         296 my $tmpdir = $self->tmpdir;
156              
157 7 50       827 open my $conffh, '>', "$tmpdir/redis.conf" or croak "cannot write conf: $!";
158 7         1592 print $conffh $self->_conf_string;
159 7         584 close $conffh;
160              
161             exec 'redis-server', "$tmpdir/redis.conf"
162 7 50       116 or do {
163 7 50       6060 if ($! == Errno::ENOENT) {
164 7         174 print STDERR "exec failed: no such file or directory\n";
165             }
166             else {
167 0         0 print STDERR "exec failed: unexpected error: $!\n";
168             }
169 7         1109 exit($?);
170             };
171             }
172              
173             sub stop {
174 0     0 1 0 my ($self, $sig) = @_;
175              
176 0         0 local $?; # waitpid may change this value :/
177 0 0       0 return unless defined $self->pid;
178              
179             # If the tmpdir has disappeared, clear the save config to prevent saving
180             # in the server terminating process. The newer Redis will save on stop
181             # for robustness, but will keep blocking if the directory is missing.
182             #
183             # It is unlikely that tmpdir will disappear first, but if both the RedisServer
184             # object and the tmpdir are defined globally, it may happen because the order
185             # in which they are DESTLOYed is uncertain.
186 0 0       0 if (! -f $self->tmpdir) {
187 0         0 $self->_redis->config_set('appendonly', 'no');
188 0         0 $self->_redis->config_set('save', '');
189             }
190              
191 0   0     0 $sig ||= SIGTERM;
192              
193 0         0 kill $sig, $self->pid;
194 0         0 while (waitpid($self->pid, WNOHANG) >= 0) {
195             }
196              
197 0         0 $self->pid(undef);
198             }
199              
200             sub wait_exit {
201 0     0 1 0 my ($self) = @_;
202              
203 0         0 local $?;
204              
205 0         0 my $kid;
206 0         0 my $pid = $self->pid;
207 0         0 do {
208 0         0 $kid = waitpid($pid, WNOHANG);
209 0         0 sleep 0.1;
210             } while $kid >= 0;
211              
212 0         0 $self->pid(undef);
213             }
214              
215             sub connect_info {
216 0     0 1 0 my ($self) = @_;
217              
218 0   0     0 my $host = $self->conf->{bind} || '0.0.0.0';
219 0         0 my $port = $self->conf->{port};
220 0         0 my $sock = $self->conf->{unixsocket};
221              
222 0 0 0     0 if ($port && $port > 0) {
223 0         0 return (server => $host . ':' . $port);
224             }
225             else {
226 0         0 return (sock => $sock);
227             }
228             }
229              
230             sub _build_tmpdir {
231 14     14   100 File::Temp->newdir( CLEANUP => 1 );
232             }
233              
234             sub _conf_string {
235 7     7   112 my ($self) = @_;
236              
237 7         137 my $conf = q[];
238 7         90 my %conf = %{ $self->conf };
  7         544  
239 7         193 while (my ($k, $v) = each %conf) {
240 21 50       205 next unless defined $v;
241 21         189 $conf .= "$k $v\n";
242             }
243              
244 7         413 $conf;
245             }
246              
247             __PACKAGE__->meta->make_immutable;
248              
249             __END__