@@ -29,6 +29,7 @@ use parallel_disk_usage::{
2929 reporter:: { ErrorOnlyReporter , ErrorReport } ,
3030 size:: Bytes ,
3131} ;
32+ use pipe_trait:: Pipe ;
3233use 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) ) ]
6970struct 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) ) ]
8586fn 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