Skip to content

Conversation

@AvhiMaz
Copy link

@AvhiMaz AvhiMaz commented Nov 3, 2025

Remove slow cargo-expand dependency and replace with faster syn-based
approach for testing Mergeable derive macro. Uses trait bounds to verify
actual Merge implementation instead of macro expansion output.

Fixes #504

Summary by CodeRabbit

  • Tests

    • Replaced fixture-based macro tests with a deterministic, self-contained code-generation test suite and removed several obsolete positive and negative fixtures.
  • Chores

    • Removed unused workspace/dev dev-dependencies (including cargo-expand, macrotest, and a workspace trybuild dev-dep) and removed the CI step that installed the removed tooling.

  Remove slow cargo-expand dependency and replace with faster syn-based
  approach for testing Mergeable derive macro. Uses trait bounds to verify
  actual Merge implementation instead of macro expansion output.

  Fixes magicblock-labs#504

Signed-off-by: AvhiMaz <avhimazumder5@outlook.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

Walkthrough

Removed workspace test tooling (cargo-expand, macrotest, trybuild) and CI install; replaced expand/fixture-based macro tests with in-repo codegen tests using proc_macro2/quote/syn. Deleted many fixture files and adjusted macro crate dev-dependencies and CI step accordingly.

Changes

Cohort / File(s) Summary
Workspace dependency removal
Cargo.toml
Removed cargo-expand = "1" and macrotest = "1" from [workspace.dependencies].
Macro crate dev-deps update
magicblock-config-macro/Cargo.toml
Removed dev-dependencies trybuild = { workspace = true } and macrotest = { workspace = true }.
Test implementation refactor
magicblock-config-macro/tests/test_merger.rs
Replaced trybuild/macrotest/expand-based tests with direct codegen tests using proc_macro2, quote, and syn; added helpers (merge_impl, pretty_print, assert_merge_impl_fn) and multiple positive/negative test cases validating generated Merge impl TokenStreams.
Removed fixture-based tests
magicblock-config-macro/tests/fixtures/*
fail_merge_enum.rs, fail_merge_enum.stderr, fail_merge_union.rs, fail_merge_union.stderr, fail_merge_unnamed.rs, fail_merge_unnamed.stderr, pass_merge.rs, pass_merge.expanded.rs
Deleted fixture source files and their expected stderr/expanded outputs formerly used for expand/trybuild-based tests.
CI workflow change
.github/workflows/ci-test-unit.yml
Removed step that installed cargo-expand (cargo install cargo-expand).

Sequence Diagram(s)

sequenceDiagram
    participant Test as Unit tests (tests/test_merger.rs)
    participant Gen as Codegen helper (`merge_impl`)
    participant Syn as `syn` / `proc_macro2` / `quote`
    participant Assert as Assertions

    Note over Test,Gen: In-repo generation replaces external cargo-expand fixtures
    Test->>Gen: Provide input struct TokenStream
    Gen->>Syn: Inspect fields & build impl TokenStream
    Syn-->>Gen: Generated impl TokenStream
    Gen-->>Test: Return generated impl
    Test->>Syn: Pretty-print / normalize generated and expected TokenStreams
    Syn-->>Test: Normalized tokens
    Test->>Assert: Compare normalized tokens and assert trait bounds / error cases
    Assert-->>Test: Pass / Fail
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review focus:
    • correctness and edge-case handling in merge_impl (detection of nested Config fields, default checks, and panic messages),
    • robustness of pretty_print normalization to avoid brittle token ordering/formatting comparisons,
    • coverage of removed negative fixtures now asserted via tests (ensure all previous error scenarios remain tested),
    • CI workflow change removing cargo-expand installation.

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: replacing cargo-expand testing with syn-based verification for the Mergeable derive macro.
Linked Issues check ✅ Passed The PR successfully addresses issue #504 by removing cargo-expand dependencies and replacing them with syn-based verification while maintaining test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the objective to remove cargo-expand and replace it with syn-based verification; no unrelated modifications are present.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1faae18 and 399edb2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml (0 hunks)
  • magicblock-config-macro/Cargo.toml (1 hunks)
  • magicblock-config-macro/tests/test_merger.rs (2 hunks)
💤 Files with no reviewable changes (1)
  • Cargo.toml
🔇 Additional comments (2)
magicblock-config-macro/tests/test_merger.rs (2)

3-4: LGTM!

The new imports are necessary for the syn-based verification approach and are used appropriately in the new test.


89-136: Verification acceptable—fixture files exist and test coverage is adequate.

All four referenced fixture files exist:

  • pass_merge.rs
  • fail_merge_enum.rs
  • fail_merge_union.rs
  • fail_merge_unnamed.rs

The test provides sufficient coverage across three verification layers:

  1. Compile-time verification (lines 132-135): trybuild ensures pass/fail fixtures behave as expected
  2. Trait implementation verification (line 86): assert_merge::<TestConfig>() confirms the Merge trait is implemented
  3. Behavioral verification (lines 22-72): Two behavioral tests execute actual merge operations with concrete assertions, catching any issues in the generated implementation logic

While the syn-based approach (lines 89-136) doesn't expose full macro expansion details like cargo-expand would, the behavioral tests verify the generated code's correctness at runtime. If the implementation structure was incorrect, the behavioral tests would fail. This trade-off between test speed and verification depth is acceptable for the stated performance goal.

Comment on lines 100 to 114
let has_mergeable_derive = file.items.iter().any(|item| {
if let Item::Struct(item_struct) = item {
item_struct
.attrs
.iter()
.any(|attr| attr.path().is_ident("derive"))
} else {
false
}
});

assert!(
has_mergeable_derive,
"Expected struct with #[derive(Mergeable)]"
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Strengthen the derive attribute verification.

The current check only verifies that some struct has any derive attribute, not specifically #[derive(Mergeable)]. This check would pass for #[derive(Debug)] or any other derive macro, making it a very weak verification.

Consider parsing the derive attribute's tokens to verify Mergeable is actually present:

     // Verify the file contains the Mergeable derive
     let has_mergeable_derive = file.items.iter().any(|item| {
         if let Item::Struct(item_struct) = item {
             item_struct
                 .attrs
                 .iter()
-                .any(|attr| attr.path().is_ident("derive"))
+                .any(|attr| {
+                    attr.path().is_ident("derive") && {
+                        attr.parse_args::<syn::Path>()
+                            .map(|path| path.is_ident("Mergeable"))
+                            .unwrap_or(false)
+                    }
+                })
         } else {
             false
         }
     });

Note: This simplified example assumes a single derive argument. For multiple derives like #[derive(Debug, Mergeable)], you'll need to parse syn::punctuated::Punctuated<syn::Path, syn::Token![,]> instead.

Committable suggestion skipped: line range outside the PR's diff.

@AvhiMaz
Copy link
Author

AvhiMaz commented Nov 4, 2025

hi @Dodecahedr0x, could you please review this?

@Dodecahedr0x Dodecahedr0x self-requested a review November 4, 2025 09:22
Copy link
Contributor

@Dodecahedr0x Dodecahedr0x left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the PR!

We're expecting something along the line of what is done here: remove rust files containing the code and embed them directly in the test, parse them with syn and check that it matches what we expect.

Also, the github workflow for unit test installs cargo-expand, which should be removed.

Finally, please address code rabbit's comments

AvhiMaz and others added 2 commits November 4, 2025 16:09
  Remove cargo-expand dependency and replace trybuild fixture-based testing with
  embedded code verification using syn and token stream comparison

Signed-off-by: AvhiMaz <avhimazumder5@outlook.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 399edb2 and eaa388c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .github/workflows/ci-test-unit.yml (0 hunks)
  • magicblock-config-macro/Cargo.toml (0 hunks)
  • magicblock-config-macro/tests/fixtures/fail_merge_enum.rs (0 hunks)
  • magicblock-config-macro/tests/fixtures/fail_merge_enum.stderr (0 hunks)
  • magicblock-config-macro/tests/fixtures/fail_merge_union.rs (0 hunks)
  • magicblock-config-macro/tests/fixtures/fail_merge_union.stderr (0 hunks)
  • magicblock-config-macro/tests/fixtures/fail_merge_unnamed.rs (0 hunks)
  • magicblock-config-macro/tests/fixtures/fail_merge_unnamed.stderr (0 hunks)
  • magicblock-config-macro/tests/fixtures/pass_merge.expanded.rs (0 hunks)
  • magicblock-config-macro/tests/fixtures/pass_merge.rs (0 hunks)
  • magicblock-config-macro/tests/test_merger.rs (2 hunks)
💤 Files with no reviewable changes (10)
  • magicblock-config-macro/tests/fixtures/fail_merge_unnamed.rs
  • magicblock-config-macro/Cargo.toml
  • magicblock-config-macro/tests/fixtures/pass_merge.rs
  • magicblock-config-macro/tests/fixtures/pass_merge.expanded.rs
  • magicblock-config-macro/tests/fixtures/fail_merge_union.rs
  • .github/workflows/ci-test-unit.yml
  • magicblock-config-macro/tests/fixtures/fail_merge_enum.rs
  • magicblock-config-macro/tests/fixtures/fail_merge_unnamed.stderr
  • magicblock-config-macro/tests/fixtures/fail_merge_enum.stderr
  • magicblock-config-macro/tests/fixtures/fail_merge_union.stderr
🧰 Additional context used
🧬 Code graph analysis (1)
magicblock-config-macro/tests/test_merger.rs (2)
magicblock-config-macro/src/merger.rs (1)
  • type_has_merge_method (8-23)
magicblock-config-helpers/src/lib.rs (1)
  • merge (2-2)
🔇 Additional comments (2)
magicblock-config-macro/tests/test_merger.rs (2)

3-5: LGTM: Imports support the syn-based approach.

The new imports (proc_macro2, quote, syn) are necessary for implementing the syn-based macro verification that replaces cargo-expand. All imports are utilized in the test helper functions below.


75-85: LGTM: Compile-time trait verification is effective.

The trait bound check approach (assert_merge<T: Merge>()) elegantly verifies at compile time that the Mergeable macro generates valid Merge implementations. This is complemented by the AST-based verification in test_merge_macro_codegen_verification which validates correctness.

Comment on lines 87 to 143
/// Generates merge impl (replicates the macro logic for testing - matches Shank pattern)
fn merge_impl(code: TokenStream2) -> TokenStream2 {
fn type_has_merge_method(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => {
let path = &type_path.path;
let segments: Vec<String> = path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
segments.iter().any(|seg| seg.contains("Config"))
}
_ => false,
}
}

let input: DeriveInput = parse2(code).expect("Failed to parse input");
let struct_name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => &fields_named.named,
_ => &syn::punctuated::Punctuated::new(),
},
_ => &syn::punctuated::Punctuated::new(),
};

let merge_fields = fields.iter().map(|f| {
let name = &f.ident;
let field_type = &f.ty;

if type_has_merge_method(field_type) {
quote! {
self.#name.merge(other.#name);
}
} else {
quote! {
if self.#name == default.#name {
self.#name = other.#name;
}
}
}
});

quote! {
impl #impl_generics ::magicblock_config_helpers::Merge for #struct_name #ty_generics #where_clause {
fn merge(&mut self, other: #struct_name #ty_generics) {
let default = Self::default();

#(#merge_fields)*
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Extract duplicated type_has_merge_method logic.

The type_has_merge_method function (lines 89-102) duplicates logic from magicblock-config-macro/src/merger.rs (lines 7-22). This violates DRY and creates a maintenance risk: if the macro's logic evolves, the test could silently become stale and fail to catch regressions.

Consider extracting this function to a shared utility module or making it public in the macro crate so tests can import it directly.

Apply this approach:

  1. In magicblock-config-macro/src/merger.rs, make the helper public or move to a shared utils module:
// In src/merger.rs or new src/utils.rs
pub fn type_has_merge_method(ty: &Type) -> bool {
    match ty {
        Type::Path(type_path) => {
            let path = &type_path.path;
            let segments: Vec<String> = path
                .segments
                .iter()
                .map(|seg| seg.ident.to_string())
                .collect();
            segments.iter().any(|seg| seg.contains("Config"))
        }
        _ => false,
    }
}
  1. In the test file, import and use it:
use magicblock_config_macro::type_has_merge_method; // or from utils module
  1. Remove the duplicate definition from the test (lines 89-102).
🤖 Prompt for AI Agents
In magicblock-config-macro/tests/test_merger.rs around lines 87 to 143, the
helper function type_has_merge_method (lines 89–102) duplicates logic from
magicblock-config-macro/src/merger.rs; remove the duplicate from the test and
instead export/reuse the original: make the helper public (or move it to a
shared utils module) in src/merger.rs (or src/utils.rs) as pub fn
type_has_merge_method(ty: &Type) -> bool, then import that function in the test
(use magicblock_config_macro::type_has_merge_method or the utils module path)
and update the test to call the imported helper.

Signed-off-by: AvhiMaz <avhimazumder5@outlook.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eaa388c and fd572ee.

📒 Files selected for processing (1)
  • magicblock-config-macro/tests/test_merger.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
magicblock-config-macro/tests/test_merger.rs (3)
magicblock-config-macro/src/merger.rs (1)
  • type_has_merge_method (8-23)
magicblock-config-helpers/src/lib.rs (1)
  • merge (2-2)
magicblock-config/src/rpc.rs (1)
  • merge (29-36)
🔇 Additional comments (4)
magicblock-config-macro/tests/test_merger.rs (4)

1-5: LGTM! Appropriate dependencies for syn-based verification.

The imports correctly bring in the necessary tools for the new token-stream-based macro testing approach, replacing the cargo-expand dependency.


22-73: LGTM! Comprehensive runtime behavior verification.

The tests effectively verify both default-value replacement and non-default value preservation, ensuring the macro generates correct merge behavior.


75-85: LGTM! Effective compile-time trait verification.

The trait bound check ensures the macro generates valid Merge implementations. Combined with the runtime behavior tests, this provides comprehensive coverage.


87-168: The helper functions enable effective codegen verification.

The merge_impl, pretty_print, and assert_merge_impl_fn helpers provide a solid foundation for comparing expected vs. generated implementations without cargo-expand. The approach is sound for testing proc macro output.

Note: The type_has_merge_method duplication is already covered by past review comments.

@AvhiMaz
Copy link
Author

AvhiMaz commented Nov 4, 2025

hi @Dodecahedr0x, could you please check if my implementation is correct? also, should i add more test cases as codehabbit suggested?

@Dodecahedr0x
Copy link
Contributor

Yes, good job! Please handle all code rabbit's comments and we're good to merge

Signed-off-by: AvhiMaz <avhimazumder5@outlook.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd572ee and 49bb91e.

📒 Files selected for processing (1)
  • magicblock-config-macro/tests/test_merger.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
magicblock-config-macro/tests/test_merger.rs (2)
magicblock-config-macro/src/merger.rs (1)
  • type_has_merge_method (8-23)
magicblock-config-helpers/src/lib.rs (1)
  • merge (2-2)
🔇 Additional comments (4)
magicblock-config-macro/tests/test_merger.rs (4)

3-5: LGTM! Imports support the new syn-based testing approach.

The addition of proc_macro2, quote, and syn imports enables direct code generation and comparison without external fixture dependencies.


22-73: LGTM! Runtime tests provide solid coverage of merge behavior.

These tests verify both the default-value-based merging for primitives and the nested merge delegation for Config types, covering the two main code paths in the generated implementation.


75-85: LGTM! Elegant compile-time verification approach.

Using trait bounds to verify the Merge implementation is an excellent replacement for cargo-expand. This catches compilation issues and ensures the trait is properly implemented without the overhead of external tooling.


297-334: Negative tests verify reference implementation panics, not macro compilation errors.

These tests verify that merge_impl panics with expected messages for invalid inputs (enum, tuple struct, unit struct). However, they don't verify the actual Mergeable macro produces compilation errors with helpful messages.

Limitation:

  • The real macro's error messages are controlled by panic! calls in the macro implementation (likely in src/lib.rs)
  • If the macro's error messages change or differ from merge_impl, these tests won't catch it
  • Testing proc_macro error messages directly is challenging without tools like trybuild

Current coverage is acceptable:

  • These tests document expected error behavior
  • If the macro accepts invalid input that should be rejected, the user will discover it at compile time
  • Runtime and compile-time tests provide the primary verification

This limitation is a trade-off of removing trybuild, but acceptable given the test suite provides strong coverage overall.

Comment on lines +156 to +163
fn pretty_print(tokens: proc_macro2::TokenStream) -> String {
let code = tokens.to_string();
// Return normalized version for comparison - just split and rejoin whitespace
code.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider more robust token stream normalization.

The pretty_print function's whitespace normalization is simplistic and might not handle edge cases like string literals containing whitespace or comments. For production use, consider using prettyplease or rustfmt via std::process::Command for more reliable normalization.

However, if you adopt the recommendation to remove codegen tests (see comment on lines 87-152), this function can be deleted entirely.

@AvhiMaz
Copy link
Author

AvhiMaz commented Nov 4, 2025

Hey @Dodecahedr0x, Rust's proc-macro architecture doesn't allow us to directly inspect generated code at runtime, so we can't verify the actual macro output matches our reference implementation. We're using runtime tests to verify the macro works correctly and codegen tests to verify the logic is sound, but we can't guarantee 100% parity. This is a known limitation of proc-macro testing in Rust, the current approach catches regressions and verifies functionality, which is the best we can do with the architectural constraints.

AvhiMaz and others added 2 commits November 4, 2025 21:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49bb91e and b5547e5.

📒 Files selected for processing (1)
  • magicblock-config-macro/tests/test_merger.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
magicblock-config-macro/tests/test_merger.rs (3)
magicblock-config-macro/src/merger.rs (1)
  • type_has_merge_method (8-23)
magicblock-config-helpers/src/lib.rs (1)
  • merge (2-2)
magicblock-config/src/rpc.rs (1)
  • merge (29-36)
🔇 Additional comments (2)
magicblock-config-macro/tests/test_merger.rs (2)

3-5: LGTM: Appropriate imports for syn-based verification.

The added imports support the new test infrastructure correctly.


75-85: LGTM: Strong compile-time verification.

This trait bound check directly verifies the actual Mergeable macro generates valid code. Combined with the runtime tests, this provides robust verification of macro correctness.

Comment on lines +87 to 340
/// Generates merge impl (replicates the macro logic for testing)
/// Panics with descriptive messages when invalid input is encountered
fn merge_impl(code: TokenStream2) -> TokenStream2 {
fn type_has_merge_method(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => {
let path = &type_path.path;
let segments: Vec<String> = path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
segments.iter().any(|seg| seg.contains("Config"))
}
_ => false,
}
}

let input: DeriveInput = parse2(code).expect("Failed to parse input");
let struct_name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

// Validate input - panic for invalid cases
let fields = match &input.data {
Data::Struct(data_struct) => {
match &data_struct.fields {
Fields::Named(fields_named) => &fields_named.named,
Fields::Unnamed(_) => {
panic!("Merge can only be derived for structs with named fields");
}
Fields::Unit => {
panic!("Merge can only be derived for structs with named fields");
}
}
}
_ => {
panic!("Merge can only be derived for structs");
}
};

let merge_fields = fields.iter().map(|f| {
let name = &f.ident;
let field_type = &f.ty;

if type_has_merge_method(field_type) {
quote! {
self.#name.merge(other.#name);
}
} else {
quote! {
if self.#name == default.#name {
self.#name = other.#name;
}
}
}
});

quote! {
impl #impl_generics ::magicblock_config_helpers::Merge for #struct_name #ty_generics #where_clause {
fn merge(&mut self, other: #struct_name #ty_generics) {
let default = Self::default();

#(#merge_fields)*
}
}
}
}

/// Pretty-prints token stream for deterministic comparison
/// Handles whitespace normalization for consistent comparisons
fn pretty_print(tokens: proc_macro2::TokenStream) -> String {
let code = tokens.to_string();
// Return normalized version for comparison - just split and rejoin whitespace
code.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
}

/// Helper function that compares generated merge impl with expected output
fn assert_merge_impl_fn(code: TokenStream2, expected: TokenStream2) {
let generated = merge_impl(code);

assert_eq!(
pretty_print(generated),
pretty_print(expected),
"Generated merge implementation does not match expected output"
);
}

/// Verifies the macro generates the correct Merge trait implementation by comparing actual vs expected output
#[test]
fn test_merge_macro_codegen_verification() {
// Embedded test input code - struct definition for TestConfig
let input = quote! {
#[derive(Default)]
struct TestConfig {
field1: u32,
field2: String,
field3: Option<String>,
nested: NestedConfig,
}
};

// Define the expected generated Merge implementation
let expected = quote! {
impl ::magicblock_config_helpers::Merge for TestConfig {
fn merge(&mut self, other: TestConfig) {
let default = Self::default();

if self.field1 == default.field1 {
self.field1 = other.field1;
}

if self.field2 == default.field2 {
self.field2 = other.field2;
}

if self.field3 == default.field3 {
self.field3 = other.field3;
}

self.nested.merge(other.nested);
}
}
};

// Compare the implementation
assert_merge_impl_fn(input, expected);
}

/// Verifies codegen for empty structs
#[test]
fn test_merge_macro_codegen_empty_struct() {
let input = quote! {
#[derive(Default)]
struct EmptyConfig {}
};

let expected = quote! {
impl ::magicblock_config_helpers::Merge for EmptyConfig {
fn merge(&mut self, other: EmptyConfig) {
let default = Self::default();
}
}
};

assert_merge_impl_fn(input, expected);
}

/// Verifies codegen for structs with only primitive fields
#[test]
fn test_merge_macro_codegen_only_primitives() {
let input = quote! {
#[derive(Default)]
struct PrimitiveConfig {
count: u32,
enabled: bool,
name: String,
}
};

let expected = quote! {
impl ::magicblock_config_helpers::Merge for PrimitiveConfig {
fn merge(&mut self, other: PrimitiveConfig) {
let default = Self::default();

if self.count == default.count {
self.count = other.count;
}

if self.enabled == default.enabled {
self.enabled = other.enabled;
}

if self.name == default.name {
self.name = other.name;
}
}
}
};

assert_merge_impl_fn(input, expected);
}

/// Verifies codegen for structs with only nested Config fields
#[test]
fn test_merge_macro_codegen_only_config_fields() {
let input = quote! {
#[derive(Default)]
struct CompositeConfig {
inner: InnerConfig,
other: OtherConfig,
}
};

let expected = quote! {
impl ::magicblock_config_helpers::Merge for CompositeConfig {
fn merge(&mut self, other: CompositeConfig) {
let default = Self::default();

self.inner.merge(other.inner);

self.other.merge(other.other);
}
}
};

assert_merge_impl_fn(input, expected);
}

/// Verifies that the macro rejects enum types with a compile error
#[test]
#[should_panic(expected = "Merge can only be derived for structs")]
fn test_merge_macro_rejects_enum() {
let input = quote! {
enum InvalidConfig {
Variant1,
Variant2,
}
};

// merge_impl should panic for enum input
let _ = merge_impl(input);
}

/// Verifies that the macro rejects tuple structs with a compile error
#[test]
#[should_panic(
expected = "Merge can only be derived for structs with named fields"
)]
fn test_merge_macro_rejects_tuple_struct() {
let input = quote! {
struct TupleConfig(u32, String);
};

// merge_impl should panic for tuple struct input
let _ = merge_impl(input);
}

/// Verifies that the macro rejects unit structs with a compile error
#[test]
#[should_panic(
expected = "Merge can only be derived for structs with named fields"
)]
fn test_merge_macro_rejects_unit_struct() {
let input = quote! {
struct UnitConfig;
};

// merge_impl should panic for unit struct input
let _ = merge_impl(input);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider alternative verification approach.

The current codegen test strategy has a fundamental issue: it tests a reference implementation (merge_impl) rather than the actual Mergeable derive macro. While I understand the goal to replace cargo-expand, the current approach doesn't achieve equivalent verification.

Current verification coverage:

  1. Runtime tests (lines 22-73): Apply actual #[derive(Mergeable)] to real structs and verify merge behavior works correctly
  2. Compile-time trait check (lines 75-85): Verify macro generates code that satisfies Merge trait bounds
  3. Codegen tests (lines 87-340): Verify merge_impl reference implementation, not actual macro

Recommendation: Remove lines 87-340 entirely. The runtime tests already verify the actual macro produces correct behavior, and the compile-time checks verify it generates valid implementations. This provides robust verification without the maintenance burden and false confidence of duplicated logic.

If you need to verify specific generated code patterns, consider using cargo-expand in a separate, optional test (perhaps behind a feature flag or in CI only) that actually expands the real macro and inspects the output, rather than testing a reference implementation.

🤖 Prompt for AI Agents
In magicblock-config-macro/tests/test_merger.rs around lines 87 to 340, remove
the entire codegen reference-implementation tests (the merge_impl, pretty_print,
assert_merge_impl_fn helpers and all tests that use them) because they test a
duplicated reference implementation rather than the real derive macro; delete
those functions and tests, leaving the runtime behavior tests and compile-time
trait checks intact, and if you need to assert actual expanded macro output
create a separate optional test (e.g., behind a feature flag or in CI) that uses
cargo-expand to expand the real #[derive(Mergeable)] macro and validate its
output.

Comment on lines +87 to +154
/// Generates merge impl (replicates the macro logic for testing)
/// Panics with descriptive messages when invalid input is encountered
fn merge_impl(code: TokenStream2) -> TokenStream2 {
fn type_has_merge_method(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => {
let path = &type_path.path;
let segments: Vec<String> = path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
segments.iter().any(|seg| seg.contains("Config"))
}
_ => false,
}
}

let input: DeriveInput = parse2(code).expect("Failed to parse input");
let struct_name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

// Validate input - panic for invalid cases
let fields = match &input.data {
Data::Struct(data_struct) => {
match &data_struct.fields {
Fields::Named(fields_named) => &fields_named.named,
Fields::Unnamed(_) => {
panic!("Merge can only be derived for structs with named fields");
}
Fields::Unit => {
panic!("Merge can only be derived for structs with named fields");
}
}
}
_ => {
panic!("Merge can only be derived for structs");
}
};

let merge_fields = fields.iter().map(|f| {
let name = &f.ident;
let field_type = &f.ty;

if type_has_merge_method(field_type) {
quote! {
self.#name.merge(other.#name);
}
} else {
quote! {
if self.#name == default.#name {
self.#name = other.#name;
}
}
}
});

quote! {
impl #impl_generics ::magicblock_config_helpers::Merge for #struct_name #ty_generics #where_clause {
fn merge(&mut self, other: #struct_name #ty_generics) {
let default = Self::default();

#(#merge_fields)*
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Duplicated macro logic creates significant maintenance risk.

The merge_impl function replicates the entire procedural macro implementation, including type_has_merge_method (lines 90-103) which duplicates logic from src/merger.rs. This creates several problems:

  1. Maintenance burden: Every macro change requires parallel updates here
  2. Drift risk: If implementations diverge, tests may pass while the actual macro is broken
  3. False confidence: These tests verify merge_impl matches expectations, not that the actual Mergeable macro is correct

The runtime tests (lines 22-73) already verify actual macro behavior by using real #[derive(Mergeable)] structs. The compile-time check (lines 75-85) verifies the macro generates valid trait implementations. Together, these provide strong verification without duplicating logic.

Recommendation: Remove the codegen tests entirely (lines 87-340) to eliminate maintenance burden and false confidence. The existing runtime + compile-time tests already provide robust verification of the actual macro.

If you must keep codegen tests, at minimum extract type_has_merge_method and the merge strategy logic into a shared test utility that both the macro and tests import, preventing divergence.

Based on learnings

🤖 Prompt for AI Agents
magicblock-config-macro/tests/test_merger.rs around lines 87 to 154: this block
duplicates the procedural macro logic (the merge_impl function and its helper
type_has_merge_method), creating maintenance and drift risk; remove the
duplicated codegen test function and any tests that only exercise this local
replica, or alternatively extract the duplicated helper(s) (e.g.,
type_has_merge_method and merge-strategy logic) into a shared test utility
module that both the procedural macro tests and these tests import, then update
tests to call the real macro or the shared helpers instead so there is no
parallel implementation to maintain.

Comment on lines +156 to +165
/// Pretty-prints token stream for deterministic comparison
/// Handles whitespace normalization for consistent comparisons
fn pretty_print(tokens: proc_macro2::TokenStream) -> String {
let code = tokens.to_string();
// Return normalized version for comparison - just split and rejoin whitespace
code.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Simplistic normalization may miss edge cases.

The whitespace-only normalization won't handle string literals containing whitespace or comments correctly. Consider using prettyplease or invoking rustfmt via std::process::Command for robust normalization.

However, if you adopt the recommendation to remove codegen tests (see comment on lines 87-154), this function can be deleted entirely.

🤖 Prompt for AI Agents
In magicblock-config-macro/tests/test_merger.rs around lines 156 to 165, the
pretty_print function normalizes whitespace by splitting tokens into words which
breaks string literals and comments; replace this simplistic normalization with
a robust formatter or remove it: either (A) use a proper Rust pretty-printer
(e.g. call prettyplease::unparse or run rustfmt via std::process::Command on the
generated TokenStream) and compare the formatted outputs, or (B) if you follow
the other review suggestion to remove codegen-based tests entirely, delete this
pretty_print helper and any tests that depend on stringified token comparison.

Comment on lines +167 to +176
/// Helper function that compares generated merge impl with expected output
fn assert_merge_impl_fn(code: TokenStream2, expected: TokenStream2) {
let generated = merge_impl(code);

assert_eq!(
pretty_print(generated),
pretty_print(expected),
"Generated merge implementation does not match expected output"
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Helper tests reference implementation, not actual macro.

While this function is correctly implemented, it compares merge_impl output against expectations. It doesn't verify what the actual Mergeable derive macro generates, creating a verification gap.

If you remove the codegen tests (recommended in comment on lines 87-154), delete this helper as well.

🤖 Prompt for AI Agents
In magicblock-config-macro/tests/test_merger.rs around lines 167-176, the helper
assert_merge_impl_fn compares the hand-called merge_impl function instead of the
actual Mergeable derive macro output, leaving a verification gap; if you are
removing the codegen tests as suggested, delete this helper entirely and any
callers, otherwise change the helper to expand the real derive macro (e.g.,
invoke the proc-macro expansion/path used by your test harness or use trybuild
to compile a test crate and capture the expanded impl) and compare that
expansion to the expected TokenStream rather than calling merge_impl directly.

Comment on lines +178 to +297
/// Verifies the macro generates the correct Merge trait implementation by comparing actual vs expected output
#[test]
fn test_merge_macro_codegen_verification() {
// Embedded test input code - struct definition for TestConfig
let input = quote! {
#[derive(Default)]
struct TestConfig {
field1: u32,
field2: String,
field3: Option<String>,
nested: NestedConfig,
}
};

// Define the expected generated Merge implementation
let expected = quote! {
impl ::magicblock_config_helpers::Merge for TestConfig {
fn merge(&mut self, other: TestConfig) {
let default = Self::default();

if self.field1 == default.field1 {
self.field1 = other.field1;
}

if self.field2 == default.field2 {
self.field2 = other.field2;
}

if self.field3 == default.field3 {
self.field3 = other.field3;
}

self.nested.merge(other.nested);
}
}
};

// Compare the implementation
assert_merge_impl_fn(input, expected);
}

/// Verifies codegen for empty structs
#[test]
fn test_merge_macro_codegen_empty_struct() {
let input = quote! {
#[derive(Default)]
struct EmptyConfig {}
};

let expected = quote! {
impl ::magicblock_config_helpers::Merge for EmptyConfig {
fn merge(&mut self, other: EmptyConfig) {
let default = Self::default();
}
}
};

assert_merge_impl_fn(input, expected);
}

/// Verifies codegen for structs with only primitive fields
#[test]
fn test_merge_macro_codegen_only_primitives() {
let input = quote! {
#[derive(Default)]
struct PrimitiveConfig {
count: u32,
enabled: bool,
name: String,
}
};

let expected = quote! {
impl ::magicblock_config_helpers::Merge for PrimitiveConfig {
fn merge(&mut self, other: PrimitiveConfig) {
let default = Self::default();

if self.count == default.count {
self.count = other.count;
}

if self.enabled == default.enabled {
self.enabled = other.enabled;
}

if self.name == default.name {
self.name = other.name;
}
}
}
};

assert_merge_impl_fn(input, expected);
}

/// Verifies codegen for structs with only nested Config fields
#[test]
fn test_merge_macro_codegen_only_config_fields() {
let input = quote! {
#[derive(Default)]
struct CompositeConfig {
inner: InnerConfig,
other: OtherConfig,
}
};

let expected = quote! {
impl ::magicblock_config_helpers::Merge for CompositeConfig {
fn merge(&mut self, other: CompositeConfig) {
let default = Self::default();

self.inner.merge(other.inner);

self.other.merge(other.other);
}
}
};

assert_merge_impl_fn(input, expected);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Critical verification gap: tests don't validate the actual macro.

These codegen tests comprehensively cover edge cases, but they compare merge_impl (the test's reference implementation) output against expected token streams. They never verify what the actual Mergeable derive macro generates.

Verification gap:

  • ✅ Runtime tests (lines 22-73) verify actual macro behavior on real structs
  • ✅ Compile-time check (lines 75-85) verifies actual macro generates valid trait impl
  • ❌ Codegen tests (lines 178-297) only verify reference implementation matches expectations

Impact: If the real macro has a bug that merge_impl doesn't replicate, these tests will pass while the macro is broken. Conversely, if merge_impl has a bug, tests will fail even if the macro is correct.

Given that runtime and compile-time tests already provide strong verification of the actual macro, these codegen tests add maintenance burden without corresponding value.

Based on learnings

Comment on lines +299 to 340
/// Verifies that the macro rejects enum types with a compile error
#[test]
#[should_panic(expected = "Merge can only be derived for structs")]
fn test_merge_macro_rejects_enum() {
let input = quote! {
enum InvalidConfig {
Variant1,
Variant2,
}
};

// merge_impl should panic for enum input
let _ = merge_impl(input);
}

/// Verifies that the macro rejects tuple structs with a compile error
#[test]
#[should_panic(
expected = "Merge can only be derived for structs with named fields"
)]
fn test_merge_macro_rejects_tuple_struct() {
let input = quote! {
struct TupleConfig(u32, String);
};

// merge_impl should panic for tuple struct input
let _ = merge_impl(input);
}

/// Verifies that the macro rejects unit structs with a compile error
#[test]
#[should_panic(
expected = "Merge can only be derived for structs with named fields"
)]
fn test_merge_macro_rejects_unit_struct() {
let input = quote! {
struct UnitConfig;
};

// merge_impl should panic for unit struct input
let _ = merge_impl(input);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Error tests verify runtime panics, not compile-time errors.

These tests verify that merge_impl panics with the correct messages for invalid input. However, the actual Mergeable derive macro would produce compile-time errors, not runtime panics.

Mismatch: When you write #[derive(Mergeable)] on an enum, the macro fails at compile time. These tests verify runtime panic behavior of merge_impl, which is a different failure mode.

If you keep these tests, they should verify compile failure using trybuild with appropriate .compile_fail() fixtures that attempt to derive Mergeable on invalid types. However, given the goal to remove trybuild and the existing verification coverage, consider removing these tests along with the other codegen tests.

🤖 Prompt for AI Agents
In magicblock-config-macro/tests/test_merger.rs around lines 299 to 340, the
three tests assert runtime panics from merge_impl for enums, tuple structs and
unit structs but the actual derive macro produces compile-time errors; either
convert these into trybuild compile_fail fixtures that attempt
#[derive(Mergeable)] on invalid types, or (preferred given the goal to remove
trybuild) delete these three tests entirely so you no longer assert runtime
panics for compile-time failures.

@AvhiMaz
Copy link
Author

AvhiMaz commented Nov 4, 2025

hi @Dodecahedr0x , there was a lint issue, I’ve pushed a fix. also noticed a deployment issue, but it seems to be an authorization problem since I don’t have permission to trigger it.

@github-actions
Copy link

github-actions bot commented Nov 6, 2025

❌ Failed to trigger deploy for branch ``. Make sure you have permission or use the manual deploy link in the PR comment.

@AvhiMaz
Copy link
Author

AvhiMaz commented Nov 6, 2025

hi @Dodecahedr0x, can you please trigger the deployment? I don’t have permission to do it. all test cases have passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove cargo-expand dependency

2 participants