use std::path::PathBuf;
use std::process::Command;
fn bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_sipmon"))
}
fn fixture() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/pcap_fixtures/sipbot_call.pcap")
}
struct CallLine {
state: String,
setup_ms: Option<i64>,
crit: i64,
}
fn run_headless(pcap: &PathBuf, extra: &[&str]) -> Vec<CallLine> {
let mut cmd = bin();
for a in extra {
cmd.arg(a);
}
cmd.arg("file").arg("-r").arg(pcap).arg("--no-tui");
let out = cmd.output().expect("run sipmon");
assert!(
out.status.success(),
"sipmon failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| {
let v: serde_json::Value = serde_json::from_str(l).ok()?;
Some(CallLine {
state: v["state"].as_str()?.to_string(),
setup_ms: v["setup_ms"].as_i64(),
crit: v["crit"].as_i64().unwrap_or(0),
})
})
.collect()
}
#[test]
fn file_analysis_finds_completed_call() {
let calls = run_headless(&fixture(), &[]);
assert_eq!(calls.len(), 1, "expected exactly one call in the fixture");
let c = &calls[0];
assert_eq!(c.state, "Completed");
assert!(c.setup_ms.unwrap_or(0) > 0, "setup_ms should be positive");
assert_eq!(
c.crit, 0,
"no critical diagnostics expected on a clean call"
);
}
#[test]
fn default_positional_pcap_mode() {
let mut cmd = bin();
cmd.arg("--no-tui").arg(fixture());
let out = cmd.output().expect("run sipmon");
assert!(
out.status.success(),
"sipmon failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("\"state\":\"Completed\""),
"default pcap mode must analyze the file, got: {stdout}"
);
}
#[test]
fn default_positional_evlog_replays() {
let dir = tempfile::tempdir().unwrap();
let evlog = dir.path().join("t.evlog");
let mut cmd = bin();
cmd.arg("--evlog")
.arg(&evlog)
.arg("file")
.arg("-r")
.arg(fixture())
.arg("--no-tui");
let out = cmd.output().expect("run sipmon");
assert!(out.status.success());
assert!(evlog.exists(), "event log must be written");
let r = bin().arg("--no-tui").arg(&evlog).output().unwrap();
assert!(
r.status.success(),
"default evlog mode failed: {}",
String::from_utf8_lossy(&r.stderr)
);
let rout = String::from_utf8_lossy(&r.stdout);
assert!(
rout.contains("Completed"),
"default evlog mode must replay the log, got: {rout}"
);
}
#[test]
fn evlog_roundtrip_and_query() {
let dir = tempfile::tempdir().unwrap();
let evlog = dir.path().join("t.evlog");
let mut cmd = bin();
cmd.arg("--evlog")
.arg(&evlog)
.arg("file")
.arg("-r")
.arg(fixture())
.arg("--no-tui");
let out = cmd.output().expect("run sipmon");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let call_id: String = {
let v: serde_json::Value = serde_json::from_str(stdout.lines().next().unwrap()).unwrap();
v["call_id"].as_str().unwrap().to_string()
};
assert!(evlog.exists(), "event log must be written");
let q = bin()
.args(["query", "-l"])
.arg(&evlog)
.arg("-c")
.arg(&call_id)
.output()
.unwrap();
assert!(q.status.success());
let qout = String::from_utf8_lossy(&q.stdout);
let sip_msgs = qout.lines().filter(|l| l.contains("\"msg\"")).count();
let streams = qout.lines().filter(|l| l.contains("\"stream\"")).count();
assert!(
sip_msgs >= 6,
"expected INVITE/100/200/ACK/BYE/200, got {sip_msgs}"
);
assert!(
streams >= 2,
"expected periodic stream snapshots, got {streams}"
);
let r = bin()
.args(["replay", "-l"])
.arg(&evlog)
.arg("--no-tui")
.output()
.unwrap();
assert!(r.status.success());
let rout = String::from_utf8_lossy(&r.stdout);
assert!(
rout.contains(&call_id) && rout.contains("Completed"),
"replay must reproduce the completed call"
);
let jsonl = dir.path().join("t.jsonl");
let e = bin()
.args(["export", "-l"])
.arg(&evlog)
.arg("--jsonl")
.arg(&jsonl)
.output()
.unwrap();
assert!(e.status.success());
let content = std::fs::read_to_string(&jsonl).unwrap();
assert!(content.contains("\"kind\":\"call\""));
assert!(content.contains("\"kind\":\"stream\""));
assert!(content.contains("PCMU"));
}
#[test]
fn dry_run_writes_no_files() {
let dir = tempfile::tempdir().unwrap();
let before: Vec<_> = std::fs::read_dir(dir.path()).unwrap().collect();
assert!(before.is_empty());
let mut cmd = bin();
cmd.arg("--dry-run")
.arg("--evlog")
.arg(dir.path().join("should-not-exist.evlog"))
.arg("file")
.arg("-r")
.arg(fixture())
.arg("--no-tui");
let out = cmd.output().unwrap();
assert!(out.status.success());
let after: Vec<_> = std::fs::read_dir(dir.path()).unwrap().collect();
assert!(after.is_empty(), "dry-run must not write any files");
}
#[test]
fn caps_limit_retained_calls() {
let calls = run_headless(&fixture(), &["--max-calls", "1", "--max-streams", "2"]);
assert!(!calls.is_empty());
}
#[test]
fn stdin_stream_mode() {
let raw = std::fs::read(fixture()).unwrap();
let mut child = bin()
.arg("-")
.env_remove("TERM")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.unwrap();
use std::io::Write;
child.stdin.take().unwrap().write_all(&raw).unwrap();
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Completed"),
"stdin mode must analyze the piped pcap stream"
);
}
#[test]
fn record_requires_evlog() {
let out = bin().arg("record").arg("-i").arg("lo").output().unwrap();
assert!(!out.status.success(), "record without --evlog must fail");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("--evlog"),
"error should mention --evlog: {err}"
);
}
#[test]
fn record_bad_interface_clean_error() {
let dir = tempfile::tempdir().unwrap();
let evlog = dir.path().join("t.evlog");
let out = bin()
.arg("record")
.arg("-i")
.arg("definitely-not-an-interface-xyz")
.arg("-w")
.arg(&evlog)
.output()
.unwrap();
assert!(!out.status.success(), "bad interface must fail cleanly");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Error"), "expected a clean error, got: {err}");
assert!(
!evlog.exists(),
"no evlog should be created when the interface fails"
);
}