hexz_core/format/version.rs
1//! Format version management and compatibility checking.
2//!
3//! This module defines the versioning strategy for Hexz snapshot files (`.hxz`),
4//! enabling safe evolution of the on-disk format while maintaining backward and
5//! forward compatibility guarantees. Version negotiation ensures that readers can
6//! detect incompatible snapshots and provide actionable error messages.
7//!
8//! # Versioning Strategy
9//!
10//! Hexz uses a monotonic integer versioning scheme where each version number
11//! represents a distinct on-disk format:
12//!
13//! - **Version 1**: Initial format with two-level index, LZ4/Zstd compression,
14//! optional encryption, thin provisioning support
15//! - **Future versions**: Reserved for incompatible format changes (e.g., new
16//! index structures, alternative serialization formats, block metadata extensions)
17//!
18//! ## Semantic Versioning Alignment
19//!
20//! Format versions are independent of Hexz software versions (semver). A software
21//! release may support multiple format versions for backward compatibility. The
22//! mapping is defined as:
23//!
24//! | Format Version | Hexz Software Versions | Key Changes |
25//! |----------------|--------------------------|-------------|
26//! | 1 | 0.1.0+ | Initial release |
27//!
28//! # Compatibility Model
29//!
30//! ## Backward Compatibility (Reading Old Snapshots)
31//!
32//! Hexz readers maintain compatibility with snapshots created by older software:
33//!
34//! - **MIN_SUPPORTED_VERSION**: Oldest format version we can read (currently 1)
35//! - **Upgrade Path**: Snapshots older than MIN_SUPPORTED_VERSION must be migrated
36//! using the `hexz-migrate` tool (see Migration section below)
37//!
38//! ## Forward Compatibility (Reading New Snapshots)
39//!
40//! Hexz readers handle snapshots created by newer software:
41//!
42//! - **MAX_SUPPORTED_VERSION**: Newest format version we can read (currently 1)
43//! - **Degraded Mode**: Future versions may enable partial reads with warnings if
44//! minor features are unrecognized (not yet implemented)
45//! - **Strict Rejection**: Snapshots with `version > MAX_SUPPORTED_VERSION` are
46//! rejected with an actionable error message
47//!
48//! # Version Negotiation Workflow
49//!
50//! When opening a snapshot file:
51//!
52//! ```text
53//! 1. Read header magic bytes (validate "HEXZ" signature)
54//! 2. Read header.version field
55//! 3. Call check_version(header.version)
56//! 4. If VersionCompatibility::Incompatible:
57//! - Generate human-readable error via compatibility_message()
58//! - Suggest upgrade/migration path
59//! - Abort operation
60//! 5. If VersionCompatibility::Full:
61//! - Proceed with normal operations
62//! 6. If VersionCompatibility::Degraded (future):
63//! - Log warning about unsupported features
64//! - Proceed with limited functionality
65//! ```
66//!
67//! # Adding New Format Versions
68//!
69//! To introduce a breaking format change:
70//!
71//! ## Step 1: Increment Version Constant
72//!
73//! ```rust,ignore
74//! pub const CURRENT_VERSION: u32 = 2; // Changed from 1
75//! pub const MAX_SUPPORTED_VERSION: u32 = 2;
76//! ```
77//!
78//! ## Step 2: Update Serialization Logic
79//!
80//! Modify [`crate::format::header::Header`] or [`crate::format::index::MasterIndex`]
81//! to include new fields or change existing structures. Use serde attributes to
82//! maintain compatibility:
83//!
84//! ```rust,no_run
85//! # use serde::{Serialize, Deserialize};
86//! #[derive(Serialize, Deserialize)]
87//! pub struct Header {
88//! // Existing fields...
89//! #[serde(skip_serializing_if = "Option::is_none")]
90//! pub new_feature: Option<String>, // Version 2+ only
91//! }
92//! ```
93//!
94//! ## Step 3: Conditional Deserialization
95//!
96//! Add version-aware deserialization in reader code:
97//!
98//! ```rust,no_run
99//! # fn read_index_v1<R>(_: R) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
100//! # fn read_index_v2<R>(_: R) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
101//! # fn example<R>(header: hexz_core::format::header::Header, reader: R) -> Result<(), Box<dyn std::error::Error>> {
102//! match header.version {
103//! 1 => {
104//! // Legacy path for version 1
105//! read_index_v1(reader)?
106//! }
107//! 2 => {
108//! // New path for version 2
109//! read_index_v2(reader)?
110//! }
111//! _ => unreachable!("check_version already validated"),
112//! }
113//! # Ok(())
114//! # }
115//! ```
116//!
117//! ## Step 4: Update Tests
118//!
119//! Add test fixtures for the new format version:
120//!
121//! ```rust,no_run
122//! # fn load_fixture(_: &str) -> hexz_core::File { todo!() }
123//! #[test]
124//! fn test_read_v2_snapshot() {
125//! let snapshot = load_fixture("testdata/v2_snapshot.hxz");
126//! assert_eq!(snapshot.header.version, 2);
127//! // Verify new features work correctly
128//! }
129//! ```
130//!
131//! ## Step 5: Document Migration Path
132//!
133//! Update the migration guide with conversion instructions (see Migration section).
134//!
135//! # Migration Between Versions
136//!
137//! The `hexz-migrate` tool converts snapshots between format versions:
138//!
139//! ```bash
140//! # Upgrade old snapshot to current format
141//! hexz-migrate upgrade --input old_v1.hxz --output new_v2.hxz
142//!
143//! # Downgrade for compatibility (if supported)
144//! hexz-migrate downgrade --input new_v2.hxz --output legacy_v1.hxz \
145//! --target-version 1
146//! ```
147//!
148//! ## Migration Algorithm
149//!
150//! The migration tool performs a streaming rewrite:
151//!
152//! 1. Open source snapshot with reader for version N
153//! 2. Create destination snapshot with writer for version M
154//! 3. Stream all blocks through decompression/re-compression
155//! 4. Rebuild index in target format
156//! 5. Preserve metadata (encryption keys, parent references, etc.)
157//!
158//! ## Lossy Migrations
159//!
160//! Downgrading may lose features unsupported in the target version:
161//!
162//! - Version 2 → 1: Hypothetical new features would be discarded with warnings
163//!
164//! # Performance Considerations
165//!
166//! Version checking is performed once per snapshot open operation:
167//!
168//! - **Overhead**: Negligible (~100ns for integer comparison)
169//! - **Caching**: Version is cached in [`crate::api::file::File`] struct
170//! - **Hot Path**: Not in block read/write paths
171//!
172//! # Examples
173//!
174//! ## Basic Version Check
175//!
176//! ```
177//! use hexz_core::format::version::{check_version, VersionCompatibility};
178//!
179//! let snapshot_version = 1;
180//! let compat = check_version(snapshot_version);
181//!
182//! match compat {
183//! VersionCompatibility::Full => {
184//! println!("Snapshot is fully compatible");
185//! }
186//! VersionCompatibility::Incompatible => {
187//! eprintln!("Cannot read snapshot (incompatible version)");
188//! }
189//! VersionCompatibility::Degraded => {
190//! println!("Snapshot readable with warnings");
191//! }
192//! }
193//! ```
194//!
195//! ## User-Facing Error Messages
196//!
197//! ```
198//! use hexz_core::format::version::compatibility_message;
199//!
200//! let version = 999; // Future version
201//! let message = compatibility_message(version);
202//! println!("{}", message);
203//! // Output: "Version 999 is too new (max supported: 1). Please upgrade Hexz."
204//! ```
205//!
206//! ## Reader Implementation
207//!
208//! ```rust,no_run
209//! # use hexz_core::format::version::{check_version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION};
210//! # use hexz_common::Error;
211//! # use hexz_core::format::header::Header;
212//! # use std::path::Path;
213//! # struct Snapshot { header: Header }
214//! # fn read_header(_: &Path) -> Result<Header, Error> { todo!() }
215//! fn open_snapshot(path: &Path) -> Result<Snapshot, Error> {
216//! let header = read_header(path)?;
217//!
218//! if !check_version(header.version).is_compatible() {
219//! return Err(Error::Format(format!(
220//! "Incompatible version {}. Supported range: [{}, {}]",
221//! header.version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION
222//! )));
223//! }
224//!
225//! // Proceed with version-aware deserialization
226//! Ok(Snapshot { header })
227//! }
228//! ```
229
230use std::fmt;
231
232/// Current format version written by this build of Hexz.
233///
234/// This constant defines the format version number written to the `version` field
235/// of new snapshot headers. It is incremented when the on-disk format changes in
236/// a way that requires readers to adapt their parsing logic.
237///
238/// # Incrementing Policy
239///
240/// Increment `CURRENT_VERSION` when:
241/// - Header fields are added/removed/reordered
242/// - Index structure changes (e.g., new page entry fields)
243/// - Serialization format changes (e.g., switch from bincode to another codec)
244/// - Block metadata semantics change
245///
246/// Do NOT increment for:
247/// - New compression algorithms (use header.compression field)
248/// - New encryption modes (use header.encryption field)
249/// - Performance optimizations that don't affect format
250///
251/// # Current Version
252///
253/// **Version 1** (initial release):
254/// - Two-level index (master index + pages)
255/// - bincode serialization
256/// - LZ4/Zstd compression support
257/// - Optional AES-256-GCM encryption
258/// - Thin provisioning via parent snapshots
259/// - Dual streams (disk + memory)
260pub const CURRENT_VERSION: u32 = 1;
261
262/// Minimum format version readable by this build.
263///
264/// Snapshots with `version < MIN_SUPPORTED_VERSION` are rejected with an
265/// [`VersionCompatibility::Incompatible`] error. Users must migrate such
266/// snapshots using `hexz-migrate upgrade` before reading.
267///
268/// # Rationale
269///
270/// Maintaining backward compatibility indefinitely is untenable as format
271/// complexity grows. This constant defines the "support horizon" for old
272/// snapshots. When incrementing, ensure migration tooling exists.
273///
274/// # Current Policy
275///
276/// - **Hexz 0.1.x - 0.9.x**: Support version 1 only
277/// - **Hexz 1.0+**: May drop support for version 1 (TBD based on adoption)
278pub const MIN_SUPPORTED_VERSION: u32 = 1;
279
280/// Maximum format version readable by this build.
281///
282/// Snapshots with `version > MAX_SUPPORTED_VERSION` are rejected unless
283/// degraded mode is enabled (not yet implemented). This prevents crashes
284/// when reading snapshots created by future Hexz versions.
285///
286/// # Forward Compatibility
287///
288/// Currently, Hexz uses strict versioning: any version mismatch results
289/// in incompatibility. Future releases may implement graceful degradation
290/// for minor version increments (e.g., ignore unknown header fields).
291///
292/// # Upgrade Path
293///
294/// If you encounter a snapshot with `version > MAX_SUPPORTED_VERSION`,
295/// upgrade Hexz to a newer release that supports that version.
296pub const MAX_SUPPORTED_VERSION: u32 = 1;
297
298/// Result of snapshot version compatibility analysis.
299///
300/// Returned by [`check_version`] to indicate whether a snapshot with a given
301/// format version can be read by this build of Hexz. This enum enables
302/// graceful handling of version mismatches with appropriate error messages.
303///
304/// # Variants
305///
306/// - **Full**: Version is within [`MIN_SUPPORTED_VERSION`]..=[`MAX_SUPPORTED_VERSION`]
307/// and all features are supported. Proceed with normal operations.
308///
309/// - **Degraded**: Version is newer than [`MAX_SUPPORTED_VERSION`] but forward
310/// compatibility rules allow partial reads with warnings. Unsupported features
311/// are ignored or substituted with defaults. (Not yet implemented; currently
312/// treated as Incompatible.)
313///
314/// - **Incompatible**: Version is outside the supported range and cannot be read.
315/// The user must upgrade Hexz (for newer snapshots) or migrate the snapshot
316/// (for older snapshots).
317///
318/// # Examples
319///
320/// ```
321/// use hexz_core::format::version::{check_version, VersionCompatibility};
322///
323/// let result = check_version(1);
324/// assert_eq!(result, VersionCompatibility::Full);
325///
326/// let result = check_version(999);
327/// assert_eq!(result, VersionCompatibility::Incompatible);
328/// assert!(!result.is_compatible());
329/// ```
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum VersionCompatibility {
332 /// Fully supported version with all features available.
333 Full,
334 /// Newer version with partial support (warnings emitted, some features unavailable).
335 ///
336 /// This variant is reserved for future forward compatibility modes. Currently,
337 /// all versions outside the supported range are marked as Incompatible.
338 Degraded,
339 /// Incompatible version that cannot be read (too old or too new).
340 Incompatible,
341}
342
343impl VersionCompatibility {
344 /// Tests whether the snapshot can be read with this compatibility status.
345 ///
346 /// Returns `true` for [`Full`](VersionCompatibility::Full) and
347 /// [`Degraded`](VersionCompatibility::Degraded) compatibility, indicating
348 /// that read operations can proceed (possibly with warnings). Returns `false`
349 /// for [`Incompatible`](VersionCompatibility::Incompatible), indicating that
350 /// the snapshot must be rejected.
351 ///
352 /// # Returns
353 ///
354 /// - `true`: Snapshot can be opened (possibly with limited functionality)
355 /// - `false`: Snapshot cannot be opened (hard error)
356 ///
357 /// # Examples
358 ///
359 /// ```
360 /// use hexz_core::format::version::{check_version, VersionCompatibility};
361 ///
362 /// let compat = check_version(1);
363 /// if compat.is_compatible() {
364 /// println!("Snapshot can be read");
365 /// } else {
366 /// eprintln!("Snapshot is incompatible");
367 /// }
368 /// ```
369 pub fn is_compatible(&self) -> bool {
370 match self {
371 VersionCompatibility::Full | VersionCompatibility::Degraded => true,
372 VersionCompatibility::Incompatible => false,
373 }
374 }
375}
376
377/// Determines compatibility status of a snapshot format version.
378///
379/// This function implements the version negotiation logic by comparing the
380/// provided version number against the supported range defined by
381/// [`MIN_SUPPORTED_VERSION`] and [`MAX_SUPPORTED_VERSION`].
382///
383/// # Parameters
384///
385/// - `version`: The format version number read from a snapshot header
386///
387/// # Returns
388///
389/// A [`VersionCompatibility`] value indicating whether the snapshot can be read:
390///
391/// - [`Full`](VersionCompatibility::Full): Version is within supported range
392/// - [`Incompatible`](VersionCompatibility::Incompatible): Version is too old or too new
393/// - [`Degraded`](VersionCompatibility::Degraded): Currently unused (reserved for future)
394///
395/// # Version Ranges
396///
397/// | Condition | Result | Reason |
398/// |-----------|--------|--------|
399/// | `version < MIN_SUPPORTED_VERSION` | Incompatible | Too old, needs migration |
400/// | `MIN_SUPPORTED_VERSION <= version <= MAX_SUPPORTED_VERSION` | Full | Within supported range |
401/// | `version > MAX_SUPPORTED_VERSION` | Incompatible | Too new, upgrade Hexz |
402///
403/// # Future Extensions
404///
405/// In future versions, this function may return `Degraded` for snapshots with
406/// minor version mismatches, enabling partial reads with warnings. For example:
407///
408/// ```rust,no_run
409/// # use hexz_core::format::version::{VersionCompatibility, CURRENT_VERSION};
410/// # struct V { major: u32, minor: u32 }
411/// # let version = V { major: 1, minor: 1 };
412/// # let current_version = V { major: 1, minor: 0 };
413/// // Hypothetical future behavior with major.minor versioning
414/// let _compat = if version.major == current_version.major && version.minor > current_version.minor {
415/// VersionCompatibility::Degraded // Newer minor version, try best-effort read
416/// } else {
417/// VersionCompatibility::Full
418/// };
419/// ```
420///
421/// # Performance
422///
423/// This function performs two integer comparisons and has negligible overhead
424/// (~100ns). It is called once per snapshot open operation and does not affect
425/// hot path performance.
426///
427/// # Examples
428///
429/// ```
430/// use hexz_core::format::version::{
431/// check_version, VersionCompatibility,
432/// MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION
433/// };
434///
435/// // Version within supported range
436/// let compat = check_version(1);
437/// assert_eq!(compat, VersionCompatibility::Full);
438///
439/// // Version too old (hypothetical example if MIN_SUPPORTED_VERSION > 1)
440/// let compat = check_version(0);
441/// assert_eq!(compat, VersionCompatibility::Incompatible);
442///
443/// // Version too new
444/// let compat = check_version(MAX_SUPPORTED_VERSION + 1);
445/// assert_eq!(compat, VersionCompatibility::Incompatible);
446/// ```
447///
448/// ## Error Handling
449///
450/// ```rust,no_run
451/// # use hexz_core::format::version::{check_version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION};
452/// # use hexz_common::{Error, Result};
453/// # use hexz_core::format::header::Header;
454/// # struct Snapshot;
455/// # impl Snapshot { fn new(_: &Header) -> Self { Snapshot } }
456/// fn open_snapshot(header: &Header) -> Result<Snapshot> {
457/// let compat = check_version(header.version);
458/// if !compat.is_compatible() {
459/// return Err(Error::Format(format!(
460/// "Incompatible version {}. Supported range: [{}, {}]",
461/// header.version, MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION
462/// )));
463/// }
464/// // Proceed with read operations
465/// Ok(Snapshot::new(header))
466/// }
467/// ```
468pub fn check_version(version: u32) -> VersionCompatibility {
469 if version < MIN_SUPPORTED_VERSION {
470 VersionCompatibility::Incompatible
471 } else if version > MAX_SUPPORTED_VERSION {
472 // For now, strict versioning. Future: Check major/minor for degraded mode.
473 VersionCompatibility::Incompatible
474 } else {
475 VersionCompatibility::Full
476 }
477}
478
479/// Generates a user-facing message describing version compatibility status.
480///
481/// This function produces human-readable error messages and recommendations
482/// for handling version mismatches. The messages are designed to be displayed
483/// directly to end users in error logs or CLI output.
484///
485/// # Parameters
486///
487/// - `version`: The format version number from a snapshot header
488///
489/// # Returns
490///
491/// A `String` containing:
492/// - **Full compatibility**: Confirmation message
493/// - **Degraded compatibility**: Warning about potential feature limitations
494/// - **Incompatibility**: Error message with actionable remediation steps
495///
496/// # Message Content
497///
498/// ## Fully Supported Version
499///
500/// ```text
501/// "Version 1 is fully supported."
502/// ```
503///
504/// ## Too Old (version < MIN_SUPPORTED_VERSION)
505///
506/// ```text
507/// "Version 0 is too old (min supported: 1). Please upgrade the snapshot."
508/// ```
509///
510/// Remediation: Use `hexz-migrate upgrade` to convert the snapshot.
511///
512/// ## Too New (version > MAX_SUPPORTED_VERSION)
513///
514/// ```text
515/// "Version 2 is too new (max supported: 1). Please upgrade Hexz."
516/// ```
517///
518/// Remediation: Install a newer version of the Hexz toolchain.
519///
520/// ## Degraded Compatibility (Future)
521///
522/// ```text
523/// "Version 2 is newer than supported (1), features may be missing."
524/// ```
525///
526/// Remediation: Upgrade Hexz for full feature support, or proceed with warnings.
527///
528/// # Usage in Error Handling
529///
530/// This function is typically called when displaying compatibility errors to users:
531///
532/// ```rust,no_run
533/// # use hexz_core::format::version::{check_version, compatibility_message};
534/// # use hexz_core::format::header::Header;
535/// fn validate_snapshot(header: &Header) -> Result<(), String> {
536/// let compat = check_version(header.version);
537/// if !compat.is_compatible() {
538/// return Err(compatibility_message(header.version));
539/// }
540/// Ok(())
541/// }
542/// ```
543///
544/// # Examples
545///
546/// ```
547/// use hexz_core::format::version::{compatibility_message, MAX_SUPPORTED_VERSION};
548///
549/// // Supported version
550/// let msg = compatibility_message(1);
551/// assert!(msg.contains("fully supported"));
552///
553/// // Version too new
554/// let msg = compatibility_message(MAX_SUPPORTED_VERSION + 1);
555/// assert!(msg.contains("too new"));
556/// assert!(msg.contains("upgrade Hexz"));
557///
558/// // Version too old (hypothetical if MIN_SUPPORTED_VERSION > 1)
559/// let msg = compatibility_message(0);
560/// assert!(msg.contains("too old"));
561/// assert!(msg.contains("upgrade the snapshot"));
562/// ```
563///
564/// ## CLI Integration
565///
566/// ```bash
567/// $ hexz open old_snapshot.hxz
568/// Error: Version 0 is too old (min supported: 1). Please upgrade the snapshot.
569///
570/// Run: hexz-migrate upgrade old_snapshot.hxz new_snapshot.hxz
571/// ```
572pub fn compatibility_message(version: u32) -> String {
573 match check_version(version) {
574 VersionCompatibility::Full => format!("Version {} is fully supported.", version),
575 VersionCompatibility::Degraded => format!(
576 "Version {} is newer than supported ({}), features may be missing.",
577 version, MAX_SUPPORTED_VERSION
578 ),
579 VersionCompatibility::Incompatible => {
580 if version < MIN_SUPPORTED_VERSION {
581 format!(
582 "Version {} is too old (min supported: {}). Please upgrade the snapshot.",
583 version, MIN_SUPPORTED_VERSION
584 )
585 } else {
586 format!(
587 "Version {} is too new (max supported: {}). Please upgrade Hexz.",
588 version, MAX_SUPPORTED_VERSION
589 )
590 }
591 }
592 }
593}
594
595impl fmt::Display for VersionCompatibility {
596 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
597 match self {
598 VersionCompatibility::Full => write!(f, "full"),
599 VersionCompatibility::Degraded => write!(f, "degraded"),
600 VersionCompatibility::Incompatible => write!(f, "incompatible"),
601 }
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 #[allow(clippy::assertions_on_constants)]
611 fn test_version_constants_are_consistent() {
612 // MIN_SUPPORTED_VERSION must be <= MAX_SUPPORTED_VERSION
613 assert!(
614 MIN_SUPPORTED_VERSION <= MAX_SUPPORTED_VERSION,
615 "MIN_SUPPORTED_VERSION ({}) must be <= MAX_SUPPORTED_VERSION ({})",
616 MIN_SUPPORTED_VERSION,
617 MAX_SUPPORTED_VERSION
618 );
619
620 // CURRENT_VERSION must be within supported range
621 assert!(
622 CURRENT_VERSION >= MIN_SUPPORTED_VERSION,
623 "CURRENT_VERSION ({}) must be >= MIN_SUPPORTED_VERSION ({})",
624 CURRENT_VERSION,
625 MIN_SUPPORTED_VERSION
626 );
627 assert!(
628 CURRENT_VERSION <= MAX_SUPPORTED_VERSION,
629 "CURRENT_VERSION ({}) must be <= MAX_SUPPORTED_VERSION ({})",
630 CURRENT_VERSION,
631 MAX_SUPPORTED_VERSION
632 );
633 }
634
635 #[test]
636 fn test_check_version_current_is_fully_supported() {
637 let compat = check_version(CURRENT_VERSION);
638 assert_eq!(compat, VersionCompatibility::Full);
639 assert!(compat.is_compatible());
640 }
641
642 #[test]
643 fn test_check_version_within_range_is_full() {
644 // Test all versions in the supported range
645 for version in MIN_SUPPORTED_VERSION..=MAX_SUPPORTED_VERSION {
646 let compat = check_version(version);
647 assert_eq!(
648 compat,
649 VersionCompatibility::Full,
650 "Version {} should be fully compatible",
651 version
652 );
653 assert!(compat.is_compatible());
654 }
655 }
656
657 #[test]
658 fn test_check_version_too_old_is_incompatible() {
659 if MIN_SUPPORTED_VERSION > 0 {
660 let old_version = MIN_SUPPORTED_VERSION - 1;
661 let compat = check_version(old_version);
662 assert_eq!(compat, VersionCompatibility::Incompatible);
663 assert!(!compat.is_compatible());
664 }
665
666 // Always test version 0 as too old
667 let compat = check_version(0);
668 assert_eq!(compat, VersionCompatibility::Incompatible);
669 assert!(!compat.is_compatible());
670 }
671
672 #[test]
673 fn test_check_version_too_new_is_incompatible() {
674 let new_version = MAX_SUPPORTED_VERSION + 1;
675 let compat = check_version(new_version);
676 assert_eq!(compat, VersionCompatibility::Incompatible);
677 assert!(!compat.is_compatible());
678
679 // Test very high version number
680 let compat = check_version(9999);
681 assert_eq!(compat, VersionCompatibility::Incompatible);
682 assert!(!compat.is_compatible());
683 }
684
685 #[test]
686 fn test_version_compatibility_is_compatible() {
687 // Full is compatible
688 assert!(VersionCompatibility::Full.is_compatible());
689
690 // Degraded is compatible (with warnings)
691 assert!(VersionCompatibility::Degraded.is_compatible());
692
693 // Incompatible is not compatible
694 assert!(!VersionCompatibility::Incompatible.is_compatible());
695 }
696
697 #[test]
698 fn test_version_compatibility_display() {
699 assert_eq!(format!("{}", VersionCompatibility::Full), "full");
700 assert_eq!(format!("{}", VersionCompatibility::Degraded), "degraded");
701 assert_eq!(
702 format!("{}", VersionCompatibility::Incompatible),
703 "incompatible"
704 );
705 }
706
707 #[test]
708 fn test_compatibility_message_for_supported_version() {
709 let msg = compatibility_message(CURRENT_VERSION);
710 assert!(msg.contains("fully supported"), "Message: {}", msg);
711 assert!(
712 msg.contains(&CURRENT_VERSION.to_string()),
713 "Message: {}",
714 msg
715 );
716 }
717
718 #[test]
719 fn test_compatibility_message_for_too_old_version() {
720 if MIN_SUPPORTED_VERSION > 0 {
721 let old_version = MIN_SUPPORTED_VERSION - 1;
722 let msg = compatibility_message(old_version);
723 assert!(msg.contains("too old"), "Message: {}", msg);
724 assert!(
725 msg.contains(&MIN_SUPPORTED_VERSION.to_string()),
726 "Message: {}",
727 msg
728 );
729 assert!(msg.contains("upgrade the snapshot"), "Message: {}", msg);
730 }
731 }
732
733 #[test]
734 fn test_compatibility_message_for_too_new_version() {
735 let new_version = MAX_SUPPORTED_VERSION + 1;
736 let msg = compatibility_message(new_version);
737 assert!(msg.contains("too new"), "Message: {}", msg);
738 assert!(
739 msg.contains(&MAX_SUPPORTED_VERSION.to_string()),
740 "Message: {}",
741 msg
742 );
743 assert!(msg.contains("upgrade Hexz"), "Message: {}", msg);
744 }
745
746 #[test]
747 fn test_compatibility_message_for_degraded() {
748 // Since we don't currently return Degraded, we can't directly test it
749 // But we can test the message format by checking what would happen
750 let msg = match VersionCompatibility::Degraded {
751 VersionCompatibility::Degraded => format!(
752 "Version {} is newer than supported ({}), features may be missing.",
753 99, MAX_SUPPORTED_VERSION
754 ),
755 _ => String::new(),
756 };
757 assert!(msg.contains("newer than supported"));
758 assert!(msg.contains("features may be missing"));
759 }
760
761 #[test]
762 fn test_version_compatibility_enum_properties() {
763 // Test Debug trait
764 let full = VersionCompatibility::Full;
765 assert!(format!("{:?}", full).contains("Full"));
766
767 // Test Clone and Copy
768 let degraded = VersionCompatibility::Degraded;
769 let degraded_copy = degraded;
770 let degraded_clone = degraded;
771 assert_eq!(degraded, degraded_copy);
772 assert_eq!(degraded, degraded_clone);
773
774 // Test PartialEq and Eq
775 assert_eq!(VersionCompatibility::Full, VersionCompatibility::Full);
776 assert_ne!(
777 VersionCompatibility::Full,
778 VersionCompatibility::Incompatible
779 );
780 }
781
782 #[test]
783 fn test_check_version_boundary_conditions() {
784 // Test MIN_SUPPORTED_VERSION boundary
785 let compat = check_version(MIN_SUPPORTED_VERSION);
786 assert_eq!(compat, VersionCompatibility::Full);
787
788 // Test MAX_SUPPORTED_VERSION boundary
789 let compat = check_version(MAX_SUPPORTED_VERSION);
790 assert_eq!(compat, VersionCompatibility::Full);
791
792 // Test just below MIN
793 if MIN_SUPPORTED_VERSION > 0 {
794 let compat = check_version(MIN_SUPPORTED_VERSION - 1);
795 assert_eq!(compat, VersionCompatibility::Incompatible);
796 }
797
798 // Test just above MAX
799 let compat = check_version(MAX_SUPPORTED_VERSION + 1);
800 assert_eq!(compat, VersionCompatibility::Incompatible);
801 }
802
803 #[test]
804 fn test_multiple_version_checks() {
805 // Verify check_version is consistent across multiple calls
806 let version = CURRENT_VERSION;
807 let result1 = check_version(version);
808 let result2 = check_version(version);
809 assert_eq!(result1, result2);
810 }
811
812 #[test]
813 fn test_compatibility_message_contains_version_number() {
814 // Test that messages include the actual version number
815 for version in [0u32, 1, 2, 99, 999] {
816 let msg = compatibility_message(version);
817 assert!(
818 msg.contains(&version.to_string()),
819 "Message for version {} should contain the version number: {}",
820 version,
821 msg
822 );
823 }
824 }
825
826 #[test]
827 #[allow(clippy::assertions_on_constants)]
828 fn test_version_constants_are_reasonable() {
829 // Ensure version numbers are in a reasonable range
830 assert!(CURRENT_VERSION > 0, "CURRENT_VERSION must be positive");
831 assert!(
832 CURRENT_VERSION < 1000,
833 "CURRENT_VERSION seems unreasonably high"
834 );
835 assert!(
836 MIN_SUPPORTED_VERSION > 0,
837 "MIN_SUPPORTED_VERSION must be positive"
838 );
839 assert!(
840 MAX_SUPPORTED_VERSION < 1000,
841 "MAX_SUPPORTED_VERSION seems unreasonably high"
842 );
843 }
844}