WriteTransaction: make sure TransactionCommitFailedException is thrown 89/12989/2
authorMarek Gradzki <mgradzki@cisco.com>
Fri, 13 Apr 2018 11:38:16 +0000 (13:38 +0200)
committerMarek Gradzki <mgradzki@cisco.com>
Mon, 11 Jun 2018 14:21:21 +0000 (16:21 +0200)
DataTreeModification.ready() used by DataModification.validate()
might throw IllegalArgumentException in case of missing mandatory nodes.

Use broader Exception type in WriteTransaction.submit()
to make sure contract defined by AsyncWriteTransaction is preserved.

Change-Id: I95cb3e1e8c6db36df90d2c78e7d63c854189e2fd
Signed-off-by: Marek Gradzki <mgradzki@cisco.com>
infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/WriteTransaction.java
infra/it/it-test/pom.xml
infra/it/it-test/src/test/java/io/fd/honeycomb/data/impl/TestValidate.java [new file with mode: 0644]
infra/it/it-test/src/test/resources/messages/commit.xml [new file with mode: 0644]
infra/it/it-test/src/test/resources/messages/edit-config/edit-config-missing-mandatory-node.xml [new file with mode: 0644]
infra/it/it-test/src/test/resources/messages/rpc-reply_ok.xml [new file with mode: 0644]
infra/it/it-test/src/test/resources/models/test-validate.yang [new file with mode: 0644]

index 648d435..8e7dca0 100644 (file)
@@ -29,7 +29,6 @@ import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import io.fd.honeycomb.data.DataModification;
-import io.fd.honeycomb.translate.TranslationException;
 import java.util.function.Consumer;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -41,7 +40,6 @@ import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
 import org.opendaylight.yangtools.yang.common.RpcResult;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -143,7 +141,7 @@ final class WriteTransaction implements DOMDataWriteTransaction {
             }
 
             status = COMMITED;
