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