diff --git a/src/opentimelineio/composition.cpp b/src/opentimelineio/composition.cpp index 01fe6b9de..bfeac70b2 100644 --- a/src/opentimelineio/composition.cpp +++ b/src/opentimelineio/composition.cpp @@ -252,7 +252,18 @@ Composition::_path_from_child( ErrorStatus* error_status) const { auto current = child->parent(); - std::vector parents{ current }; + std::vector parents; + + if (!current) + { + if (error_status) + { + *error_status = ErrorStatus::NOT_DESCENDED_FROM; + error_status->object_details = this; + } + return parents; + } + parents.push_back(current); while (current != this) { @@ -311,7 +322,7 @@ Composition::range_of_child(Composable const* child, ErrorStatus* error_status) const { auto parents = _path_from_child(child, error_status); - if (is_error(error_status)) + if (parents.empty() || is_error(error_status)) { return TimeRange(); } @@ -363,7 +374,7 @@ Composition::trimmed_range_of_child( ErrorStatus* error_status) const { auto parents = _path_from_child(child, error_status); - if (is_error(error_status)) + if (parents.empty() || is_error(error_status)) { return TimeRange(); } diff --git a/src/opentimelineio/item.cpp b/src/opentimelineio/item.cpp index e2e1bcb4f..9c1a5ebe3 100644 --- a/src/opentimelineio/item.cpp +++ b/src/opentimelineio/item.cpp @@ -87,10 +87,14 @@ Item::visible_range(ErrorStatus* error_status) const std::optional Item::trimmed_range_in_parent(ErrorStatus* error_status) const { - if (!parent() && error_status) + if (!parent()) { - *error_status = ErrorStatus::NOT_A_CHILD; - error_status->object_details = this; + if (error_status) + { + *error_status = ErrorStatus::NOT_A_CHILD; + error_status->object_details = this; + } + return std::nullopt; } return parent()->trimmed_range_of_child(this, error_status); @@ -99,10 +103,14 @@ Item::trimmed_range_in_parent(ErrorStatus* error_status) const TimeRange Item::range_in_parent(ErrorStatus* error_status) const { - if (!parent() && error_status) + if (!parent()) { - *error_status = ErrorStatus::NOT_A_CHILD; - error_status->object_details = this; + if (error_status) + { + *error_status = ErrorStatus::NOT_A_CHILD; + error_status->object_details = this; + } + return TimeRange(); } return parent()->range_of_child(this, error_status); diff --git a/src/opentimelineio/transition.cpp b/src/opentimelineio/transition.cpp index a29cb4d5b..b18c54c91 100644 --- a/src/opentimelineio/transition.cpp +++ b/src/opentimelineio/transition.cpp @@ -67,6 +67,7 @@ Transition::range_in_parent(ErrorStatus* error_status) const "cannot compute range in parent because item has no parent", this); } + return std::nullopt; } return parent()->range_of_child(this, error_status); @@ -84,6 +85,7 @@ Transition::trimmed_range_in_parent(ErrorStatus* error_status) const "cannot compute trimmed range in parent because item has no parent", this); } + return std::nullopt; } return parent()->trimmed_range_of_child(this, error_status); diff --git a/tests/test_composition.cpp b/tests/test_composition.cpp index 66a134ce3..8cbfa8826 100644 --- a/tests/test_composition.cpp +++ b/tests/test_composition.cpp @@ -55,6 +55,46 @@ main(int argc, char** argv) assertEqual(items[0].value, clip.value); }); + tests.add_test("test_orphan_ranges_report_errors", [] { + SerializableObject::Retainer clip = new Clip; + OTIO_NS::ErrorStatus error; + + assertEqual(clip->range_in_parent(&error), TimeRange()); + assertEqual(error.outcome, OTIO_NS::ErrorStatus::NOT_A_CHILD); + + error = OTIO_NS::ErrorStatus(); + assertFalse(clip->trimmed_range_in_parent(&error).has_value()); + assertEqual(error.outcome, OTIO_NS::ErrorStatus::NOT_A_CHILD); + assertEqual(clip->range_in_parent(), TimeRange()); + assertFalse(clip->trimmed_range_in_parent().has_value()); + + SerializableObject::Retainer transition = new Transition; + error = OTIO_NS::ErrorStatus(); + assertFalse(transition->range_in_parent(&error).has_value()); + assertEqual(error.outcome, OTIO_NS::ErrorStatus::NOT_A_CHILD); + + error = OTIO_NS::ErrorStatus(); + assertFalse(transition->trimmed_range_in_parent(&error).has_value()); + assertEqual(error.outcome, OTIO_NS::ErrorStatus::NOT_A_CHILD); + assertFalse(transition->range_in_parent().has_value()); + assertFalse(transition->trimmed_range_in_parent().has_value()); + + SerializableObject::Retainer stack = new Stack; + error = OTIO_NS::ErrorStatus(); + assertEqual(stack->range_of_child(clip, &error), TimeRange()); + assertEqual(error.outcome, OTIO_NS::ErrorStatus::NOT_DESCENDED_FROM); + assertEqual(stack->range_of_child(clip), TimeRange()); + assertEqual( + stack->trimmed_range_of_child(clip).value(), + TimeRange()); + + error = OTIO_NS::ErrorStatus(); + stack->trimmed_range_of_child(clip, &error); + assertEqual( + error.outcome, + OTIO_NS::ErrorStatus::NOT_DESCENDED_FROM); + }); + tests.add_test("test_trimmed_range_of_nested_child", [] { SerializableObject::Retainer root = new Stack; SerializableObject::Retainer track = new Track; diff --git a/tests/test_composition.py b/tests/test_composition.py index 1c99d37e8..29214b49e 100755 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -702,6 +702,9 @@ def test_range_of_child_with_duration(self): with self.assertRaises(otio.exceptions.NotAChildError): otio.schema.Clip().trimmed_range_in_parent() + with self.assertRaises(otio.exceptions.NotAChildError): + otio.schema.Clip().range_in_parent() + def test_transformed_time(self): st = otio.schema.Stack( name="foo", @@ -1035,6 +1038,16 @@ def test_delete_parent_container(self): del sq self.assertIsNone(it.parent()) + def test_orphan_is_not_descended_from_composition(self): + stack = otio.schema.Stack() + orphan = otio.schema.Clip() + + with self.assertRaises(otio.exceptions.NotAChildError): + stack.range_of_child(orphan) + + with self.assertRaises(otio.exceptions.NotAChildError): + stack.trimmed_range_of_child(orphan) + def test_transactional(self): item = otio.core.Item() trackA = otio.core.Track()