From cafb95182c32190b7324d790ee513c002feb39df Mon Sep 17 00:00:00 2001
From: Arnaud de Bossoreille <arnaud.debossoreille@gmail.com>
Date: Fri, 1 Nov 2024 15:32:38 +0100
Subject: [PATCH] Test data records and drop impl fragments

---
 examples/machin/machin/build.rs             |  12 +-
 truc/src/generator/config.rs                |  47 ++++-
 truc/src/generator/fragment/data_records.rs | 218 ++++++++++++++++++++
 truc/src/generator/fragment/drop_impl.rs    | 154 ++++++++++++++
 truc/src/generator/fragment/mod.rs          |   2 +-
 truc/src/generator/mod.rs                   | 172 ++++++++-------
 truc/src/record/definition.rs               |   4 +-
 truc/src/record/type_resolver.rs            |   2 +-
 8 files changed, 520 insertions(+), 91 deletions(-)

diff --git a/examples/machin/machin/build.rs b/examples/machin/machin/build.rs
index 67e68fe..8fafd11 100644
--- a/examples/machin/machin/build.rs
+++ b/examples/machin/machin/build.rs
@@ -2,7 +2,11 @@ use std::{collections::BTreeMap, env, fs::File, io::Write, path::PathBuf};
 
 use machin_data::MachinEnum;
 use truc::{
-    generator::{config::GeneratorConfig, fragment::serde::SerdeImplGenerator, generate},
+    generator::{
+        config::GeneratorConfig,
+        fragment::{serde::SerdeImplGenerator, FragmentGenerator},
+        generate,
+    },
     record::{
         definition::{DatumDefinitionOverride, RecordDefinitionBuilder},
         type_resolver::{DynamicTypeInfo, StaticTypeResolver},
@@ -216,9 +220,9 @@ fn serialize_deserialize() {
         "{}",
         generate(
             &definition,
-            &GeneratorConfig {
-                custom_fragment_generators: vec![Box::new(SerdeImplGenerator)]
-            }
+            &GeneratorConfig::default_with_custom_generators([
+                Box::new(SerdeImplGenerator) as Box<dyn FragmentGenerator>
+            ])
         )
     )
     .unwrap();
diff --git a/truc/src/generator/config.rs b/truc/src/generator/config.rs
index 397f369..b03786b 100644
--- a/truc/src/generator/config.rs
+++ b/truc/src/generator/config.rs
@@ -1,6 +1,47 @@
-use super::fragment::FragmentGenerator;
+use super::fragment::{
+    data_records::DataRecordsGenerator, drop_impl::DropImplGenerator,
+    from_previous_record_data_records::FromPreviousRecordDataRecordsGenerator,
+    from_previous_record_impls::FromPreviousRecordImplsGenerator,
+    from_unpacked_record_impls::FromUnpackedRecordImplsGenerator, record::RecordGenerator,
+    record_impl::RecordImplGenerator, FragmentGenerator,
+};
 
-#[derive(Default)]
 pub struct GeneratorConfig {
-    pub custom_fragment_generators: Vec<Box<dyn FragmentGenerator>>,
+    pub(crate) fragment_generators: Vec<Box<dyn FragmentGenerator>>,
+}
+
+impl GeneratorConfig {
+    fn common_fragment_generators() -> [Box<dyn FragmentGenerator>; 7] {
+        [
+            Box::new(DataRecordsGenerator),
+            Box::new(RecordGenerator),
+            Box::new(RecordImplGenerator),
+            Box::new(DropImplGenerator),
+            Box::new(FromUnpackedRecordImplsGenerator),
+            Box::new(FromPreviousRecordDataRecordsGenerator),
+            Box::new(FromPreviousRecordImplsGenerator),
+        ]
+    }
+
+    pub fn new(fragment_generators: impl IntoIterator<Item = Box<dyn FragmentGenerator>>) -> Self {
+        Self {
+            fragment_generators: fragment_generators.into_iter().collect(),
+        }
+    }
+
+    pub fn default_with_custom_generators(
+        custom_generators: impl IntoIterator<Item = Box<dyn FragmentGenerator>>,
+    ) -> Self {
+        Self::new(
+            Self::common_fragment_generators()
+                .into_iter()
+                .chain(custom_generators.into_iter()),
+        )
+    }
+}
+
+impl Default for GeneratorConfig {
+    fn default() -> Self {
+        Self::new(Self::common_fragment_generators())
+    }
 }
diff --git a/truc/src/generator/fragment/data_records.rs b/truc/src/generator/fragment/data_records.rs
index 690c186..60c0ff0 100644
--- a/truc/src/generator/fragment/data_records.rs
+++ b/truc/src/generator/fragment/data_records.rs
@@ -54,3 +54,221 @@ using it."#,
         }
     }
 }
