mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 05:28:09 -06:00
test: enforce page press routing and record integration regression evidence
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user