use std::ffi::OsStr;
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-lints-{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: &[&OsStr]) -> (i32, String, String) {
ridl_in(None, args)
}
fn ridl_in(current_dir: Option<&Path>, args: &[&OsStr]) -> (i32, String, String) {
let mut command = Command::new(env!("CARGO_BIN_EXE_ridl"));
command.args(args);
if let Some(dir) = current_dir {
command.current_dir(dir);
}
let output = command.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 SOURCE: &str = "package demo\n\ntype Speed: integer [0..300]\n\ninterface Sensor {\n \
signal speed: Speed\n}\n";
const DENY: &str = "\n[lints]\nmissing-timing = \"deny\"\n";
const ALLOW: &str = "\n[lints]\nmissing-timing = \"allow\"\n";
fn member_workspace(dir: &TempDir, lints: &str) -> PathBuf {
dir.write("ridl.toml", "[workspace]\nmembers = [\"sensor\"]\n");
dir.write(
"sensor/ridl.toml",
&format!("[package]\nname = \"demo\"\nversion = \"1.0.0\"\n{lints}"),
);
dir.write("sensor/sensor.ridl", SOURCE);
dir.path().to_path_buf()
}
#[test]
fn json_carries_lint_field_and_deny_exits_1() {
let dir = TempDir::new("json-deny");
let root = member_workspace(&dir, DENY);
dir.write("sensor/broken.ridl", "package demo\ntype X:");
let (code, stdout, stderr) = ridl(&[
"check".as_ref(),
"--format".as_ref(),
"json".as_ref(),
root.as_os_str(),
]);
assert_eq!(
code, 1,
"a lint at deny exits 1:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let diagnostics: serde_json::Value = serde_json::from_str(&stdout).expect("stdout is JSON");
let diagnostics = diagnostics.as_array().expect("a JSON array");
let ridl_100 = diagnostics
.iter()
.find(|diagnostic| diagnostic["code"] == "RIDL-100")
.expect("a RIDL-100 element");
assert_eq!(ridl_100["severity"], "error", "{ridl_100}");
assert_eq!(ridl_100["lint"], "missing-timing", "{ridl_100}");
let error_code = diagnostics
.iter()
.find(|diagnostic| diagnostic["code"] != "RIDL-100" && diagnostic["severity"] == "error")
.expect("an Error-code element");
assert!(
error_code.get("lint").is_none(),
"an Error code has no `lint` key: {error_code}"
);
}
fn response_bound_workspace(dir: &TempDir, timing: &str) -> PathBuf {
dir.write("ridl.toml", "[workspace]\nmembers = [\"actuator\"]\n");
dir.write(
"actuator/ridl.toml",
"[package]\nname = \"demo\"\nversion = \"1.0.0\"\n\n[lints]\n\
missing-response-bound = \"deny\"\n",
);
dir.write(
"actuator/actuator.ridl",
&format!(
"package demo\n\ntype Speed: integer [0..300]\n\ninterface Actuator {{\n \
command setTarget(speed: Speed){timing}\n}}\n"
),
);
dir.path().to_path_buf()
}
#[test]
fn missing_response_bound_deny_exits_1_until_the_bound_is_written() {
let dir = TempDir::new("response-bound-deny");
let root = response_bound_workspace(&dir, "");
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "an untimed command at deny exits 1:\n{stderr}");
assert!(
stderr.contains("error[RIDL-112]"),
"the lint is rendered as an error:\n{stderr}"
);
let dir = TempDir::new("response-bound-written");
let root = response_bound_workspace(&dir, " @[..1s]");
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "a written response bound exits 0:\n{stderr}");
assert!(!stderr.contains("RIDL-112"), "no RIDL-112:\n{stderr}");
}
#[test]
fn text_deny_exits_1() {
let dir = TempDir::new("text-deny");
let root = member_workspace(&dir, DENY);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "a lint at deny exits 1:\n{stderr}");
assert!(
stderr.contains("error[RIDL-100]"),
"the lint is rendered as an error:\n{stderr}"
);
assert!(
!stderr.contains("warning[RIDL-100]"),
"the lint is not rendered as a warning:\n{stderr}"
);
}
#[test]
fn build_fails_on_deny() {
let dir = TempDir::new("build-deny");
let root = member_workspace(&dir, DENY);
let out = TempDir::new("build-deny-out");
let (code, _, stderr) = ridl(&[
"build".as_ref(),
"--out-dir".as_ref(),
out.path().as_os_str(),
"--emit".as_ref(),
"ir-json".as_ref(),
root.as_os_str(),
]);
assert_eq!(code, 1, "a lint at deny fails the build:\n{stderr}");
let written: Vec<PathBuf> = std::fs::read_dir(out.path())
.expect("the out dir is readable")
.map(|entry| entry.expect("a readable entry").path())
.collect();
assert!(written.is_empty(), "the build wrote: {written:?}");
}
#[test]
fn diff_ignores_deny() {
let old = TempDir::new("diff-old");
let old_root = member_workspace(&old, DENY);
let new = TempDir::new("diff-new");
let new_root = member_workspace(&new, DENY);
let (code, stdout, stderr) =
ridl(&["diff".as_ref(), old_root.as_os_str(), new_root.as_os_str()]);
assert_eq!(
code, 0,
"a lint at deny does not change the diff:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn baseline_ignores_deny() {
let dir = TempDir::new("baseline-deny");
let root = member_workspace(&dir, DENY);
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,
"a lint at deny does not block the baseline:\n{stderr}"
);
let snapshot = root.join(".ridl").join("baseline").join("demo.ir.json");
assert!(
snapshot.is_file(),
"the snapshot is written at {}",
snapshot.display()
);
}
const SOURCE_WITH_CONTRACT: &str = "package demo\n\ntype Speed: integer [0..300]\n\n\
interface Sensor {\n signal speed: Speed\n \
command setRange(min: Speed, max: Speed) [\n \
require min < max\n ] @[..50ms]\n}\n";
#[test]
fn test_ignores_deny() {
let dir = TempDir::new("test-deny");
dir.write(
"ridl.toml",
&format!("[package]\nname = \"demo\"\nversion = \"1.0.0\"\n{DENY}"),
);
dir.write("sensor.ridl", SOURCE_WITH_CONTRACT);
let root = dir.path();
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "the fixture draws the denied lint:\n{stderr}");
let (code, stdout, stderr) = ridl(&["test".as_ref(), root.as_os_str()]);
assert_eq!(
code, 0,
"a lint at deny does not change the test run:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("Sensor.setRange.require[0]"),
"the contract is sampled:\n{stdout}"
);
}
#[test]
fn member_entry_applies_root_lints() {
let dir = TempDir::new("member-entry");
dir.write(
"ridl.toml",
&format!("[workspace]\nmembers = [\"sensor\"]\n{DENY}"),
);
dir.write(
"sensor/ridl.toml",
"[package]\nname = \"demo\"\nversion = \"1.0.0\"\n",
);
dir.write("sensor/sensor.ridl", SOURCE);
let root = dir.path();
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "from the root, the root's deny applies:\n{stderr}");
let member = root.join("sensor");
let (code, _, stderr) = ridl(&["check".as_ref(), member.as_os_str()]);
assert_eq!(
code, 1,
"from the member, the root's deny applies:\n{stderr}"
);
assert!(
stderr.contains("error[RIDL-100]"),
"the lint is an error at the root's level:\n{stderr}"
);
}
fn sibling_workspace(dir: &TempDir) -> PathBuf {
dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
dir.write(
"a/ridl.toml",
"[package]\nname = \"a\"\nversion = \"1.0.0\"\n",
);
dir.write(
"a/a.typl",
"package a\nimport b.Speed\nstruct Cabin { primary: Speed }\n",
);
dir.write(
"b/ridl.toml",
"[package]\nname = \"b\"\nversion = \"1.0.0\"\n",
);
dir.write("b/speed.typl", "package b\ntype Speed: integer [0..300]\n");
dir.write(
"b/broken.typl",
"package b\nstruct Broken { field: Missing }\n",
);
dir.path().to_path_buf()
}
#[test]
fn member_entry_reports_only_the_member() {
let dir = TempDir::new("member-scope");
let root = sibling_workspace(&dir);
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "from the root, b's error is reported:\n{stderr}");
assert!(stderr.contains("broken.typl"), "{stderr}");
let (code, _, stderr) = ridl(&["check".as_ref(), root.join("a").as_os_str()]);
assert_eq!(code, 0, "from a, b's error is not reported:\n{stderr}");
assert!(!stderr.contains("broken.typl"), "{stderr}");
}
#[test]
fn member_build_writes_nothing_when_another_member_has_an_error() {
let dir = TempDir::new("member-build");
let root = sibling_workspace(&dir);
let out = TempDir::new("member-build-out");
let (code, _, stderr) = ridl(&[
"build".as_ref(),
"--out-dir".as_ref(),
out.path().as_os_str(),
"--emit".as_ref(),
"ir-json".as_ref(),
root.join("a").as_os_str(),
]);
assert_eq!(code, 1, "{stderr}");
assert!(
stderr.contains("another member of the workspace has an error"),
"{stderr}"
);
assert!(!stderr.contains("broken.typl"), "{stderr}");
let written: Vec<PathBuf> = std::fs::read_dir(out.path())
.expect("the out dir is readable")
.map(|entry| entry.expect("a readable entry").path())
.collect();
assert!(written.is_empty(), "the build wrote: {written:?}");
}
#[test]
fn member_build_exits_1_when_another_member_leaves_out_a_deployment() {
let dir = TempDir::new("member-build-rsdl");
dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"demo\"]\n");
dir.write(
"a/ridl.toml",
"[package]\nname = \"a\"\nversion = \"1.0.0\"\n",
);
dir.write("a/a.typl", "package a\ntype Level: integer [0..3]\n");
dir.write(
"demo/ridl.toml",
"[package]\nname = \"veh.demo\"\nversion = \"1.0.0\"\n",
);
dir.write(
"demo/lane.ridl",
"package veh.demo\n\ntype Flag: boolean\n\n\
interface LaneAssist {\n signal active: Flag @[100ms..1s]\n}\n\n\
service veh.demo.lane : LaneAssist\n",
);
dir.write(
"demo/topology.rsdl",
"package veh.demo\n\n\
component Lane { offers veh.demo.lane }\n\
component Panel { requires LaneAssist }\n\
system Vehicle { Lane, Panel }\n\
deployment Good for Vehicle { machine A { Lane, Panel } }\n\
deployment Bad for Vehicle { machine A { Lane } }\n",
);
let out = TempDir::new("member-build-rsdl-out");
let (code, _, stderr) = ridl(&[
"build".as_ref(),
"--out-dir".as_ref(),
out.path().as_os_str(),
"--emit".as_ref(),
"ir-json".as_ref(),
dir.path().join("a").as_os_str(),
]);
assert_eq!(code, 1, "{stderr}");
assert!(
stderr.contains("another member of the workspace has an error"),
"{stderr}"
);
assert!(!stderr.contains("RSDL-701"), "{stderr}");
assert!(
out.path().join("a.ir.json").is_file(),
"the packages are written"
);
let json = std::fs::read_to_string(out.path().join("veh.demo.Vehicle.system.json"))
.expect("the system is written");
assert!(json.contains("Good") && !json.contains("\"Bad\""), "{json}");
}
#[test]
fn member_entry_resolves_a_sibling_import() {
let dir = TempDir::new("member-sibling");
let root = sibling_workspace(&dir);
std::fs::remove_file(root.join("b/broken.typl")).expect("remove the broken file");
let (code, _, stderr) = ridl(&["check".as_ref(), root.join("a").as_os_str()]);
assert_eq!(code, 0, "b.Speed resolves from a:\n{stderr}");
let (code, _, stderr) = ridl_in(Some(&root.join("a")), &["check".as_ref(), ".".as_ref()]);
assert_eq!(code, 0, "b.Speed resolves from `.` inside a:\n{stderr}");
}
#[test]
fn allow_is_absent_in_text_and_json() {
let dir = TempDir::new("allow");
let root = member_workspace(&dir, ALLOW);
let (code, stdout, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stderr.contains("RIDL-100"),
"an allowed lint is absent from the text report:\n{stderr}"
);
let (code, stdout, stderr) = ridl(&[
"check".as_ref(),
"--format".as_ref(),
"json".as_ref(),
root.as_os_str(),
]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stdout.contains("RIDL-100"),
"an allowed lint is absent from the JSON report:\n{stdout}"
);
}
const SARIF_SCHEMA: &str = include_str!("fixtures/sarif-2.1.0.json");
fn sarif_check(root: &Path) -> (i32, serde_json::Value) {
sarif_check_in(root, ".".as_ref())
}
fn sarif_check_in(current_dir: &Path, entry: &OsStr) -> (i32, serde_json::Value) {
let (code, stdout, stderr) = ridl_in(
Some(current_dir),
&[
"check".as_ref(),
"--format".as_ref(),
"sarif".as_ref(),
entry,
],
);
let log: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|err| {
panic!("stdout is JSON ({err}):\nstdout:\n{stdout}\nstderr:\n{stderr}")
});
let schema: serde_json::Value =
serde_json::from_str(SARIF_SCHEMA).expect("the vendored schema is JSON");
let validator = jsonschema::validator_for(&schema).expect("the vendored schema compiles");
let errors: Vec<String> = validator
.iter_errors(&log)
.map(|err| format!("{} at {}", err, err.instance_path()))
.collect();
assert!(
errors.is_empty(),
"the log validates against the SARIF 2.1.0 schema:\n{}\nlog:\n{stdout}",
errors.join("\n")
);
(code, log)
}
fn sarif_results<'a>(log: &'a serde_json::Value, code: &str) -> Vec<&'a serde_json::Value> {
log["runs"][0]["results"]
.as_array()
.expect("results is an array")
.iter()
.filter(|result| result["ruleId"] == code)
.collect()
}
#[test]
fn sarif_validates_and_denies() {
let dir = TempDir::new("sarif-deny");
let root = member_workspace(&dir, DENY);
let (code, log) = sarif_check(&root);
assert_eq!(code, 1, "a lint at deny exits 1:\n{log}");
let ridl_100 = sarif_results(&log, "RIDL-100");
assert_eq!(ridl_100.len(), 1, "{log}");
assert_eq!(ridl_100[0]["level"], "error", "{}", ridl_100[0]);
assert_eq!(
ridl_100[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"sensor/sensor.ridl",
"{}",
ridl_100[0]
);
let dir = TempDir::new("sarif-allow");
let root = member_workspace(&dir, ALLOW);
let (code, log) = sarif_check(&root);
assert_eq!(code, 0, "{log}");
assert!(
sarif_results(&log, "RIDL-100").is_empty(),
"an allowed lint is absent from the SARIF log:\n{log}"
);
}
const SUB_SOURCE: &str = "package demo.sub\n\ntype Level: integer [0..10]\n\ninterface Gauge {\n \
signal level: Level\n}\n";
fn two_level_package(dir: &TempDir, sub_file: &str) {
dir.write(
"ws/ridl.toml",
"[package]\nname = \"demo\"\nversion = \"1.0.0\"\n",
);
dir.write("ws/sensor.ridl", SOURCE);
dir.write(&format!("ws/sub/{sub_file}"), SUB_SOURCE);
}
fn file_uri(path: &Path) -> String {
let mut uri = String::from("file://");
for byte in path.to_str().expect("the temp dir is UTF-8").bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => {
uri.push(byte as char);
}
_ => uri.push_str(&format!("%{byte:02X}")),
}
}
uri
}
fn ridl_100_locations(log: &serde_json::Value) -> Vec<(String, Option<String>)> {
let mut locations: Vec<(String, Option<String>)> = sarif_results(log, "RIDL-100")
.iter()
.map(|result| {
let location = &result["locations"][0]["physicalLocation"]["artifactLocation"];
(
location["uri"]
.as_str()
.expect("uri is a string")
.to_string(),
location["uriBaseId"].as_str().map(str::to_string),
)
})
.collect();
locations.sort();
locations
}
fn assert_cwd_relative_uris(log: &serde_json::Value, cwd: &Path, sub_uri: &str) {
let cwd = cwd.canonicalize().expect("the temp dir resolves");
let expected_base = format!("{}/", file_uri(&cwd));
assert_eq!(
log["runs"][0]["originalUriBaseIds"]["%SRCROOT%"]["uri"], expected_base,
"{log}"
);
let base = Some("%SRCROOT%".to_string());
assert_eq!(
ridl_100_locations(log),
vec![
("ws/sensor.ridl".to_string(), base.clone()),
(sub_uri.to_string(), base),
],
"{log}"
);
}
#[test]
fn sarif_uris_are_cwd_relative_from_a_file_entry() {
let dir = TempDir::new("sarif-uri-file");
two_level_package(&dir, "gauge.ridl");
let (code, log) = sarif_check_in(dir.path(), "ws/sensor.ridl".as_ref());
assert_eq!(code, 0, "{log}");
assert_cwd_relative_uris(&log, dir.path(), "ws/sub/gauge.ridl");
}
#[test]
fn sarif_uris_are_cwd_relative_from_a_subdirectory_entry() {
let dir = TempDir::new("sarif-uri-subdir");
two_level_package(&dir, "gauge.ridl");
let (code, log) = sarif_check_in(dir.path(), "ws/sub".as_ref());
assert_eq!(code, 0, "{log}");
assert_cwd_relative_uris(&log, dir.path(), "ws/sub/gauge.ridl");
}
#[test]
fn sarif_uris_percent_encode_a_space_and_a_hash() {
let dir = TempDir::new("sarif-uri-encode");
two_level_package(&dir, "b c#1.ridl");
let (code, log) = sarif_check_in(dir.path(), "ws".as_ref());
assert_eq!(code, 0, "{log}");
assert_cwd_relative_uris(&log, dir.path(), "ws/sub/b%20c%231.ridl");
}
const NOTE_LINE: &str = " = lint: `missing-timing` (set its level in `[lints]` in ridl.toml)";
#[test]
fn text_shows_lint_note() {
let dir = TempDir::new("note");
let root = member_workspace(&dir, "");
let (code, _, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "{stderr}");
assert!(
stderr.lines().any(|line| line == NOTE_LINE),
"the text report holds the line {NOTE_LINE:?}:\n{stderr}"
);
}
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 BASE_UNTIMED: &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
event doorOpened: DoorState @[100ms..1s]
event doorClosed: DoorState @[100ms..1s]
}
";
const REORDERED_UNTIMED: &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
event doorClosed: DoorState @[100ms..1s]
event doorOpened: DoorState @[100ms..1s]
}
";
fn published_package(dir: &TempDir, lints: &str, source: &str) -> (PathBuf, PathBuf) {
dir.write(
"ridl.toml",
&format!("[package]\nname = \"veh.cluster\"\nversion = \"1.0.0\"\n{lints}"),
);
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}");
let (code, _, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "the baseline is written: {stderr}");
(root.clone(), root.join(".ridl").join("baseline"))
}
const ALLOW_ORDINAL: &str = "\n[lints]\nordinal-changed = \"allow\"\n";
const DENY_ORDINAL: &str = "\n[lints]\nordinal-changed = \"deny\"\n";
#[test]
fn ordinal_changed_deny_fails_baseline() {
let dir = TempDir::new("ordinal-deny");
let (root, baseline) = published_package(&dir, DENY_ORDINAL, BASE);
dir.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl(&[
"check".as_ref(),
"--baseline".as_ref(),
baseline.as_os_str(),
root.as_os_str(),
]);
assert_eq!(
code, 1,
"the denied desk warning moves the exit code:\n{stderr}"
);
assert!(
stderr.contains("error[RIDL-407]"),
"`ordinal-changed = \"deny\"` reports the desk warning as an error:\n{stderr}"
);
}
#[test]
fn ordinal_changed_deny_applies_through_a_file_entry() {
let dir = TempDir::new("ordinal-deny-file-entry");
let other = dir.write(
"other.ridl",
"package veh.cluster\ntype Other: integer [0..1]\n",
);
let (_, baseline) = published_package(&dir, DENY_ORDINAL, BASE);
dir.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl(&[
"check".as_ref(),
"--baseline".as_ref(),
baseline.as_os_str(),
other.as_os_str(),
]);
assert_eq!(
code, 1,
"the denied desk warning moves the exit code:\n{stderr}"
);
assert!(
stderr.contains("error[RIDL-407]"),
"the package's `[lints]` scope reaches the desk warning:\n{stderr}"
);
assert!(
stderr.contains("cluster.ridl:6:3"),
"the desk warning points at the moved declaration:\n{stderr}"
);
}
#[test]
fn ordinal_changed_allow_silences_baseline() {
let dir = TempDir::new("ordinal-allow");
let (root, baseline) = published_package(&dir, ALLOW_ORDINAL, BASE);
dir.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl(&[
"check".as_ref(),
"--baseline".as_ref(),
baseline.as_os_str(),
root.as_os_str(),
]);
assert_eq!(code, 0, "{stderr}");
assert!(
!stderr.contains("RIDL-407"),
"`ordinal-changed = \"allow\"` silences the desk warning:\n{stderr}"
);
let control = TempDir::new("ordinal-default");
let (root, baseline) = published_package(&control, "", BASE);
control.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl(&[
"check".as_ref(),
"--baseline".as_ref(),
baseline.as_os_str(),
root.as_os_str(),
]);
assert_eq!(code, 0, "{stderr}");
assert!(
stderr.contains("warning[RIDL-407]"),
"the default level keeps the desk warning:\n{stderr}"
);
}
#[test]
fn ordinal_changed_allow_silences_baseline_from_a_relative_entry() {
let dir = TempDir::new("ordinal-allow-relative");
let (root, _) = published_package(&dir, ALLOW_ORDINAL, BASE);
dir.write("cluster.ridl", REORDERED);
let (code, _, stderr) = ridl_in(
Some(&root),
&[
"check".as_ref(),
"--baseline".as_ref(),
".ridl/baseline".as_ref(),
".".as_ref(),
],
);
assert_eq!(code, 0, "{stderr}");
assert!(
!stderr.contains("RIDL-407"),
"`ordinal-changed = \"allow\"` silences the desk warning from a relative entry:\n{stderr}"
);
}
#[test]
fn deny_lint_does_not_skip_desk_check() {
let dir = TempDir::new("deny-desk");
let (root, baseline) = published_package(&dir, DENY, BASE_UNTIMED);
dir.write("cluster.ridl", REORDERED_UNTIMED);
let (code, _, stderr) = ridl(&[
"check".as_ref(),
"--baseline".as_ref(),
baseline.as_os_str(),
root.as_os_str(),
]);
assert_eq!(code, 1, "the denied lint moves the exit code:\n{stderr}");
assert!(
stderr.contains("error[RIDL-100]"),
"the denied lint is reported as an error:\n{stderr}"
);
assert!(
stderr.contains("warning[RIDL-407]"),
"the desk check still runs beside the denied lint:\n{stderr}"
);
}
#[test]
fn inconsistent_unit_levels_reach_compiler_and_cli_reports() {
let source = "package a\ntype Speed: km/h [0.0..250.0 step 0.5]\ntype SpeedMs: m/s [0.0..100.0 step 0.5]\nstruct First { speed: Speed }\nstruct Second { speed: Speed }\nstruct Third { speed: SpeedMs }\n";
for (level, expected_count, expected_severity, expected_exit) in [
("info", 1, "info", 0),
("allow", 0, "info", 0),
("deny", 1, "error", 1),
] {
let dir = TempDir::new("unit-levels");
dir.write("ridl.toml", &format!("[package]\nname = \"a\"\nversion = \"1.0.0\"\n[lints]\ninconsistent-unit = \"{level}\"\n"));
dir.write("source.ridl", source);
let compiled =
ridlc::compile_workspace(&mut ridl_core::RidlDatabase::default(), dir.path()).unwrap();
let compiled_diagnostics = compiled
.diagnostics
.iter()
.filter(|diagnostic| {
diagnostic.code != ridl_core::lint::lint_by_name("missing-docs").unwrap().code
})
.collect::<Vec<_>>();
assert_eq!(compiled_diagnostics.len(), 1, "{:?}", compiled_diagnostics);
assert_eq!(compiled_diagnostics[0].code.as_str(), "TYPL-222");
assert_eq!(
compiled_diagnostics[0].severity,
ridl_core::diag::Severity::Info
);
let run = ridlc::run_check(dir.path(), ridl_core::Frozen::Yes).unwrap();
let run_diagnostics = run
.diagnostics
.iter()
.filter(|diagnostic| {
diagnostic.code != ridl_core::lint::lint_by_name("missing-docs").unwrap().code
})
.collect::<Vec<_>>();
assert_eq!(
run_diagnostics.len(),
expected_count,
"{level}: {:?}",
run_diagnostics
);
assert_eq!(run.has_error(), expected_exit == 1);
if let Some(diagnostic) = run_diagnostics.first() {
assert_eq!(diagnostic.code.as_str(), "TYPL-222");
assert_eq!(
diagnostic.severity,
if level == "deny" {
ridl_core::diag::Severity::Error
} else {
ridl_core::diag::Severity::Info
}
);
}
let (exit, stdout, stderr) = ridl(&[
"check".as_ref(),
"--format".as_ref(),
"json".as_ref(),
dir.path().as_os_str(),
]);
assert_eq!(exit, expected_exit, "{level}: {stderr}");
let diagnostics: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let diagnostics = diagnostics
.as_array()
.unwrap()
.iter()
.filter(|diagnostic| diagnostic["lint"] != "missing-docs")
.collect::<Vec<_>>();
assert_eq!(
diagnostics.len(),
expected_count,
"{level}: {diagnostics:?}"
);
if let Some(diagnostic) = diagnostics.first() {
assert_eq!(diagnostic["code"], "TYPL-222");
assert_eq!(diagnostic["lint"], "inconsistent-unit");
assert_eq!(diagnostic["severity"], expected_severity);
assert_eq!(
diagnostic["message"],
"`speed` uses `m/s` here; elsewhere `speed` uses `km/h`"
);
}
}
}
const BLOCK_DOC_SOURCE: &str = "package demo\n\n/** A speed. */\ntype Speed: integer [0..300]\n";
fn block_doc_workspace(dir: &TempDir, lints: &str) -> PathBuf {
dir.write("ridl.toml", "[workspace]\nmembers = [\"speed\"]\n");
dir.write(
"speed/ridl.toml",
&format!("[package]\nname = \"demo\"\nversion = \"1.0.0\"\n{lints}"),
);
dir.write("speed/speed.typl", BLOCK_DOC_SOURCE);
dir.path().to_path_buf()
}
#[test]
fn doc_comment_style_is_silent_by_default() {
let dir = TempDir::new("doc-style-default");
let root = block_doc_workspace(&dir, "");
let (code, stdout, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stderr.contains("TYPL-410") && !stdout.contains("TYPL-410"),
"a lint that is allowed by default is not reported:\n{stderr}"
);
}
#[test]
fn doc_comment_style_at_warn_is_reported() {
let dir = TempDir::new("doc-style-warn");
let root = block_doc_workspace(&dir, "\n[lints]\ndoc-comment-style = \"warn\"\n");
let (code, stdout, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
stderr.contains("warning[TYPL-410]"),
"the lint at warn is rendered as a warning:\n{stderr}"
);
}
#[test]
fn doc_comment_style_at_deny_exits_1() {
let dir = TempDir::new("doc-style-deny");
let root = block_doc_workspace(&dir, "\n[lints]\ndoc-comment-style = \"deny\"\n");
let (code, stdout, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 1, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
stderr.contains("error[TYPL-410]"),
"the lint at deny is rendered as an error:\n{stderr}"
);
}
#[test]
fn doc_comment_style_is_silent_in_baseline_and_lock() {
let dir = TempDir::new("doc-style-baseline");
let root = block_doc_workspace(&dir, "");
let (code, stdout, stderr) = ridl(&["lock".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stderr.contains("TYPL-410"),
"`ridl lock` does not report an allow-by-default lint:\n{stderr}"
);
let (code, stdout, stderr) = ridl(&["baseline".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stderr.contains("TYPL-410"),
"`ridl baseline` does not report an allow-by-default lint:\n{stderr}"
);
}
#[test]
fn doc_comment_style_is_silent_in_a_diff_compile_error() {
let dir = TempDir::new("doc-style-diff");
let old = dir.write("old/speed.typl", BLOCK_DOC_SOURCE);
let new = dir.write(
"new/speed.typl",
&format!("{BLOCK_DOC_SOURCE}type Broken: Missing\n"),
);
let (code, stdout, stderr) = ridl(&["diff".as_ref(), old.as_os_str(), new.as_os_str()]);
assert_eq!(code, 2, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
stderr.contains("error"),
"the new side does not compile:\n{stderr}"
);
assert!(
!stderr.contains("TYPL-410"),
"`ridl diff` does not report an allow-by-default lint:\n{stderr}"
);
}
fn undocumented_dependency_workspace(dir: &TempDir) -> PathBuf {
dir.write("ridl.toml", "[workspace]\nmembers = [\"a\", \"b\"]\n");
dir.write(
"a/ridl.toml",
"[package]\nname = \"a\"\nversion = \"1.0.0\"\n",
);
dir.write(
"a/a.typl",
"package a\nimport b.Speed\n/// The cabin.\nstruct Cabin {\n /// The primary speed.\n \
primary: Speed\n}\n",
);
dir.write(
"b/ridl.toml",
"[package]\nname = \"b\"\nversion = \"1.0.0\"\n",
);
dir.write(
"b/speed.typl",
"package b\ntype Speed: integer [0..300]\nstruct Pair {\n left: Speed\n}\n",
);
dir.path().to_path_buf()
}
#[test]
fn missing_docs_skips_dependencies() {
let dir = TempDir::new("missing-docs-deps");
let root = undocumented_dependency_workspace(&dir);
let (code, stdout, stderr) = ridl(&["check".as_ref(), root.join("a").as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stderr.contains("TYPL-406") && !stdout.contains("TYPL-406"),
"the dependency's undocumented items are not reported:\n{stderr}"
);
let (code, stdout, stderr) = ridl(&["check".as_ref(), root.as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert_eq!(
stderr.matches("warning[TYPL-406]").count(),
3,
"from the root, `Speed`, `Pair` and `left` are reported:\n{stderr}"
);
}
#[test]
fn missing_docs_at_allow_is_silent() {
let dir = TempDir::new("missing-docs-allow");
dir.write("ridl.toml", "[workspace]\nmembers = [\"b\"]\n");
dir.write(
"b/ridl.toml",
"[package]\nname = \"b\"\nversion = \"1.0.0\"\n\n[lints]\nmissing-docs = \"allow\"\n",
);
dir.write(
"b/speed.typl",
"package b\ntype Speed: integer [0..300]\nstruct Pair {\n left: Speed\n}\n",
);
let (code, stdout, stderr) = ridl(&["check".as_ref(), dir.path().as_os_str()]);
assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stderr.contains("TYPL-406") && !stdout.contains("TYPL-406"),
"a lint at allow is not reported:\n{stderr}"
);
}