File Coverage

blib/lib/Test/BrewBuild/Dispatch.pm
Criterion Covered Total %
statement 60 283 21.2
branch 5 100 5.0
condition 0 39 0.0
subroutine 17 23 73.9
pod 3 3 100.0
total 85 448 18.9


line stmt bran cond sub pod time code
1             package Test::BrewBuild::Dispatch;
2 34     34   206598 use strict;
  34         168  
  34         938  
3 34     34   157 use warnings;
  34         58  
  34         1056  
4              
5 34     34   10727 use Capture::Tiny qw(:all);
  34         351336  
  34         3866  
6 34     34   218 use Carp qw(croak);
  34         57  
  34         1294  
7 34     34   14928 use Config::Tiny;
  34         30679  
  34         1016  
8 34     34   191 use Cwd qw(getcwd);
  34         61  
  34         1329  
9 34     34   14775 use IO::Socket::INET;
  34         390771  
  34         196  
10 34     34   18833 use Logging::Simple;
  34         120677  
  34         941  
11 34     34   16526 use Parallel::ForkManager;
  34         1639218  
  34         1155  
12 34     34   249 use POSIX;
  34         80  
  34         264  
13 34     34   57877 use Storable;
  34         86  
  34         1893  
14 34     34   7588 use Test::BrewBuild;
  34         79  
  34         1108  
15 34     34   243 use Test::BrewBuild::Constant qw(:all);
  34         65  
  34         3987  
16 34     34   10427 use Test::BrewBuild::Git;
  34         117  
  34         900  
17 34     34   178 use Test::BrewBuild::Regex;
  34         69  
  34         87984  
