mod common;
use assert_cmd::Command;
fn rat() -> Command {
let mut cmd = common::rat();
cmd.env_remove("NO_COLOR");
cmd
}
fn rat_bin() -> String {
assert_cmd::cargo::cargo_bin("rat").display().to_string()
}
fn fixture(dir: &std::path::Path, name: &str, body: &str) -> String {
let path = dir.join(name);
std::fs::write(&path, body).expect("write fixture");
path.display().to_string()
}
#[test]
fn a_dashboard_renders_its_panes_once() {
let dir = tempfile::tempdir().expect("tempdir");
let file = fixture(
dir.path(),
"board.kdl",
&format!(
r#"
defaults {{
height 3
chrome #false
}}
pane "left" {{
command "{bin}" "style" "hello"
}}
pane "right" {{
command "{bin}" "style" "world"
}}
"#,
bin = rat_bin().replace('\\', "\\\\")
),
);
rat()
.env("NO_COLOR", "1")
.args(["dashboard", &file, "--once"])
.assert()
.success()
.stdout(predicates::str::contains("hello"))
.stdout(predicates::str::contains("world"));
}
#[test]
fn a_pane_child_is_told_its_inner_geometry() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = rat_bin().replace('\\', "\\\\");
let file = fixture(
dir.path(),
"geom.kdl",
&format!(
r#"
defaults height=3 chrome=#false border="none" padding="0" width="20"
pane "cols" {{
command "{bin}" "__env" "RAT_WIDTH"
}}
pane "rows" {{
command "{bin}" "__env" "RAT_HEIGHT"
}}
pane "whoami" {{
command "{bin}" "__env" "RAT_PANE"
}}
"#
),
);
let assert = rat()
.env("NO_COLOR", "1")
.args(["dashboard", &file, "--once"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
assert!(
stdout.contains("20"),
"RAT_WIDTH is the pane's cells: {stdout:?}"
);
assert!(
stdout.contains('3'),
"RAT_HEIGHT is the pane's inner rows: {stdout:?}"
);
assert!(
stdout.contains("whoami"),
"RAT_PANE is the pane's name: {stdout:?}"
);
}
#[test]
fn a_pane_taller_than_its_box_is_truncated_keep_top() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = rat_bin().replace('\\', "\\\\");
let file = fixture(
dir.path(),
"tall.kdl",
&format!(
r#"
pane "tall" {{
height 3
chrome #false
border "none"
command "{bin}" "style" "AAA" "BBB" "CCC" "DDD" "EEE"
}}
"#
),
);
let assert = rat()
.env("NO_COLOR", "1")
.args(["dashboard", &file, "--once"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
assert!(
stdout.contains("AAA"),
"keep-top keeps the head: {stdout:?}"
);
assert!(
!stdout.contains("EEE"),
"the pin truncated nothing: {stdout:?}"
);
}
#[test]
fn a_pane_that_has_not_run_renders_blank_at_its_declared_size() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = rat_bin().replace('\\', "\\\\");
let file = fixture(
dir.path(),
"stack.kdl",
&format!(
r#"
defaults {{
height 3
chrome #false
border "none"
}}
pane "a" {{
command "{bin}" "style" "one"
}}
pane "b" {{
command "{bin}" "style" "two"
}}
"#
),
);
let assert = rat()
.env("NO_COLOR", "1")
.args(["dashboard", &file, "--once"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
let rows = stdout.lines().count();
assert!(rows >= 6, "a whole frame is 6 rows, got {rows}: {stdout:?}");
assert_eq!(
rows % 6,
0,
"every frame is exactly 6 rows; got {rows}: {stdout:?}"
);
}
#[test]
fn an_unreadable_file_names_the_path() {
let missing = "definitely-no-such-dashboard-xyz.kdl";
rat()
.args(["dashboard", missing])
.assert()
.code(1)
.stderr(predicates::str::contains(missing));
}
#[test]
fn a_file_that_is_not_kdl_names_the_path() {
let dir = tempfile::tempdir().expect("tempdir");
let file = fixture(dir.path(), "board.conf", "gap = 0\n");
rat()
.args(["dashboard", &file])
.assert()
.code(1)
.stderr(predicates::str::contains("board.conf"));
}
#[test]
fn a_failing_pane_shows_its_exit_code_and_the_rest_of_the_dashboard_survives() {
let dir = tempfile::tempdir().expect("tempdir");
let steady = dir.path().join("steady");
std::fs::write(&steady, "steady-content").expect("seed");
let decl = dir.path().join("dash.kdl");
std::fs::write(
&decl,
format!(
r#"
row-gap 0
defaults {{
height 5
border "rounded"
}}
pane "broken" {{
command "{rat}" "__exitcode" "3" "boom-from-stderr"
}}
pane "steady" {{
command "{rat}" "__cat" "{steady}"
}}
"#,
rat = rat_bin().escape_default(),
steady = steady.display().to_string().escape_default(),
),
)
.expect("write declaration");
let assert = rat()
.env("NO_COLOR", "1")
.args(["dashboard", "--once", &decl.display().to_string()])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
assert!(stdout.contains("boom-from-stderr"), "{stdout:?}");
assert!(stdout.contains(" · exit 3"), "{stdout:?}");
assert!(stdout.contains("steady-content"), "{stdout:?}");
assert_eq!(
stdout.trim_end_matches('\n').split('\n').count(),
10,
"declared heights must survive a failure: {stdout:?}"
);
assert_eq!(
String::from_utf8_lossy(&assert.get_output().stderr),
"",
"a failing pane must not leak to the terminal"
);
}
fn stdout_stream(stdout: std::process::ChildStdout) -> std::sync::mpsc::Receiver<Vec<u8>> {
use std::io::Read;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut stdout = stdout;
let mut buf = [0u8; 4096];
while let Ok(n) = stdout.read(&mut buf) {
if n == 0 || tx.send(buf[..n].to_vec()).is_err() {
return;
}
}
});
rx
}
fn read_until(stream: &std::sync::mpsc::Receiver<Vec<u8>>, seen: &mut String, needle: &str) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
loop {
if seen.contains(needle) {
return;
}
let left = deadline.saturating_duration_since(std::time::Instant::now());
assert!(!left.is_zero(), "never saw {needle:?} in {seen:?}");
match stream.recv_timeout(left) {
Ok(chunk) => seen.push_str(&String::from_utf8_lossy(&chunk)),
Err(_) => panic!("never saw {needle:?} in {seen:?}"),
}
}
}
struct KillOnDrop(std::process::Child);
impl Drop for KillOnDrop {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
#[test]
fn a_file_trigger_refreshes_only_its_own_pane() {
let dir = tempfile::tempdir().expect("tempdir");
let steady = dir.path().join("steady");
let shared = dir.path().join("shared");
let untouched = dir.path().join("untouched");
std::fs::write(&steady, "a0").expect("seed");
std::fs::write(&shared, "v0").expect("seed");
std::fs::write(&untouched, "x").expect("seed");
let decl = dir.path().join("dash.kdl");
std::fs::write(
&decl,
format!(
r#"
row-gap 0
defaults {{
height 1
border "none"
chrome #false
interval "never"
trigger-debounce "0ms"
}}
pane "alpha" {{
command "{rat}" "__cat" "{steady}"
trigger "file:{untouched}"
}}
pane "beta" {{
command "{rat}" "__cat" "{shared}"
trigger "file:{shared}"
}}
"#,
rat = rat_bin().escape_default(),
steady = steady.display().to_string().escape_default(),
shared = shared.display().to_string().escape_default(),
untouched = untouched.display().to_string().escape_default(),
),
)
.expect("write declaration");
let dash = std::process::Command::new(rat_bin())
.args(["dashboard", &decl.display().to_string()])
.stdout(std::process::Stdio::piped())
.spawn()
.expect("spawn rat dashboard piped");
let mut dash = KillOnDrop(dash);
let stream = stdout_stream(dash.0.stdout.take().expect("piped stdout"));
let mut seen = String::new();
read_until(&stream, &mut seen, "v0");
std::fs::write(&shared, "v1").expect("mtime change");
read_until(&stream, &mut seen, "v1");
let last_frame = seen.rfind("a0").expect("alpha's retained row");
assert!(
seen[last_frame..].contains("v1"),
"the refreshed frame keeps declaration order: {seen:?}"
);
}
#[test]
fn successive_trigger_changes_each_refresh_the_pane() {
let dir = tempfile::tempdir().expect("tempdir");
let watched = dir.path().join("watched");
std::fs::write(&watched, "v0").expect("seed");
let decl = dir.path().join("dash.kdl");
std::fs::write(
&decl,
format!(
r#"
row-gap 0
defaults {{
height 1
border "none"
chrome #false
interval "never"
trigger-debounce "0ms"
}}
pane "only" {{
command "{rat}" "__cat" "{watched}"
trigger "file:{watched}"
}}
"#,
rat = rat_bin().escape_default(),
watched = watched.display().to_string().escape_default(),
),
)
.expect("write declaration");
let dash = std::process::Command::new(rat_bin())
.args(["dashboard", &decl.display().to_string()])
.stdout(std::process::Stdio::piped())
.spawn()
.expect("spawn rat dashboard piped");
let mut dash = KillOnDrop(dash);
let stream = stdout_stream(dash.0.stdout.take().expect("piped stdout"));
let mut seen = String::new();
read_until(&stream, &mut seen, "v0");
for value in ["v1", "v2", "v3"] {
std::fs::write(&watched, value).expect("mtime change");
read_until(&stream, &mut seen, value);
}
}
#[test]
fn once_emits_exactly_one_complete_frame() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = rat_bin().replace('\\', "\\\\");
let file = fixture(
dir.path(),
"staggered.kdl",
&format!(
r#"
row-gap 0
defaults {{
height 1
chrome #false
border "none"
}}
pane "quick" {{
command "{bin}" "style" "one"
}}
pane "slow" {{
command "{bin}" "__sleep" "300" "two"
}}
"#
),
);
let assert = rat()
.env("NO_COLOR", "1")
.args(["dashboard", &file, "--once"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
assert_eq!(
stdout.trim_end_matches('\n').split('\n').count(),
2,
"one frame, both panes: {stdout:?}"
);
assert_eq!(
stdout.matches("one").count(),
1,
"the quick pane printed once: {stdout:?}"
);
assert!(stdout.contains("two"), "the slow pane arrived: {stdout:?}");
}
#[test]
fn a_piped_dashboard_sizes_from_rat_width() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = rat_bin().replace('\\', "\\\\");
let file = fixture(
dir.path(),
"sized.kdl",
&format!(
r#"
pane "wide" {{
height 2
chrome #false
border "none"
command "{bin}" "style" "x"
}}
"#
),
);
let assert = rat()
.env("NO_COLOR", "1")
.env("RAT_WIDTH", "40")
.env("RAT_HEIGHT", "20")
.args(["dashboard", &file, "--once"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
for line in stdout.trim_end_matches('\n').split('\n') {
assert_eq!(line.chars().count(), 40, "a 40-cell frame: {line:?}");
}
}
#[test]
fn a_nested_layout_renders_a_grid() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = rat_bin().replace('\\', "\\\\");
let file = fixture(
dir.path(),
"grid.kdl",
&format!(
r#"
defaults height=1 chrome=#false border="none"
row {{
column {{
pane "a" {{
command "{bin}" "style" "one"
}}
pane "b" {{
command "{bin}" "style" "two"
}}
}}
pane "c" height=2 {{
command "{bin}" "style" "three"
}}
}}
"#
),
);
let assert = rat()
.env("NO_COLOR", "1")
.env("RAT_WIDTH", "40")
.args(["dashboard", &file, "--once"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
let rows: Vec<&str> = stdout.trim_end_matches('\n').split('\n').collect();
assert_eq!(rows.len(), 2, "a 2-row grid: {stdout:?}");
assert!(
rows[0].contains("one") && rows[0].contains("three"),
"top of the column beside the tall pane: {stdout:?}"
);
assert!(
rows[1].contains("two"),
"bottom of the column on the second grid row: {stdout:?}"
);
}
#[test]
fn a_flooding_pane_wears_the_marker_on_its_own_chrome_row() {
let dir = tempfile::tempdir().expect("tempdir");
let bin = rat_bin().replace('\\', "\\\\");
let file = fixture(
dir.path(),
"flood.kdl",
&format!(
r#"
defaults height=6 width="60" border="none" padding="0"
pane "flood" {{
command "{bin}" "__lines" "1500"
}}
pane "quiet" {{
command "{bin}" "style" "calm"
}}
"#
),
);
let assert = rat()
.env("NO_COLOR", "1")
.args(["dashboard", &file, "--once"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
assert!(
stdout.contains("500 lines dropped"),
"the flooding pane must say so; got {stdout:?}"
);
assert!(
stdout.starts_with('0'),
"a keep-top pane shows its head: {stdout:?}"
);
assert!(
!stdout.contains("1499"),
"and its tail is what went: {stdout:?}"
);
assert_eq!(
stdout.matches("lines dropped").count(),
1,
"only the flooding pane wears it: {stdout:?}"
);
}