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 <noreply@anthropic.com>
This commit is contained in:
mtlcl410
2026-10-23 22:25:57 +08:00
co-authored by Claude Opus 5.5
parent a8beb90ccf
commit a7afaacbb9
2 changed files with 39 additions and 2 deletions
+26 -1
View File
@@ -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()]);
+13 -1
View File
@@ -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;