+\f
+# Do final grade.
+# Combines grade.txt, tests.out, review.txt, and details.out,
+# producing grade.out.
+sub assemble_final_grade {
+ open (OUT, ">grade.out") or die "grade.out: create: $!\n";
+
+ open (GRADE, "<grade.txt") or die "grade.txt: open: $!\n";
+ while (<GRADE>) {
+ last if /^\s*$/;
+ print OUT;
+ }
+ close (GRADE);
+
+ my (@tests) = snarf ("tests.out");
+ my ($p_got, $p_pos) = $tests[0] =~ m%\((\d+)/(\d+)\)% or die;
+
+ my (@review) = snarf ("review.txt");
+ my ($part_lost) = (0, 0);
+ for (my ($i) = $#review; $i >= 0; $i--) {
+ local ($_) = $review[$i];
+ if (my ($loss) = /^\s*([-+]\d+)/) {
+ $part_lost += $loss;
+ } elsif (my ($out_of) = m%\[\[/(\d+)\]\]%) {
+ my ($got) = $out_of + $part_lost;
+ $got = 0 if $got < 0;
+ $review[$i] =~ s%\[\[/\d+\]\]%($got/$out_of)% or die;
+ $part_lost = 0;
+
+ $p_got += $got;
+ $p_pos += $out_of;
+ }
+ }
+ die "Lost points outside a section\n" if $part_lost;
+
+ for (my ($i) = 1; $i <= $#review; $i++) {
+ if ($review[$i] =~ /^-{3,}\s*$/ && $review[$i - 1] !~ /^\s*$/) {
+ $review[$i] = '-' x (length ($review[$i - 1]));
+ }
+ }
+
+ print OUT "\nOVERALL SCORE\n";
+ print OUT "-------------\n";
+ print OUT "$p_got points out of $p_pos total\n\n";
+
+ print OUT map ("$_\n", @tests), "\n";
+ print OUT map ("$_\n", @review), "\n";
+
+ print OUT "DETAILS\n";
+ print OUT "-------\n\n";
+ print OUT map ("$_\n", snarf ("details.out"));
+}
+\f
+# Clean up our generated files.
+sub clean_dir {
+ # Verify that we're roughly in the correct directory
+ # before we go blasting away files.
+ choose_tarball ();
+
+ # Blow away everything.
+ xsystem ("rm -rf output pintos", VERBOSE => 1);
+ xsystem ("rm -f details.out tests.out", VERBOSE => 1);
+}
+\f
+# Provided a test's output as an array, verifies that it, in general,
+# looks sensible; that is, that there are no PANIC or FAIL messages,
+# that Pintos started up and shut down normally, and so on.
+# Die if something odd found.
+sub verify_common {
+ my (@output) = @_;
+
+ die "No output at all\n" if @output == 0;
+
+ look_for_panic (@output);
+ look_for_fail (@output);
+ look_for_triple_fault (@output);
+
+ 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 look_for_panic {
+ my (@output) = @_;
+
+ my ($panic) = grep (/PANIC/, @output);
+ return unless defined $panic;
+
+ my ($details) = "Kernel panic:\n $panic\n";
+
+ my (@stack_line) = grep (/Call stack:/, @output);
+ if (@stack_line != 0) {
+ $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";
+ }
+ my ($kernel_o);
+ if ($hw eq 'threads') {
+ $kernel_o = "output/$test/kernel.o";
+ } else {
+ $kernel_o = "pintos/src/$hw/build/kernel.o";
+ }
+ open (A2L, "$A2L -fe $kernel_o @addrs|");
+ for (;;) {
+ my ($function, $line);
+ last unless defined ($function = <A2L>);
+ $line = <A2L>;
+ chomp $function;
+ chomp $line;
+ $details .= " $function ($line)\n";
+ }
+ }
+
+ if ($panic =~ /sec_no < d->capacity/) {
+ $details .= <<EOF;
+\nThis assertion commonly fails when accessing a file via an inode that
+has been closed and freed. Freeing an inode clears all its sector
+indexes to 0xcccccccc, which is not a valid sector number for disks
+smaller than about 1.6 TB.
+EOF
+ }
+
+ $extra{$test} = $details;
+ die "Kernel panic. Details at end of file.\n";
+}
+
+sub look_for_fail {
+ my (@output) = @_;
+
+ my ($failure) = grep (/FAIL/, @output);
+ return unless defined $failure;
+
+ # Eliminate uninteresting header and trailer info if possible.
+ # The `eval' catches the `die' from get_core_output() in the "not
+ # possible" case.
+ eval {
+ my (@core) = get_core_output (@output);
+ $details{$test} = "Program output:\n\n" . join ('', map ("$_\n", @core));
+ };
+
+ # Most output lines are prefixed by (test-name). Eliminate this
+ # from our `die' message for brevity.
+ $failure =~ s/^\([^\)]+\)\s+//;
+ die "$failure. Details at end of file.\n";
+}
+
+sub look_for_triple_fault {
+ my (@output) = @_;
+
+ return unless grep (/Pintos booting/, @output) > 1;
+
+ my ($details) = <<EOF;
+Pintos spontaneously rebooted during this test. This is most often
+due to unhandled page faults. Output from initial boot through the
+first reboot is shown below:
+
+EOF
+
+ my ($i) = 0;
+ local ($_);
+ for (@output) {
+ $details .= " $_\n";
+ last if /Pintos booting/ && ++$i > 1;
+ }
+ $details{$test} = $details;
+ die "Triple-fault caused spontaneous reboot(s). "
+ . "Details at end of file.\n";
+}
+
+# Get @output without header or trailer.
+# Die if not possible.
+sub get_core_output {
+ my (@output) = @_;
+
+ my ($first);
+ for ($first = 0; $first <= $#output; $first++) {
+ my ($line) = $output[$first];
+ $first++, last
+ if ($hw ne 'threads' && $line =~ /^Executing '$test.*':$/)
+ || ($hw eq 'threads'
+ && grep (/^Boot complete.$/, @output[0...$first - 1])
+ && $line =~ /^\s*$/);
+ }
+
+ my ($last);
+ for ($last = $#output; $last >= 0; $last--) {
+ $last--, last if $output[$last] =~ /^Timer: \d+ ticks$/;
+ }
+
+ if ($last < $first) {
+ my ($no_first) = $first > $#output;
+ my ($no_last) = $last < $#output;
+ die "Couldn't locate output.\n";
+ }
+
+ return @output[$first ... $last];
+}
+
+sub canonicalize_exit_codes {
+ my (@output) = @_;
+
+ # Exit codes are supposed to be printed in the form "process: exit(code)"
+ # but people get unfortunately creative with it.
+ for my $i (0...$#output) {
+ local ($_) = $output[$i];
+
+ my ($process, $code);
+ if ((($process, $code) = /^([-a-z0-9 ]+):.*[ \(](-?\d+)\b\)?$/)
+ || (($process, $code) = /^([-a-z0-9 ]+) exit\((-?\d+)\)$/)
+ || (($process, $code)
+ = /^([-a-z0-9 ]+) \(.*\): exit\((-?\d+)\)$/)
+ || (($process, $code) = /^([-a-z0-9 ]+):\( (-?\d+) \) $/)
+ || (($code, $process) = /^shell: exit\((-?\d+)\) \| ([-a-z0-9]+)/))
+ {
+ # We additionally truncate to 15 character and strip all
+ # but the first word.
+ $process = substr ($process, 0, 15);
+ $process =~ s/\s.*//;
+ $output[$i] = "$process: exit($code)\n";
+ }
+ }