LR-BEHAV-BG-TASKS: background Reload names itself in bad-ids errors; a stopped Reload doesn't say the raw still can't be decoded

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Matthew James Sivak
2026-10-18 19:57:32 +02:00
co-authored by Claude Opus 5.5
parent 7b2d7b25ea
commit 5b5cdb646e
2 changed files with 66 additions and 1 deletions
+12 -1
View File
@@ -8,6 +8,7 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use lightcraft_engine::EngineError;
use lightcraft_engine::cmd::reload;
use serde_json::{Value, json};
@@ -25,7 +26,11 @@ pub fn command(app: &mut LightcraftApp, p: &Value) -> Option<Result<Value, Strin
}
let items = match reload::plan(&mut app.session, p) {
Ok(items) => items,
Err(e) => return Some(Err(format!("photo.reload: {e}"))),
// named as `Session::execute` names a shared parameter check's refusal
Err(EngineError::BadParams { cmd, msg }) if cmd.is_empty() => {
return Some(Err(EngineError::BadParams { cmd: "photo.reload".into(), msg }.to_string()));
}
Err(e) => return Some(Err(e.to_string())),
};
if items.is_empty() {
return None;
@@ -38,6 +43,7 @@ pub fn command(app: &mut LightcraftApp, p: &Value) -> Option<Result<Value, Strin
let probe = reload::file_probe(&app.session);
let cancel = Arc::new(AtomicBool::new(false));
let flag = cancel.clone();
let stopped = cancel.clone();
let work = move |task: lightcraft_engine::activity::TaskHandle| {
let total = items.len() as u64;
task.progress(0, total);
@@ -53,6 +59,11 @@ pub fn command(app: &mut LightcraftApp, p: &Value) -> Option<Result<Value, Strin
return app.toast_error(ctx, e.to_string());
}
let cleared = preview_only.iter().filter(|id| app.session.catalog.photo(**id).is_some_and(|ph| ph.preview_only.is_none())).count();
// stopped before the files were read: they weren't tried again, so say nothing about them
// (a raw that did decode before the stop is still reported)
if stopped.load(std::sync::atomic::Ordering::Relaxed) && cleared == 0 {
return;
}
let message = match (count, preview_only.len(), cleared) {
(1, 1, 1) => crate::i18n::tr("This raw is now developed from its raw data").to_string(),
(1, 1, _) => crate::i18n::tr("This raw variant still can't be decoded").to_string(),
+54
View File
@@ -228,3 +228,57 @@ fn a_background_reload_writes_the_sidecar_with_auto_write_on() {
assert_eq!(app.session.catalog.photo(id).unwrap().meta.gps, Some((41.0, 2.0)));
assert!(std::fs::read_to_string(&sidecar).unwrap().contains("GPSLatitude"), "the sidecar was not written");
}
/// Bad `ids` are refused naming the command once, as the engine's own reply does (`wait: true`).
#[test]
fn reload_with_bad_ids_names_the_command() {
let dir = Dir(std::env::temp_dir().join(format!("lc-ui-reload-bad-{}", std::process::id())));
let (mut app, _) = app_with_stale_photo(&dir.0);
let background = app.run("photo.reload", json!({"ids": "x"})).unwrap_err();
let waited = app.run("photo.reload", json!({"ids": "x", "wait": true})).unwrap_err();
assert_eq!(background, waited);
assert!(background.contains("`photo.reload`"), "{background}");
assert!(!background.contains("``") && !background.starts_with("photo.reload:"), "{background}");
}
/// A Reload stopped from the activity stack before its file was read says nothing (the raw wasn't
/// tried again); one stopped after the file was read still says what came of it.
#[test]
fn a_cancelled_reload_reports_only_what_it_did() {
use std::sync::{Arc, Condvar, Mutex};
let dir = Dir(std::env::temp_dir().join(format!("lc-ui-reload-cancel-{}", std::process::id())));
let (mut app, id) = app_with_stale_photo(&dir.0);
let ctx = egui::Context::default();
let native = app.session.media.file_probe.clone().unwrap();
for read in [false, true] {
// (the probe has started, the probe may return)
let gate = Arc::new((Mutex::new((false, false)), Condvar::new()));
let worker_gate = gate.clone();
let native = native.clone();
app.session.media.file_probe = Some(Arc::new(move |path| {
let mut state = worker_gate.0.lock().unwrap();
state.0 = true;
worker_gate.1.notify_all();
let _ = worker_gate.1.wait_timeout_while(state, Duration::from_secs(10), |s| !s.1).unwrap();
// a read the cancel stopped comes back as an error, as a file not reached does
if read { native(path) } else { Err(format!("{path}: cancelled")) }
}));
app.ui.toast = None;
app.run("photo.reload", json!({"ids": [id.0]})).unwrap();
let (state, _) = gate.1.wait_timeout_while(gate.0.lock().unwrap(), Duration::from_secs(5), |s| !s.0).unwrap();
assert!(state.0, "the reload started reading");
drop(state);
let row = app.session.activity.list().into_iter().find(|r| r.kind == "reload").unwrap();
app.session.activity.cancel(row.id).unwrap();
gate.0.lock().unwrap().1 = true;
gate.1.notify_all();
assert!(crate::tasks::wait(&mut app, &ctx, Duration::from_secs(10)));
if read {
assert_eq!(app.session.catalog.photo(id).unwrap().preview_only, None);
assert!(app.ui.toast.as_ref().is_some_and(|(t, _, _)| t == "This raw is now developed from its raw data"), "{:?}", app.ui.toast);
} else {
assert!(app.session.catalog.photo(id).unwrap().preview_only.is_some());
assert!(app.ui.toast.is_none(), "a cancelled reload reported {:?}", app.ui.toast);
}
}
}