+
+#[cfg(test)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+mod tests {
+    use std::collections::BTreeSet;
+
+    use maplit::btreeset;
+    use pretty_assertions::assert_eq;
+    use syn::File;
+
+    use super::*;
+    use crate::{
+        generator::{config::GeneratorConfig, generate_variant, tests::assert_fragment_eq},
+        record::{definition::RecordDefinitionBuilder, type_resolver::HostTypeResolver},
+    };
+
+    #[test]
+    fn should_generate_empty_data_record() {
+        let mut builder = RecordDefinitionBuilder::new(HostTypeResolver);
+        builder.close_record_variant();
+        let definition = builder.build();
+
+        let config =
+            GeneratorConfig::new([Box::new(DataRecordsGenerator) as Box<dyn FragmentGenerator>]);
+
+        let mut scope = Scope::new();
+        let mut type_size_assertions = BTreeSet::new();
+
+        generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().next().expect("variant"),
+            None,
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+
+        let actual = syn::parse_str::<File>(&scope.to_string()).expect("actual");
+        let expected = syn::parse_str::<File>(
+            r#"
+/// Data container for packing/unpacking records.
+/// 
+/// All the fields are named for the safe interoperability between the generated code and the code
+/// using it.
+pub struct UnpackedRecord0;
+
+/// Data container for packing/unpacking records without the data to be left uninitialized.
+/// 
+/// All the fields are named for the safe interoperability between the generated code and the code
+/// using it.
+pub struct UnpackedUninitRecord0;
+
+/// It only exists to check that the uninitialized data is actually [`Copy`] at run time.
+struct UnpackedUninitSafeRecord0;
+
+impl From<UnpackedUninitRecord0> for UnpackedUninitSafeRecord0 {
+    fn from(_from: UnpackedUninitRecord0) -> Self {
+        Self {  }
+    }
+}
+"#,
+        )
+        .expect("expected");
+
+        if actual != expected {
+            assert_eq!(quote!(#expected).to_string(), quote!(#actual).to_string());
+        }
+        assert_eq!(btreeset![], type_size_assertions);
+    }
+
+    #[test]
+    fn should_generate_data_record_with_data() {
+        let mut builder = RecordDefinitionBuilder::new(HostTypeResolver);
+        builder.add_datum_allow_uninit::<u32, _>("integer");
+        builder.add_datum::<u32, _>("not_copy_integer");
+        builder.close_record_variant();
+        let definition = builder.build();
+
+        let config =
+            GeneratorConfig::new([Box::new(DataRecordsGenerator) as Box<dyn FragmentGenerator>]);
+
+        let mut scope = Scope::new();
+        let mut type_size_assertions = BTreeSet::new();
+
+        generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().next().expect("variant"),
+            None,
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+
+        assert_fragment_eq(
+            r#"
+/// Data container for packing/unpacking records.
+/// 
+/// All the fields are named for the safe interoperability between the generated code and the code
+/// using it.
+pub struct UnpackedRecord0 {
+    pub integer: u32,
+    pub not_copy_integer: u32,
+}
+
+/// Data container for packing/unpacking records without the data to be left uninitialized.
+/// 
+/// All the fields are named for the safe interoperability between the generated code and the code
+/// using it.
+pub struct UnpackedUninitRecord0 {
+    pub not_copy_integer: u32,
+}
+
+/// It only exists to check that the uninitialized data is actually [`Copy`] at run time.
+struct UnpackedUninitSafeRecord0<T0: Copy> {
+    pub integer: std::marker::PhantomData<T0>,
+    pub not_copy_integer: u32,
+}
+
+impl<T0: Copy> From<UnpackedUninitRecord0> for UnpackedUninitSafeRecord0<T0> {
+    fn from(from: UnpackedUninitRecord0) -> Self {
+        Self { integer: std::marker::PhantomData, not_copy_integer: from.not_copy_integer }
+    }
+}
+"#,
+            &scope.to_string(),
+        );
+
+        assert_eq!(
+            btreeset![("u32", std::mem::size_of::<u32>()),],
+            type_size_assertions
+        );
+    }
+
+    #[test]
+    fn should_generate_next_data_record_with_data() {
+        let mut builder = RecordDefinitionBuilder::new(HostTypeResolver);
+        let i0 = builder.add_datum_allow_uninit::<u32, _>("integer0");
+        let nci0 = builder.add_datum::<u32, _>("not_copy_integer0");
+        builder.add_datum_allow_uninit::<bool, _>("boolean1");
+        builder.close_record_variant();
+        builder.remove_datum(i0);
+        builder.remove_datum(nci0);
+        builder.add_datum_allow_uninit::<u32, _>("integer1");
+        builder.add_datum::<u32, _>("not_copy_integer1");
+        builder.close_record_variant();
+        let definition = builder.build();
+
+        let config =
+            GeneratorConfig::new([Box::new(DataRecordsGenerator) as Box<dyn FragmentGenerator>]);
+
+        let mut scope = Scope::new();
+        let mut type_size_assertions = BTreeSet::new();
+
+        let record0_spec = generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().next().expect("variant"),
+            None,
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+        let mut scope = Scope::new();
+        type_size_assertions.clear();
+        generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().nth(1).expect("variant"),
+            Some(&record0_spec),
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+
+        assert_fragment_eq(
+            r#"
+/// Data container for packing/unpacking records.
+/// 
+/// All the fields are named for the safe interoperability between the generated code and the code
+/// using it.
+pub struct UnpackedRecord1 {
+    pub boolean1: bool,
+    pub integer1: u32,
+    pub not_copy_integer1: u32,
+}
+
+/// Data container for packing/unpacking records without the data to be left uninitialized.
+/// 
+/// All the fields are named for the safe interoperability between the generated code and the code
+/// using it.
+pub struct UnpackedUninitRecord1 {
+    pub not_copy_integer1: u32,
+}
+
+/// It only exists to check that the uninitialized data is actually [`Copy`] at run time.
+struct UnpackedUninitSafeRecord1<T0: Copy, T1: Copy> {
+    pub boolean1: std::marker::PhantomData<T0>,
+    pub integer1: std::marker::PhantomData<T1>,
+    pub not_copy_integer1: u32,
+}
+
+impl<T0: Copy, T1: Copy> From<UnpackedUninitRecord1> for UnpackedUninitSafeRecord1<T0, T1> {
+    fn from(from: UnpackedUninitRecord1) -> Self {
+        Self { boolean1: std::marker::PhantomData, integer1: std::marker::PhantomData, not_copy_integer1: from.not_copy_integer1 }
+    }
+}
+"#,
+            &scope.to_string(),
+        );
+
+        assert_eq!(
+            btreeset![("u32", std::mem::size_of::<u32>()),],
+            type_size_assertions
+        );
+    }
+}
diff --git a/truc/src/generator/fragment/drop_impl.rs b/truc/src/generator/fragment/drop_impl.rs
index 41f2838..348bdd5 100644
--- a/truc/src/generator/fragment/drop_impl.rs
+++ b/truc/src/generator/fragment/drop_impl.rs
@@ -27,3 +27,157 @@ impl FragmentGenerator for DropImplGenerator {
         }
     }
 }
