use assert_cmd::Command;
use serde_json::Value;
use std::path::Path;
use tempfile::TempDir;
fn tldr_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("tldr"))
}
fn run_json(args: &[&str]) -> Value {
let out = tldr_cmd()
.args(args)
.args(["--format", "json", "-q"])
.output()
.unwrap_or_else(|e| panic!("spawn {:?}: {}", args, e));
assert!(
out.status.success(),
"tldr {:?} failed: stderr={}",
args,
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
panic!(
"tldr {:?} JSON parse failed: {}\nstdout={}",
args,
e,
String::from_utf8_lossy(&out.stdout)
)
})
}
fn run_raw(args: &[&str]) -> (bool, String, String) {
let out = tldr_cmd()
.args(args)
.args(["--format", "json", "-q"])
.output()
.unwrap_or_else(|e| panic!("spawn {:?}: {}", args, e));
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
#[test]
fn test_n1_extract_cpp_h_uses_cpp_parser() {
let tmp = TempDir::new().expect("tempdir");
let cpp_path = tmp.path().join("foo.cpp");
let h_path = tmp.path().join("foo.h");
std::fs::write(&cpp_path, "class Foo {};\n").expect("write foo.cpp");
std::fs::write(
&h_path,
"class Bar {\npublic:\n void method();\n};\n",
)
.expect("write foo.h");
let v = run_json(&["extract", h_path.to_str().unwrap()]);
let lang = v
.get("language")
.and_then(|l| l.as_str())
.expect("extract synthetic: missing /language");
assert_eq!(
lang, "cpp",
"synthetic foo.h next to foo.cpp must autodetect as cpp, got {:?}",
lang
);
let class_count = v
.get("classes")
.and_then(|c| c.as_array())
.map(|a| a.len())
.unwrap_or(0);
assert!(
class_count >= 1,
"synthetic foo.h: expected >= 1 class, got {}; classes={:?}",
class_count,
v.get("classes")
);
if !Path::new("/tmp/repos/cpp-tinyxml2/tinyxml2.h").exists() {
return;
}
let v = run_json(&["extract", "/tmp/repos/cpp-tinyxml2/tinyxml2.h"]);
let lang = v
.get("language")
.and_then(|l| l.as_str())
.expect("extract: missing /language");
assert_eq!(
lang, "cpp",
"expected language=cpp for tinyxml2.h (sibling .cpp present), got {:?}; \
the C grammar mis-parses C++ headers and produces zero classes plus \
class-as-function leakage",
lang
);
let class_count = v
.get("classes")
.and_then(|c| c.as_array())
.map(|a| a.len())
.unwrap_or(0);
assert!(
class_count >= 6,
"expected at least 6 classes in tinyxml2.h (real count is much higher), \
got {}; classes array: {:?}",
class_count,
v.get("classes")
);
let class_as_fn: Vec<&Value> = v
.get("functions")
.and_then(|f| f.as_array())
.map(|a| {
a.iter()
.filter(|f| {
f.get("return_type")
.and_then(|r| r.as_str())
.map(|s| s == "class")
.unwrap_or(false)
})
.collect()
})
.unwrap_or_default();
assert!(
class_as_fn.is_empty(),
"expected zero functions with return_type=class (the C-grammar leakage), \
found {} such entries: {:?}",
class_as_fn.len(),
class_as_fn
);
}
#[test]
fn test_n1_extract_lang_flag_honored() {
let tmp = TempDir::new().expect("tempdir");
let h_path = tmp.path().join("bar.h");
std::fs::write(
&h_path,
"class Bar {\npublic:\n void method();\n};\n",
)
.expect("write bar.h");
let v = run_json(&["extract", "--lang", "cpp", h_path.to_str().unwrap()]);
let lang = v
.get("language")
.and_then(|l| l.as_str())
.expect("extract synthetic --lang: missing /language");
assert_eq!(
lang, "cpp",
"synthetic bar.h with --lang cpp must report cpp, got {:?}",
lang
);
if !Path::new("/tmp/repos/cpp-tinyxml2/tinyxml2.h").exists() {
return;
}
let v = run_json(&["extract", "--lang", "cpp", "/tmp/repos/cpp-tinyxml2/tinyxml2.h"]);
let lang = v
.get("language")
.and_then(|l| l.as_str())
.expect("extract: missing /language");
assert_eq!(
lang, "cpp",
"explicit --lang cpp must override extension-based detection; got {:?}",
lang
);
}
#[test]
fn test_n2_cyclomatic_complexity_explain_agree() {
let tmp = TempDir::new().expect("tempdir");
let py_path = tmp.path().join("synth_branchy.py");
let src = r#"
def branchy(x, y):
if x > 0:
if y > 0:
return 1
elif y < 0:
return 2
else:
return 3
elif x < 0:
if y > 0 and x < -1:
return 4
return 5
else:
return 6
"#;
std::fs::write(&py_path, src).expect("write synth_branchy.py");
let cmplx = run_json(&["complexity", py_path.to_str().unwrap(), "branchy"]);
let cmplx_cyc = cmplx
.get("cyclomatic")
.and_then(|v| v.as_u64())
.expect("synthetic complexity: missing cyclomatic");
let expl = run_json(&["explain", py_path.to_str().unwrap(), "branchy"]);
let expl_cyc = expl
.pointer("/complexity/cyclomatic")
.and_then(|v| v.as_u64())
.expect("synthetic explain: missing /complexity/cyclomatic");
assert_eq!(
cmplx_cyc, expl_cyc,
"synthetic branchy: cyclomatic mismatch complexity={} explain={} \
(single source of truth contract)",
cmplx_cyc, expl_cyc
);
let app_path = "/tmp/repos/flask/src/flask/app.py";
if !Path::new(app_path).exists() {
return;
}
let methods = [
"Flask.__init__",
"Flask.dispatch_request",
"Flask.full_dispatch_request",
"Flask.run",
];
for method in &methods {
let cmplx = run_json(&["complexity", app_path, method]);
let cmplx_cyc = cmplx
.get("cyclomatic")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| panic!("complexity {}: missing cyclomatic", method));
let expl = run_json(&["explain", app_path, method]);
let expl_cyc = expl
.pointer("/complexity/cyclomatic")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| panic!("explain {}: missing /complexity/cyclomatic", method));
assert_eq!(
cmplx_cyc, expl_cyc,
"cyclomatic mismatch for {}: complexity={} explain={} \
(the two commands must share a single source of truth)",
method, cmplx_cyc, expl_cyc
);
}
}
#[test]
fn test_n3_impact_accepts_qualified_names() {
if !Path::new("/tmp/repos/flask/src/flask/app.py").exists() {
return;
}
let v = run_json(&["impact", "Flask.run", "/tmp/repos/flask"]);
let targets = v
.get("targets")
.and_then(|t| t.as_object())
.expect("impact: missing /targets");
assert!(
!targets.is_empty(),
"impact Flask.run returned zero targets; report: {:?}",
v
);
let any_run = targets.iter().any(|(key, val)| {
let key_has_run = key.contains("run");
let func_field = val
.get("function")
.and_then(|f| f.as_str())
.unwrap_or("");
key_has_run
&& (func_field == "run" || func_field == "Flask.run" || func_field.ends_with(".run"))
});
assert!(
any_run,
"expected a target identifying Flask.run (key contains 'run' and \
function field is `run` / `Flask.run` / `*.run`); got: {:?}",
targets
);
}
#[test]
fn test_n3_impact_whatbreaks_name_parity() {
if !Path::new("/tmp/repos/flask/src/flask/app.py").exists() {
return;
}
let names = [
"Flask.run",
"run",
"Flask.dispatch_request",
"dispatch_request",
];
for name in &names {
let (wb_ok, _, wb_err) = run_raw(&["whatbreaks", name, "/tmp/repos/flask"]);
let (imp_ok, _, imp_err) = run_raw(&["impact", name, "/tmp/repos/flask"]);
if wb_ok {
assert!(
imp_ok,
"name parity broken for {:?}: whatbreaks succeeded but impact failed.\n\
whatbreaks stderr: {}\n\
impact stderr: {}",
name, wb_err, imp_err
);
}
}
}