use std::io::Write;
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_renkin")
}
fn run(args: &[&str]) -> std::process::Output {
Command::new(bin())
.args(args)
.output()
.expect("failed to spawn renkin")
}
fn run_json(args: &[&str]) -> serde_json::Value {
let out = run(args);
assert!(
out.status.success(),
"renkin exited non-zero: {}",
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice(&out.stdout)
.unwrap_or_else(|e| panic!("stdout must be valid JSON ({e}): {out:?}"))
}
const BUILDING_BLOCK: &str = "CC(=O)O"; const STAGE1_UNSOLVED_AT_FIXTURE: &str = "O=C1CCC(=O)N1c1ccccc1";
const BOTH_STAGES_UNSOLVED: &str = "c1ccc2c(c1)c1ccccc1c1ccccc21"; const FIXTURE_TEMPLATES: &str = "tests/fixtures/coverage_mode_templates.smi";
const COVERAGE_MODE_KEYS: &[&str] = &[
"search_mode",
"selected_stage",
"stage2_invoked",
"stage1_timeout",
"stage2_timeout",
"stage1_elapsed_ms",
"stage2_elapsed_ms",
"total_elapsed_ms",
];
fn assert_no_coverage_mode_keys(v: &serde_json::Value, context: &str) {
for key in COVERAGE_MODE_KEYS {
assert!(
v.get(key).is_none(),
"standard mode must never emit {key} ({context}): {v}"
);
}
}
#[test]
fn standard_mode_route_found_output_has_no_coverage_mode_keys() {
let v = run_json(&[
"--target",
BUILDING_BLOCK,
"--depth",
"2",
"--max-routes",
"1",
]);
assert_no_coverage_mode_keys(&v, "route-found branch");
let mut keys: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
vec![
"joint_success_probability",
"routes",
"routes_found",
"target"
]
);
}
#[test]
fn standard_mode_no_route_found_output_has_no_coverage_mode_keys() {
let v = run_json(&[
"--target",
BOTH_STAGES_UNSOLVED,
"--depth",
"1",
"--max-routes",
"1",
]);
assert_eq!(v["routes_found"], 0);
assert_no_coverage_mode_keys(&v, "no-route-found branch");
let mut keys: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
vec!["diagnostics", "routes", "routes_found", "target"]
);
}
#[test]
fn invalid_search_mode_fails() {
let out = run(&["--target", BUILDING_BLOCK, "--search-mode", "bogus"]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--search-mode"), "stderr: {stderr}");
}
#[test]
fn missing_coverage_templates_path_fails_before_search() {
let out = run(&["--target", BUILDING_BLOCK, "--search-mode", "coverage"]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--coverage-templates"), "stderr: {stderr}");
}
#[test]
fn nonexistent_coverage_templates_path_fails_before_search() {
let out = run(&[
"--target",
BUILDING_BLOCK,
"--search-mode",
"coverage",
"--coverage-templates",
"/nonexistent/renkin_coverage_mode_test_path.smi",
]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("does not exist or is not readable"),
"stderr: {stderr}"
);
}
#[test]
fn unreadable_coverage_templates_path_reports_a_read_failure_not_missing_templates() {
let dir = std::env::temp_dir();
let path = dir.join(format!(
"renkin_coverage_mode_cli_test_invalid_utf8_{}.smi",
std::process::id()
));
{
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(&[0xFF, 0xFE, 0x00, 0xFF, 0xD8, 0x00]).unwrap();
}
let out = run(&[
"--target",
BUILDING_BLOCK,
"--search-mode",
"coverage",
"--coverage-templates",
path.to_str().unwrap(),
]);
let _ = std::fs::remove_file(&path);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("could not be read as valid UTF-8"),
"stderr must describe a read failure: {stderr}"
);
assert!(
!stderr.contains("contains no valid templates"),
"must not be misreported as the empty-templates case: {stderr}"
);
}
#[test]
fn coverage_flags_in_standard_mode_fail() {
let out = run(&[
"--target",
BUILDING_BLOCK,
"--coverage-templates",
FIXTURE_TEMPLATES,
]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--search-mode coverage"),
"stderr: {stderr}"
);
let out = run(&["--target", BUILDING_BLOCK, "--coverage-timeout-secs", "10"]);
assert!(!out.status.success());
}
#[test]
fn coverage_timeout_zero_fails() {
let out = run(&[
"--target",
BUILDING_BLOCK,
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
"--coverage-timeout-secs",
"0",
]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--coverage-timeout-secs"),
"stderr: {stderr}"
);
}
#[test]
fn coverage_timeout_non_integer_fails() {
for bad in ["abc", "12.5", "-3"] {
let out = run(&[
"--target",
BUILDING_BLOCK,
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
"--coverage-timeout-secs",
bad,
]);
assert!(!out.status.success(), "expected failure for {bad:?}");
}
}
#[test]
fn bond_index_with_coverage_mode_fails_loud() {
let out = run(&[
"--target",
BUILDING_BLOCK,
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
"--bond-index",
]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("bond-index") || stderr.contains("bond_index"),
"stderr: {stderr}"
);
}
#[test]
fn ring_context_policy_with_coverage_mode_fails_loud() {
let out = run(&[
"--target",
BUILDING_BLOCK,
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
"--ring-context-policy",
"conservative",
"--ring-context-sidecar",
"/nonexistent/renkin_coverage_mode_test_sidecar.json",
]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("does not support an active --ring-context-policy"),
"must be the coverage-mode combination rejection specifically: {stderr}"
);
assert!(
!stderr.contains("failed to load ring-context sidecar"),
"must be rejected by the coverage-mode combination check, before ever attempting to \
load the (deliberately nonexistent) sidecar file -- that would be a different error \
message (the pre-existing --ring-context-sidecar load-failure path): {stderr}"
);
}
#[cfg(all(not(target_arch = "wasm32"), feature = "nn-scoring"))]
#[test]
fn onnx_scorer_with_coverage_mode_fails_loud() {
let out = run(&[
"--target",
BUILDING_BLOCK,
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
"--scorer",
"/nonexistent/renkin_coverage_mode_test_scorer.onnx",
]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--scorer") || stderr.contains("scorer"),
"stderr: {stderr}"
);
assert!(
!stderr.contains("scorer load error"),
"must be rejected by the coverage-mode combination check, before ever attempting to \
load the (deliberately nonexistent) .onnx file: {stderr}"
);
}
#[test]
fn stage1_solved_reports_stage2_invoked_false() {
let v = run_json(&[
"--target",
BUILDING_BLOCK,
"--depth",
"2",
"--max-routes",
"1",
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
]);
assert_eq!(v["routes_found"], 1);
assert_eq!(v["search_mode"], "coverage");
assert_eq!(v["selected_stage"], "stage1");
assert_eq!(v["stage2_invoked"], false);
assert!(v.get("stage2_elapsed_ms").is_none() || v["stage2_elapsed_ms"].is_null());
}
#[test]
fn stage1_unsolved_reports_stage2_invoked_true() {
let v = run_json(&[
"--target",
STAGE1_UNSOLVED_AT_FIXTURE,
"--depth",
"2",
"--max-routes",
"1",
"--beam-width",
"100",
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
]);
assert_eq!(v["selected_stage"], "stage2");
assert_eq!(v["stage2_invoked"], true);
assert_eq!(v["routes_found"], 1);
assert!(v["stage2_elapsed_ms"].as_f64().unwrap() >= 0.0);
assert!(v["total_elapsed_ms"].as_f64().unwrap() >= v["stage1_elapsed_ms"].as_f64().unwrap());
}
#[test]
fn stage1_and_stage2_both_unsolved_reports_no_routes_with_coverage_fields() {
let v = run_json(&[
"--target",
BOTH_STAGES_UNSOLVED,
"--depth",
"2",
"--max-routes",
"1",
"--beam-width",
"100",
"--search-mode",
"coverage",
"--coverage-templates",
FIXTURE_TEMPLATES,
]);
assert_eq!(v["routes_found"], 0);
assert_eq!(v["search_mode"], "coverage");
assert_eq!(v["selected_stage"], "stage2");
assert_eq!(v["stage2_invoked"], true);
assert!(
v.get("diagnostics").is_some(),
"pre-existing diagnostics block must still be present: {v}"
);
}