During the translation of JSON to MockElement using org.apache.sling dependency (a detailed explanation is provided with screenshots in the last section), the ordering of the properties is lost because of HashMap.entrySet(). According to the Javadoc, HashMap is not guaranteed to maintain order over time.
This might cause issues where the order is strictly enforced.
Specifically, the following tests expects the order of DAMContentFragment.getExportedElementsOrder() to be main followed by second
1) com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structured
2) com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structuredNonExistingVariation
3) com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structuredVariation
4) com.adobe.cq.wcm.core.components.internal.servlets.contentfragment.ElementsDataSourceServletTest.testComponentPathStructured
5) com.adobe.cq.wcm.core.components.internal.servlets.contentfragment.ElementsDataSourceServletTest.testFragmentPathOverride
6) com.adobe.cq.wcm.core.components.internal.servlets.contentfragment.ElementsDataSourceServletTest.testFragmentPathStructured
Steps to Reproduce
When the above tests are run through NonDex, which shuffles the order of elements returned by HashMap.keySet(), we could observe the mentioned issue.
# Clone the Repo
https://github.com/adobe/aem-core-wcm-components.git
**Expected behavior/code**
- For any amount of runs, the order of `DAMContentFragment.getExportedElementsOrder()` should have been
1) MAIN
2) SECOND
- The test should have passed successfully without any errors with `NonDex`.
**Environment**
- Core Components version: `2.23.5-SNAPSHOT`
- Java version: `Java(TM) SE Runtime Environment (build 1.8.0_381-b09)`
**Possible Solution**
<!--- If you have suggestions for the bug fix. Please also consider creating a Pull Request 🤘. -->
- Since the change in ordering of elements loaded by `resource.getChild()` when mocking the `ContentFragment` causes the issue, sorting the elements loaded by the resource will solve the issue. Sorting the elements based on the `resource.getValueMap().keySet()` ensures that the elements be returned in the order `MAIN` followed by `SECOND`.
- Below is the diff needed to solve the above mentioned issue.
for (String name : master.getValueMap().keySet().stream().sorted().collect(Collectors.toList()))
for (String name : master.getValueMap().keySet())
This change is made only in the ContentFragmentMockAdapter.java because the main issue results from incorrectly loading the test data.
If the proposed solution looks good, I can create a PR.
Additional context / Screenshots
Here's an example scenario where the change in ordering occurs when run through NonDex for the test com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structured. Since the issue occurs when loading the json resource for all the tests, the below scenario applies to all the tests.
1) The resource loaded will be specific for each test. However, one of the common files is bundles/core/src/test/resources/contentfragment/test-content-dam-contentfragments.json. This is the JSON from which the mocks are loaded and asserted.
2) The contents of the JSON are parsed recursively at apache sling's JsonContentParser.java:87. The contents are loaded from the resource into JsonObject. This object uses LinkedHashMap to store the tree structure. So, the issue is not in the JsonObject. Looking in the below image, we could observe that object.keySet() gives the elements in the order of the above image. However, each json object is parsed to the resource contents by extracting the values to properties map, which in turn is a HashMap. The order of values in this HashMap might differ based on its implementation in a specific environment or java version.
3) While converting the values of the json to their respective resources, the properties map populated in the above imaged is passed to the LoaderContentHandler.java:82. Since this is a HashMap, content.entrySet() might return not return the same order for all the runs. This non-deterministic order map is provider to the JcrResourceProvider, which converts the json values to JcrValueMap used by the ContentFragmentMockAdapter
4) Accessing this map's key set at ContentFragmentMockAdapter gives the following order
5) The mock elements created based on the above order of map is MAIN followed by SECOND
This is the second run for the same method run through NonDex.
1) The order of elements from the JsonObject remains constant because of LinkedHashMap
2) However, when the properties hash map is inserted, the change in order could be observed.
3) Creating the JcrValueMap based on this ordering
4) Accessing this map's key set at ContentFragmentMockAdapter gives a different order
5) The mock elements created based on the above order of map is SECOND followed by MAIN
Bug Report
Current Behavior
ContentFragmentImplTest
mocks JSON resources toContentFragment
objects throughContentFragmentMockAdapter
at https://github.com/adobe/aem-core-wcm-components/blob/3524e33424bf6831faca014284c5bad803d3744d/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/models/v1/contentfragment/AbstractContentFragmentTest.java#L124-L125 https://github.com/adobe/aem-core-wcm-components/blob/3524e33424bf6831faca014284c5bad803d3744d/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/models/v1/contentfragment/AbstractContentFragmentTest.java#L170-L171DAMContentElement
, which is part ofContentFragment
, by loading the json resource at https://github.com/adobe/aem-core-wcm-components/blob/3524e33424bf6831faca014284c5bad803d3744d/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/models/v1/contentfragment/ContentFragmentMockAdapter.java#L109-L116JSON
toMockElement
usingorg.apache.sling
dependency (a detailed explanation is provided with screenshots in the last section), the ordering of the properties is lost because ofHashMap.entrySet()
. According to the Javadoc,HashMap
is not guaranteed to maintain order over time.DAMContentFragment.getExportedElementsOrder()
to bemain
followed bysecond
1)com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structured
2)com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structuredNonExistingVariation
3)com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structuredVariation
4)com.adobe.cq.wcm.core.components.internal.servlets.contentfragment.ElementsDataSourceServletTest.testComponentPathStructured
5)com.adobe.cq.wcm.core.components.internal.servlets.contentfragment.ElementsDataSourceServletTest.testFragmentPathOverride
6)com.adobe.cq.wcm.core.components.internal.servlets.contentfragment.ElementsDataSourceServletTest.testFragmentPathStructured
Steps to Reproduce
HashMap.keySet()
, we could observe the mentioned issue.Compile the module byte-buddy-dep
mvn clean install -pl bundles/core -am -DskipTests
(Optional) Run the unit test
mvn -pl bundles/core test -Dtest=
Run the unit test using NonDex
mvn -pl bundles/core edu.illinois:nondex-maven-plugin:2.1.1:nondex -Dtest=
[INFO] Results: [INFO] [ERROR] Failures: [ERROR] ContentFragmentImplTest.structured:137->assertContentFragment:283->assertContentFragment:311 Element has wrong name ==> expected: but was:
[INFO]
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO]
[INFO] Results: [INFO] [ERROR] Failures: [ERROR] ElementsDataSourceServletTest.testComponentPathStructured:134->AbstractContentFragmentDataSourceServletTest.assertDataSource:127 expected: but was:
[INFO]
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO]
-/+ com.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentMockAdapter.java | LineNo: 111
ContentFragmentMockAdapter.java
because the main issue results from incorrectly loading the test data.Additional context / Screenshots
Here's an example scenario where the change in ordering occurs when run through
NonDex
for the testcom.adobe.cq.wcm.core.components.internal.models.v1.contentfragment.ContentFragmentImplTest.structured
. Since the issue occurs when loading the json resource for all the tests, the below scenario applies to all the tests.1) The resource loaded will be specific for each test. However, one of the common files is
bundles/core/src/test/resources/contentfragment/test-content-dam-contentfragments.json
. This is the JSON from which the mocks are loaded and asserted.2) The contents of the JSON are parsed recursively at apache sling's
JsonContentParser.java:87
. The contents are loaded from the resource intoJsonObject
. This object usesLinkedHashMap
to store the tree structure. So, the issue is not in the JsonObject. Looking in the below image, we could observe thatobject.keySet()
gives the elements in the order of the above image. However, each json object is parsed to the resource contents by extracting the values toproperties
map, which in turn is aHashMap
. The order of values in thisHashMap
might differ based on its implementation in a specific environment or java version.3) While converting the values of the json to their respective resources, the
properties
map populated in the above imaged is passed to theLoaderContentHandler.java:82
. Since this is aHashMap
,content.entrySet()
might return not return the same order for all the runs. This non-deterministic order map is provider to theJcrResourceProvider
, which converts the json values toJcrValueMap
used by theContentFragmentMockAdapter
4) Accessing this map's key set at
ContentFragmentMockAdapter
gives the following order5) The mock elements created based on the above order of map is
MAIN
followed bySECOND
This is the second run for the same method run through
NonDex
.1) The order of elements from the
JsonObject
remains constant because ofLinkedHashMap
2) However, when the
properties
hash map is inserted, the change in order could be observed.3) Creating the
JcrValueMap
based on this ordering4) Accessing this map's key set at
ContentFragmentMockAdapter
gives a different order5) The mock elements created based on the above order of map is
SECOND
followed byMAIN