use super::*;
#[test]
fn canonical_configuration_example_parses() {
let body = include_str!("../../../../../docs/config.example.toml");
let config: Config = toml::from_str(body).expect("documented config must parse");
assert_eq!(config.schema_version, SCHEMA_VERSION);
let bindings = config.stored_bindings("receiver:aabbccdd:slot:1");
let Some(Binding::LongPress(long_press)) = bindings.get(&ButtonId::DpiToggle) else {
panic!("documented long-press binding should keep its shape");
};
assert_eq!(long_press.short(), &Action::ShowDesktop);
assert_eq!(long_press.long(), &Action::MissionControl);
}
#[test]
fn first_save_preserves_the_previous_config_for_recovery() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
let backup = dir.path().join("config.toml.backup.1");
let original = b"schema_version = 3\nselected_device = \"original\"\n";
fs::write(&path, original).expect("write original config");
let mut config = Config {
selected_device: Some("replacement".to_string()),
..Config::default()
};
config.save_to_path(&path).expect("save replacement");
assert_eq!(fs::read(&backup).expect("read backup"), original);
config.selected_device = Some("second-save".to_string());
config.save_to_path(&path).expect("save again");
assert_eq!(
fs::read(&backup).expect("read original backup"),
original,
"later saves in one process must not replace the recovery copy"
);
}
#[test]
fn migrated_load_backs_up_the_pre_migration_source_exactly_once() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
let backup = dir.path().join("config.toml.v4.bak");
let original =
b"schema_version = 4\n\n[devices.\"direct:046d:c08d:unit:6be9d300\"]\ninvert_scroll = true\n";
fs::write(&path, original).expect("write v4 config");
let (config, mut file) = ConfigFile::load_from_path(&path).expect("load v4 config");
assert!(
config.devices.contains_key("unit:6be9d300"),
"sanity: the key migration ran"
);
file.save(&config).expect("save migrated config");
assert_eq!(
fs::read(&backup).expect("read migration backup"),
original,
"the backup preserves the pre-migration source, not the rewritten output"
);
let sentinel = b"sentinel: a second save must not touch this file";
fs::write(&backup, sentinel).expect("overwrite backup with sentinel");
let mut second = config.clone();
second.selected_device = Some("unit:6be9d300".to_string());
file.save(&second)
.expect("save again from the same ConfigFile");
assert_eq!(
fs::read(&backup).expect("read backup after second save"),
sentinel,
"a second save from the same ConfigFile must not rewrite the backup"
);
}
#[test]
fn config_backups_rotate_between_generations() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
fs::write(&path, b"first").expect("write first generation");
super::backup_existing_config(&path).expect("back up first generation");
fs::write(&path, b"second").expect("write second generation");
super::backup_existing_config(&path).expect("back up second generation");
assert_eq!(
fs::read(super::config_backup_path(&path, 1).expect("backup path"))
.expect("read newest backup"),
b"second"
);
assert_eq!(
fs::read(super::config_backup_path(&path, 2).expect("backup path"))
.expect("read older backup"),
b"first"
);
}
#[test]
fn missing_file_yields_default() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("nonexistent.toml");
let cfg = Config::load_from_path(&path).expect("load");
assert_eq!(cfg.schema_version, SCHEMA_VERSION);
assert!(cfg.devices.is_empty());
}
#[test]
fn human_readable_toml_layout() {
let mut cfg = Config::default();
cfg.set_binding(
"2b042",
ButtonId::Back,
Binding::Single(Action::BrowserBack),
);
let body = toml::to_string_pretty(&cfg).expect("serialize");
assert!(
body.contains(&format!("schema_version = {SCHEMA_VERSION}")),
"got: {body}"
);
assert!(body.contains("[devices.2b042.bindings]"), "got: {body}");
assert!(body.contains("Back = \"BrowserBack\""), "got: {body}");
}
#[test]
fn selected_device_roundtrips() {
let mut cfg = Config::default();
assert_eq!(cfg.selected_device(), None);
cfg.set_selected_device(Some("2b042".into()));
let parsed = write_and_read(&cfg);
assert_eq!(parsed.selected_device(), Some("2b042"));
}
#[test]
fn cleared_selected_device_omits_field() {
let mut cfg = Config::default();
cfg.set_selected_device(Some("2b042".into()));
cfg.set_selected_device(None);
let body = toml::to_string_pretty(&cfg).expect("serialize");
assert!(
!body.contains("selected_device"),
"cleared selection should not appear: {body}"
);
}
#[test]
fn empty_device_block_is_skipped_in_output() {
let mut cfg = Config::default();
cfg.set_binding("2b042", ButtonId::Back, Binding::Single(Action::Copy));
cfg.devices
.get_mut("2b042")
.expect("entry")
.bindings
.clear();
let body = toml::to_string_pretty(&cfg).expect("serialize");
assert!(
!body.contains("Back"),
"cleared bindings should not appear: {body}"
);
}
#[test]
fn tracked_save_preserves_comments_and_rejects_concurrent_edits() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
fs::write(
&path,
"# keep this comment\nschema_version = 6\nselected_device = \"one\" # and this one\n",
)
.expect("write");
let (mut config, mut file) = ConfigFile::load_from_path(&path).expect("load tracked");
config.set_selected_device(Some("two".into()));
file.save(&config).expect("save tracked");
let saved = fs::read_to_string(&path).expect("read saved");
assert!(saved.contains("# keep this comment"));
assert!(saved.contains("# and this one"));
assert!(saved.contains("selected_device = \"two\""));
let external = format!("{saved}# external editor\n");
fs::write(&path, &external).expect("external edit");
config.set_selected_device(Some("three".into()));
assert_matches!(file.save(&config), Err(ConfigError::Conflict { .. }));
assert_eq!(fs::read_to_string(path).expect("read conflict"), external);
}
#[test]
fn a_failed_backup_write_leaves_the_migration_backup_still_owed() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
let backup = dir.path().join("config.toml.v4.bak");
let original =
b"schema_version = 4\n\n[devices.\"direct:046d:c08d:unit:6be9d300\"]\ninvert_scroll = true\n";
fs::write(&path, original).expect("write v4 config");
fs::create_dir(&backup).expect("occupy the backup path");
let (config, mut file) = ConfigFile::load_from_path(&path).expect("load v4 config");
let error = file.save(&config).expect_err("the backup write must fail");
assert_matches!(error, ConfigError::Write { .. });
assert_eq!(
fs::read(&path).expect("read config"),
original,
"a failed save leaves the v4 file in place"
);
fs::remove_dir(&backup).expect("free the backup path");
file.save(&config).expect("save once the path is writable");
assert_eq!(
fs::read(&backup).expect("read migration backup"),
original,
"the retried save still has the pre-migration source to write"
);
}