#![cfg(unix)]
use std::collections::BTreeMap;
use std::rc::Rc;
use harn_hostlib::tools::ToolsCapability;
use harn_hostlib::{BuiltinRegistry, HostlibCapability, HostlibError};
use harn_vm::VmValue;
use tempfile::tempdir;
fn registry() -> BuiltinRegistry {
let mut registry = BuiltinRegistry::new();
ToolsCapability.register_builtins(&mut registry);
registry
}
fn call(builtin: &str, request: BTreeMap<String, VmValue>) -> Result<VmValue, HostlibError> {
harn_hostlib::tools::permissions::enable_for_test();
let registry = registry();
let entry = registry
.find(builtin)
.unwrap_or_else(|| panic!("builtin {builtin} not registered"));
let arg = VmValue::Dict(Rc::new(request));
(entry.handler)(&[arg])
}
fn dict() -> BTreeMap<String, VmValue> {
BTreeMap::new()
}
fn vstr(value: &str) -> VmValue {
VmValue::String(Rc::from(value))
}
fn vlist_str(values: &[&str]) -> VmValue {
VmValue::List(Rc::new(values.iter().map(|s| vstr(s)).collect()))
}
fn require_dict(value: VmValue) -> BTreeMap<String, VmValue> {
match value {
VmValue::Dict(map) => (*map).clone(),
other => panic!("expected dict response, got {other:?}"),
}
}
fn require_int(map: &BTreeMap<String, VmValue>, key: &str) -> i64 {
match map.get(key) {
Some(VmValue::Int(i)) => *i,
other => panic!("expected int at {key}, got {other:?}"),
}
}
fn require_str(map: &BTreeMap<String, VmValue>, key: &str) -> String {
match map.get(key) {
Some(VmValue::String(s)) => s.to_string(),
other => panic!("expected string at {key}, got {other:?}"),
}
}
fn require_bool(map: &BTreeMap<String, VmValue>, key: &str) -> bool {
match map.get(key) {
Some(VmValue::Bool(b)) => *b,
other => panic!("expected bool at {key}, got {other:?}"),
}
}
#[test]
fn run_command_echoes_stdout_and_reports_exit_zero() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "echo hello"]));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 0);
assert_eq!(require_str(&resp, "stdout").trim(), "hello");
assert_eq!(require_str(&resp, "stderr"), "");
assert!(!require_bool(&resp, "timed_out"));
assert!(matches!(resp.get("signal"), Some(VmValue::Nil)));
assert!(require_int(&resp, "duration_ms") >= 0);
}
#[test]
fn run_command_propagates_nonzero_exit_code() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "exit 7"]));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 7);
assert!(!require_bool(&resp, "timed_out"));
}
#[test]
fn run_command_pipes_stdin_into_child() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["cat"]));
req.insert("stdin".into(), vstr("from-stdin"));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_str(&resp, "stdout"), "from-stdin");
}
#[test]
fn run_command_runs_in_supplied_cwd() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "pwd"]));
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
let stdout = require_str(&resp, "stdout");
let canon_cwd = std::fs::canonicalize(dir.path()).unwrap();
let canon_stdout = std::fs::canonicalize(stdout.trim()).unwrap();
assert_eq!(canon_stdout, canon_cwd);
}
#[test]
fn run_command_kills_child_when_timeout_elapses() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["sleep", "30"]));
req.insert("timeout_ms".into(), VmValue::Int(150));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert!(require_bool(&resp, "timed_out"));
assert!(matches!(resp.get("signal"), Some(VmValue::String(_))));
}
#[test]
fn run_command_capture_stderr_false_merges_into_stdout() {
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&["bash", "-c", "echo out; echo err 1>&2"]),
);
req.insert("capture_stderr".into(), VmValue::Bool(false));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
let stdout = require_str(&resp, "stdout");
assert!(stdout.contains("out"), "stdout was {stdout:?}");
assert!(stdout.contains("err"), "stdout was {stdout:?}");
assert_eq!(require_str(&resp, "stderr"), "");
}
#[test]
fn run_command_passes_env_when_supplied() {
let mut env_dict: BTreeMap<String, VmValue> = BTreeMap::new();
env_dict.insert("PATH".into(), vstr(env!("PATH")));
env_dict.insert("HOSTLIB_TEST_VAR".into(), vstr("value-42"));
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&["bash", "-c", "echo $HOSTLIB_TEST_VAR"]),
);
req.insert("env".into(), VmValue::Dict(Rc::new(env_dict)));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_str(&resp, "stdout").trim(), "value-42");
}
#[test]
fn run_command_missing_argv_returns_missing_parameter() {
let err = call("hostlib_tools_run_command", dict()).unwrap_err();
match err {
HostlibError::MissingParameter { param, .. } => assert_eq!(param, "argv"),
other => panic!("expected MissingParameter, got {other:?}"),
}
}
#[test]
fn run_command_empty_argv_returns_invalid_parameter() {
let mut req = dict();
req.insert("argv".into(), VmValue::List(Rc::new(Vec::new())));
let err = call("hostlib_tools_run_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn run_command_rejects_nonexistent_cwd() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["true"]));
req.insert("cwd".into(), vstr("/this/does/not/exist/anywhere"));
let err = call("hostlib_tools_run_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "cwd"));
}
#[test]
fn run_command_argv_must_be_strings() {
let mut req = dict();
req.insert("argv".into(), VmValue::List(Rc::new(vec![VmValue::Int(1)])));
let err = call("hostlib_tools_run_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn run_test_explicit_argv_runs_and_returns_handle() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["true"]));
let resp = require_dict(call("hostlib_tools_run_test", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 0);
assert!(!require_str(&resp, "result_handle").is_empty());
}
#[test]
fn run_test_without_argv_or_manifest_errors() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let err = call("hostlib_tools_run_test", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn run_test_inspect_returns_parsed_records_for_explicit_junit() {
let dir = tempdir().unwrap();
let junit = dir.path().join("junit.xml");
std::fs::write(
&junit,
r#"<?xml version="1.0"?>
<testsuites>
<testsuite name="suite">
<testcase classname="C" name="passes" time="0.001"/>
<testcase classname="C" name="fails" time="0.005">
<failure message="boom">trace</failure>
</testcase>
</testsuite>
</testsuites>"#,
)
.unwrap();
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&[
"bash",
"-c",
"echo 'running 2 tests'; echo 'test a::passes ... ok'; echo 'test a::fails ... FAILED'; printf '\\n'; echo 'test result: FAILED. 1 passed; 1 failed; 0 ignored'; exit 1",
]),
);
let resp = require_dict(call("hostlib_tools_run_test", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 1);
let handle = require_str(&resp, "result_handle");
let mut inspect_req = dict();
inspect_req.insert("result_handle".into(), vstr(&handle));
inspect_req.insert("include_passing".into(), VmValue::Bool(true));
let inspect = require_dict(call("hostlib_tools_inspect_test_results", inspect_req).unwrap());
assert_eq!(require_str(&inspect, "result_handle"), handle);
let tests = match inspect.get("tests") {
Some(VmValue::List(l)) => (**l).clone(),
other => panic!("expected list, got {other:?}"),
};
assert_eq!(tests.len(), 2);
}
#[test]
fn run_test_summary_omitted_when_no_records_parsed() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "echo nothing"]));
let resp = require_dict(call("hostlib_tools_run_test", req).unwrap());
assert!(!resp.contains_key("summary"));
}
#[test]
fn inspect_test_results_unknown_handle_errors() {
let mut req = dict();
req.insert(
"result_handle".into(),
vstr("htr-deadbeef-this-is-not-real"),
);
let err = call("hostlib_tools_inspect_test_results", req).unwrap_err();
assert!(
matches!(err, HostlibError::InvalidParameter { param, .. } if param == "result_handle")
);
}
#[test]
fn inspect_test_results_missing_handle_errors() {
let err = call("hostlib_tools_inspect_test_results", dict()).unwrap_err();
assert!(
matches!(err, HostlibError::MissingParameter { param, .. } if param == "result_handle")
);
}
#[test]
fn run_build_command_explicit_argv_runs_and_parses_diagnostics() {
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&[
"bash",
"-c",
"echo 'src/foo.rs:3:7: error: parse error here' 1>&2; exit 2",
]),
);
let resp = require_dict(call("hostlib_tools_run_build_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 2);
let diagnostics = match resp.get("diagnostics") {
Some(VmValue::List(l)) => (**l).clone(),
other => panic!("expected list, got {other:?}"),
};
assert!(!diagnostics.is_empty());
}
#[test]
fn run_build_command_without_argv_or_manifest_errors() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let err = call("hostlib_tools_run_build_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn manage_packages_missing_operation_errors() {
let err = call("hostlib_tools_manage_packages", dict()).unwrap_err();
assert!(matches!(err, HostlibError::MissingParameter { param, .. } if param == "operation"));
}
#[test]
fn manage_packages_unknown_operation_errors() {
let mut req = dict();
req.insert("operation".into(), vstr("frobnicate"));
req.insert("ecosystem".into(), vstr("npm"));
let err = call("hostlib_tools_manage_packages", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "operation"));
}
#[test]
fn manage_packages_no_ecosystem_no_manifest_errors() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("operation".into(), vstr("install"));
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let err = call("hostlib_tools_manage_packages", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "ecosystem"));
}
#[test]
fn manage_packages_unsupported_pair_for_ecosystem_errors() {
let mut req = dict();
req.insert("operation".into(), vstr("add"));
req.insert("ecosystem".into(), vstr("gradle"));
req.insert("packages".into(), vlist_str(&["junit"]));
let err = call("hostlib_tools_manage_packages", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "operation"));
}
#[test]
fn manage_packages_runs_for_detected_npm_workspace_when_manifest_present() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("operation".into(), vstr("update"));
req.insert("ecosystem".into(), vstr("bundler"));
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let result = call("hostlib_tools_manage_packages", req);
match result {
Ok(value) => {
let resp = require_dict(value);
assert_eq!(require_str(&resp, "ecosystem"), "bundler");
assert_eq!(require_str(&resp, "operation"), "update");
assert!(matches!(
resp.get("lockfile_changed"),
Some(VmValue::Bool(_))
));
}
Err(HostlibError::Backend { .. }) => {
}
Err(other) => panic!("unexpected error variant: {other:?}"),
}
}