File Coverage

blib/lib/Test/BrewBuild/Dispatch.pm
Criterion Covered Total %
statement 59 273 21.6
branch 5 98 5.1
condition 0 39 0.0
subroutine 17 23 73.9
pod 3 3 100.0
total 84 436 19.2


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