targrep 0.6.0

Search inside compressed log archives (.tgz, .tar.zst, .tar.xz) with ripgrep's flags and per-member line numbers
Documentation
#[test]
fn files_are_taken_in_argument_order() {
    let d = tempfile::tempdir().unwrap();
    for n in ["c.tgz", "a.tgz", "b.tgz"] {
        std::fs::write(d.path().join(n), b"x").unwrap();
    }
    let given: Vec<_> = ["c.tgz", "a.tgz", "b.tgz"].iter().map(|n| d.path().join(n)).collect();
    let (items, errs) = trg::source::resolve(&given);
    let names: Vec<_> = items.iter().map(|p| p.file_name().unwrap().to_str().unwrap()).collect();
    assert_eq!(names, vec!["c.tgz", "a.tgz", "b.tgz"]);
    assert!(errs.is_empty());
}

#[test]
fn directories_are_walked_and_sorted_for_determinism() {
    let d = tempfile::tempdir().unwrap();
    std::fs::create_dir_all(d.path().join("sub")).unwrap();
    std::fs::write(d.path().join("b.tgz"), b"x").unwrap();
    std::fs::write(d.path().join("a.tgz"), b"x").unwrap();
    std::fs::write(d.path().join("sub/c.tgz"), b"x").unwrap();
    let (items, errs) = trg::source::resolve(&[d.path().to_path_buf()]);
    assert_eq!(items.len(), 3, "got {items:?}");
    assert!(errs.is_empty());
    let names: Vec<_> = items.iter().map(|p| p.file_name().unwrap().to_str().unwrap()).collect();
    assert_eq!(names, vec!["a.tgz", "b.tgz", "c.tgz"]);
}

#[test]
fn a_missing_path_is_an_error_not_a_panic() {
    let (items, errs) = trg::source::resolve(&[std::path::PathBuf::from("/no/such/file.tgz")]);
    assert!(items.is_empty());
    assert_eq!(errs.len(), 1);
    assert!(errs[0].contains("/no/such/file.tgz"));
}

#[test]
fn no_globs_means_none_not_an_empty_set() {
    assert!(trg::source::build_globs(&[]).unwrap().is_none());
    assert!(trg::source::build_globs(&["*.log".into()]).unwrap().is_some());
}

#[test]
fn an_invalid_glob_is_an_error() {
    assert!(trg::source::build_globs(&["[".into()]).is_err());
}

#[test]
fn a_symlinked_file_in_a_walked_directory_is_searched() {
    let d = tempfile::tempdir().unwrap();
    let elsewhere = tempfile::tempdir().unwrap();
    let target = elsewhere.path().join("target.log");
    std::fs::write(&target, b"NEEDLE elsewhere\n").unwrap();
    std::os::unix::fs::symlink(&target, d.path().join("linked.log")).unwrap();

    let (items, errs) = trg::source::resolve(&[d.path().to_path_buf()]);
    assert!(errs.is_empty(), "got errs {errs:?}");
    let names: Vec<_> = items.iter().map(|p| p.file_name().unwrap().to_str().unwrap()).collect();
    assert_eq!(names, vec!["linked.log"], "symlinked file must be picked up, got {items:?}");
}

#[test]
fn a_broken_symlink_in_a_walked_directory_is_an_error_not_silently_skipped() {
    let d = tempfile::tempdir().unwrap();
    std::os::unix::fs::symlink(d.path().join("nonexistent.log"), d.path().join("broken.log"))
        .unwrap();

    let (items, errs) = trg::source::resolve(&[d.path().to_path_buf()]);
    assert!(items.is_empty(), "a broken symlink must not be yielded as a work item: {items:?}");
    assert_eq!(errs.len(), 1, "got errs {errs:?}");
    assert!(errs[0].contains("broken.log"), "got errs {errs:?}");
}

#[test]
fn a_directory_of_ordinary_files_still_resolves_in_sorted_order() {
    let d = tempfile::tempdir().unwrap();
    for n in ["b.tgz", "a.tgz", "c.tgz"] {
        std::fs::write(d.path().join(n), b"x").unwrap();
    }
    let (items, errs) = trg::source::resolve(&[d.path().to_path_buf()]);
    assert!(errs.is_empty());
    let names: Vec<_> = items.iter().map(|p| p.file_name().unwrap().to_str().unwrap()).collect();
    assert_eq!(names, vec!["a.tgz", "b.tgz", "c.tgz"]);
}

#[test]
fn a_symlink_and_its_target_in_the_same_directory_dedup_to_one_item() {
    let d = tempfile::tempdir().unwrap();
    std::fs::write(d.path().join("real.log"), b"NEEDLE dup\n").unwrap();
    std::os::unix::fs::symlink(d.path().join("real.log"), d.path().join("link_to_real.log"))
        .unwrap();

    let (items, errs) = trg::source::resolve(&[d.path().to_path_buf()]);
    assert!(errs.is_empty(), "got errs {errs:?}");
    assert_eq!(items.len(), 1, "symlink and target must dedup to one item, got {items:?}");
}

#[test]
fn two_hardlinks_to_the_same_file_dedup_to_one_item() {
    let d = tempfile::tempdir().unwrap();
    let a = d.path().join("a.log");
    let b = d.path().join("b.log");
    std::fs::write(&a, b"NEEDLE dup\n").unwrap();
    std::fs::hard_link(&a, &b).unwrap();

    let (items, errs) = trg::source::resolve(&[d.path().to_path_buf()]);
    assert!(errs.is_empty(), "got errs {errs:?}");
    assert_eq!(items.len(), 1, "hardlinks must dedup to one item, got {items:?}");
}

#[test]
fn the_same_path_named_twice_on_the_command_line_dedups_to_one_item() {
    let d = tempfile::tempdir().unwrap();
    let p = d.path().join("a.log");
    std::fs::write(&p, b"NEEDLE dup\n").unwrap();

    let (items, errs) = trg::source::resolve(&[p.clone(), p.clone()]);
    assert!(errs.is_empty(), "got errs {errs:?}");
    assert_eq!(items.len(), 1, "same path named twice must dedup, got {items:?}");
}

#[test]
fn distinct_files_with_identical_content_are_not_mistaken_for_duplicates() {
    let d = tempfile::tempdir().unwrap();
    std::fs::write(d.path().join("a.log"), b"NEEDLE same content\n").unwrap();
    std::fs::write(d.path().join("b.log"), b"NEEDLE same content\n").unwrap();

    let (items, errs) = trg::source::resolve(&[d.path().to_path_buf()]);
    assert!(errs.is_empty(), "got errs {errs:?}");
    assert_eq!(items.len(), 2, "distinct files must both be kept, got {items:?}");
}