//! Local adjustments must survive the sidecar. //! //! The sidecar is the authoritative store (ARCH §6.1) — the catalog is a //! disposable index and the RAW is never written. So a mask that does not //! round-trip is not a persistence bug, it is lost work, and the tests here //! are about the ways that happens quietly rather than loudly. use dr_pipeline::descriptor::ParamId; use dr_pipeline::mask::{MaskLayer, MaskSource}; use dr_pipeline::{EditGraph, Sidecar, Version}; fn regions(ids: &[u32]) -> MaskSource { MaskSource::Regions { signature: 0xdead_beef, level: 300, ids: ids.to_vec(), } } /// A graph with one masked adjustment on it. fn graph_with_mask() -> EditGraph { let mut graph = EditGraph::default_chain(); let mut layer = MaskLayer::new("m1", regions(&[3, 7, 12])); layer.name = "Subject".into(); layer.set_param("exposure", ParamId("exposure"), 0.75); graph.masks_mut().push(layer); graph } fn round_trip(graph: &EditGraph) -> EditGraph { let mut sidecar = Sidecar::new(); sidecar.put(Version::from_graph("default", "Default", graph)); let text = sidecar.to_text(); let parsed = Sidecar::parse(&text).expect("reparse"); let mut restored = EditGraph::default_chain(); parsed .versions .get("default") .expect("version survived") .apply(&mut restored) .expect_no_film(); restored } #[test] fn a_region_mask_survives_a_round_trip() { let graph = graph_with_mask(); let restored = round_trip(&graph); let layers = restored.masks().layers(); assert_eq!(layers.len(), 1, "the layer came back"); let layer = &layers[0]; assert_eq!(layer.id, "m1"); assert_eq!(layer.name, "Subject"); assert_eq!(layer.source, regions(&[3, 7, 12])); assert_eq!( layer .ops .iter() .find(|o| o.descriptor().id.0 == "exposure") .map(|o| o.param(ParamId("exposure"))), Some(0.75) ); } #[test] fn writing_the_same_state_twice_is_byte_identical() { // What lets a caller skip an upload by comparing content rather than // trusting a dirty flag — the property the whole `versions` ordering // exists for, now that masks are in the file too. let graph = graph_with_mask(); let mut a = Sidecar::new(); a.put(Version::from_graph("default", "Default", &graph)); let once = a.to_text(); let twice = Sidecar::parse(&once).expect("reparse").to_text(); assert_eq!(once, twice); } #[test] fn gradients_keep_their_geometry() { let mut graph = EditGraph::default_chain(); let mut linear = MaskLayer::new( "m1", MaskSource::Linear { centre: (0.25, 0.75), angle: 1.25, width: 0.4, }, ); linear.set_param("exposure", ParamId("exposure"), -1.0); graph.masks_mut().push(linear); let mut radial = MaskLayer::new( "m2", MaskSource::Radial { centre: (0.6, 0.4), radii: (0.3, 0.15), angle: -0.5, feather: 0.65, }, ); radial.set_param("exposure", ParamId("exposure"), 0.5); graph.masks_mut().push(radial); let restored = round_trip(&graph); let layers = restored.masks().layers(); assert_eq!(layers.len(), 2); assert_eq!(layers[0].source, graph.masks().layers()[0].source); assert_eq!(layers[1].source, graph.masks().layers()[1].source); } #[test] fn flags_and_opacity_survive() { let mut graph = EditGraph::default_chain(); let mut layer = MaskLayer::new("m1", regions(&[1])); layer.set_param("exposure", ParamId("exposure"), 1.0); layer.invert = true; layer.opacity = 0.35; layer.enabled = false; graph.masks_mut().push(layer); let restored = round_trip(&graph); let layer = &restored.masks().layers()[0]; assert!(layer.invert); assert!((layer.opacity - 0.35).abs() < 1e-6); assert!( !layer.enabled, "a disabled layer must stay disabled, not vanish" ); } /// A selection's identity is the *set*. Two files naming the same regions in /// different orders describe one mask and must compare equal, or the two /// devices that wrote them will fight forever over a difference that is not /// one. #[test] fn region_order_is_normalised_on_read() { let text = "drsc 1\n\ \n[version default]\n\ name = Default\n\ revision = 1\n\ modified = 0\n\ \n[mask default m1]\n\ source = regions\n\ signature = 5\n\ level = 10\n\ regions = 12 3 7 3\n\ exposure.exposure = 1\n"; let parsed = Sidecar::parse(text).expect("parse"); let layer = &parsed.versions["default"].masks.layers()[0]; assert_eq!( layer.source, MaskSource::Regions { signature: 5, level: 10, ids: vec![3, 7, 12], }, "ids sort and deduplicate on the way in" ); } /// The version skew case. An older build must not delete a mask type it has /// never heard of *silently* — but neither may it apply one it cannot read. #[test] fn an_unknown_mask_source_is_skipped_not_guessed() { let text = "drsc 1\n\ \n[version default]\n\ name = Default\n\ revision = 1\n\ modified = 0\n\ \n[mask default m1]\n\ source = luminosity\n\ threshold = 0.5\n\ exposure.exposure = 1\n"; let parsed = Sidecar::parse(text).expect("parse"); assert!( parsed.versions["default"].masks.is_empty(), "an unreadable mask must not become a wrong one" ); } #[test] fn a_mask_naming_no_known_version_is_dropped() { let text = "drsc 1\n\ \n[version default]\n\ name = Default\n\ revision = 1\n\ modified = 0\n\ \n[mask ghost m1]\n\ source = regions\n\ signature = 1\n\ level = 1\n\ regions = 1\n"; let parsed = Sidecar::parse(text).expect("parse"); assert!(parsed.versions["default"].masks.is_empty()); } /// Masks name their version rather than relying on file order, so a block /// that appears before its version still lands on it. #[test] fn a_mask_block_need_not_follow_its_version() { let text = "drsc 1\n\ \n[mask second m1]\n\ source = regions\n\ signature = 1\n\ level = 1\n\ regions = 4 5\n\ exposure.exposure = 1\n\ \n[version first]\n\ name = First\n\ revision = 1\n\ modified = 0\n\ \n[version second]\n\ name = Second\n\ revision = 1\n\ modified = 0\n"; let parsed = Sidecar::parse(text).expect("parse"); assert!(parsed.versions["first"].masks.is_empty()); assert_eq!( parsed.versions["second"].masks.len(), 1, "the block named its version and should have reached it" ); } /// Loading is a replacement, not an overlay: opening an unedited image after /// an edited one must not leave the previous image's masks on screen. #[test] fn applying_a_maskless_version_clears_existing_masks() { let mut graph = graph_with_mask(); assert_eq!(graph.masks().len(), 1); let plain = Version::from_graph("clean", "Clean", &EditGraph::default_chain()); plain.apply(&mut graph).expect_no_film(); assert!(graph.masks().is_empty()); } #[test] fn a_graph_with_only_a_masked_edit_is_not_neutral() { let graph = graph_with_mask(); assert!(!graph.is_neutral(), "a local adjustment is still an edit"); } /// A bare selection with nothing applied to it changes no pixel, so the image /// is unedited — but the layer must still be written, or the selection the /// user made is lost on reload. #[test] fn a_selection_with_no_adjustment_still_persists() { let mut graph = EditGraph::default_chain(); graph.masks_mut().push(MaskLayer::new("m1", regions(&[9]))); assert!(graph.is_neutral(), "no adjustment means no pixel changes"); let restored = round_trip(&graph); assert_eq!( restored.masks().len(), 1, "the selection is work and must survive even though it renders nothing" ); } #[test] fn unknown_top_level_keys_still_round_trip_alongside_masks() { let text = "drsc 1\n\ \n[version default]\n\ name = Default\n\ revision = 1\n\ modified = 0\n\ future.thing = 3\n\ \n[mask default m1]\n\ source = regions\n\ signature = 1\n\ level = 1\n\ regions = 2\n\ exposure.exposure = 1\n"; let out = Sidecar::parse(text).expect("parse").to_text(); assert!( out.contains("future.thing"), "unknown keys are still preserved" ); assert!(out.contains("[mask default m1]")); } // --------------------------------------------------------------------------- // Brush strokes // --------------------------------------------------------------------------- /// One gesture to paint: whether it erases, its radius, and its path. type Gesture = (bool, f32, Vec<(f32, f32)>); /// Paint gestures onto a fresh brush layer. fn brushed(id: &str, gestures: &[Gesture]) -> MaskLayer { let mut layer = MaskLayer::new(id, MaskSource::brush()); layer.set_param("exposure", ParamId("exposure"), 1.0); for (erase, radius, path) in gestures { layer.begin_stroke(*erase, *radius, 0.5, 1.0); for &(x, y) in path { layer.extend_stroke(x, y); } layer.end_stroke(); } layer } /// The whole point of storing strokes as parameters: they must come back as /// the *same numbers*, not as numbers that render similarly. A coordinate that /// drifts in the sixth decimal on every save is a file that never stops /// changing, and under per-field merge that is a conflict a day. #[test] fn strokes_survive_a_round_trip_exactly() { let mut graph = EditGraph::default_chain(); let painted = brushed( "m1", &[( false, 0.0625, vec![(0.1234, 0.5), (0.4, 0.2), (0.8, 0.75), (0.9, 0.1)], )], ); let expected = painted.strokes().to_vec(); graph.masks_mut().push(painted); let restored = round_trip(&graph); let layer = &restored.masks().layers()[0]; assert_eq!(layer.strokes(), expected.as_slice()); } /// Order is the mask. An erase written before the add it was meant to cut into /// would silently repaint what the user removed — the mask still looks like a /// mask, so nothing announces it. #[test] fn stroke_order_and_direction_survive() { let mut graph = EditGraph::default_chain(); graph.masks_mut().push(brushed( "m1", &[ (false, 0.2, vec![(0.2, 0.5), (0.8, 0.5)]), (true, 0.1, vec![(0.5, 0.5)]), ], )); let restored = round_trip(&graph); let strokes = restored.masks().layers()[0].strokes(); assert_eq!(strokes.len(), 2); assert!(!strokes[0].erase, "the add must still come first"); assert!(strokes[1].erase, "and the erase second"); assert_eq!(strokes[1].radius, 0.1, "each stroke keeps its own brush"); } #[test] fn a_brush_layer_writes_one_line_per_stroke() { let mut graph = EditGraph::default_chain(); graph.masks_mut().push(brushed( "m1", &[ (false, 0.05, vec![(0.2, 0.5), (0.8, 0.5)]), (true, 0.05, vec![(0.5, 0.5)]), ], )); let mut sidecar = Sidecar::new(); sidecar.put(Version::from_graph("default", "Default", &graph)); let text = sidecar.to_text(); let lines: Vec<&str> = text .lines() .filter(|l| l.starts_with("stroke = ")) .collect(); assert_eq!( lines, [ "stroke = add 0.05 0.5 1 0.2,0.5 0.8,0.5", "stroke = erase 0.05 0.5 1 0.5,0.5" ] ); } #[test] fn writing_a_painted_mask_twice_is_byte_identical() { let mut graph = EditGraph::default_chain(); graph.masks_mut().push(brushed( "m1", &[(false, 0.05, vec![(0.2, 0.5), (0.4, 0.6), (0.8, 0.5)])], )); let mut sidecar = Sidecar::new(); sidecar.put(Version::from_graph("default", "Default", &graph)); let once = sidecar.to_text(); let twice = Sidecar::parse(&once).expect("reparse").to_text(); assert_eq!(once, twice); } /// One damaged line must not cost the strokes either side of it. Refusing the /// whole layer would throw away a mask over a typo, and guessing at the missing /// half would put paint where nobody touched. #[test] fn a_malformed_stroke_costs_only_that_stroke() { let text = "drsc 1\n\ \n[version default]\n\ name = Default\n\ revision = 1\n\ modified = 0\n\ \n[mask default m1]\n\ source = brush\n\ stroke = add 0.05 0.5 1 0.2,0.5\n\ stroke = sideways 0.05 0.5 1 0.3,0.5\n\ stroke = add 0.05 0.5 1 0.4 0.5\n\ stroke = add 0.05 0.5 1 0.6,0.5\n\ exposure.exposure = 1\n"; let parsed = Sidecar::parse(text).expect("parse"); let strokes = parsed.versions["default"].masks.layers()[0].strokes(); assert_eq!(strokes.len(), 2, "the two readable strokes survived"); assert_eq!(strokes[0].points, vec![(0.2, 0.5)]); assert_eq!( strokes[1].points, vec![(0.6, 0.5)], "a point without its comma is refused rather than read as one number" ); } /// A brush layer with nothing painted on it is still work — the user made the /// layer and set its adjustment — and must not disappear because it happens to /// render nothing yet. #[test] fn an_unpainted_brush_layer_still_persists() { let mut graph = EditGraph::default_chain(); let mut layer = MaskLayer::new("m1", MaskSource::brush()); layer.set_param("exposure", ParamId("exposure"), 1.0); graph.masks_mut().push(layer); let restored = round_trip(&graph); assert_eq!(restored.masks().len(), 1); assert_eq!(restored.masks().layers()[0].source, MaskSource::brush()); } // --------------------------------------------------------------------------- // Sync merge (FR-NC-9) // --------------------------------------------------------------------------- fn version_with(uuid: &str, revision: u64, build: impl FnOnce(&mut EditGraph)) -> Version { let mut graph = EditGraph::default_chain(); build(&mut graph); let mut v = Version::from_graph(uuid, "Default", &graph); v.revision = revision; v } fn lit(id: &str, ids: &[u32], ev: f32) -> MaskLayer { let mut layer = MaskLayer::new(id, regions(ids)); layer.set_param("exposure", ParamId("exposure"), ev); layer } /// The case the whole key-wise merge exists for, one level up: a layer added /// on the phone and a layer added on the desktop are not a conflict. #[test] fn disjoint_layers_from_two_devices_both_survive() { let base = version_with("default", 1, |_| {}); let mut ours = version_with("default", 2, |g| { g.masks_mut().push(lit("m1", &[1], 1.0)); }); let theirs = version_with("default", 2, |g| { g.masks_mut().push(lit("m2", &[2], -1.0)); }); let conflicts = ours.merge(&theirs, Some(&base)); assert!(conflicts.is_empty(), "different layers are not a conflict"); assert_eq!(ours.masks.len(), 2); assert!(ours.masks.get("m1").is_some()); assert!(ours.masks.get("m2").is_some()); } #[test] fn a_layer_only_the_remote_added_arrives() { let base = version_with("default", 1, |_| {}); let mut ours = version_with("default", 2, |_| {}); let theirs = version_with("default", 3, |g| { g.masks_mut().push(lit("m1", &[5], 2.0)); }); assert!(ours.merge(&theirs, Some(&base)).is_empty()); assert_eq!(ours.masks.len(), 1); } #[test] fn both_editing_one_layer_is_a_conflict_resolved_by_revision() { let base = version_with("default", 1, |g| { g.masks_mut().push(lit("m1", &[1], 0.5)); }); let mut ours = version_with("default", 2, |g| { g.masks_mut().push(lit("m1", &[1], 1.0)); }); let theirs = version_with("default", 9, |g| { g.masks_mut().push(lit("m1", &[1], -1.0)); }); let conflicts = ours.merge(&theirs, Some(&base)); assert_eq!(conflicts, vec![("mask".to_string(), "m1".to_string())]); let layer = ours.masks.get("m1").expect("layer survived"); let ev = layer .ops .iter() .find(|o| o.descriptor().id.0 == "exposure") .map(|o| o.param(ParamId("exposure"))); assert_eq!(ev, Some(-1.0), "the higher revision wins the layer whole"); } /// A layer deleted on one device and untouched on the other must stay /// deleted, or a mask the user removed reappears on every sync. #[test] fn a_remote_deletion_is_honoured() { let base = version_with("default", 1, |g| { g.masks_mut().push(lit("m1", &[1], 1.0)); }); let mut ours = version_with("default", 2, |g| { g.masks_mut().push(lit("m1", &[1], 1.0)); }); let theirs = version_with("default", 3, |_| {}); assert!(ours.merge(&theirs, Some(&base)).is_empty()); assert!(ours.masks.is_empty(), "the deletion should not be undone"); } #[test] fn an_untouched_layer_is_left_alone() { let base = version_with("default", 1, |g| { g.masks_mut().push(lit("m1", &[1], 1.0)); }); let mut ours = version_with("default", 2, |g| { g.masks_mut().push(lit("m1", &[1], 1.0)); g.masks_mut().push(lit("m2", &[2], 0.5)); }); let theirs = version_with("default", 3, |g| { g.masks_mut().push(lit("m1", &[1], 1.0)); }); assert!(ours.merge(&theirs, Some(&base)).is_empty()); assert_eq!(ours.masks.len(), 2, "our new layer is not a conflict"); } /// Print a real sidecar carrying a subject mask, for eyeballing the format. /// /// Ignored because it asserts nothing — it exists so the on-disk shape can be /// looked at without reverse-engineering it from the writer. /// /// ```sh /// cargo test -p dr-pipeline --test mask_sidecar show_a_sidecar -- --ignored --nocapture /// ``` #[test] #[ignore = "prints the format rather than checking it"] fn show_a_sidecar() { let mut graph = EditGraph::default_chain(); graph.set_param(dr_pipeline::OpId("exposure"), ParamId("exposure"), 0.35); let mut subject = MaskLayer::new( "m1", MaskSource::Subject { signature: 0x9f2c_41aa, index: 0, class: "person".into(), score: 0.94, }, ); subject.name = "person".into(); subject.invert = true; subject.feather = 0.02; subject.falloff = dr_pipeline::mask::Falloff::Gaussian; subject.morphology = dr_pipeline::mask::Morphology::Dilate; subject.morph_radius = 0.012; subject.set_param("saturation", ParamId("saturation"), -100.0); subject.set_param("exposure", ParamId("exposure"), -0.4); graph.masks_mut().push(subject); let mut grad = MaskLayer::new( "m2", MaskSource::Linear { centre: (0.5, 0.25), // The constant rather than four digits of it: clippy rejects the // literal, and a quarter turn written as a number is a quarter // turn nobody can see at a glance. angle: std::f32::consts::FRAC_PI_2, width: 0.4, }, ); grad.set_param("exposure", ParamId("exposure"), -0.6); graph.masks_mut().push(grad); let mut brush = brushed( "m3", &[ (false, 0.06, vec![(0.31, 0.44), (0.35, 0.46), (0.4, 0.52)]), (true, 0.03, vec![(0.36, 0.47)]), ], ); brush.name = "dodge".into(); graph.masks_mut().push(brush); let mut sidecar = Sidecar::new(); let mut v = Version::from_graph("default", "Default", &graph); v.is_default = true; v.rating = 4; sidecar.put(v); println!("\n{}", sidecar.to_text()); } /// A category mask is a name plus a signature, and both have to survive. /// /// The name especially. `MaskSource::Category` stores one rather than an index /// precisely so that editing `models/scene/categories.txt` cannot repoint a /// stored layer — and that reasoning is worth nothing if the name is what the /// sidecar drops. #[test] fn category_masks_keep_their_name() { let mut graph = EditGraph::default_chain(); let mut sky = MaskLayer::new( "m1", MaskSource::Category { signature: 0x5EED_1234, name: "sky".into(), }, ); sky.name = "sky".into(); sky.feather = 0.03; sky.set_param("exposure", ParamId("exposure"), 0.5); graph.masks_mut().push(sky); // A multi-word name too: the writer emits `category = swimming pool` on one // line, and a reader that split on whitespace would silently truncate it to // a category no model has. let mut pool = MaskLayer::new( "m2", MaskSource::Category { signature: 0x5EED_1234, name: "swimming pool".into(), }, ); pool.set_param("saturation", ParamId("saturation"), -30.0); graph.masks_mut().push(pool); let restored = round_trip(&graph); let layers = restored.masks().layers(); assert_eq!(layers.len(), 2); assert_eq!(layers[0].source, graph.masks().layers()[0].source); assert_eq!(layers[1].source, graph.masks().layers()[1].source); assert!((layers[0].feather - 0.03).abs() < 1e-6); } /// The refine strictness survives, and its absence means zero. /// /// Both halves matter and they are different claims. A dropped strictness /// would reopen a sky the photographer had closed, silently, on the next /// launch — the mask would still be there and would simply have got the /// chimneys back. /// /// And a *missing* key has to read as zero rather than as the default the /// panel starts a new layer at, because that is what makes a file written /// before this control existed render exactly as it did then. A default of 4 /// on absence would quietly re-grade every stored category mask in the /// catalogue. #[test] fn a_categorys_refine_strictness_survives_and_defaults_off() { let mut graph = EditGraph::default_chain(); let mut sky = MaskLayer::new( "m1", MaskSource::Category { signature: 0x5EED_1234, name: "sky".into(), }, ); sky.refine = 3.7; graph.masks_mut().push(sky); // Left at zero, so the writer omits the key entirely — which is the case // the second half of this test is really about. let plain = MaskLayer::new( "m2", MaskSource::Category { signature: 0x5EED_1234, name: "water".into(), }, ); graph.masks_mut().push(plain); let text = { let mut sidecar = Sidecar::new(); sidecar.put(Version::from_graph("default", "Default", &graph)); sidecar.to_text() }; assert_eq!( text.matches("refine =").count(), 1, "only the layer that set one should write the key:\n{text}" ); let layers = round_trip(&graph); let layers = layers.masks().layers(); assert!( (layers[0].refine - 3.7).abs() < 1e-6, "got {}", layers[0].refine ); assert_eq!( layers[1].refine, 0.0, "an absent key is the model's own mask" ); } /// A file is not trusted to ask for a strictness off the end of the scale. /// /// Past the top of the range every colour fails the test, so the mask deletes /// itself — which reads as a lost edit rather than as a bad file. #[test] fn an_out_of_range_refine_is_clamped() { let mut graph = EditGraph::default_chain(); let mut sky = MaskLayer::new( "m1", MaskSource::Category { signature: 0x5EED_1234, name: "sky".into(), }, ); sky.refine = 3.0; graph.masks_mut().push(sky); // Edited in the written text rather than assembled by hand, so this test // cannot drift out of step with the format the writer actually emits. let mut sidecar = Sidecar::new(); sidecar.put(Version::from_graph("default", "Default", &graph)); let text = sidecar.to_text().replace("refine = 3", "refine = 900"); assert!(text.contains("refine = 900"), "the edit must have applied"); let parsed = Sidecar::parse(&text).expect("reparse"); let mut restored = EditGraph::default_chain(); parsed .versions .get("default") .expect("version survived") .apply(&mut restored) .expect_no_film(); let refine = restored.masks().layers()[0].refine; assert!( refine <= dr_pipeline::mask::MAX_REFINE, "a file asked for 900 and got {refine}" ); } /// A category is stale when its run is, exactly as a subject is. /// /// The coverage buffer lives in the session, not the sidecar, so a layer whose /// signature no longer matches is pointing at pixels that were never computed. /// Rendering it anyway would mask nothing and read as a broken adjustment. #[test] fn a_category_from_another_run_is_stale() { let layer = MaskLayer::new( "m1", MaskSource::Category { signature: 1, name: "sky".into(), }, ); assert!(layer.is_stale(2), "a different run must invalidate it"); assert!(!layer.is_stale(1), "the run it was built against must not"); } // --------------------------------------------------------------------------- // Stored coverage (dr_pipeline::coverage) // --------------------------------------------------------------------------- // // A subject or a category is stored as *identity* — which run, which instance, // which category — and identity alone is only enough while the run is still in // memory. These pin down the raster that goes beside it, which is what lets a // reopened photograph and a batch export render the layer without a model. use dr_pipeline::coverage::{Coverage, RENDERED_LEVELS}; use std::sync::Arc; const PROXY: (usize, usize) = (96, 64); fn subject(signature: u64, index: u32) -> MaskSource { MaskSource::Subject { signature, index, class: "dog".into(), score: 0.94, } } /// A soft-edged disc, which is the shape a model actually hands out: a coarse /// sigmoid with a shoulder several pixels wide. fn a_disc() -> Vec { let (w, h) = PROXY; let mut values = vec![0u8; w * h]; for y in 0..h { for x in 0..w { let dx = (x as f32 - 40.0) / 20.0; let dy = (y as f32 - 30.0) / 20.0; let r = (dx * dx + dy * dy).sqrt(); values[y * w + x] = ((1.0 / (1.0 + ((r - 1.0) * 4.0).exp())) * 255.0) as u8; } } values } /// What the renderer would make of a coverage: the distance transform reads /// `>= 128` and nothing else, so this is the whole of the information a stored /// mask has to preserve. fn inside(values: &[u8]) -> Vec { values.iter().map(|&v| v >= 128).collect() } fn with_coverage(id: &str, source: MaskSource, values: &[u8]) -> MaskLayer { let mut layer = MaskLayer::new(id, source); layer.set_param("exposure", ParamId("exposure"), 0.75); layer.coverage = Some(Arc::new( Coverage::encode(values, PROXY.0, PROXY.1, RENDERED_LEVELS).expect("a disc should encode"), )); layer } /// The bug this whole thing exists for: without the raster, reopening the /// photograph gave the layer nothing to be, and the local adjustment was /// silently absent until somebody pressed "find subjects". #[test] fn a_subjects_coverage_survives_a_round_trip() { let values = a_disc(); let mut graph = EditGraph::default_chain(); graph .masks_mut() .push(with_coverage("m1", subject(0x1234, 2), &values)); let restored = round_trip(&graph); let layer = &restored.masks().layers()[0]; assert_eq!( layer.source, subject(0x1234, 2), "the identity is still there" ); let coverage = layer.coverage.as_ref().expect("the pixels came back too"); assert_eq!((coverage.width(), coverage.height()), PROXY); assert_eq!( inside(&coverage.decode()), inside(&values), "every pixel must fall on the same side of the threshold" ); } #[test] fn a_categorys_coverage_survives_a_round_trip() { let values = a_disc(); let source = MaskSource::Category { signature: 0x5678, name: "sky".into(), }; let mut graph = EditGraph::default_chain(); graph .masks_mut() .push(with_coverage("m1", source.clone(), &values)); let restored = round_trip(&graph); let layer = &restored.masks().layers()[0]; assert_eq!(layer.source, source); assert_eq!( inside(&layer.coverage.as_ref().expect("coverage").decode()), inside(&values) ); } /// The property that lets a caller skip an upload by comparing content — now /// with several kilobytes of run-length payload in the file. An encoder that /// re-coded the same raster differently on the way out would put a spurious /// upload on every save. #[test] fn a_stored_coverage_writes_the_same_bytes_every_time() { let mut graph = EditGraph::default_chain(); graph .masks_mut() .push(with_coverage("m1", subject(1, 0), &a_disc())); let mut sidecar = Sidecar::new(); sidecar.put(Version::from_graph("default", "Default", &graph)); let once = sidecar.to_text(); let twice = Sidecar::parse(&once).expect("reparse").to_text(); assert_eq!(once, twice); } /// The line goes last in its block, and that is a claim about reading the file /// by hand: it is thousands of characters against a dozen everywhere else, and /// a sidecar is what somebody opens when an edit has gone wrong (ARCH §6.12). #[test] fn the_coverage_line_comes_after_everything_a_human_is_looking_for() { let mut graph = EditGraph::default_chain(); graph .masks_mut() .push(with_coverage("m1", subject(1, 0), &a_disc())); let mut sidecar = Sidecar::new(); sidecar.put(Version::from_graph("default", "Default", &graph)); let text = sidecar.to_text(); let block = text.split("[mask ").nth(1).expect("a mask block"); let keys: Vec<&str> = block .lines() .filter_map(|l| l.split_once(" = ")) .map(|(k, _)| k) .collect(); assert_eq!( keys.last(), Some(&"coverage"), "coverage should be the last key in the block: {keys:?}" ); assert!( keys.contains(&"class") && keys.contains(&"exposure.exposure"), "and everything else should still be above it: {keys:?}" ); assert_eq!( block.lines().filter(|l| l.starts_with("coverage")).count(), 1, "one line, because a node is a line and the merge is key-wise" ); } /// Version skew, backwards: a file written before any of this existed. /// /// The layer must load and behave exactly as it did then — which is to say it /// needs the model run — rather than reading as a mask with no pixels. #[test] fn a_sidecar_written_before_coverage_existed_still_loads() { let text = "\ drsc 1 [version default] name = Default default = 1 revision = 3 [mask default m1] name = Dog source = subject signature = 4660 index = 2 class = dog score = 0.94 exposure.exposure = 0.75 "; let sidecar = Sidecar::parse(text).expect("an old file must still parse"); let mut graph = EditGraph::default_chain(); sidecar .versions .get("default") .expect("version") .apply(&mut graph) .expect_no_film(); let layer = &graph.masks().layers()[0]; assert_eq!(layer.source, subject(4660, 2)); assert_eq!(layer.name, "Dog"); assert!( layer.coverage.is_none(), "absent means absent, not an empty mask" ); } /// Version skew, forwards: a coverage this build cannot make sense of. /// /// The stand-in for a payload written by a build that means something else by /// the key. It costs the pixels — a model run — and must not cost the layer, /// because the layer is the edit and the raster is a cache of it. #[test] fn an_unreadable_coverage_costs_the_pixels_and_not_the_layer() { let text = "\ drsc 1 [version default] name = Default default = 1 [mask default m1] name = Dog source = subject signature = 4660 index = 2 class = dog score = 0.94 exposure.exposure = 0.75 coverage = 96 64 999 not-a-payload "; let sidecar = Sidecar::parse(text).expect("parse"); let mut graph = EditGraph::default_chain(); sidecar .versions .get("default") .expect("version") .apply(&mut graph) .expect_no_film(); let layer = &graph.masks().layers()[0]; assert_eq!(layer.source, subject(4660, 2)); assert_eq!(layer.name, "Dog"); assert!(layer.coverage.is_none()); assert_eq!( layer .ops .iter() .find(|o| o.descriptor().id.0 == "exposure") .map(|o| o.param(ParamId("exposure"))), Some(0.75), "the adjustment is the thing that must not be lost" ); } /// A raster only means anything against a source a model produced. A gradient /// carrying one is a hand-edited or mis-written file, and honouring it would /// upload a buffer nothing samples. #[test] fn coverage_is_not_loaded_onto_a_source_with_no_model_behind_it() { let payload = Coverage::encode(&a_disc(), PROXY.0, PROXY.1, RENDERED_LEVELS) .expect("encode") .to_text(); let text = format!( "drsc 1\n\n[version default]\nname = Default\ndefault = 1\n\n\ [mask default m1]\nsource = radial\ncentre = 0.5 0.5\nradii = 0.25 0.25\n\ coverage = {payload}\n" ); let sidecar = Sidecar::parse(&text).expect("parse"); let mut graph = EditGraph::default_chain(); sidecar .versions .get("default") .expect("version") .apply(&mut graph) .expect_no_film(); let layer = &graph.masks().layers()[0]; assert!(matches!(layer.source, MaskSource::Radial { .. })); assert!(layer.coverage.is_none()); } /// TRACES: FR-NC-9 /// One device has run the model and the other has not. That is the same edit. /// /// The merge decides "did this device change the layer" by comparing layers, /// so a cached raster taking part would make a photograph opened on the phone /// conflict with itself on the desktop — and, with `remote_wins`, resolve the /// conflict by discarding the only copy of the pixels. #[test] fn a_coverage_one_device_has_and_the_other_lacks_is_not_a_conflict() { let layer = |with: bool| { let mut l = lit("m1", &[1], 1.0); l.source = subject(7, 0); if with { l.coverage = Some(Arc::new( Coverage::encode(&a_disc(), PROXY.0, PROXY.1, RENDERED_LEVELS).expect("encode"), )); } l }; let base = version_with("default", 1, |g| { g.masks_mut().push(layer(false)); }); let mut ours = version_with("default", 2, |g| { g.masks_mut().push(layer(true)); }); let theirs = version_with("default", 9, |g| { g.masks_mut().push(layer(false)); }); let conflicts = ours.merge(&theirs, Some(&base)); assert!( conflicts.is_empty(), "running the model is not an edit: {conflicts:?}" ); assert!( ours.masks.get("m1").expect("layer").coverage.is_some(), "and the side that has the pixels keeps them" ); } /// The other half of the same claim: a real edit is still a conflict when both /// sides also happen to hold coverage. #[test] fn a_real_edit_is_still_a_conflict_with_coverage_present() { let stored = || { Some(Arc::new( Coverage::encode(&a_disc(), PROXY.0, PROXY.1, RENDERED_LEVELS).expect("encode"), )) }; let layer = |ev: f32| { let mut l = lit("m1", &[1], ev); l.source = subject(7, 0); l.coverage = stored(); l }; let base = version_with("default", 1, |g| { g.masks_mut().push(layer(0.5)); }); let mut ours = version_with("default", 2, |g| { g.masks_mut().push(layer(1.0)); }); let theirs = version_with("default", 9, |g| { g.masks_mut().push(layer(-1.0)); }); assert_eq!( ours.merge(&theirs, Some(&base)), vec![("mask".to_string(), "m1".to_string())] ); }