Skip to main content

noxu_engine/
verify.rs

1//! Environment verification utilities.
2//!
3//! Related verification functionality.
4
5use noxu_dbi::DatabaseImpl;
6use noxu_tree::NodeRwLock as RwLock;
7use noxu_tree::Tree;
8use noxu_tree::tree::{BinStub, InNodeStub, TreeNode};
9use noxu_util::{Lsn, NULL_LSN};
10use std::collections::HashSet;
11use std::fmt;
12use std::sync::Arc;
13
14/// Result of an environment verification.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct VerifyResult {
17    /// Errors found during verification.
18    pub errors: Vec<VerifyError>,
19    /// Non-fatal warnings.
20    pub warnings: Vec<String>,
21    /// Number of databases verified.
22    pub databases_verified: u32,
23    /// Number of records verified.
24    pub records_verified: u64,
25    /// Whether the verification passed (no errors).
26    pub passed: bool,
27}
28
29impl VerifyResult {
30    /// Create a new passing result with no errors or warnings.
31    pub fn new() -> Self {
32        Self {
33            errors: Vec::new(),
34            warnings: Vec::new(),
35            databases_verified: 0,
36            records_verified: 0,
37            passed: true,
38        }
39    }
40
41    /// Create a result with errors.
42    pub fn with_errors(errors: Vec<VerifyError>) -> Self {
43        Self {
44            passed: errors.is_empty(),
45            errors,
46            warnings: Vec::new(),
47            databases_verified: 0,
48            records_verified: 0,
49        }
50    }
51
52    /// Add an error to the result.
53    pub fn add_error(&mut self, error: VerifyError) {
54        self.errors.push(error);
55        self.passed = false;
56    }
57
58    /// Add a warning to the result.
59    pub fn add_warning(&mut self, warning: String) {
60        self.warnings.push(warning);
61    }
62
63    /// Check if the verification passed.
64    pub fn is_passed(&self) -> bool {
65        self.passed
66    }
67
68    /// Get the number of errors.
69    pub fn error_count(&self) -> usize {
70        self.errors.len()
71    }
72
73    /// Get the number of warnings.
74    pub fn warning_count(&self) -> usize {
75        self.warnings.len()
76    }
77}
78
79impl Default for VerifyResult {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl fmt::Display for VerifyResult {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        writeln!(f, "Verification Result")?;
88        writeln!(f, "===================")?;
89        writeln!(
90            f,
91            "Status: {}",
92            if self.passed { "PASSED" } else { "FAILED" }
93        )?;
94        writeln!(f, "Databases verified: {}", self.databases_verified)?;
95        writeln!(f, "Records verified: {}", self.records_verified)?;
96        writeln!(f)?;
97
98        if !self.errors.is_empty() {
99            writeln!(f, "Errors ({}):", self.errors.len())?;
100            for error in &self.errors {
101                writeln!(f, "  - {}", error)?;
102            }
103            writeln!(f)?;
104        }
105
106        if !self.warnings.is_empty() {
107            writeln!(f, "Warnings ({}):", self.warnings.len())?;
108            for warning in &self.warnings {
109                writeln!(f, "  - {}", warning)?;
110            }
111            writeln!(f)?;
112        }
113
114        if self.errors.is_empty() && self.warnings.is_empty() {
115            writeln!(f, "No errors or warnings found.")?;
116        }
117
118        Ok(())
119    }
120}
121
122/// Types of verification errors.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum VerifyError {
125    /// B-tree structure error.
126    BtreeError { db_name: String, description: String },
127    /// Log file error.
128    LogError { file_number: u32, description: String },
129    /// Data inconsistency.
130    DataInconsistency { description: String },
131    /// Checksum mismatch.
132    ChecksumError { location: String, description: String },
133    /// Invalid node reference.
134    InvalidNodeReference { node_id: u64, description: String },
135    /// Database metadata error.
136    MetadataError { db_name: String, description: String },
137}
138
139impl fmt::Display for VerifyError {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            VerifyError::BtreeError { db_name, description } => {
143                write!(f, "B-tree error in '{}': {}", db_name, description)
144            }
145            VerifyError::LogError { file_number, description } => {
146                write!(
147                    f,
148                    "Log file {:08x}.ndb error: {}",
149                    file_number, description
150                )
151            }
152            VerifyError::DataInconsistency { description } => {
153                write!(f, "Data inconsistency: {}", description)
154            }
155            VerifyError::ChecksumError { location, description } => {
156                write!(f, "Checksum error at {}: {}", location, description)
157            }
158            VerifyError::InvalidNodeReference { node_id, description } => {
159                write!(
160                    f,
161                    "Invalid node reference (ID {}): {}",
162                    node_id, description
163                )
164            }
165            VerifyError::MetadataError { db_name, description } => {
166                write!(f, "Metadata error in '{}': {}", db_name, description)
167            }
168        }
169    }
170}
171
172/// Configuration for verification.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct VerifyConfig {
175    /// Whether to verify the B-tree structure.
176    pub verify_btree: bool,
177    /// Whether to verify log files.
178    pub verify_log: bool,
179    /// Whether to verify data checksums.
180    pub verify_data_checksums: bool,
181    /// Whether to repair problems found.
182    pub repair: bool,
183    /// Maximum number of errors before stopping.
184    pub max_errors: u32,
185    /// Whether to print verbose progress information.
186    pub verbose: bool,
187    /// Whether to verify only a specific database.
188    pub database_name: Option<String>,
189}
190
191impl VerifyConfig {
192    /// Create a new verification config with default settings.
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    /// Enable B-tree verification.
198    pub fn with_btree_verification(mut self, enabled: bool) -> Self {
199        self.verify_btree = enabled;
200        self
201    }
202
203    /// Enable log file verification.
204    pub fn with_log_verification(mut self, enabled: bool) -> Self {
205        self.verify_log = enabled;
206        self
207    }
208
209    /// Enable data checksum verification.
210    pub fn with_checksum_verification(mut self, enabled: bool) -> Self {
211        self.verify_data_checksums = enabled;
212        self
213    }
214
215    /// Enable repair mode.
216    pub fn with_repair(mut self, enabled: bool) -> Self {
217        self.repair = enabled;
218        self
219    }
220
221    /// Set maximum number of errors.
222    pub fn with_max_errors(mut self, max: u32) -> Self {
223        self.max_errors = max;
224        self
225    }
226
227    /// Enable verbose output.
228    pub fn with_verbose(mut self, enabled: bool) -> Self {
229        self.verbose = enabled;
230        self
231    }
232
233    /// Verify only a specific database.
234    pub fn for_database(mut self, name: String) -> Self {
235        self.database_name = Some(name);
236        self
237    }
238}
239
240impl Default for VerifyConfig {
241    fn default() -> Self {
242        VerifyConfig {
243            verify_btree: true,
244            verify_log: true,
245            verify_data_checksums: true,
246            repair: false,
247            max_errors: 100,
248            verbose: false,
249            database_name: None,
250        }
251    }
252}
253
254// ============================================================================
255// Tree structural verification helpers
256// ============================================================================
257
258/// Verifies the structural integrity of a B-tree.
259///
260/// Walks the tree from root to BIN leaves and checks:
261///
262/// 1. Each upper IN's children are accessible (non-null child references).
263/// 2. For each IN, every child's leftmost key is >= the parent key entry that
264///    routes to it (key-range containment).
265/// 3. Each BIN entry that is not known-deleted has a valid (non-NULL) LSN.
266///
267/// Returns a `VerifyResult` with any anomalies found and the count of records
268/// verified.
269///
270///
271pub fn verify_tree(
272    tree: &Tree,
273    db_name: &str,
274    config: &VerifyConfig,
275) -> VerifyResult {
276    let mut result = VerifyResult::new();
277
278    if !config.verify_btree {
279        return result;
280    }
281
282    let root = match tree.get_root() {
283        Some(r) => r,
284        None => {
285            // Empty tree is valid.
286            result.databases_verified = 1;
287            return result;
288        }
289    };
290
291    let mut records: u64 = 0;
292    verify_node(&root, None, db_name, config, &mut result, &mut records);
293    result.records_verified = records;
294    result.databases_verified = 1;
295    result
296}
297
298/// Recursively verifies a tree node.
299fn verify_node(
300    node_arc: &Arc<RwLock<TreeNode>>,
301    parent_key: Option<&[u8]>,
302    db_name: &str,
303    config: &VerifyConfig,
304    result: &mut VerifyResult,
305    records: &mut u64,
306) {
307    let guard = node_arc.read();
308
309    match &*guard {
310        TreeNode::Internal(in_node) => {
311            verify_internal_node(
312                in_node, parent_key, db_name, config, result, records,
313            );
314        }
315        TreeNode::Bottom(bin_stub) => {
316            verify_bin_stub(
317                bin_stub, parent_key, db_name, config, result, records,
318            );
319        }
320    }
321}
322
323/// Verifies an upper internal node (IN).
324///
325/// `VerifyUtils.verifyIN()`: checks that each child's first key is
326/// within the key range implied by the parent entry.
327fn verify_internal_node(
328    in_node: &InNodeStub,
329    _parent_key: Option<&[u8]>,
330    db_name: &str,
331    config: &VerifyConfig,
332    result: &mut VerifyResult,
333    records: &mut u64,
334) {
335    if in_node.entries.is_empty() {
336        // An internal node with no entries is structurally empty but not
337        // necessarily an error (can occur transiently during splits).
338        return;
339    }
340
341    // Walk each child entry.
342    for (i, entry) in in_node.entries.iter().enumerate() {
343        let child_owned = in_node.get_child(i);
344        let child_arc = match &child_owned {
345            Some(c) => c,
346            None => {
347                result.add_error(VerifyError::BtreeError {
348                    db_name: db_name.to_string(),
349                    description: format!(
350                        "IN node (id={}) entry {} has null child reference",
351                        in_node.node_id, i
352                    ),
353                });
354                if result.error_count() >= config.max_errors as usize {
355                    return;
356                }
357                continue;
358            }
359        };
360
361        // The key carried in slot 0 of an IN is the virtual -infinity key;
362        // entries at i > 0 carry the first key of that child's subtree.
363        // IN slot-0 special case.
364        let expected_parent_key: Option<&[u8]> =
365            if i == 0 { None } else { Some(entry.key.as_slice()) };
366
367        verify_node(
368            child_arc,
369            expected_parent_key,
370            db_name,
371            config,
372            result,
373            records,
374        );
375
376        if result.error_count() >= config.max_errors as usize {
377            return;
378        }
379    }
380}
381
382/// Verifies a BIN stub (leaf-level node).
383///
384/// `VerifyUtils.verifyBIN()`: checks that non-deleted slots carry
385/// valid (non-NULL) LSNs, and that the BIN's first key is >= the routing key
386/// passed from the parent.
387fn verify_bin_stub(
388    bin: &BinStub,
389    parent_key: Option<&[u8]>,
390    db_name: &str,
391    config: &VerifyConfig,
392    result: &mut VerifyResult,
393    records: &mut u64,
394) {
395    // Check that the BIN's first key is >= the routing key from the parent.
396    if let Some(pk) = parent_key
397        && !bin.entries.is_empty()
398    {
399        let first_full = bin.get_full_key(0);
400        if let Some(ref first_key) = first_full
401            && first_key.as_slice() < pk
402        {
403            result.add_error(VerifyError::BtreeError {
404                        db_name: db_name.to_string(),
405                        description: format!(
406                            "BIN (id={}) first key {:?} is less than parent routing key {:?}",
407                            bin.node_id, first_key, pk
408                        ),
409                    });
410        }
411    }
412
413    // Check each slot.
414    for (i, entry) in bin.entries.iter().enumerate() {
415        // Non-deleted entries must have a valid LSN.
416        if !entry.known_deleted && entry.lsn == NULL_LSN {
417            result.add_error(VerifyError::BtreeError {
418                db_name: db_name.to_string(),
419                description: format!(
420                    "BIN (id={}) slot {} has NULL LSN but is not known-deleted",
421                    bin.node_id, i
422                ),
423            });
424            if result.error_count() >= config.max_errors as usize {
425                return;
426            }
427        }
428
429        if !entry.known_deleted {
430            *records += 1;
431        }
432    }
433}
434
435// ============================================================================
436// Public verification entry points
437// ============================================================================
438
439// NOTE: the former standalone `verify_environment(&VerifyConfig)` and
440// `verify_database(&str, &VerifyConfig)` entry points were removed: they
441// could not perform real verification without a live `EnvironmentImpl` /
442// `DatabaseImpl` handle, so they returned a fake passing result. The real
443// entry points are `noxu_db::Environment::verify` and
444// `noxu_db::Database::verify`, which route through `verify_database_impl`
445// (below) → `verify_tree`. This mirrors `DbVerify` / `Environment.verify`,
446// which always operate on an opened environment.
447
448/// Verify a `DatabaseImpl`'s B-tree structural integrity.
449///
450/// Calls `verify_tree()` on the underlying real B-tree when one is present.
451/// Used by `Database::verify()` in `noxu-db` to bridge the crate boundary
452/// (noxu-db does not depend directly on noxu-tree).
453///
454/// Mirrors `DatabaseImpl.verify(VerifyConfig)` in— calls BtreeVerifier
455/// on the tree owned by the DatabaseImpl.
456///
457/// # Arguments
458///
459/// * `db_impl` - The database implementation to verify.
460/// * `config` - Configuration controlling what to verify.
461///
462/// # Returns
463///
464/// A `VerifyResult` with structural errors and the count of records verified.
465pub fn verify_database_impl(
466    db_impl: &DatabaseImpl,
467    config: &VerifyConfig,
468) -> VerifyResult {
469    let db_name = db_impl.get_name();
470    match db_impl.get_real_tree() {
471        Some(tree) => verify_tree(&tree, db_name, config),
472        None => {
473            // No real B-tree attached (e.g., stub / metadata DB) — treat as empty.
474            VerifyResult {
475                errors: Vec::new(),
476                warnings: Vec::new(),
477                databases_verified: 1,
478                records_verified: 0,
479                passed: true,
480            }
481        }
482    }
483}
484
485// ============================================================================
486// checkLsns: live-tree-LSN <-> UtilizationProfile overlap check (CLN-2)
487// ============================================================================
488
489/// Gather the set of LIVE LSNs referenced by a B-tree.
490///
491/// Mirrors JE's `GatherLSNs` `TreeNodeProcessor` driven by a
492/// `SortedLSNTreeWalker` in `VerifyUtils.checkLsns()`: every non-NULL child
493/// LSN reachable from the root is collected. Here we collect the LN LSNs
494/// recorded in each live (non-known-deleted) BIN slot.
495pub fn gather_tree_lsns(tree: &Tree) -> HashSet<Lsn> {
496    let mut lsns = HashSet::new();
497    if let Some(root) = tree.get_root() {
498        gather_node_lsns(&root, &mut lsns);
499    }
500    lsns
501}
502
503fn gather_node_lsns(node_arc: &Arc<RwLock<TreeNode>>, lsns: &mut HashSet<Lsn>) {
504    let guard = node_arc.read();
505    match &*guard {
506        TreeNode::Internal(in_node) => {
507            for child in in_node.resident_children() {
508                gather_node_lsns(&child, lsns);
509            }
510        }
511        TreeNode::Bottom(bin) => {
512            for entry in &bin.entries {
513                // JE GatherLSNs.processLSN skips DbLsn.NULL_LSN.
514                if !entry.known_deleted && entry.lsn != NULL_LSN {
515                    lsns.insert(entry.lsn);
516                }
517            }
518        }
519    }
520}
521
522/// Compare the live LSNs of a `DatabaseImpl`'s tree against the obsolete set
523/// recorded in the `UtilizationTracker`, adding a `DataInconsistency` error
524/// for each live LSN wrongly recorded as obsolete.
525///
526/// Faithful port of `VerifyUtils.checkLsns()`: a live tree LSN must NOT be in
527/// the obsolete set (JE: "Obsolete LSN set contains valid LSN" ->
528/// `LOG_INTEGRITY` `EnvironmentFailureException`). The disjointness test
529/// itself lives in `noxu_cleaner::check_lsns`; this function bridges the
530/// engine-side tree walk to it.
531pub fn check_lsns_against_tracker(
532    db_impl: &DatabaseImpl,
533    tracker: &noxu_cleaner::UtilizationTracker,
534    result: &mut VerifyResult,
535) {
536    let tree = match db_impl.get_real_tree() {
537        Some(t) => t,
538        None => return,
539    };
540    let live = gather_tree_lsns(&tree);
541    let check = noxu_cleaner::check_lsns(live, tracker);
542    for lsn in check.obsolete_contains_live {
543        result.add_error(VerifyError::DataInconsistency {
544            description: format!(
545                "Obsolete LSN set contains valid LSN {} in database '{}' \
546                 (VerifyUtils.checkLsns: live tree LSN recorded obsolete)",
547                lsn,
548                db_impl.get_name()
549            ),
550        });
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn test_verify_result_new() {
560        let result = VerifyResult::new();
561        assert!(result.passed);
562        assert_eq!(result.errors.len(), 0);
563        assert_eq!(result.warnings.len(), 0);
564        assert_eq!(result.databases_verified, 0);
565        assert_eq!(result.records_verified, 0);
566    }
567
568    #[test]
569    fn test_verify_result_default() {
570        let result = VerifyResult::default();
571        assert!(result.passed);
572        assert!(result.errors.is_empty());
573    }
574
575    #[test]
576    fn test_verify_result_with_errors() {
577        let errors = vec![VerifyError::BtreeError {
578            db_name: "test".to_string(),
579            description: "Invalid node".to_string(),
580        }];
581        let result = VerifyResult::with_errors(errors);
582        assert!(!result.passed);
583        assert_eq!(result.errors.len(), 1);
584    }
585
586    #[test]
587    fn test_verify_result_with_no_errors() {
588        let errors = vec![];
589        let result = VerifyResult::with_errors(errors);
590        assert!(result.passed);
591        assert_eq!(result.errors.len(), 0);
592    }
593
594    #[test]
595    fn test_add_error() {
596        let mut result = VerifyResult::new();
597        assert!(result.passed);
598
599        result.add_error(VerifyError::DataInconsistency {
600            description: "Test error".to_string(),
601        });
602
603        assert!(!result.passed);
604        assert_eq!(result.errors.len(), 1);
605    }
606
607    #[test]
608    fn test_add_warning() {
609        let mut result = VerifyResult::new();
610        result.add_warning("Test warning".to_string());
611
612        assert!(result.passed); // warnings don't affect passed status
613        assert_eq!(result.warnings.len(), 1);
614    }
615
616    #[test]
617    fn test_error_count() {
618        let mut result = VerifyResult::new();
619        assert_eq!(result.error_count(), 0);
620
621        result.add_error(VerifyError::DataInconsistency {
622            description: "Error 1".to_string(),
623        });
624        result.add_error(VerifyError::DataInconsistency {
625            description: "Error 2".to_string(),
626        });
627
628        assert_eq!(result.error_count(), 2);
629    }
630
631    #[test]
632    fn test_warning_count() {
633        let mut result = VerifyResult::new();
634        assert_eq!(result.warning_count(), 0);
635
636        result.add_warning("Warning 1".to_string());
637        result.add_warning("Warning 2".to_string());
638
639        assert_eq!(result.warning_count(), 2);
640    }
641
642    #[test]
643    fn test_is_passed() {
644        let result = VerifyResult::new();
645        assert!(result.is_passed());
646
647        let mut failed_result = VerifyResult::new();
648        failed_result.add_error(VerifyError::DataInconsistency {
649            description: "Error".to_string(),
650        });
651        assert!(!failed_result.is_passed());
652    }
653
654    #[test]
655    fn test_verify_error_btree() {
656        let error = VerifyError::BtreeError {
657            db_name: "mydb".to_string(),
658            description: "Invalid child reference".to_string(),
659        };
660        let s = format!("{}", error);
661        assert!(s.contains("B-tree error"));
662        assert!(s.contains("mydb"));
663        assert!(s.contains("Invalid child reference"));
664    }
665
666    #[test]
667    fn test_verify_error_log() {
668        let error = VerifyError::LogError {
669            file_number: 42,
670            description: "Corrupted entry".to_string(),
671        };
672        let s = format!("{}", error);
673        assert!(s.contains("Log file"));
674        assert!(s.contains("0000002a.ndb"));
675        assert!(s.contains("Corrupted entry"));
676    }
677
678    #[test]
679    fn test_verify_error_data_inconsistency() {
680        let error = VerifyError::DataInconsistency {
681            description: "Mismatched LSN".to_string(),
682        };
683        let s = format!("{}", error);
684        assert!(s.contains("Data inconsistency"));
685        assert!(s.contains("Mismatched LSN"));
686    }
687
688    #[test]
689    fn test_verify_error_checksum() {
690        let error = VerifyError::ChecksumError {
691            location: "file 10, offset 1024".to_string(),
692            description: "CRC mismatch".to_string(),
693        };
694        let s = format!("{}", error);
695        assert!(s.contains("Checksum error"));
696        assert!(s.contains("file 10, offset 1024"));
697        assert!(s.contains("CRC mismatch"));
698    }
699
700    #[test]
701    fn test_verify_error_invalid_node_reference() {
702        let error = VerifyError::InvalidNodeReference {
703            node_id: 12345,
704            description: "Node not found".to_string(),
705        };
706        let s = format!("{}", error);
707        assert!(s.contains("Invalid node reference"));
708        assert!(s.contains("12345"));
709        assert!(s.contains("Node not found"));
710    }
711
712    #[test]
713    fn test_verify_error_metadata() {
714        let error = VerifyError::MetadataError {
715            db_name: "testdb".to_string(),
716            description: "Invalid format version".to_string(),
717        };
718        let s = format!("{}", error);
719        assert!(s.contains("Metadata error"));
720        assert!(s.contains("testdb"));
721        assert!(s.contains("Invalid format version"));
722    }
723
724    #[test]
725    fn test_verify_config_default() {
726        let config = VerifyConfig::default();
727        assert!(config.verify_btree);
728        assert!(config.verify_log);
729        assert!(config.verify_data_checksums);
730        assert!(!config.repair);
731        assert_eq!(config.max_errors, 100);
732        assert!(!config.verbose);
733        assert!(config.database_name.is_none());
734    }
735
736    #[test]
737    fn test_verify_config_new() {
738        let config = VerifyConfig::new();
739        assert_eq!(config, VerifyConfig::default());
740    }
741
742    #[test]
743    fn test_verify_config_builder() {
744        let config = VerifyConfig::new()
745            .with_btree_verification(false)
746            .with_log_verification(true)
747            .with_checksum_verification(false)
748            .with_repair(true)
749            .with_max_errors(50)
750            .with_verbose(true)
751            .for_database("mydb".to_string());
752
753        assert!(!config.verify_btree);
754        assert!(config.verify_log);
755        assert!(!config.verify_data_checksums);
756        assert!(config.repair);
757        assert_eq!(config.max_errors, 50);
758        assert!(config.verbose);
759        assert_eq!(config.database_name, Some("mydb".to_string()));
760    }
761
762    #[test]
763    fn test_verify_result_display_passed() {
764        let result = VerifyResult {
765            errors: Vec::new(),
766            warnings: Vec::new(),
767            databases_verified: 5,
768            records_verified: 1000,
769            passed: true,
770        };
771
772        let output = format!("{}", result);
773        assert!(output.contains("PASSED"));
774        assert!(output.contains("Databases verified: 5"));
775        assert!(output.contains("Records verified: 1000"));
776        assert!(output.contains("No errors or warnings"));
777    }
778
779    #[test]
780    fn test_verify_result_display_with_errors() {
781        let mut result = VerifyResult::new();
782        result.add_error(VerifyError::BtreeError {
783            db_name: "test".to_string(),
784            description: "Bad node".to_string(),
785        });
786        result.databases_verified = 2;
787        result.records_verified = 500;
788
789        let output = format!("{}", result);
790        assert!(output.contains("FAILED"));
791        assert!(output.contains("Errors (1)"));
792        assert!(output.contains("B-tree error"));
793    }
794
795    #[test]
796    fn test_verify_result_display_with_warnings() {
797        let mut result = VerifyResult::new();
798        result.add_warning("Low cache utilization".to_string());
799        result.databases_verified = 3;
800
801        let output = format!("{}", result);
802        assert!(output.contains("PASSED"));
803        assert!(output.contains("Warnings (1)"));
804        assert!(output.contains("Low cache utilization"));
805    }
806
807    #[test]
808    fn test_verify_result_clone() {
809        let mut result = VerifyResult::new();
810        result.add_error(VerifyError::DataInconsistency {
811            description: "Test".to_string(),
812        });
813
814        let cloned = result.clone();
815        assert_eq!(cloned.errors.len(), result.errors.len());
816        assert_eq!(cloned.passed, result.passed);
817    }
818
819    #[test]
820    fn test_verify_error_equality() {
821        let error1 = VerifyError::BtreeError {
822            db_name: "db1".to_string(),
823            description: "error".to_string(),
824        };
825        let error2 = VerifyError::BtreeError {
826            db_name: "db1".to_string(),
827            description: "error".to_string(),
828        };
829        let error3 = VerifyError::BtreeError {
830            db_name: "db2".to_string(),
831            description: "error".to_string(),
832        };
833
834        assert_eq!(error1, error2);
835        assert_ne!(error1, error3);
836    }
837
838    #[test]
839    fn test_verify_config_equality() {
840        let config1 = VerifyConfig::default();
841        let config2 = VerifyConfig::default();
842        let config3 = VerifyConfig::new().with_repair(true);
843
844        assert_eq!(config1, config2);
845        assert_ne!(config1, config3);
846    }
847
848    // ── verify_tree tests ────────────────────────────────────────────────────
849
850    /// verify_tree on an empty tree returns a passing result.
851    #[test]
852    fn test_verify_tree_empty() {
853        use noxu_dbi::{DatabaseConfig, DatabaseId, DatabaseImpl, DbType};
854        use noxu_sync::RwLock;
855        use std::sync::Arc;
856
857        let db_id = DatabaseId::new(1);
858        let config = DatabaseConfig::default();
859        let db_impl = DatabaseImpl::new(
860            db_id,
861            "verify_test".to_string(),
862            DbType::User,
863            &config,
864        );
865        let db = Arc::new(RwLock::new(db_impl));
866        let guard = db.read();
867        let cfg = VerifyConfig::default();
868
869        if let Some(t) = guard.get_real_tree() {
870            let result = verify_tree(&t, "verify_test", &cfg);
871            assert!(
872                result.passed,
873                "empty tree should pass: {:?}",
874                result.errors
875            );
876            assert_eq!(result.databases_verified, 1);
877        }
878        // If no real tree is present the test is a no-op.
879    }
880
881    /// verify_tree on a populated tree returns a passing result.
882    ///
883    /// Uses a real LogManager so that each put() receives a valid (non-NULL)
884    /// LSN — the verifier requires this for all non-deleted BIN entries.
885    #[test]
886    fn test_verify_tree_populated() {
887        use noxu_dbi::{
888            CursorImpl, DatabaseConfig, DatabaseId, DatabaseImpl, DbType,
889            PutMode,
890        };
891        use noxu_log::{FileManager, LogManager};
892        use noxu_sync::RwLock;
893        use std::sync::Arc;
894        use tempfile::TempDir;
895
896        let dir = TempDir::new().unwrap();
897        let fm = Arc::new(
898            FileManager::new(dir.path(), false, 64 * 1024 * 1024, 100).unwrap(),
899        );
900        let lm =
901            Arc::new(LogManager::new(Arc::clone(&fm), 3, 1024 * 1024, 65536));
902
903        let db_id = DatabaseId::new(2);
904        let config = DatabaseConfig::default();
905        let db_impl = DatabaseImpl::new(
906            db_id,
907            "pop_test".to_string(),
908            DbType::User,
909            &config,
910        );
911        let db = Arc::new(RwLock::new(db_impl));
912
913        {
914            let mut cursor = CursorImpl::with_log_manager(
915                Arc::clone(&db),
916                1,
917                Arc::clone(&lm),
918            );
919            cursor.put(b"alpha", b"1", PutMode::Overwrite).unwrap();
920            cursor.put(b"beta", b"2", PutMode::Overwrite).unwrap();
921            cursor.put(b"gamma", b"3", PutMode::Overwrite).unwrap();
922        }
923
924        let guard = db.read();
925        let cfg = VerifyConfig::default();
926
927        if let Some(t) = guard.get_real_tree() {
928            let result = verify_tree(&t, "pop_test", &cfg);
929            assert!(
930                result.passed,
931                "populated tree should pass: {:?}",
932                result.errors
933            );
934            assert_eq!(result.databases_verified, 1);
935        }
936    }
937
938    /// verify_tree must DETECT a real structural fault, not silently pass.
939    ///
940    /// Builds a populated tree, then corrupts one non-deleted BIN slot to
941    /// carry a NULL LSN. `VerifyUtils.verifyBIN()` flags this; the
942    /// former standalone `verify_environment` / `verify_database` stubs would
943    /// have returned `passed = true` for the same corruption (the bug this
944    /// removal fixes).
945    #[test]
946    fn test_verify_tree_detects_null_lsn() {
947        use noxu_dbi::{
948            CursorImpl, DatabaseConfig, DatabaseId, DatabaseImpl, DbType,
949            PutMode,
950        };
951        use noxu_log::{FileManager, LogManager};
952        use noxu_sync::RwLock;
953        use noxu_tree::tree::TreeNode;
954        use std::sync::Arc;
955        use tempfile::TempDir;
956
957        let dir = TempDir::new().unwrap();
958        let fm = Arc::new(
959            FileManager::new(dir.path(), false, 64 * 1024 * 1024, 100).unwrap(),
960        );
961        let lm =
962            Arc::new(LogManager::new(Arc::clone(&fm), 3, 1024 * 1024, 65536));
963
964        let db_id = DatabaseId::new(3);
965        let config = DatabaseConfig::default();
966        let db_impl = DatabaseImpl::new(
967            db_id,
968            "corrupt_test".to_string(),
969            DbType::User,
970            &config,
971        );
972        let db = Arc::new(RwLock::new(db_impl));
973
974        {
975            let mut cursor = CursorImpl::with_log_manager(
976                Arc::clone(&db),
977                1,
978                Arc::clone(&lm),
979            );
980            cursor.put(b"alpha", b"1", PutMode::Overwrite).unwrap();
981            cursor.put(b"beta", b"2", PutMode::Overwrite).unwrap();
982            cursor.put(b"gamma", b"3", PutMode::Overwrite).unwrap();
983        }
984
985        let guard = db.read();
986        let t = guard
987            .get_real_tree()
988            .expect("invariant: populated db has a real tree");
989
990        // Corrupt the first reachable BIN: set one live slot's LSN to NULL.
991        let corrupted = corrupt_first_bin_slot(&t, NULL_LSN);
992        assert!(corrupted, "test setup: expected at least one BIN slot");
993
994        let cfg = VerifyConfig::default();
995        let result = verify_tree(&t, "corrupt_test", &cfg);
996        assert!(
997            !result.passed,
998            "verifier must detect the NULL-LSN corruption, got passed=true"
999        );
1000        assert!(
1001            result.errors.iter().any(|e| matches!(
1002                e,
1003                VerifyError::BtreeError { description, .. }
1004                    if description.contains("NULL LSN")
1005            )),
1006            "expected a NULL-LSN BtreeError, got: {:?}",
1007            result.errors
1008        );
1009
1010        // Helper: descend from the root to the first BIN and corrupt slot 0.
1011        fn corrupt_first_bin_slot(
1012            tree: &noxu_tree::Tree,
1013            null_lsn: noxu_util::Lsn,
1014        ) -> bool {
1015            fn recurse(
1016                node: &Arc<noxu_tree::NodeRwLock<TreeNode>>,
1017                null_lsn: noxu_util::Lsn,
1018            ) -> bool {
1019                let mut guard = node.write();
1020                match &mut *guard {
1021                    TreeNode::Bottom(bin) => {
1022                        if let Some(entry) = bin.entries.first_mut() {
1023                            entry.known_deleted = false;
1024                            entry.lsn = null_lsn;
1025                            return true;
1026                        }
1027                        false
1028                    }
1029                    TreeNode::Internal(in_node) => {
1030                        for child in in_node.resident_children() {
1031                            if recurse(&child, null_lsn) {
1032                                return true;
1033                            }
1034                        }
1035                        false
1036                    }
1037                }
1038            }
1039            match tree.get_root() {
1040                Some(root) => recurse(&root, null_lsn),
1041                None => false,
1042            }
1043        }
1044    }
1045}