-
Notifications
You must be signed in to change notification settings - Fork 19
fix: replace cargo-expand with syn-based verification #611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
fix: replace cargo-expand with syn-based verification #611
Conversation
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>
WalkthroughRemoved workspace test tooling ( Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Pre-merge checks and finishing touches✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
⛔ Files ignored due to path filters (1)
Cargo.lockis 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:
- Compile-time verification (lines 132-135): trybuild ensures pass/fail fixtures behave as expected
- Trait implementation verification (line 86):
assert_merge::<TestConfig>()confirms the Merge trait is implemented- 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.
| 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)]" | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
hi @Dodecahedr0x, could you please review this? |
Dodecahedr0x
left a comment
There was a problem hiding this 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
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>
There was a problem hiding this 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
⛔ Files ignored due to path filters (1)
Cargo.lockis 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 intest_merge_macro_codegen_verificationwhich validates correctness.
| /// 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)* | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- 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,
}
}- In the test file, import and use it:
use magicblock_config_macro::type_has_merge_method; // or from utils module- 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>
There was a problem hiding this 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
📒 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
Mergeimplementations. Combined with the runtime behavior tests, this provides comprehensive coverage.
87-168: The helper functions enable effective codegen verification.The
merge_impl,pretty_print, andassert_merge_impl_fnhelpers 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_methodduplication is already covered by past review comments.
|
hi @Dodecahedr0x, could you please check if my implementation is correct? also, should i add more test cases as codehabbit suggested? |
|
Yes, good job! Please handle all code rabbit's comments and we're good to merge |
Signed-off-by: AvhiMaz <avhimazumder5@outlook.com>
There was a problem hiding this 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
📒 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, andsynimports 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
Mergeimplementation 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_implpanics with expected messages for invalid inputs (enum, tuple struct, unit struct). However, they don't verify the actualMergeablemacro produces compilation errors with helpful messages.Limitation:
- The real macro's error messages are controlled by
panic!calls in the macro implementation (likely insrc/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
trybuildCurrent 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.
| 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(" ") | ||
| } |
There was a problem hiding this comment.
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.
|
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. |
Signed-off-by: AvhiMaz <avhimazumder5@outlook.com>
There was a problem hiding this 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
📒 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
Mergeablemacro generates valid code. Combined with the runtime tests, this provides robust verification of macro correctness.
| /// 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); | ||
| } |
There was a problem hiding this comment.
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:
- ✅ Runtime tests (lines 22-73): Apply actual
#[derive(Mergeable)]to real structs and verify merge behavior works correctly - ✅ Compile-time trait check (lines 75-85): Verify macro generates code that satisfies
Mergetrait bounds - ❌ Codegen tests (lines 87-340): Verify
merge_implreference 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.
| /// 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)* | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Maintenance burden: Every macro change requires parallel updates here
- Drift risk: If implementations diverge, tests may pass while the actual macro is broken
- False confidence: These tests verify
merge_implmatches expectations, not that the actualMergeablemacro 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.
| /// 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(" ") | ||
| } |
There was a problem hiding this comment.
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.
| /// 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" | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| /// 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| /// 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
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. |
|
❌ Failed to trigger deploy for branch ``. Make sure you have permission or use the manual deploy link in the PR comment. |
|
hi @Dodecahedr0x, can you please trigger the deployment? I don’t have permission to do it. all test cases have passed. |
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
Chores