use super::{StoreError, StoreLockMode};
use crate::coordinate::CoordinateError;
use std::error::Error as _;
use std::io;
fn assert_display_contains(error: &StoreError, needle: &str) {
let display = error.to_string();
assert!(
display.contains(needle),
"helper constructor display should contain {needle:?}, got {display:?}"
);
}
#[test]
fn batch_failed_helper_preserves_item_index_and_source() {
let error = StoreError::batch_failed(
3,
StoreError::Io(io::Error::new(io::ErrorKind::TimedOut, "append timed out")),
);
assert!(matches!(
&error,
StoreError::BatchFailed {
item_index: 3,
source
} if matches!(source.as_ref(), StoreError::Io(_))
));
assert_display_contains(&error, "batch failed at item 3");
assert_display_contains(&error, "append timed out");
assert!(
error
.source()
.is_some_and(|source| source.to_string().contains("append timed out")),
"BatchFailed helper should expose the wrapped StoreError as source"
);
}
#[test]
fn batch_sync_failed_helper_preserves_count_and_source() {
let error = StoreError::batch_sync_failed(4, StoreError::Io(io::Error::other("fsync failed")));
assert!(matches!(
&error,
StoreError::BatchSyncFailed {
item_count: 4,
source
} if matches!(source.as_ref(), StoreError::Io(_))
));
assert_display_contains(&error, "batch sync failed after writing 4 items");
assert_display_contains(&error, "fsync failed");
assert!(
error
.source()
.is_some_and(|source| source.to_string().contains("fsync failed")),
"BatchSyncFailed helper should expose the wrapped StoreError as source"
);
}
#[test]
fn corrupt_magic_helper_builds_corrupt_segment() {
let error = StoreError::corrupt_magic(9);
assert!(
matches!(
error,
StoreError::CorruptSegment {
segment_id: 9,
ref detail
} if detail == "bad magic"
),
"expected bad-magic CorruptSegment, got {error:?}"
);
assert_display_contains(&error, "corrupt segment 9");
assert_display_contains(&error, "bad magic");
assert!(error.source().is_none());
}
#[test]
fn corrupt_eof_helper_builds_corrupt_segment() {
let error = StoreError::corrupt_eof(11);
assert!(
matches!(
error,
StoreError::CorruptSegment {
segment_id: 11,
ref detail
} if detail == "unexpected EOF during read"
),
"expected EOF CorruptSegment, got {error:?}"
);
assert_display_contains(&error, "corrupt segment 11");
assert_display_contains(&error, "unexpected EOF during read");
assert!(error.source().is_none());
}
#[test]
fn corrupt_version_helper_builds_corrupt_segment() {
let error = StoreError::corrupt_version(12, 99);
assert!(
matches!(
error,
StoreError::CorruptSegment {
segment_id: 12,
ref detail
} if detail.contains("unsupported segment version: 99")
),
"expected version CorruptSegment, got {error:?}"
);
assert_display_contains(&error, "corrupt segment 12");
assert_display_contains(&error, "unsupported segment version: 99");
assert!(error.source().is_none());
}
#[test]
fn cache_msg_helper_builds_cache_failed_without_typed_source() {
let error = StoreError::cache_msg("cache metadata short read");
assert!(matches!(error, StoreError::CacheFailed(_)));
assert_display_contains(&error, "cache error");
assert_display_contains(&error, "cache metadata short read");
assert!(
error
.source()
.is_some_and(|source| source.to_string().contains("cache metadata short read")),
"CacheFailed helper should expose the boxed message error as source"
);
}
#[test]
fn cache_error_helper_builds_cache_failed_with_typed_source() {
let error = StoreError::cache_error(io::Error::new(
io::ErrorKind::PermissionDenied,
"cache dir denied",
));
assert!(matches!(error, StoreError::CacheFailed(_)));
assert_display_contains(&error, "cache error");
assert_display_contains(&error, "cache dir denied");
assert!(
error
.source()
.is_some_and(|source| source.to_string().contains("cache dir denied")),
"CacheFailed typed helper should expose the wrapped source"
);
}
#[test]
fn ser_msg_helper_builds_serialization_error() {
let error = StoreError::ser_msg("frame exceeds u32::MAX");
assert!(matches!(error, StoreError::Serialization(_)));
assert_display_contains(&error, "serialization error");
assert_display_contains(&error, "frame exceeds u32::MAX");
assert!(
error
.source()
.is_some_and(|source| source.to_string().contains("frame exceeds u32::MAX")),
"Serialization helper should expose the boxed message error as source"
);
}
#[test]
fn corrupt_segment_with_detail_helper_builds_corrupt_segment() {
let error = StoreError::corrupt_segment_with_detail(13, "valid CRC but malformed msgpack");
assert!(
matches!(
error,
StoreError::CorruptSegment {
segment_id: 13,
ref detail
} if detail == "valid CRC but malformed msgpack"
),
"expected detail-preserving CorruptSegment, got {error:?}"
);
assert_display_contains(&error, "corrupt segment 13");
assert_display_contains(&error, "valid CRC but malformed msgpack");
assert!(error.source().is_none());
}
#[test]
fn store_locked_display_names_modes() {
let read_only = StoreError::StoreLocked {
path: "fixtures/store".into(),
mode: StoreLockMode::ReadOnly,
};
let mutable = StoreError::StoreLocked {
path: "fixtures/store".into(),
mode: StoreLockMode::Mutable,
};
assert_display_contains(&read_only, "read-only");
assert_display_contains(&mutable, "mutable");
}
#[test]
fn from_coordinate_error_routes_each_variant_to_its_dedicated_store_error() {
assert!(
matches!(
StoreError::from(CoordinateError::NulByte),
StoreError::CoordinateNulByte
),
"NulByte must route to the dedicated CoordinateNulByte variant"
);
assert!(
matches!(
StoreError::from(CoordinateError::ControlChar),
StoreError::CoordinateControlChar
),
"ControlChar must route to the dedicated CoordinateControlChar variant"
);
assert!(
matches!(
StoreError::from(CoordinateError::PathTraversal),
StoreError::CoordinatePathTraversal
),
"PathTraversal must route to the dedicated CoordinatePathTraversal variant"
);
assert!(matches!(
StoreError::from(CoordinateError::EmptyEntity),
StoreError::Coordinate(CoordinateError::EmptyEntity)
));
assert!(matches!(
StoreError::from(CoordinateError::EmptyScope),
StoreError::Coordinate(CoordinateError::EmptyScope)
));
assert!(matches!(
StoreError::from(CoordinateError::ForbiddenSeparator),
StoreError::Coordinate(CoordinateError::ForbiddenSeparator)
));
assert!(matches!(
StoreError::from(CoordinateError::EntityTooLong { len: 5, max: 4 }),
StoreError::Coordinate(CoordinateError::EntityTooLong { len: 5, max: 4 })
));
assert!(matches!(
StoreError::from(CoordinateError::ScopeTooLong { len: 9, max: 8 }),
StoreError::Coordinate(CoordinateError::ScopeTooLong { len: 9, max: 8 })
));
}
#[test]
fn from_io_error_wraps_as_the_io_variant() {
let error = StoreError::from(io::Error::new(io::ErrorKind::NotFound, "missing segment"));
assert!(matches!(error, StoreError::Io(_)));
assert!(
error
.source()
.is_some_and(|source| source.to_string().contains("missing segment")),
"the wrapped io::Error must be exposed as the error source"
);
}
#[test]
fn source_exposes_wrapped_errors_and_is_none_for_leaf_variants() {
assert!(
StoreError::Io(io::Error::other("disk gone"))
.source()
.is_some(),
"Io must expose its inner io::Error as source"
);
assert!(
StoreError::Coordinate(CoordinateError::EmptyEntity)
.source()
.is_some(),
"Coordinate must expose its inner CoordinateError as source"
);
assert!(
StoreError::WriterCrashed.source().is_none(),
"WriterCrashed is a leaf variant with no source"
);
assert!(
StoreError::IdempotencyRequired.source().is_none(),
"IdempotencyRequired is a leaf variant with no source"
);
}
#[test]
fn delegated_display_helpers_render_their_group_nonempty() {
assert_display_contains(&StoreError::CoordinateNulByte, "NUL");
assert_display_contains(
&StoreError::ReservedKind {
index: Some(2),
kind: 0x0001,
},
"reserved kind",
);
assert_display_contains(
&StoreError::InvalidCoordinate {
index: None,
reason: "empty entity".to_owned(),
},
"invalid coordinate",
);
let mmap = StoreError::MmapFutureVersion {
found: 9,
supported: 3,
};
assert_display_contains(&mmap, "mmap index");
assert_display_contains(&mmap, "version 9");
assert_display_contains(&mmap, "version 3");
assert_display_contains(
&StoreError::RangeMalformed { start: 8, end: 4 },
"malformed range",
);
assert_display_contains(
&StoreError::AncestryCorrupt {
cycle_at: crate::id::EventId::from(1u128),
},
"cycle",
);
let chain = StoreError::ChainVerificationFailed {
content_hash_mismatches: 2,
dangling_links: 5,
};
assert_display_contains(&chain, "hash-chain verification failed");
assert_display_contains(&chain, "2 content-hash");
assert_display_contains(&chain, "5 dangling");
assert_display_contains(
&StoreError::ProjectionStateContractUnspecified {
projection: "proj:orders".to_owned(),
},
"growth contract",
);
assert_display_contains(
&StoreError::PlatformAdmissionFailed {
capability: "mmap",
reason: "no huge pages".to_owned(),
},
"platform admission failed",
);
}