create src

This commit is contained in:
awfixer
2026-03-11 02:04:19 -07:00
commit 52f7a22bf2
2595 changed files with 402870 additions and 0 deletions

1188
src-lock/CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

24
src-lock/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
lints.workspace = true
[package]
name = "src-lock"
version = "21.0.1"
repository = "https://github.com/GitoxideLabs/gitoxide"
license = "MIT OR Apache-2.0"
description = "A git-style lock-file implementation"
authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
edition = "2021"
include = ["src/**/*", "LICENSE-*", "README.md"]
rust-version = "1.82"
[lib]
doctest = false
test = true
[dependencies]
src-utils = { version = "^0.3.1", default-features = false, path = "../src-utils" }
src-tempfile = { version = "^21.0.0", default-features = false, path = "../src-tempfile" }
thiserror = "2.0.18"
[dev-dependencies]
tempfile = "3.26.0"

1
src-lock/LICENSE-APACHE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE-APACHE

1
src-lock/LICENSE-MIT Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE-MIT

5
src-lock/README.md Normal file
View File

@@ -0,0 +1,5 @@
Use lock-files in the way git does with auto-cleanup being the most notable feature.
* [x] writable lock files that can be committed to atomically replace the resource they lock
* [x] read-only markers that lock a resource without the intend to overwrite it
* [x] auto-removal of the lockfiles and intermediate directories on drop or on signal

242
src-lock/src/acquire.rs Normal file
View File

