1#!/pxrpythonsubst
2#
3# Copyright 2017 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, platform, sys, unittest
26from pxr import Ar,Sdf,Usd,Tf
27
28allFormats = ['usd' + x for x in 'ac']
29
30class TestUsdNotices(unittest.TestCase):
31    def setUp(self):
32        self._ResetCounters()
33
34    def _ResetCounters(self):
35        self._changeCount = 0
36        self._contentsCount = 0
37        self._objectsCount = 0
38        self._editTargetsCount = 0
39        self._layerMutingCount = 0
40
41    def OnStageContentsChanged(self, *args):
42        self._changeCount += 1
43        self._contentsCount += 1
44
45    def OnObjectsChanged(self, *args):
46        self._changeCount += 1
47        self._objectsCount += 1
48
49    def OnStageEditTargetChanged(self, *args):
50        self._changeCount += 1
51        self._editTargetsCount += 1
52
53    def OnLayerMutingChanged(self, *args):
54        self._changeCount += 1
55        self._layerMutingCount += 1
56
57    def test_Basics(self):
58        contentsChanged = Tf.Notice.RegisterGlobally(
59            Usd.Notice.StageContentsChanged,
60            self.OnStageContentsChanged)
61        objectsChanged = Tf.Notice.RegisterGlobally(
62            Usd.Notice.ObjectsChanged,
63            self.OnObjectsChanged)
64        stageEditTargetChanged = Tf.Notice.RegisterGlobally(
65            Usd.Notice.StageEditTargetChanged,
66            self.OnStageEditTargetChanged)
67        layerMutingChanged = Tf.Notice.RegisterGlobally(
68            Usd.Notice.LayerMutingChanged,
69            self.OnLayerMutingChanged)
70
71        for fmt in allFormats:
72            self._ResetCounters()
73            s = Usd.Stage.CreateInMemory('Basics.'+fmt)
74            s.DefinePrim("/Foo")
75            self.assertEqual(self._changeCount, 2)
76            self.assertEqual(self._contentsCount, 1)
77            self.assertEqual(self._objectsCount, 1)
78            self.assertEqual(self._editTargetsCount, 0)
79
80            self._ResetCounters()
81            s.SetEditTarget(s.GetSessionLayer())
82            self.assertEqual(self._changeCount, 1)
83            self.assertEqual(self._contentsCount, 0)
84            self.assertEqual(self._objectsCount, 0)
85            self.assertEqual(self._editTargetsCount, 1)
86
87            self._ResetCounters()
88            s.GetPrimAtPath("/Foo").SetMetadata("comment", "")
89            self.assertEqual(self._changeCount, 4)
90            self.assertEqual(self._contentsCount, 2)     # Why 2? I expected 1.
91            self.assertEqual(self._objectsCount, 2)      # We get an additional
92                                                    # object resync notice when
93                                                    # we first drop an over, in
94                                                    # addition to the info-only
95                                                    # change notice.
96            self.assertEqual(self._editTargetsCount, 0)
97
98            self._ResetCounters()
99            s.GetPrimAtPath("/Foo").SetMetadata("comment", "x")
100            # Now that the over(s) have been established, setting a value
101            # behaves as expected.
102            self.assertEqual(self._changeCount, 2)
103            self.assertEqual(self._contentsCount, 1)
104            self.assertEqual(self._objectsCount, 1)
105            self.assertEqual(self._editTargetsCount, 0)
106
107            self._ResetCounters()
108            s.MuteAndUnmuteLayers([s.GetSessionLayer().identifier],
109                                    [s.GetRootLayer().identifier])
110            self.assertEqual(self._changeCount, 3)
111            self.assertEqual(self._objectsCount, 1)
112            self.assertEqual(self._contentsCount, 1)
113            self.assertEqual(self._layerMutingCount, 1)
114
115            # no notice should be called
116            self._ResetCounters()
117            with self.assertRaises(Tf.ErrorException):
118                s.MuteAndUnmuteLayers([s.GetRootLayer().identifier],
119                                        [s.GetRootLayer().identifier])
120            self.assertEqual(self._changeCount, 0)
121            self.assertEqual(self._objectsCount, 0)
122            self.assertEqual(self._contentsCount, 0)
123            self.assertEqual(self._layerMutingCount, 0)
124        # testing for payload specific updates previously, load/unload calls
125        # didn't trigger notices
126        self._ResetCounters()
127        self.assertTrue(self._objectsCount == 0)
128        payloadBasedFile = 'payload_base.usda'
129        payloadBasedStage = Usd.Stage.Open(payloadBasedFile)
130
131        # the payload will be already loaded since we didn't supply
132        # the additional parameter to the stage constructor
133        payloadBasedStage.Unload('/Foo')
134        self.assertEqual(self._objectsCount, 1)
135        payloadBasedStage.Load('/Foo')
136        self.assertEqual(self._objectsCount, 2)
137
138        del contentsChanged
139        del objectsChanged
140        del stageEditTargetChanged
141        del layerMutingChanged
142
143
144    def test_ObjectsChangedNotice(self):
145        def OnResync(notice, stage):
146            self.assertEqual(notice.GetStage(), stage)
147            self.assertEqual(notice.GetResyncedPaths(), [Sdf.Path("/Foo")])
148            self.assertEqual(notice.GetChangedInfoOnlyPaths(), [])
149            self.assertTrue(notice.AffectedObject(stage.GetPrimAtPath("/Foo")))
150            self.assertTrue(notice.ResyncedObject(stage.GetPrimAtPath("/Foo")))
151            self.assertTrue(not notice.ChangedInfoOnly(stage.GetPrimAtPath("/Foo")))
152
153        def OnUpdate(notice, stage):
154            self.assertEqual(notice.GetStage(), stage)
155            self.assertEqual(notice.GetResyncedPaths(), [])
156            self.assertEqual(notice.GetChangedInfoOnlyPaths(), [Sdf.Path("/Foo")])
157            self.assertTrue(notice.AffectedObject(stage.GetPrimAtPath("/Foo")))
158            self.assertTrue(not notice.ResyncedObject(stage.GetPrimAtPath("/Foo")))
159            self.assertTrue(notice.ChangedInfoOnly(stage.GetPrimAtPath("/Foo")))
160
161        for fmt in allFormats:
162            self._ResetCounters()
163            s = Usd.Stage.CreateInMemory('ObjectsChangedNotice.'+fmt)
164
165            objectsChanged = Tf.Notice.Register(Usd.Notice.ObjectsChanged,
166                                                       OnResync, s)
167            s.DefinePrim("/Foo")
168
169            objectsChanged = Tf.Notice.Register(Usd.Notice.ObjectsChanged,
170                                                       OnUpdate, s)
171            s.GetPrimAtPath("/Foo").SetMetadata("comment", "")
172        del objectsChanged
173
174    def test_ObjectsChangedNoticeForAttributes(self):
175        for fmt in allFormats:
176            self._ResetCounters()
177            s = Usd.Stage.CreateInMemory('ObjectsChangedNoticeForProps.'+fmt)
178            prim = s.DefinePrim("/Foo")
179
180            def OnAttributeCreation(notice, stage):
181                self.assertEqual(notice.GetStage(), stage)
182                self.assertEqual(notice.GetResyncedPaths(), [Sdf.Path("/Foo.attr")])
183                self.assertEqual(notice.GetChangedInfoOnlyPaths(), [])
184                self.assertTrue(notice.AffectedObject(stage.GetPrimAtPath("/Foo").GetAttribute("attr")))
185                self.assertTrue(notice.ResyncedObject(stage.GetPrimAtPath("/Foo").GetAttribute("attr")))
186                self.assertTrue(not notice.ChangedInfoOnly(stage.GetPrimAtPath("/Foo").GetAttribute("attr")))
187
188            objectsChanged = Tf.Notice.Register(
189                Usd.Notice.ObjectsChanged, OnAttributeCreation, s)
190            attr = prim.CreateAttribute("attr", Sdf.ValueTypeNames.Int)
191
192            def OnAttributeValueChange(notice, stage):
193                self.assertEqual(notice.GetStage(), stage)
194                self.assertEqual(notice.GetResyncedPaths(), [])
195                self.assertTrue(notice.GetChangedInfoOnlyPaths() == \
196                    [Sdf.Path("/Foo.attr")])
197                self.assertTrue(notice.AffectedObject(
198                    stage.GetPrimAtPath("/Foo").GetAttribute("attr")))
199                self.assertTrue(not notice.ResyncedObject(
200                    stage.GetPrimAtPath("/Foo").GetAttribute("attr")))
201                self.assertTrue(notice.ChangedInfoOnly(
202                    stage.GetPrimAtPath("/Foo").GetAttribute("attr")))
203
204            objectsChanged = Tf.Notice.Register(
205                Usd.Notice.ObjectsChanged, OnAttributeValueChange, s)
206            attr.Set(42)
207
208            del objectsChanged
209
210    def test_ObjectsChangedNoticeForRelationships(self):
211        for fmt in allFormats:
212            self._ResetCounters()
213            s = Usd.Stage.CreateInMemory('ObjectsChangedNoticeForRels.'+fmt)
214            prim = s.DefinePrim("/Foo")
215
216            def OnRelationshipCreation(notice, stage):
217                self.assertEqual(notice.GetStage(), stage)
218                self.assertEqual(notice.GetResyncedPaths(), [Sdf.Path("/Foo.rel")])
219                self.assertEqual(notice.GetChangedInfoOnlyPaths(), [])
220                self.assertTrue(notice.AffectedObject(
221                    stage.GetPrimAtPath("/Foo").GetRelationship("rel")))
222                self.assertTrue(notice.ResyncedObject(
223                    stage.GetPrimAtPath("/Foo").GetRelationship("rel")))
224                self.assertTrue(not notice.ChangedInfoOnly(
225                    stage.GetPrimAtPath("/Foo").GetRelationship("rel")))
226
227            objectsChanged = Tf.Notice.Register(
228                Usd.Notice.ObjectsChanged, OnRelationshipCreation, s)
229            rel = prim.CreateRelationship("rel")
230
231            def OnRelationshipTargetChange(notice, stage):
232                self.assertEqual(notice.GetStage(), stage)
233
234                self.assertEqual(notice.GetResyncedPaths(), [])
235                self.assertEqual(notice.GetChangedInfoOnlyPaths(),
236                                 [Sdf.Path("/Foo.rel")])
237                self.assertTrue(notice.AffectedObject(
238                    stage.GetPrimAtPath("/Foo").GetRelationship("rel")))
239                self.assertTrue(not notice.ResyncedObject(
240                    stage.GetPrimAtPath("/Foo").GetRelationship("rel")))
241                self.assertTrue(notice.ChangedInfoOnly(
242                    stage.GetPrimAtPath("/Foo").GetRelationship("rel")))
243
244            objectsChanged = Tf.Notice.Register(
245                Usd.Notice.ObjectsChanged, OnRelationshipTargetChange, s)
246            rel.AddTarget("/Bar")
247
248            del objectsChanged
249
250    def test_LayerMutingChange(self):
251        expectedResult = {}
252        def OnLayerMutingChange(notice, stage):
253            self.assertEqual(notice.GetStage(), stage)
254            self.assertEqual(notice.GetMutedLayers(), expectedResult["muted"])
255            self.assertEqual(notice.GetUnmutedLayers(),
256                    expectedResult["unmuted"])
257
258        for fmt in allFormats:
259            s = Usd.Stage.CreateInMemory('LayerMutingChange.'+fmt)
260
261            # mute session layer and try unmute root layer
262            # note that root layer cannot be muted or unmuted
263            expectedResult = {
264                    "muted": [s.GetSessionLayer().identifier],
265                    "unmuted": []
266                    }
267            layerMutingChanged = Tf.Notice.Register(
268                Usd.Notice.LayerMutingChanged,
269                OnLayerMutingChange, s)
270            s.MuteAndUnmuteLayers([s.GetSessionLayer().identifier],
271                                    [s.GetRootLayer().identifier])
272
273            # undo session layer muting and try to mute root layer
274            expectedResult = {
275                    "muted": [],
276                    "unmuted": [s.GetSessionLayer().identifier]
277                    }
278            with self.assertRaises(Tf.ErrorException):
279                s.MuteAndUnmuteLayers([s.GetRootLayer().identifier],
280                        [s.GetSessionLayer().identifier])
281
282            # test to make sure callback is still called for muting of layers
283            # not part of the stage
284            layer1 = Sdf.Layer.CreateNew('nonStageLayer.'+fmt)
285            expectedResult = {
286                    "muted": [layer1.identifier],
287                    "unmuted": []
288                    }
289            s.MuteAndUnmuteLayers([layer1.identifier],[])
290
291            del layerMutingChanged
292
293    def test_Reload(self):
294        s = Usd.Stage.CreateInMemory()
295        s.DefinePrim('/Root')
296
297        def OnReload(notice, sender):
298            self.assertEqual(notice.GetStage(), s)
299            self.assertEqual(notice.GetResyncedPaths(), ['/Root'])
300            self.assertFalse(notice.HasChangedFields('/Root'))
301            self.assertEqual(notice.GetChangedFields('/Root'), [])
302
303            # XXX: The pseudo-root is marked as having info changed
304            # because of the didReloadContent bit in the layer changelist.
305            # This doesn't seem correct.
306            self.assertEqual(notice.GetChangedInfoOnlyPaths(), ['/'])
307            self.assertFalse(notice.HasChangedFields('/'))
308            self.assertFalse(notice.GetChangedFields('/'), [])
309
310        notice = Tf.Notice.Register(Usd.Notice.ObjectsChanged, OnReload, s)
311
312        s.Reload()
313
314    @unittest.skipIf(platform.system() == "Windows" and
315                     not hasattr(Ar.Resolver, "CreateIdentifier"),
316                     "This test case currently fails on Windows due to "
317                     "path canonicalization issues except with Ar 2.0.")
318    def test_InvalidLayerReloadChange(self):
319        s = Usd.Stage.CreateNew('LayerReloadChange.usda')
320
321        # Create a prim with a reference to a non-existent layer.
322        # This should result in a composition error.
323        prim = s.DefinePrim('/ModelReference')
324        prim.GetReferences().AddReference(
325            assetPath='./Model.usda', primPath='/Model')
326
327        s.Save()
328
329        # Create a new layer at the referenced path and reload the referencing
330        # stage. This should cause the layer to be opened and a resync for the
331        # referencing prim.
332        try:
333            layer = Sdf.Layer.CreateNew('Model.usda')
334            Sdf.CreatePrimInLayer(layer, '/Model')
335
336            class OnReload(object):
337                def __init__(self, stage, fixture):
338                    super(OnReload, self).__init__()
339                    self.receivedNotice = False
340                    self.fixture = fixture
341                    self.stage = stage
342                    self._key = Tf.Notice.Register(
343                        Usd.Notice.ObjectsChanged, self._HandleChange, stage)
344
345                def _HandleChange(self, notice, sender):
346                    self.receivedNotice = True
347                    self.fixture.assertEqual(notice.GetStage(), self.stage)
348                    self.fixture.assertEqual(notice.GetResyncedPaths(),
349                                             [Sdf.Path('/ModelReference')])
350                    self.fixture.assertFalse(
351                        notice.HasChangedFields('/ModelReference'))
352                    self.fixture.assertEqual(
353                        notice.GetChangedFields('/ModelReference'), [])
354                    self.fixture.assertEqual(notice.GetChangedInfoOnlyPaths(),
355                                             [])
356
357            l = OnReload(s, self)
358            s.Reload()
359
360            # Should have received a resync notice and composed the referenced
361            # /Model prim into /ModelReference.
362            self.assertTrue(l.receivedNotice)
363            self.assertEqual(
364                s.GetPrimAtPath('/ModelReference').GetPrimStack()[1],
365                layer.GetPrimAtPath('/Model'))
366
367        finally:
368            # Make sure to remove Model.usda so subsequent test runs don't
369            # pick it up, otherwise test case won't work.
370            try:
371                os.remove('Model.usda')
372            except:
373                pass
374
375if __name__ == "__main__":
376    unittest.main()
377