Skip to main content

immich_lib/testing/
scenarios.rs

1//! Test scenario types for duplicate group categorization.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Test scenarios for categorizing duplicate groups.
7///
8/// Each scenario represents a specific test case that needs coverage
9/// in the integration test suite.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum TestScenario {
12    // Winner selection scenarios (W)
13    /// Clear dimension winner (different width x height)
14    W1ClearDimensionWinner,
15    /// Same dimensions, different file size
16    W2SameDimensionsDifferentSize,
17    /// Same dimensions, same file size
18    W3SameDimensionsSameSize,
19    /// Some assets missing dimensions
20    W4SomeMissingDimensions,
21    /// Only one asset has dimensions
22    W5OnlyOneHasDimensions,
23    /// All assets missing dimensions
24    W6AllMissingDimensions,
25    /// 3+ assets in group
26    W7ThreePlusDuplicates,
27    /// Same pixel count, different aspect ratio
28    W8SamePixelsDifferentAspect,
29
30    // Consolidation scenarios (C)
31    /// Winner lacks GPS, loser has GPS
32    C1WinnerLacksGpsLoserHas,
33    /// Winner lacks datetime, loser has datetime
34    C2WinnerLacksDatetimeLoserHas,
35    /// Winner lacks description, loser has description
36    C3WinnerLacksDescriptionLoserHas,
37    /// Winner lacks all three, loser has all
38    C4WinnerLacksAllLoserHasAll,
39    /// Both have GPS (no consolidation needed)
40    C5BothHaveGps,
41    /// Multiple losers contribute different fields
42    C6MultipleLosersContribute,
43    /// No loser has what winner lacks
44    C7NoLoserHasNeeded,
45    /// Winner already has everything
46    C8WinnerHasEverything,
47
48    // Conflict scenarios (F)
49    /// GPS conflict (different locations)
50    F1GpsConflict,
51    /// GPS within threshold (should NOT conflict)
52    F2GpsWithinThreshold,
53    /// Timezone conflict
54    F3TimezoneConflict,
55    /// Camera info conflict
56    F4CameraConflict,
57    /// Capture time conflict
58    F5CaptureTimeConflict,
59    /// Multiple conflicts
60    F6MultipleConflicts,
61    /// No conflicts
62    F7NoConflicts,
63
64    // Edge case scenarios (X)
65    /// Single asset "group"
66    X1SingleAssetGroup,
67    /// Large group (10+ duplicates)
68    X2LargeGroup,
69    /// Large file (>50MB)
70    X3LargeFile,
71    /// Special characters in filename
72    X4SpecialCharsFilename,
73    /// Video duplicates
74    X5Video,
75    /// PNG files (limited EXIF)
76    X7Png,
77    /// Unicode in description
78    X9UnicodeDescription,
79    /// Very old date (<1990)
80    X10VeryOldDate,
81    /// Future date
82    X11FutureDate,
83}
84
85impl TestScenario {
86    /// Returns all test scenarios.
87    pub fn all() -> Vec<TestScenario> {
88        vec![
89            // Winner selection
90            Self::W1ClearDimensionWinner,
91            Self::W2SameDimensionsDifferentSize,
92            Self::W3SameDimensionsSameSize,
93            Self::W4SomeMissingDimensions,
94            Self::W5OnlyOneHasDimensions,
95            Self::W6AllMissingDimensions,
96            Self::W7ThreePlusDuplicates,
97            Self::W8SamePixelsDifferentAspect,
98            // Consolidation
99            Self::C1WinnerLacksGpsLoserHas,
100            Self::C2WinnerLacksDatetimeLoserHas,
101            Self::C3WinnerLacksDescriptionLoserHas,
102            Self::C4WinnerLacksAllLoserHasAll,
103            Self::C5BothHaveGps,
104            Self::C6MultipleLosersContribute,
105            Self::C7NoLoserHasNeeded,
106            Self::C8WinnerHasEverything,
107            // Conflicts
108            Self::F1GpsConflict,
109            Self::F2GpsWithinThreshold,
110            Self::F3TimezoneConflict,
111            Self::F4CameraConflict,
112            Self::F5CaptureTimeConflict,
113            Self::F6MultipleConflicts,
114            Self::F7NoConflicts,
115            // Edge cases
116            Self::X1SingleAssetGroup,
117            Self::X2LargeGroup,
118            Self::X3LargeFile,
119            Self::X4SpecialCharsFilename,
120            Self::X5Video,
121            Self::X7Png,
122            Self::X9UnicodeDescription,
123            Self::X10VeryOldDate,
124            Self::X11FutureDate,
125        ]
126    }
127
128    /// Returns the short code (e.g., "w1", "c2", "f3", "x5").
129    pub fn code(&self) -> &'static str {
130        match self {
131            Self::W1ClearDimensionWinner => "w1",
132            Self::W2SameDimensionsDifferentSize => "w2",
133            Self::W3SameDimensionsSameSize => "w3",
134            Self::W4SomeMissingDimensions => "w4",
135            Self::W5OnlyOneHasDimensions => "w5",
136            Self::W6AllMissingDimensions => "w6",
137            Self::W7ThreePlusDuplicates => "w7",
138            Self::W8SamePixelsDifferentAspect => "w8",
139            Self::C1WinnerLacksGpsLoserHas => "c1",
140            Self::C2WinnerLacksDatetimeLoserHas => "c2",
141            Self::C3WinnerLacksDescriptionLoserHas => "c3",
142            Self::C4WinnerLacksAllLoserHasAll => "c4",
143            Self::C5BothHaveGps => "c5",
144            Self::C6MultipleLosersContribute => "c6",
145            Self::C7NoLoserHasNeeded => "c7",
146            Self::C8WinnerHasEverything => "c8",
147            Self::F1GpsConflict => "f1",
148            Self::F2GpsWithinThreshold => "f2",
149            Self::F3TimezoneConflict => "f3",
150            Self::F4CameraConflict => "f4",
151            Self::F5CaptureTimeConflict => "f5",
152            Self::F6MultipleConflicts => "f6",
153            Self::F7NoConflicts => "f7",
154            Self::X1SingleAssetGroup => "x1",
155            Self::X2LargeGroup => "x2",
156            Self::X3LargeFile => "x3",
157            Self::X4SpecialCharsFilename => "x4",
158            Self::X5Video => "x5",
159            Self::X7Png => "x7",
160            Self::X9UnicodeDescription => "x9",
161            Self::X10VeryOldDate => "x10",
162            Self::X11FutureDate => "x11",
163        }
164    }
165
166    /// Returns the category prefix (W, C, F, or X).
167    pub fn category(&self) -> &'static str {
168        match self {
169            Self::W1ClearDimensionWinner
170            | Self::W2SameDimensionsDifferentSize
171            | Self::W3SameDimensionsSameSize
172            | Self::W4SomeMissingDimensions
173            | Self::W5OnlyOneHasDimensions
174            | Self::W6AllMissingDimensions
175            | Self::W7ThreePlusDuplicates
176            | Self::W8SamePixelsDifferentAspect => "Winner Selection",
177            Self::C1WinnerLacksGpsLoserHas
178            | Self::C2WinnerLacksDatetimeLoserHas
179            | Self::C3WinnerLacksDescriptionLoserHas
180            | Self::C4WinnerLacksAllLoserHasAll
181            | Self::C5BothHaveGps
182            | Self::C6MultipleLosersContribute
183            | Self::C7NoLoserHasNeeded
184            | Self::C8WinnerHasEverything => "Consolidation",
185            Self::F1GpsConflict
186            | Self::F2GpsWithinThreshold
187            | Self::F3TimezoneConflict
188            | Self::F4CameraConflict
189            | Self::F5CaptureTimeConflict
190            | Self::F6MultipleConflicts
191            | Self::F7NoConflicts => "Conflicts",
192            Self::X1SingleAssetGroup
193            | Self::X2LargeGroup
194            | Self::X3LargeFile
195            | Self::X4SpecialCharsFilename
196            | Self::X5Video
197            | Self::X7Png
198            | Self::X9UnicodeDescription
199            | Self::X10VeryOldDate
200            | Self::X11FutureDate => "Edge Cases",
201        }
202    }
203}
204
205impl fmt::Display for TestScenario {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        let name = match self {
208            Self::W1ClearDimensionWinner => "W1: Clear dimension winner",
209            Self::W2SameDimensionsDifferentSize => "W2: Same dimensions, different size",
210            Self::W3SameDimensionsSameSize => "W3: Same dimensions, same size",
211            Self::W4SomeMissingDimensions => "W4: Some missing dimensions",
212            Self::W5OnlyOneHasDimensions => "W5: Only one has dimensions",
213            Self::W6AllMissingDimensions => "W6: All missing dimensions",
214            Self::W7ThreePlusDuplicates => "W7: 3+ duplicates",
215            Self::W8SamePixelsDifferentAspect => "W8: Same pixels, different aspect",
216            Self::C1WinnerLacksGpsLoserHas => "C1: Winner lacks GPS, loser has",
217            Self::C2WinnerLacksDatetimeLoserHas => "C2: Winner lacks datetime, loser has",
218            Self::C3WinnerLacksDescriptionLoserHas => "C3: Winner lacks description, loser has",
219            Self::C4WinnerLacksAllLoserHasAll => "C4: Winner lacks all, loser has all",
220            Self::C5BothHaveGps => "C5: Both have GPS",
221            Self::C6MultipleLosersContribute => "C6: Multiple losers contribute",
222            Self::C7NoLoserHasNeeded => "C7: No loser has needed",
223            Self::C8WinnerHasEverything => "C8: Winner has everything",
224            Self::F1GpsConflict => "F1: GPS conflict",
225            Self::F2GpsWithinThreshold => "F2: GPS within threshold",
226            Self::F3TimezoneConflict => "F3: Timezone conflict",
227            Self::F4CameraConflict => "F4: Camera conflict",
228            Self::F5CaptureTimeConflict => "F5: Capture time conflict",
229            Self::F6MultipleConflicts => "F6: Multiple conflicts",
230            Self::F7NoConflicts => "F7: No conflicts",
231            Self::X1SingleAssetGroup => "X1: Single asset group",
232            Self::X2LargeGroup => "X2: Large group (10+)",
233            Self::X3LargeFile => "X3: Large file (>50MB)",
234            Self::X4SpecialCharsFilename => "X4: Special chars in filename",
235            Self::X5Video => "X5: Video",
236            Self::X7Png => "X7: PNG",
237            Self::X9UnicodeDescription => "X9: Unicode description",
238            Self::X10VeryOldDate => "X10: Very old date (<1990)",
239            Self::X11FutureDate => "X11: Future date",
240        };
241        write!(f, "{}", name)
242    }
243}
244
245/// A match between a test scenario and a duplicate group.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ScenarioMatch {
248    /// The matched scenario
249    pub scenario: TestScenario,
250    /// Duplicate group ID
251    pub duplicate_id: String,
252    /// Description of why this matched
253    pub details: String,
254}