diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index 39bf305..112f204 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -c622e3a6b751cdf9f55e4de2bebf546e94b58a02 +0a2fb17d7262c890ed31554704a8df7f7cbfa902 diff --git a/crates/pdftract-cli/tests/forms_integration.rs b/crates/pdftract-cli/tests/forms_integration.rs new file mode 100644 index 0000000..cebe4f2 --- /dev/null +++ b/crates/pdftract-cli/tests/forms_integration.rs @@ -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 { + 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` containing paths to all discovered PDF files +fn discover_pdf_fixtures>(fixtures_path: P) -> Vec { + 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 +} diff --git a/notes/bf-394lb.md b/notes/bf-394lb.md new file mode 100644 index 0000000..7c904d6 --- /dev/null +++ b/notes/bf-394lb.md @@ -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 ` 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 +```