@@ -0,0 +1,242 @@
use std::{
fmt,
path::{Path, PathBuf},
time::Duration,
};
use gix_tempfile::{AutoRemove, ContainingDirectory};
use crate::{backoff, File, Marker, DOT_LOCK_SUFFIX};
/// Describe what to do if a lock cannot be obtained as it's already held elsewhere.
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Fail {
/// Fail after the first unsuccessful attempt of obtaining a lock.
#[default]
Immediately,
/// Retry after failure with quadratically longer sleep times to block the current thread.
/// Fail once the given duration is exceeded, similar to [Fail::Immediately]
AfterDurationWithBackoff(Duration),
}
impl fmt::Display for Fail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Fail::Immediately => f.write_str("immediately"),
Fail::AfterDurationWithBackoff(duration) => {
write!(f, "after {:.02}s", duration.as_secs_f32())
}
}
}
}
impl From<Duration> for Fail {
fn from(value: Duration) -> Self {
if value.is_zero() {
Fail::Immediately
} else {
Fail::AfterDurationWithBackoff(value)
}
}
}
/// The error returned when acquiring a [`File`] or [`Marker`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Another IO error occurred while obtaining the lock")]
Io(#[from] std::io::Error),
#[error("The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{}' might need manual deletion.", super::DOT_LOCK_SUFFIX)]
PermanentlyLocked {
resource_path: PathBuf,
mode: Fail,
attempts: usize,
},
}
impl File {
/// Create a writable lock file with failure `mode` whose content will eventually overwrite the given resource `at_path`.
///
/// If `boundary_directory` is given, non-existing directories will be created automatically and removed in the case of
/// a rollback. Otherwise the containing directory is expected to exist, even though the resource doesn't have to.
///
/// Note that permissions will be set to `0o666`, which usually results in `0o644` after passing a default umask, on Unix systems.
///
/// ### Warning of potential resource leak
///
/// Please note that the underlying file will remain if destructors don't run, as is the case when interrupting the application.
/// This results in the resource being locked permanently unless the lock file is removed by other means.
/// See [the crate documentation](crate) for more information.
pub fn acquire_to_update_resource(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
) -> Result<File, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
if let Some(permissions) = default_permissions() {
gix_tempfile::writable_at_with_permissions(p, d, c, permissions)
} else {
gix_tempfile::writable_at(p, d, c)
}
})?;
Ok(File {
inner: handle,
lock_path,
})
}
/// Like [`acquire_to_update_resource()`](File::acquire_to_update_resource), but allows to set filesystem permissions using `make_permissions`.
pub fn acquire_to_update_resource_with_permissions(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
make_permissions: impl Fn() -> std::fs::Permissions,
) -> Result<File, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
gix_tempfile::writable_at_with_permissions(p, d, c, make_permissions())
})?;
Ok(File {
inner: handle,
lock_path,
})
}
}
impl Marker {
/// Like [`acquire_to_update_resource()`](File::acquire_to_update_resource()) but _without_ the possibility to make changes
/// and commit them.
///
/// If `boundary_directory` is given, non-existing directories will be created automatically and removed in the case of
/// a rollback.
///
/// Note that permissions will be set to `0o666`, which usually results in `0o644` after passing a default umask, on Unix systems.
///
/// ### Warning of potential resource leak
///
/// Please note that the underlying file will remain if destructors don't run, as is the case when interrupting the application.
/// This results in the resource being locked permanently unless the lock file is removed by other means.
/// See [the crate documentation](crate) for more information.
pub fn acquire_to_hold_resource(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
) -> Result<Marker, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
if let Some(permissions) = default_permissions() {
gix_tempfile::mark_at_with_permissions(p, d, c, permissions)
} else {
gix_tempfile::mark_at(p, d, c)
}
})?;
Ok(Marker {
created_from_file: false,
inner: handle,
lock_path,
})
}
/// Like [`acquire_to_hold_resource()`](Marker::acquire_to_hold_resource), but allows to set filesystem permissions using `make_permissions`.
pub fn acquire_to_hold_resource_with_permissions(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
make_permissions: impl Fn() -> std::fs::Permissions,
) -> Result<Marker, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
gix_tempfile::mark_at_with_permissions(p, d, c, make_permissions())
})?;
Ok(Marker {
created_from_file: false,
inner: handle,
lock_path,
})
}
}
fn dir_cleanup(boundary: Option<PathBuf>) -> (ContainingDirectory, AutoRemove) {
match boundary {
None => (ContainingDirectory::Exists, AutoRemove::Tempfile),
Some(boundary_directory) => (
ContainingDirectory::CreateAllRaceProof(Default::default()),
AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory },
),
}
}
fn lock_with_mode<T>(
resource: &Path,
mode: Fail,
boundary_directory: Option<PathBuf>,
try_lock: &dyn Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result<T>,
) -> Result<(PathBuf, T), Error> {
use std::io::ErrorKind::*;
let (directory, cleanup) = dir_cleanup(boundary_directory);
let lock_path = add_lock_suffix(resource);
let mut attempts = 1;
match mode {
Fail::Immediately => try_lock(&lock_path, directory, cleanup),
Fail::AfterDurationWithBackoff(time) => {
for wait in backoff::Quadratic::default_with_random().until_no_remaining(time) {
attempts += 1;
match try_lock(&lock_path, directory, cleanup.clone()) {
Ok(v) => return Ok((lock_path, v)),
#[cfg(windows)]
Err(err) if err.kind() == AlreadyExists || err.kind() == PermissionDenied => {
std::thread::sleep(wait);
continue;
}
#[cfg(not(windows))]
Err(err) if err.kind() == AlreadyExists => {
std::thread::sleep(wait);
continue;
}
Err(err) => return Err(Error::from(err)),
}
}
try_lock(&lock_path, directory, cleanup)
}
}
.map(|v| (lock_path, v))
.map_err(|err| match err.kind() {
AlreadyExists => Error::PermanentlyLocked {
resource_path: resource.into(),
mode,
attempts,
},
_ => Error::Io(err),
})
}
fn add_lock_suffix(resource_path: &Path) -> PathBuf {
resource_path.with_extension(resource_path.extension().map_or_else(
|| DOT_LOCK_SUFFIX.chars().skip(1).collect(),
|ext| format!("{}{}", ext.to_string_lossy(), DOT_LOCK_SUFFIX),
))
}
fn default_permissions() -> Option<std::fs::Permissions> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Some(std::fs::Permissions::from_mode(0o666))
}
#[cfg(not(unix))]
{
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_lock_suffix_to_file_with_extension() {
assert_eq!(add_lock_suffix(Path::new("hello.ext")), Path::new("hello.ext.lock"));
}
#[test]
fn add_lock_suffix_to_file_without_extension() {
assert_eq!(add_lock_suffix(Path::new("hello")), Path::new("hello.lock"));
}
}

