Fix automatic lifecycle delete stacking 28/80028/1
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 30 Jan 2019 15:39:35 +0000 (16:39 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 30 Jan 2019 15:47:37 +0000 (16:47 +0100)
Nodes with automatic lifecycle have the strange property that their
delete operations may actually stack with other operations.

Unfortunately delete operation itself has no semantics of having
underlying modifications, as it has no baseline to which to apply
them.

In order to fix this, we need to turn deletes into empty writes
for these constructs and once we are performing disappearing, undo
this trick. This effectively means that both empty writes and deletes
result in a delete operation, if they ended up modifying the tree,
or into a no-op if they did not.

JIRA: YANGTOOLS-938
Change-Id: I68b9f304e76d6b1bdb707f411a87cd6085ed8bc7
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
13 files changed:
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractNodeContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractValueNodeModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ChoiceModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ListEntryModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/MinMaxElementsValidation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ModifiedNode.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/PresenceContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/SchemaAwareApplyOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/StructuralContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UnkeyedListModificationStrategy.java
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug2690Test.java
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/TestModel.java
yang/yang-data-impl/src/test/resources/odl-datastore-test.yang

index 5ea52b1686cf3aca334ac8d1961125cac15ad78d..b0b6cecba8c3ec67cc73e1d78c6dea31ba5e1dd1 100644 (file)
@@ -78,9 +78,8 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification,
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
             final Optional<TreeNode> currentMeta, final Version version) {
-        final NormalizedNode<?, ?> newValue = modification.getWrittenValue();
         final TreeNode newValueMeta = TreeNodeFactory.createTreeNode(newValue, version);
 
         if (modification.getChildren().isEmpty()) {
index 2cd8bbdac39db50086c1122f55f164576196c25d..ff50d991af3d23bb38c0bd12f0c764a22d0f887e 100644 (file)
@@ -57,15 +57,16 @@ abstract class AbstractValueNodeModificationStrategy<T extends DataSchemaNode> e
     protected final TreeNode applyMerge(final ModifiedNode modification, final TreeNode currentMeta,
             final Version version) {
         // Just overwrite whatever was there, but be sure to run validation
-        verifyStructure(modification.getWrittenValue(), true);
+        final NormalizedNode<?, ?> newValue = modification.getWrittenValue();
+        verifyStructure(newValue, true);
         modification.resolveModificationType(ModificationType.WRITE);
-        return applyWrite(modification, null, version);
+        return applyWrite(modification, newValue, null, version);
     }
 
     @Override
-    protected final TreeNode applyWrite(final ModifiedNode modification,
+    protected final TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
             final Optional<TreeNode> currentMeta, final Version version) {
-        return TreeNodeFactory.createTreeNode(modification.getWrittenValue(), version);
+        return TreeNodeFactory.createTreeNode(newValue, version);
     }
 
     @Override
index ddf81930e185efff154fedf795336912355082a5..568bf4c0dc748dcab1a778cdfd5a1ba1b4e6847c 100644 (file)
@@ -131,9 +131,9 @@ final class ChoiceModificationStrategy extends AbstractNodeContainerModification
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification, final Optional<TreeNode> currentMeta,
-            final Version version) {
-        final TreeNode ret = super.applyWrite(modification, currentMeta, version);
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
+            final Optional<TreeNode> currentMeta, final Version version) {
+        final TreeNode ret = super.applyWrite(modification, newValue, currentMeta, version);
         enforceCases(ret);
         return ret;
     }
index d64442ba57cd21fe1f12c7cae1ef1f3744b1f933..9b44410f08012b447f8c0e81d066565c70af8e88 100644 (file)
@@ -43,9 +43,9 @@ final class ListEntryModificationStrategy extends AbstractDataNodeContainerModif
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification, final Optional<TreeNode> currentMeta,
-            final Version version) {
-        final TreeNode ret = super.applyWrite(modification, currentMeta, version);
+    protected TreeNode applyWrite(final ModifiedNode modification,  final NormalizedNode<?, ?> newValue,
+            final Optional<TreeNode> currentMeta, final Version version) {
+        final TreeNode ret = super.applyWrite(modification, newValue, currentMeta, version);
         enforcer.enforceOnTreeNode(ret);
         return ret;
     }
index 92f96c9e76d92ae5d3169b52d471720b4a201c0d..02335ff6765a2cb82181a7a09ffe80ce6673952b 100644 (file)
@@ -166,15 +166,15 @@ final class MinMaxElementsValidation extends SchemaAwareApplyOperation {
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification, final Optional<TreeNode> currentMeta,
-            final Version version) {
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
+            final Optional<TreeNode> currentMeta, final Version version) {
         final TreeNode validated = modification.getValidatedNode(this, currentMeta);
         if (validated != null) {
             return validated;
         }
 
         // FIXME: the result moved, make sure we enforce again
-        return delegate.applyWrite(modification, currentMeta, version);
+        return delegate.applyWrite(modification, newValue, currentMeta, version);
     }
 
     @Override
index 9e163f538aaa2cd2325ec9cd5325dca2a4cce09f..699a785bd856b9572234f6aea00bd2d42af7c74c 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.yangtools.yang.data.impl.schema.tree;
 
+import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
 import java.util.Collection;
@@ -101,8 +102,9 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
      *
      * @return Currently-written value
      */
+    @Nonnull
     NormalizedNode<?, ?> getWrittenValue() {
-        return value;
+        return verifyNotNull(value);
     }
 
     /**
index 578c50de2458c820367996df655ac6af8c543b57..5be8745cad758f1623a76030a250261c699d67a1 100644 (file)
@@ -44,9 +44,9 @@ final class PresenceContainerModificationStrategy extends ContainerModificationS
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification, final Optional<TreeNode> currentMeta,
-            final Version version) {
-        final TreeNode ret = super.applyWrite(modification, currentMeta, version);
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
+            final Optional<TreeNode> currentMeta, final Version version) {
+        final TreeNode ret = super.applyWrite(modification, newValue, currentMeta, version);
         enforcer.enforceOnTreeNode(ret);
         return ret;
     }
index 03654a619750a5ff60a4f6289e95103663cbf1a0..d50ad5a7c61b138f0d2e8ea7592f2c6e4fd086b8 100644 (file)
@@ -13,6 +13,7 @@ import java.util.Optional;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ConflictingModificationAppliedException;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
@@ -217,7 +218,7 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
                     // structure is usually verified when the transaction is sealed. To preserve correctness, we have
                     // to run that validation here.
                     modification.resolveModificationType(ModificationType.WRITE);
-                    result = applyWrite(modification, currentMeta, version);
+                    result = applyWrite(modification, modification.getWrittenValue(), currentMeta, version);
                     verifyStructure(result.getData(), true);
                 } else {
                     result = applyMerge(modification, currentMeta.get(), version);
@@ -226,7 +227,8 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
                 return modification.setSnapshot(Optional.of(result));
             case WRITE:
                 modification.resolveModificationType(ModificationType.WRITE);
-                return modification.setSnapshot(Optional.of(applyWrite(modification, currentMeta, version)));
+                return modification.setSnapshot(Optional.of(applyWrite(modification, modification.getWrittenValue(),
+                    currentMeta, version)));
             case NONE:
                 modification.resolveModificationType(ModificationType.UNMODIFIED);
                 return currentMeta;
@@ -247,7 +249,8 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
      */
     protected abstract TreeNode applyMerge(ModifiedNode modification, TreeNode currentMeta, Version version);
 
-    protected abstract TreeNode applyWrite(ModifiedNode modification, Optional<TreeNode> currentMeta, Version version);
+    protected abstract TreeNode applyWrite(ModifiedNode modification, NormalizedNode<?, ?> newValue,
+            Optional<TreeNode> currentMeta, Version version);
 
     /**
      * Apply a nested operation. Since there may not actually be a nested operation
index 877f994faf90ff294ca807928edf2fc0d5ba7b5d..d5df4716f6e9fc6f443f0dc582a7eb62c44413ab 100644 (file)
@@ -12,6 +12,7 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgum
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
+import org.opendaylight.yangtools.yang.data.api.schema.OrderedNodeContainer;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
@@ -54,42 +55,24 @@ final class StructuralContainerModificationStrategy extends ModificationApplyOpe
     @Override
     Optional<TreeNode> apply(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
             final Version version) {
-        final Optional<TreeNode> ret;
-        if (modification.getOperation() == LogicalOperation.TOUCH && !storeMeta.isPresent()) {
-            // Container is not present, let's take care of the 'magically appear' part of our job
-            ret = delegate.apply(modification, fakeMeta(version), version);
-
-            // Fake container got removed: that is a no-op
-            if (!ret.isPresent()) {
-                modification.resolveModificationType(ModificationType.UNMODIFIED);
-                return ret;
-            }
-
-            // If the delegate indicated SUBTREE_MODIFIED, account for the fake and report APPEARED
-            if (modification.getModificationType() == ModificationType.SUBTREE_MODIFIED) {
-                modification.resolveModificationType(ModificationType.APPEARED);
+        // The only way a tree node can disappear is through delete (which we handle here explicitly) or through
+        // actions of disappearResult(). It is therefore safe to perform Optional.get() on the results of
+        // delegate.apply()
+        final TreeNode ret;
+        if (modification.getOperation() == LogicalOperation.DELETE) {
+            if (modification.getChildren().isEmpty()) {
+                return delegate.apply(modification, storeMeta, version);
             }
+            // Delete with children, implies it really is an empty write
+            ret = delegate.applyWrite(modification, emptyNode, storeMeta, version);
+        } else if (modification.getOperation() == LogicalOperation.TOUCH && !storeMeta.isPresent()) {
+            ret = applyTouch(modification, storeMeta, version);
         } else {
-            // Container is present, run normal apply operation
-            ret = delegate.apply(modification, storeMeta, version);
-
-            // Container was explicitly deleted, no magic required
-            if (!ret.isPresent()) {
-                return ret;
-            }
-        }
-
-        /*
-         * At this point ret is guaranteed to be present. We need to take care of the 'magically disappear' part of
-         * our job. Check if there are any child nodes left. If there are none, remove this container and turn the
-         * modification into a DISAPPEARED.
-         */
-        if (((NormalizedNodeContainer<?, ?, ?>) ret.get().getData()).getValue().isEmpty()) {
-            modification.resolveModificationType(ModificationType.DISAPPEARED);
-            return Optional.empty();
+            // No special handling required here, run normal apply operation
+            ret = delegate.apply(modification, storeMeta, version).get();
         }
 
-        return ret;
+        return disappearResult(modification, ret, storeMeta);
     }
 
     @Override
