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