- Analyze current test assertions in error_recovery_integration.rs - Analyze current test assertions in stream_decoder_fixtures.rs - Identify 5 major coverage gaps in truncated-flate recovery testing - Document missing diagnostic emission in FlateDecoder - Document lack of actual extraction in integration test - Map out recommendations for new assertions at high/medium/low priority Closes bf-303t6.
10 KiB
Test Coverage Analysis: Truncated-Flate Recovery
Bead: bf-303t6 Date: 2026-07-05 Analysis: Current test assertions and coverage gaps for truncated-flate error recovery
Current Test Landscape
1. Integration-Level Test: error_recovery_integration.rs::test_truncated_mid_stream()
Location: crates/pdftract-core/tests/error_recovery_integration.rs:165-188
Current Assertions:
fn test_truncated_mid_stream() {
let fixture_path = fixture_path("truncated_mid_stream.pdf");
let expected = load_expected_diagnostics(&fixture_path);
let result = assert_no_panic("test_truncated_mid_stream", || {
let pdf_data = fs::read(&fixture_path).expect("fixture should exist");
assert!(pdf_data.starts_with(b"%PDF-"), "Should be a valid PDF");
// Verify expected: STREAM_DECODE_ERROR
let stream_diags: Vec<_> = expected
.expected_diagnostics
.iter()
.filter(|d| d.code.contains("STREAM_DECODE"))
.collect();
assert!(
!stream_diags.is_empty(),
"Should expect STREAM_DECODE_ERROR diagnostic"
);
});
assert!(result.is_ok(), "Test should not panic");
}
What's Validated:
- ✅ Fixture file exists and is valid PDF structure
- ✅ Expected diagnostics JSON file exists
- ✅ Expected diagnostics includes
STREAM_DECODE_ERROR - ✅ Test doesn't panic (INV-8 compliance)
What's NOT Validated:
- ❌ Actual extraction is never run
- ❌ No diagnostics are actually emitted or verified
- ❌ No verification of partial output behavior
- ❌ No verification that extraction succeeds despite truncation
2. Stream Decoder Test: stream_decoder_fixtures.rs
Location: crates/pdftract-core/tests/stream_decoder_fixtures.rs:61-65
Fixture Configuration:
FixtureInfo {
name: "flate_truncated",
filter: FixtureFilter::Single("FlateDecode", None),
expected_diags: vec![], // ⚠️ EMPTY - no diagnostics expected!
bomb_limit: None,
},
Current Assertions:
// Decode the fixture
let result = decode_fixture(&fixture, &input);
let decoded = match result {
Ok(data) => data,
Err(e) => {
failures.push(format!("{}: {}", fixture.name, e));
continue;
}
};
// Compare against expected
if &decoded[..expected_bytes.len().min(decoded.len())] != expected_bytes {
failures.push(format!(
"{}: output mismatch (expected {} bytes, got {} bytes)",
fixture.name,
expected.len(),
decoded.len()
));
continue;
}
What's Validated:
- ✅ Decoder doesn't return
Err(returnsOkwith partial bytes) - ✅ Output matches expected file (0 bytes for truncated fixture)
- ✅ No panic occurs
What's NOT Validated:
- ❌
STREAM_DECODE_ERRORdiagnostic is NOT verified (expected_diags is empty!) - ❌ Metadata says "expects partial bytes + STREAM_DECODE_ERROR" but test doesn't verify
- ❌ No verification that error is properly propagated
- ❌ No distinction between success and partial recovery
3. Implementation: FlateDecoder::decode_impl()
Location: crates/pdftract-core/src/parser/stream.rs:524-558
Error Handling:
fn decode_impl<R: std::io::Read>(
mut decoder: R,
doc_counter: &mut u64,
max_bytes: u64,
) -> Vec<u8> {
let mut output = Vec::new();
let mut chunk = vec![0u8; BOMB_CHECK_CHUNK];
loop {
match decoder.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
// Check bomb limit BEFORE adding bytes to output
if *doc_counter + output.len() as u64 + n as u64 > max_bytes {
// Bomb limit exceeded - return partial bytes
let remaining = (max_bytes - *doc_counter - output.len() as u64) as usize;
let to_add = remaining.min(n);
output.extend_from_slice(&chunk[..to_add]);
return output;
}
output.extend_from_slice(&chunk[..n]);
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
// Truncated stream - return partial bytes (INV-8)
break; // ⚠️ SILENT - no diagnostic emitted
}
Err(_) => {
// Other decoder errors - return partial bytes decoded so far
break; // ⚠️ SILENT - no diagnostic emitted
}
}
}
output // ⚠️ Returns Vec<u8> - caller can't detect error occurred
}
Critical Gaps:
- ❌
UnexpectedEofis caught but no diagnostic is emitted - ❌ Other decoder errors are silently caught
- ❌ Return type is
Vec<u8>, notResult<Vec<u8>, FilterError> - ❌ Caller has no way to distinguish success from partial recovery
Coverage Gap Summary
Gap 1: Missing Diagnostic Emission
Severity: HIGH
Issue: FlateDecoder catches UnexpectedEof but never emits STREAM_DECODE_ERROR diagnostic
Evidence:
- Metadata file says: "expects partial bytes + STREAM_DECODE_ERROR"
- Implementation catches error but doesn't emit diagnostic
- Test has
expected_diags: vec
Impact:
- Diagnostics system is bypassed
- Users can't detect truncated streams in their PDFs
- INV-8 requires graceful recovery, but silent recovery hides issues
Gap 2: No Error Indication in Return Type
Severity: HIGH
Issue: decode_impl() returns Vec<u8> directly, not Result
Evidence:
- Method signature:
fn decode_impl(...) -> Vec<u8> - Both success and error return
Vec<u8> - Caller can't distinguish partial recovery from clean decode
Impact:
- No way for callers to know truncation occurred
- Violates principle of least surprise
- Makes debugging harder
Gap 3: Integration Test Doesn't Extract
Severity: MEDIUM
Issue: test_truncated_mid_stream() validates JSON but doesn't run extraction
Evidence:
- Test reads PDF data but doesn't call
pdftract::extract() - Only validates fixture structure and expected diagnostics JSON
- Comment says:
// TODO: Extract with pdftract once API is available
Impact:
- End-to-end behavior is untested
- Integration between stream decoder and extraction pipeline is unverified
- Real-world behavior with actual PDF extraction is unknown
Gap 4: No Verification of Partial Output Quality
Severity: MEDIUM Issue: Tests don't verify partial output is usable or meaningful
Evidence:
flate_truncated.expectedis 0 bytes (trivially passes)- No test verifies partial data represents valid prefix
- No test verifies remaining bytes are sensible
Impact:
- Can't detect if partial output is garbage
- Can't verify recovery produces useful data
- Quality of partial recovery is untested
Gap 5: Stream Decoder Test Mismatch with Metadata
Severity: LOW Issue: Test expects no diagnostics, metadata says STREAM_DECODE_ERROR expected
Evidence:
stream_decoder_fixtures.rs:expected_diags: vec![]flate_truncated.meta: "expects partial bytes + STREAM_DECODE_ERROR"
Impact:
- Conflicting documentation
- Unclear what the actual expectation is
- Test may be out of sync with requirements
Test Files Examined
-
crates/pdftract-core/tests/error_recovery_integration.rs(315 lines)- Integration-level adversarial test corpus
- Tests 7 fixtures including
truncated_mid_stream.pdf
-
crates/pdftract-core/tests/stream_decoder_fixtures.rs(394 lines)- Unit-level stream decoder tests
- Tests 15+ fixtures including
flate_truncated
-
crates/pdftract-core/src/parser/stream.rs(600+ lines)- FlateDecoder implementation
- Error handling in
decode_impl()
-
Fixture files:
/tests/stream_decoder/fixtures/flate_truncated.bin(10 bytes, truncated zlib)/tests/stream_decoder/fixtures/flate_truncated.expected(0 bytes)/tests/stream_decoder/fixtures/flate_truncated.meta("expects partial bytes + STREAM_DECODE_ERROR")/tests/error_recovery/fixtures/truncated_mid_stream.pdf/tests/error_recovery/fixtures/truncated_mid_stream.expected_diagnostics.json
Recommendations for New Assertions
High Priority (Required for Complete Coverage)
-
Verify diagnostic emission in FlateDecoder
- Assert
STREAM_DECODE_ERRORis emitted onUnexpectedEof - Update test:
expected_diags: vec![DiagCode::StreamDecodeError]
- Assert
-
Add integration test that actually extracts
- Call
pdftract::extract()ontruncated_mid_stream.pdf - Verify extraction succeeds despite truncation
- Verify diagnostics contain
STREAM_DECODE_ERROR
- Call
-
Verify partial output is meaningful
- Create fixture with known valid prefix
- Assert decoded bytes match expected prefix
- Verify partial data isn't garbage
Medium Priority (Improves Robustness)
-
Test error propagation through extraction pipeline
- Verify stream decoder errors are properly reported
- Test diagnostics are collected and surfaced
- Verify no panic occurs (INV-8)
-
Add test for multiple truncation scenarios
- Truncated at different points (early, mid, late)
- Truncated with different filter types (Flate, LZW, etc.)
- Truncated in filter arrays (multi-stage decoding)
Low Priority (Nice to Have)
-
Test bomb limit interaction with truncation
- Verify truncation before bomb limit
- Verify truncation after bomb limit
- Verify both diagnostics can co-occur
-
Add regression test for specific real-world PDFs
- Collect corpus of PDFs with truncated streams
- Test each produces reasonable output
- Track quality of partial recovery over time
Conclusion
The current test coverage for truncated-flate recovery has significant gaps:
- ✅ Basic INV-8 compliance is tested: No panics occur
- ❌ Diagnostic emission is NOT tested: Stream decoder doesn't emit diagnostics
- ❌ Integration behavior is NOT tested: PDF extraction is never called
- ❌ Output quality is NOT verified: Partial data quality is unchecked
- ❌ Error detection is impossible: Return type can't indicate errors
Most Critical Gap: The FlateDecoder silently catches UnexpectedEof but never emits STREAM_DECODE_ERROR diagnostic, making it impossible for users or downstream code to detect that truncation occurred.
Path Forward:
- Fix FlateDecoder to emit diagnostics
- Update stream decoder test to expect diagnostics
- Make integration test actually run extraction
- Add assertions for partial output quality