## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/realizar/src/gguf/test_factory.rs

    Line
    Count
    Source
    1
    //! GGUF Test Factory - Synthesizes valid GGUF files in memory
    2
    //!
    3
    //! This module provides `GGUFBuilder` for creating valid GGUF v3 files
    4
    //! without needing real model files. Essential for testing transformer
    5
    //! loading code that requires properly formatted binary data.
    6
    //!
    7
    //! # Example
    8
    //!
    9
    //! ```ignore
    10
    //! let data = GGUFBuilder::new()
    11
    //!     .architecture("llama")
    12
    //!     .hidden_dim(64)
    13
    //!     .num_layers(1)
    14
    //!     .add_f32_tensor("token_embd.weight", &[100, 64], &embedding_data)
    15
    //!     .add_q4_k_tensor("blk.0.attn_q.weight", &[64, 64], &q4k_data)
    16
    //!     .build();
    17
    //!
    18
    //! let model = GGUFModel::from_bytes(&data)?;
    19
    //! ```
    20
    21
    use super::types::{
    22
        GGUF_ALIGNMENT, GGUF_MAGIC, GGUF_TYPE_F32, GGUF_TYPE_Q4_0, GGUF_TYPE_Q4_K, GGUF_TYPE_Q5_K,
    23
        GGUF_TYPE_Q6_K, GGUF_TYPE_Q8_0, GGUF_VERSION_V3,
    24
    };
    25
    26
    /// Builder for creating valid GGUF v3 files in memory
    27
    pub struct GGUFBuilder {
    28
        /// Metadata key-value pairs (key, type, value_bytes)
    29
        metadata: Vec<(String, u32, Vec<u8>)>,
    30
        /// Tensor info entries (name, dims, qtype, data)
    31
        tensors: Vec<(String, Vec<u64>, u32, Vec<u8>)>,
    32
    }
    33
    34
    impl Default for GGUFBuilder {
    35
    0
        fn default() -> Self {
    36
    0
            Self::new()
    37
    0
        }
    38
    }
    39
    40
    impl GGUFBuilder {
    41
        /// Create a new GGUF builder
    42
        #[must_use]
    43
    18
        pub fn new() -> Self {
    44
    18
            Self {
    45
    18
                metadata: Vec::new(),
    46
    18
                tensors: Vec::new(),
    47
    18
            }
    48
    18
        }
    49
    50
        // =========================================================================
    51
        // Metadata Helpers
    52
        // =========================================================================
    53
    54
        /// Add a string metadata value
    55
        #[must_use]
    56
    14
        pub fn add_string(mut self, key: &str, value: &str) -> Self {
    57
    14
            let mut bytes = Vec::new();
    58
            // String: u64 length + UTF-8 bytes
    59
    14
            bytes.extend_from_slice(&(value.len() as u64).to_le_bytes());
    60
    14
            bytes.extend_from_slice(value.as_bytes());
    61
    14
            self.metadata.push((key.to_string(), 8, bytes)); // type 8 = string
    62
    14
            self
    63
    14
        }
    64
    65
        /// Add a u32 metadata value
    66
        #[must_use]
    67
    77
        pub fn add_u32(mut self, key: &str, value: u32) -> Self {
    68
    77
            self.metadata
    69
    77
                .push((key.to_string(), 4, value.to_le_bytes().to_vec())); // type 4 = u32
    70
    77
            self
    71
    77
        }
    72
    73
        /// Add a f32 metadata value
    74
        #[must_use]
    75
    25
        pub fn add_f32(mut self, key: &str, value: f32) -> Self {
    76
    25
            self.metadata
    77
    25
                .push((key.to_string(), 6, value.to_le_bytes().to_vec())); // type 6 = f32
    78
    25
            self
    79
    25
        }
    80
    81
        /// Set architecture (shorthand for general.architecture)
    82
        #[must_use]
    83
    14
        pub fn architecture(self, arch: &str) -> Self {
    84
    14
            self.add_string("general.architecture", arch)
    85
    14
        }
    86
    87
        /// Set hidden dimension (embedding length)
    88
        #[must_use]
    89
    13
        pub fn hidden_dim(self, arch: &str, dim: u32) -> Self {
    90
    13
            self.add_u32(&format!("{}.embedding_length", arch), dim)
    91
    13
        }
    92
    93
        /// Set number of layers (block count)
    94
        #[must_use]
    95
    13
        pub fn num_layers(self, arch: &str, count: u32) -> Self {
    96
    13
            self.add_u32(&format!("{}.block_count", arch), count)
    97
    13
        }
    98
    99
        /// Set number of attention heads
    100
        #[must_use]
    101
    13
        pub fn num_heads(self, arch: &str, count: u32) -> Self {
    102
    13
            self.add_u32(&format!("{}.attention.head_count", arch), count)
    103
    13
        }
    104
    105
        /// Set number of KV heads (for GQA)
    106
        #[must_use]
    107
    13
        pub fn num_kv_heads(self, arch: &str, count: u32) -> Self {
    108
    13
            self.add_u32(&format!("{}.attention.head_count_kv", arch), count)
    109
    13
        }
    110
    111
        /// Set context length
    112
        #[must_use]
    113
    13
        pub fn context_length(self, arch: &str, len: u32) -> Self {
    114
    13
            self.add_u32(&format!("{}.context_length", arch), len)
    115
    13
        }
    116
    117
        /// Set RoPE frequency base
    118
        #[must_use]
    119
    12
        pub fn rope_freq_base(self, arch: &str, base: f32) -> Self {
    120
    12
            self.add_f32(&format!("{}.rope.freq_base", arch), base)
    121
    12
        }
    122
    123
        /// Set RMS epsilon
    124
        #[must_use]
    125
    12
        pub fn rms_epsilon(self, arch: &str, eps: f32) -> Self {
    126
    12
            self.add_f32(&format!("{}.attention.layer_norm_rms_epsilon", arch), eps)
    127
    12
        }
    128
    129
        /// Set feed-forward hidden dimension
    130
        #[must_use]
    131
    11
        pub fn ffn_hidden_dim(self, arch: &str, dim: u32) -> Self {
    132
    11
            self.add_u32(&format!("{}.feed_forward_length", arch), dim)
    133
    11
        }
    134
    135
        /// Set vocab size (for completeness)
    136
        #[must_use]
    137
    0
        pub fn vocab_size(self, _arch: &str, size: u32) -> Self {
    138
            // Vocab size is typically inferred from token_embd.weight shape
    139
            // But we can store it in metadata if needed
    140
    0
            self.add_u32("tokenizer.ggml.tokens.size", size)
    141
    0
        }
    142
    143
        // =========================================================================
    144
        // Tensor Helpers
    145
        // =========================================================================
    146
    147
        /// Add an F32 tensor
    148
        #[must_use]
    149
    52
        pub fn add_f32_tensor(mut self, name: &str, dims: &[u64], data: &[f32]) -> Self {
    150
    118k
            let bytes52: Vec<u8>52 = data52.iter52().flat_map52(|f| f.to_le_bytes()).collect52();
    151
    52
            self.tensors
    152
    52
                .push((name.to_string(), dims.to_vec(), GGUF_TYPE_F32, bytes));
    153
    52
            self
    154
    52
        }
    155
    156
        /// Add a Q4_0 tensor (18 bytes per 32 elements)
    157
        #[must_use]
    158
    7
        pub fn add_q4_0_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self {
    159
    7
            self.tensors
    160
    7
                .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q4_0, data.to_vec()));
    161
    7
            self
    162
    7
        }
    163
    164
        /// Add a Q8_0 tensor (34 bytes per 32 elements)
    165
        #[must_use]
    166
    7
        pub fn add_q8_0_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self {
    167
    7
            self.tensors
    168
    7
                .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q8_0, data.to_vec()));
    169
    7
            self
    170
    7
        }
    171
    172
        /// Add a Q4_K tensor (144 bytes per 256 elements)
    173
        #[must_use]
    174
    61
        pub fn add_q4_k_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self {
    175
    61
            self.tensors
    176
    61
                .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q4_K, data.to_vec()));
    177
    61
            self
    178
    61
        }
    179
    180
        /// Add a Q5_K tensor (176 bytes per 256 elements)
    181
        #[must_use]
    182
    7
        pub fn add_q5_k_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self {
    183
    7
            self.tensors
    184
    7
                .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q5_K, data.to_vec()));
    185
    7
            self
    186
    7
        }
    187
    188
        /// Add a Q6_K tensor (210 bytes per 256 elements)
    189
        #[must_use]
    190
    7
        pub fn add_q6_k_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self {
    191
    7
            self.tensors
    192
    7
                .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q6_K, data.to_vec()));
    193
    7
            self
    194
    7
        }
    195
    196
        // =========================================================================
    197
        // Build
    198
        // =========================================================================
    199
    200
        /// Build the GGUF file as a byte vector
    201
        #[must_use]
    202
    18
        pub fn build(self) -> Vec<u8> {
    203
    18
            let mut data = Vec::new();
    204
    205
            // Header
    206
    18
            data.extend_from_slice(&GGUF_MAGIC.to_le_bytes());
    207
    18
            data.extend_from_slice(&GGUF_VERSION_V3.to_le_bytes());
    208
    18
            data.extend_from_slice(&(self.tensors.len() as u64).to_le_bytes());
    209
    18
            data.extend_from_slice(&(self.metadata.len() as u64).to_le_bytes());
    210
    211
            // Metadata
    212
    134
            for (key116, value_type116, value_bytes116) in &self.metadata {
    213
    116
                // Key string: u64 length + UTF-8 bytes
    214
    116
                data.extend_from_slice(&(key.len() as u64).to_le_bytes());
    215
    116
                data.extend_from_slice(key.as_bytes());
    216
    116
                // Value type
    217
    116
                data.extend_from_slice(&value_type.to_le_bytes());
    218
    116
                // Value bytes
    219
    116
                data.extend_from_slice(value_bytes);
    220
    116
            }
    221
    222
            // Tensor info
    223
    18
            let mut tensor_data_offset = 0u64;
    224
    159
            for (name141, dims141, qtype141, tensor_bytes141) in &self.tensors {
    225
                // Name string
    226
    141
                data.extend_from_slice(&(name.len() as u64).to_le_bytes());
    227
    141
                data.extend_from_slice(name.as_bytes());
    228
    229
                // n_dims
    230
    141
                data.extend_from_slice(&(dims.len() as u32).to_le_bytes());
    231
    232
                // Dimensions (reversed for GGML order)
    233
    243
                for dim in dims.iter()141.rev141() {
    234
    243
                    data.extend_from_slice(&dim.to_le_bytes());
    235
    243
                }
    236
    237
                // Quantization type
    238
    141
                data.extend_from_slice(&qtype.to_le_bytes());
    239
    240
                // Offset (relative to tensor data start)
    241
    141
                data.extend_from_slice(&tensor_data_offset.to_le_bytes());
    242
    243
    141
                tensor_data_offset += tensor_bytes.len() as u64;
    244
            }
    245
    246
            // Align to GGUF_ALIGNMENT (32 bytes)
    247
    18
            let current_len = data.len();
    248
    18
            let aligned = current_len.div_ceil(GGUF_ALIGNMENT) * GGUF_ALIGNMENT;
    249
    18
            data.resize(aligned, 0);
    250
    251
            // Tensor data
    252
    159
            for (_, _, _, tensor_bytes141) in &self.tensors {
    253
    141
                data.extend_from_slice(tensor_bytes);
    254
    141
            }
    255
    256
    18
            data
    257
    18
        }
    258
    }
    259
    260
    // =============================================================================
    261
    // Helper Functions for Creating Quantized Data
    262
    // =============================================================================
    263
    264
    /// Create valid Q4_0 data for a tensor with given dimensions
    265
    /// Q4_0: 18 bytes per 32 elements (2 f16 scale + 16 bytes quants)
    266
    #[must_use]
    267
    5
    pub fn create_q4_0_data(num_elements: usize) -> Vec<u8> {
    268
    5
        let num_blocks = num_elements.div_ceil(32);
    269
    5
        let mut data = Vec::with_capacity(num_blocks * 18);
    270
    271
    898
        for _ in 0..num_blocks5 {
    272
    898
            // f16 scale = 0.1
    273
    898
            let scale = half::f16::from_f32(0.1);
    274
    898
            data.extend_from_slice(&scale.to_le_bytes());
    275
    898
            // 16 bytes of quants (mid-range values)
    276
    898
            data.extend([0x88u8; 16]);
    277
    898
        }
    278
    279
    5
        data
    280
    5
    }
    281
    282
    /// Create valid Q8_0 data for a tensor with given dimensions
    283
    /// Q8_0: 34 bytes per 32 elements (2 f16 scale + 32 i8 quants)
    284
    #[must_use]
    285
    5
    pub fn create_q8_0_data(num_elements: usize) -> Vec<u8> {
    286
    5
        let num_blocks = num_elements.div_ceil(32);
    287
    5
        let mut data = Vec::with_capacity(num_blocks * 34);
    288
    289
    898
        for _ in 0..num_blocks5 {
    290
    898
            // f16 scale = 0.1
    291
    898
            let scale = half::f16::from_f32(0.1);
    292
    898
            data.extend_from_slice(&scale.to_le_bytes());
    293
    898
            // 32 i8 quants (zeros)
    294
    898
            data.extend([0i8 as u8; 32]);
    295
    898
        }
    296
    297
    5
        data
    298
    5
    }
    299
    300
    /// Create valid Q4_K data for a tensor with given dimensions
    301
    /// Q4_K: 144 bytes per 256 elements
    302
    #[must_use]
    303
    55
    pub fn create_q4_k_data(num_elements: usize) -> Vec<u8> {
    304
    55
        let num_super_blocks = num_elements.div_ceil(256);
    305
    55
        vec![0u8; num_super_blocks * 144]
    306
    55
    }
    307
    308
    /// Create valid Q5_K data for a tensor with given dimensions
    309
    /// Q5_K: 176 bytes per 256 elements
    310
    #[must_use]
    311
    5
    pub fn create_q5_k_data(num_elements: usize) -> Vec<u8> {
    312
    5
        let num_super_blocks = num_elements.div_ceil(256);
    313
    5
        vec![0u8; num_super_blocks * 176]
    314
    5
    }
    315
    316
    /// Create valid Q6_K data for a tensor with given dimensions
    317
    /// Q6_K: 210 bytes per 256 elements
    318
    #[must_use]
    319
    5
    pub fn create_q6_k_data(num_elements: usize) -> Vec<u8> {
    320
    5
        let num_super_blocks = num_elements.div_ceil(256);
    321
    5
        vec![0u8; num_super_blocks * 210]
    322
    5
    }
    323
    324
    /// Create F32 embedding data (small random-ish values)
    325
    #[must_use]
    326
    21
    pub fn create_f32_embedding_data(vocab_size: usize, hidden_dim: usize) -> Vec<f32> {
    327
    21
        let mut data = Vec::with_capacity(vocab_size * hidden_dim);
    328
    153k
        for i in 0..(vocab_size * hidden_dim)21 {
    329
    153k
            // Pseudo-random but deterministic values
    330
    153k
            let val = ((i % 1000) as f32 - 500.0) / 5000.0;
    331
    153k
            data.push(val);
    332
    153k
        }
    333
    21
        data
    334
    21
    }
    335
    336
    /// Create F32 norm weights (typically ~1.0)
    337
    #[must_use]
    338
    19
    pub fn create_f32_norm_weights(dim: usize) -> Vec<f32> {
    339
    19
        vec![1.0f32; dim]
    340
    19
    }
    341
    342
    // =============================================================================
    343
    // Complete Model Builder
    344
    // =============================================================================
    345
    346
    /// Build a minimal valid LLaMA-style GGUF model
    347
    ///
    348
    /// This creates a complete model with:
    349
    /// - Token embeddings (F32)
    350
    /// - One transformer layer with Q4_K weights
    351
    /// - Output norm (F32)
    352
    /// - LM head (tied to token embeddings)
    353
    #[must_use]
    354
    5
    pub fn build_minimal_llama_gguf(
    355
    5
        vocab_size: usize,
    356
    5
        hidden_dim: usize,
    357
    5
        intermediate_dim: usize,
    358
    5
        num_heads: usize,
    359
    5
        num_kv_heads: usize,
    360
    5
    ) -> Vec<u8> {
    361
    5
        let head_dim = hidden_dim / num_heads;
    362
    5
        let kv_dim = num_kv_heads * head_dim;
    363
    364
        // Create tensor data
    365
    5
        let embed_data = create_f32_embedding_data(vocab_size, hidden_dim);
    366
    5
        let norm_data = create_f32_norm_weights(hidden_dim);
    367
    368
        // Q4_K weights for layer 0
    369
    5
        let q_data = create_q4_k_data(hidden_dim * hidden_dim);
    370
    5
        let k_data = create_q4_k_data(hidden_dim * kv_dim);
    371
    5
        let v_data = create_q4_k_data(hidden_dim * kv_dim);
    372
    5
        let attn_out_data = create_q4_k_data(hidden_dim * hidden_dim);
    373
    5
        let ffn_up_data = create_q4_k_data(hidden_dim * intermediate_dim);
    374
    5
        let ffn_down_data = create_q4_k_data(intermediate_dim * hidden_dim);
    375
    5
        let ffn_gate_data = create_q4_k_data(hidden_dim * intermediate_dim);
    376
    377
    5
        GGUFBuilder::new()
    378
            // Metadata
    379
    5
            .architecture("llama")
    380
    5
            .hidden_dim("llama", hidden_dim as u32)
    381
    5
            .num_layers("llama", 1)
    382
    5
            .num_heads("llama", num_heads as u32)
    383
    5
            .num_kv_heads("llama", num_kv_heads as u32)
    384
    5
            .context_length("llama", 256)
    385
    5
            .rope_freq_base("llama", 10000.0)
    386
    5
            .rms_epsilon("llama", 1e-5)
    387
    5
            .ffn_hidden_dim("llama", intermediate_dim as u32)
    388
            // Token embedding
    389
    5
            .add_f32_tensor(
    390
    5
                "token_embd.weight",
    391
    5
                &[vocab_size as u64, hidden_dim as u64],
    392
    5
                &embed_data,
    393
            )
    394
            // Layer 0 attention
    395
    5
            .add_f32_tensor("blk.0.attn_norm.weight", &[hidden_dim as u64], &norm_data)
    396
    5
            .add_q4_k_tensor(
    397
    5
                "blk.0.attn_q.weight",
    398
    5
                &[hidden_dim as u64, hidden_dim as u64],
    399
    5
                &q_data,
    400
            )
    401
    5
            .add_q4_k_tensor(
    402
    5
                "blk.0.attn_k.weight",
    403
    5
                &[hidden_dim as u64, kv_dim as u64],
    404
    5
                &k_data,
    405
            )
    406
    5
            .add_q4_k_tensor(
    407
    5
                "blk.0.attn_v.weight",
    408
    5
                &[hidden_dim as u64, kv_dim as u64],
    409
    5
                &v_data,
    410
            )
    411
    5
            .add_q4_k_tensor(
    412
    5
                "blk.0.attn_output.weight",
    413
    5
                &[hidden_dim as u64, hidden_dim as u64],
    414
    5
                &attn_out_data,
    415
            )
    416
            // Layer 0 FFN
    417
    5
            .add_f32_tensor("blk.0.ffn_norm.weight", &[hidden_dim as u64], &norm_data)
    418
    5
            .add_q4_k_tensor(
    419
    5
                "blk.0.ffn_up.weight",
    420
    5
                &[hidden_dim as u64, intermediate_dim as u64],
    421
    5
                &ffn_up_data,
    422
            )
    423
    5
            .add_q4_k_tensor(
    424
    5
                "blk.0.ffn_down.weight",
    425
    5
                &[intermediate_dim as u64, hidden_dim as u64],
    426
    5
                &ffn_down_data,
    427
            )
    428
    5
            .add_q4_k_tensor(
    429
    5
                "blk.0.ffn_gate.weight",
    430
    5
                &[hidden_dim as u64, intermediate_dim as u64],
    431
    5
                &ffn_gate_data,
    432
            )
    433
            // Output norm and head
    434
    5
            .add_f32_tensor("output_norm.weight", &[hidden_dim as u64], &norm_data)
    435
            // Note: LM head often tied to token_embd, so we don't add output.weight
    436
            // The loader will fallback to token_embd.weight
    437
    5
            .build()
    438
    5
    }
    439
    440
    /// Build a minimal Phi-2 style GGUF model (fused QKV)
    441
    #[must_use]
    442
    1
    pub fn build_minimal_phi2_gguf(
    443
    1
        vocab_size: usize,
    444
    1
        hidden_dim: usize,
    445
    1
        intermediate_dim: usize,
    446
    1
        num_heads: usize,
    447
    1
    ) -> Vec<u8> {
    448
        // Create tensor data
    449
    1
        let embed_data = create_f32_embedding_data(vocab_size, hidden_dim);
    450
    1
        let norm_data = create_f32_norm_weights(hidden_dim);
    451
    452
        // Fused QKV: hidden -> 3 * hidden
    453
    1
        let qkv_out_dim = 3 * hidden_dim;
    454
    1
        let qkv_data = create_q4_k_data(hidden_dim * qkv_out_dim);
    455
    1
        let attn_out_data = create_q4_k_data(hidden_dim * hidden_dim);
    456
    1
        let ffn_up_data = create_q4_k_data(hidden_dim * intermediate_dim);
    457
    1
        let ffn_down_data = create_q4_k_data(intermediate_dim * hidden_dim);
    458
    459
    1
        GGUFBuilder::new()
    460
            // Metadata
    461
    1
            .architecture("phi2")
    462
    1
            .hidden_dim("phi2", hidden_dim as u32)
    463
    1
            .num_layers("phi2", 1)
    464
    1
            .num_heads("phi2", num_heads as u32)
    465
    1
            .num_kv_heads("phi2", num_heads as u32) // MHA, not GQA
    466
    1
            .context_length("phi2", 256)
    467
    1
            .rope_freq_base("phi2", 10000.0)
    468
    1
            .rms_epsilon("phi2", 1e-5)
    469
            // Token embedding
    470
    1
            .add_f32_tensor(
    471
    1
                "token_embd.weight",
    472
    1
                &[vocab_size as u64, hidden_dim as u64],
    473
    1
                &embed_data,
    474
            )
    475
            // Layer 0 attention (fused QKV)
    476
    1
            .add_f32_tensor("blk.0.attn_norm.weight", &[hidden_dim as u64], &norm_data)
    477
    1
            .add_q4_k_tensor(
    478
    1
                "blk.0.attn_qkv.weight",
    479
    1
                &[hidden_dim as u64, qkv_out_dim as u64],
    480
    1
                &qkv_data,
    481
            )
    482
    1
            .add_q4_k_tensor(
    483
    1
                "blk.0.attn_output.weight",
    484
    1
                &[hidden_dim as u64, hidden_dim as u64],
    485
    1
                &attn_out_data,
    486
            )
    487
            // Layer 0 FFN (no gate for Phi-2 style GELU)
    488
    1
            .add_f32_tensor("blk.0.ffn_norm.weight", &[hidden_dim as u64], &norm_data)
    489
    1
            .add_q4_k_tensor(
    490
    1
                "blk.0.ffn_up.weight",
    491
    1
                &[hidden_dim as u64, intermediate_dim as u64],
    492
    1
                &ffn_up_data,
    493
            )
    494
    1
            .add_q4_k_tensor(
    495
    1
                "blk.0.ffn_down.weight",
    496
    1
                &[intermediate_dim as u64, hidden_dim as u64],
    497
    1
                &ffn_down_data,
    498
            )
    499
            // Output
    500
    1
            .add_f32_tensor("output_norm.weight", &[hidden_dim as u64], &norm_data)
    501
    1
            .build()
    502
    1
    }
    503
    504
    #[cfg(test)]
    505
    mod tests {
    506
        use super::*;
    507
        use crate::gguf::GGUFModel;
    508
    509
        #[test]
    510
    1
        fn test_gguf_builder_empty() {
    511
    1
            let data = GGUFBuilder::new().build();
    512
    513
            // Should have valid header
    514
    1
            assert!(data.len() >= 24); // magic + version + 2 counts
    515
    516
    1
            let model = GGUFModel::from_bytes(&data).expect("Should parse empty GGUF");
    517
    1
            assert_eq!(model.header.magic, GGUF_MAGIC);
    518
    1
            assert_eq!(model.header.version, GGUF_VERSION_V3);
    519
    1
            assert_eq!(model.metadata.len(), 0);
    520
    1
            assert_eq!(model.tensors.len(), 0);
    521
    1
        }
    522
    523
        #[test]
    524
    1
        fn test_gguf_builder_metadata_only() {
    525
    1
            let data = GGUFBuilder::new()
    526
    1
                .architecture("llama")
    527
    1
                .add_u32("test.value", 42)
    528
    1
                .add_f32("test.float", 3.14)
    529
    1
                .build();
    530
    531
    1
            let model = GGUFModel::from_bytes(&data).expect("Should parse");
    532
    1
            assert_eq!(model.metadata.len(), 3);
    533
    1
            assert_eq!(model.architecture(), Some("llama"));
    534
    1
        }
    535
    536
        #[test]
    537
    1
        fn test_gguf_builder_with_tensor() {
    538
    1
            let data = GGUFBuilder::new()
    539
    1
                .add_f32_tensor("test.weight", &[4, 8], &vec![0.0f32; 32])
    540
    1
                .build();
    541
    542
    1
            let model = GGUFModel::from_bytes(&data).expect("Should parse");
    543
    1
            assert_eq!(model.tensors.len(), 1);
    544
    1
            assert_eq!(model.tensors[0].name, "test.weight");
    545
    1
            assert_eq!(model.tensors[0].n_dims, 2);
    546
    1
        }
    547
    548
        #[test]
    549
    1
        fn test_gguf_builder_q4_k_tensor() {
    550
    1
            let q4k_data = create_q4_k_data(256);
    551
    1
            let data = GGUFBuilder::new()
    552
    1
                .add_q4_k_tensor("layer.weight", &[256], &q4k_data)
    553
    1
                .build();
    554
    555
    1
            let model = GGUFModel::from_bytes(&data).expect("Should parse");
    556
    1
            assert_eq!(model.tensors[0].qtype, GGUF_TYPE_Q4_K);
    557
    1
        }
    558
    559
        #[test]
    560
    1
        fn test_minimal_llama_model() {
    561
    1
            let data = build_minimal_llama_gguf(100, 64, 128, 4, 4);
    562
    563
    1
            let model = GGUFModel::from_bytes(&data).expect("Should parse minimal LLaMA");
    564
    565
    1
            assert_eq!(model.architecture(), Some("llama"));
    566
    1
            assert_eq!(model.embedding_dim(), Some(64));
    567
    1
            assert_eq!(model.num_layers(), Some(1));
    568
    1
            assert_eq!(model.num_heads(), Some(4));
    569
    570
            // Should have all expected tensors
    571
    11
            let tensor_names1: Vec<_>1 = model.tensors.iter()1.map1(|t| t.name.as_str()).collect1();
    572
    1
            assert!(tensor_names.contains(&"token_embd.weight"));
    573
    1
            assert!(tensor_names.contains(&"blk.0.attn_q.weight"));
    574
    1
            assert!(tensor_names.contains(&"blk.0.ffn_up.weight"));
    575
    1
            assert!(tensor_names.contains(&"output_norm.weight"));
    576
    1
        }
    577
    }