1 # -*- Mode: Python; py-indent-offset: 4 -*-
2 # test_generictreemodel - Tests for GenericTreeModel
3 # Copyright (C) 2013 Simon Feltman
5 # test_generictreemodel.py: Tests for GenericTreeModel
7 # This library is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation; either
10 # version 2.1 of the License, or (at your option) any later version.
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # Lesser General Public License for more details.
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with this library; if not, see <http://www.gnu.org/licenses/>.
20 from __future__
import absolute_import
30 from gi
.repository
import GObject
33 from gi
.repository
import Gtk
34 from pygtkcompat
.generictreemodel
import GenericTreeModel
35 from pygtkcompat
.generictreemodel
import _get_user_data_as_pyobject
38 GenericTreeModel
= object
43 """Represents a generic node with name, value, and children."""
44 def __init__(self
, name
, value
, *children
):
47 self
.children
= list(children
)
51 for i
, child
in enumerate(children
):
52 child
.parent
= weakref
.ref(self
)
53 if i
< len(children
) - 1:
54 child
.next
= weakref
.ref(children
[i
+ 1])
57 return 'Node("%s", %s)' % (self
.name
, self
.value
)
60 class ATesterModel(GenericTreeModel
):
62 super(ATesterModel
, self
).__init
__()
63 self
.root
= Node('root', 0,
71 def on_get_flags(self
):
74 def on_get_n_columns(self
):
77 def on_get_column_type(self
, n
):
80 def on_get_iter(self
, path
):
86 node
= node
.children
[idx
]
89 def on_get_path(self
, node
):
91 for i
, child
in enumerate(n
.children
):
95 res
= rec_get_path(child
)
99 return rec_get_path(self
.root
)
101 def on_get_value(self
, node
, column
):
107 def on_iter_has_child(self
, node
):
108 return bool(node
.children
)
110 def on_iter_next(self
, node
):
114 def on_iter_children(self
, node
):
116 return node
.children
[0]
120 def on_iter_n_children(self
, node
):
123 return len(node
.children
)
125 def on_iter_nth_child(self
, node
, n
):
129 return node
.children
[n
]
131 def on_iter_parent(self
, child
):
133 return child
.parent()
136 @unittest.skipUnless(has_gtk
, 'Gtk not available')
137 class TestReferences(unittest
.TestCase
):
141 @unittest.skipIf(platform
.python_implementation() == "PyPy", "not with PyPy")
142 def test_c_tree_iter_user_data_as_pyobject(self
):
145 ref_count
= sys
.getrefcount(obj
)
147 # This is essentially a stolen ref in the context of _CTreeIter.get_user_data_as_pyobject
149 it
.user_data
= obj_id
151 obj2
= _get_user_data_as_pyobject(it
)
152 self
.assertEqual(obj
, obj2
)
153 self
.assertEqual(sys
.getrefcount(obj
), ref_count
+ 1)
155 def test_leak_references_on(self
):
156 model
= ATesterModel()
157 obj_ref
= weakref
.ref(model
.root
)
158 # Initial refcount is 1 for model.root + the temporary
159 if hasattr(sys
, "getrefcount"):
160 self
.assertEqual(sys
.getrefcount(model
.root
), 2)
162 # Iter increases by 1 do to assignment to iter.user_data
163 res
, it
= model
.do_get_iter([0])
164 self
.assertEqual(id(model
.root
), it
.user_data
)
165 if hasattr(sys
, "getrefcount"):
166 self
.assertEqual(sys
.getrefcount(model
.root
), 3)
168 # Verify getting a TreeIter more then once does not further increase
170 res2
, it2
= model
.do_get_iter([0])
171 self
.assertEqual(id(model
.root
), it2
.user_data
)
172 if hasattr(sys
, "getrefcount"):
173 self
.assertEqual(sys
.getrefcount(model
.root
), 3)
175 # Deleting the iter does not decrease refcount because references
176 # leak by default (they are stored in the held_refs pool)
179 if hasattr(sys
, "getrefcount"):
180 self
.assertEqual(sys
.getrefcount(model
.root
), 3)
182 # Deleting a model should free all held references to user data
183 # stored by TreeIters
186 self
.assertEqual(obj_ref(), None)
188 def test_row_deleted_frees_refs(self
):
189 model
= ATesterModel()
190 obj_ref
= weakref
.ref(model
.root
)
191 # Initial refcount is 1 for model.root + the temporary
192 if hasattr(sys
, "getrefcount"):
193 self
.assertEqual(sys
.getrefcount(model
.root
), 2)
195 # Iter increases by 1 do to assignment to iter.user_data
196 res
, it
= model
.do_get_iter([0])
197 self
.assertEqual(id(model
.root
), it
.user_data
)
198 if hasattr(sys
, "getrefcount"):
199 self
.assertEqual(sys
.getrefcount(model
.root
), 3)
201 # Notifying the underlying model of a row_deleted should decrease the
203 model
.row_deleted(Gtk
.TreePath('0'), model
.root
)
204 if hasattr(sys
, "getrefcount"):
205 self
.assertEqual(sys
.getrefcount(model
.root
), 2)
207 # Finally deleting the actual object should collect it completely
210 self
.assertEqual(obj_ref(), None)
212 def test_leak_references_off(self
):
213 model
= ATesterModel()
214 model
.leak_references
= False
216 obj_ref
= weakref
.ref(model
.root
)
217 # Initial refcount is 1 for model.root + the temporary
218 if hasattr(sys
, "getrefcount"):
219 self
.assertEqual(sys
.getrefcount(model
.root
), 2)
221 # Iter does not increas count by 1 when leak_references is false
222 res
, it
= model
.do_get_iter([0])
223 self
.assertEqual(id(model
.root
), it
.user_data
)
224 if hasattr(sys
, "getrefcount"):
225 self
.assertEqual(sys
.getrefcount(model
.root
), 2)
227 # Deleting the iter does not decrease refcount because assigning user_data
228 # eats references and does not release them.
231 if hasattr(sys
, "getrefcount"):
232 self
.assertEqual(sys
.getrefcount(model
.root
), 2)
234 # Deleting the model decreases the final ref, and the object is collected
237 self
.assertEqual(obj_ref(), None)
239 def test_iteration_refs(self
):
240 # Pull iterators off the model using the wrapped C API which will
241 # then call back into the python overrides.
242 model
= ATesterModel()
243 nodes
= [node
for node
in model
.iter_depth_first()]
244 values
= [node
.value
for node
in nodes
]
246 # Verify depth first ordering
247 self
.assertEqual(values
, [0, 1, 2, 3, 4])
249 # Verify ref counts for each of the nodes.
250 # 5 refs for each node at this point:
251 # 1 - ref held in getrefcount function
252 # 2 - ref held by "node" var during iteration
253 # 3 - ref held by local "nodes" var
254 # 4 - ref held by the root/children graph itself
255 # 5 - ref held by the model "held_refs" instance var
257 if hasattr(sys
, "getrefcount"):
258 self
.assertEqual(sys
.getrefcount(node
), 5)
260 # A second iteration and storage of the nodes in a new list
261 # should only increase refcounts by 1 even though new
262 # iterators are created and assigned.
263 nodes2
= [node
for node
in model
.iter_depth_first()]
265 if hasattr(sys
, "getrefcount"):
266 self
.assertEqual(sys
.getrefcount(node
), 6)
268 # Hold weak refs and start verifying ref collection.
269 node_refs
= [weakref
.ref(node
) for node
in nodes
]
271 # First round of collection
275 if hasattr(sys
, "getrefcount"):
276 self
.assertEqual(sys
.getrefcount(node
), 5)
278 # Second round of collection, no more local lists of nodes.
281 for ref
in node_refs
:
283 if hasattr(sys
, "getrefcount"):
284 self
.assertEqual(sys
.getrefcount(node
), 4)
286 # Using invalidate_iters or row_deleted(path, node) will clear out
287 # the pooled refs held internal to the GenericTreeModel implementation.
288 model
.invalidate_iters()
289 self
.assertEqual(len(model
._held
_refs
), 0)
291 for ref
in node_refs
:
293 if hasattr(sys
, "getrefcount"):
294 self
.assertEqual(sys
.getrefcount(node
), 3)
296 # Deleting the root node at this point should allow all nodes to be collected
297 # as there is no longer a way to reach the children
298 del node
# node still in locals() from last iteration
301 for ref
in node_refs
:
302 self
.assertEqual(ref(), None)
305 @unittest.skipUnless(has_gtk
, 'Gtk not available')
306 class TestIteration(unittest
.TestCase
):
307 def test_iter_next_root(self
):
308 model
= ATesterModel()
309 it
= model
.get_iter([0])
310 self
.assertEqual(it
.user_data
, id(model
.root
))
311 self
.assertEqual(model
.root
.next
, None)
313 it
= model
.iter_next(it
)
314 self
.assertEqual(it
, None)
316 def test_iter_next_multiple(self
):
317 model
= ATesterModel()
318 it
= model
.get_iter([0, 0])
319 self
.assertEqual(it
.user_data
, id(model
.root
.children
[0]))
321 it
= model
.iter_next(it
)
322 self
.assertEqual(it
.user_data
, id(model
.root
.children
[1]))
324 it
= model
.iter_next(it
)
325 self
.assertEqual(it
, None)
328 class ErrorModel(GenericTreeModel
):
329 # All on_* methods will raise a NotImplementedError by default
333 @unittest.skipUnless(has_gtk
, 'Gtk not available')
334 class ExceptHook(object):
336 Temporarily installs an exception hook in a context which
337 expects the given exc_type to be raised. This allows verification
338 of exceptions that occur within python gi callbacks but
339 are never bubbled through from python to C back to python.
340 This works because exception hooks are called in PyErr_Print.
342 def __init__(self
, *expected_exc_types
):
343 self
._expected
_exc
_types
= expected_exc_types
344 self
._exceptions
= []
346 def _excepthook(self
, exc_type
, value
, traceback
):
347 self
._exceptions
.append((exc_type
, value
))
350 self
._oldhook
= sys
.excepthook
351 sys
.excepthook
= self
._excepthook
354 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
355 sys
.excepthook
= self
._oldhook
356 error_message
= 'Expecting the following exceptions: %s, got: %s' % \
357 (str(self
._expected
_exc
_types
), '\n'.join([str(item
) for item
in self
._exceptions
]))
359 assert len(self
._expected
_exc
_types
) == len(self
._exceptions
), error_message
361 for expected
, got
in zip(self
._expected
_exc
_types
, [exc
[0] for exc
in self
._exceptions
]):
362 assert issubclass(got
, expected
), error_message
365 @unittest.skipUnless(has_gtk
, 'Gtk not available')
366 class TestReturnsAfterError(unittest
.TestCase
):
368 self
.model
= ErrorModel()
370 def test_get_flags(self
):
371 with
ExceptHook(NotImplementedError):
372 flags
= self
.model
.get_flags()
373 self
.assertEqual(flags
, 0)
375 def test_get_n_columns(self
):
376 with
ExceptHook(NotImplementedError):
377 count
= self
.model
.get_n_columns()
378 self
.assertEqual(count
, 0)
380 def test_get_column_type(self
):
381 with
ExceptHook(NotImplementedError, TypeError):
382 col_type
= self
.model
.get_column_type(0)
383 self
.assertEqual(col_type
, GObject
.TYPE_INVALID
)
385 def test_get_iter(self
):
386 with
ExceptHook(NotImplementedError):
387 self
.assertRaises(ValueError, self
.model
.get_iter
, Gtk
.TreePath(0))
389 def test_get_path(self
):
390 it
= self
.model
.create_tree_iter('foo')
391 with
ExceptHook(NotImplementedError):
392 path
= self
.model
.get_path(it
)
393 self
.assertEqual(path
, None)
395 def test_get_value(self
):
396 it
= self
.model
.create_tree_iter('foo')
397 with
ExceptHook(NotImplementedError):
399 self
.model
.get_value(it
, 0)
401 pass # silence TypeError converting None to GValue
403 def test_iter_has_child(self
):
404 it
= self
.model
.create_tree_iter('foo')
405 with
ExceptHook(NotImplementedError):
406 res
= self
.model
.iter_has_child(it
)
407 self
.assertEqual(res
, False)
409 def test_iter_next(self
):
410 it
= self
.model
.create_tree_iter('foo')
411 with
ExceptHook(NotImplementedError):
412 res
= self
.model
.iter_next(it
)
413 self
.assertEqual(res
, None)
415 def test_iter_children(self
):
416 with
ExceptHook(NotImplementedError):
417 res
= self
.model
.iter_children(None)
418 self
.assertEqual(res
, None)
420 def test_iter_n_children(self
):
421 with
ExceptHook(NotImplementedError):
422 res
= self
.model
.iter_n_children(None)
423 self
.assertEqual(res
, 0)
425 def test_iter_nth_child(self
):
426 with
ExceptHook(NotImplementedError):
427 res
= self
.model
.iter_nth_child(None, 0)
428 self
.assertEqual(res
, None)
430 def test_iter_parent(self
):
431 child
= self
.model
.create_tree_iter('foo')
432 with
ExceptHook(NotImplementedError):
433 res
= self
.model
.iter_parent(child
)
434 self
.assertEqual(res
, None)