@@ -128,4 +111,50 @@ final class StructuralContainerModificationStrategy extends ModificationApplyOpe
     public Optional<ModificationApplyOperation> getChild(final PathArgument child) {
         return delegate.getChild(child);
     }
+
+    private TreeNode applyTouch(final ModifiedNode modification, final Optional<TreeNode> storeMeta,
+            final Version version) {
+        // Container is not present, let's take care of the 'magically appear' part of our job
+        final Optional<TreeNode> ret = delegate.apply(modification, fakeMeta(version), version);
+
+        // If the delegate indicated SUBTREE_MODIFIED, account for the fake and report APPEARED
+        if (modification.getModificationType() == ModificationType.SUBTREE_MODIFIED) {
+            modification.resolveModificationType(ModificationType.APPEARED);
+        }
+        return ret.get();
+    }
+
+    private static Optional<TreeNode> disappearResult(final ModifiedNode modification, final TreeNode result,
+            final Optional<TreeNode> storeMeta) {
+        // Check if the result is in fact empty before pulling any tricks
+        if (!isEmpty(result)) {
+            return Optional.of(result);
+        }
+
+        // We are pulling the 'disappear' trick, but what we report can be three different things
+        final ModificationType finalType;
+        if (!storeMeta.isPresent()) {
+            // ... there was nothing in the datastore, no change
+            finalType = ModificationType.UNMODIFIED;
+        } else if (modification.getModificationType() == ModificationType.WRITE) {
+            // ... this was an empty write, possibly originally a delete
+            finalType = ModificationType.DELETE;
+        } else {
+            // ... it really disappeared
+            finalType = ModificationType.DISAPPEARED;
+        }
+        modification.resolveModificationType(finalType);
+        return Optional.empty();
+    }
+
+    private static boolean isEmpty(final TreeNode treeNode) {
+        final NormalizedNode<?, ?> data = treeNode.getData();
+        if (data instanceof NormalizedNodeContainer) {
+            return ((NormalizedNodeContainer<?, ?, ?>) data).getValue().isEmpty();
+        }
+        if (data instanceof OrderedNodeContainer) {
+            return ((OrderedNodeContainer<?>) data).getSize() == 0;
+        }
+        throw new IllegalStateException("Unhandled data " + data);
+    }
 }
