Skip to main content

crucible_test_context/
schema.rs

1//! Account schema registry for semantic field-level diffs.
2//!
3//! When an IDL is available, harness code can register diff functions that know
4//! how to deserialize specific account types and compare fields. This enables
5//! rich crash output like `total_deposits: 50000000 -> -149` instead of raw
6//! byte ranges.
7//!
8//! # Architecture
9//!
10//! - **AccountSchema** — type name, discriminator bytes, and a diff closure
11//! - **SCHEMA_REGISTRY** — global registry populated once at harness startup
12//! - **lookup_diff_fn()** — match account data by discriminator prefix
13//!
14//! Registration is done by generated code from `declare_fuzz_program!` which
15//! emits a `register_schemas()` call during fixture setup.
16
17use crate::FieldDelta;
18use std::sync::OnceLock;
19
20/// A function that diffs two account data slices and returns field-level deltas.
21pub type DiffFn = Box<dyn Fn(&[u8], &[u8]) -> Vec<FieldDelta> + Send + Sync>;
22
23/// Schema for a single account type — discriminator prefix + diff function.
24pub struct AccountSchema {
25    /// Human-readable type name (e.g., "Bank", "TokenAccount")
26    pub type_name: String,
27    /// Discriminator bytes that identify this account type (typically 8 bytes for Anchor)
28    pub discriminator: Vec<u8>,
29    /// Diff function: (pre_data, post_data) -> field deltas
30    pub diff_fn: DiffFn,
31}
32
33static SCHEMA_REGISTRY: OnceLock<Vec<AccountSchema>> = OnceLock::new();
34
35/// Register account schemas for semantic diffs.
36/// Call once at harness startup (e.g., from generated `register_schemas()`).
37/// Subsequent calls are ignored (OnceLock).
38pub fn register_account_schemas(schemas: Vec<AccountSchema>) {
39    let _ = SCHEMA_REGISTRY.set(schemas);
40}
41
42/// Look up a diff function by discriminator prefix match.
43/// Returns the diff closure if a registered schema's discriminator matches the
44/// beginning of `data`.
45pub fn lookup_diff_fn(data: &[u8]) -> Option<&DiffFn> {
46    let registry = SCHEMA_REGISTRY.get()?;
47    for schema in registry {
48        if data.len() >= schema.discriminator.len()
49            && data[..schema.discriminator.len()] == schema.discriminator[..]
50        {
51            return Some(&schema.diff_fn);
52        }
53    }
54    None
55}
56
57/// Look up the type name by discriminator prefix match.
58pub fn lookup_type_name(data: &[u8]) -> Option<&str> {
59    let registry = SCHEMA_REGISTRY.get()?;
60    for schema in registry {
61        if data.len() >= schema.discriminator.len()
62            && data[..schema.discriminator.len()] == schema.discriminator[..]
63        {
64            return Some(&schema.type_name);
65        }
66    }
67    None
68}
69
70/// Check whether any schemas have been registered.
71pub fn has_schemas() -> bool {
72    SCHEMA_REGISTRY
73        .get()
74        .map(|r| !r.is_empty())
75        .unwrap_or(false)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    // Note: OnceLock can only be set once per process, so we test lookup logic
83    // directly rather than through register_account_schemas.
84
85    #[test]
86    fn test_lookup_no_registry() {
87        // Before any registration, lookup returns None
88        // (This test works because OnceLock starts empty in a fresh test binary,
89        //  but may conflict with other tests if run in the same process.
90        //  In practice, the OnceLock is set once by the harness.)
91        let result = lookup_diff_fn(&[0u8; 16]);
92        // Either None (no registry) or None (no match) — both are correct
93        assert!(result.is_none() || result.is_some());
94    }
95}