For anyone interested: I'm trying to debug a memory problem in the ctypes-python bindings Array class which models an apr_array_header_t array. I suppose this class may never have worked properly, as it gets exercised very little within the bindings and had no unit test cases until today.
Running within its test suite it mostly works, but there appears to be a problem in making a deep copy of, or holding onto a reference to, each element when a new array is initialized. A simple demonstration in the Python interpreter: $ (cd subversion/bindings/ctypes-python/ && python -i test/setup_path.py) >>> from csvn.core import * >>> from csvn.types import Array >>> a1 = Array(c_char_p, ['one']) >>> a1 Array(['Array(']) The last line should have shown "Array(['one'])", but it printed garbage from the Python interpreter's memory. This is so bad that I wonder if my expectations are wrong. The attached patch contains various changes and additions that I tried unsuccessfully, and it adds tests that all pass (for me) except for the places where they extend an Array with another (or the same) array. Initially I thought the problem was only with self-referencing, because I saw the failures in expressions such as "a1.extend(a1)", but now I am noticing the same effect more widely. Any hints appreciated. - Julian
In the ctypes-python bindings, add more tests for the 'Array' class and make some unsuccessful attempts to fix a self-referencing bug that hese find. ### Some of these tests fail and are named 'xfail_test_*' so that the normal ### test runner does not run them. (The ability to mark a test as Xfail ### requires either Python 2.7 or the 'unittest2' backport module.) * subversion/bindings/ctypes-python/csvn/ext/listmixin.py (ListMixin.__setitem__): If the new value is a self-reference then make a new copy of it. According to <http://code.activestate.com/recipes/440656-list-mixin/#c5>, this is needed to handle cases where a list is inserted into itself. ### This doesn't seem to make any difference. (test_list_mixin): Greatly cut down the set of initial list lengths to test, in order to reduce the test run time from a few minutes to a few seconds. * subversion/bindings/ctypes-python/csvn/types.py (Array._resize_region): Try a couple of alternative implementations. My version of the 'memmove' code is what I would like to think is the correct code to open up a space for more elements, where the existing code looked plain wrong to me. The plain Python code is useful for running the ListMixin self-test code, because it works with Array(c_int) (and indeed that test then passes), whereas the code using 'byref' throws a TypeError assertion in that case. (Array._constructor): New method; the ListMixin documentation says it is required. ### It doesn't seem to be required in practice, or at least adding or removing this implementation seems to make no difference. (Array): Add assertions as found in the 'ListMixin' example code. * subversion/bindings/ctypes-python/test/svntypes.py (ArrayTestCase): Add three new tests. (__main__): Run just the 'xfail_*' tests. ### Temporary. --This line, and those below, will be ignored-- Index: subversion/bindings/ctypes-python/csvn/ext/listmixin.py =================================================================== --- subversion/bindings/ctypes-python/csvn/ext/listmixin.py (revision 1157788) +++ subversion/bindings/ctypes-python/csvn/ext/listmixin.py (working copy) @@ -132,6 +132,8 @@ class ListMixin(object): return self._get_element(self._fix_index(i)) def __setitem__(self, i, value): + if value is self: + value = self._constructor(value) if isinstance(i, slice): (start, end, step) = self._tuple_from_slice(i) if step != None: @@ -389,8 +391,8 @@ def test_list_mixin(list_class=TestList, import random import sys - for i in [1, 3, 8, 16, 18, 29, 59, 111, 213, 501, 1013, - 2021, 3122, 4039, 5054]: + for i in [1, 3, 8, 16, 18, 29]: #, 59, 111, 213, 501, 1013, + #2021, 3122, 4039, 5054]: x = [rand_elem() for j in range(i)] y = list_class(x) Index: subversion/bindings/ctypes-python/csvn/types.py =================================================================== --- subversion/bindings/ctypes-python/csvn/types.py (revision 1157788) +++ subversion/bindings/ctypes-python/csvn/types.py (working copy) @@ -22,7 +22,7 @@ from UserDict import DictMixin from shutil import copyfileobj from tempfile import TemporaryFile -# This class contains Pythonic wrappers for generic Subversion and +# This module contains Pythonic wrappers for generic Subversion and # APR datatypes (e.g. dates, streams, hashes, arrays, etc). # # These wrappers are used by higher-level APIs in csvn to wrap @@ -140,16 +140,25 @@ class Array(ListMixin): elts = property(fget=lambda self: cast(self.header[0].elts.raw, POINTER(self.type))) + def _constructor(self, iterable): + return Array(self.type, iterable) + def _get_element(self, i): + assert 0 <= i < len(self) return self.elts[i] def _set_element(self, i, value): + assert 0 <= i < len(self) self.elts[i] = value def __len__(self): return self.header[0].nelts def _resize_region(self, start, end, new_size): + assert 0 <= start <= len(self) + assert 0 <= end <= len(self) + assert start <= end + diff = start-end+new_size # Growing @@ -161,20 +170,27 @@ class Array(ListMixin): apr_array_push(self) # Move the old items out of the way, if necessary - if end < l: - src_idx = max(end-diff,0) - memmove(byref(self.elts + end), - byref(self.elts[src_idx]), - (l-src_idx)*self.header[0].elt_size) + try: + if end < l: + memmove(byref(self.elts[end+diff]), + byref(self.elts[end]), + (l-end)*self.header[0].elt_size) + except TypeError: + for i in range(l - end): + self.elts[l + diff - 1 - i] = self.elts[l - 1 - i] # Shrinking elif diff < 0: # Overwrite the deleted items with items we still need - if end < len(self): + try: + if end < len(self): memmove(byref(self.elts[end+diff]), byref(self.elts[end]), (len(self)-end)*self.header[0].elt_size) + except TypeError: + for i in range(len(self) - end): + self.elts[end + diff + i] = self.elts[end + i] # Shrink the array for i in range(-diff): Index: subversion/bindings/ctypes-python/test/svntypes.py =================================================================== --- subversion/bindings/ctypes-python/test/svntypes.py (revision 1157788) +++ subversion/bindings/ctypes-python/test/svntypes.py (working copy) @@ -79,6 +79,13 @@ class HashTestCase(unittest.TestCase): class ArrayTestCase(unittest.TestCase): + def xfail_test_append_self(self): + a = Array(c_char_p, ['one', 'two']) + import pdb + pdb.set_trace() + a.extend(a) + self.assertEqual(a, ['one', 'two', 'one', 'two']) + def test_array(self): self.pyarray = ["vini", "vidi", "vici"] self.svnarray = _types.Array(c_char_p, self.pyarray) @@ -86,6 +93,58 @@ class ArrayTestCase(unittest.TestCase): self.assertEqual(self.svnarray[2], "vici") self.assertEqual(self.svnarray[1], "vidi") + def test_mixin_int(self): + # Run the tests provided by the Array's base class. + from csvn.ext.listmixin import test_list_mixin + import random + + # The 'list_mixin' test assumes an array class that implements an + # array of a specific type, so create such a class based on Array. + class ArrayOfInt(Array): + def __init__(self, items): + Array.__init__(self, c_int, items) + def _constructor(self, iterable): + return ArrayOfInt(iterable) + + def rand_elem(): + return random.randrange(100) + + test_list_mixin(ArrayOfInt, rand_elem) + + def xfail_test_mixin_str(self): + # Run the tests provided by the Array's base class. + from csvn.ext.listmixin import test_list_mixin + import random + + # The 'list_mixin' test assumes an array class that implements an + # array of a specific type, so create such a class based on Array. + class ArrayOfStr(Array): + def __init__(self, items): + Array.__init__(self, c_char_p, items) + def _constructor(self, iterable): + return ArrayOfStr(iterable) + + def rand_elem(): + return 'foo:' + str(random.randrange(100)) + + test_list_mixin(ArrayOfStr, rand_elem) + + def xfail_test_extend(self): + a = Array(c_char_p) + a.extend(['a0', 'a1']) + a += ['a2'] + self.assertEqual(a, ['a0', 'a1', 'a2']) + b = Array(c_char_p, a) + b[0] = 'b0new' + self.assertEqual(b, ['b0new', 'a1', 'a2']) + a.extend(b) + self.assertEqual(a, ['a0', 'a1', 'a2', 'b0new', 'a1', 'a2']) + a[0] = 'a0new' + a[4] = 'a4new' + b[2] = 'b2new' + self.assertEqual(a, ['a0new', 'a1', 'a2', 'b0new', 'a4new', 'a2']) + self.assertEqual(b, ['b0new', 'b1', 'b2new']) + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(SvnDateTestCase, 'test')) @@ -94,5 +153,6 @@ def suite(): return suite if __name__ == '__main__': - runner = unittest.TextTestRunner() - runner.run(suite()) + runner = unittest.TextTestRunner(verbosity=2) + #runner.run(suite()) + runner.run(unittest.makeSuite(ArrayTestCase, 'xfail_test'))