diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/Makefile.am | 2 | ||||
-rw-r--r-- | tests/Makefile.in | 2 | ||||
-rw-r--r-- | tests/test_generictreemodel.py | 406 | ||||
-rw-r--r-- | tests/test_gi.py | 6 | ||||
-rw-r--r-- | tests/test_object_marshaling.py | 616 | ||||
-rw-r--r-- | tests/test_overrides_gtk.py | 123 |
6 files changed, 1114 insertions, 41 deletions
diff --git a/tests/Makefile.am b/tests/Makefile.am index 1d40539..287542d 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -92,6 +92,7 @@ EXTRA_DIST = \ test_internal_api.py \ test_iochannel.py \ test_mainloop.py \ + test_object_marshaling.py \ test_option.py \ test_properties.py \ test_signal.py \ @@ -107,6 +108,7 @@ EXTRA_DIST = \ test_overrides_gdk.py \ test_overrides_gtk.py \ test_atoms.py \ + test_generictreemodel.py \ compat_test_pygtk.py \ gi/__init__.py \ gi/overrides/__init__.py \ diff --git a/tests/Makefile.in b/tests/Makefile.in index a29792b..5c8f709 100644 --- a/tests/Makefile.in +++ b/tests/Makefile.in @@ -337,6 +337,7 @@ EXTRA_DIST = \ test_internal_api.py \ test_iochannel.py \ test_mainloop.py \ + test_object_marshaling.py \ test_option.py \ test_properties.py \ test_signal.py \ @@ -352,6 +353,7 @@ EXTRA_DIST = \ test_overrides_gdk.py \ test_overrides_gtk.py \ test_atoms.py \ + test_generictreemodel.py \ compat_test_pygtk.py \ gi/__init__.py \ gi/overrides/__init__.py \ diff --git a/tests/test_generictreemodel.py b/tests/test_generictreemodel.py new file mode 100644 index 0000000..ff0f523 --- /dev/null +++ b/tests/test_generictreemodel.py @@ -0,0 +1,406 @@ +# -*- Mode: Python; py-indent-offset: 4 -*- +# test_generictreemodel - Tests for GenericTreeModel +# Copyright (C) 2013 Simon Feltman +# +# test_generictreemodel.py: Tests for GenericTreeModel +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + + +# system +import gc +import sys +import weakref +import unittest + +# pygobject +from gi.repository import GObject +from gi.repository import Gtk +from pygtkcompat.generictreemodel import GenericTreeModel +from pygtkcompat.generictreemodel import _get_user_data_as_pyobject + + +class Node(object): + """Represents a generic node with name, value, and children.""" + def __init__(self, name, value, *children): + self.name = name + self.value = value + self.children = list(children) + self.parent = None + self.next = None + + for i, child in enumerate(children): + child.parent = weakref.ref(self) + if i < len(children) - 1: + child.next = weakref.ref(children[i + 1]) + + def __repr__(self): + return 'Node("%s", %s)' % (self.name, self.value) + + +class TesterModel(GenericTreeModel): + def __init__(self): + super(TesterModel, self).__init__() + self.root = Node('root', 0, + Node('spam', 1, + Node('sushi', 2), + Node('bread', 3) + ), + Node('eggs', 4) + ) + + def on_get_flags(self): + return 0 + + def on_get_n_columns(self): + return 2 + + def on_get_column_type(self, n): + return (str, int)[n] + + def on_get_iter(self, path): + node = self.root + path = list(path) + idx = path.pop(0) + while path: + idx = path.pop(0) + node = node.children[idx] + return node + + def on_get_path(self, node): + def rec_get_path(n): + for i, child in enumerate(n.children): + if child == node: + return [i] + else: + res = rec_get_path(child) + if res: + res.insert(0, i) + + return rec_get_path(self.root) + + def on_get_value(self, node, column): + if column == 0: + return node.name + elif column == 1: + return node.value + + def on_iter_has_child(self, node): + return bool(node.children) + + def on_iter_next(self, node): + if node.next: + return node.next() + + def on_iter_children(self, node): + if node: + return node.children[0] + else: + return self.root + + def on_iter_n_children(self, node): + if node is None: + return 1 + return len(node.children) + + def on_iter_nth_child(self, node, n): + if node is None: + assert n == 0 + return self.root + return node.children[n] + + def on_iter_parent(self, child): + if child.parent: + return child.parent() + + +class TestReferences(unittest.TestCase): + def setUp(self): + pass + + def test_c_tree_iter_user_data_as_pyobject(self): + obj = object() + obj_id = id(obj) + ref_count = sys.getrefcount(obj) + + # This is essentially a stolen ref in the context of _CTreeIter.get_user_data_as_pyobject + it = Gtk.TreeIter() + it.user_data = obj_id + + obj2 = _get_user_data_as_pyobject(it) + self.assertEqual(obj, obj2) + self.assertEqual(sys.getrefcount(obj), ref_count + 1) + + def test_leak_references_on(self): + model = TesterModel() + obj_ref = weakref.ref(model.root) + # Initial refcount is 1 for model.root + the temporary + self.assertEqual(sys.getrefcount(model.root), 2) + + # Iter increases by 1 do to assignment to iter.user_data + res, it = model.do_get_iter([0]) + self.assertEqual(id(model.root), it.user_data) + self.assertEqual(sys.getrefcount(model.root), 3) + + # Verify getting a TreeIter more then once does not further increase + # the ref count. + res2, it2 = model.do_get_iter([0]) + self.assertEqual(id(model.root), it2.user_data) + self.assertEqual(sys.getrefcount(model.root), 3) + + # Deleting the iter does not decrease refcount because references + # leak by default (they are stored in the held_refs pool) + del it + gc.collect() + self.assertEqual(sys.getrefcount(model.root), 3) + + # Deleting a model should free all held references to user data + # stored by TreeIters + del model + gc.collect() + self.assertEqual(obj_ref(), None) + + def test_row_deleted_frees_refs(self): + model = TesterModel() + obj_ref = weakref.ref(model.root) + # Initial refcount is 1 for model.root + the temporary + self.assertEqual(sys.getrefcount(model.root), 2) + + # Iter increases by 1 do to assignment to iter.user_data + res, it = model.do_get_iter([0]) + self.assertEqual(id(model.root), it.user_data) + self.assertEqual(sys.getrefcount(model.root), 3) + + # Notifying the underlying model of a row_deleted should decrease the + # ref count. + model.row_deleted(Gtk.TreePath('0'), model.root) + self.assertEqual(sys.getrefcount(model.root), 2) + + # Finally deleting the actual object should collect it completely + del model.root + gc.collect() + self.assertEqual(obj_ref(), None) + + def test_leak_references_off(self): + model = TesterModel() + model.leak_references = False + + obj_ref = weakref.ref(model.root) + # Initial refcount is 1 for model.root + the temporary + self.assertEqual(sys.getrefcount(model.root), 2) + + # Iter does not increas count by 1 when leak_references is false + res, it = model.do_get_iter([0]) + self.assertEqual(id(model.root), it.user_data) + self.assertEqual(sys.getrefcount(model.root), 2) + + # Deleting the iter does not decrease refcount because assigning user_data + # eats references and does not release them. + del it + gc.collect() + self.assertEqual(sys.getrefcount(model.root), 2) + + # Deleting the model decreases the final ref, and the object is collected + del model + gc.collect() + self.assertEqual(obj_ref(), None) + + def test_iteration_refs(self): + # Pull iterators off the model using the wrapped C API which will + # then call back into the python overrides. + model = TesterModel() + nodes = [node for node in model.iter_depth_first()] + values = [node.value for node in nodes] + + # Verify depth first ordering + self.assertEqual(values, [0, 1, 2, 3, 4]) + + # Verify ref counts for each of the nodes. + # 5 refs for each node at this point: + # 1 - ref held in getrefcount function + # 2 - ref held by "node" var during iteration + # 3 - ref held by local "nodes" var + # 4 - ref held by the root/children graph itself + # 5 - ref held by the model "held_refs" instance var + for node in nodes: + self.assertEqual(sys.getrefcount(node), 5) + + # A second iteration and storage of the nodes in a new list + # should only increase refcounts by 1 even though new + # iterators are created and assigned. + nodes2 = [node for node in model.iter_depth_first()] + for node in nodes2: + self.assertEqual(sys.getrefcount(node), 6) + + # Hold weak refs and start verifying ref collection. + node_refs = [weakref.ref(node) for node in nodes] + + # First round of collection + del nodes2 + gc.collect() + for node in nodes: + self.assertEqual(sys.getrefcount(node), 5) + + # Second round of collection, no more local lists of nodes. + del nodes + gc.collect() + for ref in node_refs: + node = ref() + self.assertEqual(sys.getrefcount(node), 4) + + # Using invalidate_iters or row_deleted(path, node) will clear out + # the pooled refs held internal to the GenericTreeModel implementation. + model.invalidate_iters() + self.assertEqual(len(model._held_refs), 0) + gc.collect() + for ref in node_refs: + node = ref() + self.assertEqual(sys.getrefcount(node), 3) + + # Deleting the root node at this point should allow all nodes to be collected + # as there is no longer a way to reach the children + del node # node still in locals() from last iteration + del model.root + gc.collect() + for ref in node_refs: + self.assertEqual(ref(), None) + + +class TestIteration(unittest.TestCase): + def test_iter_next_root(self): + model = TesterModel() + it = model.get_iter([0]) + self.assertEqual(it.user_data, id(model.root)) + self.assertEqual(model.root.next, None) + + it = model.iter_next(it) + self.assertEqual(it, None) + + def test_iter_next_multiple(self): + model = TesterModel() + it = model.get_iter([0, 0]) + self.assertEqual(it.user_data, id(model.root.children[0])) + + it = model.iter_next(it) + self.assertEqual(it.user_data, id(model.root.children[1])) + + it = model.iter_next(it) + self.assertEqual(it, None) + + +class ErrorModel(GenericTreeModel): + # All on_* methods will raise a NotImplementedError by default + pass + + +class ExceptHook(object): + """ + Temporarily installs an exception hook in a context which + expects the given exc_type to be raised. This allows verification + of exceptions that occur within python gi callbacks but + are never bubbled through from python to C back to python. + This works because exception hooks are called in PyErr_Print. + """ + def __init__(self, exc_type): + self._exc_type = exc_type + self._exceptions = [] + + def _excepthook(self, exc_type, value, traceback): + self._exceptions.append(exc_type) + + def __enter__(self): + self._oldhook = sys.excepthook + sys.excepthook = self._excepthook + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + sys.excepthook = self._oldhook + assert len(self._exceptions) == 1, 'Expecting exactly one exception of type %s' % self._exc_type + assert issubclass(self._exceptions[0], self._exc_type), 'Expecting exactly one exception of type %s' % self._exc_type + + +class TestReturnsAfterError(unittest.TestCase): + def setUp(self): + self.model = ErrorModel() + + def test_get_flags(self): + with ExceptHook(NotImplementedError): + flags = self.model.get_flags() + self.assertEqual(flags, 0) + + def test_get_n_columns(self): + with ExceptHook(NotImplementedError): + count = self.model.get_n_columns() + self.assertEqual(count, 0) + + def test_get_column_type(self): + with ExceptHook(NotImplementedError): + col_type = self.model.get_column_type(0) + self.assertEqual(col_type, GObject.TYPE_INVALID) + + def test_get_iter(self): + with ExceptHook(NotImplementedError): + self.assertRaises(ValueError, self.model.get_iter, Gtk.TreePath(0)) + + def test_get_path(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + path = self.model.get_path(it) + self.assertEqual(path, None) + + def test_get_value(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + try: + self.model.get_value(it, 0) + except TypeError: + pass # silence TypeError converting None to GValue + + def test_iter_has_child(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + res = self.model.iter_has_child(it) + self.assertEqual(res, False) + + def test_iter_next(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + res = self.model.iter_next(it) + self.assertEqual(res, None) + + def test_iter_children(self): + with ExceptHook(NotImplementedError): + res = self.model.iter_children(None) + self.assertEqual(res, None) + + def test_iter_n_children(self): + with ExceptHook(NotImplementedError): + res = self.model.iter_n_children(None) + self.assertEqual(res, 0) + + def test_iter_nth_child(self): + with ExceptHook(NotImplementedError): + res = self.model.iter_nth_child(None, 0) + self.assertEqual(res, None) + + def test_iter_parent(self): + child = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + res = self.model.iter_parent(child) + self.assertEqual(res, None) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_gi.py b/tests/test_gi.py index ee2f3de..4545539 100644 --- a/tests/test_gi.py +++ b/tests/test_gi.py @@ -2785,6 +2785,12 @@ class TestPropertiesObject(unittest.TestCase): self.assertEqual(obj.props.some_variant.get_type_string(), 'b') self.assertEqual(obj.props.some_variant.get_boolean(), True) + def test_setting_several_properties(self): + obj = GIMarshallingTests.PropertiesObject() + obj.set_properties(some_uchar=54, some_int=42) + self.assertEqual(42, obj.props.some_int) + self.assertEqual(54, obj.props.some_uchar) + class TestKeywords(unittest.TestCase): def test_method(self): diff --git a/tests/test_object_marshaling.py b/tests/test_object_marshaling.py new file mode 100644 index 0000000..62570bc --- /dev/null +++ b/tests/test_object_marshaling.py @@ -0,0 +1,616 @@ +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab + +import unittest +import weakref +import gc +import sys +import warnings + +from gi.repository import GObject +from gi.repository import GIMarshallingTests + + +class StrongRef(object): + # A class that behaves like weakref.ref but holds a strong reference. + # This allows re-use of the VFuncsBase by swapping out the ObjectRef + # class var with either weakref.ref or StrongRef. + + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return self.obj + + +class VFuncsBase(GIMarshallingTests.Object): + # Class which generically implements the vfuncs used for reference counting tests + # in a way that can be easily sub-classed and modified. + + #: Object type used by this class for testing + #: This can be GObject.Object or GObject.InitiallyUnowned + Object = GObject.Object + + #: Reference type used by this class for holding refs to in/out objects. + #: This can be set to weakref.ref or StrongRef + ObjectRef = weakref.ref + + def __init__(self): + super(VFuncsBase, self).__init__() + + #: Hold ref of input or output python wrappers + self.object_ref = None + + #: store grefcount of input object + self.in_object_grefcount = None + self.in_object_is_floating = None + + def do_vfunc_return_object_transfer_none(self): + # Return an object but keep a python reference to it. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_return_object_transfer_full(self): + # Return an object and hand off the reference to the caller. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_out_object_transfer_none(self): + # Same as do_vfunc_return_object_transfer_none but the pygi + # internals convert the return here into an out arg. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_out_object_transfer_full(self): + # Same as do_vfunc_return_object_transfer_full but the pygi + # internals convert the return here into an out arg. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_in_object_transfer_none(self, obj): + # 'obj' will have a python wrapper as well as still held + # by the caller. + self.object_ref = self.ObjectRef(obj) + self.in_object_grefcount = obj.__grefcount__ + self.in_object_is_floating = obj.is_floating() + + def do_vfunc_in_object_transfer_full(self, obj): + # 'obj' will now be owned by the Python GObject wrapper. + # When obj goes out of scope and is collected, the GObject + # should also be fully released. + self.object_ref = self.ObjectRef(obj) + self.in_object_grefcount = obj.__grefcount__ + self.in_object_is_floating = obj.is_floating() + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithObjectArg(unittest.TestCase): + # Basic set of tests which work on non-floating objects which python does + # not keep an additional reference of. + + class VFuncs(VFuncsBase): + # Object for testing non-floating objects without holding any refs. + Object = GObject.Object + ObjectRef = weakref.ref + + def test_vfunc_self_arg_ref_count(self): + # Check to make sure vfunc "self" arguments don't leak. + vfuncs = self.VFuncs() + vfuncs_ref = weakref.ref(vfuncs) + vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() # Use any vfunc to test this. + + gc.collect() + self.assertEqual(sys.getrefcount(vfuncs), 2) + self.assertEqual(vfuncs.__grefcount__, 1) + + del vfuncs + gc.collect() + self.assertTrue(vfuncs_ref() is None) + + def test_vfunc_return_object_transfer_none(self): + # This tests a problem case where the vfunc returns a GObject owned solely by Python + # but the argument is marked as transfer-none. + # In this case pygobject marshaling adds an additional ref and gives a warning + # of a potential leak. If this occures it is really a bug in the underlying library + # but pygobject tries to react to this in a reasonable way. + vfuncs = self.VFuncs() + with warnings.catch_warnings(record=True) as warn: + warnings.simplefilter('always') + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + self.assertTrue(issubclass(warn[0].category, RuntimeWarning)) + + # The ref count of the GObject returned to the caller (get_ref_info_for_vfunc_return_object_transfer_none) + # should be a single floating ref + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_none(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + with warnings.catch_warnings(record=True) as warn: + warnings.simplefilter('always') + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + self.assertTrue(issubclass(warn[0].category, RuntimeWarning)) + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_return_object_transfer_full(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # The vfunc caller receives full ownership of a single ref which should not + # be floating. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_full(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_none(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + + gc.collect() + self.assertEqual(vfuncs.in_object_grefcount, 2) # initial + python wrapper + self.assertFalse(vfuncs.in_object_is_floating) + + self.assertEqual(ref_count, 1) # ensure python wrapper released + self.assertFalse(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_full(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + + gc.collect() + + # python wrapper should take sole ownership of the gobject + self.assertEqual(vfuncs.in_object_grefcount, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + # ensure python wrapper took ownership and released, after vfunc was complete + self.assertEqual(ref_count, 0) + self.assertFalse(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithFloatingArg(unittest.TestCase): + # All tests here work with a floating object by using InitiallyUnowned as the argument + + class VFuncs(VFuncsBase): + # Object for testing non-floating objects without holding any refs. + Object = GObject.InitiallyUnowned + ObjectRef = weakref.ref + + def test_vfunc_return_object_transfer_none_with_floating(self): + # Python is expected to return a single floating reference without warning. + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_none() + + # The ref count of the GObject returned to the caller (get_ref_info_for_vfunc_return_object_transfer_none) + # should be a single floating ref + self.assertEqual(ref_count, 1) + self.assertTrue(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_none_with_floating(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + + self.assertEqual(ref_count, 1) + self.assertTrue(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_return_object_transfer_full_with_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # The vfunc caller receives full ownership of a single ref. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_full_with_floating(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_none_with_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + + gc.collect() + + # python wrapper should maintain the object as floating and add an additional ref + self.assertEqual(vfuncs.in_object_grefcount, 2) + self.assertTrue(vfuncs.in_object_is_floating) + + # vfunc caller should only have a single floating ref after the vfunc finishes + self.assertEqual(ref_count, 1) + self.assertTrue(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_full_with_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + + gc.collect() + + # python wrapper sinks and owns the gobject + self.assertEqual(vfuncs.in_object_grefcount, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + # ensure python wrapper took ownership and released + self.assertEqual(ref_count, 0) + self.assertFalse(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithHeldObjectArg(unittest.TestCase): + # Same tests as TestVFuncsWithObjectArg except we hold + # onto the python object reference in all cases. + + class VFuncs(VFuncsBase): + # Object for testing non-floating objects with a held ref. + Object = GObject.Object + ObjectRef = StrongRef + + def test_vfunc_return_object_transfer_none_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_none() + + # Python holds the single gobject ref in 'vfuncs.object_ref' + # Because of this, we do not expect a floating ref or a ref increase. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + # The actual grefcount should stay at 1 even after the vfunc return. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_none_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_return_object_transfer_full_with_held_object(self): + # The vfunc caller receives full ownership which should not + # be floating. However, the held python wrapper also has a ref. + + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + # The vfunc caller receives a new reference which should not + # be floating. However, the held python wrapper also has a ref. + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # The vfunc caller should have decremented its reference. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_full_with_held_object(self): + # Same test as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + # The vfunc caller receives a new reference which should not + # be floating. However, the held python wrapper also has a ref. + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # The vfunc caller should have decremented its reference. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_object_transfer_none_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + + gc.collect() + + # Ref count inside vfunc from the perspective of Python + self.assertEqual(vfuncs.in_object_grefcount, 2) # initial + python wrapper + self.assertFalse(vfuncs.in_object_is_floating) + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 2) # kept after vfunc + held python wrapper + self.assertFalse(is_floating) + + # Current ref count after C cleans up its reference + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_object_transfer_full_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + + gc.collect() + + # Ref count inside vfunc from the perspective of Python + self.assertEqual(vfuncs.in_object_grefcount, 1) # python wrapper takes ownership of the gobject + self.assertFalse(vfuncs.in_object_is_floating) + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + # Current ref count + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithHeldFloatingArg(unittest.TestCase): + # Tests for a floating object which we hold a reference to the python wrapper + # on the VFuncs test class. + + class VFuncs(VFuncsBase): + # Object for testing floating objects with a held ref. + Object = GObject.InitiallyUnowned + ObjectRef = StrongRef + + def test_vfunc_return_object_transfer_none_with_held_floating(self): + # Python holds onto the wrapper which basically means the floating ref + # should also be owned by python. + + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_none() + + # This is a borrowed ref from what is held in python. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + # The actual grefcount should stay at 1 even after the vfunc return. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_none_with_held_floating(self): + # Same as above + + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_return_object_transfer_full_with_held_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # vfunc wrapper destroyes ref it was given + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_full_with_held_floating(self): + # Same test as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # vfunc wrapper destroyes ref it was given + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_floating_transfer_none_with_held_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + gc.collect() + + # Ref count inside vfunc from the perspective of Python + self.assertTrue(vfuncs.in_object_is_floating) + self.assertEqual(vfuncs.in_object_grefcount, 2) # python wrapper sinks and owns the gobject + + # Ref count from the perspective of C after the vfunc is called + self.assertTrue(is_floating) + self.assertEqual(ref_count, 2) # floating + held by wrapper + + # Current ref count after C cleans up its reference + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_floating_transfer_full_with_held_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + gc.collect() + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(vfuncs.in_object_grefcount, 1) # python wrapper sinks and owns the gobject + self.assertFalse(vfuncs.in_object_is_floating) + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 1) # held by wrapper + self.assertFalse(is_floating) + + # Current ref count + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + +class TestPropertyHoldingObject(unittest.TestCase): + @unittest.expectedFailure # https://bugzilla.gnome.org/show_bug.cgi?id=675726 + def test_props_getter_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + holder.set_property('some-object', held) + self.assertEqual(holder.__grefcount__, 1) + + initial_ref_count = held.__grefcount__ + holder.props.some_object + gc.collect() + self.assertEqual(held.__grefcount__, initial_ref_count) + + @unittest.skipUnless(hasattr(GIMarshallingTests.PropertiesObject.props, 'some_object'), + 'too old gobject-introspection') + def test_get_property_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + holder.set_property('some-object', held) + self.assertEqual(holder.__grefcount__, 1) + + initial_ref_count = held.__grefcount__ + holder.get_property('some-object') + gc.collect() + self.assertEqual(held.__grefcount__, initial_ref_count) + + @unittest.expectedFailure # https://bugzilla.gnome.org/show_bug.cgi?id=675726 + def test_props_setter_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + # Setting property should only increase ref count by 1 + holder.props.some_object = held + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 2) + + # Clearing should pull it back down + holder.props.some_object = None + self.assertEqual(held.__grefcount__, 1) + + @unittest.expectedFailure # https://bugzilla.gnome.org/show_bug.cgi?id=675726 + def test_set_property_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + # Setting property should only increase ref count by 1 + holder.set_property('some-object', held) + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 2) + + # Clearing should pull it back down + holder.set_property('some-object', None) + self.assertEqual(held.__grefcount__, 1) diff --git a/tests/test_overrides_gtk.py b/tests/test_overrides_gtk.py index 9315019..69f0d38 100644 --- a/tests/test_overrides_gtk.py +++ b/tests/test_overrides_gtk.py @@ -2,6 +2,7 @@ # coding: UTF-8 # vim: tabstop=4 shiftwidth=4 expandtab +import contextlib import unittest from compathelper import _unicode, _bytes @@ -17,6 +18,38 @@ except ImportError: Gtk = None +@contextlib.contextmanager +def realized(widget): + """Makes sure the widget is realized. + + view = Gtk.TreeView() + with realized(view): + do_something(view) + """ + + if isinstance(widget, Gtk.Window): + toplevel = widget + else: + toplevel = widget.get_parent_window() + + if toplevel is None: + window = Gtk.Window() + window.add(widget) + + widget.realize() + while Gtk.events_pending(): + Gtk.main_iteration() + assert widget.get_realized() + yield widget + + if toplevel is None: + window.remove(widget) + window.destroy() + + while Gtk.events_pending(): + Gtk.main_iteration() + + @unittest.skipUnless(Gtk, 'Gtk not available') class TestGtk(unittest.TestCase): def test_container(self): @@ -519,6 +552,38 @@ class TestGtk(unittest.TestCase): self.assertTrue(hasattr(widget.drag_dest_set_proxy, '__call__')) self.assertTrue(hasattr(widget.drag_get_data, '__call__')) + def test_drag_target_list(self): + mixed_target_list = [Gtk.TargetEntry.new('test0', 0, 0), + ('test1', 1, 1), + Gtk.TargetEntry.new('test2', 2, 2), + ('test3', 3, 3)] + + def _test_target_list(targets): + for i, target in enumerate(targets): + self.assertTrue(isinstance(target, Gtk.TargetEntry)) + self.assertEqual(target.target, 'test' + str(i)) + self.assertEqual(target.flags, i) + self.assertEqual(target.info, i) + + _test_target_list(Gtk._construct_target_list(mixed_target_list)) + + widget = Gtk.Button() + widget.drag_dest_set(Gtk.DestDefaults.DROP, None, Gdk.DragAction.COPY) + widget.drag_dest_set_target_list(mixed_target_list) + widget.drag_dest_get_target_list() + + widget.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, None, Gdk.DragAction.MOVE) + widget.drag_source_set_target_list(mixed_target_list) + widget.drag_source_get_target_list() + + treeview = Gtk.TreeView() + treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, + mixed_target_list, + Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) + + treeview.enable_model_drag_dest(mixed_target_list, + Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) + def test_scrollbar(self): # PyGTK compat adjustment = Gtk.Adjustment() @@ -1363,19 +1428,13 @@ class TestTreeView(unittest.TestCase): store.append((0, "foo")) store.append((1, "bar")) view = Gtk.TreeView() - # FIXME: We can't easily call get_cursor() to make sure this works as - # expected as we need to realize and focus the column; the following - # will raise a Gtk-CRITICAL which we ignore for now - old_mask = GLib.log_set_always_fatal( - GLib.LogLevelFlags.LEVEL_WARNING | GLib.LogLevelFlags.LEVEL_ERROR) - try: + + with realized(view): view.set_cursor(store[1].path) view.set_cursor(str(store[1].path)) view.get_cell_area(store[1].path) view.get_cell_area(str(store[1].path)) - finally: - GLib.log_set_always_fatal(old_mask) def test_tree_view_column(self): cell = Gtk.CellRendererText() @@ -1403,28 +1462,21 @@ class TestTreeView(unittest.TestCase): # unconnected tree.insert_column_with_attributes(-1, 'Head4', cell4) - # might cause a Pango warning, do not break on this - old_mask = GLib.log_set_always_fatal( - GLib.LogLevelFlags.LEVEL_CRITICAL | GLib.LogLevelFlags.LEVEL_ERROR) - try: - # We must realize the TreeView for cell.props.text to receive a value - dialog = Gtk.Dialog() - dialog.get_action_area().add(tree) - dialog.show_all() - dialog.hide() - finally: - GLib.log_set_always_fatal(old_mask) + with realized(tree): + tree.set_cursor(model[0].path) + while Gtk.events_pending(): + Gtk.main_iteration() - self.assertEqual(tree.get_column(0).get_title(), 'Head1') - self.assertEqual(tree.get_column(1).get_title(), 'Head2') - self.assertEqual(tree.get_column(2).get_title(), 'Head3') - self.assertEqual(tree.get_column(3).get_title(), 'Head4') + self.assertEqual(tree.get_column(0).get_title(), 'Head1') + self.assertEqual(tree.get_column(1).get_title(), 'Head2') + self.assertEqual(tree.get_column(2).get_title(), 'Head3') + self.assertEqual(tree.get_column(3).get_title(), 'Head4') - # cursor should be at the first row - self.assertEqual(cell1.props.text, 'cell11') - self.assertEqual(cell2.props.text, 'cell12') - self.assertEqual(cell3.props.text, 'cell13') - self.assertEqual(cell4.props.text, None) + # cursor should be at the first row + self.assertEqual(cell1.props.text, 'cell11') + self.assertEqual(cell2.props.text, 'cell12') + self.assertEqual(cell3.props.text, 'cell13') + self.assertEqual(cell4.props.text, None) def test_tree_view_column_set_attributes(self): store = Gtk.ListStore(int, str) @@ -1442,19 +1494,8 @@ class TestTreeView(unittest.TestCase): column.pack_start(cell, expand=True) column.set_attributes(cell, text=1) - # might cause a Pango warning, do not break on this - old_mask = GLib.log_set_always_fatal( - GLib.LogLevelFlags.LEVEL_CRITICAL | GLib.LogLevelFlags.LEVEL_ERROR) - try: - # We must realize the TreeView for cell.props.text to receive a value - dialog = Gtk.Dialog() - dialog.get_action_area().add(treeview) - dialog.show_all() - dialog.hide() - finally: - GLib.log_set_always_fatal(old_mask) - - self.assertTrue(cell.props.text in directors) + with realized(treeview): + self.assertTrue(cell.props.text in directors) def test_tree_selection(self): store = Gtk.ListStore(int, str) |