hexz_core/format/version.rs
1//! Format version management and compatibility checking.
2//!
3//! This module defines the versioning strategy for Hexz archive 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 archives 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 Archives)
31//!
32//! Hexz readers maintain compatibility with archives created by older software:
33//!
34//! - **`MIN_SUPPORTED_VERSION`**: Oldest format version we can read (currently 1)
35//! - **Upgrade Path**: Archives older than `MIN_SUPPORTED_VERSION` must be migrated
36//! using the `hexz-migrate` tool (see Migration section below)
37//!
38//! ## Forward Compatibility (Reading New Archives)
39//!
40//! Hexz readers handle archives 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**: Archives with `version > MAX_SUPPORTED_VERSION` are
46//! rejected with an actionable error message
47//!
48//! # Version Negotiation Workflow
49//!
50//! When opening a archive 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::Archive { todo!() }
123//! #[test]
124//! fn test_read_v2_archive() {
125//! let archive = load_fixture("testdata/v2_archive.hxz");
126//! assert_eq!(archive.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 archives between format versions:
138//!
139//! ```bash
140//! # Upgrade old archive 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 archive with reader for version N
153//! 2. Create destination archive 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 archive open operation:
167//!
168//! - **Overhead**: Negligible (~100ns for integer comparison)
169//! - **Caching**: Version is cached in the archive reader 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 archive_version = 1;
180//! let compat = check_version(archive_version);
181//!
182//! match compat {
183//! VersionCompatibility::Full => {
184//! println!("Archive is fully compatible");
185//! }
186//! VersionCompatibility::Incompatible => {
187//! eprintln!("Cannot read archive (incompatible version)");
188//! }
189//! VersionCompatibility::Degraded => {
190//! println!("Archive 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 Archive { header: Header }
214//! # fn read_header(_: &Path) -> Result<Header, Error> { todo!() }
215//! fn open_archive(path: &Path) -> Result<Archive, 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(Archive { 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 archive 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 archives
259/// - Dual streams (disk + memory)
260pub const CURRENT_VERSION: u32 = 1;
261
262/// Minimum format version readable by this build.
263///
264/// Archives with `version < MIN_SUPPORTED_VERSION` are rejected with an
265/// [`VersionCompatibility::Incompatible`] error. Users must migrate such
266/// archives 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/// archives. 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/// Archives with `version > MAX_SUPPORTED_VERSION` are rejected unless
283/// degraded mode is enabled (not yet implemented). This prevents crashes
284/// when reading archives 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 archive 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 archive version compatibility analysis.
299///
300/// Returned by [`check_version`] to indicate whether a archive 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 archives) or migrate the archive
316/// (for older archives).
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 archive 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 archive must be rejected.
351 ///
352 /// # Returns
353 ///
354 /// - `true`: Archive can be opened (possibly with limited functionality)
355 /// - `false`: Archive 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!("Archive can be read");
365 /// } else {
366 /// eprintln!("Archive is incompatible");
367 /// }
368 /// ```
369 pub const fn is_compatible(&self) -> bool {
370 match self {
371 Self::Full | Self::Degraded => true,
372 Self::Incompatible => false,
373 }
374 }
375}
376
377/// Determines compatibility status of a archive 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 archive header
386///
387/// # Returns
388///
389/// A [`VersionCompatibility`] value indicating whether the archive 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 archives 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 archive 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 Archive;
455/// # impl Archive { fn new(_: &Header) -> Self { Archive } }
456/// fn open_archive(header: &Header) -> Result<Archive> {
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(Archive::new(header))
466/// }
467/// ```
468pub const 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 archive 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 archive."
508/// ```
509///
510/// Remediation: Use `hexz-migrate upgrade` to convert the archive.
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_archive(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 archive"));
562/// ```
563///
564/// ## CLI Integration
565///
566/// ```bash
567/// $ hexz open old_archive.hxz
568/// Error: Version 0 is too old (min supported: 1). Please upgrade the archive.
569///
570/// Run: hexz-migrate upgrade old_archive.hxz new_archive.hxz
571/// ```
572pub fn compatibility_message(version: u32) -> String {
573 match check_version(version) {
574 VersionCompatibility::Full => format!("Version {version} is fully supported."),
575 VersionCompatibility::Degraded => format!(
576 "Version {version} is newer than supported ({MAX_SUPPORTED_VERSION}), features may be missing."
577 ),
578 VersionCompatibility::Incompatible => {
579 if version < MIN_SUPPORTED_VERSION {
580 format!(
581 "Version {version} is too old (min supported: {MIN_SUPPORTED_VERSION}). Please upgrade the archive."
582 )
583 } else {
584 format!(
585 "Version {version} is too new (max supported: {MAX_SUPPORTED_VERSION}). Please upgrade Hexz."
586 )
587 }
588 }
589 }
590}
591
592impl fmt::Display for VersionCompatibility {
593 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594 match self {
595 Self::Full => write!(f, "full"),
596 Self::Degraded => write!(f, "degraded"),
597 Self::Incompatible => write!(f, "incompatible"),
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 #[allow(clippy::assertions_on_constants)]
608 fn test_version_constants_are_consistent() {
609 // MIN_SUPPORTED_VERSION must be <= MAX_SUPPORTED_VERSION
610 assert!(
611 MIN_SUPPORTED_VERSION <= MAX_SUPPORTED_VERSION,
612 "MIN_SUPPORTED_VERSION ({MIN_SUPPORTED_VERSION}) must be <= MAX_SUPPORTED_VERSION ({MAX_SUPPORTED_VERSION})"
613 );
614
615 // CURRENT_VERSION must be within supported range
616 assert!(
617 CURRENT_VERSION >= MIN_SUPPORTED_VERSION,
618 "CURRENT_VERSION ({CURRENT_VERSION}) must be >= MIN_SUPPORTED_VERSION ({MIN_SUPPORTED_VERSION})"
619 );
620 assert!(
621 CURRENT_VERSION <= MAX_SUPPORTED_VERSION,
622 "CURRENT_VERSION ({CURRENT_VERSION}) must be <= MAX_SUPPORTED_VERSION ({MAX_SUPPORTED_VERSION})"
623 );
624 }
625
626 #[test]
627 fn test_check_version_current_is_fully_supported() {
628 let compat = check_version(CURRENT_VERSION);
629 assert_eq!(compat, VersionCompatibility::Full);
630 assert!(compat.is_compatible());
631 }
632
633 #[test]
634 fn test_check_version_within_range_is_full() {
635 // Test all versions in the supported range
636 for version in MIN_SUPPORTED_VERSION..=MAX_SUPPORTED_VERSION {
637 let compat = check_version(version);
638 assert_eq!(
639 compat,
640 VersionCompatibility::Full,
641 "Version {version} should be fully compatible"
642 );
643 assert!(compat.is_compatible());
644 }
645 }
646
647 #[test]
648 fn test_check_version_too_old_is_incompatible() {
649 if MIN_SUPPORTED_VERSION > 0 {
650 let old_version = MIN_SUPPORTED_VERSION - 1;
651 let compat = check_version(old_version);
652 assert_eq!(compat, VersionCompatibility::Incompatible);
653 assert!(!compat.is_compatible());
654 }
655
656 // Always test version 0 as too old
657 let compat = check_version(0);
658 assert_eq!(compat, VersionCompatibility::Incompatible);
659 assert!(!compat.is_compatible());
660 }
661
662 #[test]
663 fn test_check_version_too_new_is_incompatible() {
664 let new_version = MAX_SUPPORTED_VERSION + 1;
665 let compat = check_version(new_version);
666 assert_eq!(compat, VersionCompatibility::Incompatible);
667 assert!(!compat.is_compatible());
668
669 // Test very high version number
670 let compat = check_version(9999);
671 assert_eq!(compat, VersionCompatibility::Incompatible);
672 assert!(!compat.is_compatible());
673 }
674
675 #[test]
676 fn test_version_compatibility_is_compatible() {
677 // Full is compatible
678 assert!(VersionCompatibility::Full.is_compatible());
679
680 // Degraded is compatible (with warnings)
681 assert!(VersionCompatibility::Degraded.is_compatible());
682
683 // Incompatible is not compatible
684 assert!(!VersionCompatibility::Incompatible.is_compatible());
685 }
686
687 #[test]
688 fn test_version_compatibility_display() {
689 assert_eq!(format!("{}", VersionCompatibility::Full), "full");
690 assert_eq!(format!("{}", VersionCompatibility::Degraded), "degraded");
691 assert_eq!(
692 format!("{}", VersionCompatibility::Incompatible),
693 "incompatible"
694 );
695 }
696
697 #[test]
698 fn test_compatibility_message_for_supported_version() {
699 let msg = compatibility_message(CURRENT_VERSION);
700 assert!(msg.contains("fully supported"), "Message: {msg}");
701 assert!(msg.contains(&CURRENT_VERSION.to_string()), "Message: {msg}");
702 }
703
704 #[test]
705 fn test_compatibility_message_for_too_old_version() {
706 if MIN_SUPPORTED_VERSION > 0 {
707 let old_version = MIN_SUPPORTED_VERSION - 1;
708 let msg = compatibility_message(old_version);
709 assert!(msg.contains("too old"), "Message: {msg}");
710 assert!(
711 msg.contains(&MIN_SUPPORTED_VERSION.to_string()),
712 "Message: {msg}"
713 );
714 assert!(msg.contains("upgrade the archive"), "Message: {msg}");
715 }
716 }
717
718 #[test]
719 fn test_compatibility_message_for_too_new_version() {
720 let new_version = MAX_SUPPORTED_VERSION + 1;
721 let msg = compatibility_message(new_version);
722 assert!(msg.contains("too new"), "Message: {msg}");
723 assert!(
724 msg.contains(&MAX_SUPPORTED_VERSION.to_string()),
725 "Message: {msg}"
726 );
727 assert!(msg.contains("upgrade Hexz"), "Message: {msg}");
728 }
729
730 #[test]
731 fn test_compatibility_message_for_degraded() {
732 // Since we don't currently return Degraded, we can't directly test it
733 // But we can test the message format by checking what would happen
734 let msg = match VersionCompatibility::Degraded {
735 VersionCompatibility::Degraded => format!(
736 "Version {} is newer than supported ({}), features may be missing.",
737 99, MAX_SUPPORTED_VERSION
738 ),
739 _ => String::new(),
740 };
741 assert!(msg.contains("newer than supported"));
742 assert!(msg.contains("features may be missing"));
743 }
744
745 #[test]
746 fn test_version_compatibility_enum_properties() {
747 // Test Debug trait
748 let full = VersionCompatibility::Full;
749 assert!(format!("{full:?}").contains("Full"));
750
751 // Test Clone and Copy
752 let degraded = VersionCompatibility::Degraded;
753 let degraded_copy = degraded;
754 let degraded_clone = degraded;
755 assert_eq!(degraded, degraded_copy);
756 assert_eq!(degraded, degraded_clone);
757
758 // Test PartialEq and Eq
759 assert_eq!(VersionCompatibility::Full, VersionCompatibility::Full);
760 assert_ne!(
761 VersionCompatibility::Full,
762 VersionCompatibility::Incompatible
763 );
764 }
765
766 #[test]
767 fn test_check_version_boundary_conditions() {
768 // Test MIN_SUPPORTED_VERSION boundary
769 let compat = check_version(MIN_SUPPORTED_VERSION);
770 assert_eq!(compat, VersionCompatibility::Full);
771
772 // Test MAX_SUPPORTED_VERSION boundary
773 let compat = check_version(MAX_SUPPORTED_VERSION);
774 assert_eq!(compat, VersionCompatibility::Full);
775
776 // Test just below MIN
777 if MIN_SUPPORTED_VERSION > 0 {
778 let compat = check_version(MIN_SUPPORTED_VERSION - 1);
779 assert_eq!(compat, VersionCompatibility::Incompatible);
780 }
781
782 // Test just above MAX
783 let compat = check_version(MAX_SUPPORTED_VERSION + 1);
784 assert_eq!(compat, VersionCompatibility::Incompatible);
785 }
786
787 #[test]
788 fn test_multiple_version_checks() {
789 // Verify check_version is consistent across multiple calls
790 let version = CURRENT_VERSION;
791 let result1 = check_version(version);
792 let result2 = check_version(version);
793 assert_eq!(result1, result2);
794 }
795
796 #[test]
797 fn test_compatibility_message_contains_version_number() {
798 // Test that messages include the actual version number
799 for version in [0u32, 1, 2, 99, 999] {
800 let msg = compatibility_message(version);
801 assert!(
802 msg.contains(&version.to_string()),
803 "Message for version {version} should contain the version number: {msg}"
804 );
805 }
806 }
807
808 #[test]
809 #[allow(clippy::assertions_on_constants)]
810 fn test_version_constants_are_reasonable() {
811 // Ensure version numbers are in a reasonable range
812 assert!(CURRENT_VERSION > 0, "CURRENT_VERSION must be positive");
813 assert!(
814 CURRENT_VERSION < 1000,
815 "CURRENT_VERSION seems unreasonably high"
816 );
817 assert!(
818 MIN_SUPPORTED_VERSION > 0,
819 "MIN_SUPPORTED_VERSION must be positive"
820 );
821 assert!(
822 MAX_SUPPORTED_VERSION < 1000,
823 "MAX_SUPPORTED_VERSION seems unreasonably high"
824 );
825 }
826}