-        } catch (DataValidationFailedException | TranslationException e) {
+        } catch (Exception e) {
             status = FAILED;
             LOG.error("Submit failed", e);
             return Futures.immediateFailedCheckedFuture(
index 41d48ca..25bb575 100644 (file)
             <artifactId>honeycomb-test-model</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>xmlunit</groupId>
+            <artifactId>xmlunit</artifactId>
+            <version>1.6</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.netconf</groupId>
+            <artifactId>netconf-util</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.yangtools</groupId>
+            <artifactId>yang-test-util</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.netconf</groupId>
+            <artifactId>mdsal-netconf-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/infra/it/it-test/src/test/java/io/fd/honeycomb/data/impl/TestValidate.java b/infra/it/it-test/src/test/java/io/fd/honeycomb/data/impl/TestValidate.java
new file mode 100644 (file)
index 0000000..5bc933f
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2018 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.honeycomb.data.impl;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration.DEFAULT_CONFIGURATION;
+
+import com.google.common.io.ByteSource;
+import com.google.common.util.concurrent.Futures;
+import io.fd.honeycomb.data.ReadableDataManager;
+import java.io.StringWriter;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.custommonkey.xmlunit.DetailedDiff;
+import org.custommonkey.xmlunit.Diff;
+import org.custommonkey.xmlunit.XMLUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.controller.config.util.xml.DocumentedException;
+import org.opendaylight.controller.config.util.xml.XmlUtil;
+import org.opendaylight.controller.sal.core.api.model.SchemaService;
+import org.opendaylight.netconf.mapping.api.NetconfOperation;
+import org.opendaylight.netconf.mapping.api.NetconfOperationChainedExecution;
+import org.opendaylight.netconf.mdsal.connector.CurrentSchemaContext;
+import org.opendaylight.netconf.mdsal.connector.TransactionProvider;
+import org.opendaylight.netconf.mdsal.connector.ops.Commit;
+import org.opendaylight.netconf.mdsal.connector.ops.EditConfig;
+import org.opendaylight.netconf.util.test.NetconfXmlUnitRecursiveQualifier;
+import org.opendaylight.netconf.util.test.XmlFileLoader;
+import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree;
+import org.opendaylight.yangtools.yang.data.impl.schema.tree.InMemoryDataTreeFactory;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaContextListener;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+
+public class TestValidate {
+    private static final Logger LOG = LoggerFactory.getLogger(TestValidate.class);
+    private static final String SESSION_ID_FOR_REPORTING = "netconf-test-session";
+    private static final Document RPC_REPLY_OK = getReplyOk();
+
+    @Mock
+    private ReadableDataManager operationalData;
+    @Mock
+    private SchemaService schemaService;
+
+    private CurrentSchemaContext currentSchemaContext;
+    private DataBroker dataBroker;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        final SchemaContext schemaContext =
+            YangParserTestUtils.parseYangResources(TestValidate.class, "/models/test-validate.yang");
+        when(schemaService.registerSchemaContextListener(any())).thenAnswer(invocation -> {
+            SchemaContextListener listener = invocation.getArgument(0);
+            listener.onGlobalContextUpdated(schemaContext);
+            return new ListenerRegistration<SchemaContextListener>() {
+                @Override
+                public void close() {
+                }
+
+                @Override
+                public SchemaContextListener getInstance() {
+                    return listener;
+                }
+            };
+        });
+        currentSchemaContext = new CurrentSchemaContext(schemaService, sourceIdentifier -> {
+            final YangTextSchemaSource yangTextSchemaSource =
+                YangTextSchemaSource.delegateForByteSource(sourceIdentifier, ByteSource.wrap("module test".getBytes()));
+            return Futures.immediateCheckedFuture(yangTextSchemaSource);
+        });
+
+        final DataTree dataTree = new InMemoryDataTreeFactory().create(DEFAULT_CONFIGURATION);
+        dataTree.setSchemaContext(schemaContext);
+        dataBroker = DataBroker.create(new ModifiableDataTreeManager(dataTree), operationalData);
+        XMLUnit.setIgnoreWhitespace(true);
+        XMLUnit.setIgnoreAttributeOrder(true);
+    }
+
+    @Test
+    public void testValidateMissingMandatoryNode() throws Exception {
+        final TransactionProvider transactionProvider = new TransactionProvider(dataBroker, SESSION_ID_FOR_REPORTING);
+        verifyResponse(edit("messages/edit-config/edit-config-missing-mandatory-node.xml", transactionProvider));
+        try {
+            verifyResponse(commit(transactionProvider));
+            fail("Should have failed due to missing mandatory node");
+        } catch (DocumentedException e) {
+            assertTrue(e.getMessage().contains("missing mandatory descendant"));
+        }
+    }
+
+    private static Document getReplyOk() {
+        Document doc;
+        try {
+            doc = XmlFileLoader.xmlFileToDocument("messages/rpc-reply_ok.xml");
+        } catch (final Exception e) {
+            LOG.debug("unable to load rpc reply ok.", e);
+            doc = XmlUtil.newDocument();
+        }
+        return doc;
+    }
+
+    private static void verifyResponse(final Document actual) throws Exception {
+        final DetailedDiff dd = new DetailedDiff(new Diff(RPC_REPLY_OK, actual));
+        dd.overrideElementQualifier(new NetconfXmlUnitRecursiveQualifier());
+        if (!dd.similar()) {
+            LOG.warn("Actual response:");
+            printDocument(actual);
+            LOG.warn("Expected response:");
+            printDocument(RPC_REPLY_OK);
+            fail("Differences found: " + dd.toString());
+        }
+    }
+
+    private static void printDocument(final Document doc) throws Exception {
+        final TransformerFactory tf = TransformerFactory.newInstance();
+        final Transformer transformer = tf.newTransformer();
+        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
+
+        final StringWriter writer = new StringWriter();
+        transformer.transform(new DOMSource(doc), new StreamResult(writer));
+        LOG.warn(writer.getBuffer().toString());
+    }
+
+    private Document edit(final String resource, final TransactionProvider transactionProvider) throws Exception {
+        final EditConfig editConfig = new EditConfig(SESSION_ID_FOR_REPORTING, currentSchemaContext,
+            transactionProvider);
+        return executeOperation(editConfig, resource);
+    }
+
+    private static Document commit(final TransactionProvider transactionProvider) throws Exception {
+        final Commit commit = new Commit(SESSION_ID_FOR_REPORTING, transactionProvider);
+        return executeOperation(commit, "messages/commit.xml");
+    }
+
+    private static Document executeOperation(final NetconfOperation op, final String filename) throws Exception {
+        final Document request = XmlFileLoader.xmlFileToDocument(filename);
+        final Document response = op.handle(request, NetconfOperationChainedExecution.EXECUTION_TERMINATION_POINT);
+        LOG.debug("Got response {}", response);
+        return response;
+    }
+}
diff --git a/infra/it/it-test/src/test/resources/messages/commit.xml b/infra/it/it-test/src/test/resources/messages/commit.xml
new file mode 100644 (file)
index 0000000..a4a98f4
--- /dev/null
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (c) 2018 Cisco and/or its affiliates.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at:
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<rpc message-id="a" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <commit/>
+</rpc>
diff --git a/infra/it/it-test/src/test/resources/messages/edit-config/edit-config-missing-mandatory-node.xml b/infra/it/it-test/src/test/resources/messages/edit-config/edit-config-missing-mandatory-node.xml
new file mode 100644 (file)
index 0000000..829ef56
--- /dev/null
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright (c) 2018 Cisco and/or its affiliates.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at:
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<rpc message-id="a" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <edit-config>
+        <target>
+            <candidate/>
+        </target>
+        <default-operation>none</default-operation>
+        <config>
+            <top-container xmlns="urn:honeycomb:params:xml:ns:yang:test:validate" xmlns:a="urn:ietf:params:xml:ns:netconf:base:1.0" a:operation="create">
+                <list-in-container>
+                    <name>item1</name>>
+                </list-in-container>
+                <list-in-container>
+                    <name>item2</name>
+                    <description>description2</description>
+                </list-in-container>
+            </top-container>
+        </config>
+    </edit-config>
+</rpc>
\ No newline at end of file
diff --git a/infra/it/it-test/src/test/resources/messages/rpc-reply_ok.xml b/infra/it/it-test/src/test/resources/messages/rpc-reply_ok.xml
new file mode 100644 (file)
index 0000000..df205b6
--- /dev/null
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (c) 2018 Cisco and/or its affiliates.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at:
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="a">
+    <ok/>
+</rpc-reply>
\ No newline at end of file
diff --git a/infra/it/it-test/src/test/resources/models/test-validate.yang b/infra/it/it-test/src/test/resources/models/test-validate.yang
new file mode 100644 (file)
index 0000000..1440456
--- /dev/null
@@ -0,0 +1,27 @@
+module test-validate {
+    yang-version 1;
+    namespace "urn:honeycomb:params:xml:ns:yang:test:validate";
+    prefix "td";
+
+    revision "2018-06-08" {
+        description "Initial revision";
+    }
+
+    container top-container {
+        leaf name {
+            type string;
+        }
+        list list-in-container {
+            key "name";
+
+            leaf name {
+                type string;
+            }
+
+            leaf description {
+                type string;
+                mandatory true;
+            }
+        }
+    }
+}