Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 101 additions & 10 deletions src/ui/main_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,7 @@ impl MainWindow {
Ok(settings_dir) => {
let fields_file = settings_dir.join(SETTINGS_FILE);
match fs::read(fields_file) {
Ok(content) => match serde_json::from_slice::<Self>(&content) {
Ok(obj) => {
self.tab_daplink = obj.tab_daplink;
self.tab_ws = obj.tab_ws;
println!("Settings loaded !");
}
Err(e) => {
eprintln!("Failed to deserialize settings. Error: {e}");
}
},
Ok(content) => self.load_settings(&content),
Err(e) => eprintln!("Failed to open settings file ({e})"),
}
}
Expand Down Expand Up @@ -112,4 +103,104 @@ impl MainWindow {
pub fn application_subscription(&self) -> Subscription<Message> {
event::listen().map(Message::ApplicationEvent)
}

/// Deserialize the on-disk settings, with a fallback to the legacy 0.1.x
/// schema where `TabDaplink` fields were serialized directly at the top
/// level and are now mapped onto `tab_daplink`. Successful migrations get
/// rewritten in the new shape on the next `CloseRequested`.
fn load_settings(&mut self, content: &[u8]) {
match serde_json::from_slice::<Self>(content) {
Ok(obj) => {
self.tab_daplink = obj.tab_daplink;
self.tab_ws = obj.tab_ws;
println!("Settings loaded !");
}
Err(new_err) => match serde_json::from_slice::<TabDaplink>(content) {
Ok(legacy) => {
self.tab_daplink = legacy;
println!("Legacy settings migrated to the new schema");
}
Err(legacy_err) => {
eprintln!(
"Failed to deserialize settings. New schema error: {new_err}. Legacy schema error: {legacy_err}"
);
}
},
}
}
}

#[cfg(test)]
mod tests {
use super::*;

const LEGACY_JSON: &[u8] = br#"{
"bootloader_path": "/tmp/bl.bin",
"firmware_path": "/tmp/fw.bin",
"user_file_path": "/tmp/user.bin",
"target_waiting_time": 7,
"target_name": "LEGACY-STEAMI"
}"#;

const NEW_JSON: &[u8] = br#"{
"tab_daplink": {
"bootloader_path": "/tmp/bl.bin",
"firmware_path": "/tmp/fw.bin",
"user_file_path": "/tmp/user.bin",
"target_waiting_time": 7,
"target_name": "LEGACY-STEAMI"
},
"tab_ws": {
"fw_selected": "BleHciExt"
}
}"#;

#[test]
fn loads_new_schema() {
let mut w = MainWindow::default();
w.load_settings(NEW_JSON);
let s = serde_json::to_string(&w).unwrap();
assert!(s.contains(r#""target_name":"LEGACY-STEAMI""#));
assert!(s.contains(r#""fw_selected":"BleHciExt""#));
}

#[test]
fn legacy_schema_migrates_and_serializes_in_new_shape() {
let mut w = MainWindow::default();
w.load_settings(LEGACY_JSON);

// Re-serialize the way the CloseRequested handler does.
let s = serde_json::to_string(&w).unwrap();

// New shape: top-level keys for both tabs.
assert!(s.contains(r#""tab_daplink""#), "missing tab_daplink in: {s}");
assert!(s.contains(r#""tab_ws""#), "missing tab_ws in: {s}");

// Legacy data preserved under tab_daplink.
assert!(s.contains(r#""bootloader_path":"/tmp/bl.bin""#));
assert!(s.contains(r#""firmware_path":"/tmp/fw.bin""#));
assert!(s.contains(r#""user_file_path":"/tmp/user.bin""#));
assert!(s.contains(r#""target_waiting_time":7"#));
assert!(s.contains(r#""target_name":"LEGACY-STEAMI""#));
}

#[test]
fn malformed_json_keeps_defaults() {
let mut w = MainWindow::default();
w.load_settings(b"{ this is not json");
// No assertion on stderr — load_settings reports both errors via eprintln,
// exercised manually. The state must remain at defaults: serializing
// shouldn't carry any of the malformed input.
let s = serde_json::to_string(&w).unwrap();
assert!(!s.contains("LEGACY-STEAMI"));
}

#[test]
fn empty_content_keeps_defaults() {
let mut w = MainWindow::default();
w.load_settings(b"");
let s = serde_json::to_string(&w).unwrap();
assert!(s.contains(r#""tab_daplink""#));
assert!(s.contains(r#""tab_ws""#));
}
}
Loading