File Coverage

blib/lib/Test/Parallel.pm
Criterion Covered Total %
statement 108 110 98.1
branch 21 32 65.6
condition 6 13 46.1
subroutine 17 17 100.0
pod 5 5 100.0
total 157 177 88.7


line stmt bran cond sub pod time code
1             package Test::Parallel;
2             {
3             $Test::Parallel::VERSION = '0.20';
4             }
5 39     39   33802 use strict;
  39         87  
  39         1502  
6 39     39   220 use warnings;
  39         120  
  39         1152  
7 39     39   11147 use Test::More ();
  39         163549  
  39         615  
8 39     39   230578 use Parallel::ForkManager;
  39         2084384  
  39         1294  
9 39     39   35436 use Sys::Info;
  39         361727  
  39         218  
10              
11             # ABSTRACT: launch your test in parallel
12              
13             =head1 NAME
14             Test::Parallel - simple object interface to launch unit test in parallel
15              
16             =head1 VERSION
17              
18             version 0.20
19              
20             =head1 DESCRIPTION
21              
22             Test::Parallel is a simple object interface used to launch test in parallel.
23             It uses Parallel::ForkManager to launch tests in parallel and get the results.
24              
25             Alias for basic methods are available
26              
27             ok is isnt like unlike cmp_ok is_deeply
28              
29             =head1 Usage
30              
31             =head2 Wrap common Test::More methods
32            
33             It can be used nearly the same way as Test::More
34              
35             use Test::More tests => 8;
36             use Test::Parallel;
37            
38             my $p = Test::Parallel->new();
39            
40             # queue some tests that can be parallelized
41             $p->ok( sub { 1 }, "can do ok" );
42             $p->is( sub { 42 }, 42, "can do is" );
43             $p->isnt( sub { 42 }, 51, "can do isnt" );
44             $p->like( sub { "abc" }, qr{ab}, "can do like: match ab");
45             $p->unlike( sub { "abc" }, qr{xy}, "can do unlike: match ab");
46             $p->cmp_ok( sub { 'abc' }, 'eq', 'abc', "can do cmp ok");
47             $p->cmp_ok( sub { '1421' }, '==', 1_421, "can do cmp ok");
48             $p->is_deeply( sub { [ 1..15 ] }, [ 1..15 ], "can do is_deeply");
49              
50             # run the tests in background
51             $p->done();
52              
53             =head2 Implement your own logic
54              
55             You could also use the results returned by the test function to launch multiple test
56              
57             use Test::Parallel;
58             use Test::More;
59              
60             my $p = Test::Parallel->new();
61             $p->add( sub {
62             # will be launched in parallel
63             # any code that take time to execute need to go there
64             my $time = int( rand(42) );
65             sleep( $time );
66             return { number => 123, time => $time };
67             },
68             sub {
69             # will be execute from the main thread ( not in parallel )
70             my $result = shift;
71             is $result->{number} => 123;
72             cmp_ok $result->{time}, '<=', 42;
73             }
74             );
75            
76             $p->done();
77              
78             =for Pod::Coverage ok is isnt like unlike cmp_ok is_deeply can_ok isa_ok
79              
80             =head1 METHODS
81              
82             =head2 new
83              
84             Create a new Test::Parallel object.
85             By default it will use the number of cores you have as a maximum limit of parallelized job,
86             but you can control this value with two options :
87             - max_process : set the maximum process to this value
88             - max_process_per_cpu : set the maximum process per cpu, this value
89             will be multiplied by the number of cpu ( core ) avaiable on your server
90             - max_memory : in MB per job. Will use the minimum between #cpu and total memory available / max_memory
91              
92             my $p = Test::Parallel->new()
93             or Test::Parallel->new( max_process => N )
94             or Test::Parallel->new( max_process_per_cpu => P )
95             or Test::Parallel->new( max_memory => M )
96              
97             =cut
98              
99             my @methods = qw{ok is isnt like unlike cmp_ok is_deeply can_ok isa_ok};
100              
101             sub new {
102 97     97 1 90578 my ( $class, %opts ) = @_;
103              
104 97         348 my $self = bless {}, __PACKAGE__;
105              
106 97         492 $self->_init(%opts);
107              
108 97         571 return $self;
109             }
110              
111             =head2 ok
112              
113             Same as Test::More::ok but need a code ref in first argument
114              
115             =head2 is
116              
117             Same as Test::More::is but need a code ref in first argument
118              
119             =head2 isnt
120              
121             Same as Test::More::isnt but need a code ref in first argument
122              
123             =head2 like
124              
125             Same as Test::More::like but need a code ref in first argument
126              
127             =head2 unlike
128              
129             Same as Test::More::unlike but need a code ref in first argument
130              
131             =head2 cmp_ok
132              
133             Same as Test::More::cmp_ok but need a code ref in first argument
134              
135             =head2 is_deeply
136              
137             Same as Test::More::is_deeply but need a code ref in first argument
138              
139             =cut
140              
141             sub _init {
142 97     97   226 my ( $self, %opts ) = @_;
143              
144 97         492 $self->_add_methods();
145 97         569 $self->_pfork(%opts);
146 97         84439 $self->{result} = {};
147             $self->{pfork}->run_on_finish(
148             sub {
149 239     239   44905604 my ( $pid, $exit, $id, $exit_signal, $core_dump, $data ) = @_;
150 239 50 33     2985 die "Failed to process on one job, stop here !"
151             if $exit || $exit_signal;
152 239         2514 $self->{result}->{$id} = $data->{result};
153             }
154 97         1448 );
155 97         2330 $self->{jobs} = [];
156 97         353 $self->{tests} = [];
157             }
158              
159             sub _pfork {
160 97     97   243 my ( $self, %opts ) = @_;
161              
162 97         147 my $cpu;
163 97 100       518 if ( defined $opts{max_process} ) {
164 19         114 $cpu = $opts{max_process};
165             }
166             else {
167 78   100     1078 my $factor = $opts{max_process_per_cpu} || 1;
168 78         158 eval { $cpu = Sys::Info->new()->device('CPU')->count() * $factor; };
  78         1052  
169             }
170 97 100       872469 if ( defined $opts{max_memory} ) {
171 11         33 my $free_mem;
172 11         22 eval {
173 11         15983 require Sys::Statistics::Linux::MemStats;
174 11         10153 $free_mem = Sys::Statistics::Linux::MemStats->new->get->{realfree};
175             };
176 11         12155 my $max_mem = $opts{max_memory} * 1024; # 1024 **2 = 1 GO => expr in Kb
177 11         22 my $cpu_for_mem;
178 11 50       77 if ($@) {
179 0         0 warn "Cannot guess amount of available free memory need Sys::Statistics::Linux::MemStats";
180 0         0 $cpu_for_mem = 2;
181             }
182             else {
183 11         44 $cpu_for_mem = int( $free_mem / $max_mem );
184             }
185              
186             # min
187 11 50       44 $cpu = ( $cpu_for_mem < $cpu ) ? $cpu_for_mem : $cpu;
188             }
189 97   50     465 $cpu ||= 1;
190              
191             # we could also set a minimum amount of required memory
192 97         1315 $self->{pfork} = new Parallel::ForkManager( int($cpu) );
193             }
194              
195             =head2 $pm->add($code)
196              
197             You can manually add some code to be launched in parallel,
198             but if you uses this method you will need to manipulate yourself the final
199             result.
200              
201             Prefer using one of the following methods :
202            
203             ok is isnt like unlike cmp_ok is_deeply
204              
205             =cut
206              
207             sub add {
208 423     423 1 73168 my ( $self, $code, $test ) = @_;
209              
210 423 50 33     3006 return unless $code && ref $code eq 'CODE';
211 423         1058 push(
212 423         1730 @{ $self->{jobs} },
213 423         596 { name => ( scalar( @{ $self->{jobs} } ) + 1 ), code => $code }
214             );
215 423         583 push( @{ $self->{tests} }, $test );
  423         1888  
216             }
217              
218             =head2 $p->run
219              
220             will run and wait for all jobs added
221             you do not need to use this method except if you prefer to add jobs yourself and manipulate the results
222              
223             =cut
224              
225             sub run {
226 97     97 1 3246 my ($self) = @_;
227              
228 97 50       191 return unless scalar @{ $self->{jobs} };
  97         476  
229 97         218 my $pfm = $self->{pfork};
230 97         615 for my $job ( @{ $self->{jobs} } ) {
  97         293  
231 346 100       1086410 $pfm->start( $job->{name} ) and next;
232 36         152831 my $job_result = $job->{code}();
233              
234             # can be used to stop on first error
235 36         12017213 my $job_error = 0;
236 36         3290 $pfm->finish( $job_error, { result => $job_result } );
237             }
238              
239             # wait for all jobs
240 61         139804 $pfm->wait_all_children;
241              
242 61         2256 return $self->{result};
243             }
244              
245             sub _add_methods {
246              
247 97 100   97   549 return unless scalar @methods;
248              
249 39         92 foreach my $sub (@methods) {
250 351         664 my $accessor = __PACKAGE__ . "::$sub";
251 351         586 my $map_to = "Test::More::$sub";
252 351 50       379 next unless defined &{$map_to};
  351         1353  
253              
254             # allow symbolic refs to typeglob
255 39     39   32689 no strict 'refs';
  39         110  
  39         16960  
256             *$accessor = sub {
257 72     72   675 my ( $self, $code, @args ) = @_;
258 72         297 $self->add( $code, { test => $map_to, args => \@args } );
259 351         2683 };
260             }
261              
262 39         154 @methods = ();
263             }
264              
265             =head2 $p->done
266              
267             you need to call this function when you are ready to launch all jobs in bg
268             this method will call run and also check results with Test::More
269              
270             =cut
271              
272             sub done {
273 22     22 1 6179 my ($self) = @_;
274              
275             # run tests
276 22 50       95 die "Cannot run tests" unless $self->run();
277              
278 8         39 my $c = 0;
279              
280             # check all results with Test::More
281 8         67 my $results = $self->results();
282 19         14600 map {
283 8         50 my $test = $_;
284 19 50       57 return unless $test;
285 19 50       108 die "cannot find result for test #${c}" unless exists $results->[$c];
286 19         72 my $res = $results->[ $c++ ];
287              
288 19 100       164 if ( ref $test eq 'HASH' ) {
    50          
289              
290             # internal mechanism
291 8 50 33     57 return unless defined $test->{test} && defined $test->{args};
292              
293 8         9 my @args = ( $res, @{ $test->{args} } );
  8         68  
294 8         26 my $t = $test->{test};
295 8         20 my $str = join( ', ', map { "\$args[$_]" } ( 0 .. $#args ) );
  25         71  
296 8         766 eval "$t(" . $str . ")";
297             }
298             elsif ( ref $test eq 'CODE' ) {
299              
300             # execute user function
301 11         146 $test->($res);
302             }
303              
304 8         35 } @{ $self->{tests} };
305              
306             }
307              
308             =head2 $p->results
309              
310             get an array of results, in the same order of jobs
311              
312             =cut
313              
314             sub results {
315 165     165 1 346 my ($self) = @_;
316              
317 649         1474 my @sorted =
318 814         1654 map { $self->{result}{$_} }
319 165         309 sort { int($a) <=> int($b) } keys %{ $self->{result} };
  165         8540  
320 165         1536 return \@sorted;
321             }
322              
323             =head2 $p->result
324              
325             alias to results
326              
327             =cut
328              
329             {
330 39     39   293 no warnings;
  39         78  
  39         3204  
331             *result = \&results;
332             }
333              
334             1;
335              
336             __END__