76
src-lock/src/commit.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::path::PathBuf;
use crate::{File, Marker};
mod error {
use std::{
fmt,
fmt::{Debug, Display},
};
/// The error returned by various [`commit(…)`][super::Marker::commit()] methods
#[derive(Debug)]
pub struct Error<T: Debug> {
/// The io error that prevented the attempt to succeed
pub error: std::io::Error,
/// The marker or file which was used in the attempt to persist it
pub instance: T,
}
impl<T: Debug> Display for Error<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.error, f)
}
}
impl<T: Debug> std::error::Error for Error<T> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.error.source()
}
}
}
pub use error::Error;
impl Marker {
/// Commit the changes written to the previously open file and overwrite the original file atomically, returning the resource path
/// on success.
///
/// This fails for markers which weren't created with [`File::close()`]
pub fn commit(mut self) -> Result<PathBuf, Error<Self>> {
if !self.created_from_file {
return Err(Error {
error: std::io::Error::other("refusing to commit marker that was never opened"),
instance: self,
});
}
let resource_path = self.resource_path();
match self.inner.persist(&resource_path) {
Ok(_) => Ok(resource_path),
Err(err) => Err(Error {
error: err.error,
instance: {
self.inner = err.handle;
self
},
}),
}
}
}
impl File {
/// Commit the changes written to this lock file and overwrite the original file atomically, returning the resource path
/// and an open file handle on success.
pub fn commit(mut self) -> Result<(PathBuf, Option<std::fs::File>), Error<Self>> {
let resource_path = self.resource_path();
match self.inner.persist(&resource_path) {
Ok(possibly_file) => Ok((resource_path, possibly_file)),
Err(err) => Err(Error {
error: err.error,
instance: {
self.inner = err.handle;
self
},
}),
}
}
}

78
src-lock/src/file.rs Normal file
View File

@@ -0,0 +1,78 @@
use std::path::{Path, PathBuf};
use crate::{File, Marker, DOT_LOCK_SUFFIX};
fn strip_lock_suffix(lock_path: &Path) -> PathBuf {
let ext = lock_path
.extension()
.expect("at least our own extension")
.to_str()
.expect("no illegal UTF8 in extension");
lock_path.with_extension(ext.split_at(ext.len().saturating_sub(DOT_LOCK_SUFFIX.len())).0)
}
impl File {
/// Obtain a mutable reference to the write handle and call `f(out)` with it.
pub fn with_mut<T>(&mut self, f: impl FnOnce(&mut std::fs::File) -> std::io::Result<T>) -> std::io::Result<T> {
self.inner.with_mut(|tf| f(tf.as_file_mut())).and_then(|res| res)
}
/// Close the lock file to prevent further writes and to save system resources.
/// A call to [`Marker::commit()`] is allowed on the [`Marker`] to write changes back to the resource.
pub fn close(self) -> std::io::Result<Marker> {
Ok(Marker {
inner: self.inner.close()?,
created_from_file: true,
lock_path: self.lock_path,
})
}
/// Return the path at which the lock file resides
pub fn lock_path(&self) -> &Path {
&self.lock_path
}
/// Return the path at which the locked resource resides
pub fn resource_path(&self) -> PathBuf {
strip_lock_suffix(&self.lock_path)
}
}
mod io_impls {
use std::{io, io::SeekFrom};
use super::File;
impl io::Write for File {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.with_mut(|f| f.write(buf))?
}
fn flush(&mut self) -> io::Result<()> {
self.inner.with_mut(io::Write::flush)?
}
}
impl io::Seek for File {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.inner.with_mut(|f| f.seek(pos))?
}
}
impl io::Read for File {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.with_mut(|f| f.read(buf))?
}
}
}
impl Marker {
/// Return the path at which the lock file resides
pub fn lock_path(&self) -> &Path {
&self.lock_path
}
/// Return the path at which the locked resource resides
pub fn resource_path(&self) -> PathBuf {
strip_lock_suffix(&self.lock_path)
}
}

56
src-lock/src/lib.rs Normal file
View File

