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_arc = match &entry.child {
344            Some(c) => c,
345            None => {
346                result.add_error(VerifyError::BtreeError {
347                    db_name: db_name.to_string(),
348                    description: format!(
349                        "IN node (id={}) entry {} has null child reference",
350                        in_node.node_id, i
351                    ),
352                });
353                if result.error_count() >= config.max_errors as usize {
354                    return;
355                }
356                continue;
357            }
358        };
359
360        // The key carried in slot 0 of an IN is the virtual -infinity key;
361        // entries at i > 0 carry the first key of that child's subtree.
362        // IN slot-0 special case.
363        let expected_parent_key: Option<&[u8]> =
364            if i == 0 { None } else { Some(entry.key.as_slice()) };
365
366        verify_node(
367            child_arc,
368            expected_parent_key,
369            db_name,
370            config,
371            result,
372            records,
373        );
374
375        if result.error_count() >= config.max_errors as usize {
376            return;
377        }
378    }
379}
380
381/// Verifies a BIN stub (leaf-level node).
382///
383/// `VerifyUtils.verifyBIN()`: checks that non-deleted slots carry
384/// valid (non-NULL) LSNs, and that the BIN's first key is >= the routing key
385/// passed from the parent.
386fn verify_bin_stub(
387    bin: &BinStub,
388    parent_key: Option<&[u8]>,
389    db_name: &str,
390    config: &VerifyConfig,
391    result: &mut VerifyResult,
392    records: &mut u64,
393) {
394    // Check that the BIN's first key is >= the routing key from the parent.
395    if let Some(pk) = parent_key
396        && !bin.entries.is_empty()
397    {
398        let first_full = bin.get_full_key(0);
399        if let Some(ref first_key) = first_full
400            && first_key.as_slice() < pk
401        {
402            result.add_error(VerifyError::BtreeError {
403                        db_name: db_name.to_string(),
404                        description: format!(
405                            "BIN (id={}) first key {:?} is less than parent routing key {:?}",
406                            bin.node_id, first_key, pk
407                        ),
408                    });
409        }
410    }
411
412    // Check each slot.
413    for (i, entry) in bin.entries.iter().enumerate() {
414        // Non-deleted entries must have a valid LSN.
415        if !entry.known_deleted && entry.lsn == NULL_LSN {
416            result.add_error(VerifyError::BtreeError {
417                db_name: db_name.to_string(),
418                description: format!(
419                    "BIN (id={}) slot {} has NULL LSN but is not known-deleted",
420                    bin.node_id, i
421                ),
422            });
423            if result.error_count() >= config.max_errors as usize {
424                return;
425            }
426        }
427
428        if !entry.known_deleted {
429            *records += 1;
430        }
431    }
432}
433
434// ============================================================================
435// Public verification entry points
436// ============================================================================
437
438// NOTE: the former standalone `verify_environment(&VerifyConfig)` and
439// `verify_database(&str, &VerifyConfig)` entry points were removed: they
440// could not perform real verification without a live `EnvironmentImpl` /
441// `DatabaseImpl` handle, so they returned a fake passing result. The real
442// entry points are `noxu_db::Environment::verify` and
443// `noxu_db::Database::verify`, which route through `verify_database_impl`
444// (below) → `verify_tree`. This mirrors `DbVerify` / `Environment.verify`,
445// which always operate on an opened environment.
446
447/// Verify a `DatabaseImpl`'s B-tree structural integrity.
448///
449/// Calls `verify_tree()` on the underlying real B-tree when one is present.
450/// Used by `Database::verify()` in `noxu-db` to bridge the crate boundary
451/// (noxu-db does not depend directly on noxu-tree).
452///
453/// Mirrors `DatabaseImpl.verify(VerifyConfig)` in— calls BtreeVerifier
454/// on the tree owned by the DatabaseImpl.
455///
456/// # Arguments
457///
458/// * `db_impl` - The database implementation to verify.
459/// * `config` - Configuration controlling what to verify.
460///
461/// # Returns
462///
463/// A `VerifyResult` with structural errors and the count of records verified.
464pub fn verify_database_impl(
465    db_impl: &DatabaseImpl,
466    config: &VerifyConfig,
467) -> VerifyResult {
468    let db_name = db_impl.get_name();
469    match db_impl.get_real_tree() {
470        Some(tree) => verify_tree(&tree, db_name, config),
471        None => {
472            // No real B-tree attached (e.g., stub / metadata DB) — treat as empty.
473            VerifyResult {
474                errors: Vec::new(),
475                warnings: Vec::new(),
476                databases_verified: 1,
477                records_verified: 0,
478                passed: true,
479            }
480        }
481    }
482}
483
484// ============================================================================
485// checkLsns: live-tree-LSN <-> UtilizationProfile overlap check (CLN-2)
486// ============================================================================
487
488/// Gather the set of LIVE LSNs referenced by a B-tree.
489///
490/// Mirrors JE's `GatherLSNs` `TreeNodeProcessor` driven by a
491/// `SortedLSNTreeWalker` in `VerifyUtils.checkLsns()`: every non-NULL child
492/// LSN reachable from the root is collected. Here we collect the LN LSNs
493/// recorded in each live (non-known-deleted) BIN slot.
494pub fn gather_tree_lsns(tree: &Tree) -> HashSet<Lsn> {
495    let mut lsns = HashSet::new();
496    if let Some(root) = tree.get_root() {
497        gather_node_lsns(&root, &mut lsns);
498    }
499    lsns
500}
501
502fn gather_node_lsns(node_arc: &Arc<RwLock<TreeNode>>, lsns: &mut HashSet<Lsn>) {
503    let guard = node_arc.read();
504    match &*guard {
505        TreeNode::Internal(in_node) => {
506            for entry in &in_node.entries {
507                if let Some(child) = &entry.child {
508                    gather_node_lsns(child, lsns);
509                }
510            }
511        }
512        TreeNode::Bottom(bin) => {
513            for entry in &bin.entries {
514                // JE GatherLSNs.processLSN skips DbLsn.NULL_LSN.
515                if !entry.known_deleted && entry.lsn != NULL_LSN {
516                    lsns.insert(entry.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 let Some(entry) = bin.entries.first_mut() {
1024                            entry.known_deleted = false;
1025                            entry.lsn = null_lsn;
1026                            return true;
1027                        }
1028                        false
1029                    }
1030                    TreeNode::Internal(in_node) => {
1031                        for e in &in_node.entries {
1032                            if let Some(child) = &e.child
1033                                && recurse(child, null_lsn)
1034                            {
1035                                return true;
1036                            }
1037                        }
1038                        false
1039                    }
1040                }
1041            }
1042            match tree.get_root() {
1043                Some(root) => recurse(&root, null_lsn),
1044                None => false,
1045            }
1046        }
1047    }
1048}