+
+#[cfg(test)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+mod tests {
+    use std::collections::BTreeSet;
+
+    use maplit::btreeset;
+    use pretty_assertions::assert_eq;
+    use syn::File;
+
+    use super::*;
+    use crate::{
+        generator::{config::GeneratorConfig, generate_variant, tests::assert_fragment_eq},
+        record::{definition::RecordDefinitionBuilder, type_resolver::HostTypeResolver},
+    };
+
+    #[test]
+    fn should_generate_empty_drop_impl() {
+        let mut builder = RecordDefinitionBuilder::new(HostTypeResolver);
+        builder.close_record_variant();
+        let definition = builder.build();
+
+        let config =
+            GeneratorConfig::new([Box::new(DropImplGenerator) as Box<dyn FragmentGenerator>]);
+
+        let mut scope = Scope::new();
+        let mut type_size_assertions = BTreeSet::new();
+
+        generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().next().expect("variant"),
+            None,
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+
+        let actual = syn::parse_str::<File>(&scope.to_string()).expect("actual");
+        let expected = syn::parse_str::<File>(
+            r#"
+impl < const CAP : usize > Drop for CappedRecord0 < CAP > { fn drop (& mut self) { } }
+"#,
+        )
+        .expect("expected");
+
+        if actual != expected {
+            assert_eq!(quote!(#expected).to_string(), quote!(#actual).to_string());
+        }
+        assert_eq!(btreeset![], type_size_assertions);
+    }
+
+    #[test]
+    fn should_generate_drop_impl_with_data() {
+        let mut builder = RecordDefinitionBuilder::new(HostTypeResolver);
+        builder.add_datum_allow_uninit::<u32, _>("integer");
+        builder.add_datum::<u32, _>("not_copy_integer");
+        builder.close_record_variant();
+        let definition = builder.build();
+
+        let config =
+            GeneratorConfig::new([Box::new(DropImplGenerator) as Box<dyn FragmentGenerator>]);
+
+        let mut scope = Scope::new();
+        let mut type_size_assertions = BTreeSet::new();
+
+        generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().next().expect("variant"),
+            None,
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+
+        assert_fragment_eq(
+            r#"
+impl<const CAP: usize> Drop for CappedRecord0<CAP> {
+    fn drop(&mut self) {
+        let _integer: u32 = unsafe { self.data.read(0) };
+        let _not_copy_integer: u32 = unsafe { self.data.read(4) };
+    }
+}
+"#,
+            &scope.to_string(),
+        );
+
+        assert_eq!(
+            btreeset![("u32", std::mem::size_of::<u32>()),],
+            type_size_assertions
+        );
+    }
+
+    #[test]
+    fn should_generate_next_drop_impl_with_data() {
+        let mut builder = RecordDefinitionBuilder::new(HostTypeResolver);
+        let i0 = builder.add_datum_allow_uninit::<u32, _>("integer0");
+        let nci0 = builder.add_datum::<u32, _>("not_copy_integer0");
+        builder.add_datum_allow_uninit::<bool, _>("boolean1");
+        builder.close_record_variant();
+        builder.remove_datum(i0);
+        builder.remove_datum(nci0);
+        builder.add_datum_allow_uninit::<u32, _>("integer1");
+        builder.add_datum::<u32, _>("not_copy_integer1");
+        builder.close_record_variant();
+        let definition = builder.build();
+
+        let config =
+            GeneratorConfig::new([Box::new(DropImplGenerator) as Box<dyn FragmentGenerator>]);
+
+        let mut scope = Scope::new();
+        let mut type_size_assertions = BTreeSet::new();
+
+        let record0_spec = generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().next().expect("variant"),
+            None,
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+        let mut scope = Scope::new();
+        type_size_assertions.clear();
+        generate_variant(
+            &definition,
+            definition.max_type_align(),
+            definition.variants().nth(1).expect("variant"),
+            Some(&record0_spec),
+            &config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
+
+        assert_fragment_eq(
+            r#"
+impl<const CAP: usize> Drop for CappedRecord1<CAP> {
+    fn drop(&mut self) {
+        let _boolean1: bool = unsafe { self.data.read(8) };
+        let _integer1: u32 = unsafe { self.data.read(0) };
+        let _not_copy_integer1: u32 = unsafe { self.data.read(4) };
+    }
+}
+"#,
+            &scope.to_string(),
+        );
+
+        assert_eq!(
+            btreeset![("u32", std::mem::size_of::<u32>()),],
+            type_size_assertions
+        );
+    }
+}
diff --git a/truc/src/generator/fragment/mod.rs b/truc/src/generator/fragment/mod.rs
index 43b2399..1fb9136 100644
--- a/truc/src/generator/fragment/mod.rs
+++ b/truc/src/generator/fragment/mod.rs
@@ -23,7 +23,7 @@ pub struct FragmentGeneratorSpecs<'a> {
     pub prev_record: Option<&'a RecordSpec<'a>>,
 }
 
-#[derive(Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct RecordSpec<'a> {
     pub max_type_align: usize,
     pub variant: &'a RecordVariant,
diff --git a/truc/src/generator/mod.rs b/truc/src/generator/mod.rs
index 5de9db0..ba0f3b1 100644
--- a/truc/src/generator/mod.rs
+++ b/truc/src/generator/mod.rs
@@ -5,16 +5,9 @@ use itertools::{Either, EitherOrBoth, Itertools};
 
 use self::{
     config::GeneratorConfig,
-    fragment::{
-        data_records::DataRecordsGenerator, drop_impl::DropImplGenerator,
-        from_previous_record_data_records::FromPreviousRecordDataRecordsGenerator,
-        from_previous_record_impls::FromPreviousRecordImplsGenerator,
-        from_unpacked_record_impls::FromUnpackedRecordImplsGenerator, record::RecordGenerator,
-        record_impl::RecordImplGenerator, FragmentGenerator, FragmentGeneratorSpecs, RecordGeneric,
-        RecordSpec,
-    },
+    fragment::{FragmentGeneratorSpecs, RecordGeneric, RecordSpec},
 };
-use crate::record::definition::{DatumDefinition, RecordDefinition};
+use crate::record::definition::{DatumDefinition, RecordDefinition, RecordVariant};
 
 pub mod config;
 pub mod fragment;
@@ -27,7 +20,7 @@ pub fn generate(definition: &RecordDefinition, config: &GeneratorConfig) -> Stri
 
     scope.import("truc_runtime::data", "RecordMaybeUninit");
 
-    for customizer in &config.custom_fragment_generators {
+    for customizer in &config.fragment_generators {
         customizer.imports(&mut scope);
     }
 
@@ -66,75 +59,15 @@ This is to be used in custom allocators."#,
     let mut type_size_assertions = BTreeSet::new();
 
     for variant in definition.variants() {
-        let data = variant
-            .data()
-            .sorted()
-            .map(|d| &definition[d])
-            .collect::<Vec<_>>();
-        let (minus_data, plus_data) = if let Some(prev_record_spec) = &prev_record_spec {
-            prev_record_spec
-                .variant
-                .data()
-                .sorted()
-                .merge_join_by(&data, |left_id, right| left_id.cmp(&right.id()))
-                .filter_map(|either| match either {
-                    EitherOrBoth::Left(left_id) => Some(Either::Left(&definition[left_id])),
-                    EitherOrBoth::Right(right) => Some(Either::Right(right)),
-                    EitherOrBoth::Both(_, _) => None,
-                })
-                .partition_map::<Vec<_>, Vec<_>, _, _, _>(|e| e)
-        } else {
-            (Vec::new(), data.clone())
-        };
-        let unpacked_uninit_safe_generic = safe_record_generic(&data);
-        let plus_uninit_safe_generic = safe_record_generic(&plus_data);
-        let record_spec = RecordSpec {
+        let record_spec = generate_variant(
+            definition,
             max_type_align,
             variant,
-            capped_record_name: format!("CappedRecord{}", variant.id()),
-            record_name: format!("Record{}", variant.id()),
-            unpacked_record_name: format!("UnpackedRecord{}", variant.id()),
-            unpacked_uninit_record_name: format!("UnpackedUninitRecord{}", variant.id()),
-            unpacked_uninit_safe_record_name: format!("UnpackedUninitSafeRecord{}", variant.id()),
-            unpacked_record_in_name: format!("UnpackedRecordIn{}", variant.id()),
-            unpacked_uninit_record_in_name: format!("UnpackedUninitRecordIn{}", variant.id()),
-            unpacked_uninit_safe_record_in_name: format!(
-                "UnpackedUninitSafeRecordIn{}",
-                variant.id()
-            ),
-            record_and_unpacked_out_name: format!("Record{}AndUnpackedOut", variant.id()),
-            data,
-            minus_data,
-            plus_data,
-            unpacked_uninit_safe_generic,
-            plus_uninit_safe_generic,
-        };
-
-        for datum in &record_spec.plus_data {
-            type_size_assertions.insert((datum.type_name(), datum.size()));
-        }
-
-        let specs = FragmentGeneratorSpecs {
-            record: &record_spec,
-            prev_record: prev_record_spec.as_ref(),
-        };
-
-        let common_fragment_generators: [Box<dyn FragmentGenerator>; 7] = [
-            Box::new(DataRecordsGenerator),
-            Box::new(RecordGenerator),
-            Box::new(RecordImplGenerator),
-            Box::new(DropImplGenerator),
-            Box::new(FromUnpackedRecordImplsGenerator),
-            Box::new(FromPreviousRecordDataRecordsGenerator),
-            Box::new(FromPreviousRecordImplsGenerator),
-        ];
-        let fragment_generators = common_fragment_generators
-            .iter()
-            .chain(config.custom_fragment_generators.iter());
-
-        for generator in fragment_generators {
-            generator.generate(&specs, &mut scope);
-        }
+            prev_record_spec.as_ref(),
+            config,
+            &mut scope,
+            &mut type_size_assertions,
+        );
 
         prev_record_spec = Some(record_spec);
     }
@@ -151,6 +84,74 @@ This is to be used in custom allocators."#,
     scope.to_string()
 }
 
+pub(crate) fn generate_variant<'a>(
+    definition: &'a RecordDefinition,
+    max_type_align: usize,
+    variant: &'a RecordVariant,
+    prev_record_spec: Option<&RecordSpec>,
+    config: &GeneratorConfig,
+    scope: &mut Scope,
+    type_size_assertions: &mut BTreeSet<(&'a str, usize)>,
+) -> RecordSpec<'a> {
+    let data = variant
+        .data()
+        .sorted()
+        .map(|d| &definition[d])
+        .collect::<Vec<_>>();
+    let (minus_data, plus_data) = if let Some(prev_record_spec) = &prev_record_spec {
+        prev_record_spec
+            .variant
+            .data()
+            .sorted()
+            .merge_join_by(&data, |left_id, right| left_id.cmp(&right.id()))
+            .filter_map(|either| match either {
+                EitherOrBoth::Left(left_id) => Some(Either::Left(&definition[left_id])),
+                EitherOrBoth::Right(right) => Some(Either::Right(right)),
+                EitherOrBoth::Both(_, _) => None,
+            })
+            .partition_map::<Vec<_>, Vec<_>, _, _, _>(|e| e)
+    } else {
+        (Vec::new(), data.clone())
+    };
+    let unpacked_uninit_safe_generic = safe_record_generic(&data);
+    let plus_uninit_safe_generic = safe_record_generic(&plus_data);
+    let record_spec = RecordSpec {
+        max_type_align,
+        variant,
+        capped_record_name: format!("CappedRecord{}", variant.id()),
+        record_name: format!("Record{}", variant.id()),
+        unpacked_record_name: format!("UnpackedRecord{}", variant.id()),
+        unpacked_uninit_record_name: format!("UnpackedUninitRecord{}", variant.id()),
+        unpacked_uninit_safe_record_name: format!("UnpackedUninitSafeRecord{}", variant.id()),
+        unpacked_record_in_name: format!("UnpackedRecordIn{}", variant.id()),
+        unpacked_uninit_record_in_name: format!("UnpackedUninitRecordIn{}", variant.id()),
+        unpacked_uninit_safe_record_in_name: format!("UnpackedUninitSafeRecordIn{}", variant.id()),
+        record_and_unpacked_out_name: format!("Record{}AndUnpackedOut", variant.id()),
+        data,
+        minus_data,
+        plus_data,
+        unpacked_uninit_safe_generic,
+        plus_uninit_safe_generic,
+    };
+
+    for datum in &record_spec.plus_data {
+        type_size_assertions.insert((datum.type_name(), datum.size()));
+    }
+
+    let specs = FragmentGeneratorSpecs {
+        record: &record_spec,
+        prev_record: prev_record_spec,
+    };
+
+    let fragment_generators = config.fragment_generators.iter();
+
+    for generator in fragment_generators {
+        generator.generate(&specs, scope);
+    }
+
+    record_spec
+}
+
 struct RecordImplRecordNames<'a> {
     name: &'a str,
     unpacked: &'a str,