index 076f9df776db52048740cbdb4585e926c004ae6e..646222cf1b49baf8fb28620ccbf05216b69f6a9e 100644 (file)
@@ -48,9 +48,8 @@ final class UnkeyedListModificationStrategy extends SchemaAwareApplyOperation {
     }
 
     @Override
-    protected TreeNode applyWrite(final ModifiedNode modification,
+    protected TreeNode applyWrite(final ModifiedNode modification, final NormalizedNode<?, ?> newValue,
             final Optional<TreeNode> currentMeta, final Version version) {
-        final NormalizedNode<?, ?> newValue = modification.getWrittenValue();
         final TreeNode newValueMeta = TreeNodeFactory.createTreeNode(newValue, version);
 
         if (modification.getChildren().isEmpty()) {
index fd19f5bdae905c1ca8c794586830d008520aa869..518f6eb641d9a46ef7f815f47326615116b7242e 100644 (file)
@@ -14,6 +14,7 @@ import java.util.Optional;
 import org.junit.Before;
 import org.junit.Test;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
@@ -30,6 +31,9 @@ import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
 
 public class Bug2690Test {
+    private static final YangInstanceIdentifier NAME_PATH = YangInstanceIdentifier.of(TestModel.NON_PRESENCE_QNAME)
+            .node(TestModel.NAME_QNAME);
+
     private DataTree inMemoryDataTree;
 
     @Before
@@ -43,28 +47,24 @@ public class Bug2690Test {
         final MapEntryNode fooEntryNode = ImmutableNodes.mapEntry(TestModel.OUTER_LIST_QNAME, TestModel.ID_QNAME, 1);
         final MapEntryNode barEntryNode = ImmutableNodes.mapEntry(TestModel.OUTER_LIST_QNAME, TestModel.ID_QNAME, 2);
         final MapNode mapNode1 = ImmutableNodes.mapNodeBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.OUTER_LIST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.OUTER_LIST_QNAME))
                 .withChild(fooEntryNode).build();
         final MapNode mapNode2 = ImmutableNodes.mapNodeBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.OUTER_LIST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.OUTER_LIST_QNAME))
                 .withChild(barEntryNode).build();
 
         final ContainerNode cont1 = Builders.containerBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.TEST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.TEST_QNAME))
                 .withChild(mapNode1).build();
 
         final ContainerNode cont2 = Builders.containerBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TestModel.TEST_QNAME))
