@@ -3,6 +3,9 @@ use clap::Parser;
33use portable_network_archive:: cli;
44use 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]
710fn 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]
4551fn 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]
143149fn 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]
204209fn 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]
264270fn 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]
322327fn 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]
373379fn 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]
419426fn 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]
473483fn 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]
514524fn 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]
569580fn 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]
616628fn 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
0 commit comments