Skip to main content

presentar_test/
fixture.rs

1//! Fixture loading system for Presentar tests.
2//!
3//! Provides utilities for loading test fixtures from tar archives,
4//! directories, and inline definitions. Zero external dependencies.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use presentar_test::fixture::{Fixture, FixtureBuilder};
10//!
11//! // Load from embedded tar archive
12//! let fixture = Fixture::from_tar(include_bytes!("fixtures/app.tar"))?;
13//!
14//! // Access fixture files
15//! let app_yaml = fixture.get_file("app.yaml")?;
16//! let data = fixture.get_data("metrics.ald")?;
17//! ```
18
19use std::collections::HashMap;
20
21/// Error type for fixture operations.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum FixtureError {
24    /// Failed to parse tar archive
25    InvalidTar(String),
26    /// File not found in fixture
27    FileNotFound(String),
28    /// Invalid fixture format
29    InvalidFormat(String),
30    /// IO error (represented as string for no-std compatibility)
31    IoError(String),
32    /// YAML parsing error
33    YamlError(String),
34}
35
36impl std::fmt::Display for FixtureError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::InvalidTar(msg) => write!(f, "invalid tar archive: {msg}"),
40            Self::FileNotFound(path) => write!(f, "file not found: {path}"),
41            Self::InvalidFormat(msg) => write!(f, "invalid fixture format: {msg}"),
42            Self::IoError(msg) => write!(f, "IO error: {msg}"),
43            Self::YamlError(msg) => write!(f, "YAML error: {msg}"),
44        }
45    }
46}
47
48impl std::error::Error for FixtureError {}
49
50/// A test fixture containing files and configuration.
51#[derive(Debug, Clone)]
52pub struct Fixture {
53    /// Files by path
54    files: HashMap<String, Vec<u8>>,
55    /// Fixture manifest
56    manifest: FixtureManifest,
57}
58
59/// Fixture manifest describing contents.
60#[derive(Debug, Clone, Default)]
61pub struct FixtureManifest {
62    /// Fixture name
63    pub name: String,
64    /// App YAML path (if present)
65    pub app_yaml: Option<String>,
66    /// Data files (.ald)
67    pub data_files: Vec<String>,
68    /// Model files (.apr)
69    pub model_files: Vec<String>,
70    /// Asset files (images, fonts)
71    pub assets: Vec<String>,
72    /// Snapshot baselines
73    pub snapshots: Vec<String>,
74}
75
76impl Fixture {
77    /// Create a new empty fixture.
78    #[must_use]
79    pub fn new() -> Self {
80        Self {
81            files: HashMap::new(),
82            manifest: FixtureManifest::default(),
83        }
84    }
85
86    /// Create fixture from a tar archive (embedded bytes).
87    ///
88    /// # Errors
89    ///
90    /// Returns error if tar parsing fails.
91    pub fn from_tar(data: &[u8]) -> Result<Self, FixtureError> {
92        let mut fixture = Self::new();
93        fixture.parse_tar(data)?;
94        fixture.build_manifest();
95        Ok(fixture)
96    }
97
98    /// Create fixture from inline files.
99    #[must_use]
100    pub fn from_files(files: Vec<(&str, &[u8])>) -> Self {
101        let mut fixture = Self::new();
102        for (path, content) in files {
103            fixture.files.insert(path.to_string(), content.to_vec());
104        }
105        fixture.build_manifest();
106        fixture
107    }
108
109    /// Get the fixture manifest.
110    #[must_use]
111    pub fn manifest(&self) -> &FixtureManifest {
112        &self.manifest
113    }
114
115    /// List all file paths in the fixture.
116    #[must_use]
117    pub fn list_files(&self) -> Vec<&str> {
118        self.files.keys().map(String::as_str).collect()
119    }
120
121    /// Check if a file exists.
122    #[must_use]
123    pub fn has_file(&self, path: &str) -> bool {
124        self.files.contains_key(path)
125    }
126
127    /// Get file contents as bytes.
128    pub fn get_file(&self, path: &str) -> Result<&[u8], FixtureError> {
129        self.files
130            .get(path)
131            .map(Vec::as_slice)
132            .ok_or_else(|| FixtureError::FileNotFound(path.to_string()))
133    }
134
135    /// Get file contents as string.
136    pub fn get_file_str(&self, path: &str) -> Result<&str, FixtureError> {
137        let bytes = self.get_file(path)?;
138        std::str::from_utf8(bytes).map_err(|e| FixtureError::InvalidFormat(e.to_string()))
139    }
140
141    /// Get the app.yaml content if present.
142    pub fn get_app_yaml(&self) -> Result<&str, FixtureError> {
143        let path = self
144            .manifest
145            .app_yaml
146            .as_deref()
147            .ok_or_else(|| FixtureError::FileNotFound("app.yaml".to_string()))?;
148        self.get_file_str(path)
149    }
150
151    /// Get a data file (.ald) content.
152    pub fn get_data(&self, name: &str) -> Result<&[u8], FixtureError> {
153        let path = if name.ends_with(".ald") {
154            name.to_string()
155        } else {
156            format!("{name}.ald")
157        };
158        self.get_file(&path)
159    }
160
161    /// Get a model file (.apr) content.
162    pub fn get_model(&self, name: &str) -> Result<&[u8], FixtureError> {
163        let path = if name.ends_with(".apr") {
164            name.to_string()
165        } else {
166            format!("{name}.apr")
167        };
168        self.get_file(&path)
169    }
170
171    /// Get a snapshot baseline image.
172    pub fn get_snapshot(&self, name: &str) -> Result<&[u8], FixtureError> {
173        let path = if name.contains('/') {
174            name.to_string()
175        } else {
176            format!("snapshots/{name}.png")
177        };
178        self.get_file(&path)
179    }
180
181    /// Add a file to the fixture.
182    pub fn add_file(&mut self, path: &str, content: Vec<u8>) {
183        self.files.insert(path.to_string(), content);
184    }
185
186    /// Remove a file from the fixture.
187    pub fn remove_file(&mut self, path: &str) -> Option<Vec<u8>> {
188        self.files.remove(path)
189    }
190
191    /// Parse a minimal tar archive format.
192    fn parse_tar(&mut self, data: &[u8]) -> Result<(), FixtureError> {
193        const BLOCK_SIZE: usize = 512;
194
195        let mut pos = 0;
196        while pos + BLOCK_SIZE <= data.len() {
197            let header = &data[pos..pos + BLOCK_SIZE];
198
199            // Check for end of archive (two empty blocks)
200            if header.iter().all(|&b| b == 0) {
201                break;
202            }
203
204            // Parse tar header
205            let name = Self::parse_tar_string(&header[0..100]);
206            if name.is_empty() {
207                break;
208            }
209
210            // Parse file size (octal)
211            let size_str = Self::parse_tar_string(&header[124..136]);
212            let size = usize::from_str_radix(size_str.trim(), 8)
213                .map_err(|_| FixtureError::InvalidTar("invalid file size".to_string()))?;
214
215            // Parse file type (0 = regular file, 5 = directory)
216            let typeflag = header[156];
217
218            pos += BLOCK_SIZE; // Move past header
219
220            // Only process regular files
221            if typeflag == b'0' || typeflag == 0 {
222                let content = if pos + size <= data.len() {
223                    data[pos..pos + size].to_vec()
224                } else {
225                    return Err(FixtureError::InvalidTar(
226                        "truncated file content".to_string(),
227                    ));
228                };
229
230                self.files.insert(name, content);
231            }
232
233            // Move to next block boundary
234            pos += (size + BLOCK_SIZE - 1) / BLOCK_SIZE * BLOCK_SIZE;
235        }
236
237        Ok(())
238    }
239
240    /// Parse a null-terminated string from tar header.
241    fn parse_tar_string(data: &[u8]) -> String {
242        let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
243        String::from_utf8_lossy(&data[..end]).trim().to_string()
244    }
245
246    /// Build manifest from loaded files.
247    fn build_manifest(&mut self) {
248        // Find app.yaml
249        for name in &["app.yaml", "app.yml", "presentar.yaml", "presentar.yml"] {
250            if self.files.contains_key(*name) {
251                self.manifest.app_yaml = Some((*name).to_string());
252                break;
253            }
254        }
255
256        // Categorize files
257        for path in self.files.keys() {
258            if path.ends_with(".ald") {
259                self.manifest.data_files.push(path.clone());
260            } else if path.ends_with(".apr") {
261                self.manifest.model_files.push(path.clone());
262            } else if path.ends_with(".png") || path.ends_with(".jpg") || path.ends_with(".svg") {
263                if path.starts_with("snapshots/") {
264                    self.manifest.snapshots.push(path.clone());
265                } else {
266                    self.manifest.assets.push(path.clone());
267                }
268            } else if path.ends_with(".ttf") || path.ends_with(".otf") || path.ends_with(".woff2") {
269                self.manifest.assets.push(path.clone());
270            }
271        }
272    }
273
274    /// Get fixture file count.
275    #[must_use]
276    pub fn file_count(&self) -> usize {
277        self.files.len()
278    }
279
280    /// Get total fixture size in bytes.
281    #[must_use]
282    pub fn total_size(&self) -> usize {
283        self.files.values().map(Vec::len).sum()
284    }
285}
286
287impl Default for Fixture {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293/// Builder for creating fixtures programmatically.
294#[derive(Debug, Default)]
295pub struct FixtureBuilder {
296    files: Vec<(String, Vec<u8>)>,
297    name: String,
298}
299
300impl FixtureBuilder {
301    /// Create a new fixture builder.
302    #[must_use]
303    pub fn new(name: impl Into<String>) -> Self {
304        Self {
305            files: Vec::new(),
306            name: name.into(),
307        }
308    }
309
310    /// Add a file with string content.
311    #[must_use]
312    pub fn file(mut self, path: impl Into<String>, content: impl Into<String>) -> Self {
313        self.files.push((path.into(), content.into().into_bytes()));
314        self
315    }
316
317    /// Add a file with binary content.
318    #[must_use]
319    pub fn binary_file(mut self, path: impl Into<String>, content: Vec<u8>) -> Self {
320        self.files.push((path.into(), content));
321        self
322    }
323
324    /// Add app.yaml configuration.
325    #[must_use]
326    pub fn app_yaml(self, content: impl Into<String>) -> Self {
327        self.file("app.yaml", content)
328    }
329
330    /// Add a data file.
331    #[must_use]
332    pub fn data(self, name: impl Into<String>, content: Vec<u8>) -> Self {
333        let name = name.into();
334        let path = if name.ends_with(".ald") {
335            name
336        } else {
337            format!("{name}.ald")
338        };
339        self.binary_file(path, content)
340    }
341
342    /// Add a snapshot baseline.
343    #[must_use]
344    pub fn snapshot(self, name: impl Into<String>, png_data: Vec<u8>) -> Self {
345        let name = name.into();
346        let path = format!("snapshots/{name}.png");
347        self.binary_file(path, png_data)
348    }
349
350    /// Build the fixture.
351    #[must_use]
352    pub fn build(self) -> Fixture {
353        let files: Vec<(&str, &[u8])> = self
354            .files
355            .iter()
356            .map(|(p, c)| (p.as_str(), c.as_slice()))
357            .collect();
358        let mut fixture = Fixture::from_files(files);
359        fixture.manifest.name = self.name;
360        fixture
361    }
362}
363
364/// Test data generator for common test scenarios.
365pub struct TestData;
366
367impl TestData {
368    /// Generate sample metrics data.
369    #[must_use]
370    pub fn metrics_json(count: usize) -> String {
371        let mut data = String::from("[");
372        for i in 0..count {
373            if i > 0 {
374                data.push(',');
375            }
376            data.push_str(&format!(
377                r#"{{"timestamp":{},"cpu":{},"memory":{},"requests":{}}}"#,
378                1700000000 + i * 60,
379                20.0 + (i as f64 * 0.5).sin() * 10.0,
380                45.0 + (i as f64 * 0.3).cos() * 15.0,
381                100 + (i % 50)
382            ));
383        }
384        data.push(']');
385        data
386    }
387
388    /// Generate sample chart data.
389    #[must_use]
390    pub fn chart_points(count: usize) -> Vec<(f64, f64)> {
391        (0..count)
392            .map(|i| {
393                let x = i as f64;
394                let y = (x * 0.1).sin() * 50.0 + 50.0;
395                (x, y)
396            })
397            .collect()
398    }
399
400    /// Generate sample table data as CSV.
401    #[must_use]
402    pub fn table_csv(rows: usize, cols: usize) -> String {
403        let mut csv = String::new();
404
405        // Header
406        for c in 0..cols {
407            if c > 0 {
408                csv.push(',');
409            }
410            csv.push_str(&format!("col{c}"));
411        }
412        csv.push('\n');
413
414        // Rows
415        for r in 0..rows {
416            for c in 0..cols {
417                if c > 0 {
418                    csv.push(',');
419                }
420                csv.push_str(&format!("r{r}c{c}"));
421            }
422            csv.push('\n');
423        }
424
425        csv
426    }
427
428    /// Generate minimal PNG image data.
429    #[must_use]
430    pub fn minimal_png(width: u32, height: u32, color: [u8; 4]) -> Vec<u8> {
431        // This is a minimal valid PNG with solid color
432        // In real implementation would use proper PNG encoding
433        let mut png = Vec::new();
434
435        // PNG signature
436        png.extend_from_slice(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]);
437
438        // IHDR chunk
439        let mut ihdr = Vec::new();
440        ihdr.extend_from_slice(&width.to_be_bytes());
441        ihdr.extend_from_slice(&height.to_be_bytes());
442        ihdr.push(8); // bit depth
443        ihdr.push(6); // color type (RGBA)
444        ihdr.push(0); // compression
445        ihdr.push(0); // filter
446        ihdr.push(0); // interlace
447
448        png.extend_from_slice(&(ihdr.len() as u32).to_be_bytes());
449        png.extend_from_slice(b"IHDR");
450        png.extend_from_slice(&ihdr);
451        png.extend_from_slice(&Self::crc32(b"IHDR", &ihdr).to_be_bytes());
452
453        // IDAT chunk (minimal - just one pixel repeated conceptually)
454        // This is simplified; real PNG needs proper zlib compression
455        let mut idat = vec![0x08, 0x1D]; // zlib header
456        let pixel_row_size = 1 + width as usize * 4; // filter byte + RGBA
457        let raw_size = pixel_row_size * height as usize;
458
459        // Simplified: store uncompressed (this is not valid zlib but demonstrates structure)
460        idat.push(0x01); // final block, uncompressed
461        idat.extend_from_slice(&(raw_size as u16).to_le_bytes());
462        idat.extend_from_slice(&(!(raw_size as u16)).to_le_bytes());
463
464        for _ in 0..height {
465            idat.push(0); // filter: none
466            for _ in 0..width {
467                idat.extend_from_slice(&color);
468            }
469        }
470
471        // Adler32 checksum (simplified)
472        let adler = Self::adler32(&idat[2..]);
473        idat.extend_from_slice(&adler.to_be_bytes());
474
475        png.extend_from_slice(&(idat.len() as u32).to_be_bytes());
476        png.extend_from_slice(b"IDAT");
477        png.extend_from_slice(&idat);
478        png.extend_from_slice(&Self::crc32(b"IDAT", &idat).to_be_bytes());
479
480        // IEND chunk
481        png.extend_from_slice(&0u32.to_be_bytes());
482        png.extend_from_slice(b"IEND");
483        png.extend_from_slice(&Self::crc32(b"IEND", &[]).to_be_bytes());
484
485        png
486    }
487
488    /// Compute CRC32 for PNG chunk.
489    fn crc32(chunk_type: &[u8], data: &[u8]) -> u32 {
490        const CRC_TABLE: [u32; 256] = {
491            let mut table = [0u32; 256];
492            let mut i = 0;
493            while i < 256 {
494                let mut c = i as u32;
495                let mut k = 0;
496                while k < 8 {
497                    if c & 1 != 0 {
498                        c = 0xedb8_8320 ^ (c >> 1);
499                    } else {
500                        c >>= 1;
501                    }
502                    k += 1;
503                }
504                table[i] = c;
505                i += 1;
506            }
507            table
508        };
509
510        let mut crc = 0xFFFF_FFFF_u32;
511        for &byte in chunk_type.iter().chain(data.iter()) {
512            crc = CRC_TABLE[((crc ^ byte as u32) & 0xFF) as usize] ^ (crc >> 8);
513        }
514        !crc
515    }
516
517    /// Compute Adler32 for zlib.
518    fn adler32(data: &[u8]) -> u32 {
519        let mut a: u32 = 1;
520        let mut b: u32 = 0;
521        for &byte in data {
522            a = (a + byte as u32) % 65521;
523            b = (b + a) % 65521;
524        }
525        (b << 16) | a
526    }
527}
528
529/// Context for fixture-based tests.
530#[derive(Debug)]
531pub struct FixtureContext {
532    /// The loaded fixture
533    pub fixture: Fixture,
534    /// Test name
535    pub test_name: String,
536    /// Output directory for generated files
537    pub output_dir: Option<String>,
538}
539
540impl FixtureContext {
541    /// Create a new fixture context.
542    #[must_use]
543    pub fn new(fixture: Fixture, test_name: impl Into<String>) -> Self {
544        Self {
545            fixture,
546            test_name: test_name.into(),
547            output_dir: None,
548        }
549    }
550
551    /// Set output directory.
552    #[must_use]
553    pub fn with_output_dir(mut self, dir: impl Into<String>) -> Self {
554        self.output_dir = Some(dir.into());
555        self
556    }
557
558    /// Get the fixture.
559    #[must_use]
560    pub fn fixture(&self) -> &Fixture {
561        &self.fixture
562    }
563
564    /// Get app yaml.
565    pub fn app_yaml(&self) -> Result<&str, FixtureError> {
566        self.fixture.get_app_yaml()
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    // =========================================================================
575    // Fixture Tests
576    // =========================================================================
577
578    #[test]
579    fn test_fixture_new() {
580        let fixture = Fixture::new();
581        assert_eq!(fixture.file_count(), 0);
582        assert_eq!(fixture.total_size(), 0);
583    }
584
585    #[test]
586    fn test_fixture_from_files() {
587        let fixture = Fixture::from_files(vec![
588            ("app.yaml", b"name: test" as &[u8]),
589            ("data.ald", b"binary data"),
590        ]);
591
592        assert_eq!(fixture.file_count(), 2);
593        assert!(fixture.has_file("app.yaml"));
594        assert!(fixture.has_file("data.ald"));
595    }
596
597    #[test]
598    fn test_fixture_get_file() {
599        let fixture = Fixture::from_files(vec![("test.txt", b"hello world" as &[u8])]);
600
601        let content = fixture.get_file("test.txt").unwrap();
602        assert_eq!(content, b"hello world");
603    }
604
605    #[test]
606    fn test_fixture_get_file_str() {
607        let fixture = Fixture::from_files(vec![("test.txt", b"hello world" as &[u8])]);
608
609        let content = fixture.get_file_str("test.txt").unwrap();
610        assert_eq!(content, "hello world");
611    }
612
613    #[test]
614    fn test_fixture_get_file_not_found() {
615        let fixture = Fixture::new();
616        let result = fixture.get_file("missing.txt");
617        assert!(matches!(result, Err(FixtureError::FileNotFound(_))));
618    }
619
620    #[test]
621    fn test_fixture_list_files() {
622        let fixture = Fixture::from_files(vec![
623            ("a.txt", b"a" as &[u8]),
624            ("b.txt", b"b"),
625            ("c.txt", b"c"),
626        ]);
627
628        let files = fixture.list_files();
629        assert_eq!(files.len(), 3);
630        assert!(files.contains(&"a.txt"));
631        assert!(files.contains(&"b.txt"));
632        assert!(files.contains(&"c.txt"));
633    }
634
635    #[test]
636    fn test_fixture_add_remove_file() {
637        let mut fixture = Fixture::new();
638
639        fixture.add_file("test.txt", b"content".to_vec());
640        assert!(fixture.has_file("test.txt"));
641
642        let removed = fixture.remove_file("test.txt");
643        assert_eq!(removed, Some(b"content".to_vec()));
644        assert!(!fixture.has_file("test.txt"));
645    }
646
647    #[test]
648    fn test_fixture_total_size() {
649        let fixture = Fixture::from_files(vec![("a.txt", b"12345" as &[u8]), ("b.txt", b"67890")]);
650
651        assert_eq!(fixture.total_size(), 10);
652    }
653
654    // =========================================================================
655    // Manifest Tests
656    // =========================================================================
657
658    #[test]
659    fn test_manifest_app_yaml_detection() {
660        let fixture = Fixture::from_files(vec![("app.yaml", b"name: test" as &[u8])]);
661
662        assert_eq!(fixture.manifest().app_yaml, Some("app.yaml".to_string()));
663    }
664
665    #[test]
666    fn test_manifest_app_yml_detection() {
667        let fixture = Fixture::from_files(vec![("app.yml", b"name: test" as &[u8])]);
668
669        assert_eq!(fixture.manifest().app_yaml, Some("app.yml".to_string()));
670    }
671
672    #[test]
673    fn test_manifest_data_files() {
674        let fixture = Fixture::from_files(vec![
675            ("metrics.ald", b"data" as &[u8]),
676            ("users.ald", b"data"),
677        ]);
678
679        assert_eq!(fixture.manifest().data_files.len(), 2);
680    }
681
682    #[test]
683    fn test_manifest_model_files() {
684        let fixture = Fixture::from_files(vec![("model.apr", b"data" as &[u8])]);
685
686        assert_eq!(fixture.manifest().model_files.len(), 1);
687    }
688
689    #[test]
690    fn test_manifest_snapshots() {
691        let fixture = Fixture::from_files(vec![
692            ("snapshots/button.png", b"png" as &[u8]),
693            ("snapshots/chart.png", b"png"),
694        ]);
695
696        assert_eq!(fixture.manifest().snapshots.len(), 2);
697    }
698
699    #[test]
700    fn test_manifest_assets() {
701        let fixture = Fixture::from_files(vec![
702            ("assets/logo.png", b"png" as &[u8]),
703            ("fonts/roboto.ttf", b"ttf"),
704        ]);
705
706        assert_eq!(fixture.manifest().assets.len(), 2);
707    }
708
709    // =========================================================================
710    // Get Data/Model Tests
711    // =========================================================================
712
713    #[test]
714    fn test_get_data_with_extension() {
715        let fixture = Fixture::from_files(vec![("metrics.ald", b"data" as &[u8])]);
716
717        let data = fixture.get_data("metrics.ald").unwrap();
718        assert_eq!(data, b"data");
719    }
720
721    #[test]
722    fn test_get_data_without_extension() {
723        let fixture = Fixture::from_files(vec![("metrics.ald", b"data" as &[u8])]);
724
725        let data = fixture.get_data("metrics").unwrap();
726        assert_eq!(data, b"data");
727    }
728
729    #[test]
730    fn test_get_model() {
731        let fixture = Fixture::from_files(vec![("model.apr", b"model data" as &[u8])]);
732
733        let data = fixture.get_model("model").unwrap();
734        assert_eq!(data, b"model data");
735    }
736
737    #[test]
738    fn test_get_snapshot() {
739        let fixture = Fixture::from_files(vec![("snapshots/button.png", b"png" as &[u8])]);
740
741        let data = fixture.get_snapshot("button").unwrap();
742        assert_eq!(data, b"png");
743    }
744
745    // =========================================================================
746    // FixtureBuilder Tests
747    // =========================================================================
748
749    #[test]
750    fn test_builder_new() {
751        let fixture = FixtureBuilder::new("test-fixture").build();
752        assert_eq!(fixture.manifest().name, "test-fixture");
753    }
754
755    #[test]
756    fn test_builder_file() {
757        let fixture = FixtureBuilder::new("test")
758            .file("test.txt", "hello")
759            .build();
760
761        assert_eq!(fixture.get_file_str("test.txt").unwrap(), "hello");
762    }
763
764    #[test]
765    fn test_builder_binary_file() {
766        let fixture = FixtureBuilder::new("test")
767            .binary_file("data.bin", vec![1, 2, 3, 4])
768            .build();
769
770        assert_eq!(fixture.get_file("data.bin").unwrap(), &[1, 2, 3, 4]);
771    }
772
773    #[test]
774    fn test_builder_app_yaml() {
775        let fixture = FixtureBuilder::new("test")
776            .app_yaml("name: my-app\nversion: 1.0")
777            .build();
778
779        assert!(fixture.get_app_yaml().is_ok());
780        assert!(fixture.get_app_yaml().unwrap().contains("my-app"));
781    }
782
783    #[test]
784    fn test_builder_data() {
785        let fixture = FixtureBuilder::new("test")
786            .data("metrics", vec![1, 2, 3])
787            .build();
788
789        assert_eq!(fixture.get_data("metrics").unwrap(), &[1, 2, 3]);
790    }
791
792    #[test]
793    fn test_builder_snapshot() {
794        let fixture = FixtureBuilder::new("test")
795            .snapshot("button", vec![0x89, b'P', b'N', b'G'])
796            .build();
797
798        assert!(fixture.get_snapshot("button").is_ok());
799    }
800
801    #[test]
802    fn test_builder_chaining() {
803        let fixture = FixtureBuilder::new("complex-fixture")
804            .app_yaml("name: test")
805            .file("readme.md", "# Test")
806            .data("data1", vec![1, 2, 3])
807            .data("data2", vec![4, 5, 6])
808            .snapshot("main", vec![0])
809            .build();
810
811        assert_eq!(fixture.file_count(), 5);
812    }
813
814    // =========================================================================
815    // TestData Tests
816    // =========================================================================
817
818    #[test]
819    fn test_metrics_json() {
820        let json = TestData::metrics_json(5);
821        assert!(json.starts_with('['));
822        assert!(json.ends_with(']'));
823        assert!(json.contains("timestamp"));
824        assert!(json.contains("cpu"));
825        assert!(json.contains("memory"));
826    }
827
828    #[test]
829    fn test_metrics_json_count() {
830        let json = TestData::metrics_json(10);
831        let count = json.matches("timestamp").count();
832        assert_eq!(count, 10);
833    }
834
835    #[test]
836    fn test_chart_points() {
837        let points = TestData::chart_points(100);
838        assert_eq!(points.len(), 100);
839        assert_eq!(points[0].0, 0.0);
840        assert_eq!(points[99].0, 99.0);
841    }
842
843    #[test]
844    fn test_chart_points_values() {
845        let points = TestData::chart_points(10);
846        for (x, y) in &points {
847            assert!(*y >= 0.0 && *y <= 100.0, "y={y} should be in [0, 100]");
848            assert!(*x >= 0.0);
849        }
850    }
851
852    #[test]
853    fn test_table_csv() {
854        let csv = TestData::table_csv(3, 2);
855        let lines: Vec<&str> = csv.lines().collect();
856
857        assert_eq!(lines.len(), 4); // header + 3 rows
858        assert_eq!(lines[0], "col0,col1");
859        assert_eq!(lines[1], "r0c0,r0c1");
860    }
861
862    #[test]
863    fn test_table_csv_dimensions() {
864        let csv = TestData::table_csv(5, 4);
865        let lines: Vec<&str> = csv.lines().collect();
866
867        assert_eq!(lines.len(), 6); // header + 5 rows
868
869        // Check column count
870        let cols: Vec<&str> = lines[0].split(',').collect();
871        assert_eq!(cols.len(), 4);
872    }
873
874    #[test]
875    fn test_minimal_png_structure() {
876        let png = TestData::minimal_png(2, 2, [255, 0, 0, 255]);
877
878        // Check PNG signature
879        assert_eq!(
880            &png[0..8],
881            &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]
882        );
883    }
884
885    #[test]
886    fn test_minimal_png_has_ihdr() {
887        let png = TestData::minimal_png(4, 4, [0, 255, 0, 255]);
888
889        // Find IHDR chunk
890        let ihdr_pos = png.windows(4).position(|w| w == b"IHDR");
891        assert!(ihdr_pos.is_some());
892    }
893
894    #[test]
895    fn test_minimal_png_has_iend() {
896        let png = TestData::minimal_png(4, 4, [0, 0, 255, 255]);
897
898        // Find IEND chunk
899        let iend_pos = png.windows(4).position(|w| w == b"IEND");
900        assert!(iend_pos.is_some());
901    }
902
903    // =========================================================================
904    // Tar Parsing Tests
905    // =========================================================================
906
907    #[test]
908    fn test_parse_tar_string() {
909        let data = b"hello\0\0\0\0\0";
910        let result = Fixture::parse_tar_string(data);
911        assert_eq!(result, "hello");
912    }
913
914    #[test]
915    fn test_parse_tar_string_empty() {
916        let data = b"\0\0\0\0\0";
917        let result = Fixture::parse_tar_string(data);
918        assert_eq!(result, "");
919    }
920
921    #[test]
922    fn test_parse_tar_string_no_null() {
923        let data = b"hello";
924        let result = Fixture::parse_tar_string(data);
925        assert_eq!(result, "hello");
926    }
927
928    // =========================================================================
929    // FixtureContext Tests
930    // =========================================================================
931
932    #[test]
933    fn test_context_new() {
934        let fixture = FixtureBuilder::new("test").app_yaml("name: test").build();
935        let context = FixtureContext::new(fixture, "my_test");
936
937        assert_eq!(context.test_name, "my_test");
938        assert!(context.output_dir.is_none());
939    }
940
941    #[test]
942    fn test_context_with_output_dir() {
943        let fixture = Fixture::new();
944        let context = FixtureContext::new(fixture, "test").with_output_dir("/tmp/output");
945
946        assert_eq!(context.output_dir, Some("/tmp/output".to_string()));
947    }
948
949    #[test]
950    fn test_context_app_yaml() {
951        let fixture = FixtureBuilder::new("test").app_yaml("name: my-app").build();
952        let context = FixtureContext::new(fixture, "test");
953
954        let yaml = context.app_yaml().unwrap();
955        assert!(yaml.contains("my-app"));
956    }
957
958    // =========================================================================
959    // FixtureError Tests
960    // =========================================================================
961
962    #[test]
963    fn test_error_display() {
964        let errors = vec![
965            (
966                FixtureError::InvalidTar("bad header".to_string()),
967                "invalid tar archive: bad header",
968            ),
969            (
970                FixtureError::FileNotFound("test.txt".to_string()),
971                "file not found: test.txt",
972            ),
973            (
974                FixtureError::InvalidFormat("bad format".to_string()),
975                "invalid fixture format: bad format",
976            ),
977            (
978                FixtureError::IoError("read failed".to_string()),
979                "IO error: read failed",
980            ),
981            (
982                FixtureError::YamlError("parse error".to_string()),
983                "YAML error: parse error",
984            ),
985        ];
986
987        for (error, expected) in errors {
988            assert_eq!(error.to_string(), expected);
989        }
990    }
991
992    #[test]
993    fn test_error_equality() {
994        let e1 = FixtureError::FileNotFound("test.txt".to_string());
995        let e2 = FixtureError::FileNotFound("test.txt".to_string());
996        let e3 = FixtureError::FileNotFound("other.txt".to_string());
997
998        assert_eq!(e1, e2);
999        assert_ne!(e1, e3);
1000    }
1001
1002    // =========================================================================
1003    // Integration Tests
1004    // =========================================================================
1005
1006    #[test]
1007    fn test_full_fixture_workflow() {
1008        // Build a complete fixture
1009        let fixture = FixtureBuilder::new("dashboard-fixture")
1010            .app_yaml(
1011                r#"
1012name: Dashboard
1013version: 1.0.0
1014layout:
1015  type: column
1016  children:
1017    - type: chart
1018      data: "{{ metrics }}"
1019"#,
1020            )
1021            .data("metrics", TestData::metrics_json(100).into_bytes())
1022            .snapshot(
1023                "dashboard",
1024                TestData::minimal_png(100, 100, [255, 255, 255, 255]),
1025            )
1026            .build();
1027
1028        // Verify structure
1029        assert!(fixture.has_file("app.yaml"));
1030        assert!(fixture.has_file("metrics.ald"));
1031        assert!(fixture.has_file("snapshots/dashboard.png"));
1032
1033        // Verify manifest
1034        let manifest = fixture.manifest();
1035        assert_eq!(manifest.name, "dashboard-fixture");
1036        assert!(manifest.app_yaml.is_some());
1037        assert_eq!(manifest.data_files.len(), 1);
1038        assert_eq!(manifest.snapshots.len(), 1);
1039
1040        // Verify app yaml access
1041        let yaml = fixture.get_app_yaml().unwrap();
1042        assert!(yaml.contains("Dashboard"));
1043        assert!(yaml.contains("metrics"));
1044    }
1045
1046    #[test]
1047    fn test_fixture_as_harness_input() {
1048        // Simulate how fixtures would be used with test harness
1049        let fixture = FixtureBuilder::new("button-test")
1050            .app_yaml(
1051                r#"
1052name: Button Test
1053widgets:
1054  - type: button
1055    text: "Click Me"
1056    test-id: submit-btn
1057"#,
1058            )
1059            .build();
1060
1061        let context = FixtureContext::new(fixture, "test_button_click");
1062        let yaml = context.app_yaml().unwrap();
1063
1064        assert!(yaml.contains("submit-btn"));
1065    }
1066}