Last active
August 29, 2023 10:29
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add coloursed diff output in
print_diff()
.