18              
19             our $VERSION = '2.22';
20              
21             $| = 1;
22              
23             my ($log, $last_run_status, $results_returned);
24             $ENV{BB_RUN_STATUS} = 'PASS';
25              
26             my $lcd; # RPi specific testing
27              
28             sub new {
29 1     1 1 77 my ($class, %args) = @_;
30              
31 1         3 my $self = bless {}, $class;
32              
33 1         10 $log = Logging::Simple->new(level => 0, name => 'Dispatch');
34              
35 1 50       153 if (defined $args{debug}){
36 0 0       0 $log->level($args{debug}) if defined $args{debug};
37 0         0 $self->{debug} = $args{debug};
38             }
39              
40 1         5 $log->child('new')->_5("instantiating new object");
41              
42 1         105 $self->{auto} = $args{auto};
43 1 50       4 $self->{autotest} = $args{autotest} if defined $args{autotest};
44 1 50       4 $self->{forks} = defined $args{forks} ? $args{forks} : 4;
45 1 50       3 $self->{rpi} = defined $args{rpi} ? $args{rpi} : undef;
46 1         3 $self->{fail_count} = 0;
47              
48 1         4 $self->_config;
49              
50 1         4 return $self;
51             }
52             sub auto {
53 0     0 1 0 my ($self, %params) = @_;
54              
55 0         0 my $log = $log->child('auto');
56              
57 0         0 $log->_5("Commencing auto run dispatch sequence");
58              
59 0         0 $last_run_status = $ENV{BB_RUN_STATUS};
60              
61 0 0       0 if (! defined $params{repo}){
62 0         0 $log->_5("auto() requires the --repo param sent in. Can't continue...");
63 0         0 croak "auto mode requires the repository parameter sent in.\n";
64             }
65             else {
66 0 0 0     0 if ($params{repo} !~ /^http/ || $params{repo} !~ /^git/){
67 0         0 $params{repo} = REPO_PREFIX . $params{repo};
68             }
69             }
70              
71 0 0       0 my $sleep = defined $self->{auto_sleep} ? $self->{auto_sleep} : 60;
72              
73 0         0 $log->_7("waiting $sleep seconds between test runs");
74              
75 0         0 my $runs = $self->{auto};
76 0         0 my $run_count = 1;
77              
78 0 0       0 $log->_7("$runs auto runs planned") if $runs > 0;
79 0 0       0 $log->_7("continuous integration mode enabled") if $runs == 0;
80              
81 0         0 my $git = Test::BrewBuild::Git->new;
82              
83 0         0 while (1){
84              
85 0 0       0 if (! $runs){
86 0         0 $log->_6("COMMENCING RUN: $run_count\n");
87             }
88             else {
89 0         0 $log->_6("COMMENCING RUN: $run_count of $runs");
90             }
91              
92 0         0 my $results = $self->dispatch(%params);
93             my @short_results
94 0         0 = $results =~ /${ re_dispatch('extract_short_results')}/g;
  0         0  
95              
96 0         0 print "$_\n" for @short_results;
97              
98 0 0       0 if (grep /FAIL/, @short_results){
    0          
99 0         0 $log->_5("auto run status: FAIL");
100 0         0 $self->{fail_count}++;
101 0         0 $ENV{BB_RUN_STATUS} = 'FAIL';
102 0         0 $results_returned = 1;
103             }
104             elsif (grep /PASS/, @short_results){
105 0         0 $log->_5("auto run status: PASS");
106 0         0 $ENV{BB_RUN_STATUS} = 'PASS';
107 0         0 $results_returned = 1;
108             }
109             else {
110 0         0 $log->_5("no results returned");
111 0         0 $results_returned = 0;
112             }
113              
114 0 0       0 if ($self->{rpi}){
115 0         0 $log->_7("RPi LCD test result output enabled");
116              
117 0 0       0 if ($ENV{BB_RPI_LCD}){
118 0 0       0 if ($results_returned){
119              
120 0         0 my @lcd_info = split /,/, $ENV{BB_RPI_LCD};
121 0         0 my @pins;
122              
123 0 0       0 if (@lcd_info == 8){
124 0         0 $self->{rpi_lcd_rows} = $lcd_info[6];
125 0         0 $self->{rpi_lcd_cols} = $lcd_info[7];
126 0         0 @pins = @lcd_info[0..5];
127             }
128 0 0 0     0 if (! $lcd && @pins == 6){
    0 0        
129 0         0 $lcd = _lcd(\@pins, $self->{rpi_lcd_rows}, $self->{rpi_lcd_cols});
130             }
131             elsif (! $lcd && @pins != 6) {
132 0         0 $log->_1(
133             "in --rpi mode, but BB_RPI_LCD env var not set " .
134             "correctly"
135             );
136 0         0 warn "bbdispatch is in --rpi mode, but the BB_RPI_LCD ".
137             " env var isn't set correctly. See the documentation" .
138             "...\n";
139             }
140              
141             my $commit = $git->revision(
142             remote => 1, repo => $params{repo}
143 0         0 );
144              
145 0         0 $commit = substr $commit, 0, 7;
146              
147 0         0 my $time;
148              
149 0 0       0 if ($self->{rpi_lcd_cols} == 20){
150 0         0 $time = strftime(
151             "%Y/%m/%d %H:%M", localtime(time)
152             );
153             }
154             else {
155 0         0 $time = strftime(
156             "%m/%d %H:%M", localtime(time)
157             );
158             }
159              
160 0         0 my ($repo) = $params{repo} =~ m|.*/(.*)|;
161              
162 0         0 $lcd->clear;
163 0         0 $self->_lcd_display(
164             $lcd,
165             commit => $commit,
166             time => $time,
167             run_count => $run_count,
168             repo => $repo,
169             );
170              
171             # $lcd->position(0, 0);
172             # $lcd->print($time);
173            
174             # $lcd->position(12, 0);
175             # $lcd->print($ENV{BB_RUN_STATUS});
176              
177             # $lcd->position(9, 1);
178             # $lcd->print($commit);
179              
180             # $lcd->position(0, 1);
181             # $lcd->print($run_count);
182              
183             }
184             }
185             else {
186 0         0 $log->_7("in --rpi mode, but BB_RPI_LCD env var not set");
187             }
188             }
189             else {
190 0         0 $log->_7("not in --rpi mode");
191             }
192              
193 0 0 0     0 if ($run_count >= $runs && $runs != 0){
194 0         0 $log->_6(
195             "auto run complete. No more runs to perform, exiting...\n"
196             );
197 0         0 exit;
198             }
199             else {
200 0         0 $log->_6(
201             "auto run complete. Sleeping for $sleep seconds, then " .
202             "commencing the next run\n"
203             );
204 0         0 $run_count++;
205             }
206              
207 0         0 sleep $sleep;
208             }
209             }
210             sub dispatch {
211 0     0 1 0 my ($self, %params) = @_;
212              
213 0   0     0 my $cmd = $params{cmd} || $self->{cmd};
214 0 0       0 $cmd = 'brewbuild' if ! $cmd;
215 0   0     0 my $repo = $params{repo} || $self->{repo};
216              
217 0 0 0     0 if (defined $repo && ($repo !~ /^http/ && $repo !~ /^git/)){
      0        
218 0         0 $repo = REPO_PREFIX . $repo;
219             }
220              
221 0   0     0 my $testers = $params{testers} || $self->{testers};
222              
223 0         0 my $log = $log->child('dispatch');
224              
225 0         0 my %remotes;
226              
227 0 0       0 if (! $testers->[0]){
228 0         0 $log->_6("no --testers passed in, and failed to fetch testers from " .
229             "config file, croaking"
230             );
231 0         0 croak "dispatch requires testers sent in or config file, which " .
232             "can't be found. Run \"bbdispatch -h\" for help.\n";
233             }
234             else {
235 0         0 $log->_7("working on testers: " . join ', ', @$testers);
236              
237 0         0 for my $tester (@$testers){
238 0         0 my ($host, $port);
239 0 0       0 if ($tester =~ /:/){
240 0         0 ($host, $port) = split /:/, $tester;
241             }
242             else {
243 0         0 $host = $tester;
244 0         0 $port = 7800;
245             }
246 0         0 $remotes{$host}{port} = $port;
247 0         0 $log->_5("configured $host with port $port");
248             }
249             }
250              
251             # spin up the comms
252              
253 0         0 %remotes = $self->_fork(\%remotes, $cmd, $repo);
254              
255 0 0       0 if (! -d 'bblog'){
256 0 0       0 mkdir 'bblog' or croak $!;
257 0         0 $log->_7("created log dir: bblog");
258             }
259              
260             # init the return string
261              
262 0         0 my $return = "\n";
263              
264 0         0 for my $ip (keys %remotes){
265 0 0       0 if (! defined $remotes{$ip}{build}){
266 0         0 $log->_5("tester: $ip didn't supply results... deleting");
267 0         0 delete $remotes{$ip};
268 0         0 next;
269             }
270              
271             # build log file generation
272              
273 0         0 for my $build_log (keys %{ $remotes{$ip}{build}{files} }){
  0         0  
274 0         0 $log->_7("generating build log: $build_log");
275              
276 0         0 my $content = $remotes{$ip}{build}{files}{$build_log};
277 0         0 my $timestamp = Test::BrewBuild::timestamp();
278 0         0 $log->_7("writing out log: " . getcwd() . "/bblog/$ip\_$build_log");
279 0 0       0 open my $wfh, '>', "bblog/$ip\_$build_log.$timestamp" or croak $!;
280 0         0 for (@$content){
281 0         0 print $wfh $_;
282             }
283             }
284              
285             # build the return string
286              
287 0         0 my $build = $remotes{$ip}{build};
288              
289 0         0 $return .= "$ip - $build->{platform}\n";
290 0 0       0 $return .= "$build->{log}" if $build->{log};
291              
292 0 0       0 if (ref $build->{data} eq 'ARRAY'){
293 0         0 $return .= $_ for @{ $build->{data} };
  0         0  
294             }
295             else {
296 0 0       0 $build->{data} = '' if ! $build->{data};
297 0         0 $return .= "$build->{data}\n";
298             }
299             }
300 0         0 $log->_7("returning results if available...");
301 0         0 return $return;
302             }
303             sub _config {
304             # slurp in config file elements
305              
306 1     1   3 my $self = shift;
307              
308 1         6 my $conf_file = Test::BrewBuild->config_file;
309              
310 1 50       15 if (-f $conf_file){
311 0           my $conf = Config::Tiny->read($conf_file)->{dispatch};
312 0 0         if ($conf->{testers}){
313 0           $conf->{testers} =~ s/\s+//;
314 0           $self->{testers} = [ split /,/, $conf->{testers} ];
315             }
316 0 0         $self->{repo} = $conf->{repo} if $conf->{repo};
317 0 0         $self->{cmd} = $conf->{cmd} if $conf->{cmd};
318             $self->{auto_sleep} = $conf->{auto_sleep}
319 0 0         if defined $conf->{auto_sleep};
320 0   0       $self->{rpi} = $conf->{rpi} || 0;
321 0   0       $self->{rpi_lcd_rows} = $conf->{rpi_lcd_rows} || 4;
322 0   0       $self->{rpi_lcd_cols} = $conf->{rpi_lcd_cols} || 20;
323             }
324             }
325             sub _fork {
326             # handles the tester communications
327              
328 0     0     my ($self, $remotes, $cmd, $repo) = @_;
329              
330 0           my $log = $log->child('_fork');
331              
332 0           my $pm = Parallel::ForkManager->new($self->{forks});
333              
334             $pm->run_on_finish(
335             sub {
336 0     0     my (undef, undef, undef, undef, undef, $tester_data) = @_;
337 0           map {$remotes->{$_} = $tester_data->{$_}} keys %$tester_data;
  0            
338 0 0         $log->_5("tester: " . (keys %$tester_data)[0] ." finished")
339             if keys %$tester_data;
340             }
341 0           );
342              
343 0           for my $tester (keys %$remotes){
344 0           $log->_7("spinning up tester: $tester");
345              
346 0           my $log = $log->child($tester);
347              
348 0 0         $pm->start and next;
349              
350 0           my %return;
351              
352             my $socket = new IO::Socket::INET (
353             PeerHost => $tester,
354             PeerPort => $remotes->{$tester}{port},
355 0           Proto => 'tcp',
356             );
357              
358 0 0         if (! $socket){
359 0           die "\nCAN'T CONNECT TO REMOTE TESTER $tester on port " .
360             "$remotes->{$tester}{port}: $!\n\n";
361             }
362              
363 0           $log->_7("tester $tester socket created ok");
364              
365             # syn
366 0           $socket->send($tester);
367 0           $log->_7("syn \"$tester\" sent");
368              
369             # ack
370 0           my $ack;
371 0           $socket->recv($ack, 1024);
372 0           $log->_7("ack \"$ack\" received");
373              
374 0 0         if ($ack ne $tester){
375 0           $log->_0("comm error: syn \"$tester\" doesn't match ack \"$ack\"");
376 0           croak "comm discrepancy: expected $tester, got $ack\n";
377             }
378              
379 0 0         if (! $cmd){
380 0           $log->_6("no command specified, Tester default will ensue");
381             }
382 0           $socket->send($cmd);
383 0           $log->_7("sent command: $cmd");
384              
385 0           my $check = '';
386 0           $socket->recv($check, 1024);
387 0           $log->_7("received \"$check\"");
388              
389 0 0         if ($check =~ /^error:/){
390 0           $log->_0("received an error: $check... killing all procs");
391 0           kill '-9', $$;
392             }
393 0 0         if ($check eq 'ok'){
394 0           my $repo_link;
395              
396 0 0         if (! $repo){
397 0           my $git = Test::BrewBuild::Git->new(debug => $self->{debug});
398 0           $log->_5("repo not sent in, attempting to set via Git");
399 0           $repo_link = $git->link;
400              
401 0 0         if ($repo_link){
402 0           $log->_5("repo set to $repo_link from Git");
403             }
404             else {
405 0           $log->_7(
406             "\$repo_link could not be set, we're about to fail..."
407             );
408             }
409             }
410             else {
411 0           $repo_link = $repo;
412 0           $log->_5("repo was sent in, and set to: $repo_link");
413             }
414              
415 0 0         if (! $repo_link){
416 0           $log->_0(
417             "no repository supplied and not in a repo dir... croaking"
418             );
419 0           croak
420             "\nno repository found, and none sent in via param, " .
421             "can't continue...";
422             }
423              
424 0           $log->_6("dispatching out to and waiting for tester: '$tester'...");
425              
426 0           $socket->send($repo_link);
427              
428 0           my $repo_clone_check = '';
429 0           $socket->recv($repo_clone_check, 1024);
430              
431 0 0         if ($repo_clone_check =~ /error/) {
432 0           $log->_0("REPO CLONE ERROR: $repo_clone_check");
433 0           exit;
434             }
435              
436 0           my $ok = eval {
437 0           $return{$tester}{build} = Storable::fd_retrieve($socket);
438 0           1;
439             };
440              
441 0           $log->_7("tester work has concluded");
442              
443 0 0 0       if (! $ok && ! defined $self->{auto}){
444 0           $log->_0("errors occurred... check your command line " .
445             "string for invalid args. You sent in: $cmd.\n" .
446             "The full error: $@"
447             );
448 0           exit;
449             }
450             }
451             else {
452 0           $log->_5(
453             "deleted tester: $remotes->{$tester}... incomplete session"
454             );
455 0           delete $remotes->{$tester};
456             }
457 0           $socket->close();
458 0           $pm->finish(0, \%return);
459             }
460              
461 0           $pm->wait_all_children;
462              
463 0           return %$remotes;
464             }
465             sub _lcd {
466             # used only for dispatching to an RPi in auto mode
467              
468 0     0     my ($pins, $rows, $cols) = @_;
469              
470 0           require RPi::LCD;
471              
472 0           my $lcd = RPi::LCD->new;
473              
474 0           $lcd->init(
475             rows => $rows,
476             cols => $cols,
477             bits => 4,
478             rs => $pins->[0],
479             strb => $pins->[1],
480             d0 => $pins->[2],
481             d1 => $pins->[3],
482             d2 => $pins->[4],
483             d3 => $pins->[5],
484             d4 => 0,
485             d5 => 0,
486             d6 => 0,
487             d7 => 0
488             );
489              
490 0           return $lcd;
491             }
492             sub _lcd_display {
493 0     0     my ($self, $lcd, %args) = @_;
494              
495 0 0 0       if ($self->{rpi_lcd_rows} == 4 && $self->{rpi_lcd_cols} == 20){
496 0           $lcd->position(0, 0);
497 0           $lcd->print($args{repo});
498              
499 0           $lcd->position(0, 1);
500 0           $lcd->print($args{time});
501              
502 0           $lcd->position(0, 2);
503 0           $lcd->print($ENV{BB_RUN_STATUS});
504              
505 0           $lcd->position(5, 2);
506 0           $lcd->print("commit: $args{commit}");
507              
508 0           $lcd->position(0, 3);
509 0           $lcd->print("run: $args{run_count}");
510              
511 0           $lcd->position(10, 3);
512 0           $lcd->print("fails: $self->{fail_count}");
513             }
514             else {
515 0           $lcd->position(0, 0);
516 0           $lcd->print($args{time});
517              
518 0           $lcd->position(12, 0);
519 0           $lcd->print($ENV{BB_RUN_STATUS});
520              
521 0           $lcd->position(9, 1);
522 0           $lcd->print($args{commit});
523              
524 0           $lcd->position(0, 1);
525 0           $lcd->print($args{run_count});
526             }
527             }
528              
529             1;
530              
531             =head1 NAME
532              
533             Test::BrewBuild::Dispatch - Dispatch C test runs to remote test
534             servers.
535              
536             =head1 SYNOPSIS
537              
538             use Test::BrewBuild::Dispatch;
539              
540             my $d = Test::BrewBuild::Dispatch->new;
541              
542             my $return = $d->dispatch(
543             cmd => 'brewbuild -r -R',
544             testers => [qw(127.0.0.1 10.1.1.1:9999)],
545             repo => 'https://github.com/user/repo',
546             );
547              
548             print $return;
549              
550             =head1 DESCRIPTION
551              
552             This is the remote dispatching system of L.
553              
554             It dispatches out test runs to L remote test servers
555             to perform, then processes the results returned from those testers.
556              
557             By default, we try to look up the repository information from your current
558             working directory. If it can't be found, you must supply it on the command line
559             or within the configuration file.
560              
561             =head1 METHODS
562              
563             =head2 new
564              
565             Returns a new C object.
566              
567             =head2 dispatch(cmd => '', repo => '', testers => ['', ''], debug => 0-7)
568              
569             C is the C command string that will be executed.
570              
571             C is the name of the repo to test against, and is optional.
572             If not supplied, we'll attempt to get a repo name from the local working
573             directory you're working in. If it's a Github repo, you need not enter in the full
574             path... we'll prepend C if you send in C.
575              
576             C is manadory unless you've set up a config file, and contains an
577             array reference of IP/Port pairs for remote testers to dispatch to and follow.
578             eg: C<[qw(10.1.1.5 172.16.5.5:9999)]>. If the port portion of the tester is
579             omitted, we'll default to C<7800>.
580              
581             By default, the testers run on all IPs and port C.
582              
583             C optional, set to a level between 0 and 7.
584              
585             See L for more details on the testers that the
586             dispatcher dispatches to.
587              
588             =head2 auto(%params)
589              
590             This function will spin off a continuous run of C runs, based on
591             whether the commit revision checksum locally is different than that from the
592             remote. It takes all of the same parameters as C, and the
593             C<-r|--repo> parameter is mandatory.
594              
595             There is also a configuration file directive in the C<[Dispatch]> section,
596             C, which dictates how many seconds to sleep in between each run. The
597             default is C<60>, or one minute.
598              
599             =head1 AUTHOR
600              
601             Steve Bertrand, C<< >>
602              
603             =head1 LICENSE AND COPYRIGHT
604              
605             Copyright 2017 Steve Bertrand.
606              
607             This program is free software; you can redistribute it and/or modify it
608             under the terms of either: the GNU General Public License as published
609             by the Free Software Foundation; or the Artistic License.
610              
611             See L for more information.
612              
613             =cut
614