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-baseline-{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.cluster\"\nversion = \"1.0.0\"\n";
fn package_workspace(dir: &TempDir, source: &str) -> PathBuf {
dir.write("ridl.toml", MANIFEST);
dir.write("cluster.ridl", source);
let root = dir.path().to_path_buf();
let (code, _, stderr) = ridl(&["lock".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the fixture's lock is allocated: {stderr}");
root
}
const THREE: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const BARE_REMOVAL: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const TOMBSTONED_REMOVAL: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
reserved doorClosed
event doorLocked: DoorState @[100ms..1s]
}
";
const FOUR: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
event doorAjar: DoorState @[100ms..1s]
}
";
const TWO_BARE_REMOVALS: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorAjar: DoorState @[100ms..1s]
}
";
const MISPLACED_TOMBSTONE: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
reserved doorClosed
}
";
const THREE_WITH_ANOTHER_TOMBSTONE: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
reserved legacyTemp
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const BARE_REMOVAL_WITH_ANOTHER_TOMBSTONE: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
reserved legacyTemp
event doorOpened: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const SIBLING_RESERVES_THE_NAME: &str = "package veh.cluster
type DoorState: integer [0..1]
interface Legacy {
event ping: DoorState @[100ms..1s]
reserved doorClosed
}
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const SIBLING_RESERVES_THE_NAME_REMOVED: &str = "package veh.cluster
type DoorState: integer [0..1]
interface Legacy {
event ping: DoorState @[100ms..1s]
reserved doorClosed
}
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const INLINE_SERVICE_TOMBSTONED: &str = "package veh.cluster
type DoorState: integer [0..1]
service veh.cluster.doors {
event doorOpened: DoorState @[100ms..1s]
reserved doorClosed
event doorLocked: DoorState @[100ms..1s]
}
";
const INLINE_SERVICE_TOMBSTONE_DROPPED: &str = "package veh.cluster
type DoorState: integer [0..1]
service veh.cluster.doors {
event doorOpened: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const TOMBSTONE_REDECLARED: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
}
";
const RENAMED_MANIFEST: &str = "[package]\nname = \"veh.other\"\nversion = \"1.0.0\"\n";
const RENAMED_THREE: &str = "package veh.other
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
";
const THREE_AND_LIGHTS: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
event doorLocked: DoorState @[100ms..1s]
}
interface Lights {
event lampOn: DoorState @[100ms..1s]
}
";
const LOCK_HEADER: &str = "# interfaces.lock — written by ridl lock; do not edit by hand.\n";
const LOCK_WITHOUT_LIGHTS: &str = "next 3\nVehicleStatus 2\n";
const SERVICE_SOURCE: &str = "package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
}
service veh.cluster.status : VehicleStatus
";
const PLACED_TOPOLOGY: &str = "package veh.cluster
component Cluster { offers veh.cluster.status }
component Panel { requires VehicleStatus }
system Vehicle { Cluster, Panel }
deployment Desk for Vehicle {
machine Top { Cluster }
machine Front { Panel }
}
";
const UNPLACED_TOPOLOGY: &str = "package veh.cluster
component Cluster { offers veh.cluster.status }
component Panel { requires VehicleStatus }
system Vehicle { Cluster, Panel }
deployment Desk for Vehicle {
machine Top { Cluster }
}
";
fn snapshot(root: &Path) -> PathBuf {
root.join(".ridl")
.join("baseline")
.join("veh.cluster.ir.json")
}
fn publish(root: &Path) {
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is written: {stderr}");
}
#[test]
fn baseline_refuses_to_publish_an_untombstoned_removal() {
let dir = TempDir::new("gate-refuses");
let root = package_workspace(&dir, THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", BARE_REMOVAL);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 1,
"a refused publication is a negative answer, not a tool failure:\n{stderr}",
);
assert!(
stderr.contains("RIDL-408"),
"the refusal carries its code:\n{stderr}",
);
assert!(
stderr.contains("doorClosed"),
"the message names the interaction that would be dropped:\n{stderr}",
);
assert!(
stderr.contains("reserved doorClosed"),
"the message names the line that would sanction the removal:\n{stderr}",
);
assert!(
stderr.contains("is gone from the source"),
"a bare removal draws the bare-removal wording, not the misplaced- or \
dropped-tombstone one:\n{stderr}",
);
assert_eq!(
stderr.matches("RIDL-408").count(),
1,
"one refusal for the one removed interaction — `doorLocked` sliding into the freed \
slot is the removal's consequence, not a second refusal:\n{stderr}",
);
assert!(
stderr.contains("┌─") && stderr.contains("interface VehicleStatus {"),
"the interaction is gone from the source, so the span falls back to the interface's \
name:\n{stderr}",
);
}
#[test]
fn a_refused_publication_leaves_the_baseline_byte_identical() {
let dir = TempDir::new("gate-preserves");
let root = package_workspace(&dir, THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
let snapshot = root
.join(".ridl")
.join("baseline")
.join("veh.cluster.ir.json");
let before = std::fs::read(&snapshot).expect("the published snapshot is readable");
dir.write("cluster.ridl", BARE_REMOVAL);
let (code, _, _) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the publication is refused");
let after = std::fs::read(&snapshot).expect("the published snapshot survives the refusal");
assert_eq!(before, after, "a refused publication rewrites nothing",);
let staging = root.join(".ridl").join(".baseline.staging");
assert!(
!staging.exists(),
"a refused publication removes the staging directory it built, not just the files it \
declines to move: {}",
staging.display(),
);
}
#[test]
fn baseline_discards_staging_on_an_rsdl_error() {
let dir = TempDir::new("rsdl-staging");
dir.write("ridl.toml", MANIFEST);
dir.write("cluster.ridl", SERVICE_SOURCE);
dir.write("topology.rsdl", PLACED_TOPOLOGY);
let root = dir.path().to_path_buf();
let (code, _, stderr) = ridl(&["lock".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the fixture's lock is allocated: {stderr}");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
let snapshot = root
.join(".ridl")
.join("baseline")
.join("veh.cluster.ir.json");
let before = std::fs::read(&snapshot).expect("the published snapshot is readable");
dir.write("topology.rsdl", UNPLACED_TOPOLOGY);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 1,
"an RSDL-7xx error is a negative answer, not a tool failure:\n{stderr}"
);
assert!(
stderr.contains("RSDL-701"),
"the placement error is reported:\n{stderr}"
);
let after = std::fs::read(&snapshot).expect("the published snapshot survives the error");
assert_eq!(
before, after,
"an RSDL-7xx-only run publishes nothing, even though it writes every artifact",
);
let staging = root.join(".ridl").join(".baseline.staging");
assert!(
!staging.exists(),
"the staging directory the RSDL-7xx run wrote into is discarded, not left: {}",
staging.display(),
);
}
#[test]
fn a_failed_publication_keeps_the_stale_snapshot() {
let dir = TempDir::new("gate-partial-publish");
let root = package_workspace(&dir, THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
let published = root.join(".ridl").join("baseline");
dir.write("ridl.toml", RENAMED_MANIFEST);
dir.write("cluster.ridl", RENAMED_THREE);
std::fs::create_dir_all(published.join("veh.other.ir.json"))
.expect("block the fresh snapshot's destination with a directory");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 2,
"a snapshot that cannot be moved into place is a tool failure:\n{stderr}",
);
assert!(
stderr.contains("cannot publish the baseline"),
"the failure is reported:\n{stderr}",
);
assert!(
published.join("veh.cluster.ir.json").is_file(),
"the stale snapshot survives a publication that failed before it was replaced; \
without it the next run reads an empty directory as a first publication",
);
assert!(
!root.join(".ridl").join(".baseline.staging").exists(),
"the staging directory is removed after the failure",
);
}
#[test]
fn baseline_publishes_a_tombstoned_removal() {
let dir = TempDir::new("gate-admits-tombstone");
let root = package_workspace(&dir, THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
let snapshot = root
.join(".ridl")
.join("baseline")
.join("veh.cluster.ir.json");
let before = std::fs::read(&snapshot).expect("the first published snapshot is readable");
dir.write("cluster.ridl", TOMBSTONED_REMOVAL);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"a removal that leaves a tombstone is the sanctioned path:\n{stderr}",
);
assert!(
!stderr.contains("RIDL-408"),
"the sanctioned path draws no refusal:\n{stderr}",
);
let after = std::fs::read(&snapshot).expect("the republished snapshot is readable");
assert_ne!(
before, after,
"the sanctioned path still publishes: the snapshot must now record `doorClosed` as \
retired, not read back as the untouched first publication",
);
}
#[test]
fn the_first_publication_is_never_refused() {
let dir = TempDir::new("gate-first");
let root = package_workspace(&dir, BARE_REMOVAL);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"a first publication has no prior record:\n{stderr}"
);
assert!(
!stderr.contains("RIDL-408"),
"a first publication draws no refusal:\n{stderr}",
);
}
#[test]
fn an_existing_empty_baseline_directory_is_still_a_first_publication() {
let dir = TempDir::new("gate-empty-dir-first");
let root = package_workspace(&dir, THREE);
let published = root.join(".ridl").join("baseline");
std::fs::create_dir_all(&published).expect("create the empty baseline directory");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"an empty output directory is a first publication, not something to compare against:\n\
{stderr}",
);
assert!(
!stderr.contains("RIDL-408"),
"a first publication draws no refusal:\n{stderr}",
);
assert!(
published.join("veh.cluster.ir.json").is_file(),
"the snapshot is written into the directory that was already there",
);
}
#[test]
fn a_refusal_names_every_untombstoned_removal_in_one_run() {
let dir = TempDir::new("gate-reports-all");
let root = package_workspace(&dir, FOUR);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", TWO_BARE_REMOVALS);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 1,
"a refused publication is a negative answer, not a tool failure:\n{stderr}",
);
assert!(
stderr.contains("doorClosed"),
"the refusal names the first removed interaction:\n{stderr}",
);
assert!(
stderr.contains("doorLocked"),
"the refusal names the second removed interaction, not just the first:\n{stderr}",
);
assert_eq!(
stderr.matches("RIDL-408").count(),
2,
"two refusals for two removed interactions, and no third for `doorAjar` sliding:\n\
{stderr}",
);
}
#[test]
fn baseline_refuses_a_tombstone_at_the_wrong_ordinal() {
let dir = TempDir::new("gate-misplaced-tombstone");
let root = package_workspace(&dir, THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", MISPLACED_TOMBSTONE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 1,
"a tombstone at the wrong ordinal is still a refusal:\n{stderr}",
);
assert_eq!(
stderr.matches("RIDL-408").count(),
1,
"one refusal for the one misplaced tombstone; `doorLocked` moving up is its \
consequence, not a second refusal:\n{stderr}",
);
assert!(
stderr.contains("not at the ordinal the interaction held (ordinal 2)")
&& stderr.contains("Move `reserved doorClosed` to ordinal 2"),
"the misplaced-tombstone wording is drawn and names the ordinal to move the \
tombstone to:\n{stderr}",
);
assert!(
!stderr.contains("is gone from the source"),
"the bare-removal wording must not fire when the source does retire the name:\n{stderr}",
);
assert!(
stderr.contains("┌─") && stderr.contains("reserved doorClosed"),
"the tombstone is in the source, so the span points at it:\n{stderr}",
);
}
#[test]
fn baseline_refuses_a_dropped_tombstone() {
let dir = TempDir::new("gate-dropped-tombstone");
let root = package_workspace(&dir, TOMBSTONED_REMOVAL);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", BARE_REMOVAL);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 1,
"dropping an already-published tombstone is still a refusal:\n{stderr}",
);
assert_eq!(
stderr.matches("RIDL-408").count(),
1,
"one refusal for the one dropped tombstone:\n{stderr}",
);
assert!(
stderr.contains("records `doorClosed` in `VehicleStatus` as retired (ordinal 2)")
&& stderr.contains("dropped the tombstone")
&& stderr.contains("Put `reserved doorClosed` back at ordinal 2"),
"the dropped-tombstone wording is drawn, naming the interface and the ordinal the \
tombstone held:\n{stderr}",
);
assert!(
!stderr.contains("is gone from the source"),
"the bare-removal wording must not fire when the baseline already retired the \
name:\n{stderr}",
);
assert!(
stderr.contains("┌─") && stderr.contains("interface VehicleStatus {"),
"the tombstone is gone from the source, so the span falls back to the interface's \
name:\n{stderr}",
);
}
#[test]
fn a_tombstone_for_another_name_does_not_change_the_wording() {
let dir = TempDir::new("gate-other-tombstone");
let root = package_workspace(&dir, THREE_WITH_ANOTHER_TOMBSTONE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", BARE_REMOVAL_WITH_ANOTHER_TOMBSTONE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the bare removal is refused:\n{stderr}");
assert!(
stderr.contains("`doorClosed` is gone from the source") && stderr.contains("(ordinal 3)"),
"the published IR reserves `legacyTemp`, not `doorClosed`, so this is a bare removal \
of the interaction at ordinal 3:\n{stderr}",
);
assert!(
!stderr.contains("dropped the tombstone"),
"another name's tombstone is not this name's:\n{stderr}",
);
}
#[test]
fn a_sibling_interface_reserving_the_name_does_not_change_the_wording() {
let dir = TempDir::new("gate-sibling-tombstone");
let root = package_workspace(&dir, SIBLING_RESERVES_THE_NAME);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", SIBLING_RESERVES_THE_NAME_REMOVED);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the bare removal is refused:\n{stderr}");
assert_eq!(
stderr.matches("RIDL-408").count(),
1,
"`Legacy` is unchanged, so only `VehicleStatus` draws a refusal:\n{stderr}",
);
assert!(
stderr.contains("`doorClosed` is gone from the source")
&& stderr.contains("in `VehicleStatus`"),
"`VehicleStatus` never retired `doorClosed`; `Legacy` did, and it is not the \
interface the removal happened in:\n{stderr}",
);
assert!(
!stderr.contains("dropped the tombstone"),
"a sibling's tombstone is not this interface's:\n{stderr}",
);
}
#[test]
fn baseline_refuses_a_dropped_tombstone_in_an_inline_service() {
let dir = TempDir::new("gate-inline-dropped-tombstone");
let root = package_workspace(&dir, INLINE_SERVICE_TOMBSTONED);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", INLINE_SERVICE_TOMBSTONE_DROPPED);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 1,
"dropping a tombstone from an inline body is still a refusal:\n{stderr}",
);
assert!(
stderr.contains("records `doorClosed` in `veh.cluster.doors` as retired (ordinal 2)")
&& stderr.contains("dropped the tombstone"),
"the inline body's tombstone is found under the service's dotted name:\n{stderr}",
);
}
#[test]
fn baseline_publishes_an_append() {
let dir = TempDir::new("gate-admits-append");
let root = package_workspace(&dir, THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
let snapshot = root
.join(".ridl")
.join("baseline")
.join("veh.cluster.ir.json");
let before = std::fs::read(&snapshot).expect("the first published snapshot is readable");
dir.write("cluster.ridl", FOUR);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "an append publishes:\n{stderr}");
assert!(
!stderr.contains("RIDL-408"),
"an append drops nothing, so it draws no refusal:\n{stderr}",
);
let after = std::fs::read(&snapshot).expect("the republished snapshot is readable");
assert_ne!(
before, after,
"the append is published: the snapshot must now carry `doorAjar`",
);
}
#[test]
fn baseline_publishes_a_moved_existing_tombstone() {
let dir = TempDir::new("gate-moved-tombstone");
let root = package_workspace(&dir, TOMBSTONED_REMOVAL);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", MISPLACED_TOMBSTONE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"a moved existing tombstone publishes today (ridl §17.12 records the question):\n\
{stderr}",
);
assert!(
!stderr.contains("RIDL-408"),
"the gate reads no `InteractionReordered` change:\n{stderr}",
);
}
#[test]
fn the_gate_reads_the_out_directory() {
let dir = TempDir::new("gate-out");
let root = package_workspace(&dir, THREE);
let out = dir.path().join("published");
let (code, _, stderr) = ridl(&[
"baseline".as_ref(),
root.as_os_str(),
"--out".as_ref(),
out.as_os_str(),
]);
assert_eq!(
code, 0,
"the first baseline is written to `--out`: {stderr}"
);
dir.write("cluster.ridl", BARE_REMOVAL);
let (code, _, stderr) = ridl(&[
"baseline".as_ref(),
root.as_os_str(),
"--out".as_ref(),
out.as_os_str(),
]);
assert_eq!(
code, 1,
"the removal is refused against the snapshot in `--out`:\n{stderr}",
);
assert!(
stderr.contains("RIDL-408"),
"the refusal carries its code:\n{stderr}",
);
assert!(
!root.join(".ridl").exists(),
"the default directory is neither read nor created when `--out` is given",
);
}
#[test]
fn baseline_refuses_an_interaction_redeclared_over_its_tombstone() {
let dir = TempDir::new("gate-redeclared-tombstone");
let root = package_workspace(&dir, TOMBSTONED_REMOVAL);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
dir.write("cluster.ridl", TOMBSTONE_REDECLARED);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 1,
"redeclaring a retired name is still a refusal:\n{stderr}",
);
assert_eq!(
stderr.matches("RIDL-408").count(),
1,
"one refusal for the one redeclared name:\n{stderr}",
);
assert!(
stderr.contains("`doorClosed` is declared again in `VehicleStatus`")
&& stderr.contains("keep `reserved doorClosed` at ordinal 2"),
"the redeclared-name wording names the interaction, the interface and the tombstone's \
ordinal:\n{stderr}",
);
assert!(
stderr.contains("┌─") && stderr.contains("event doorClosed: DoorState @[100ms..1s]"),
"the span points at the redeclaration, which is in the source:\n{stderr}",
);
}
#[test]
fn baseline_refuses_to_republish_over_a_corrupt_snapshot() {
let dir = TempDir::new("gate-corrupt-snapshot");
let root = package_workspace(&dir, THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first baseline is written: {stderr}");
let snapshot = root
.join(".ridl")
.join("baseline")
.join("veh.cluster.ir.json");
let corrupt = "<<<<<<< HEAD\n";
std::fs::write(&snapshot, corrupt).expect("overwrite the published snapshot with garbage");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 2,
"a published snapshot that cannot be parsed cannot be shown safe to replace:\n{stderr}",
);
assert!(
stderr.contains("restore it from version control")
&& stderr.contains("check the source against it with that toolchain"),
"the message names a remedy for each cause it cannot tell apart — a damaged file, and \
a snapshot another toolchain wrote:\n{stderr}",
);
assert!(
!stderr.contains("delete it"),
"the remedy never tells the author to discard the record unread:\n{stderr}",
);
let after =
std::fs::read_to_string(&snapshot).expect("the corrupt file is still readable, untouched");
assert_eq!(
after, corrupt,
"a refused publication rewrites nothing, corrupt or not",
);
let staging = root.join(".ridl").join(".baseline.staging");
assert!(
!staging.exists(),
"an exit-2 refusal removes the staging directory the compile wrote, exactly as an \
exit-1 refusal does: {}",
staging.display(),
);
}
#[test]
fn baseline_refuses_a_provisional_number() {
let dir = TempDir::new("gate-provisional");
let root = package_workspace(&dir, THREE);
dir.write("cluster.ridl", THREE_AND_LIGHTS);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "a provisional number is refused:\n{stderr}");
assert!(
stderr.contains("error[RIDL-411]"),
"the refusal carries its code:\n{stderr}",
);
assert!(
stderr.contains("`Lights` has a provisional interface number (2)"),
"the message names the interface and the number the checker showed:\n{stderr}",
);
assert!(
stderr.contains(&format!("Run `ridl lock {}`", root.display())),
"the message names the command that records the number:\n{stderr}",
);
assert!(
stderr.contains("┌─") && stderr.contains("interface Lights"),
"the span points at the declaration:\n{stderr}",
);
assert!(
!stderr.contains("RIDL-408"),
"the interaction level has nothing to refuse:\n{stderr}",
);
assert!(
!root.join(".ridl").join("baseline").exists(),
"nothing is published",
);
assert!(
!root.join(".ridl").join(".baseline.staging").exists(),
"the staging directory is removed",
);
let (code, _, stderr) = ridl(&["lock".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the number is recorded: {stderr}");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"with the number recorded, the same source publishes:\n{stderr}",
);
assert!(snapshot(&root).is_file(), "the snapshot is written");
}
#[test]
fn the_first_publication_after_a_lock_fix_compares_nothing() {
let dir = TempDir::new("gate-first-after-fix");
dir.write("ridl.toml", MANIFEST);
dir.write("cluster.ridl", THREE_AND_LIGHTS);
dir.write(
"interfaces.lock",
&format!("{LOCK_HEADER}next 3\nVehicleStatus 1\nLamps 2\n"),
);
let root = dir.path().to_path_buf();
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the compile fails on the orphan entry:\n{stderr}");
assert!(
stderr.contains("error[RIDL-409]"),
"the compile's diagnostic:\n{stderr}",
);
assert!(
!stderr.contains("RIDL-41"),
"the gate does not run after a failed compile:\n{stderr}",
);
assert!(!root.join(".ridl").exists(), "nothing is published");
let (code, _, stderr) = ridl(&[
"lock".as_ref(),
root.as_os_str(),
"--rename".as_ref(),
"Lamps=Lights".as_ref(),
]);
assert_eq!(code, 0, "the rename is recorded: {stderr}");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the first publication after the fix:\n{stderr}");
assert!(
!stderr.contains("RIDL-"),
"nothing to compare against, nothing provisional:\n{stderr}",
);
assert!(snapshot(&root).is_file(), "the snapshot is written");
}
#[test]
fn a_hand_deleted_lock_line_is_ridl_411_while_its_declaration_is_provisional() {
let dir = TempDir::new("gate-deleted-line-provisional");
let root = package_workspace(&dir, THREE_AND_LIGHTS);
publish(&root);
let before = std::fs::read(snapshot(&root)).expect("the published snapshot is readable");
dir.write(
"interfaces.lock",
&format!("{LOCK_HEADER}{LOCK_WITHOUT_LIGHTS}"),
);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the build cannot see a deleted line:\n{stderr}");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the provisional number is refused:\n{stderr}");
assert!(
stderr.contains("error[RIDL-411]")
&& stderr.contains("`Lights` has a provisional interface number (3)"),
"the number comes from `next`, not from the deleted line:\n{stderr}",
);
let after = std::fs::read(snapshot(&root)).expect("the published snapshot survives");
assert_eq!(before, after, "a refused publication rewrites nothing");
assert!(
!root.join(".ridl").join(".baseline.staging").exists(),
"the staging directory is removed",
);
}
#[test]
fn baseline_refuses_a_number_dropped_without_a_retired_entry() {
let dir = TempDir::new("gate-dropped-number");
let root = package_workspace(&dir, THREE_AND_LIGHTS);
publish(&root);
let before = std::fs::read(snapshot(&root)).expect("the published snapshot is readable");
dir.write(
"interfaces.lock",
&format!("{LOCK_HEADER}{LOCK_WITHOUT_LIGHTS}"),
);
let (code, stdout, stderr) = ridl(&["lock".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"the kept declaration gets a fresh number: {stderr}"
);
assert_eq!(stdout, "allocated Lights 3\n", "`next` was never lowered");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the dropped number is refused:\n{stderr}");
assert_eq!(
stderr.matches("RIDL-412").count(),
1,
"one refusal for the one dropped number:\n{stderr}",
);
assert!(
stderr.contains("`Lights` holds interface number 1 in the baseline being replaced")
&& stderr.contains("Restore the line `Lights 1`")
&& stderr.contains("`Lights 1 retired` when the interface is gone"),
"the message names the number and the line that restores its record:\n{stderr}",
);
assert!(
!stderr.contains("RIDL-408") && !stderr.contains("RIDL-411"),
"the interaction level is untouched and nothing is provisional:\n{stderr}",
);
let after = std::fs::read(snapshot(&root)).expect("the published snapshot survives");
assert_eq!(before, after, "a refused publication rewrites nothing");
assert!(
!root.join(".ridl").join(".baseline.staging").exists(),
"the staging directory is removed",
);
}
#[test]
fn baseline_publishes_a_retired_number() {
let dir = TempDir::new("gate-retired-number");
let root = package_workspace(&dir, THREE_AND_LIGHTS);
publish(&root);
dir.write("cluster.ridl", THREE);
let (code, stdout, stderr) = ridl(&[
"lock".as_ref(),
root.as_os_str(),
"--retire".as_ref(),
"Lights".as_ref(),
]);
assert_eq!(code, 0, "the retirement is recorded: {stderr}");
assert_eq!(stdout, "retired Lights 1\n");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "a retired number publishes:\n{stderr}");
assert!(
!stderr.contains("RIDL-"),
"the sanctioned removal draws no refusal:\n{stderr}",
);
let text = std::fs::read_to_string(snapshot(&root)).expect("the republished snapshot");
assert!(
text.contains("\"retired\"") && text.contains("\"name\": \"Lights\""),
"the snapshot records the retired entry:\n{text}",
);
}
#[test]
fn a_pre_lock_baseline_is_not_refused_for_an_interface_removal() {
let dir = TempDir::new("gate-pre-lock");
let root = package_workspace(&dir, THREE_AND_LIGHTS);
publish(&root);
let path = snapshot(&root);
let text = std::fs::read_to_string(&path).expect("the published snapshot is readable");
assert!(
text.contains("\"number\": 1"),
"`Lights` was published under number 1:\n{text}",
);
std::fs::write(&path, text.replace("\"number\": 1", "\"number\": 0"))
.expect("rewrite the snapshot as a pre-lock one");
dir.write("cluster.ridl", THREE);
dir.write(
"interfaces.lock",
&format!("{LOCK_HEADER}{LOCK_WITHOUT_LIGHTS}"),
);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"a number the lock never allocated has no record to lose:\n{stderr}",
);
assert!(
!stderr.contains("RIDL-412") && !stderr.contains("RIDL-408"),
"neither gate refuses:\n{stderr}",
);
}
#[test]
fn a_whole_interface_removed_is_refused_by_the_lock_not_the_tombstone_gate() {
let dir = TempDir::new("gate-whole-interface");
let root = package_workspace(&dir, THREE_AND_LIGHTS);
publish(&root);
let before = std::fs::read(snapshot(&root)).expect("the published snapshot is readable");
dir.write("cluster.ridl", THREE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the live entry fails the compile:\n{stderr}");
assert!(
stderr.contains("error[RIDL-409]"),
"the build's diagnostic, naming the retire:\n{stderr}",
);
assert!(
!stderr.contains("RIDL-408") && !stderr.contains("RIDL-412"),
"no gate runs after a failed compile:\n{stderr}",
);
let after = std::fs::read(snapshot(&root)).expect("the published snapshot survives");
assert_eq!(before, after, "a failed compile rewrites nothing");
}