File Coverage

blib/lib/Test/BrewBuild/Dispatch.pm
Criterion Covered Total %
statement 60 277 21.6
branch 5 98 5.1
condition 0 39 0.0
subroutine 17 23 73.9
pod 3 3 100.0
total 85 440 19.3


line stmt bran cond sub pod time code
1             package Test::BrewBuild::Dispatch;
2 34     34   193868 use strict;
  34         153  
  34         979  
3 34     34   203 use warnings;
  34         68  
  34         996  
4              
5 34     34   11973 use Capture::Tiny qw(:all);
  34         342330  
  34         4700  
6 34     34   271 use Carp qw(croak);
  34         69  
  34         1509  
7 34     34   15612 use Config::Tiny;
  34         40328  
  34         1177  
8 34     34   230 use Cwd qw(getcwd);
  34         78  
  34         1700  
9 34     34   16027 use IO::Socket::INET;
  34         415877  
  34         225  
10 34     34   22326 use Logging::Simple;
  34         138467  
  34         1158  
11 34     34   19074 use Parallel::ForkManager;
  34         1786816  
  34         1460  
12 34     34   320 use POSIX;
  34         81  
  34         319  
13 34     34   70811 use Storable;
  34         138  
  34         2220  
14 34     34   7740 use Test::BrewBuild;
  34         93  
  34         1345  
15 34     34   250 use Test::BrewBuild::Constant qw(:all);
  34         79  
  34         4653  
16 34     34   11686 use Test::BrewBuild::Git;
  34         120  
  34         1089  
17 34     34   231 use Test::BrewBuild::Regex;
  34         85  
  34         102313  
