use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use bynk_emit::project;
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn scratch_project(tag: &str, files: &[(&str, &str)]) -> Scratch {
let dir = std::env::temp_dir().join(format!(
"bynk_696_{tag}_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = fs::remove_dir_all(&dir);
for (rel, body) in files {
let p = dir.join(rel);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(&p, body).unwrap();
}
Scratch(dir)
}
#[test]
fn a_consumes_cycle_renders_with_ariadne_source_context() {
let root = scratch_project(
"cycle",
&[
(
"src/commerce/a.bynk",
"context commerce.a {\n consumes commerce.b\n}\n",
),
(
"src/commerce/b.bynk",
"context commerce.b {\n consumes commerce.a\n}\n",
),
],
);
let opts = bynk_driver::project_options(&root.0);
let failure = match project::compile_project(&opts) {
Err(f) => f,
Ok(_) => panic!("a `consumes` cycle must fail the build"),
};
let snapshots: HashMap<&Path, &str> = failure
.snapshots
.iter()
.map(|(p, t)| (p.as_path(), t.as_str()))
.collect();
let cycle = failure
.errors
.iter()
.find(|e| e.error.category == "bynk.context.consumes_cycle")
.expect("the build must report a consumes cycle");
let path = cycle
.source_path
.as_deref()
.expect("the consumes cycle must be attributed to a file, not left unattributed");
let text = snapshots
.get(path)
.expect("the attributed file must have a snapshot to render against");
assert!(
cycle.error.span.start > 0,
"the cycle must be anchored on a real `consumes` clause span, got {:?}",
cycle.error.span,
);
let label = path.to_string_lossy().replace('\\', "/");
let rendered =
bynk_render::render_errors_plain(std::slice::from_ref(&cycle.error), text, &label);
assert!(
rendered.contains("╭─["),
"expected an ariadne source-context report, got:\n{rendered}",
);
assert!(
rendered.contains(&format!("{label}:")),
"the report must locate the diagnostic in its source file, got:\n{rendered}",
);
}
#[test]
fn a_cross_file_kind_conflict_renders_the_other_file_as_a_note() {
let root = scratch_project(
"kindconflict",
&[
(
"src/shop/orders/a.bynk",
"commons shop.orders {\n type T = Int\n}\n",
),
("src/shop/orders/b.bynk", "context shop.orders {\n}\n"),
],
);
let opts = bynk_driver::project_options(&root.0);
let failure = match project::compile_project(&opts) {
Err(f) => f,
Ok(_) => panic!("a kind conflict must fail the build"),
};
let snapshots: HashMap<&Path, &str> = failure
.snapshots
.iter()
.map(|(p, t)| (p.as_path(), t.as_str()))
.collect();
let conflict = failure
.errors
.iter()
.find(|e| e.error.category == "bynk.project.kind_conflict")
.expect("the build must report a kind conflict");
let path = conflict
.source_path
.as_deref()
.expect("the kind conflict must be attributed to a file");
let text = snapshots
.get(path)
.expect("the attributed file must have a snapshot");
let label = path.to_string_lossy().replace('\\', "/");
let rendered =
bynk_render::render_errors_plain(std::slice::from_ref(&conflict.error), text, &label);
assert!(
rendered.contains("╭─["),
"expected an ariadne source-context report, got:\n{rendered}",
);
assert!(
rendered.contains("first declared as a") && rendered.contains(".bynk`"),
"the first-declaration site must render as a note naming the other file, got:\n{rendered}",
);
let other = if label.ends_with("a.bynk") {
"b.bynk"
} else {
"a.bynk"
};
assert!(
rendered.contains(other),
"the note must point at the *other* file (`{other}`), got:\n{rendered}",
);
}