Files
Alejandro Londono 6006abb757 feat(settings): add shared config storage for Craft apps
Add shared config.toml storage while keeping settings types, defaults and UI policy in each app.

Includes isolated in-memory operation, configurable portable paths, atomic saves with private backups, damaged-file preservation, schema migrations, and documented best-effort handling of external edits.

Validation: native macOS formatting, strict workspace clippy, 172 workspace tests and the craft-settings WASM check. Independent backup-permission and failed external-migration repros fail before the fixes and pass afterward.

Hosted CI: all seven UI and printing jobs pass across macOS, Linux, Windows and UI WASM.

Refs #12
2026-10-12 15:40:50 -06:00

75 lines
3.2 KiB
Rust

mod common;
use std::time::{Duration, Instant};
use common::{Prefs, Theme, empty_probe, probe_with_env};
use craft_settings::{Location, OpenOptions, SaveStatus, SettingsStore, Source};
/// A probe whose platform folder is `dir`, so any write would land there.
fn probe_in(dir: &std::path::Path, env: Option<(&'static str, &'static str)>) -> craft_settings::Probe {
let mut probe = match env {
Some((name, value)) => probe_with_env(name, value),
None => empty_probe(),
};
probe.platform_config_dir = Some(dir.to_path_buf());
probe
}
fn assert_untouched(dir: &std::path::Path) {
let entries: Vec<_> = std::fs::read_dir(dir).unwrap().collect();
assert!(entries.is_empty(), "nothing may be written: {entries:?}");
}
fn exercise(store: &mut SettingsStore<Prefs>) {
assert!(!store.is_persistent());
assert_eq!(store.dir(), None);
assert_eq!(store.path(), None);
assert!(store.update(|p| p.appearance.theme = Theme::Dark), "updates still apply");
assert_eq!(store.get().appearance.theme, Theme::Dark);
assert!(!store.is_dirty(), "nothing to save");
assert!(matches!(store.tick(Instant::now() + Duration::from_secs(5)).status, SaveStatus::Idle));
store.flush().unwrap();
}
#[test]
fn empty_env_var_reads_and_writes_nothing() {
let tmp = tempfile::tempdir().unwrap();
// A real settings file in the platform folder must not be read either.
std::fs::create_dir(tmp.path().join("testcraft")).unwrap();
std::fs::write(tmp.path().join("testcraft/config.toml"), "schema_version = 2\n[general]\nlanguage = \"de\"\n").unwrap();
let (mut store, report) =
SettingsStore::<Prefs>::open_with(OpenOptions::new(Location::Auto).probe(probe_in(tmp.path(), Some(("TESTCRAFT_CONFIG_DIR", "")))));
assert_eq!(report.dir, None);
assert_eq!(report.source, Source::Defaults);
assert!(report.rule.contains("isolated"), "{}", report.rule);
assert!(!report.has_problems(), "{report:?}");
assert_eq!(store.get().general.language, "en", "the real profile is not read");
exercise(&mut store);
assert_eq!(
std::fs::read_to_string(tmp.path().join("testcraft/config.toml")).unwrap(),
"schema_version = 2\n[general]\nlanguage = \"de\"\n",
"the real profile is not written"
);
assert_eq!(std::fs::read_dir(tmp.path().join("testcraft")).unwrap().count(), 1);
}
#[test]
fn memory_location_reads_and_writes_nothing() {
let tmp = tempfile::tempdir().unwrap();
let (mut store, report) = SettingsStore::<Prefs>::open_with(OpenOptions::new(Location::Memory).probe(probe_in(tmp.path(), None)));
assert_eq!(report.rule, "in memory only");
exercise(&mut store);
assert_untouched(tmp.path());
}
#[test]
fn memory_ignores_legacy_files() {
let tmp = tempfile::tempdir().unwrap();
let old = tmp.path().join("prefs.json");
std::fs::write(&old, "{}").unwrap();
let files = vec![craft_settings::LegacyFile { path: old, parse: |_| Ok(Prefs::default()) }];
let (_, report) = SettingsStore::<Prefs>::open_with(OpenOptions::new(Location::Memory).probe(empty_probe()).legacy(files));
assert_eq!(report.source, Source::Defaults, "an isolated store must not import from the real profile");
}