test(bf-394lb): implement basic test skeleton calling pdftract extract
- Add test_extract_all_discovered_pdfs() function to forms_integration.rs - Use walkdir-based discover_pdf_fixtures() for recursive PDF discovery - Spawn pdftract extract --json process for each PDF and capture output - Print detailed status and output preview for debugging - Fix pdftract_bin() path resolution (workspace target dir) Closes bf-394lb. Verification: notes/bf-394lb.md. Tests: PASS (compiles, runs, no hangs).
This commit is contained in:
parent
0a2fb17d72
commit
ace8c2a26c
3 changed files with 358 additions and 1 deletions
|
|
@ -1 +1 @@
|
|||
c622e3a6b751cdf9f55e4de2bebf546e94b58a02
|
||||
0a2fb17d7262c890ed31554704a8df7f7cbfa902
|
||||
|
|
|
|||
313
crates/pdftract-cli/tests/forms_integration.rs
Normal file
313
crates/pdftract-cli/tests/forms_integration.rs
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
//! Forms integration tests.
|
||||
//!
|
||||
//! This test module validates PDF form field extraction and analysis:
|
||||
//! - AcroForm text fields, checkboxes, radio buttons, dropdowns
|
||||
//! - XFA form detection and structure
|
||||
//! - Form field values, flags, and properties
|
||||
//! - Form submission metadata
|
||||
//!
|
||||
//! Tests iterate through PDF fixtures in tests/fixtures/forms/ and verify
|
||||
//! that form fields are correctly extracted and represented in JSON output.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Get the path to the pdftract binary (cargo build output)
|
||||
fn pdftract_bin() -> PathBuf {
|
||||
// The binary should be built at target/debug/pdftract or target/release/pdftract
|
||||
// CARGO_MANIFEST_DIR is the crate directory; workspace target is two levels up
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("../../target/debug/pdftract");
|
||||
|
||||
// Fall back to release if debug doesn't exist
|
||||
if !path.exists() {
|
||||
let mut release_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
release_path.push("../../target/release/pdftract");
|
||||
return release_path;
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Path to fixtures directory
|
||||
fn fixtures_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/forms")
|
||||
}
|
||||
|
||||
/// Find all PDF files in the fixtures directory (non-recursive)
|
||||
fn find_pdf_fixtures() -> Vec<PathBuf> {
|
||||
let fixtures_dir = fixtures_dir();
|
||||
let mut pdf_files = Vec::new();
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(&fixtures_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("pdf") {
|
||||
pdf_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pdf_files.sort();
|
||||
pdf_files
|
||||
}
|
||||
|
||||
/// Discover all PDF files in the given directory recursively using walkdir.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fixtures_path` - Path to the fixtures directory to search
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Vec<PathBuf>` containing paths to all discovered PDF files
|
||||
fn discover_pdf_fixtures<P: AsRef<Path>>(fixtures_path: P) -> Vec<PathBuf> {
|
||||
let mut pdf_files = Vec::new();
|
||||
|
||||
let walker = WalkDir::new(fixtures_path)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.file_type().is_file()
|
||||
&& e.path().extension().map(|ext| ext == "pdf").unwrap_or(false)
|
||||
})
|
||||
.map(|e| e.path().to_path_buf());
|
||||
|
||||
pdf_files.extend(walker);
|
||||
pdf_files.sort();
|
||||
|
||||
pdf_files
|
||||
}
|
||||
|
||||
/// Test discover_pdf_fixtures function and print discovered fixtures
|
||||
///
|
||||
/// This test verifies that the walkdir-based PDF discovery function works correctly
|
||||
/// and prints the names of all discovered fixtures to stdout.
|
||||
#[test]
|
||||
fn test_discover_pdf_fixtures() {
|
||||
let fixtures_dir = fixtures_dir();
|
||||
let pdf_files = discover_pdf_fixtures(&fixtures_dir);
|
||||
|
||||
println!("\n=== Discovered PDF Fixtures ===");
|
||||
if pdf_files.is_empty() {
|
||||
println!("No PDF files found in {}", fixtures_dir.display());
|
||||
} else {
|
||||
for pdf_path in &pdf_files {
|
||||
println!(" - {}", pdf_path.display());
|
||||
}
|
||||
println!("Total: {} PDF file(s)", pdf_files.len());
|
||||
}
|
||||
println!("==============================\n");
|
||||
|
||||
// Test that the function runs without errors
|
||||
// (We don't assert a count since fixtures may be added/removed)
|
||||
let _ = pdf_files;
|
||||
}
|
||||
|
||||
/// Basic test that discovers all form PDF fixtures
|
||||
///
|
||||
/// This test iterates through all PDF files in the forms fixtures directory
|
||||
/// and verifies they can be opened and parsed by pdftract.
|
||||
#[test]
|
||||
fn test_forms_fixtures_discovery() {
|
||||
let pdf_files = find_pdf_fixtures();
|
||||
let bin = pdftract_bin();
|
||||
|
||||
// Ensure binary exists
|
||||
assert!(bin.exists(), "pdftract binary not found at {:?}", bin);
|
||||
|
||||
// If no fixtures yet, test passes (scaffold for future fixtures)
|
||||
if pdf_files.is_empty() {
|
||||
println!("No PDF fixtures found in {:?} - test scaffold ready for fixtures", fixtures_dir());
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Found {} form PDF fixtures", pdf_files.len());
|
||||
|
||||
// Test each fixture can be processed
|
||||
for pdf_path in pdf_files {
|
||||
println!("Testing fixture: {}", pdf_path.display());
|
||||
|
||||
// Run pdftract extract --json on the fixture
|
||||
let output = Command::new(&bin)
|
||||
.arg("extract")
|
||||
.arg("--json")
|
||||
.arg(&pdf_path)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
// For now, just verify it doesn't crash
|
||||
// Future tests will validate the JSON output structure
|
||||
if result.status.success() {
|
||||
println!(" ✓ Successfully processed");
|
||||
} else {
|
||||
println!(" ⚠ Processing failed with status: {}", result.status);
|
||||
println!(" stderr: {}", String::from_utf8_lossy(&result.stderr));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Failed to run pdftract: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic test skeleton that calls pdftract extract on all discovered PDFs
|
||||
///
|
||||
/// This test iterates through all PDF files discovered by the walkdir-based
|
||||
/// discover_pdf_fixtures helper and calls pdftract extract --json on each.
|
||||
/// It captures stdout/stderr and prints results without strict validation.
|
||||
#[test]
|
||||
fn test_extract_all_discovered_pdfs() {
|
||||
let fixtures_dir = fixtures_dir();
|
||||
let pdf_files = discover_pdf_fixtures(&fixtures_dir);
|
||||
let bin = pdftract_bin();
|
||||
|
||||
// Ensure binary exists
|
||||
assert!(bin.exists(), "pdftract binary not found at {:?}", bin);
|
||||
|
||||
// If no fixtures yet, test passes (scaffold for future fixtures)
|
||||
if pdf_files.is_empty() {
|
||||
println!(
|
||||
"No PDF fixtures found in {:?} - test scaffold ready for fixtures",
|
||||
fixtures_dir
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n=== Running pdftract extract on {} discovered PDF(s) ===\n",
|
||||
pdf_files.len()
|
||||
);
|
||||
|
||||
// Process each fixture with pdftract extract
|
||||
let mut success_count = 0;
|
||||
let mut failure_count = 0;
|
||||
|
||||
for pdf_path in &pdf_files {
|
||||
println!("Processing: {}", pdf_path.display());
|
||||
|
||||
// Run pdftract extract --json on the fixture
|
||||
let output = Command::new(&bin)
|
||||
.arg("extract")
|
||||
.arg("--json")
|
||||
.arg(pdf_path)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
// Capture stdout/stderr for debugging
|
||||
let stdout = String::from_utf8_lossy(&result.stdout);
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
|
||||
if result.status.success() {
|
||||
println!(" ✓ Status: SUCCESS");
|
||||
// Print first 200 chars of stdout to show we got JSON output
|
||||
let preview = if stdout.len() > 200 {
|
||||
&stdout[..200]
|
||||
} else {
|
||||
&stdout
|
||||
};
|
||||
println!(" Output preview: {}...", preview);
|
||||
success_count += 1;
|
||||
} else {
|
||||
println!(" ✗ Status: FAILED (exit code: {:?})", result.status.code());
|
||||
if !stderr.is_empty() {
|
||||
println!(" stderr: {}", stderr);
|
||||
}
|
||||
if !stdout.is_empty() {
|
||||
let preview = if stdout.len() > 200 {
|
||||
&stdout[..200]
|
||||
} else {
|
||||
&stdout
|
||||
};
|
||||
println!(" stdout preview: {}...", preview);
|
||||
}
|
||||
failure_count += 1;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Failed to run pdftract: {}", e);
|
||||
failure_count += 1;
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("=== Summary: {} succeeded, {} failed ===\n", success_count, failure_count);
|
||||
|
||||
// For this skeleton test, we don't assert success_count > 0
|
||||
// PDF extraction failures are OK at this stage - we're just verifying
|
||||
// that the test framework and CLI invocation work correctly
|
||||
let _ = (success_count, failure_count);
|
||||
}
|
||||
|
||||
/// Test that form field structure is correctly extracted
|
||||
///
|
||||
/// This test verifies that form fields are correctly represented in the JSON output:
|
||||
/// - Field names (T entries)
|
||||
/// - Field types (FT entries: Tx for text, Btn for buttons, Ch for choice)
|
||||
/// - Field values (V entries)
|
||||
/// - Field flags (Ff entries)
|
||||
/// - Field appearance characteristics (appearance streams, fonts, etc.)
|
||||
#[test]
|
||||
fn test_form_field_structure() {
|
||||
let pdf_files = find_pdf_fixtures();
|
||||
|
||||
if pdf_files.is_empty() {
|
||||
println!("No PDF fixtures - skipping form field structure test");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement form field structure validation
|
||||
// This will parse the JSON output and verify:
|
||||
// - Form fields array exists
|
||||
// - Each field has required properties
|
||||
// - Field types match expected values
|
||||
// - Field values are correctly extracted
|
||||
}
|
||||
|
||||
/// Test AcroForm-specific features
|
||||
///
|
||||
/// Validates AcroForm (classic PDF form) extraction:
|
||||
/// - Terminal fields (leaf nodes)
|
||||
/// - Field hierarchies (non-terminal fields with kids)
|
||||
/// - Calculate actions, format, keystroke, validate actions
|
||||
/// - Appearance dictionaries
|
||||
#[test]
|
||||
fn test_acroform_features() {
|
||||
let pdf_files = find_pdf_fixtures();
|
||||
|
||||
if pdf_files.is_empty() {
|
||||
println!("No PDF fixtures - skipping AcroForm features test");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement AcroForm-specific validation
|
||||
// This will verify AcroForm dictionary entries:
|
||||
// - Fields array
|
||||
// - NeedAppearances flag
|
||||
// - CO (signature calculation order)
|
||||
// - DR (default resources)
|
||||
// - DA (default appearance)
|
||||
}
|
||||
|
||||
/// Test XFA form detection
|
||||
///
|
||||
/// Verifies that XFA (XML Forms Architecture) forms are detected:
|
||||
/// - XFA presence in AcroForm dictionary
|
||||
/// - XFA streams identification
|
||||
#[test]
|
||||
fn test_xfa_detection() {
|
||||
let pdf_files = find_pdf_fixtures();
|
||||
|
||||
if pdf_files.is_empty() {
|
||||
println!("No PDF fixtures - skipping XFA detection test");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement XFA detection validation
|
||||
// This will check for XFA presence and structure
|
||||
}
|
||||
44
notes/bf-394lb.md
Normal file
44
notes/bf-394lb.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# bf-394lb Verification Note
|
||||
|
||||
## Summary
|
||||
Implemented basic test skeleton `test_extract_all_discovered_pdfs` that calls `pdftract extract --json` on discovered PDF fixtures.
|
||||
|
||||
## Implementation
|
||||
- Added `test_extract_all_discovered_pdfs()` function to `tests/forms_integration.rs`
|
||||
- Test uses the walkdir-based `discover_pdf_fixtures()` helper for recursive PDF discovery
|
||||
- For each PDF, spawns `pdftract extract --json <file>` and captures stdout/stderr
|
||||
- Prints detailed status and output preview for each processed file
|
||||
- Counts and reports success/failure summary
|
||||
|
||||
## Bug Fix
|
||||
Fixed path resolution bug in `pdftract_bin()` helper:
|
||||
- Changed from `../target/debug/pdftract` to `../../target/debug/pdftract`
|
||||
- Binary lives in workspace target directory, not relative to crate directory
|
||||
- Path: `crates/pdftract-cli/` → `../../target/debug/` → `/home/coding/pdftract/target/debug/`
|
||||
|
||||
## Testing
|
||||
```bash
|
||||
cargo test --package pdftract-cli --test forms_integration test_extract_all_discovered_pdfs -- --nocapture
|
||||
```
|
||||
|
||||
Result: PASS
|
||||
- Test compiled and ran successfully
|
||||
- No hangs or panics
|
||||
- Gracefully handles empty fixtures directory
|
||||
- Ready to process PDF fixtures when they are added
|
||||
|
||||
## Acceptance Criteria Status
|
||||
- [x] Test function exists and compiles
|
||||
- [x] Test calls `pdftract extract` for each discovered fixture
|
||||
- [x] Test runs via `cargo test forms_integration`
|
||||
- [x] No test hangs or panics
|
||||
- [x] May have failures (PDF extraction errors) - that's OK for this step
|
||||
|
||||
## Files Modified
|
||||
- `crates/pdftract-cli/tests/forms_integration.rs` (added test function, fixed pdftract_bin path)
|
||||
|
||||
## Commit
|
||||
Commit will be made with:
|
||||
```
|
||||
test(bf-394lb): implement basic test skeleton calling pdftract extract
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue