use super::*;
fn sel_entry(path: &str, size: u64) -> ManifestEntry {
ManifestEntry::new(path, "b3:00", Some(size))
}
fn entry(path: &str, hash: &str) -> ManifestEntry {
ManifestEntry::new(path, hash, None)
}
fn cache_manifest(out: &Path, manifest: &serde_json::Value) -> String {
let bytes = serde_json::to_vec(manifest).unwrap();
let hash = *blake3::hash(&bytes).as_bytes();
let path = BundleManifest::cache_path(out, hash);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, &bytes).unwrap();
format!("b3:{}", blake3::Hash::from_bytes(hash).to_hex())
}
fn report_of(fetched: u64, linked: u64, skipped: u64, failures: &[(&str, &str)]) -> BundleReport {
let mut rep = BundleReport::default();
rep.fetched = fetched;
rep.linked = linked;
rep.skipped = skipped;
rep.failed = u64::try_from(failures.len()).unwrap();
rep.failures = failures
.iter()
.map(|(path, error)| {
let mut failure = decdn_client::bundle::EntryFailure::default();
failure.path = (*path).to_string();
failure.error = (*error).to_string();
failure
})
.collect();
rep
}
fn strs(items: &[&str]) -> Vec<String> {
items.iter().map(|s| (*s).to_string()).collect()
}
fn sample() -> Vec<ManifestEntry> {
vec![
entry("models/a.bin", "b3:1"),
entry("models/b.txt", "b3:2"),
entry("docs/readme.md", "b3:3"),
entry("docs/deep/notes.txt", "b3:4"),
]
}
#[test]
fn entry_retries_flag_is_gone() {
use clap::Parser as _;
#[derive(clap::Parser)]
struct T {
#[command(flatten)]
args: BundlePullArgs,
}
let base = ["t", "-o", "out", "--hash", "b3:aa"];
assert!(T::try_parse_from(base).is_ok());
let parsed = T::try_parse_from(base.iter().copied().chain(["--entry-retries", "2"]));
assert!(parsed.is_err());
}
#[test]
fn parse_selection_drops_commented_and_deleted_lines() {
let entries = vec![
sel_entry("a.bin", 1),
sel_entry("b.bin", 2),
sel_entry("c.bin", 3),
];
let edited = "# header\na.bin\t1 B\n#b.bin\t2 B\n";
let kept = parse_selection(edited, entries).expect("parse");
let paths: Vec<&str> = kept.iter().map(|e| e.path.as_str()).collect();
assert_eq!(paths, vec!["a.bin"]);
}
#[test]
fn parse_selection_preserves_manifest_order_regardless_of_edit_order() {
let entries = vec![sel_entry("a.bin", 1), sel_entry("b.bin", 2)];
let edited = "b.bin\nb.bin is not a path\n".replace("b.bin is not a path", "a.bin");
let kept = parse_selection(&edited, entries).expect("parse");
let paths: Vec<&str> = kept.iter().map(|e| e.path.as_str()).collect();
assert_eq!(paths, vec!["a.bin", "b.bin"]);
}
#[test]
fn parse_selection_empty_when_everything_removed() {
let entries = vec![sel_entry("a.bin", 1)];
let kept = parse_selection("# all gone\n", entries).expect("parse");
assert!(kept.is_empty());
}
#[test]
fn parse_selection_rejects_unknown_path() {
let entries = vec![sel_entry("a.bin", 1)];
let err = parse_selection("a.bin\ntypo.bin\n", entries).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("typo.bin"), "{msg}");
}
#[test]
fn parse_selection_ignores_the_size_annotation_after_the_tab() {
let entries = vec![sel_entry("weights.bin", 1500)];
let kept = parse_selection("weights.bin\t1.5 KB\n", entries).expect("parse");
assert_eq!(kept.len(), 1);
}
#[test]
fn render_then_parse_unedited_keeps_every_entry() {
let entries = vec![sel_entry("a.bin", 1), sel_entry("dir/b.bin", 2000)];
let buffer = render_selection(&entries);
let kept = parse_selection(&buffer, entries).expect("parse");
let paths: Vec<&str> = kept.iter().map(|e| e.path.as_str()).collect();
assert_eq!(paths, vec!["a.bin", "dir/b.bin"]);
}
#[test]
fn editor_command_prefers_visual_then_editor_then_vi() {
assert_eq!(editor_command(Some("nano"), Some("vim")), vec!["nano"]);
assert_eq!(editor_command(None, Some("vim")), vec!["vim"]);
assert_eq!(editor_command(None, None), vec!["vi"]);
}
#[test]
fn editor_command_treats_blank_as_unset() {
assert_eq!(editor_command(Some(" "), Some("vim")), vec!["vim"]);
assert_eq!(editor_command(Some(""), None), vec!["vi"]);
}
#[test]
fn editor_command_splits_arguments() {
assert_eq!(
editor_command(Some("code --wait"), None),
vec!["code", "--wait"]
);
}
#[test]
fn check_selectable_rejects_hash_leading_and_tabbed_paths() {
assert!(check_selectable(&[sel_entry("#weird.bin", 1)]).is_err());
assert!(check_selectable(&[sel_entry("has\ttab.bin", 1)]).is_err());
assert!(check_selectable(&[sel_entry(" #weird.bin", 1)]).is_err());
assert!(check_selectable(&[sel_entry(" leading.bin", 1)]).is_err());
assert!(check_selectable(&[sel_entry("trailing.bin ", 1)]).is_err());
assert!(check_selectable(&[sel_entry("fine/name.bin", 1)]).is_ok());
}
#[test]
fn report_errors_when_any_failed() {
let rep = report_of(1, 0, 1, &[("x", "boom")]);
let err = report(&rep, 0, Path::new("/out"), true).unwrap_err();
assert!(format!("{err:#}").contains("1 entr"), "{err:#}");
}
#[test]
fn report_ok_when_none_failed() {
let rep = report_of(1, 0, 1, &[]);
assert!(report(&rep, 0, Path::new("/out"), false).is_ok());
}
#[test]
fn a_report_warning_does_not_fail_the_pull() {
let mut rep = report_of(2, 0, 0, &[]);
rep.warnings = vec!["big.bin: manifest says 4 bytes, the blob is 12 bytes".to_string()];
assert!(
report(&rep, 0, Path::new("/out"), false).is_ok(),
"a size warning is not a failure"
);
}
#[test]
fn the_summary_counts_reused_and_excluded_entries() {
let mut rep = report_of(1, 1, 1, &[("x", "boom")]);
rep.reused = 1;
rep.reused_bytes = 7;
rep.downloaded = 4;
rep.reconstructed = 20;
rep.resumed_bytes = 6;
let rep = pull_report(&rep, 3, Path::new("/out"));
assert_eq!(
counts_line(&rep),
"pulled into /out (1 fetched, 1 linked, 1 skipped, 1 reused, 3 excluded, 1 failed)"
);
assert_eq!(rep.reused_bytes, 7);
let json = serde_json::to_value(&rep).expect("serialize");
assert_eq!(json["reused"], 1);
assert_eq!(json["reused_bytes"], 7);
assert_eq!(json["excluded"], 3);
assert_eq!(json["resumed_bytes"], 6);
assert_eq!(json["downloaded"], 4);
assert!(json.get("deduped").is_none(), "{json}");
}
#[test]
fn human_bytes_scales_units() {
assert_eq!(human_bytes(0), "0 B");
assert_eq!(human_bytes(512), "512 B");
assert_eq!(human_bytes(1_500), "1.5 KB");
assert_eq!(human_bytes(27_600_000_000), "27.6 GB");
}
#[test]
fn transfer_line_shows_arrow_only_when_they_differ() {
assert_eq!(
transfer_line(27_600_000_000, 27_600_000_000),
"downloaded 27.6 GB"
);
assert_eq!(
transfer_line(13_800_000_000, 27_600_000_000),
"downloaded 13.8 GB → reconstructed 27.6 GB"
);
}
fn filtered_paths(include: &[&str], exclude: &[&str], entries: Vec<ManifestEntry>) -> Vec<String> {
let filter = EntryFilter::compile(&strs(include), &strs(exclude)).unwrap();
filter
.apply_reporting(entries)
.kept
.into_iter()
.map(|e| e.path)
.collect()
}
#[test]
fn entry_filter_reports_each_pattern_that_matched_nothing() {
let mut entries = sample();
entries.push(entry("gpt/metal/model.bin", "b3:5"));
let filter = EntryFilter::compile(
&strs(&["models/*", "no/such/*"]),
&strs(&["metal/*", "**/*.md"]),
)
.unwrap();
let pass = filter.apply_reporting(entries);
assert_eq!(
pass.kept
.iter()
.map(|e| e.path.as_str())
.collect::<Vec<_>>(),
vec!["models/a.bin", "models/b.txt"]
);
assert_eq!(pass.excluded, 3);
assert_eq!(
pass.unmatched,
vec![
Unmatched {
flag: "--include",
pattern: "no/such/*".into(),
suggestion: None,
},
Unmatched {
flag: "--exclude",
pattern: "metal/*".into(),
suggestion: Some("**/metal/*".into()),
},
]
);
assert_eq!(
pass.unmatched[0].warning(),
"--include 'no/such/*' matched no entries"
);
assert_eq!(
pass.unmatched[1].warning(),
"--exclude 'metal/*' matched no entries (a pattern matches the whole path from the \
bundle root; did you mean '**/metal/*'?)"
);
}
#[test]
fn entry_filter_hints_an_anchored_pattern_without_a_slash() {
let filter = EntryFilter::compile(&[], &strs(&["*.txt", "**/*.png"])).unwrap();
let pass = filter.apply_reporting(sample());
assert_eq!(pass.excluded, 0);
assert_eq!(
pass.unmatched
.iter()
.map(|u| (u.pattern.as_str(), u.suggestion.as_deref()))
.collect::<Vec<_>>(),
vec![("*.txt", Some("**/*.txt")), ("**/*.png", None)]
);
}
#[test]
fn entry_filter_reports_nothing_when_every_pattern_matches() {
let filter = EntryFilter::compile(&strs(&["models/*"]), &strs(&["docs/**"])).unwrap();
let pass = filter.apply_reporting(sample());
assert!(pass.unmatched.is_empty(), "{:?}", pass.unmatched);
assert_eq!(pass.excluded, 2);
let pass = EntryFilter::compile(&[], &[])
.unwrap()
.apply_reporting(sample());
assert!(pass.unmatched.is_empty());
assert_eq!(pass.excluded, 0);
}
#[test]
fn a_selection_adds_its_dropped_entries_to_the_excluded_count() {
let kept = Kept {
manifest: BundleManifest::new(sample()),
excluded: 2,
};
let kept = apply_selection(kept, |entries| {
parse_selection("models/a.bin\ndocs/readme.md\n", entries)
})
.unwrap();
assert_eq!(kept.manifest.entries.len(), 2);
assert_eq!(kept.excluded, 4);
}
#[test]
fn leftover_partials_counts_only_unselected_blobs() {
let tmp = tempfile::tempdir().expect("tmp");
assert_eq!(
leftover_partials(tmp.path(), &HashSet::new()),
Leftovers::default(),
"no staging dir, nothing left over"
);
let dir = tmp.path().join(STAGING_DIR);
std::fs::create_dir_all(&dir).expect("staging dir");
let hex = |b: u8| blake3::Hash::from_bytes([b; 32]).to_hex().to_string();
let write = |name: String, len: usize| {
std::fs::write(dir.join(name), vec![0u8; len]).expect("write");
};
write(format!("{}.partial", hex(1)), 500);
write(format!("{}.partial.ranges", hex(1)), 5);
write(format!("{}.partial", hex(2)), 300);
write(format!("{}.partial", hex(3)), 1000);
write(format!("{}.partial.ranges", hex(3)), 10);
write(hex(4), 200);
write(".tmpAbC123".into(), 50);
write("notes.txt".into(), 50);
let keep: HashSet<[u8; 32]> = [[1; 32], [2; 32]].into_iter().collect();
let on_disk =
|name: String| allocated_bytes(&std::fs::metadata(dir.join(name)).expect("metadata"));
let expected = on_disk(format!("{}.partial", hex(3)))
+ on_disk(format!("{}.partial.ranges", hex(3)))
+ on_disk(hex(4));
let leftovers = leftover_partials(tmp.path(), &keep);
assert_eq!(
leftovers,
Leftovers {
blobs: 2,
bytes: expected,
}
);
assert_eq!(
leftover_warning(
Path::new("/out/.decdn-partial"),
&Leftovers {
blobs: 2,
bytes: 1210,
}
)
.as_deref(),
Some(
"2 partial downloads (1.2 KB on disk) that this run did not use remain in \
/out/.decdn-partial; delete them to reclaim the space"
)
);
assert_eq!(
leftover_warning(
Path::new("/out/.decdn-partial"),
&Leftovers {
blobs: 1,
bytes: 1_500_000_000,
}
)
.as_deref(),
Some(
"1 partial download (1.5 GB on disk) that this run did not use remains in \
/out/.decdn-partial; delete it to reclaim the space"
)
);
assert_eq!(
leftover_warning(Path::new("/out"), &Leftovers::default()),
None
);
}
#[cfg(unix)]
#[test]
fn leftover_partials_count_a_sparse_partial_by_its_allocated_space() {
let tmp = tempfile::tempdir().expect("tmp");
let dir = tmp.path().join(STAGING_DIR);
std::fs::create_dir_all(&dir).expect("staging dir");
let hex = blake3::Hash::from_bytes([9; 32]).to_hex().to_string();
let file = std::fs::File::create(dir.join(format!("{hex}.partial"))).expect("create");
file.set_len(64 * 1024 * 1024).expect("size the partial");
let leftovers = leftover_partials(tmp.path(), &HashSet::new());
assert_eq!(leftovers.blobs, 1);
assert!(
leftovers.bytes < 1024 * 1024,
"a hole-only partial takes almost no space: {}",
leftovers.bytes
);
}
#[test]
fn entry_filter_rejects_bad_glob() {
let err = EntryFilter::compile(&strs(&["["]), &[]).unwrap_err();
assert!(
format!("{err:#}").contains("--include"),
"error should name the offending flag: {err:#}"
);
}
#[test]
fn report_counts_linked_without_failing() {
let rep = report_of(1, 1, 1, &[]);
assert!(report(&rep, 0, Path::new("/out"), false).is_ok());
}
#[test]
fn bundle_pull_namespace_flag_parses_and_rejects_zero() {
use clap::Parser;
#[derive(Parser)]
struct T {
#[command(flatten)]
a: decdn_common::cli::BundlePullArgs,
}
let ok = T::try_parse_from(["t", "-o", "out", "--hash", "b3:aa", "--namespace", "7"])
.expect("valid namespace parses");
assert_eq!(ok.a.namespace, Some(7));
assert!(
T::try_parse_from(["t", "-o", "out", "--hash", "b3:aa", "--namespace", "0"]).is_err(),
"namespace 0 is the reserved sentinel and must be rejected"
);
let none =
T::try_parse_from(["t", "-o", "out", "--hash", "b3:aa"]).expect("namespace is optional");
assert_eq!(none.a.namespace, None, "absent flag stays None");
}
#[tokio::test]
async fn dry_run_still_rejects_a_dangling_provider_address() {
use clap::Parser;
#[derive(Parser)]
struct T {
#[command(flatten)]
a: decdn_common::cli::BundlePullArgs,
}
let addr = "0x0000000000000000000000000000000000000001";
let args = T::parse_from([
"t",
"-o",
"out",
"--hash",
"b3:aa",
"--dry-run",
"--provider-address",
addr,
])
.a;
let err = super::bundle_pull(&args, None)
.await
.expect_err("a dangling --provider-address must be rejected even for --dry-run");
assert!(
format!("{err:#}").contains("--provider-address requires --node-id"),
"expected the validate() guard error, got: {err:#}"
);
}
fn pull_args(extra: &[&str]) -> BundlePullArgs {
use clap::Parser;
#[derive(Parser)]
struct T {
#[command(flatten)]
a: decdn_common::cli::BundlePullArgs,
}
T::parse_from(std::iter::once("t").chain(extra.iter().copied())).a
}
#[test]
fn dry_run_reads_a_cached_hash_manifest() {
let tmp = tempfile::tempdir().expect("tmp");
let out = tmp.path().to_str().expect("utf-8 tmp path");
let hash = cache_manifest(
tmp.path(),
&serde_json::json!({
"version": 1,
"entries": [
{"path": "openai-gpt-oss-20b/config.json", "hash": "b3:aa", "size": 10},
{"path": "openai-gpt-oss-20b/metal/model.bin", "hash": "b3:bb", "size": 20},
],
}),
);
for overwrite in [false, true] {
let mut argv = vec!["-o", out, "--hash", &hash, "--dry-run"];
if overwrite {
argv.push("--overwrite");
}
let manifest = dry_run_manifest(&pull_args(&argv))
.unwrap()
.expect("the cached manifest is read");
assert_eq!(
manifest
.entries
.iter()
.map(|e| e.path.as_str())
.collect::<Vec<_>>(),
vec![
"openai-gpt-oss-20b/config.json",
"openai-gpt-oss-20b/metal/model.bin"
],
"overwrite={overwrite}"
);
}
}
#[tokio::test]
async fn dry_run_has_no_manifest_for_an_uncached_hash() {
let tmp = tempfile::tempdir().expect("tmp");
let out = tmp.path().to_str().expect("utf-8 tmp path");
let hash = format!("b3:{}", "ab".repeat(32));
let args = pull_args(&["-o", out, "--hash", &hash, "--dry-run"]);
assert!(dry_run_manifest(&args).unwrap().is_none());
super::bundle_pull(&args, None).await.unwrap();
}
#[tokio::test]
async fn dry_run_fails_on_a_cached_manifest_that_does_not_parse() {
let tmp = tempfile::tempdir().expect("tmp");
let out = tmp.path().to_str().expect("utf-8 tmp path");
let hash = cache_manifest(
tmp.path(),
&serde_json::json!({"version": 2, "entries": []}),
);
let args = pull_args(&["-o", out, "--hash", &hash, "--dry-run"]);
let err = super::bundle_pull(&args, None)
.await
.expect_err("a cached non-v1 manifest fails the dry run");
let msg = format!("{err:#}");
assert!(msg.contains("unsupported bundle version 2"), "{msg}");
assert!(msg.contains(".decdn-bundles"), "{msg}");
}
#[tokio::test]
async fn dry_run_fails_naming_an_unreadable_cached_manifest() {
let tmp = tempfile::tempdir().expect("tmp");
let out = tmp.path().to_str().expect("utf-8 tmp path");
let raw = [0xab_u8; 32];
let hex = blake3::Hash::from_bytes(raw).to_hex();
std::fs::create_dir_all(BundleManifest::cache_path(tmp.path(), raw)).expect("mkdir");
let hash = format!("b3:{hex}");
let args = pull_args(&["-o", out, "--hash", &hash, "--dry-run"]);
let err = super::bundle_pull(&args, None)
.await
.expect_err("an unreadable cached manifest fails the dry run");
let msg = format!("{err:#}");
assert!(msg.contains(".decdn-bundles"), "{msg}");
assert!(msg.contains(hex.as_str()), "{msg}");
assert!(msg.contains("os error"), "{msg}");
}
#[tokio::test]
async fn dry_run_fails_naming_a_cached_manifest_that_fails_its_hash_check() {
let tmp = tempfile::tempdir().expect("tmp");
let out = tmp.path().to_str().expect("utf-8 tmp path");
let hash = cache_manifest(
tmp.path(),
&serde_json::json!({"version": 1, "entries": []}),
);
let raw = fetch::parse_hash(&hash).expect("hash");
std::fs::write(BundleManifest::cache_path(tmp.path(), raw), b"tampered").expect("tamper");
let args = pull_args(&["-o", out, "--hash", &hash, "--dry-run"]);
let err = super::bundle_pull(&args, None)
.await
.expect_err("a tampered cached manifest fails the dry run");
let msg = format!("{err:#}");
assert!(msg.contains("does not hash to the bundle"), "{msg}");
assert!(msg.contains(".decdn-bundles"), "{msg}");
}
#[tokio::test]
async fn dry_run_rejects_a_malformed_hash() {
let tmp = tempfile::tempdir().expect("tmp");
let out = tmp.path().to_str().expect("utf-8 tmp path");
let args = pull_args(&["-o", out, "--hash", "b3:aa", "--dry-run"]);
let err = super::bundle_pull(&args, None)
.await
.expect_err("a malformed --hash fails the dry run");
assert!(format!("{err:#}").contains("invalid hash"), "{err:#}");
}
#[test]
fn entry_done_line_rates_a_plain_entry_on_its_download() {
let line = entry_done_line(
[1; 32],
4 << 20,
EntryBytes {
paid: 4 << 20,
spliced: 0,
resumed: 0,
},
std::time::Duration::from_secs(2),
);
assert!(line.ends_with(": 4.00 MiB in 2.0s (2.00 MiB/s)"), "{line}");
}
#[test]
fn entry_done_line_splits_a_dedup_entrys_spliced_bytes() {
let line = entry_done_line(
[1; 32],
12 << 20,
EntryBytes {
paid: 2 << 20,
spliced: 10 << 20,
resumed: 0,
},
std::time::Duration::from_secs(2),
);
assert!(
line.ends_with(
": 12.00 MiB in 2.0s (2.00 MiB downloaded at 1.00 MiB/s, 10.00 MiB spliced \
from disk)"
),
"{line}"
);
}
#[test]
fn entry_done_line_renders_a_fully_spliced_entry() {
let line = entry_done_line(
[1; 32],
12 << 20,
EntryBytes {
paid: 0,
spliced: 12 << 20,
resumed: 0,
},
std::time::Duration::from_secs(2),
);
assert!(
line.ends_with(": 12.00 MiB in 2.0s (0 B downloaded at --, 12.00 MiB spliced from disk)"),
"{line}"
);
}
#[test]
fn entry_done_line_splits_a_resumed_entrys_bytes() {
let line = entry_done_line(
[1; 32],
12 << 20,
EntryBytes {
paid: 2 << 20,
spliced: 6 << 20,
resumed: 4 << 20,
},
std::time::Duration::from_secs(2),
);
assert!(
line.ends_with(
": 12.00 MiB in 2.0s (2.00 MiB downloaded at 1.00 MiB/s, 4.00 MiB resumed, \
6.00 MiB spliced from disk)"
),
"{line}"
);
let line = entry_done_line(
[1; 32],
6 << 20,
EntryBytes {
paid: 2 << 20,
spliced: 0,
resumed: 4 << 20,
},
std::time::Duration::from_secs(2),
);
assert!(
line.ends_with(": 6.00 MiB in 2.0s (2.00 MiB downloaded at 1.00 MiB/s, 4.00 MiB resumed)"),
"{line}"
);
}
#[test]
fn entry_done_line_guards_a_zero_elapsed() {
let line = entry_done_line(
[1; 32],
1 << 20,
EntryBytes {
paid: 1 << 20,
spliced: 0,
resumed: 0,
},
std::time::Duration::ZERO,
);
assert!(line.ends_with(": 1.00 MiB in 0.0s (--)"), "{line}");
}
#[test]
fn entry_filter_passthrough_when_no_flags() {
assert_eq!(
filtered_paths(&[], &[], sample()),
vec![
"models/a.bin",
"models/b.txt",
"docs/readme.md",
"docs/deep/notes.txt"
]
);
}
#[test]
fn entry_filter_include_is_a_whitelist_gate() {
assert_eq!(
filtered_paths(&["models/*"], &[], sample()),
vec!["models/a.bin", "models/b.txt"]
);
}
#[test]
fn entry_filter_includes_are_ored() {
assert_eq!(
filtered_paths(&["models/*.bin", "docs/readme.md"], &[], sample()),
vec!["models/a.bin", "docs/readme.md"]
);
}
#[test]
fn entry_filter_excludes_are_ored() {
assert_eq!(
filtered_paths(&[], &["**/*.txt", "**/*.md"], sample()),
vec!["models/a.bin"]
);
}
#[test]
fn entry_filter_star_does_not_cross_slash() {
assert_eq!(
filtered_paths(&[], &["*.txt"], sample()),
vec![
"models/a.bin",
"models/b.txt",
"docs/readme.md",
"docs/deep/notes.txt"
]
);
}
#[test]
fn entry_filter_exclude_beats_include() {
assert_eq!(
filtered_paths(&["models/*"], &["**/*.txt"], sample()),
vec!["models/a.bin"]
);
}
#[test]
fn entry_filter_double_star_recurses() {
assert_eq!(
filtered_paths(&["docs/**"], &[], sample()),
vec!["docs/readme.md", "docs/deep/notes.txt"]
);
assert_eq!(
filtered_paths(&["docs/*"], &[], sample()),
vec!["docs/readme.md"]
);
}
#[test]
fn entry_filter_can_empty_the_set() {
assert!(filtered_paths(&["no/such/*"], &[], sample()).is_empty());
}