1#!/usr/bin/env perl 2 3# 4# Were we told where to find tcpdump? 5# 6if (!($TCPDUMP = $ENV{TCPDUMP_BIN})) { 7 # 8 # No. Use the appropriate path. 9 # 10 if ($^O eq 'MSWin32') { 11 # 12 # XXX - assume, for now, a Visual Studio debug build, so that 13 # tcpdump is in the Debug subdirectory. 14 # 15 $TCPDUMP = "Debug\\tcpdump.exe" 16 } else { 17 $TCPDUMP = "./tcpdump" 18 } 19} 20 21# 22# Make true and false work as Booleans. 23# 24use constant true => 1; 25use constant false => 0; 26 27use File::Basename; 28use POSIX qw( WEXITSTATUS WIFEXITED); 29use Cwd qw(abs_path getcwd); 30use File::Path qw(mkpath); # mkpath works with ancient perl, as well as newer perl 31use File::Spec; 32use Data::Dumper; # for debugging. 33 34# these are created in the directory where we are run, which might be 35# a build directory. 36my $newdir = "tests/NEW"; 37my $diffdir= "tests/DIFF"; 38mkpath($newdir); 39mkpath($diffdir); 40my $origdir = getcwd(); 41my $srcdir = $ENV{'srcdir'} || "."; 42# Default to unified diff and allow to fall back to basic diff if necessary. 43my $diff_flags = defined $ENV{'DIFF_FLAGS'} ? $ENV{'DIFF_FLAGS'} : '-u'; 44 45# 46# Force UTC, so time stamps are printed in a standard time zone, and 47# tests don't have to be run in the time zone in which the output 48# file was generated. 49# 50$ENV{'TZ'}='GMT0'; 51 52# 53# Get the tests directory from $0. 54# 55my $testsdir = dirname($0); 56 57# 58# Convert it to an absolute path, so it works even after we do a cd. 59# 60$testsdir = abs_path($testsdir); 61print "Running tests from ${testsdir}\n"; 62print "with ${TCPDUMP}, version:\n"; 63system "${TCPDUMP} --version"; 64 65unshift(@INC, $testsdir); 66 67$passedcount = 0; 68$failedcount = 0; 69$skippedcount = 0; 70# 71my $failureoutput=$origdir . "/tests/failure-outputs.txt"; 72 73# truncate the output file 74open(FAILUREOUTPUT, ">" . $failureoutput); 75close(FAILUREOUTPUT); 76 77$confighhash = undef; 78 79sub showfile { 80 local($path) = @_; 81 82 # 83 # XXX - just do this directly in Perl? 84 # 85 if ($^O eq 'MSWin32') { 86 my $winpath = File::Spec->canonpath($path); 87 system "type $winpath"; 88 } else { 89 system "cat $path"; 90 } 91} 92 93sub runtest { 94 local($name, $input, $output, $options) = @_; 95 my $r; 96 97 $outputbase = basename($output); 98 my $coredump = false; 99 my $status = 0; 100 my $linecount = 0; 101 my $rawstderrlog = "${newdir}/${outputbase}.raw.stderr"; 102 my $stderrlog = "${newdir}/${outputbase}.stderr"; 103 my $diffstat = 0; 104 my $errdiffstat = 0; 105 106 # we used to do this as a nice pipeline, but the problem is that $r fails to 107 # to be set properly if the tcpdump core dumps. 108 # 109 # Furthermore, on Windows, fc can't read the standard input, so we 110 # can't do it as a pipeline in any case. 111 if (index($options, "SPECIAL_t") != -1) { 112 # Hack to keep specific time options for tcp-handshake-micro-t, etc. 113 # -t, -tt, etc. 114 $options =~ s/ SPECIAL_t//; 115 } else { 116 # No specific time option, use -tttt 117 $options .= " -tttt"; 118 } 119 $r = system "$TCPDUMP -# -n -r $input $options >${newdir}/${outputbase} 2>${rawstderrlog}"; 120 121 if($r != 0) { 122 # 123 # Something other than "tcpdump opened the file, read it, and 124 # dissected all the packets". What happened? 125 # 126 # We write out an exit status after whatever the subprocess 127 # wrote out, so it shows up when we diff the expected output 128 # with it. 129 # 130 open(OUTPUT, ">>"."${newdir}/$outputbase") || die "fail to open $outputbase\n"; 131 if($r == -1) { 132 # failed to start due to error. 133 $status = $!; 134 printf OUTPUT "FAILED TO RUN: status: %d\n", $status; 135 } else { 136 if ($^O eq 'MSWin32' or $^O eq 'msys') { 137 # 138 # On Windows, the return value of system is the lower 8 139 # bits of the exit status of the process, shifted left 140 # 8 bits. 141 # 142 # If the process crashed, rather than exiting, the 143 # exit status will be one of the EXCEPTION_ values 144 # listed in the documentation for the GetExceptionCode() 145 # macro. 146 # 147 # Those are defined as STATUS_ values, which should have 148 # 0xC in the topmost 4 bits (being fatal error 149 # statuses); some of them have a value that fits in 150 # the lower 8 bits. We could, I guess, assume that 151 # any value that 1) isn't returned by tcpdump and 2) 152 # corresponds to the lower 8 bits of a STATUS_ value 153 # used as an EXCEPTION_ value indicates that tcpdump 154 # exited with that exception. 155 # 156 # However, as we're running tcpdump with system, which 157 # runs the command through cmd.exe, and as cmd.exe 158 # doesn't map the command's exit code to its own exit 159 # code in any straightforward manner, we can't get 160 # that information in any case, so there's no point 161 # in trying to interpret it in that fashion. 162 # 163 $status = $r >> 8; 164 } else { 165 # 166 # On UN*Xes, the return status is a POSIX as filled in 167 # by wait() or waitpid(). 168 # 169 # POSIX offers some calls for analyzing it, such as 170 # WIFSIGNALED() to test whether it indicates that the 171 # process was terminated by a signal, WTERMSIG() to 172 # get the signal number from it, WIFEXITED() to test 173 # whether it indicates that the process exited normally, 174 # and WEXITSTATUS() to get the exit status from it. 175 # 176 # POSIX doesn't standardize core dumps, so the POSIX 177 # calls can't test whether a core dump occurred. 178 # However, all the UN*Xes we are likely to encounter 179 # follow Research UNIX in this regard, with the exit 180 # status containing either 0 or a signal number in 181 # the lower 7 bits, with 0 meaning "exited rather 182 # than being terminated by a signal", the "core dumped" 183 # flag in the 0x80 bit, and, if the signal number is 184 # 0, the exit status in the next 8 bits up. 185 # 186 # This should be cleaned up to use the POSIX calls 187 # from the Perl library - and to define an additional 188 # WCOREDUMP() call to test the "core dumped" bit and 189 # use that. 190 # 191 # But note also that, as we're running tcpdump with 192 # system, which runs the command through a shell, if 193 # tcpdump crashes, we'll only know that if the shell 194 # maps the signal indication and uses that as its 195 # exit status. 196 # 197 # The good news is that the Bourne shell, and compatible 198 # shells, have traditionally done that. If the process 199 # for which the shell reports the exit status terminates 200 # with a signal, it adds 128 to the signal number and 201 # returns that as its exit status. (This is why the 202 # "this is now working right" behavior described in a 203 # comment below is occurring.) 204 # 205 # As tcpdump itself never returns with an exit status 206 # >= 128, we can try checking for an exit status with 207 # the 0x80 bit set and, if we have one, get the signal 208 # number from the lower 7 bits of the exit status. We 209 # can't get the "core dumped" indication from the 210 # shell's exit status; all we can do is check whether 211 # there's a core file. 212 # 213 if( $r & 128 ) { 214 $coredump = $r & 127; 215 } 216 if( WIFEXITED($r)) { 217 $status = WEXITSTATUS($r); 218 } 219 } 220 221 if($coredump || $status) { 222 printf OUTPUT "EXIT CODE %08x: dump:%d code: %d\n", $r, $coredump, $status; 223 } else { 224 printf OUTPUT "EXIT CODE %08x\n", $r; 225 } 226 $r = 0; 227 } 228 close(OUTPUT); 229 } 230 if($r == 0) { 231 # 232 # Compare tcpdump's output with what we think it should be. 233 # If tcpdump failed to produce output, we've produced our own 234 # "output" above, with the exit status. 235 # 236 if ($^O eq 'MSWin32') { 237 my $winoutput = File::Spec->canonpath($output); 238 my $winnewdir = File::Spec->canonpath($newdir); 239 my $windiffdir = File::Spec->canonpath($diffdir); 240 $r = system "fc /lb1000 /t /1 $winoutput ${winnewdir}\\$outputbase >${windiffdir}\\$outputbase.diff"; 241 $diffstat = $r >> 8; 242 } else { 243 $r = system "diff $diff_flags $output ${newdir}/$outputbase >${diffdir}/$outputbase.diff"; 244 $diffstat = WEXITSTATUS($r); 245 } 246 } 247 248 # process the standard error file, sanitize "reading from" line, 249 # and count lines 250 $linecount = 0; 251 open(ERRORRAW, "<" . $rawstderrlog); 252 open(ERROROUT, ">" . $stderrlog); 253 while(<ERRORRAW>) { 254 next if /^$/; # blank lines are boring 255 if(/^(reading from file )(.*)(,.*)$/) { 256 my $filename = basename($2); 257 print ERROROUT "${1}${filename}${3}\n"; 258 next; 259 } 260 print ERROROUT; 261 $linecount++; 262 } 263 close(ERROROUT); 264 close(ERRORRAW); 265 266 if ( -f "$output.stderr" ) { 267 # 268 # Compare the standard error with what we think it should be. 269 # 270 if ($^O eq 'MSWin32') { 271 my $winoutput = File::Spec->canonpath($output); 272 my $windiffdir = File::Spec->canonpath($diffdir); 273 my $canonstderrlog = File::Spec->canonpath($stderrlog); 274 $nr = system "fc /lb1000 /t /1 $winoutput.stderr $canonstderrlog >${windiffdir}\\$outputbase.stderr.diff"; 275 $errdiffstat = $nr >> 8; 276 } else { 277 $nr = system "diff $output.stderr $stderrlog >${diffdir}/$outputbase.stderr.diff"; 278 $errdiffstat = WEXITSTATUS($nr); 279 } 280 if($r == 0) { 281 $r = $nr; 282 } 283 } 284 285 if($r == 0) { 286 if($linecount == 0 && $status == 0) { 287 unlink($stderrlog); 288 } else { 289 $errdiffstat = 1; 290 } 291 } 292 293 #print sprintf("END: %08x\n", $r); 294 295 if($r == 0) { 296 if($linecount == 0) { 297 printf " %-40s: passed\n", $name; 298 } else { 299 printf " %-40s: passed with error messages:\n", $name; 300 showfile($stderrlog); 301 } 302 unlink "${diffdir}/$outputbase.diff"; 303 return 0; 304 } 305 # must have failed! 306 printf " %-40s: TEST FAILED(exit core=%d/diffstat=%d,%d/r=%d)", $name, $coredump, $diffstat, $errdiffstat, $r; 307 open FOUT, '>>tests/failure-outputs.txt'; 308 printf FOUT "\nFailed test: $name\n\n"; 309 close FOUT; 310 if(-f "${diffdir}/$outputbase.diff") { 311 # 312 # XXX - just do this directly in Perl? 313 # 314 if ($^O eq 'MSWin32') { 315 my $windiffdir = File::Spec->canonpath($diffdir); 316 system "type ${windiffdir}\\$outputbase.diff >> tests\\failure-outputs.txt"; 317 } else { 318 system "cat ${diffdir}/$outputbase.diff >> tests/failure-outputs.txt"; 319 } 320 } 321 322 if($r == -1) { 323 print " (failed to execute: $!)\n"; 324 return(30); 325 } 326 327 # this is not working right, $r == 0x8b00 when there is a core dump. 328 # clearly, we need some platform specific perl magic to take this apart, so look for "core" 329 # too. 330 # In particular, on Solaris 10 SPARC an alignment problem results in SIGILL, 331 # a core dump and $r set to 0x00008a00 ($? == 138 in the shell). 332 if($r & 127 || -f "core") { 333 my $with = ($r & 128) ? 'with' : 'without'; 334 if(-f "core") { 335 $with = "with"; 336 } 337 printf " (terminated with signal %u, %s coredump)", ($r & 127), $with; 338 if($linecount == 0) { 339 print "\n"; 340 } else { 341 print " with error messages:\n"; 342 showfile($stderrlog); 343 } 344 return(($r & 128) ? 10 : 20); 345 } 346 if($linecount == 0) { 347 print "\n"; 348 } else { 349 print " with error messages:\n"; 350 showfile($stderrlog); 351 } 352 return(5); 353} 354 355sub loadconfighash { 356 if(defined($confighhash)) { 357 return $confighhash; 358 } 359 360 $main::confighhash = {}; 361 362 # this could be loaded once perhaps. 363 open(CONFIG_H, "config.h") || die "Can not open config.h: $!\n"; 364 while(<CONFIG_H>) { 365 chomp; 366 if(/^\#define (.*) 1/) { 367 #print "Setting $1\n"; 368 $main::confighhash->{$1} = 1; 369 } 370 } 371 close(CONFIG_H); 372 #print Dumper($main::confighhash); 373 374 # also run tcpdump --fp-type to get the type of floating-point 375 # arithmetic we're doing, setting a HAVE_{fptype} key based 376 # on the value it prints 377 open(FPTYPE_PIPE, "$TCPDUMP --fp-type |") or die("piping tcpdump --fp-type failed\n"); 378 my $fptype_val = <FPTYPE_PIPE>; 379 close(FPTYPE_PIPE); 380 my $have_fptype; 381 if($fptype_val == "9877.895") { 382 $have_fptype = "HAVE_FPTYPE1"; 383 } else { 384 $have_fptype = "HAVE_FPTYPE2"; 385 } 386 printf "$TCPDUMP --fp-type => %s\n", $have_fptype; 387 $main::confighhash->{$have_fptype} = 1; 388 389 # and check whether this is OpenBSD, as one test fails in OpenBSD 390 # due to the sad hellscape of low-numbered DLT_ values, due to 391 # 12 meaning "OpenBSD loopback" rather than "raw IP" on OpenBSD 392 if($^O eq "openbsd") { 393 $main::confighhash->{"IS_OPENBSD"} = 1; 394 } 395 396 return $main::confighhash; 397} 398 399 400sub runOneComplexTest { 401 local($testconfig) = @_; 402 403 my $output = $testconfig->{output}; 404 my $input = $testconfig->{input}; 405 my $name = $testconfig->{name}; 406 my $options= $testconfig->{args}; 407 my $foundit = 1; 408 my $unfoundit=1; 409 410 my $configset = $testconfig->{config_set}; 411 my $configunset = $testconfig->{config_unset}; 412 my $ch = loadconfighash(); 413 #print Dumper($ch); 414 415 if(defined($configset)) { 416 $foundit = ($ch->{$configset} == 1); 417 } 418 if(defined($configunset)) { 419 $unfoundit=($ch->{$configunset} != 1); 420 } 421 422 if(!$foundit) { 423 printf " %-40s: skipped (%s not set)\n", $name, $configset; 424 $skippedcount++; 425 return 0; 426 } 427 428 if(!$unfoundit) { 429 printf " %-40s: skipped (%s set)\n", $name, $configunset; 430 $skippedcount++; 431 return 0; 432 } 433 434 #use Data::Dumper; 435 #print Dumper($testconfig); 436 437 # EXPAND any occurrences of @TESTDIR@ to $testsdir 438 $options =~ s/\@TESTDIR\@/$testsdir/; 439 440 my $result = runtest($name, 441 $testsdir . "/" . $input, 442 $testsdir . "/" . $output, 443 $options); 444 445 if($result == 0) { 446 $passedcount++; 447 } else { 448 $failedcount++; 449 } 450} 451 452# *.tests files are PERL hash definitions. They should create an array of hashes 453# one per test, and place it into the variable @testlist. 454sub runComplexTests { 455 my @files = glob( $testsdir . '/*.tests' ); 456 foreach $file (@files) { 457 my @testlist = undef; 458 my $definitions; 459 print "FILE: ${file}\n"; 460 open(FILE, "<".$file) || die "can not open $file: $!"; 461 { 462 local $/ = undef; 463 $definitions = <FILE>; 464 } 465 close(FILE); 466 #print "STUFF: ${definitions}\n"; 467 eval $definitions; 468 if(defined($testlist)) { 469 #use Data::Dumper; 470 #print Dumper($testlist); 471 foreach $test (@$testlist) { 472 runOneComplexTest($test); 473 } 474 } else { 475 warn "File: ${file} could not be loaded as PERL: $!"; 476 } 477 } 478} 479 480sub runSimpleTests { 481 482 local($only)=@_; 483 484 open(TESTLIST, "<" . "${testsdir}/TESTLIST") || die "no ${testsdir}/TESTFILE: $!\n"; 485 while(<TESTLIST>) { 486 next if /^\#/; 487 next if /^$/; 488 489 unlink("core"); 490 ($name, $input, $output, @options) = split; 491 #print "processing ${only} vs ${name}\n"; 492 next if(defined($only) && $only ne $name); 493 494 my $options = join(" ", @options); 495 #print "@{options} becomes ${options}\n"; 496 497 my $hash = { name => $name, 498 input=> $input, 499 output=>$output, 500 args => $options }; 501 502 runOneComplexTest($hash); 503 } 504} 505 506if(scalar(@ARGV) == 0) { 507 runSimpleTests(); 508 runComplexTests(); 509} else { 510 runSimpleTests($ARGV[0]); 511} 512 513# exit with number of failing tests. 514print "------------------------------------------------\n"; 515printf("%4u tests skipped\n",$skippedcount); 516printf("%4u tests failed\n",$failedcount); 517printf("%4u tests passed\n",$passedcount); 518 519showfile(${failureoutput}); 520exit $failedcount; 521