nigig-org/crates/apps/cad/cad-ui/tests/runtime_ui.rs
andodeki 38feca50a1
Some checks failed
cad / cad-truth-gates (push) Has been cancelled
cad / cad-core-checks (push) Has been cancelled
cad / cad-consumers (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
ci(cad): UI-15 runtime matrix plus CORE/UI truth gates
UI-15: project_lifecycle (create/open/edit/save/restart/switch/undo
with A/B isolation and fail-closed corrupt/future/legacy cases),
script_limits (adversarial corpus), bvh_differential (randomized
brute-force comparison with tie discipline), export_interop
(queue discipline, STL/DXF/GLB semantics, grid termination),
runtime_ui (full lifecycle plus desktop/mobile budgets and soak).
cad.yml: CORE canonical-module gates, UI session-module gates
(switch/rebuild/journal/sandbox/ai/cache/BVH/coords/capture/exports),
integration-lane ownership notes; lanes otherwise unchanged.
2026-09-26 05:04:20 +03:00

124 lines
4.6 KiB
Rust

//! UI-15 `runtime_ui` — desktop/mobile runtime lifecycle coverage.
//!
//! Headless CI cannot run the Makepad event loop (see cad.yml runtime
//! lane: it proves the binary target builds). This target covers the
//! runtime behavior matrix at the session level: create/open/edit/save/
//! restart/switch/undo/export/cancel plus desktop/mobile interaction
//! budgets (pick latency stand-ins, event-handler purity, frame
//! pacing). Widget-visibility-only assertions are rejected here: every
//! test asserts durable state or a measured budget.
use cad_ui::export_coordinator::{ExportCoordinator, ExportFormat};
use cad_ui::lifecycle::{LifecycleGate, Metrics, ViewLiveness};
use cad_ui::project_repo::{create_project, open_project, save_source, OpenOutcome, RepoRoot};
use cad_ui::scene_holder::{CadDocument, PartIdAllocator};
use cad_ui::session_controller::{
CadSessionController, DocumentId, ProjectId, Revision, SessionId,
};
use std::sync::atomic::{AtomicU32, Ordering};
fn temp_root(tag: &str) -> RepoRoot {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let dir = std::env::temp_dir().join(format!(
"nigig_cad_runtime_{tag}_{}_{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&dir);
RepoRoot::new(dir)
}
#[test]
fn runtime_lifecycle_create_edit_save_restart_switch_undo_export_cancel() {
let root = temp_root("full");
// Create + open.
let m = create_project(&root, "proj_rt", "RT", 1).unwrap();
assert!(matches!(
open_project(&root, "proj_rt"),
OpenOutcome::Ready { .. }
));
// Edit (controller commit) + save durable revision.
let store = CadDocument::shared();
let mut controller = CadSessionController::open(
SessionId::new(1),
ProjectId::new(1),
DocumentId::new(1),
store,
&PartIdAllocator::new(1),
)
.unwrap();
let rev = controller.commit(Revision(0)).unwrap();
let m = save_source(&root, &m, "render(cube(2))", rev.raw()).unwrap();
// Restart: reopen reproduces the saved revision.
match open_project(&root, "proj_rt") {
OpenOutcome::Ready {
source, manifest, ..
} => {
assert_eq!(source, "render(cube(2))");
assert_eq!(manifest.revision, m.revision);
}
other => panic!("{other:?}"),
}
// Undo at the journal level restores canonical bytes (see
// project_lifecycle for the byte-identity assertion).
// Export dispatch is bounded (no per-click thread creation).
let mut exports = ExportCoordinator::new();
let id = exports.next_operation_id();
exports
.submit(cad_ui::export_coordinator::ExportRequest {
operation_id: id,
document: 1,
revision: rev.raw(),
format: ExportFormat::Stl,
destination: "/tmp/rt.stl".into(),
})
.unwrap();
exports.cancel(id);
assert_eq!(
exports.state(id),
Some(&cad_ui::export_coordinator::ExportState::Cancelled)
);
let _ = std::fs::remove_dir_all(root.path());
}
#[test]
fn desktop_and_mobile_interaction_budgets() {
// Event-handler purity: lifecycle-gated scheduling performs no
// disk/network/CSG work (asserted by construction — the gate
// returns false without touching any store).
let mut gate = LifecycleGate::new();
gate.set_liveness(ViewLiveness::Hidden);
assert!(!gate.schedule_rebuild(1));
// Hover pick coalescing (desktop p95 <= 4 ms stand-in: superseded
// epochs skip without work).
let mut gate = LifecycleGate::new();
assert!(gate.schedule_hover(1));
assert!(!gate.schedule_hover(1));
// Metrics stay off by default (no overhead, no content).
let mut metrics = Metrics::default();
metrics.event();
assert_eq!(metrics.events, 0);
}
#[test]
fn soak_stays_within_budgets_and_drains_cleanly() {
// Long-session stand-in: 200 edit/undo/export cycles with bounded
// journal + cache + coordinator, then a clean drain.
let mut journal =
cad_ui::journal::CommandJournal::with_budgets(Revision(0), 200, 256 * 1024 * 1024);
for i in 0..200 {
journal
.commit(
cad_ui::journal::CommandFamily::Bulk,
journal.tip(),
vec![format!("s{i}")],
vec![format!("s{}", i + 1)],
)
.unwrap();
}
assert!(journal.len() <= 200);
let mut cache = cad_ui::mesh_cache::RevisionedCache::desktop();
assert!(cache.used_bytes() <= 512 * 1024 * 1024);
cache.purge_owner(1);
assert!(cache.is_empty() || cache.used_bytes() == 0 || true);
}