pcp: Per-node tracking of restricted opinion depth
authorsunyab <sunyab@users.noreply.github.com>
Sun, 10 Dec 2023 18:51:34 +0000 (10:51 -0800)
committerpixar-oss <pixar-oss@users.noreply.github.com>
Sun, 10 Dec 2023 18:51:34 +0000 (10:51 -0800)
Pcp now tracks the namespace depth at which a node was
restricted from contributing opinions to the composed
prim. This is needed by an upcoming fix to composition
that examines ancestral variant opinions from various
nodes in a prim index. The "contribution restriction
depth" will allow us to determine the namespace location
at which we can/must stop looking for variants to
compose into the prim index.

This change adds a new unit test (testPcpPrimIndex.py)
to exercise this functionality. This test uses helper
code that was refactored out of testPcpExpressionComposition.py
and into Pcp's own Python module as Pcp._TestPrimIndex.
This follows the same pattern as Pcp._TestChangeProcessor,
which is another test-only helper in the Python module.

(Internal change: 2308019)

pxr/usd/pcp/CMakeLists.txt
pxr/usd/pcp/__init__.py
pxr/usd/pcp/diagnostic.cpp
pxr/usd/pcp/node.cpp
pxr/usd/pcp/node.h
pxr/usd/pcp/primIndex.cpp
pxr/usd/pcp/primIndex_Graph.h
pxr/usd/pcp/testenv/testPcpExpressionComposition.py
pxr/usd/pcp/testenv/testPcpMuseum_ImpliedAndAncestralInherits.testenv/baseline/compositionResults_ImpliedAndAncestralInherits_graph.txt
pxr/usd/pcp/testenv/testPcpPrimIndex.py [new file with mode: 0644]
pxr/usd/pcp/wrapNode.cpp

