#! /usr/bin/perl use warnings; use strict; use POSIX; use Algorithm::Diff; use Getopt::Long; our ($VERBOSE) = 0; # Verbosity of output our (@TESTS); # Tests to run. my ($clean) = 0; GetOptions ("v|verbose+" => \$VERBOSE, "h|help" => sub { usage (0) }, "t|test=s" => \@TESTS, "c|clean" => \$clean) or die "Malformed command line; use --help for help.\n"; die "Non-option argument not supported; use --help for help.\n" if @ARGV > 0; sub usage { my ($exitcode) = @_; print "run-tests, for grading Pintos thread projects.\n\n"; print "Invoke from a directory containing a student tarball named by\n"; print "the submit script, e.g. username.Oct.12.04.20.04.09.tar.gz.\n"; print "In normal usage, no options are needed.\n\n"; print "Output is produced in tests.out and details.out.\n\n"; print "Options:\n"; print " -c, --clean Remove old output files before starting\n"; print " -t, --test=TEST Execute TEST only (allowed multiple times)\n"; print " -v, --verbose Print commands before executing them\n"; print " -h, --help Print this help message\n"; exit $exitcode; } # Default set of tests. @TESTS = ("alarm-single", "alarm-multiple", "alarm-zero", "alarm-negative", "join-simple", "join-quick", "join-multiple", "join-nested", "join-dummy", "join-invalid", "join-no", "priority-preempt", "priority-fifo", "priority-donate-one", "priority-donate-multiple", "priority-donate-nest", "mlfqs-on", "mlfqs-off") unless @TESTS > 0; # Find the directory that contains the grading files. our ($GRADES_DIR); ($GRADES_DIR = $0) =~ s#/[^/]+$##; -d $GRADES_DIR or die "$GRADES_DIR: stat: $!\n"; if ($clean) { # Verify that we're roughly in the correct directory # before we go blasting away files. choose_tarball (); xsystem ("rm -rf output pintos", VERBOSE => 1); xsystem ("rm -f details.out tests.out", VERBOSE => 1); } # Create output directory, if it doesn't already exist. -d ("output") || mkdir ("output") or die "output: mkdir: $!\n"; # Extract submission. extract_tarball () if ! -d "pintos"; # Verify that the proper directory was submitted. -d "pintos/src/threads" or die "pintos/src/threads: stat: $!\n"; # Run and grade the tests. our $test; our %result; our %details; our %extra; for $test (@TESTS) { my ($result); do { print "$test: "; $result = run_test ($test); if ($result eq 'ok') { $result = grade_test ($test); $result =~ s/\n$//; } print "$result\n"; } while ($result eq 'rerun'); $result{$test} = $result; } # Write output. write_grades (); write_details (); sub choose_tarball { my (@tarballs) = grep (/^[a-z0-9]+\.[A-Za-z]+\.\d+\.\d+\.\d+\.\d+.\d+\.tar\.gz$/, glob ("*.tar.gz")); die "no pintos dir and no source tarball\n" if scalar (@tarballs) == 0; # Sort tarballs in reverse order by time. @tarballs = sort { ext_mdyHMS ($b) cmp ext_mdyHMS ($a) } @tarballs; print "Multiple tarballs: choosing $tarballs[0]\n" if scalar (@tarballs) > 1; return $tarballs[0]; } sub extract_tarball { my ($tarball) = choose_tarball (); mkdir "pintos" or die "pintos: mkdir: $!\n"; mkdir "pintos/src" or die "pintos: mkdir: $!\n"; print "Extracting $tarball...\n"; xsystem ("cd pintos/src && tar xzf ../../$tarball", DIE => "extraction failed\n"); print "Patching...\n"; xsystem ("patch -fs pintos/src/lib/debug.c < $GRADES_DIR/panic.diff", LOG => "patch", DIE => "patch failed\n"); } sub ext_mdyHMS { my ($s) = @_; my ($ms, $d, $y, $H, $M, $S) = $s =~ /.([A-Za-z]+)\.(\d+)\.(\d+)\.(\d+)\.(\d+).(\d+)\.tar\.gz$/ or die; my ($m) = index ("janfebmaraprmayjunjulaugsepoctnovdec", lc $ms) / 3 or die; return sprintf "%02d-%02d-%02d %02d:%02d:%02d", $y, $m, $d, $H, $M, $S; } sub test_source { my ($test) = @_; my ($src) = "$GRADES_DIR/$test.c"; $src = "$GRADES_DIR/mlfqs.c" if $test =~ /^mlfqs/; -e $src or die "$src: stat: $!\n"; return $src; } sub test_constants { my ($defines) = ""; $defines .= "#define MLFQS 1\n" if $test eq 'mlfqs-on'; return $defines; } sub run_test { my ($test) = @_; return "ok" if -f "output/$test.run.out"; my ($defines) = test_constants ($test); if ($defines ne snarf ("pintos/src/constants.h")) { open (CONSTANTS, ">pintos/src/constants.h"); print CONSTANTS $defines; close (CONSTANTS); } my ($src) = test_source ($test); xsystem ("cp $src pintos/src/threads/test.c", DIE => "cp failed\n"); unlink ("pintos/src/threads/build/threads/test.o"); unlink ("pintos/src/threads/build/kernel.o"); unlink ("pintos/src/threads/build/kernel.bin"); unlink ("pintos/src/threads/build/os.dsk"); xsystem ("cd pintos/src/threads && make", LOG => "$test.make") or return "compile error"; my ($timeout) = 10; $timeout = 600 if $test =~ /^mlfqs/; xsystem ("cd pintos/src/threads/build && pintos -v run -q", LOG => "$test.run", TIMEOUT => $timeout) or return "Bochs error"; return "ok"; } sub grade_test { my ($test) = @_; my (@output) = snarf ("output/$test.run.out"); if (-e "$GRADES_DIR/$test.exp") { eval { verify_common (@output); compare_output ("$GRADES_DIR/$test.exp", @output); } } else { my ($grade_func); ($grade_func = $test) =~ s/-/_/g; eval "grade_$grade_func (\@output)"; } if ($@) { die $@ if $@ =~ /at \S+ line \d+$/; return $@; } return "ok"; } sub grade_alarm_single { verify_alarm (1, @_); } sub grade_alarm_multiple { verify_alarm (7, @_); } sub verify_alarm { my ($iterations, @output) = @_; verify_common (@output); my (@products); for (my ($i) = 0; $i < $iterations; $i++) { for (my ($t) = 0; $t < 5; $t++) { push (@products, ($i + 1) * ($t + 1) * 10); } } @products = sort {$a <=> $b} @products; local ($_); foreach (@output) { die $_ if /Out of order/; my ($p) = /product=(\d+)$/; next if !defined $p; my ($q) = shift (@products); die "Too many wakeups.\n" if !defined $q; die "Out of order wakeups ($p vs. $q).\n" if $p != $q; # FIXME } die scalar (@products) . " fewer wakeups than expected.\n" if @products != 0; } sub grade_alarm_zero { my (@output) = @_; verify_common (@output); die "Crashed in timer_sleep()\n" if !grep (/^Success\.$/, @output); } sub grade_alarm_negative { my (@output) = @_; verify_common (@output); die "Crashed in timer_sleep()\n" if !grep (/^Success\.$/, @output); } sub grade_join_invalid { my (@output) = @_; verify_common (@output); grep (/Testing invalid join/, @output) or die "Test didn't start\n"; grep (/Invalid join test done/, @output) or die "Test didn't complete\n"; } sub grade_join_no { my (@output) = @_; verify_common (@output); grep (/Testing no join/, @output) or die "Test didn't start\n"; grep (/No join test done/, @output) or die "Test didn't complete\n"; } sub grade_join_multiple { my (@output) = @_; verify_common (@output); my (@t); $t[4] = $t[5] = $t[6] = -1; local ($_); foreach (@output) { my ($idx) = /^Thread (\d+)/ or next; my ($iter) = /iteration (\d+)$/; $iter = 5 if /done!$/; die "Malformed output\n" if !defined $iter; if ($idx == 6) { die "Thread 6 started before either other thread finished\n" if $t[4] < 5 && $t[5] < 5; die "Thread 6 started before thread 4 finished\n" if $t[4] < 5; die "Thread 6 started before thread 5 finished\n" if $t[5] < 5; } die "Thread $idx out of order output\n" if $t[$idx] != $iter - 1; $t[$idx] = $iter; } my ($err) = ""; for my $idx (4, 5, 6) { if ($t[$idx] == -1) { $err .= "Thread $idx did not run at all\n"; } elsif ($t[$idx] != 5) { $err .= "Thread $idx only completed $t[$idx] iterations\n"; } } die $err if $err ne ''; } sub grade_priority_fifo { my (@output) = @_; verify_common (@output); my ($thread_cnt) = 10; my ($iter_cnt) = 5; my (@order); my (@t) = (-1) x $thread_cnt; local ($_); foreach (@output) { my ($idx) = /^Thread (\d+)/ or next; my ($iter) = /iteration (\d+)$/; $iter = $iter_cnt if /done!$/; die "Malformed output\n" if !defined $iter; if (@order < $thread_cnt) { push (@order, $idx); die "Thread $idx repeated within first $thread_cnt iterations: " . join (' ', @order) . ".\n" if grep ($_ == $idx, @order) != 1; } else { die "Thread $idx ran when $order[0] should have.\n" if $idx != $order[0]; push (@order, shift @order); } die "Thread $idx out of order output.\n" if $t[$idx] != $iter - 1; $t[$idx] = $iter; } my ($err) = ""; for my $idx (0..$#t) { if ($t[$idx] == -1) { $err .= "Thread $idx did not run at all.\n"; } elsif ($t[$idx] != $iter_cnt) { $err .= "Thread $idx only completed $t[$idx] iterations.\n"; } } die $err if $err ne ''; } sub grade_mlfqs_on { my (@output) = @_; verify_common (@output); mlfqs_stats (@output); } sub grade_mlfqs_off { my (@output) = @_; verify_common (@output); mlfqs_stats (@output); } sub mlfqs_stats { my (@output) = @_; my (%stats) = ("io" => {}, "cpu" => {}, "mix" => {}); my (%map) = ("CPU intensive" => "cpu", "IO intensive" => "io", "Alternating IO/CPU" => "mix"); local ($_); foreach (@output) { my ($thread, $pri) = /^([A-Za-z\/ ]+): (\d+)$/ or next; my ($t) = $map{$thread} or next; my ($s) = $stats{$t}; $$s{"n"}++; $$s{"sum"} += $pri; $$s{"sum2"} += $pri * $pri; $$s{"min"} = $pri if !defined ($$s{"min"}) || $pri < $$s{"min"}; $$s{"max"} = $pri if !defined ($$s{"max"}) || $pri > $$s{"max"}; } my ($details) = ""; for my $t (keys %stats) { my ($s) = $stats{$t}; $details .= "$t: n=$$s{'n'}, min=$$s{'min'}, max=$$s{'max'}, avg=" . int ($$s{'sum'} / $$s{'n'}) . "\n"; } $details{$test} = $details; die "MLFQS\n"; } sub verify_common { my (@output) = @_; my (@assertion) = grep (/PANIC/, @output); if (@assertion != 0) { my ($details) = "Kernel panic:\n $assertion[0]\n"; my (@stack_line) = grep (/Call stack:/, @output); if (@stack_line != 0) { get_binaries (); $details .= " $stack_line[0]\n\n"; $details .= "Translation of backtrace:\n"; my ($addrs) = $stack_line[0] =~ /Call stack:((?: 0x[0-9a-f]+)+)/; my ($A2L); if (`uname -m` =~ /i.86|pentium.*|[pk][56]|nexgen|viac3|6x86|athlon.*/) { $A2L = "addr2line"; } else { $A2L = "i386-elf-addr2line"; } open (A2L, "$A2L -fe pintos/src/threads/build/kernel.o $addrs|"); while (my $function = ) { my ($line) = ; chomp $function; chomp $line; $details .= " $function ($line)\n"; } } $extra{$test} = $details; die "Kernel panic. Details at end of file.\n" } die "No output at all\n" if @output == 0; die "Didn't start up properly: no \"Pintos booting\" startup message\n" if !grep (/Pintos booting with.*kB RAM\.\.\./, @output); die "Didn't start up properly: no \"Boot complete\" startup message\n" if !grep (/Boot complete/, @output); die "Didn't shut down properly: no \"Timer: # ticks\" shutdown message\n" if !grep (/Timer: \d+ ticks/, @output); die "Didn't shut down properly: no \"Powering off\" shutdown message\n" if !grep (/Powering off/, @output); } sub get_binaries { if (!files_equal ("pintos/src/threads/test.c", test_source ($test)) || !file_contains ("pintos/src/constants.h", test_constants ($test))) { unlink ("output/$test.run.out") or die "output/$test.run.out: unlink: $!\n"; die "rerun\n"; } } sub compare_output { my ($exp_file, @actual) = @_; my (@expected) = snarf ($exp_file); @actual = map ("$_\n", @actual); @expected = map ("$_\n", @expected); # Trim header and trailer from @actual. while (scalar (@actual) && $actual[0] ne $expected[0]) { shift (@actual); } die "First line of expected output was not present.\n" if !@actual; while (scalar (@actual) && $actual[$#actual] ne $expected[$#expected]) { pop (@actual); } die "Final line of expected output was not present.\n" if !@actual; # Check whether they're the same. if ($#actual == $#expected) { my ($eq) = 1; for (my ($i) = 0; $i <= $#expected; $i++) { $eq = 0 if $actual[$i] ne $expected[$i]; } return if $eq; } # They differ. Output a diff. my ($diff) = ""; my ($d) = Algorithm::Diff->new (\@expected, \@actual); $d->Base (1); while ($d->Next ()) { my ($ef, $el, $af, $al) = $d->Get (qw (min1 max1 min2 max2)); if ($d->Same ()) { if ($af != $al) { $diff .= "Actual lines $af...$al match expected lines " . "$ef...$el.\n"; } else { $diff .= "Actual line $af matches expected line $ef.\n"; } } else { my (@i1) = $d->Items (1); my (@i2) = $d->Items (2); if (!@i1) { $diff .= "Extra or misplaced line(s) $af...$al " . "in actual output:\n"; $diff .= number_lines ($af, \@i2); } elsif (!$d->Items (2)) { $diff .= "Expected line(s) $ef...$el missing or misplaced:\n"; $diff .= number_lines ($ef, \@i1); } else { $diff .= "The following expected line(s) $ef...$el:\n"; $diff .= number_lines ($ef, \@i1); $diff .= "became actual line(s) $af...$al:\n"; $diff .= number_lines ($af, \@i2); } } } my ($details) = ""; $details .= "$test actual output (line numbers added):\n"; $details .= number_lines (1, \@actual); $details .= "\n$test expected output (line numbers added):\n"; $details .= number_lines (1, \@expected); $details .= "\n$diff\n"; $details{$test} = $details; die "Output differs from expected. Details at end of file.\n"; } sub write_grades { my (@summary) = snarf ("$GRADES_DIR/tests.txt"); my ($ploss) = 0; my ($tloss) = 0; my ($total) = 0; for (my ($i) = 0; $i <= $#summary; $i++) { local ($_) = $summary[$i]; if (my ($loss, $test) = /^ -(\d+) ([-a-zA-Z0-9]+):/) { my ($result) = $result{$test} || "Not tested."; if ($result eq 'ok') { splice (@summary, $i, 1); $i--; } else { $ploss += $loss; $tloss += $loss; splice (@summary, $i + 1, 0, map (" $_", split ("\n", $result))); } } elsif (my ($ptotal) = /^Score: \/(\d+)$/) { $total += $ptotal; $summary[$i] = "Score: " . ($ptotal - $ploss) . "/$ptotal"; splice (@summary, $i, 0, " All tests passed.") if $ploss == 0; $ploss = 0; $i++; } } my ($ts) = "(" . ($total - $tloss) . "/" . $total . ")"; $summary[0] =~ s/\[\[total\]\]/$ts/; open (SUMMARY, ">tests.out"); print SUMMARY map ("$_\n", @summary); close (SUMMARY); } sub write_details { open (DETAILS, ">details.out"); my ($n) = 0; for my $test (@TESTS) { next if $result{$test} eq 'ok'; my ($details) = $details{$test}; next if !defined ($details) && ! -e "output/$test.run.out"; print DETAILS "\n" if $n++; print DETAILS "--- $test details ", '-' x (50 - length ($test)); print DETAILS "\n\n"; if (!defined $details) { $details = "Output:\n\n" . snarf ("output/$test.run.out"); } print DETAILS $details; print DETAILS "\n", "-" x 10, "\n\n$extra{$test}" if defined $extra{$test}; } close (DETAILS); } sub xsystem { my ($command, %options) = @_; print "$command\n" if $VERBOSE || $options{VERBOSE}; my ($log) = $options{LOG}; if (defined ($log)) { $command = "($command) >output/$log.out 2>output/$log.err"; } my ($pid, $status); eval { local $SIG{ALRM} = sub { die "alarm\n" }; alarm $options{TIMEOUT} if defined $options{TIMEOUT}; $pid = fork; die "fork: $!\n" if !defined $pid; exec ($command), die "$command: exec: $!\n" if !$pid; waitpid ($pid, 0); $status = $?; alarm 0; }; if ($@) { die unless $@ eq "alarm\n"; # propagate unexpected errors print "Timed out.\n"; kill SIGTERM, $pid; $status = 0; } if (WIFSIGNALED ($status)) { my ($signal) = WTERMSIG ($status); die "Interrupted\n" if $signal == SIGINT; print "Child terminated with signal $signal\n"; } unlink ("output/$log.err") if defined ($log) && $status == 0; return $status == 0; } sub snarf { my ($file) = @_; open (OUTPUT, $file) or die "$file: open: $!\n"; my (@lines) = ; chomp (@lines); close (OUTPUT); return wantarray ? @lines : join ('', map ("$_\n", @lines)); } sub files_equal { my ($a, $b) = @_; my ($equal); open (A, "<$a") or die "$a: open: $!\n"; open (B, "<$b") or die "$b: open: $!\n"; if (-s A != -s B) { $equal = 0; } else { my ($sa, $sb); for (;;) { sysread (A, $sa, 1024); sysread (B, $sb, 1024); $equal = 0, last if $sa ne $sb; $equal = 1, last if $sa eq ''; } } close (A); close (B); return $equal; } sub file_contains { my ($file, $expected) = @_; open (FILE, "<$file") or die "$file: open: $!\n"; my ($actual); sysread (FILE, $actual, -s FILE); my ($equal) = $actual eq $expected; close (FILE); return $equal; } sub number_lines { my ($ln, $lines) = @_; my ($out); for my $line (@$lines) { chomp $line; $out .= sprintf "%4d %s\n", $ln++, $line; } return $out; }