Skip to content

Commit d1f683c

Browse files
committed
✅ Expand test assert of create subcommand
1 parent ab2dad6 commit d1f683c

File tree

5 files changed

+103
-71
lines changed

5 files changed

+103
-71
lines changed

cli/tests/cli/create.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,27 @@ mod exclude_from;
55
mod exclude_vcs;
66
mod files_from;
77
mod files_from_stdin;
8-
mod gitignore;
98
mod include;
109
mod mtime;
11-
mod no_recursive;
1210
mod numeric_owner;
11+
mod option_gitignore;
1312
#[cfg(any(windows, target_os = "macos"))]
1413
mod option_newer_ctime;
1514
mod option_newer_ctime_than;
1615
mod option_newer_mtime;
1716
mod option_newer_mtime_than;
17+
mod option_no_recursive;
1818
#[cfg(any(windows, target_os = "macos"))]
1919
mod option_older_ctime;
2020
mod option_older_ctime_than;
2121
mod option_older_mtime;
2222
mod option_older_mtime_than;
2323
#[cfg(unix)]
2424
mod option_one_file_system;
25+
mod option_strip_components;
2526
mod password_from_file;
2627
mod password_hash;
2728
mod sanitize_parent_components;
28-
mod strip_components;
2929
mod substitution;
3030
mod symlink;
3131
mod transform;

cli/tests/cli/create/no_recursive.rs

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ use clap::Parser;
33
use portable_network_archive::cli;
44
use std::{collections::HashSet, fs};
55

