1use std::collections::HashMap;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum FixtureError {
24 InvalidTar(String),
26 FileNotFound(String),
28 InvalidFormat(String),
30 IoError(String),
32 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#[derive(Debug, Clone)]
52pub struct Fixture {
53 files: HashMap<String, Vec<u8>>,
55 manifest: FixtureManifest,
57}
58
59#[derive(Debug, Clone, Default)]
61pub struct FixtureManifest {
62 pub name: String,
64 pub app_yaml: Option<String>,
66 pub data_files: Vec<String>,
68 pub model_files: Vec<String>,
70 pub assets: Vec<String>,
72 pub snapshots: Vec<String>,
74}
75
76impl Fixture {
77 #[must_use]
79 pub fn new() -> Self {
80 Self {
81 files: HashMap::new(),
82 manifest: FixtureManifest::default(),
83 }
84 }
85
86 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 #[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 #[must_use]
111 pub fn manifest(&self) -> &FixtureManifest {
112 &self.manifest
113 }
114
115 #[must_use]
117 pub fn list_files(&self) -> Vec<&str> {
118 self.files.keys().map(String::as_str).collect()
119 }
120
121 #[must_use]
123 pub fn has_file(&self, path: &str) -> bool {
124 self.files.contains_key(path)
125 }
126
127 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 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 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 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 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 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 pub fn add_file(&mut self, path: &str, content: Vec<u8>) {
183 self.files.insert(path.to_string(), content);
184 }
185
186 pub fn remove_file(&mut self, path: &str) -> Option<Vec<u8>> {
188 self.files.remove(path)
189 }
190
191 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 if header.iter().all(|&b| b == 0) {
201 break;
202 }
203
204 let name = Self::parse_tar_string(&header[0..100]);
206 if name.is_empty() {
207 break;
208 }
209
210 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 let typeflag = header[156];
217
218 pos += BLOCK_SIZE; 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 pos += (size + BLOCK_SIZE - 1) / BLOCK_SIZE * BLOCK_SIZE;
235 }
236
237 Ok(())
238 }
239
240 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 fn build_manifest(&mut self) {
248 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 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 #[must_use]
276 pub fn file_count(&self) -> usize {
277 self.files.len()
278 }
279
280 #[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#[derive(Debug, Default)]
295pub struct FixtureBuilder {
296 files: Vec<(String, Vec<u8>)>,
297 name: String,
298}
299
300impl FixtureBuilder {
301 #[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 #[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 #[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 #[must_use]
326 pub fn app_yaml(self, content: impl Into<String>) -> Self {
327 self.file("app.yaml", content)
328 }
329
330 #[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 #[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 #[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
364pub struct TestData;
366
367impl TestData {
368 #[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 #[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 #[must_use]
402 pub fn table_csv(rows: usize, cols: usize) -> String {
403 let mut csv = String::new();
404
405 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 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 #[must_use]
430 pub fn minimal_png(width: u32, height: u32, color: [u8; 4]) -> Vec<u8> {
431 let mut png = Vec::new();
434
435 png.extend_from_slice(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]);
437
438 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); ihdr.push(6); ihdr.push(0); ihdr.push(0); ihdr.push(0); 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 let mut idat = vec![0x08, 0x1D]; let pixel_row_size = 1 + width as usize * 4; let raw_size = pixel_row_size * height as usize;
458
459 idat.push(0x01); 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); for _ in 0..width {
467 idat.extend_from_slice(&color);
468 }
469 }
470
471 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 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 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 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#[derive(Debug)]
531pub struct FixtureContext {
532 pub fixture: Fixture,
534 pub test_name: String,
536 pub output_dir: Option<String>,
538}
539
540impl FixtureContext {
541 #[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 #[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 #[must_use]
560 pub fn fixture(&self) -> &Fixture {
561 &self.fixture
562 }
563
564 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 #[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 #[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 #[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 #[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 #[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); 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); 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 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 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 let iend_pos = png.windows(4).position(|w| w == b"IEND");
900 assert!(iend_pos.is_some());
901 }
902
903 #[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 #[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 #[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 #[test]
1007 fn test_full_fixture_workflow() {
1008 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 assert!(fixture.has_file("app.yaml"));
1030 assert!(fixture.has_file("metrics.ald"));
1031 assert!(fixture.has_file("snapshots/dashboard.png"));
1032
1033 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 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 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}