mod common;
use common::sqry_bin;
use assert_cmd::Command;
use serde_json::Value;
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn write_badliveware_fixture(root: &Path) {
let src = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root")
.join("test-fixtures")
.join("badliveware")
.join("main.go");
let body = fs::read_to_string(&src)
.unwrap_or_else(|e| panic!("read badliveware fixture {}: {e}", src.display()));
fs::write(root.join("main.go"), body).expect("write fixture into temp root");
}
fn index(root: &Path) {
Command::new(sqry_bin())
.arg("index")
.arg(root)
.assert()
.success();
}
#[derive(Clone, Copy)]
struct SmokeCase {
subcommand: &'static str,
args: &'static [&'static str],
}
fn normalize_for_comparison(subcommand: &str, value: &mut Value) {
if subcommand == "status"
&& let Some(obj) = value.as_object_mut()
{
obj.remove("age_seconds");
}
canonicalize_arrays(value);
}
fn canonicalize_arrays(value: &mut Value) {
match value {
Value::Array(items) => {
for item in items.iter_mut() {
canonicalize_arrays(item);
}
items.sort_by_key(|v| v.to_string());
}
Value::Object(map) => {
for (_, v) in map.iter_mut() {
canonicalize_arrays(v);
}
}
_ => {}
}
}
const SUBCOMMAND_SMOKE_CASES: &[SmokeCase] = &[
SmokeCase {
subcommand: "trace-path",
args: &["useSelector", "parseConfig"],
},
SmokeCase {
subcommand: "call-chain-depth",
args: &["useSelector"],
},
SmokeCase {
subcommand: "dependency-tree",
args: &["main"],
},
SmokeCase {
subcommand: "cross-language",
args: &[],
},
SmokeCase {
subcommand: "nodes",
args: &[],
},
SmokeCase {
subcommand: "edges",
args: &[],
},
SmokeCase {
subcommand: "stats",
args: &[],
},
SmokeCase {
subcommand: "status",
args: &[],
},
SmokeCase {
subcommand: "provenance",
args: &["parseConfig"],
},
SmokeCase {
subcommand: "resolve",
args: &["parseConfig"],
},
SmokeCase {
subcommand: "cycles",
args: &[],
},
SmokeCase {
subcommand: "complexity",
args: &[],
},
SmokeCase {
subcommand: "direct-callers",
args: &["parseConfig"],
},
SmokeCase {
subcommand: "direct-callees",
args: &["useSelector"],
},
SmokeCase {
subcommand: "call-hierarchy",
args: &["useSelector"],
},
SmokeCase {
subcommand: "is-in-cycle",
args: &["parseConfig"],
},
];
fn run_and_parse(label: &str, args: &[&str]) -> Value {
let output = Command::new(sqry_bin())
.args(args)
.output()
.unwrap_or_else(|e| panic!("{label}: spawn failed: {e}"));
assert!(
output.status.success(),
"{label}: nonzero exit ({:?}). args={args:?}\nstdout={}\nstderr={}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
serde_json::from_str(&stdout).unwrap_or_else(|e| {
panic!("{label}: expected JSON. args={args:?}\nstdout={stdout}\nparse error: {e}")
})
}
fn run_case_three_orders(fixture_root: &Path, case: &SmokeCase) {
let path_str = fixture_root.to_str().expect("fixture path is valid UTF-8");
let mut order_1_args: Vec<&str> = vec!["--json", "graph", "--path", path_str, case.subcommand];
order_1_args.extend_from_slice(case.args);
let label_1 = format!("{} order 1 (`--json graph <sub>`)", case.subcommand);
let mut parsed_1 = run_and_parse(&label_1, &order_1_args);
let mut order_2_args: Vec<&str> = vec!["graph", "--path", path_str, case.subcommand];
order_2_args.extend_from_slice(case.args);
order_2_args.push("--json");
let label_2 = format!("{} order 2 (`graph <sub> --json`)", case.subcommand);
let mut parsed_2 = run_and_parse(&label_2, &order_2_args);
let mut order_3_args: Vec<&str> = vec![
"graph",
"--path",
path_str,
"--format",
"json",
case.subcommand,
];
order_3_args.extend_from_slice(case.args);
let label_3 = format!("{} order 3 (`graph --format json <sub>`)", case.subcommand);
let mut parsed_3 = run_and_parse(&label_3, &order_3_args);
normalize_for_comparison(case.subcommand, &mut parsed_1);
normalize_for_comparison(case.subcommand, &mut parsed_2);
normalize_for_comparison(case.subcommand, &mut parsed_3);
assert_eq!(
parsed_1, parsed_2,
"{} order 1 vs order 2 JSON mismatch\norder 1: {parsed_1}\norder 2: {parsed_2}",
case.subcommand
);
assert_eq!(
parsed_2, parsed_3,
"{} order 2 vs order 3 JSON mismatch\norder 2: {parsed_2}\norder 3: {parsed_3}",
case.subcommand
);
}
#[test]
fn every_graph_subcommand_honors_all_three_invocation_orders() {
let temp = TempDir::new().unwrap();
write_badliveware_fixture(temp.path());
index(temp.path());
for case in SUBCOMMAND_SMOKE_CASES {
run_case_three_orders(temp.path(), case);
}
}
#[test]
fn badliveware_direct_callers_parse_config_matches_public_report() {
let temp = TempDir::new().unwrap();
write_badliveware_fixture(temp.path());
index(temp.path());
let case = SmokeCase {
subcommand: "direct-callers",
args: &["parseConfig"],
};
run_case_three_orders(temp.path(), &case);
let path_str = temp.path().to_str().unwrap();
let parsed = run_and_parse(
"badliveware direct-callers parseConfig",
&[
"--json",
"graph",
"--path",
path_str,
"direct-callers",
"parseConfig",
],
);
assert_eq!(
parsed["symbol"], "parseConfig",
"expected `symbol: parseConfig` in JSON, got {parsed}"
);
let callers = parsed["callers"]
.as_array()
.unwrap_or_else(|| panic!("expected `callers` array, got {parsed}"));
let names: Vec<&str> = callers
.iter()
.filter_map(|c| c.get("name").and_then(Value::as_str))
.collect();
assert!(
names.contains(&"useSelector"),
"expected `useSelector` in callers list, got {names:?} (full JSON: {parsed})"
);
}
#[test]
fn graph_subcommand_enumeration_matches_help_output() {
let output = Command::new(sqry_bin())
.args(["graph", "--help"])
.output()
.expect("spawn `sqry graph --help`");
assert!(
output.status.success(),
"`sqry graph --help` exited nonzero: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let help_text = String::from_utf8_lossy(&output.stdout);
let mut in_commands = false;
let mut help_subcommands: BTreeSet<String> = BTreeSet::new();
for line in help_text.lines() {
let trimmed = line.trim_end();
if trimmed.starts_with("Commands:") {
in_commands = true;
continue;
}
if in_commands {
if trimmed.is_empty() || !line.starts_with(' ') {
break;
}
if let Some(token) = trimmed.split_whitespace().next() {
help_subcommands.insert(token.to_string());
}
}
}
help_subcommands.remove("help");
assert!(
!help_subcommands.is_empty(),
"failed to parse any subcommands out of `sqry graph --help`. \
Help text was:\n{help_text}"
);
let enumerated: BTreeSet<String> = SUBCOMMAND_SMOKE_CASES
.iter()
.map(|c| c.subcommand.to_string())
.collect();
let missing_from_enumeration: Vec<&String> = help_subcommands.difference(&enumerated).collect();
let extra_in_enumeration: Vec<&String> = enumerated.difference(&help_subcommands).collect();
assert!(
missing_from_enumeration.is_empty(),
"`SUBCOMMAND_SMOKE_CASES` is missing subcommand(s) listed by \
`sqry graph --help`: {missing_from_enumeration:?}. \
Add a `SmokeCase` entry for each so `--json` threading is \
covered, then update this assertion."
);
assert!(
extra_in_enumeration.is_empty(),
"`SUBCOMMAND_SMOKE_CASES` references subcommand(s) that no \
longer appear in `sqry graph --help`: {extra_in_enumeration:?}. \
Remove the stale entry."
);
}