mod support;
use std::fs;
use std::os::unix::fs as unix_fs;
use std::process::Command as StdCommand;
use predicates::prelude::*;
use support::{lx, lx_no_colour};
use tempfile::tempdir;
fn lx_with_config(config_content: &str) -> (tempfile::TempDir, assert_cmd::Command) {
let dir = tempdir().expect("failed to create tempdir");
let config_path = dir.path().join("config.toml");
let content = if config_content.contains("version") {
config_content.to_string()
} else {
format!("version = \"0.3\"\n{config_content}")
};
fs::write(&config_path, content).unwrap();
let mut cmd = lx_no_colour();
cmd.env("LX_CONFIG", config_path);
(dir, cmd)
}
#[test]
fn config_lx_personality_group_dirs() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.lx]
group-dirs = "first"
"#);
let work = tempdir().expect("failed to create workdir");
fs::write(work.path().join("aaa_file.txt"), "").unwrap();
fs::create_dir(work.path().join("zzz_dir")).unwrap();
cmd.args(["-1"])
.arg(work.path())
.assert()
.success()
.stdout(predicate::function(|output: &str| {
let dir_pos = output.find("zzz_dir").unwrap();
let file_pos = output.find("aaa_file.txt").unwrap();
dir_pos < file_pos
}));
}
#[test]
fn config_lx_overridden_by_cli() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.lx]
group-dirs = "first"
"#);
let work = tempdir().expect("failed to create workdir");
fs::write(work.path().join("aaa_file.txt"), "").unwrap();
fs::create_dir(work.path().join("zzz_dir")).unwrap();
cmd.args(["-1", "--group-dirs=last"])
.arg(work.path())
.assert()
.success()
.stdout(predicate::function(|output: &str| {
let file_pos = output.find("aaa_file.txt").unwrap();
let dir_pos = output.find("zzz_dir").unwrap();
file_pos < dir_pos
}));
}
#[test]
fn config_lx_personality_time_style() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.lx]
time-style = "long-iso"
"#);
cmd.args(["-l", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::is_match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}").unwrap());
}
#[test]
fn config_custom_format() {
let (_dir, mut cmd) = lx_with_config(r#"
[format]
tiny = ["size", "modified"]
"#);
cmd.args(["--format=tiny", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Cargo.toml"))
.stdout(predicate::str::contains(".rw").not());
}
#[test]
fn config_format_overrides_compiled_in() {
let (_dir, mut cmd) = lx_with_config(r#"
[format]
long = ["size", "modified"]
"#);
cmd.args(["-l", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains(".rw").not());
}
#[test]
fn config_custom_personality() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.myview]
columns = ["perms", "size"]
group-dirs = "first"
"#);
let work = tempdir().expect("failed to create workdir");
fs::write(work.path().join("file.txt"), "").unwrap();
fs::create_dir(work.path().join("subdir")).unwrap();
cmd.args(["-pmyview"])
.arg(work.path())
.assert()
.success()
.stdout(predicate::function(|output: &str| {
let dir_pos = output.find("subdir").unwrap();
let file_pos = output.find("file.txt").unwrap();
dir_pos < file_pos
}));
}
#[test]
fn inherit_single_level() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.base]
group-dirs = "first"
[personality.child]
inherits = "base"
format = "long"
"#);
let work = tempdir().expect("failed to create workdir");
fs::write(work.path().join("aaa_file.txt"), "").unwrap();
fs::create_dir(work.path().join("zzz_dir")).unwrap();
cmd.args(["-pchild"])
.arg(work.path())
.assert()
.success()
.stdout(predicate::function(|output: &str| {
let dir_pos = output.find("zzz_dir").unwrap();
let file_pos = output.find("aaa_file.txt").unwrap();
dir_pos < file_pos
}));
}
#[test]
fn inherit_multi_level() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.root]
group-dirs = "first"
[personality.mid]
inherits = "root"
format = "long"
[personality.leaf]
inherits = "mid"
header = true
"#);
cmd.args(["-pleaf", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Permissions"));
}
#[test]
fn inherit_child_overrides_parent_setting() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.parent]
format = "long"
sort = "name"
[personality.child]
inherits = "parent"
sort = "size"
"#);
cmd.args(["-pchild", "Cargo.toml"])
.assert()
.success();
}
#[test]
fn inherit_child_overrides_format() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.parent]
format = "long2"
[personality.child]
inherits = "parent"
format = "long"
"#);
cmd.args(["-pchild", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("staff").not());
}
#[test]
fn inherit_cycle_detected() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.a]
inherits = "b"
[personality.b]
inherits = "a"
"#);
cmd.args(["-pa", "Cargo.toml"])
.assert()
.failure()
.stderr(predicate::str::contains("inheritance cycle"));
}
#[test]
fn inherit_from_compiled_in() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.myll]
inherits = "ll"
header = true
"#);
cmd.args(["-pmyll", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Permissions"))
.stdout(predicate::str::contains(support::current_group()));
}
#[test]
fn standalone_no_inherits() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.base]
group-dirs = "first"
header = true
[personality.standalone]
format = "long"
"#);
cmd.args(["-pstandalone", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Permissions").not());
}
#[test]
fn config_personality_bool_setting() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.hdr]
format = "long"
header = true
"#);
cmd.args(["-phdr", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Permissions"));
}
#[test]
fn config_personality_columns_as_string() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.tiny]
columns = "size,modified"
"#);
cmd.args(["-ptiny", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Cargo.toml"))
.stdout(predicate::str::contains(".rw").not());
}
#[test]
fn config_unknown_setting_warns() {
let (_dir, mut cmd) = lx_with_config(r#"
[personality.bad]
format = "long"
frobnicate = true
"#);
cmd.args(["-pbad", "Cargo.toml"])
.assert()
.success()
.stderr(predicate::str::contains("unknown setting 'frobnicate'"));
}
#[test]
fn personality_ll() {
lx_no_colour()
.args(["-pll", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains(support::current_group()));
}
#[test]
fn personality_lll_has_header() {
lx_no_colour()
.args(["-plll", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("Permissions"));
}
#[test]
fn personality_lll_has_long_iso() {
lx_no_colour()
.args(["-plll", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::is_match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}").unwrap());
}
#[test]
fn personality_tree() {
lx_no_colour()
.args(["-ptree", "-L1", "src"])
.assert()
.success()
.stdout(predicate::str::contains("main.rs"))
.stdout(predicate::str::contains("├──").or(predicate::str::contains("└──")));
}
#[test]
fn personality_cli_override() {
lx_no_colour()
.args(["-pll", "--no-group", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains("staff").not());
}
#[test]
fn personality_long_flag() {
lx_no_colour()
.args(["--personality=ll", "Cargo.toml"])
.assert()
.success()
.stdout(predicate::str::contains(support::current_group()));
}
#[test]
fn argv0_ll_dispatch() {
let dir = tempdir().expect("failed to create tempdir");
let lx_path = assert_cmd::cargo::cargo_bin("lx");
let link_path = dir.path().join("ll");
unix_fs::symlink(&lx_path, &link_path).unwrap();
let output = StdCommand::new(&link_path)
.args(["--colour=never", "Cargo.toml"])
.output()
.expect("failed to run symlink");
let stdout = String::from_utf8_lossy(&output.stdout);
let group = support::current_group();
assert!(
stdout.contains(&group),
"argv[0]=ll should show group column ({group}), got: {stdout}"
);
}
#[test]
fn argv0_unknown_falls_back() {
let dir = tempdir().expect("failed to create tempdir");
let lx_path = assert_cmd::cargo::cargo_bin("lx");
let link_path = dir.path().join("unknown_name");
unix_fs::symlink(&lx_path, &link_path).unwrap();
let output = StdCommand::new(&link_path)
.args(["--colour=never", "Cargo.toml"])
.output()
.expect("failed to run symlink");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Cargo.toml"));
}
#[test]
fn init_config_creates_file() {
let dir = tempdir().expect("failed to create tempdir");
let config_path = dir.path().join(".lxconfig.toml");
lx()
.args(["--init-config"])
.env("HOME", dir.path())
.assert()
.success()
.stderr(predicate::str::contains("Wrote default config"));
assert!(config_path.exists());
let contents = fs::read_to_string(&config_path).unwrap();
assert!(contents.contains("## lx configuration file"));
assert!(contents.contains("version = \"0.3\""));
assert!(contents.contains("[personality.default]"));
assert!(contents.contains("[personality.lx]"));
assert!(contents.contains("inherits"));
}
#[test]
fn init_config_refuses_overwrite() {
let dir = tempdir().expect("failed to create tempdir");
let config_path = dir.path().join(".lxconfig.toml");
fs::write(&config_path, "existing").unwrap();
lx()
.args(["--init-config"])
.env("HOME", dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("already exists"));
}
#[test]
fn lx_config_env_takes_priority() {
let dir = tempdir().expect("failed to create tempdir");
let config_path = dir.path().join("custom.toml");
fs::write(&config_path, r#"
version = "0.3"
[personality.lx]
group-dirs = "first"
"#).unwrap();
let work = tempdir().expect("failed to create workdir");
fs::write(work.path().join("aaa.txt"), "").unwrap();
fs::create_dir(work.path().join("zzz")).unwrap();
lx_no_colour()
.args(["-1"])
.arg(work.path())
.env("LX_CONFIG", &config_path)
.assert()
.success()
.stdout(predicate::function(|output: &str| {
let dir_pos = output.find("zzz").unwrap();
let file_pos = output.find("aaa.txt").unwrap();
dir_pos < file_pos
}));
}
#[test]
fn no_config_file_is_fine() {
let dir = tempdir().expect("failed to create tempdir");
lx_no_colour()
.args(["-1", "Cargo.toml"])
.env("LX_CONFIG", dir.path().join("nonexistent.toml"))
.env("HOME", dir.path())
.env_remove("XDG_CONFIG_HOME")
.assert()
.success()
.stdout(predicate::str::contains("Cargo.toml"));
}
#[test]
fn dump_class_all() {
lx_no_colour()
.arg("--dump-class")
.assert()
.success()
.stdout(predicate::str::contains("[class]"))
.stdout(predicate::str::contains("temp = ["))
.stdout(predicate::str::contains("image = ["));
}
#[test]
fn dump_class_single() {
lx_no_colour()
.arg("--dump-class=temp")
.assert()
.success()
.stdout(predicate::str::contains("[class]"))
.stdout(predicate::str::contains("temp = ["))
.stdout(predicate::str::contains("*.tmp"));
}
#[test]
fn dump_class_unknown() {
lx_no_colour()
.arg("--dump-class=bogus")
.assert()
.failure()
.code(3)
.stderr(predicate::str::contains("unknown class"))
.stderr(predicate::str::contains("Known classes:"));
}
#[test]
fn dump_format_all() {
lx_no_colour()
.arg("--dump-format")
.assert()
.success()
.stdout(predicate::str::contains("[format]"))
.stdout(predicate::str::contains("long = ["))
.stdout(predicate::str::contains("long3 = ["));
}
#[test]
fn dump_format_single() {
lx_no_colour()
.arg("--dump-format=long2")
.assert()
.success()
.stdout(predicate::str::contains("[format]"))
.stdout(predicate::str::contains("long2 = ["))
.stdout(predicate::str::contains("\"perms\""));
}
#[test]
fn dump_format_unknown() {
lx_no_colour()
.arg("--dump-format=bogus")
.assert()
.failure()
.code(3)
.stderr(predicate::str::contains("unknown format"))
.stderr(predicate::str::contains("Known formats:"));
}
#[test]
fn dump_personality_all() {
lx_no_colour()
.arg("--dump-personality")
.assert()
.success()
.stdout(predicate::str::contains("[personality.ll]"))
.stdout(predicate::str::contains("[personality.tree]"));
}
#[test]
fn dump_personality_single() {
lx_no_colour()
.arg("--dump-personality=ll")
.assert()
.success()
.stdout(predicate::str::contains("[personality.ll]"))
.stdout(predicate::str::contains("inherits = \"lx\""))
.stdout(predicate::str::contains("format = \"long2\""));
}
#[test]
fn dump_personality_unknown() {
lx_no_colour()
.arg("--dump-personality=bogus")
.assert()
.failure()
.code(3)
.stderr(predicate::str::contains("unknown personality"))
.stderr(predicate::str::contains("Known personalities:"));
}
#[test]
fn dump_theme_exa() {
lx_no_colour()
.arg("--dump-theme=exa")
.assert()
.success()
.stdout(predicate::str::contains("compiled-in"))
.stdout(predicate::str::contains("inherits = \"exa\""));
}
#[test]
fn dump_theme_unknown() {
lx_no_colour()
.arg("--dump-theme=bogus")
.assert()
.failure()
.code(3)
.stderr(predicate::str::contains("unknown theme"));
}
#[test]
fn dump_style_exa() {
lx_no_colour()
.arg("--dump-style=exa")
.assert()
.success()
.stdout(predicate::str::contains("[style.exa]"))
.stdout(predicate::str::contains("class.image"))
.stdout(predicate::str::contains("class.temp"));
}
#[test]
fn dump_style_unknown() {
lx_no_colour()
.arg("--dump-style=bogus")
.assert()
.failure()
.code(3)
.stderr(predicate::str::contains("unknown style"));
}
#[test]
fn dump_class_shows_config_override() {
let (_dir, mut cmd) = lx_with_config(r#"
[class]
temp = ["*.tmp", "*.bak"]
"#);
cmd.arg("--dump-class=temp")
.assert()
.success()
.stdout(predicate::str::contains("\"*.tmp\", \"*.bak\""))
.stdout(predicate::str::contains("*.swp").not());
}