Fix test failure by ordering leaf-lists 91/137191/8
authordanielhanrahan <daniel.hanrahan@est.tech>
Thu, 8 Feb 2024 15:04:05 +0000 (15:04 +0000)
committerDaniel Hanrahan <daniel.hanrahan@est.tech>
Mon, 12 Feb 2024 14:56:01 +0000 (14:56 +0000)
YANG specifies two ways that leaf-lists can be ordered:
- ordered-by user: original order in JSON is preserved
- ordered-by system (default): it is up to the system how to order

For leaf-lists to preserve same order as the JSON, the Yang module
must specify 'ordered-by user'. To ensure consistent behaviour even
when system ordering is used, the leaf-list is sorted during parsing.

- Add 'ordered-by user' to authors field in bookstore.yang
- Sort leaf-list during parsing when using 'ordered-by system'
- Add new tests to verify ordering

Issue-ID: CPS-2057
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
Change-Id: I6ab688ec2fa4a22182e853d1a8b26642f278c40a

cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
csit/tests/cps-data/cps-data.robot
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsModuleServiceIntegrationSpec.groovy
integration-test/src/test/resources/data/bookstore/bookstore.yang

index b040af5..9859acd 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Bell Canada. All rights reserved.
  *  Modifications Copyright (C) 2021 Pantheon.tech
- *  Modifications Copyright (C) 2022-2023 Nordix Foundation.
+ *  Modifications Copyright (C) 2022-2024 Nordix Foundation.
  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,6 +34,7 @@ import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.utils.YangUtils;
+import org.opendaylight.yangtools.yang.common.Ordering;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
@@ -242,10 +243,14 @@ public class DataNodeBuilder {
 
     private static void addYangLeafList(final DataNode currentDataNode, final LeafSetNode<?> leafSetNode) {
         final String leafListName = leafSetNode.getIdentifier().getNodeType().getLocalName();
-        final List<?> leafListValues = ((Collection<? extends NormalizedNode>) leafSetNode.body())
+        List<?> leafListValues = ((Collection<? extends NormalizedNode>) leafSetNode.body())
                 .stream()
-                .map(normalizedNode -> (normalizedNode).body())
-                .collect(Collectors.toUnmodifiableList());
+                .map(NormalizedNode::body)
+                .collect(Collectors.toList());
+        if (leafSetNode.ordering() == Ordering.SYSTEM) {
+            leafListValues.sort(null);
+        }
+        leafListValues = Collections.unmodifiableList(leafListValues);
         addYangLeaf(currentDataNode, leafListName, (Serializable) leafListValues);
     }
 
index f506b28..e83857c 100644 (file)
@@ -59,10 +59,7 @@ Get Updated Data Node by XPath
     Should Be Equal As Strings              ${responseJson['name']}   Bigger
     ${length_birds}=    Get Length          ${responseJson['birds']}
     Should Be Equal As Integers             ${length_birds}   3
-    ${expected_list}=         Create List   Pigeon   Falcon   Eagle
-    FOR      ${item_to_check}     IN      @{expected_list}
-        Should Contain     ${responseJson['birds']}     ${item_to_check}
-    END
+    Should Be Equal As Strings              ${responseJson['birds']}   ['Eagle', 'Falcon', 'Pigeon']
 
 Get Data Node by XPath
     ${uri}=             Set Variable        ${basePath}/v1/dataspaces/${dataspaceName}/anchors/${anchorName}/node
index 6499653..f967c62 100644 (file)
@@ -423,8 +423,34 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
         then: 'the updated data nodes are retrieved'
             def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
         and: 'the leaf values are updated as expected'
-            assert result.leaves['lang'] == ['English/French']
-            assert result.leaves['price'] == [100]
+            assert result[0].leaves['lang'] == 'English/French'
+            assert result[0].leaves['price'] == 100
+        cleanup:
+            restoreBookstoreDataAnchor(2)
+    }
+
+    def 'Order of leaf-list elements is preserved when "ordered-by user" is set in the YANG model.'() {
+        given: 'Updated json for bookstore data'
+            def jsonData =  "{'book-store:books':{'title':'Matilda', 'authors': ['beta', 'alpha', 'gamma', 'delta']}}"
+        when: 'update is performed for leaves'
+            objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
+        and: 'the updated data nodes are retrieved'
+            def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
+        then: 'the leaf-list values have expected order'
+            assert result[0].leaves['authors'] == ['beta', 'alpha', 'gamma', 'delta']
+        cleanup:
+            restoreBookstoreDataAnchor(2)
+    }
+
+    def 'Leaf-list elements are sorted when "ordered-by user" is not set in the YANG model.'() {
+        given: 'Updated json for bookstore data'
+            def jsonData =  "{'book-store:books':{'title':'Matilda', 'editions': [2011, 1988, 2001, 2022, 2025]}}"
+        when: 'update is performed for leaves'
+            objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
+        and: 'the updated data nodes are retrieved'
+            def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
+        then: 'the leaf-list values have natural order'
+            assert result[0].leaves['editions'] == [1988, 2001, 2011, 2022, 2025]
         cleanup:
             restoreBookstoreDataAnchor(2)
     }
