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 && bin.get_lsn(i) == 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 (i, entry) in bin.entries.iter().enumerate() {
513                // JE GatherLSNs.processLSN skips DbLsn.NULL_LSN.
514                let lsn = bin.get_lsn(i);
515                if !entry.known_deleted && lsn != NULL_LSN {
516                    lsns.insert(lsn);
517                }
518            }
519        }
520    }
521}
522
523/// Compare the live LSNs of a `DatabaseImpl`'s tree against the obsolete set
524/// recorded in the `UtilizationTracker`, adding a `DataInconsistency` error
525/// for each live LSN wrongly recorded as obsolete.
526///
527/// Faithful port of `VerifyUtils.checkLsns()`: a live tree LSN must NOT be in
528/// the obsolete set (JE: "Obsolete LSN set contains valid LSN" ->
529/// `LOG_INTEGRITY` `EnvironmentFailureException`). The disjointness test
530/// itself lives in `noxu_cleaner::check_lsns`; this function bridges the
531/// engine-side tree walk to it.
532pub fn check_lsns_against_tracker(
533    db_impl: &DatabaseImpl,
534    tracker: &noxu_cleaner::UtilizationTracker,
535    result: &mut VerifyResult,
536) {
537    let tree = match db_impl.get_real_tree() {
538        Some(t) => t,
539        None => return,
540    };
541    let live = gather_tree_lsns(&tree);
542    let check = noxu_cleaner::check_lsns(live, tracker);
543    for lsn in check.obsolete_contains_live {
544        result.add_error(VerifyError::DataInconsistency {
545            description: format!(
546                "Obsolete LSN set contains valid LSN {} in database '{}' \
547                 (VerifyUtils.checkLsns: live tree LSN recorded obsolete)",
548                lsn,
549                db_impl.get_name()
550            ),
551        });
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    #[test]
560    fn test_verify_result_new() {
561        let result = VerifyResult::new();
562        assert!(result.passed);
563        assert_eq!(result.errors.len(), 0);
564        assert_eq!(result.warnings.len(), 0);
565        assert_eq!(result.databases_verified, 0);
566        assert_eq!(result.records_verified, 0);
567    }
568
569    #[test]
570    fn test_verify_result_default() {
571        let result = VerifyResult::default();
572        assert!(result.passed);
573        assert!(result.errors.is_empty());
574    }
575
576    #[test]
577    fn test_verify_result_with_errors() {
578        let errors = vec![VerifyError::BtreeError {
579            db_name: "test".to_string(),
580            description: "Invalid node".to_string(),
581        }];
582        let result = VerifyResult::with_errors(errors);
583        assert!(!result.passed);
584        assert_eq!(result.errors.len(), 1);
585    }
586
587    #[test]
588    fn test_verify_result_with_no_errors() {
589        let errors = vec![];
590        let result = VerifyResult::with_errors(errors);
591        assert!(result.passed);
592        assert_eq!(result.errors.len(), 0);
593    }
594
595    #[test]
596    fn test_add_error() {
597        let mut result = VerifyResult::new();
598        assert!(result.passed);
599
600        result.add_error(VerifyError::DataInconsistency {
601            description: "Test error".to_string(),
602        });
603
604        assert!(!result.passed);
605        assert_eq!(result.errors.len(), 1);
606    }
607
608    #[test]
609    fn test_add_warning() {
610        let mut result = VerifyResult::new();
611        result.add_warning("Test warning".to_string());
612
613        assert!(result.passed); // warnings don't affect passed status
614        assert_eq!(result.warnings.len(), 1);
615    }
616
617    #[test]
618    fn test_error_count() {
619        let mut result = VerifyResult::new();
620        assert_eq!(result.error_count(), 0);
621
622        result.add_error(VerifyError::DataInconsistency {
623            description: "Error 1".to_string(),
624        });
625        result.add_error(VerifyError::DataInconsistency {
626            description: "Error 2".to_string(),
627        });
628
629        assert_eq!(result.error_count(), 2);
630    }
631
632    #[test]
633    fn test_warning_count() {
634        let mut result = VerifyResult::new();
635        assert_eq!(result.warning_count(), 0);
636
637        result.add_warning("Warning 1".to_string());
638        result.add_warning("Warning 2".to_string());
639
640        assert_eq!(result.warning_count(), 2);
641    }
642
643    #[test]
644    fn test_is_passed() {
645        let result = VerifyResult::new();
646        assert!(result.is_passed());
647
648        let mut failed_result = VerifyResult::new();
649        failed_result.add_error(VerifyError::DataInconsistency {
650            description: "Error".to_string(),
651        });
652        assert!(!failed_result.is_passed());
653    }
654
655    #[test]
656    fn test_verify_error_btree() {
657        let error = VerifyError::BtreeError {
658            db_name: "mydb".to_string(),
659            description: "Invalid child reference".to_string(),
660        };
661        let s = format!("{}", error);
662        assert!(s.contains("B-tree error"));
663        assert!(s.contains("mydb"));
664        assert!(s.contains("Invalid child reference"));
665    }
666
667    #[test]
668    fn test_verify_error_log() {
669        let error = VerifyError::LogError {
670            file_number: 42,
671            description: "Corrupted entry".to_string(),
672        };
673        let s = format!("{}", error);
674        assert!(s.contains("Log file"));
675        assert!(s.contains("0000002a.ndb"));
676        assert!(s.contains("Corrupted entry"));
677    }
678
679    #[test]
680    fn test_verify_error_data_inconsistency() {
681        let error = VerifyError::DataInconsistency {
682            description: "Mismatched LSN".to_string(),
683        };
684        let s = format!("{}", error);
685        assert!(s.contains("Data inconsistency"));
686        assert!(s.contains("Mismatched LSN"));
687    }
688
689    #[test]
690    fn test_verify_error_checksum() {
691        let error = VerifyError::ChecksumError {
692            location: "file 10, offset 1024".to_string(),
693            description: "CRC mismatch".to_string(),
694        };
695        let s = format!("{}", error);
696        assert!(s.contains("Checksum error"));
697        assert!(s.contains("file 10, offset 1024"));
698        assert!(s.contains("CRC mismatch"));
699    }
700
701    #[test]
702    fn test_verify_error_invalid_node_reference() {
703        let error = VerifyError::InvalidNodeReference {
704            node_id: 12345,
705            description: "Node not found".to_string(),
706        };
707        let s = format!("{}", error);
708        assert!(s.contains("Invalid node reference"));
709        assert!(s.contains("12345"));
710        assert!(s.contains("Node not found"));
711    }
712
713    #[test]
714    fn test_verify_error_metadata() {
715        let error = VerifyError::MetadataError {
716            db_name: "testdb".to_string(),
717            description: "Invalid format version".to_string(),
718        };
719        let s = format!("{}", error);
720        assert!(s.contains("Metadata error"));
721        assert!(s.contains("testdb"));
722        assert!(s.contains("Invalid format version"));
723    }
724
725    #[test]
726    fn test_verify_config_default() {
727        let config = VerifyConfig::default();
728        assert!(config.verify_btree);
729        assert!(config.verify_log);
730        assert!(config.verify_data_checksums);
731        assert!(!config.repair);
732        assert_eq!(config.max_errors, 100);
733        assert!(!config.verbose);
734        assert!(config.database_name.is_none());
735    }
736
737    #[test]
738    fn test_verify_config_new() {
739        let config = VerifyConfig::new();
740        assert_eq!(config, VerifyConfig::default());
741    }
742
743    #[test]
744    fn test_verify_config_builder() {
745        let config = VerifyConfig::new()
746            .with_btree_verification(false)
747            .with_log_verification(true)
748            .with_checksum_verification(false)
749            .with_repair(true)
750            .with_max_errors(50)
751            .with_verbose(true)
752            .for_database("mydb".to_string());
753
754        assert!(!config.verify_btree);
755        assert!(config.verify_log);
756        assert!(!config.verify_data_checksums);
757        assert!(config.repair);
758        assert_eq!(config.max_errors, 50);
759        assert!(config.verbose);
760        assert_eq!(config.database_name, Some("mydb".to_string()));
761    }
762
763    #[test]
764    fn test_verify_result_display_passed() {
765        let result = VerifyResult {
766            errors: Vec::new(),
767            warnings: Vec::new(),
768            databases_verified: 5,
769            records_verified: 1000,
770            passed: true,
771        };
772
773        let output = format!("{}", result);
774        assert!(output.contains("PASSED"));
775        assert!(output.contains("Databases verified: 5"));
776        assert!(output.contains("Records verified: 1000"));
777        assert!(output.contains("No errors or warnings"));
778    }
779
780    #[test]
781    fn test_verify_result_display_with_errors() {
782        let mut result = VerifyResult::new();
783        result.add_error(VerifyError::BtreeError {
784            db_name: "test".to_string(),
785            description: "Bad node".to_string(),
786        });
787        result.databases_verified = 2;
788        result.records_verified = 500;
789
790        let output = format!("{}", result);
791        assert!(output.contains("FAILED"));
792        assert!(output.contains("Errors (1)"));
793        assert!(output.contains("B-tree error"));
794    }
795
796    #[test]
797    fn test_verify_result_display_with_warnings() {
798        let mut result = VerifyResult::new();
799        result.add_warning("Low cache utilization".to_string());
800        result.databases_verified = 3;
801
802        let output = format!("{}", result);
803        assert!(output.contains("PASSED"));
804        assert!(output.contains("Warnings (1)"));
805        assert!(output.contains("Low cache utilization"));
806    }
807
808    #[test]
809    fn test_verify_result_clone() {
810        let mut result = VerifyResult::new();
811        result.add_error(VerifyError::DataInconsistency {
812            description: "Test".to_string(),
813        });
814
815        let cloned = result.clone();
816        assert_eq!(cloned.errors.len(), result.errors.len());
817        assert_eq!(cloned.passed, result.passed);
818    }
819
820    #[test]
821    fn test_verify_error_equality() {
822        let error1 = VerifyError::BtreeError {
823            db_name: "db1".to_string(),
824            description: "error".to_string(),
825        };
826        let error2 = VerifyError::BtreeError {
827            db_name: "db1".to_string(),
828            description: "error".to_string(),
829        };
830        let error3 = VerifyError::BtreeError {
831            db_name: "db2".to_string(),
832            description: "error".to_string(),
833        };
834
835        assert_eq!(error1, error2);
836        assert_ne!(error1, error3);
837    }
838
839    #[test]
840    fn test_verify_config_equality() {
841        let config1 = VerifyConfig::default();
842        let config2 = VerifyConfig::default();
843        let config3 = VerifyConfig::new().with_repair(true);
844
845        assert_eq!(config1, config2);
846        assert_ne!(config1, config3);
847    }
848
849    // ── verify_tree tests ────────────────────────────────────────────────────
850
851    /// verify_tree on an empty tree returns a passing result.
852    #[test]
853    fn test_verify_tree_empty() {
854        use noxu_dbi::{DatabaseConfig, DatabaseId, DatabaseImpl, DbType};
855        use noxu_sync::RwLock;
856        use std::sync::Arc;
857
858        let db_id = DatabaseId::new(1);
859        let config = DatabaseConfig::default();
860        let db_impl = DatabaseImpl::new(
861            db_id,
862            "verify_test".to_string(),
863            DbType::User,
864            &config,
865        );
866        let db = Arc::new(RwLock::new(db_impl));
867        let guard = db.read();
868        let cfg = VerifyConfig::default();
869
870        if let Some(t) = guard.get_real_tree() {
871            let result = verify_tree(&t, "verify_test", &cfg);
872            assert!(
873                result.passed,
874                "empty tree should pass: {:?}",
875                result.errors
876            );
877            assert_eq!(result.databases_verified, 1);
878        }
879        // If no real tree is present the test is a no-op.
880    }
881
882    /// verify_tree on a populated tree returns a passing result.
883    ///
884    /// Uses a real LogManager so that each put() receives a valid (non-NULL)
885    /// LSN — the verifier requires this for all non-deleted BIN entries.
886    #[test]
887    fn test_verify_tree_populated() {
888        use noxu_dbi::{
889            CursorImpl, DatabaseConfig, DatabaseId, DatabaseImpl, DbType,
890            PutMode,
891        };
892        use noxu_log::{FileManager, LogManager};
893        use noxu_sync::RwLock;
894        use std::sync::Arc;
895        use tempfile::TempDir;
896
897        let dir = TempDir::new().unwrap();
898        let fm = Arc::new(
899            FileManager::new(dir.path(), false, 64 * 1024 * 1024, 100).unwrap(),
900        );
901        let lm =
902            Arc::new(LogManager::new(Arc::clone(&fm), 3, 1024 * 1024, 65536));
903
904        let db_id = DatabaseId::new(2);
905        let config = DatabaseConfig::default();
906        let db_impl = DatabaseImpl::new(
907            db_id,
908            "pop_test".to_string(),
909            DbType::User,
910            &config,
911        );
912        let db = Arc::new(RwLock::new(db_impl));
913
914        {
915            let mut cursor = CursorImpl::with_log_manager(
916                Arc::clone(&db),
917                1,
918                Arc::clone(&lm),
919            );
920            cursor.put(b"alpha", b"1", PutMode::Overwrite).unwrap();
921            cursor.put(b"beta", b"2", PutMode::Overwrite).unwrap();
922            cursor.put(b"gamma", b"3", PutMode::Overwrite).unwrap();
923        }
924
925        let guard = db.read();
926        let cfg = VerifyConfig::default();
927
928        if let Some(t) = guard.get_real_tree() {
929            let result = verify_tree(&t, "pop_test", &cfg);
930            assert!(
931                result.passed,
932                "populated tree should pass: {:?}",
933                result.errors
934            );
935            assert_eq!(result.databases_verified, 1);
936        }
937    }
938
939    /// verify_tree must DETECT a real structural fault, not silently pass.
940    ///
941    /// Builds a populated tree, then corrupts one non-deleted BIN slot to
942    /// carry a NULL LSN. `VerifyUtils.verifyBIN()` flags this; the
943    /// former standalone `verify_environment` / `verify_database` stubs would
944    /// have returned `passed = true` for the same corruption (the bug this
945    /// removal fixes).
946    #[test]
947    fn test_verify_tree_detects_null_lsn() {
948        use noxu_dbi::{
949            CursorImpl, DatabaseConfig, DatabaseId, DatabaseImpl, DbType,
950            PutMode,
951        };
952        use noxu_log::{FileManager, LogManager};
953        use noxu_sync::RwLock;
954        use noxu_tree::tree::TreeNode;
955        use std::sync::Arc;
956        use tempfile::TempDir;
957
958        let dir = TempDir::new().unwrap();
959        let fm = Arc::new(
960            FileManager::new(dir.path(), false, 64 * 1024 * 1024, 100).unwrap(),
961        );
962        let lm =
963            Arc::new(LogManager::new(Arc::clone(&fm), 3, 1024 * 1024, 65536));
964
965        let db_id = DatabaseId::new(3);
966        let config = DatabaseConfig::default();
967        let db_impl = DatabaseImpl::new(
968            db_id,
969            "corrupt_test".to_string(),
970            DbType::User,
971            &config,
972        );
973        let db = Arc::new(RwLock::new(db_impl));
974
975        {
976            let mut cursor = CursorImpl::with_log_manager(
977                Arc::clone(&db),
978                1,
979                Arc::clone(&lm),
980            );
981            cursor.put(b"alpha", b"1", PutMode::Overwrite).unwrap();
982            cursor.put(b"beta", b"2", PutMode::Overwrite).unwrap();
983            cursor.put(b"gamma", b"3", PutMode::Overwrite).unwrap();
984        }
985
986        let guard = db.read();
987        let t = guard
988            .get_real_tree()
989            .expect("invariant: populated db has a real tree");
990
991        // Corrupt the first reachable BIN: set one live slot's LSN to NULL.
992        let corrupted = corrupt_first_bin_slot(&t, NULL_LSN);
993        assert!(corrupted, "test setup: expected at least one BIN slot");
994
995        let cfg = VerifyConfig::default();
996        let result = verify_tree(&t, "corrupt_test", &cfg);
997        assert!(
998            !result.passed,
999            "verifier must detect the NULL-LSN corruption, got passed=true"
1000        );
1001        assert!(
1002            result.errors.iter().any(|e| matches!(
1003                e,
1004                VerifyError::BtreeError { description, .. }
1005                    if description.contains("NULL LSN")
1006            )),
1007            "expected a NULL-LSN BtreeError, got: {:?}",
1008            result.errors
1009        );
1010
1011        // Helper: descend from the root to the first BIN and corrupt slot 0.
1012        fn corrupt_first_bin_slot(
1013            tree: &noxu_tree::Tree,
1014            null_lsn: noxu_util::Lsn,
1015        ) -> bool {
1016            fn recurse(
1017                node: &Arc<noxu_tree::NodeRwLock<TreeNode>>,
1018                null_lsn: noxu_util::Lsn,
1019            ) -> bool {
1020                let mut guard = node.write();
1021                match &mut *guard {
1022                    TreeNode::Bottom(bin) => {
1023                        if !bin.entries.is_empty() {
1024                            bin.entries[0].known_deleted = false;
1025                            bin.set_lsn(0, null_lsn);
1026                            return true;
1027                        }
1028                        false
1029                    }
1030                    TreeNode::Internal(in_node) => {
1031                        for child in in_node.resident_children() {
1032                            if recurse(&child, null_lsn) {
1033                                return true;
1034                            }
1035                        }
1036                        false
1037                    }
1038                }
1039            }
1040            match tree.get_root() {
1041                Some(root) => recurse(&root, null_lsn),
1042                None => false,
1043            }
1044        }
1045    }
1046}