+                .withNodeIdentifier(new NodeIdentifier(TestModel.TEST_QNAME))
                 .withChild(mapNode2).build();
 
         final DataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
         modificationTree.write(TestModel.TEST_PATH, cont1);
         modificationTree.merge(TestModel.TEST_PATH, cont2);
-        modificationTree.ready();
-
-        inMemoryDataTree.validate(modificationTree);
-        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
-        inMemoryDataTree.commit(prepare);
+        commit(modificationTree);
 
         final DataTreeSnapshot snapshotAfterTx = inMemoryDataTree.takeSnapshot();
         final DataTreeModification modificationAfterTx = snapshotAfterTx.newModification();
@@ -72,4 +72,38 @@ public class Bug2690Test {
         assertTrue(readNode.isPresent());
         assertEquals(2, ((NormalizedNodeContainer<?,?,?>)readNode.get()).getValue().size());
     }
+
+    @Test
+    public void testDeleteStructuralAndWriteChild() throws DataValidationFailedException {
+        final DataTreeModification modificationTree = setupTestDeleteStructuralAndWriteChild();
+        verifyTestDeleteStructuralAndWriteChild(modificationTree);
+    }
+
+    @Test
+    public void testDeleteStructuralAndWriteChildWithCommit() throws DataValidationFailedException {
+        final DataTreeModification modificationTree = setupTestDeleteStructuralAndWriteChild();
+        commit(modificationTree);
+        verifyTestDeleteStructuralAndWriteChild(inMemoryDataTree.takeSnapshot());
+    }
+
+    private DataTreeModification setupTestDeleteStructuralAndWriteChild() {
+        final DataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.delete(YangInstanceIdentifier.of(TestModel.NON_PRESENCE_QNAME));
+        modificationTree.write(NAME_PATH, Builders.leafBuilder()
+            .withNodeIdentifier(new NodeIdentifier(TestModel.NAME_QNAME)).withValue("abc").build());
+        return modificationTree;
+    }
+
+    private static void verifyTestDeleteStructuralAndWriteChild(final DataTreeSnapshot snapshot) {
+        final Optional<NormalizedNode<?, ?>> readNode = snapshot.readNode(NAME_PATH);
+        assertTrue(readNode.isPresent());
+    }
+
+    private void commit(final DataTreeModification modificationTree) throws DataValidationFailedException {
+        modificationTree.ready();
+
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
 }
