use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
struct TempDir(PathBuf);
impl TempDir {
fn new(label: &str) -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let mut path = std::env::temp_dir();
path.push(format!(
"ridl-lock-protocol-{label}-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::SeqCst),
));
std::fs::create_dir_all(&path).expect("create the temp dir");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
fn write(&self, relative: &str, text: &str) -> PathBuf {
let path = self.0.join(relative);
std::fs::create_dir_all(path.parent().expect("a relative path has a parent"))
.expect("create parent directories");
std::fs::write(&path, text).expect("write the fixture file");
path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn ridl(args: &[&std::ffi::OsStr]) -> (i32, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_ridl"))
.args(args)
.output()
.expect("the ridl binary must run");
let code = output.status.code().expect("the process exits with a code");
(
code,
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
const MANIFEST: &str = "[package]\nname = \"veh.hvac\"\nversion = \"1.0.0\"\n";
const HEADER: &str = "# interfaces.lock — written by ridl lock; do not edit by hand.\n";
const LOCK: &str = "next 3\nCabin 1\nZone 2\n";
const BASE: &str = "package veh.hvac
type State: integer [0..1]
interface Cabin { signal c : State @[100ms..1s] }
interface Zone {
signal level : State @[100ms..1s]
event opened : State @[100ms..1s]
}
";
const ZONE_RENAMED: &str = "package veh.hvac
type State: integer [0..1]
interface Cabin { signal c : State @[100ms..1s] }
interface Lane {
/// The zone's level.
signal level : State @[100ms..1s]
event opened : State @[100ms..1s]
}
";
const TWO_CANDIDATES: &str = "package veh.hvac
type State: integer [0..1]
interface Cabin { signal c : State @[100ms..1s] }
interface Lane {
signal level : State @[100ms..1s]
event opened : State @[100ms..1s]
}
interface Strip {
signal level : State @[100ms..1s]
event opened : State @[100ms..1s]
}
";
const OTHER_SHAPE: &str = "package veh.hvac
type State: integer [0..1]
interface Cabin { signal c : State @[100ms..1s] }
interface Lane {
signal level : State @[100ms..1s]
signal opened : State @[100ms..1s]
}
";
const ZONE_RENAMED_WITH_AN_ERROR: &str = "package veh.hvac
type State: integer [0..1]
type Bad: integer [10..0]
interface Cabin { signal c : State @[100ms..1s] }
interface Lane {
signal level : State @[100ms..1s]
event opened : State @[100ms..1s]
}
";
const ZONE_REMOVED: &str = "package veh.hvac
type State: integer [0..1]
interface Cabin { signal c : State @[100ms..1s] }
";
const ZONE_REORDERED: &str = "package veh.hvac
type State: integer [0..1]
interface Cabin { signal c : State @[100ms..1s] }
interface Zone {
event opened : State @[100ms..1s]
signal level : State @[100ms..1s]
}
";
fn package_workspace(dir: &TempDir, source: &str, lock: Option<&str>) -> PathBuf {
dir.write("ridl.toml", MANIFEST);
dir.write("hvac.ridl", source);
if let Some(lock) = lock {
dir.write("interfaces.lock", &format!("{HEADER}{lock}"));
}
dir.path().to_path_buf()
}
fn publish_then_edit(dir: &TempDir, root: &Path, edited: &str) {
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is published: {stderr}");
dir.write("hvac.ridl", edited);
}
fn read_lock(root: &Path) -> String {
std::fs::read_to_string(root.join("interfaces.lock")).expect("the lock file exists")
}
fn check(root: &Path) -> (i32, String) {
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
(code, stderr)
}
fn hint(root: &Path, old: &str, new: &str) -> String {
format!(
"same shape as `{old}` in the published baseline: run `ridl lock {} --rename {old}={new}`",
root.display()
)
}
#[test]
fn a_declaration_with_no_entry_checks_clean_with_a_provisional_number() {
let dir = TempDir::new("provisional");
let root = package_workspace(&dir, BASE, None);
let (code, stderr) = check(&root);
assert_eq!(code, 0, "stderr:\n{stderr}");
assert!(!stderr.contains("RIDL-409"), "stderr:\n{stderr}");
assert!(
!root.join("interfaces.lock").exists(),
"`ridl check` never writes the lock"
);
}
#[test]
fn an_orphan_entry_with_no_candidate_names_retire() {
let dir = TempDir::new("no-candidate");
let root = package_workspace(&dir, ZONE_REMOVED, Some(LOCK));
let text = read_lock(&root);
let (code, stderr) = check(&root);
assert_eq!(code, 1, "stderr:\n{stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(
stderr.contains(&format!(
"run `ridl lock {} --retire Zone` to record",
root.display()
)),
"stderr:\n{stderr}"
);
assert!(!stderr.contains("--rename"), "stderr:\n{stderr}");
assert!(!stderr.contains("same shape"), "stderr:\n{stderr}");
assert_eq!(read_lock(&root), text, "byte-identical");
}
#[test]
fn one_same_shape_candidate_adds_the_rename_label() {
let dir = TempDir::new("one-candidate");
let root = package_workspace(&dir, BASE, Some(LOCK));
publish_then_edit(&dir, &root, ZONE_RENAMED);
let text = read_lock(&root);
let (code, stderr) = check(&root);
assert_eq!(code, 1, "RIDL-409 keeps its exit code: {stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(
stderr.contains("--rename Zone=New` when a declaration"),
"the message still names both commands: {stderr}"
);
assert!(
stderr.contains(&hint(&root, "Zone", "Lane")),
"the label names the one rename: {stderr}"
);
assert!(
stderr.contains("interface Lane"),
"the label points at the candidate's declaration: {stderr}"
);
assert_eq!(read_lock(&root), text, "byte-identical");
}
#[test]
fn the_desk_check_runs_with_ridl_409_present() {
let dir = TempDir::new("desk-check-runs");
let root = package_workspace(&dir, BASE, Some(LOCK));
publish_then_edit(&dir, &root, ZONE_REORDERED);
dir.write(
"interfaces.lock",
&format!("{HEADER}next 4\nCabin 1\nZone 2\nLegacy 3\n"),
);
let (code, stderr) = check(&root);
assert_eq!(code, 1, "stderr:\n{stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(
stderr.contains("warning[RIDL-407]"),
"the desk check ran beside RIDL-409: {stderr}"
);
}
#[test]
fn several_same_shape_candidates_add_no_label() {
let dir = TempDir::new("several-candidates");
let root = package_workspace(&dir, BASE, Some(LOCK));
publish_then_edit(&dir, &root, TWO_CANDIDATES);
let (code, stderr) = check(&root);
assert_eq!(code, 1, "stderr:\n{stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(stderr.contains("--rename Zone=New"), "stderr:\n{stderr}");
assert!(!stderr.contains("same shape"), "stderr:\n{stderr}");
}
#[test]
fn no_same_shape_candidate_adds_no_label() {
let dir = TempDir::new("no-same-shape");
let root = package_workspace(&dir, BASE, Some(LOCK));
publish_then_edit(&dir, &root, OTHER_SHAPE);
let (code, stderr) = check(&root);
assert_eq!(code, 1, "stderr:\n{stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(stderr.contains("--rename Zone=New"), "stderr:\n{stderr}");
assert!(!stderr.contains("same shape"), "stderr:\n{stderr}");
}
#[test]
fn no_baseline_means_no_label() {
let dir = TempDir::new("no-baseline");
let root = package_workspace(&dir, ZONE_RENAMED, Some(LOCK));
let text = read_lock(&root);
let (code, stderr) = check(&root);
assert_eq!(code, 1, "stderr:\n{stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(stderr.contains("--rename Zone=New"), "stderr:\n{stderr}");
assert!(stderr.contains("--retire Zone"), "stderr:\n{stderr}");
assert!(!stderr.contains("same shape"), "stderr:\n{stderr}");
assert_eq!(read_lock(&root), text, "byte-identical");
}
#[test]
fn a_hand_deleted_line_is_invisible_to_check() {
let dir = TempDir::new("hand-deleted");
let root = package_workspace(&dir, BASE, Some("next 3\nCabin 1\n"));
let text = read_lock(&root);
let (code, stderr) = check(&root);
assert_eq!(code, 0, "stderr:\n{stderr}");
assert!(!stderr.contains("RIDL-4"), "stderr:\n{stderr}");
assert_eq!(read_lock(&root), text, "byte-identical");
}
#[test]
fn build_fails_on_an_orphan_entry_too() {
let dir = TempDir::new("build");
let root = package_workspace(&dir, ZONE_REMOVED, Some(LOCK));
let out = TempDir::new("build-out");
let (code, _, stderr) = ridl(&[
"build".as_ref(),
root.as_os_str(),
"--out-dir".as_ref(),
out.path().as_os_str(),
]);
assert_eq!(code, 1, "stderr:\n{stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(
std::fs::read_dir(out.path())
.expect("the out dir exists")
.next()
.is_none(),
"nothing is written"
);
}
#[test]
fn a_second_error_beside_ridl_409_skips_the_desk_check() {
let dir = TempDir::new("second-error");
let root = package_workspace(&dir, BASE, Some(LOCK));
publish_then_edit(&dir, &root, ZONE_RENAMED_WITH_AN_ERROR);
let (code, stderr) = check(&root);
assert_eq!(code, 1, "stderr:\n{stderr}");
assert!(stderr.contains("error[RIDL-409]"), "stderr:\n{stderr}");
assert!(stderr.contains("error[TYPL-104]"), "stderr:\n{stderr}");
assert!(!stderr.contains("same shape"), "stderr:\n{stderr}");
}