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();
let docs = split_json_docs(stdout);
for doc in &docs {
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 diagnostic_codes(stdout: &str) -> Vec<String> {
parse_diagnostics(stdout).into_iter().map(|d| d.code).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);
}
}
#[test]
fn cross_module_ref_suppresses_unused_entity() {
let dir = TempDir::new("suppress-entity");
dir.write("core.allium", "-- allium: 3\nentity InputEvent {\n payload: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nrule Handle {\n when: e: core/InputEvent\n ensures: e.payload = \"done\"\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
!codes.iter().any(|c| c == "allium.entity.unused"),
"InputEvent should not be flagged as unused when referenced cross-module.\nDiagnostics: {codes:?}"
);
}
#[test]
fn unqualified_entity_ref_in_rule_suppresses_unused() {
let dir = TempDir::new("unqualified-rule");
dir.write("core.allium", "-- allium: 3\nentity InputPartition {\n current_offset: Integer\n}\n");
dir.write(
"usher.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nrule Advance {\n when: record: Record\n ensures: InputPartition.current_offset = record.offset\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.code == "allium.entity.unused" && d.message.contains("InputPartition")),
"InputPartition referenced unqualified in rule body should not be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn alias_dot_member_ref_suppresses_unused() {
let dir = TempDir::new("alias-dot");
dir.write("core.allium", "-- allium: 3\nentity EntityMap {\n entries: Set<String>\n}\n");
dir.write(
"surface.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nsurface Dashboard {\n facing user: User\n exposes:\n core.EntityMap\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.code == "allium.entity.unused" && d.message.contains("EntityMap")),
"EntityMap referenced via core.EntityMap should not be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn unqualified_type_in_contract_suppresses_unused() {
let dir = TempDir::new("contract-type");
dir.write("core.allium", "-- allium: 3\nentity EntityMap {\n entries: Set<String>\n}\n\nvalue EventOutcome {\n success: Boolean\n}\n");
dir.write(
"contracts.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\ncontract DeterministicEvaluation {\n evaluate: EntityMap\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.code == "allium.entity.unused" && d.message.contains("EntityMap")),
"EntityMap referenced in contract signature should not be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
assert!(
diags.iter().any(|d| d.code == "allium.definition.unused" && d.message.contains("EventOutcome")),
"EventOutcome is genuinely unused and should still warn.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn local_declaration_does_not_suppress_imported_same_name() {
let dir = TempDir::new("local-shadows");
dir.write("shared.allium", "-- allium: 3\nentity Order {\n total: Decimal\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./shared.allium\" as shared\n\nentity Order {\n amount: Decimal\n}\n\nrule Process {\n when: o: Order\n ensures: o.amount > 0\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
diags.iter().any(|d| d.code == "allium.entity.unused" && d.message.contains("Order")),
"shared's Order should still be flagged — consumer's local Order shadows it.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn cross_module_ref_in_ensures_suppresses_unused() {
let dir = TempDir::new("suppress-ensures");
dir.write("statuses.allium", "-- allium: 3\nvalue Active {\n since: Timestamp\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./statuses.allium\" as statuses\n\nrule Activate {\n when: order: Order\n ensures: order.state = statuses/Active\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.code == "allium.definition.unused" && d.message.contains("Active")),
"Active referenced in ensures should not be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn cross_module_ref_suppresses_unused_value() {
let dir = TempDir::new("suppress-value");
dir.write("shared.allium", "-- allium: 3\nvalue Snapshot {\n version: Integer\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./shared.allium\" as shared\n\nentity Record {\n snap: shared/Snapshot\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
!codes.iter().any(|c| c == "allium.definition.unused"),
"Snapshot should not be flagged as unused.\nDiagnostics: {codes:?}"
);
}
#[test]
fn cross_module_ref_suppresses_unused_enum() {
let dir = TempDir::new("suppress-enum");
dir.write("shared.allium", "-- allium: 3\nenum Priority {\n low\n medium\n high\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./shared.allium\" as shared\n\nentity Task {\n priority: shared/Priority\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
!codes.iter().any(|c| c == "allium.definition.unused"),
"Priority enum should not be flagged as unused.\nDiagnostics: {codes:?}"
);
}
#[test]
fn unreferenced_entity_still_warns_in_multi_file() {
let dir = TempDir::new("still-warns");
dir.write("core.allium", "-- allium: 3\nentity Used {\n x: String\n}\n\nentity Orphan {\n y: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nentity Handler {\n event: core/Used\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Orphan"),
"Orphan should still be flagged as unused.\nOutput: {stdout}"
);
}
#[test]
fn single_file_check_still_warns_for_unused() {
let dir = TempDir::new("single-file");
dir.write("core.allium", "-- allium: 3\nentity InputEvent {\n payload: String\n}\n");
let output = allium()
.args(["check", &dir.path().join("core.allium").to_string_lossy()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
codes.iter().any(|c| c == "allium.entity.unused"),
"InputEvent should be flagged when checked alone.\nDiagnostics: {codes:?}"
);
}
#[test]
fn multiple_consumers_all_contribute_refs() {
let dir = TempDir::new("multi-consumer");
dir.write(
"core.allium",
"-- allium: 3\nentity EventA {\n x: String\n}\n\nentity EventB {\n y: String\n}\n",
);
dir.write(
"consumer1.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nentity Handler1 {\n a: core/EventA\n}\n",
);
dir.write(
"consumer2.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nentity Handler2 {\n b: core/EventB\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.code == "allium.entity.unused" && d.message.contains("EventA")),
"EventA should not be flagged as unused.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
assert!(
!diags.iter().any(|d| d.code == "allium.entity.unused" && d.message.contains("EventB")),
"EventB should not be flagged as unused.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn use_without_alias_does_not_suppress() {
let dir = TempDir::new("no-alias");
dir.write("core.allium", "-- allium: 3\nentity InputEvent {\n payload: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./core.allium\"\n\nentity Handler {\n x: String\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
codes.iter().any(|c| c == "allium.entity.unused"),
"InputEvent should still be unused — consumer has no alias to reference it.\nDiagnostics: {codes:?}"
);
}
#[test]
fn alias_to_file_outside_check_set_does_not_suppress() {
let dir = TempDir::new("outside-set");
dir.write("core.allium", "-- allium: 3\nentity InputEvent {\n payload: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./other.allium\" as other\n\nentity Handler {\n event: other/InputEvent\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
codes.iter().any(|c| c == "allium.entity.unused"),
"InputEvent in core.allium is not referenced by consumer's alias.\nDiagnostics: {codes:?}"
);
}
#[test]
fn analyse_command_respects_cross_module_refs() {
let dir = TempDir::new("analyse-xmod");
dir.write("core.allium", "-- allium: 3\nentity InputEvent {\n payload: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nrule Handle {\n when: e: core/InputEvent\n ensures: e.payload = \"done\"\n}\n",
);
let output = allium()
.args(["analyse", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
!codes.iter().any(|c| c == "allium.entity.unused"),
"analyse should also suppress cross-module unused warnings.\nDiagnostics: {codes:?}"
);
}
#[test]
fn subdirectory_use_path_resolves() {
let dir = TempDir::new("subdir-path");
fs::create_dir_all(dir.path().join("shared")).unwrap();
dir.write("shared/types.allium", "-- allium: 3\nvalue Money {\n amount: Decimal\n currency: String\n}\n");
dir.write(
"order.allium",
"-- allium: 3\nuse \"./shared/types.allium\" as types\n\nentity Order {\n total: types/Money\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
!codes.iter().any(|c| c == "allium.definition.unused"),
"Money should not be flagged — referenced via subdirectory path.\nDiagnostics: {codes:?}"
);
}
#[test]
fn bare_relative_use_path_resolves() {
let dir = TempDir::new("bare-relative");
dir.write("core.allium", "-- allium: 3\nentity Event {\n x: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"core.allium\" as core\n\nentity Handler {\n event: core/Event\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.code == "allium.entity.unused" && d.message.contains("Event")),
"Event should not be flagged — referenced via bare relative path.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
assert!(
!diags.iter().any(|d| d.code == "allium.use.unresolvedPath"),
"Bare relative path should resolve.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn single_file_check_does_not_warn_unresolved_use() {
let dir = TempDir::new("single-no-unresolved");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./missing.allium\" as missing\n\nentity Handler {\n x: String\n}\n",
);
let output = allium()
.args(["check", &dir.path().join("consumer.allium").to_string_lossy()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
codes.iter().any(|c| c == "allium.use.unresolvedPath"),
"Unresolved use path should warn even with a single file.\nDiagnostics: {codes:?}"
);
}
#[test]
fn unresolved_use_path_warns_when_target_missing() {
let dir = TempDir::new("unresolved-missing");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./missing.allium\" as missing\n\nentity Handler {\n x: String\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
codes.iter().any(|c| c == "allium.use.unresolvedPath"),
"Should warn about unresolved use path.\nDiagnostics: {codes:?}"
);
}
#[test]
fn resolved_use_path_no_warning() {
let dir = TempDir::new("resolved-ok");
dir.write("core.allium", "-- allium: 3\nentity Event {\n x: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nentity Handler {\n event: core/Event\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
!codes.iter().any(|c| c == "allium.use.unresolvedPath"),
"Should not warn — target file is in the check set.\nDiagnostics: {codes:?}"
);
}
#[test]
fn use_target_exists_but_not_in_check_set_warns() {
let dir = TempDir::new("not-in-set");
dir.write("core.allium", "-- allium: 3\nentity Event {\n x: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./core.allium\" as core\n\nentity Handler {\n event: core/Event\n}\n",
);
let output = allium()
.args(["check", &dir.path().join("consumer.allium").to_string_lossy()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
codes.iter().any(|c| c == "allium.use.unresolvedPath"),
"Should warn — core.allium exists but is not in the check set.\nDiagnostics: {codes:?}"
);
}
#[test]
fn unresolved_use_path_message_names_file() {
let dir = TempDir::new("unresolved-msg");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./phantom.allium\" as phantom\n\nentity Handler {\n x: String\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
let d = diags.iter().find(|d| d.code == "allium.use.unresolvedPath")
.expect("expected allium.use.unresolvedPath diagnostic");
assert!(
d.message.contains("phantom.allium"),
"message should name the path: {}", d.message
);
}
#[test]
fn mixed_resolved_and_unresolved_use_paths() {
let dir = TempDir::new("mixed-use");
dir.write("found.allium", "-- allium: 3\nentity Found {\n x: String\n}\n");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./found.allium\" as found\nuse \"./lost.allium\" as lost\n\nentity Handler {\n f: found/Found\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
let unresolved: Vec<_> = diags.iter()
.filter(|d| d.code == "allium.use.unresolvedPath")
.collect();
assert_eq!(unresolved.len(), 1, "only lost.allium should be unresolved: {:?}",
unresolved.iter().map(|d| &d.message).collect::<Vec<_>>());
assert!(unresolved[0].message.contains("lost.allium"));
}
#[test]
fn analyse_also_reports_unresolved_use_paths() {
let dir = TempDir::new("analyse-unresolved");
dir.write(
"consumer.allium",
"-- allium: 3\nuse \"./ghost.allium\" as ghost\n\nentity Handler {\n x: String\n}\n",
);
let output = allium()
.args(["analyse", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let codes = diagnostic_codes(&stdout);
assert!(
codes.iter().any(|c| c == "allium.use.unresolvedPath"),
"analyse should also report unresolved use paths.\nDiagnostics: {codes:?}"
);
}
#[test]
fn qualified_contract_in_fulfils_parses_across_modules() {
let dir = TempDir::new("qualified-contract");
dir.write(
"base.allium",
"-- allium: 3\n\nentity Caller { id: String }\n\ncontract MyContract {\n do_thing: (caller: Caller) -> String\n}\n",
);
dir.write(
"impl.allium",
"-- allium: 3\n\nuse \"./base.allium\" as base\n\nsurface MySurface {\n facing user: base/Caller\n contracts:\n fulfils base/MyContract\n demands base/MyContract\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.message.contains("expected block item")),
"qualified contract names should parse in contracts: clauses.\nDiagnostics: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
assert!(
output.status.success(),
"check should exit 0; stdout:\n{stdout}"
);
}
#[test]
fn qualified_trigger_subscription_resolves_across_modules() {
let dir = TempDir::new("qualified-trigger");
dir.write(
"emitter.allium",
"-- allium: 3\n\nexternal entity Subject { id: Integer }\n\nrule EmitPing {\n when: Tick()\n ensures: Pinged(subject: Subject{id: 1})\n}\n",
);
dir.write(
"listener.allium",
"-- allium: 3\n\nuse \"./emitter.allium\" as emitter\n\nrule HandlePing {\n when: emitter/Pinged(subject)\n ensures: PingHandled(subject: subject)\n}\n",
);
let output = allium()
.args(["check", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| d.code == "allium.rule.invalidTrigger"),
"qualified trigger call should be a valid trigger form.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
assert!(
!diags.iter().any(|d| {
d.code == "allium.rule.unreachableTrigger" && d.message.contains("Pinged")
}),
"emitter/Pinged is emitted by emitter.allium and should not be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
assert!(
output.status.success(),
"check should exit 0; stdout:\n{stdout}"
);
}
#[test]
fn qualified_trigger_flagged_when_target_module_lacks_it() {
let dir = TempDir::new("qualified-trigger-missing");
dir.write(
"emitter.allium",
"-- allium: 3\n\nrule EmitPing {\n when: Tick()\n ensures: Pinged(subject: 1)\n}\n",
);
dir.write(
"listener.allium",
"-- allium: 3\n\nuse \"./emitter.allium\" as emitter\n\nrule HandlePong {\n when: emitter/Ponged(subject)\n ensures: PongHandled(subject: subject)\n}\n",
);
let output = allium()
.args(["analyse", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
let flagged: Vec<_> = diags
.iter()
.filter(|d| {
d.code == "allium.rule.unreachableTrigger" && d.message.contains("emitter/Ponged")
})
.collect();
assert_eq!(
flagged.len(),
1,
"emitter/Ponged is not emitted by emitter.allium and should be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
assert!(
flagged[0].message.contains("imported module 'emitter'"),
"message should name the imported module: {}",
flagged[0].message
);
}
#[test]
fn unqualified_trigger_subscription_resolves_across_modules() {
let dir = TempDir::new("unqualified-trigger");
dir.write(
"emitter.allium",
"-- allium: 3\n\nrule EmitPing {\n when: Tick()\n ensures: Pinged(subject: 1)\n}\n",
);
dir.write(
"listener.allium",
"-- allium: 3\n\nuse \"./emitter.allium\" as emitter\n\nrule HandlePing {\n when: Pinged(subject)\n ensures: PingHandled(subject: subject)\n}\n",
);
let output = allium()
.args(["analyse", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| {
d.code == "allium.rule.unreachableTrigger" && d.message.contains("Pinged")
}),
"Pinged is emitted by the imported emitter.allium and should not be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn conditional_branch_emission_reaches_cross_module_listener() {
let dir = TempDir::new("branch-emission");
dir.write(
"router.allium",
"-- allium: 3\n\nrule AdvertRouted {\n when: AdvertReceived(envelope)\n ensures:\n if exists envelope:\n Logged(envelope: envelope)\n else:\n SensorAdvertDecoded(advert: envelope)\n}\n",
);
dir.write(
"handler.allium",
"-- allium: 3\n\nuse \"./router.allium\" as router\n\nrule HandleDecoded {\n when: router/SensorAdvertDecoded(advert)\n ensures: Done(advert: advert)\n}\n",
);
let output = allium()
.args(["analyse", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| {
d.code == "allium.rule.unreachableTrigger"
&& d.message.contains("SensorAdvertDecoded")
}),
"the else-branch emission should reach the cross-module listener.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}
#[test]
fn qualified_trigger_with_alias_outside_check_set_not_flagged() {
let dir = TempDir::new("trigger-outside-set");
dir.write(
"listener.allium",
"-- allium: 3\n\nuse \"github.com/allium-specs/oauth/abc123\" as oauth\n\nrule Audit {\n when: oauth/SessionCreated(session)\n ensures: Logged(session: session)\n}\n",
);
let output = allium()
.args(["analyse", dir.path().to_str().unwrap()])
.output()
.expect("spawn allium");
let stdout = String::from_utf8_lossy(&output.stdout);
let diags = parse_diagnostics(&stdout);
assert!(
!diags.iter().any(|d| {
d.code == "allium.rule.unreachableTrigger"
&& d.message.contains("SessionCreated")
}),
"subscription to a module outside the check set should not be flagged.\nDiagnostics: {:?}",
diags.iter().map(|d| (&d.code, &d.message)).collect::<Vec<_>>()
);
}