Skip to content

Commit ab2dad6

Browse files
committed
✅ Expand test case for create subcommand
1 parent afbf755 commit ab2dad6

File tree

3 files changed

+318
-0
lines changed

3 files changed

+318
-0
lines changed

cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ maplit = "1.0.2"
6969
path-slash = "0.2.1"
7070
rust-embed = { version = "8.9.0", features = ["debug-embed"] }
7171
same-file = "1.0.6"
72+
scopeguard = "1.2.0"
7273
walkdir = "2.5.0"
7374
criterion = { version = "0.7.0", default-features = false, features = ["cargo_bench_support", "plotters"] }
7475

cli/tests/cli/create.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ mod option_older_ctime;
2020
mod option_older_ctime_than;
2121
mod option_older_mtime;
2222
mod option_older_mtime_than;
23+
#[cfg(unix)]
24+
mod option_one_file_system;
2325
mod password_from_file;
2426
mod password_hash;
2527
mod sanitize_parent_components;
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
use crate::utils::{archive, setup};
2+
use clap::Parser;
3+
use portable_network_archive::cli;
4+
use std::{collections::HashSet, fs};
5+
6+
#[cfg(target_os = "linux")]
7+
mod platform {
8+
use super::*;
9+
use std::os::unix::fs::MetadataExt;
10+
use std::process::Command;
11+
12+
fn can_mount_tmpfs() -> bool {
13+
let uid = unsafe { libc::getuid() };
14+
if uid != 0 {
15+
return false;
16+
}
17+
let test_path = "/tmp/pna_mount_test";
18+
let _ = fs::remove_dir_all(test_path);
19+
if fs::create_dir_all(test_path).is_err() {
20+
return false;
21+
}
22+
let result = Command::new("mount")
23+
.args(["-t", "tmpfs", "tmpfs", test_path])
24+
.status()
25+
.map(|s| s.success())
26+
.unwrap_or(false);
27+
if result {
28+
let _ = Command::new("umount").arg(test_path).status();
29+
}
30+
let _ = fs::remove_dir_all(test_path);
31+
result
32+
}
33+
34+
/// Precondition: A directory contains a file and a subdirectory mounted as tmpfs with its own file.
35+
/// Action: Run `pna create` with `--one-file-system`.
36+
/// Expectation: The archive contains only entries from the original filesystem; the tmpfs file is excluded.
37+
/// Note: This test requires root privileges to mount tmpfs.
38+
#[test]
39+
fn create_with_one_file_system() {
40+
setup();
41+
if !can_mount_tmpfs() {
42+
eprintln!("Skipping test: cannot create tmpfs mount (requires root)");
43+
return;
44+
}
45+
46+
let _ = fs::remove_dir_all("option_one_file_system_mount");
47+
fs::create_dir_all("option_one_file_system_mount/in/subdir").unwrap();
48+
49+
// Defer cleanup to ensure it runs even if the test panics.
50+
scopeguard::defer! {
51+
let _ = Command::new("umount").arg("option_one_file_system_mount/in/subdir").status();
52+
let _ = fs::remove_dir_all("option_one_file_system_mount");
53+
};
54+
55+
fs::write("option_one_file_system_mount/in/main_file.txt", "main fs").unwrap();
56+
57+
let mount_status = Command::new("mount")
58+
.args([
59+
"-t",
60+
"tmpfs",
61+
"tmpfs",
62+
"option_one_file_system_mount/in/subdir",
63+
])
64+
.status()
65+
.expect("failed to execute mount");
66+
if !mount_status.success() {
67+
eprintln!("Skipping test: failed to mount tmpfs");
68+
return;
69+
}
70+
71+
fs::write(
72+
"option_one_file_system_mount/in/subdir/tmpfs_file.txt",
73+
"tmpfs content",
74+
)
75+
.unwrap();
76+
77+
let main_dev = fs::metadata("option_one_file_system_mount/in/main_file.txt")
78+
.unwrap()
79+
.dev();
80+
let tmpfs_dev = fs::metadata("option_one_file_system_mount/in/subdir/tmpfs_file.txt")
81+
.unwrap()
82+
.dev();
83+
assert_ne!(
84+
main_dev, tmpfs_dev,
85+
"files should be on different filesystems"
86+
);
87+
88+
cli::Cli::try_parse_from([
89+
"pna",
90+
"--quiet",
91+
"create",
92+
"option_one_file_system_mount/archive_with_flag.pna",
93+
"--overwrite",
94+
"--keep-dir",
95+
"--unstable",
96+
"--one-file-system",
97+
"option_one_file_system_mount/in/",
98+
])
99+
.unwrap()
100+
.execute()
101+
.unwrap();
102+
103+
cli::Cli::try_parse_from([
104+
"pna",
105+
"--quiet",
106+
"create",
107+
"option_one_file_system_mount/archive_without_flag.pna",
108+
"--overwrite",
109+
"--keep-dir",
110+
"option_one_file_system_mount/in/",
111+
])
112+
.unwrap()
113+
.execute()
114+
.unwrap();
115+
116+
let _ = Command::new("umount")
117+
.arg("option_one_file_system_mount/in/subdir")
118+
.status();
119+
120+
// Verify archive with --one-file-system
121+
let mut seen = HashSet::new();
122+
archive::for_each_entry(
123+
"option_one_file_system_mount/archive_with_flag.pna",
124+
|entry| {
125+
seen.insert(entry.header().path().to_string());
126+
},
127+
)
128+
.unwrap();
129+
130+
assert!(
131+
seen.contains("option_one_file_system_mount/in"),
132+
"input directory should be included"
133+
);
134+
assert!(
135+
seen.contains("option_one_file_system_mount/in/main_file.txt"),
136+
"main file should be included"
137+
);
138+
assert!(
139+
!seen.contains("option_one_file_system_mount/in/subdir"),
140+
"subdir directory should NOT be included (on different filesystem)"
141+
);
142+
assert!(
143+
!seen.contains("option_one_file_system_mount/in/subdir/tmpfs_file.txt"),
144+
"tmpfs file should NOT be included"
145+
);
146+
assert_eq!(
147+
seen.len(),
148+
2,
149+
"Expected exactly 2 entries (directory and main file), but found {}: {seen:?}",
150+
seen.len()
151+
);
152+
153+
// Verify archive without --one-file-system
154+
let mut seen = HashSet::new();
155+
archive::for_each_entry(
156+
"option_one_file_system_mount/archive_without_flag.pna",
157+
|entry| {
158+
seen.insert(entry.header().path().to_string());
159+
},
160+
)
161+
.unwrap();
162+
163+
assert!(
164+
seen.contains("option_one_file_system_mount/in"),
165+
"input directory should be included"
166+
);
167+
assert!(
168+
seen.contains("option_one_file_system_mount/in/main_file.txt"),
169+
"main file should be included"
170+
);
171+
assert!(
172+
seen.contains("option_one_file_system_mount/in/subdir"),
173+
"subdir directory should be included"
174+
);
175+
assert!(
176+
seen.contains("option_one_file_system_mount/in/subdir/tmpfs_file.txt"),
177+
"tmpfs file should be included"
178+
);
179+
assert_eq!(
180+
seen.len(),
181+
4,
182+
"Expected exactly 4 entries (2 directories and 2 files), but found {}: {seen:?}",
183+
seen.len()
184+
);
185+
}
186+
187+
/// Precondition: A directory contains files and subdirectories all on the same filesystem.
188+
/// Action: Run `pna create` with `--one-file-system`.
189+
/// Expectation: All entries are included in the archive.
190+
#[test]
191+
fn create_with_one_file_system_same_fs() {
192+
setup();
193+
194+
let _ = fs::remove_dir_all("option_one_file_system_local");
195+
fs::create_dir_all("option_one_file_system_local/in/subdir").unwrap();
196+
197+
fs::write("option_one_file_system_local/in/file1.txt", "content1").unwrap();
198+
fs::write(
199+
"option_one_file_system_local/in/subdir/file2.txt",
200+
"content2",
201+
)
202+
.unwrap();
203+
204+
cli::Cli::try_parse_from([
205+
"pna",
206+
"--quiet",
207+
"create",
208+
"option_one_file_system_local/archive.pna",
209+
"--overwrite",
210+
"--keep-dir",
211+
"--unstable",
212+
"--one-file-system",
213+
"option_one_file_system_local/in/",
214+
])
215+
.unwrap()
216+
.execute()
217+
.unwrap();
218+
219+
let mut seen = HashSet::new();
220+
archive::for_each_entry("option_one_file_system_local/archive.pna", |entry| {
221+
seen.insert(entry.header().path().to_string());
222+
})
223+
.unwrap();
224+
225+
assert!(
226+
seen.contains("option_one_file_system_local/in"),
227+
"input directory should be included"
228+
);
229+
assert!(
230+
seen.contains("option_one_file_system_local/in/file1.txt"),
231+
"file1.txt should be included"
232+
);
233+
assert!(
234+
seen.contains("option_one_file_system_local/in/subdir"),
235+
"subdir directory should be included"
236+
);
237+
assert!(
238+
seen.contains("option_one_file_system_local/in/subdir/file2.txt"),
239+
"file2.txt should be included"
240+
);
241+
assert_eq!(
242+
seen.len(),
243+
4,
244+
"Expected exactly 4 entries (2 directories and 2 files), but found {}: {seen:?}",
245+
seen.len()
246+
);
247+
}
248+
}
249+
250+
#[cfg(all(unix, not(target_os = "linux")))]
251+
mod platform {
252+
use super::*;
253+
254+
/// Precondition: A directory contains files and subdirectories all on the same filesystem.
255+
/// Action: Run `pna create` with `--one-file-system`.
256+
/// Expectation: All entries are included in the archive.
257+
#[test]
258+
fn create_with_one_file_system_same_fs() {
259+
setup();
260+
261+
let _ = fs::remove_dir_all("option_one_file_system_local");
262+
fs::create_dir_all("option_one_file_system_local/in/subdir").unwrap();
263+
264+
fs::write("option_one_file_system_local/in/file1.txt", "content1").unwrap();
265+
fs::write(
266+
"option_one_file_system_local/in/subdir/file2.txt",
267+
"content2",
268+
)
269+
.unwrap();
270+
271+
cli::Cli::try_parse_from([
272+
"pna",
273+
"--quiet",
274+
"create",
275+
"option_one_file_system_local/archive.pna",
276+
"--overwrite",
277+
"--keep-dir",
278+
"--unstable",
279+
"--one-file-system",
280+
"option_one_file_system_local/in/",
281+
])
282+
.unwrap()
283+
.execute()
284+
.unwrap();
285+
286+
let mut seen = HashSet::new();
287+
archive::for_each_entry("option_one_file_system_local/archive.pna", |entry| {
288+
seen.insert(entry.header().path().to_string());
289+
})
290+
.unwrap();
291+
292+
assert!(
293+
seen.contains("option_one_file_system_local/in"),
294+
"input directory should be included"
295+
);
296+
assert!(
297+
seen.contains("option_one_file_system_local/in/file1.txt"),
298+
"file1.txt should be included"
299+
);
300+
assert!(
301+
seen.contains("option_one_file_system_local/in/subdir"),
302+
"subdir directory should be included"
303+
);
304+
assert!(
305+
seen.contains("option_one_file_system_local/in/subdir/file2.txt"),
306+
"file2.txt should be included"
307+
);
308+
assert_eq!(
309+
seen.len(),
310+
4,
311+
"Expected exactly 4 entries (2 directories and 2 files), but found {}: {seen:?}",
312+
seen.len()
313+
);
314+
}
315+
}

0 commit comments

Comments
 (0)