File Coverage

blib/lib/DR/Tarantool/StartTest.pm
Criterion Covered Total %
statement 50 205 24.3
branch 6 102 5.8
condition 3 20 15.0
subroutine 14 34 41.1
pod 9 15 60.0
total 82 376 21.8


line stmt bran cond sub pod time code
1 14     14   278315 use utf8;
  14         23  
  14         103  
2 14     14   401 use strict;
  14         22  
  14         421  
3 14     14   54 use warnings;
  14         18  
  14         586  
4              
5             package DR::Tarantool::StartTest;
6 14     14   72 use Carp;
  14         18  
  14         1092  
7 14     14   10232 use File::Temp qw(tempfile tempdir);
  14         262749  
  14         991  
8 14     14   98 use File::Path 'rmtree';
  14         23  
  14         538  
9 14     14   6266 use File::Spec::Functions qw(catfile rel2abs);
  14         9396  
  14         972  
10 14     14   92 use Cwd;
  14         19  
  14         783  
11 14     14   8024 use IO::Socket::INET;
  14         171651  
  14         95  
12 14     14   15591 use POSIX ();
  14         75911  
  14         442  
13 14     14   6757 use List::MoreUtils 'any';
  14         10215  
  14         32260  
14              
15              
16             =head1 NAME
17              
18             DR::Tarantool::StartTest - finds and starts Tarantool on free port.
19              
20             =head1 SYNOPSIS
21              
22             my $t = run DR::Tarantool::StartTest ( cfg => $file_spaces_cfg );
23              
24             =head1 DESCRIPTION
25              
26             The module tries to find and then to start B.
27              
28             The module is used inside tests.
29              
30              
31             =head1 METHODS
32              
33             =head2 run
34              
35             Constructor. Receives the following arguments:
36              
37             =over
38              
39             =item cfg
40              
41             path to tarantool.cfg
42              
43             =back
44              
45             =cut
46              
47              
48             sub compare_versions($$) {
49 0     0 0 0 my ($v1, $v2) = @_;
50 0         0 my @v1 = split /\./, $v1;
51 0         0 my @v2 = split /\./, $v2;
52              
53 0 0       0 for (0 .. (@v1 < @v2 ? $#v1 : $#v2)) {
54 0 0       0 return 'gt' if $v1[$_] > $v2[$_];
55 0 0       0 return 'lt' if $v1[$_] < $v2[$_];
56             }
57 0 0       0 return 'gt' if @v1 > @v2;
58 0 0       0 return 'lt' if @v1 < @v2;
59 0         0 return 'eq';
60             }
61              
62              
63             =head2 is_version(VERSION[, FAMILY])
64              
65             return true if tarantool_box is found and its version is more than L.
66              
67             FAMILY can be:
68              
69             =over
70              
71             =item B<1> (default)
72              
73             For tarantool < 1.6.
74              
75             =item B<2>
76              
77             For tarantool >= 1.6.
78              
79             =back
80              
81             =cut
82              
83             sub is_version($;$) {
84 12     12 1 1088 my ($version, $family) = @_;
85              
86 12         19 my $box;
87 12   100     73 $family ||= 1;
88              
89 12 50   12   137 croak "Unknown family: $family" unless any { $family == $_ } 1, 2;
  12         42  
90              
91 12 50       50 if ($family == 1) {
92 12   50     95 $box = $ENV{TARANTOOL_BOX} || 'tarantool_box';
93             } else {
94 0   0     0 $box = $ENV{TARANTOOL_BOX} || 'tarantool';
95             }
96            
97 12         22 my $str;
98             {
99 12     0   19 local $SIG{__WARN__} = sub { };
  12         57  
  0         0  
100 12         21193 $str = `$box -V`;
101             }
102              
103 12 50       281 return 0 unless $str;
104 0 0       0 return 0 if $str =~ /^tarantool client, version/;
105 0         0 my ($vt) = $str =~ /^Tarantool:?\s+(\d(?:\.\d+)+).*\s*$/s;
106 0 0       0 return 0 unless $vt;
107 0         0 my $res = compare_versions $version, $vt;
108 0 0   0   0 return 0 unless any { $_ eq $res } 'eq', 'lt';
  0         0  
109 0         0 return 1;
110             }
111              
112             sub run {
113 0     0 1 0 my ($module, %opts) = @_;
114              
115 0 0       0 my $cfg_file = delete $opts{cfg} or croak "config file not defined";
116 0 0       0 croak "File not found" unless -r $cfg_file;
117 0 0       0 open my $fh, '<:encoding(UTF-8)', $cfg_file or die "$@\n";
118 0         0 local $/;
119 0         0 my $cfg = <$fh>;
120              
121 0   0     0 my $family = $opts{family} || 1;
122 0 0   0   0 croak "Unknown family: $family" unless any { $family == $_ } 1, 2;
  0         0  
123              
124 0         0 my %self = (
125             admin_port => $module->_find_free_port,
126             primary_port => $module->_find_free_port,
127             secondary_port => $module->_find_free_port,
128             cfg_data => $cfg,
129             master => $$,
130             cwd => getcwd,
131             add_opts => \%opts,
132             family => $family,
133             );
134              
135 0 0       0 $opts{script_dir} = rel2abs $opts{script_dir} if $opts{script_dir};
136              
137 0         0 my $self = bless \%self => $module;
138 0         0 $self->_start_tarantool;
139 0         0 $self;
140             }
141              
142              
143             sub family {
144 0     0 0 0 my ($self) = @_;
145 0         0 return $self->{family};
146             }
147              
148              
149             =head2 started
150              
151             Return true if Tarantool is found and started
152              
153             =cut
154              
155             sub started {
156 0     0 1 0 my ($self) = @_;
157 0         0 return $self->{started};
158             }
159              
160              
161             =head2 log
162              
163             Return Tarantool logs
164              
165             =cut
166              
167             sub log {
168 0     0 1 0 my ($self) = @_;
169 0 0 0     0 return '' unless $self->{log} and -r $self->{log};
170 0         0 open my $fh, '{log};
171 0         0 local $/;
172 0         0 my $l = <$fh>;
173 0         0 return $l;
174             }
175              
176             sub admin {
177 0     0 0 0 my ($self, @cmd) = @_;
178 0         0 $cmd[-1] =~ s/\s*$/\n/;
179 0         0 my $cmd = join ' ' => @cmd;
180              
181 0 0       0 my $s = IO::Socket::INET->new(
182             PeerHost => '127.0.0.1',
183             PeerPort => $self->admin_port,
184             Proto => 'tcp',
185             (($^O eq 'MSWin32') ? () : (ReuseAddr => 1)),
186             );
187              
188 0 0       0 croak "Can't connect to admin port: $!" unless $s;
189 0         0 print $s $cmd;
190 0         0 my @lines;
191 0         0 while(<$s>) {
192 0         0 s/\s*$//;
193 0 0       0 next if $_ eq '---';
194 0 0       0 last if $_ eq '...';
195 0         0 push @lines => $_;
196             }
197 0         0 close $s;
198 0         0 return @lines;
199             }
200              
201             sub _start_tarantool {
202 0     0   0 my ($self) = @_;
203 0 0       0 if ($ENV{TARANTOOL_TEMPDIR}) {
204 0         0 $self->{temp} = $ENV{TARANTOOL_TEMPDIR};
205 0         0 $self->{dont_unlink_temp} = 1;
206 0 0       0 rmtree $self->{temp} if -d $self->{temp};
207 0         0 mkdir $self->{temp};
208             } else {
209 0         0 $self->{temp} = tempdir;
210             }
211              
212 0 0       0 if ($self->family) {
213 0         0 $self->{cfg} = catfile $self->{temp}, 'tarantool.cfg';
214             } else {
215 0         0 $self->{cfg} = catfile $self->{temp}, 'box.lua';
216             }
217 0         0 $self->{log} = catfile $self->{temp}, 'tarantool.log';
218 0         0 $self->{pid} = catfile $self->{temp}, 'tarantool.pid';
219 0         0 $self->{core} = catfile $self->{temp}, 'core';
220              
221              
222              
223 0 0       0 if ($self->family == 1) {
224 0 0       0 croak "Available tarantool is not valid (is_version '1.4.0')"
225             unless is_version '1.4.0', $self->family;
226             } else {
227 0 0       0 croak "Available tarantool is not valid (is_version '1.4.0')"
228             unless is_version '1.6.0', $self->family;
229             }
230              
231              
232 0         0 $self->{config_body} = $self->{cfg_data};
233 0 0       0 if ($self->family == 1) {
234 0         0 $self->{config_body} .= "\n\n";
235 0         0 $self->{config_body} .= "slab_alloc_arena = 1.1\n";
236 0         0 $self->{config_body} .= sprintf "pid_file = %s\n", $self->{pid};
237 0   0     0 $self->{box} = $ENV{TARANTOOL_BOX} || 'tarantool_box';
238              
239             $self->{config_body} .= sprintf "%s = %s\n", $_, $self->{$_}
240 0         0 for (qw(admin_port primary_port secondary_port));
241              
242 0         0 $self->{config_body} .=
243             sprintf qq{logger = "cat >> %s"\n}, $self->{log};
244              
245 0         0 for (keys %{ $self->{add_opts} }) {
  0         0  
246 0         0 my $v = $self->{add_opts}{ $_ };
247              
248 0 0       0 if ($v =~ /^\d+$/) {
249 0         0 $self->{config_body} .= sprintf qq{%s = %s\n}, $_, $v;
250             } else {
251 0         0 $self->{config_body} .= sprintf qq{%s = "%s"\n}, $_, $v;
252             }
253             }
254             } else {
255 0   0     0 $self->{box} = $ENV{TARANTOOL_BOX} || 'tarantool';
256 0         0 for ($self->{config_body}) {
257 0 0       0 if (/primary_port\s*=/) {
258 0         0 s{listen\s*=\s*['"]?\d+['"]}
259 0         0 /listen = @{[$self->primary_port]}/;
260             } else {
261 0         0 s
262 0         0 /$& listen = '127.0.0.1:@{[$self->primary_port]}',/;
263             }
264              
265 0         0 $_ .= "\n\nrequire('console')".
266 0         0 ".listen('127.0.0.1:@{[$self->admin_port]}')";
267             }
268             }
269              
270 0 0       0 return unless open my $fh, '>:encoding(UTF-8)', $self->{cfg};
271              
272 0         0 print $fh $self->{config_body};
273              
274 0         0 close $fh;
275              
276 0         0 chdir $self->{temp};
277              
278 0 0       0 if ($self->family == 1) {
279 0         0 system "$self->{box} -c $self->{cfg} ".
280             "--check-config >> $self->{log} 2>&1";
281 0 0       0 goto EXIT if $?;
282              
283 0         0 system "$self->{box} -c $self->{cfg} --init-storage ".
284             ">> $self->{log} 2>&1";
285 0 0       0 goto EXIT if $?;
286             }
287 0         0 $self->_restart;
288 0         0 EXIT:
289             chdir $self->{cwd};
290              
291             }
292              
293             sub _restart {
294 0     0   0 my ($self) = @_;
295              
296 0 0       0 unless ($self->{child} = fork) {
297 0         0 chdir $self->{temp};
298 0 0       0 die "Can't fork: $!" unless defined $self->{child};
299 0         0 POSIX::setsid();
300 0 0       0 if ($self->family == 1) {
301 0         0 exec "ulimit -c unlimited; ".
302             "exec $self->{box} -c $self->{cfg} >> $self->{log} 2>&1";
303             } else {
304 0         0 exec "ulimit -c unlimited; ".
305             "exec $self->{box} $self->{cfg} >> $self->{log} 2>&1";
306             }
307 0         0 die "Can't start $self->{box}: $!\n";
308             }
309              
310 0         0 $self->{started} = 1;
311              
312              
313             # wait for starting Tarantool
314 0         0 for (my $i = 0; $i < 100; $i++) {
315 0 0       0 last if IO::Socket::INET->new(
316             PeerAddr => '127.0.0.1', PeerPort => $self->primary_port
317             );
318              
319 0         0 sleep 0.01;
320             }
321              
322 0         0 for (my $i = 0; $i < 100; $i++) {
323 0 0       0 last if $self->log =~ /entering event loop/;
324 0         0 sleep 0.01;
325             }
326              
327 0 0       0 sleep 1 unless $self->log =~ /entering event loop/;
328             }
329              
330             sub restart {
331 0     0 0 0 my ($self) = @_;
332 0         0 $self->kill('KILL');
333 0         0 $self->_restart;
334             }
335              
336             =head2 primary_port
337              
338             Return Tarantool primary port
339              
340             =cut
341              
342 0     0 1 0 sub primary_port { return $_[0]->{primary_port} }
343              
344              
345             =head2 admin_port
346              
347             Return Tarantool admin port
348              
349             =cut
350              
351 0     0 1 0 sub admin_port { return $_[0]->{admin_port} }
352              
353              
354             =head2 tarantool_pid
355              
356             Return B
357              
358             =cut
359              
360 0     0 1 0 sub tarantool_pid { return $_[0]->{child} }
361              
362              
363             =head2 kill
364              
365             Kills Tarantool
366              
367             =cut
368              
369             sub kill :method {
370 0     0 1 0 my ($self, $signame) = @_;
371              
372 0   0     0 $signame ||= 'TERM';
373 0 0       0 if ($self->{child}) {
374 0         0 kill $signame => $self->{child};
375 0         0 waitpid $self->{child}, 0;
376 0         0 delete $self->{child};
377             }
378 0         0 $self->{started} = 0;
379             }
380              
381              
382             =head2 is_dead
383              
384             Return true if child Tarantool process is dead.
385              
386             =cut
387              
388             sub is_dead {
389 0     0 1 0 my ($self) = @_;
390 0 0       0 return 1 unless $self->{child};
391 0 0       0 return 0 if 0 < kill 0 => $self->{child};
392 0         0 return 1;
393             }
394              
395             =head2 DESTROY
396              
397             Destructor. Kills tarantool, removes temporary files.
398              
399             =cut
400              
401             sub DESTROY {
402 0     0   0 my ($self) = @_;
403 0         0 local $?;
404 0         0 chdir $self->{cwd};
405 0 0       0 return unless $self->{master} == $$;
406              
407 0 0       0 if (-r $self->{core}) {
408 0 0       0 warn "Tarantool was coredumped\n" if -r $self->{core};
409 0         0 system "echo bt|gdb $self->{box} $self->{core}";
410             }
411              
412 0         0 $self->kill;
413 0 0 0     0 rmtree $self->{temp} if $self->{temp} and !$self->{dont_unlink_temp};
414             }
415              
416              
417             sub temp_dir {
418 0     0 0 0 my ($self) = @_;
419 0         0 return $self->{temp};
420             }
421              
422              
423             sub clean_xlogs {
424 0     0 0 0 my ($self) = @_;
425 0 0       0 return unless $self->{temp};
426 0         0 my @xlogs = glob catfile $self->{temp}, '*.xlog';
427 0         0 unlink for @xlogs;
428             }
429              
430             {
431             my %busy_ports;
432              
433             sub _find_free_port {
434              
435 1     1   4 while( 1 ) {
436 1         4 my $port = 10000 + int rand 30000;
437 1 50       4 next if exists $busy_ports{ $port };
438 1 50       12 next unless IO::Socket::INET->new(
    50          
439             Listen => 5,
440             LocalAddr => '127.0.0.1',
441             LocalPort => $port,
442             Proto => 'tcp',
443             (($^O eq 'MSWin32') ? () : (ReuseAddr => 1)),
444             );
445 1         320 return $busy_ports{ $port } = $port;
446             }
447             }
448             }
449              
450             =head1 COPYRIGHT AND LICENSE
451              
452             Copyright (C) 2011 Dmitry E. Oboukhov
453             Copyright (C) 2011 Roman V. Nikolaev
454              
455             This program is free software, you can redistribute it and/or
456             modify it under the terms of the Artistic License.
457              
458             =head1 VCS
459              
460             The project is placed git repo on github:
461             L.
462              
463             =cut
464              
465             1;