TESTBOX-448 MockBox $args() relies on unordered struct order#194
Open
zspitzer wants to merge 4 commits into
Open
TESTBOX-448 MockBox $args() relies on unordered struct order#194zspitzer wants to merge 4 commits into
zspitzer wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request addresses nondeterministic $args() matching in MockBox when arguments include nested structs (and other deep structures), where argument hashing could differ purely due to struct key iteration order. It introduces recursive value normalization to produce a stable, deterministic string representation for hashing, and adds tests to prevent regressions across nesting and CFC-containing structures.
Changes:
- Replaced non-simple argument hashing fallback with a deterministic
normalizeValue()that canonicalizes nested structs (sorted keys) and arrays (stable positional traversal). - Preserved existing CFC handling by serializing component metadata, now extended to components nested inside structs/arrays.
- Added new MockBox specs to validate order-independence, CFC-in-struct handling, and deep nesting behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
system/MockBox.cfc |
Adds normalizeValue() and routes complex argument hashing through it to make $args() matching deterministic for nested structs/arrays and CFCs. |
tests/specs/mockbox/MockBoxTest.cfc |
Adds regression tests covering struct insertion-order independence, CFC-in-struct cases, and deep nesting canonicalization. |
Comment on lines
+676
to
+684
| // Struct - sort keys | ||
| if ( isStruct( arguments.value ) && !isObject( arguments.value ) ) { | ||
| var sorted = createObject( "java", "java.util.TreeMap" ).init( arguments.value ); | ||
| var parts = []; | ||
| for ( var key in sorted ) { | ||
| arrayAppend( | ||
| parts, | ||
| key & "=" & ( isNull( sorted[ key ] ) ? "null" : normalizeValue( sorted[ key ] ) ) | ||
| ); |
Comment on lines
+665
to
+678
| if ( | ||
| isObject( arguments.value ) and | ||
| ( | ||
| isInstanceOf( arguments.value, "Component" ) or structKeyExists( | ||
| getMetadata( arguments.value ), | ||
| "extends" | ||
| ) | ||
| ) | ||
| ) { | ||
| return serializeJSON( getMetadata( arguments.value ) ); | ||
| } | ||
| // Struct - sort keys | ||
| if ( isStruct( arguments.value ) && !isObject( arguments.value ) ) { | ||
| var sorted = createObject( "java", "java.util.TreeMap" ).init( arguments.value ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
$args()arg matching fails for nested struct arguments becausenormalizeArguments()falls through tostruct.toString(), whose output depends on HashMap iteration order. Two structurally-equal structs built in different insertion order hash differently, so the mock silently returnsnull.normalizeValue()helper that walks struct/array values recursively, sorting struct keys viajava.util.TreeMapso iteration order can't affect the hash. Nested CFCs are serialized via metadata to match the top-level-arg behaviour and avoid Adobe'sserializeJSONcycle on component metadata.ConcurrentHashMapNullSupportwith a thinner wrapper overjava.util.concurrent.ConcurrentHashMap. Different bucket layout = different iteration order = latent fragility becomes consistently visible. Reproduces deterministically on Lucee 7.0.3.43 too (usingstructNew("ordered")to force insertion order).Details
normalizeArguments()atsystem/MockBox.cfcsorts top-level arg keys viaTreeMap, but for non-simple values it fell through toargOrderedTree[ arg ].toString(). That's Java'sHashMap.toString()for a struct — output is not stable across iteration orders. Regular structs have never guaranteed iteration order in CFML, so this was always latent; it rarely hit in practice because tests usually build setup and call-site structs the same way. LDEV-5098 just made the fragility consistently visible.The new
normalizeValue()handles:toString()(unchanged fast path, preserves the integer++/--workaround).serializeJSON( getMetadata( value ) )(mirrors the top-level-arg branch; must run before the struct branch because on Adobe CFCs are bothisStructandisObject)..toString()withserializeJSONcatch.Test plan
testMockArgsStructOrderIndependence— struct args built in different insertion order match.testMockArgsStructContainingCFC— structs containing CFC values match and don't trigger Adobe's JSON serializer cycle.testMockArgsDeepNesting— struct → array → struct canonicalises all the way down.