Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
Kill dependency on pod2usage since we can really do without it
[simgrid.git] / buildtools / Cmake / tesh.pl
1 #! /usr/bin/perl
2 eval 'exec perl -S $0 ${1+"$@"}'
3     if $running_under_some_shell;
4
5 =encoding UTF-8
6
7 =head1 NAME
8
9 tesh -- testing shell
10
11 =head1 SYNOPSIS
12
13 B<tesh> [I<options>] I<tesh_file>
14
15 =cut
16
17 my $path = $0;
18 $path =~ s|[^/]*$||;
19 push @INC,$path;
20
21 use Getopt::Long qw(GetOptions);
22 use strict;
23 use Term::ANSIColor;
24 use IPC::Open3;
25
26
27 # make sure we received a tesh file
28 scalar @ARGV > 0 || die "Usage:\n    tesh [*options*] *tesh_file*\n";
29
30 #Add current directory to path
31 $ENV{PATH} = "$ENV{PATH}:.";
32
33
34 ##
35 ## Command line option handling
36 ##
37
38 # option handling helper subs
39 sub cd_cmd {
40     my $directory=$_[1];
41     if (-e $directory && -d $directory) {
42         chdir("$directory");
43         print "[Tesh/INFO] change directory to $directory\n";
44     } elsif (-e $directory) {
45         die "[Tesh/CRITICAL] Cannot change directory to '$directory': it is not a directory\n";
46     } else {
47         die "[Tesh/CRITICAL] Cannot change directory to '$directory': no such directory\n";
48     }
49 }
50
51 sub setenv_cmd {
52     if ($_[1] =~ /^(.*)=(.*)$/) {
53         my($var,$ctn)=($1,$2);
54         $ENV{$var} = $ctn;
55         print "[Tesh/INFO] setenv $var=$ctn\n";
56     } else { 
57         die "[Tesh/CRITICAL] Malformed argument to setenv: expected 'name=value' but got '$_[1]'\n";
58     }
59 }
60
61 # Main option parsing sub
62 my $tesh_file;
63 sub get_options {
64     # remove the tesh file from the ARGV used
65     my @ARGV = @_;
66     $tesh_file = pop @ARGV;
67
68     # temporary arrays for GetOption
69     my @verbose = ();
70     my @cfg;
71     my $log; # ignored
72
73     my %opt = (
74         "help"    => 0,
75         "debug"   => 0,
76         "verbose" => 0
77         );
78
79     Getopt::Long::config('bundling', 'no_getopt_compat', 'no_auto_abbrev');
80     
81     GetOptions(
82         'help|h'     => \$opt{'help'},
83
84         'verbose|v'  => \@verbose,
85         'debug|d'    => \$opt{"debug"},
86
87         'cd=s'       => \&cd_cmd,
88         'setenv=s'   => \&setenv_cmd,
89         'cfg=s'      => \@cfg,
90         'log=s'      => \$log,
91         );
92
93     $opt{'verbose'} = scalar @verbose;
94     foreach (@cfg) {
95         $opt{'cfg'} .= " --cfg=$_";
96     }
97     return %opt;
98 }
99
100 my %opts = get_options(@ARGV);
101
102 ##
103 ## File parsing
104 ##
105 my($sort)=0;
106 my($nb_arg)=0;
107 my($timeout)=0;
108 my($old_buffer);
109 my($linebis);
110 my($SIGABRT)=0;
111 my($no_output_ignore)=1;
112 my($verbose)=0;
113 my($return)=-1;
114 my($pid);
115 my($result);
116 my($result_err);
117 my($forked);
118 my($config)="";
119
120 my($tesh_command)=0;
121 my(@buffer_tesh)=();
122
123 eval {
124     use POSIX;
125     sub exit_status {
126         my $status = shift;
127         if (POSIX::WIFEXITED($status)) {
128             return "returned code ".POSIX::WEXITSTATUS($status);
129         } elsif (POSIX::WIFSIGNALED($status)) {
130             return "got signal ".$SIG{POSIX::WTERMSIG($status)};
131         }
132         return "Unparsable status. Is the process stopped?";
133     }
134 };
135 if ($@) { # no POSIX available?
136     warn "POSIX not usable to parse the return value of forked child: $@\n";
137     sub exit_status {
138         return "returned code 0";
139     }
140 }
141
142 sub exec_cmd { 
143     my %cmd = %{$_[0]};
144     if ($opts{'debug'}) {
145         print "IN BEGIN\n";
146         map {print "  $_"} @{$cmd{'in'}};
147         print "IN END\n";
148         print "OUT BEGIN\n";
149         map {print "  $_"} @{$cmd{'out'}};
150         print "OUT END\n";
151         print "CMD: $cmd{'cmd'}\n";
152     }
153
154     # cleanup the command line
155     $cmd{'cmd'} =~ s/\${EXEEXT:=}//g;
156     $cmd{'cmd'} =~ s|^\./||g;
157 #    $cmd{'cmd'} =~ s|tesh|tesh.pl|g;
158     $cmd{'cmd'} =~ s/\(%i:%P@%h\)/\\\(%i:%P@%h\\\)/g;
159     $cmd{'cmd'} .= " $opts{'cfg'}" if (defined($opts{'cfg'}) && length($opts{'cfg'}));
160
161     print "[$cmd{'file'}:$cmd{'line'}] $cmd{'cmd'}\n";
162
163     ###
164     # exec the command line
165     ###
166     $pid = open3(\*IN, \*OUT, \*OUT, $cmd{'cmd'} );
167
168     # if timeout specified, fork and kill executing child at the end of timeout
169     if ($timeout){
170         $forked = fork();
171         die "fork() failed: $!" unless defined $forked;
172         if ( $forked == 0 ) { # child
173             sleep $timeout;
174             kill(9, $pid);
175             exit;
176         }
177     }
178
179     # push all provided input to executing child
180     map { print IN "$_\n" } $cmd{'in'};
181     close IN;
182
183     # pop all output from executing child
184     my @got;
185     while(defined(my $got=<OUT>)) {
186         $got =~ s/\r//g;
187         $got =~ s/^( )*//g;
188         chomp $got;
189         push @got, "$got";
190     }   
191     close OUT;
192     
193     # Cleanup the executing child, and kill the timeouter brother on need
194     $cmd{'return'} = 0 unless defined($cmd{'return'});
195     my $wantret = "returned code ".(defined($cmd{'return'})? $cmd{'return'} : 0);
196     waitpid ($pid, 0);
197     my $gotret = exit_status($?);
198     if($gotret ne $wantret) {
199         my $msg = "Test suite `$cmd{'file'}': NOK (<$cmd{'file'}:$cmd{'line'}> $gotret)\n".
200             "Output of <$cmd{'file'}:$cmd{'line'}> so far:\n";      
201         map {$msg .=  "|| $_\n"} @got;
202         print STDERR "$msg";
203         exit(1);
204     }
205     if($timeout){kill(9, $forked);$timeout=0;}
206     $timeout = 0;
207             
208     ###
209     # Check the result of execution 
210     ###
211     my $diff = build_diff(\@{$cmd{'out'}}, \@got);
212     if (length $diff) {
213         print color("red")."[TESH/CRITICAL$$] Output mismatch\n";
214         map { print "[TESH/CRITICAL] $_\n" } split(/\n/,$diff);
215         print color("reset");
216         die "Tesh failed\n";
217     }
218 }
219
220 sub mkfile_cmd {
221     my %cmd = %{$_[0]};
222     my $file = $cmd{'arg'};
223     print "[Tesh/INFO] mkfile $file\n";
224
225     die "[TESH/CRITICAL] no input provided to mkfile\n" unless defined($cmd{'in'}) && scalar @{$cmd{'in'}};
226     unlink($file);
227     open(FILE,">$file") or die "[Tesh/CRITICAL] Unable to create file $file: $!\n";
228     print FILE join("\n", @{$cmd{'in'}});
229     print FILE "\n" if (scalar @{$cmd{'in'}} > 0);
230     close(FILE);
231 }
232
233 # parse tesh file
234 print "Test suite $tesh_file\n";
235 open TESH_FILE, $tesh_file or die "[Tesh/CRITICAL] Unable to open $tesh_file $!\n";
236
237
238 my %cmd; # everything about the next command to run
239 my $line_num=0;
240 LINE: while (defined(my $line=<TESH_FILE>)) {
241     $line_num++;
242     chomp $line;
243     print "[TESH/debug] $line_num: $line\n" if $opts{'debug'};
244
245     # deal with line continuations
246     while ($line =~ /^(.*?)\\$/) {
247         my $next=<TESH_FILE>;
248         die "[TESH/CRITICAL] Continued line at end of file\n"
249             unless defined($next);
250         chomp $next;
251         print "[TESH/debug] $line_num: $next\n" if $opts{'debug'};
252         $line = $1.$next;
253     }
254
255     # Push delayed commands on empty lines
256     unless ($line =~ m/^(..)(.*)$/) {
257         if (defined($cmd{'cmd'})) {
258             exec_cmd(\%cmd);
259             %cmd = ();
260         }
261         next LINE;
262     }   
263  
264     my ($cmd,$arg) = ($1,$2);
265     $arg =~ s/\r//g;
266
267     # handle the commands
268     if ($cmd =~ /^#/) { #comment
269     } elsif ($cmd eq '> '){     #expected result line
270         print "[TESH/debug] push expected result\n" if $opts{'debug'};
271         push @{$cmd{'out'}}, $arg;
272
273     } elsif ($cmd eq '< ') {    # provided input
274         print "[TESH/debug] push provided input\n" if $opts{'debug'};
275         push @{$cmd{'in'}}, $arg;
276
277     } elsif ($cmd eq 'p ') {    # comment
278         print "[Tesh/INFO] $arg\n";
279
280     } elsif ($cmd eq '$ ') {  # Command
281         # if we have something buffered, run it now
282         if (defined($cmd{'cmd'})) {
283             exec_cmd(\%cmd);
284             %cmd = ();
285         }
286         if ($arg =~ /^ *mkfile /){      # "mkfile" command line
287             die "[TESH/CRITICAL] Output expected from mkfile command!\n" if scalar @{cmd{'out'}};
288
289             $cmd{'arg'} = $arg;
290             $cmd{'arg'} =~ s/ *mkfile //;
291             mkfile_cmd(\%cmd);
292             %cmd = ();
293
294         } elsif ($arg =~ /^ *cd /) {
295             die "[TESH/CRITICAL] Input provided to cd command!\n" if scalar @{cmd{'in'}};
296             die "[TESH/CRITICAL] Output expected from cd command!\n" if scalar @{cmd{'out'}};
297
298             $arg =~ s/^ *cd //;
299             cd_cmd("",$arg);
300             %cmd = ();
301
302         } else { # regular command
303             $cmd{'cmd'} = $arg;
304             $cmd{'file'} = $tesh_file;
305             $cmd{'line'} = $line_num;
306         }
307     }
308     elsif($cmd eq '& '){        # parallel command line
309         $cmd{'background'} = 1;
310         $cmd{'cmd'} = $arg;
311     }   
312     elsif($line =~ /^! output sort/){   #output sort
313         $cmd{'sort'} = 1;
314     }
315     elsif($line =~ /^! output ignore/){ #output ignore
316         $cmd{'output ignore'} = 1;
317     }
318     elsif($line =~ /^! expect signal SIGABRT$/) {#expect signal SIGABRT
319         $cmd{'expect'} = "SIGABRT";
320     }
321     elsif($line =~ /^! expect return/){ #expect return
322         $line =~ s/^! expect return //g;
323         $line =~ s/\r//g;
324         $cmd{'return'} = $line;
325     }
326     elsif($line =~ /^! setenv/){        #setenv
327         $line =~ s/^! setenv //g;
328         $line =~ s/\r//g;
329         setenv_cmd($line);
330     }
331     elsif($line =~ /^! include/){       #output sort
332         print color("red"), "[Tesh/CRITICAL] need include";
333         print color("reset"), "\n";
334         die;
335     }
336     elsif($line =~ /^! timeout/){       #timeout
337         $line =~ s/^! timeout //;
338         $line =~ s/\r//g;
339         $cmd{'timeout'} = $line;
340     } else {
341         die "[TESH/CRITICAL] parse error: $line\n";
342     }
343 }
344
345 # Deal with last command
346 if (defined($cmd{'cmd'})) {
347     exec_cmd(\%cmd);
348     %cmd = ();
349 }
350
351 #my (@a,@b);
352 #push @a,"bl1";   push @b,"bl1";
353 #push @a,"bl2";   push @b,"bl2";
354 #push @a,"bl3";   push @b,"bl3";
355 #push @a,"bl4";   push @b,"bl4";
356 #push @a,"bl5";   push @b,"bl5";
357 #push @a,"bl6";   push @b,"bl6";
358 #push @a,"bl7";   push @b,"bl7";
359 ##push @a,"Perl";  push @b,"ruby";
360 #push @a,"END1";   push @b,"END1";
361 #push @a,"END2";   push @b,"END2";
362 #push @a,"END3";   push @b,"END3";
363 #push @a,"END4";   push @b,"END4";
364 #push @a,"END5";   push @b,"END5";
365 #push @a,"END6";   push @b,"END6";
366 #push @a,"END7";   push @b,"END7";
367 #print "Identical:\n". build_diff(\@a,\@b);
368
369 #@a = (); @b =();
370 #push @a,"AZE"; push @b,"EZA";
371 #print "Different:\n".build_diff(\@a,\@b);
372
373 use Diff qw(diff); # postpone a bit to have time to change INC
374
375 sub build_diff {
376     my $res;
377     my $diff = Diff->new(@_);
378     
379     $diff->Base( 1 );   # Return line numbers, not indices
380     my $chunk_count = $diff->Next(-1); # Compute the amount of chuncks
381     return ""     if ($chunk_count == 1 && $diff->Same());
382     $diff->Reset();
383     while(  $diff->Next()  ) {
384         my @same = $diff->Same();
385         if ($diff->Same() ) {
386             if ($diff->Next(0) > 1) { # not first chunk: print 2 first lines
387                 $res .= '  '.$same[0]."\n" ;
388                 $res .= '  '.$same[1]."\n" if (scalar @same>1);
389             }   
390             $res .= "...\n"  if (scalar @same>2);
391 #       $res .= $diff->Next(0)."/$chunk_count\n";
392             if ($diff->Next(0) < $chunk_count) { # not last chunk: print 2 last lines
393                 $res .= '  '.$same[scalar @same -2]."\n" if (scalar @same>1);
394                 $res .= '  '.$same[scalar @same -1]."\n";
395             } 
396         } 
397         next if  $diff->Same();
398         map { $res .= "- $_\n" } $diff->Items(1);
399         map { $res .= "+ $_\n" } $diff->Items(2);
400     }
401     return $res;
402 }
403
404