use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
use std::fs;
use tempfile::TempDir;
#[allow(deprecated)]
fn cmd() -> Command {
Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap()
}
fn info_json(dir: &std::path::Path) -> Value {
let output = cmd()
.args(["-C", dir.to_str().unwrap(), "info", "--json"])
.output()
.expect("failed to run command");
assert!(
output.status.success(),
"command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).expect("invalid JSON output")
}
#[test]
fn runs_without_config_file() {
let tmp = TempDir::new().unwrap();
let json = info_json(tmp.path());
assert_eq!(
json["config"]["log_level"], "info",
"should use default log level"
);
assert!(
json["config"]["config_file"].is_null(),
"no config file should be reported"
);
}
#[test]
fn discovers_dotfile_config_in_current_dir() {
let tmp = TempDir::new().unwrap();
let config_path = tmp.path().join(".bito-lint.toml");
fs::write(&config_path, r#"log_level = "debug""#).unwrap();
let json = info_json(tmp.path());
assert_eq!(json["config"]["log_level"], "debug");
let reported = json["config"]["config_file"].as_str().unwrap();
assert!(
reported.ends_with(".bito-lint.toml"),
"should report dotfile: {reported}"
);
}
#[test]
fn discovers_regular_config_in_current_dir() {
let tmp = TempDir::new().unwrap();
let config_path = tmp.path().join("bito-lint.toml");
fs::write(&config_path, r#"log_level = "warn""#).unwrap();
let json = info_json(tmp.path());
assert_eq!(json["config"]["log_level"], "warn");
let reported = json["config"]["config_file"].as_str().unwrap();
assert!(
reported.ends_with("bito-lint.toml"),
"should report regular config: {reported}"
);
}
#[test]
fn discovers_config_in_parent_directory() {
let tmp = TempDir::new().unwrap();
let sub_dir = tmp.path().join("nested").join("deep");
fs::create_dir_all(&sub_dir).unwrap();
fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap();
let json = info_json(&sub_dir);
assert_eq!(json["config"]["log_level"], "debug");
assert!(
json["config"]["config_file"].as_str().is_some(),
"should find parent config"
);
}
#[test]
fn regular_name_overrides_dotfile() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap();
fs::write(tmp.path().join("bito-lint.toml"), r#"log_level = "error""#).unwrap();
let json = info_json(tmp.path());
assert_eq!(
json["config"]["log_level"], "error",
"regular file should override dotfile"
);
}
#[test]
fn parses_toml_config() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "warn""#).unwrap();
let json = info_json(tmp.path());
assert_eq!(json["config"]["log_level"], "warn");
}
#[test]
fn parses_yaml_config() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".bito-lint.yaml"), "log_level: warn\n").unwrap();
let json = info_json(tmp.path());
assert_eq!(json["config"]["log_level"], "warn");
}
#[test]
fn parses_yml_config() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".bito-lint.yml"), "log_level: debug\n").unwrap();
let json = info_json(tmp.path());
assert_eq!(json["config"]["log_level"], "debug");
}
#[test]
fn parses_json_config() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join(".bito-lint.json"),
r#"{"log_level": "error"}"#,
)
.unwrap();
let json = info_json(tmp.path());
assert_eq!(json["config"]["log_level"], "error");
}
#[test]
fn closer_config_takes_precedence() {
let tmp = TempDir::new().unwrap();
let sub_dir = tmp.path().join("project");
fs::create_dir_all(&sub_dir).unwrap();
fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "error""#).unwrap();
fs::write(sub_dir.join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap();
let json = info_json(&sub_dir);
assert_eq!(
json["config"]["log_level"], "debug",
"closer config should win"
);
}
#[test]
fn later_extension_overrides_earlier_in_same_directory() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap();
fs::write(tmp.path().join(".bito-lint.yaml"), "log_level: error\n").unwrap();
let json = info_json(tmp.path());
assert_eq!(
json["config"]["log_level"], "error",
"later extension (YAML) should override earlier (TOML) in merge"
);
}
#[test]
fn explicit_config_overrides_discovered() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap();
let explicit = tmp.path().join("override.toml");
fs::write(&explicit, r#"log_level = "error""#).unwrap();
let output = cmd()
.args([
"-C",
tmp.path().to_str().unwrap(),
"--config",
explicit.to_str().unwrap(),
"info",
"--json",
])
.output()
.expect("failed to run command");
assert!(output.status.success());
let json: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
json["config"]["log_level"], "error",
"--config should override discovered config"
);
let reported = json["config"]["config_file"].as_str().unwrap();
assert!(
reported.ends_with("override.toml"),
"--config path should be reported: {reported}"
);
}
#[test]
fn invalid_toml_config_shows_error() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join(".bito-lint.toml"),
"this is not valid toml [[[",
)
.unwrap();
cmd()
.args(["-C", tmp.path().to_str().unwrap(), "info"])
.assert()
.failure()
.stderr(predicate::str::contains("configuration").or(predicate::str::contains("config")));
}
#[test]
fn invalid_yaml_config_shows_error() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join(".bito-lint.yaml"),
"invalid:\n yaml\n content:\n[broken",
)
.unwrap();
cmd()
.args(["-C", tmp.path().to_str().unwrap(), "info"])
.assert()
.failure();
}
#[test]
fn invalid_json_config_shows_error() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".bito-lint.json"), "{not valid json}").unwrap();
cmd()
.args(["-C", tmp.path().to_str().unwrap(), "info"])
.assert()
.failure();
}
#[test]
fn unknown_config_field_is_ignored() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join(".bito-lint.toml"),
"log_level = \"info\"\nunknown_field = \"should be ignored\"\nanother_unknown = 42\n",
)
.unwrap();
let json = info_json(tmp.path());
assert_eq!(json["config"]["log_level"], "info");
}
#[test]
fn git_boundary_stops_config_search() {
let tmp = TempDir::new().unwrap();
let parent = tmp.path().join("parent");
let repo = parent.join("repo");
let src = repo.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(parent.join(".bito-lint.toml"), r#"log_level = "error""#).unwrap();
fs::create_dir(repo.join(".git")).unwrap();
let json = info_json(&src);
assert_eq!(
json["config"]["log_level"], "info",
"should use default — boundary stops search"
);
assert!(
json["config"]["config_file"].is_null(),
"should not find config beyond boundary"
);
}
#[test]
fn config_in_same_dir_as_git_is_found() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
let src = repo.join("src");
fs::create_dir_all(&src).unwrap();
fs::create_dir(repo.join(".git")).unwrap();
fs::write(repo.join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap();
let json = info_json(&src);
assert_eq!(
json["config"]["log_level"], "debug",
"config next to .git should be found"
);
assert!(
json["config"]["config_file"].as_str().is_some(),
"should report config file"
);
}