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
}