@@ -0,0 +1,56 @@
//! git-style registered lock files to make altering resources atomic.
//!
//! In this model, reads are always atomic and can be performed directly while writes are facilitated by the locking mechanism
//! implemented here. Locks are acquired atomically, then written to, to finally atomically overwrite the actual resource.
//!
//! Lock files are wrapped [`src-tempfile`](gix_tempfile)-handles and add the following:
//!
//! * consistent naming of lock files
//! * block the thread (with timeout) or fail immediately if a lock cannot be obtained right away
//! * commit lock files to atomically put them into the location of the originally locked file
//!
//! # Limitations
//!
//! * [All limitations of `src-tempfile`](gix_tempfile) apply. **A highlight of such a limitation is resource leakage
//! which results in them being permanently locked unless there is user-intervention.**
//! * As the lock file is separate from the actual resource, locking is merely a convention rather than being enforced.
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
use std::path::PathBuf;
pub use gix_tempfile as tempfile;
use gix_tempfile::handle::{Closed, Writable};
const DOT_LOCK_SUFFIX: &str = ".lock";
///
pub mod acquire;
pub use gix_utils::backoff;
///
pub mod commit;
/// Locks a resource to eventually be overwritten with the content of this file.
///
/// Dropping the file without [committing][File::commit] will delete it, leaving the underlying resource unchanged.
#[must_use = "A File that is immediately dropped doesn't allow resource updates"]
#[derive(Debug)]
pub struct File {
inner: gix_tempfile::Handle<Writable>,
lock_path: PathBuf,
}
/// Locks a resource to allow related resources to be updated using [files][File].
///
/// As opposed to the [File] type this one won't keep the tempfile open for writing and thus consumes no
/// system resources, nor can it be persisted.
#[must_use = "A Marker that is immediately dropped doesn't lock a resource meaningfully"]
#[derive(Debug)]
pub struct Marker {
inner: gix_tempfile::Handle<Closed>,
created_from_file: bool,
lock_path: PathBuf,
}
///
pub mod file;

3
src-lock/tests/all.rs Normal file
View File

@@ -0,0 +1,3 @@
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
mod lock;

155
src-lock/tests/lock/file.rs Normal file
View File

@@ -0,0 +1,155 @@
mod close {
use std::io::Write;
use gix_lock::acquire::Fail;
#[test]
fn acquire_close_commit_to_existing_file() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("resource-existing.ext");
std::fs::write(&resource, b"old state")?;
let resource_lock = resource.with_extension("ext.lock");
let mut file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None)?;
assert!(resource_lock.is_file());
file.with_mut(|out| out.write_all(b"hello world"))?;
let mark = file.close()?;
assert_eq!(mark.lock_path(), resource_lock);
assert_eq!(mark.resource_path(), resource);
assert_eq!(mark.commit()?, resource, "returned and initial resource path match");
assert_eq!(
std::fs::read(resource)?,
&b"hello world"[..],
"it created the resource and wrote the data"
);
assert!(!resource_lock.is_file());
Ok(())
}
}
mod commit {
use gix_lock::acquire::Fail;
#[test]
fn failure_to_commit_does_return_a_registered_marker() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("resource-existing.ext");
std::fs::create_dir(&resource)?;
let mark = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?;
let lock_path = mark.lock_path().to_owned();
assert!(lock_path.is_file(), "the lock is placed");
let err = mark
.commit()
.expect_err("cannot commit onto existing directory, empty or not");
assert!(err.instance.lock_path().is_file(), "the lock is still present");
drop(err);
assert!(
!lock_path.is_file(),
"the lock file is still owned by the lock instance (and ideally still registered, but hard to test)"
);
Ok(())
}
#[test]
fn failure_to_commit_does_return_a_registered_file() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("resource-existing.ext");
std::fs::create_dir(&resource)?;
let file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None)?;
let lock_path = file.lock_path().to_owned();
assert!(lock_path.is_file(), "the lock is placed");
let err = file
.commit()
.expect_err("cannot commit onto existing directory, empty or not");
assert!(err.instance.lock_path().is_file(), "the lock is still present");
std::fs::remove_dir(resource)?;
let (resource, open_file) = err.instance.commit()?;
let mut open_file = open_file.expect("file to be present as no interrupt has messed with us");
assert!(
!lock_path.is_file(),
"the lock was moved into place, now it's the resource"
);
use std::io::Write;
write!(open_file, "hello")?;
drop(open_file);
assert_eq!(
std::fs::read(resource)?,
b"hello".to_vec(),
"and committing returned a writable file handle"
);
Ok(())
}
}
mod acquire {
use std::io::{ErrorKind, Write};
use gix_lock::acquire;
fn fail_immediately() -> gix_lock::acquire::Fail {
acquire::Fail::Immediately
}
#[test]
fn lock_create_dir_write_commit() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("a").join("resource-nonexisting");
let resource_lock = resource.with_extension("lock");
let mut file =
gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), Some(dir.path().into()))?;
assert_eq!(file.lock_path(), resource_lock);
assert_eq!(file.resource_path(), resource);
assert!(resource_lock.is_file());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = resource_lock.metadata()?.permissions();
assert_ne!(
perms.mode() & !0o170000,
0o600,
"mode is more permissive now, even after passing the umask"
);
}
file.with_mut(|out| out.write_all(b"hello world"))?;
assert_eq!(file.commit()?.0, resource, "returned and computed resource path match");
assert_eq!(
std::fs::read(resource)?,
&b"hello world"[..],
"it created the resource and wrote the data"
);
assert!(!resource_lock.is_file());
Ok(())
}
#[test]
fn lock_write_drop() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("resource-nonexisting.ext");
{
let mut file = gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), None)?;
file.with_mut(|out| out.write_all(b"probably we will be interrupted"))?;
}
assert!(!resource.is_file(), "the file wasn't created");
Ok(())
}
#[test]
fn lock_non_existing_dir_fails() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("a").join("resource.ext");
let res = gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), None);
assert!(matches!(res, Err(acquire::Error::Io(err)) if err.kind() == ErrorKind::NotFound));
assert!(dir.path().is_dir(), "it won't meddle with the containing directory");
assert!(!resource.is_file(), "the resource is not created");
assert!(
!resource.parent().unwrap().is_dir(),
"parent dire wasn't created either"
);
Ok(())
}
}

