style: cargo fmt the moved test modules

This commit is contained in:
aiongg
2026-10-26 18:30:36 +00:00
parent d4e44696fb
commit d151df0149
24 changed files with 122 additions and 319 deletions
+1 -2
View File
@@ -47,8 +47,7 @@ fn the_default_filter_shows_lightcraft_info_and_other_crates_warnings() {
/// `LIGHTCRAFT_LOG` keeps working exactly as it did with the old stderr-only logger.
#[test]
fn lightcraft_log_keeps_its_meaning() {
let targets =
["lightcraft", "lightcraft::control_server", "lightcraft_engine::segment", "lightcraft_gpu", "wgpu_core::device", "naga", "eframe"];
let targets = ["lightcraft", "lightcraft::control_server", "lightcraft_engine::segment", "lightcraft_gpu", "wgpu_core::device", "naga", "eframe"];
let levels = [log::Level::Error, log::Level::Warn, log::Level::Info, log::Level::Debug, log::Level::Trace];
for value in ["info", "debug", "warn", "error", "verbose", "INFO"] {
let f = Filter::parse(&filter_spec(Some(value), None));
+1 -2
View File
@@ -413,8 +413,7 @@ fn the_writer_follows_the_window_from_frame_to_frame() {
let mut writer = PrefsWriter::default();
let ctx = egui::Context::default();
let pass = |look: &mut dyn FnMut(&egui::Context)| {
let raw =
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1300.0, 800.0))), ..Default::default() };
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1300.0, 800.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| look(ui.ctx()));
out.textures_delta.clear();
out.viewport_output.get(&egui::ViewportId::ROOT).map(|v| v.repaint_delay)
+8 -11
View File
@@ -136,10 +136,7 @@ fn renaming_a_keyword_carries_its_listing_along() {
let op = c.rename_keyword_ops("Travel", "Places|Europe").unwrap();
let undo = c.apply(op).unwrap();
assert_eq!(kws(&c, ids[0]), ["Places|Europe|italy"]);
assert_eq!(
listed(&c),
[("Places|Europe".to_string(), with_synonyms(&["trip"])), ("Places|Europe|Spain".to_string(), KeywordInfo::default())]
);
assert_eq!(listed(&c), [("Places|Europe".to_string(), with_synonyms(&["trip"])), ("Places|Europe|Spain".to_string(), KeywordInfo::default())]);
c.apply(undo).unwrap();
assert_eq!(c.to_snapshot(), before);
}
@@ -161,11 +158,8 @@ fn merging_keeps_the_targets_attributes_and_gathers_synonyms() {
let (mut c, _) = lib(&[&["holiday"], &["travel"]]);
let person = KeywordInfo { person: true, ..with_synonyms(&["trip"]) };
c.apply(Op::SetKeyword { path: "travel".into(), info: Some(person.clone()) }).unwrap();
c.apply(Op::SetKeyword {
path: "holiday".into(),
info: Some(KeywordInfo { include_on_export: false, ..with_synonyms(&["vacation", "Trip"]) }),
})
.unwrap();
c.apply(Op::SetKeyword { path: "holiday".into(), info: Some(KeywordInfo { include_on_export: false, ..with_synonyms(&["vacation", "Trip"]) }) })
.unwrap();
let op = c.merge_keywords_ops(&["holiday".into()], "travel").unwrap();
c.apply(op).unwrap();
assert_eq!(listed(&c), [("travel".to_string(), KeywordInfo { person: true, ..with_synonyms(&["trip", "vacation"]) })]);
@@ -529,8 +523,11 @@ fn a_photo_supreme_vocabulary_file_reads() {
let paths: Vec<&str> = read.iter().map(|(p, _)| p.as_str()).collect();
assert_eq!(
paths,
["Places", "Portugal", "Portugal|Lisbon", "Portugal|Lisbon|Alfama", "Portugal|Porto", "Spain", "Spain|Seville"]
.map(|p| if p == "Places" { p.to_string() } else { format!("Places|{p}") })
["Places", "Portugal", "Portugal|Lisbon", "Portugal|Lisbon|Alfama", "Portugal|Porto", "Spain", "Spain|Seville"].map(|p| if p == "Places" {
p.to_string()
} else {
format!("Places|{p}")
})
);
assert!(!read[0].1.include_on_export, "the category organizes: it isn't exported");
assert!(read[1..].iter().all(|(_, i)| i.include_on_export));
+8 -16
View File
@@ -22,8 +22,7 @@ fn rs(v: serde_json::Value) -> RuleSet {
#[test]
fn f32_boundary_values_match_gte_lte() {
let cat = Catalog::new();
let matches =
|p: &Photo, field: &str, op: &str, value: f64| rs(json!({"rules": [{"field": field, "op": op, "value": value}]})).matches(p, &cat);
let matches = |p: &Photo, field: &str, op: &str, value: f64| rs(json!({"rules": [{"field": field, "op": op, "value": value}]})).matches(p, &cat);
for v in [1.4f32, 1.8, 2.8, 5.6, 0.95] {
let mut p = photo(1);
p.meta.aperture = Some(v);
@@ -49,9 +48,7 @@ fn fields_ops_and_groups() {
yes(json!({"match": "any", "rules": [{"field": "rating", "op": "is", "value": 1}, {"field": "label", "op": "is", "value": "red"}]}));
no(json!({"match": "none", "rules": [{"field": "label", "op": "is", "value": "red"}]}));
yes(json!({"rules": [{"field": "keywords", "op": "is", "value": "italy"}]}));
yes(
json!({"rules": [{"field": "keywords", "op": "contains", "value": "ROME"}, {"field": "keywords", "op": "notContains", "value": "beach"}]}),
);
yes(json!({"rules": [{"field": "keywords", "op": "contains", "value": "ROME"}, {"field": "keywords", "op": "notContains", "value": "beach"}]}));
no(json!({"rules": [{"field": "keywords", "op": "isEmpty"}]}));
yes(json!({"rules": [{"field": "fileName", "op": "startsWith", "value": "img_"}, {"field": "fileName", "op": "endsWith", "value": ".cr2"}]}));
// file path: the whole string (spaces included), either separator, case-insensitive
@@ -116,9 +113,8 @@ fn problems_and_description() {
fn keyword_count_rules() {
let cat = Catalog::new();
let mut p = photo(1); // travel|italy|rome, food
let m = |p: &Photo, op: &str, value: serde_json::Value| {
rs(json!({"rules": [{"field": "keywordCount", "op": op, "value": value}]})).matches(p, &cat)
};
let m =
|p: &Photo, op: &str, value: serde_json::Value| rs(json!({"rules": [{"field": "keywordCount", "op": op, "value": value}]})).matches(p, &cat);
assert!(m(&p, "gte", json!(2)) && m(&p, "lte", json!(2)) && m(&p, "is", json!(2)));
assert!(!m(&p, "gte", json!(3)) && !m(&p, "lt", json!(2)) && m(&p, "between", json!([1, 2])));
p.meta.keywords.push("Food".into());
@@ -167,9 +163,8 @@ fn people_rules() {
fn shutter_speed_rules() {
let cat = Catalog::new();
let mut p = photo(1);
let m = |p: &Photo, op: &str, value: serde_json::Value| {
rs(json!({"rules": [{"field": "shutterSpeed", "op": op, "value": value}]})).matches(p, &cat)
};
let m =
|p: &Photo, op: &str, value: serde_json::Value| rs(json!({"rules": [{"field": "shutterSpeed", "op": op, "value": value}]})).matches(p, &cat);
assert!(!m(&p, "gte", json!(0)), "no shutter speed recorded");
p.meta.shutter = "1/250".into();
assert!(m(&p, "is", json!("1/250")) && m(&p, "lte", json!("1/60")) && !m(&p, "gte", json!("1/60")));
@@ -279,9 +274,7 @@ fn size_rules_follow_orientation_and_crop() {
assert!(m(&p, "megapixels", "lt", json!(20)) && !m(&p, "megapixels", "gte", json!(24)));
assert!(m(&p, "aspect", "isNot", json!("portrait")));
let empty = Photo::new(PhotoId(9), Source::Demo { scene: 0 }, "x.jpg", "JPEG", 0, 0, "2026-09-20T10:00:00");
assert!(
!m(&empty, "aspect", "is", json!("landscape")) && !m(&empty, "aspect", "is", json!("square")) && m(&empty, "longEdge", "is", json!(0))
);
assert!(!m(&empty, "aspect", "is", json!("landscape")) && !m(&empty, "aspect", "is", json!("square")) && m(&empty, "longEdge", "is", json!(0)));
assert!(m(&empty, "megapixels", "is", json!(0)));
}
@@ -358,8 +351,7 @@ fn yes_no_values() {
let p = photo(1); // unedited
assert!(rs(json!({"rules": [{"field": "edited", "op": "is", "value": "false"}]})).matches(&p, &cat), "\"false\" means no");
assert!(
!rs(json!({"rules": [{"field": "edited", "op": "is", "value": "false"}, {"field": "hasGps", "op": "is", "value": "yes"}]}))
.matches(&p, &cat)
!rs(json!({"rules": [{"field": "edited", "op": "is", "value": "false"}, {"field": "hasGps", "op": "is", "value": "yes"}]})).matches(&p, &cat)
);
assert!(!rs(json!({"rules": [{"field": "edited", "op": "is", "value": "maybe"}]})).matches(&p, &cat), "an unreadable value matches nothing");
let r = rs(json!({"rules": [{"field": "edited", "op": "is", "value": true}, {"field": "cropped", "op": "is", "value": "false"}]}));
+6 -23
View File
@@ -102,8 +102,7 @@ fn camera_toe_requires_lower_independent_dark_error() {
.collect(),
)
.unwrap();
let held: Pairs =
sensor.data.iter().zip(&reference.data).take(600).step_by(3).map(|(x, y)| (x.map(f64::from), y.map(f64::from))).collect();
let held: Pairs = sensor.data.iter().zip(&reference.data).take(600).step_by(3).map(|(x, y)| (x.map(f64::from), y.map(f64::from))).collect();
assert_eq!(held.len(), 200);
assert!(held.iter().all(|(_, y)| (0.0003..0.015).contains(&luma(*y))));
assert!(rank_correlation_ties(&held) > 0.99);
@@ -277,11 +276,7 @@ fn hue_table_follows_a_hue_dependent_camera_rendering() {
for (i, (src, dst)) in sensor.data.iter_mut().zip(&mut reference.data).enumerate() {
let ev = 0.05 + (i % 29) as f32 * 0.02;
// mostly greys and mild colours, plus a patch of saturated yellow-green
*src = if i % 7 == 0 {
[ev * 0.75, ev, ev * 0.2]
} else {
[ev * (0.85 + (i % 11) as f32 * 0.03), ev, ev * (0.85 + (i % 13) as f32 * 0.025)]
};
*src = if i % 7 == 0 { [ev * 0.75, ev, ev * 0.2] } else { [ev * (0.85 + (i % 11) as f32 * 0.03), ev, ev * (0.85 + (i % 13) as f32 * 0.025)] };
let (h, s) = hue_saturation(to.apply(src.map(f64::from))).unwrap();
// the camera turns hues in 50°..110° (ProPhoto) by up to +15° and boosts their saturation
let k = (1.0 - ((h - 80.0) / 30.0).powi(2)).max(0.0) * s.min(1.0);
@@ -1465,12 +1460,7 @@ fn corpus_arw_white_balance_in_kelvin_through_the_presets() {
let (_, info) = crate::files::load_bytes(&bytes, 400).unwrap();
assert_eq!(header.as_shot_wb, Some((info.as_shot_temp, info.as_shot_tint)), "{name}");
assert!(info.raw && !info.relative_wb && info.legacy_relative_wb, "{name}: {info:?}");
assert!(
(info.as_shot_temp - kelvin).abs() <= 20.0 && info.as_shot_tint.abs() <= 2.0,
"{name}: {} {}",
info.as_shot_temp,
info.as_shot_tint
);
assert!((info.as_shot_temp - kelvin).abs() <= 20.0 && info.as_shot_tint.abs() <= 2.0, "{name}: {} {}", info.as_shot_temp, info.as_shot_tint);
let camera = format!("SONY {}", lightcraft_meta::extract(&bytes).model.unwrap_or_default());
assert!(!lightcraft_catalog::relative_wb_camera("ARW", &camera), "{camera}");
}
@@ -1622,8 +1612,7 @@ fn clipped_sky_does_not_dull_the_look() {
colourfulness_ratio(&old, &sensor, &reference, &clipped, mid.clone()),
colourfulness_ratio(&old, &sensor, &reference, &clipped, bright.clone()),
);
let new_ratio =
(colourfulness_ratio(&new, &sensor, &reference, &clipped, mid), colourfulness_ratio(&new, &sensor, &reference, &clipped, bright));
let new_ratio = (colourfulness_ratio(&new, &sensor, &reference, &clipped, mid), colourfulness_ratio(&new, &sensor, &reference, &clipped, bright));
// with the clipped sky in the pairs, the bright colours lose much of their colour
assert!(old_ratio.1 < 0.8, "earlier fit: colourfulness vs camera {old_ratio:?}, chroma {:?}", old.tone.chroma());
// without it, the render keeps the camera's colourfulness at every brightness
@@ -1764,12 +1753,7 @@ fn a_near_black_scene_gets_tone_without_learning_colour() {
assert!(look.hue_sat.is_none());
let tone = ToneMap::camera(&look.tone, 0.0, 0.0, 0.0);
let error = |tone: &ToneMap| {
sensor
.data
.iter()
.zip(&reference.data)
.map(|(x, y)| (luma(displayed(x.map(f64::from), tone)) - luma(y.map(f64::from))).powi(2))
.sum::<f64>()
sensor.data.iter().zip(&reference.data).map(|(x, y)| (luma(displayed(x.map(f64::from), tone)) - luma(y.map(f64::from))).powi(2)).sum::<f64>()
};
assert!(error(&tone) < error(&ToneMap::new(0.0, 0.0, 0.0)) * 0.001);
assert_eq!(look.tone.apply(0.0), 0.0);
@@ -1863,8 +1847,7 @@ fn partial_look_brightens_a_dull_scene_without_learning_colour() {
assert_eq!(look.matrix, Mat3::IDENTITY, "no colour matrix from a colourless scene");
let tone = ToneMap::camera(&look.tone, 0.0, 0.0, 0.0);
let neutral = ToneMap::new(0.0, 0.0, 0.0);
let lightness =
|tone: &ToneMap| sensor.data.iter().map(|p| f64::from(tone.apply(luminance_2020(*p)))).sum::<f64>() / sensor.data.len() as f64;
let lightness = |tone: &ToneMap| sensor.data.iter().map(|p| f64::from(tone.apply(luminance_2020(*p)))).sum::<f64>() / sensor.data.len() as f64;
let target = reference.data.iter().map(|p| f64::from(luminance_2020(*p))).sum::<f64>() / reference.data.len() as f64;
assert!((lightness(&tone) / target - 1.0).abs() < 0.05, "{} vs {target}", lightness(&tone));
assert!(lightness(&neutral) < target * 0.6, "the neutral fallback is far darker");
+5 -8
View File
@@ -71,9 +71,8 @@ fn corrections_become_masks() {
fn masks_render() {
let (s, _) = settings();
let img = lightcraft_scenes::demo_library()[0].render(96, 64);
let render = |d: &DevelopSettings| {
lightcraft_pipeline::render(&img, &Default::default(), d, &lightcraft_pipeline::RenderRequest::fit(96, 64)).image.data
};
let render =
|d: &DevelopSettings| lightcraft_pipeline::render(&img, &Default::default(), d, &lightcraft_pipeline::RenderRequest::fit(96, 64)).image.data;
assert_ne!(render(&DevelopSettings { light: s.light, ..Default::default() }), render(&s), "the masks change the picture");
}
@@ -115,8 +114,7 @@ fn lrtemplate_corrections() {
fn presets_add_masks_once() {
let (s, _) = settings();
let partial = json!({"masks": serde_json::to_value(&s.masks).unwrap(), "light": {"exposure": 0.5}});
let p =
lightcraft_develop::Preset { id: "t".into(), name: "t".into(), group: "g".into(), settings: partial, favorite: false, builtin: false };
let p = lightcraft_develop::Preset { id: "t".into(), name: "t".into(), group: "g".into(), settings: partial, favorite: false, builtin: false };
let mut mine = DevelopSettings::default();
mine.masks.push(lightcraft_develop::Mask { id: 7, name: "Mine".into(), ..Default::default() });
let once = p.apply(&mine, 1.0);
@@ -325,8 +323,7 @@ fn spots_render() {
let (partial, _) = crate::crs::to_partial_report(&d.properties, Some(&d.values), crate::crs::Target::Any, 1.5);
let s = DevelopSettings::default().merged(&partial).unwrap();
let img = lightcraft_scenes::demo_library()[0].render(96, 64);
let render = |d: &DevelopSettings| {
lightcraft_pipeline::render(&img, &Default::default(), d, &lightcraft_pipeline::RenderRequest::fit(96, 64)).image.data
};
let render =
|d: &DevelopSettings| lightcraft_pipeline::render(&img, &Default::default(), d, &lightcraft_pipeline::RenderRequest::fit(96, 64)).image.data;
assert_ne!(render(&DevelopSettings { light: s.light, ..Default::default() }), render(&s), "the spots change the picture");
}
+12 -31
View File
@@ -77,9 +77,7 @@ fn hdr_jpeg_size_limit_respected() {
width: img.width,
height: img.height,
space: OutputSpace::Srgb,
samples: DeepSamples::F32(
img.data.iter().flat_map(|p| p[..3].iter().map(|v| lightcraft_color::transfer::decode_srgb8(*v) * 4.0)).collect(),
),
samples: DeepSamples::F32(img.data.iter().flat_map(|p| p[..3].iter().map(|v| lightcraft_color::transfer::decode_srgb8(*v) * 4.0)).collect()),
};
let opts = ExportOptions { hdr: true, quality: 100, limit_kb: Some(u32::MAX), ..Default::default() };
let full = encode_gain_map_jpeg(&img, &hdr, &opts, None).unwrap();
@@ -375,8 +373,7 @@ fn watermark_preview_scales_the_minimum_text_size() {
}
fn vertical_japanese() -> Watermark {
let options =
ExportOptions::from_json(&json!({"watermark": {"text": "日本語", "vertical": true, "size": 0.1, "shadow": false, "opacity": 1.0}}));
let options = ExportOptions::from_json(&json!({"watermark": {"text": "日本語", "vertical": true, "size": 0.1, "shadow": false, "opacity": 1.0}}));
options.watermark.unwrap()
}
@@ -543,14 +540,8 @@ fn graphic_watermark_is_placed_scaled_and_blended() {
std::fs::write(&logo_path, png).unwrap();
let mut img = Rgba8::new(400, 300);
img.data.iter_mut().for_each(|p| *p = [0, 0, 0, 255]);
let wm = Watermark {
image: logo_path.to_string_lossy().into(),
image_width: 0.25,
opacity: 1.0,
inset_x: 0.0,
inset_y: 0.0,
..Default::default()
};
let wm =
Watermark { image: logo_path.to_string_lossy().into(), image_width: 0.25, opacity: 1.0, inset_x: 0.0, inset_y: 0.0, ..Default::default() };
draw_watermark(&mut img, &wm);
// 100 px wide × 50 px high in the bottom-right corner
assert!(img.get(310, 280)[0] > 200, "opaque half: {:?}", img.get(310, 280));
@@ -836,8 +827,7 @@ fn every_space_embeds_its_own_profile_and_round_trips() {
let src = lightcraft_raster::Rgb32f::filled(16, 16, [0.18; 3]);
let base = lightcraft_pipeline::RenderRequest::fit(16, 16);
move |space| {
lightcraft_pipeline::render(&src, &Default::default(), &Default::default(), &lightcraft_pipeline::RenderRequest { space, ..base })
.image
lightcraft_pipeline::render(&src, &Default::default(), &Default::default(), &lightcraft_pipeline::RenderRequest { space, ..base }).image
}
};
let reference =
@@ -915,11 +905,8 @@ fn sixteen_bit_tiff_and_png_have_no_banding() {
assert!(levels > 10 * levels8, "{levels}");
assert_eq!(d.space.named, Some(NamedSpace::DisplayP3));
// sharpening and watermarking work on deep images too
let o = ExportOptions {
sharpen: SharpenFor::Matte,
watermark: Some(Watermark { text: "LC".into(), size: 0.5, ..Default::default() }),
..tiff(None)
};
let o =
ExportOptions { sharpen: SharpenFor::Matte, watermark: Some(Watermark { text: "LC".into(), size: 0.5, ..Default::default() }), ..tiff(None) };
assert!(encode_rendered(&r16, &o, None).is_ok());
}
@@ -1176,10 +1163,7 @@ fn destination_mode_param() {
assert!(e.contains("`destinationMode` must be one of specificFolder|sameAsOriginal"), "{e}");
assert!(ExportOptions::from_params(&json!({"destinationMode": 1})).is_err());
assert!(
ExportOptions::from_params(&json!({"destinationMod": "sameAsOriginal"}))
.unwrap_err()
.to_string()
.contains("did you mean `destinationMode`")
ExportOptions::from_params(&json!({"destinationMod": "sameAsOriginal"})).unwrap_err().to_string().contains("did you mean `destinationMode`")
);
// saved (a preset, `lastExport`) and read back
let o = ExportOptions { destination_mode: DestinationMode::SameAsOriginal, subfolder: "Exports".into(), ..Default::default() };
@@ -1207,8 +1191,7 @@ fn accepted_choices_are_the_ones_exported() {
assert_eq!(read("metadata", &v).unwrap().metadata, want, "metadata: {v}");
}
}
for (name, want) in [("none", SharpenFor::None), ("screen", SharpenFor::Screen), ("matte", SharpenFor::Matte), ("glossy", SharpenFor::Glossy)]
{
for (name, want) in [("none", SharpenFor::None), ("screen", SharpenFor::Screen), ("matte", SharpenFor::Matte), ("glossy", SharpenFor::Glossy)] {
assert_eq!(read("sharpen", &name.to_ascii_uppercase()).unwrap().sharpen, want);
}
for (name, want) in [("low", SharpenAmount::Low), ("standard", SharpenAmount::Standard), ("high", SharpenAmount::High)] {
@@ -1402,9 +1385,8 @@ fn a_panic_in_progress_does_not_hang_the_lanes() {
assert!(calls < 2, "progress panics on its second call");
true
};
let r = crate::guard::catch("test batch", || {
run_batch_with(items, &mut Placer::new(&o, &to, &mut write, &|_| false, true), &mut progress, 2)
});
let r =
crate::guard::catch("test batch", || run_batch_with(items, &mut Placer::new(&o, &to, &mut write, &|_| false, true), &mut progress, 2));
tx.send(r.and_then(|r| r)).ok();
});
let r = rx.recv_timeout(std::time::Duration::from_secs(60));
@@ -1418,8 +1400,7 @@ fn side_by_side_batches_report_failures_in_order() {
use lightcraft_catalog::{Op, Photo, PhotoId, Source};
let mut s = crate::Session::with_demo();
let mut ids: Vec<_> = s.visible().iter().copied().take(3).collect();
let gone =
Photo::new(PhotoId(9_496), Source::File { path: "no/such/dir/gone.jpg".into() }, "gone.jpg", "JPEG", 600, 400, "2026-10-09T00:00:00");
let gone = Photo::new(PhotoId(9_496), Source::File { path: "no/such/dir/gone.jpg".into() }, "gone.jpg", "JPEG", 600, 400, "2026-10-09T00:00:00");
s.catalog.apply(Op::AddPhoto { photo: Box::new(gone) }).unwrap();
ids.insert(1, PhotoId(9_496));
let o = ExportOptions::from_json(&json!({"format": "jpeg", "width": 40}));
+5 -11
View File
@@ -333,11 +333,7 @@ fn preview_loading_plans_the_smallest_covering_jpeg() {
assert!(&window == want, "a {edge} px box takes the {which} JPEG");
let quick = quick_image_srgb(&header, &window, read.adobe_rgb, edge).expect("the window holds a decodable preview");
let whole = embedded_preview_srgb(&file, edge).expect("the whole file decodes too");
assert_eq!(
(quick.width, quick.height, quick.data),
(whole.width, whole.height, whole.data),
"{edge} px: the plan and the whole file agree"
);
assert_eq!((quick.width, quick.height, quick.data), (whole.width, whole.height, whole.data), "{edge} px: the plan and the whole file agree");
}
let _ = std::fs::remove_dir_all(&dir);
}
@@ -994,8 +990,7 @@ fn darkened_ramp(rows: std::ops::Range<usize>, height: usize) -> ([[u8; 4]; 3],
let colour = CameraColourModel { wb, matrix: lightcraft_color::Mat3::IDENTITY, gain: 1.0, tables: None, hue_sat: None };
let w = 1600usize;
let k = |x: usize| 0.8 + 0.55 * x as f32 / (w - 1) as f32;
let sensor =
Rgb32f::from_fn(w, height, |x, y| if rows.contains(&y) { [0.9 * k(x), k(x), 0.8 * k(x)].map(|v| v.min(1.0)) } else { [0.01; 3] });
let sensor = Rgb32f::from_fn(w, height, |x, y| if rows.contains(&y) { [0.9 * k(x), k(x), 0.8 * k(x)].map(|v| v.min(1.0)) } else { [0.01; 3] });
let y = (rows.start + rows.end) / 2;
let first = (0..w).find(|&x| sensor.get(x, y)[1] >= HIGHLIGHT_CLIP).unwrap();
let mut img = sensor.clone();
@@ -1126,10 +1121,9 @@ fn corpus_clipped_sky_beside_a_tree_has_no_coloured_halo() {
(a..b).map(|x| chroma(row[x])).fold([0f64; 2], |s, c| [s[0] + c[0] as f64 / n, s[1] + c[1] as f64 / n])
};
let (n0, n1, f0, f1) = (px(8.0 / 6000.0, w).max(2), px(40.0 / 6000.0, w), (200.0 * k) as usize, (320.0 * k) as usize);
for (side, (n, f)) in
[(mean(left - n1, left - n0), mean(left - f1, left - f0)), (mean(right + n0, right + n1), mean(right + f0, right + f1))]
.into_iter()
.enumerate()
for (side, (n, f)) in [(mean(left - n1, left - n0), mean(left - f1, left - f0)), (mean(right + n0, right + n1), mean(right + f0, right + f1))]
.into_iter()
.enumerate()
{
for c in 0..2 {
near[side][c] += n[c];
+3 -6
View File
@@ -364,8 +364,7 @@ fn deferred_auto_tone_sentinels_never_become_real_slider_values() {
let mut data = sample();
data.photos.truncate(1);
data.photos[0].image.remove("masterImage");
data.photos[0].settings =
"s = { AutoTone = true, Exposure2012 = -999999, Contrast2012 = -999999, Shadows2012 = 65, Temperature = 6150 }".into();
data.photos[0].settings = "s = { AutoTone = true, Exposure2012 = -999999, Contrast2012 = -999999, Shadows2012 = 65, Temperature = 6150 }".into();
data.photos[0].xmp = r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/" crs:Exposure2012="-999999" crs:Contrast2012="-999999" crs:Blacks2012="-22"/></rdf:RDF></x:xmpmeta>"#.into();
let mut s = crate::Session::new();
let report = apply(&mut s, data, false).unwrap();
@@ -376,8 +375,7 @@ fn deferred_auto_tone_sentinels_never_become_real_slider_values() {
assert_eq!(p.develop.light.blacks, -22.0);
assert!(report["unmapped"]["2"].as_array().unwrap().iter().any(|v| v.as_str().is_some_and(|v| v.contains("Deferred"))));
let (partial, _) =
mapped_settings("s = { Exposure2012 = -5, Contrast2012 = -100 }", crate::crs::Target::RawAbsolute, 1.0, Orientation::Normal, 1.5)
.unwrap();
mapped_settings("s = { Exposure2012 = -5, Contrast2012 = -100 }", crate::crs::Target::RawAbsolute, 1.0, Orientation::Normal, 1.5).unwrap();
assert_eq!(partial["light"]["exposure"], -5.0);
assert_eq!(partial["light"]["contrast"], -100.0);
}
@@ -804,8 +802,7 @@ fn oversized_optional_table_warns_and_is_skipped() {
page[8..10].copy_from_slice(&u16::try_from(start).unwrap_or(0).to_be_bytes());
page[5..7].copy_from_slice(&u16::try_from(start).unwrap_or(0).to_be_bytes());
let db = Database::open_with_wal(main, &[]).unwrap();
let reader =
Reader { db, tables: vec![LiveTable { name: "optional_history".into(), rootpage: 2, column_names: Some(vec!["payload".into()]) }] };
let reader = Reader { db, tables: vec![LiveTable { name: "optional_history".into(), rootpage: 2, column_names: Some(vec!["payload".into()]) }] };
let mut warnings = Vec::new();
let rows = reader.optional_table("optional_history", &mut warnings).unwrap();
assert!(rows.is_empty());
+1 -4
View File
@@ -249,10 +249,7 @@ fn synthetic_btree_substitutes_rowid_and_reads_schema() {
malformed[92..96].copy_from_slice(&1u32.to_be_bytes());
malformed[28..32].copy_from_slice(&3u32.to_be_bytes());
let oversized = Scratch::new(&malformed);
assert_eq!(
Database::open_file_with_wal(FileSource::open(&oversized.0).unwrap(), &[]).err().unwrap(),
"SQLite header page count exceeds image"
);
assert_eq!(Database::open_file_with_wal(FileSource::open(&oversized.0).unwrap(), &[]).err().unwrap(), "SQLite header page count exceeds image");
}
#[test]
+1 -4
View File
@@ -156,10 +156,7 @@ fn old_relative_white_balance_on_a_kelvin_camera_keeps_its_look_and_learns_the_a
let old_info = SourceInfo { raw: true, relative_wb: true, as_shot_temp: 6500.0, as_shot_tint: 0.0, ..Default::default() };
let old = lightcraft_pipeline::render(&pixels, &old_info, &d, &request).image.data;
assert_eq!(new, old, "the look of an existing edit changed");
assert_ne!(
new,
lightcraft_pipeline::render(&pixels, &decoded, &lightcraft_develop::DevelopSettings::for_raw(4986.0, -2.0), &request).image.data
);
assert_ne!(new, lightcraft_pipeline::render(&pixels, &decoded, &lightcraft_develop::DevelopSettings::for_raw(4986.0, -2.0), &request).image.data);
// accepting the result records the as-shot white, journaled but not undoable, and the settings are untouched
s.accept(&r);
let p = s.catalog.photo(id).unwrap();
+7 -14
View File
@@ -117,10 +117,8 @@ fn rating_stars_count_matches_the_rating() {
fn text_with_a_shadow_draws_the_shadow_at_its_angle() {
let l = layout(800, 500);
let s = OverlaySettings::default()
.merged(
&json!({"texts": [{"text": "Shadow", "size": 0.08, "color": "#ffffff", "opacity": 1, "anchor": "center", "offsetX": 0, "offsetY": 0,
"shadow": true, "shadowOpacity": 1, "shadowOffset": 0.02, "shadowRadius": 0, "shadowAngle": -90}]}),
)
.merged(&json!({"texts": [{"text": "Shadow", "size": 0.08, "color": "#ffffff", "opacity": 1, "anchor": "center", "offsetX": 0, "offsetY": 0,
"shadow": true, "shadowOpacity": 1, "shadowOffset": 0.02, "shadowRadius": 0, "shadowAngle": -90}]}))
.unwrap();
let o = SlideOverlays::resolve(&s, Some(&photo()));
let b = o.boxes(&l)[0].1;
@@ -165,10 +163,9 @@ fn identity_plate_image_draws_at_its_opacity_and_a_missing_one_is_reported() {
let behind = s.merged(&json!({"identityPlate": {"behind": true, "anchor": "center"}})).unwrap();
let photo_img = Image::filled(30, 20, [0u8, 0, 255, 255]);
let ob = SlideOverlays::resolve(&behind, None);
let (img, l2) =
lightcraft_slideshow::compose_with(&photo_img, None, &lightcraft_slideshow::SlideSettings::default(), 800, 500, &mut |i, l| {
ob.draw_behind(i, l)
});
let (img, l2) = lightcraft_slideshow::compose_with(&photo_img, None, &lightcraft_slideshow::SlideSettings::default(), 800, 500, &mut |i, l| {
ob.draw_behind(i, l)
});
let _ = l2;
assert_eq!(img.get(400, 250), [0, 0, 255, 255], "the photo is on top");
// a missing file: reported, nothing drawn, no panic
@@ -249,14 +246,10 @@ fn hostile_settings_are_refused_or_clamped_and_never_panic() {
wild.texts[0].place.offset_y = f64::INFINITY;
wild.watermark.size = f32::NAN;
let w = wild.sanitized();
assert_eq!(
(w.identity_plate.size, w.texts[0].place.offset_y, w.watermark.size),
(IdentityPlate::default().size, 0.0, Watermark::default().size)
);
assert_eq!((w.identity_plate.size, w.texts[0].place.offset_y, w.watermark.size), (IdentityPlate::default().size, 0.0, Watermark::default().size));
// drawing hostile text at every size: empty, huge, CJK, RTL, control characters
let huge = "W".repeat(50_000);
for text in ["", " ", "\n\n", huge.as_str(), "日本語のテキスト", "שלום עולם", "مرحبا", "a\u{0}b\u{7}c\r\n", "{caption}{"]
{
for text in ["", " ", "\n\n", huge.as_str(), "日本語のテキスト", "שלום עולם", "مرحبا", "a\u{0}b\u{7}c\r\n", "{caption}{"] {
let s = d
.merged(&json!({"identityPlate": {"enabled": true, "text": text, "size": PLATE_SIZE_MAX}, "ratingStars": {"enabled": true}, "addText": {"text": text, "size": TEXT_SIZE_MAX, "shadow": true, "shadowRadius": SHADOW_RADIUS_MAX}, "showWatermark": true, "watermark": text}))
.unwrap();
+2 -5
View File
@@ -191,11 +191,8 @@ fn smooth_scene(w: usize, h: usize) -> Rgb32f {
/// input is clamped into 0..1, the only range a proxy ever stores.
fn lab(c: [f32; 3]) -> [f64; 3] {
let (r, g, b) = (c[0].clamp(0.0, 1.0) as f64, c[1].clamp(0.0, 1.0) as f64, c[2].clamp(0.0, 1.0) as f64);
let (x, y, z) = (
(0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047,
0.2126 * r + 0.7152 * g + 0.0722 * b,
(0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883,
);
let (x, y, z) =
((0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047, 0.2126 * r + 0.7152 * g + 0.0722 * b, (0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883);
let f = |t: f64| if t > 216.0 / 24389.0 { t.cbrt() } else { (841.0 / 108.0) * t + 4.0 / 29.0 };
let (fx, fy, fz) = (f(x), f(y), f(z));
[116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)]
+7 -16
View File
@@ -43,8 +43,7 @@ fn reference_conv(x: &Tensor, wd: &[f32], dims: [usize; 4], bias: &[f32], s: usi
for kx in 0..kw {
let (iy, ix) = ((oy * s + ky) as isize - p as isize, (ox * s + kx) as isize - p as isize);
if iy >= 0 && ix >= 0 && (iy as usize) < h && (ix as usize) < wi {
acc +=
x.data[((g * cin_g + cl) * h + iy as usize) * wi + ix as usize] * wd[((co * cin_g + cl) * kh + ky) * kw + kx];
acc += x.data[((g * cin_g + cl) * h + iy as usize) * wi + ix as usize] * wd[((co * cin_g + cl) * kh + ky) * kw + kx];
}
}
}
@@ -73,10 +72,8 @@ fn convolutions_match_a_slow_reference() {
let bias: Vec<f32> = (0..cout).map(|i| i as f32 * 0.25 - 0.5).collect();
let x = input(cin, h, wi, |i| ((i * 13 % 17) as f32 - 8.0) * 0.2);
let attrs = [("group", Attr::Int(groups as i64)), ("strides", Attr::Ints(vec![s as i64; 2])), ("pads", Attr::Ints(vec![p as i64; 4]))];
let g = graph(
vec![node("Conv", &["x", "w", "b"], &attrs)],
vec![("w", w(&[cout, cin_g, k, k], wd.clone())), ("b", w(&[cout], bias.clone()))],
);
let g =
graph(vec![node("Conv", &["x", "w", "b"], &attrs)], vec![("w", w(&[cout, cin_g, k, k], wd.clone())), ("b", w(&[cout], bias.clone()))]);
let got = run(g, x.clone()).unwrap();
let want = reference_conv(&x, &wd, [cout, cin_g, k, k], &bias, s, p, groups);
assert_eq!(got.shape, want.shape, "{cin} {cout} {groups} {k} {s} {p}");
@@ -102,8 +99,8 @@ fn the_small_operators() {
.unwrap();
assert_eq!((m.shape.clone(), m.data.clone()), (vec![1, 1, 2, 2], vec![-1.0, 1.0, 7.0, 9.0]));
// Resize x2 nearest
let z = run(graph(vec![node("Resize", &["x", "", "s"], &[])], vec![("s", w(&[4], vec![1.0, 1.0, 2.0, 2.0]))]), input(1, 2, 2, |i| i as f32))
.unwrap();
let z =
run(graph(vec![node("Resize", &["x", "", "s"], &[])], vec![("s", w(&[4], vec![1.0, 1.0, 2.0, 2.0]))]), input(1, 2, 2, |i| i as f32)).unwrap();
assert_eq!(z.shape, vec![1, 1, 4, 4]);
assert_eq!(&z.data[..8], &[0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0]);
// Add
@@ -364,10 +361,7 @@ fn unsupported_and_inconsistent_models_are_errors() {
let x = input(1, 3, 3, |i| i as f32);
for (what, g) in [
("unknown op", graph(vec![node("Softmax", &["x"], &[])], vec![])),
(
"dilated conv",
graph(vec![node("Conv", &["x", "w"], &[("dilations", Attr::Ints(vec![2, 2]))])], vec![("w", w(&[1, 1, 1, 1], vec![1.0]))]),
),
("dilated conv", graph(vec![node("Conv", &["x", "w"], &[("dilations", Attr::Ints(vec![2, 2]))])], vec![("w", w(&[1, 1, 1, 1], vec![1.0]))])),
("conv without weights", graph(vec![node("Conv", &["x", "w"], &[])], vec![])),
("channels do not match", graph(vec![node("Conv", &["x", "w"], &[])], vec![("w", w(&[1, 5, 1, 1], vec![1.0; 5]))])),
("kernel too big", graph(vec![node("Conv", &["x", "w"], &[])], vec![("w", w(&[1, 1, 5, 5], vec![1.0; 25]))])),
@@ -378,10 +372,7 @@ fn unsupported_and_inconsistent_models_are_errors() {
graph(vec![node("Conv", &["x", "w"], &[("pads", Attr::Ints(vec![-1, 0, 0, 0]))])], vec![("w", w(&[1, 1, 1, 1], vec![1.0]))]),
),
("bad perm", graph(vec![node("Transpose", &["x"], &[("perm", Attr::Ints(vec![0, 0, 1, 2]))])], vec![])),
(
"huge reshape",
graph(vec![node("Reshape", &["x", "s"], &[])], vec![("s", Weight { dims: vec![2], data: Data::I64(vec![i64::MAX, 1]) })]),
),
("huge reshape", graph(vec![node("Reshape", &["x", "s"], &[])], vec![("s", Weight { dims: vec![2], data: Data::I64(vec![i64::MAX, 1]) })])),
("reshape mismatch", graph(vec![node("Reshape", &["x", "s"], &[])], vec![("s", Weight { dims: vec![2], data: Data::I64(vec![4, 4]) })])),
("non-integer resize", graph(vec![node("Resize", &["x", "", "s"], &[])], vec![("s", w(&[4], vec![1.0, 1.0, 1.5, 1.5]))])),
("add that broadcasts", graph(vec![node("Add", &["x", "c"], &[])], vec![])),
+6 -21
View File
@@ -147,12 +147,9 @@ fn a_block_that_disagrees_is_ignored() {
#[test]
fn missing_or_garbage_frame_size_gets_a_small_box_and_edges_are_clipped() {
for fs in [
Value::Undefined(vec![0, 0, 0, 0, 0, 0]),
Value::Undefined(vec![1]),
Value::Short(vec![5]),
Value::Undefined(vec![0xaf, 0, 0xb4, 0, 0, 0]),
] {
for fs in
[Value::Undefined(vec![0, 0, 0, 0, 0, 0]), Value::Undefined(vec![1]), Value::Short(vec![5]), Value::Undefined(vec![0xaf, 0, 0xb4, 0, 0, 0])]
{
let mut ifd = alpha1();
set(&mut ifd, FOCUS_FRAME_SIZE, fs);
let f = sony(&ifd, ByteOrder::Little, Orientation::Normal).unwrap();
@@ -416,16 +413,8 @@ fn real_sony_files_match_exiftool() {
// (file, EXIF orientation, ExifTool's FocusLocation and FocusFrameSize, the words we show; None = no point)
let cases: [(&str, u16, Want); 8] = [
// α1, AF-C, Wide, Face tracking, AFAreaMode Animal Eye Tracking
(
"2023/11-november/17th/_DSC6954.ARW",
1,
Some(([8640.0, 5760.0, 3226.0, 1980.0], [175.0, 180.0], &["AF-C", "Wide", "Animal Eye Tracking"])),
),
(
"2023/11-november/17th/_DSC6894.ARW",
1,
Some(([8640.0, 5760.0, 4725.0, 1860.0], [135.0, 132.0], &["AF-C", "Wide", "Animal Eye Tracking"])),
),
("2023/11-november/17th/_DSC6954.ARW", 1, Some(([8640.0, 5760.0, 3226.0, 1980.0], [175.0, 180.0], &["AF-C", "Wide", "Animal Eye Tracking"]))),
("2023/11-november/17th/_DSC6894.ARW", 1, Some(([8640.0, 5760.0, 4725.0, 1860.0], [135.0, 132.0], &["AF-C", "Wide", "Animal Eye Tracking"]))),
// α1, manual focus (the location 4441 3060 is where the frame was parked)
("2023/11-november/15th/_DSC6613.ARW", 1, None),
// α1 portrait, AF-S, Wide, Multi at the exact centre 4320 2880
@@ -433,11 +422,7 @@ fn real_sony_files_match_exiftool() {
// α1 APS-C crop mode portrait, AF-C, Wide, Lock On AF, AFAreaMode Tracking
("2022/12-december/16th/_DSC1575.ARW", 8, Some(([5616.0, 3744.0, 4071.0, 1848.0], [280.0, 280.0], &["AF-C", "Wide", "Tracking"]))),
// A7S III portrait, AF-A, Wide, Face tracking, Animal Eye Tracking
(
"2020/10-october/31st/DSC00065.ARW",
8,
Some(([4240.0, 2832.0, 1192.0, 1675.0], [278.0, 277.0], &["AF-A", "Wide", "Animal Eye Tracking"])),
),
("2020/10-october/31st/DSC00065.ARW", 8, Some(([4240.0, 2832.0, 1192.0, 1675.0], [278.0, 277.0], &["AF-A", "Wide", "Animal Eye Tracking"]))),
// A7S III upside down, AF-C, a centred Flexible Spot: a real point
("2021/11-november/7th/DSC05975.ARW", 3, Some(([4240.0, 2832.0, 2120.0, 1416.0], [251.0, 224.0], &["AF-C", "Flexible Spot"]))),
// A7S III, manual focus
+2 -8
View File
@@ -156,10 +156,7 @@ fn interval_framing_encloses_every_f64_result() {
};
let (x, y) = (x.clamp(ix.lo(), ix.hi()), y.clamp(iy.lo(), iy.hi()));
let (_, (fx, fy)) = wp.framing(&a, x, y);
assert!(
sx.lo() <= fx && fx <= sx.hi() && sy.lo() <= fy && fy <= sy.hi(),
"({x}, {y}) → ({fx}, {fy}) outside {sx:?} × {sy:?}"
);
assert!(sx.lo() <= fx && fx <= sx.hi() && sy.lo() <= fy && fy <= sy.hi(), "({x}, {y}) → ({fx}, {fy}) outside {sx:?} × {sy:?}");
}
}
}
@@ -405,10 +402,7 @@ fn warp_rectilinear2_interval_framing_encloses_every_f64_result() {
for j in 0..=4 {
let (x, y) = ((x0 + bw * i as f64 / 4.0).clamp(ix.lo(), ix.hi()), (y0 + bh * j as f64 / 4.0).clamp(iy.lo(), iy.hi()));
let (_, (fx, fy)) = wp.framing(&a, x, y);
assert!(
sx.lo() <= fx && fx <= sx.hi() && sy.lo() <= fy && fy <= sy.hi(),
"({x}, {y}) → ({fx}, {fy}) outside {sx:?} × {sy:?}"
);
assert!(sx.lo() <= fx && fx <= sx.hi() && sy.lo() <= fy && fy <= sy.hi(), "({x}, {y}) → ({fx}, {fy}) outside {sx:?} × {sy:?}");
}
}
}
+18 -60
View File
@@ -886,11 +886,9 @@ fn a_persons_page_shows_their_faces_and_back_returns_to_everyone() {
description: None,
};
// Jane Doe twice in the first photo and once in the second; John Roe in the third
for (id, regions) in [
(ids[0], vec![face(0.1, "Jane Doe"), face(0.5, "jane doe")]),
(ids[1], vec![face(0.3, "Jane Doe")]),
(ids[2], vec![face(0.3, "John Roe")]),
] {
for (id, regions) in
[(ids[0], vec![face(0.1, "Jane Doe"), face(0.5, "jane doe")]), (ids[1], vec![face(0.3, "Jane Doe")]), (ids[2], vec![face(0.3, "John Roe")])]
{
let mut meta = h.app.session.catalog.photo(id).unwrap().meta.clone();
meta.regions = regions;
h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap();
@@ -994,8 +992,7 @@ fn person_more_confirms_all_above_with_one_undo_step() {
pending: 0,
};
let set_page = |h: &mut Headless| {
h.app.caches.person_page =
Some(("Jane Doe".into(), h.app.session.catalog.revision, h.app.caches.faces_indexed, 0.0, Arc::new(page.clone())));
h.app.caches.person_page = Some(("Jane Doe".into(), h.app.session.catalog.revision, h.app.caches.faces_indexed, 0.0, Arc::new(page.clone())));
h.step();
};
h.app.ui.dismissed_faces.insert((ids[4].0, 0));
@@ -1009,15 +1006,7 @@ fn person_more_confirms_all_above_with_one_undo_step() {
let names = |h: &Headless| ids.iter().map(|id| h.app.session.catalog.photo(*id).unwrap().meta.regions[0].name.clone()).collect::<Vec<_>>();
assert_eq!(
names(&h),
vec![
Some("Jane Doe".into()),
Some("Jane Doe".into()),
Some("Jane Doe".into()),
Some("Jane Doe".into()),
None,
None,
Some("John Roe".into())
]
vec![Some("Jane Doe".into()), Some("Jane Doe".into()), Some("Jane Doe".into()), Some("Jane Doe".into()), None, None, Some("John Roe".into())]
);
assert_eq!(h.app.session.undo.len(), undo + 1);
h.request("engine.execute", json!({"command": "edit.undo"}), t);
@@ -1058,15 +1047,7 @@ fn person_more_confirms_all_above_with_one_undo_step() {
h.step();
assert_eq!(
names(&h),
vec![
Some("Jane Doe".into()),
Some("Jane Doe".into()),
Some("Jane Doe".into()),
Some("Jane Doe".into()),
None,
None,
Some("John Roe".into())
]
vec![Some("Jane Doe".into()), Some("Jane Doe".into()), Some("Jane Doe".into()), Some("Jane Doe".into()), None, None, Some("John Roe".into())]
);
assert_eq!(h.app.session.undo.len(), undo + 1);
h.request("engine.execute", json!({"command": "view.photoGrid"}), t);
@@ -2061,8 +2042,7 @@ fn a_slow_photos_import_never_blocks_the_window() {
Ok(lightcraft_engine::apple_photos::Output { status: Some(0), stdout: "ID\n".repeat(n), stderr: String::new() })
}));
let t0 = Instant::now();
let r =
h.request("engine.execute", json!({"command": "export.addToPhotos", "params": {"paths": [file.to_string_lossy()], "album": "Slow"}}), t);
let r = h.request("engine.execute", json!({"command": "export.addToPhotos", "params": {"paths": [file.to_string_lossy()], "album": "Slow"}}), t);
assert!(t0.elapsed() < Duration::from_secs(5), "answered at once: {r}");
assert_eq!(r["result"]["running"], true, "{r}");
go.send(()).unwrap();
@@ -2108,8 +2088,7 @@ fn a_headless_run_waits_for_its_photos_import_before_ending() {
std::fs::write(&m, b"done").unwrap();
Ok(lightcraft_engine::apple_photos::Output { status: Some(0), stdout: "ID\n".into(), stderr: String::new() })
}));
let r =
h.request("engine.execute", json!({"command": "export.addToPhotos", "params": {"paths": [file.to_string_lossy()], "wait": false}}), t);
let r = h.request("engine.execute", json!({"command": "export.addToPhotos", "params": {"paths": [file.to_string_lossy()], "wait": false}}), t);
assert_eq!(r["result"]["running"], true, "{r}");
assert!(!marker.exists(), "still importing");
let release = std::thread::spawn(move || {
@@ -2214,11 +2193,8 @@ fn drag_photos_onto_an_album() {
h.settle(SETTLE);
let rect = |h: &mut Headless, id: &str| {
let w = h.request("ui.widgets", json!({"filter": id}), t);
let r = w["result"]
.as_array()
.and_then(|a| a.iter().find(|x| x["id"] == id))
.map(|x| x["rect"].clone())
.unwrap_or_else(|| panic!("{id}: {w}"));
let r =
w["result"].as_array().and_then(|a| a.iter().find(|x| x["id"] == id)).map(|x| x["rect"].clone()).unwrap_or_else(|| panic!("{id}: {w}"));
let f = |i: usize| r[i].as_f64().unwrap();
(f(0) + f(2) / 2.0, f(1) + f(3) / 2.0)
};
@@ -2616,25 +2592,12 @@ fn info_mixed_fields_are_the_ones_that_differ() {
let ids: Vec<u64> = s.visible_cloned().iter().take(3).map(|p| p.0).collect();
let all = |key: &str, v: serde_json::Value| json!({"ids": ids, key: v});
// (every field the panel compares starts the same on the three photos)
let fields = [
"title",
"caption",
"altText",
"extendedDescription",
"copyright",
"usageTerms",
"copyrightUrl",
"creator",
"location",
"state",
"country",
];
let fields =
["title", "caption", "altText", "extendedDescription", "copyright", "usageTerms", "copyrightUrl", "creator", "location", "state", "country"];
for key in fields {
s.execute("photo.setMeta", &all(key, json!(""))).unwrap();
}
for (key, v) in
[("title", json!("Same")), ("city", json!("Porto")), ("copyrightStatus", json!("copyrighted")), ("gps", json!([41.15, -8.61]))]
{
for (key, v) in [("title", json!("Same")), ("city", json!("Porto")), ("copyrightStatus", json!("copyrighted")), ("gps", json!([41.15, -8.61]))] {
s.execute("photo.setMeta", &all(key, v)).unwrap();
}
let photos: Vec<lightcraft_catalog::PhotoId> = ids.iter().map(|&i| lightcraft_catalog::PhotoId(i)).collect();
@@ -3279,8 +3242,7 @@ fn browsing_the_same_folder_again_does_not_restart() {
}
Ok(lightcraft_engine::media::ProbeInfo { format: "PNG".into(), ..Default::default() })
}));
let browse =
|h: &mut Headless| h.request("engine.execute", json!({"command": "library.browse", "params": {"path": dir.to_string_lossy()}}), t);
let browse = |h: &mut Headless| h.request("engine.execute", json!({"command": "library.browse", "params": {"path": dir.to_string_lossy()}}), t);
assert_eq!(browse(&mut h)["result"]["scanning"], true);
std::thread::sleep(Duration::from_millis(200));
assert_eq!(browse(&mut h)["result"]["scanning"], true);
@@ -3585,8 +3547,7 @@ fn import_review_offers_recently_deleted_files() {
for i in 0..2u8 {
let data: Vec<[u8; 4]> = (0..40 * 30).map(|k| [(k % 40 * 6) as u8, i * 90, 7, 255]).collect();
let img = lightcraft_raster::Rgba8 { width: 40, height: 30, data };
let png =
lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
std::fs::write(dir.join(format!("img{i}.png")), png).unwrap();
}
let services = crate::Services { png: None, ..Default::default() };
@@ -3594,8 +3555,7 @@ fn import_review_offers_recently_deleted_files() {
app.ui.view = crate::state::ViewMode::PhotoGrid;
let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
let t = Duration::from_secs(10);
let r =
h.request("engine.execute", json!({"command": "library.import", "params": {"paths": [dir.join("img0.png").to_string_lossy()]}}), t);
let r = h.request("engine.execute", json!({"command": "library.import", "params": {"paths": [dir.join("img0.png").to_string_lossy()]}}), t);
let id = r["result"]["imported"][0].as_u64().unwrap();
h.request("engine.execute", json!({"command": "photo.rate", "params": {"ids": [id], "rating": 3}}), t);
h.request("engine.execute", json!({"command": "photo.delete", "params": {"ids": [id]}}), t);
@@ -4066,8 +4026,7 @@ fn import_rename_tags_insert_at_the_cursor() {
app.ui.view = crate::state::ViewMode::PhotoGrid;
let mut h = Headless::new(app, [1300.0, 1000.0], 1.0);
let t = Duration::from_secs(10);
let r =
h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.join("IMG_0007.png").to_string_lossy()]}}), t);
let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.join("IMG_0007.png").to_string_lossy()]}}), t);
assert_eq!(r["result"]["scanning"], true, "{r}");
h.settle(SETTLE);
let r = h.request("ui.clickWidget", json!({"id": "button:importCopy"}), t);
@@ -4142,8 +4101,7 @@ fn display_profile_converts_previews() {
let mut h = demo([1000.0, 700.0]);
let t = Duration::from_secs(10);
h.request("ui.set", json!({"view": "detail"}), t);
let thumb_slot =
|h: &Headless| h.app.renderer.textures.keys().copied().filter(|s| matches!(s, Slot::Thumb(_))).min_by_key(|s| format!("{s:?}"));
let thumb_slot = |h: &Headless| h.app.renderer.textures.keys().copied().filter(|s| matches!(s, Slot::Thumb(_))).min_by_key(|s| format!("{s:?}"));
let ready = |h: &Headless| {
h.app.renderer.textures.get(&Slot::Main).is_some_and(|t| t.pixels.is_some())
&& thumb_slot(h).is_some()
+15 -38
View File
@@ -87,11 +87,8 @@ fn catalogs_load() {
/// `locales/*.json`, sorted.
fn catalog_files() -> Vec<std::path::PathBuf> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales");
let mut files: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| path.extension().is_some_and(|ext| ext == "json"))
.collect();
let mut files: Vec<std::path::PathBuf> =
std::fs::read_dir(dir).unwrap().map(|entry| entry.unwrap().path()).filter(|path| path.extension().is_some_and(|ext| ext == "json")).collect();
files.sort();
files
}
@@ -282,11 +279,8 @@ fn catalog_characters_are_paintable() {
ctx.fonts_mut(|fonts| {
for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into())] {
let font = egui::FontId::new(13.0, family.clone());
let mut missing: Vec<char> = messages
.values()
.flat_map(|message| message.chars())
.filter(|ch| !ch.is_whitespace() && !fonts.has_glyph(&font, *ch))
.collect();
let mut missing: Vec<char> =
messages.values().flat_map(|message| message.chars()).filter(|ch| !ch.is_whitespace() && !fonts.has_glyph(&font, *ch)).collect();
missing.sort_unstable();
missing.dedup();
assert!(
@@ -426,9 +420,8 @@ fn choice_values_read_as_words_in_every_language() {
fn problems_read_in_every_language() {
use lightcraft_catalog::rules::Issue;
let cat = lightcraft_catalog::Catalog::new();
let problems = |rules: serde_json::Value| {
serde_json::from_value::<lightcraft_catalog::RuleSet>(serde_json::json!({"rules": rules})).unwrap().check(&cat)
};
let problems =
|rules: serde_json::Value| serde_json::from_value::<lightcraft_catalog::RuleSet>(serde_json::json!({"rules": rules})).unwrap().check(&cat);
let p = problems(serde_json::json!([{"field": "rating", "op": "gte", "value": 3}, {"field": "title", "op": "contains", "value": ""}]));
assert_eq!(problem_text_in(Locale::En, &p[0]), "Title: needs something to look for (or use “is empty”)");
assert_eq!(problem_line_in(Locale::En, &p[0]), "#2 Title: needs something to look for (or use “is empty”)");
@@ -566,11 +559,7 @@ fn german_switches_via_menu_and_control_and_persists() {
assert!(rules_label(&album, &catalog).ends_with(" “Ausgeschlossen”"), "{}", rules_label(&album, &catalog));
let rules = lightcraft_catalog::RuleSet {
mode: lightcraft_catalog::Match::All,
rules: vec![lightcraft_catalog::Rule::Field {
field: "copyrightStatus".into(),
op: "is".into(),
value: serde_json::json!("publicDomain"),
}],
rules: vec![lightcraft_catalog::Rule::Field { field: "copyrightStatus".into(), op: "is".into(), value: serde_json::json!("publicDomain") }],
};
let public_domain = format!(" {}", choice_text_in(Locale::De, "copyrightStatus", "publicDomain"));
assert!(rules_label(&rules, &catalog).ends_with(&public_domain), "{}", rules_label(&rules, &catalog));
@@ -596,10 +585,8 @@ fn german_switches_via_menu_and_control_and_persists() {
assert_eq!(filter_label(&both, &app.session.catalog), format!("{label}: {red} {and} {blue}"));
let both = lightcraft_catalog::Filter { labels: vec![lightcraft_catalog::ColorLabel::Blue, lightcraft_catalog::ColorLabel::Green], ..both };
assert_eq!(filter_label(&both, &app.session.catalog), format!("{label}: {red} {and} ({blue} {or} {green})"));
let filter = lightcraft_catalog::Filter {
labels: vec![lightcraft_catalog::ColorLabel::Red, lightcraft_catalog::ColorLabel::Blue],
..Default::default()
};
let filter =
lightcraft_catalog::Filter { labels: vec![lightcraft_catalog::ColorLabel::Red, lightcraft_catalog::ColorLabel::Blue], ..Default::default() };
let blue = color_label(&app.session.catalog, lightcraft_catalog::ColorLabel::Blue);
let red = color_label(&app.session.catalog, lightcraft_catalog::ColorLabel::Red);
let label = filter_label(&filter, &app.session.catalog);
@@ -773,9 +760,7 @@ fn craft_fonts_render_cjk_in_the_ui() {
set_language(language);
let ctx = fonts_ctx(lightcraft_engine::CRAFT_FONTS);
ctx.fonts_mut(|fonts| {
for family in
[egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into()), egui::FontFamily::Monospace]
{
for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into()), egui::FontFamily::Monospace] {
let font = egui::FontId::new(13.0, family);
for ch in sample.chars() {
assert!(fonts.has_glyph(&font, ch), "{language:?}: {ch} in {font:?}");
@@ -952,12 +937,8 @@ fn translation_gaps_report() {
let messages: std::collections::BTreeSet<String> = plain_messages().into_iter().filter(|m| !notes.contains(&m.as_str())).collect();
let formats = format_messages();
let quoted = |list: &[&str]| list.iter().map(|m| format!("{m:?}")).collect::<Vec<_>>().join(", ");
let mut out = format!(
"Translation gaps, shown in English ({} messages, {} formats, {} What's New lines):\n",
messages.len(),
formats.len(),
notes.len()
);
let mut out =
format!("Translation gaps, shown in English ({} messages, {} formats, {} What's New lines):\n", messages.len(), formats.len(), notes.len());
for language in Locale::ALL.iter().filter(|language| **language != Locale::En) {
let catalog = language.catalog();
let own_formats = formats_catalog(language.code());
@@ -1051,11 +1032,8 @@ fn formats_catalog(code: &str) -> BTreeMap<String, String> {
/// Every format message: the keys of every formats catalog and the message of every
/// `tr_format!` call in `src/` outside its test modules.
fn format_messages() -> std::collections::BTreeSet<String> {
let mut all: std::collections::BTreeSet<String> = Locale::ALL
.iter()
.filter(|language| **language != Locale::En)
.flat_map(|language| formats_catalog(language.code()).into_keys())
.collect();
let mut all: std::collections::BTreeSet<String> =
Locale::ALL.iter().filter(|language| **language != Locale::En).flat_map(|language| formats_catalog(language.code()).into_keys()).collect();
let mut dirs = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")];
while let Some(dir) = dirs.pop() {
for path in std::fs::read_dir(&dir).unwrap().map(|entry| entry.unwrap().path()) {
@@ -1125,8 +1103,7 @@ fn a_dropped_photos_notice_names_every_outcome_of_the_import_and_is_silent_for_d
let png = |name: &str, seed: u8| {
let data: Vec<[u8; 4]> = (0..48 * 32).map(|i| [(i % 48 * 5) as u8, (i / 48 * 7) as u8, seed, 255]).collect();
let img = lightcraft_raster::Rgba8 { width: 48, height: 32, data };
let bytes =
lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
let bytes = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
std::fs::write(dir.join(name), bytes).unwrap();
dir.join(name).to_string_lossy().into_owned()
};
+3 -6
View File
@@ -354,8 +354,7 @@ fn select_by_submenu_and_text_prompt() {
let mut app = app();
let bar = menu_bar(&app);
let edit = &bar.iter().find(|(t, _)| t == "Edit").unwrap().1;
let Some(MenuNode::Submenu { children, .. }) = edit.iter().find(|n| matches!(n, MenuNode::Submenu { label, .. } if label == "Select by"))
else {
let Some(MenuNode::Submenu { children, .. }) = edit.iter().find(|n| matches!(n, MenuNode::Submenu { label, .. } if label == "Select by")) else {
panic!("no Select by submenu")
};
let Some(MenuNode::Item { id, params, .. }) = children.first() else { panic!() };
@@ -468,10 +467,8 @@ fn in_window_menu_headings_are_spaced_apart() {
fn a_refused_backup_from_the_menu_shows_why() {
// the host refuses, as the web one does while changes are still unsaved (issue #1508)
let refusal = "Backup not started: 1 change could not be saved to browser storage yet";
let services = crate::Services {
backup_library: Some(Box::new(move |_: &mut lightcraft_engine::Session| Err(refusal.to_string()))),
..Default::default()
};
let services =
crate::Services { backup_library: Some(Box::new(move |_: &mut lightcraft_engine::Session| Err(refusal.to_string()))), ..Default::default() };
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
let mut h = crate::headless::Headless::new(app, [1400.0, 900.0], 1.0);
let t = std::time::Duration::from_secs(30);
+2 -8
View File
@@ -586,14 +586,8 @@ fn the_margin_grows_with_the_zoom_up_to_a_cap() {
#[test]
fn a_tile_is_drawn_only_while_it_shows_what_the_loupe_shows() {
use lightcraft_catalog::PhotoId;
let v = RegionView {
photo: PhotoId(1),
before: false,
key: 7,
full: (6000, 4000),
window: PixelWindow { x: 0, y: 0, w: 256, h: 256 },
settings: 11,
};
let v =
RegionView { photo: PhotoId(1), before: false, key: 7, full: (6000, 4000), window: PixelWindow { x: 0, y: 0, w: 256, h: 256 }, settings: 11 };
assert!(v.is_current(PhotoId(1), (6000, 4000), 11, false));
assert!(!v.is_current(PhotoId(2), (6000, 4000), 11, false), "another photo");
assert!(!v.is_current(PhotoId(1), (6000, 4001), 11, false), "another zoom");
+2 -4
View File
@@ -92,8 +92,7 @@ fn queue_promotions_running_work_and_cancellation() {
}
fn queued_quick_thumbnail(id: PhotoId) -> (Arc<Photo>, QuickJob) {
let mut session = Session::new();
session.media.preview_loader =
Some(Arc::new(|_, _| Some(lightcraft_raster::Rgba8 { width: 4, height: 4, data: vec![[200, 100, 50, 255]; 16] })));
session.media.preview_loader = Some(Arc::new(|_, _| Some(lightcraft_raster::Rgba8 { width: 4, height: 4, data: vec![[200, 100, 50, 255]; 16] })));
let mut raw = (*photo()).clone();
raw.id = id;
raw.kind = lightcraft_catalog::MediaKind::Raw;
@@ -651,8 +650,7 @@ fn undo_to_existing_texture_supersedes_pending_edit() {
let original = app.renderer.thumb(PhotoId(1)).unwrap().pixels.clone().unwrap();
let mut edit = lightcraft_develop::DevelopSettings::default();
edit.light.exposure = 1.0;
let undo =
app.session.catalog.apply(Op::SetDevelop { id: PhotoId(1), settings: Arc::new(edit), label: "edit".into(), edited: None }).unwrap();
let undo = app.session.catalog.apply(Op::SetDevelop { id: PhotoId(1), settings: Arc::new(edit), label: "edit".into(), edited: None }).unwrap();
request(&mut app);
let (_, edited) = take_job(&work);
app.session.catalog.apply(undo).unwrap();
+2 -14
View File
@@ -687,25 +687,13 @@ mod issue1771 {
#[test]
fn issue1771_repeat_event_opens_once_and_detail_keeps_selection() {
let mut h = grid(ViewMode::PhotoGrid);
h.events.push(egui::Event::Key {
key: egui::Key::Enter,
physical_key: None,
pressed: true,
repeat: true,
modifiers: egui::Modifiers::NONE,
});
h.events.push(egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: true, modifiers: egui::Modifiers::NONE });
h.step();
h.step();
assert_eq!(h.app.ui.view, ViewMode::Detail);
assert_eq!(h.app.session.active(), Some(PhotoId(2)));
assert_eq!(h.app.session.selection.ids.len(), 3);
h.events.push(egui::Event::Key {
key: egui::Key::Enter,
physical_key: None,
pressed: true,
repeat: true,
modifiers: egui::Modifiers::NONE,
});
h.events.push(egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: true, modifiers: egui::Modifiers::NONE });
h.step();
h.step();
assert_eq!(h.app.ui.view, ViewMode::Detail);
+2 -2
View File
@@ -19,6 +19,7 @@ mod dialog_primary;
mod dialog_settings;
mod dialog_tab;
mod dialogs;
mod docking_acceptance;
mod drop_import;
mod edit_layout;
mod empty_library;
@@ -49,6 +50,7 @@ mod masking;
mod masking_layout;
mod menubar;
mod merge_preview;
mod model_setup;
mod offline;
mod open_lightroom;
pub(super) mod panels;
@@ -80,5 +82,3 @@ mod view_cost;
mod wb_selector;
mod zoom;
mod zoom_keys;
mod model_setup;
mod docking_acceptance;
+3 -5
View File
@@ -266,11 +266,9 @@ fn a_zoomed_window_is_still_zoomed_on_another_screen_where_the_system_does_not_s
fn nothing_waits_while_the_window_is_full_screen_or_minimized() {
let rest = place([1300.0, 800.0], false);
for live in [true, false] {
for away in [
Seen { full_screen: true, ..window(SCREEN) },
Seen { minimized: true, ..window([160.0, 28.0]) },
Seen { size: None, ..window(SCREEN) },
] {
for away in
[Seen { full_screen: true, ..window(SCREEN) }, Seen { minimized: true, ..window([160.0, 28.0]) }, Seen { size: None, ..window(SCREEN) }]
{
let mut watch = WindowWatch::told_live(live);
watch.note(Some(rest), &window([1500.0, 900.0]), 1.0);
assert!(watch.waits(1.0).is_some());