Improve automatic test failure interpretation by extending backtrace
[pintos-anon] / src / tests / tests.pm
1 use strict;
2 use warnings;
3 use tests::Algorithm::Diff;
4 use File::Temp 'tempfile';
5
6 sub fail;
7 sub pass;
8
9 die if @ARGV != 2;
10 our ($test, $src_dir) = @ARGV;
11
12 my ($msg_file) = tempfile ();
13 select ($msg_file);
14
15 sub check_expected {
16     my ($expected) = pop @_;
17     my (@options) = @_;
18     my (@output) = read_text_file ("$test.output");
19     common_checks (@output);
20     compare_output (@options, \@output, $expected);
21 }
22
23 sub common_checks {
24     my (@output) = @_;
25
26     fail "No output at all\n" if @output == 0;
27
28     check_for_panic (@output);
29     check_for_keyword ("FAIL", @output);
30     check_for_triple_fault (@output);
31     check_for_keyword ("TIMEOUT", @output);
32
33     fail "Didn't start up properly: no \"Pintos booting\" startup message\n"
34       if !grep (/Pintos booting with.*kB RAM\.\.\./, @output);
35     fail "Didn't start up properly: no \"Boot complete\" startup message\n"
36       if !grep (/Boot complete/, @output);
37     fail "Didn't shut down properly: no \"Timer: # ticks\" shutdown message\n"
38       if !grep (/Timer: \d+ ticks/, @output);
39     fail "Didn't shut down properly: no \"Powering off\" shutdown message\n"
40       if !grep (/Powering off/, @output);
41 }
42
43 sub check_for_panic {
44     my (@output) = @_;
45
46     my ($panic) = grep (/PANIC/, @output);
47     return unless defined $panic;
48
49     print "Kernel panic: ", substr ($panic, index ($panic, "PANIC")), "\n";
50
51     my (@stack_line) = grep (/Call stack:/, @output);
52     if (@stack_line != 0) {
53         my ($addrs) = $stack_line[0] =~ /Call stack:((?: 0x[0-9a-f]+)+)/;
54
55         # Find a user program to translate user virtual addresses.
56         my ($userprog) = "";
57         $userprog = "$test"
58           if grep (hex ($_) < 0xc0000000, split (' ', $addrs)) > 0 && -e $test;
59
60         # Get and print the backtrace.
61         my ($trace) = scalar (`backtrace kernel.o $userprog $addrs`);
62         print "Call stack:$addrs\n";
63         print "Translation of call stack:\n";
64         print $trace;
65
66         # Print disclaimer.
67         if ($userprog ne '' && index ($trace, $userprog) >= 0) {
68             print <<EOF;
69 Translations of user virtual addresses above are based on a guess at
70 the binary to use.  If this guess is incorrect, then those
71 translations will be misleading.
72 EOF
73         }
74     }
75
76     if ($panic =~ /sec_no \< d-\>capacity/) {
77         print <<EOF;
78 \nThis assertion commonly fails when accessing a file via an inode that
79 has been closed and freed.  Freeing an inode clears all its sector
80 indexes to 0xcccccccc, which is not a valid sector number for disks
81 smaller than about 1.6 TB.
82 EOF
83     }
84
85     fail;
86 }
87
88 sub check_for_keyword {
89     my ($keyword, @output) = @_;
90     
91     my ($kw_line) = grep (/$keyword/, @output);
92     return unless defined $kw_line;
93
94     # Most output lines are prefixed by (test-name).  Eliminate this
95     # from our message for brevity.
96     $kw_line =~ s/^\([^\)]+\)\s+//;
97     print "$kw_line\n";
98
99     fail;
100 }
101
102 sub check_for_triple_fault {
103     my (@output) = @_;
104
105     return unless grep (/Pintos booting/, @output) > 1;
106
107     print <<EOF;
108 Pintos spontaneously rebooted during this test.
109 This is most often caused by unhandled page faults.
110 EOF
111
112     fail;
113 }
114
115 # Get @output without header or trailer.
116 sub get_core_output {
117     my ($p);
118     do { $p = shift; } while (defined ($p) && $p !~ /^Executing '.*':$/);
119     do { $p = pop; } while (defined ($p) && $p !~ /^Execution of '.*' complete.$/);
120     return @_;
121 }
122
123 sub compare_output {
124     my ($expected) = pop @_;
125     my ($output) = pop @_;
126     my (%options) = @_;
127
128     my (@output) = get_core_output (@$output);
129     fail "'$test' didn't run or didn't produce any output\n" if !@output;
130
131     if (exists $options{IGNORE_EXIT_CODES}) {
132         delete $options{IGNORE_EXIT_CODES};
133         @output = grep (!/^[a-zA-Z0-9-_]+: exit\(\d+\)$/, @output);
134     }
135     die "unknown option " . (keys (%options))[0] . "\n" if %options;
136
137     my ($msg);
138
139     # Compare actual output against each allowed output.
140     foreach my $exp_string (@$expected) {
141         my (@expected) = split ("\n", $exp_string);
142
143         $msg .= "Acceptable output:\n";
144         $msg .= join ('', map ("  $_\n", @expected));
145
146         # Check whether actual and expected match.
147         # If it's a perfect match, we're done.
148         if ($#output == $#expected) {
149             my ($eq) = 1;
150             for (my ($i) = 0; $i <= $#expected; $i++) {
151                 $eq = 0 if $output[$i] ne $expected[$i];
152             }
153             pass if $eq;
154         }
155
156         # They differ.  Output a diff.
157         my (@diff) = "";
158         my ($d) = Algorithm::Diff->new (\@expected, \@output);
159         while ($d->Next ()) {
160             my ($ef, $el, $af, $al) = $d->Get (qw (min1 max1 min2 max2));
161             if ($d->Same ()) {
162                 push (@diff, map ("  $_\n", $d->Items (1)));
163             } else {
164                 push (@diff, map ("- $_\n", $d->Items (1))) if $d->Items (1);
165                 push (@diff, map ("+ $_\n", $d->Items (2))) if $d->Items (2);
166             }
167         }
168
169         $msg .= "Differences in `diff -u' format:\n";
170         $msg .= join ('', @diff);
171     }
172
173     # Failed to match.  Report failure.
174     fail "Test output failed to match any acceptable form.\n\n$msg";
175 }
176
177 sub fail {
178     finish ("FAIL", @_);
179 }
180
181 sub pass {
182     finish ("PASS", @_);
183 }
184
185 sub finish {
186     my ($verdict, @rest) = @_;
187
188     my ($messages) = "";
189     seek ($msg_file, 0, 0);
190     while (<$msg_file>) {
191         $messages .= $_;
192     }
193     close ($msg_file);
194
195     my ($result_fn) = "$test.result";
196     open (RESULT, '>', $result_fn) or die "$result_fn: create: $!\n";
197     print RESULT "$verdict\n", $messages, @rest;
198     close (RESULT);
199
200     if ($verdict eq 'PASS') {
201         print STDOUT "pass $test\n";
202     } else {
203         print STDOUT "FAIL $test\n";
204     }
205     print STDOUT $messages, @rest, "\n";
206
207     exit 0;
208 }
209
210 sub read_text_file {
211     my ($file_name) = @_;
212     open (FILE, '<', $file_name) or die "$file_name: open: $!\n";
213     my (@content) = <FILE>;
214     chomp (@content);
215     close (FILE);
216     return @content;
217 }
218
219 1;