Split integration tests into per-topic modules
Extracted 34 tests from the monolithic tests/integration.rs (2,728 lines) 
into focused modules:

- integration_structs.rs (5 tests) - Basic and nested struct serialization
- integration_collections.rs (8 tests) - Vec, HashMap, dash children
- integration_primitives.rs (11 tests) - Integers, floats, booleans, chars, tuples  
- integration_enums.rs (6 tests) - Unit, newtype, and struct enum variants
- integration_options.rs (4 tests) - Option field handling and null values

Each module includes its own copy of needed test macros and shared types
to avoid cross-module dependencies. This reduces merge friction and improves
navigability for contributors working on specific functionality areas.

The original integration.rs remains unchanged with all 199 tests.
Remaining 165 tests could follow the same pattern for topics like error
handling, roundtrip tests, edge cases, and advanced deserialization paths.
change lxmoywwtvslquymrlxwpxvpuzqovtkqq
commit 15ffcc155e032ad11a5f07a0f67e37e40fd6ad3c
author Alpha Chen <alpha@kejadlen.dev>
date
parent nmlylmrw
diff --git a/tests/integration_collections.rs b/tests/integration_collections.rs
new file mode 100644
index 0000000..0afac2a
--- /dev/null
+++ b/tests/integration_collections.rs
@@ -0,0 +1,189 @@
+//! Integration tests for collection types (Vec, HashMap, etc.).
+
+use indoc::indoc;
+use std::collections::HashMap;
+use serde::{Deserialize, Serialize};
+
+// Test macros
+macro_rules! deser_ok {
+    ($name:ident, $ty:ty, $input:expr, $expected:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = serde_kdl2::from_str($input).unwrap();
+            assert_eq!(val, $expected);
+        }
+    };
+}
+
+macro_rules! roundtrip {
+    ($name:ident, $ty:ty, $val:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = $val;
+            let output = serde_kdl2::to_string(&val).unwrap();
+            let rt: $ty = serde_kdl2::from_str(&output).unwrap();
+            assert_eq!(val, rt);
+        }
+    };
+}
+
+macro_rules! deser_field {
+    ($name:ident, $field:ident: $ty:ty, $input:expr, $expected:expr) => {
+        #[test]
+        fn $name() {
+            #[derive(Debug, Deserialize, PartialEq)]
+            struct W {
+                $field: $ty,
+            }
+            let val: W = serde_kdl2::from_str($input).unwrap();
+            assert_eq!(val.$field, $expected);
+        }
+    };
+}
+
+// Shared types
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Server {
+    host: String,
+    port: u16,
+}
+
+// ── Vec of primitives ──────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Tagged {
+    name: String,
+    tags: Vec<String>,
+}
+
+deser_ok!(
+    deserialize_vec_primitives,
+    Tagged,
+    indoc! {r#"
+        name "project"
+        tags "web" "rust" "config"
+    "#},
+    Tagged {
+        name: "project".into(),
+        tags: vec!["web".into(), "rust".into(), "config".into()],
+    }
+);
+
+roundtrip!(
+    serialize_vec_primitives,
+    Tagged,
+    Tagged {
+        name: "project".into(),
+        tags: vec!["web".into(), "rust".into(), "config".into()],
+    }
+);
+
+// ── Vec of structs ─────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Cluster {
+    server: Vec<Server>,
+}
+
+deser_ok!(
+    deserialize_vec_structs,
+    Cluster,
+    indoc! {r#"
+        server {
+            host "localhost"
+            port 8080
+        }
+        server {
+            host "example.com"
+            port 443
+        }
+    "#},
+    Cluster {
+        server: vec![
+            Server {
+                host: "localhost".into(),
+                port: 8080
+            },
+            Server {
+                host: "example.com".into(),
+                port: 443
+            },
+        ],
+    }
+);
+
+roundtrip!(
+    serialize_vec_structs,
+    Cluster,
+    Cluster {
+        server: vec![
+            Server {
+                host: "localhost".into(),
+                port: 8080
+            },
+            Server {
+                host: "example.com".into(),
+                port: 443
+            },
+        ],
+    }
+);
+
+// ── Dash children ──────────────────────────────────────────────────────
+
+deser_field!(
+    deserialize_dash_children,
+    items: Vec<i32>,
+    indoc! {"
+        items {
+            - 1
+            - 2
+            - 3
+        }
+    "},
+    vec![1, 2, 3]
+);
+
+// ── HashMap ────────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithMap {
+    settings: HashMap<String, String>,
+}
+
+#[test]
+fn deserialize_hashmap() {
+    let val: WithMap = serde_kdl2::from_str(indoc! {r#"
+        settings {
+            key1 "value1"
+            key2 "value2"
+        }
+    "#})
+    .unwrap();
+    assert_eq!(val.settings.get("key1"), Some(&"value1".into()));
+    assert_eq!(val.settings.get("key2"), Some(&"value2".into()));
+}
+
+#[test]
+fn serialize_hashmap() {
+    let mut settings = HashMap::new();
+    settings.insert("key1".into(), "value1".into());
+    let val = WithMap { settings };
+    let output = serde_kdl2::to_string(&val).unwrap();
+    let rt: WithMap = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(val, rt);
+}
+
+// ── Empty vec ──────────────────────────────────────────────────────────
+
+#[test]
+fn serialize_empty_vec() {
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct W {
+        items: Vec<String>,
+    }
+    let val = W { items: vec![] };
+    let output = serde_kdl2::to_string(&val).unwrap();
+    let rt: W = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(rt.items, Vec::<String>::new());
+}
\ No newline at end of file
diff --git a/tests/integration_enums.rs b/tests/integration_enums.rs
new file mode 100644
index 0000000..9be8341
--- /dev/null
+++ b/tests/integration_enums.rs
@@ -0,0 +1,142 @@
+//! Integration tests for enum serialization and deserialization.
+
+use indoc::indoc;
+use serde::{Deserialize, Serialize};
+
+// Test macros
+macro_rules! deser_ok {
+    ($name:ident, $ty:ty, $input:expr, $expected:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = serde_kdl2::from_str($input).unwrap();
+            assert_eq!(val, $expected);
+        }
+    };
+}
+
+macro_rules! roundtrip {
+    ($name:ident, $ty:ty, $val:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = $val;
+            let output = serde_kdl2::to_string(&val).unwrap();
+            let rt: $ty = serde_kdl2::from_str(&output).unwrap();
+            assert_eq!(val, rt);
+        }
+    };
+}
+
+// Shared types
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Color {
+    Red,
+    Green,
+    Blue,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Colored {
+    name: String,
+    color: Color,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Shape {
+    Circle { radius: f64 },
+    Rectangle { width: f64, height: f64 },
+}
+
+// ── Enum variants ──────────────────────────────────────────────────────
+
+deser_ok!(
+    deserialize_unit_variant,
+    Colored,
+    indoc! {r#"
+        name "widget"
+        color "Red"
+    "#},
+    Colored {
+        name: "widget".into(),
+        color: Color::Red
+    }
+);
+
+roundtrip!(
+    serialize_unit_variant,
+    Colored,
+    Colored {
+        name: "widget".into(),
+        color: Color::Green
+    }
+);
+
+// ── Struct variant enum ────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Drawing {
+    name: String,
+    shape: Shape,
+}
+
+deser_ok!(
+    deserialize_struct_variant,
+    Drawing,
+    indoc! {r#"
+        name "my drawing"
+        shape {
+            Circle {
+                radius 5.0
+            }
+        }
+    "#},
+    Drawing {
+        name: "my drawing".into(),
+        shape: Shape::Circle { radius: 5.0 }
+    }
+);
+
+roundtrip!(
+    serialize_struct_variant,
+    Drawing,
+    Drawing {
+        name: "my drawing".into(),
+        shape: Shape::Rectangle {
+            width: 10.0,
+            height: 20.0
+        },
+    }
+);
+
+// ── Newtype variant enum ───────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Wrapper {
+    Text(String),
+    Number(i64),
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Wrapped {
+    value: Wrapper,
+}
+
+deser_ok!(
+    deserialize_newtype_variant,
+    Wrapped,
+    indoc! {r#"
+        value {
+            Text "hello"
+        }
+    "#},
+    Wrapped {
+        value: Wrapper::Text(String::from("hello"))
+    }
+);
+
+roundtrip!(
+    serialize_newtype_variant,
+    Wrapped,
+    Wrapped {
+        value: Wrapper::Text(String::from("hello"))
+    }
+);
\ No newline at end of file
diff --git a/tests/integration_options.rs b/tests/integration_options.rs
new file mode 100644
index 0000000..ada7292
--- /dev/null
+++ b/tests/integration_options.rs
@@ -0,0 +1,80 @@
+//! Integration tests for Option field handling.
+
+use indoc::indoc;
+use serde::{Deserialize, Serialize};
+
+// Test macros
+macro_rules! deser_ok {
+    ($name:ident, $ty:ty, $input:expr, $expected:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = serde_kdl2::from_str($input).unwrap();
+            assert_eq!(val, $expected);
+        }
+    };
+}
+
+// ── Option fields ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct OptionalFields {
+    required: String,
+    optional: Option<String>,
+}
+
+deser_ok!(
+    deserialize_option_present,
+    OptionalFields,
+    indoc! {r#"
+        required "hello"
+        optional "world"
+    "#},
+    OptionalFields {
+        required: String::from("hello"),
+        optional: Some(String::from("world"))
+    }
+);
+
+deser_ok!(
+    deserialize_option_absent,
+    OptionalFields,
+    r#"required "hello""#,
+    OptionalFields {
+        required: String::from("hello"),
+        optional: None
+    }
+);
+
+deser_ok!(
+    deserialize_option_null,
+    OptionalFields,
+    indoc! {r#"
+        required "hello"
+        optional #null
+    "#},
+    OptionalFields {
+        required: String::from("hello"),
+        optional: None
+    }
+);
+
+#[test]
+fn serialize_option() {
+    let with = OptionalFields {
+        required: String::from("hello"),
+        optional: Some(String::from("world")),
+    };
+    let output = serde_kdl2::to_string(&with).unwrap();
+    assert!(output.contains("optional"));
+    let rt: OptionalFields = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(with, rt);
+
+    let without = OptionalFields {
+        required: String::from("hello"),
+        optional: None,
+    };
+    let output = serde_kdl2::to_string(&without).unwrap();
+    assert!(!output.contains("optional"), "None options should not serialize");
+    let rt: OptionalFields = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(without, rt);
+}
\ No newline at end of file
diff --git a/tests/integration_primitives.rs b/tests/integration_primitives.rs
new file mode 100644
index 0000000..b83e081
--- /dev/null
+++ b/tests/integration_primitives.rs
@@ -0,0 +1,145 @@
+//! Integration tests for primitive types (integers, floats, booleans, chars, tuples).
+
+use indoc::indoc;
+use serde::{Deserialize, Serialize};
+
+// Test macros
+macro_rules! roundtrip {
+    ($name:ident, $ty:ty, $val:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = $val;
+            let output = serde_kdl2::to_string(&val).unwrap();
+            let rt: $ty = serde_kdl2::from_str(&output).unwrap();
+            assert_eq!(val, rt);
+        }
+    };
+}
+
+macro_rules! deser_ok {
+    ($name:ident, $ty:ty, $input:expr, $expected:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = serde_kdl2::from_str($input).unwrap();
+            assert_eq!(val, $expected);
+        }
+    };
+}
+
+macro_rules! deser_field {
+    ($name:ident, $field:ident: $ty:ty, $input:expr, $expected:expr) => {
+        #[test]
+        fn $name() {
+            #[derive(Debug, Deserialize, PartialEq)]
+            struct W {
+                $field: $ty,
+            }
+            let val: W = serde_kdl2::from_str($input).unwrap();
+            assert_eq!(val.$field, $expected);
+        }
+    };
+}
+
+// ── Various integer types ──────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct IntTypes {
+    a: u8,
+    b: u16,
+    c: u32,
+    d: u64,
+    e: i8,
+    f: i16,
+    g: i32,
+    h: i64,
+}
+
+roundtrip!(
+    roundtrip_int_types,
+    IntTypes,
+    IntTypes {
+        a: 1,
+        b: 2,
+        c: 3,
+        d: 4,
+        e: -1,
+        f: -2,
+        g: -3,
+        h: -4
+    }
+);
+
+// ── Tuple ──────────────────────────────────────────────────────────────
+
+deser_field!(deserialize_tuple, point: (f64, f64, f64), "point 1.0 2.0 3.0", (1.0, 2.0, 3.0));
+
+#[test]
+fn roundtrip_tuple() {
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct W {
+        point: (f64, f64, f64),
+    }
+    let val = W {
+        point: (1.0, 2.0, 3.0),
+    };
+    let output = serde_kdl2::to_string(&val).unwrap();
+    let rt: W = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(val, rt);
+}
+
+// ── Booleans ───────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Booleans {
+    yes: bool,
+    no: bool,
+}
+
+deser_ok!(
+    booleans_deser,
+    Booleans,
+    indoc! {"
+        yes #true
+        no #false
+    "},
+    Booleans {
+        yes: true,
+        no: false
+    }
+);
+
+roundtrip!(
+    booleans_roundtrip,
+    Booleans,
+    Booleans {
+        yes: true,
+        no: false
+    }
+);
+
+// ── Characters ──────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WCh {
+    letter: char,
+}
+
+roundtrip!(roundtrip_char, WCh, WCh { letter: 'X' });
+roundtrip!(roundtrip_char_multibyte, WCh, WCh { letter: 'é' });
+roundtrip!(roundtrip_char_cjk, WCh, WCh { letter: '中' });
+roundtrip!(roundtrip_char_emoji, WCh, WCh { letter: '🦀' });
+
+// ── Floats ──────────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WF32 {
+    value: f32,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WF64 {
+    value: f64,
+}
+
+roundtrip!(roundtrip_f32, WF32, WF32 { value: 3.125 });
+roundtrip!(roundtrip_f64, WF64, WF64 { value: 3.125 });
\ No newline at end of file
diff --git a/tests/integration_structs.rs b/tests/integration_structs.rs
new file mode 100644
index 0000000..39d6db9
--- /dev/null
+++ b/tests/integration_structs.rs
@@ -0,0 +1,142 @@
+//! Integration tests for struct serialization and deserialization.
+
+use indoc::indoc;
+use serde::{Deserialize, Serialize};
+
+// Test macros
+macro_rules! deser_ok {
+    ($name:ident, $ty:ty, $input:expr, $expected:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = serde_kdl2::from_str($input).unwrap();
+            assert_eq!(val, $expected);
+        }
+    };
+}
+
+macro_rules! roundtrip {
+    ($name:ident, $ty:ty, $val:expr) => {
+        #[test]
+        fn $name() {
+            let val: $ty = $val;
+            let output = serde_kdl2::to_string(&val).unwrap();
+            let rt: $ty = serde_kdl2::from_str(&output).unwrap();
+            assert_eq!(val, rt);
+        }
+    };
+}
+
+// Shared types
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct SimpleConfig {
+    title: String,
+    count: i32,
+    enabled: bool,
+    ratio: f64,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Server {
+    host: String,
+    port: u16,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct AppConfig {
+    name: String,
+    server: Server,
+}
+
+// ════════════════════════════════════════════════════════════════════════
+// Basic struct tests
+// ════════════════════════════════════════════════════════════════════════
+
+deser_ok!(
+    deserialize_simple_struct,
+    SimpleConfig,
+    indoc! {r#"
+        title "My App"
+        count 42
+        enabled #true
+        ratio 3.125
+    "#},
+    SimpleConfig {
+        title: "My App".into(),
+        count: 42,
+        enabled: true,
+        ratio: 3.125
+    }
+);
+
+roundtrip!(
+    serialize_simple_struct,
+    SimpleConfig,
+    SimpleConfig {
+        title: "My App".into(),
+        count: 42,
+        enabled: true,
+        ratio: 3.125,
+    }
+);
+
+deser_ok!(
+    deserialize_nested_struct,
+    AppConfig,
+    indoc! {r#"
+        name "webapp"
+        server {
+            host "localhost"
+            port 8080
+        }
+    "#},
+    AppConfig {
+        name: "webapp".into(),
+        server: Server {
+            host: "localhost".into(),
+            port: 8080,
+        },
+    }
+);
+
+roundtrip!(
+    serialize_nested_struct,
+    AppConfig,
+    AppConfig {
+        name: "webapp".into(),
+        server: Server {
+            host: "localhost".into(),
+            port: 8080,
+        },
+    }
+);
+
+// ── Deeply nested ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level3 {
+    value: String,
+}
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level2 {
+    inner: Level3,
+}
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level1 {
+    middle: Level2,
+}
+
+#[test]
+fn deeply_nested() {
+    let input = indoc! {r#"
+        middle {
+            inner {
+                value "deep"
+            }
+        }
+    "#};
+    let val: Level1 = serde_kdl2::from_str(input).unwrap();
+    assert_eq!(val.middle.inner.value, "deep");
+    let output = serde_kdl2::to_string(&val).unwrap();
+    let rt: Level1 = serde_kdl2::from_str(&output).unwrap();
+    assert_eq!(val, rt);
+}
\ No newline at end of file