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";
const BASE: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
type DoorState: integer [0..1]
interface VehicleStatus {
signal currentSpeed: Speed @10ms
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
}
";
const REORDERED: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
type DoorState: integer [0..1]
interface VehicleStatus {
signal currentSpeed: Speed @10ms
event doorClosed: DoorState @[100ms..1s]
event doorOpened: DoorState @[100ms..1s]
}
";
const APPENDED: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
type DoorState: integer [0..1]
interface VehicleStatus {
signal currentSpeed: Speed @10ms
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
event hoodOpened: DoorState @[100ms..1s]
}
";
const REMOVED: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
type DoorState: integer [0..1]
interface VehicleStatus {
signal currentSpeed: Speed @10ms
event doorClosed: DoorState @[100ms..1s]
}
";
const NON_ORDINAL: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
type DoorState: integer [0..1]
type NarrowState: integer [0..0]
interface VehicleStatus {
signal currentSpeed: Speed @10ms
event doorOpened: NarrowState @[100ms..2s]
event doorClosed: DoorState @[100ms..1s]
}
";
const SVC_BASE: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
interface DoorBlock {
signal locked: Speed @10ms
}
interface HealthBlock {
signal uptime: Speed @10ms
}
service veh.cluster.doors : DoorBlock, HealthBlock
";
const SVC_REORDERED: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
interface DoorBlock {
signal locked: Speed @10ms
}
interface HealthBlock {
signal uptime: Speed @10ms
}
service veh.cluster.doors : HealthBlock, DoorBlock
";
const SVC_INSERTED: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
interface DoorBlock {
signal locked: Speed @10ms
}
interface HealthBlock {
signal uptime: Speed @10ms
}
interface NewBlock {
signal fresh: Speed @10ms
}
service veh.cluster.doors : NewBlock, DoorBlock, HealthBlock
";
const SVC_REMOVED: &str = "package veh.cluster
type Speed: km/h [0.0..250.0 step 0.5]
interface DoorBlock {
signal locked: Speed @10ms
}
interface HealthBlock {
signal uptime: Speed @10ms
}
service veh.cluster.doors : DoorBlock
";
const NAMES_A_STANDARD_TYPE: &str = "package veh.cluster
type DoorState: integer [0..1]
struct DoorReport {
observedAt: Timestamp
door: DoorState
}
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
}
";
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();
lock(&root);
root
}
fn lock(root: &Path) {
let (code, _, stderr) = ridl(&["lock".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the fixture's lock is allocated: {stderr}");
}
#[test]
fn baseline_writes_one_snapshot_per_package() {
let dir = TempDir::new("write");
let root = package_workspace(&dir, BASE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "a clean workspace snapshots cleanly: {stderr}");
assert!(
root.join(".ridl/baseline/veh.cluster.ir.json").is_file(),
"the package snapshot lands under `.ridl/baseline/`",
);
}
#[test]
fn baseline_writes_one_file_per_package_in_a_multi_package_workspace() {
let dir = TempDir::new("multi");
dir.write(
"ridl.toml",
"[workspace]\nmembers = [\"common\", \"cluster\"]\n",
);
dir.write(
"common/ridl.toml",
"[package]\nname = \"veh.common\"\nversion = \"1.0.0\"\n",
);
dir.write(
"common/common.typl",
"package veh.common\ntype Speed: km/h [0.0..250.0 step 0.5]\n",
);
dir.write("cluster/ridl.toml", MANIFEST);
dir.write(
"cluster/cluster.ridl",
"package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
}
",
);
let root = dir.path();
lock(root);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the workspace snapshots cleanly: {stderr}");
let baseline = root.join(".ridl/baseline");
let mut written: Vec<String> = std::fs::read_dir(&baseline)
.expect("the baseline directory exists")
.map(|entry| {
entry
.expect("a readable entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.collect();
written.sort();
assert_eq!(
written,
vec![
"veh.cluster.ir.json".to_string(),
"veh.common.ir.json".to_string(),
],
"two packages produce two snapshot files",
);
}
#[test]
fn baseline_out_flag_selects_the_directory() {
let dir = TempDir::new("out");
let root = package_workspace(&dir, BASE);
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 workspace snapshots cleanly: {stderr}");
assert!(
out.join("veh.cluster.ir.json").is_file(),
"`--out` receives the snapshot",
);
assert!(
!root.join(".ridl/baseline").exists(),
"`--out` replaces the default directory rather than adding to it",
);
}
#[test]
fn check_flags_a_reorder_against_the_baseline() {
let dir = TempDir::new("reorder");
let root = package_workspace(&dir, BASE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is written: {stderr}");
dir.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert!(
stderr.contains("RIDL-407"),
"the reorder draws the coded desk warning:\n{stderr}",
);
assert!(
stderr.contains("`doorClosed` has moved in `VehicleStatus`"),
"the message names the interaction and the shape it is declared in:\n{stderr}",
);
assert!(
stderr.contains("wire identity") && stderr.contains("add new ones at the end"),
"the message states the consequence and the remedy:\n{stderr}",
);
assert!(
stderr.contains("event doorClosed: DoorState"),
"the span underlines the interaction's declaration:\n{stderr}",
);
assert_eq!(
code, 0,
"a warning never moves the exit code of an otherwise clean check:\n{stderr}",
);
}
#[test]
fn check_flags_a_removal_against_the_baseline() {
let dir = TempDir::new("removal");
let root = package_workspace(&dir, BASE);
ridl(&["baseline".as_ref(), root.as_os_str()]);
dir.write("cluster.ridl", REMOVED);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert!(
stderr.contains("RIDL-407")
&& stderr.contains("`doorOpened` is gone in `VehicleStatus`")
&& stderr.contains("`reserved doorOpened`"),
"the removal draws the desk warning, and it names the tombstone that \
keeps the slot:\n{stderr}",
);
assert_eq!(code, 0, "the warning leaves the exit code alone:\n{stderr}");
}
#[test]
fn check_is_silent_for_a_service_set_change() {
for (label, source) in [
("svcreorder", SVC_REORDERED),
("svcinsert", SVC_INSERTED),
("svcremoval", SVC_REMOVED),
] {
let dir = TempDir::new(label);
let root = package_workspace(&dir, SVC_BASE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is written: {stderr}");
dir.write("cluster.ridl", source);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert!(
!stderr.contains("RIDL-407"),
"{label}: a set change draws no desk warning:\n{stderr}"
);
assert_eq!(code, 0, "{label}: exit code:\n{stderr}");
}
}
#[test]
fn check_is_silent_for_an_append() {
let dir = TempDir::new("append");
let root = package_workspace(&dir, BASE);
ridl(&["baseline".as_ref(), root.as_os_str()]);
dir.write("cluster.ridl", APPENDED);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert!(
!stderr.contains("RIDL-407"),
"an appended interaction is not ordinal-affecting:\n{stderr}",
);
assert_eq!(code, 0, "a compatible change stays clean:\n{stderr}");
}
#[test]
fn check_without_a_baseline_is_unchanged() {
let dir = TempDir::new("nobaseline");
let root = package_workspace(&dir, REORDERED);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the workspace is clean:\n{stderr}");
assert!(
stderr.is_empty(),
"a missing baseline is silently skipped:\n{stderr}",
);
}
#[test]
fn check_accepts_a_single_baseline_file() {
let dir = TempDir::new("file");
let root = package_workspace(&dir, BASE);
let out = dir.path().join("published");
ridl(&[
"baseline".as_ref(),
root.as_os_str(),
"--out".as_ref(),
out.as_os_str(),
]);
dir.write("cluster.ridl", REORDERED);
let snapshot = out.join("veh.cluster.ir.json");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
snapshot.as_os_str(),
]);
assert!(
stderr.contains("RIDL-407"),
"a single-file baseline drives the same check:\n{stderr}",
);
assert_eq!(code, 0, "the exit code is untouched:\n{stderr}");
}
#[test]
fn check_reports_a_missing_explicit_baseline() {
let dir = TempDir::new("missing");
let root = package_workspace(&dir, BASE);
let absent = dir.path().join("no-such-dir");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
absent.as_os_str(),
]);
assert_eq!(code, 2, "a named baseline that is absent is an error");
assert_eq!(
stderr,
format!(
"error: the baseline `{}` does not exist\n",
absent.display()
),
"the error names the missing path"
);
}
#[test]
fn check_refuses_a_baseline_directory_of_non_json_artifacts() {
let dir = TempDir::new("txtpb-dir");
let root = package_workspace(&dir, BASE);
dir.write("artifacts/veh.cluster.ir.txtpb", "name: \"veh.cluster\"\n");
let artifacts = dir.path().join("artifacts");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
artifacts.as_os_str(),
]);
assert_eq!(
code, 2,
"the artifact directory is an input error:\n{stderr}"
);
assert_eq!(
stderr,
format!(
"error: {}: the directory holds IR artifacts (`veh.cluster.ir.txtpb`) but no \
`.ir.json` snapshot; a baseline stays `.ir.json` (ADR-0014 decision 5); publish \
one with `ridl baseline`\n",
artifacts.display()
),
"the message describes the directory, not an empty baseline"
);
}
#[test]
fn check_refuses_a_baseline_directory_whose_snapshots_are_nested() {
let dir = TempDir::new("nested");
let root = package_workspace(&dir, BASE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is published: {stderr}");
dir.write("cluster.ridl", REORDERED);
let published = root.join(".ridl/baseline");
let (_, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
published.as_os_str(),
]);
assert!(
stderr.contains("RIDL-407"),
"the drift this run must not lose sight of:\n{stderr}",
);
let nest = root.join(".ridl");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
nest.as_os_str(),
]);
assert_eq!(code, 2, "one level too high is an input error:\n{stderr}");
assert_eq!(
stderr,
format!(
"error: {}: no `.ir.json` snapshot directly inside, but the subdirectory \
`baseline` holds one; snapshots are read from one directory, never from the \
directories below it; pass `--baseline {}` instead\n",
nest.display(),
published.display(),
),
"the message names the subdirectory that holds the snapshots"
);
}
#[cfg(unix)]
#[test]
fn check_reports_a_baseline_subdirectory_it_cannot_read() {
use std::os::unix::fs::PermissionsExt as _;
let dir = TempDir::new("unreadable");
let root = package_workspace(&dir, BASE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is published: {stderr}");
dir.write("cluster.ridl", REORDERED);
let published = root.join(".ridl/baseline");
let restore = std::fs::metadata(&published)
.expect("the published directory exists")
.permissions();
std::fs::set_permissions(&published, std::fs::Permissions::from_mode(0o000))
.expect("make the published directory unreadable");
if std::fs::read_dir(&published).is_ok() {
std::fs::set_permissions(&published, restore).expect("restore the permissions");
return;
}
let nest = root.join(".ridl");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
nest.as_os_str(),
]);
std::fs::set_permissions(&published, restore).expect("restore the permissions");
assert_eq!(
code, 2,
"a baseline that cannot be read is not an absent one:\n{stderr}"
);
assert!(
stderr.starts_with(&format!("error: cannot read {}: ", published.display())),
"the message names the directory it could not list:\n{stderr}"
);
}
#[test]
fn check_refuses_an_empty_baseline_directory() {
let dir = TempDir::new("emptydir");
let root = package_workspace(&dir, BASE);
let empty = dir.path().join("published");
std::fs::create_dir_all(&empty).expect("create the empty baseline directory");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
empty.as_os_str(),
]);
assert_eq!(
code, 2,
"an explicit baseline holding no snapshot is an input error:\n{stderr}"
);
assert!(
stderr.contains(&format!(
"the baseline `{}` holds no `.ir.json` snapshot directly inside it",
empty.display()
)),
"the cause names the directory — the nested and artifact-directory refusals say \
`no `.ir.json` snapshot` too, so the wording must be this refusal's own:\n{stderr}",
);
assert!(
stderr.contains("point `--baseline` at the directory that holds the snapshots")
&& stderr.contains(&format!("`ridl baseline --out {}`", empty.display())),
"the remedy names both ways out, the aimed-too-high one first:\n{stderr}",
);
}
#[test]
fn check_refuses_a_non_json_baseline() {
let dir = TempDir::new("refuse");
let root = package_workspace(&dir, BASE);
for name in ["published.ir.txtpb", "published.ir.binpb"] {
let artifact = dir.write(name, "name: \"veh.cluster\"\n");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
artifact.as_os_str(),
]);
assert_eq!(code, 2, "`{name}` is an input error, stderr:\n{stderr}");
assert!(
stderr.contains(".ir.json"),
"the refusal must name the accepted encoding:\n{stderr}"
);
}
}
#[test]
fn check_skips_the_desk_check_when_the_compile_fails() {
let dir = TempDir::new("broken");
let root = package_workspace(&dir, BASE);
ridl(&["baseline".as_ref(), root.as_os_str()]);
dir.write(
"cluster.ridl",
"package veh.cluster
type DoorState: integer [0..1]
interface VehicleStatus {
event doorClosed: Nope
}
",
);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the compile error still gates:\n{stderr}");
assert!(
!stderr.contains("RIDL-407"),
"no desk warning over a broken compile:\n{stderr}",
);
}
#[test]
fn check_is_silent_on_a_non_ordinal_breaking_change() {
let dir = TempDir::new("nonordinal");
let root = package_workspace(&dir, BASE);
let out = dir.path().join("published");
ridl(&[
"baseline".as_ref(),
root.as_os_str(),
"--out".as_ref(),
out.as_os_str(),
]);
dir.write("cluster.ridl", NON_ORDINAL);
let snapshot = out.join("veh.cluster.ir.json");
let (diff_code, diff_stdout, _) =
ridl(&["diff".as_ref(), snapshot.as_os_str(), root.as_os_str()]);
assert_eq!(diff_code, 1, "the edit is breaking:\n{diff_stdout}");
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
out.as_os_str(),
]);
assert!(
!stderr.contains("RIDL-407"),
"a breaking change that moves no ordinal is not the desk check's job:\n{stderr}",
);
assert_eq!(code, 0, "and it does not gate the check:\n{stderr}");
}
#[test]
fn check_matches_snapshots_by_package_name_not_file_name() {
let dir = TempDir::new("byname");
let root = package_workspace(&dir, BASE);
let out = dir.path().join("published");
ridl(&[
"baseline".as_ref(),
root.as_os_str(),
"--out".as_ref(),
out.as_os_str(),
]);
std::fs::rename(
out.join("veh.cluster.ir.json"),
out.join("zzz-totally-wrong-name.ir.json"),
)
.expect("rename the snapshot");
dir.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
out.as_os_str(),
]);
assert!(
stderr.contains("RIDL-407") && stderr.contains("`doorClosed` has moved"),
"the misnamed snapshot is still attributed to its package:\n{stderr}",
);
assert_eq!(code, 0, "the exit code is untouched:\n{stderr}");
}
#[test]
fn diff_reads_a_baseline_directory_as_a_snapshot_set() {
let dir = TempDir::new("diffdir");
let root = package_workspace(&dir, BASE);
ridl(&["baseline".as_ref(), root.as_os_str()]);
let baseline = root.join(".ridl/baseline");
dir.write("cluster.ridl", REORDERED);
let (code, stdout, stderr) = ridl(&["diff".as_ref(), baseline.as_os_str(), root.as_os_str()]);
assert_eq!(code, 1, "the reorder is breaking:\n{stdout}{stderr}");
assert!(
stdout.contains("interaction_reordered")
&& stdout.contains("veh.cluster/VehicleStatus/doorClosed"),
"the report names the change:\n{stdout}",
);
}
#[test]
fn diff_of_an_unchanged_workspace_against_its_baseline_is_clean() {
let dir = TempDir::new("diffsame");
let root = package_workspace(&dir, BASE);
ridl(&["baseline".as_ref(), root.as_os_str()]);
let baseline = root.join(".ridl/baseline");
let (code, stdout, stderr) = ridl(&["diff".as_ref(), baseline.as_os_str(), root.as_os_str()]);
assert_eq!(code, 0, "nothing changed:\n{stdout}{stderr}");
assert!(stdout.contains("identical"), "and it says so:\n{stdout}");
}
#[test]
fn diff_of_an_unchanged_workspace_naming_a_standard_type_is_clean() {
let dir = TempDir::new("diffstd");
let root = package_workspace(&dir, NAMES_A_STANDARD_TYPE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the workspace snapshots cleanly:\n{stderr}");
let baseline = root.join(".ridl/baseline");
assert!(
baseline.join("veh.cluster.ir.json").is_file(),
"the declared package is published",
);
assert!(
!baseline.join("ridl.std.ir.json").exists(),
"the standard package is not part of the workspace's contract snapshot",
);
let (code, stdout, stderr) = ridl(&["diff".as_ref(), baseline.as_os_str(), root.as_os_str()]);
assert_eq!(code, 0, "nothing changed:\n{stdout}{stderr}");
assert!(stdout.contains("identical"), "and it says so:\n{stdout}");
}
#[test]
fn diff_of_a_directory_without_snapshots_compiles_it_as_source() {
let old = TempDir::new("srcold");
let old_root = package_workspace(&old, BASE);
let new = TempDir::new("srcnew");
let new_root = package_workspace(&new, REORDERED);
let (code, stdout, stderr) =
ridl(&["diff".as_ref(), old_root.as_os_str(), new_root.as_os_str()]);
assert_eq!(code, 1, "two source trees still compare:\n{stdout}{stderr}");
assert!(
stdout.contains("interaction_reordered"),
"and the reorder is found:\n{stdout}",
);
}
#[test]
fn baseline_drops_a_snapshot_whose_package_is_gone() {
let dir = TempDir::new("rename");
let root = package_workspace(&dir, BASE);
ridl(&["baseline".as_ref(), root.as_os_str()]);
assert!(
root.join(".ridl/baseline/veh.cluster.ir.json").is_file(),
"the first baseline is published",
);
dir.write(
"ridl.toml",
"[package]\nname = \"veh.dash\"\nversion = \"1.0.0\"\n",
);
dir.write("cluster.ridl", &BASE.replace("veh.cluster", "veh.dash"));
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the renamed workspace compiles:\n{stderr}");
let baseline = root.join(".ridl/baseline");
assert!(
baseline.join("veh.dash.ir.json").is_file(),
"the new package is published",
);
assert!(
!baseline.join("veh.cluster.ir.json").exists(),
"the snapshot under the old package name is gone",
);
}
#[test]
fn baseline_keeps_the_published_snapshots_when_the_compile_fails() {
let dir = TempDir::new("keep");
let root = package_workspace(&dir, BASE);
ridl(&["baseline".as_ref(), root.as_os_str()]);
let snapshot = root.join(".ridl/baseline/veh.cluster.ir.json");
let published = std::fs::read_to_string(&snapshot).expect("read the published snapshot");
dir.write(
"cluster.ridl",
"package veh.cluster
interface VehicleStatus {
event doorClosed: Nope @[100ms..1s]
}
",
);
let (code, _, _) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the broken workspace fails to publish");
assert_eq!(
std::fs::read_to_string(&snapshot).expect("the snapshot survives"),
published,
"the published baseline is exactly as it was",
);
}
fn ridl_407_block<'a>(stderr: &'a str, name: &str) -> &'a str {
let needle = format!("warning[RIDL-407]: `{name}` ");
let start = stderr
.find(&needle)
.unwrap_or_else(|| panic!("no RIDL-407 for `{name}` in:\n{stderr}"));
let after = start + needle.len();
match stderr[after..].find("warning[RIDL-407]") {
Some(next) => &stderr[start..after + next],
None => &stderr[start..],
}
}
#[test]
fn check_reports_ordinal_drift_against_the_committed_baseline() {
let entry = Path::new("tests/baseline-corpus");
assert!(
entry
.join(".ridl/baseline/corpus.baseline.ir.json")
.is_file(),
"the committed baseline snapshot is the point of this fixture",
);
let (code, _, stderr) = ridl(&["check".as_ref(), entry.as_os_str()]);
assert_eq!(code, 0, "a desk-check warning never moves the exit code");
for (name, remedy, line) in [
(
"setGear",
"retire it in place with `reserved setGear`",
"interface VehicleStatus {",
),
(
"tyrePressure",
"declare it at the end of the body instead",
"event tyrePressure : DoorState @[100ms..1s]",
),
(
"legacyWheelPhase",
"give this interaction a different name",
"signal legacyWheelPhase : Speed @10ms",
),
(
"doorOpened",
"put the declarations back in the baseline's order",
"event doorOpened : DoorState @[100ms..1s]",
),
(
"doorClosed",
"put the declarations back in the baseline's order",
"event doorClosed : DoorState @[100ms..1s]",
),
] {
let block = ridl_407_block(&stderr, name);
assert!(
block.starts_with(&format!("warning[RIDL-407]: `{name}` ")),
"the message opens by naming `{name}` in:\n{stderr}"
);
assert!(
block.contains(remedy),
"the message for `{name}` must carry its own remedy — `{remedy}` — in:\n{stderr}"
);
assert!(
block.contains("ridl §11"),
"the message for `{name}` must cite the rule it enforces in:\n{stderr}"
);
assert!(
block.contains(line),
"the span for `{name}` must point at `{line}` in:\n{stderr}"
);
}
assert!(
ridl_407_block(&stderr, "doorOpened").contains("(position 2 there, position 4 here)"),
"a reorder names both positions in:\n{stderr}"
);
assert!(
!ridl_407_block(&stderr, "doorClosed").contains("position"),
"a reorder whose absolute ordinal is unchanged must not print it:\n{stderr}"
);
let message_lines: Vec<&str> = stderr
.lines()
.filter(|line| line.starts_with("warning[RIDL-407]:"))
.collect();
assert_eq!(message_lines.len(), 6, "one line per diagnostic:\n{stderr}");
for line in &message_lines {
for internal in [
"ordinal",
"interaction_reordered",
"interaction_removed",
"interaction_inserted",
"reserved_name_redeclared",
] {
assert!(
!line.contains(internal),
"RIDL-407 must not answer in `{internal}` — that is IR or diff-report \
vocabulary, not the reader's:\n{line}"
);
}
assert!(
!line.contains('/'),
"RIDL-407 must not print a diff path — the reader wrote no `/`:\n{line}"
);
}
assert_eq!(
stderr.matches("RIDL-407").count(),
6,
"exactly six ordinal-affecting changes, no more:\n{stderr}"
);
}
#[test]
fn inline_shape_removal_spans_the_service_name() {
let (_, _, stderr) = ridl(&["check".as_ref(), "tests/baseline-corpus".as_ref()]);
let inline = ridl_407_block(&stderr, "setEcoMode");
assert!(
inline.starts_with(
"warning[RIDL-407]: `setEcoMode` is gone in `corpus.baseline.hvac` but the \
published baseline still declares it."
),
"the inline-shape removal is reported, naming the service it left:\n{stderr}"
);
assert!(
inline.contains("┌─"),
"the inline-shape removal carries a span — this is the sixth-instance \
regression, back:\n{inline}"
);
assert!(
inline.contains("service corpus.baseline.hvac {"),
"the fallback span points at the service's own declaration:\n{inline}"
);
assert!(
inline.contains("cluster.ridl:46:9"),
"the span starts at the dotted name, column 9 — not column 1, where \
the `service` keyword is:\n{inline}"
);
assert!(
inline.contains("^^^^^^^^^^^^^^^^^^^^"),
"the underline covers `corpus.baseline.hvac`, the 20-character dotted \
name:\n{inline}"
);
let named = ridl_407_block(&stderr, "setGear");
assert!(
named.contains("┌─") && named.contains("interface VehicleStatus {"),
"a removal from a named interface still spans its interface name:\n{named}"
);
}
#[test]
fn an_explicit_baseline_holding_no_snapshot_is_an_input_error() {
let dir = TempDir::new("empty-explicit");
let root = package_workspace(&dir, BASE);
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is written: {stderr}");
dir.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl(&[
"check".as_ref(),
root.as_os_str(),
"--baseline".as_ref(),
root.as_os_str(),
]);
assert_eq!(
code, 2,
"the tool could not answer, so it says so instead of passing:\n{stderr}",
);
assert!(
stderr.contains(&format!(
"the baseline `{}` holds no `.ir.json` snapshot directly inside it",
root.display()
)),
"the cause names the directory the flag aimed at:\n{stderr}",
);
assert!(
stderr.contains("point `--baseline` at the directory that holds the snapshots"),
"the remedy says to aim the flag at the snapshots:\n{stderr}",
);
}
#[test]
fn auto_discovery_of_an_empty_baseline_directory_stays_silent() {
let dir = TempDir::new("empty-auto");
let root = package_workspace(&dir, BASE);
std::fs::create_dir_all(root.join(".ridl").join("baseline"))
.expect("create the empty baseline directory");
let (code, stdout, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"a clean check with no baseline succeeds:\n{stderr}"
);
assert!(
stdout.is_empty() && stderr.is_empty(),
"no baseline means no drift report at all:\nstdout: {stdout}\nstderr: {stderr}",
);
}