use super::*;
use crate::{
durable::{
supported::{FileCommitStep, WriterCustody, commit_path_with_options},
tests::REPLACE_OPTIONS,
},
test_support::Fixture,
};
use std::{
fs,
os::unix::fs::{MetadataExt as _, symlink},
};
#[test]
fn absolute_spaced_stage_publishes_validated_output_and_returns_value() {
let fixture = Fixture::new();
let output = fixture.root.join("parent with spaces/output file");
let value = write_named_with(&output, REPLACE_OPTIONS, |stage| -> io::Result<_> {
assert!(stage.is_absolute());
assert_eq!(stage.parent(), output.parent());
let initial = fs::metadata(stage)?;
assert_eq!(initial.len(), 0);
fs::write(stage, b"validated output")?;
assert_eq!(initial.ino(), fs::metadata(stage)?.ino());
assert!(!output.exists());
Ok(42)
})
.unwrap();
assert_eq!(value, 42);
assert_eq!(fs::read(&output).unwrap(), b"validated output");
assert_eq!(fs::read_dir(output.parent().unwrap()).unwrap().count(), 1);
}
#[test]
fn named_create_only_uses_selected_permissions_and_preserves_a_competing_entry() {
use crate::durable::PublicationMode;
let fixture = Fixture::new();
let output = fixture.root.join("output");
let options = WriteOptions {
mode: PublicationMode::CreateNew,
permissions: 0o600,
};
write_named_with(&output, options, |stage| -> io::Result<()> {
assert_eq!(fs::metadata(stage)?.mode() & 0o777, 0o600);
fs::write(stage, b"created")
})
.unwrap();
assert_eq!(fs::read(&output).unwrap(), b"created");
assert_eq!(fs::metadata(&output).unwrap().mode() & 0o777, 0o600);
fs::remove_file(&output).unwrap();
let result = write_named_with(&output, options, |stage| -> io::Result<()> {
fs::write(stage, b"must not replace")?;
fs::write(&output, b"competing entry")
});
assert!(matches!(result,
Err(NamedWriteError::BeforePublication { source, cleanup_error: None })
if source.kind() == io::ErrorKind::AlreadyExists));
assert_eq!(fs::read(&output).unwrap(), b"competing entry");
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
}
#[test]
fn named_invalid_permissions_refuse_before_parent_creation_or_production() {
use crate::durable::PublicationMode;
let fixture = Fixture::new();
let output = fixture.root.join("missing/nested/output");
let result = write_named_with(
&output,
WriteOptions {
mode: PublicationMode::Replace,
permissions: 0o1000,
},
|_| -> io::Result<()> { panic!("invalid permissions must not invoke production") },
);
assert!(matches!(result,
Err(NamedWriteError::BeforePublication { source, cleanup_error: None })
if source.kind() == io::ErrorKind::InvalidInput));
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 0);
}
#[test]
fn producer_and_validation_errors_preserve_original_and_remove_owned_stage() {
let fixture = Fixture::new();
let output = fixture.root.join("output");
fs::write(&output, b"old").unwrap();
for bytes in [b"".as_slice(), b"invalid", b"partial"] {
let error = write_named_with(&output, REPLACE_OPTIONS, |stage| {
fs::write(stage, bytes).unwrap();
Err::<(), _>(vec![1, 2, 3])
})
.unwrap_err();
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: None } if source == [1, 2, 3])
);
assert_eq!(fs::read(&output).unwrap(), b"old");
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
}
}
#[test]
fn replaced_missing_or_linked_staging_is_never_published_or_blindly_removed() {
let fixture = Fixture::new();
for substitution in [
"missing",
"file",
"symlink",
"directory",
"fifo",
"hardlink",
] {
let parent = fixture.root.join(substitution);
fs::create_dir(&parent).unwrap();
let output = parent.join("output");
let other = parent.join("other");
fs::write(&output, b"old").unwrap();
fs::write(&other, b"unrelated").unwrap();
let mut staged_path = None;
let result = write_named_with(&output, REPLACE_OPTIONS, |stage| -> io::Result<()> {
staged_path = Some(stage.to_path_buf());
fs::write(stage, b"produced")?;
if substitution == "hardlink" {
fs::hard_link(stage, parent.join("extra-link"))?;
} else {
fs::remove_file(stage)?;
match substitution {
"missing" => {}
"file" => fs::write(stage, b"foreign")?,
"symlink" => symlink(&other, stage)?,
"directory" => fs::create_dir(stage)?,
"fifo" => crate::test_support::create_fifo(stage, None)?,
_ => unreachable!(),
}
}
Ok(())
});
let NamedWriteError::BeforePublication {
source,
cleanup_error,
} = result.unwrap_err()
else {
panic!("identity drift must stop publication");
};
assert!(matches!(
source.kind(),
io::ErrorKind::InvalidData | io::ErrorKind::NotFound
));
let stage = staged_path.unwrap();
if matches!(substitution, "missing" | "hardlink") {
assert!(cleanup_error.is_none());
assert!(!stage.exists());
} else {
assert_eq!(cleanup_error.unwrap().kind(), io::ErrorKind::InvalidData);
assert!(fs::symlink_metadata(&stage).is_ok());
}
assert_eq!(fs::read(&output).unwrap(), b"old");
assert_eq!(fs::read(&other).unwrap(), b"unrelated");
}
}
#[test]
fn parent_replacement_is_rejected_and_cleanup_uses_original_directory() {
let fixture = Fixture::new();
let parent = fixture.root.join("parent");
let moved = fixture.root.join("moved");
fs::create_dir(&parent).unwrap();
let output = parent.join("output");
fs::write(&output, b"old").unwrap();
let result = write_named_with(&output, REPLACE_OPTIONS, |stage| -> io::Result<()> {
fs::write(stage, b"produced")?;
fs::rename(&parent, &moved)?;
fs::create_dir(&parent)?;
fs::write(&output, b"foreign output")?;
Ok(())
});
assert!(matches!(
result,
Err(NamedWriteError::BeforePublication {
cleanup_error: None,
..
})
));
assert_eq!(fs::read(&output).unwrap(), b"foreign output");
assert_eq!(fs::read(moved.join("output")).unwrap(), b"old");
assert_eq!(fs::read_dir(moved).unwrap().count(), 1);
}
#[test]
fn sync_errors_have_correct_publication_state() {
let fixture = Fixture::new();
let output = fixture.root.join("output");
fs::write(&output, b"old").unwrap();
let result = commit_path_with_options(
&output,
REPLACE_OPTIONS,
|_, file| {
use std::io::Write as _;
file.write_all(b"new")
},
(WriterCustody::Writable, |_, ()| Ok(())),
|step, _| {
if step == FileCommitStep::TemporaryFileSync {
Err(io::Error::from_raw_os_error(5))
} else {
Ok(())
}
},
);
assert!(
matches!(result, Err(NamedWriteError::BeforePublication { source, cleanup_error: None }) if source.raw_os_error() == Some(5))
);
assert_eq!(fs::read(&output).unwrap(), b"old");
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
let result = commit_path_with_options(
&output,
REPLACE_OPTIONS,
|_, file| {
use std::io::Write as _;
file.write_all(b"new")
},
(WriterCustody::Writable, |_, ()| Ok(())),
|step, _| {
if step == FileCommitStep::FinalParentSync {
Err(io::Error::from_raw_os_error(5))
} else {
Ok(())
}
},
);
assert!(
matches!(result, Err(NamedWriteError::AfterPublication { source }) if source.raw_os_error() == Some(5))
);
assert_eq!(fs::read(&output).unwrap(), b"new");
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
}
#[test]
fn failed_publication_cleans_staging_before_dropping_the_produced_value() {
use std::{cell::Cell, path::PathBuf};
struct ProducedValue<'a> {
stage: PathBuf,
present_on_drop: &'a Cell<Option<bool>>,
}
impl Drop for ProducedValue<'_> {
fn drop(&mut self) {
self.present_on_drop.set(Some(self.stage.exists()));
}
}
let fixture = Fixture::new();
let output = fixture.root.join("output");
for step in [
FileCommitStep::TemporaryFileSync,
FileCommitStep::Publication,
] {
fs::write(&output, b"old").unwrap();
let present_on_drop = Cell::new(None);
let result = commit_path_with_options(
&output,
REPLACE_OPTIONS,
|stage, file| {
use std::io::Write as _;
file.write_all(b"new")?;
Ok::<_, io::Error>(ProducedValue {
stage: stage.to_path_buf(),
present_on_drop: &present_on_drop,
})
},
(WriterCustody::Writable, |_, _| Ok(())),
|current, _| {
if current == step {
Err(io::Error::from_raw_os_error(5))
} else {
Ok(())
}
},
);
assert!(matches!(result,
Err(NamedWriteError::BeforePublication { source, cleanup_error: None })
if source.raw_os_error() == Some(5)));
assert_eq!(present_on_drop.get(), Some(false));
assert_eq!(fs::read(&output).unwrap(), b"old");
}
}
#[test]
fn replacement_after_sync_is_rejected_before_rename() {
use std::cell::RefCell;
let fixture = Fixture::new();
let output = fixture.root.join("output");
fs::write(&output, b"old").unwrap();
let staged_path = RefCell::new(None);
let result = commit_path_with_options(
&output,
REPLACE_OPTIONS,
|stage, file| {
use std::io::Write as _;
staged_path.replace(Some(stage.to_path_buf()));
file.write_all(b"produced")
},
(WriterCustody::Writable, |_, ()| Ok(())),
|step, _| {
if step == FileCommitStep::Publication {
let borrowed = staged_path.borrow();
let stage = borrowed.as_ref().unwrap();
fs::remove_file(stage)?;
fs::write(stage, b"foreign")?;
}
Ok(())
},
);
assert!(matches!(
result,
Err(NamedWriteError::BeforePublication {
cleanup_error: Some(_),
..
})
));
assert_eq!(fs::read(&output).unwrap(), b"old");
assert_eq!(
fs::read(staged_path.into_inner().unwrap()).unwrap(),
b"foreign"
);
}
#[test]
fn producer_error_retains_cleanup_rejection_and_error_source() {
use std::error::Error as _;
let fixture = Fixture::new();
let output = fixture.root.join("output");
let error = write_named_with(&output, REPLACE_OPTIONS, |stage| {
fs::remove_file(stage).unwrap();
fs::write(stage, b"foreign").unwrap();
Err::<(), _>(io::Error::from_raw_os_error(13))
})
.unwrap_err();
assert_eq!(
error
.source()
.unwrap()
.downcast_ref::<io::Error>()
.unwrap()
.raw_os_error(),
Some(13)
);
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: Some(cleanup) }
if source.raw_os_error() == Some(13) && cleanup.kind() == io::ErrorKind::InvalidData)
);
assert!(!output.exists());
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
}
#[test]
fn display_keeps_phase_primary_cause_and_secondary_cleanup() {
use std::error::Error as _;
for error in [
NamedWriteError::Producer {
source: io::Error::other("producer failure"),
cleanup_error: Some(io::Error::other("cleanup failure")),
},
NamedWriteError::BeforePublication {
source: io::Error::other("filesystem failure"),
cleanup_error: Some(io::Error::other("cleanup failure")),
},
] {
let text = error.to_string();
assert!(text.contains(&error.source().unwrap().to_string()));
assert!(text.contains("staging cleanup failed: cleanup failure"));
match error {
NamedWriteError::Producer { .. } => {
assert!(text.starts_with("staged producer failed:"));
}
NamedWriteError::BeforePublication { .. } => {
assert!(text.starts_with("publication failed before rename:"));
}
NamedWriteError::AfterPublication { .. } => unreachable!(),
}
}
}