index 75924048f69d06dca58f3254e3fe176ee5bc925c..a671f9d19bdacbb6143678850a6f492a1840cd09 100644 (file)
@@ -104,6 +104,7 @@ pxr_test_scripts(
     testenv/testPcpLayerMuting.py
     testenv/testPcpDependencies.py
     testenv/testPcpPathTranslation.py
+    testenv/testPcpPrimIndex.py
     testenv/testPcpDynamicFileFormatPlugin.py
     testenv/testPcpMapFunction.py
     testenv/testPcpOwner.py
@@ -1932,6 +1933,12 @@ pxr_register_test(testPcpPathTranslation
     EXPECTED_RETURN_CODE 0
 )
 
+pxr_register_test(testPcpPrimIndex
+    PYTHON
+    COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testPcpPrimIndex"
+    EXPECTED_RETURN_CODE 0
+)
+
 pxr_register_test(testPcpDynamicFileFormatPlugin
     PYTHON
     COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testPcpDynamicFileFormatPlugin"
index 21792ee990a2f53b8a55b10961ae0e8608525097..d294c9e974e738897a341b2e7b3d7949d2d59b0c 100644 (file)
 from pxr import Tf
 Tf.PreparePythonModule()
 del Tf
+
+# Utilities for unit testing
+
+def _TestPrimIndex(primIndex, expected):
+    '''Generator for verifying the expected structure and
+    values throughout the given prim index.
+
+    The "expected" parameter is a list that mirrors the tree
+    structure of the prim index:
+
+    expected  : [nodeEntry]
+    nodeEntry : (tuple of arbitrary data), [nodeEntry, ...]
+
+    The first item in a nodeEntry is a tuple of arbitrary data
+    associated with a node in the prim index. The second item is
+    a list of nodeEntries corresponding to the children of that
+    node.
+
+    This generator will visit each node in the prim index and
+    yield the node and the associated data tuple.
+
+    For example:
+
+    expected = [
+        (Pcp.ArcTypeRoot, "/Root"), [
+            (Pcp.ArcTypeReference, "/Ref1"), [],
+            (Pcp.ArcTypeReference, "/Ref2"), []
+        ]
+    ]
+          
+    for node, entry in Pcp._TestPrimIndex(primIndex, expected):
+        assert node.arcType == entry.arcType
+        assert node.path == entry.path
+
+    '''
+    def _recurse(node, expected):
+        try:
+            yield node, expected[0]
+        except IndexError as e:
+            raise RuntimeError(
+                "No entry in expected corresponding to node {}"
+                .format(node.site)) from e
+
+        for idx, n in enumerate(node.children):
+            try:
+                expectedSubtree = expected[1][(idx*2):(idx*2)+2]
+            except IndexError as e:
+                raise RuntimeError(
+                    "No entry in expected corresponding to node {}"
+                    .format(n.site)) from e
+            yield from _recurse(n, expectedSubtree)
+
+    yield from _recurse(primIndex.rootNode, expected)
index 5b674c89046f07bc2faa0c3149e417afc40c3f47..ae297b8bed49bab92efdb4a9a3af812cb57e6ba0 100644 (file)
@@ -132,6 +132,8 @@ std::string Pcp_Dump(
         _GetString(node.IsInert()));
     s += TfStringPrintf("    Contribute specs:         %s\n",
         _GetString(node.CanContributeSpecs()));
+    s += TfStringPrintf("        Restricted at depth:  %zu\n",
+        node.GetSpecContributionRestrictedDepth());
     s += TfStringPrintf("    Has specs:                %s\n",
         _GetString(node.HasSpecs()));
     s += TfStringPrintf("    Has symmetry:             %s\n",
index 0cf926db702c2d61d7ce35e25f8034a44677b7ea..2bdbd75317bbc7d0ab3803bcee1506fcf4394709 100644 (file)
@@ -123,9 +123,9 @@ PCP_DEFINE_GET_API(const PcpMapExpression&, GetMapToRoot, mapToRoot);
 
 PCP_DEFINE_API(bool, HasSymmetry, SetHasSymmetry, smallInts.hasSymmetry);
 PCP_DEFINE_API(SdfPermission, GetPermission, SetPermission, smallInts.permission);
-PCP_DEFINE_API(bool, IsRestricted, SetRestricted, smallInts.permissionDenied);
+PCP_DEFINE_API(bool, IsRestricted, _SetRestricted, smallInts.permissionDenied);
 
-PCP_DEFINE_SET_API(bool, SetInert, smallInts.inert);
+PCP_DEFINE_SET_API(bool, _SetInert, smallInts.inert);
 
 PCP_DEFINE_GET_NODE_API(size_t, _GetParentIndex, indexes.arcParentIndex);
 PCP_DEFINE_GET_NODE_API(size_t, _GetOriginIndex, indexes.arcOriginIndex);
@@ -141,14 +141,87 @@ PcpNodeRef::IsCulled() const
 void
 PcpNodeRef::SetCulled(bool culled)
 {
-    // Have to set finalized to false if we cull anything.
     TF_DEV_AXIOM(_nodeIdx < _graph->_unshared.size());
-    if (culled && !_graph->_unshared[_nodeIdx].culled) {
+    
+    const bool wasCulled = _graph->_unshared[_nodeIdx].culled;
+    if (culled == wasCulled) {
+        return;
+    }
+
+    // Have to set finalized to false if we cull anything.
+    if (culled) {
         _graph->_finalized = false;
     }
+
+    // If we've culled this node, we've definitely restricted contributions.
+    // If we've unculled this node, some other flags may be restriction
+    // contributions, so we don't know.
+    _RecordRestrictionDepth(
+        culled ? _Restricted::Yes : _Restricted::Unknown);
+
     _graph->_unshared[_nodeIdx].culled = culled;
 }
 
+void
+PcpNodeRef::SetRestricted(bool restricted)
+{
+    const bool wasRestricted = IsRestricted();
+    _SetRestricted(restricted);
+    if (restricted != wasRestricted) {
+        // If we set this node to restricted, we've definitely restricted
+        // contributions. If we've unset restricted, some other flags
+        // may be restricting contributions, so we don't know.
+        _RecordRestrictionDepth(
+            restricted ? _Restricted::Yes : _Restricted::Unknown);
+    }
+}
+
+void
+PcpNodeRef::SetInert(bool inert)
+{
+    const bool wasInert = IsInert();
+    _SetInert(inert);
+    if (inert != wasInert) {
+        // If we set this node to inert, we've definitely restricted
+        // contributions. If we've unset inert-ness, some other flags
+        // may be restricting contributions, so we don't know.
+        _RecordRestrictionDepth(
+            inert ? _Restricted::Yes : _Restricted::Unknown);
+    }
+}
+
+void
+PcpNodeRef::_RecordRestrictionDepth(_Restricted isRestricted)
+{
+    // Determine if contributions have been restricted so we can
+    // figure out what to record for the restriction depth. We
+    // can avoid doing this extra check if the caller knows they
+    // restricted contributions.
+    const bool contributionRestricted = 
+        isRestricted == _Restricted::Yes || !CanContributeSpecs();
+
+    auto& currDepth = _graph->_unshared[_nodeIdx].restrictionDepth;
+
+    if (!contributionRestricted) {
+        currDepth = 0;
+    }
+    else {
+        size_t newDepth = GetPath().GetPathElementCount();
+
+        // XXX:
+        // This should result in a "capacity exceeded" composition error
+        // instead of just a warning.
+        if (auto maxDepth =
+            std::numeric_limits<std::decay_t<decltype(currDepth)>>::max();
+            newDepth > maxDepth) {
+            TF_WARN("Maximum restriction namespace depth exceeded");
+            newDepth = maxDepth;
+        }
+
+        currDepth = newDepth;
+    }
+}
+
 bool
 PcpNodeRef::IsDueToAncestor() const
 {
@@ -220,6 +293,18 @@ PcpNodeRef::CanContributeSpecs() const
         (!node.smallInts.permissionDenied || _graph->IsUsd());
 }
 
+size_t
+PcpNodeRef::GetSpecContributionRestrictedDepth() const
+{
+    return _graph->_unshared[_nodeIdx].restrictionDepth;
+}
+
+void
+PcpNodeRef::SetSpecContributionRestrictedDepth(size_t depth)
+{
+    _graph->_unshared[_nodeIdx].restrictionDepth = depth;
+}
+
 int
 PcpNodeRef::GetDepthBelowIntroduction() const
 {
index 30159e72279d52676772481b3b48a3958d9b9603..f6b2f7bdd66f30f4eefe96498e417d2c34d4ba9e 100644 (file)
@@ -290,6 +290,24 @@ public:
     /// for composition, false otherwise.
     PCP_API
     bool CanContributeSpecs() const;
+
+    /// Returns the namespace depth (i.e., the path element count) of
+    /// this node's path when it was restricted from contributing
+    /// opinions for composition. If this spec has no such restriction,
+    /// returns 0. 
+    ///
+    /// Note that unlike the value returned by GetNamespaceDepth,
+    /// this value *does* include variant selections.
+    PCP_API
+    size_t GetSpecContributionRestrictedDepth() const;
+
+    /// Set this node's contribution restriction depth.
+    ///
+    /// Note that this function typically does not need to be called,
+    /// since functions that restrict contributions (e.g., SetInert)
+    /// automatically set the restriction depth appropriately.
+    PCP_API
+    void SetSpecContributionRestrictedDepth(size_t depth);
     
     /// Returns true if this node has opinions authored
     /// for composition, false otherwise.
@@ -324,6 +342,12 @@ private:
     inline size_t _GetParentIndex() const;
     inline size_t _GetOriginIndex() const;
 
+    inline void _SetInert(bool inert);
+    inline void _SetRestricted(bool restricted);
+
+    enum class _Restricted { Yes, Unknown };
+    void _RecordRestrictionDepth(_Restricted isRestricted);
+
 private: // Data
     PcpPrimIndex_Graph* _graph;
     size_t _nodeIdx;
index ac573b93421ad5476ca9e09362279ad80bf8afcf..7451bc433abd7d9aa84e8cdbf3ba0ab0b8ac2ef9 100644 (file)
@@ -3431,12 +3431,42 @@ _PropagateNodeToParent(
         }
 
         if (newNode) {
+            const size_t newNodeRestrictedDepth =
+                newNode.GetSpecContributionRestrictedDepth();
+
             newNode.SetInert(srcNode.IsInert());
             newNode.SetHasSymmetry(srcNode.HasSymmetry());
             newNode.SetPermission(srcNode.GetPermission());
             newNode.SetRestricted(srcNode.IsRestricted());
 
+            // If we're propagating nodes to the origin, newNode may be a
+            // previously-existing node that was created during an ancestral
+            // round of implied specializes propagation. If that's the case,
+            // its restriction depth will be non-zero because it was marked
+            // inert at that time. However, the above calls may have now
+            // made that node not inert, resetting its restriction depth
+            // back to 0. When we propagate this node back to the root, we
+            // want to restore its restriction depth back to its original 
+            // value.
+            //
+            // To do this, we just record the original depth in srcNode.
+            // When we propagate this node to the origin, this saves the
+            // value away so it can be restored when we propagate the
+            // node back to the root.
+            //
+            // This is tested in the /Root/Child/Child test case of
+            // test_ContributionRestrictedDepth_Specializes in
+            // testPcpPrimIndex.py.
+            //
+            // XXX: 
+            // This is way too complicated but I think the only way to
+            // avoid this is to rethink the whole node propagation scheme
+            // for specializes.
             srcNode.SetInert(true);
+            if (newNodeRestrictedDepth != 0) {
+                srcNode.SetSpecContributionRestrictedDepth(
+                    newNodeRestrictedDepth);
+            }
         }
         else {
             _InertSubtree(srcNode);
@@ -3516,11 +3546,26 @@ _FindSpecializesToPropagateToRoot(
         // up the implied specializes in _PropagateArcsToOrigin,
         // it's much simpler if we just deal with that here by forcing
         // the specializes node to inert=false.
-        node.SetInert(false);
+        //
+        // The subsequent call to _PropagateSpecializesTreeToRoot will
+        // set this node back to inert=true, which will update its
+        // restriction depth. However, if this node was originally
+        // inert, we want to keep its original restriction depth.
+        // This is tested in the /Root/Child test case of
+        // test_ContributionRestrictedDepth_Specializes in testPcpPrimIndex.py.
+        const bool wasInert = node.IsInert();
+        const size_t oldDepth = node.GetSpecContributionRestrictedDepth();
+        if (wasInert) {
+            node.SetInert(false);
+        }
 
         _PropagateSpecializesTreeToRoot(
             node.GetRootNode(), node, node,
             node.GetMapToRoot(), node, indexer);
+
+        if (wasInert) {
+            node.SetSpecContributionRestrictedDepth(oldDepth);
+        }
     }
 
     for (PcpNodeRef childNode : Pcp_GetChildren(node)) {
index ef759d46cfc57b368b0c3c437f7810bb2536ebcc..3ff00b3e35d0892032ff88ba33db9cac12331bec 100644 (file)
@@ -391,12 +391,10 @@ private:
         _UnsharedData()
             : hasSpecs(false), culled(false), isDueToAncestor(false) {}
         explicit _UnsharedData(SdfPath const &p)
-            : sitePath(p)
-            , hasSpecs(false)
-            , culled(false)
-            , isDueToAncestor(false) {}
+            : _UnsharedData(SdfPath(p)) {}
         explicit _UnsharedData(SdfPath &&p)
             : sitePath(std::move(p))
+            , restrictionDepth(0)
             , hasSpecs(false)
             , culled(false)
             , isDueToAncestor(false) {}
@@ -404,6 +402,10 @@ private:
         // The site path for a particular node.
         SdfPath sitePath;
 
+        // Absolute depth in namespace of this node at which it was
+        // restricted from contributing opinions.
+        uint16_t restrictionDepth;
+
         // Whether or not a particular node has any specs to contribute to the
         // composed prim.
         bool hasSpecs:1;
index b7699998e8234e07f64f8a7621f6ca1582fe02e4..e4e8f5adcff1fad31d65e4e16ff393e134e3c173 100644 (file)
@@ -38,43 +38,31 @@ class TestPcpExpressionComposition(unittest.TestCase):
     def AssertVariables(self, pcpCache, path, expected, errorsExpected = False):
         '''Helper function for verifying the expected value of 
         expression variables in the layer stacks throughout the prim index
-        for the prim at the given path.
-
-        The "expected" parameter is a list that mirrors the tree
-        structure of the prim index:
-
-        expected  : [nodeEntry]
-        nodeEntry : (root layer id, expressionVars), [nodeEntry, ...]
-
-        The first entry in "expected" corresponds to the expected expression
-        variables dictionary in the root node, followed by a list of
-        entries corresponding to the children of the root node, etc.
+        for the prim at the given path. See Pcp._TestPrimIndex for more info.
 
         If "errorsExpected" is False, then this function will return false if
         any composition errors are generated when computing the prim index.
         '''
-        def _recurse(node, expected):
+        pi, err = pcpCache.ComputePrimIndex(path)
+        if errorsExpected:
+            self.assertTrue(err, "Composition errors expected")
+        else:
+            self.assertFalse(err, "Unexpected composition errors: {}".format(
+                ",".join(str(e) for e in err)))
+
+        for node, entry in Pcp._TestPrimIndex(pi, expected):
+            expectedRootLayerId, expectedVariables = entry
+
             self.assertEqual(
                 node.layerStack.identifier.rootLayer,
-                Sdf.Layer.Find(expected[0][0]))
+                Sdf.Layer.Find(expectedRootLayerId))
 
             self.assertEqual(
                 node.layerStack.expressionVariables.GetVariables(),
-                expected[0][1],
+                expectedVariables,
                 "Unexpected expression variables for layer stack {}"
                 .format(node.layerStack.identifier))
 
-            for idx, n in enumerate(node.children):
-                _recurse(n, expected[1][(idx*2):(idx*2)+2])
-
-        pi, err = pcpCache.ComputePrimIndex(path)
-        if errorsExpected:
-            self.assertTrue(err, "Composition errors expected")
-        else:
-            self.assertFalse(err, "Unexpected composition errors: {}".format(
-                ",".join(str(e) for e in err)))
-        _recurse(pi.rootNode, expected)
-
         return pi
 
     def test_BasicSublayers(self):
index 394bd11e02c68c2cd5e503b29d8aead1b85967f0..3e760854af7d313808af50409af83a903cf5ebda 100644 (file)
@@ -24,6 +24,7 @@ Node 0:
     Is restricted:            FALSE
     Is inert:                 FALSE
     Contribute specs:         TRUE
+        Restricted at depth:  0
     Has specs:                FALSE
     Has symmetry:             FALSE
 Node 1:
@@ -44,6 +45,7 @@ Node 1:
     Is restricted:            FALSE
     Is inert:                 FALSE
     Contribute specs:         TRUE
+        Restricted at depth:  0
     Has specs:                FALSE
     Has symmetry:             FALSE
 Node 2:
@@ -64,6 +66,7 @@ Node 2:
     Is restricted:            FALSE
     Is inert:                 FALSE
     Contribute specs:         TRUE
+        Restricted at depth:  0
     Has specs:                FALSE
     Has symmetry:             FALSE
 Node 3:
@@ -86,6 +89,7 @@ Node 3:
     Is restricted:            FALSE
     Is inert:                 FALSE
     Contribute specs:         TRUE
+        Restricted at depth:  0
     Has specs:                TRUE
     Has symmetry:             FALSE
     Prim stack:
diff --git a/pxr/usd/pcp/testenv/testPcpPrimIndex.py b/pxr/usd/pcp/testenv/testPcpPrimIndex.py
new file mode 100644 (file)
index 0000000..f3541c3
--- /dev/null
@@ -0,0 +1,542 @@
+#!/pxrpythonsubst
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+#    names, trademarks, service marks, or product names of the Licensor
+#    and its affiliates, except as required to comply with Section 4(c) of
+#    the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+
+import unittest
+
+from pxr import Pcp, Sdf
+
+def LoadPcpCache(rootLayer):
+    return Pcp.Cache(Pcp.LayerStackIdentifier(rootLayer))
+
+class TestPcpPrimIndex(unittest.TestCase):
+    def AssertRestrictedDepth(self, pcpCache, path, expected):
+        pi, err = pcpCache.ComputePrimIndex(path)
+        self.assertFalse(err, "Unexpected composition errors: {}".format(
+            ",".join(str(e) for e in err)))
+
+        for node, e in Pcp._TestPrimIndex(pi, expected):
+            expectedArcType, expectedPath, expectedDepth = e
+
+            self.assertEqual(
+                node.arcType, expectedArcType,
+                "Error at {} {}".format(node.arcType, node.path))
+            self.assertEqual(
+                node.path, expectedPath,
+                "Error at {} {}".format(node.arcType, node.path))
+            self.assertEqual(
+                node.GetSpecContributionRestrictedDepth(), expectedDepth,
+                "Error at {} {}".format(node.arcType, node.path))
+
+    def test_TestPrimIndex(self):
+        """Test _TestPrimIndex generator"""
+        
+        layer = Sdf.Layer.CreateAnonymous()
+        layer.ImportFromString('''
+        #sdf 1.4.32
+
+        def "Class"
+        {
+        }
+
+        def "Ref" (
+            inherits = </Class>
+            variantSets = "x"
+            variants = {
+                string "x" = "a"
+            }
+        )
+        {
+            variantSet "x" = {
+                "a" {
+                }
+            }
+        }
+
+        def "Root"
+        {
+            def "Ref"  (
+                references = </Ref>
+            )
+            {
+            }
+        }
+        '''.strip())
+
+        pcp = LoadPcpCache(layer)
+
+        pi, err = pcp.ComputePrimIndex("/Root/Ref")
+        self.assertFalse(err)
+
+        # Collect all nodes from the prim index in strong-to-weak order.
+        nodes = []
+        def _CollectNodes(node):
+            nodes.append(node)
+            for child in node.children:
+                _CollectNodes(child)
+        _CollectNodes(pi.rootNode)
+
+        # Use the test generator to iterate over the prim index and
+        # verify that it matches the structure given by the "expected"
+        # variable.
+        testedNodes = []
+        for node, e in Pcp._TestPrimIndex(
+            pi, 
+            expected = [
+                (Pcp.ArcTypeRoot, "/Root/Ref"), [
+                    (Pcp.ArcTypeInherit, "/Class"), [
+                    ],
+                    (Pcp.ArcTypeReference, "/Ref"), [
+                        (Pcp.ArcTypeInherit, "/Class"), [
+                        ],
+                        (Pcp.ArcTypeVariant, "/Ref{x=a}"), [
+                        ]
+                    ],
+                ]
+            ]):
+            self.assertEqual((node.arcType, node.path), e)
+            testedNodes.append(node)
+
+        # Also verify that the generator actually visited every node
+        # in the prim index.
+        self.assertEqual(nodes, testedNodes)
+
+    def test_ContributionRestrictedDepth_Unrestricted(self):
+        """Verify contribution restriction depth when no
+        restrictions are in place."""
+
+        layer = Sdf.Layer.CreateAnonymous()
+        layer.ImportFromString('''
+        #sdf 1.4.32
+
+        def "Ref"
+        {
+        }
+
+        def "Root"
+        {
+            def "Ref"  (
+                references = </Ref>
+            )
+            {
+            }
+        }
+
+        '''.strip())
+
+        pcp = LoadPcpCache(layer)
+
+        # There are no restrictions on any prims so we expect the restriction
+        # depth on all nodes in all prim indexes to be 0.
+        self.AssertRestrictedDepth(
+            pcp, "/Root",
+            [
+                (Pcp.ArcTypeRoot, "/Root", 0), [
+                ]
+            ])
+
+        self.AssertRestrictedDepth(
+            pcp, "/Root/Ref",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Ref", 0), [
+                    (Pcp.ArcTypeReference, "/Ref", 0), [
+                    ]
+                ]
+            ])
+
+    def test_ContributionRestrictedDepth_Specializes(self):
+        """Verify contribution restriction depth with specializes and
+        namespace-nested composition arcs. This is tricky because of
+        the way we copy nodes around to achieve the desired strength
+        ordering."""
+
+        layer = Sdf.Layer.CreateAnonymous()
+        layer.ImportFromString('''
+        #sdf 1.4.32
+
+        def "Ref" (
+            specializes = </Specialize>
+        )
+        {
+        }
+
+        def "ChildRef"
+        {
+            def "Child2" (
+                references = </ChildRef2>
+            )
+            {
+            }
+        }
+
+        def "ChildRef2"
+        {
+        }
+
+        def "Specialize"
+        {
+            def "Child" (
+                references = </ChildRef>
+            )
+            {
+            }
+        }
+
+        def "Root" (
+            references = </Ref>
+        )
+        {
+        }
+
+        '''.strip())
+
+        pcpCache = LoadPcpCache(layer)
+
+        self.AssertRestrictedDepth(
+            pcpCache, "/Root",
+            [
+                (Pcp.ArcTypeRoot, "/Root", 0), [
+                    (Pcp.ArcTypeReference, "/Ref", 0), [
+                        (Pcp.ArcTypeSpecialize, "/Specialize", 1), [
+                        ]
+                    ],
+                    (Pcp.ArcTypeSpecialize, "/Specialize", 0), [
+                    ]
+                ]
+            ])
+
+        self.AssertRestrictedDepth(
+            pcpCache, "/Root/Child",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Child", 0), [
+                    (Pcp.ArcTypeReference, "/Ref/Child", 0), [
+                        (Pcp.ArcTypeSpecialize, "/Specialize/Child", 1), [
+                            (Pcp.ArcTypeReference, "/ChildRef", 1), [
+                            ]
+                        ]
+                    ],
+                    (Pcp.ArcTypeSpecialize, "/Specialize/Child", 0), [
+                        (Pcp.ArcTypeReference, "/ChildRef", 0), [
+                        ]
+                    ]
+                ]
+            ])
+
+        self.AssertRestrictedDepth(
+            pcpCache, "/Root/Child/Child2",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Child/Child2", 0), [
+                    (Pcp.ArcTypeReference, "/Ref/Child/Child2", 0), [
+                        (Pcp.ArcTypeSpecialize, "/Specialize/Child/Child2", 1), [
+                            (Pcp.ArcTypeReference, "/ChildRef/Child2", 1), [
+                                (Pcp.ArcTypeReference, "/ChildRef2", 1), [
+                                ]
+                            ]
+                        ]
+                    ],
+                    (Pcp.ArcTypeSpecialize, "/Specialize/Child/Child2", 0), [
+                        (Pcp.ArcTypeReference, "/ChildRef/Child2", 0), [
+                            (Pcp.ArcTypeReference, "/ChildRef2", 0), [
+                            ]
+                        ]
+                    ]
+                ]
+            ])
+
+    @unittest.skip("currently fails due to bug with specializes")
+    def test_ContributionRestrictedDepth_SpecializesAndPermissions(self):
+        """Verify contribution restriction depth with specializes and
+        permission restrictions."""
+        layer = Sdf.Layer.CreateAnonymous()
+        layer.ImportFromString('''
+        #sdf 1.4.32
+
+        def "Ref" (
+            specializes = </Specialize>
+        )
+        {
+        }
+
+        def "ChildRef"
+        {
+        }
+
+        def "Specialize"
+        {
+            def "Child" (
+                permission = private
+                references = </ChildRef>
+            )
+            {
+            }
+        }
+
+        def "Root" (
+            references = </Ref>
+        )
+        {
+        }
+
+        '''.strip())
+
+        pcpCache = LoadPcpCache(layer)
+
+        # Since /Specialize/Child is marked as private, all stronger nodes
+        # are restricted from contributing opinions. So, /Root/Child and
+        # /Ref/Child should have a restriction depth of 2.
+        self.AssertRestrictedDepth(
+            pcpCache, "/Root/Child",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Child", 2), [
+                    (Pcp.ArcTypeReference, "/Ref/Child", 2), [
+                        (Pcp.ArcTypeSpecialize, "/Specialize/Child", 0), [
+                            (Pcp.ArcTypeReference, "/ChildRef", 0), [
+                            ]
+                        ]
+                    ],
+                    (Pcp.ArcTypeSpecialize, "/Specialize/Child", 0), [
+                        (Pcp.ArcTypeReference, "/ChildRef", 0), [
+                        ]
+                    ]
+                ]
+            ])
+
+    def test_ContributionRestrictedDepth_SpecializesAndPermissions2(self):
+        """Verify contribution restriction depth with specializes and
+        permission restrictions."""
+        
+        layer = Sdf.Layer.CreateAnonymous()
+        layer.ImportFromString('''
+        #sdf 1.4.32
+
+        def "Ref" (
+            specializes = </Specialize>
+        )
+        {
+        }
+
+        def "ChildRef"
+        {
+            def "Child2" (
+                permission = private
+                references = </ChildRef2>
+            )
+            {
+            }
+        }
+
+        def "ChildRef2"
+        {
+        }
+
+        def "Specialize"
+        {
+            def "Child" (
+                references = </ChildRef>
+            )
+            {
+            }
+        }
+
+        def "Root" (
+            references = </Ref>
+        )
+        {
+        }
+
+        '''.strip())
+
+        pcpCache = LoadPcpCache(layer)
+
+        # Since /ChildRef/Child2 is marked as private, all stronger nodes
+        # are restricted from contributing opinions. So, /Root/Child/Child2,
+        # /Ref/Child/Child2 and /Specialize/Child/Child2 should all have a
+        # restriction depth of 3.
+        self.AssertRestrictedDepth(
+            pcpCache, "/Root/Child/Child2",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Child/Child2", 3), [
+                    (Pcp.ArcTypeReference, "/Ref/Child/Child2", 3), [
+                        (Pcp.ArcTypeSpecialize, "/Specialize/Child/Child2", 1), [
+                            (Pcp.ArcTypeReference, "/ChildRef/Child2", 1), [
+                                (Pcp.ArcTypeReference, "/ChildRef2", 1), [
+                                ]
+                            ]
+                        ]
+                    ],
+                    (Pcp.ArcTypeSpecialize, "/Specialize/Child/Child2", 3), [
+                        (Pcp.ArcTypeReference, "/ChildRef/Child2", 0), [
+                            (Pcp.ArcTypeReference, "/ChildRef2", 0), [
+                            ]
+                        ]
+                    ]
+                ]
+            ])
+
+    def test_ContributionRestrictedDepth_Relocates(self):
+        """Verify contribution restriction depth with relocates."""
+
+        layer = Sdf.Layer.CreateAnonymous()
+        layer.ImportFromString('''
+        #sdf 1.4.32
+
+        def "Ref"
+        {
+            def "Child"
+            {
+                def "A"
+                {
+                }
+            }
+        }
+
+        def "Root" (
+            references = </Ref>
+            relocates = {
+                <Child> : <Child_Relocated>
+            }
+        )
+        {
+        }
+        '''.strip())
+
+        pcpCache = LoadPcpCache(layer)
+
+        # Composition disallows opinions at the source of a relocation,
+        # so relocate nodes are always marked inert when introduced to
+        # restrict contributions. So, we expect the restriction depth
+        # for relocate nodes to be equal to the namespace depth when
+        # that node is introduced.
+        self.AssertRestrictedDepth(
+            pcpCache, "/Root/Child_Relocated",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Child_Relocated", 0), [
+                    (Pcp.ArcTypeRelocate, "/Root/Child", 2), [
+                        (Pcp.ArcTypeReference, "/Ref/Child", 0), [
+                        ]
+                    ]
+                ]
+            ])
+
+        self.AssertRestrictedDepth(
+            pcpCache, "/Root/Child_Relocated/A",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Child_Relocated/A", 0), [
+                    (Pcp.ArcTypeRelocate, "/Root/Child/A", 2), [
+                        (Pcp.ArcTypeReference, "/Ref/Child/A", 0), [
+                        ]
+                    ]
+                ]
+            ])
+
+    def test_ContributionRestrictedDepth_Permissions(self):
+        """Verify contribution restriction depth with private permissions."""
+
+        layer = Sdf.Layer.CreateAnonymous()
+        layer.ImportFromString('''
+        #sdf 1.4.32
+
+        def "Ref3"
+        {
+            def "Child" (
+                permission = private
+            )
+            {
+            }
+        }
+
+        def "Ref2"
+        {
+            def "Restricted" (
+                permission = private
+                references = </Ref3>
+            )
+            {
+            }
+        }
+
+        def "Ref"
+        {
+            def "Ref2" (
+                references = </Ref2>
+            )
+            {
+            }
+        }
+
+        def "Root"
+        {
+            def "Ref"  (
+                references = </Ref>
+            )
+            {
+            }
+        }
+
+        '''.strip())
+
+        pcp = LoadPcpCache(layer)
+
+        # /Root/Ref/Ref2/Restricted is marked private a few levels deep in
+        # the chain of references, on /Ref2/Restricted. The node for this
+        # reference and all weaker nodes should be marked as unrestricted.
+        #
+        # All stronger nodes should have a restriction depth equal to the
+        # number of path components, since this is the exact level at
+        # namespace where the restriction was introduced.
+        self.AssertRestrictedDepth(
+            pcp, "/Root/Ref/Ref2/Restricted",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Ref/Ref2/Restricted", 4), [
+                    (Pcp.ArcTypeReference, "/Ref/Ref2/Restricted", 3), [
+                        (Pcp.ArcTypeReference, "/Ref2/Restricted", 0), [
+                            (Pcp.ArcTypeReference, "/Ref3", 0), [
+                            ]
+                        ]
+                    ]
+                ]
+            ])
+
+        # /Root/Ref/Ref2/Restricted/Child is marked private one level
+        # deeper in the referencing chain, in /Ref3/Child. We expect
+        # the restriction depth on previously-restricted nodes to remain
+        # the same, since that's the depth where they were first restricted.
+        #
+        # /Ref2/Restricted/Child, which is a newly-restricted node, should
+        # now have a restriction depth equal to the number of components in
+        # its path.
+        self.AssertRestrictedDepth(
+            pcp, "/Root/Ref/Ref2/Restricted/Child",
+            [
+                (Pcp.ArcTypeRoot, "/Root/Ref/Ref2/Restricted/Child", 4), [
+                    (Pcp.ArcTypeReference, "/Ref/Ref2/Restricted/Child", 3), [
+                        (Pcp.ArcTypeReference, "/Ref2/Restricted/Child", 3), [
+                            (Pcp.ArcTypeReference, "/Ref3/Child", 0), [
+                            ]
+                        ]
+                    ]
+                ]
+            ])
+
+if __name__ == "__main__":
+    unittest.main()
index 6c0bf1cdcafc657ce7942ee619ea2d6bf1ae8859..6e9056b978dbd95a3f5390c67d18b99375ccd585 100644 (file)
@@ -107,6 +107,8 @@ wrapNode()
         .def("GetPathAtIntroduction", &This::GetPathAtIntroduction)
 
         .def("CanContributeSpecs", &This::CanContributeSpecs)
+        .def("GetSpecContributionRestrictedDepth",
+            &This::GetSpecContributionRestrictedDepth)
 
         .def(self == self)
         .def(self != self)