Files
komorebi/komorebic/tests/linux_check.rs
T
LGUG2Z d41cdc8979 refactor(komorebic): move the KDE CLI into the unified komorebic package (pure move)
komorebi-for-kde/komorebic/src/{main.rs,hotkeys.rs,applications.json} and tests/{check,cli,hotkeys}.rs
move to komorebic/src/linux/{mod.rs,hotkeys.rs,applications.json} and komorebic/tests/linux_*.rs
(#![cfg(target_os = "linux")]). The only edits are paths: include_str! and the tests' fixture and
repository-root paths point into komorebi-for-kde/ (docs, komorebi-hotkeys, schema files),
komorebi/tests/asc.rs reads the moved applications.json. The old package komorebic-linux stays and
builds from the moved file through a shim (src/main.rs includes ../../../komorebic/src/linux/mod.rs), so
the old name works until the cutover; its tests now run in the unified package. The KDE flake builds and
tests komorebic (unified) instead of komorebic-linux, its source filter takes the unified package's
manifest, build.rs, main.rs, src/linux and tests/linux_*; the Windows source filter leaves the Linux
and macOS files out. Comments and docs that name komorebic/src/main.rs or komorebic/tests/*.rs
(check.rs, ARCHITECTURE.md, scripts/compare-gen.py, the VM check suite) were not touched: they would
change the KDE derivations for text only.

Gates run: KDE cargo test -p komorebi-linux -p komorebic -p komorebic-linux -p komorebi-client-linux
-p komorebi-hotkeys --no-fail-fast: 888 passed, 0 failed (the moved komorebic tests included); Windows
clippy -D warnings PASS; macOS fake-CC check PASS; fast-check nix evaluation PASS; CLI docs oracle for
kde now generated by the unified komorebic: 166 pages, 0 diff, and its --help first line and --version
equal the old package's. Not run: nix builds (the KDE derivations changed), VM suites. Note: cargo
build/test of both komorebic and komorebic-linux in one command writes the same binary name twice
(use one of them).
2026-10-31 12:19:01 -07:00

311 lines
9.8 KiB
Rust

#![cfg(target_os = "linux")]
//! `komorebic check`, as a person runs it: a configuration in, a report and an exit status out.
use std::path::Path;
use std::process::Command;
use std::process::Output;
fn check(config: Option<&str>, extra: &[&str]) -> (Output, String) {
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let dir = std::env::temp_dir().join(format!(
"kmc-cli-{}-{}",
std::process::id(),
NEXT.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut command = Command::new(env!("CARGO_BIN_EXE_komorebic"));
command
.arg("check")
.arg("--configuration-only")
.env("KOMOREBI_CONFIG_HOME", &dir)
// there is no desktop to ask about, and nothing from this one to leak in
.env_remove("DBUS_SESSION_BUS_ADDRESS");
if let Some(config) = config {
let path = dir.join("komorebi.json");
std::fs::write(&path, config).unwrap();
command.arg("--komorebi-config").arg(&path);
}
command.args(extra);
let output = command.output().unwrap();
let text = String::from_utf8_lossy(&output.stdout).into_owned();
let _ = std::fs::remove_dir_all(Path::new(&dir));
(output, text)
}
#[test]
fn a_good_configuration_passes() {
let (output, text) = check(
Some(
r#"{ "border": true, "monitors": [ { "workspaces": [ { "name": "a", "layout": "BSP" } ] } ] }"#,
),
&[],
);
assert!(output.status.success(), "{text}");
assert!(text.contains("is valid"), "{text}");
assert!(text.contains("0 error(s), 0 warning(s)"), "{text}");
}
#[test]
fn a_file_that_is_not_json_fails_and_shows_the_line() {
let (output, text) = check(Some("{\n \"border\": tru,\n}\n"), &[]);
assert_eq!(output.status.code(), Some(1), "{text}");
assert!(text.contains("not valid JSON"), "{text}");
assert!(text.contains("2 | \"border\": tru,"), "{text}");
assert!(text.contains("^"), "{text}");
}
#[test]
fn a_value_that_does_not_exist_fails_and_shows_the_line() {
let (output, text) = check(
Some("{\n \"cross_boundary_behaviour\": \"Sideways\"\n}\n"),
&[],
);
assert_eq!(output.status.code(), Some(1), "{text}");
assert!(
text.contains("does not match what komorebi reads"),
"{text}"
);
assert!(text.contains("2 |"), "{text}");
}
#[test]
fn a_typo_is_a_warning_with_the_word_that_was_meant_and_does_not_fail() {
let (output, text) = check(
Some(r#"{ "bordr_width": 10, "border_style": "System" }"#),
&[],
);
assert!(output.status.success(), "{text}");
assert!(text.contains("`bordr_width` is not a setting"), "{text}");
assert!(text.contains("did you mean `border_width`?"), "{text}");
// a key for another platform is not a mistake
assert!(
text.contains("accepted and ignored here: border_style"),
"{text}"
);
assert!(text.contains("1 warning(s)"), "{text}");
}
#[test]
fn no_file_is_not_an_error() {
let (output, text) = check(None, &[]);
assert!(output.status.success(), "{text}");
assert!(text.contains("starts with its defaults"), "{text}");
}
#[test]
fn configuration_only_leaves_the_desktop_out() {
let (output, text) = check(None, &[]);
assert!(output.status.success(), "{text}");
assert!(!text.contains("The desktop"), "{text}");
}
/// Runs `komorebic check` with no `--configuration-only`, in a config home and a config directory
/// of its own, against the session bus at `bus`.
fn check_desktop(dir: &Path, bus: &Path) -> (Output, String) {
let output = Command::new(env!("CARGO_BIN_EXE_komorebic"))
.arg("check")
.env("KOMOREBI_CONFIG_HOME", dir)
.env("XDG_CONFIG_HOME", dir.join("config"))
// a bus that is not where the address says, whatever the environment has to offer: with
// no address, a connection looks for the user's bus in the runtime directory
.env(
"DBUS_SESSION_BUS_ADDRESS",
format!("unix:path={}", bus.display()),
)
.output()
.unwrap();
let text = String::from_utf8_lossy(&output.stdout).into_owned();
(output, text)
}
fn scratch(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("kmc-cli-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn without_a_session_bus_the_desktop_is_an_error() {
let dir = scratch("desktop");
let (output, text) = check_desktop(&dir, &dir.join("no-bus"));
assert_eq!(output.status.code(), Some(1), "{text}");
assert!(text.contains("The desktop"), "{text}");
assert!(
text.contains("could not connect to the session bus"),
"{text}"
);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn a_session_bus_that_never_answers_is_given_up_on() {
let dir = scratch("stuck");
let bus = dir.join("stuck.bus");
// it takes the connection and says nothing, so the handshake never ends
let listener = std::os::unix::net::UnixListener::bind(&bus).unwrap();
let holder = std::thread::spawn(move || listener.accept().map(|(stream, _)| stream));
let started = std::time::Instant::now();
let (output, text) = check_desktop(&dir, &bus);
assert_eq!(output.status.code(), Some(1), "{text}");
assert!(
text.contains("could not connect to the session bus"),
"{text}"
);
assert!(
started.elapsed() < std::time::Duration::from_secs(10),
"{:?}",
started.elapsed()
);
let _ = holder.join();
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn what_starts_komorebi_at_login_is_said_whether_or_not_the_desktop_is_there() {
let dir = scratch("login");
let no_bus = dir.join("no-bus");
let (_, text) = check_desktop(&dir, &no_bus);
assert!(text.contains("komorebi is not started at login"), "{text}");
let enabled = Command::new(env!("CARGO_BIN_EXE_komorebic"))
.arg("enable-autostart")
.env("KOMOREBI_CONFIG_HOME", &dir)
.env("XDG_CONFIG_HOME", dir.join("config"))
.output()
.unwrap();
assert!(enabled.status.success(), "{enabled:?}");
let (_, text) = check_desktop(&dir, &no_bus);
let entry = dir.join("config/autostart/komorebi.desktop");
assert!(
text.contains(&format!(
"komorebi is started at login by {}",
entry.display()
)),
"{text}"
);
let _ = std::fs::remove_dir_all(dir);
}
/// Runs `komorebic check --configuration-only` in the config home `dir`, with a data home of its
/// own: the section of the key bindings is the one this looks at.
fn check_keys(dir: &Path) -> (Output, String) {
let output = Command::new(env!("CARGO_BIN_EXE_komorebic"))
.arg("check")
.arg("--configuration-only")
.env("KOMOREBI_CONFIG_HOME", dir)
.env("XDG_DATA_HOME", dir.join("data"))
.output()
.unwrap();
let text = String::from_utf8_lossy(&output.stdout).into_owned();
(output, text)
}
#[test]
fn without_a_key_binding_file_the_section_says_where_key_bindings_come_from() {
let dir = scratch("keys-none");
let (output, text) = check_keys(&dir);
assert!(output.status.success(), "{text}");
assert!(text.contains("Key bindings"), "{text}");
assert!(
text.contains("there is no skhdrc, whkdrc or sxhkdrc in"),
"{text}"
);
assert!(text.contains("komorebic hotkeys compile"), "{text}");
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn the_key_binding_file_of_the_config_home_is_read_and_a_line_it_cannot_read_is_a_warning() {
let dir = scratch("keys-file");
std::fs::write(
dir.join("skhdrc"),
"alt - h : komorebic focus left\nalt - nosuchkey : komorebic focus up\n",
)
.unwrap();
let (output, text) = check_keys(&dir);
// a warning is not a reason to fail
assert!(output.status.success(), "{text}");
assert!(text.contains("skhdrc has 1 key binding"), "{text}");
assert!(
text.contains("1 line of it cannot be read and left out"),
"{text}"
);
assert!(text.contains("line 2: error: "), "{text}");
assert!(text.contains("1 warning(s)"), "{text}");
let _ = std::fs::remove_dir_all(dir);
}
// the data home is where `dirs` looks on Linux only
#[cfg(target_os = "linux")]
#[test]
fn shortcut_files_the_file_does_not_call_for_are_a_warning_until_compile_replaces_them() {
let dir = scratch("keys-stale");
std::fs::write(dir.join("skhdrc"), "alt - h : komorebic focus left\n").unwrap();
let shortcuts = dir.join("data/kglobalaccel");
std::fs::create_dir_all(&shortcuts).unwrap();
std::fs::write(
shortcuts.join("komorebi-old.desktop"),
"[Desktop Entry]\nX-Komorebi-Generated=true\n",
)
.unwrap();
let (output, text) = check_keys(&dir);
assert!(output.status.success(), "{text}");
assert!(
text.contains(
"the shortcut files are not the ones `komorebic hotkeys compile` would write"
),
"{text}"
);
assert!(text.contains("1 to remove"), "{text}");
assert!(
text.contains("komorebic hotkeys compile --reload"),
"{text}"
);
let compiled = Command::new(env!("CARGO_BIN_EXE_komorebic"))
.args(["hotkeys", "compile"])
.env("KOMOREBI_CONFIG_HOME", &dir)
.env("XDG_DATA_HOME", dir.join("data"))
.output()
.unwrap();
assert!(compiled.status.success(), "{compiled:?}");
let (_, text) = check_keys(&dir);
assert!(!text.contains("are not the ones"), "{text}");
assert!(
text.contains("the shortcut files are the ones")
|| text.contains("no shortcut files are needed"),
"{text}"
);
let _ = std::fs::remove_dir_all(dir);
}