use super::*;
#[test]
fn python_heredoc_literal_write_text_is_a_row() {
let cmd = "python3 - <<'PY'\nfrom pathlib import Path\nPath('notes.md').write_text('x')\nPY";
assert_eq!(paths(cmd), vec![("notes.md".to_string(), "interp-write")]);
let m = parse_bash_mutations(cmd);
assert_eq!(
m[0].resolve(Some("/work/proj")),
("/work/proj/notes.md".to_string(), Resolution::CwdJoined)
);
}
#[test]
fn python_one_hop_constant_binding_resolves() {
let cmd = "python3 - <<'PY'\np = 'out/report.json'\ns = build()\nopen(p, 'w').write(s)\nPY";
assert_eq!(
paths(cmd),
vec![("out/report.json".to_string(), "interp-write")]
);
let cmd2 = "python3 - <<'PY'\ntarget = Path('data.csv')\ntarget.write_bytes(blob)\nPY";
assert_eq!(paths(cmd2), vec![("data.csv".to_string(), "interp-write")]);
}
#[test]
fn one_hop_guards_reject_and_fall_back_to_the_opaque_marker() {
let reassigned = "python3 - <<'PY'\np = 'a.txt'\np = 'b.txt'\nopen(p, 'w').write(s)\nPY";
assert_eq!(
paths(reassigned),
vec![("interp:python".to_string(), "interp")]
);
let loop_bound = "python3 - <<'PY'\nfor p in files:\n open(p, 'w').write(s)\nPY";
assert_eq!(
paths(loop_bound),
vec![("interp:python".to_string(), "interp")]
);
let argv = "python3 - <<'PY'\np = sys.argv[1]\nopen(p, 'w').write(s)\nPY";
assert_eq!(paths(argv), vec![("interp:python".to_string(), "interp")]);
let fstring = "python3 - <<'PY'\nPath(f'{base}/x.md').write_text(s)\nPY";
assert_eq!(
paths(fstring),
vec![("interp:python".to_string(), "interp")]
);
}
#[test]
fn open_without_a_write_mode_is_not_a_write() {
assert!(paths("python3 - <<'PY'\ndata = open('cfg.json').read()\nPY").is_empty());
assert!(paths("python3 - <<'PY'\ndata = open('cfg.json', 'r').read()\nPY").is_empty());
assert!(paths("python3 - <<'PY'\nf = open('cfg.json', mode)\nPY").is_empty());
}
#[test]
fn node_and_ruby_idioms_are_covered() {
assert_eq!(
paths(r#"node -e "require('fs').writeFileSync('cfg.json', body)""#),
vec![("cfg.json".to_string(), "interp-write")]
);
let one_hop = "node - <<'JS'\nconst p = 'dist/app.js';\nfs.writeFileSync(p, code);\nJS";
assert_eq!(
paths(one_hop),
vec![("dist/app.js".to_string(), "interp-write")]
);
assert_eq!(
paths(r#"ruby -e "File.write('log.txt', line)""#),
vec![("log.txt".to_string(), "interp-write")]
);
}
#[test]
fn literal_and_opaque_writes_in_one_payload_emit_both() {
let cmd = "python3 - <<'PY'\nopen('known.md', 'w').write(a)\nopen(dynamic, 'w').write(b)\nPY";
assert_eq!(
paths(cmd),
vec![
("known.md".to_string(), "interp-write"),
("interp:python".to_string(), "interp"),
]
);
}
#[test]
fn payload_without_a_write_idiom_emits_nothing() {
assert!(paths(r#"python3 -c "print(1 + 1)""#).is_empty());
assert!(paths("python3 - <<'PY'\nprint(compute())\nPY").is_empty());
assert!(paths("python3 build_report.py --verbose input.csv").is_empty());
}
#[test]
fn open_needle_requires_a_word_boundary() {
assert!(paths("python3 - <<'PY'\nconn.reopen('x.db', 'w')\nPY").is_empty());
assert_eq!(
paths("python3 - <<'PY'\nio.open('x.log', 'w').write(s)\nPY"),
vec![("x.log".to_string(), "interp-write")]
);
}
#[test]
fn interp_marker_is_a_class_marker_never_resolved() {
assert!(is_class_marker("interp:python"));
let m = parse_bash_mutations("python3 - <<'PY'\nopen(p, 'w').write(s)\nPY");
assert_eq!(m.len(), 1);
assert_eq!(
m[0].resolve(Some("/base")),
("interp:python".to_string(), Resolution::Unresolved)
);
}
#[test]
fn argument_splitting_respects_strings_and_empties() {
assert_eq!(
paths(r#"python3 -c "open('a,b.md', 'w').write(x)""#),
[("a,b.md".to_string(), "interp-write")]
);
assert_eq!(
paths(r#"python3 -c "open('', 'w').write(x)""#),
[("interp:python".to_string(), "interp")]
);
}
#[test]
fn one_hop_survives_an_unrelated_loop_binder() {
let cmd = "python3 - <<'PY'\nfor i in range(3):\n step(i)\np = 'out.md'\nopen(p, 'w').write(s)\nPY";
assert_eq!(paths(cmd), [("out.md".to_string(), "interp-write")]);
}