use std::path::PathBuf;
use std::sync::Arc;
use cljrs_reader::Parser;
use cljrs_runtime::env::env::{Env, GlobalEnv};
use cljrs_value::Value;
fn env_with_sources(files: &[(&str, &str)]) -> (tempfile::TempDir, Arc<GlobalEnv>, Env) {
let dir = tempfile::tempdir().expect("tempdir");
for (rel, src) in files {
let path = dir.path().join(rel);
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(&path, src).expect("write");
}
let globals = cljrs_runtime::Runtime::builder()
.execution_mode(cljrs_runtime::ExecutionMode::TreeWalk)
.source_paths(vec![PathBuf::from(dir.path())])
.build()
.expect("runtime")
.into_globals();
let env = Env::new(globals.clone(), "user");
(dir, globals, env)
}
fn eval_in(env: &mut Env, src: &str) -> Result<Value, String> {
let mut parser = Parser::new(src.to_string(), "<test>".to_string());
let forms = parser.parse_all().map_err(|e| format!("parse: {e:?}"))?;
let mut result = Value::Nil;
for form in forms {
result = cljrs_runtime::interp::eval::eval(&form, env).map_err(|e| format!("{e:?}"))?;
}
Ok(result)
}
const OWNER: &str = r#"
(ns shapes.core)
(defmulti area "Area of a shape." :kind)
(defmethod area :default [_] :unknown)
"#;
#[test]
fn defmethod_reaches_a_multimethod_through_a_require_alias() {
let extender = r#"
(ns shapes.square (:require [shapes.core :as core]))
(defmethod core/area :square [s] (* (:side s) (:side s)))
"#;
let (_dir, _g, mut env) = env_with_sources(&[
("shapes/core.cljrs", OWNER),
("shapes/square.cljrs", extender),
]);
eval_in(&mut env, "(require 'shapes.square)").expect("require the extending ns");
let v = eval_in(
&mut env,
"(do (require '[shapes.core :as c]) (c/area {:kind :square :side 4}))",
)
.expect("dispatch");
assert_eq!(v, Value::Long(16));
}
#[test]
fn defmethod_reaches_a_multimethod_by_its_full_namespace() {
let extender = r#"
(ns shapes.circle (:require [shapes.core]))
(defmethod shapes.core/area :circle [c] (:r c))
"#;
let (_dir, _g, mut env) = env_with_sources(&[
("shapes/core.cljrs", OWNER),
("shapes/circle.cljrs", extender),
]);
eval_in(&mut env, "(require 'shapes.circle)").expect("require the extending ns");
let v = eval_in(
&mut env,
"(do (require '[shapes.core :as c]) (c/area {:kind :circle :r 7}))",
)
.expect("dispatch");
assert_eq!(v, Value::Long(7));
}
#[test]
fn several_namespaces_can_extend_the_same_multimethod() {
let square = r#"
(ns shapes.square (:require [shapes.core :as core]))
(defmethod core/area :square [s] (* (:side s) (:side s)))
"#;
let circle = r#"
(ns shapes.circle (:require [shapes.core :as core]))
(defmethod core/area :circle [c] (:r c))
"#;
let (_dir, _g, mut env) = env_with_sources(&[
("shapes/core.cljrs", OWNER),
("shapes/square.cljrs", square),
("shapes/circle.cljrs", circle),
]);
eval_in(&mut env, "(require 'shapes.square)").expect("square");
eval_in(&mut env, "(require 'shapes.circle)").expect("circle");
eval_in(&mut env, "(require '[shapes.core :as c])").expect("alias");
assert_eq!(
eval_in(&mut env, "(c/area {:kind :square :side 3})").expect("square dispatch"),
Value::Long(9)
);
assert_eq!(
eval_in(&mut env, "(c/area {:kind :circle :r 5})").expect("circle dispatch"),
Value::Long(5)
);
assert_eq!(
eval_in(&mut env, "(c/area {:kind :hexagon})").expect("default"),
Value::keyword(cljrs_value::Keyword::simple("unknown"))
);
}
#[test]
fn an_unqualified_name_still_resolves_in_the_current_namespace() {
let (_dir, _g, mut env) = env_with_sources(&[]);
let v = eval_in(
&mut env,
"(do (defmulti f :k) (defmethod f :a [_] 1) (f {:k :a}))",
)
.expect("same-ns defmethod");
assert_eq!(v, Value::Long(1));
}
#[test]
fn extending_something_that_is_not_a_multimethod_says_so() {
let (_dir, _g, mut env) = env_with_sources(&[]);
let err = eval_in(&mut env, "(do (def g 1) (defmethod g :a [_] 1))")
.expect_err("a def is not a multimethod");
assert!(err.contains("not a multimethod"), "unhelpful error: {err}");
}
#[test]
fn extending_something_undefined_says_that_instead() {
let (_dir, _g, mut env) = env_with_sources(&[]);
let err = eval_in(&mut env, "(defmethod nope/area :a [_] 1)").expect_err("no such multimethod");
assert!(err.contains("not defined"), "unhelpful error: {err}");
}
#[test]
fn a_multimethod_reached_through_refer_needs_no_qualification() {
let extender = r#"
(ns shapes.referred (:require [shapes.core :refer [area]]))
(defmethod area :triangle [t] (:base t))
"#;
let (_dir, _g, mut env) = env_with_sources(&[
("shapes/core.cljrs", OWNER),
("shapes/referred.cljrs", extender),
]);
eval_in(&mut env, "(require 'shapes.referred)").expect("require the extending ns");
let v = eval_in(
&mut env,
"(do (require '[shapes.core :as c]) (c/area {:kind :triangle :base 9}))",
)
.expect("dispatch");
assert_eq!(v, Value::Long(9));
}
#[test]
fn a_private_multimethod_cannot_be_extended_from_another_namespace() {
let owner = r#"
(ns shapes.hidden)
(defmulti ^:private secret :kind)
(defmethod secret :default [_] :owner-only)
"#;
let extender = r#"
(ns shapes.intruder (:require [shapes.hidden :as h]))
(defmethod h/secret :x [_] :extended)
"#;
let (_dir, _g, mut env) = env_with_sources(&[
("shapes/hidden.cljrs", owner),
("shapes/intruder.cljrs", extender),
]);
let err = eval_in(&mut env, "(require 'shapes.intruder)")
.expect_err("a private multimethod is not extensible from outside");
assert!(err.contains("not public"), "unhelpful error: {err}");
}
#[test]
fn a_private_multimethod_is_still_extensible_by_its_owner() {
let owner = r#"
(ns shapes.own)
(defmulti ^:private secret :kind)
(defmethod secret :x [_] :extended)
(def result (secret {:kind :x}))
"#;
let (_dir, _g, mut env) = env_with_sources(&[("shapes/own.cljrs", owner)]);
eval_in(&mut env, "(require 'shapes.own)").expect("own-ns defmethod");
assert_eq!(
eval_in(&mut env, "shapes.own/result").expect("result"),
Value::keyword(cljrs_value::Keyword::simple("extended"))
);
}
#[test]
fn a_versioned_name_is_refused_rather_than_silently_unpinned() {
let (_dir, _g, mut env) = env_with_sources(&[]);
let err = eval_in(
&mut env,
"(do (defmulti f :k) (defmethod f@abc1234 :a [_] 1))",
)
.expect_err("a versioned defmethod target should be refused");
assert!(err.contains("versioned"), "unhelpful error: {err}");
}
#[test]
fn a_pin_carried_in_the_namespace_half_is_refused_too() {
let (_dir, globals, mut env) = env_with_sources(&[]);
let mut pinned = Env::new(globals.clone(), "mylib@abc1234");
eval_in(
&mut pinned,
"(defmulti render :kind) (defmethod render :default [_] :from-the-pin)",
)
.expect("defmulti in the versioned namespace");
globals.add_alias("user", "v1", "mylib@abc1234");
let err = eval_in(&mut env, "(defmethod v1/render :x [_] :extended)")
.expect_err("extending a pinned multimethod should be refused");
assert!(
err.contains("versioned namespace"),
"unhelpful error: {err}"
);
assert_eq!(
eval_in(&mut env, "(v1/render {:kind :x})").expect("dispatch through the pin"),
Value::keyword(cljrs_value::Keyword::simple("from-the-pin"))
);
}
#[test]
fn a_versioned_namespace_can_still_extend_its_own_multimethods() {
let (_dir, globals, _env) = env_with_sources(&[]);
let mut pinned = Env::new(globals.clone(), "mylib@abc1234");
eval_in(
&mut pinned,
"(defmulti render :kind) (defmethod render :x [_] :own-method)",
)
.expect("a versioned namespace extending itself");
assert_eq!(
eval_in(&mut pinned, "(render {:kind :x})").expect("dispatch"),
Value::keyword(cljrs_value::Keyword::simple("own-method"))
);
}