summaryrefslogtreecommitdiffstats
path: root/pintos-progos/tests/tests.pm
diff options
context:
space:
mode:
Diffstat (limited to 'pintos-progos/tests/tests.pm')
-rw-r--r--pintos-progos/tests/tests.pm625
1 files changed, 0 insertions, 625 deletions
diff --git a/pintos-progos/tests/tests.pm b/pintos-progos/tests/tests.pm
deleted file mode 100644
index 4599cb9..0000000
--- a/pintos-progos/tests/tests.pm
+++ /dev/null
@@ -1,625 +0,0 @@
1use strict;
2use warnings;
3use tests::Algorithm::Diff;
4use File::Temp 'tempfile';
5use Fcntl qw(SEEK_SET SEEK_CUR);
6
7sub fail;
8sub pass;
9
10die if @ARGV != 2;
11our ($test, $src_dir) = @ARGV;
12
13my ($msg_file) = tempfile ();
14select ($msg_file);
15
16our (@prereq_tests) = ();
17if ($test =~ /^(.*)-persistence$/) {
18 push (@prereq_tests, $1);
19}
20for my $prereq_test (@prereq_tests) {
21 my (@result) = read_text_file ("$prereq_test.result");
22 fail "Prerequisite test $prereq_test failed.\n" if $result[0] ne 'PASS';
23}
24
25
26# Generic testing.
27
28sub check_expected {
29 my ($expected) = pop @_;
30 my (@options) = @_;
31 my (@output) = read_text_file ("$test.output");
32 common_checks ("run", @output);
33 compare_output ("run", @options, \@output, $expected);
34}
35
36sub common_checks {
37 my ($run, @output) = @_;
38
39 fail "\u$run produced no output at all\n" if @output == 0;
40
41 check_for_panic ($run, @output);
42 check_for_keyword ($run, "FAIL", @output);
43 check_for_triple_fault ($run, @output);
44 check_for_keyword ($run, "TIMEOUT", @output);
45
46 fail "\u$run didn't start up properly: no \"Pintos booting\" message\n"
47 if !grep (/Pintos booting with.*kB RAM\.\.\./, @output);
48 fail "\u$run didn't start up properly: no \"Boot complete\" message\n"
49 if !grep (/Boot complete/, @output);
50 fail "\u$run didn't shut down properly: no \"Timer: # ticks\" message\n"
51 if !grep (/Timer: \d+ ticks/, @output);
52 fail "\u$run didn't shut down properly: no \"Powering off\" message\n"
53 if !grep (/Powering off/, @output);
54}
55
56sub check_for_panic {
57 my ($run, @output) = @_;
58
59 my ($panic) = grep (/PANIC/, @output);
60 return unless defined $panic;
61
62 print "Kernel panic in $run: ", substr ($panic, index ($panic, "PANIC")),
63 "\n";
64
65 my (@stack_line) = grep (/Call stack:/, @output);
66 if (@stack_line != 0) {
67 my ($addrs) = $stack_line[0] =~ /Call stack:((?: 0x[0-9a-f]+)+)/;
68
69 # Find a user program to translate user virtual addresses.
70 my ($userprog) = "";
71 $userprog = "$test"
72 if grep (hex ($_) < 0xc0000000, split (' ', $addrs)) > 0 && -e $test;
73
74 # Get and print the backtrace.
75 my ($trace) = scalar (`backtrace kernel.o $userprog $addrs`);
76 print "Call stack:$addrs\n";
77 print "Translation of call stack:\n";
78 print $trace;
79
80 # Print disclaimer.
81 if ($userprog ne '' && index ($trace, $userprog) >= 0) {
82 print <<EOF;
83Translations of user virtual addresses above are based on a guess at
84the binary to use. If this guess is incorrect, then those
85translations will be misleading.
86EOF
87 }
88 }
89
90 if ($panic =~ /sec_no \< d-\>capacity/) {
91 print <<EOF;
92\nThis assertion commonly fails when accessing a file via an inode that
93has been closed and freed. Freeing an inode clears all its sector
94indexes to 0xcccccccc, which is not a valid sector number for disks
95smaller than about 1.6 TB.
96EOF
97 }
98
99 fail;
100}
101
102sub check_for_keyword {
103 my ($run, $keyword, @output) = @_;
104
105 my ($kw_line) = grep (/$keyword/, @output);
106 return unless defined $kw_line;
107
108 # Most output lines are prefixed by (test-name). Eliminate this
109 # from our message for brevity.
110 $kw_line =~ s/^\([^\)]+\)\s+//;
111 print "$run: $kw_line\n";
112
113 fail;
114}
115
116sub check_for_triple_fault {
117 my ($run, @output) = @_;
118
119 my ($reboots) = grep (/Pintos booting/, @output) - 1;
120 return unless $reboots > 0;
121
122 print <<EOF;
123\u$run spontaneously rebooted $reboots times.
124This is most often caused by unhandled page faults.
125Read the Triple Faults section in the Debugging chapter
126of the Pintos manual for more information.
127EOF
128
129 fail;
130}
131
132# Get @output without header or trailer.
133sub get_core_output {
134 my ($run, @output) = @_;
135 my ($p);
136
137 my ($process);
138 my ($start);
139 for my $i (0...$#_) {
140 $start = $i + 1, last
141 if ($process) = $output[$i] =~ /^Executing '(\S+).*':$/;
142 }
143
144 my ($end);
145 for my $i ($start...$#output) {
146 $end = $i - 1, last if $output[$i] =~ /^Execution of '.*' complete.$/;
147 }
148
149 fail "\u$run didn't start a thread or process\n" if !defined $start;
150 fail "\u$run started '$process' but it never finished\n" if !defined $end;
151
152 return @output[$start...$end];
153}
154
155sub compare_output {
156 my ($run) = shift @_;
157 my ($expected) = pop @_;
158 my ($output) = pop @_;
159 my (%options) = @_;
160
161 my (@output) = get_core_output ($run, @$output);
162 fail "\u$run didn't produce any output" if !@output;
163
164 my $ignore_exit_codes = exists $options{IGNORE_EXIT_CODES};
165 if ($ignore_exit_codes) {
166 delete $options{IGNORE_EXIT_CODES};
167 @output = grep (!/^[a-zA-Z0-9-_]+: exit\(\-?\d+\)$/, @output);
168 }
169 my $ignore_user_faults = exists $options{IGNORE_USER_FAULTS};
170 if ($ignore_user_faults) {
171 delete $options{IGNORE_USER_FAULTS};
172 @output = grep (!/^Page fault at.*in user context\.$/
173 && !/: dying due to interrupt 0x0e \(.*\).$/
174 && !/^Interrupt 0x0e \(.*\) at eip=/
175 && !/^ cr2=.* error=.*/
176 && !/^ eax=.* ebx=.* ecx=.* edx=.*/
177 && !/^ esi=.* edi=.* esp=.* ebp=.*/
178 && !/^ cs=.* ds=.* es=.* ss=.*/, @output);
179 }
180 die "unknown option " . (keys (%options))[0] . "\n" if %options;
181
182 my ($msg);
183
184 # Compare actual output against each allowed output.
185 if (ref ($expected) eq 'ARRAY') {
186 my ($i) = 0;
187 $expected = {map ((++$i => $_), @$expected)};
188 }
189 foreach my $key (keys %$expected) {
190 my (@expected) = split ("\n", $expected->{$key});
191
192 $msg .= "Acceptable output:\n";
193 $msg .= join ('', map (" $_\n", @expected));
194
195 # Check whether actual and expected match.
196 # If it's a perfect match, we're done.
197 if ($#output == $#expected) {
198 my ($eq) = 1;
199 for (my ($i) = 0; $i <= $#expected; $i++) {
200 $eq = 0 if $output[$i] ne $expected[$i];
201 }
202 return $key if $eq;
203 }
204
205 # They differ. Output a diff.
206 my (@diff) = "";
207 my ($d) = Algorithm::Diff->new (\@expected, \@output);
208 while ($d->Next ()) {
209 my ($ef, $el, $af, $al) = $d->Get (qw (min1 max1 min2 max2));
210 if ($d->Same ()) {
211 push (@diff, map (" $_\n", $d->Items (1)));
212 } else {
213 push (@diff, map ("- $_\n", $d->Items (1))) if $d->Items (1);
214 push (@diff, map ("+ $_\n", $d->Items (2))) if $d->Items (2);
215 }
216 }
217
218 $msg .= "Differences in `diff -u' format:\n";
219 $msg .= join ('', @diff);
220 }
221
222 # Failed to match. Report failure.
223 $msg .= "\n(Process exit codes are excluded for matching purposes.)\n"
224 if $ignore_exit_codes;
225 $msg .= "\n(User fault messages are excluded for matching purposes.)\n"
226 if $ignore_user_faults;
227 fail "Test output failed to match any acceptable form.\n\n$msg";
228}
229
230# File system extraction.
231
232# check_archive (\%CONTENTS)
233#
234# Checks that the extracted file system's contents match \%CONTENTS.
235# Each key in the hash is a file name. Each value may be:
236#
237# - $FILE: Name of a host file containing the expected contents.
238#
239# - [$FILE, $OFFSET, $LENGTH]: An excerpt of host file $FILE
240# comprising the $LENGTH bytes starting at $OFFSET.
241#
242# - [$CONTENTS]: The literal expected file contents, as a string.
243#
244# - {SUBDIR}: A subdirectory, in the same form described here,
245# recursively.
246sub check_archive {
247 my ($expected_hier) = @_;
248
249 my (@output) = read_text_file ("$test.output");
250 common_checks ("file system extraction run", @output);
251
252 @output = get_core_output ("file system extraction run", @output);
253 @output = grep (!/^[a-zA-Z0-9-_]+: exit\(\d+\)$/, @output);
254 fail join ("\n", "Error extracting file system:", @output) if @output;
255
256 my ($test_base_name) = $test;
257 $test_base_name =~ s%.*/%%;
258 $test_base_name =~ s%-persistence$%%;
259 $expected_hier->{$test_base_name} = $prereq_tests[0];
260 $expected_hier->{'tar'} = 'tests/filesys/extended/tar';
261
262 my (%expected) = normalize_fs (flatten_hierarchy ($expected_hier, ""));
263 my (%actual) = read_tar ("$prereq_tests[0].tar");
264
265 my ($errors) = 0;
266 foreach my $name (sort keys %expected) {
267 if (exists $actual{$name}) {
268 if (is_dir ($actual{$name}) && !is_dir ($expected{$name})) {
269 print "$name is a directory but should be an ordinary file.\n";
270 $errors++;
271 } elsif (!is_dir ($actual{$name}) && is_dir ($expected{$name})) {
272 print "$name is an ordinary file but should be a directory.\n";
273 $errors++;
274 }
275 } else {
276 print "$name is missing from the file system.\n";
277 $errors++;
278 }
279 }
280 foreach my $name (sort keys %actual) {
281 if (!exists $expected{$name}) {
282 if ($name =~ /^[[:print:]]+$/) {
283 print "$name exists in the file system but it should not.\n";
284 } else {
285 my ($esc_name) = $name;
286 $esc_name =~ s/[^[:print:]]/./g;
287 print <<EOF;
288$esc_name exists in the file system but should not. (The name
289of this file contains unusual characters that were printed as `.'.)
290EOF
291 }
292 $errors++;
293 }
294 }
295 if ($errors) {
296 print "\nActual contents of file system:\n";
297 print_fs (%actual);
298 print "\nExpected contents of file system:\n";
299 print_fs (%expected);
300 } else {
301 foreach my $name (sort keys %expected) {
302 if (!is_dir ($expected{$name})) {
303 my ($exp_file, $exp_length) = open_file ($expected{$name});
304 my ($act_file, $act_length) = open_file ($actual{$name});
305 $errors += !compare_files ($exp_file, $exp_length,
306 $act_file, $act_length, $name,
307 !$errors);
308 close ($exp_file);
309 close ($act_file);
310 }
311 }
312 }
313 fail "Extracted file system contents are not correct.\n" if $errors;
314}
315
316# open_file ([$FILE, $OFFSET, $LENGTH])
317# open_file ([$CONTENTS])
318#
319# Opens a file for the contents passed in, which must be in one of
320# the two above forms that correspond to check_archive() arguments.
321#
322# Returns ($HANDLE, $LENGTH), where $HANDLE is the file's handle and
323# $LENGTH is the number of bytes in the file's content.
324sub open_file {
325 my ($value) = @_;
326 die if ref ($value) ne 'ARRAY';
327
328 my ($file) = tempfile ();
329 my ($length);
330 if (@$value == 1) {
331 $length = length ($value->[0]);
332 $file = tempfile ();
333 syswrite ($file, $value->[0]) == $length
334 or die "writing temporary file: $!\n";
335 sysseek ($file, 0, SEEK_SET);
336 } elsif (@$value == 3) {
337 $length = $value->[2];
338 open ($file, '<', $value->[0]) or die "$value->[0]: open: $!\n";
339 die "$value->[0]: file is smaller than expected\n"
340 if -s $file < $value->[1] + $length;
341 sysseek ($file, $value->[1], SEEK_SET);
342 } else {
343 die;
344 }
345 return ($file, $length);
346}
347
348# compare_files ($A, $A_SIZE, $B, $B_SIZE, $NAME, $VERBOSE)
349#
350# Compares $A_SIZE bytes in $A to $B_SIZE bytes in $B.
351# ($A and $B are handles.)
352# If their contents differ, prints a brief message describing
353# the differences, using $NAME to identify the file.
354# The message contains more detail if $VERBOSE is nonzero.
355# Returns 1 if the contents are identical, 0 otherwise.
356sub compare_files {
357 my ($a, $a_size, $b, $b_size, $name, $verbose) = @_;
358 my ($ofs) = 0;
359 select(STDOUT);
360 for (;;) {
361 my ($a_amt) = $a_size >= 1024 ? 1024 : $a_size;
362 my ($b_amt) = $b_size >= 1024 ? 1024 : $b_size;
363 my ($a_data, $b_data);
364 if (!defined (sysread ($a, $a_data, $a_amt))
365 || !defined (sysread ($b, $b_data, $b_amt))) {
366 die "reading $name: $!\n";
367 }
368
369 my ($a_len) = length $a_data;
370 my ($b_len) = length $b_data;
371 last if $a_len == 0 && $b_len == 0;
372
373 if ($a_data ne $b_data) {
374 my ($min_len) = $a_len < $b_len ? $a_len : $b_len;
375 my ($diff_ofs);
376 for ($diff_ofs = 0; $diff_ofs < $min_len; $diff_ofs++) {
377 last if (substr ($a_data, $diff_ofs, 1)
378 ne substr ($b_data, $diff_ofs, 1));
379 }
380
381 printf "\nFile $name differs from expected "
382 . "starting at offset 0x%x.\n", $ofs + $diff_ofs;
383 if ($verbose ) {
384 print "Expected contents:\n";
385 hex_dump (substr ($a_data, $diff_ofs, 64), $ofs + $diff_ofs);
386 print "Actual contents:\n";
387 hex_dump (substr ($b_data, $diff_ofs, 64), $ofs + $diff_ofs);
388 }
389 return 0;
390 }
391
392 $ofs += $a_len;
393 $a_size -= $a_len;
394 $b_size -= $b_len;
395 }
396 return 1;
397}
398
399# hex_dump ($DATA, $OFS)
400#
401# Prints $DATA in hex and text formats.
402# The first byte of $DATA corresponds to logical offset $OFS
403# in whatever file the data comes from.
404sub hex_dump {
405 my ($data, $ofs) = @_;
406
407 if ($data eq '') {
408 printf " (File ends at offset %08x.)\n", $ofs;
409 return;
410 }
411
412 my ($per_line) = 16;
413 while ((my $size = length ($data)) > 0) {
414 my ($start) = $ofs % $per_line;
415 my ($end) = $per_line;
416 $end = $start + $size if $end - $start > $size;
417 my ($n) = $end - $start;
418
419 printf "0x%08x ", int ($ofs / $per_line) * $per_line;
420
421 # Hex version.
422 print " " x $start;
423 for my $i ($start...$end - 1) {
424 printf "%02x", ord (substr ($data, $i - $start, 1));
425 print $i == $per_line / 2 - 1 ? '-' : ' ';
426 }
427 print " " x ($per_line - $end);
428
429 # Character version.
430 my ($esc_data) = substr ($data, 0, $n);
431 $esc_data =~ s/[^[:print:]]/./g;
432 print "|", " " x $start, $esc_data, " " x ($per_line - $end), "|";
433
434 print "\n";
435
436 $data = substr ($data, $n);
437 $ofs += $n;
438 }
439}
440
441# print_fs (%FS)
442#
443# Prints a list of files in %FS, which must be a file system
444# as flattened by flatten_hierarchy() and normalized by
445# normalize_fs().
446sub print_fs {
447 my (%fs) = @_;
448 foreach my $name (sort keys %fs) {
449 my ($esc_name) = $name;
450 $esc_name =~ s/[^[:print:]]/./g;
451 print "$esc_name: ";
452 if (!is_dir ($fs{$name})) {
453 print +file_size ($fs{$name}), "-byte file";
454 } else {
455 print "directory";
456 }
457 print "\n";
458 }
459 print "(empty)\n" if !@_;
460}
461
462# normalize_fs (%FS)
463#
464# Takes a file system as flattened by flatten_hierarchy().
465# Returns a similar file system in which values of the form $FILE
466# are replaced by those of the form [$FILE, $OFFSET, $LENGTH].
467sub normalize_fs {
468 my (%fs) = @_;
469 foreach my $name (keys %fs) {
470 my ($value) = $fs{$name};
471 next if is_dir ($value) || ref ($value) ne '';
472 die "can't open $value\n" if !stat $value;
473 $fs{$name} = [$value, 0, -s _];
474 }
475 return %fs;
476}
477
478# is_dir ($VALUE)
479#
480# Takes a value like one in the hash returned by flatten_hierarchy()
481# and returns 1 if it represents a directory, 0 otherwise.
482sub is_dir {
483 my ($value) = @_;
484 return ref ($value) eq '' && $value eq 'directory';
485}
486
487# file_size ($VALUE)
488#
489# Takes a value like one in the hash returned by flatten_hierarchy()
490# and returns the size of the file it represents.
491sub file_size {
492 my ($value) = @_;
493 die if is_dir ($value);
494 die if ref ($value) ne 'ARRAY';
495 return @$value > 1 ? $value->[2] : length ($value->[0]);
496}
497
498# flatten_hierarchy ($HIER_FS, $PREFIX)
499#
500# Takes a file system in the format expected by check_archive() and
501# returns a "flattened" version in which file names include all parent
502# directory names and the value of directories is just "directory".
503sub flatten_hierarchy {
504 my (%hier_fs) = %{$_[0]};
505 my ($prefix) = $_[1];
506 my (%flat_fs);
507 for my $name (keys %hier_fs) {
508 my ($value) = $hier_fs{$name};
509 if (ref $value eq 'HASH') {
510 %flat_fs = (%flat_fs, flatten_hierarchy ($value, "$prefix$name/"));
511 $flat_fs{"$prefix$name"} = 'directory';
512 } else {
513 $flat_fs{"$prefix$name"} = $value;
514 }
515 }
516 return %flat_fs;
517}
518
519# read_tar ($ARCHIVE)
520#
521# Reads the ustar-format tar file in $ARCHIVE
522# and returns a flattened file system for it.
523sub read_tar {
524 my ($archive) = @_;
525 my (%content);
526 open (ARCHIVE, '<', $archive) or fail "$archive: open: $!\n";
527 for (;;) {
528 my ($header);
529 if ((my $retval = sysread (ARCHIVE, $header, 512)) != 512) {
530 fail "$archive: unexpected end of file\n" if $retval >= 0;
531 fail "$archive: read: $!\n";
532 }
533
534 last if $header eq "\0" x 512;
535
536 # Verify magic numbers.
537 if (substr ($header, 257, 6) ne "ustar\0"
538 || substr ($header, 263, 2) ne '00') {
539 fail "$archive: corrupt ustar header\n";
540 }
541
542 # Verify checksum.
543 my ($chksum) = oct (unpack ("Z*", substr ($header, 148, 8, ' ' x 8)));
544 my ($correct_chksum) = unpack ("%32a*", $header);
545 fail "$archive: bad header checksum\n" if $chksum != $correct_chksum;
546
547 # Get file name.
548 my ($name) = unpack ("Z100", $header);
549 my ($prefix) = unpack ("Z*", substr ($header, 345));
550 $name = "$prefix/$name" if $prefix ne '';
551 fail "$archive: contains file with empty name" if $name eq '';
552
553 # Get type.
554 my ($typeflag) = substr ($header, 156, 1);
555 $typeflag = '0' if $typeflag eq "\0";
556 fail "unknown file type '$typeflag'\n" if $typeflag !~ /[05]/;
557
558 # Get size.
559 my ($size) = oct (unpack ("Z*", substr ($header, 124, 12)));
560 fail "bad size $size\n" if $size < 0;
561 $size = 0 if $typeflag eq '5';
562
563 # Store content.
564 $name =~ s%^(/|\./|\.\./)*%%; # Strip leading "/", "./", "../".
565 $name = '' if $name eq '.' || $name eq '..';
566 if (exists $content{$name}) {
567 fail "$archive: contains multiple entries for $name\n";
568 }
569 if ($typeflag eq '5') {
570 $content{$name} = 'directory' if $name ne '';
571 } else {
572 fail "$archive: contains file with empty name\n" if $name eq '';
573 my ($position) = sysseek (ARCHIVE, 0, SEEK_CUR);
574 $content{$name} = [$archive, $position, $size];
575 sysseek (ARCHIVE, int (($size + 511) / 512) * 512, SEEK_CUR);
576 }
577 }
578 close (ARCHIVE);
579 return %content;
580}
581
582# Utilities.
583
584sub fail {
585 finish ("FAIL", @_);
586}
587
588sub pass {
589 finish ("PASS", @_);
590}
591
592sub finish {
593 my ($verdict, @messages) = @_;
594
595 seek ($msg_file, 0, 0);
596 push (@messages, <$msg_file>);
597 close ($msg_file);
598 chomp (@messages);
599
600 my ($result_fn) = "$test.result";
601 open (RESULT, '>', $result_fn) or die "$result_fn: create: $!\n";
602 print RESULT "$verdict\n";
603 print RESULT "$_\n" foreach @messages;
604 close (RESULT);
605
606 if ($verdict eq 'PASS') {
607 print STDOUT "pass $test\n";
608 } else {
609 print STDOUT "FAIL $test\n";
610 }
611 print STDOUT "$_\n" foreach @messages;
612
613 exit 0;
614}
615
616sub read_text_file {
617 my ($file_name) = @_;
618 open (FILE, '<', $file_name) or die "$file_name: open: $!\n";
619 my (@content) = <FILE>;
620 chomp (@content);
621 close (FILE);
622 return @content;
623}
624
6251;