use std::time::{Duration, Instant};
use aura_lang::{Budget, Error, Script};
use serde::{Deserialize, Serialize};
fn load(src: &str) -> Script {
Script::builder()
.source("t.aura", src)
.load()
.unwrap_or_else(|e| panic!("load failed:\n{e}"))
}
fn load_err(src: &str) -> Error {
match Script::builder().source("t.aura", src).load() {
Ok(_) => panic!("load was expected to fail"),
Err(e) => e,
}
}
fn code(e: &Error) -> &'static str {
e.code()
}
#[test]
fn load_is_hermetic_by_default() {
let e = load_err(concat!(
"pub def f(x)\n",
" v: x > 1000 ? env(\"HOME\", \"-\") : \"never\"\n",
"end\n",
));
assert_eq!(code(&e), "E0505", "{e}");
}
#[test]
fn grants_mirror_the_cli() {
let rules = Script::builder()
.source(
"t.aura",
"pub def f()\n r: env(\"AURA_TEST_REGION\", \"none\")\nend\n",
)
.allow_env(["AURA_TEST_REGION"])
.load()
.expect("granted variable loads");
let _: serde_json::Value = rules.call("f", ()).expect("granted variable is readable");
let denied = Script::builder()
.source("t.aura", "pub def f()\n h: env(\"HOME\", \"none\")\nend\n")
.allow_env(["AURA_TEST_REGION"])
.load()
.expect("loads: the analysis cannot know which name is read")
.call::<_, serde_json::Value>("f", ());
assert_eq!(code(&denied.unwrap_err()), "E0310");
}
#[test]
fn grants_do_not_reach_imports() {
let rules = Script::builder()
.source(
"lib.aura",
"pub def peek()\n v: env(\"AURA_TEST_REGION\", \"-\")\nend\n",
)
.source(
"main.aura",
"import \"./lib.aura\" as lib\npub def f()\n v: lib.peek()\nend\n",
)
.entry("main.aura")
.allow_env(["AURA_TEST_REGION"])
.load()
.expect("loads");
let err = rules.call::<_, serde_json::Value>("f", ()).unwrap_err();
assert_eq!(code(&err), "E0310", "{err}");
}
#[test]
fn a_budget_stops_the_runaway_that_motivated_it() {
let rules = load(concat!(
"pub def f()\n",
" x: range(1000).map (a, i) -> range(1000).map (b, j) -> ",
"range(1000).map (c, k) -> 1 end end end\n",
"end\n",
));
let started = Instant::now();
let err = rules.call::<_, serde_json::Value>("f", ()).unwrap_err();
assert!(matches!(code(&err), "E0325" | "E0326"), "{err}");
assert!(
started.elapsed() < Duration::from_secs(5),
"the budget did not stop the call promptly: {:?}",
started.elapsed()
);
}
#[test]
fn the_step_budget_stops_a_call_that_allocates_nothing() {
let rules = Script::builder()
.source(
"t.aura",
concat!(
"pub def f()\n",
" n: range(200).reduce(0) (acc, a) -> range(200).reduce(acc) (acc2, b) -> ",
"range(200).reduce(acc2) (acc3, c) -> acc3 + 1 end end end\n",
"end\n",
),
)
.budget(Budget::per_call().steps(100_000).memory_bytes(u64::MAX))
.load()
.expect("loads");
let err = rules.call::<_, serde_json::Value>("f", ()).unwrap_err();
assert_eq!(code(&err), "E0325", "{err}");
}
#[test]
fn the_memory_budget_stops_a_call_that_allocates_too_much() {
let rules = Script::builder()
.source(
"t.aura",
"pub def f()\n xs: range(1000000).map (x, i) -> \"#{x}#{x}#{x}\" end\nend\n",
)
.budget(Budget::per_call().memory_mb(4))
.load()
.expect("loads");
let err = rules.call::<_, serde_json::Value>("f", ()).unwrap_err();
assert_eq!(code(&err), "E0326", "{err}");
}
#[test]
fn the_budget_is_deterministic() {
let src = "pub def f(n)\n xs: range(n).map (x, i) -> x * 2 end\nend\n";
let tight = |n: i64| {
Script::builder()
.source("t.aura", src)
.budget(Budget::per_call().steps(10_000))
.load()
.expect("loads")
.call::<_, serde_json::Value>("f", (n,))
.map_err(|e| e.code())
};
let first = tight(4_000);
for _ in 0..20 {
assert_eq!(tight(4_000), first);
}
assert!(tight(10).is_ok());
assert_eq!(tight(1_000_000), Err("E0325"));
}
#[test]
fn the_default_budget_is_finite() {
let rules = load(concat!(
"pub def f()\n",
" x: range(1000).map (a, i) -> range(1000).map (b, j) -> ",
"range(1000).map (c, k) -> 1 end end end\n",
"end\n",
));
let err = rules.call::<_, serde_json::Value>("f", ()).unwrap_err();
assert!(matches!(code(&err), "E0325" | "E0326"), "{err}");
}
#[test]
fn a_failed_call_leaves_the_script_usable() {
let rules = Script::builder()
.source(
"t.aura",
"pub def f(n)\n assert n >= 0, \"n must not be negative\"\n xs: range(n).map (x, i) -> x end.len()\nend\n",
)
.budget(Budget::per_call().steps(10_000))
.load()
.expect("loads");
assert_eq!(
code(&rules.call::<_, serde_json::Value>("f", (-1,)).unwrap_err()),
"E0530"
);
assert_eq!(
code(
&rules
.call::<_, serde_json::Value>("f", (1_000_000,))
.unwrap_err()
),
"E0325"
);
#[derive(Deserialize)]
struct Out {
xs: i64,
}
let ok: Out = rules.call("f", (5,)).expect("a later call is unaffected");
assert_eq!(ok.xs, 5);
}
#[test]
fn a_script_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Script>();
}
#[test]
fn concurrent_calls_agree() {
let rules = std::sync::Arc::new(load("pub def double(x)\n v: x * 2\nend\n"));
#[derive(Deserialize)]
struct V {
v: i64,
}
let handles: Vec<_> = (0..8)
.map(|t| {
let rules = rules.clone();
std::thread::spawn(move || {
for i in 0..1_000i64 {
let out: V = rules.call("double", (i + t,)).expect("call");
assert_eq!(out.v, (i + t) * 2);
}
})
})
.collect();
for h in handles {
h.join().expect("thread");
}
}
#[test]
fn binding_a_function_checks_it_once() {
let rules = load("pub def f(a, b)\n v: a\nend\nversion: \"1\"\n");
let missing = rules.function::<(i64, i64), serde_json::Value>("g");
assert_eq!(code(&missing.unwrap_err()), "E0002");
let not_fn = rules.function::<(i64, i64), serde_json::Value>("version");
assert_eq!(code(¬_fn.unwrap_err()), "E0003");
let arity = rules.function::<(i64,), serde_json::Value>("f");
assert_eq!(code(&arity.unwrap_err()), "E0004");
assert!(rules.function::<(i64, i64), serde_json::Value>("f").is_ok());
}
#[test]
fn the_boundary_refuses_what_it_cannot_carry() {
let rules = load("pub def id(x)\n v: x\nend\n");
let too_big = rules.call::<_, serde_json::Value>("id", (u64::MAX,));
assert_eq!(code(&too_big.unwrap_err()), "E0005");
let nan = rules.call::<_, serde_json::Value>("id", (f64::NAN,));
assert_eq!(code(&nan.unwrap_err()), "E0005");
}
#[test]
fn a_result_of_the_wrong_shape_names_the_path() {
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct Out {
inner: Inner,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct Inner {
port: u16,
}
let rules = load("pub def f()\n inner:\n port: \"eighty\"\n end\nend\n");
let err = rules.call::<_, Out>("f", ()).unwrap_err();
assert_eq!(code(&err), "E0006");
assert!(err.to_string().contains("inner.port"), "{err}");
}
#[test]
fn host_types_cross_both_ways() {
#[derive(Serialize)]
struct Event {
device: &'static str,
temp_c: f64,
}
#[derive(Deserialize, Debug, PartialEq)]
struct Summary {
total: u64,
alerts: u64,
}
let rules = load(concat!(
"pub def summarize(events)\n",
" total: events.len()\n",
" alerts: events.filter (e, i) -> e.temp_c > 80.0 end.len()\n",
"end\n",
));
let summarize = rules
.function::<(&[Event],), Summary>("summarize")
.expect("binds");
let events = [
Event {
device: "a",
temp_c: 20.0,
},
Event {
device: "b",
temp_c: 85.5,
},
];
assert_eq!(
summarize.call((&events[..],)).expect("call"),
Summary {
total: 2,
alerts: 1
}
);
let keys: serde_json::Value = rules.call("summarize", (&events[..],)).expect("call");
let order: Vec<_> = keys.as_object().expect("object").keys().cloned().collect();
assert_eq!(order, ["total", "alerts"]);
}
#[test]
fn errors_read_like_the_cli() {
let e = load_err("pub def f()\n v: 1 +\nend\n");
assert!(e.code().starts_with("E02"), "{e}");
let text = e.to_string();
assert!(text.contains("t.aura:"), "no position in: {text}");
}