From fbbe93021d7cd3793003911931bb3abd5e69596a Mon Sep 17 00:00:00 2001 From: Simon Marchi Date: Wed, 8 May 2019 16:55:37 -0400 Subject: [PATCH] bt2: Add bindings for trace classes This patch adds a Python wrapper for the bt_trace_class concept. Trace classes in Python are created using the _create_trace_class on a _UserComponent. A TraceClass is seen as a mapping of stream class ids to stream classes. This means that with trace class `tc`, it's possible to access its stream classes by id using `tc[id]`. It's also possible to iterate on `tc` to iterate on stream class ids. A brand new test is also added, along with some test utils that should be useful in future tests as well. It is not feature-complete yet, for example there is no way to instantiate a trace using this trace class, this will come in a subsequent patch (that will handle test_trace.py). It is possible to create stream classes in a trace class, but not with every possible option, just enough to support the trace class tests. Change-Id: Ib9f29d7ebc21b6ade19e9ffbad3f4b85e790321d Signed-off-by: Simon Marchi Signed-off-by: Francis Deslauriers Reviewed-on: https://review.lttng.org/c/babeltrace/+/1283 Tested-by: jenkins Reviewed-by: Philippe Proulx --- bindings/python/bt2/Makefile.am | 1 + bindings/python/bt2/bt2/__init__.py.in | 1 + bindings/python/bt2/bt2/component.py | 24 +++ .../python/bt2/bt2/native_bt_trace_class.i | 2 +- bindings/python/bt2/bt2/stream_class.py | 3 + bindings/python/bt2/bt2/trace_class.py | 193 ++++++++++++++++++ tests/bindings/python/bt2/test_trace_class.py | 148 ++++++++++++++ tests/bindings/python/bt2/utils.py | 40 ++++ 8 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 bindings/python/bt2/bt2/trace_class.py create mode 100644 tests/bindings/python/bt2/test_trace_class.py create mode 100644 tests/bindings/python/bt2/utils.py diff --git a/bindings/python/bt2/Makefile.am b/bindings/python/bt2/Makefile.am index bc5c7f18..90ee322e 100644 --- a/bindings/python/bt2/Makefile.am +++ b/bindings/python/bt2/Makefile.am @@ -55,6 +55,7 @@ STATIC_BINDINGS_DEPS = \ bt2/stream_class.py \ bt2/stream.py \ bt2/trace.py \ + bt2/trace_class.py \ bt2/trace_collection_message_iterator.py \ bt2/utils.py \ bt2/value.py diff --git a/bindings/python/bt2/bt2/__init__.py.in b/bindings/python/bt2/bt2/__init__.py.in index 7b866fbd..c46007a3 100644 --- a/bindings/python/bt2/bt2/__init__.py.in +++ b/bindings/python/bt2/bt2/__init__.py.in @@ -68,6 +68,7 @@ from bt2.query_executor import * from bt2.stream import _Stream from bt2.stream_class import * from bt2.trace import * +from bt2.trace_class import * from bt2.trace_collection_message_iterator import * from bt2.value import * from bt2.value import _Value diff --git a/bindings/python/bt2/bt2/component.py b/bindings/python/bt2/bt2/component.py index e41e00bd..fcd50785 100644 --- a/bindings/python/bt2/bt2/component.py +++ b/bindings/python/bt2/bt2/component.py @@ -620,9 +620,31 @@ class _UserComponent(metaclass=_UserComponentType): other_port_ptr, other_port_type) self._port_connected(port, other_port) + def _create_trace_class(self, env=None, uuid=None, + assigns_automatic_stream_class_id=True): + ptr = self._as_self_component_ptr(self._ptr) + tc_ptr = native_bt.trace_class_create(ptr) + + if tc_ptr is None: + raise bt2.CreationError('could not create trace class') + + tc = bt2.TraceClass._create_from_ptr(tc_ptr) + + if env is not None: + for key, value in env.items(): + tc.env[key] = value + + if uuid is not None: + tc._uuid = uuid + + tc._assigns_automatic_stream_class_id = assigns_automatic_stream_class_id + + return tc + class _UserSourceComponent(_UserComponent, _SourceComponent): _as_not_self_specific_component_ptr = staticmethod(native_bt.self_component_source_as_component_source) + _as_self_component_ptr = staticmethod(native_bt.self_component_source_as_self_component) @property def _output_ports(self): @@ -648,6 +670,7 @@ class _UserSourceComponent(_UserComponent, _SourceComponent): class _UserFilterComponent(_UserComponent, _FilterComponent): _as_not_self_specific_component_ptr = staticmethod(native_bt.self_component_filter_as_component_filter) + _as_self_component_ptr = staticmethod(native_bt.self_component_filter_as_self_component) @property def _output_ports(self): @@ -694,6 +717,7 @@ class _UserFilterComponent(_UserComponent, _FilterComponent): class _UserSinkComponent(_UserComponent, _SinkComponent): _as_not_self_specific_component_ptr = staticmethod(native_bt.self_component_sink_as_component_sink) + _as_self_component_ptr = staticmethod(native_bt.self_component_sink_as_self_component) @property def _input_ports(self): diff --git a/bindings/python/bt2/bt2/native_bt_trace_class.i b/bindings/python/bt2/bt2/native_bt_trace_class.i index bd023393..71061ffa 100644 --- a/bindings/python/bt2/bt2/native_bt_trace_class.i +++ b/bindings/python/bt2/bt2/native_bt_trace_class.i @@ -46,7 +46,7 @@ extern uint64_t bt_trace_class_get_environment_entry_count( extern void bt_trace_class_borrow_environment_entry_by_index_const( const bt_trace_class *trace_class, uint64_t index, - const char **OUT, const bt_value **OUT_VALUE); + const char **OUT, const bt_value **OUT); extern const bt_value * bt_trace_class_borrow_environment_entry_value_by_name_const( diff --git a/bindings/python/bt2/bt2/stream_class.py b/bindings/python/bt2/bt2/stream_class.py index 97cd593f..e94c3b22 100644 --- a/bindings/python/bt2/bt2/stream_class.py +++ b/bindings/python/bt2/bt2/stream_class.py @@ -49,6 +49,9 @@ class _EventClassIterator(collections.abc.Iterator): class StreamClass(object._SharedObject, collections.abc.Mapping): + _get_ref = staticmethod(native_bt.stream_class_get_ref) + _put_ref = staticmethod(native_bt.stream_class_put_ref) + def __init__(self, name=None, id=None, packet_context_field_class=None, event_header_field_class=None, event_context_field_class=None, event_classes=None): diff --git a/bindings/python/bt2/bt2/trace_class.py b/bindings/python/bt2/bt2/trace_class.py new file mode 100644 index 00000000..1f473a30 --- /dev/null +++ b/bindings/python/bt2/bt2/trace_class.py @@ -0,0 +1,193 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 Philippe Proulx +# Copyright (c) 2018 Francis Deslauriers +# Copyright (c) 2019 Simon Marchi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__all__ = ['TraceClass'] + +import bt2 +from bt2 import native_bt, utils, object +import uuid as uuidp +import collections.abc +import functools + + +class _TraceClassEnv(collections.abc.MutableMapping): + def __init__(self, trace_class): + self._trace_class = trace_class + + def __getitem__(self, key): + utils._check_str(key) + + borrow_entry_fn = native_bt.trace_class_borrow_environment_entry_value_by_name_const + value_ptr = borrow_entry_fn(self._trace_class._ptr, key) + + if value_ptr is None: + raise KeyError(key) + + return bt2.value._create_from_ptr_and_get_ref(value_ptr) + + def __setitem__(self, key, value): + if isinstance(value, str): + set_env_entry_fn = native_bt.trace_class_set_environment_entry_string + elif isinstance(value, int): + set_env_entry_fn = native_bt.trace_class_set_environment_entry_integer + else: + raise TypeError('expected str or int, got {}'.format(type(value))) + + ret = set_env_entry_fn(self._trace_class._ptr, key, value) + + utils._handle_ret(ret, "cannot set trace class object's environment entry") + + def __delitem__(self, key): + raise NotImplementedError + + def __len__(self): + count = native_bt.trace_class_get_environment_entry_count(self._trace_class._ptr) + assert count >= 0 + return count + + def __iter__(self): + trace_class_ptr = self._trace_class_env._trace_class._ptr + + for idx in range(len(self)): + borrow_entry_fn = native_bt.trace_class_borrow_environment_entry_by_index_const + entry_name, _ = borrow_entry_fn(trace_class_ptr, idx) + assert entry_name is not None + yield entry_name + + +class _StreamClassIterator(collections.abc.Iterator): + def __init__(self, trace_class): + self._trace_class = trace_class + self._at = 0 + + def __next__(self): + if self._at == len(self._trace_class): + raise StopIteration + + borrow_stream_class_fn = native_bt.trace_class_borrow_stream_class_by_index_const + sc_ptr = borrow_stream_class_fn(self._trace_class._ptr, self._at) + assert sc_ptr + id = native_bt.stream_class_get_id(sc_ptr) + assert id >= 0 + self._at += 1 + return id + + +def _trace_class_destruction_listener_from_native(user_listener, trace_class_ptr): + trace_class = bt2.trace_class.TraceClass._create_from_ptr_and_get_ref(trace_class_ptr) + user_listener(trace_class) + + +class TraceClass(object._SharedObject, collections.abc.Mapping): + _get_ref = staticmethod(native_bt.trace_class_get_ref) + _put_ref = staticmethod(native_bt.trace_class_put_ref) + + @property + def uuid(self): + uuid_bytes = native_bt.trace_class_get_uuid(self._ptr) + if uuid_bytes is None: + return + + return uuidp.UUID(bytes=uuid_bytes) + + def _uuid(self, uuid): + utils._check_type(uuid, uuidp.UUID) + native_bt.trace_class_set_uuid(self._ptr, uuid.bytes) + + _uuid = property(fset=_uuid) + + # Number of stream classes in this trace class. + + def __len__(self): + count = native_bt.trace_class_get_stream_class_count(self._ptr) + assert count >= 0 + return count + + # Get a stream class by stream id. + + def __getitem__(self, key): + utils._check_uint64(key) + + sc_ptr = native_bt.trace_class_borrow_stream_class_by_id_const(self._ptr, key) + if sc_ptr is None: + raise KeyError(key) + + return bt2.StreamClass._create_from_ptr_and_get_ref(sc_ptr) + + def __iter__(self): + for idx in range(len(self)): + sc_ptr = native_bt.trace_class_borrow_stream_class_by_index_const(self._ptr, idx) + assert sc_ptr is not None + + id = native_bt.stream_class_get_id(sc_ptr) + assert id >= 0 + + yield id + + @property + def env(self): + return _TraceClassEnv(self) + + def create_stream_class(self, id=None): + + if self.assigns_automatic_stream_class_id: + if id is not None: + raise bt2.CreationError('id provided, but trace class assigns automatic stream class ids') + + sc_ptr = native_bt.stream_class_create(self._ptr) + else: + if id is None: + raise bt2.CreationError('id not provided, but trace class does not assign automatic stream class ids') + + utils._check_uint64(id) + sc_ptr = native_bt.stream_class_create_with_id(self._ptr, id) + + return bt2.StreamClass._create_from_ptr(sc_ptr) + + @property + def assigns_automatic_stream_class_id(self): + return native_bt.trace_class_assigns_automatic_stream_class_id(self._ptr) + + def _assigns_automatic_stream_class_id(self, auto_id): + utils._check_bool(auto_id) + return native_bt.trace_class_set_assigns_automatic_stream_class_id(self._ptr, auto_id) + + _assigns_automatic_stream_class_id = property(fset=_assigns_automatic_stream_class_id) + + # Add a listener to be called when the trace class is destroyed. + + def add_destruction_listener(self, listener): + + if not callable(listener): + raise TypeError("'listener' parameter is not callable") + + fn = native_bt.py3_trace_class_add_destruction_listener + listener_from_native = functools.partial(_trace_class_destruction_listener_from_native, + listener) + + listener_id = fn(self._ptr, listener_from_native) + if listener_id is None: + utils._raise_bt2_error('cannot add destruction listener to trace class object') + + return bt2._ListenerHandle(listener_id, self) diff --git a/tests/bindings/python/bt2/test_trace_class.py b/tests/bindings/python/bt2/test_trace_class.py new file mode 100644 index 00000000..265b0de8 --- /dev/null +++ b/tests/bindings/python/bt2/test_trace_class.py @@ -0,0 +1,148 @@ +import uuid +import unittest +import bt2 +from utils import run_in_component_init, get_default_trace_class + + +class TraceClassTestCase(unittest.TestCase): + + def test_create_default(self): + def f(comp_self): + return comp_self._create_trace_class() + + tc = run_in_component_init(f) + + self.assertEqual(len(tc), 0) + self.assertEqual(len(tc.env), 0) + self.assertIsNone(tc.uuid) + self.assertTrue(tc.assigns_automatic_stream_class_id) + + def test_uuid(self): + def f(comp_self): + return comp_self._create_trace_class(uuid=uuid.UUID('da7d6b6f-3108-4706-89bd-ab554732611b')) + + tc = run_in_component_init(f) + + self.assertEqual(tc.uuid, uuid.UUID('da7d6b6f-3108-4706-89bd-ab554732611b')) + + def test_automatic_stream_class_id(self): + def f(comp_self): + return comp_self._create_trace_class(assigns_automatic_stream_class_id=True) + + tc = run_in_component_init(f) + self.assertTrue(tc.assigns_automatic_stream_class_id) + + # This should not throw. + sc1 = tc.create_stream_class() + sc2 = tc.create_stream_class() + + self.assertNotEqual(sc1.id, sc2.id) + + def test_automatic_stream_class_id_raises(self): + def f(comp_self): + return comp_self._create_trace_class(assigns_automatic_stream_class_id=True) + + tc = run_in_component_init(f) + self.assertTrue(tc.assigns_automatic_stream_class_id) + + with self.assertRaises(bt2.CreationError): + sc1 = tc.create_stream_class(23) + + def test_no_assigns_automatic_stream_class_id(self): + def f(comp_self): + return comp_self._create_trace_class(assigns_automatic_stream_class_id=False) + + tc = run_in_component_init(f) + self.assertFalse(tc.assigns_automatic_stream_class_id) + + sc = tc.create_stream_class(id=28) + self.assertEqual(sc.id, 28) + + def test_no_assigns_automatic_stream_class_id_raises(self): + def f(comp_self): + return comp_self._create_trace_class(assigns_automatic_stream_class_id=False) + + tc = run_in_component_init(f) + self.assertFalse(tc.assigns_automatic_stream_class_id) + + # In this mode, it is required to pass an explicit id. + with self.assertRaises(bt2.CreationError): + tc.create_stream_class() + + def test_env_get(self): + def f(comp_self): + return comp_self._create_trace_class(env={'hello': 'you', 'foo': -5}) + + tc = run_in_component_init(f) + + self.assertEqual(tc.env['hello'], 'you') + self.assertEqual(tc.env['foo'], -5) + + def test_env_get_non_existent(self): + def f(comp_self): + return comp_self._create_trace_class(env={'hello': 'you', 'foo': -5}) + + tc = run_in_component_init(f) + + with self.assertRaises(KeyError): + tc.env['lel'] + + @staticmethod + def _create_trace_class_with_some_stream_classes(): + def f(comp_self): + return comp_self._create_trace_class(assigns_automatic_stream_class_id=False) + + tc = run_in_component_init(f) + sc1 = tc.create_stream_class(id=12) + sc2 = tc.create_stream_class(id=54) + sc3 = tc.create_stream_class(id=2018) + return tc, sc1, sc2, sc3 + + def test_getitem(self): + tc, _, _, sc3 = self._create_trace_class_with_some_stream_classes() + self.assertEqual(tc[2018].addr, sc3.addr) + + def test_getitem_wrong_key_type(self): + tc, _, _, _ = self._create_trace_class_with_some_stream_classes() + with self.assertRaises(TypeError): + tc['hello'] + + def test_getitem_wrong_key(self): + tc, _, _, _ = self._create_trace_class_with_some_stream_classes() + with self.assertRaises(KeyError): + tc[4] + + def test_len(self): + tc = get_default_trace_class() + self.assertEqual(len(tc), 0) + tc.create_stream_class() + self.assertEqual(len(tc), 1) + + def test_iter(self): + tc, sc1, sc2, sc3 = self._create_trace_class_with_some_stream_classes() + + for sc_id, stream_class in tc.items(): + self.assertIsInstance(stream_class, bt2.stream_class.StreamClass) + + if sc_id == 12: + self.assertEqual(stream_class.addr, sc1.addr) + elif sc_id == 54: + self.assertEqual(stream_class.addr, sc2.addr) + elif sc_id == 2018: + self.assertEqual(stream_class.addr, sc3.addr) + + def test_destruction_listener(self): + def on_trace_class_destruction(trace_class): + nonlocal trace_class_destroyed + trace_class_destroyed = True + + trace_class_destroyed = False + + trace_class = get_default_trace_class() + trace_class.add_destruction_listener(on_trace_class_destruction) + + self.assertFalse(trace_class_destroyed) + + del trace_class + + self.assertTrue(trace_class_destroyed) diff --git a/tests/bindings/python/bt2/utils.py b/tests/bindings/python/bt2/utils.py new file mode 100644 index 00000000..768462ec --- /dev/null +++ b/tests/bindings/python/bt2/utils.py @@ -0,0 +1,40 @@ +import bt2 + +# Run callable `func` in the context of a component's __init__ method. The +# callable is passed the Component being instantiated. +# +# The value returned by the callable is returned by run_in_component_init. + +def run_in_component_init(func): + class MySink(bt2._UserSinkComponent): + def __init__(self, params): + nonlocal res_bound + res_bound = func(self) + + def _consume(self): + pass + + g = bt2.Graph() + res_bound = None + g.add_component(MySink, 'comp') + + # We deliberately use a different variable for returning the result than + # the variable bound to the MySink.__init__ context and delete res_bound. + # The MySink.__init__ context stays alive until the end of the program, so + # if res_bound were to still point to our result, it would contribute an + # unexpected reference to the refcount of the result, from the point of view + # of the user of this function. It would then affect destruction tests, + # for example, which want to test what happens when the refcount of a Python + # object reaches 0. + + res = res_bound + del res_bound + return res + +# Create an empty trace class with default values. + +def get_default_trace_class(): + def f(comp_self): + return comp_self._create_trace_class() + + return run_in_component_init(f) -- 2.34.1