@@ -300,8 +301,11 @@ fn generate_data_out_record(
 #[cfg(test)]
 #[cfg_attr(coverage_nightly, coverage(off))]
 mod tests {
+    use fragment::FragmentGenerator;
+    use pretty_assertions::assert_eq;
     use rand::Rng;
     use rand_chacha::rand_core::SeedableRng;
+    use syn::File;
 
     use super::*;
     use crate::{
@@ -312,6 +316,14 @@ mod tests {
         },
     };
 
+    pub(crate) fn assert_fragment_eq(left: &str, right: &str) {
+        let parsed_left = syn::parse_str::<File>(left).expect("left");
+        let parsed_right = syn::parse_str::<File>(right).expect("right");
+        if parsed_left != parsed_right {
+            assert_eq!(left, right.to_string());
+        }
+    }
+
     fn add_one<R: TypeResolver>(
         definition: &mut RecordDefinitionBuilder<R>,
         rng: &mut rand_chacha::ChaCha8Rng,
@@ -384,9 +396,9 @@ mod tests {
             let def = definition.build();
             generate(
                 &def,
-                &GeneratorConfig {
-                    custom_fragment_generators: vec![Box::new(SerdeImplGenerator)],
-                },
+                &GeneratorConfig::default_with_custom_generators([
+                    Box::new(SerdeImplGenerator) as Box<dyn FragmentGenerator>
+                ]),
             );
         }
     }
diff --git a/truc/src/record/definition.rs b/truc/src/record/definition.rs
index 08f2ed1..c42b038 100644
--- a/truc/src/record/definition.rs
+++ b/truc/src/record/definition.rs
@@ -14,7 +14,7 @@ impl Debug for DatumId {
     }
 }
 
-#[derive(Debug, new)]
+#[derive(PartialEq, Eq, Debug, new)]
 pub struct DatumDefinition {
     id: DatumId,
     name: String,
@@ -108,7 +108,7 @@ impl DatumDefinitionCollection {
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display, From)]
 pub struct RecordVariantId(usize);
 
-#[derive(Debug)]
+#[derive(PartialEq, Eq, Debug, new)]
 pub struct RecordVariant {
     id: RecordVariantId,
     data: Vec<DatumId>,
diff --git a/truc/src/record/type_resolver.rs b/truc/src/record/type_resolver.rs
index 116ca0a..78f05ef 100644
--- a/truc/src/record/type_resolver.rs
+++ b/truc/src/record/type_resolver.rs
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::record::type_name::{truc_dynamic_type_name, truc_type_name};
 
-#[derive(Clone, Debug, Serialize, Deserialize)]
+#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
 pub struct TypeInfo {
     pub name: String,
     pub size: usize,