Skip to content

Instantly share code, notes, and snippets.

@gvanem
Last active August 29, 2023 10:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gvanem/5c3b359d5f1fc623035e938307f894a5 to your computer and use it in GitHub Desktop.
Save gvanem/5c3b359d5f1fc623035e938307f894a5 to your computer and use it in GitHub Desktop.
Tcpdump test-script in Python (not any horrid Perl stuff). With colourised output. Put in tcpdump's "tests" directory.
#!/usr/bin/env python
#
# TESTrun.py; my feeble attempt at a Python version of
# the Perl-script 'TESTrun'.
#
# G. Vanem <gisle.vanem@gmail.com> 2015 - 2023.
#
# Reads './TESTLIST' and by default perform all the tcpdump tests specified
# therein (test-spec="*"). Use option '-l' to list them.
#
from __future__ import print_function
import os, sys, subprocess, fnmatch, argparse, shutil
try:
import difflib
except ImportError:
raise SystemExit ("Failed to load 'difflib'")
class Colour:
GREEN = RED = WHITE = RESET = ""
try:
from colorama import init, Fore, Style
init()
Colour.GREEN = Fore.GREEN + Style.BRIGHT
Colour.RED = Fore.RED + Style.BRIGHT
Colour.WHITE = Fore.WHITE + Style.BRIGHT
Colour.RESET = Style.RESET_ALL
except ImportError:
pass
opt = None
who_am_I = os.path.realpath (sys.argv[0])
my_dir = os.path.dirname (who_am_I)
OUT_py_dir = "OUTPUT_py"
DIFF_py_dir = "DIFF_py"
fail_file = "failure-outputs_py.txt"
#
# Indices into the test array.
#
TEST_NAME = 0
PCAP_FILE = 1
OUT_NAME = 2
OTHER_ARGS = 3
if os.sys.platform == "win32":
tcpdump = "..\\windump.exe"
dev_null = "NUL"
elif os.sys.platform == "cygwin":
tcpdump = "../windump.exe"
dev_null = "/dev/null"
else:
tcpdump = "../tcpdump"
dev_null = "/dev/null"
def trace (level, s):
if opt.verbose >= level:
print (s, end="")
def error (s, prefix=""):
if s is None:
s = ""
print ("%s%s\n" % (prefix, s), file=sys.stderr)
sys.exit (1)
def fatal (s):
error (s, "Fatal error: ")
#
# Run a single test.
#
# Force UTC, so time stamps are printed in a standard time zone, and
# tests don't have to be run in the time zone in which the output
# file was generated.
#
# Also reset any trace-levels I may have enabled in libpcap or elsewhere.
#
def run_single_test (test_name, args):
os.environ ["TZ"] = "GMT0"
os.environ ["PCAP_TRACE"] = "0"
os.environ ["WSOCK_TRACE_LEVEL"] = "0"
cmd = tcpdump + " " + args
if opt.verbose <= 2:
cmd += " 2> " + dev_null
trace (1-opt.dry_run, "%-50s: \"%s\"\n" % (Colour.GREEN + test_name + Colour.RESET, cmd))
if opt.dry_run:
return 0
return subprocess.call (cmd, shell=True)
#
# Open (or create) a file for write, read or append.
#
def open_file (fname, mode):
try:
return open (fname, mode)
except (IOError, NameError):
fatal ("Failed to open %s." % fname)
#
# Open and read entire file (mode is always text).
# Return content as list of lines with no trailing white-space or newlines.
#
def read_file (fname):
lines = []
f = open_file (fname, "rt")
for l in f.readlines():
l = l.rstrip (" \t\r\n")
lines.append (l)
f.close()
return lines
def make_dir (d):
try:
os.mkdir (d)
except OSError:
pass
#
# Compare contents of file1 and file2.
# If the result differ, do:
# 1) write a unified (n=2 context lines) to 'fdiff' file.
# 2) append diffs to 'fail_file'.
#
# Return number of lines that differ; >= 0.
#
# Also return number of lines that differ only in timestamps.
# I.e. lines that contains '([localtime() or gmtime()...'.
#
def get_diffs (file1, file2, fdiff, new_exit_str, fail_file):
set1 = read_file (file1)
set2 = read_file (file2)
if new_exit_str:
set1.append (new_exit_str)
trace (4, "set1[-1]: '%s'\n" % set1[-1])
num_time_diffs = 0
diffs = []
for l in difflib.unified_diff (set1, set2, n=2, tofile=file2, fromfile=file1):
l = l.rstrip ("\r\n")
diffs.append (l + "\n")
if l.find ("([localtime() or gmtime()") > 0:
num_time_diffs += 1
if len(diffs) > 0:
f = open_file (fdiff, "wt")
f.writelines (diffs)
f.close()
f = open_file (fail_file, "a+t")
f.writelines (diffs)
f.close()
return len(diffs), num_time_diffs
def print_diff (s):
if len(s) > 1:
if s[0:2] == "@@":
print ("%s%s%s" % (Colour.WHITE, s, Colour.RESET))
return 0
if s[0] == "+":
print ("%s%s%s" % (Colour.GREEN, s, Colour.RESET))
if s[0:3] == "+++":
return 1
return 0
if s[0] == "-":
print ("%s%s%s" % (Colour.RED, s, Colour.RESET))
return 0
print (s)
return 0
#
# Run tests matching 'opt.test_spec'
# Dump any failed tests (and the context diffs) to the fail_file.
#
def run_tests (tests, fail_file):
if opt.dry_run == 0:
make_dir (OUT_py_dir)
make_dir (DIFF_py_dir)
num_ok = num_err = num_ran = 0
time_err = []
other_err = []
for t in tests:
test_name = t [TEST_NAME]
if not fnmatch.fnmatch (test_name, opt.test_spec):
continue
num_ran += 1
pcap_file = t [PCAP_FILE]
other_args = t [OTHER_ARGS]
out_name = t [OUT_NAME]
out_fname = OUT_py_dir + "/" + out_name
diff_name = DIFF_py_dir + "/" + out_name + ".diff"
cmd = "-#nr " + pcap_file + " " + other_args
cmd += " > %s/%s" % (OUT_py_dir, out_name)
rc = run_single_test (test_name, cmd)
# Do as Perl does: convert rc from the external command into Perl's
# upper 8 bits of it's rc.
#
if rc == 1:
rc_str = ("EXIT CODE %08x: dump:%d code: %d" % (0x100, 0, rc))
else:
rc_str = ""
if opt.dry_run:
is_diff = is_time_diff = 0
else:
is_diff, is_time_diff = get_diffs (out_fname, out_name, diff_name, rc_str, fail_file)
if is_time_diff:
time_err.append (test_name)
elif is_diff:
other_err.append (test_name)
if is_diff:
trace (opt.dry_run, "%-40s : %sFailed%s %s\n" % (test_name, Colour.RED, Colour.RESET, rc_str))
num_err += 1
else:
trace (opt.dry_run, "%-40s : %sPassed%s %s\n" % (test_name, Colour.GREEN, Colour.RESET, rc_str))
num_ok += 1
return num_ok, num_err, num_ran, time_err, other_err
#
# Print some help (do not let argparse() do it).
#
def show_help():
print (""" usage: %s [options] [test_spec]
test_spec: runs tests matching <test-spec> (default '*')
options:
-h, --help Show this help message and exit
-c, --clean Clean-up after a prior run
-f, --diff Show the diffs from a prior run
-n, --dry-run Do nothing (dry-run)
-l, --list List all tests and exit
-v, --verbose Increase verbosity level""" % who_am_I)
return 0
#
# Parse the cmd-line
#
def parse_cmdline():
parser = argparse.ArgumentParser (add_help = False)
parser.add_argument ("-c", "--clean", dest = "clean", action ="store_true")
parser.add_argument ("-f", "--diff", dest = "fails", action ="store_true")
parser.add_argument ("-h", "--help", dest = "help", action ="store_true")
parser.add_argument ("-l", "--list", dest = "list", action ="store_true")
parser.add_argument ("-n", "--dry-run", dest = "dry_run", action ="store_true")
parser.add_argument ("-v", "--verbose", dest = "verbose", action ="count", default = 0)
parser.add_argument ("test_spec", nargs = "?", default = "*")
return parser.parse_args()
#
# Print a list of all tests and exit
#
def list_tests():
i = 0
tests = get_tests ("./TESTLIST")
print ("The list of test(s) that can be specified by a <test-spec>, are:")
for t in tests:
if not fnmatch.fnmatch (t[TEST_NAME], opt.test_spec):
continue
print (" %-36s" % t[TEST_NAME], end="")
i += 1
if (i % 3) == 0:
print ("")
print ("\n%d tests in total. E.g. \"ospf*\" can be used to run only the OSPF tests." % len(tests))
return 0
#
# Parse a line from "./TESTLIST".
#
def get_test_line (line, s):
s = s.strip()
if not s or s[0] == "\n" or s[0] == "#":
return 0, None, None, None, None
trace (4, "line %3d: '%s'\n" % (line, s))
parts = s.split (None, 3)
trace (4, " %s\n" % parts)
try:
return 1, parts [TEST_NAME], parts [PCAP_FILE], parts [OUT_NAME], parts [OTHER_ARGS]
except IndexError:
return 1, parts [TEST_NAME], parts [PCAP_FILE], parts [OUT_NAME], ""
#
# Return a list of all the tests from "./TESTLIST".
#
def get_tests (fname):
tests = []
line_num = 0
for l in read_file (fname):
line_num += 1
rc, t_name, pcap_file, out_name, other_args = get_test_line (line_num, l)
if not rc:
continue
if t_name in tests:
fatal ("%s (%u): test %s is already in list of tests." % \
(fname, line_num, t_name))
tests.append ([t_name, pcap_file, out_name, other_args])
return sorted (tests)
def cleanup():
try:
trace (1, "Removing 'fail_file=%s': " % fail_file)
os.remove (fail_file)
trace (1, "Ok\n")
except:
trace (1, "Failed\n")
pass
trace (1, "Removing 'OUT_py_dir=%s'\n" % OUT_py_dir)
shutil.rmtree (OUT_py_dir, True)
trace (1, "Removing 'DIFF_py_dir=%s'\n" % DIFF_py_dir)
shutil.rmtree (DIFF_py_dir, True)
print ("cleanup okay")
def show_fail_file (show_errs):
if not os.path.exists (fail_file):
print ("No %s file found" % fail_file)
else:
print ("%s details:" % fail_file)
errs = 0
for l in read_file (fail_file):
errs += print_diff (l)
if show_errs:
print ("\n%d tests had errors." % errs)
else:
print ("")
return 0
#######################################################################
opt = parse_cmdline()
trace (3, "os.getcwd(): %s, my_dir: %s\n" % (os.getcwd(), my_dir))
if opt.help:
sys.exit (show_help())
if opt.fails:
sys.exit (show_fail_file(True))
if os.getcwd() != my_dir or not os.path.exists("./TESTLIST"):
print ("Do NOT run this script outside the tcpdump ./tests dir")
sys.exit (1)
if opt.list:
sys.exit (list_tests())
if opt.clean:
sys.exit (cleanup())
try:
if os.path.exists (fail_file):
os.remove (fail_file)
tests = get_tests ("./TESTLIST")
num_okay, num_fail, num_ran, time_err, other_err = run_tests (tests, fail_file)
except KeyboardInterrupt:
error ("Interrupted")
except IOError:
fatal ("IOerror")
if num_ran == 0:
print ("No tests matched test-spec '%s'" % opt.test_spec)
else:
if num_fail > 0:
show_fail_file (False)
print ("%3u tests passed" % num_okay)
print ("%3u tests had errors with broken timestamps:" % len(time_err))
for e in time_err:
print (" %s" % e)
print ("%3u tests had other failures:" % len(other_err))
for e in other_err:
print (" %s" % e)
@gvanem
Copy link
Author

gvanem commented Oct 20, 2017

Add coloursed diff output in print_diff().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment