1#!/pxrpythonsubst
2#
3# Copyright 2020 Pixar
4#
5# Licensed under the Apache License, Version 2.0 (the "Apache License")
6# with the following modification; you may not use this file except in
7# compliance with the Apache License and the following modification to it:
8# Section 6. Trademarks. is deleted and replaced with:
9#
10# 6. Trademarks. This License does not grant permission to use the trade
11#    names, trademarks, service marks, or product names of the Licensor
12#    and its affiliates, except as required to comply with Section 4(c) of
13#    the License and to reproduce the content of the NOTICE file.
14#
15# You may obtain a copy of the Apache License at
16#
17#     http://www.apache.org/licenses/LICENSE-2.0
18#
19# Unless required by applicable law or agreed to in writing, software
20# distributed under the Apache License with the above modification is
21# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22# KIND, either express or implied. See the Apache License for the specific
23# language governing permissions and limitations under the Apache License.
24
25import os, unittest
26from pxr import Plug, Sdf, Usd, Vt, Tf
27
28# Helper for verifying change procesing related to fallback prim type metadata
29# changes. This is used to wrap a call that will change the layer metadata and
30# verifies that the ObjectsChanged notice is sent and whether the change caused
31# all prims to be resynced or not.
32class ChangeNoticeVerifier(object):
33    def __init__(self, testRunner, expectedResyncAll=False):
34        self.testRunner = testRunner
35        self.expectedResyncAll = expectedResyncAll
36        self.receivedNotice = False
37    def __enter__(self):
38        self._listener = Tf.Notice.RegisterGlobally(
39            'UsdNotice::ObjectsChanged', self._OnNotice)
40        return self
41    def __exit__(self, exc_type, exc_val, exc_tb):
42        self._listener.Revoke()
43        # Verify that we actually did receive a notice on exit, otherwise we'd
44        # end up with a false positive if no notice got sent
45        self.testRunner.assertTrue(self.receivedNotice)
46
47    def _OnNotice(self, notice, sender):
48        self.receivedNotice = True
49        # If we expected a resync all the root path will be in the notices
50        # resynced paths, otherwise it will be its changed info only paths.
51        if self.expectedResyncAll:
52            self.testRunner.assertEqual(notice.GetResyncedPaths(),
53                                        [Sdf.Path('/')])
54            self.testRunner.assertEqual(notice.GetChangedInfoOnlyPaths(), [])
55        else :
56            self.testRunner.assertEqual(notice.GetResyncedPaths(), [])
57            self.testRunner.assertEqual(notice.GetChangedInfoOnlyPaths(),
58                                        [Sdf.Path('/')])
59
60class TestUsdFallbackPrimTypes(unittest.TestCase):
61
62    @classmethod
63    def setUpClass(cls):
64        pr = Plug.Registry()
65        testPlugins = pr.RegisterPlugins(os.path.abspath("resources"))
66        assert len(testPlugins) == 1, \
67            "Failed to load expected test plugin"
68        assert testPlugins[0].name == "testUsdFallbackPrimTypes", \
69            "Failed to load expected test plugin"
70
71        cls.validType1 = Usd.SchemaRegistry.GetConcreteTypeFromSchemaTypeName(
72            "ValidType_1")
73        assert cls.validType1
74        cls.validType2 = Usd.SchemaRegistry.GetConcreteTypeFromSchemaTypeName(
75            "ValidType_2")
76        assert cls.validType2
77
78    def test_OpenLayerWithFallbackTypes(self):
79        stage = Usd.Stage.Open("WithFallback.usda")
80        self.assertTrue(stage)
81
82        def _VerifyFallbacksInStageMetdata(typeName, expectedFallbacksList):
83            fallbacks = stage.GetMetadataByDictKey("fallbackPrimTypes", typeName)
84            if expectedFallbacksList is None:
85                self.assertIsNone(fallbacks)
86            else:
87                self.assertEqual(fallbacks, Vt.TokenArray(expectedFallbacksList))
88
89        emptyPrimDef = Usd.SchemaRegistry().GetEmptyPrimDefinition()
90        validType1PrimDef = Usd.SchemaRegistry().FindConcretePrimDefinition(
91            "ValidType_1")
92        validType2PrimDef = Usd.SchemaRegistry().FindConcretePrimDefinition(
93            "ValidType_2")
94
95        def _VerifyPrimDefsSame(primDef1, primDef2):
96            self.assertEqual(primDef1.GetAppliedAPISchemas(),
97                             primDef2.GetAppliedAPISchemas())
98            self.assertEqual(primDef1.GetPropertyNames(),
99                             primDef2.GetPropertyNames())
100            for propName in primDef1.GetPropertyNames():
101                self.assertEqual(primDef1.GetSchemaPropertySpec(propName),
102                                 primDef2.GetSchemaPropertySpec(propName))
103            self.assertEqual(primDef1.ListMetadataFields(),
104                             primDef2.ListMetadataFields())
105            for fieldName in primDef1.ListMetadataFields():
106                self.assertEqual(primDef1.GetMetadata(fieldName),
107                                 primDef2.GetMetadata(fieldName))
108
109        # ValidPrim_1 : Has no fallbacks defined in metadata but its type name
110        # is a valid schema so it doesn't matter. This is the most typical case.
111        prim = stage.GetPrimAtPath("/ValidPrim_1")
112        _VerifyFallbacksInStageMetdata("ValidType_1", None)
113        self.assertTrue(prim)
114        self.assertEqual(prim.GetTypeName(), "ValidType_1")
115        primTypeInfo = prim.GetPrimTypeInfo()
116        self.assertEqual(primTypeInfo.GetTypeName(), "ValidType_1")
117        self.assertEqual(primTypeInfo.GetSchemaType(), self.validType1)
118        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "ValidType_1")
119        self.assertTrue(prim.IsA(self.validType1))
120        self.assertTrue(prim.IsA(Usd.Typed))
121        _VerifyPrimDefsSame(prim.GetPrimDefinition(), validType1PrimDef)
122
123        # ValidPrim_2 : Has fallbacks defined in metadata but its type name
124        # is a valid schema, so it ignores fallbacks.
125        prim = stage.GetPrimAtPath("/ValidPrim_2")
126        _VerifyFallbacksInStageMetdata("ValidType_2", ["ValidType_1"])
127        self.assertTrue(prim)
128        self.assertEqual(prim.GetTypeName(), "ValidType_2")
129        primTypeInfo = prim.GetPrimTypeInfo()
130        self.assertEqual(primTypeInfo.GetTypeName(), "ValidType_2")
131        self.assertEqual(primTypeInfo.GetSchemaType(), self.validType2)
132        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "ValidType_2")
133        self.assertTrue(prim.IsA(self.validType2))
134        self.assertTrue(prim.IsA(Usd.Typed))
135        _VerifyPrimDefsSame(prim.GetPrimDefinition(), validType2PrimDef)
136
137        # InvalidPrim_1 : Has no fallbacks defined in metadata and its type name
138        # is not a valid schema. This will have an invalid schema type.
139        prim = stage.GetPrimAtPath("/InvalidPrim_1")
140        _VerifyFallbacksInStageMetdata("InvalidType_1", None)
141        self.assertTrue(prim)
142        self.assertEqual(prim.GetTypeName(), "InvalidType_1")
143        primTypeInfo = prim.GetPrimTypeInfo()
144        self.assertEqual(primTypeInfo.GetTypeName(), "InvalidType_1")
145        self.assertEqual(primTypeInfo.GetSchemaType(), Tf.Type.Unknown)
146        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "")
147        self.assertFalse(prim.IsA(Usd.Typed))
148        _VerifyPrimDefsSame(prim.GetPrimDefinition(), emptyPrimDef)
149
150        # InvalidPrim_2 : This prim's type is not a valid schema type, but the
151        # type has a single fallback defined in metadata which is a valid type.
152        # This prim's schema type will be this fallback type.
153        prim = stage.GetPrimAtPath("/InvalidPrim_2")
154        _VerifyFallbacksInStageMetdata("InvalidType_2", ["ValidType_2"])
155        self.assertTrue(prim)
156        self.assertEqual(prim.GetTypeName(), "InvalidType_2")
157        primTypeInfo = prim.GetPrimTypeInfo()
158        self.assertEqual(primTypeInfo.GetTypeName(), "InvalidType_2")
159        self.assertEqual(primTypeInfo.GetSchemaType(), self.validType2)
160        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "ValidType_2")
161        self.assertTrue(prim.IsA(self.validType2))
162        self.assertTrue(prim.IsA(Usd.Typed))
163        _VerifyPrimDefsSame(prim.GetPrimDefinition(), validType2PrimDef)
164
165        # InvalidPrim_3 : This prim's type is not a valid schema type, but the
166        # type has two fallbacks defined in metadata which are both valid types.
167        # This prim's schema type will be the first fallback type in the list.
168        prim = stage.GetPrimAtPath("/InvalidPrim_3")
169        _VerifyFallbacksInStageMetdata("InvalidType_3",
170                                       ["ValidType_1", "ValidType_2"])
171        self.assertTrue(prim)
172        self.assertEqual(prim.GetTypeName(), "InvalidType_3")
173        primTypeInfo = prim.GetPrimTypeInfo()
174        self.assertEqual(primTypeInfo.GetTypeName(), "InvalidType_3")
175        self.assertEqual(primTypeInfo.GetSchemaType(), self.validType1)
176        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "ValidType_1")
177        self.assertTrue(prim.IsA(self.validType1))
178        self.assertTrue(prim.IsA(Usd.Typed))
179        _VerifyPrimDefsSame(prim.GetPrimDefinition(), validType1PrimDef)
180
181        # InvalidPrim_4 : This prim's type is not a valid schema type, but the
182        # type has two fallbacks defined in metadata. The first fallback type
183        # is itself invalid, but second is a valid type. This prim's schema type
184        # will be the second fallback type in the list.
185        prim = stage.GetPrimAtPath("/InvalidPrim_4")
186        _VerifyFallbacksInStageMetdata("InvalidType_4",
187                                       ["InvalidType_3", "ValidType_2"])
188        self.assertTrue(prim)
189        self.assertEqual(prim.GetTypeName(), "InvalidType_4")
190        primTypeInfo = prim.GetPrimTypeInfo()
191        self.assertEqual(primTypeInfo.GetTypeName(), "InvalidType_4")
192        self.assertEqual(primTypeInfo.GetSchemaType(), self.validType2)
193        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "ValidType_2")
194        self.assertTrue(prim.IsA(self.validType2))
195        self.assertTrue(prim.IsA(Usd.Typed))
196        _VerifyPrimDefsSame(prim.GetPrimDefinition(), validType2PrimDef)
197
198        # InvalidPrim_5 : This prim's type is not a valid schema type, but the
199        # type has two fallbacks defined in metadata. However, both of these
200        # types are also invalid types. This will have an invalid schema type.
201        prim = stage.GetPrimAtPath("/InvalidPrim_5")
202        _VerifyFallbacksInStageMetdata("InvalidType_5",
203                                       ["InvalidType_3", "InvalidType_2"])
204        self.assertTrue(prim)
205        self.assertEqual(prim.GetTypeName(), "InvalidType_5")
206        primTypeInfo = prim.GetPrimTypeInfo()
207        self.assertEqual(primTypeInfo.GetTypeName(), "InvalidType_5")
208        self.assertEqual(primTypeInfo.GetSchemaType(), Tf.Type.Unknown)
209        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "")
210        self.assertFalse(prim.IsA(Usd.Typed))
211        _VerifyPrimDefsSame(prim.GetPrimDefinition(), emptyPrimDef)
212
213        # InvalidPrim_WithAPISchemas_1 : Has no fallbacks defined in metadata
214        # and its type name is not a valid schema. This will have an invalid
215        # schema type, but the API schemas will still be applied
216        prim = stage.GetPrimAtPath("/InvalidPrim_WithAPISchemas_1")
217        _VerifyFallbacksInStageMetdata("InvalidType_1", None)
218        self.assertTrue(prim)
219        self.assertEqual(prim.GetTypeName(), "InvalidType_1")
220        primTypeInfo = prim.GetPrimTypeInfo()
221        self.assertEqual(primTypeInfo.GetTypeName(), "InvalidType_1")
222        self.assertEqual(primTypeInfo.GetSchemaType(), Tf.Type.Unknown)
223        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "")
224        self.assertEqual(primTypeInfo.GetAppliedAPISchemas(),
225                         ["CollectionAPI:foo"])
226        self.assertFalse(prim.IsA(Usd.Typed))
227        self.assertEqual(prim.GetAppliedSchemas(), ["CollectionAPI:foo"])
228
229        # Create a new prim with an empty typename and the same API schemas
230        # as the above. Verify that because these two prims have the same
231        # effective schema type and applied schemas, that they use the same
232        # prim definition even though their type names differ.
233        otherPrim = stage.DefinePrim("/EmptyWithCollection", "")
234        Usd.CollectionAPI.Apply(otherPrim, "foo")
235        otherPrimTypeInfo = otherPrim.GetPrimTypeInfo()
236        self.assertNotEqual(otherPrim.GetTypeName(), prim.GetTypeName())
237        self.assertNotEqual(otherPrimTypeInfo, primTypeInfo)
238        self.assertEqual(otherPrimTypeInfo.GetSchemaTypeName(),
239                         primTypeInfo.GetSchemaTypeName())
240        self.assertEqual(otherPrimTypeInfo.GetSchemaType(),
241                         primTypeInfo.GetSchemaType())
242        self.assertEqual(otherPrim.GetAppliedSchemas(),
243                         prim.GetAppliedSchemas())
244        _VerifyPrimDefsSame(otherPrim.GetPrimDefinition(),
245                             prim.GetPrimDefinition())
246
247        # InvalidPrim_2 : This prim's type is not a valid schema type, but the
248        # type has a single fallback defined in metadata which is a valid type.
249        # This prim's schema type will be this fallback type and the API schemas
250        # will be applied over the fallbacktype.
251        prim = stage.GetPrimAtPath("/InvalidPrim_WithAPISchemas_2")
252        _VerifyFallbacksInStageMetdata("InvalidType_2", ["ValidType_2"])
253        self.assertTrue(prim)
254        self.assertEqual(prim.GetTypeName(), "InvalidType_2")
255        primTypeInfo = prim.GetPrimTypeInfo()
256        self.assertEqual(primTypeInfo.GetTypeName(), "InvalidType_2")
257        self.assertEqual(primTypeInfo.GetSchemaType(), self.validType2)
258        self.assertEqual(primTypeInfo.GetSchemaTypeName(), "ValidType_2")
259        self.assertEqual(primTypeInfo.GetAppliedAPISchemas(),
260                         ["CollectionAPI:foo"])
261        self.assertTrue(prim.IsA(self.validType2))
262        self.assertTrue(prim.IsA(Usd.Typed))
263        self.assertEqual(prim.GetAppliedSchemas(), ["CollectionAPI:foo"])
264
265        # Create a new prim using the fallback typename and the same API schemas
266        # as the above. Verify that because these two prims have the same
267        # effective schema type and applied schemas, that they use the same
268        # prim definition even though their type names differ.
269        otherPrim = stage.DefinePrim("/Valid2WithCollection", "ValidType_2")
270        Usd.CollectionAPI.Apply(otherPrim, "foo")
271        otherPrimTypeInfo = otherPrim.GetPrimTypeInfo()
272        self.assertNotEqual(otherPrim.GetTypeName(), prim.GetTypeName())
273        self.assertNotEqual(otherPrimTypeInfo, primTypeInfo)
274        self.assertEqual(otherPrimTypeInfo.GetSchemaTypeName(),
275                         primTypeInfo.GetSchemaTypeName())
276        self.assertEqual(otherPrimTypeInfo.GetSchemaType(),
277                         primTypeInfo.GetSchemaType())
278        self.assertEqual(otherPrim.GetAppliedSchemas(),
279                         prim.GetAppliedSchemas())
280        _VerifyPrimDefsSame(otherPrim.GetPrimDefinition(),
281                             prim.GetPrimDefinition())
282
283    def test_Sublayer(self):
284        # Create a new layer with same layer above as a sublayer. We don't
285        # compose sublayer metadata in stage level metadata so we don't get
286        # the fallback types like in the above test case.
287        layer = Sdf.Layer.CreateAnonymous()
288        layer.subLayerPaths.append('WithFallback.usda')
289        stage = Usd.Stage.Open(layer)
290
291        # The two prims with valid schema type names are still valid types.
292        prim = stage.GetPrimAtPath("/ValidPrim_1")
293        self.assertTrue(prim)
294        self.assertEqual(prim.GetTypeName(), "ValidType_1")
295        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaType(),
296                         self.validType1)
297        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaTypeName(),
298                         "ValidType_1")
299
300        prim = stage.GetPrimAtPath("/ValidPrim_2")
301        self.assertTrue(prim)
302        self.assertEqual(prim.GetTypeName(), "ValidType_2")
303        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaType(),
304                         self.validType2)
305        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaTypeName(),
306                         "ValidType_2")
307
308        # All of the prims with invalid types have no schema type since we
309        # have no fallbacks from the sublayer.
310        for primPath, typeName in [("/InvalidPrim_1", "InvalidType_1"),
311                                   ("/InvalidPrim_2", "InvalidType_2"),
312                                   ("/InvalidPrim_3", "InvalidType_3"),
313                                   ("/InvalidPrim_4", "InvalidType_4"),
314                                   ("/InvalidPrim_5", "InvalidType_5")] :
315            prim = stage.GetPrimAtPath(primPath)
316            self.assertTrue(prim)
317            self.assertEqual(prim.GetTypeName(), typeName)
318            self.assertEqual(prim.GetPrimTypeInfo().GetSchemaType(),
319                             Tf.Type.Unknown)
320            self.assertEqual(prim.GetPrimTypeInfo().GetSchemaTypeName(), "")
321
322    def test_FallbackAuthoring(self):
323        # Test the manual authoring of fallback type metadata on the stage.
324        stage = Usd.Stage.Open("WithFallback.usda")
325        self.assertTrue(stage)
326
327        # InvalidPrim_1 : Has no fallbacks defined in metadata and its type name
328        # is not a valid schema. This will have an invalid schema type.
329        prim = stage.GetPrimAtPath("/InvalidPrim_1")
330        self.assertTrue(prim)
331        self.assertEqual(prim.GetTypeName(), "InvalidType_1")
332        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaType(),
333                         Tf.Type.Unknown)
334        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaTypeName(), "")
335
336        # Author a fallback value for InvalidType_1 in the stage's
337        # fallbackPrimTypes metadata dictionary. This will resync all prims
338        # on the stage.
339        with ChangeNoticeVerifier(self, expectedResyncAll=True):
340            stage.SetMetadataByDictKey("fallbackPrimTypes", "InvalidType_1",
341                                       Vt.TokenArray(["ValidType_2"]))
342
343        # InvalidPrim_1 now has a schema type from the fallback type.
344        self.assertEqual(prim.GetTypeName(), "InvalidType_1")
345        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaType(),
346                         self.validType2)
347        self.assertEqual(prim.GetPrimTypeInfo().GetSchemaTypeName(),
348                         "ValidType_2")
349
350    def test_WritingSchemaFallbacks(self):
351        # Test the writing of fallback types defined in the schema through the
352        # stage API.
353
354        # Our test schema plugin has some fallbacks defined. Verify that we
355        # can get these as dictionary from the schema registry
356        schemaFallbacks = {
357            "ValidType_1" : Vt.TokenArray(["FallbackType_1"]),
358            "ValidType_2" : Vt.TokenArray(["FallbackType_2", "FallbackType_1"])
359        }
360        self.assertEqual(Usd.SchemaRegistry().GetFallbackPrimTypes(),
361                         schemaFallbacks)
362
363        # Create a new empty layer and stage from that layer. There will be
364        # no fallbackPrimTypes set on the newly opened stage
365        layer = Sdf.Layer.CreateNew("tempNew.usda")
366        self.assertTrue(layer)
367        stage = Usd.Stage.Open(layer)
368        self.assertTrue(stage)
369        self.assertFalse(stage.GetMetadata("fallbackPrimTypes"))
370        self.assertFalse(layer.GetPrimAtPath('/').GetInfo('fallbackPrimTypes'))
371
372        # Create a prim spec on the layer and save the layer itself. There will
373        # be no fallback metadata on the stage since we saved through the
374        # layer's API.
375        spec = Sdf.PrimSpec(layer, "ValidPrim", Sdf.SpecifierDef, "ValidType_2")
376        self.assertTrue(stage.GetPrimAtPath("/ValidPrim"))
377        layer.Save()
378        self.assertFalse(stage.GetMetadata("fallbackPrimTypes"))
379        self.assertFalse(layer.GetPrimAtPath('/').GetInfo('fallbackPrimTypes'))
380
381        # Now write the schema fallbacks to the layer using the stage API. This
382        # will write the schema fallbacks dictionary to the root layer. Verify
383        # that this layer change does not a cause a full stage resync.
384        with ChangeNoticeVerifier(self, expectedResyncAll=False):
385            stage.WriteFallbackPrimTypes()
386        self.assertEqual(stage.GetMetadata("fallbackPrimTypes"),
387                         schemaFallbacks)
388        self.assertEqual(layer.GetPrimAtPath('/').GetInfo('fallbackPrimTypes'),
389                         schemaFallbacks)
390
391        # Now manually author a different dictionary of fallback to the root
392        # layer. This will trigger a full stage resync and changes the stage's
393        # fallback types.
394        newFallbacks = {
395            "ValidType_1" : Vt.TokenArray(["ValidType_2, FallbackType_2"]),
396            "InValidType_1" : Vt.TokenArray(["ValidType_1"])
397        }
398        with ChangeNoticeVerifier(self, expectedResyncAll=True):
399            layer.GetPrimAtPath('/').SetInfo('fallbackPrimTypes',
400                                             newFallbacks)
401        self.assertEqual(stage.GetMetadata("fallbackPrimTypes"),
402                         newFallbacks)
403        self.assertEqual(layer.GetPrimAtPath('/').GetInfo('fallbackPrimTypes'),
404                         newFallbacks)
405
406        # Write the schema fallbacks again and verify that it will add fallback
407        # types to the metadata dictionary from the schema registry but only for
408        # types that aren't already in the dictionary.
409        with ChangeNoticeVerifier(self, expectedResyncAll=False):
410            stage.WriteFallbackPrimTypes()
411        postSaveFallbacks = {
412            "ValidType_1" : Vt.TokenArray(["ValidType_2, FallbackType_2"]),
413            "ValidType_2" : Vt.TokenArray(["FallbackType_2", "FallbackType_1"]),
414            "InValidType_1" : Vt.TokenArray(["ValidType_1"])
415        }
416        self.assertEqual(stage.GetMetadata("fallbackPrimTypes"),
417                         postSaveFallbacks)
418        self.assertEqual(layer.GetPrimAtPath('/').GetInfo('fallbackPrimTypes'),
419                         postSaveFallbacks)
420
421
422if __name__ == "__main__":
423    unittest.main()
424