use std::fs;
use std::path::Path;
use std::process::Command;
fn allium() -> Command {
Command::new(env!("CARGO_BIN_EXE_allium"))
}
struct Diag {
code: String,
message: String,
}
fn parse_diagnostics(stdout: &str) -> Vec<Diag> {
let mut diags = Vec::new();
for doc in split_json_docs(stdout) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&doc) {
if let Some(arr) = v["diagnostics"].as_array() {
for d in arr {
if let (Some(c), Some(m)) = (d["code"].as_str(), d["message"].as_str()) {
diags.push(Diag {
code: c.to_string(),
message: m.to_string(),
});
}
}
}
}
}
diags
}
fn ambiguity_warnings(stdout: &str) -> Vec<Diag> {
parse_diagnostics(stdout)
.into_iter()
.filter(|d| d.code == "allium.use.ambiguousReference")
.collect()
}
fn split_json_docs(s: &str) -> Vec<String> {
let mut docs = Vec::new();
let mut depth = 0i32;
let mut start = None;
for (i, ch) in s.char_indices() {
match ch {
'{' => {
if depth == 0 {
start = Some(i);
}
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
if let Some(s_idx) = start {
docs.push(s[s_idx..=i].to_string());
}
start = None;
}
}
_ => {}
}
}
docs
}
struct TempDir {
path: std::path::PathBuf,
}
impl TempDir {
fn new(name: &str) -> Self {
let path = std::env::temp_dir().join(format!("allium-test-{name}-{}", std::process::id()));
fs::create_dir_all(&path).unwrap();
Self { path }
}
fn write(&self, name: &str, content: &str) {
fs::write(self.path.join(name), content).unwrap();
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn check(dir: &TempDir) -> String {
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
String::from_utf8_lossy(&output.stdout).into_owned()
}
const ORDERS_INVOICE: &str = "-- allium: 3\nentity Invoice {\n processed: Boolean\n}\n";
const BILLING_INVOICE: &str = "-- allium: 3\nentity Invoice {\n processed: Boolean\n}\n";
#[test]
fn ambiguous_entity_reference_warns() {
let dir = TempDir::new("ambiguous-entity");
dir.write("orders.allium", ORDERS_INVOICE);
dir.write("billing.allium", BILLING_INVOICE);
dir.write(
"main.allium",
"-- allium: 3\nuse \"./orders.allium\" as orders\nuse \"./billing.allium\" as billing\n\nrule Process {\n when: i: Invoice.created()\n ensures: i.processed = true\n}\n",
);
let warnings = ambiguity_warnings(&check(&dir));
assert_eq!(
warnings.len(),
1,
"expected one ambiguity warning for 'Invoice'"
);
let msg = &warnings[0].message;
assert!(msg.contains("'Invoice'"), "message: {msg}");
assert!(msg.contains("'billing' and 'orders'"), "message: {msg}");
assert!(msg.contains("billing/Invoice"), "message: {msg}");
}
#[test]
fn qualified_reference_not_ambiguous() {
let dir = TempDir::new("qualified-ref");
dir.write("orders.allium", ORDERS_INVOICE);
dir.write("billing.allium", BILLING_INVOICE);
dir.write(
"main.allium",
"-- allium: 3\nuse \"./orders.allium\" as orders\nuse \"./billing.allium\" as billing\n\nrule Process {\n when: i: orders/Invoice.created()\n ensures: i.processed = true\n}\n",
);
let warnings = ambiguity_warnings(&check(&dir));
assert!(
warnings.is_empty(),
"qualified reference should not warn: {:?}",
warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn local_declaration_shadows_imports() {
let dir = TempDir::new("local-shadow");
dir.write("orders.allium", ORDERS_INVOICE);
dir.write("billing.allium", BILLING_INVOICE);
dir.write(
"main.allium",
"-- allium: 3\nuse \"./orders.allium\" as orders\nuse \"./billing.allium\" as billing\n\nentity Invoice {\n processed: Boolean\n}\n\nrule Process {\n when: i: Invoice.created()\n ensures: i.processed = true\n}\n",
);
let warnings = ambiguity_warnings(&check(&dir));
assert!(
warnings.is_empty(),
"local declaration shadows imports: {:?}",
warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn single_import_declaring_name_not_ambiguous() {
let dir = TempDir::new("single-import");
dir.write("orders.allium", ORDERS_INVOICE);
dir.write(
"billing.allium",
"-- allium: 3\nentity Receipt {\n total: Decimal\n}\n",
);
dir.write(
"main.allium",
"-- allium: 3\nuse \"./orders.allium\" as orders\nuse \"./billing.allium\" as billing\n\nrule Process {\n when: i: Invoice.created()\n ensures: i.processed = true\n}\n",
);
let warnings = ambiguity_warnings(&check(&dir));
assert!(
warnings.is_empty(),
"one declaring import is not ambiguous: {:?}",
warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn two_aliases_for_same_file_not_ambiguous() {
let dir = TempDir::new("same-target");
dir.write("orders.allium", ORDERS_INVOICE);
dir.write(
"main.allium",
"-- allium: 3\nuse \"./orders.allium\" as orders\nuse \"./orders.allium\" as billing\n\nrule Process {\n when: i: Invoice.created()\n ensures: i.processed = true\n}\n",
);
let warnings = ambiguity_warnings(&check(&dir));
assert!(
warnings.is_empty(),
"two aliases for one file resolve identically: {:?}",
warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn ambiguous_trigger_subscription_warns() {
let dir = TempDir::new("ambiguous-trigger");
dir.write(
"a.allium",
"-- allium: 3\nrule EmitA {\n when: StartA(x)\n ensures: Pinged(subject: x)\n}\n",
);
dir.write(
"b.allium",
"-- allium: 3\nrule EmitB {\n when: StartB(x)\n ensures: Pinged(subject: x)\n}\n",
);
dir.write(
"main.allium",
"-- allium: 3\nuse \"./a.allium\" as a\nuse \"./b.allium\" as b\n\nrule HandlePing {\n when: Pinged(subject)\n ensures: PingHandled(subject: subject)\n}\n",
);
let warnings = ambiguity_warnings(&check(&dir));
assert_eq!(
warnings.len(),
1,
"expected one ambiguity warning for trigger 'Pinged'"
);
let msg = &warnings[0].message;
assert!(msg.contains("'Pinged'"), "message: {msg}");
assert!(msg.contains("'a' and 'b'"), "message: {msg}");
assert!(msg.contains("a/Pinged"), "message: {msg}");
}
#[test]
fn qualified_trigger_subscription_not_ambiguous() {
let dir = TempDir::new("qualified-trigger");
dir.write(
"a.allium",
"-- allium: 3\nrule EmitA {\n when: StartA(x)\n ensures: Pinged(subject: x)\n}\n",
);
dir.write(
"b.allium",
"-- allium: 3\nrule EmitB {\n when: StartB(x)\n ensures: Pinged(subject: x)\n}\n",
);
dir.write(
"main.allium",
"-- allium: 3\nuse \"./a.allium\" as a\nuse \"./b.allium\" as b\n\nrule HandlePing {\n when: a/Pinged(subject)\n ensures: PingHandled(subject: subject)\n}\n",
);
let warnings = ambiguity_warnings(&check(&dir));
assert!(
warnings.is_empty(),
"qualified subscription should not warn: {:?}",
warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}