use core::fmt;
use std::path::PathBuf;
use lgwks_std::wire::WireError;
use crate::effect::{ActionId, AttemptId, EffectKey};
use super::frame::SaturatingFrom;
use super::{
AttemptStatus, EventKind, JournalError, JournalLimitKind, JournalPosition, Verification,
};
pub(crate) const CHECKPOINT_MAGIC: &[u8; 32] = b"lgwks.journal.checkpoint.v1\0\0\0\0\0";
pub const CONTINUATION_WATERMARK_NUMERATOR: u64 = 8;
pub const CONTINUATION_WATERMARK_DENOMINATOR: u64 = 10;
pub const MAX_CHECKPOINT_UNRESOLVED: usize = 128;
pub const MAX_CHECKPOINT_SETTLED: usize = 160;
const GENERATION_DIRECTORY_SUFFIX: &str = ".cont";
const GENERATION_DIGITS: usize = 6;
const MAX_GENERATION_NAME: u64 = 999_999;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SealPause {
AfterSealWrite,
AfterSuccessorWrite,
AfterSuccessorSync,
AfterDirectorySync,
}
impl SealPause {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::AfterSealWrite => "after_seal_write",
Self::AfterSuccessorWrite => "after_successor_write",
Self::AfterSuccessorSync => "after_successor_sync",
Self::AfterDirectorySync => "after_directory_sync",
}
}
#[must_use]
pub const fn all() -> [Self; 4] {
[
Self::AfterSealWrite,
Self::AfterSuccessorWrite,
Self::AfterSuccessorSync,
Self::AfterDirectorySync,
]
}
}
impl fmt::Display for SealPause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
const fn watermark_of(limit: u64) -> u64 {
match limit
.saturating_mul(CONTINUATION_WATERMARK_NUMERATOR)
.checked_div(CONTINUATION_WATERMARK_DENOMINATOR)
{
Some(at) => at,
None => limit,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContinuationPolicy {
events_at: u64,
bytes_at: u64,
}
impl ContinuationPolicy {
#[must_use]
pub const fn declared() -> Self {
Self {
events_at: watermark_of(SHIPPED_EVENT_CEILING),
bytes_at: watermark_of(SHIPPED_BYTE_CEILING),
}
}
pub fn new(events_at: u64, bytes_at: u64) -> Result<Self, JournalError> {
if events_at >= SHIPPED_EVENT_CEILING || bytes_at >= SHIPPED_BYTE_CEILING {
let requested = events_at.max(bytes_at);
let refusal = Err(JournalError::CapacityExceeded {
resource: JournalLimitKind::ContinuationWatermark,
limit: SHIPPED_EVENT_CEILING,
requested,
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "ContinuationPolicy::new: returning an error to the caller");
return refusal;
}
Ok(Self {
events_at,
bytes_at,
})
}
#[must_use]
pub const fn events_at(&self) -> u64 {
self.events_at
}
#[must_use]
pub const fn bytes_at(&self) -> u64 {
self.bytes_at
}
}
impl Default for ContinuationPolicy {
fn default() -> Self {
Self::declared()
}
}
const SHIPPED_EVENT_CEILING: u64 = 100_000;
const SHIPPED_BYTE_CEILING: u64 = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContinuationWatermark {
continuing: bool,
events: u64,
events_limit: u64,
bytes: u64,
bytes_limit: u64,
events_at: u64,
bytes_at: u64,
}
impl ContinuationWatermark {
#[must_use]
pub const fn inert(events: u64, events_limit: u64, bytes: u64, bytes_limit: u64) -> Self {
Self {
continuing: false,
events,
events_limit,
bytes,
bytes_limit,
events_at: 0,
bytes_at: 0,
}
}
#[must_use]
pub const fn measured(
events: u64,
events_limit: u64,
bytes: u64,
bytes_limit: u64,
policy: ContinuationPolicy,
) -> Self {
Self {
continuing: true,
events,
events_limit,
bytes,
bytes_limit,
events_at: policy.events_at,
bytes_at: policy.bytes_at,
}
}
#[must_use]
pub const fn continues(&self) -> bool {
self.continuing
}
#[must_use]
pub const fn is_due(&self) -> bool {
self.continuing && (self.events >= self.events_at || self.bytes >= self.bytes_at)
}
#[must_use]
pub const fn events(&self) -> (u64, u64) {
(self.events, self.events_limit)
}
#[must_use]
pub const fn bytes(&self) -> (u64, u64) {
(self.bytes, self.bytes_limit)
}
#[must_use]
pub const fn events_watermark(&self) -> u64 {
self.events_at
}
#[must_use]
pub const fn bytes_watermark(&self) -> u64 {
self.bytes_at
}
#[must_use]
pub const fn event_headroom(&self) -> u64 {
self.events_limit.saturating_sub(self.events)
}
#[must_use]
pub const fn byte_headroom(&self) -> u64 {
self.bytes_limit.saturating_sub(self.bytes)
}
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct SettledAttempt {
key: EffectKey,
rung: u8,
status: u8,
verification: Option<Verification>,
}
impl SettledAttempt {
#[must_use]
pub fn new(
key: EffectKey,
rung: EventKind,
status: AttemptStatus,
verification: Option<Verification>,
) -> Self {
Self {
key,
rung: rung_index(rung),
status: status_index(status),
verification,
}
}
#[must_use]
pub const fn action(&self) -> ActionId {
self.key.action()
}
#[must_use]
pub const fn attempt(&self) -> AttemptId {
self.key.attempt()
}
#[must_use]
pub const fn key(&self) -> EffectKey {
self.key
}
#[must_use]
pub const fn rung(&self) -> Option<EventKind> {
rung_of(self.rung)
}
#[must_use]
pub const fn status(&self) -> Option<AttemptStatus> {
status_of(self.status)
}
#[must_use]
pub const fn verification(&self) -> Option<Verification> {
self.verification
}
#[must_use]
pub fn already_walked(&self, key: EffectKey) -> bool {
key.action() == self.key.action() && key.attempt() <= self.key.attempt()
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct UnresolvedAttempt {
key: EffectKey,
rung: u8,
}
impl UnresolvedAttempt {
#[must_use]
pub const fn new(key: EffectKey, rung: EventKind) -> Self {
Self {
key,
rung: rung_index(rung),
}
}
#[must_use]
pub const fn key(&self) -> EffectKey {
self.key
}
#[must_use]
pub const fn rung(&self) -> Option<EventKind> {
rung_of(self.rung)
}
#[must_use]
pub const fn recovered_status(&self) -> Option<AttemptStatus> {
match rung_of(self.rung) {
Some(EventKind::IntentAdmitted) => Some(AttemptStatus::Prepared),
Some(EventKind::DispatchPrepared) => Some(AttemptStatus::OutcomeUnknown),
_ => None,
}
}
}
const fn rung_index(rung: EventKind) -> u8 {
match rung {
EventKind::IntentAdmitted => 0,
EventKind::DispatchPrepared => 1,
EventKind::OutcomeObserved => 2,
EventKind::Verified => 3,
}
}
const fn status_index(status: AttemptStatus) -> u8 {
match status {
AttemptStatus::Prepared => 0,
AttemptStatus::OutcomeUnknown => 1,
AttemptStatus::Applied => 2,
AttemptStatus::NotApplied => 3,
AttemptStatus::Verified => 4,
AttemptStatus::VerificationFailed => 5,
}
}
const fn rung_of(index: u8) -> Option<EventKind> {
match index {
0 => Some(EventKind::IntentAdmitted),
1 => Some(EventKind::DispatchPrepared),
2 => Some(EventKind::OutcomeObserved),
3 => Some(EventKind::Verified),
_ => None,
}
}
const fn status_of(index: u8) -> Option<AttemptStatus> {
match index {
0 => Some(AttemptStatus::Prepared),
1 => Some(AttemptStatus::OutcomeUnknown),
2 => Some(AttemptStatus::Applied),
3 => Some(AttemptStatus::NotApplied),
4 => Some(AttemptStatus::Verified),
5 => Some(AttemptStatus::VerificationFailed),
_ => None,
}
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct Continuation {
generation: u64,
predecessor: JournalPosition,
settled: Vec<SettledAttempt>,
unresolved: Vec<UnresolvedAttempt>,
}
impl Continuation {
pub fn new(
generation: u64,
predecessor: JournalPosition,
settled: Vec<SettledAttempt>,
unresolved: Vec<UnresolvedAttempt>,
) -> Result<Self, JournalError> {
if settled.len() > MAX_CHECKPOINT_SETTLED {
let requested = u64::saturating_from(settled.len());
let refusal = Err(JournalError::CapacityExceeded {
resource: JournalLimitKind::CheckpointActions,
limit: u64::saturating_from(MAX_CHECKPOINT_SETTLED),
requested,
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "Continuation::new: returning an error to the caller");
return refusal;
}
if unresolved.len() > MAX_CHECKPOINT_UNRESOLVED {
let requested = u64::saturating_from(unresolved.len());
let refusal = Err(JournalError::CapacityExceeded {
resource: JournalLimitKind::CheckpointUnresolved,
limit: u64::saturating_from(MAX_CHECKPOINT_UNRESOLVED),
requested,
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "Continuation::new: returning an error to the caller");
return refusal;
}
Ok(Self {
generation,
predecessor,
settled,
unresolved,
})
}
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
#[must_use]
pub const fn predecessor(&self) -> JournalPosition {
self.predecessor
}
#[must_use]
pub fn settled(&self) -> &[SettledAttempt] {
&self.settled
}
#[must_use]
pub fn unresolved(&self) -> &[UnresolvedAttempt] {
&self.unresolved
}
#[must_use]
pub fn settled_for(&self, action: ActionId) -> Option<&SettledAttempt> {
self.settled.iter().find(|entry| entry.action() == action)
}
pub fn to_payload(&self) -> Result<Vec<u8>, JournalError> {
let archived =
lgwks_std::wire::to_bytes::<WireError>(self).map_err(JournalError::Encoding)?;
let mut payload = Vec::with_capacity(CHECKPOINT_MAGIC.len().saturating_add(archived.len()));
payload.extend_from_slice(CHECKPOINT_MAGIC);
payload.extend_from_slice(archived.as_ref());
Ok(payload)
}
#[must_use]
pub fn is_payload(payload: &[u8]) -> bool {
payload.starts_with(CHECKPOINT_MAGIC)
}
pub fn from_payload(payload: &[u8]) -> Option<Result<Self, JournalError>> {
let archived = payload.strip_prefix(CHECKPOINT_MAGIC.as_slice())?;
match lgwks_std::wire::from_bytes::<Self, WireError>(archived) {
Ok(checkpoint) => Some(Ok(checkpoint)),
Err(cause) => {
let refusal = Err(JournalError::Encoding(cause));
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "Continuation::from_payload: returning an error to the caller");
Some(refusal)
}
}
}
}
#[must_use]
pub fn successor_path(path: &std::path::Path) -> std::path::PathBuf {
let (Some(name), Some(parent)) = (path.file_name(), path.parent()) else {
return path.to_path_buf();
};
if let Some(number) = name_number(name) {
let next = number.saturating_add(1).min(MAX_GENERATION_NAME);
return parent.join(format!("{next:0GENERATION_DIGITS$}"));
}
let mut directory = path.as_os_str().to_os_string();
directory.push(GENERATION_DIRECTORY_SUFFIX);
PathBuf::from(directory).join(format!("{:0GENERATION_DIGITS$}", 1))
}
#[must_use]
pub fn generation_of(path: &std::path::Path) -> u64 {
match path.file_name().and_then(name_number) {
Some(number) if is_generation_path(path) => number.saturating_add(1),
Some(_) | None => 1,
}
}
fn name_number(name: &std::ffi::OsStr) -> Option<u64> {
if !is_generation_name(name) {
return None;
}
let number = name.to_string_lossy().bytes().fold(0_u64, |number, digit| {
number
.saturating_mul(10)
.saturating_add(u64::from(digit.saturating_sub(b'0')))
});
Some(number)
}
fn is_generation_name(name: &std::ffi::OsStr) -> bool {
let text = name.to_string_lossy();
text.len() == GENERATION_DIGITS && text.bytes().all(|byte| byte.is_ascii_digit())
}
#[must_use]
pub fn is_generation_path(path: &std::path::Path) -> bool {
let named = path.file_name().is_some_and(is_generation_name);
let in_directory = path
.parent()
.and_then(std::path::Path::file_name)
.is_some_and(|name| {
name.to_string_lossy()
.ends_with(GENERATION_DIRECTORY_SUFFIX)
});
named && in_directory
}
pub(crate) fn refuse_ambiguous_base(path: &std::path::Path) -> Result<(), JournalError> {
let Some(number) = path.file_name().and_then(name_number) else {
return Ok(());
};
if is_generation_path(path) {
return Ok(());
}
let refusal = Err(JournalError::CapacityExceeded {
resource: JournalLimitKind::Generation,
limit: MAX_GENERATION_NAME,
requested: number,
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "refuse_ambiguous_base: this name could be read as a generation");
refusal
}
#[cfg(test)]
mod tests {
use super::*;
use crate::effect::{
ActionDigest, EffectIdentity, EnvironmentEpoch, EnvironmentId, FlowRevision, RunId,
};
use crate::journal::EffectEvidence;
use lgwks_std::hash::blake3;
use std::path::Path;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const RUN: &str = "0102030405060708090a0b0c0d0e0f10";
const ACTION: &str = "1112131415161718191a1b1c1d1e1f20";
const ENV: &str = "2122232425262728292a2b2c2d2e2f30";
const FLOW_HEX: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
const DIGEST_HEX: &str = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeef";
fn key(attempt: &str) -> Result<EffectKey, Box<dyn std::error::Error>> {
let run = RunId::from_hex(RUN)?;
let environment = EnvironmentId::from_hex(ENV)?;
let flow = FlowRevision::from_tagged("blake3_256", FLOW_HEX)?;
let action = ActionId::from_hex(ACTION)?;
let attempt = AttemptId::from_decimal(attempt)?;
let digest = ActionDigest::from_tagged("blake3_256", DIGEST_HEX)?;
let epoch = EnvironmentEpoch::from_decimal("1")?;
Ok(EffectIdentity::new(run, environment, flow).key(action, attempt, digest, epoch))
}
#[test]
fn the_watermark_leaves_a_fifth_of_each_ceiling() {
assert_eq!(watermark_of(100), 80);
assert_eq!(watermark_of(100_000), 80_000);
assert_eq!(watermark_of(64 * 1024 * 1024), 53_687_091);
assert_eq!(watermark_of(0), 0);
assert_eq!(watermark_of(u64::MAX), watermark_of(u64::MAX));
}
#[test]
fn a_journal_that_does_not_continue_is_never_due() -> TestResult {
let mut watermark = ContinuationWatermark::inert(1, 2, 1, 2);
assert!(!watermark.continues());
assert!(
!watermark.is_due(),
"an inert journal is never due, at any size"
);
watermark =
ContinuationWatermark::measured(80, 100, 80, 100, ContinuationPolicy::new(80, 80)?);
assert!(
watermark.is_due(),
"eighty percent of either ceiling is due"
);
assert_eq!(watermark.event_headroom(), 20);
assert_eq!(watermark.byte_headroom(), 20);
Ok(())
}
#[test]
fn the_watermark_is_either_ceiling_not_both() -> TestResult {
let policy = ContinuationPolicy::new(80, 80)?;
let events = ContinuationWatermark::measured(100, 100, 0, 1_000_000, policy);
assert!(
events.is_due(),
"a full event ceiling is due whatever the byte count says"
);
let bytes = ContinuationWatermark::measured(0, 1_000_000, 1_000_000, 1_000_000, policy);
assert!(
bytes.is_due(),
"a full byte ceiling is due whatever the event count says"
);
Ok(())
}
#[test]
fn the_declared_policy_leaves_the_shipped_headroom_and_a_late_one_is_refused() -> TestResult {
let declared = ContinuationPolicy::declared();
assert_eq!(declared, ContinuationPolicy::default());
assert_eq!(declared.events_at(), 80_000);
assert_eq!(declared.bytes_at(), 53_687_091);
assert!(
declared.events_at() < SHIPPED_EVENT_CEILING
&& declared.bytes_at() < SHIPPED_BYTE_CEILING,
"the watermark is strictly inside both ceilings, so settlement always has room"
);
assert!(
ContinuationPolicy::new(SHIPPED_EVENT_CEILING, 1).is_err(),
"a trigger at the ceiling would ask for a continuation the journal cannot afford"
);
Ok(())
}
#[test]
fn a_checkpoint_round_trips_through_its_payload() -> TestResult {
let key = key("7")?;
let verification = Verification::new(
crate::effect::Id128::from_hex(&"42".repeat(16))?,
3,
blake3(b"observed"),
crate::journal::VerificationResult::NotSatisfied,
);
let checkpoint = Continuation::new(
4,
JournalPosition::genesis(),
vec![SettledAttempt::new(
key,
EventKind::Verified,
AttemptStatus::VerificationFailed,
Some(verification),
)],
vec![UnresolvedAttempt::new(key, EventKind::DispatchPrepared)],
)?;
let payload = checkpoint.to_payload()?;
assert!(
Continuation::is_payload(&payload),
"a checkpoint payload carries the magic a reader asks for first"
);
assert!(
!Continuation::is_payload(&[0x00, 0x01, 0x02]),
"an event payload does not"
);
let read =
Continuation::from_payload(&payload).ok_or("a checkpoint payload reads as one")??;
assert_eq!(read, checkpoint, "the archive is what a writer wrote");
assert_eq!(read.generation(), 4);
assert_eq!(read.settled().len(), 1);
assert_eq!(read.unresolved().len(), 1);
let carried = read.settled().first().ok_or("the fold is missing")?;
assert_eq!(carried.status(), Some(AttemptStatus::VerificationFailed));
assert_eq!(
carried.verification().map(Verification::predicate_version),
Some(3),
"the verification digest's predicate version crosses the boundary"
);
assert_eq!(
read.unresolved()
.first()
.map(UnresolvedAttempt::recovered_status),
Some(Some(AttemptStatus::OutcomeUnknown)),
"an unresolved attempt stays unknown on the far side"
);
Ok(())
}
#[test]
fn a_folded_action_refuses_an_older_attempt_and_admits_a_newer_one()
-> Result<(), Box<dyn std::error::Error>> {
let latest = key("9")?;
let folded = SettledAttempt::new(latest, EventKind::Verified, AttemptStatus::Applied, None);
assert!(
folded.already_walked(latest),
"the latest attempt itself is already walked"
);
assert!(
folded.already_walked(key("3")?),
"an older attempt of the same action is already walked"
);
assert!(
!folded.already_walked(key("10")?),
"a newer attempt is new work, not a replay"
);
Ok(())
}
#[test]
fn the_largest_checkpoint_the_counts_admit_fits_one_frame() -> TestResult {
let one = key("1")?;
let verification = Verification::new(
crate::effect::Id128::from_hex(ACTION)?,
u64::MAX,
blake3(b"the widest verification a settled record carries"),
crate::journal::VerificationResult::NotSatisfied,
);
let settled = (0..MAX_CHECKPOINT_SETTLED)
.map(|_| {
SettledAttempt::new(
one,
EventKind::Verified,
AttemptStatus::VerificationFailed,
Some(verification),
)
})
.collect::<Vec<_>>();
let unresolved = (0..MAX_CHECKPOINT_UNRESOLVED)
.map(|_| UnresolvedAttempt::new(one, EventKind::DispatchPrepared))
.collect::<Vec<_>>();
let widest = Continuation::new(u64::MAX, JournalPosition::genesis(), settled, unresolved)?;
let archived = widest.to_payload()?.len();
assert!(
archived <= super::super::file::MAX_FRAME_BYTES,
"the widest checkpoint the counts admit archives to {archived} bytes, past \
the {} byte frame it must be sealed in",
super::super::file::MAX_FRAME_BYTES
);
Ok(())
}
#[test]
fn a_carry_past_its_declared_bound_is_refused_before_anything_is_written() -> TestResult {
let one = key("1")?;
let folded = (1..=MAX_CHECKPOINT_SETTLED + 1)
.map(|_| SettledAttempt::new(one, EventKind::Verified, AttemptStatus::Applied, None))
.collect::<Vec<_>>();
let refused = Continuation::new(1, JournalPosition::genesis(), folded, Vec::new());
assert!(
matches!(
refused,
Err(JournalError::CapacityExceeded {
resource: JournalLimitKind::CheckpointActions,
..
})
),
"an over-large carry is refused, not framed: {refused:?}"
);
let unresolved = (0..MAX_CHECKPOINT_UNRESOLVED + 1)
.map(|_| UnresolvedAttempt::new(one, EventKind::DispatchPrepared))
.collect::<Vec<_>>();
let refused = Continuation::new(1, JournalPosition::genesis(), Vec::new(), unresolved);
assert!(
matches!(
refused,
Err(JournalError::CapacityExceeded {
resource: JournalLimitKind::CheckpointUnresolved,
..
})
),
"an over-large unresolved carry is refused: {refused:?}"
);
Ok(())
}
#[test]
fn an_index_this_build_does_not_name_reads_as_absent_not_as_the_ladder_foot() {
assert_eq!(rung_of(u8::MAX), None);
assert_eq!(status_of(u8::MAX), None);
assert_eq!(rung_of(0), Some(EventKind::IntentAdmitted));
assert_eq!(
status_index(AttemptStatus::VerificationFailed),
status_of(5).map_or(u8::MAX, status_index)
);
}
#[test]
fn a_successor_is_a_fixed_width_name_in_the_predecessors_own_directory() -> TestResult {
let first = Path::new("/var/run/bot.jrnl");
let second = successor_path(first);
assert_eq!(second, Path::new("/var/run/bot.jrnl.cont/000001"));
assert_eq!(generation_of(first), 1);
assert_eq!(generation_of(&second), 2);
let third = successor_path(&second);
assert_eq!(third, Path::new("/var/run/bot.jrnl.cont/000002"));
assert_eq!(generation_of(&third), 3);
let mut here = first.to_path_buf();
for _ in 0..500 {
here = successor_path(&here);
}
assert_eq!(
here.file_name().map(std::ffi::OsStr::len),
Some(GENERATION_DIGITS),
"a five-hundredth generation's name is still {} characters",
GENERATION_DIGITS
);
assert_eq!(generation_of(&here), 501);
assert!(
refuse_ambiguous_base(Path::new("/var/run/000123")).is_err(),
"a six-digit base name would be read as a generation"
);
assert!(refuse_ambiguous_base(first).is_ok());
assert!(refuse_ambiguous_base(&third).is_ok());
assert!(
is_generation_path(&second),
"and the successor is this scheme's own, both halves of the name"
);
assert!(!is_generation_path(Path::new("/var/run/bot.jrnl.cont/abc")));
Ok(())
}
#[test]
fn every_boundary_names_itself() {
let mut seen = std::collections::BTreeSet::new();
for pause in SealPause::all() {
assert!(
seen.insert(pause.as_str()),
"two boundaries share a spelling"
);
assert!(!pause.as_str().is_empty());
}
assert_eq!(seen.len(), SealPause::all().len());
}
#[test]
fn a_carried_rung_reads_back_as_the_status_a_recovery_fold_reports() -> TestResult {
assert_eq!(
UnresolvedAttempt::new(key("1")?, EventKind::IntentAdmitted).recovered_status(),
Some(AttemptStatus::Prepared)
);
assert_eq!(
UnresolvedAttempt::new(key("1")?, EventKind::DispatchPrepared).recovered_status(),
Some(AttemptStatus::OutcomeUnknown)
);
assert_eq!(
UnresolvedAttempt::new(key("1")?, EventKind::Verified).recovered_status(),
None,
"a resolved rung is not an unresolved attempt and reads as absent"
);
let evidence = EffectEvidence::Applied;
assert_eq!(
format!("{evidence:?}"),
"Applied",
"the evidence arm exists"
);
Ok(())
}
}