Skip to content

Commit f3a73e0

Browse files
committed
refactor(test): improve FUSE probe and cleanup in cross-device test
- Use `which` crate instead of executing binaries to probe for tools - Prefer `fusermount3` (libfuse v3) over `fusermount` (libfuse v2) - Replace `catch_unwind` with `FuseMount` Drop guard for cleanup - Use `unwrap_or_else` instead of match for `fuse_probe` result - Use `pipe-trait` for `Command::new` in `FuseMount::drop` - Use generic error messages instead of distro-specific `apt install` - Reformat `unexpected_cfgs` in Cargo.toml to multi-line style https://claude.ai/code/session_01LfpnUZrgq93MVZgA3KVqE6
1 parent 1af976d commit f3a73e0

3 files changed

Lines changed: 127 additions & 108 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ maplit = "1.0.2"
9191
normalize-path = "0.2.1"
9292
pretty_assertions = "1.4.1"
9393
rand = "0.10.0"
94+
which = "8.0.2"
9495

9596
[lints.rust]
96-
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(pdu_test_skip_fs_errors)', 'cfg(pdu_test_skip_cross_device)'] }
97+
unexpected_cfgs.level = "warn"
98+
unexpected_cfgs.check-cfg = [
99+
'cfg(pdu_test_skip_fs_errors)',
100+
'cfg(pdu_test_skip_cross_device)',
101+
]

tests/one_file_system.rs

Lines changed: 111 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use parallel_disk_usage::{
2929
reporter::{ErrorOnlyReporter, ErrorReport},
3030
size::Bytes,
3131
};
32+
use pipe_trait::Pipe;
3233
use pretty_assertions::assert_eq;
3334

