test: enforce page press routing and record integration regression evidence

This commit is contained in:
FredApps
2026-10-30 12:38:09 +01:00
parent bdf56209ce
commit 6d7f739e3c
3 changed files with 123 additions and 2 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ PdfCraft is a clean-room, open-source, Rust-native PDF application targeting Ado
- **Layering.** Nothing below L7 depends on egui/winit/eframe/rfd (`plan/architecture.md` §3). `cos`/`filters`/`crypt`/`arlington` stay standalone.
- **Page input.** Page modules read `PagePointer` (`crates/ui-egui/src/page_pointer.rs`), never egui's raw pointer state (`hover_pos`, `interact_pos`, `latest_pos`, `press_origin`, button reads); `page_pointer::tests::page_modules_read_the_pointer_only_through_page_pointer` enforces it.
- **Page cursor.** Page modules claim the cursor for what is under the pointer (`view.cursor.claim(Layer::…, icon)`, `crates/ui-egui/src/canvas/page_cursor.rs`), never set it (`set_cursor_icon`, `cursor_icon =`, `on_hover_cursor`); `document_area` sets it once, from the claim on the highest layer the active tool reaches. `page_pointer::tests::page_modules_claim_the_cursor_and_never_set_it` enforces it.
- **Page presses.** A page module decides what of its own is under a point in one hit test, which returns a `Hit` (owner, layer, cursor, tip; `crates/ui-egui/src/canvas/page_router.rs`) and gives its cursor, its tip and its press. It acts on a press only when the router says the press is its own (`view.router.owner(page)`), never by testing the press point again or by asking another module whether it wants it. Escape cancels any press the pages hold (`cancel_on_escape`), and that press is ignored until the button is up.
- **Page presses.** A page module decides what of its own is under a point in one hit test, which returns a `Hit` (owner, layer, cursor, tip; `crates/ui-egui/src/canvas/page_router.rs`) and gives its cursor, its tip and its press. It acts on a press only when the router says the press is its own (`view.router.owner(page)`), never by testing the press point again or by asking another module whether it wants it. Escape cancels any press the pages hold (`cancel_on_escape`), and that press is ignored until the button is up. `page_modules_take_presses_from_the_router_without_repeating_hit_tests` guards the centralized hit calls and captured-owner handlers; runtime routing tests cover gesture behavior.
- **Fidelity.** The PDF object graph is the model. Preserve unknown data. Saves are incremental unless a full rewrite is required. Never silently drop or repair data without recording it.
- **Never panic.** Every PDF, script, MCP call, settings file and keystroke is untrusted input, and none of it may crash the app or lose the user's work. This outranks feature work. Return `Result` (the crate's error enum) and propagate with `?`, or fall back leniently and record it. In non-test code:
- No `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` unless it is provably infallible, with a comment saying why.
@@ -361,3 +361,84 @@ fn page_modules_claim_the_cursor_and_never_set_it() {
);
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
/// Hit producers are called only by the central routing table, not again by an input handler.
const PAGE_HIT_CALLS: [&str; 8] = [
"prepare::hit(",
"forms_ui::hit(",
"comments::hit(",
"edit_text_ui::hit(",
"edit_text_ui::picture_hit(",
"edit_text_ui::picture_handle_hit(",
"object_ui::hit(",
"content_ui::hit(",
];
fn routed_press_handler(code: &str) -> bool {
let code = code.lines().filter(|line| !line.trim_start().starts_with("//")).collect::<Vec<_>>().join("\n");
code.contains("router.owner(")
&& !PAGE_HIT_CALLS.iter().any(|call| code.contains(call))
&& !uses(&code, "hit(")
&& !uses(&code, "body_hit(")
&& !uses(&code, "handle_hit(")
}
/// Source-level guard for the Page presses rule, alongside the raw-pointer and cursor guards.
/// Runtime routing, capture lifetime and Escape behavior have separate behavioral tests.
#[test]
fn page_modules_take_presses_from_the_router_without_repeating_hit_tests() {
let mut problems = misuses(
&PAGE_HIT_CALLS,
&[("canvas.rs", "at", "the central PageHits table")],
"repeats a module hit test outside PageHits::at; use the captured router owner",
"no longer collects module hits",
);
problems.extend(misuses(
&["router.press("],
&[("canvas.rs", "page_tool_input", "the central press capture")],
"captures a page press outside the central routing path",
"no longer captures page presses",
));
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
for (file, function) in [
("tools/forms_ui.rs", "page_input"),
("tools/prepare.rs", "page_input"),
("tools/content_ui.rs", "page_input"),
("tools/edit_text_ui.rs", "page_input"),
("tools/edit_text_ui/image.rs", "image_input"),
("tools/object_ui.rs", "input"),
("tools/comments/input.rs", "select_input"),
] {
let source = std::fs::read_to_string(src.join(file)).expect("page input module");
let body = source
.lines()
.skip_while(|line| declares(line) != Some(function))
.take_while(|line| *line != "}")
.collect::<Vec<_>>()
.join("\n");
// Forms reuse their hit test for hover text and to require release over the captured
// widget. Neither chooses the press owner. Pin both calls to the current pointer `p`,
// not the press origin, and reject an additional call or a changed argument.
let body = if file == "tools/forms_ui.rs" {
let call = "hit(xf, info, page, form, allowed, p)";
assert_eq!(body.matches(call).count(), 2, "only the existing hover and release checks are exempt");
body.replace(call, "hover_or_release_check")
} else {
body
};
if !routed_press_handler(&body) {
problems.push(format!("src/{file}: {function} must consume the captured router owner without repeating a hit test"));
}
}
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
#[test]
fn the_page_press_guard_rejects_missing_owners_and_second_hit_tests() {
assert!(routed_press_handler("let owner = view.router.owner(page);"));
assert!(!routed_press_handler("// view.router.owner(page)\nlet owner = None;"));
assert!(!routed_press_handler("let owner = view.router.owner(page);\nlet again = cx.hit(origin);"));
for call in PAGE_HIT_CALLS {
assert!(!routed_press_handler(&format!("let owner = view.router.owner(page);\n{call}origin);")), "{call}");
}
}
+41 -1
View File
@@ -2,7 +2,7 @@
Base: 1d1cbc4ec92012767f5b48c92b451e15eb401c42
Validation: pending combined GitHub CI. No fresh manual GUI pass has been performed.
Validation: combined GitHub CI passed on bdf56209ce6a22980ef488eec92a55073f5a4e18 (Linux and Windows, run 41038860617). No fresh manual GUI pass has been performed. The subsequent architectural-guard additions require a fresh run.
All included source heads are preserved as merge parents, retaining original authorship. No upstream PR was merged. Conflicts were resolved after the user requested inclusion despite merge conflicts.
## Included
@@ -41,3 +41,43 @@ Excluded held PRs #4648, #2877 and #3762 were not included.
Source reviews and individual hosted CI do not establish combined-build safety; the integration gate must pass.
Integration-specific changes: preserve hard/soft wrap metadata together with per-row alignment; share CCITT/JBIG2 export paths with the 32 MiB globals cap retained; keep downsampling and RGB ownership guards with JBIG2 encoding; retain all translation keys and both sets of regression tests.
## Per-PR regression evidence
Evidence: [Linux job 124006360624](https://github.com/FredApps/pdfcraft/actions/runs/41038860617/job/124006360624) on bdf56209ce6a22980ef488eec92a55073f5a4e18: 5,627 passed, 15 skipped. The Windows leg of the same run also passed.
For each source-head diff above, the added `#[test]` function names were matched against PASS/SKIP entries in the combined Linux log. Every listed source regression was found passing, none skipped or missing. Counts overlap because stacked PRs carry their prerequisites; they must not be summed as distinct tests. This records source regression coverage, not manual feature acceptance.
- #4304: 11/11 added source regression names passed; 0 missing, 0 skipped.
- #4320: 21/21 added source regression names passed; 0 missing, 0 skipped.
- #4325: 18/18 added source regression names passed; 0 missing, 0 skipped.
- #4335: 19/19 added source regression names passed; 0 missing, 0 skipped.
- #4384: 39/39 added source regression names passed; 0 missing, 0 skipped.
- #4404: 50/50 added source regression names passed; 0 missing, 0 skipped.
- #4805: 36/36 added source regression names passed; 0 missing, 0 skipped.
- #5300: 18/18 added source regression names passed; 0 missing, 0 skipped.
- #4719: 6/6 added source regression names passed; 0 missing, 0 skipped.
- #4541: 5/5 added source regression names passed; 0 missing, 0 skipped.
- #2815: 35/35 added source regression names passed; 0 missing, 0 skipped.
- #4620: 1/1 added source regression names passed; 0 missing, 0 skipped.
- #2529: 35/35 added source regression names passed; 0 missing, 0 skipped.
- #3789: 5/5 added source regression names passed; 0 missing, 0 skipped.
- #3779: 4/4 added source regression names passed; 0 missing, 0 skipped.
- #2960: 1/1 added source regression names passed; 0 missing, 0 skipped.
- #5208: 2/2 added source regression names passed; 0 missing, 0 skipped.
- #4596: 1/1 added source regression names passed; 0 missing, 0 skipped.
- #3636: 3/3 added source regression names passed; 0 missing, 0 skipped.
- #4494: 4/4 added source regression names passed; 0 missing, 0 skipped.
- #4013: 4/4 added source regression names passed; 0 missing, 0 skipped.
- #4180: 54/54 added source regression names passed; 0 missing, 0 skipped.
- #4179: 20/20 added source regression names passed; 0 missing, 0 skipped.
- #5134: the source diff adds no test function. Combined gates passed; no new manual scrollbar acceptance is claimed.
- #2133: 1/1 added source regression names passed; 0 missing, 0 skipped.
- #2635: 6/6 added source regression names passed; 0 missing, 0 skipped.
- #1952: 6/6 added source regression names passed; 0 missing, 0 skipped.
The conflict-specific tests `wrapped_rows_keep_alignment_separate_from_hard_break_markers` and `black_and_white_images_become_jbig2` (including the CCITT-to-JBIG2 conversion/render assertions) passed in that same run. Source tests for alignment/history, paragraph boundaries, field calculations and button export values remain present and passed.
## Protect dependency check
The integration base main commit 1d1cbc4ec92012767f5b48c92b451e15eb401c42 already locks registry `getrandom` 0.4.3 (checksum 300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099). #4013 makes it a direct engine dependency rather than introducing an unrecorded package. The combined dependency-policy and asset gates passed. The fail-closed randomness regression also passed.