6+
/// Precondition: A directory contains a `.gitignore` file with `*.log` pattern and both `.txt` and `.log` files.
7+
/// Action: Run `pna create` with `--gitignore`.
8+
/// Expectation: The `.log` file is excluded; `.gitignore` and `.txt` files are included.
69
#[test]
710
fn create_with_gitignore() {
811
setup();
@@ -25,7 +28,6 @@ fn create_with_gitignore() {
2528
.execute()
2629
.unwrap();
2730

28-
// Expect: only `.gitignore` and `keep.txt` are present.
2931
let mut seen = HashSet::new();
3032
archive::for_each_entry("gitignore/gitignore.pna", |entry| {
3133
seen.insert(entry.header().path().to_string());
@@ -41,6 +43,10 @@ fn create_with_gitignore() {
4143
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
4244
}
4345

46+
/// Precondition: A complex directory tree with nested `.gitignore` files containing various patterns
47+
/// including negation (`!`), directory ignore (`build/`), and double-star globs (`**/secret.txt`).
48+
/// Action: Run `pna create` with `--gitignore`.
49+
/// Expectation: Files are included or excluded according to gitignore rules; child rules can override parent rules.
4450
#[test]
4551
fn create_with_gitignore_subdirs_and_negation() {
4652
// Matrix (path => expected with --gitignore):
@@ -97,7 +103,6 @@ fn create_with_gitignore_subdirs_and_negation() {
97103
fs::write("gitignore/complex/source/tmponly/file.tmp", b"ignored").unwrap();
98104
fs::write("gitignore/complex/source/tmponly/file.txt", b"ok").unwrap();
99105

100-
// Create archive with --gitignore
101106
cli::Cli::try_parse_from([
102107
"pna",
103108
"--quiet",
@@ -112,14 +117,12 @@ fn create_with_gitignore_subdirs_and_negation() {
112117
.execute()
113118
.unwrap();
114119

115-
// Verify using entries inside the archive (no extraction).
116120
let mut seen = HashSet::new();
117121
archive::for_each_entry("gitignore/complex/archive.pna", |entry| {
118122
seen.insert(entry.header().path().to_string());
119123
})
120124
.unwrap();
121125

122-
// Exact set of expected entries.
123126
for required in [
124127
"gitignore/complex/source/.gitignore",
125128
"gitignore/complex/source/keep.txt",
@@ -139,17 +142,17 @@ fn create_with_gitignore_subdirs_and_negation() {
139142
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
140143
}
141144

145+
/// Precondition: Parent `.gitignore` unignores `*.log`; child `.gitignore` re-ignores `*.log`.
146+
/// Action: Run `pna create` with `--gitignore`.
147+
/// Expectation: Parent's `.log` files are included; child's `.log` files are excluded (child rule overrides).
142148
#[test]
143149
fn create_with_gitignore_child_overrides_parent_ignore() {
144-
// Parent unignores (*.log), child re-ignores (*.log)
145150
setup();
146151
fs::create_dir_all("gitignore/child_overrides/source/child").unwrap();
147152

148-
// Parent allows all .log
149153
fs::write("gitignore/child_overrides/source/.gitignore", "!*.log\n").unwrap();
150154
fs::write("gitignore/child_overrides/source/root.log", b"ok").unwrap();
151155

152-
// Child ignores .log
153156
fs::write(
154157
"gitignore/child_overrides/source/child/.gitignore",
155158
"*.log\n",
@@ -160,7 +163,6 @@ fn create_with_gitignore_child_overrides_parent_ignore() {
160163
b"ignored by child",
161164
)
162165
.unwrap();
163-
// Another .log in child should also be excluded
164166
fs::write(
165167
"gitignore/child_overrides/source/child/other.log",
166168
b"ignored too",
@@ -200,9 +202,11 @@ fn create_with_gitignore_child_overrides_parent_ignore() {
200202
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
201203
}
202204

205+
/// Precondition: Three-level nesting where parent ignores `*.log`, child unignores, grandchild re-ignores.
206+
/// Action: Run `pna create` with `--gitignore`.
207+
/// Expectation: Each level's rule applies to its subtree; grandchild's `.log` files are excluded.
203208
#[test]
204209
fn create_with_gitignore_multi_level_toggle() {
205-
// Parent: *.log (ignore) -> Child: !*.log (unignore) -> Grandchild: *.log (ignore again)
206210
setup();
207211
fs::create_dir_all("gitignore/multi_toggle/source/child/nested").unwrap();
208212

@@ -222,7 +226,6 @@ fn create_with_gitignore_multi_level_toggle() {
222226
b"drop",
223227
)
224228
.unwrap();
225-
// Another file under child that should be kept
226229
fs::write("gitignore/multi_toggle/source/child/extra.log", b"keep").unwrap();
227230

228231
cli::Cli::try_parse_from([
@@ -260,24 +263,23 @@ fn create_with_gitignore_multi_level_toggle() {
260263
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
261264
}
262265

266+
/// Precondition: A `.gitignore` contains multiple rules where later rules override earlier ones.
267+
/// Action: Run `pna create` with `--gitignore`.
268+
/// Expectation: The last matching rule wins; `*.log` then `!keep.log` keeps `keep.log`.
263269
#[test]
264270
fn create_with_gitignore_last_match_wins() {
265-
// Within a single .gitignore, the last matching rule wins
266271
setup();
267272
fs::create_dir_all("gitignore/last_match/source/order_allow").unwrap();
268273
fs::create_dir_all("gitignore/last_match/source/order_deny").unwrap();
269274

270-
// Case A: ignore then unignore (should be kept)
271275
fs::write(
272276
"gitignore/last_match/source/order_allow/.gitignore",
273277
"*.log\n!keep.log\n",
274278
)
275279
.unwrap();
276280
fs::write("gitignore/last_match/source/order_allow/keep.log", b"keep").unwrap();
277-
// This one should remain ignored
278281
fs::write("gitignore/last_match/source/order_allow/drop.log", b"drop").unwrap();
279282

280-
// Case B: unignore then ignore (should be dropped)
281283
fs::write(
282284
"gitignore/last_match/source/order_deny/.gitignore",
283285
"!keep.log\n*.log\n",
@@ -318,13 +320,14 @@ fn create_with_gitignore_last_match_wins() {
318320
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
319321
}
320322

323+
/// Precondition: Child `.gitignore` contains `/only_here.txt` (anchored pattern).
324+
/// Action: Run `pna create` with `--gitignore`.
325+
/// Expectation: Only `child/only_here.txt` is excluded; `child/nested/only_here.txt` is included.
321326
#[test]
322327
fn create_with_gitignore_child_anchored_slash() {
323-
// Leading slash in child .gitignore anchors to the child directory root
324328
setup();
325329
fs::create_dir_all("gitignore/child_anchor/source/child/nested").unwrap();
326330

327-
// Child rule: only child/only_here.txt is excluded
328331
fs::write(
329332
"gitignore/child_anchor/source/child/.gitignore",
330333
"/only_here.txt\n",
@@ -369,9 +372,11 @@ fn create_with_gitignore_child_anchored_slash() {
369372
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
370373
}
371374

375+
/// Precondition: Parent `.gitignore` prunes `sub/` directory; child `.gitignore` tries to unignore files.
376+
/// Action: Run `pna create` with `--gitignore`.
377+
/// Expectation: Once a directory is pruned, child rules cannot resurrect files inside it.
372378
#[test]
373379
fn create_with_gitignore_pruned_dir_cannot_unignore_inside() {
374-
// If parent prunes 'sub/', child '!keep.txt' cannot resurrect files inside
375380
setup();
376381
fs::create_dir_all("gitignore/pruned_dir/source/sub").unwrap();
377382

@@ -382,7 +387,6 @@ fn create_with_gitignore_pruned_dir_cannot_unignore_inside() {
382387
b"should not be included",
383388
)
384389
.unwrap();
385-
// Another file under the pruned dir
386390
fs::write("gitignore/pruned_dir/source/sub/also.txt", b"not included").unwrap();
387391

388392
cli::Cli::try_parse_from([
@@ -415,9 +419,11 @@ fn create_with_gitignore_pruned_dir_cannot_unignore_inside() {
415419
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
416420
}
417421

422+
/// Precondition: Parent `.gitignore` prunes `sub/` but then unignores `!sub/` and `!sub/keep.txt`.
423+
/// Action: Run `pna create` with `--gitignore`.
424+
/// Expectation: Re-inclusion works when parent explicitly unignores the directory and specific files.
418425
#[test]
419426
fn create_with_gitignore_pruned_dir_unignore_with_parent_exceptions() {
420-
// Re-inclusion works only if parent adds '!sub/' and '!sub/keep.txt'
421427
setup();
422428
fs::create_dir_all("gitignore/pruned_dir_fix/source/sub").unwrap();
423429

@@ -467,16 +473,17 @@ fn create_with_gitignore_pruned_dir_unignore_with_parent_exceptions() {
467473
"required entry missing: {required}"
468474
);
469475
}
476+
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
470477
}
471478

479+
/// Precondition: A `.gitignore` file contains a pattern that matches `.gitignore` itself.
480+
/// Action: Run `pna create` with `--gitignore`.
481+
/// Expectation: The `.gitignore` file is excluded from the archive.
472482
#[test]
473483
fn create_with_gitignore_excludes_gitignore_file_itself() {
474-
// A .gitignore rule can exclude the .gitignore file itself.
475-
// When the pattern contains ".gitignore", the file should not be archived.
476484
setup();
477485
fs::create_dir_all("gitignore/self_exclude/source").unwrap();
478486

479-
// Ignore the .gitignore file itself.
480487
fs::write("gitignore/self_exclude/source/.gitignore", ".gitignore\n").unwrap();
481488
fs::write("gitignore/self_exclude/source/keep.txt", b"ok").unwrap();
482489

@@ -510,6 +517,9 @@ fn create_with_gitignore_excludes_gitignore_file_itself() {
510517
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
511518
}
512519

520+
/// Precondition: Sibling directories A and B each have their own `.gitignore` with different rules.
521+
/// Action: Run `pna create` with `--gitignore`.
522+
/// Expectation: Each sibling's rules apply only to its own subtree; no rule leakage across siblings.
513523
#[test]
514524
fn create_with_gitignore_sibling_scopes_do_not_leak() {
515525
// Sibling directories each have their own .gitignore, and rules apply only to their subtree.
@@ -529,7 +539,6 @@ fn create_with_gitignore_sibling_scopes_do_not_leak() {
529539
fs::write("gitignore/sibling_scope/source/B/b.log", b"ok").unwrap();
530540
fs::write("gitignore/sibling_scope/source/B/tmp.tmp", b"drop").unwrap();
531541

532-
// Create archive with --gitignore
533542
cli::Cli::try_parse_from([
534543
"pna",
535544
"--quiet",
@@ -544,7 +553,6 @@ fn create_with_gitignore_sibling_scopes_do_not_leak() {
544553
.execute()
545554
.unwrap();
546555

547-
// Verify exact set of entries; no leakage across siblings.
548556
let mut seen = HashSet::new();
549557
archive::for_each_entry("gitignore/sibling_scope/archive.pna", |entry| {
550558
seen.insert(entry.header().path().to_string());
@@ -565,10 +573,11 @@ fn create_with_gitignore_sibling_scopes_do_not_leak() {
565573
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
566574
}
567575

576+
/// Precondition: A `.gitignore` contains a comment line (`#...`) and an escaped `#` pattern (`\#secret.txt`).
577+
/// Action: Run `pna create` with `--gitignore`.
578+
/// Expectation: Comment lines are ignored; escaped `#` matches a file starting with `#`.
568579
#[test]
569580
fn create_with_gitignore_comment_and_escape() {
570-
// '#' starts a comment unless escaped; '\#file' matches a file literally starting with '#'.
571-
// Verify that comments don't act as patterns and escaped '#' does.
572581
setup();
573582
fs::create_dir_all("gitignore/comment_escape/source").unwrap();
574583

@@ -612,10 +621,11 @@ fn create_with_gitignore_comment_and_escape() {
612621
assert!(seen.is_empty(), "unexpected entries found: {seen:?}");
613622
}
614623

624+
/// Precondition: A `.gitignore` contains `\!file.txt` to match a file literally named `!file.txt`.
625+
/// Action: Run `pna create` with `--gitignore`.
626+
/// Expectation: The file `!file.txt` is excluded; leading `!` in patterns unignores, but escaped `\!` matches literal.
615627
#[test]
616628
fn create_with_gitignore_literal_bang_pattern() {
617-
// Leading '!' unignores patterns; to match a literal '!' in filename, it must be escaped.
618-
// Verify that '\!file.txt' ignores a file literally named '!file.txt'.
619629
setup();
620630
fs::create_dir_all("gitignore/literal_bang/source").unwrap();
621631

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use crate::utils::{archive, setup};
2+
use clap::Parser;
3+
use portable_network_archive::cli;
4+
use std::{collections::HashSet, fs};
5+
6+
/// Precondition: A directory contains files and subdirectories.
7+
/// Action: Run `pna create` with `--no-recursive`.
8+
/// Expectation: The archive contains only the top-level directory entry, not its contents.
9+
#[test]
10+
fn no_recursive() {
11+
setup();
12+
13+
let _ = fs::remove_dir_all("no_recursive");
14+
fs::create_dir_all("no_recursive/in/subdir").unwrap();
15+
16+
fs::write("no_recursive/in/file.txt", "content").unwrap();
17+
fs::write("no_recursive/in/subdir/nested.txt", "nested content").unwrap();
18+
19+
cli::Cli::try_parse_from([
20+
"pna",
21+
"--quiet",
22+
"create",
23+
"no_recursive/no_recursive.pna",
24+
"--overwrite",
25+
"--keep-dir",
26+
"--no-recursive",
27+
"no_recursive/in/",
28+
])
29+
.unwrap()
30+
.execute()
31+
.unwrap();
32+
33+
let mut seen = HashSet::new();
34+
archive::for_each_entry("no_recursive/no_recursive.pna", |entry| {
35+
seen.insert(entry.header().path().to_string());
36+
})
37+
.unwrap();
38+
39+
// With --no-recursive, only the top-level directory should be included.
40+
assert!(
41+
seen.contains("no_recursive/in"),
42+
"top-level directory should be included"
43+
);
44+
assert!(
45+
!seen.contains("no_recursive/in/file.txt"),
46+
"file inside directory should NOT be included"
47+
);
48+
assert!(
49+
!seen.contains("no_recursive/in/subdir"),
50+
"subdirectory should NOT be included"
51+
);
52+
assert!(
53+
!seen.contains("no_recursive/in/subdir/nested.txt"),
54+
"nested file should NOT be included"
55+
);
56+
assert_eq!(
57+
seen.len(),
58+
1,
59+
"Expected exactly 1 entry (top-level directory only), but found {}: {seen:?}",
60+
seen.len()
61+
);
62+
}
File renamed without changes.

0 commit comments

Comments
 (0)