Skip to main content

i_slint_compiler/generator/python/
diff.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::collections::{BTreeMap, BTreeSet};
5
6use smol_str::SmolStr;
7
8use super::{PyComponent, PyEnum, PyModule, PyProperty, PyStruct, PyStructField, PyStructOrEnum};
9
10#[cfg(test)]
11use super::PyEnumVariant;
12
13impl PyModule {
14    pub fn changed_globals(&self, other: &Self) -> Option<PyComponentsDifference> {
15        PyComponentsDifference::compare(&self.globals, &other.globals)
16    }
17
18    pub fn changed_components(&self, other: &Self) -> Option<PyComponentsDifference> {
19        PyComponentsDifference::compare(&self.components, &other.components)
20    }
21
22    pub fn changed_structs_or_enums(&self, other: &Self) -> Option<PyStructsOrEnumsDifference> {
23        PyStructsOrEnumsDifference::compare(&self.structs_and_enums, &other.structs_and_enums)
24    }
25}
26
27pub struct PyComponentsDifference {
28    pub added_components: Vec<SmolStr>,
29    pub removed_components: Vec<SmolStr>,
30    pub changed_components: Vec<(SmolStr, ComponentDifference)>,
31}
32
33impl PyComponentsDifference {
34    fn compare(orig: &[PyComponent], new: &[PyComponent]) -> Option<Self> {
35        let orig_components = orig
36            .iter()
37            .map(|compo| (compo.name.as_str(), compo))
38            .collect::<BTreeMap<&str, &PyComponent>>();
39
40        let new_components = new
41            .iter()
42            .map(|compo| (compo.name.as_str(), compo))
43            .collect::<BTreeMap<&str, &PyComponent>>();
44
45        let added_components = new_components
46            .iter()
47            .filter_map(|(name, _)| {
48                if orig_components.contains_key(name) { None } else { Some((*name).into()) }
49            })
50            .collect::<Vec<_>>();
51
52        let removed_components =
53            orig_components
54                .iter()
55                .filter_map(|(name, _)| {
56                    if new_components.contains_key(name) { None } else { Some((*name).into()) }
57                })
58                .collect::<Vec<_>>();
59
60        let changed_components = orig_components
61            .iter()
62            .filter_map(|(name, orig_global)| {
63                let new_glob = new_components.get(name)?;
64
65                let diff = ComponentDifference::compare(&orig_global, &new_glob);
66
67                diff.map(|diff| ((*name).into(), diff))
68            })
69            .collect::<Vec<_>>();
70
71        if !added_components.is_empty()
72            || !removed_components.is_empty()
73            || !changed_components.is_empty()
74        {
75            Some(PyComponentsDifference {
76                added_components,
77                removed_components,
78                changed_components,
79            })
80        } else {
81            None
82        }
83    }
84
85    pub fn incompatible_changes(&self) -> bool {
86        !self.removed_components.is_empty()
87            || self.changed_components.iter().any(|(_, change)| change.incompatible_changes())
88    }
89}
90
91#[derive(PartialEq, Debug)]
92pub struct TypeChange {
93    pub name: SmolStr,
94    pub old_type: SmolStr,
95    pub new_type: SmolStr,
96}
97
98#[derive(PartialEq, Debug)]
99pub struct ComponentDifference {
100    // TODO: represent callbacks and functions differently?
101    pub added_properties: Vec<PyProperty>,
102    pub removed_properties: Vec<PyProperty>,
103    pub type_changed_properties: Vec<TypeChange>,
104    pub added_aliases: Vec<SmolStr>,
105    pub removed_aliases: Vec<SmolStr>,
106}
107
108impl ComponentDifference {
109    fn compare(old_compo: &PyComponent, new_compo: &PyComponent) -> Option<Self> {
110        let orig_props = old_compo
111            .properties
112            .iter()
113            .map(|p| (p.name.as_str(), p))
114            .collect::<BTreeMap<&str, &PyProperty>>();
115        let new_props = new_compo
116            .properties
117            .iter()
118            .map(|p| (p.name.as_str(), p))
119            .collect::<BTreeMap<&str, &PyProperty>>();
120
121        let added_properties = new_props
122            .iter()
123            .filter_map(|(name, new_prop)| {
124                if orig_props.contains_key(name) { None } else { Some((*new_prop).clone()) }
125            })
126            .collect::<Vec<_>>();
127
128        let removed_properties =
129            orig_props
130                .iter()
131                .filter_map(|(name, old_prop)| {
132                    if new_props.contains_key(name) { None } else { Some((*old_prop).clone()) }
133                })
134                .collect::<Vec<_>>();
135
136        let type_changed_properties = orig_props
137            .iter()
138            .filter_map(|(name, orig_prop)| {
139                let new_prop = new_props.get(name)?;
140
141                if orig_prop.ty != new_prop.ty {
142                    Some(TypeChange {
143                        name: (*name).into(),
144                        old_type: orig_prop.ty.clone(),
145                        new_type: new_prop.ty.clone(),
146                    })
147                } else {
148                    None
149                }
150            })
151            .collect::<Vec<_>>();
152
153        let old_aliases = old_compo.aliases.iter().collect::<BTreeSet<_>>();
154        let new_aliases = new_compo.aliases.iter().collect::<BTreeSet<_>>();
155
156        let added_aliases =
157            new_aliases.difference(&old_aliases).map(|s| (*s).clone()).collect::<Vec<_>>();
158        let removed_aliases =
159            old_aliases.difference(&new_aliases).map(|s| (*s).clone()).collect::<Vec<_>>();
160
161        let diff = Self {
162            added_properties,
163            removed_properties,
164            type_changed_properties,
165            added_aliases,
166            removed_aliases,
167        };
168        if diff.has_difference() { Some(diff) } else { None }
169    }
170
171    fn has_difference(&self) -> bool {
172        !self.added_properties.is_empty()
173            || !self.removed_properties.is_empty()
174            || !self.type_changed_properties.is_empty()
175            || !self.added_aliases.is_empty()
176            || !self.removed_aliases.is_empty()
177    }
178
179    fn incompatible_changes(&self) -> bool {
180        !self.removed_properties.is_empty()
181            || !self.type_changed_properties.is_empty()
182            || !self.removed_aliases.is_empty()
183    }
184}
185
186pub struct PyStructsOrEnumsDifference {
187    pub added_structs: Vec<SmolStr>,
188    pub removed_structs: Vec<SmolStr>,
189    pub changed_structs: Vec<(SmolStr, StructDifference)>,
190    pub added_enums: Vec<SmolStr>,
191    pub removed_enums: Vec<SmolStr>,
192    pub changed_enums: Vec<(SmolStr, EnumDifference)>,
193}
194
195impl PyStructsOrEnumsDifference {
196    fn compare(orig: &[PyStructOrEnum], new: &[PyStructOrEnum]) -> Option<Self> {
197        let mut orig_structs = BTreeMap::new();
198        let mut orig_enums = BTreeMap::new();
199        for struct_or_enum in orig {
200            match struct_or_enum {
201                PyStructOrEnum::Struct(py_struct) => {
202                    orig_structs.insert(py_struct.name.as_str(), py_struct);
203                }
204                PyStructOrEnum::Enum(py_enum) => {
205                    orig_enums.insert(py_enum.name.as_str(), py_enum);
206                }
207            }
208        }
209
210        let mut new_structs = BTreeMap::new();
211        let mut new_enums = BTreeMap::new();
212        for struct_or_enum in new {
213            match struct_or_enum {
214                PyStructOrEnum::Struct(py_struct) => {
215                    new_structs.insert(py_struct.name.as_str(), py_struct);
216                }
217                PyStructOrEnum::Enum(py_enum) => {
218                    new_enums.insert(py_enum.name.as_str(), py_enum);
219                }
220            }
221        }
222
223        let added_structs =
224            new_structs
225                .iter()
226                .filter_map(|(name, _)| {
227                    if orig_structs.contains_key(name) { None } else { Some((*name).into()) }
228                })
229                .collect::<Vec<_>>();
230
231        let added_enums = new_enums
232            .iter()
233            .filter_map(
234                |(name, _)| {
235                    if orig_enums.contains_key(name) { None } else { Some((*name).into()) }
236                },
237            )
238            .collect::<Vec<_>>();
239
240        let removed_structs =
241            orig_structs
242                .iter()
243                .filter_map(|(name, _)| {
244                    if new_structs.contains_key(name) { None } else { Some((*name).into()) }
245                })
246                .collect::<Vec<_>>();
247
248        let removed_enums = orig_enums
249            .iter()
250            .filter_map(
251                |(name, _)| {
252                    if new_enums.contains_key(name) { None } else { Some((*name).into()) }
253                },
254            )
255            .collect::<Vec<_>>();
256
257        let changed_structs = orig_structs
258            .iter()
259            .filter_map(|(name, orig_struct)| {
260                let new_struct = new_structs.get(name)?;
261
262                let diff = StructDifference::compare(&orig_struct, &new_struct);
263
264                diff.map(|diff| ((*name).into(), diff))
265            })
266            .collect::<Vec<_>>();
267
268        let changed_enums = orig_enums
269            .iter()
270            .filter_map(|(name, orig_enum)| {
271                let new_enum = new_enums.get(name)?;
272
273                let diff = EnumDifference::compare(&orig_enum, &new_enum);
274
275                diff.map(|diff| ((*name).into(), diff))
276            })
277            .collect::<Vec<_>>();
278
279        if !added_structs.is_empty()
280            || !removed_structs.is_empty()
281            || !changed_structs.is_empty()
282            || !added_enums.is_empty()
283            || !removed_enums.is_empty()
284            || !changed_enums.is_empty()
285        {
286            Some(Self {
287                added_structs,
288                removed_structs,
289                changed_structs,
290                added_enums,
291                removed_enums,
292                changed_enums,
293            })
294        } else {
295            None
296        }
297    }
298
299    pub fn incompatible_changes(&self) -> bool {
300        !self.removed_structs.is_empty()
301            || !self.removed_enums.is_empty()
302            || self.changed_structs.iter().any(|(_, c)| c.incompatible_changes())
303            || self.changed_enums.iter().any(|(_, c)| c.incompatible_changes())
304    }
305}
306
307#[derive(PartialEq, Debug)]
308pub struct StructDifference {
309    pub added_fields: Vec<PyStructField>,
310    pub removed_fields: Vec<PyStructField>,
311    pub type_changed_fields: Vec<TypeChange>,
312    pub added_aliases: Vec<SmolStr>,
313    pub removed_aliases: Vec<SmolStr>,
314}
315
316impl StructDifference {
317    fn compare(old_struct: &PyStruct, new_struct: &PyStruct) -> Option<Self> {
318        let orig_fields = old_struct
319            .fields
320            .iter()
321            .map(|f| (f.name.as_str(), f))
322            .collect::<BTreeMap<&str, &PyStructField>>();
323        let new_fields = new_struct
324            .fields
325            .iter()
326            .map(|f| (f.name.as_str(), f))
327            .collect::<BTreeMap<&str, &PyStructField>>();
328
329        let added_fields = new_fields
330            .iter()
331            .filter_map(|(name, new_field)| {
332                if orig_fields.contains_key(name) { None } else { Some((*new_field).clone()) }
333            })
334            .collect::<Vec<_>>();
335
336        let removed_fields = orig_fields
337            .iter()
338            .filter_map(|(name, old_field)| {
339                if new_fields.contains_key(name) { None } else { Some((*old_field).clone()) }
340            })
341            .collect::<Vec<_>>();
342
343        let type_changed_fields = orig_fields
344            .iter()
345            .filter_map(|(name, orig_field)| {
346                let new_field = new_fields.get(name)?;
347
348                if orig_field.ty != new_field.ty {
349                    Some(TypeChange {
350                        name: (*name).into(),
351                        old_type: orig_field.ty.clone(),
352                        new_type: new_field.ty.clone(),
353                    })
354                } else {
355                    None
356                }
357            })
358            .collect::<Vec<_>>();
359
360        let old_aliases = old_struct.aliases.iter().collect::<BTreeSet<_>>();
361        let new_aliases = new_struct.aliases.iter().collect::<BTreeSet<_>>();
362
363        let added_aliases =
364            new_aliases.difference(&old_aliases).map(|s| (*s).clone()).collect::<Vec<_>>();
365        let removed_aliases =
366            old_aliases.difference(&new_aliases).map(|s| (*s).clone()).collect::<Vec<_>>();
367
368        let diff = Self {
369            added_fields,
370            removed_fields,
371            type_changed_fields,
372            added_aliases,
373            removed_aliases,
374        };
375        if diff.has_difference() { Some(diff) } else { None }
376    }
377
378    fn has_difference(&self) -> bool {
379        !self.added_fields.is_empty()
380            || !self.removed_fields.is_empty()
381            || !self.type_changed_fields.is_empty()
382            || !self.added_aliases.is_empty()
383            || !self.removed_aliases.is_empty()
384    }
385
386    fn incompatible_changes(&self) -> bool {
387        !self.removed_fields.is_empty()
388            || !self.removed_aliases.is_empty()
389            || !self.type_changed_fields.is_empty()
390    }
391}
392
393#[derive(Debug, PartialEq)]
394pub struct EnumDifference {
395    pub added_variants: Vec<SmolStr>,
396    pub removed_variants: Vec<SmolStr>,
397    pub added_aliases: Vec<SmolStr>,
398    pub removed_aliases: Vec<SmolStr>,
399}
400
401impl EnumDifference {
402    fn compare(old_enum: &PyEnum, new_enum: &PyEnum) -> Option<Self> {
403        let old_variants = old_enum.variants.iter().map(|v| &v.name).collect::<BTreeSet<_>>();
404        let new_variants = new_enum.variants.iter().map(|v| &v.name).collect::<BTreeSet<_>>();
405
406        let added_variants =
407            new_variants.difference(&old_variants).map(|s| (*s).clone()).collect::<Vec<_>>();
408        let removed_variants =
409            old_variants.difference(&new_variants).map(|s| (*s).clone()).collect::<Vec<_>>();
410
411        let old_aliases = old_enum.aliases.iter().collect::<BTreeSet<_>>();
412        let new_aliases = new_enum.aliases.iter().collect::<BTreeSet<_>>();
413
414        let added_aliases =
415            new_aliases.difference(&old_aliases).map(|s| (*s).clone()).collect::<Vec<_>>();
416        let removed_aliases =
417            old_aliases.difference(&new_aliases).map(|s| (*s).clone()).collect::<Vec<_>>();
418
419        let diff = Self { added_variants, removed_variants, added_aliases, removed_aliases };
420        if diff.has_difference() { Some(diff) } else { None }
421    }
422
423    fn has_difference(&self) -> bool {
424        !self.added_variants.is_empty()
425            || !self.removed_variants.is_empty()
426            || !self.added_aliases.is_empty()
427            || !self.removed_aliases.is_empty()
428    }
429
430    fn incompatible_changes(&self) -> bool {
431        !self.removed_variants.is_empty() || !self.removed_aliases.is_empty()
432    }
433}
434
435#[test]
436fn globals() {
437    let old = super::PyModule {
438        globals: vec![
439            PyComponent {
440                name: SmolStr::new_static("SameGlobal"),
441                properties: vec![PyProperty {
442                    name: SmolStr::new_static("str_prop"),
443                    ty: SmolStr::new_static("str"),
444                }],
445                aliases: vec![SmolStr::new_static("SameGlobalAlias")],
446            },
447            PyComponent {
448                name: SmolStr::new_static("ChangedGlobal"),
449                properties: vec![
450                    PyProperty {
451                        name: SmolStr::new_static("same_str_prop"),
452                        ty: SmolStr::new_static("str"),
453                    },
454                    PyProperty {
455                        name: SmolStr::new_static("change_to_int_prop"),
456                        ty: SmolStr::new_static("str"),
457                    },
458                    PyProperty {
459                        name: SmolStr::new_static("removed_prop"),
460                        ty: SmolStr::new_static("int"),
461                    },
462                ],
463                aliases: vec![SmolStr::new_static("ChangedGlobalAlias")],
464            },
465            PyComponent {
466                name: SmolStr::new_static("ToBeRemoved"),
467                properties: Vec::new(),
468                aliases: Vec::new(),
469            },
470        ],
471        ..Default::default()
472    };
473
474    let new = super::PyModule {
475        globals: vec![
476            PyComponent {
477                name: SmolStr::new_static("SameGlobal"),
478                properties: vec![PyProperty {
479                    name: SmolStr::new_static("str_prop"),
480                    ty: SmolStr::new_static("str"),
481                }],
482                aliases: vec![SmolStr::new_static("SameGlobalAlias")],
483            },
484            PyComponent {
485                name: SmolStr::new_static("ChangedGlobal"),
486                properties: vec![
487                    PyProperty {
488                        name: SmolStr::new_static("same_str_prop"),
489                        ty: SmolStr::new_static("str"),
490                    },
491                    PyProperty {
492                        name: SmolStr::new_static("change_to_int_prop"),
493                        ty: SmolStr::new_static("int"),
494                    },
495                    PyProperty {
496                        name: SmolStr::new_static("new_prop"),
497                        ty: SmolStr::new_static("float"),
498                    },
499                ],
500                aliases: vec![SmolStr::new_static("NewGlobalAlias")],
501            },
502            PyComponent {
503                name: SmolStr::new_static("NewGlobal"),
504                properties: vec![PyProperty {
505                    name: SmolStr::new_static("str_prop"),
506                    ty: SmolStr::new_static("str"),
507                }],
508                aliases: Vec::new(),
509            },
510        ],
511        ..Default::default()
512    };
513
514    assert!(old.changed_globals(&old).is_none());
515
516    let changed = old.changed_globals(&new);
517    assert!(changed.is_some());
518    let changed = changed.unwrap();
519
520    assert_eq!(changed.added_components, vec![SmolStr::new_static("NewGlobal")]);
521    assert_eq!(changed.removed_components, vec![SmolStr::new_static("ToBeRemoved")]);
522
523    let expected_glob_change = ComponentDifference {
524        added_properties: vec![PyProperty {
525            name: SmolStr::new_static("new_prop"),
526            ty: SmolStr::new_static("float"),
527        }],
528        removed_properties: vec![PyProperty {
529            name: SmolStr::new_static("removed_prop"),
530            ty: SmolStr::new_static("int"),
531        }],
532        type_changed_properties: vec![TypeChange {
533            name: SmolStr::new_static("change_to_int_prop"),
534            old_type: SmolStr::new_static("str"),
535            new_type: SmolStr::new_static("int"),
536        }],
537        added_aliases: vec![SmolStr::new_static("NewGlobalAlias")],
538        removed_aliases: vec![SmolStr::new_static("ChangedGlobalAlias")],
539    };
540
541    assert_eq!(
542        changed.changed_components,
543        vec![(SmolStr::new_static("ChangedGlobal"), expected_glob_change)]
544    );
545}
546
547#[test]
548fn structs_and_enums() {
549    let old = super::PyModule {
550        structs_and_enums: vec![
551            PyStructOrEnum::Struct(PyStruct {
552                name: SmolStr::new_static("SameStruct"),
553                fields: vec![PyStructField {
554                    name: SmolStr::new_static("intfield"),
555                    ty: SmolStr::new_static("int"),
556                }],
557                aliases: vec![SmolStr::new_static("SameStructalias")],
558            }),
559            PyStructOrEnum::Struct(PyStruct {
560                name: SmolStr::new_static("StructWithChangedFields"),
561                fields: vec![
562                    PyStructField {
563                        name: SmolStr::new_static("removed_field"),
564                        ty: SmolStr::new_static("str"),
565                    },
566                    PyStructField {
567                        name: SmolStr::new_static("unchanged_field"),
568                        ty: SmolStr::new_static("str"),
569                    },
570                    PyStructField {
571                        name: SmolStr::new_static("to_int_field"),
572                        ty: SmolStr::new_static("float"),
573                    },
574                ],
575                aliases: vec![SmolStr::new_static("RemovedAlias")],
576            }),
577            PyStructOrEnum::Struct(PyStruct {
578                name: SmolStr::new_static("RemovedStruct"),
579                fields: Vec::new(),
580                aliases: Vec::new(),
581            }),
582            PyStructOrEnum::Struct(PyStruct {
583                name: SmolStr::new_static("StructBecomesEnum"),
584                fields: Vec::new(),
585                aliases: Vec::new(),
586            }),
587            PyStructOrEnum::Enum(PyEnum {
588                name: SmolStr::new_static("SameEnum"),
589                variants: vec![
590                    PyEnumVariant {
591                        name: SmolStr::new_static("Variant1"),
592                        strvalue: SmolStr::new_static("Variant1"),
593                    },
594                    PyEnumVariant {
595                        name: SmolStr::new_static("Variant2"),
596                        strvalue: SmolStr::new_static("Variant2"),
597                    },
598                ],
599                aliases: vec![SmolStr::new_static("SameEnumAlias")],
600            }),
601            PyStructOrEnum::Enum(PyEnum {
602                name: SmolStr::new_static("ChangedEnum"),
603                variants: vec![
604                    PyEnumVariant {
605                        name: SmolStr::new_static("Variant1"),
606                        strvalue: SmolStr::new_static("Variant1"),
607                    },
608                    PyEnumVariant {
609                        name: SmolStr::new_static("Variant2"),
610                        strvalue: SmolStr::new_static("Variant2"),
611                    },
612                ],
613                aliases: vec![SmolStr::new_static("ChangedEnumRemovedAlias")],
614            }),
615            PyStructOrEnum::Enum(PyEnum {
616                name: SmolStr::new_static("RemovedEnum"),
617                variants: vec![
618                    PyEnumVariant {
619                        name: SmolStr::new_static("Variant1"),
620                        strvalue: SmolStr::new_static("Variant1"),
621                    },
622                    PyEnumVariant {
623                        name: SmolStr::new_static("Variant2"),
624                        strvalue: SmolStr::new_static("Variant2"),
625                    },
626                ],
627                aliases: Vec::new(),
628            }),
629        ],
630        ..Default::default()
631    };
632
633    let new = super::PyModule {
634        structs_and_enums: vec![
635            PyStructOrEnum::Struct(PyStruct {
636                name: SmolStr::new_static("SameStruct"),
637                fields: vec![PyStructField {
638                    name: SmolStr::new_static("intfield"),
639                    ty: SmolStr::new_static("int"),
640                }],
641                aliases: vec![SmolStr::new_static("SameStructalias")],
642            }),
643            PyStructOrEnum::Struct(PyStruct {
644                name: SmolStr::new_static("StructWithChangedFields"),
645                fields: vec![
646                    PyStructField {
647                        name: SmolStr::new_static("added_field"),
648                        ty: SmolStr::new_static("str"),
649                    },
650                    PyStructField {
651                        name: SmolStr::new_static("unchanged_field"),
652                        ty: SmolStr::new_static("str"),
653                    },
654                    PyStructField {
655                        name: SmolStr::new_static("to_int_field"),
656                        ty: SmolStr::new_static("int"),
657                    },
658                ],
659                aliases: vec![SmolStr::new_static("NewAlias")],
660            }),
661            PyStructOrEnum::Struct(PyStruct {
662                name: SmolStr::new_static("AddedStruct"),
663                fields: Vec::new(),
664                aliases: Vec::new(),
665            }),
666            PyStructOrEnum::Enum(PyEnum {
667                name: SmolStr::new_static("StructBecomesEnum"),
668                variants: vec![
669                    PyEnumVariant {
670                        name: SmolStr::new_static("Variant1"),
671                        strvalue: SmolStr::new_static("Variant1"),
672                    },
673                    PyEnumVariant {
674                        name: SmolStr::new_static("Variant2"),
675                        strvalue: SmolStr::new_static("Variant2"),
676                    },
677                ],
678                aliases: Vec::new(),
679            }),
680            PyStructOrEnum::Enum(PyEnum {
681                name: SmolStr::new_static("SameEnum"),
682                variants: vec![
683                    PyEnumVariant {
684                        name: SmolStr::new_static("Variant1"),
685                        strvalue: SmolStr::new_static("Variant1"),
686                    },
687                    PyEnumVariant {
688                        name: SmolStr::new_static("Variant2"),
689                        strvalue: SmolStr::new_static("Variant2"),
690                    },
691                ],
692                aliases: vec![SmolStr::new_static("SameEnumAlias")],
693            }),
694            PyStructOrEnum::Enum(PyEnum {
695                name: SmolStr::new_static("ChangedEnum"),
696                variants: vec![
697                    PyEnumVariant {
698                        name: SmolStr::new_static("Variant3"),
699                        strvalue: SmolStr::new_static("Variant3"),
700                    },
701                    PyEnumVariant {
702                        name: SmolStr::new_static("Variant4"),
703                        strvalue: SmolStr::new_static("Variant4"),
704                    },
705                ],
706                aliases: vec![SmolStr::new_static("ChangedEnumAddedAlias")],
707            }),
708            PyStructOrEnum::Enum(PyEnum {
709                name: SmolStr::new_static("AddedEnum"),
710                variants: vec![
711                    PyEnumVariant {
712                        name: SmolStr::new_static("Variant1"),
713                        strvalue: SmolStr::new_static("Variant1"),
714                    },
715                    PyEnumVariant {
716                        name: SmolStr::new_static("Variant2"),
717                        strvalue: SmolStr::new_static("Variant2"),
718                    },
719                ],
720                aliases: Vec::new(),
721            }),
722        ],
723        ..Default::default()
724    };
725
726    assert!(old.changed_structs_or_enums(&old).is_none());
727
728    let changed = old.changed_structs_or_enums(&new);
729    assert!(changed.is_some());
730    let changed = changed.unwrap();
731
732    assert_eq!(changed.added_structs, vec![SmolStr::new_static("AddedStruct")]);
733    assert_eq!(
734        changed.removed_structs,
735        vec![SmolStr::new_static("RemovedStruct"), SmolStr::new_static("StructBecomesEnum")]
736    );
737
738    assert_eq!(
739        changed.added_enums,
740        vec![SmolStr::new_static("AddedEnum"), SmolStr::new_static("StructBecomesEnum")]
741    );
742    assert_eq!(changed.removed_enums, vec![SmolStr::new_static("RemovedEnum")]);
743
744    let expected_struct_change = StructDifference {
745        added_fields: vec![PyStructField {
746            name: SmolStr::new_static("added_field"),
747            ty: SmolStr::new_static("str"),
748        }],
749        removed_fields: vec![PyStructField {
750            name: SmolStr::new_static("removed_field"),
751            ty: SmolStr::new_static("str"),
752        }],
753        type_changed_fields: vec![TypeChange {
754            name: SmolStr::new_static("to_int_field"),
755            old_type: SmolStr::new_static("float"),
756            new_type: SmolStr::new_static("int"),
757        }],
758        added_aliases: vec![SmolStr::new_static("NewAlias")],
759        removed_aliases: vec![SmolStr::new_static("RemovedAlias")],
760    };
761
762    assert_eq!(
763        changed.changed_structs,
764        vec![(SmolStr::new_static("StructWithChangedFields"), expected_struct_change)]
765    );
766
767    let expected_enum_change = EnumDifference {
768        added_variants: vec![SmolStr::new_static("Variant3"), SmolStr::new_static("Variant4")],
769        removed_variants: vec![SmolStr::new_static("Variant1"), SmolStr::new_static("Variant2")],
770        added_aliases: vec![SmolStr::new_static("ChangedEnumAddedAlias")],
771        removed_aliases: vec![SmolStr::new_static("ChangedEnumRemovedAlias")],
772    };
773
774    assert_eq!(
775        changed.changed_enums,
776        vec![(SmolStr::new_static("ChangedEnum"), expected_enum_change)]
777    );
778}