index 664d7d6561741d5f0001044116392c34be1112f6..f98d9d2bb041192af4a93fcf98eabaa9daf4e23e 100644 (file)
@@ -25,6 +25,11 @@ public final class TestModel {
     public static final QName VALUE_QNAME = QName.create(TEST_QNAME, "value");
     private static final String DATASTORE_TEST_YANG = "/odl-datastore-test.yang";
 
+    public static final QName NON_PRESENCE_QNAME = QName.create(TEST_QNAME, "non-presence");
+    public static final QName DEEP_CHOICE_QNAME = QName.create(TEST_QNAME, "deep-choice");
+    public static final QName A_LIST_QNAME = QName.create(TEST_QNAME, "a-list");
+    public static final QName A_NAME_QNAME = QName.create(TEST_QNAME, "a-name");
+
     public static final YangInstanceIdentifier TEST_PATH = YangInstanceIdentifier.of(TEST_QNAME);
     public static final YangInstanceIdentifier OUTER_LIST_PATH = YangInstanceIdentifier.builder(TEST_PATH)
             .node(OUTER_LIST_QNAME).build();
index 7241758eaab5101acc46defdb7be08caf80d2f02..43143f20cb8b59433d1f36a2c2b2270def94eb36 100644 (file)
@@ -7,6 +7,30 @@ module odl-datastore-test {
         description "Initial revision.";
     }
 
+    container non-presence {
+        description "Deep structure of (structural) nodes to test structural ";
+
+        leaf name {
+            type string;
+        }
+
+        choice deep-choice {
+            list a-list {
+                key "a-name";
+
+                leaf a-name {
+                    type string;
+                }
+            }
+
+            container b-container {
+                leaf b-name {
+                    type string;
+                }
+            }
+        }
+    }
+
     container test {
         presence true;
         choice choice1 {