From a7afaacbb9d313209e45ee9a527bdbcc6d0c3e60 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:49:43 +0800 Subject: [PATCH] M6: merged script output reports a document script's error once too (#3940) Review on #3985 (benandez): JsOutput::absorb reported a failing document script once, but append added every error again, so merging two outputs repeated it. pdfcraft_js now says whether an error is a document script's (is_doc_error, next to the doc_error format), and append skips one already reported; each field script error is still its own. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/js.rs | 27 ++++++++++++++++++++++++++- crates/js/src/lib.rs | 14 +++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs index 28e593531..301b0592f 100644 --- a/crates/engine/src/js.rs +++ b/crates/engine/src/js.rs @@ -44,7 +44,13 @@ impl JsOutput { self.alerts.extend(other.alerts); self.console.extend(other.console); self.requests.extend(other.requests); - self.errors.extend(other.errors); + // As in `absorb`: a document script's error, already reported, is not repeated; a field + // script's errors are each their own. + for e in other.errors { + if !(pdfcraft_js::is_doc_error(&e) && self.errors.contains(&e)) { + self.errors.push(e); + } + } } } @@ -455,6 +461,25 @@ mod review_tests { assert!(read <= 4 * n, "{read} characters read for {n} glyphs"); } + /// Review on #3985: merging two outputs reports a failing document script once, as one run + /// does, while each field script error stays. + #[test] + fn merged_output_reports_a_document_script_error_once() { + let run = |errors: &[&str]| JsOutput { errors: errors.iter().map(|e| e.to_string()).collect(), ..JsOutput::default() }; + let mut out = run(&["document script 2: TypeError: not a callable function", "ReferenceError: x is not defined"]); + out.append(run(&["document script 2: TypeError: not a callable function", "ReferenceError: x is not defined"])); + assert_eq!( + out.errors, + ["document script 2: TypeError: not a callable function", "ReferenceError: x is not defined", "ReferenceError: x is not defined"] + ); + // A field script's message that only looks like one (no script number) is still its own. + for look_alike in ["document script x: thrown by a field", "document script : thrown by a field"] { + out.append(run(&[look_alike])); + out.append(run(&[look_alike])); + assert_eq!(out.errors.iter().filter(|e| *e == look_alike).count(), 2, "{look_alike}"); + } + } + #[test] fn duplicate_field_names_diff_against_first_original() { let first = pdfcraft_js::FieldState::new("duplicate", pdfcraft_js::FieldType::Text, vec!["first".into()]); diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index f6d5a076c..cc835c79e 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -1274,9 +1274,21 @@ impl HostHooks for CheckedCodeFromStrings { } } +/// The prefix of a document script's error ([`doc_error`]). +const DOC_ERROR_PREFIX: &str = "document script "; + /// How [`Outcome::doc_errors`] names a failing document script: by its 1-based place. fn doc_error(index: usize, error: &dyn std::fmt::Display) -> String { - format!("document script {}: {error}", index.saturating_add(1)) + format!("{DOC_ERROR_PREFIX}{}: {error}", index.saturating_add(1)) +} + +/// Whether `error` is one of [`Outcome::doc_errors`]: a document script's failure, which repeats +/// each time the document scripts run again, unlike a field script's own error. +pub fn is_doc_error(error: &str) -> bool { + error + .strip_prefix(DOC_ERROR_PREFIX) + .and_then(|rest| rest.split_once(": ")) + .is_some_and(|(n, _)| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) } /// `doc_scripts` are the document scripts to run with their places among all of them;