#!/usr/bin/perl -w # Copyright (c) 2007, 2008 Andreas Gruenbacher. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions, and the following disclaimer, # without modification, immediately at the beginning of the file. # 2. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # Alternatively, this software may be distributed under the terms of the # GNU Public License ("GPL"). # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # Possible improvements: # # - distinguish stdout and stderr output # - resume at a specific line # # # Command line parameters: # # -l N Stop the test after line N # -q Quiet mode: buffer the output while processing and print the # result at the end; on success, only the summary is printed # -v Verbose mode: print the output of successful commands (by # default the output is only printed for failed commands) # use strict; use FileHandle; use Getopt::Std; use POSIX qw(isatty setuid getcwd); use Text::ParseWords; use vars qw($opt_l $opt_q $opt_v %output $OK $FAILED $tests $failed); $opt_l = ~0; # a really huge number getopts('l:qv'); $OK = "ok"; $FAILED = "failed"; if (isatty(fileno(STDOUT))) { $OK = "\033[32m" . $OK . "\033[m"; $FAILED = "\033[31m\033[1m" . $FAILED . "\033[m"; } $tests = $failed = 0; my $prog; my ($in, $out) = ([], []); my $prog_line = 0; my $last_status = 0; my $lineno; my $width = ($ENV{COLUMNS} || 80) >> 1; my $origdir = getcwd; my $workdir = defined $ARGV[0] ? "$ARGV[0].$$" : "d.$$"; my $workdir_absolute; my $keep_workdir = $ENV{KEEP_WORKDIR_IF_FAILED} || 0; sub print_header($) { if ($opt_q) { $output{header} = $_[0]; } else { print $_[0]; } } sub print_body($) { if ($opt_q) { $output{body} .= $_[0]; } else { print $_[0]; } } sub print_footer($) { if ($opt_q) { $output{footer} .= $_[0]; } else { print $_[0]; } } sub flush_output() { return unless $opt_q; print $output{header} || "", $failed ? $output{body} : "", $output{footer} || ""; } # Substitute %{VAR} with environment variables sub substitute_vars($) { my ($line) = @_; $line =~ s[%\{(\w+)\}][defined $ENV{$1} ? $ENV{$1} : ""]eg; return $line; } sub begins_with($$) { return substr($_[0], 0, length($_[1])) eq $_[1]; } sub exec_test($$) { my ($raw_prog, $in) = @_; local (*IN, *IN_DUP, *IN2, *OUT_DUP, *OUT, *OUT2); my $prog = [ shellwords($raw_prog) ]; my $needs_shell; if ($prog->[0] eq "umask") { umask oct $prog->[1]; return 0, []; } elsif ($prog->[0] eq "cd") { if (!chdir $prog->[1]) { return 1, [ "chdir: $prog->[1]: $!\n" ]; } $ENV{PWD} = getcwd; # We must always stay inside the test directory unless (begins_with("$ENV{PWD}/", "$workdir_absolute/")) { return 1, [ "Illegal chdir $prog->[1]\n", "$ENV{PWD} is not inside $workdir_absolute" ], 1; } return 0, []; } elsif ($prog->[0] eq "export") { my ($name, $value) = split /=/, $prog->[1]; $ENV{$name} = $value; return 0, []; } elsif ($prog->[0] eq "unset") { delete $ENV{$prog->[1]}; return 0, []; } $needs_shell = ($raw_prog =~ /[][|&<>;`\$*?]/); pipe *IN2, *OUT or die "Can't create pipe for reading: $!"; open *IN_DUP, "<&STDIN" or *IN_DUP = undef; open *STDIN, "<&IN2" or die "Can't duplicate pipe for reading: $!"; close *IN2; open *OUT_DUP, ">&STDOUT" or die "Can't duplicate STDOUT: $!"; pipe *IN, *OUT2 or die "Can't create pipe for writing: $!"; open *STDOUT, ">&OUT2" or die "Can't duplicate pipe for writing: $!"; close *OUT2; *STDOUT->autoflush(); *OUT->autoflush(); if (fork()) { # Server if (*IN_DUP) { open *STDIN, "<&IN_DUP" or die "Can't duplicate STDIN: $!"; close *IN_DUP or die "Can't close STDIN duplicate: $!"; } open *STDOUT, ">&OUT_DUP" or die "Can't duplicate STDOUT: $!"; close *OUT_DUP or die "Can't close STDOUT duplicate: $!"; foreach my $line (@$in) { #print "> $line"; print OUT $line; } close *OUT or die "Can't close pipe for writing: $!"; my $result = []; while () { #print "< $_"; if ($needs_shell) { s#^/bin/sh: (?:line \d+: )?##; } push @$result, $_; } wait(); return $? >> 8, $result; } else { # Client $< = $>; close IN or die "Can't close read end for input pipe: $!"; close OUT or die "Can't close write end for output pipe: $!"; close OUT_DUP or die "Can't close STDOUT duplicate: $!"; local *ERR_DUP; open ERR_DUP, ">&STDERR" or die "Can't duplicate STDERR: $!"; open STDERR, ">&STDOUT" or die "Can't join STDOUT and STDERR: $!"; if ($needs_shell) { exec ('/bin/sh', '-c', $raw_prog); } else { exec @$prog; } print STDERR $prog->[0], ": $!\n"; exit; } } sub process_test($$$$) { my ($prog, $prog_line, $in, $out) = @_; print_body "[$prog_line] \$ $prog -- "; my ($exec_status, $result, $stop) = exec_test($prog, $in); my @good = (); my $good = 1; my $nmax = (@$out > @$result) ? @$out : @$result; for (my $n = 0; $n < $nmax; $n++) { my $use_re = defined($out->[$n]) && ($out->[$n] =~ s/^~ //); if (!defined($out->[$n]) || !defined($result->[$n]) || (!$use_re && $result->[$n] ne $out->[$n]) || ( $use_re && $result->[$n] !~ /^$out->[$n]/)) { push @good, ($use_re ? '!~' : '!='); $good = 0; } else { push @good, ($use_re ? '=~' : '=='); } } $tests++; $failed++ unless $good; print_body(($good ? $OK : $FAILED)."\n"); if (!$good || $opt_v) { for (my $n = 0; $n < $nmax; $n++) { my $l = defined($out->[$n]) ? $out->[$n] : "~"; chomp $l; my $r = defined($result->[$n]) ? $result->[$n] : "~"; chomp $r; print_body sprintf("%-" . ($width - 3) . "s %s %s\n", $r, $good[$n], $l); } } return $exec_status, $stop; } # Create a dedicated working directory mkdir $workdir or die "Failed to create directory $workdir: $!"; # Clean up on Ctrl+C $SIG{INT} = sub { if (chdir $origdir) { system "rm -rf $workdir" }; exit 1; }; chdir $workdir or die "Failed to change to directory $workdir: $!"; $ENV{PWD} = getcwd; $workdir_absolute = $ENV{PWD}; if (defined $ARGV[0]) { open(SOURCE, "$origdir/$ARGV[0]"); print_header "[$ARGV[0]]\n"; } else { *SOURCE = *STDIN; } while (defined(my $line = )) { $lineno++; # Collect input and output for the previous command if ($line =~ s/^\s*< ?//) { push @$in, substitute_vars($line); next; } if ($line =~ s/^\s*> ?//) { push @$out, substitute_vars($line); next; } # We have all input and output, we can execute the command if (defined $prog) { my $stop; ($last_status, $stop) = process_test($prog, $prog_line, $in, $out); $prog = undef; last if $prog_line >= $opt_l || $stop; } # Parse the next command if ($line =~ s/^\s*\$ ?//) { # Substitute %{?} with the last command's status $line =~ s[%\{\?\}][$last_status]eg; chomp($prog = substitute_vars($line)); $prog_line = $lineno; $in = []; $out = []; } } # Execute last command if needed process_test($prog, $prog_line, $in, $out) if defined $prog; close(SOURCE); # Clean up the mess chdir $origdir or die "Failed to change back to directory $origdir: $!"; system "rm -rf $workdir" unless $keep_workdir and $failed; my $status = sprintf("%d commands (%d passed, %d failed)", $tests, $tests - $failed, $failed); if (isatty(fileno(STDOUT))) { if ($failed) { $status = "\033[31m\033[1m" . $status . "\033[m"; } else { $status = "\033[32m" . $status . "\033[m"; } } print_footer "$status\n"; flush_output; exit $failed ? 1 : 0;