From f81f80bb7235aab4695f624b69ee78d80f640dda Mon Sep 17 00:00:00 2001 From: Pierre Baillargeon Date: Thu, 25 Jan 2024 10:05:54 -0500 Subject: [PATCH] EMSUSD-972 add unit test for circular relationships. The test revealed a flaw in the code that follows relations and connections. Fixed the code to not go into an infinite loop when there are circular relationships or connections. - Fix infinite recursion when relations or connections form a circle. - Added many test for connections, similar to those for relations. - Refactored the test code to allow testing relations or connections. - Added tests for circular targets. - Added test for mix or relationships and connections. --- lib/mayaUsd/utils/copyLayerPrims.cpp | 70 ++-- test/lib/mayaUsd/fileio/testCopyLayerPrims.py | 374 ++++++++++++++---- 2 files changed, 344 insertions(+), 100 deletions(-) diff --git a/lib/mayaUsd/utils/copyLayerPrims.cpp b/lib/mayaUsd/utils/copyLayerPrims.cpp index b308eeed85..a0b7f42bb7 100644 --- a/lib/mayaUsd/utils/copyLayerPrims.cpp +++ b/lib/mayaUsd/utils/copyLayerPrims.cpp @@ -147,6 +147,21 @@ void renamePath(SdfPath& pathToVerify, const MayaUsd::CopyLayerPrimsResult& resu } } +// Verify if the given path has already been copied. +bool isAlreadyCopied(const SdfPath& pathToVerify, MayaUsd::CopyLayerPrimsResult& result) +{ + for (const auto& srcAndDest : result.copiedPaths) { + const SdfPath& alreadyDone = srcAndDest.first; + if (pathToVerify.HasPrefix(alreadyDone)) { + DEBUG_LOG_COPY_LAYER_PRIMS(TfStringPrintf( + "Already copied source prim %s, skipping additional copies", + pathToVerify.GetAsString().c_str())); + return true; + } + } + return false; +} + // Prim hierarchy traverser (a function called for every SdfSpec starting // from a prim to be copied, recursively) that copies each prim encountered // and optionally adds the targets of relationships to the list of other paths @@ -171,13 +186,15 @@ bool copyTraverser( if (options.followRelationships) { const SdfPath& targetPath = pathToCopy.GetTargetPath(); if (!targetPath.IsEmpty()) { - DEBUG_LOG_COPY_LAYER_PRIMS(TfStringPrintf( - "Adding %s to be copied due to target in %s", - targetPath.GetAsString().c_str(), - pathToCopy.GetAsString().c_str())); - - otherPathsToCopy.emplace_back(targetPath); - addProgressSteps(options, 1); + if (!isAlreadyCopied(targetPath, result)) { + DEBUG_LOG_COPY_LAYER_PRIMS(TfStringPrintf( + "Adding %s to be copied due to target in %s", + targetPath.GetAsString().c_str(), + pathToCopy.GetAsString().c_str())); + + otherPathsToCopy.emplace_back(targetPath); + addProgressSteps(options, 1); + } } } return true; @@ -191,30 +208,23 @@ bool copyTraverser( return true; } - for (const auto& srcAndDest : result.copiedPaths) { - const SdfPath& alreadyDone = srcAndDest.first; - if (pathToCopy.HasPrefix(alreadyDone)) { - DEBUG_LOG_COPY_LAYER_PRIMS(TfStringPrintf( - "Already copied source prim %s, skipping additional copies", - pathToCopy.GetAsString().c_str())); - - // Note: it may have been copied indirectly, in that case it will not - // have been added to the list copied paths, so we want to add - // it to the list of copied paths now. It's important that it be - // added because the list of copied prims is used to post-process - // the copy by the callers. For example, we post-process it to - // handle display layers. - if (result.copiedPaths.count(pathToCopy) == 0) { - SdfPath dstPath = pathToCopy.ReplacePrefix(srcParentPath, dstParentPath); - // Verify if the prim that contained this prim was renamed. - renamePath(dstPath, result); - result.copiedPaths[pathToCopy] = dstPath; - } - - // Note: we must not prevent traversing children otherwise we will - // not process relationships. - return true; + if (isAlreadyCopied(pathToCopy, result)) { + // Note: it may have been copied indirectly, in that case it will not + // have been added to the list copied paths, so we want to add + // it to the list of copied paths now. It's important that it be + // added because the list of copied prims is used to post-process + // the copy by the callers. For example, we post-process it to + // handle display layers. + if (result.copiedPaths.count(pathToCopy) == 0) { + SdfPath dstPath = pathToCopy.ReplacePrefix(srcParentPath, dstParentPath); + // Verify if the prim that contained this prim was renamed. + renamePath(dstPath, result); + result.copiedPaths[pathToCopy] = dstPath; } + + // Note: we must not prevent traversing children otherwise we will + // not process relationships. + return true; } // Make the destination path unique and make sure parent prims diff --git a/test/lib/mayaUsd/fileio/testCopyLayerPrims.py b/test/lib/mayaUsd/fileio/testCopyLayerPrims.py index e8a9a1700a..4ef5fccb7c 100644 --- a/test/lib/mayaUsd/fileio/testCopyLayerPrims.py +++ b/test/lib/mayaUsd/fileio/testCopyLayerPrims.py @@ -72,12 +72,24 @@ def _createPrims(self, stage, srcPathAndTypes): prim = stage.DefinePrim(Sdf.Path(srcPath), typeName) self.assertTrue(prim, 'Expected to create prim %s with type %s' % (srcPath, typeName)) - def _createRelationships(self, stage, relations): + def _createTargets(self, stage, targets, asConnections): '''Creates the relationships described in the map (dict) of source to target.''' - for srcPath, dstPath in relations.items(): + for srcPath, dstPath in targets.items(): srcPrim = stage.GetPrimAtPath(Sdf.Path(srcPath)) - rel = srcPrim.CreateRelationship('arrow') - rel.AddTarget(Sdf.Path(dstPath)) + if asConnections: + attr = srcPrim.CreateAttribute('tunnel', Sdf.ValueTypeNames.String, True) + attr.AddConnection(Sdf.Path(dstPath)) + else: + rel = srcPrim.CreateRelationship('arrow') + rel.AddTarget(Sdf.Path(dstPath)) + + def _createRelationships(self, stage, relations): + '''Creates the relationships described in the map (dict) of source to target.''' + self._createTargets(stage, relations, asConnections=False) + + def _createConnections(self, stage, connections): + '''Creates the connections described in the map (dict) of source to target.''' + self._createTargets(stage, connections, asConnections=True) def _createStageLayerAndPrims(self, srcPathAndTypes): '''Create and return an in-memory stage and its root layer and creates prims.''' @@ -102,17 +114,31 @@ def _verifyDestinationPrims(self, stage, copiedPrims, srcPathAndTypes, expected) self.assertTrue(dstPrim) self.assertEqual(dstPrim.GetTypeName(), srcPathAndTypes[srcPath]) - def _verifyDestinationRelTargets(self, stage, expected): - '''Verify that the relationships have the expected targets.''' + def _verifyDestinationTargets(self, stage, expected, asConnections): + '''Verify that the prims have the expected targets.''' for dstPath, expectedTarget in expected.items(): dstPrim = stage.GetPrimAtPath(Sdf.Path(dstPath)) self.assertTrue(dstPrim) - rel = dstPrim.GetRelationship('arrow') - self.assertIsNotNone(rel) - self.assertTrue(rel) - targets = rel.GetTargets() + if asConnections: + attr = dstPrim.GetAttribute('tunnel') + self.assertIsNotNone(attr) + self.assertTrue(attr) + targets = attr.GetConnections() + else: + rel = dstPrim.GetRelationship('arrow') + self.assertIsNotNone(rel) + self.assertTrue(rel) + targets = rel.GetTargets() self.assertIn(Sdf.Path(expectedTarget), targets) + def _verifyDestinationRelationships(self, stage, expected): + '''Verify that the relationships have the expected targets.''' + return self._verifyDestinationTargets(stage, expected, asConnections=False) + + def _verifyDestinationConnections(self, stage, expected): + '''Verify that the connections have the expected targets.''' + return self._verifyDestinationTargets(stage, expected, asConnections=True) + def testCopyLayerPrimsSimple(self): '''Copy a layer containing a cube.''' @@ -145,8 +171,8 @@ def testCopyLayerPrimsSimple(self): # Verify the results. self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) - def testCopyLayerPrimsWithRelation(self): - '''Copy a layer containing a cube. May or may not follow the relation to the sphere.''' + def _runTestCopyLayerPrimsWithTarget(self, followTargets, asConnections): + '''Copy a layer containing a cube. May or may not follow the target to the sphere.''' # Prims to be created. Might not necessarily all be copied srcPrims = { @@ -155,13 +181,13 @@ def testCopyLayerPrimsWithRelation(self): "/related": "Sphere", } - # Map of source to target relationships. - relationships = { + # Map of source to target. + targets = { "/hello": "/related", } # Map of destination prim path indexed by the corresponding source path. - # One map when relationships are followed, one when not. + # One map when targets are followed, one when not. expectedDstForFollow = { True: { "/hello" : "/hello", @@ -173,7 +199,7 @@ def testCopyLayerPrimsWithRelation(self): } # Map of relationship targets indexed by the destination path that - # containing the targeting 'arrow' relationship. + # containing the targeting 'arrow' relationship or 'tunnel' connection. expectedTargetsForFollow = { True: { "/hello" : "/related", @@ -181,32 +207,47 @@ def testCopyLayerPrimsWithRelation(self): False: {}, } - # Create the source stage, layer and prims. - srcStage, srcLayer = self._createStageLayerAndPrims(srcPrims) - self._createRelationships(srcStage, relationships) - toCopy = [Sdf.Path("/hello")] srcParentPath = Sdf.Path("/") dstParentPath = Sdf.Path("/") - for followRelationships in [True, False]: - # Create the destination stage and layer. - dstStage, dstLayer = self._createStageAndLayer() + # Create the source stage, layer and prims. + srcStage, srcLayer = self._createStageLayerAndPrims(srcPrims) + self._createTargets(srcStage, targets, asConnections=asConnections) - # Copy the desired prims for the test. - copiedPrims = mayaUsd.lib.copyLayerPrims(srcStage, srcLayer, srcParentPath, - dstStage, dstLayer, dstParentPath, - toCopy, followRelationships) - - # Verify the results. - expectedDstPrims = expectedDstForFollow[followRelationships] - self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) + # Create the destination stage and layer. + dstStage, dstLayer = self._createStageAndLayer() - expectedDstTargets = expectedTargetsForFollow[followRelationships] - self._verifyDestinationRelTargets(dstStage, expectedDstTargets) + # Copy the desired prims for the test. + copiedPrims = mayaUsd.lib.copyLayerPrims(srcStage, srcLayer, srcParentPath, + dstStage, dstLayer, dstParentPath, + toCopy, followTargets) + + # Verify the results. + expectedDstPrims = expectedDstForFollow[followTargets] + self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) + + expectedDstTargets = expectedTargetsForFollow[followTargets] + self._verifyDestinationTargets(dstStage, expectedDstTargets, asConnections=asConnections) + + def testCopyLayerPrimsWithFollowedRelation(self): + '''Copy a layer containing a cube, following the relationship to the sphere.''' + self._runTestCopyLayerPrimsWithTarget(followTargets=True, asConnections=False) + + def testCopyLayerPrimsWithIgnoredRelation(self): + '''Copy a layer containing a cube, ignoring the relationship to the sphere.''' + self._runTestCopyLayerPrimsWithTarget(followTargets=False, asConnections=False) + + def testCopyLayerPrimsWithFollowedConnection(self): + '''Copy a layer containing a cube, following the conneection to the sphere.''' + self._runTestCopyLayerPrimsWithTarget(followTargets=True, asConnections=True) + + def testCopyLayerPrimsWithIgnoredConnection(self): + '''Copy a layer containing a cube, ignoring the conneection to the sphere.''' + self._runTestCopyLayerPrimsWithTarget(followTargets=False, asConnections=True) def testCopyLayerPrimsNested(self): - '''Copy a layer containing a cube and a nexted cone.''' + '''Copy a layer containing a cube and a nested cone.''' # Prims to be created. Might not necessarily all be copied srcPrims = { @@ -240,7 +281,7 @@ def testCopyLayerPrimsNested(self): self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) def testCopyLayerPrimsNestedLower(self): - '''Copy a layer containing a cube and a nexted cone under a non-root destination prim.''' + '''Copy a layer containing a cube and a nested cone under a non-root destination prim.''' # Prims to be created. Might not necessarily all be copied srcPrims = { @@ -274,10 +315,10 @@ def testCopyLayerPrimsNestedLower(self): # Verify the results. self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) - def testCopyLayerPrimsNestedToLowerWithRelation(self): + def _runTestCopyLayerPrimsNestedToLowerWithTarget(self, followTargets, asConnections): ''' - Copy a layer containing a cube and a nexted cone under a non-root destination prim. - There is also a relationship that may or may not be followed depending on options. + Copy a layer containing a cube and a nested cone under a non-root destination prim. + There is also a target that may or may not be followed depending on options. ''' # Prims to be created. Might not necessarily all be copied @@ -288,13 +329,13 @@ def testCopyLayerPrimsNestedToLowerWithRelation(self): "/related": "Sphere", } - # Map of source to target relationships. - relationships = { + # Map of source to target. + targets = { "/hello": "/related", } # Map of destination prim path indexed by the corresponding source path. - # One map when relationships are followed, one when not. + # One map when targets are followed, one when not. expectedDstForFollow = { True: { "/hello" : "/there/hello", @@ -308,7 +349,7 @@ def testCopyLayerPrimsNestedToLowerWithRelation(self): } # Map of relationship targets indexed by the destination path that - # containing the targeting 'arrow' relationship. + # containing the targeting 'arrow' relationship or 'tunnel' connection. expectedTargetsForFollow = { True: { "/there/hello" : "/related", @@ -318,32 +359,59 @@ def testCopyLayerPrimsNestedToLowerWithRelation(self): # Create the source stage, layer and prims. srcStage, srcLayer = self._createStageLayerAndPrims(srcPrims) - self._createRelationships(srcStage, relationships) + self._createTargets(srcStage, targets, asConnections=asConnections) toCopy = [Sdf.Path("/hello")] srcParentPath = Sdf.Path("/") dstParentPath = Sdf.Path("/there") - for followRelationships in [True, False]: - # Create the destination stage, layer and destination prim. - dstStage, dstLayer = self._createStageAndLayer() - dstStage.DefinePrim("/there") + # Create the destination stage, layer and destination prim. + dstStage, dstLayer = self._createStageAndLayer() + dstStage.DefinePrim("/there") - # Copy the desired prims for the test. - copiedPrims = mayaUsd.lib.copyLayerPrims(srcStage, srcLayer, srcParentPath, - dstStage, dstLayer, dstParentPath, - toCopy, followRelationships) - - # Verify the results. - expectedDstPrims = expectedDstForFollow[followRelationships] - self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) - - expectedDstTargets = expectedTargetsForFollow[followRelationships] - self._verifyDestinationRelTargets(dstStage, expectedDstTargets) + # Copy the desired prims for the test. + copiedPrims = mayaUsd.lib.copyLayerPrims(srcStage, srcLayer, srcParentPath, + dstStage, dstLayer, dstParentPath, + toCopy, followTargets) + + # Verify the results. + expectedDstPrims = expectedDstForFollow[followTargets] + self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) + + expectedDstTargets = expectedTargetsForFollow[followTargets] + self._verifyDestinationTargets(dstStage, expectedDstTargets, asConnections=asConnections) - def testCopyLayerPrimsChainedRenaming(self): + def testCopyLayerPrimsNestedToLowerWithRelation(self): + ''' + Copy a layer containing a cube and a nested cone under a non-root destination prim. + There is also a relationship that is followed. + ''' + self._runTestCopyLayerPrimsNestedToLowerWithTarget(followTargets=True, asConnections=False) + + def testCopyLayerPrimsNestedToLowerIgnoreRelation(self): + ''' + Copy a layer containing a cube and a nested cone under a non-root destination prim. + There is also a relationship that ignored. + ''' + self._runTestCopyLayerPrimsNestedToLowerWithTarget(followTargets=False, asConnections=False) + + def testCopyLayerPrimsNestedToLowerWithConnection(self): ''' - Copy a layer containing three nested prims, each with a reltion to a prim with + Copy a layer containing a cube and a nested cone under a non-root destination prim. + There is also a connection that is followed. + ''' + self._runTestCopyLayerPrimsNestedToLowerWithTarget(followTargets=True, asConnections=True) + + def testCopyLayerPrimsNestedToLowerIgnoreConnection(self): + ''' + Copy a layer containing a cube and a nested cone under a non-root destination prim. + There is also a connection that ignored. + ''' + self._runTestCopyLayerPrimsNestedToLowerWithTarget(followTargets=False, asConnections=True) + + def _runTestCopyLayerPrimsChainedRenaming(self, asConnections): + ''' + Copy a layer containing three nested prims, each with a relation to a prim with a name ending with an increasing digit, which will collide in the destination. This will create a chain of renaming: @@ -351,10 +419,10 @@ def testCopyLayerPrimsChainedRenaming(self): r2 will be renamed to r3 r3 will be renamed to r4 - When handling the renamed relationship target, the test verifies that the target - only follow one level of renaming. Meaning the correct result we expect is that - r1 is now r2, r2 -> r3 and r4 -> r4. If the code were wrong and applied all renaming - mappings to all relationships, all targets would incorrectly end-up pointing to r4. + When handling the renamed targets, the test verifies that the target only follow one + level of renaming. Meaning the correct result we expect is that r1 is now r2, r2 -> r3 + and r4 -> r4. If the code were wrong and applied all renaming mappings to all targets, + all targets would incorrectly end-up pointing to r4. ''' # Prims to be created. Might not necessarily all be copied @@ -368,12 +436,12 @@ def testCopyLayerPrimsChainedRenaming(self): "/r3": "Sphere", } - # Map of source to target relationships. + # Map of source to targets. # - # Note: due to the order of traversal, the relationships of the lowest + # Note: due to the order of traversal, the targets of the lowest # prims are copied first, so to create the chain of renaming, we # need to have r1 be in the lowest prim, not the highest. - relationships = { + targets = { "/a": "/r3", "/a/b": "/r2", "/a/b/c": "/r1", @@ -390,7 +458,7 @@ def testCopyLayerPrimsChainedRenaming(self): } # Map of relationship targets indexed by the destination path that - # containing the targeting 'arrow' relationship. + # containing the targeting 'arrow' relationship or 'tunnel' connection. expectedDstTargets = { "/a" : "/r4", "/a/b" : "/r3", @@ -399,7 +467,7 @@ def testCopyLayerPrimsChainedRenaming(self): # Create the source stage, layer and prims. srcStage, srcLayer = self._createStageLayerAndPrims(srcPrims) - self._createRelationships(srcStage, relationships) + self._createTargets(srcStage, targets, asConnections=asConnections) toCopy = [Sdf.Path("/a")] srcParentPath = Sdf.Path("/") @@ -418,7 +486,173 @@ def testCopyLayerPrimsChainedRenaming(self): # Verify the results. self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) - self._verifyDestinationRelTargets(dstStage, expectedDstTargets) + self._verifyDestinationTargets(dstStage, expectedDstTargets, asConnections=asConnections) + + def testCopyLayerPrimsChainedRelationRenaming(self): + ''' + Copy a layer containing three nested prims, each with a relation to a prim with + a name ending with an increasing digit, which will collide in the destination. + ''' + self._runTestCopyLayerPrimsChainedRenaming(asConnections=False) + + def testCopyLayerPrimsChainedConnectionRenaming(self): + ''' + Copy a layer containing three nested prims, each with a connection to a prim with + a name ending with an increasing digit, which will collide in the destination. + ''' + self._runTestCopyLayerPrimsChainedRenaming(asConnections=True) + + def _runTestCopyLayerPrimsChainedTargets(self, asConnections): + ''' + Copy a layer containing a prim with a target to a prim with another target. + Verify that all related prims in the chain get copied. + + Also, the targets form a circle, verify this does not cause infinite + recursion. + ''' + + # Prims to be created. Might not necessarily all be copied + srcPrims = { + "/hello": "Cube", + "/ignored": "Capsule", + "/r1": "Cone", + "/r2": "Sphere", + } + + # Map of source to targets. + targets = { + "/hello": "/r1", + "/r1": "/r2", + "/r2": "/r1", + } + + # Map of destination prim path indexed by the corresponding source path. + expectedDstPrims = { + "/hello" : "/hello", + "/r1": "/r1", + "/r2": "/r2", + } + + # Map of relationship targets indexed by the destination path that + # containing the targeting 'arrow' relationship or 'tunnel' connection. + expectedDstTargets = { + "/hello" : "/r1", + "/r1": "/r2", + "/r2": "/r1", + } + + # Create the source stage, layer and prims. + srcStage, srcLayer = self._createStageLayerAndPrims(srcPrims) + self._createTargets(srcStage, targets, asConnections=asConnections) + + toCopy = [Sdf.Path("/hello")] + srcParentPath = Sdf.Path("/") + dstParentPath = Sdf.Path("/") + followRelationships = True + + # Create the destination stage, layer and a prim that will collide + # with a relationship target, which will trigger the chain of renaming. + dstStage, dstLayer = self._createStageAndLayer() + + # Copy the desired prims for the test. + copiedPrims = mayaUsd.lib.copyLayerPrims(srcStage, srcLayer, srcParentPath, + dstStage, dstLayer, dstParentPath, + toCopy, followRelationships) + + # Verify the results. + self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) + self._verifyDestinationTargets(dstStage, expectedDstTargets, asConnections=asConnections) + + def testCopyLayerPrimsChainedRelations(self): + ''' + Copy a layer containing with a relation to a prim with another relation. + Verify that all related prims in the chain get copied. + + Also, the relations form a circle, verify this does not cause infinite + recursion. + ''' + self._runTestCopyLayerPrimsChainedTargets(asConnections=False) + + def testCopyLayerPrimsChainedConnections(self): + ''' + Copy a layer containing with a connection to a prim with another connection. + Verify that all related prims in the chain get copied. + + Also, the connections form a circle, verify this does not cause infinite + recursion. + ''' + self._runTestCopyLayerPrimsChainedTargets(asConnections=True) + + def testCopyLayerPrimsWithMixedRelationAndConnection(self): + ''' + Copy a layer containing a prim with both relationship and connection. + + Also, the targets form a circle, verify this does not cause infinite + recursion. + ''' + + # Prims to be created. Might not necessarily all be copied + srcPrims = { + "/hello": "Cube", + "/ignored": "Capsule", + "/r1": "Cone", + "/r2": "Sphere", + } + + # Map of source to targets. + relationships = { + "/hello": "/r1", + "/r1": "/r2", + } + + # Map of source to targets. + connections = { + "/r2": "/r1", + } + + # Map of destination prim path indexed by the corresponding source path. + expectedDstPrims = { + "/hello" : "/hello", + "/r1": "/r1", + "/r2": "/r2", + } + + # Map of relationship targets indexed by the destination path that + # containing the targeting 'arrow' relationship. + expectedDstRelations = { + "/hello" : "/r1", + "/r1": "/r2", + } + + # Map of relationship targets indexed by the destination path that + # containing the targeting 'tunnel' connection. + expectedDstConnections = { + "/r2": "/r1", + } + + # Create the source stage, layer and prims. + srcStage, srcLayer = self._createStageLayerAndPrims(srcPrims) + self._createRelationships(srcStage, relationships) + self._createConnections(srcStage, connections) + + toCopy = [Sdf.Path("/hello")] + srcParentPath = Sdf.Path("/") + dstParentPath = Sdf.Path("/") + followRelationships = True + + # Create the destination stage, layer and a prim that will collide + # with a relationship target, which will trigger the chain of renaming. + dstStage, dstLayer = self._createStageAndLayer() + + # Copy the desired prims for the test. + copiedPrims = mayaUsd.lib.copyLayerPrims(srcStage, srcLayer, srcParentPath, + dstStage, dstLayer, dstParentPath, + toCopy, followRelationships) + + # Verify the results. + self._verifyDestinationPrims(dstStage, copiedPrims, srcPrims, expectedDstPrims) + self._verifyDestinationRelationships(dstStage, expectedDstRelations) + self._verifyDestinationConnections(dstStage, expectedDstConnections) if __name__ == '__main__':