You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/10/26 07:39:29 UTC
(opendal-yinyang) branch xuanwo/linux-fuse-acceptance updated: test: fix Linux FUSE acceptance compilation
This is an automated email from the ASF dual-hosted git repository.
Xuanwo pushed a commit to branch xuanwo/linux-fuse-acceptance
in repository https://gitbox.apache.org/repos/asf/opendal-yinyang.git
The following commit(s) were added to refs/heads/xuanwo/linux-fuse-acceptance by this push:
new e0cfec8 test: fix Linux FUSE acceptance compilation
e0cfec8 is described below
commit e0cfec8dd4629455a142abc8e29885d6e1628bd0
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Mon Oct 26 15:39:23 2026 +0800
test: fix Linux FUSE acceptance compilation
Pass owned commands to the timeout runner, use lossy path conversion at the CLI boundary, remove unneeded mutability, and prove pending fsync reaches frozen state before the daemon crash.
Co-authored-by: Chojan Shang <ps...@apache.org>
---
docs/linux-fuse-acceptance.md | 7 ++-
tests/linux_fuse/acceptance.rs | 26 ++++----
tests/linux_fuse/harness.rs | 135 +++++++++++++++++++++++++++++++++--------
3 files changed, 125 insertions(+), 43 deletions(-)
diff --git a/docs/linux-fuse-acceptance.md b/docs/linux-fuse-acceptance.md
index 8befa12..8971122 100644
--- a/docs/linux-fuse-acceptance.md
+++ b/docs/linux-fuse-acceptance.md
@@ -63,9 +63,10 @@ cargo x test-linux-fuse --read-only
- Fsync/network failure retention: stopping the metadata service makes
`fsync` fail within a bound; staged bytes survive, the service restarts, and
a reopened mount publishes them through the unchanged service address.
-- Interrupted call retention: a `fsync` left in flight against a paused
- metadata service is interrupted by killing the mount daemon, the authority is
- untouched, and a reopened mount publishes the staged bytes.
+- Pending fsync interruption: `yy status` (no lease, no remote access) is polled
+ until the handle reports `frozen: true`, then killing the mount daemon
+ interrupts that pending publication. The authority is untouched and a
+ reopened mount replays the frozen request and publishes the staged bytes.
- Busy unmount: `fusermount3 -u` fails while a descriptor is open and succeeds
after the descriptor is released.
- Unmount lifecycle: after a successful external `fusermount3 -u`, `yy` exits
diff --git a/tests/linux_fuse/acceptance.rs b/tests/linux_fuse/acceptance.rs
index 28cec80..d8fc17c 100644
--- a/tests/linux_fuse/acceptance.rs
+++ b/tests/linux_fuse/acceptance.rs
@@ -17,7 +17,9 @@
//! Real /dev/fuse acceptance scenarios for the YinYang mount adapter.
-use crate::harness::{Fixture, Frontend, Publication, assert_errno, sync_all_with_deadline};
+use crate::harness::{
+ Fixture, Frontend, Publication, assert_errno, run_external, sync_all_with_deadline,
+};
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::fs::FileExt;
@@ -134,7 +136,7 @@ fn basic_io(fixture: &Fixture) {
drop(append);
assert_eq!(fixture.remote_read(&library, "basic.txt", 8), b"hello!");
- let mut truncate = OpenOptions::new()
+ let truncate = OpenOptions::new()
.write(true)
.open(root.join("basic.txt"))
.expect("open mounted file for truncate");
@@ -498,7 +500,7 @@ fn fuse_process_kill_recovery_object() {
b"dirty-local",
"a killed mount process must recover staged bytes from persistent staging"
);
- let mut publish = OpenOptions::new()
+ let publish = OpenOptions::new()
.write(true)
.open(reopened.mountpoint().join("recover.txt"))
.expect("reopen recovered file");
@@ -555,7 +557,7 @@ fn fuse_fsync_failure_retention_service() {
fs::read(reopened.mountpoint().join("network.txt")).expect("read staged recovery"),
b"survives"
);
- let mut publish = OpenOptions::new()
+ let publish = OpenOptions::new()
.write(true)
.open(reopened.mountpoint().join("network.txt"))
.expect("reopen recovered file");
@@ -570,7 +572,7 @@ fn fuse_fsync_failure_retention_service() {
#[test]
#[ignore = "requires /dev/fuse and isolated S3; run with cargo x test-linux-fuse"]
-fn fuse_interrupted_call_retention_service() {
+fn fuse_daemon_crash_interrupts_pending_fsync_service() {
let fixture = Fixture::new(Publication::Service);
let library = fixture.volume_config("lib-interrupted", Frontend::Library, false);
fixture.library_seed(&library, "interrupted.txt", b"seed");
@@ -593,7 +595,8 @@ fn fuse_interrupted_call_retention_service() {
let caller = thread::spawn(move || {
let _ = sender.send(in_flight.sync_all());
});
- thread::sleep(Duration::from_millis(200));
+ let frozen = fixture.wait_for_frozen(&config);
+ println!("fsync reached the frozen publication boundary:\n{frozen}");
mounted.kill();
let outcome = receiver
.recv_timeout(Duration::from_secs(20))
@@ -620,7 +623,7 @@ fn fuse_interrupted_call_retention_service() {
fs::read(reopened.mountpoint().join("interrupted.txt")).expect("read staged recovery"),
b"interrupted-local"
);
- let mut publish = OpenOptions::new()
+ let publish = OpenOptions::new()
.write(true)
.open(reopened.mountpoint().join("interrupted.txt"))
.expect("reopen recovered file");
@@ -643,13 +646,8 @@ fn fuse_busy_unmount_object() {
let mut mounted = fixture.mount(&config, &mountpoint);
let file = File::create(mounted.mountpoint().join("held.txt")).expect("create held file");
- let output = crate::harness::run_timeout(
- std::process::Command::new("fusermount3").args([
- "-u",
- mounted.mountpoint().to_str().expect("UTF-8 mountpoint"),
- ]),
- Duration::from_secs(30),
- );
+ let mountpoint = mounted.mountpoint().to_str().expect("UTF-8 mountpoint");
+ let output = run_external("fusermount3", &["-u", mountpoint], Duration::from_secs(30));
assert!(
!output.status.success(),
"busy unmount unexpectedly succeeded while a descriptor was open: {}",
diff --git a/tests/linux_fuse/harness.rs b/tests/linux_fuse/harness.rs
index 7445f25..fb0d3a8 100644
--- a/tests/linux_fuse/harness.rs
+++ b/tests/linux_fuse/harness.rs
@@ -110,6 +110,12 @@ pub fn run_timeout(mut cmd: Command, timeout: Duration) -> Captured {
}
}
+pub fn run_external(program: &str, args: &[&str], timeout: Duration) -> Captured {
+ let mut cmd = Command::new(program);
+ cmd.args(args);
+ run_timeout(cmd, timeout)
+}
+
fn read_all(mut reader: impl Read) -> Vec<u8> {
let mut bytes = Vec::new();
let _ = reader.read_to_end(&mut bytes);
@@ -313,8 +319,9 @@ impl Fixture {
.expect("service publication requires a running metadata service")
.child
.id();
- let output = run_timeout(
- Command::new("kill").args(["-STOP", &pid.to_string()]),
+ let output = run_external(
+ "kill",
+ &["-STOP", &pid.to_string()],
Duration::from_secs(30),
);
output.success_or("pause metadata service");
@@ -327,13 +334,38 @@ impl Fixture {
.expect("service publication requires a running metadata service")
.child
.id();
- let output = run_timeout(
- Command::new("kill").args(["-CONT", &pid.to_string()]),
+ let output = run_external(
+ "kill",
+ &["-CONT", &pid.to_string()],
Duration::from_secs(30),
);
output.success_or("resume metadata service");
}
+ /// Polls the existing no-lease status entry until a handle reports the
+ /// frozen publication boundary. A hang is a hard failure, not a skip.
+ pub fn wait_for_frozen(&self, config: &Path) -> String {
+ let deadline = Instant::now() + Duration::from_secs(30);
+ let config = config.to_str().expect("UTF-8 volume config");
+ loop {
+ let output = self.yy(&["--volume", config, "status"]);
+ output.success_or("yy status");
+ let text = String::from_utf8_lossy(&output.stdout).into_owned();
+ if text
+ .lines()
+ .any(|line| line.starts_with("handle: ") && line.contains("frozen: true"))
+ {
+ return text;
+ }
+ if Instant::now() >= deadline {
+ panic!(
+ "fsync never reached the frozen publication boundary within 30s; last yy status:\n{text}"
+ );
+ }
+ thread::sleep(Duration::from_millis(100));
+ }
+ }
+
/// Writes a strict volume config and returns its path. Rewriting the same
/// path is intentional: a restarted service keeps its original loopback
/// address, so an unchanged config keeps working.
@@ -360,58 +392,114 @@ impl Fixture {
}
pub fn library_seed(&self, config: &Path, path: &str, bytes: &[u8]) {
- self.yy_ok(&["--volume", config.as_str(), "file", "create", path]);
+ self.yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "create",
+ path,
+ ]);
self.remote_write(config, path, bytes);
}
pub fn remote_write(&self, config: &Path, path: &str, bytes: &[u8]) {
let handle = self
- .yy_ok(&["--volume", config.as_str(), "file", "open", path, "--write"])
+ .yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "open",
+ path,
+ "--write",
+ ])
.text();
let source = self.temp.path().join("remote-write-source");
fs::write(&source, bytes).expect("write remote source");
self.yy_ok(&[
"--volume",
- config.as_str(),
+ config.to_str().expect("UTF-8 volume config"),
"file",
"write",
&handle,
source.to_str().expect("UTF-8 source path"),
]);
- self.yy_ok(&["--volume", config.as_str(), "file", "fsync", &handle]);
- self.yy_ok(&["--volume", config.as_str(), "file", "close", &handle]);
+ self.yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "fsync",
+ &handle,
+ ]);
+ self.yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "close",
+ &handle,
+ ]);
}
pub fn remote_truncate(&self, config: &Path, path: &str, length: u64) {
let handle = self
- .yy_ok(&["--volume", config.as_str(), "file", "open", path, "--write"])
+ .yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "open",
+ path,
+ "--write",
+ ])
.text();
self.yy_ok(&[
"--volume",
- config.as_str(),
+ config.to_str().expect("UTF-8 volume config"),
"file",
"truncate",
&handle,
&length.to_string(),
]);
- self.yy_ok(&["--volume", config.as_str(), "file", "fsync", &handle]);
- self.yy_ok(&["--volume", config.as_str(), "file", "close", &handle]);
+ self.yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "fsync",
+ &handle,
+ ]);
+ self.yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "close",
+ &handle,
+ ]);
}
pub fn remote_read(&self, config: &Path, path: &str, length: usize) -> Vec<u8> {
let handle = self
- .yy_ok(&["--volume", config.as_str(), "file", "open", path])
+ .yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "open",
+ path,
+ ])
.text();
let output = self.yy_ok(&[
"--volume",
- config.as_str(),
+ config.to_str().expect("UTF-8 volume config"),
"file",
"read",
&handle,
"--length",
&length.to_string(),
]);
- self.yy_ok(&["--volume", config.as_str(), "file", "close", &handle]);
+ self.yy_ok(&[
+ "--volume",
+ config.to_str().expect("UTF-8 volume config"),
+ "file",
+ "close",
+ &handle,
+ ]);
output.stdout
}
@@ -525,10 +613,7 @@ impl Mounted {
pub fn terminate(&mut self) {
let pid = self.child.id().to_string();
- let output = run_timeout(
- Command::new("kill").args(["-TERM", &pid]),
- Duration::from_secs(30),
- );
+ let output = run_external("kill", &["-TERM", &pid], Duration::from_secs(30));
output.success_or("send SIGTERM to yy mount");
let status = self.wait_exit(Duration::from_secs(30));
assert!(
@@ -545,10 +630,7 @@ impl Mounted {
pub fn unmount(&mut self) {
let mountpoint = self.mountpoint.to_str().expect("UTF-8 mountpoint");
- let output = run_timeout(
- Command::new("fusermount3").args(["-u", mountpoint]),
- Duration::from_secs(30),
- );
+ let output = run_external("fusermount3", &["-u", mountpoint], Duration::from_secs(30));
output.success_or(&format!("fusermount3 -u {mountpoint}"));
let status = self.wait_exit(Duration::from_secs(30));
assert!(
@@ -568,8 +650,9 @@ impl Mounted {
return;
}
let mountpoint = self.mountpoint.to_str().expect("UTF-8 mountpoint");
- let _ = run_timeout(
- Command::new("fusermount3").args(["-u", "-z", mountpoint]),
+ let _ = run_external(
+ "fusermount3",
+ &["-u", "-z", mountpoint],
Duration::from_secs(30),
);
}