use cljrs_env::env::Env;
use cljrs_env::error::EvalResult;
use cljrs_value::Symbol;
pub fn resolve_versioned_symbol(sym: &Symbol, commit: &str, env: &mut Env) -> EvalResult {
cljrs_env::versioned::resolve_versioned_value(
&env.globals,
&env.current_ns,
sym.namespace.as_deref(),
&sym.name,
commit,
)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use cljrs_env::env::{Env, GlobalEnv};
use cljrs_gc::GcPtr;
use cljrs_value::{NativeFn, Value};
fn make_env(ns: &str) -> (Arc<GlobalEnv>, Env) {
let globals = crate::standard_env_minimal(None, None, None);
globals.get_or_create_ns(ns);
let env = Env::new(globals.clone(), ns);
(globals, env)
}
fn fake_commit() -> &'static str {
"abc1234def56"
}
fn const_native(tag: i64) -> NativeFn {
NativeFn {
name: Arc::from("test-fn"),
arity: cljrs_value::Arity::Fixed(0),
func: Arc::new(move |_args| Ok(Value::Long(tag))),
}
}
#[test]
fn head_fallback_for_native_function() {
let (globals, mut env) = make_env("mylib");
let commit = "deadbeef01234";
let nf = const_native(99);
globals.intern(
"mylib",
Arc::from("stable-fn"),
Value::NativeFunction(GcPtr::new(nf)),
);
let sym = cljrs_value::Symbol {
namespace: Some(Arc::from("mylib")),
name: Arc::from("stable-fn"),
version: Some(Arc::from(commit)),
};
let result = super::resolve_versioned_symbol(&sym, commit, &mut env)
.expect("HEAD fallback should succeed");
assert!(matches!(result, Value::NativeFunction(_)));
}
#[test]
fn missing_symbol_gives_unbound_error() {
let (_globals, mut env) = make_env("mylib");
let commit = fake_commit();
let sym = cljrs_value::Symbol {
namespace: Some(Arc::from("mylib")),
name: Arc::from("does-not-exist"),
version: Some(Arc::from(commit)),
};
let err = super::resolve_versioned_symbol(&sym, commit, &mut env)
.expect_err("should error for unknown symbol");
assert!(
matches!(err, cljrs_env::error::EvalError::UnboundSymbol(_)),
"expected UnboundSymbol, got {err:?}"
);
}
fn versioned_sym(ns: &str, name: &str, commit: &str) -> cljrs_value::Symbol {
cljrs_value::Symbol {
namespace: Some(Arc::from(ns)),
name: Arc::from(name),
version: Some(Arc::from(commit)),
}
}
#[test]
fn matching_provenance_is_silent() {
let (globals, mut env) = make_env("provlib");
let full_commit = "deadbeef0123456789abcdef0123456789abcdef";
globals.intern(
"provlib",
Arc::from("f"),
Value::NativeFunction(GcPtr::new(const_native(1))),
);
globals.set_native_provenance("provlib", full_commit);
let sym = versioned_sym("provlib", "f", "deadbeef012");
super::resolve_versioned_symbol(&sym, "deadbeef012", &mut env).expect("should resolve");
assert!(
globals.provenance_warned.lock().unwrap().is_empty(),
"matching provenance must not warn"
);
}
#[test]
fn mismatched_provenance_warns_once() {
let (globals, mut env) = make_env("provlib2");
globals.intern(
"provlib2",
Arc::from("f"),
Value::NativeFunction(GcPtr::new(const_native(2))),
);
globals.set_native_provenance("provlib2", "1111111111111111");
let commit = "2222222222222222";
let sym = versioned_sym("provlib2", "f", commit);
let val = super::resolve_versioned_symbol(&sym, commit, &mut env)
.expect("mismatch still resolves to HEAD by default");
assert!(matches!(val, Value::NativeFunction(_)));
let warned = globals.provenance_warned.lock().unwrap();
assert_eq!(warned.len(), 1, "exactly one warning per pin");
assert!(warned.contains(&Arc::<str>::from("provlib2@2222222222222222")));
}
#[test]
fn enforce_native_versions_makes_mismatch_an_error() {
let (globals, mut env) = make_env("provlib3");
globals.intern(
"provlib3",
Arc::from("f"),
Value::NativeFunction(GcPtr::new(const_native(3))),
);
globals.set_native_provenance("provlib3", "1111111111111111");
globals.set_enforce_native_versions(true);
let commit = "2222222222222222";
let sym = versioned_sym("provlib3", "f", commit);
let err = super::resolve_versioned_symbol(&sym, commit, &mut env)
.expect_err("strict mode must reject a provenance mismatch");
let msg = format!("{err:?}");
assert!(
msg.contains("provlib3") && msg.contains("1111111111111111"),
"error should describe the mismatch: {msg}"
);
}
}