Skip to content

Commit dd983aa

Browse files
davidbaraffDavid Baraff
andauthored
detect cycles (#848)
Co-authored-by: David Baraff <nosuch@nowhere>
1 parent 47bd489 commit dd983aa

File tree

4 files changed

+46
-4
lines changed

4 files changed

+46
-4
lines changed

src/opentimelineio/errorStatus.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ struct ErrorStatus {
3636
CANNOT_COMPUTE_AVAILABLE_RANGE,
3737
INVALID_TIME_RANGE,
3838
OBJECT_WITHOUT_DURATION,
39-
CANNOT_TRIM_TRANSITION
39+
CANNOT_TRIM_TRANSITION,
40+
OBJECT_CYCLE
4041
};
4142

4243
ErrorStatus()

src/opentimelineio/serialization.cpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -556,16 +556,26 @@ void SerializableObject::Writer::write(std::string const& key, SerializableObjec
556556
return;
557557
}
558558

559-
#ifdef OTIO_INSTANCING_SUPPORT
560559
auto e = _id_for_object.find(value);
561560
if (e != _id_for_object.end()) {
561+
#ifdef OTIO_INSTANCING_SUPPORT
562562
/*
563563
* We've already written this value.
564564
*/
565565
_encoder.write_value(SerializableObject::ReferenceId { e->second });
566+
#else
567+
/*
568+
* We're encountering the same object while it is still
569+
* in the map, meaning we're in the middle of writing it out.
570+
* That's a cycle, as opposed to mere instancing, which we
571+
* allow so as not to break old allowed behavior.
572+
*/
573+
std::string s = string_printf("cyclically encountered object has schema %s",
574+
value->schema_name().c_str());
575+
_encoder._error(ErrorStatus(ErrorStatus::OBJECT_CYCLE, s));
576+
#endif
566577
return;
567578
}
568-
#endif
569579

570580
std::string const& schema_type_name = value->_schema_name_for_reference();
571581
if (_next_id_for_type.find(schema_type_name) == _next_id_for_type.end()) {
@@ -590,10 +600,16 @@ void SerializableObject::Writer::write(std::string const& key, SerializableObjec
590600
_encoder.write_key("OTIO_REF_ID");
591601
_encoder.write_value(next_id);
592602
#endif
593-
594603
value->write_to(*this);
595604

596605
_encoder.end_object();
606+
607+
#ifndef OTIO_INSTANCING_SUPPORT
608+
auto valueEntry = _id_for_object.find(value);
609+
if (valueEntry != _id_for_object.end()) {
610+
_id_for_object.erase(valueEntry);
611+
}
612+
#endif
597613
}
598614

599615
void SerializableObject::Writer::write(std::string const& key, AnyDictionary const& value) {

src/py-opentimelineio/opentimelineio-bindings/otio_errorStatusHandler.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ ErrorStatusHandler::~ErrorStatusHandler() noexcept(false) {
5858
throw _NotAChildException(full_details());
5959
case ErrorStatus::CANNOT_COMPUTE_AVAILABLE_RANGE:
6060
throw _CannotComputeAvailableRangeException(full_details());
61+
case ErrorStatus::OBJECT_CYCLE:
62+
throw py::value_error("Detected SerializableObject cycle while copying/serializing: " + details());
6163
default:
6264
throw py::value_error(full_details());
6365
}

tests/test_serializable_object.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,29 @@ def test_truthiness(self):
173173
o = otio.core.SerializableObject()
174174
self.assertTrue(o)
175175

176+
def test_instancing_without_instancing_support(self):
177+
o = otio.core.SerializableObjectWithMetadata()
178+
c = otio.core.SerializableObjectWithMetadata()
179+
o.metadata["child1"] = c
180+
o.metadata["child2"] = c
181+
self.assertTrue(o.metadata["child1"] is o.metadata["child2"])
182+
183+
oCopy = o.clone()
184+
# Note: If we ever enable INSTANCING_SUPPORT in the C++ code,
185+
# then this will (and should) fail
186+
self.assertTrue(oCopy.metadata["child1"] is not oCopy.metadata["child2"])
187+
188+
def test_cycle_detection(self):
189+
o = otio.core.SerializableObjectWithMetadata()
190+
o.metadata["myself"] = o
191+
192+
# Note: If we ever enable INSTANCING_SUPPORT in the C++ code,
193+
# then modify the code below to be:
194+
# oCopy = o.clone()
195+
# self.assertTrue(oCopy is oCopy.metadata["myself"])
196+
with self.assertRaises(ValueError):
197+
o.clone()
198+
176199

177200
if __name__ == '__main__':
178201
unittest.main()

0 commit comments

Comments
 (0)