Work on grading.
[pintos-anon] / grading / threads / run-tests
1 #! /usr/bin/perl -w
2
3 use POSIX;
4 use Text::Diff;
5
6 $verbose = 0;
7 ($GRADES_DIR = $0) =~ s#/[^/]+$##;
8 -d $GRADES_DIR or die "$GRADES_DIR: stat: $!\n";
9
10 -d ("output") || mkdir ("output") or die "output: mkdir: $!\n";
11
12 if (! -d "pintos") {
13     my (@tarballs)
14         = grep (/^[a-z0-9]+\.[A-Za-z]+\.\d+\.\d+\.\d+\.\d+.\d+\.tar\.gz$/,
15                 glob ("*.tar.gz"));
16     die "no pintos dir and no source tarball\n" if scalar (@tarballs) == 0;
17
18     # Sort tarballs in reverse order by time.
19     @tarballs = sort { ext_mdyHMS ($b) cmp ext_mdyHMS ($a) } @tarballs;
20         
21     die "no pintos dir and multiple tarballs\n" if scalar (@tarballs) > 1;
22     mkdir "pintos" or die "pintos: mkdir: $!\n";
23     mkdir "pintos/src" or die "pintos: mkdir: $!\n";
24
25     print "Extracting $tarballs[0]...\n";
26     xsystem ("", "cd pintos/src && tar xzf ../../$tarballs[0]")
27         or die "extraction failed\n";
28
29     print "Patching...\n";
30     xsystem ("panic.diff",
31              "patch -fs pintos/src/lib/debug.c < $GRADES_DIR/panic.diff")
32         or die "patch failed\n";
33 }
34 -d "pintos/src/threads" or die "pintos/src/threads: stat: $!\n";
35
36 sub ext_mdyHMS {
37     my ($s) = $_;
38     my ($ms, $d, $y, $H, $M, $S) =
39         $s =~ /.([A-Za-z]+)\.(\d+)\.(\d+)\.(\d+)\.(\d+).(\d+)\.tar\.gz$/
40         or die;
41     my ($m) = index ("janfebmaraprmayjunjulaugsepoctnovdec", lc $ms) / 3
42         or die;
43     return sprintf "%02d-%02d-%02d %02d:%02d:%02d", $m, $d, $y, $H, $M, $S;
44 }
45
46 @TESTS = ("alarm-single", "alarm-multiple", "alarm-zero", "alarm-negative",
47           "join-simple",
48           "join-quick", "join-multiple", "join-nested",
49           "join-dummy", "join-invalid", "join-no",
50           "priority-preempt", "priority-fifo", "priority-donate-one",
51           "priority-donate-multiple", "priority-donate-nest",
52           "mlfqs-on", "mlfqs-off");
53
54 for $test (@TESTS) {
55     my ($result);
56     do {
57         print "$test: ";
58         $result = run_test ($test);
59         if ($result eq 'ok') {
60             $result = grade_test ($test);
61             $result =~ s/\n$//;
62         }
63         print "$result\n";
64     } while ($result eq 'rerun');
65     
66     $result{$test} = $result;
67 }
68
69 make_summary ();
70
71 sub grade_test {
72     my ($test) = @_;
73
74     my (@output) = snarf ("output/$test.run.out");
75
76     if (-e "$GRADES_DIR/$test.exp") {
77         eval {
78             verify_common (@output);
79             compare_output ("$GRADES_DIR/$test.exp", @output);
80         }
81     } else {    
82         ($grade_func = $test) =~ s/-/_/g;
83         eval "grade_$grade_func(\@output)";
84     }
85     if ($@) {
86         die $@ if $@ =~ /at \S+ line \d+$/;
87         return $@;
88     }
89     return "ok";
90 }
91
92 sub grade_alarm_single {
93     verify_alarm (1, @_);
94 }
95
96 sub grade_alarm_multiple {
97     verify_alarm (7, @_);
98 }
99
100 sub grade_alarm_zero {
101     my (@output) = @_;
102     verify_common (@output);
103     die "Crashed in timer_sleep()\n" if !grep (/^Success\.$/, @output);
104 }
105
106 sub grade_alarm_negative {
107     my (@output) = @_;
108     verify_common (@output);
109     die "Crashed in timer_sleep()\n" if !grep (/^Success\.$/, @output);
110 }
111
112 sub grade_join_invalid {
113     my (@output) = @_;
114     verify_common (@output);
115     grep (/Testing invalid join/, @output) or die "Test didn't start\n";
116     grep (/Invalid join test done/, @output) or die "Test didn't complete\n";
117 }
118
119 sub grade_join_no {
120     my (@output) = @_;
121     verify_common (@output);
122     grep (/Testing no join/, @output) or die "Test didn't start\n";
123     grep (/No join test done/, @output) or die "Test didn't complete\n";
124 }
125
126 sub grade_join_multiple {
127     my (@output) = @_;
128
129     verify_common (@output);
130     my (@t);
131     $t[4] = $t[5] = $t[6] = -1;
132     for $_ (@output) {
133         my ($idx) = /^Thread (\d+)/ or next;
134         my ($iter) = /iteration (\d+)$/;
135         $iter = 5 if /done!$/;
136         die "Malformed output\n" if !defined $iter;
137         if ($idx == 6) {
138             die "Thread 6 started before either other thread finished\n"
139                 if $t[4] < 5 && $t[5] < 5;
140             die "Thread 6 started before thread 4 finished\n"
141                 if $t[4] < 5;
142             die "Thread 6 started before thread 5 finished\n"
143                 if $t[5] < 5;
144         }
145         die "Thread $idx out of order output\n" if $t[$idx] != $iter - 1;
146         $t[$idx] = $iter;
147     }
148
149     my ($err) = "";
150     for my $idx (4, 5, 6) {
151         if ($t[$idx] == -1) {
152             $err .= "Thread $idx did not run at all\n";
153         } elsif ($t[$idx] != 5) {
154             $err .= "Thread $idx only completed $t[$idx] iterations\n";
155         }
156     }
157     die $err if $err ne '';
158 }
159
160 sub grade_priority_fifo {
161     my (@output) = @_;
162
163     verify_common (@output);
164     my ($thread_cnt) = 10;
165     my ($iter_cnt) = 5;
166     my (@order);
167     my (@t) = (-1) x $thread_cnt;
168     for $_ (@output) {
169         my ($idx) = /^Thread (\d+)/ or next;
170         my ($iter) = /iteration (\d+)$/;
171         $iter = $iter_cnt if /done!$/;
172         die "Malformed output\n" if !defined $iter;
173         if (@order < $thread_cnt) {
174             push (@order, $idx);
175             die "Thread $idx repeated within first $thread_cnt iterations: "
176                 . join (' ', @order) . ".\n"
177                 if grep ($_ == $idx, @order) != 1;
178         } else {
179             die "Thread $idx ran when $order[0] should have.\n"
180                 if $idx != $order[0];
181             push (@order, shift @order);
182         }
183         die "Thread $idx out of order output.\n" if $t[$idx] != $iter - 1;
184         $t[$idx] = $iter;
185     }
186
187     my ($err) = "";
188     for my $idx (0..$#t) {
189         if ($t[$idx] == -1) {
190             $err .= "Thread $idx did not run at all.\n";
191         } elsif ($t[$idx] != $iter_cnt) {
192             $err .= "Thread $idx only completed $t[$idx] iterations.\n";
193         }
194     }
195     die $err if $err ne '';
196 }
197
198 sub grade_mlfqs_on {
199     my (@output) = @_;
200     verify_common (@output);
201     mlfqs_stats (@output);
202 }
203
204 sub grade_mlfqs_off {
205     my (@output) = @_;
206     verify_common (@output);
207     mlfqs_stats (@output);
208 }
209
210 sub mlfqs_stats {
211     my (@output) = @_;
212     my (%stats) = ("io" => {}, "cpu" => {}, "mix" => {});
213     my (%map) = ("CPU intensive" => "cpu",
214                  "IO intensive" => "io",
215                  "Alternating IO/CPU" => "mix");
216     for $_ (@output) {
217         my ($thread, $pri) = /^([A-Za-z\/ ]+): (\d+)$/ or next;
218         my ($t) = $map{$thread} or next;
219         
220         my ($s) = $stats{$t};
221         $$s{"n"}++;
222         $$s{"sum"} += $pri;
223         $$s{"sum2"} += $pri * $pri;
224         $$s{"min"} = $pri if !defined ($$s{"min"}) || $pri < $$s{"min"};
225         $$s{"max"} = $pri if !defined ($$s{"max"}) || $pri > $$s{"max"};
226     }
227     for my $t (keys %stats) {
228         my ($s) = $stats{$t};
229         print "$t: n=$$s{'n'}, min=$$s{'min'}, max=$$s{'max'}, avg=", int ($$s{'sum'} / $$s{'n'}), "\n";
230     }
231 }
232
233 sub get_binaries {
234     if (!files_equal ("pintos/src/threads/test.c", test_source ($test))
235         || !file_contains ("pintos/src/constants.h",
236                            test_constants ($test))) {
237         unlink ("output/$test.run.out")
238             or die "output/$test.run.out: unlink: $!\n";
239         die "rerun\n";
240     }
241 }
242
243 sub verify_common {
244     my (@output) = @_;
245
246     my (@assertion) = grep (/PANIC/, @output);
247     if (@assertion != 0) {
248         my ($details) = "Kernel panic:\n  $assertion[0]\n";
249
250         my (@stack_line) = grep (/Call stack:/, @output);
251         if (@stack_line != 0) {
252             get_binaries ();
253             $details .= "  $stack_line[0]\n\n";
254             $details .= "Translation of backtrace:\n";
255             my ($addrs) = $stack_line[0] =~ /Call stack:((?: 0x[0-9a-f]+)+)/;
256
257             my ($A2L);
258             if (`uname -m`
259                 =~ /i.86|pentium.*|[pk][56]|nexgen|viac3|6x86|athlon.*/) {
260                 $A2L = "addr2line";
261             } else {
262                 $A2L = "i386-elf-addr2line";
263             }
264             open (A2L, "$A2L -fe pintos/src/threads/build/kernel.o $addrs|");
265             while ($function = <A2L>) {
266                 $line = <A2L>;
267                 chomp $function;
268                 chomp $line;
269                 $details .= "  $function ($line)\n";
270             }
271         }
272         $extra{$test} = $details;
273         die "Kernel panic.  Details at end of file.\n"
274     }
275
276     die "No output at all\n" if @output == 0;
277     die "Didn't start up properly: no \"Pintos booting\" startup message\n"
278         if !grep (/Pintos booting with.*kB RAM\.\.\./, @output);
279     die "Didn't start up properly: no \"Boot complete\" startup message\n"
280         if !grep (/Boot complete/, @output);
281     die "Didn't shut down properly: no \"Timer: # ticks\" shutdown message\n"
282         if !grep (/Timer: \d+ ticks/, @output);
283     die "Didn't shut down properly: no \"Powering off\" shutdown message\n"
284         if !grep (/Powering off/, @output);
285 }
286
287 sub compare_output {
288     my ($exp_file, @actual) = @_;
289     my (@expected) = snarf ($exp_file);
290
291     @actual = map ("$_\n", @actual);
292     @expected = map ("$_\n", @expected);
293
294     # Trim header and trailer from @actual.
295     while (scalar (@actual) && $actual[0] ne $expected[0]) {
296         shift (@actual);
297     }
298     die "First line of expected output was not present.\n" if !@actual;
299     while (scalar (@actual) && $actual[$#actual] ne $expected[$#expected]) {
300         pop (@actual);
301     }
302     die "Final line of expected output was not present.\n" if !@actual;
303     
304     # Check whether they're the same.
305     if ($#actual == $#expected) {
306         my ($eq) = 1;
307         for (my ($i) = 0; $i <= $#expected; $i++) {
308             $eq = 0 if $actual[$i] ne $expected[$i];
309         }
310         return if $eq;
311     }
312
313     # They differ.  Output a diff.
314     my ($diff) = "";
315     my ($d) = Algorithm::Diff->new (\@expected, \@actual);
316     $d->Base (1);
317     while ($d->Next ()) {
318         my ($ef, $el, $af, $al) = $d->Get (qw (min1 max1 min2 max2));
319         if ($d->Same ()) {
320             if ($af != $al) {
321                 $diff .= "Actual lines $af...$al match expected lines "
322                     . "$ef...$el.\n";
323             } else {
324                 $diff .= "Actual line $af matches expected line $ef.\n";
325             }
326         } else {
327             my (@i1) = $d->Items (1);
328             my (@i2) = $d->Items (2);
329             if (!@i1) {
330                 $diff .= "Extra or misplaced line(s) $af...$al "
331                     . "in actual output:\n";
332                 $diff .= number_lines ($af, \@i2);
333             } elsif (!$d->Items (2)) {
334                 $diff .= "Expected line(s) $ef...$el missing or misplaced:\n";
335                 $diff .= number_lines ($ef, \@i1);
336             } else {
337                 $diff .= "The following expected line(s) $ef...$el:\n";
338                 $diff .= number_lines ($ef, \@i1);
339                 $diff .= "became actual line(s) $af...$al:\n";
340                 $diff .= number_lines ($af, \@i2);
341             }
342         }
343     }
344
345     my ($details) = "";
346     $details .= "$test actual output (line numbers added):\n";
347     $details .= number_lines (1, \@actual);
348     $details .= "\n$test expected output (line numbers added):\n";
349     $details .= number_lines (1, \@expected);
350     $details .= "\n$diff\n";
351     $details{$test} = $details;
352     die "Output differs from expected.  Details at end of file.\n";
353 }
354
355 sub verify_alarm {
356     my ($iterations, @output) = @_;
357
358     verify_common (@output);
359
360     my (@products);
361     for (my ($i) = 0; $i < $iterations; $i++) {
362         for (my ($t) = 0; $t < 5; $t++) {
363             push (@products, ($i + 1) * ($t + 1) * 10);
364         }
365     }
366     @products = sort {$a <=> $b} @products;
367
368     for $_ (@output) {
369         die $_ if /Out of order/;
370
371         my ($p) = /product=(\d+)$/;
372         next if !defined $p;
373
374         my ($q) = shift (@products);
375         die "Too many wakeups.\n" if !defined $q;
376         die "Out of order wakeups ($p vs. $q).\n" if $p != $q; # FIXME
377     }
378     die scalar (@products) . " fewer wakeups than expected.\n"
379         if @products != 0;
380 }
381
382 sub test_source {
383     my ($test) = @_;
384     my ($src) = "$GRADES_DIR/$test.c";
385     $src = "$GRADES_DIR/mlfqs.c" if $test =~ /^mlfqs/;
386     -e $src or die "$src: stat: $!\n";
387     return $src;
388 }
389
390 sub test_constants {
391    my ($defines) = "";
392    $defines .= "#define MLFQS 1\n" if $test eq 'mlfqs-on';
393    return $defines;
394  }
395
396 sub run_test {
397     my ($test) = @_;
398     return "ok" if -f "output/$test.run.out";
399
400     my ($defines) = test_constants ($test);
401     if ($defines ne snarf ("pintos/src/constants.h")) {
402         open (CONSTANTS, ">pintos/src/constants.h");
403         print CONSTANTS $defines;
404         close (CONSTANTS);
405     }
406
407     $src = test_source ($test);
408     xsystem ("", "cp $src pintos/src/threads/test.c") or die;
409     unlink ("pintos/src/threads/build/threads/test.o");
410     unlink ("pintos/src/threads/build/kernel.o");
411     unlink ("pintos/src/threads/build/kernel.bin");
412     unlink ("pintos/src/threads/build/os.dsk");
413     xsystem ("$test.make", "cd pintos/src/threads && make")
414         or return "compile error";
415
416     my ($timeout) = 10;
417     $timeout = 600 if $test =~ /^mlfqs/;
418     xsystem ("$test.run", "cd pintos/src/threads/build && pintos -v run -q",
419              $timeout)
420         or return "Bochs error";
421     return "ok";
422 }
423
424 sub xsystem {
425     my ($log, $command, $timeout) = @_;
426     print "$command\n" if $verbose;
427
428     $timeout = 0 if !defined $timeout;
429
430     my ($status);
431     if ($log ne '') {
432         $status = systimeout ("($command) >output/$log.out 2>output/$log.err",
433                               $timeout);
434         unlink ("output/$log.err") unless $status != 0;
435     } else {
436         $status = systimeout ($command, $timeout);
437     }
438
439     die "Interrupted\n"
440         if WIFSIGNALED ($status) && WTERMSIG ($status) == SIGINT;
441
442     return $status == 0;
443 }
444
445 sub systimeout {
446     my ($command, $timeout) = @_;
447     my ($pid, $status);
448     eval {
449         local $SIG{ALRM} = sub { die "alarm\n" };
450         alarm $timeout;
451         $pid = fork;
452         die "fork: $!\n" if !defined $pid;
453         exec ($command), die "$command: exec: $!\n" if !$pid;
454         waitpid ($pid, 0);
455         $status = $?;
456         alarm 0;
457     };
458     if ($@) {
459         die unless $@ eq "alarm\n";   # propagate unexpected errors
460         print "Timed out.\n";
461         kill SIGTERM, $pid;
462         $status = 0;
463     }
464     return $status;
465 }
466
467 sub snarf {
468     my ($file) = @_;
469     open (OUTPUT, $file) or die "$file: open: $!\n";
470     my (@lines) = <OUTPUT>;
471     chomp (@lines);
472     close (OUTPUT);
473     return wantarray ? @lines : join ('', map ("$_\n", @lines));
474 }
475
476 sub make_summary {
477     @summary = snarf ("$GRADES_DIR/tests.txt");
478
479     my ($ploss) = 0;
480     my ($tloss) = 0;
481     my ($total) = 0;
482     for (my ($i) = 0; $i <= $#summary; $i++) {
483         $_ = $summary[$i];
484         if (my ($loss, $test) = /^  -(\d+) ([-a-zA-Z0-9]+):/) {
485             my ($result) = $result{$test} or die "missing results for $test\n";
486
487             if ($result eq 'ok') {
488                 splice (@summary, $i, 1);
489                 $i--;
490             } else {
491                 $ploss += $loss;
492                 $tloss += $loss;
493                 splice (@summary, $i + 1, 0,
494                         map ("     $_", split ("\n", $result)));
495             }
496         } elsif (my ($ptotal) = /^Score: \/(\d+)$/) {
497             $total += $ptotal;
498             $summary[$i] = "Score: " . ($ptotal - $ploss) . "/$ptotal";
499             splice (@summary, $i, 0, "  All tests passed.") if $ploss == 0;
500             $ploss = 0;
501             $i++;
502         }
503     }
504     my ($ts) = "(" . ($total - $tloss) . "/" . $total . ")";
505     $summary[0] =~ s/\[\[total\]\]/$ts/;
506
507     open (SUMMARY, ">tests.out");
508     print SUMMARY map ("$_\n", @summary);
509     close (SUMMARY);
510
511     open (DETAILS, ">details.out");
512     my ($n) = 0;
513     for my $test (@TESTS) {
514         next if $result{$test} eq 'ok';
515         
516         my ($details) = $details{$test};
517         next if !defined ($details) && ! -e "output/$test.run.out";
518
519         print DETAILS "\n" if $n++;
520         print DETAILS "--- $test details ", '-' x (50 - length ($test));
521         print DETAILS "\n\n";
522
523         if (!defined $details) {
524             $details = "Output:\n\n" . snarf ("output/$test.run.out");
525         }
526         print DETAILS $details;
527
528         print DETAILS "\n", "-" x 10, "\n\n$extra{$test}"
529             if defined $extra{$test};
530     }
531     close (DETAILS);
532
533 }
534
535 sub files_equal {
536     my ($a, $b) = @_;
537     my ($equal);
538     open (A, "<$a") or die "$a: open: $!\n";
539     open (B, "<$b") or die "$b: open: $!\n";
540     if (-s A != -s B) {
541         $equal = 0;
542     } else {
543         my ($sa, $sb);
544         for (;;) {
545             sysread (A, $sa, 1024);
546             sysread (B, $sb, 1024);
547             $equal = 0, last if $sa ne $sb;
548             $equal = 1, last if $sa eq '';
549         }
550     }
551     close (A);
552     close (B);
553     return $equal;
554 }
555
556 sub file_contains {
557     my ($file, $expected) = @_;
558     open (FILE, "<$file") or die "$file: open: $!\n";
559     my ($actual);
560     sysread (FILE, $actual, -s FILE);
561     my ($equal) = $actual eq $expected;
562     close (FILE);
563     return $equal;
564 }
565
566 sub number_lines {
567     my ($ln, $lines) = @_;
568     my ($out);
569     for my $line (@$lines) {
570         chomp $line;
571         $out .= sprintf "%4d  %s\n", $ln++, $line;
572     }
573     return $out;
574 }