18              
19             our $VERSION = '2.21';
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 91 my ($class, %args) = @_;
30              
31 1         4 my $self = bless {}, $class;
32              
33 1         9 $log = Logging::Simple->new(level => 0, name => 'Dispatch');
34              
35 1 50       168 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         4 $log->child('new')->_5("instantiating new object");
41              
42 1         110 $self->{auto} = $args{auto};
43 1 50       5 $self->{autotest} = $args{autotest} if defined $args{autotest};
44 1 50       5 $self->{forks} = defined $args{forks} ? $args{forks} : 4;
45 1 50       4 $self->{rpi} = defined $args{rpi} ? $args{rpi} : undef;
46 1         3 $self->{fail_count} = 0;
47              
48 1         5 $self->_config;
49              
50 1         6 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 _lcd {
211             # used only for dispatching to an RPi in auto mode
212              
213 0     0   0 my ($pins, $rows, $cols) = @_;
214              
215 0         0 require RPi::LCD;
216              
217 0         0 my $lcd = RPi::LCD->new;
218              
219 0         0 $lcd->init(
220             rows => $rows,
221             cols => $cols,
222             bits => 4,
223             rs => $pins->[0],
224             strb => $pins->[1],
225             d0 => $pins->[2],
226             d1 => $pins->[3],
227             d2 => $pins->[4],
228             d3 => $pins->[5],
229             d4 => 0,
230             d5 => 0,
231             d6 => 0,
232             d7 => 0
233             );
234              
235 0         0 return $lcd;
236             }
237             sub _lcd_display {
238 0     0   0 my ($self, $lcd, %args) = @_;
239              
240 0 0 0     0 if ($self->{rpi_lcd_rows} == 4 && $self->{rpi_lcd_cols} == 20){
241 0         0 $lcd->position(0, 0);
242 0         0 $lcd->print($args{repo});
243              
244 0         0 $lcd->position(0, 1);
245 0         0 $lcd->print($args{time});
246              
247 0         0 $lcd->position(0, 2);
248 0         0 $lcd->print($ENV{BB_RUN_STATUS});
249              
250 0         0 $lcd->position(5, 2);
251 0         0 $lcd->print("commit: $args{commit}");
252              
253 0         0 $lcd->position(0, 3);
254 0         0 $lcd->print("run: $args{run_count}");
255              
256 0         0 $lcd->position(10, 3);
257 0         0 $lcd->print("fails: $self->{fail_count}");
258             }
259             else {
260 0         0 $lcd->position(0, 0);
261 0         0 $lcd->print($args{time});
262              
263 0         0 $lcd->position(12, 0);
264 0         0 $lcd->print($ENV{BB_RUN_STATUS});
265              
266 0         0 $lcd->position(9, 1);
267 0         0 $lcd->print($args{commit});
268              
269 0         0 $lcd->position(0, 1);
270 0         0 $lcd->print($args{run_count});
271             }
272             }
273             sub dispatch {
274 0     0 1 0 my ($self, %params) = @_;
275              
276 0   0     0 my $cmd = $params{cmd} || $self->{cmd};
277 0 0       0 $cmd = 'brewbuild' if ! $cmd;
278 0   0     0 my $repo = $params{repo} || $self->{repo};
279              
280 0 0 0     0 if (defined $repo && ($repo !~ /^http/ && $repo !~ /^git/)){
      0        
281 0         0 $repo = REPO_PREFIX . $repo;
282             }
283              
284 0   0     0 my $testers = $params{testers} || $self->{testers};
285              
286 0         0 my $log = $log->child('dispatch');
287              
288 0         0 my %remotes;
289              
290 0 0       0 if (! $testers->[0]){
291 0         0 $log->_6("no --testers passed in, and failed to fetch testers from " .
292             "config file, croaking"
293             );
294 0         0 croak "dispatch requires testers sent in or config file, which " .
295             "can't be found. Run \"bbdispatch -h\" for help.\n";
296             }
297             else {
298 0         0 $log->_7("working on testers: " . join ', ', @$testers);
299              
300 0         0 for my $tester (@$testers){
301 0         0 my ($host, $port);
302 0 0       0 if ($tester =~ /:/){
303 0         0 ($host, $port) = split /:/, $tester;
304             }
305             else {
306 0         0 $host = $tester;
307 0         0 $port = 7800;
308             }
309 0         0 $remotes{$host}{port} = $port;
310 0         0 $log->_5("configured $host with port $port");
311             }
312             }
313              
314             # spin up the comms
315              
316 0         0 %remotes = $self->_fork(\%remotes, $cmd, $repo);
317              
318 0 0       0 if (! -d 'bblog'){
319 0 0       0 mkdir 'bblog' or croak $!;
320 0         0 $log->_7("created log dir: bblog");
321             }
322              
323             # init the return string
324              
325 0         0 my $return = "\n";
326              
327 0         0 for my $ip (keys %remotes){
328 0 0       0 if (! defined $remotes{$ip}{build}){
329 0         0 $log->_5("tester: $ip didn't supply results... deleting");
330 0         0 delete $remotes{$ip};
331 0         0 next;
332             }
333              
334             # build log file generation
335              
336 0         0 for my $build_log (keys %{ $remotes{$ip}{build}{files} }){
  0         0  
337 0         0 $log->_7("generating build log: $build_log");
338              
339 0         0 my $content = $remotes{$ip}{build}{files}{$build_log};
340 0         0 $log->_7("writing out log: " . getcwd() . "/bblog/$ip\_$build_log");
341 0 0       0 open my $wfh, '>', "bblog/$ip\_$build_log" or croak $!;
342 0         0 for (@$content){
343 0         0 print $wfh $_;
344             }
345             }
346              
347             # build the return string
348              
349 0         0 my $build = $remotes{$ip}{build};
350              
351 0         0 $return .= "$ip - $build->{platform}\n";
352 0 0       0 $return .= "$build->{log}" if $build->{log};
353              
354 0 0       0 if (ref $build->{data} eq 'ARRAY'){
355 0         0 $return .= $_ for @{ $build->{data} };
  0         0  
356             }
357             else {
358 0 0       0 $build->{data} = '' if ! $build->{data};
359 0         0 $return .= "$build->{data}\n";
360             }
361             }
362 0         0 $log->_7("returning results if available...");
363 0         0 return $return;
364             }
365             sub _config {
366             # slurp in config file elements
367              
368 1     1   2 my $self = shift;
369              
370 1         9 my $conf_file = Test::BrewBuild->config_file;
371              
372 1 50       16 if (-f $conf_file){
373 0           my $conf = Config::Tiny->read($conf_file)->{dispatch};
374 0 0         if ($conf->{testers}){
375 0           $conf->{testers} =~ s/\s+//;
376 0           $self->{testers} = [ split /,/, $conf->{testers} ];
377             }
378 0 0         $self->{repo} = $conf->{repo} if $conf->{repo};
379 0 0         $self->{cmd} = $conf->{cmd} if $conf->{cmd};
380             $self->{auto_sleep} = $conf->{auto_sleep}
381 0 0         if defined $conf->{auto_sleep};
382 0   0       $self->{rpi} = $conf->{rpi} || 0;
383 0   0       $self->{rpi_lcd_rows} = $conf->{rpi_lcd_rows} || 4;
384 0   0       $self->{rpi_lcd_cols} = $conf->{rpi_lcd_cols} || 20;
385             }
386             }
387             sub _fork {
388             # handles the tester communications
389              
390 0     0     my ($self, $remotes, $cmd, $repo) = @_;
391              
392 0           my $log = $log->child('_fork');
393              
394 0           my $pm = Parallel::ForkManager->new($self->{forks});
395              
396             $pm->run_on_finish(
397             sub {
398 0     0     my (undef, undef, undef, undef, undef, $tester_data) = @_;
399 0           map {$remotes->{$_} = $tester_data->{$_}} keys %$tester_data;
  0            
400 0 0         $log->_5("tester: " . (keys %$tester_data)[0] ." finished")
401             if keys %$tester_data;
402             }
403 0           );
404              
405 0           for my $tester (keys %$remotes){
406 0           $log->_7("spinning up tester: $tester");
407              
408 0           my $log = $log->child($tester);
409              
410 0 0         $pm->start and next;
411              
412 0           my %return;
413              
414             my $socket = new IO::Socket::INET (
415             PeerHost => $tester,
416             PeerPort => $remotes->{$tester}{port},
417 0           Proto => 'tcp',
418             );
419 0 0         if (! $socket){
420 0           croak "can't connect to remote $tester on port " .
421             "$remotes->{$tester}{port} $!\n";
422             }
423              
424 0           $log->_7("tester $tester socket created ok");
425              
426             # syn
427 0           $socket->send($tester);
428 0           $log->_7("syn \"$tester\" sent");
429              
430             # ack
431 0           my $ack;
432 0           $socket->recv($ack, 1024);
433 0           $log->_7("ack \"$ack\" received");
434              
435 0 0         if ($ack ne $tester){
436 0           $log->_0("comm error: syn \"$tester\" doesn't match ack \"$ack\"");
437 0           croak "comm discrepancy: expected $tester, got $ack\n";
438             }
439              
440 0 0         if (! $cmd){
441 0           $log->_6("no command specified, Tester default will ensue");
442             }
443 0           $socket->send($cmd);
444 0           $log->_7("sent command: $cmd");
445              
446 0           my $check = '';
447 0           $socket->recv($check, 1024);
448 0           $log->_7("received \"$check\"");
449              
450 0 0         if ($check =~ /^error:/){
451 0           $log->_0("received an error: $check... killing all procs");
452 0           kill '-9', $$;
453             }
454 0 0         if ($check eq 'ok'){
455 0           my $repo_link;
456              
457 0 0         if (! $repo){
458 0           my $git = Test::BrewBuild::Git->new(debug => $self->{debug});
459 0           $log->_5("repo not sent in, attempting to set via Git");
460 0           $repo_link = $git->link;
461              
462 0 0         if ($repo_link){
463 0           $log->_5("repo set to $repo_link from Git");
464             }
465             else {
466 0           $log->_7(
467             "\$repo_link could not be set, we're about to fail..."
468             );
469             }
470             }
471             else {
472 0           $repo_link = $repo;
473 0           $log->_5("repo was sent in, and set to: $repo_link");
474             }
475              
476 0 0         if (! $repo_link){
477 0           $log->_0(
478             "no repository supplied and not in a repo dir... croaking"
479             );
480 0           croak
481             "\nno repository found, and none sent in via param, " .
482             "can't continue...";
483             }
484              
485 0           $log->_6("dispatching out to and waiting for tester: '$tester'...");
486              
487 0           $socket->send($repo_link);
488              
489 0           my $ok = eval {
490 0           $return{$tester}{build} = Storable::fd_retrieve($socket);
491 0           1;
492             };
493              
494 0           $log->_7("tester work has concluded");
495              
496 0 0 0       if (! $ok && ! defined $self->{auto}){
497 0           $log->_0("errors occurred... check your command line " .
498             "string for invalid args. You sent in: $cmd.\n" .
499             "The full error: $@"
500             );
501 0           exit;
502             }
503             }
504             else {
505 0           $log->_5(
506             "deleted tester: $remotes->{$tester}... incomplete session"
507             );
508 0           delete $remotes->{$tester};
509             }
510 0           $socket->close();
511 0           $pm->finish(0, \%return);
512             }
513              
514 0           $pm->wait_all_children;
515              
516 0           return %$remotes;
517             }
518             1;
519              
520             =head1 NAME
521              
522             Test::BrewBuild::Dispatch - Dispatch C test runs to remote test
523             servers.
524              
525             =head1 SYNOPSIS
526              
527             use Test::BrewBuild::Dispatch;
528              
529             my $d = Test::BrewBuild::Dispatch->new;
530              
531             my $return = $d->dispatch(
532             cmd => 'brewbuild -r -R',
533             testers => [qw(127.0.0.1 10.1.1.1:9999)],
534             repo => 'https://github.com/user/repo',
535             );
536              
537             print $return;
538              
539             =head1 DESCRIPTION
540              
541             This is the remote dispatching system of L.
542              
543             It dispatches out test runs to L remote test servers
544             to perform, then processes the results returned from those testers.
545              
546             By default, we try to look up the repository information from your current
547             working directory. If it can't be found, you must supply it on the command line
548             or within the configuration file.
549              
550             =head1 METHODS
551              
552             =head2 new
553              
554             Returns a new C object.
555              
556             =head2 dispatch(cmd => '', repo => '', testers => ['', ''], debug => 0-7)
557              
558             C is the C command string that will be executed.
559              
560             C is the name of the repo to test against, and is optional.
561             If not supplied, we'll attempt to get a repo name from the local working
562             directory you're working in. If it's a Github repo, you need not enter in the full
563             path... we'll prepend C if you send in C.
564              
565             C is manadory unless you've set up a config file, and contains an
566             array reference of IP/Port pairs for remote testers to dispatch to and follow.
567             eg: C<[qw(10.1.1.5 172.16.5.5:9999)]>. If the port portion of the tester is
568             omitted, we'll default to C<7800>.
569              
570             By default, the testers run on all IPs and port C.
571              
572             C optional, set to a level between 0 and 7.
573              
574             See L for more details on the testers that the
575             dispatcher dispatches to.
576              
577             =head2 auto(%params)
578              
579             This function will spin off a continuous run of C runs, based on
580             whether the commit revision checksum locally is different than that from the
581             remote. It takes all of the same parameters as C, and the
582             C<-r|--repo> parameter is mandatory.
583              
584             There is also a configuration file directive in the C<[Dispatch]> section,
585             C, which dictates how many seconds to sleep in between each run. The
586             default is C<60>, or one minute.
587              
588             =head1 AUTHOR
589              
590             Steve Bertrand, C<< >>
591              
592             =head1 LICENSE AND COPYRIGHT
593              
594             Copyright 2017 Steve Bertrand.
595              
596             This program is free software; you can redistribute it and/or modify it
597             under the terms of either: the GNU General Public License as published
598             by the Free Software Foundation; or the Artistic License.
599              
600             See L for more information.
601              
602             =cut
603