View File

@@ -0,0 +1,92 @@
mod acquire {
use std::time::{Duration, Instant};
use gix_lock::acquire::Fail;
#[test]
fn fail_mode_immediately_produces_a_descriptive_error() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("the-resource");
let guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?;
assert!(guard.lock_path().ends_with("the-resource.lock"));
assert!(guard.resource_path().ends_with("the-resource"));
let err_str = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None)
.expect_err("the lock is taken and there is a failure obtaining it again")
.to_string();
assert!(err_str.contains("the-resource' could not be obtained immediately"));
assert!(err_str.contains("the-resource.lock"), "it mentions the lockfile itself");
Ok(())
}
#[test]
fn fail_mode_after_duration_fails_after_a_given_duration_or_more() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("the-resource");
let _guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?;
let start = Instant::now();
let time_to_wait = Duration::from_millis(50);
let err_str =
gix_lock::Marker::acquire_to_hold_resource(resource, Fail::AfterDurationWithBackoff(time_to_wait), None)
.expect_err("the lock is taken and there is a failure obtaining it again after some delay")
.to_string();
assert!(
start.elapsed() >= time_to_wait,
"it should never wait less than the given wait time"
);
assert!(
err_str.contains("could not be obtained after 0.05s"),
"it lets us know that we were waiting for some time"
);
assert!(err_str.contains("the-resource.lock"), "it mentions the lockfile itself");
Ok(())
}
}
mod commit {
use gix_lock::acquire::Fail;
#[test]
fn failure_to_commit_does_return_a_registered_marker() {
let dir = tempfile::tempdir().unwrap();
let resource = dir.path().join("the-resource");
let file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None).unwrap();
let mark = file.close().unwrap();
let resource_lock_path = mark.lock_path().to_owned();
std::fs::create_dir(&resource).unwrap();
let err = mark.commit().expect_err("it fails as the resource path is a directory");
assert!(
resource_lock_path.is_file(),
"the underlying lock wasn't consumed after all"
);
drop(err);
assert!(
!resource_lock_path.is_file(),
"and is linked to the err which makes the lock recoverable"
);
}
#[test]
fn fails_for_ordinary_marker_that_was_never_writable() -> crate::Result {
let dir = tempfile::tempdir()?;
let resource = dir.path().join("the-resource");
let mark = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = mark.lock_path().metadata()?.permissions();
assert_ne!(
perms.mode() & !0o170000,
0o600,
"mode is more permissive now, even after passing the umask"
);
}
let err = mark.commit().expect_err("should always fail");
assert_eq!(err.error.kind(), std::io::ErrorKind::Other);
assert_eq!(
err.error.get_ref().expect("custom error").to_string(),
"refusing to commit marker that was never opened"
);
Ok(())
}
}

View File

@@ -0,0 +1,2 @@
mod file;
mod marker;