blob: 3b1f9c6399bb7704b620f1a1c5bfaf371c177a3e [file]
#!/usr/bin/env python3
"""
Cross-platform packet decode test runner for OpenCSD.
This replaces the shell-only packet decode test scripts with a single Python
entry point that runs on Linux, macOS, and Windows.
"""
from __future__ import annotations
import argparse
import difflib
import os
import platform
import re
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Iterable, Sequence
STANDARD_DECODE_TESTS = (
"a57_single_step",
"armv8_1m_branches",
"bugfix-exact-match",
"itm_only_csformat",
"itm_only_raw",
"juno_r1_1",
"juno-ret-stck",
"juno-uname-001",
"juno-uname-002",
"nmi_partial_exec",
"Snowball",
"stm-issue-27",
"stm_only",
"stm_only-2",
"stm_only-juno",
"TC2",
"tc2-ptm-rstk-t32",
"test-file-mem-offsets",
"trace_cov_a15",
"v7m_svc_vector",
"v8m_svc_vector",
)
ETE_DECODE_TESTS = (
"001-ack_test",
"002-ack_test_scr",
"ete-bc-instr",
"ete_ip",
"ete-ite-instr",
"ete_mem",
"ete_spec_1",
"ete_spec_2",
"ete_spec_3",
"ete-wfet",
"event_test",
"feat_cmpbr",
"infrastructure",
"maxspec0_commopt1",
"maxspec78_commopt0",
"pauth_lr",
"pauth_lr_Rm",
"q_elem",
"rme_test",
"s_9001",
"src_addr",
"ss_ib_el1ns",
"texit-poe2",
"tme_simple",
"tme_tcancel",
"tme_test",
"trace_file_cid_vmid",
"trace_file_vmid",
"ts_bit64_set",
"ts_marker",
)
ETE_SRC_ADDR_N_TESTS = (
"002-ack_test_scr",
"ete_ip",
"src_addr",
)
ETE_MULTI_SESSION_TESTS = (
"ss_ib_el1ns",
"ete-ite-instr",
"pauth_lr",
"pauth_lr_Rm",
"q_elem",
"rme_test",
"s_9001",
)
@dataclass(frozen=True)
class SuiteConfig:
name: str
out_dir: str
snapshot_dir: str
SUITES = {
"standard": SuiteConfig("standard", "results", "snapshots"),
"ete": SuiteConfig("ete", "results-ete", "snapshots-ete"),
}
class TestFailure(RuntimeError):
pass
@dataclass(frozen=True)
class CompareResultsSummary:
diff_counts_by_test: dict[str, int]
issue_count: int
@dataclass(frozen=True)
class CompareDirResolution:
current_out_dir: Path | None
baseline_dir: Path | None
issue: str | None
def is_windows() -> bool:
return os.name == "nt"
def exe_name(program: str) -> str:
return f"{program}.exe" if is_windows() else program
def prepend_env_path(env: dict[str, str], key: str, value: str) -> None:
current = env.get(key, "")
env[key] = value if not current else os.pathsep.join((value, current))
def platform_candidate_dirs(tests_dir: Path) -> list[Path]:
machine = platform.machine().lower()
bin_root = tests_dir / "bin"
candidates: list[Path] = []
if sys.platform.startswith("linux"):
if machine in {"aarch64", "arm64"}:
candidates.extend((bin_root / "linux-arm64" / "rel", bin_root / "builddir"))
elif machine in {"arm", "armv7l", "armv8l", "aarch32"}:
candidates.extend((bin_root / "linux-arm" / "rel", bin_root / "builddir"))
elif machine in {"x86", "i386", "i686"}:
candidates.extend((bin_root / "linux32" / "rel", bin_root / "builddir"))
else:
candidates.extend((bin_root / "linux64" / "rel", bin_root / "builddir"))
elif sys.platform == "darwin":
if machine in {"arm64", "aarch64"}:
candidates.extend((bin_root / "darwin-arm64" / "rel", bin_root / "builddir"))
else:
candidates.extend((bin_root / "darwin64" / "rel", bin_root / "builddir"))
elif is_windows():
if machine in {"arm64", "aarch64"}:
candidates.append(bin_root / "winarm64" / "rel")
elif machine in {"x86", "i386", "i686"}:
candidates.append(bin_root / "win32" / "rel")
else:
candidates.append(bin_root / "win64" / "rel")
candidates.extend(sorted(bin_root.glob("*/rel")))
candidates.append(bin_root / "builddir")
deduped: list[Path] = []
seen: set[Path] = set()
for candidate in candidates:
resolved = candidate.resolve()
if resolved not in seen:
deduped.append(candidate)
seen.add(resolved)
return deduped
def resolve_bin_dir(tests_dir: Path, explicit: str | None, use_installed: bool) -> Path | None:
if use_installed:
return None
if explicit:
bin_dir = Path(explicit).expanduser()
if not bin_dir.is_absolute():
bin_dir = (Path.cwd() / bin_dir).resolve()
if not (bin_dir / exe_name("trc_pkt_lister")).exists():
raise FileNotFoundError(
f"Binary directory does not contain {exe_name('trc_pkt_lister')}: {bin_dir}"
)
return bin_dir
for candidate in platform_candidate_dirs(tests_dir):
if (candidate / exe_name("trc_pkt_lister")).exists():
return candidate
searched = "\n".join(str(path) for path in platform_candidate_dirs(tests_dir))
raise FileNotFoundError(
"Could not locate trc_pkt_lister in a default test bin directory.\n"
f"Searched:\n{searched}\n"
"Use --bin-dir to specify the binary directory."
)
def suite_output_dir(tests_dir: Path, suite: SuiteConfig, suffix: str | None) -> Path:
out_dir = suite.out_dir
if suffix:
out_dir = f"{out_dir}-{suffix}"
return tests_dir / out_dir
def default_results_suffix() -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S")
def iter_result_files(results_dir: Path) -> Iterable[Path]:
return (
path for path in sorted(results_dir.rglob("*")) if path.is_file() and path.suffix != ".diff"
)
def relative_display_path(root_dir: Path, path: Path) -> str:
try:
return str(path.relative_to(root_dir))
except ValueError:
return str(path)
SNAPSHOT_PATH_RE = re.compile(r"(snapshots(?:-ete)?/.*)$")
SNAPSHOT_READ_PATH_RE = re.compile(
r"^(Trace Packet Lister : reading snapshot from path )(.+)$"
)
SNAPSHOT_NOT_FOUND_PATH_RE = re.compile(
r"^(Trace Packet Lister : Snapshot path)(.+?)( not found)$"
)
FILENAME_PATH_RE = re.compile(r"^(Filename=)(.+)$")
SNAPSHOT_RELATIVE_PREFIX_RE = re.compile(r"(?<!\w)\./(?=snapshots(?:-ete)?/)")
SNAPSHOT_ABSOLUTE_PREFIX_RE = re.compile(
r"(?:[A-Za-z]:)?(?:[^ \t:;=]+/)+(?=snapshots(?:-ete)?/)"
)
def normalize_snapshot_path_prefixes(text: str) -> str:
normalized = text.replace("\\", "/")
normalized = SNAPSHOT_RELATIVE_PREFIX_RE.sub("", normalized)
return SNAPSHOT_ABSOLUTE_PREFIX_RE.sub("", normalized)
def normalize_result_path(path_text: str) -> str:
normalized = normalize_snapshot_path_prefixes(path_text.strip())
match = SNAPSHOT_PATH_RE.search(normalized)
if match is not None:
return match.group(1)
trimmed = normalized.rstrip("/")
if "/" in trimmed:
return trimmed.rsplit("/", 1)[-1]
return normalized
def normalize_result_line(line: str) -> str:
if "/" not in line and "\\" not in line:
return line
line = normalize_snapshot_path_prefixes(line)
match = SNAPSHOT_READ_PATH_RE.match(line)
if match is not None:
return f"{match.group(1)}{normalize_result_path(match.group(2))}"
match = SNAPSHOT_NOT_FOUND_PATH_RE.match(line)
if match is not None:
return (
f"{match.group(1)}{normalize_result_path(match.group(2))}{match.group(3)}"
)
match = FILENAME_PATH_RE.match(line)
if match is not None:
return f"{match.group(1)}{normalize_result_path(match.group(2))}"
return line
def filter_result_lines(path: Path) -> list[str]:
filtered: list[str] = []
skip_next = False
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
if skip_next:
skip_next = False
continue
if "Version" in line:
continue
filtered.append(normalize_result_line(line))
if "Test Command Line" in line:
skip_next = True
return filtered
def is_suite_results_dir_name(candidate_name: str, suite: SuiteConfig) -> bool:
matched_out_dir: str | None = None
matched_length = -1
for candidate_suite in SUITES.values():
prefix = f"{candidate_suite.out_dir}-"
if candidate_name != candidate_suite.out_dir and not candidate_name.startswith(prefix):
continue
if len(candidate_suite.out_dir) > matched_length:
matched_out_dir = candidate_suite.out_dir
matched_length = len(candidate_suite.out_dir)
return matched_out_dir == suite.out_dir
def iter_suite_result_dirs(tests_dir: Path, suite: SuiteConfig) -> Iterable[Path]:
for candidate in tests_dir.iterdir():
if not candidate.is_dir():
continue
if is_suite_results_dir_name(candidate.name, suite):
yield candidate
def find_latest_results_dir(
tests_dir: Path,
suite: SuiteConfig,
exclude_dir: Path | None = None,
) -> Path | None:
exclude_resolved = exclude_dir.resolve() if exclude_dir is not None else None
candidates = [
candidate
for candidate in iter_suite_result_dirs(tests_dir, suite)
if exclude_resolved is None or candidate.resolve() != exclude_resolved
]
if not candidates:
return None
return max(candidates, key=lambda path: (path.stat().st_mtime_ns, path.name))
def find_previous_results_dir(
tests_dir: Path,
suite: SuiteConfig,
current_out_dir: Path,
) -> Path | None:
return find_latest_results_dir(tests_dir, suite, exclude_dir=current_out_dir)
def resolve_compare_dirs(
tests_dir: Path,
suite: SuiteConfig,
results_suffix: str | None,
diff_results_suffixes: Sequence[str],
diff_only: bool,
) -> CompareDirResolution:
if diff_only:
if len(diff_results_suffixes) == 1:
current_out_dir = find_latest_results_dir(tests_dir, suite)
if current_out_dir is None:
return CompareDirResolution(
None,
None,
f"no results directory found for suite {suite.name}",
)
baseline_dir = suite_output_dir(tests_dir, suite, diff_results_suffixes[0])
else:
current_out_dir = suite_output_dir(tests_dir, suite, diff_results_suffixes[0])
baseline_dir = suite_output_dir(tests_dir, suite, diff_results_suffixes[1])
else:
current_out_dir = suite_output_dir(tests_dir, suite, results_suffix)
if diff_results_suffixes:
baseline_dir = suite_output_dir(tests_dir, suite, diff_results_suffixes[0])
else:
baseline_dir = find_previous_results_dir(tests_dir, suite, current_out_dir)
if baseline_dir is None:
return CompareDirResolution(
current_out_dir,
None,
f"no previous results directory found for {current_out_dir.name}",
)
if current_out_dir.resolve() == baseline_dir.resolve():
return CompareDirResolution(
current_out_dir,
baseline_dir,
f"comparison results directory matches current results directory ({current_out_dir.name})",
)
if not baseline_dir.is_dir():
return CompareDirResolution(
current_out_dir,
baseline_dir,
f"comparison results directory does not exist: {baseline_dir}",
)
if diff_only and not current_out_dir.is_dir():
return CompareDirResolution(
current_out_dir,
baseline_dir,
f"current results directory does not exist: {current_out_dir}",
)
return CompareDirResolution(current_out_dir, baseline_dir, None)
def compare_results(
tests_dir: Path,
selected_suites: Sequence[str],
results_suffix: str | None,
diff_results_suffixes: Sequence[str],
diff_verbose: bool,
diff_only: bool,
) -> CompareResultsSummary:
diff_counts_by_test: dict[str, int] = {}
issue_count = 0
for suite_name in selected_suites:
suite = SUITES[suite_name]
resolution = resolve_compare_dirs(
tests_dir,
suite,
results_suffix,
diff_results_suffixes,
diff_only,
)
if resolution.issue is not None:
issue_count += 1
print(f"\nComparison issue for {suite_name}: {resolution.issue}")
continue
current_out_dir = resolution.current_out_dir
baseline_dir = resolution.baseline_dir
assert current_out_dir is not None
assert baseline_dir is not None
print(f"\nComparing {suite_name} results:")
print(f" Current : {current_out_dir}")
print(f" Against : {baseline_dir}")
current_files = list(iter_result_files(current_out_dir))
if not current_files:
issue_count += 1
print(f" Comparison issue: no result files found in {current_out_dir}")
continue
suite_differences = 0
for current_file in current_files:
relative_path = current_file.relative_to(current_out_dir)
baseline_file = baseline_dir / relative_path
diff_path = current_file.with_suffix(current_file.suffix + ".diff")
test_name = current_file.stem
summary_name = f"{suite_name}:{test_name}"
if not baseline_file.is_file():
suite_differences += 1
diff_counts_by_test[summary_name] = diff_counts_by_test.get(summary_name, 0) + 1
print(
" Missing comparison file for "
f"{relative_path}: {relative_display_path(tests_dir, baseline_file)}"
)
continue
baseline_lines = filter_result_lines(baseline_file)
current_lines = filter_result_lines(current_file)
if baseline_lines == current_lines:
if diff_path.exists():
try:
diff_path.unlink()
except OSError:
pass
continue
suite_differences += 1
diff_group_count = sum(
1
for tag, _, _, _, _ in difflib.SequenceMatcher(
None, baseline_lines, current_lines
).get_opcodes()
if tag != "equal"
)
diff_counts_by_test[summary_name] = (
diff_counts_by_test.get(summary_name, 0) + diff_group_count
)
diff_lines = list(
difflib.unified_diff(
baseline_lines,
current_lines,
fromfile=relative_display_path(tests_dir, baseline_file),
tofile=relative_display_path(tests_dir, current_file),
lineterm="",
)
)
diff_output = "\n".join(diff_lines)
diff_path.write_text(diff_output + "\n", encoding="utf-8")
print(f" Wrote diff file: {relative_display_path(tests_dir, diff_path)}")
if diff_verbose:
print("\n" + ("=" * 40))
print(f"Diff for {suite_name}:{relative_path}")
print("=" * 40)
print()
print(diff_output)
print()
print("=" * 40)
print()
if suite_differences == 0:
print(" No filtered differences found.")
return CompareResultsSummary(diff_counts_by_test=diff_counts_by_test, issue_count=issue_count)
def list_available_tests(selected_suites: Sequence[str]) -> None:
for suite_name in selected_suites:
print(f"{suite_name} suite tests:")
test_names = STANDARD_DECODE_TESTS if suite_name == "standard" else ETE_DECODE_TESTS
for test_name in test_names:
print(f" {test_name}")
def resolve_selected_suite(test_name: str, requested_suite: str | None) -> str:
in_standard = test_name in STANDARD_DECODE_TESTS
in_ete = test_name in ETE_DECODE_TESTS
if not in_standard and not in_ete:
raise ValueError(
f"Unknown test '{test_name}'. "
"Use a test name from STANDARD_DECODE_TESTS or ETE_DECODE_TESTS."
)
if requested_suite is None:
if in_standard and not in_ete:
return "standard"
if in_ete and not in_standard:
return "ete"
raise ValueError(f"Test '{test_name}' is ambiguous across suites.")
if requested_suite == "standard" and not in_standard:
raise ValueError(f"Test '{test_name}' is not part of the standard suite.")
if requested_suite == "ete" and not in_ete:
raise ValueError(f"Test '{test_name}' is not part of the ETE suite.")
return requested_suite
def base_env(bin_dir: Path | None, memacc_req_trace: bool = False) -> dict[str, str]:
env = os.environ.copy()
if memacc_req_trace:
env["OPENCSD_MEMACC_REQ_TRACE"] = "1"
if not bin_dir:
return env
bin_path = str(bin_dir)
if is_windows():
prepend_env_path(env, "PATH", bin_path)
elif sys.platform == "darwin":
prepend_env_path(env, "DYLD_LIBRARY_PATH", bin_path)
else:
prepend_env_path(env, "LD_LIBRARY_PATH", bin_path)
return env
def program_path(program: str, bin_dir: Path | None) -> str:
name = exe_name(program)
if bin_dir is None:
return name
return str((bin_dir / name).resolve())
def remove_if_exists(path: Path) -> None:
if path.exists():
path.unlink()
def move_output(src: Path, dst: Path) -> None:
if not src.exists():
raise TestFailure(f"Expected output file was not created: {src}")
dst.parent.mkdir(parents=True, exist_ok=True)
if dst.exists():
dst.unlink()
shutil.move(str(src), str(dst))
def run_command(
description: str,
command: Sequence[str],
cwd: Path,
env: dict[str, str],
quiet_stdout: bool = False,
) -> int:
print(description)
print(" " + " ".join(command))
stdout = subprocess.DEVNULL if quiet_stdout else None
completed = subprocess.run(command, cwd=str(cwd), env=env, stdout=stdout, check=False)
print(f"Done : Return {completed.returncode}")
return completed.returncode
def run_optional_command(
tool_name: str,
description: str,
command: Sequence[str],
cwd: Path,
env: dict[str, str],
quiet_stdout: bool = False,
) -> int | None:
try:
return run_command(description, command, cwd, env, quiet_stdout=quiet_stdout)
except OSError as exc:
print(f"Warning: skipping {tool_name}: failed to start program ({exc})")
return None
def run_standard_suite(
tests_dir: Path,
bin_dir: Path | None,
use_installed: bool,
results_suffix: str | None,
selected_test: str | None,
lister_args: Sequence[str],
memacc_req_trace: bool,
failures: list[str],
) -> None:
suite = SUITES["standard"]
out_dir = suite_output_dir(tests_dir, suite, results_suffix)
snapshot_dir = tests_dir / suite.snapshot_dir
out_dir.mkdir(parents=True, exist_ok=True)
env = base_env(bin_dir, memacc_req_trace)
trc_pkt_lister = program_path("trc_pkt_lister", bin_dir)
tests_to_run = (selected_test,) if selected_test else STANDARD_DECODE_TESTS
for test_dir in tests_to_run:
log_name = out_dir / f"{test_dir}.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / test_dir),
*lister_args,
"-decode",
"-no_time_print",
"-logfilename",
str(log_name),
]
if run_command(f"Testing {test_dir}...", cmd, tests_dir, env) != 0:
failures.append(test_dir)
if selected_test:
return
env_range = dict(env)
env_range["OPENCSD_INSTR_RANGE_LIMIT"] = "100"
log_name = out_dir / "juno_r1_1_rangelimit.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / "juno_r1_1"),
*lister_args,
"-decode",
"-no_time_print",
"-logfilename",
str(log_name),
]
if run_command("Test with run limit on...", cmd, tests_dir, env_range) != 0:
failures.append("juno_r1_1_rangelimit")
env_bad_opcode = dict(env)
env_bad_opcode["OPENCSD_ERR_ON_AA64_BAD_OPCODE"] = "1"
log_name = out_dir / "juno_r1_1_badopcode.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / "juno_r1_1"),
*lister_args,
"-decode",
"-no_time_print",
"-logfilename",
str(log_name),
]
if run_command("Test with bad opcode detect on using env var...", cmd, tests_dir, env_bad_opcode) != 0:
failures.append("juno_r1_1_badopcode")
log_name = out_dir / "juno_r1_1_badopcode_flag.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / "juno_r1_1"),
*lister_args,
"-decode",
"-no_time_print",
"-aa64_opcode_chk",
"-logfilename",
str(log_name),
]
if run_command("Test with bad opcode detect on using flag...", cmd, tests_dir, env) != 0:
failures.append("juno_r1_1_badopcode_flag")
log_name = out_dir / "init-short-addr.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / "init-short-addr"),
*lister_args,
"-pkt_mon",
"-no_time_print",
"-logfilename",
str(log_name),
]
if run_command("Testing init-short-addr...", cmd, tests_dir, env) != 0:
failures.append("init-short-addr")
log_name = out_dir / "a55-test-tpiu.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / "a55-test-tpiu"),
*lister_args,
"-dstream_format",
"-no_time_print",
"-o_raw_packed",
"-o_raw_unpacked",
"-logfilename",
str(log_name),
]
if run_command("Testing a55-test-tpiu...", cmd, tests_dir, env) != 0:
failures.append("a55-test-tpiu")
if use_installed:
return
c_api_log = tests_dir / "c_api_test.log"
remove_if_exists(c_api_log)
cmd = [
program_path("c_api_pkt_print_test", bin_dir),
"-ss_path",
str(snapshot_dir),
"-decode",
]
rc = run_optional_command(
"c_api_pkt_print_test",
"Testing C-API library...",
cmd,
tests_dir,
env,
quiet_stdout=True,
)
if rc is None:
pass
elif rc != 0:
failures.append("c_api_pkt_print_test")
else:
move_output(c_api_log, out_dir / "c_api_test.ppl")
frame_log = tests_dir / "frame_demux_test.ppl"
remove_if_exists(frame_log)
cmd = [program_path("frame-demux-test", bin_dir)]
rc = run_optional_command(
"frame-demux-test",
"Running Frame demux test...",
cmd,
tests_dir,
env,
quiet_stdout=True,
)
if rc is None:
pass
elif rc != 0:
failures.append("frame-demux-test")
else:
move_output(frame_log, out_dir / "frame_demux_test.ppl")
for leftover in tests_dir.glob("mem_buff_demo*.ppl"):
leftover.unlink()
cmd = [
program_path("mem-buffer-eg", bin_dir),
"-logfile",
"-ss_path",
str(snapshot_dir),
"-noprint",
]
rc = run_optional_command(
"mem-buffer-eg",
"Running mem-buffer-eg with memory buffer...",
cmd,
tests_dir,
env,
)
if rc is None:
pass
elif rc != 0:
failures.append("mem-buffer-eg")
cmd = [
program_path("mem-buffer-eg", bin_dir),
"-logfile",
"-ss_path",
str(snapshot_dir),
"-noprint",
"-callback",
]
rc = run_optional_command(
"mem-buffer-eg",
"Running mem-buffer-eg with callback function...",
cmd,
tests_dir,
env,
)
if rc is None:
pass
elif rc != 0:
failures.append("mem-buffer-eg-callback")
for produced in sorted(tests_dir.glob("mem_buff_demo*.ppl")):
move_output(produced, out_dir / produced.name)
itm_log = out_dir / "itm-decode-test.ppl"
cmd = [
program_path("itm-decode-test", bin_dir),
"-logfilename",
str(itm_log),
]
rc = run_optional_command(
"itm-decode-test",
"Running ITM decoder test...",
cmd,
tests_dir,
env,
)
if rc is None:
pass
elif rc != 0:
failures.append("itm-decode-test")
def run_ete_suite(
tests_dir: Path,
bin_dir: Path | None,
results_suffix: str | None,
selected_test: str | None,
lister_args: Sequence[str],
memacc_req_trace: bool,
failures: list[str],
) -> None:
suite = SUITES["ete"]
out_dir = suite_output_dir(tests_dir, suite, results_suffix)
snapshot_dir = tests_dir / suite.snapshot_dir
out_dir.mkdir(parents=True, exist_ok=True)
env = base_env(bin_dir, memacc_req_trace)
trc_pkt_lister = program_path("trc_pkt_lister", bin_dir)
tests_to_run = (selected_test,) if selected_test else ETE_DECODE_TESTS
for test_dir in tests_to_run:
log_name = out_dir / f"{test_dir}.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / test_dir),
*lister_args,
"-decode",
"-no_time_print",
"-logfilename",
str(log_name),
]
if run_command(f"Testing {test_dir}...", cmd, tests_dir, env) != 0:
failures.append(f"ete:{test_dir}")
if selected_test:
return
for test_dir in ETE_SRC_ADDR_N_TESTS:
log_name = out_dir / f"{test_dir}_src_addr_N.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / test_dir),
*lister_args,
"-decode",
"-no_time_print",
"-src_addr_n",
"-logfilename",
str(log_name),
]
if run_command(f"Testing with -src_addr_n {test_dir}...", cmd, tests_dir, env) != 0:
failures.append(f"ete:{test_dir}_src_addr_N")
for test_dir in ETE_MULTI_SESSION_TESTS:
log_name = out_dir / f"{test_dir}_multi_sess.ppl"
cmd = [
trc_pkt_lister,
"-ss_dir",
str(snapshot_dir / test_dir),
*lister_args,
"-decode",
"-no_time_print",
"-multi_session",
"-logfilename",
str(log_name),
]
if run_command(f"Testing with -multi_session {test_dir}...", cmd, tests_dir, env) != 0:
failures.append(f"ete:{test_dir}_multi_sess")
def parse_args(argv: Sequence[str]) -> tuple[argparse.Namespace, list[str]]:
parser = argparse.ArgumentParser(
description="Run OpenCSD packet decode regression tests on Linux, macOS, or Windows.",
epilog="Pass additional trc_pkt_lister arguments only after '--'.",
)
parser.add_argument(
"--suite",
choices=("standard", "ete"),
help="Run only the selected suite. Defaults to running both suites.",
)
parser.add_argument(
"--use-installed",
action="store_true",
help="Use trc_pkt_lister from PATH instead of a repository test binary directory.",
)
parser.add_argument(
"--bin-dir",
help="Directory containing trc_pkt_lister and related test binaries.",
)
parser.add_argument(
"--list-only",
action="store_true",
help="Print the resolved binary directory and selected suites without running tests.",
)
parser.add_argument(
"--list-tests",
action="store_true",
help="List the available tests in the selected suite or suites and exit.",
)
parser.add_argument(
"--results-suffix",
help=(
"Append '-<suffix>' to each suite's results directory name. "
"Defaults to a timestamp in YYYYMMDD_HHMMSS format."
),
)
parser.add_argument(
"--diff-previous",
action="store_true",
help=(
"After the requested suites finish, compare each current results file against "
"the matching file from the most recent previous results directory."
),
)
parser.add_argument(
"--diff-results-suffix",
action="append",
help=(
"Specify a results directory suffix to use for comparison. May be supplied once "
"for normal post-run compare, or once / twice with --diff-only."
),
)
parser.add_argument(
"--diff-only",
action="store_true",
help="Run only the result comparison step without running any tests.",
)
parser.add_argument(
"--diff-verbose",
action="store_true",
help="Print unified diffs to stdout as well as writing .diff files in the current results directory.",
)
parser.add_argument(
"--test",
help="Run a single named test from the standard or ETE decode test lists.",
)
parser.add_argument(
"--memacc-req-trace",
action="store_true",
help="Set OPENCSD_MEMACC_REQ_TRACE=1 for every test process started by this runner.",
)
passthrough: list[str] = []
parseable_argv = list(argv)
if "--" in parseable_argv:
separator_index = parseable_argv.index("--")
passthrough = parseable_argv[separator_index + 1 :]
parseable_argv = parseable_argv[:separator_index]
namespace = parser.parse_args(parseable_argv)
diff_results_suffixes = namespace.diff_results_suffix or []
if namespace.diff_previous and diff_results_suffixes:
parser.error("--diff-previous cannot be used with --diff-results-suffix")
if len(diff_results_suffixes) > 2:
parser.error("--diff-results-suffix may be specified at most twice")
if namespace.diff_only:
if namespace.diff_previous:
parser.error("--diff-only cannot be used with --diff-previous")
if len(diff_results_suffixes) not in {1, 2}:
parser.error("--diff-only requires --diff-results-suffix to be used 1 or 2 times")
if len(diff_results_suffixes) == 2 and diff_results_suffixes[0] == diff_results_suffixes[1]:
parser.error("the two --diff-results-suffix values must be different")
elif len(diff_results_suffixes) > 1:
parser.error("without --diff-only, --diff-results-suffix may be specified only once")
return namespace, passthrough
def main(argv: Sequence[str]) -> int:
args, lister_args = parse_args(argv)
tests_dir = Path(__file__).resolve().parent
diff_results_suffixes = args.diff_results_suffix or []
results_suffix = None if args.diff_only else (args.results_suffix or default_results_suffix())
diff_requested = args.diff_only or args.diff_previous or bool(diff_results_suffixes)
selected_suite = args.suite
selected_suites = ("standard", "ete") if selected_suite is None else (selected_suite,)
if args.list_tests:
list_available_tests(selected_suites)
return 0
try:
if args.test:
selected_suite = resolve_selected_suite(args.test, args.suite)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 2
bin_dir: Path | None = None
if not args.diff_only:
try:
bin_dir = resolve_bin_dir(tests_dir, args.bin_dir, args.use_installed)
except FileNotFoundError as exc:
print(str(exc), file=sys.stderr)
return 2
print("Running trc_pkt_lister on snapshot directories.")
if bin_dir is None:
print("Tests using installed binaries.")
else:
print(f"Tests using BIN_DIR = {bin_dir}")
else:
print("Running diff-only comparison on existing results directories.")
selected_suites = ("standard", "ete") if selected_suite is None else (selected_suite,)
print(f"Selected suites: {', '.join(selected_suites)}")
if args.test:
print(f"Selected test: {args.test}")
if results_suffix is not None:
print(f"Results suffix: {results_suffix}")
if diff_requested:
if args.diff_only:
if len(diff_results_suffixes) == 1:
print(
"Diff-only mode: comparing the most recent results directory for each selected "
"suite against the supplied suffix."
)
print(f"Diff-only baseline suffix: {diff_results_suffixes[0]}")
else:
print(f"Diff-only current suffix: {diff_results_suffixes[0]}")
print(f"Diff-only baseline suffix: {diff_results_suffixes[1]}")
elif not diff_results_suffixes:
print("Post-run diff: comparing against the most recent previous results directory.")
else:
print(f"Post-run diff suffix: {diff_results_suffixes[0]}")
if args.diff_verbose:
print("Post-run diff verbosity: enabled")
if args.memacc_req_trace:
print("Environment override: OPENCSD_MEMACC_REQ_TRACE=1")
for suite_name in selected_suites:
suite = SUITES[suite_name]
if results_suffix is not None:
print(f"Results dir for {suite_name}: {suite_output_dir(tests_dir, suite, results_suffix)}")
if diff_requested:
resolution = resolve_compare_dirs(
tests_dir,
suite,
results_suffix,
diff_results_suffixes,
args.diff_only,
)
if resolution.issue is None:
print(f"Compare current dir for {suite_name}: {resolution.current_out_dir}")
print(f"Compare baseline dir for {suite_name}: {resolution.baseline_dir}")
else:
print(f"Compare setup issue for {suite_name}: {resolution.issue}")
if lister_args:
print(f"Additional trc_pkt_lister args: {' '.join(lister_args)}")
if args.list_only:
return 0
failures: list[str] = []
if not args.diff_only:
try:
for suite_name in selected_suites:
if suite_name == "standard":
run_standard_suite(
tests_dir,
bin_dir,
args.use_installed,
results_suffix,
args.test,
lister_args,
args.memacc_req_trace,
failures,
)
else:
run_ete_suite(
tests_dir,
bin_dir,
results_suffix,
args.test,
lister_args,
args.memacc_req_trace,
failures,
)
except TestFailure as exc:
print(str(exc), file=sys.stderr)
return 1
except OSError as exc:
print(f"Failed to start a test program: {exc}", file=sys.stderr)
return 1
comparison_summary = CompareResultsSummary(diff_counts_by_test={}, issue_count=0)
if diff_requested:
comparison_summary = compare_results(
tests_dir,
selected_suites,
results_suffix,
diff_results_suffixes,
args.diff_verbose,
args.diff_only,
)
if failures:
print("\nFailures:")
for failure in failures:
print(f" {failure}")
if comparison_summary.diff_counts_by_test or comparison_summary.issue_count:
print("\nResult comparison issues:")
if comparison_summary.diff_counts_by_test:
for test_name, diff_count in sorted(comparison_summary.diff_counts_by_test.items()):
if diff_count == 1:
print(f" {test_name}: {diff_count} difference")
else:
print(f" {test_name}: {diff_count} differences")
if comparison_summary.issue_count:
print(f" Comparison setup issues: {comparison_summary.issue_count}")
if failures or comparison_summary.diff_counts_by_test or comparison_summary.issue_count:
return 1
print("\nAll requested packet decode tests completed successfully.")
return 0
if __name__ == "__main__":
start_time = time.perf_counter()
try:
raise SystemExit(main(sys.argv[1:]))
finally:
elapsed = time.perf_counter() - start_time
print(f"\nTotal execution time: {elapsed:.2f} seconds")