Files
galaxy/crates/settings/src/mod_tests.rs
T

636 lines
23 KiB
Rust

use galaxyui::SingletonEntity;
use crate::manager::SettingsManager;
use crate::{Setting, SupportedPlatforms, SyncToCloud};
use crate::*;
define_settings_group!(TestSettings, settings: [
never_sync_setting: SimpleSetting {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.simple_setting",
},
global_setting: GlobalSetting {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "test.global_setting",
},
global_setting_no_respect: GlobalSettingNoRespect {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No),
private: false,
toml_path: "test.global_setting_no_respect",
},
per_platform_setting: PerPlatformSetting {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::MAC,
sync_to_cloud: SyncToCloud::PerPlatform(RespectUserSyncSetting::Yes),
private: false,
toml_path: "test.per_platform_setting",
},
per_platform_setting_no_respect: PerPlatformSettingNoRespect {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::MAC,
sync_to_cloud: SyncToCloud::PerPlatform(RespectUserSyncSetting::No),
private: false,
toml_path: "test.per_platform_setting_no_respect",
},
]);
pub fn init_and_register_user_preferences(ctx: &mut AppContext) {
ctx.add_singleton_model(move |_| {
crate::PublicPreferences::new(Box::<
galaxyui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
ctx.add_singleton_model(move |_| {
crate::PrivatePreferences::new(Box::<
galaxyui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
}
#[test]
fn test_is_setting_syncable_on_current_platform() {
galaxyui::App::test((), |mut app| async move {
app.update(init_and_register_user_preferences);
app.add_singleton_model(|_| SettingsManager::default());
// Register our TestSettings settings group with the app.
TestSettings::register(&mut app);
app.read(|app| {
let settings = TestSettings::as_ref(app);
assert!(
!settings
.never_sync_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.never_sync_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
settings
.global_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.global_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
settings
.global_setting_no_respect
.is_setting_syncable_on_current_platform(true)
);
assert!(
settings
.global_setting_no_respect
.is_setting_syncable_on_current_platform(false)
);
if cfg!(target_os = "macos") {
assert!(
settings
.per_platform_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.per_platform_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(true)
);
assert!(
settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(false)
);
} else {
assert!(
!settings
.per_platform_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.per_platform_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
!settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(false)
);
}
});
});
}
mod reload_all_public_settings_tests {
use galaxyui::SingletonEntity;
use crate::manager::SettingsManager;
use crate::{Setting, SupportedPlatforms, SyncToCloud};
use crate::*;
define_settings_group!(ReloadTestSettings, settings: [
public_flag: PublicFlag {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.public_flag",
},
private_flag: PrivateFlag {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: true,
},
]);
fn init_prefs(ctx: &mut AppContext) {
ctx.add_singleton_model(move |_| -> crate::PublicPreferences {
crate::PublicPreferences::new(Box::<
galaxyui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
ctx.add_singleton_model(move |_| -> crate::PrivatePreferences {
crate::PrivatePreferences(Box::<
galaxyui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
}
/// Verifies that `reload_all_public_settings` picks up values present
/// in the preferences backend.
#[test]
fn test_loads_present_keys() {
galaxyui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write a non-default value directly to the public backend.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "true".to_string())
.unwrap();
});
// Reload — the in-memory value should update.
app.update(|ctx| {
SettingsManager::handle(ctx).update(ctx, |manager, ctx| {
manager.reload_all_public_settings(ctx);
});
});
app.read(|ctx| {
assert!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
"reload should load present key from preferences"
);
});
});
}
/// Verifies that absent keys are reset to their default values during
/// reload (the key-deletion scenario).
#[test]
fn test_resets_absent_keys_to_defaults() {
galaxyui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Set a non-default value via set_value (updates both in-memory and storage).
app.update(|ctx| {
ReloadTestSettings::handle(ctx).update(ctx, |settings, ctx| {
settings.public_flag.set_value(true, ctx).unwrap();
});
});
// Remove the key from storage (simulates the user deleting a key from
// the settings file, then reload_from_disk picking up the deletion).
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public.remove_value("public_flag").unwrap();
});
// Reload — the in-memory value should reset to default.
app.update(|ctx| {
SettingsManager::handle(ctx).update(ctx, |manager, ctx| {
manager.reload_all_public_settings(ctx);
});
});
app.read(|ctx| {
assert_eq!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
PublicFlag::default_value(),
"absent key should be reset to default"
);
});
});
}
/// Verifies that reload does NOT write absent keys back to storage.
/// This is the property that prevents the infinite watcher loop.
#[test]
fn test_absent_keys_are_not_written_back() {
galaxyui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Set a non-default value, then remove from storage.
app.update(|ctx| {
ReloadTestSettings::handle(ctx).update(ctx, |settings, ctx| {
settings.public_flag.set_value(true, ctx).unwrap();
});
});
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public.remove_value("public_flag").unwrap();
});
// Reload.
app.update(|ctx| {
SettingsManager::handle(ctx).update(ctx, |manager, ctx| {
manager.reload_all_public_settings(ctx);
});
});
// Storage should still be empty — reload must not write back.
app.read(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
let stored = public.read_value("public_flag").unwrap();
assert!(
stored.is_none(),
"reload should not write absent keys back to storage"
);
});
});
}
/// Verifies that `reload_all_public_settings` returns the storage keys
/// of settings that fail to deserialize (invalid value in file).
#[test]
fn test_reload_returns_failed_keys_for_invalid_values() {
galaxyui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write an invalid value for the public bool setting.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "not_a_bool".to_string())
.unwrap();
});
// Reload — should return the failed key.
let failed_keys = app.update(|ctx| {
SettingsManager::handle(ctx)
.update(ctx, |manager, ctx| manager.reload_all_public_settings(ctx))
});
assert_eq!(
failed_keys,
vec!["public_flag".to_string()],
"reload should return the key that failed to deserialize"
);
// The setting should remain at its default (not crash).
app.read(|ctx| {
assert_eq!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
PublicFlag::default_value(),
"setting should remain at default after failed reload"
);
});
});
}
/// Verifies that `reload_all_public_settings` returns an empty vec
/// when all values are valid.
#[test]
fn test_reload_returns_empty_vec_on_success() {
galaxyui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write a valid value.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "true".to_string())
.unwrap();
});
let failed_keys = app.update(|ctx| {
SettingsManager::handle(ctx)
.update(ctx, |manager, ctx| manager.reload_all_public_settings(ctx))
});
assert!(
failed_keys.is_empty(),
"reload should return empty vec when all values are valid"
);
});
}
/// Verifies that `validate_all_public_settings` detects invalid stored
/// values without modifying in-memory state.
#[test]
fn test_validate_detects_invalid_values() {
galaxyui::App::test((), |mut app| async move {
crate::set_settings_file_enabled(true);
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write an invalid value directly to the public preferences.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "not_valid_json_bool".to_string())
.unwrap();
});
let invalid_keys =
app.read(|ctx| SettingsManager::as_ref(ctx).validate_all_public_settings(ctx));
assert_eq!(
invalid_keys,
vec!["public_flag".to_string()],
"validate should detect the invalid key"
);
// In-memory value should be unchanged (validate is read-only).
app.read(|ctx| {
assert_eq!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
PublicFlag::default_value(),
"validate should not modify in-memory state"
);
});
});
}
/// Verifies that `validate_all_public_settings` returns empty when all
/// stored values are valid.
#[test]
fn test_validate_returns_empty_when_all_valid() {
galaxyui::App::test((), |mut app| async move {
crate::set_settings_file_enabled(true);
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write a valid value.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "true".to_string())
.unwrap();
});
let invalid_keys =
app.read(|ctx| SettingsManager::as_ref(ctx).validate_all_public_settings(ctx));
assert!(
invalid_keys.is_empty(),
"validate should return empty when all values are valid"
);
});
}
}
mod write_to_preferences_tests {
use crate::*;
#[derive(
Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct StructWithOptionals {
required_field: String,
optional_field: Option<String>,
nested: NestedStruct,
}
impl SettingsValue for StructWithOptionals {}
impl Default for StructWithOptionals {
fn default() -> Self {
Self {
required_field: "hello".to_string(),
optional_field: None,
nested: NestedStruct::default(),
}
}
}
#[derive(
Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct NestedStruct {
width: u32,
height: u32,
}
impl Default for NestedStruct {
fn default() -> Self {
Self {
width: 100,
height: 50,
}
}
}
define_settings_group!(StructTestSettings, settings: [
struct_setting: StructSetting {
type: StructWithOptionals,
default: StructWithOptionals::default(),
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.struct_setting",
},
]);
/// Verifies that `write_to_preferences` does NOT write back when the stored
/// JSON differs in formatting (missing null fields, different key ordering)
/// but is semantically equal to the new value (in-memory backend).
#[test]
fn test_no_spurious_write_with_format_differences() {
let prefs =
Box::<galaxyui_extras::user_preferences::in_memory::InMemoryPreferences>::default();
// Simulate what a TOML backend produces after a round-trip:
// - null fields (optional_field) are stripped
// - key ordering may differ (nested before required_field)
let stored_json_without_nulls =
r#"{"nested":{"width":100,"height":50},"required_field":"hello"}"#;
prefs
.write_value("StructSetting", stored_json_without_nulls.to_string())
.unwrap();
// The Rust value has optional_field: None, which serde_json serializes as
// `"optional_field":null` with a different key order. The stored JSON
// doesn't have that field at all and has different key ordering.
let value = StructWithOptionals::default();
let canonical_json = serde_json::to_string(&value).unwrap();
assert_ne!(
canonical_json, stored_json_without_nulls,
"precondition: the JSON strings should differ"
);
// write_to_preferences should detect they're semantically equal and NOT write.
let changed = StructSetting::write_to_preferences(&value, prefs.as_ref()).unwrap();
assert!(
!changed,
"write_to_preferences should not report a change for semantically equal values"
);
}
/// Test with HashMap fields (non-deterministic key order) and missing
/// Option fields — reproduces the exact QuakeModeSettings scenario.
#[test]
fn test_no_spurious_write_with_hashmap_and_missing_options() {
use galaxyui_extras::user_preferences::toml_backed::TomlBackedUserPreferences;
use std::collections::HashMap;
#[derive(
Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct QuakeLike {
pub keybinding: Option<String>,
pub active_pin_position: String,
pub sizes: HashMap<String, u32>,
pub pin_screen: Option<u32>,
pub hide_when_unfocused: bool,
}
impl SettingsValue for QuakeLike {}
impl Default for QuakeLike {
fn default() -> Self {
let mut sizes = HashMap::new();
sizes.insert("top".to_string(), 30);
sizes.insert("bottom".to_string(), 30);
Self {
keybinding: None,
active_pin_position: "Top".to_string(),
sizes,
pin_screen: None,
hide_when_unfocused: true,
}
}
}
define_settings_group!(QuakeLikeGroup, settings: [
quake_setting: QuakeLikeSetting {
type: QuakeLike,
default: QuakeLike::default(),
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.quake_like_setting",
},
]);
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone());
let value = QuakeLike::default();
// First write.
let changed = QuakeLikeSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(changed, "first write should report a change");
// Second write of same value should NOT report a change.
let changed_again = QuakeLikeSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(
!changed_again,
"second write of same value should not report a change on TOML backend"
);
}
/// Same test but using the real TOML backend
/// null-stripping and key-reordering happens.
#[test]
fn test_no_spurious_write_with_toml_backend() {
use galaxyui_extras::user_preferences::toml_backed::TomlBackedUserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone());
let value = StructWithOptionals::default();
// First write stores the value. The TOML backend will strip the null
// `optional_field` and may reorder keys.
let changed = StructSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(changed, "first write should report a change");
// Verify the TOML file doesn't contain the null field.
let contents = std::fs::read_to_string(&file_path).unwrap();
assert!(
!contents.contains("optional_field"),
"TOML should strip null fields, but file contains: {contents}"
);
// Second write of the same value should NOT report a change,
// even though the JSON serialization differs from what's stored.
let changed_again = StructSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(
!changed_again,
"second write of same value should not report a change on TOML backend"
);
}
}