3435
/// When all files reside on a single filesystem, `one_file_system: true` should produce
@@ -67,7 +68,7 @@ fn same_device_on_sample_workspace() {
6768
#[cfg(target_os = "linux")]
6869
#[cfg(not(pdu_test_skip_cross_device))]
6970
struct FuseTools {
70-
/// The fusermount command to use for unmounting (`"fusermount"` or `"fusermount3"`).
71+
/// The fusermount command to use for unmounting (`"fusermount3"` or `"fusermount"`).
7172
fusermount: &'static str,
7273
}
7374

@@ -77,54 +78,71 @@ struct FuseTools {
7778
/// 1. `mksquashfs` binary exists
7879
/// 2. `squashfuse` binary exists
7980
/// 3. `/dev/fuse` is accessible
80-
/// 4. `fusermount` (or `fusermount3`) binary exists
81+
/// 4. `fusermount3` (or `fusermount`) binary exists
8182
///
8283
/// Returns `Ok(FuseTools)` with the discovered tool paths, or `Err` with a diagnostic message.
8384
#[cfg(target_os = "linux")]
8485
#[cfg(not(pdu_test_skip_cross_device))]
8586
fn fuse_probe() -> Result<FuseTools, String> {
86-
use std::{path::Path, process::Command};
87+
use std::path::Path;
8788

88-
// Check that mksquashfs is installed
89-
Command::new("mksquashfs")
90-
.arg("-version")
91-
.output()
92-
.map_err(|error| {
93-
format!("`mksquashfs` not found: {error}. Install squashfs-tools for your platform.")
94-
})?;
89+
which::which("mksquashfs").map_err(|error| {
90+
format!("`mksquashfs` not found: {error}. Install squashfs-tools for your platform.")
91+
})?;
9592

96-
// Check that squashfuse is installed
97-
Command::new("squashfuse")
98-
.arg("--help")
99-
.output()
100-
.map_err(|error| {
101-
format!("`squashfuse` not found: {error}. Install squashfuse for your platform.")
102-
})?;
93+
which::which("squashfuse").map_err(|error| {
94+
format!("`squashfuse` not found: {error}. Install squashfuse for your platform.")
95+
})?;
10396

104-
// Check that /dev/fuse is accessible
10597
if !Path::new("/dev/fuse").exists() {
106-
return Err("/dev/fuse does not exist. \
107-
The FUSE kernel module may not be loaded (`modprobe fuse`)."
108-
.to_string());
98+
return Err(
99+
"/dev/fuse does not exist. The FUSE kernel module may not be loaded (`modprobe fuse`)."
100+
.to_string(),
101+
);
109102
}
110103

111-
// Check that fusermount is available (needed for unmounting)
112-
let has_fusermount = Command::new("fusermount").arg("-V").output().is_ok();
113-
let has_fusermount3 = Command::new("fusermount3").arg("-V").output().is_ok();
114-
let fusermount = match (has_fusermount, has_fusermount3) {
115-
(true, _) => "fusermount",
116-
(_, true) => "fusermount3",
117-
_ => {
118-
return Err(
119-
"Neither `fusermount` nor `fusermount3` found. Install FUSE for your platform."
120-
.to_string(),
121-
);
122-
}
104+
// Prefer fusermount3 (libfuse v3, actively developed) over fusermount (libfuse v2)
105+
let fusermount = if which::which("fusermount3").is_ok() {
106+
"fusermount3"
107+
} else if which::which("fusermount").is_ok() {
108+
"fusermount"
109+
} else {
110+
return Err(
111+
"Neither `fusermount3` nor `fusermount` found. Install FUSE for your platform."
112+
.to_string(),
113+
);
123114
};
124115

125116
Ok(FuseTools { fusermount })
126117
}
127118

119+
/// RAII guard that unmounts a FUSE mount point on drop.
120+
#[cfg(target_os = "linux")]
121+
#[cfg(not(pdu_test_skip_cross_device))]
122+
struct FuseMount {
123+
mount_point: std::path::PathBuf,
124+
fusermount: &'static str,
125+
}
126+
127+
#[cfg(target_os = "linux")]
128+
#[cfg(not(pdu_test_skip_cross_device))]
129+
impl Drop for FuseMount {
130+
fn drop(&mut self) {
131+
use command_extra::CommandExtra;
132+
let status = self
133+
.fusermount
134+
.pipe(std::process::Command::new)
135+
.with_arg("-u")
136+
.with_arg(&self.mount_point)
137+
.status();
138+
match status {
139+
Ok(status) if status.success() => {}
140+
Ok(status) => eprintln!("warning: {} exited with {status}", self.fusermount),
141+
Err(error) => eprintln!("warning: failed to run {}: {error}", self.fusermount),
142+
}
143+
}
144+
}
145+
128146
/// When a subdirectory is a mount point for a different filesystem, `-x` should exclude it.
129147
///
130148
/// Uses `squashfuse` to mount a squashfs image via FUSE — no root privileges or
@@ -143,16 +161,15 @@ fn cross_device_excludes_mount() {
143161
time::Duration,
144162
};
145163

146-
let fuse_tools = match fuse_probe() {
147-
Ok(tools) => tools,
148-
Err(reason) => panic!(
164+
let fuse_tools = fuse_probe().unwrap_or_else(|reason| {
165+
panic!(
149166
"error: This test requires FUSE (`mksquashfs`, `squashfuse`, `/dev/fuse`, \
150167
`fusermount`) but the probe failed.\n\
151168
reason: {reason}\n\
152169
hint: Install `squashfs-tools`, `squashfuse`, and FUSE for your platform, \
153170
or set `RUSTFLAGS='--cfg pdu_test_skip_cross_device'` to skip this test.",
154-
),
155-
};
171+
)
172+
});
156173

157174
let pdu = env!("CARGO_BIN_EXE_pdu");
158175
let temp = Temp::new_dir().expect("create temp dir for cross-device test");
@@ -187,7 +204,8 @@ fn cross_device_excludes_mount() {
187204
String::from_utf8_lossy(&mksquashfs_output.stderr),
188205
);
189206

190-
// Mount the squashfs image via squashfuse (read-only)
207+
// Mount the squashfs image via squashfuse (read-only).
208+
// The _fuse_mount guard ensures we unmount even if assertions panic.
191209
let mount_output = Command::new("squashfuse")
192210
.with_arg(&image_path)
193211
.with_arg(&mount_point)
@@ -200,79 +218,65 @@ fn cross_device_excludes_mount() {
200218
"squashfuse mount failed: {}",
201219
String::from_utf8_lossy(&mount_output.stderr),
202220
);
221+
let _fuse_mount = FuseMount {
222+
mount_point: mount_point.clone(),
223+
fusermount: fuse_tools.fusermount,
224+
};
203225

204226
// Small delay to let FUSE settle
205227
thread::sleep(Duration::from_millis(100));
206228

207-
// Ensure we unmount even if assertions fail
208-
let test_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
209-
// Run pdu WITHOUT -x — should see both files
210-
let without_x = Command::new(pdu)
211-
.with_args(["--bytes-format=plain"])
212-
.with_arg(&workspace)
213-
.with_stdout(Stdio::piped())
214-
.with_stderr(Stdio::piped())
215-
.output()
216-
.expect("run pdu without -x");
217-
let without_x_stdout = String::from_utf8_lossy(&without_x.stdout);
218-
let without_x_stderr = String::from_utf8_lossy(&without_x.stderr);
219-
if !without_x_stderr.is_empty() {
220-
eprintln!("pdu (no -x) STDERR:\n{without_x_stderr}");
221-
}
222-
eprintln!("pdu (no -x) STDOUT:\n{without_x_stdout}");
223-
assert!(
224-
without_x.status.success(),
225-
"pdu without -x failed: {without_x_stderr}",
226-
);
227-
assert!(
228-
without_x_stdout.contains("inside.txt"),
229-
"without -x should show inside.txt:\n{without_x_stdout}",
230-
);
231-
assert!(
232-
without_x_stdout.contains("outside.txt"),
233-
"without -x should show outside.txt:\n{without_x_stdout}",
234-
);
235-
236-
// Run pdu WITH -x — should only see outside.txt
237-
let with_x = Command::new(pdu)
238-
.with_args(["--bytes-format=plain", "-x"])
239-
.with_arg(&workspace)
240-
.with_stdout(Stdio::piped())
241-
.with_stderr(Stdio::piped())
242-
.output()
243-
.expect("run pdu with -x");
244-
let with_x_stdout = String::from_utf8_lossy(&with_x.stdout);
245-
let with_x_stderr = String::from_utf8_lossy(&with_x.stderr);
246-
if !with_x_stderr.is_empty() {
247-
eprintln!("pdu (-x) STDERR:\n{with_x_stderr}");
248-
}
249-
eprintln!("pdu (-x) STDOUT:\n{with_x_stdout}");
250-
assert!(
251-
with_x.status.success(),
252-
"pdu with -x failed: {with_x_stderr}",
253-
);
254-
assert!(
255-
with_x_stdout.contains("outside.txt"),
256-
"with -x should show outside.txt:\n{with_x_stdout}",
257-
);
258-
assert!(
259-
!with_x_stdout.contains("inside.txt"),
260-
"with -x should exclude inside.txt (on different filesystem):\n{with_x_stdout}",
261-
);
262-
}));
263-
264-
// Always unmount using the fusermount variant discovered by fuse_probe
265-
let unmount_status = Command::new(fuse_tools.fusermount)
266-
.with_arg("-u")
267-
.with_arg(&mount_point)
268-
.status();
269-
match unmount_status {
270-
Ok(status) if status.success() => {}
271-
Ok(status) => eprintln!("warning: {} exited with {status}", fuse_tools.fusermount),
272-
Err(error) => eprintln!("warning: failed to run {}: {error}", fuse_tools.fusermount),
229+
// Run pdu WITHOUT -x — should see both files
230+
let without_x = Command::new(pdu)
231+
.with_args(["--bytes-format=plain"])
232+
.with_arg(&workspace)
233+
.with_stdout(Stdio::piped())
234+
.with_stderr(Stdio::piped())
235+
.output()
236+
.expect("run pdu without -x");
237+
let without_x_stdout = String::from_utf8_lossy(&without_x.stdout);
238+
let without_x_stderr = String::from_utf8_lossy(&without_x.stderr);
239+
if !without_x_stderr.is_empty() {
240+
eprintln!("pdu (no -x) STDERR:\n{without_x_stderr}");
273241
}
242+
eprintln!("pdu (no -x) STDOUT:\n{without_x_stdout}");
243+
assert!(
244+
without_x.status.success(),
245+
"pdu without -x failed: {without_x_stderr}",
246+
);
247+
assert!(
248+
without_x_stdout.contains("inside.txt"),
249+
"without -x should show inside.txt:\n{without_x_stdout}",
250+
);
251+
assert!(
252+
without_x_stdout.contains("outside.txt"),
253+
"without -x should show outside.txt:\n{without_x_stdout}",
254+
);
274255

275-
if let Err(payload) = test_result {
276-
std::panic::resume_unwind(payload);
256+
// Run pdu WITH -x — should only see outside.txt
257+
let with_x = Command::new(pdu)
258+
.with_args(["--bytes-format=plain", "-x"])
259+
.with_arg(&workspace)
260+
.with_stdout(Stdio::piped())
261+
.with_stderr(Stdio::piped())
262+
.output()
263+
.expect("run pdu with -x");
264+
let with_x_stdout = String::from_utf8_lossy(&with_x.stdout);
265+
let with_x_stderr = String::from_utf8_lossy(&with_x.stderr);
266+
if !with_x_stderr.is_empty() {
267+
eprintln!("pdu (-x) STDERR:\n{with_x_stderr}");
277268
}
269+
eprintln!("pdu (-x) STDOUT:\n{with_x_stdout}");
270+
assert!(
271+
with_x.status.success(),
272+
"pdu with -x failed: {with_x_stderr}",
273+
);
274+
assert!(
275+
with_x_stdout.contains("outside.txt"),
276+
"with -x should show outside.txt:\n{with_x_stdout}",
277+
);
278+
assert!(
279+
!with_x_stdout.contains("inside.txt"),
280+
"with -x should exclude inside.txt (on different filesystem):\n{with_x_stdout}",
281+
);
278282
}

0 commit comments

Comments
 (0)