1use chrono::{TimeZone, Utc};
14
15use super::generator::{ExifSpec, TestImage, TransformSpec};
16use super::scenarios::TestScenario;
17
18#[derive(Debug, Clone)]
20pub struct ScenarioFixture {
21 pub scenario: TestScenario,
23 pub images: Vec<TestImage>,
25 pub expected_winner_index: usize,
27 pub description: String,
29}
30
31pub fn all_fixtures() -> Vec<ScenarioFixture> {
33 vec![
34 w1_clear_dimension_winner(),
36 w2_same_dimensions_different_size(),
37 w3_same_dimensions_same_size(),
38 w4_some_missing_dimensions(),
39 w5_only_one_has_dimensions(),
40 w6_all_missing_dimensions(),
41 w7_three_plus_duplicates(),
42 w8_same_pixels_different_aspect(),
43 c1_winner_lacks_gps_loser_has(),
45 c2_winner_lacks_datetime_loser_has(),
46 c3_winner_lacks_description_loser_has(),
47 c4_winner_lacks_all_loser_has_all(),
48 c5_both_have_gps(),
49 c6_multiple_losers_contribute(),
50 c7_no_loser_has_needed(),
51 c8_winner_has_everything(),
52 f1_gps_conflict(),
54 f2_gps_within_threshold(),
55 f3_timezone_conflict(),
56 f4_camera_conflict(),
57 f5_capture_time_conflict(),
58 f6_multiple_conflicts(),
59 f7_no_conflicts(),
60 x1_single_asset_group(),
62 x2_large_group(),
63 x3_large_file(),
64 x4_special_chars_filename(),
65 x5_video(),
66 x7_png(),
67 x9_unicode_description(),
68 x10_very_old_date(),
69 x11_future_date(),
70 ]
71}
72
73fn w1_clear_dimension_winner() -> ScenarioFixture {
77 ScenarioFixture {
78 scenario: TestScenario::W1ClearDimensionWinner,
79 images: vec![
80 TestImage::new(
81 "w1_large.jpg",
82 TransformSpec::new("base_w1.jpg")
83 .with_scale(100)
84 .with_quality(95),
85 ),
86 TestImage::new(
87 "w1_small.jpg",
88 TransformSpec::new("base_w1.jpg")
89 .with_scale(99)
90 .with_quality(60),
91 ),
92 ],
93 expected_winner_index: 0,
94 description: "Larger dimensions should win (100% vs 99% scale)".into(),
95 }
96}
97
98fn w2_same_dimensions_different_size() -> ScenarioFixture {
99 ScenarioFixture {
101 scenario: TestScenario::W2SameDimensionsDifferentSize,
102 images: vec![
103 TestImage::new(
104 "w2_a.jpg",
105 TransformSpec::new("base_w2.jpg")
106 .with_scale(100)
107 .with_quality(95),
108 ),
109 TestImage::new(
110 "w2_b.jpg",
111 TransformSpec::new("base_w2.jpg")
112 .with_scale(100)
113 .with_quality(60),
114 ),
115 ],
116 expected_winner_index: 0, description: "Same dimensions - first in list wins on tie".into(),
118 }
119}
120
121fn w3_same_dimensions_same_size() -> ScenarioFixture {
122 ScenarioFixture {
123 scenario: TestScenario::W3SameDimensionsSameSize,
124 images: vec![
125 TestImage::new(
126 "w3_a.jpg",
127 TransformSpec::new("base_w3.jpg")
128 .with_scale(100)
129 .with_quality(85),
130 )
131 .with_exif(ExifSpec {
132 description: Some("W3 variant A".into()),
133 ..Default::default()
134 }),
135 TestImage::new(
136 "w3_b.jpg",
137 TransformSpec::new("base_w3.jpg")
138 .with_scale(100)
139 .with_quality(85),
140 )
141 .with_exif(ExifSpec {
142 description: Some("W3 variant B".into()),
143 ..Default::default()
144 }),
145 ],
146 expected_winner_index: 0,
147 description: "Identical dimensions and size - first wins".into(),
148 }
149}
150
151fn w4_some_missing_dimensions() -> ScenarioFixture {
152 ScenarioFixture {
153 scenario: TestScenario::W4SomeMissingDimensions,
154 images: vec![
155 TestImage::new(
156 "w4_with_dims.jpg",
157 TransformSpec::new("base_w4.jpg")
158 .with_scale(100)
159 .with_quality(95),
160 ),
161 TestImage::new(
162 "w4_no_dims.jpg",
163 TransformSpec::new("base_w4.jpg")
164 .with_scale(99)
165 .with_quality(60)
166 .without_dimensions(),
167 ),
168 ],
169 expected_winner_index: 0,
170 description: "Asset with dimensions beats asset without".into(),
171 }
172}
173
174fn w5_only_one_has_dimensions() -> ScenarioFixture {
175 ScenarioFixture {
176 scenario: TestScenario::W5OnlyOneHasDimensions,
177 images: vec![
178 TestImage::new(
179 "w5_no_dims.jpg",
180 TransformSpec::new("base_w5.jpg")
181 .with_scale(100)
182 .with_quality(95)
183 .without_dimensions(),
184 ),
185 TestImage::new(
186 "w5_with_dims.jpg",
187 TransformSpec::new("base_w5.jpg")
188 .with_scale(99)
189 .with_quality(60),
190 ),
191 ],
192 expected_winner_index: 1, description: "Only second asset has dimensions - it wins".into(),
194 }
195}
196
197fn w6_all_missing_dimensions() -> ScenarioFixture {
198 ScenarioFixture {
199 scenario: TestScenario::W6AllMissingDimensions,
200 images: vec![
201 TestImage::new(
202 "w6_a.jpg",
203 TransformSpec::new("base_w6.jpg")
204 .with_scale(100)
205 .with_quality(95)
206 .without_dimensions(),
207 ),
208 TestImage::new(
209 "w6_b.jpg",
210 TransformSpec::new("base_w6.jpg")
211 .with_scale(99)
212 .with_quality(60)
213 .without_dimensions(),
214 ),
215 ],
216 expected_winner_index: 0,
217 description: "No dimensions on any - first wins".into(),
218 }
219}
220
221fn w7_three_plus_duplicates() -> ScenarioFixture {
222 ScenarioFixture {
223 scenario: TestScenario::W7ThreePlusDuplicates,
224 images: vec![
225 TestImage::new(
226 "w7_small.jpg",
227 TransformSpec::new("base_w7.jpg")
228 .with_scale(97)
229 .with_quality(50),
230 ),
231 TestImage::new(
232 "w7_large.jpg",
233 TransformSpec::new("base_w7.jpg")
234 .with_scale(100)
235 .with_quality(95),
236 ),
237 TestImage::new(
238 "w7_medium.jpg",
239 TransformSpec::new("base_w7.jpg")
240 .with_scale(98)
241 .with_quality(70),
242 ),
243 ],
244 expected_winner_index: 1, description: "3 duplicates - largest dimensions wins".into(),
246 }
247}
248
249fn w8_same_pixels_different_aspect() -> ScenarioFixture {
250 ScenarioFixture {
252 scenario: TestScenario::W8SamePixelsDifferentAspect,
253 images: vec![
254 TestImage::new(
255 "w8_wide.jpg",
256 TransformSpec::new("base_w8.jpg")
257 .with_size(600, 400)
258 .with_quality(95),
259 ),
260 TestImage::new(
261 "w8_tall.jpg",
262 TransformSpec::new("base_w8.jpg")
263 .with_size(594, 396)
264 .with_quality(60),
265 ),
266 ],
267 expected_winner_index: 0, description: "Similar pixel count, different aspect - larger wins".into(),
269 }
270}
271
272fn c1_winner_lacks_gps_loser_has() -> ScenarioFixture {
275 ScenarioFixture {
276 scenario: TestScenario::C1WinnerLacksGpsLoserHas,
277 images: vec![
278 TestImage::new(
279 "c1_winner_no_gps.jpg",
280 TransformSpec::new("base_c1.jpg")
281 .with_scale(100)
282 .with_quality(95),
283 ),
284 TestImage::new(
285 "c1_loser_has_gps.jpg",
286 TransformSpec::new("base_c1.jpg")
287 .with_scale(99)
288 .with_quality(60),
289 )
290 .with_exif(ExifSpec {
291 gps: Some((51.5074, -0.1278)), ..Default::default()
293 }),
294 ],
295 expected_winner_index: 0,
296 description: "Winner lacks GPS, loser has it - consolidate GPS".into(),
297 }
298}
299
300fn c2_winner_lacks_datetime_loser_has() -> ScenarioFixture {
301 ScenarioFixture {
302 scenario: TestScenario::C2WinnerLacksDatetimeLoserHas,
303 images: vec![
304 TestImage::new(
305 "c2_winner_no_dt.jpg",
306 TransformSpec::new("base_c2.jpg")
307 .with_scale(100)
308 .with_quality(95),
309 ),
310 TestImage::new(
311 "c2_loser_has_dt.jpg",
312 TransformSpec::new("base_c2.jpg")
313 .with_scale(99)
314 .with_quality(60),
315 )
316 .with_exif(ExifSpec {
317 datetime: Some(Utc.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap()),
318 ..Default::default()
319 }),
320 ],
321 expected_winner_index: 0,
322 description: "Winner lacks datetime, loser has it".into(),
323 }
324}
325
326fn c3_winner_lacks_description_loser_has() -> ScenarioFixture {
327 ScenarioFixture {
328 scenario: TestScenario::C3WinnerLacksDescriptionLoserHas,
329 images: vec![
330 TestImage::new(
331 "c3_winner_no_desc.jpg",
332 TransformSpec::new("base_c3.jpg")
333 .with_scale(100)
334 .with_quality(95),
335 ),
336 TestImage::new(
337 "c3_loser_has_desc.jpg",
338 TransformSpec::new("base_c3.jpg")
339 .with_scale(99)
340 .with_quality(60),
341 )
342 .with_exif(ExifSpec {
343 description: Some("Delicious salad".into()),
344 ..Default::default()
345 }),
346 ],
347 expected_winner_index: 0,
348 description: "Winner lacks description, loser has it".into(),
349 }
350}
351
352fn c4_winner_lacks_all_loser_has_all() -> ScenarioFixture {
353 ScenarioFixture {
354 scenario: TestScenario::C4WinnerLacksAllLoserHasAll,
355 images: vec![
356 TestImage::new(
357 "c4_winner_bare.jpg",
358 TransformSpec::new("base_c4.jpg")
359 .with_scale(100)
360 .with_quality(95),
361 ),
362 TestImage::new(
363 "c4_loser_rich.jpg",
364 TransformSpec::new("base_c4.jpg")
365 .with_scale(99)
366 .with_quality(60),
367 )
368 .with_exif(ExifSpec {
369 gps: Some((40.7128, -74.0060)), datetime: Some(Utc.with_ymd_and_hms(2023, 12, 25, 10, 0, 0).unwrap()),
371 timezone: Some("-05:00".into()),
372 camera_make: Some("Canon".into()),
373 camera_model: Some("EOS R5".into()),
374 description: Some("Lion at the zoo".into()),
375 }),
376 ],
377 expected_winner_index: 0,
378 description: "Winner has no metadata, loser has everything".into(),
379 }
380}
381
382fn c5_both_have_gps() -> ScenarioFixture {
383 let gps = Some((48.8566, 2.3522)); ScenarioFixture {
385 scenario: TestScenario::C5BothHaveGps,
386 images: vec![
387 TestImage::new(
388 "c5_a_gps.jpg",
389 TransformSpec::new("base_c5.jpg")
390 .with_scale(100)
391 .with_quality(95),
392 )
393 .with_exif(ExifSpec {
394 gps,
395 ..Default::default()
396 }),
397 TestImage::new(
398 "c5_b_gps.jpg",
399 TransformSpec::new("base_c5.jpg")
400 .with_scale(99)
401 .with_quality(60),
402 )
403 .with_exif(ExifSpec {
404 gps,
405 ..Default::default()
406 }),
407 ],
408 expected_winner_index: 0,
409 description: "Both have same GPS - no consolidation needed".into(),
410 }
411}
412
413fn c6_multiple_losers_contribute() -> ScenarioFixture {
414 ScenarioFixture {
415 scenario: TestScenario::C6MultipleLosersContribute,
416 images: vec![
417 TestImage::new(
418 "c6_winner.jpg",
419 TransformSpec::new("base_c6.jpg")
420 .with_scale(100)
421 .with_quality(95),
422 ),
423 TestImage::new(
424 "c6_loser_gps.jpg",
425 TransformSpec::new("base_c6.jpg")
426 .with_scale(99)
427 .with_quality(65),
428 )
429 .with_exif(ExifSpec {
430 gps: Some((35.6762, 139.6503)), ..Default::default()
432 }),
433 TestImage::new(
434 "c6_loser_dt.jpg",
435 TransformSpec::new("base_c6.jpg")
436 .with_scale(98)
437 .with_quality(55),
438 )
439 .with_exif(ExifSpec {
440 datetime: Some(Utc.with_ymd_and_hms(2024, 3, 20, 9, 0, 0).unwrap()),
441 ..Default::default()
442 }),
443 ],
444 expected_winner_index: 0,
445 description: "Multiple losers contribute different metadata".into(),
446 }
447}
448
449fn c7_no_loser_has_needed() -> ScenarioFixture {
450 ScenarioFixture {
451 scenario: TestScenario::C7NoLoserHasNeeded,
452 images: vec![
453 TestImage::new(
454 "c7_winner.jpg",
455 TransformSpec::new("base_c7.jpg")
456 .with_scale(100)
457 .with_quality(95),
458 ),
459 TestImage::new(
460 "c7_loser.jpg",
461 TransformSpec::new("base_c7.jpg")
462 .with_scale(99)
463 .with_quality(60),
464 ),
465 ],
466 expected_winner_index: 0,
467 description: "Winner lacks GPS but no loser has it either".into(),
468 }
469}
470
471fn c8_winner_has_everything() -> ScenarioFixture {
472 ScenarioFixture {
473 scenario: TestScenario::C8WinnerHasEverything,
474 images: vec![
475 TestImage::new(
476 "c8_winner_full.jpg",
477 TransformSpec::new("base_c8.jpg")
478 .with_scale(100)
479 .with_quality(95),
480 )
481 .with_exif(ExifSpec {
482 gps: Some((37.7749, -122.4194)), datetime: Some(Utc.with_ymd_and_hms(2024, 7, 4, 12, 0, 0).unwrap()),
484 timezone: Some("-07:00".into()),
485 camera_make: Some("Sony".into()),
486 camera_model: Some("A7R IV".into()),
487 description: Some("Golden Gate at noon".into()),
488 }),
489 TestImage::new(
490 "c8_loser_bare.jpg",
491 TransformSpec::new("base_c8.jpg")
492 .with_scale(99)
493 .with_quality(60),
494 ),
495 ],
496 expected_winner_index: 0,
497 description: "Winner already has all metadata - nothing to consolidate".into(),
498 }
499}
500
501fn f1_gps_conflict() -> ScenarioFixture {
504 ScenarioFixture {
505 scenario: TestScenario::F1GpsConflict,
506 images: vec![
507 TestImage::new(
508 "f1_london.jpg",
509 TransformSpec::new("base_f1.jpg")
510 .with_scale(100)
511 .with_quality(95),
512 )
513 .with_exif(ExifSpec {
514 gps: Some((51.5074, -0.1278)), ..Default::default()
516 }),
517 TestImage::new(
518 "f1_paris.jpg",
519 TransformSpec::new("base_f1.jpg")
520 .with_scale(99)
521 .with_quality(60),
522 )
523 .with_exif(ExifSpec {
524 gps: Some((48.8566, 2.3522)), ..Default::default()
526 }),
527 ],
528 expected_winner_index: 0,
529 description: "GPS conflict - London vs Paris (should flag conflict)".into(),
530 }
531}
532
533fn f2_gps_within_threshold() -> ScenarioFixture {
534 ScenarioFixture {
535 scenario: TestScenario::F2GpsWithinThreshold,
536 images: vec![
537 TestImage::new(
538 "f2_pos_a.jpg",
539 TransformSpec::new("base_f2.jpg")
540 .with_scale(100)
541 .with_quality(95),
542 )
543 .with_exif(ExifSpec {
544 gps: Some((51.50740, -0.12780)),
545 ..Default::default()
546 }),
547 TestImage::new(
548 "f2_pos_b.jpg",
549 TransformSpec::new("base_f2.jpg")
550 .with_scale(99)
551 .with_quality(60),
552 )
553 .with_exif(ExifSpec {
554 gps: Some((51.50745, -0.12785)), ..Default::default()
556 }),
557 ],
558 expected_winner_index: 0,
559 description: "GPS within threshold - should NOT conflict".into(),
560 }
561}
562
563fn f3_timezone_conflict() -> ScenarioFixture {
564 ScenarioFixture {
565 scenario: TestScenario::F3TimezoneConflict,
566 images: vec![
567 TestImage::new(
568 "f3_tz_a.jpg",
569 TransformSpec::new("base_f3.jpg")
570 .with_scale(100)
571 .with_quality(95),
572 )
573 .with_exif(ExifSpec {
574 timezone: Some("+00:00".into()), ..Default::default()
576 }),
577 TestImage::new(
578 "f3_tz_b.jpg",
579 TransformSpec::new("base_f3.jpg")
580 .with_scale(99)
581 .with_quality(60),
582 )
583 .with_exif(ExifSpec {
584 timezone: Some("-08:00".into()), ..Default::default()
586 }),
587 ],
588 expected_winner_index: 0,
589 description: "Timezone conflict - UTC vs PST".into(),
590 }
591}
592
593fn f4_camera_conflict() -> ScenarioFixture {
594 ScenarioFixture {
595 scenario: TestScenario::F4CameraConflict,
596 images: vec![
597 TestImage::new(
598 "f4_canon.jpg",
599 TransformSpec::new("base_f4.jpg")
600 .with_scale(100)
601 .with_quality(95),
602 )
603 .with_exif(ExifSpec {
604 camera_make: Some("Canon".into()),
605 camera_model: Some("EOS R5".into()),
606 ..Default::default()
607 }),
608 TestImage::new(
609 "f4_nikon.jpg",
610 TransformSpec::new("base_f4.jpg")
611 .with_scale(99)
612 .with_quality(60),
613 )
614 .with_exif(ExifSpec {
615 camera_make: Some("Nikon".into()),
616 camera_model: Some("Z6 II".into()),
617 ..Default::default()
618 }),
619 ],
620 expected_winner_index: 0,
621 description: "Camera conflict - Canon vs Nikon".into(),
622 }
623}
624
625fn f5_capture_time_conflict() -> ScenarioFixture {
626 ScenarioFixture {
627 scenario: TestScenario::F5CaptureTimeConflict,
628 images: vec![
629 TestImage::new(
630 "f5_morning.jpg",
631 TransformSpec::new("base_f5.jpg")
632 .with_scale(100)
633 .with_quality(95),
634 )
635 .with_exif(ExifSpec {
636 datetime: Some(Utc.with_ymd_and_hms(2024, 1, 15, 8, 0, 0).unwrap()),
637 ..Default::default()
638 }),
639 TestImage::new(
640 "f5_evening.jpg",
641 TransformSpec::new("base_f5.jpg")
642 .with_scale(99)
643 .with_quality(60),
644 )
645 .with_exif(ExifSpec {
646 datetime: Some(Utc.with_ymd_and_hms(2024, 1, 15, 20, 0, 0).unwrap()),
647 ..Default::default()
648 }),
649 ],
650 expected_winner_index: 0,
651 description: "Capture time conflict - morning vs evening".into(),
652 }
653}
654
655fn f6_multiple_conflicts() -> ScenarioFixture {
656 ScenarioFixture {
657 scenario: TestScenario::F6MultipleConflicts,
658 images: vec![
659 TestImage::new(
660 "f6_a.jpg",
661 TransformSpec::new("base_f6.jpg")
662 .with_scale(100)
663 .with_quality(95),
664 )
665 .with_exif(ExifSpec {
666 gps: Some((51.5074, -0.1278)), camera_make: Some("Canon".into()),
668 timezone: Some("+00:00".into()),
669 ..Default::default()
670 }),
671 TestImage::new(
672 "f6_b.jpg",
673 TransformSpec::new("base_f6.jpg")
674 .with_scale(99)
675 .with_quality(60),
676 )
677 .with_exif(ExifSpec {
678 gps: Some((40.7128, -74.0060)), camera_make: Some("Sony".into()),
680 timezone: Some("-05:00".into()),
681 ..Default::default()
682 }),
683 ],
684 expected_winner_index: 0,
685 description: "Multiple conflicts - GPS, camera, timezone all differ".into(),
686 }
687}
688
689fn f7_no_conflicts() -> ScenarioFixture {
690 let exif = ExifSpec {
691 gps: Some((51.5074, -0.1278)),
692 camera_make: Some("Canon".into()),
693 ..Default::default()
694 };
695 ScenarioFixture {
696 scenario: TestScenario::F7NoConflicts,
697 images: vec![
698 TestImage::new(
699 "f7_a.jpg",
700 TransformSpec::new("base_f7.jpg")
701 .with_scale(100)
702 .with_quality(95),
703 )
704 .with_exif(exif.clone()),
705 TestImage::new(
706 "f7_b.jpg",
707 TransformSpec::new("base_f7.jpg")
708 .with_scale(99)
709 .with_quality(60),
710 )
711 .with_exif(exif),
712 ],
713 expected_winner_index: 0,
714 description: "No conflicts - metadata matches".into(),
715 }
716}
717
718fn x1_single_asset_group() -> ScenarioFixture {
721 ScenarioFixture {
722 scenario: TestScenario::X1SingleAssetGroup,
723 images: vec![TestImage::new(
724 "x1_single.jpg",
725 TransformSpec::new("base_x1.jpg")
726 .with_scale(100)
727 .with_quality(90),
728 )],
729 expected_winner_index: 0,
730 description: "Single asset group - trivial case".into(),
731 }
732}
733
734fn x2_large_group() -> ScenarioFixture {
735 let scales: [u32; 12] = [97, 97, 97, 98, 98, 98, 98, 99, 99, 99, 99, 100];
738 let qualities: [u8; 12] = [50, 55, 60, 65, 70, 75, 80, 85, 88, 91, 94, 97];
739
740 let images: Vec<TestImage> = (0..12)
741 .map(|i| {
742 TestImage::new(
743 format!("x2_dup_{:02}.jpg", i),
744 TransformSpec::new("base_x2.jpg")
745 .with_scale(scales[i])
746 .with_quality(qualities[i]),
747 )
748 })
749 .collect();
750
751 ScenarioFixture {
752 scenario: TestScenario::X2LargeGroup,
753 images,
754 expected_winner_index: 11, description: "12 duplicates - largest should win".into(),
756 }
757}
758
759fn x3_large_file() -> ScenarioFixture {
760 ScenarioFixture {
761 scenario: TestScenario::X3LargeFile,
762 images: vec![
763 TestImage::new(
764 "x3_large.jpg",
765 TransformSpec::new("base_x3.jpg")
766 .with_scale(100)
767 .with_quality(100),
768 ),
769 TestImage::new(
770 "x3_small.jpg",
771 TransformSpec::new("base_x3.jpg")
772 .with_scale(99)
773 .with_quality(60),
774 ),
775 ],
776 expected_winner_index: 0,
777 description: "Large file handling (full size, max quality)".into(),
778 }
779}
780
781fn x4_special_chars_filename() -> ScenarioFixture {
782 ScenarioFixture {
783 scenario: TestScenario::X4SpecialCharsFilename,
784 images: vec![
785 TestImage::new(
786 "x4_photo (1).jpg",
787 TransformSpec::new("base_x4.jpg")
788 .with_scale(100)
789 .with_quality(95),
790 ),
791 TestImage::new(
792 "x4_photo-copy_2024.jpg",
793 TransformSpec::new("base_x4.jpg")
794 .with_scale(99)
795 .with_quality(60),
796 ),
797 ],
798 expected_winner_index: 0,
799 description: "Filenames with spaces, parens, hyphens".into(),
800 }
801}
802
803fn x5_video() -> ScenarioFixture {
804 ScenarioFixture {
805 scenario: TestScenario::X5Video,
806 images: vec![
807 TestImage::new(
808 "x5_video_hd.mp4",
809 TransformSpec::new("base_x5.jpg").with_size(1920, 1080),
810 ),
811 TestImage::new(
812 "x5_video_sd.mp4",
813 TransformSpec::new("base_x5.jpg").with_size(640, 480),
814 ),
815 ],
816 expected_winner_index: 0,
817 description: "Video duplicates - HD vs SD".into(),
818 }
819}
820
821fn x7_png() -> ScenarioFixture {
822 ScenarioFixture {
823 scenario: TestScenario::X7Png,
824 images: vec![
825 TestImage::new(
826 "x7_image.png",
827 TransformSpec::new("base_x7.jpg").with_scale(100),
828 ),
829 TestImage::new(
830 "x7_image.jpg",
831 TransformSpec::new("base_x7.jpg")
832 .with_scale(99)
833 .with_quality(60),
834 ),
835 ],
836 expected_winner_index: 0,
837 description: "PNG vs JPEG - PNG larger".into(),
838 }
839}
840
841fn x9_unicode_description() -> ScenarioFixture {
842 ScenarioFixture {
843 scenario: TestScenario::X9UnicodeDescription,
844 images: vec![
845 TestImage::new(
846 "x9_unicode.jpg",
847 TransformSpec::new("base_x9.jpg")
848 .with_scale(100)
849 .with_quality(95),
850 )
851 .with_exif(ExifSpec {
852 description: Some("日本の桜 🌸 Cherry blossoms".into()),
853 ..Default::default()
854 }),
855 TestImage::new(
856 "x9_plain.jpg",
857 TransformSpec::new("base_x9.jpg")
858 .with_scale(99)
859 .with_quality(60),
860 ),
861 ],
862 expected_winner_index: 0,
863 description: "Unicode in description - Japanese, emoji".into(),
864 }
865}
866
867fn x10_very_old_date() -> ScenarioFixture {
868 ScenarioFixture {
869 scenario: TestScenario::X10VeryOldDate,
870 images: vec![
871 TestImage::new(
872 "x10_old.jpg",
873 TransformSpec::new("base_x10.jpg")
874 .with_scale(100)
875 .with_quality(95),
876 )
877 .with_exif(ExifSpec {
878 datetime: Some(Utc.with_ymd_and_hms(1985, 7, 20, 12, 0, 0).unwrap()),
879 ..Default::default()
880 }),
881 TestImage::new(
882 "x10_scan.jpg",
883 TransformSpec::new("base_x10.jpg")
884 .with_scale(99)
885 .with_quality(60),
886 ),
887 ],
888 expected_winner_index: 0,
889 description: "Very old date (1985) - film scan scenario".into(),
890 }
891}
892
893fn x11_future_date() -> ScenarioFixture {
894 ScenarioFixture {
895 scenario: TestScenario::X11FutureDate,
896 images: vec![
897 TestImage::new(
898 "x11_future.jpg",
899 TransformSpec::new("base_x11.jpg")
900 .with_scale(100)
901 .with_quality(95),
902 )
903 .with_exif(ExifSpec {
904 datetime: Some(Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap()),
905 ..Default::default()
906 }),
907 TestImage::new(
908 "x11_normal.jpg",
909 TransformSpec::new("base_x11.jpg")
910 .with_scale(99)
911 .with_quality(60),
912 )
913 .with_exif(ExifSpec {
914 datetime: Some(Utc.with_ymd_and_hms(2024, 6, 15, 14, 0, 0).unwrap()),
915 ..Default::default()
916 }),
917 ],
918 expected_winner_index: 0,
919 description: "Future date (2030) - camera clock error scenario".into(),
920 }
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926
927 #[test]
928 fn test_all_fixtures_count() {
929 let fixtures = all_fixtures();
930 assert_eq!(fixtures.len(), 32, "Should have exactly 32 fixtures");
931 }
932
933 #[test]
934 fn test_all_scenarios_covered() {
935 let fixtures = all_fixtures();
936 let all_scenarios = TestScenario::all();
937
938 for scenario in &all_scenarios {
939 let found = fixtures.iter().any(|f| f.scenario == *scenario);
940 assert!(found, "Missing fixture for scenario: {:?}", scenario);
941 }
942 }
943
944 #[test]
945 fn test_each_fixture_has_images() {
946 let fixtures = all_fixtures();
947 for fixture in &fixtures {
948 assert!(
949 !fixture.images.is_empty(),
950 "Fixture {:?} has no images",
951 fixture.scenario
952 );
953 }
954 }
955
956 #[test]
957 fn test_winner_index_valid() {
958 let fixtures = all_fixtures();
959 for fixture in &fixtures {
960 assert!(
961 fixture.expected_winner_index < fixture.images.len(),
962 "Fixture {:?} has invalid winner index {} (only {} images)",
963 fixture.scenario,
964 fixture.expected_winner_index,
965 fixture.images.len()
966 );
967 }
968 }
969}