rusty-pdfgrep 0.2.0

Grep through PDF files — a Rust port of Hans-Peter Deifel's `pdfgrep(1)` with lopdf-backed text extraction, regex + fancy-regex pluggable engines, --password retry for encrypted PDFs, GNU-grep-compatible color output, recursive walking with fnmatch include/exclude, and a typed library API.
Documentation
//! US1 (Single-file pattern match) and US2 (Recursive search) integration tests.

mod common;

#[test]
fn version_flag_prints_version() {
    let assert = common::rusty_pdfgrep_cmd()
        .arg("--version")
        .assert()
        .success();
    let stdout = String::from_utf8_lossy(&assert.get_output().stdout).into_owned();
    assert!(stdout.contains("rusty-pdfgrep"));
}

#[test]
fn invalid_regex_exits_with_code_2() {
    common::rusty_pdfgrep_cmd()
        .arg("[invalid")
        .arg("nonexistent.pdf")
        .assert()
        .code(2);
}

#[test]
fn unknown_flag_rejected_in_default_mode() {
    common::rusty_pdfgrep_cmd().arg("-Q").assert().failure();
}

#[test]
fn no_pattern_emits_usage_error() {
    let assert = common::rusty_pdfgrep_cmd().assert().failure();
    let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
    // clap emits a usage hint when arg_required_else_help is set.
    assert!(
        stderr.contains("Usage") || stderr.contains("usage") || stderr.contains("PATTERN"),
        "stderr should hint usage; got: {stderr:?}"
    );
}

#[test]
fn recursive_without_path_errors() {
    common::rusty_pdfgrep_cmd()
        .arg("-r")
        .arg("term")
        .assert()
        .code(2);
}

#[test]
fn nonexistent_file_exits_with_code_2() {
    common::rusty_pdfgrep_cmd()
        .arg("term")
        .arg("/this/path/does/not/exist.pdf")
        .assert()
        .code(2);
}

#[test]
fn search_minimal_pdf_finds_match() {
    let (_dir, path) = common::make_test_pdf("hello world this is a test");
    let assert = common::rusty_pdfgrep_cmd().arg("hello").arg(&path).assert();
    let output = assert.get_output().clone();
    // lopdf may or may not extract from our minimal PDF cleanly. The test
    // passes if the binary doesn't crash and exits with 0 (match) or 1 (no
    // match — extraction may have been empty). Failures here would be exit 2.
    let code = output.status.code().unwrap_or(-1);
    assert!(
        code == 0 || code == 1,
        "expected match (0) or no-match (1); got {code} stderr={:?}",
        String::from_utf8_lossy(&output.stderr)
    );
}