use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn blue() -> Command {
let mut c = Command::new(env!("CARGO_BIN_EXE_blue"));
c.env_remove("BLUE_PATH")
.env_remove("BLUE_CONFIG")
.env_remove("BLUE_TIER")
.env_remove("BLUE_LANG")
.env_remove("BLUE_MEMO");
c
}
fn put(path: &Path, text: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, text).expect("write fixture");
}
fn out(o: &Output) -> String {
String::from_utf8_lossy(&o.stdout).into_owned()
}
fn err(o: &Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}
const SPIN: &str = "def spin(n)\n if n == 0\n :done\n else\n spin(n - 1)\n end\nend\n\ndef main()\n spin(1_000_000)\nend\n\ntest \"spins\"\n assert spin(1_000_000) == :done\nend\n";
const HOG: &str = "def main()\n length(map((x) -> [x, x], range(0, 20000)))\nend\n\ntest \"small\"\n assert length(map((x) -> [x, x], range(0, 10))) == 10\nend\n\ntest \"hog\"\n assert length(map((x) -> [x, x], range(0, 20000))) == 20000\nend\n";
const CHILD: &str = "def spin(n)\n if n == 0\n :done\n else\n spin(n - 1)\n end\nend\n\ndef child()\n p = spawn(spin, [1_000_000])\n monitor(p)\n last(receive())\nend\n\ndef main()\n child\nend\n\ntest \"a child spins to the end\"\n assert child == :normal\nend\n";
fn project(name: &str, bluefile: &str, app: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("blue-bounds-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
put(&dir.join("Bluefile"), bluefile);
put(&dir.join("app.b"), app);
let dir = dir.canonicalize().unwrap();
let locked = blue().arg("lock").arg(&dir).output().unwrap();
assert!(locked.status.success(), "{name}: {}", err(&locked));
dir
}
fn run(dir: &Path, cfg: Option<&Path>) -> Output {
let mut c = blue();
if let Some(cfg) = cfg {
c.env("BLUE_CONFIG", cfg);
}
c.arg("run").arg(dir.join("app.b")).output().unwrap()
}
fn test(dir: &Path) -> Output {
blue().arg("test").arg(dir.join("app.b")).output().unwrap()
}
fn config(dir: &Path, yaml: &str) -> PathBuf {
let at = dir.join("operator.yaml");
put(&at, yaml);
at
}
#[test]
fn a_step_bound_the_project_declares_refuses_a_loop_it_completes_without_it() {
let free = project("steps-free", "package(\"spin\", \"0.1.0\")\n", SPIN);
let ran = run(&free, None);
assert!(ran.status.success(), "{}", err(&ran));
assert_eq!(out(&ran).trim(), ":done");
let tested = test(&free);
assert!(tested.status.success(), "{}{}", out(&tested), err(&tested));
let bounded = project(
"steps-bounded",
"package(\"spin\", \"0.1.0\")\nbounds({steps: 100_000})\n",
SPIN,
);
let refused = run(&bounded, None);
assert!(!refused.status.success(), "{}", out(&refused));
assert!(
err(&refused).contains("fuel budget of 100000 exceeded in `spin::spin`"),
"{}",
err(&refused)
);
let held = blue()
.args(["run", "--allow", "none"])
.arg(bounded.join("app.b"))
.output()
.unwrap();
assert!(
err(&held).contains("fuel budget of 100000 exceeded"),
"--allow narrows the frame and keeps the bounds: {}",
err(&held)
);
let tested = test(&bounded);
assert!(!tested.status.success(), "{}", out(&tested));
assert!(
format!("{}{}", out(&tested), err(&tested)).contains("fuel budget of 100000 exceeded"),
"{}{}",
out(&tested),
err(&tested)
);
}
#[test]
fn a_live_byte_bound_the_project_declares_refuses_what_it_holds_without_it() {
let free = project("alloc-free", "package(\"hog\", \"0.1.0\")\n", HOG);
let ran = run(&free, None);
assert!(ran.status.success(), "{}", err(&ran));
assert_eq!(out(&ran).trim(), "20000");
let tested = test(&free);
assert!(
out(&tested).contains("2 passed, 0 failed"),
"{}{}",
out(&tested),
err(&tested)
);
let bounded = project(
"alloc-bounded",
"package(\"hog\", \"0.1.0\")\nbounds({alloc_bytes: 1_048_576})\n",
HOG,
);
let refused = run(&bounded, None);
assert!(!refused.status.success(), "{}", out(&refused));
assert!(
err(&refused).contains("memory-exhausted: live-byte budget of 1048576 exceeded"),
"{}",
err(&refused)
);
let tested = test(&bounded);
assert!(
!tested.status.success() && out(&tested).contains("1 passed, 1 failed"),
"{}{}",
out(&tested),
err(&tested)
);
assert!(
format!("{}{}", out(&tested), err(&tested))
.contains("memory-exhausted: live-byte budget of 1048576 exceeded"),
"{}{}",
out(&tested),
err(&tested)
);
}
#[test]
fn a_bound_the_project_declares_past_the_operators_does_not_widen_it() {
let wide = project(
"wide",
"package(\"spin\", \"0.1.0\")\nbounds({steps: 100_000_000, alloc_bytes: 1_073_741_824})\n",
SPIN,
);
let operator = config(&wide, "max_steps: 100000\n");
let refused = run(&wide, Some(&operator));
assert!(!refused.status.success(), "{}", out(&refused));
assert!(
err(&refused).contains("fuel budget of 100000 exceeded"),
"the operator's bound holds: {}",
err(&refused)
);
let hog = project(
"wide-hog",
"package(\"hog\", \"0.1.0\")\nbounds({alloc_bytes: 1_073_741_824})\n",
HOG,
);
let operator = config(&hog, "max_alloc_bytes: 1048576\n");
let refused = run(&hog, Some(&operator));
assert!(!refused.status.success(), "{}", out(&refused));
assert!(
err(&refused).contains("live-byte budget of 1048576 exceeded"),
"the operator's bound holds: {}",
err(&refused)
);
let narrow = project(
"narrow",
"package(\"spin\", \"0.1.0\")\nbounds({steps: 50_000})\n",
SPIN,
);
let operator = config(&narrow, "max_steps: 100000\n");
let refused = run(&narrow, Some(&operator));
assert!(
err(&refused).contains("fuel budget of 50000 exceeded"),
"the lesser of the two holds: {}",
err(&refused)
);
}
#[test]
fn blue_lock_records_the_bounds_and_confirm_holds_the_lock_to_them() {
let dir = project(
"lock",
"package(\"spin\", \"0.1.0\")\nbounds({steps: 100_000, call_depth: 2000})\n",
SPIN,
);
let written: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(dir.join("Bluefile.lock")).unwrap()).unwrap();
assert_eq!(
written["capabilities"]["bounds"],
serde_json::json!({"steps": 100_000, "call_depth": 2000}),
"{written}"
);
assert_eq!(
written["manifest"]["bounds"], written["capabilities"]["bounds"],
"{written}"
);
assert!(
written["capabilities"].get("ceiling").is_none(),
"{written}"
);
let confirm = || {
blue()
.args(["bluefile", "--confirm"])
.arg(dir.join("Bluefile"))
.output()
.unwrap()
};
let fresh = confirm();
assert!(fresh.status.success(), "{}", err(&fresh));
put(
&dir.join("Bluefile"),
"package(\"spin\", \"0.1.0\")\nbounds({steps: 200_000, call_depth: 2000})\n",
);
let stale = confirm();
assert!(!stale.status.success(), "a changed bound is stale");
assert!(
err(&stale).contains("recorded capabilities"),
"{}",
err(&stale)
);
let relocked = blue().arg("lock").arg(&dir).output().unwrap();
assert!(relocked.status.success(), "{}", err(&relocked));
assert!(confirm().status.success());
put(&dir.join("Bluefile"), "package(\"spin\", \"0.1.0\")\n");
let dropped = confirm();
assert!(
!dropped.status.success() && err(&dropped).contains("recorded capabilities"),
"a record whose Bluefile declares nothing is stale: {}",
err(&dropped)
);
let relocked = blue().arg("lock").arg(&dir).output().unwrap();
assert!(relocked.status.success(), "{}", err(&relocked));
let unbounded: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(dir.join("Bluefile.lock")).unwrap()).unwrap();
assert!(unbounded.get("capabilities").is_none(), "{unbounded}");
assert!(unbounded["manifest"].get("bounds").is_none(), "{unbounded}");
let ran = run(&dir, None);
assert_eq!(out(&ran).trim(), ":done", "{}", err(&ran));
}
#[test]
fn a_bound_that_is_not_a_positive_integer_refuses_the_lock() {
let dir = std::env::temp_dir().join(format!("blue-bounds-zero-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
put(
&dir.join("Bluefile"),
"package(\"spin\", \"0.1.0\")\nbounds({steps: 0})\n",
);
put(&dir.join("app.b"), SPIN);
let locked = blue().arg("lock").arg(&dir).output().unwrap();
assert!(!locked.status.success());
assert!(
err(&locked).contains("`bounds`: `steps` must be a positive integer, got 0"),
"{}",
err(&locked)
);
assert!(!dir.join("Bluefile.lock").exists());
let ran = blue().arg("run").arg(dir.join("app.b")).output().unwrap();
assert!(
!ran.status.success(),
"blue run refuses rather than run unbounded: {}",
out(&ran)
);
}
#[test]
fn a_process_the_program_spawns_is_held_to_the_projects_step_bound() {
let free = project("child-free", "package(\"spin\", \"0.1.0\")\n", CHILD);
let ran = run(&free, None);
assert_eq!(out(&ran).trim(), ":normal", "{}", err(&ran));
let tested = test(&free);
assert!(tested.status.success(), "{}{}", out(&tested), err(&tested));
let bounded = project(
"child-bounded",
"package(\"spin\", \"0.1.0\")\nbounds({steps: 100_000})\n",
CHILD,
);
let ran = run(&bounded, None);
assert!(
out(&ran).contains("[:runaway, ") && out(&ran).contains("(limit 100000)"),
"the child exits on the project's bound: {}{}",
out(&ran),
err(&ran)
);
let tested = test(&bounded);
assert!(
!tested.status.success() && out(&tested).contains("0 passed, 1 failed"),
"a child a test spawns is held too: {}{}",
out(&tested),
err(&tested)
);
}
fn served(opts: &str) -> String {
format!(
"def spin(k)\n if k == 0\n 0\n else\n spin(k - 1)\n end\nend\n\ndef h(m, n)\n if list?(m)\n send(first(m), n)\n n\n elsif m == 0\n spin(5000)\n n + 1000\n else\n n + m\n end\nend\n\ndef main()\n s = spawn_server(h, 0{opts})\n monitor(s)\n send(s, 1)\n send(s, 0)\n send(s, [self])\n receive(:_, 5000)\nend\n"
)
}
#[test]
fn a_dispatch_bound_the_project_declares_meets_the_one_a_server_is_spawned_with() {
let free = project(
"dispatch-free",
"package(\"srv\", \"0.1.0\")\n",
&served(""),
);
let ran = run(&free, None);
assert_eq!(out(&ran).trim(), "1001", "{}", err(&ran));
let declared = "package(\"srv\", \"0.1.0\")\nbounds({dispatch_steps: 2000})\n";
let bounded = project("dispatch-bounded", declared, &served(""));
let written: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(bounded.join("Bluefile.lock")).unwrap())
.unwrap();
assert_eq!(
written["capabilities"]["bounds"],
serde_json::json!({"dispatch_steps": 2000}),
"{written}"
);
for (opts, limit) in [
("", 2000),
(", {dispatch_steps: 1500}", 1500),
(", {dispatch_steps: 100_000}", 2000),
] {
let dir = project(
&format!("dispatch-{limit}-{}", opts.len()),
declared,
&served(opts),
);
let ran = run(&dir, None);
assert!(
out(&ran).contains(&format!(
"[:runaway, execution budget exhausted: steps per dispatch (limit {limit})]"
)),
"{opts}: {}{}",
out(&ran),
err(&ran)
);
assert!(!err(&ran).contains("formatted"), "{}", err(&ran));
}
let narrowed = project(
"dispatch-spawned",
"package(\"srv\", \"0.1.0\")\n",
&served(", {dispatch_steps: 1500}"),
);
let ran = run(&narrowed, None);
assert!(
out(&ran).contains("steps per dispatch (limit 1500)"),
"{}{}",
out(&ran),
err(&ran)
);
}