Fuzz harness executed successfully: - No crashes or panics on startup - Process ran for full 0.5-second duration - Coverage expanded from 124 to 518+ edges - Generated 203+ test cases from empty corpus - Clean exit via timeout (expected) Verification: notes/bf-1bpzw.md Acceptance criteria: PASS (all 4 criteria met)
14 KiB
Errors Array Format and Test Integration Guide
Overview
This document provides comprehensive documentation of the errors array format in pdftract extraction results and how to write test assertions for error conditions.
Errors Array Structure
JSON Output Format (Canonical)
When extraction results are converted to JSON via result_to_output(), errors are exposed in the top-level errors array:
// In Output struct (schema/mod.rs line 1539)
pub struct Output {
pub schema_version: String,
pub metadata: DocumentMetadata,
pub pages: Vec<PageJson>,
pub extraction_quality: ExtractionQuality,
pub errors: Vec<DiagnosticJson>, // ← Errors array here
}
DiagnosticJson Structure
Each error in the errors array has the following structure:
pub struct DiagnosticJson {
/// Stable string identifier (e.g., "STREAM_DECODE_ERROR")
pub code: String,
/// Human-readable description
pub message: String,
/// Severity level: "info", "warning", "error", or "fatal"
pub severity: String,
/// Page index where diagnostic occurred (null for document-level)
pub page_index: Option<usize>,
/// PDF object reference where issue originated
pub location: Option<ObjectLocationJson>,
/// Optional hint for resolution
pub hint: Option<String>,
}
ObjectLocationJson Structure
When a diagnostic includes a PDF object reference:
pub struct ObjectLocationJson {
pub object_number: u32, // Zero-based xref index
pub generation_number: u16, // Incremented on each save
}
Internal ExtractionResult Format
Before JSON conversion, errors exist in two locations within ExtractionResult:
1. Document-Level Diagnostics
pub struct ExtractionResult {
pub metadata: ExtractionMetadata,
// ... other fields
}
pub struct ExtractionMetadata {
pub error_count: usize, // Number of failed pages
pub diagnostics: Vec<String>, // ← Document-level diagnostics
// ... other fields
}
Purpose: Contains warnings and errors affecting the entire extraction.
Examples:
- "XREF_REPAIRED: xref reconstructed via forward scan"
- "TAGGED_PDF_STRUCT_TREE_DEFERRED: StructTree traversal deferred to Phase 7.1"
- "COVERAGE_WARNING: Some content not fully analyzed"
2. Per-Page Errors
pub struct PageResult {
pub index: usize,
pub page_number: u32,
pub error: Option<String>, // ← Per-page error if extraction failed
// ... other fields
}
Purpose: Contains error messages when a specific page fails to extract.
Example: "PAGE_EXTRACTION_FAILED: Mediabox missing or invalid"
Severity Levels
Diagnostics have four severity levels that determine their impact:
| Severity | Impact | Examples |
|---|---|---|
info |
Does not affect output validity | XREF_REPAIRED, TAGGED_PDF_STRUCT_TREE_DEFERRED |
warning |
Output usable but degraded | STRUCT_INVALID_NAME, FONT_GLYPH_UNMAPPED, STREAM_DECODE_ERROR |
error |
This region/page invalid; others OK | STREAM_BOMB, PAGE_OUT_OF_RANGE, REMOTE_FETCH_INTERRUPTED |
fatal |
Extraction aborted, no usable output | ENCRYPTION_UNSUPPORTED, REMOTE_TLS_FAILED |
Diagnostic Code Categories
Error codes follow naming conventions with prefixes indicating the layer:
| Category | Prefix | Phase Origin | Examples |
|---|---|---|---|
| Structure | STRUCT_* |
1.1 | STRUCT_INVALID_NAME, STRUCT_MISSING_KEY |
| Stream | STREAM_* |
1.5 | STREAM_DECODE_ERROR, STREAM_BOMB, STREAM_UNKNOWN_FILTER |
| XREF | XREF_* |
1.3 | XREF_REPAIRED, XREF_INVALID_STREAM_FORMAT |
| Encryption | ENCRYPTION_* |
1.4 | ENCRYPTION_UNSUPPORTED |
| Page | PAGE_* |
2.x | PAGE_MISSING_MEDIABOX |
| Font | FONT_* |
3.2 | FONT_GLYPH_UNMAPPED |
| Graphics State | GSTATE_* |
3.1 | GSTATE_INVALID_MATRIX |
| Layout | LAYOUT_* |
4.x | LAYOUTReadingOrderBroken |
| OCR | OCR_* |
5.x | OCR_LANGUAGE_INVALID |
| Remote | REMOTE_* |
1.8 | REMOTE_FETCH_INTERRUPTED |
| MCP | MCP_* |
6.7 | MCP_REQUEST_TIMEOUT |
| Cache | CACHE_* |
6.9 | CACHE_WRITE_FAILED |
Accessing Errors from Extraction Results
Method 1: From JSON Output (Recommended for Integration Tests)
use pdftract_core::{extract_pdf, ExtractionOptions, output::json::result_to_output};
let result = extract_pdf(&pdf_path, &ExtractionOptions::default())?;
let output = result_to_output(&result);
// Access all errors
for error in &output.errors {
println!("Code: {}", error.code);
println!("Message: {}", error.message);
println!("Severity: {}", error.severity);
if let Some(page_idx) = error.page_index {
println!("Page: {}", page_idx);
}
}
// Check for specific error codes
let has_decode_errors = output.errors
.iter()
.any(|e| e.code == "STREAM_DECODE_ERROR");
Method 2: From ExtractionResult (For Unit Tests)
use pdftract_core::{extract_pdf, ExtractionOptions};
let result = extract_pdf(&pdf_path, &ExtractionOptions::default())?;
// Document-level diagnostics
for diag in &result.metadata.diagnostics {
println!("{}", diag);
}
// Check error count
assert_eq!(result.metadata.error_count, 0, "Expected no failed pages");
// Per-page errors
for page in &result.pages {
if let Some(error) = &page.error {
eprintln!("Page {} error: {}", page.index, error);
}
}
Assertion Patterns for Error Conditions
Pattern 1: Assert No Errors (Clean Extraction)
let result = extract_pdf(&pdf_path, &ExtractionOptions::default())?;
let output = result_to_output(&result);
// Assert no errors with severity >= warning
let has_warnings_or_errors = output.errors
.iter()
.any(|e| e.severity != "info");
assert!(!has_warnings_or_errors,
"Expected clean extraction but found errors: {:?}",
output.errors);
Pattern 2: Assert Specific Error Code Present
let result = extract_pdf(&truncated_pdf, &ExtractionOptions::default())?;
let output = result_to_output(&result);
// Check that STREAM_DECODE_ERROR is present
let has_decode_error = output.errors
.iter()
.any(|e| e.code == "STREAM_DECODE_ERROR");
assert!(has_decode_error,
"Expected STREAM_DECODE_ERROR in output");
Pattern 3: Assert Error Count
let output = result_to_output(&result);
// Should have exactly one error
assert_eq!(output.errors.len(), 1,
"Expected exactly 1 error, found {}",
output.errors.len());
// First error should be warning severity
assert_eq!(output.errors[0].severity, "warning");
Pattern 4: Assert Error on Specific Page
let output = result_to_output(&result);
// Find errors on page 2
let page_2_errors: Vec<_> = output.errors
.iter()
.filter(|e| e.page_index == Some(2))
.collect();
assert!(!page_2_errors.is_empty(),
"Expected errors on page 2");
Pattern 5: Assert Error Message Contains Text
let output = result_to_output(&result);
// Find error with specific message content
let has_truncation_error = output.errors
.iter()
.any(|e| e.message.contains("truncated"));
assert!(has_truncation_error,
"Expected error message to contain 'truncated'");
Pattern 6: Assert No Fatal Errors
let output = result_to_output(&result);
// Check extraction didn't fail completely
let has_fatal = output.errors
.iter()
.any(|e| e.severity == "fatal");
assert!(!has_fatal,
"Expected no fatal errors but found: {:?}",
output.errors.iter().filter(|e| e.severity == "fatal").collect::<Vec<_>>());
Complete Test Example
Here's a complete test demonstrating error assertion patterns:
#[test]
fn test_truncated_flate_recovery() {
use pdftract_core::{extract_pdf, ExtractionOptions, output::json::result_to_output};
use std::path::PathBuf;
let pdf_path = PathBuf::from("tests/fixtures/malformed/truncated-flate.pdf");
let result = extract_pdf(&pdf_path, &ExtractionOptions::default())
.expect("Extraction should succeed (recovery mode)");
let output = result_to_output(&result);
// Pattern 1: Extraction should succeed (no fatal errors)
let has_fatal = output.errors
.iter()
.any(|e| e.severity == "fatal");
assert!(!has_fatal, "Extraction should not fail fatally");
// Pattern 2: Should detect the stream decode error
let has_decode_error = output.errors
.iter()
.any(|e| e.code == "STREAM_DECODE_ERROR");
assert!(has_decode_error, "Should detect truncated flate stream");
// Pattern 3: Should have at least one warning/error
assert!(!output.errors.is_empty(), "Should have diagnostics");
// Pattern 4: Message should mention truncation
let has_truncation_msg = output.errors
.iter()
.any(|e| e.message.to_lowercase().contains("truncat"));
assert!(has_truncation_msg, "Error message should mention truncation");
// Pattern 5: Should still extract some pages (recovery)
assert!(output.pages.len() > 0, "Should extract at least one page");
}
Diagnostic Code Reference
Common Structure Errors
STRUCT_INVALID_NAME- Invalid name character or length > 127 bytesSTRUCT_INVALID_HEX- Invalid hex character in hex stringSTRUCT_INVALID_OCTAL- Invalid octal escape sequenceSTRUCT_INVALID_STREAM_HEADER- Stream keyword not followed by proper newlineSTRUCT_UNEXPECTED_BYTE- Lexer encountered unexpected byteSTRUCT_UNEXPECTED_EOF- File ended mid-tokenSTRUCT_UNTERMINATED_STRING- Missing closing paren on literal stringSTRUCT_MISSING_KEY- Required dictionary key absent
Common Stream Errors
STREAM_DECODE_ERROR- Decompression failed (corrupt data)STREAM_BOMB- Decompressed size exceeds max_decompress_bytesSTREAM_UNKNOWN_FILTER- Unsupported filter nameSTREAM_INVALID_PARAMS- Invalid /DecodeParms dictionarySTREAM_INVALID_JPEG- Missing SOI/EOI markers
Common XREF Errors
XREF_REPAIRED- Xref reconstructed via forward scan (EC-07)XREF_LINEARIZED_NO_FORWARD_SCAN- Forward scan disabled for linearized PDFXREF_REMOTE_NO_FORWARD_SCAN- Forward scan disabled for HTTP sourcesXREF_INVALID_STREAM_FORMAT- Malformed xref streamXREF_INVALID_STREAM_ENTRY- Unparseable xref stream entry
Adding Assertions to Existing Tests
Step 1: Identify Expected Error Conditions
Before adding assertions, determine what error conditions the test should verify:
- Does the fixture have known issues? (e.g., truncated streams, corrupt xref)
- What diagnostic codes should be emitted? (check fixture
.metafiles) - What severity level is expected? (info/warning/error/fatal)
- Which pages are affected? (document-level vs page-specific)
Step 2: Add Error Collection to Test
Modify the test to collect errors:
// Before
let result = extract_pdf(&pdf_path, &options)?;
assert!(result.pages.len() > 0);
// After
let result = extract_pdf(&pdf_path, &options)?;
let output = result_to_output(&result);
assert!(output.pages.len() > 0);
// Now we can also assert on output.errors
Step 3: Add Targeted Assertions
Add assertions that verify the expected error conditions:
// Example: Assert expected warning is present
let expected_warning = output.errors
.iter()
.find(|e| e.code == "STREAM_DECODE_ERROR");
assert!(expected_warning.is_some(), "Missing expected STREAM_DECODE_ERROR");
Step 4: Document Expected Behavior
Add comments explaining why the assertion exists:
// This fixture has a truncated flate stream, so we expect
// STREAM_DECODE_ERROR but extraction should still succeed
// (recovery per EC-07)
assert!(has_decode_error);
assert!(output.pages.len() > 0);
Conversion Flow: Internal → JSON
Understanding how errors flow from internal diagnostics to JSON output:
1. Emission (during parsing)
emit!(diagnostics, STREAM_DECODE_ERROR, offset = 12345);
2. Internal Diagnostic
Diagnostic {
code: DiagCode::StreamDecodeError,
byte_offset: Some(12345),
object_ref: None,
message: Cow::Borrowed("zlib stream truncated mid-inflation"),
}
3. String conversion (for ExtractionResult)
"STREAM_DECODE_ERROR: zlib stream truncated mid-inflation (offset 12345)"
4. JSON conversion (result_to_output)
DiagnosticJson {
code: "STREAM_DECODE_ERROR".to_string(),
message: "zlib stream truncated mid-inflation".to_string(),
severity: "warning".to_string(),
page_index: None,
location: None,
hint: None,
}
Key Files Reference
crates/pdftract-core/src/diagnostics.rs- Diagnostic system,Diagnosticstruct,DiagCodeenumcrates/pdftract-core/src/extract.rs-ExtractionResult,ExtractionMetadatacrates/pdftract-core/src/schema/mod.rs-Output,DiagnosticJsonstructscrates/pdftract-core/src/output/json.rs-result_to_output(),convert_diagnostics()crates/pdftract-core/tests/error_recovery_integration.rs- Existing error test patterns
Test Fixture Convention
When adding new test fixtures that expect errors:
-
Create
.metafile documenting expected diagnostics:filter: FlateDecode expected_diagnostics: STREAM_DECODE_ERROR description: Mid-stream EOF during decompression -
Add assertions to match expected diagnostics:
let meta = load_fixture_meta("truncated-flate.meta"); for expected_code in meta.expected_diagnostics { let found = output.errors.iter().any(|e| e.code == expected_code); assert!(found, "Missing expected diagnostic: {}", expected_code); }
Acceptance Criteria Status
✅ Complete errors array documentation created
- Documented JSON
Output.errorsarray structure - Documented
DiagnosticJsonfields and types - Documented severity levels and impact
✅ Assertion integration guide written
- Provided 6 common assertion patterns with examples
- Included complete working test example
- Documented step-by-step process for adding assertions
✅ Examples provided for common error checking patterns
- Pattern 1: Assert no errors (clean extraction)
- Pattern 2: Assert specific error code present
- Pattern 3: Assert error count
- Pattern 4: Assert error on specific page
- Pattern 5: Assert error message contains text
- Pattern 6: Assert no fatal errors