Skip to content

TESTBOX-448 MockBox $args() relies on unordered struct order#194

Open
zspitzer wants to merge 4 commits intoOrtus-Solutions:developmentfrom
zspitzer:TESTBOX-448-struct-arg-matching
Open

TESTBOX-448 MockBox $args() relies on unordered struct order#194
zspitzer wants to merge 4 commits intoOrtus-Solutions:developmentfrom
zspitzer:TESTBOX-448-struct-arg-matching

Conversation

@zspitzer
Copy link
Copy Markdown
Contributor

@zspitzer zspitzer commented Apr 17, 2026

Summary

  • TESTBOX-448$args() arg matching fails for nested struct arguments because normalizeArguments() falls through to struct.toString(), whose output depends on HashMap iteration order. Two structurally-equal structs built in different insertion order hash differently, so the mock silently returns null.
  • Fix: introduces a private normalizeValue() helper that walks struct/array values recursively, sorting struct keys via java.util.TreeMap so iteration order can't affect the hash. Nested CFCs are serialized via metadata to match the top-level-arg behaviour and avoid Adobe's serializeJSON cycle on component metadata.
  • Surfaced by Lucee 7.1 (LDEV-5098), which replaced ConcurrentHashMapNullSupport with a thinner wrapper over java.util.concurrent.ConcurrentHashMap. Different bucket layout = different iteration order = latent fragility becomes consistently visible. Reproduces deterministically on Lucee 7.0.3.43 too (using structNew("ordered") to force insertion order).

Details

normalizeArguments() at system/MockBox.cfc sorts top-level arg keys via TreeMap, but for non-simple values it fell through to argOrderedTree[ arg ].toString(). That's Java's HashMap.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:

  • Simple values → toString() (unchanged fast path, preserves the integer ++/-- workaround).
  • CFCs → metadata via serializeJSON( getMetadata( value ) ) (mirrors the top-level-arg branch; must run before the struct branch because on Adobe CFCs are both isStruct and isObject).
  • Structs → TreeMap-sorted key/value pairs, recursing on each value.
  • Arrays → position-preserving, recursing on each element.
  • Fallback → .toString() with serializeJSON catch.

Test plan

  • New testMockArgsStructOrderIndependence — struct args built in different insertion order match.
  • New testMockArgsStructContainingCFC — structs containing CFC values match and don't trigger Adobe's JSON serializer cycle.
  • New testMockArgsDeepNesting — struct → array → struct canonicalises all the way down.
  • Full CI matrix green: Lucee 5/6/7, BoxLang 1/be/cfml@1, Adobe 2023/2025, format check.
  • Local full suite on Lucee 7.0.3.43 and 7.1.0.93-SNAPSHOT: 361/0/0/22.

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.

1 participant