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