From 3eb7ac7c5eeb8b6b2daa20d63ebb2aa7e2ef3931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Kozlovsk=C3=BD?= Date: Tue, 3 Nov 2026 12:47:53 +0100 Subject: [PATCH] Hold the probe build in the remaining crypto-pick reading tests The "while reading" sections of the waiting-click, in-order and undo/redo tests now hold the build with the Held wrapper until their asserts are done, instead of relying on a 4K plate being slow to read. The undo tests use the shared helpers from ui_crypto_pick.rs, so they now go through crypto_exr with k = 1 rather than a copy without it. --- crates/ui-egui/tests/it/ui_crypto_pick.rs | 92 ++++++++++++--- .../ui-egui/tests/it/ui_crypto_pick_undo.rs | 105 ++++++------------ 2 files changed, 109 insertions(+), 88 deletions(-) diff --git a/crates/ui-egui/tests/it/ui_crypto_pick.rs b/crates/ui-egui/tests/it/ui_crypto_pick.rs index f75f0c8c6..0d1767c7e 100644 --- a/crates/ui-egui/tests/it/ui_crypto_pick.rs +++ b/crates/ui-egui/tests/it/ui_crypto_pick.rs @@ -23,13 +23,13 @@ const MAC_HINT: &str = "Click: replace · Shift: add · Option or Cmd: remove"; const PAUSED: &str = "Stop playback to pick"; /// The ID of the 37 × 29 px cell at pixel (x, y) of a full-size frame. -fn cell(x: usize, y: usize) -> String { +pub(crate) fn cell(x: usize, y: usize) -> String { format!("c{}_{}", x / 37, y / 29) } /// A `w` × `h` EXR (white RGBA) whose CryptoObject has an ID per cell of a `k`-times larger frame /// (`k` = 2: a half-size proxy), all named in its manifest. -fn crypto_exr(dir: &std::path::Path, name: &str, w: usize, h: usize, k: usize) -> String { +pub(crate) fn crypto_exr(dir: &std::path::Path, name: &str, w: usize, h: usize, k: usize) -> String { use exr::prelude::*; let ids: Vec = (0..w * h).map(|i| crypto_float(crypto_hash(&cell(i % w * k, i / w * k)))).collect(); let ch = |n: &str, v: Vec| AnyChannel::new(n, FlatSamples::F32(v)); @@ -105,7 +105,7 @@ fn app(s: Session, ec: f32) -> EffectcraftApp { app } -fn settle(h: &mut Harness<'_, EffectcraftApp>) { +pub(crate) fn settle(h: &mut Harness<'_, EffectcraftApp>) { for _ in 0..1500 { h.step(); if h.state().frames.inflight() == 0 && h.state().frames.last_ms.lock().is_ok_and(|v| *v > 0.0) { @@ -116,12 +116,12 @@ fn settle(h: &mut Harness<'_, EffectcraftApp>) { h.run_steps(4); } -fn chip(h: &Harness<'_, EffectcraftApp>) -> Option { +pub(crate) fn chip(h: &Harness<'_, EffectcraftApp>) -> Option { h.state().auto.find("viewer.cryptoPick").map(|e| e.label.clone()) } /// Step until the chip stops saying the probe is being built (the job runs on its own thread). -fn wait_probe(h: &mut Harness<'_, EffectcraftApp>) { +pub(crate) fn wait_probe(h: &mut Harness<'_, EffectcraftApp>) { for _ in 0..3000 { h.step(); if chip(h).as_deref() != Some(READING) { @@ -153,7 +153,7 @@ fn click(h: &mut Harness<'_, EffectcraftApp>, pos: Pos2, modifiers: Modifiers) { h.run_steps(2); } -fn at(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 { +pub(crate) fn at(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 { let e = h.state().auto.find(id).unwrap_or_else(|| panic!("missing {id}")).clone(); pos2(e.rect[0] + e.rect[2] * 0.5, e.rect[1] + e.rect[3] * 0.5) } @@ -443,43 +443,74 @@ fn drain(h: &mut Harness<'_, EffectcraftApp>) { /// Waiting clicks: the first hover starts the probe; a click made meanwhile waits then picks; /// an edit, Esc, disarming, or hiding the viewer while it waits drops it. -fn waiting_clicks_are_dropped_by_edits_esc_disarm_and_hiding_the_viewer(h: &mut Harness<'_, EffectcraftApp>, ids: &Ids, a: Pos2, b: Pos2) { +/// Each section's probe build is held (`hold` / `log`) across its "while reading" / "waits" +/// asserts and the action that drops the click, then released before `wait_probe` / `drain` / +/// `hover` so a held build can finish. +fn waiting_clicks_are_dropped_by_edits_esc_disarm_and_hiding_the_viewer( + h: &mut Harness<'_, EffectcraftApp>, + ids: &Ids, + a: Pos2, + b: Pos2, + hold: &std::sync::atomic::AtomicBool, + log: &ReadLog, +) { + use std::sync::atomic::Ordering; // A click before the probe is built waits for it. // (egui reports the hover a pass after the pointer moves.) + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); h.event(Event::PointerMoved(a)); h.run_steps(2); + wait_held_id_read(h, log); assert_eq!(chip(h).as_deref(), Some(READING), "the first hover starts the probe off the UI thread"); press(h, a); assert_eq!(selection(h, ids), "", "the click waits"); + hold.store(false, Ordering::Release); wait_probe(h); h.run_steps(2); assert_eq!(selection(h, ids), "c27_20", "then picks"); assert_eq!(hover(h, b), "c50_40"); let steps = h.state().session.history.undo.len(); // An edit while a click waits drops the click. + // edit(41) drops the probe; press starts a new build (held). edit(42) cancels that build and + // drops the click — release so the cancelled build can leave the hold, then wait_probe. edit(h, 41.0); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); press(h, b); + wait_held_id_read(h, log); assert_eq!(chip(h).as_deref(), Some(READING)); edit(h, 42.0); + hold.store(false, Ordering::Release); wait_probe(h); h.run_steps(2); assert_eq!(selection(h, ids), "c27_20", "not picked after the edit"); assert_eq!(h.state().session.history.undo.len(), steps + 2, "only the two edits"); - // Esc while a click waits: no pick, disarmed. + // Esc while a click waits: no pick, disarmed. Hold the build started by press; Esc cancels it. edit(h, 43.0); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); press(h, b); + wait_held_id_read(h, log); + assert_eq!(chip(h).as_deref(), Some(READING)); h.key_press(egui::Key::Escape); h.run_steps(2); assert!(h.state().ui.fx_pick.is_none() && chip(h).is_none()); + hold.store(false, Ordering::Release); drain(h); assert_eq!(selection(h, ids), "c27_20"); // Disarmed while a click waits, then armed again: the old click is not made. + // arm() waits the probe — hold only after arming, across the READING assert and disarm. edit(h, 44.0); arm(h, ids); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); press(h, b); + wait_held_id_read(h, log); assert_eq!(chip(h).as_deref(), Some(READING)); h.state_mut().ui.fx_pick = None; h.run_steps(2); + hold.store(false, Ordering::Release); arm(h, ids); drain(h); assert_eq!(selection(h, ids), "c27_20", "the old click stays unmade"); @@ -487,11 +518,15 @@ fn waiting_clicks_are_dropped_by_edits_esc_disarm_and_hiding_the_viewer(h: &mut // The viewer hidden while a click waits (Effect Controls maximised), then shown again: the // pick still on, the old click is not made either. edit(h, 45.0); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); press(h, b); + wait_held_id_read(h, log); assert_eq!(chip(h).as_deref(), Some(READING)); h.state_mut().ui.maximized = Some(PanelKind::EffectControls); h.run_steps(2); assert!(h.state().auto.find("viewer.fxPick").is_none(), "the viewer is hidden"); + hold.store(false, Ordering::Release); drain(h); h.state_mut().ui.maximized = None; h.run_steps(2); @@ -555,17 +590,20 @@ fn popups_dialogs_and_comp_switches_end_or_block_them(h: &mut Harness<'_, Effect assert!(h.state().ui.fx_pick.is_none() && chip(h).is_none(), "ended with the comp switch"); } -/// The asynchronous probe (a 4K plate, so it takes a while): the first hover says it is reading, -/// a click made meanwhile waits and then picks; an edit, Esc or disarming while it waits drops -/// it. Popups and dialogs over the viewer take the pointer. Another selected layer keeps the -/// pick; deleting the layer or switching comps ends it. +/// The asynchronous probe (a 4K plate behind a blur; each "while reading" section holds the +/// build): the first hover says it is reading, a click made meanwhile waits and then picks; an +/// edit, Esc or disarming while it waits drops it. Popups and dialogs over the viewer take the +/// pointer. Another selected layer keeps the pick; deleting the layer or switching comps ends it. #[test] fn clicks_wait_for_the_probe_and_edits_popups_dialogs_and_comp_switches_end_or_block_them() { - let (s, ids) = crypto_session("async", 3840, 2160, true); + let (mut s, ids) = crypto_session("async", 3840, 2160, true); + let hold = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let log = ReadLog::default(); + s.footage = std::sync::Arc::new(Held(s.footage.clone(), hold.clone(), log.clone())); let mut h = harness(s, vec2(1280.0, 800.0), 1.0, 420.0); arm(&mut h, &ids); let (a, b) = (comp_to_screen(&h.ctx, [1017.5, 594.5]).unwrap(), comp_to_screen(&h.ctx, [1868.5, 1174.5]).unwrap()); - waiting_clicks_are_dropped_by_edits_esc_disarm_and_hiding_the_viewer(&mut h, &ids, a, b); + waiting_clicks_are_dropped_by_edits_esc_disarm_and_hiding_the_viewer(&mut h, &ids, a, b, &hold, &log); popups_dialogs_and_comp_switches_end_or_block_them(&mut h, &ids, b); let _ = std::fs::remove_dir_all(&ids.dir); } @@ -654,15 +692,23 @@ fn builds(h: &Harness<'_, EffectcraftApp>) -> u64 { h.state().session.crypto_probe_stats().1 } -/// Clicks made while the IDs are being read are all made, in order, once they are. +/// Clicks made while the IDs are being read (a 4K plate behind a blur; the build is held until +/// the clicks are checked) are all made, in order, once they are. #[test] fn every_click_made_while_reading_is_made_in_order() { - let (s, ids) = crypto_session("multi", 3840, 2160, true); + use std::sync::atomic::Ordering; + let (mut s, ids) = crypto_session("multi", 3840, 2160, true); + let hold = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let log = ReadLog::default(); + s.footage = std::sync::Arc::new(Held(s.footage.clone(), hold.clone(), log.clone())); let mut h = harness(s, vec2(1280.0, 800.0), 1.0, 420.0); arm(&mut h, &ids); let (a, b) = (comp_to_screen(&h.ctx, [1017.5, 594.5]).unwrap(), comp_to_screen(&h.ctx, [1868.5, 1174.5]).unwrap()); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); h.event(Event::PointerMoved(a)); h.run_steps(2); + wait_held_id_read(&mut h, &log); assert_eq!(chip(&h).as_deref(), Some(READING)); let steps = h.state().session.history.undo.len(); for (p, m) in [(a, Modifiers::NONE), (b, Modifiers::SHIFT), (a, Modifiers::ALT), (a, Modifiers::SHIFT)] { @@ -678,6 +724,7 @@ fn every_click_made_while_reading_is_made_in_order() { } assert_eq!(chip(&h).as_deref(), Some(READING), "all four made while reading"); assert_eq!(selection(&h, &ids), ""); + hold.store(false, Ordering::Release); wait_probe(&mut h); h.run_steps(3); assert_eq!(selection(&h, &ids), "c50_40, c27_20", "replace, add, remove, add, in order"); @@ -1159,6 +1206,19 @@ impl effectcraft_engine::render::FootageSource for Held { } } +/// Step until a held probe build has reached the ID (`aux`) read, so READING asserts are about a +/// build that is really running. +pub(crate) fn wait_held_id_read(h: &mut Harness<'_, EffectcraftApp>, log: &ReadLog) { + for _ in 0..3000 { + h.step(); + if log.lock().unwrap().iter().any(|(_, frame)| !*frame) { + return; + } + std::thread::sleep(std::time::Duration::from_millis(2)); + } + panic!("the held build never reached the ID read"); +} + /// Disarmed and armed again while a probe is built: the new hover waits for that build to end /// before it starts its own (one build at a time, so one probe's memory). #[test] diff --git a/crates/ui-egui/tests/it/ui_crypto_pick_undo.rs b/crates/ui-egui/tests/it/ui_crypto_pick_undo.rs index a83ba29e6..22aaeb9a5 100644 --- a/crates/ui-egui/tests/it/ui_crypto_pick_undo.rs +++ b/crates/ui-egui/tests/it/ui_crypto_pick_undo.rs @@ -6,14 +6,13 @@ //! viewer's chip and highlight, and the eyedropper's armed state are checked. Undo while the IDs //! are being read and with clicks waiting, across Open / Revert / New, and after Esc. -use crate::ui_crypto_pick::{Held, ReadLog}; +use crate::ui_crypto_pick::{Held, ReadLog, at, chip, crypto_exr, settle, wait_held_id_read, wait_probe}; use effectcraft_engine::Session; use effectcraft_engine::project::{LayerId, Resolution}; -use effectcraft_engine::raster::channels3d::{crypto_float, crypto_hash}; use effectcraft_ui_egui::EffectcraftApp; use effectcraft_ui_egui::dock::{self, PanelKind, SplitSize}; use effectcraft_ui_egui::panels::viewer::comp_to_screen; -use egui::{Event, Key, Modifiers, PointerButton, Pos2, pos2, vec2}; +use egui::{Event, Key, Modifiers, PointerButton, Pos2, vec2}; use egui_kittest::Harness; use serde_json::{Value, json}; use std::sync::atomic::Ordering; @@ -30,32 +29,7 @@ const CTRL: Modifiers = Modifiers { alt: false, ctrl: !MAC, shift: false, mac_cm const CTRL_SHIFT: Modifiers = Modifiers { alt: false, ctrl: !MAC, shift: true, mac_cmd: MAC, command: true }; // --------------------------------------------------------------------------------------------- -// Fixture (as in ui_crypto_pick.rs): an EXR whose CryptoObject has an ID per 37 × 29 px cell. - -fn cell(x: usize, y: usize) -> String { - format!("c{}_{}", x / 37, y / 29) -} - -fn crypto_exr(dir: &std::path::Path, name: &str, w: usize, h: usize) -> String { - use exr::prelude::*; - let ids: Vec = (0..w * h).map(|i| crypto_float(crypto_hash(&cell(i % w, i / w)))).collect(); - let ch = |n: &str, v: Vec| AnyChannel::new(n, FlatSamples::F32(v)); - let one = || vec![1.0f32; w * h]; - let channels = AnyChannels::sort( - vec![ch("R", one()), ch("G", one()), ch("B", one()), ch("A", one()), ch("CryptoObject00.R", ids), ch("CryptoObject00.G", one())].into(), - ); - let mut layer = Layer::new((w, h), LayerAttributes::default(), Encoding::FAST_LOSSLESS, channels); - let names: serde_json::Map = (0..w.div_ceil(37)) - .flat_map(|x| (0..h.div_ceil(29)).map(move |y| cell(x * 37, y * 29))) - .map(|n| (n.clone(), json!(format!("{:08x}", crypto_hash(&n))))) - .collect(); - for (key, v) in [("name", "CryptoObject".to_string()), ("manifest", Value::Object(names).to_string())] { - layer.attributes.other.insert(Text::from(format!("cryptomatte/f834d0a/{key}").as_str()), AttributeValue::Text(Text::from(v.as_str()))); - } - let path = dir.join(name); - Image::from_layer(layer).write().to_file(&path).unwrap(); - path.to_string_lossy().to_string() -} +// Fixture: session / populate / harness (shared EXR helpers from ui_crypto_pick.rs). struct Fix { comp: u64, @@ -74,7 +48,7 @@ struct Fix { fn session(tag: &str, w: usize, h: usize, blur: bool, precomp: bool) -> (Session, Fix) { let dir = std::env::temp_dir().join(format!("ec-ui-crypick-undo-{tag}-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); - let path = crypto_exr(&dir, "plate.exr", w, h); + let path = crypto_exr(&dir, "plate.exr", w, h, 1); let mut s = effectcraft_host::session(); let f = populate(&mut s, &path, w, h, blur, precomp, dir); (s, f) @@ -125,36 +99,11 @@ fn harness(s: Session, size: egui::Vec2) -> Harness<'static, EffectcraftApp> { h } -fn settle(h: &mut Harness<'_, EffectcraftApp>) { - for _ in 0..1500 { - h.step(); - if h.state().frames.inflight() == 0 && h.state().frames.last_ms.lock().is_ok_and(|v| *v > 0.0) { - break; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - } - h.run_steps(4); -} - -fn chip(h: &Harness<'_, EffectcraftApp>) -> Option { - h.state().auto.find("viewer.cryptoPick").map(|e| e.label.clone()) -} - fn highlight(h: &Harness<'_, EffectcraftApp>) -> bool { h.state().auto.find("viewer.cryptoPick.highlight").is_some() } -fn wait_probe(h: &mut Harness<'_, EffectcraftApp>) { - for _ in 0..3000 { - h.step(); - if chip(h).as_deref() != Some(READING) { - return; - } - std::thread::sleep(std::time::Duration::from_millis(2)); - } - panic!("the probe never finished"); -} - +/// Like `ui_crypto_pick::hover`, plus an extra step so the chip label is current after the probe. fn hover(h: &mut Harness<'_, EffectcraftApp>, pos: Pos2) -> String { h.event(Event::PointerMoved(pos)); h.step(); @@ -190,11 +139,6 @@ fn key(h: &mut Harness<'_, EffectcraftApp>, key: Key, modifiers: Modifiers) { h.run_steps(2); } -fn at(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 { - let e = h.state().auto.find(id).unwrap_or_else(|| panic!("missing {id}")).clone(); - pos2(e.rect[0] + e.rect[2] * 0.5, e.rect[1] + e.rect[3] * 0.5) -} - /// The Selection property uid of the layer's (first) Cryptomatte effect. fn sel_uid(s: &Session, comp: u64, layer: u64) -> Option { let l = s.project.comp(effectcraft_engine::project::ItemId(comp))?.layer(LayerId(layer))?; @@ -873,16 +817,7 @@ fn clicks_queued_while_reading_are_one_step_each_and_undo_in_order() { } assert_eq!(chip(&h).as_deref(), Some(READING), "all four made while reading"); assert_eq!(h.state().session.history.undo.len(), before, "none made yet"); - for _ in 0..3000 { - h.step(); - if log.lock().unwrap().iter().any(|(_, frame)| !*frame) { - break; - } - std::thread::sleep(std::time::Duration::from_millis(2)); - } - if !log.lock().unwrap().iter().any(|(_, frame)| !*frame) { - panic!("the held build never reached the ID read"); - } + wait_held_id_read(&mut h, &log); for _ in 0..10 { h.step(); std::thread::sleep(std::time::Duration::from_millis(2)); @@ -934,8 +869,11 @@ fn blur_value(h: &Harness<'_, EffectcraftApp>, f: &Fix) -> Value { #[test] fn undo_and_redo_while_reading_and_with_clicks_waiting() { - let (s, f) = session("reading", 3840, 2160, true, false); + let (mut s, f) = session("reading", 3840, 2160, true, false); let dims = [3840.0, 2160.0]; + let hold = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let log = ReadLog::default(); + s.footage = std::sync::Arc::new(Held(s.footage.clone(), hold.clone(), log.clone())); let mut h = harness(s, vec2(1280.0, 860.0)); let mut m = Model::new(&h, f.comp); let pl = f.plate; @@ -970,26 +908,38 @@ fn undo_and_redo_while_reading_and_with_clicks_waiting() { // (b) Ctrl+Z while the IDs are being read (a build in flight for the state undone). blur(&mut h, &mut m, 41.0); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); h.event(Event::PointerMoved(pa)); h.run_steps(2); + wait_held_id_read(&mut h, &log); assert_eq!(chip(&h).as_deref(), Some(READING)); undo_now(&mut h, &mut m, false); + hold.store(false, Ordering::Release); assert_eq!(blur_value(&h, &f), json!(40.0)); verify(&mut h, &f, &mut m, dims, "(b) undo while reading"); assert_eq!(hover(&mut h, pb), "c50_40", "the probe answers for the state undone to"); // ... and Ctrl+Shift+Z while reading. + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); h.state_mut().session.drop_crypto_probe(); h.event(Event::PointerMoved(pa)); h.run_steps(2); + wait_held_id_read(&mut h, &log); + assert_eq!(chip(&h).as_deref(), Some(READING)); undo_now(&mut h, &mut m, true); + hold.store(false, Ordering::Release); assert_eq!(blur_value(&h, &f), json!(41.0)); verify(&mut h, &f, &mut m, dims, "(b) redo while reading"); // (c) A click waiting for the probe, then Ctrl+Z: the undo goes first and the waiting click // is not made on the state undone to (it was aimed at the state before the undo). blur(&mut h, &mut m, 42.0); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); h.event(Event::PointerMoved(pb)); h.run_steps(2); + wait_held_id_read(&mut h, &log); assert_eq!(chip(&h).as_deref(), Some(READING)); press(&mut h, pb, Modifiers::SHIFT); assert_eq!(chip(&h).as_deref(), Some(READING), "the click waits"); @@ -997,6 +947,7 @@ fn undo_and_redo_while_reading_and_with_clicks_waiting() { undo_now(&mut h, &mut m, false); // (Asked at once: a status message goes after 5 s, which is 20 steps here.) assert!(h.state().ui.status.starts_with("Pick not made"), "the dropped click says so: {:?}", h.state().ui.status); + hold.store(false, Ordering::Release); wait_probe(&mut h); h.run_steps(3); verify(&mut h, &f, &mut m, dims, "(c) waiting click, then Ctrl+Z: the click is not made"); @@ -1023,26 +974,36 @@ fn undo_and_redo_while_reading_and_with_clicks_waiting() { blur(&mut h, &mut m, 43.0); verify(&mut h, &f, &mut m, dims, "(e) blur 43"); // (Undo goes back to a render the layer cache keeps: forget it, so the probe takes a while.) + // Hold before undo_now: it steps the UI and would otherwise start (and finish) a rebuild. h.state().session.layer_cache.clear(); + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); undo_now(&mut h, &mut m, false); let pa = sp(&h, &m, a); h.event(Event::PointerMoved(pa)); h.run_steps(2); + wait_held_id_read(&mut h, &log); assert_eq!(chip(&h).as_deref(), Some(READING)); press(&mut h, pa, Modifiers::SHIFT); undo_now(&mut h, &mut m, true); + hold.store(false, Ordering::Release); wait_probe(&mut h); h.run_steps(3); verify(&mut h, &f, &mut m, dims, "(e) waiting click, then Ctrl+Shift+Z: the click is not made"); // (f) A click waiting, Ctrl+Z, another click: only the second is made, on the undone state. + hold.store(true, Ordering::Release); + log.lock().unwrap().clear(); blur(&mut h, &mut m, 44.0); h.event(Event::PointerMoved(pa)); h.run_steps(2); + wait_held_id_read(&mut h, &log); assert_eq!(chip(&h).as_deref(), Some(READING)); press(&mut h, pa, Modifiers::ALT); undo_now(&mut h, &mut m, false); press(&mut h, pa, Modifiers::SHIFT); + assert_eq!(chip(&h).as_deref(), Some(READING), "both clicks made while the build is held"); + hold.store(false, Ordering::Release); wait_probe(&mut h); h.run_steps(3); let mut o = m.state().clone();