@@ -540,7 +566,7 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             def expectedSourceDataInParentNode = ['name':'Children']
             def expectedTargetDataInParentNode = ['name':'Kids']
             def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
-            def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[2023, 1988, 2000]]]
+            def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[1988, 2000, 2023]]]
         when: 'attempt to get delta between leaves of existing data nodes'
             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
             def deltaReportEntities = getDeltaReportEntities(result)
@@ -555,7 +581,7 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             assert deltaReportEntities.get('xpaths').containsAll(["/bookstore/categories[@code='1']/books[@title='The Gruffalo']", "/bookstore/categories[@code='1']/books[@title='Matilda']"])
         and: 'the delta report also has expected source and target data of child nodes'
             assert deltaReportEntities.get('sourcePayload').containsAll(expectedSourceDataInChildNode)
-            //assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode) CPS-2057
+            assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode)
     }
 
     def getDeltaReportEntities(List<DeltaReport> deltaReport) {
index 3807a14..b7b6fa1 100644 (file)
@@ -36,8 +36,8 @@ class CpsModuleServiceIntegrationSpec extends FunctionalSpecBase {
     CpsModuleService objectUnderTest
 
     private static def originalNumberOfModuleReferences = 2 // bookstore has two modules
-    private static def bookStoreModuleReference = new ModuleReference('stores','2024-01-30')
-    private static def bookStoreModuleReferenceWithNamespace = new ModuleReference('stores','2024-01-30', 'org:onap:cps:sample')
+    private static def bookStoreModuleReference = new ModuleReference('stores','2024-02-08')
+    private static def bookStoreModuleReferenceWithNamespace = new ModuleReference('stores','2024-02-08', 'org:onap:cps:sample')
     private static def bookStoreTypesModuleReference = new ModuleReference('bookstore-types','2024-01-30')
     private static def bookStoreTypesModuleReferenceWithNamespace = new ModuleReference('bookstore-types','2024-01-30', 'org:onap:cps:types:sample')
     static def NEW_RESOURCE_REVISION = '2023-05-10'
@@ -155,7 +155,7 @@ class CpsModuleServiceIntegrationSpec extends FunctionalSpecBase {
             def result = objectUnderTest.getModuleDefinitionsByAnchorName(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1)
         then: 'the correct module definitions are returned'
             assert result.size() == 2
-            assert result.contains(new ModuleDefinition('stores','2024-01-30',bookstoreModelFileContent))
+            assert result.contains(new ModuleDefinition('stores','2024-02-08',bookstoreModelFileContent))
             assert result.contains(new ModuleDefinition('bookstore-types','2024-01-30', bookstoreTypesFileContent))
     }
 
@@ -165,12 +165,12 @@ class CpsModuleServiceIntegrationSpec extends FunctionalSpecBase {
         then: 'the correct module definitions are returned'
             if (expectedNumberOfDefinitions > 0) {
                 assert result.size() == expectedNumberOfDefinitions
-                def expectedModuleDefinition = new ModuleDefinition('stores', '2024-01-30', bookstoreModelFileContent)
+                def expectedModuleDefinition = new ModuleDefinition('stores', '2024-02-08', bookstoreModelFileContent)
                 assert result[0] == expectedModuleDefinition
             }
         where: 'following parameters are used'
             scenarios                          | moduleName | moduleRevision || expectedNumberOfDefinitions
-            'correct module name and revision' | 'stores'   | '2024-01-30'   || 1
+            'correct module name and revision' | 'stores'   | '2024-02-08'   || 1
             'correct module name'              | 'stores'   | null           || 1
             'incorrect module name'            | 'other'    | null           || 0
             'incorrect revision'               | 'stores'   | '2025-11-22'   || 0
index 2abde65..0d093ea 100644 (file)
@@ -9,6 +9,11 @@ module stores {
         revision-date 2024-01-30;
     }
 
+    revision "2024-02-08" {
+        description
+            "Order of book authors is preserved";
+    }
+
     revision "2024-01-30" {
         description
             "Extracted bookstore types";
@@ -107,6 +112,7 @@ module stores {
                     type string;
                 }
                 leaf-list authors {
+                    ordered-by user;
                     type string;
                 }
                 leaf-list editions {