use std::fmt;
use std::path::{Path, PathBuf};
use async_trait::async_trait;
use tokio::sync::Mutex;
use tracing::{debug, warn};
pub const RECOVERY_LOCAL_OR_TUI: &str = "local_or_tui_required";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirtyState {
Clean,
Dirty,
Unknown,
}
impl DirtyState {
pub fn as_option(self) -> Option<bool> {
match self {
Self::Clean => Some(false),
Self::Dirty => Some(true),
Self::Unknown => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SafetyFact {
Yes,
No,
Unknown,
}
impl SafetyFact {
pub fn observed<E>(result: Result<bool, E>) -> Self {
match result {
Ok(true) => Self::Yes,
Ok(false) => Self::No,
Err(_) => Self::Unknown,
}
}
pub fn is_known_yes(self) -> bool {
self == Self::Yes
}
}
impl From<bool> for SafetyFact {
fn from(value: bool) -> Self {
if value {
Self::Yes
} else {
Self::No
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeFacts {
pub path: PathBuf,
pub identity: String,
pub branch: String,
pub head: String,
pub is_main: bool,
pub is_detached: bool,
pub has_commits_ahead: SafetyFact,
pub conflict_files: Vec<String>,
pub dirty: DirtyState,
pub base_merge_in_progress: SafetyFact,
pub inspection: crate::worktree_ops::InspectionState,
}
impl WorktreeFacts {
#[cfg_attr(not(test), allow(dead_code))]
pub fn new(path: impl Into<PathBuf>, branch: impl Into<String>) -> Self {
Self {
path: path.into(),
identity: String::new(),
branch: branch.into(),
head: String::new(),
is_main: false,
is_detached: false,
has_commits_ahead: SafetyFact::No,
conflict_files: Vec::new(),
dirty: DirtyState::Clean,
base_merge_in_progress: SafetyFact::No,
inspection: crate::worktree_ops::InspectionState::Checked,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirtyTarget {
pub path: PathBuf,
pub identity: String,
pub branch: String,
pub head: String,
}
impl DirtyTarget {
pub fn from_facts(facts: &WorktreeFacts) -> Self {
Self {
path: facts.path.clone(),
identity: facts.identity.clone(),
branch: facts.branch.clone(),
head: facts.head.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AheadTarget {
pub path: PathBuf,
pub identity: String,
pub branch: String,
pub head: String,
pub dirty: bool,
}
impl AheadTarget {
pub fn from_facts(facts: &WorktreeFacts) -> Self {
Self {
path: facts.path.clone(),
identity: facts.identity.clone(),
branch: facts.branch.clone(),
head: facts.head.clone(),
dirty: facts.dirty == DirtyState::Dirty,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorktreeOpError {
NotFound(String),
Exists(String),
Dirty {
message: String,
target: Box<DirtyTarget>,
},
DirtyUnknown(String),
CommitsAhead {
message: String,
target: Box<AheadTarget>,
},
Ineligible(String),
RootBusy(String),
MergeConflict {
files: Vec<String>,
recovery: &'static str,
},
Internal(String),
}
impl fmt::Display for WorktreeOpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound(message)
| Self::Exists(message)
| Self::Dirty { message, .. }
| Self::DirtyUnknown(message)
| Self::CommitsAhead { message, .. }
| Self::Ineligible(message)
| Self::RootBusy(message)
| Self::Internal(message) => write!(f, "{message}"),
Self::MergeConflict { files, recovery } => write!(
f,
"base merge conflicted in {} file(s) [{}]; intermediate merge state was preserved, recovery: {}",
files.len(),
files.join(", "),
recovery
),
}
}
}
pub type WorktreeOpResult<T> = Result<T, WorktreeOpError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictPolicy {
AbortOnConflict,
PreserveConflict,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeleteOptions {
pub skip_teardown: bool,
pub allow_known_dirty: bool,
pub allow_commits_ahead: bool,
}
impl DeleteOptions {
pub fn fail_closed() -> Self {
Self {
skip_teardown: false,
allow_known_dirty: false,
allow_commits_ahead: false,
}
}
pub fn local(skip_teardown: bool) -> Self {
Self {
skip_teardown,
allow_known_dirty: false,
allow_commits_ahead: false,
}
}
pub fn local_discarding_dirty(skip_teardown: bool) -> Self {
Self {
skip_teardown,
allow_known_dirty: true,
allow_commits_ahead: false,
}
}
pub fn local_discarding_ahead(skip_teardown: bool, discard_known_dirty: bool) -> Self {
Self {
skip_teardown,
allow_known_dirty: discard_known_dirty,
allow_commits_ahead: true,
}
}
}
pub fn classify_delete_eligibility(
facts: &WorktreeFacts,
options: DeleteOptions,
) -> WorktreeOpResult<()> {
if facts.is_main {
return Err(WorktreeOpError::Ineligible(
"the main worktree cannot be deleted".to_string(),
));
}
match facts.base_merge_in_progress {
SafetyFact::Yes => {
return Err(WorktreeOpError::RootBusy(format!(
"the repository root is holding an unresolved merge; recovery: {RECOVERY_LOCAL_OR_TUI}"
)))
}
SafetyFact::Unknown => {
return Err(WorktreeOpError::Ineligible(
"the repository root's merge state could not be determined".to_string(),
))
}
SafetyFact::No => {}
}
match facts.has_commits_ahead {
SafetyFact::Yes if !options.allow_commits_ahead && facts.dirty == DirtyState::Unknown => {
return Err(WorktreeOpError::DirtyUnknown(
"the worktree's uncommitted-change state could not be determined".to_string(),
))
}
SafetyFact::Yes if !options.allow_commits_ahead => {
return Err(WorktreeOpError::CommitsAhead {
message: "the worktree has unmerged commits ahead of base".to_string(),
target: Box::new(AheadTarget::from_facts(facts)),
})
}
SafetyFact::Unknown => {
return Err(WorktreeOpError::Ineligible(
"the worktree's commits-ahead state could not be determined".to_string(),
))
}
SafetyFact::Yes | SafetyFact::No => {}
}
match facts.dirty {
DirtyState::Dirty if !options.allow_known_dirty => {
return Err(WorktreeOpError::Dirty {
message: "the worktree has uncommitted changes".to_string(),
target: Box::new(DirtyTarget::from_facts(facts)),
})
}
DirtyState::Unknown => {
return Err(WorktreeOpError::DirtyUnknown(
"the worktree's uncommitted-change state could not be determined".to_string(),
))
}
DirtyState::Dirty | DirtyState::Clean => {}
}
Ok(())
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExpectedTarget {
pub branch: Option<String>,
pub identity: Option<String>,
pub head: Option<String>,
}
impl ExpectedTarget {
pub fn unchecked() -> Self {
Self::default()
}
pub fn on_branch(branch: impl Into<String>) -> Self {
Self {
branch: Some(branch.into()),
..Self::default()
}
}
pub fn with_identity(mut self, identity: impl Into<String>) -> Self {
self.identity = Some(identity.into());
self
}
pub fn with_head(mut self, head: impl Into<String>) -> Self {
self.head = Some(head.into());
self
}
}
pub fn classify_delete_identity(
facts: &WorktreeFacts,
expected: &ExpectedTarget,
) -> WorktreeOpResult<()> {
if let Some(expected_branch) = expected.branch.as_deref() {
if facts.branch != expected_branch {
let observed = if facts.branch.is_empty() {
"<detached>"
} else {
facts.branch.as_str()
};
return Err(WorktreeOpError::NotFound(format!(
"the confirmed worktree on branch '{expected_branch}' is no longer at this path (it is now on '{observed}'); refresh and confirm again"
)));
}
}
if let Some(expected_identity) = expected.identity.as_deref() {
if facts.identity != expected_identity {
return Err(WorktreeOpError::NotFound(
"the confirmed worktree's Git identity no longer matches this path; refresh and confirm again"
.to_string(),
));
}
}
if let Some(expected_head) = expected.head.as_deref() {
if facts.head != expected_head {
return Err(WorktreeOpError::NotFound(format!(
"the confirmed worktree moved from HEAD '{expected_head}' to '{}'; refresh and confirm again",
facts.head
)));
}
}
Ok(())
}
pub fn classify_delete_drift(
before: &WorktreeFacts,
after: &WorktreeFacts,
) -> WorktreeOpResult<()> {
if !same_path(&before.path, &after.path) {
return Err(WorktreeOpError::NotFound(format!(
"the worktree moved from '{}' to '{}' while it was being torn down; nothing was removed",
before.path.display(),
after.path.display()
)));
}
if before.identity != after.identity {
return Err(WorktreeOpError::NotFound(
"the worktree's Git identity changed while it was being torn down; nothing was removed"
.to_string(),
));
}
if before.branch != after.branch {
return Err(WorktreeOpError::NotFound(format!(
"the worktree moved from branch '{}' to '{}' while it was being torn down; nothing was removed",
before.branch, after.branch
)));
}
if before.head != after.head {
return Err(WorktreeOpError::NotFound(format!(
"the worktree moved from HEAD '{}' to '{}' while it was being torn down; nothing was removed",
before.head, after.head
)));
}
for (fact, before, after) in [
(
"uncommitted-change",
DriftFact::from(before.dirty),
DriftFact::from(after.dirty),
),
(
"commits-ahead",
DriftFact::from(before.has_commits_ahead),
DriftFact::from(after.has_commits_ahead),
),
(
"base merge",
DriftFact::from(before.base_merge_in_progress),
DriftFact::from(after.base_merge_in_progress),
),
] {
if before != after {
return Err(WorktreeOpError::Ineligible(format!(
"the worktree's {fact} state changed from {before} to {after} while it was being torn down; nothing was removed"
)));
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DriftFact {
Yes,
No,
Unknown,
}
impl From<DirtyState> for DriftFact {
fn from(value: DirtyState) -> Self {
match value {
DirtyState::Dirty => Self::Yes,
DirtyState::Clean => Self::No,
DirtyState::Unknown => Self::Unknown,
}
}
}
impl From<SafetyFact> for DriftFact {
fn from(value: SafetyFact) -> Self {
match value {
SafetyFact::Yes => Self::Yes,
SafetyFact::No => Self::No,
SafetyFact::Unknown => Self::Unknown,
}
}
}
impl fmt::Display for DriftFact {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Yes => "yes",
Self::No => "no",
Self::Unknown => "unknown",
})
}
}
pub fn classify_merge_eligibility(
facts: &WorktreeFacts,
policy: ConflictPolicy,
) -> WorktreeOpResult<()> {
if facts.is_main {
return Err(WorktreeOpError::Ineligible(
"the main worktree cannot be merged into itself".to_string(),
));
}
match facts.base_merge_in_progress {
SafetyFact::Yes => {
return Err(WorktreeOpError::RootBusy(format!(
"the repository root is holding an unresolved merge; recovery: {RECOVERY_LOCAL_OR_TUI}"
)))
}
SafetyFact::Unknown => {
return Err(WorktreeOpError::Ineligible(
"the repository root's merge state could not be determined".to_string(),
))
}
SafetyFact::No => {}
}
if facts.is_detached || facts.branch.is_empty() {
return Err(WorktreeOpError::Ineligible(
"a detached worktree has no branch to merge".to_string(),
));
}
if !facts.inspection.is_inspected() {
return Err(WorktreeOpError::Ineligible(
"the worktree has not been inspected against base; a fresh inspection is required"
.to_string(),
));
}
if policy == ConflictPolicy::AbortOnConflict && !facts.conflict_files.is_empty() {
return Err(WorktreeOpError::Ineligible(
"the worktree conflicts with base".to_string(),
));
}
match facts.has_commits_ahead {
SafetyFact::Yes => {}
SafetyFact::No => {
return Err(WorktreeOpError::Ineligible(
"the worktree has no commits ahead of base".to_string(),
))
}
SafetyFact::Unknown => {
return Err(WorktreeOpError::Ineligible(
"the worktree's commits-ahead state could not be determined".to_string(),
))
}
}
Ok(())
}
pub fn branch_name_for_change(change_id: &str) -> String {
change_id.replace(['/', '\\', ' '], "-")
}
pub fn worktree_path_for_change(workspace_base_dir: &Path, change_id: &str) -> PathBuf {
workspace_base_dir.join(branch_name_for_change(change_id))
}
use super::same_path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MergeAttempt {
Merged,
Conflict {
files: Vec<String>,
},
}
#[async_trait]
pub trait WorktreeBackend: Send + Sync {
async fn observe(
&self,
request: crate::worktree_ops::ObservationRequest,
) -> WorktreeOpResult<Vec<WorktreeFacts>>;
async fn base_head(&self) -> WorktreeOpResult<String>;
async fn create(&self, path: &Path, branch: &str, base_commit: &str) -> WorktreeOpResult<()>;
async fn teardown(&self, path: &Path) -> WorktreeOpResult<()>;
async fn remove_worktree(&self, path: &Path) -> WorktreeOpResult<()>;
async fn branch_ref(&self, branch: &str) -> WorktreeOpResult<Option<String>>;
async fn delete_branch_if_merged(&self, branch: &str) -> WorktreeOpResult<()>;
async fn delete_branch_at(&self, branch: &str, expected_oid: &str) -> WorktreeOpResult<()>;
async fn merge_into_base(
&self,
branch: &str,
policy: ConflictPolicy,
) -> WorktreeOpResult<MergeAttempt>;
async fn run_on_merged(&self, change_id: &str, worktree_path: &Path) -> WorktreeOpResult<()>;
async fn change_is_eligible(&self, change_id: &str) -> WorktreeOpResult<()>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorktreeOperationEvent {
Created {
branch: String,
},
Deleted {
branch: String,
},
MergeStarted {
branch: String,
},
MergeCompleted {
branch: String,
},
MergeFailed {
branch: String,
error: String,
},
Refreshed,
}
#[async_trait]
pub trait WorktreeEventSink: Send + Sync {
async fn emit(&self, event: WorktreeOperationEvent);
}
#[cfg_attr(not(test), allow(dead_code))]
pub struct NullEventSink;
#[async_trait]
impl WorktreeEventSink for NullEventSink {
async fn emit(&self, _event: WorktreeOperationEvent) {}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeOpOutcome {
pub branch: String,
pub path: PathBuf,
pub detail: String,
pub branch_retained: bool,
}
pub struct WorktreeService {
backend: std::sync::Arc<dyn WorktreeBackend>,
events: std::sync::Arc<dyn WorktreeEventSink>,
workspace_base_dir: PathBuf,
root_guard: Mutex<()>,
}
impl WorktreeService {
pub fn new(
backend: std::sync::Arc<dyn WorktreeBackend>,
events: std::sync::Arc<dyn WorktreeEventSink>,
workspace_base_dir: PathBuf,
) -> Self {
Self {
backend,
events,
workspace_base_dir,
root_guard: Mutex::new(()),
}
}
pub async fn observe(&self) -> WorktreeOpResult<Vec<WorktreeFacts>> {
self.backend
.observe(crate::worktree_ops::ObservationRequest::Periodic)
.await
}
pub async fn create_change_worktree(&self, change_id: &str) -> WorktreeOpResult<WorktreeFacts> {
let _guard = self.acquire_root()?;
self.backend.change_is_eligible(change_id).await?;
let branch = branch_name_for_change(change_id);
let path = worktree_path_for_change(&self.workspace_base_dir, change_id);
let observed = self
.backend
.observe(crate::worktree_ops::ObservationRequest::Listing)
.await?;
if observed
.iter()
.any(|facts| facts.branch == branch || same_path(&facts.path, &path))
{
return Err(WorktreeOpError::Exists(format!(
"a worktree for change '{change_id}' already exists"
)));
}
let base_commit = self.backend.base_head().await?;
self.backend.create(&path, &branch, &base_commit).await?;
self.events
.emit(WorktreeOperationEvent::Created {
branch: branch.clone(),
})
.await;
self.events.emit(WorktreeOperationEvent::Refreshed).await;
self.backend
.observe(crate::worktree_ops::ObservationRequest::Target(
path.clone(),
))
.await?
.into_iter()
.find(|facts| same_path(&facts.path, &path))
.ok_or_else(|| {
WorktreeOpError::Internal(format!(
"worktree for change '{change_id}' was created but is not observable"
))
})
}
pub async fn delete_worktree(
&self,
path: &Path,
expected: &ExpectedTarget,
options: DeleteOptions,
) -> WorktreeOpResult<WorktreeOpOutcome> {
let _guard = self.acquire_root()?;
let before = self.locate(path).await?;
classify_delete_identity(&before, expected)?;
classify_delete_eligibility(&before, options)?;
if !options.skip_teardown {
self.backend.teardown(&before.path).await?;
}
let after = self.locate(&before.path).await?;
classify_delete_identity(&after, expected)?;
classify_delete_drift(&before, &after)?;
classify_delete_eligibility(&after, options)?;
self.confirm_branch_ref(&after).await?;
if options.allow_known_dirty {
warn!(
path = %after.path.display(),
branch = %after.branch,
dirty_discard = true,
skip_teardown = options.skip_teardown,
"discarding known uncommitted changes: forcing removal of a dirty worktree on explicit operator confirmation"
);
}
if options.allow_commits_ahead {
warn!(
path = %after.path.display(),
branch = %after.branch,
head = %after.head,
ahead_discard = true,
dirty_discard = options.allow_known_dirty,
skip_teardown = options.skip_teardown,
"discarding known commits ahead of base: removing the worktree and deleting its unmerged branch on explicit operator confirmation"
);
}
self.backend.remove_worktree(&after.path).await?;
let (branch_note, branch_retained) = if options.allow_commits_ahead {
self.discard_ahead_branch(&after).await
} else {
self.cleanup_branch(&after).await
};
self.events
.emit(WorktreeOperationEvent::Deleted {
branch: after.branch.clone(),
})
.await;
self.events.emit(WorktreeOperationEvent::Refreshed).await;
let teardown_note = if options.skip_teardown {
"removed without teardown"
} else {
"torn down and removed"
};
Ok(WorktreeOpOutcome {
detail: format!(
"worktree on branch '{}' was {teardown_note}{branch_note}",
after.branch
),
branch: after.branch,
path: after.path,
branch_retained,
})
}
async fn confirm_branch_ref(&self, facts: &WorktreeFacts) -> WorktreeOpResult<()> {
if facts.branch.is_empty() {
return Ok(());
}
match self.backend.branch_ref(&facts.branch).await {
Ok(Some(oid)) if oid == facts.head => Ok(()),
Ok(Some(oid)) => Err(WorktreeOpError::NotFound(format!(
"branch '{}' moved from '{}' to '{oid}' after the deletion was authorized; nothing was removed",
facts.branch, facts.head
))),
Ok(None) => Err(WorktreeOpError::NotFound(format!(
"branch '{}' no longer exists; nothing was removed",
facts.branch
))),
Err(error) => Err(WorktreeOpError::Ineligible(format!(
"branch '{}' ref state could not be determined ({error}); nothing was removed",
facts.branch
))),
}
}
async fn cleanup_branch(&self, facts: &WorktreeFacts) -> (String, bool) {
if facts.branch.is_empty() {
return (String::new(), false);
}
let retained = match self.backend.branch_ref(&facts.branch).await {
Ok(None) => return (String::new(), false),
Ok(Some(oid)) if oid == facts.head => {
match self.backend.delete_branch_if_merged(&facts.branch).await {
Ok(()) => return (format!("; branch '{}' was deleted", facts.branch), false),
Err(error) => {
format!("its commits are not reachable from elsewhere ({error})")
}
}
}
Ok(Some(oid)) => format!(
"its ref moved from '{}' to '{oid}' after the deletion was authorized",
facts.head
),
Err(error) => format!("its ref state could not be reconfirmed ({error})"),
};
warn!(
branch = %facts.branch,
"branch was retained after worktree removal: {retained}"
);
(
format!(
"; branch '{}' was retained because {retained}",
facts.branch
),
true,
)
}
async fn discard_ahead_branch(&self, facts: &WorktreeFacts) -> (String, bool) {
if facts.branch.is_empty() {
return (String::new(), false);
}
match self
.backend
.delete_branch_at(&facts.branch, &facts.head)
.await
{
Ok(()) => (
format!(
"; branch '{}' and its unmerged commits were deleted",
facts.branch
),
false,
),
Err(error) => {
warn!(
branch = %facts.branch,
head = %facts.head,
"explicitly confirmed ahead branch was retained after worktree removal: {error}"
);
(
format!(
"; branch '{}' was retained because its ref no longer matched the confirmed commit '{}' or could not be deleted ({error})",
facts.branch, facts.head
),
true,
)
}
}
}
pub async fn merge_worktree(
&self,
path: &Path,
policy: ConflictPolicy,
) -> WorktreeOpResult<WorktreeOpOutcome> {
let _guard = self.acquire_root()?;
let facts = self.locate(path).await?;
if let Err(error) = classify_merge_eligibility(&facts, policy) {
self.events
.emit(WorktreeOperationEvent::MergeFailed {
branch: facts.branch.clone(),
error: error.to_string(),
})
.await;
return Err(error);
}
self.events
.emit(WorktreeOperationEvent::MergeStarted {
branch: facts.branch.clone(),
})
.await;
let attempt = match self.backend.merge_into_base(&facts.branch, policy).await {
Ok(attempt) => attempt,
Err(error) => {
self.events
.emit(WorktreeOperationEvent::MergeFailed {
branch: facts.branch.clone(),
error: error.to_string(),
})
.await;
return Err(error);
}
};
if let MergeAttempt::Conflict { files } = attempt {
let error = WorktreeOpError::MergeConflict {
files,
recovery: RECOVERY_LOCAL_OR_TUI,
};
self.events
.emit(WorktreeOperationEvent::MergeFailed {
branch: facts.branch.clone(),
error: error.to_string(),
})
.await;
return Err(error);
}
if let Some(change_id) =
crate::vcs::GitWorkspaceManager::extract_change_id_from_worktree_name(&facts.branch)
{
if let Err(error) = self.backend.run_on_merged(&change_id, &facts.path).await {
let message = format!(
"on_merged hook failed for '{change_id}'; branch merged transition blocked: {error}"
);
self.events
.emit(WorktreeOperationEvent::MergeFailed {
branch: facts.branch.clone(),
error: message.clone(),
})
.await;
return Err(WorktreeOpError::Internal(message));
}
} else {
debug!(
branch = %facts.branch,
"no change_id could be extracted from the branch name; skipping on_merged"
);
}
self.events
.emit(WorktreeOperationEvent::MergeCompleted {
branch: facts.branch.clone(),
})
.await;
self.events.emit(WorktreeOperationEvent::Refreshed).await;
Ok(WorktreeOpOutcome {
detail: format!("branch '{}' was merged into base", facts.branch),
branch: facts.branch,
path: facts.path,
branch_retained: false,
})
}
#[cfg(test)]
pub fn acquire_root_for_test(&self) -> WorktreeOpResult<tokio::sync::MutexGuard<'_, ()>> {
self.acquire_root()
}
fn acquire_root(&self) -> WorktreeOpResult<tokio::sync::MutexGuard<'_, ()>> {
self.root_guard.try_lock().map_err(|_| {
WorktreeOpError::RootBusy(
"another worktree operation is already mutating this repository".to_string(),
)
})
}
async fn locate(&self, path: &Path) -> WorktreeOpResult<WorktreeFacts> {
self.backend
.observe(crate::worktree_ops::ObservationRequest::Target(
path.to_path_buf(),
))
.await?
.into_iter()
.find(|facts| same_path(&facts.path, path))
.ok_or_else(|| {
WorktreeOpError::NotFound(
"the addressed worktree is not present in the current observation".to_string(),
)
})
}
}
#[cfg(test)]
mod tests;