Skip to content

Commit

Permalink
Test for overflow conditions in all appropriate btree types.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamadden committed Feb 27, 2020
1 parent 3876f3a commit a6f02b3
Show file tree
Hide file tree
Showing 28 changed files with 143 additions and 572 deletions.
16 changes: 7 additions & 9 deletions BTrees/__init__.py
Expand Up @@ -60,11 +60,17 @@
globals()[name.split('.', 1)[1]] = mod
__all__.append(name)


@zope.interface.implementer(BTrees.Interfaces.IBTreeFamily)
class _Family(object):
from BTrees import OOBTree as OO
_BITSIZE = 0
minint = maxint = maxuint = 0
minint = maxint = maxuint = None

def __init__(self):
self.maxint = 2 ** (self._BITSIZE - 1) - 1
self.minint = -self.maxint - 1
self.maxuint = 2 ** self._BITSIZE - 1

def __str__(self):
return (
Expand Down Expand Up @@ -93,10 +99,6 @@ class _Family32(_Family):
from BTrees import UOBTree as UO
from BTrees import UUBTree as UU

maxuint = int(2**32)
maxint = int(2**31-1)
minint = -maxint - 1

def __reduce__(self):
return _family32, ()

Expand All @@ -115,10 +117,6 @@ class _Family64(_Family):
from BTrees import QOBTree as UO
from BTrees import QQBTree as UU

maxuint = 2**64
maxint = 2**63-1
minint = -maxint - 1

def __reduce__(self):
return _family64, ()

Expand Down
21 changes: 20 additions & 1 deletion BTrees/_datatypes.py
Expand Up @@ -98,6 +98,15 @@ def get_lower_bound(self):
"""
return None

def get_upper_bound(self):
"""
If there is an upper bound (inclusive) on the data type,
return it. Otherwise, return ``None``.
Remarks are as for `get_lower_bound`.
"""
return None


class KeyDataType(DataType):
"""
Expand Down Expand Up @@ -137,6 +146,7 @@ def __call__(self, item):
def supports_value_union(self):
return False


class O(KeyDataType):
"""
Arbitrary (sortable) Python objects.
Expand Down Expand Up @@ -250,17 +260,26 @@ def getTwoExamples(self):
return 1, 2

def get_lower_bound(self):
exp = 32 if self.using64bits else 64
exp = 64 if self.using64bits else 32
exp -= 1
return -(2 ** exp)

def get_upper_bound(self):
exp = 64 if self.using64bits else 32
exp -= 1
return 2 ** exp - 1


class _AbstractUIntDataType(_AbstractIntDataType):
long_name = 'Unsigned'

def get_lower_bound(self):
return 0

def get_upper_bound(self):
exp = 64 if self.using64bits else 32
return 2 ** exp - 1


class I(_AbstractIntDataType):
_struct_format = 'i'
Expand Down
4 changes: 2 additions & 2 deletions BTrees/intkeymacros.h
Expand Up @@ -25,7 +25,7 @@
long vcopy = INT_AS_LONG(ARG); \
if (PyErr_Occurred()) { (STATUS)=0; (TARGET)=0; } \
else if ((int)vcopy != vcopy) { \
PyErr_SetString(PyExc_TypeError, "integer out of range"); \
PyErr_SetString(PyExc_OverflowError, "integer out of range"); \
(STATUS)=0; (TARGET)=0; \
} \
else TARGET = vcopy; \
Expand Down Expand Up @@ -61,7 +61,7 @@
(STATUS)=0; (TARGET)=0; \
} \
else if ((unsigned int)vcopy != vcopy) { \
PyErr_SetString(PyExc_TypeError, "integer out of range"); \
PyErr_SetString(PyExc_OverflowError, "integer out of range"); \
(STATUS)=0; (TARGET)=0; \
} \
else TARGET = vcopy; \
Expand Down
4 changes: 2 additions & 2 deletions BTrees/intvaluemacros.h
Expand Up @@ -31,7 +31,7 @@
long vcopy = INT_AS_LONG(ARG); \
if (PyErr_Occurred()) { (STATUS)=0; (TARGET)=0; } \
else if ((int)vcopy != vcopy) { \
PyErr_SetString(PyExc_TypeError, "integer out of range"); \
PyErr_SetString(PyExc_OverflowError, "integer out of range"); \
(STATUS)=0; (TARGET)=0; \
} \
else TARGET = vcopy; \
Expand Down Expand Up @@ -68,7 +68,7 @@
(STATUS)=0; (TARGET)=0; \
} \
else if ((unsigned int)vcopy != vcopy) { \
PyErr_SetString(PyExc_TypeError, "integer out of range"); \
PyErr_SetString(PyExc_OverflowError, "integer out of range"); \
(STATUS)=0; (TARGET)=0; \
} \
else TARGET = vcopy; \
Expand Down
22 changes: 1 addition & 21 deletions BTrees/tests/__init__.py
@@ -1,21 +1 @@
import importlib
import sys
import types

from BTrees import _FAMILIES

from ._test_builder import update_module

# If there is no .py file on disk, create the module in memory.
# This is helpful during early development. However, it
# doesn't work with zope-testrunner's ``-m`` filter.
for family in _FAMILIES:
mod_qname = "BTrees.tests.test_" + family + 'BTree'
try:
importlib.import_module(mod_qname)
except ImportError:
btree = importlib.import_module("BTrees." + family + 'BTree')
mod = types.ModuleType(mod_qname)
update_module(vars(mod), btree)
sys.modules[mod_qname] = mod
globals()[mod_qname.split('.', 1)[1]] = mod
# Make this a package.
20 changes: 17 additions & 3 deletions BTrees/tests/_test_builder.py
Expand Up @@ -58,6 +58,17 @@ def __getattr__(self, name):
return attr


def _flattened(*args):
def f(tuple_or_klass):
if isinstance(tuple_or_klass, tuple):
for x in tuple_or_klass:
for c in f(x):
yield c
else:
yield tuple_or_klass

return tuple(f(args))

class ClassBuilder(object):

# Use TestAuto as a prefix to avoid clashing with manual tests
Expand Down Expand Up @@ -108,6 +119,8 @@ def _fixup_and_store_class(self, btree_module, fut, test_cls):
def _name_for_test(self, btree_module, fut, test_base):
fut = getattr(fut, '__name__', fut)
fut = str(fut)
if isinstance(test_base, tuple):
test_base = test_base[0]
test_name = (
self.TESTCASE_PREFIX
+ (self.prefix if not fut.startswith(self.prefix) else '')
Expand Down Expand Up @@ -207,13 +220,14 @@ class Test(ModuleTest, unittest.TestCase):
self._fixup_and_store_class(_FilteredModuleProxy(self.btree_module, ''), '', Test)

def _create_type_tests(self, btree_module, type_name, test_bases):
tree = getattr(btree_module, type_name)
for test_base in test_bases:
tree = getattr(btree_module, type_name)
if not self._needs_test(tree, test_base):
continue

test_name = self._name_for_test(btree_module, tree, test_base)
test_cls = type(test_name, (self.bounds_mixin, test_base, unittest.TestCase), {
bases = _flattened(self.bounds_mixin, test_base, unittest.TestCase)
test_cls = type(test_name, bases, {
'__module__': self.test_module,
'_getTargetClass': lambda _, t=tree: t,
'getTwoKeys': self.key_type.getTwoExamples,
Expand Down Expand Up @@ -244,7 +258,7 @@ def create_classes(self):
for type_name, test_bases in (
('BTree', (InternalKeysMappingTest,
MappingConflictTestBase,
self.btree_tests_base)),
btree_tests_base)),
('Bucket', (MappingBase,
MappingConflictTestBase,)),
('Set', (ExtendedSetTests,
Expand Down
48 changes: 48 additions & 0 deletions BTrees/tests/common.py
Expand Up @@ -1046,6 +1046,54 @@ def testEmptyFirstBucketReportedByGuido(self):

self.assertEqual(b.keys()[0], 30)

def testKeyAndValueOverflow(self):
if self.key_type.get_upper_bound() is None or self.value_type.get_upper_bound() is None:
self.skipTest("Needs bounded key and value")

good = set()
b = self._makeOne()

def trial(i):
# As key
if i > self.key_type.get_upper_bound():
with self.assertRaises(OverflowError):
b[i] = 0
elif i < self.key_type.get_lower_bound():
with self.assertRaises(OverflowError):
b[i] = 0
else:
good.add(i)
b[i] = 0
self.assertEqual(b[i], 0)

# As value
if i > self.value_type.get_upper_bound():
with self.assertRaises(OverflowError):
b[0] = i
elif i < self.value_type.get_lower_bound():
with self.assertRaises(OverflowError):
b[0] = i
else:
b[0] = i
self.assertEqual(b[0], i)

for i in range(self.key_type.get_upper_bound() - 3,
self.key_type.get_upper_bound() + 3):
__traceback_info__ = i
trial(i)
trial(-i)

if 0 in b:
del b[0]
self.assertEqual(sorted(good), sorted(b))
if self.key_type.get_lower_bound() == 0:
# None of the negative values got in
self.assertEqual(4, len(b))
else:
# 9, not 4 * 2, because of the asymmetry
# of twos complement binary integers
self.assertEqual(9, len(b))


class BTreeTests(MappingBase):
# Tests common to all BTrees
Expand Down
20 changes: 0 additions & 20 deletions BTrees/tests/test_IFBTree.py

This file was deleted.

47 changes: 0 additions & 47 deletions BTrees/tests/test_IIBTree.py

This file was deleted.

19 changes: 0 additions & 19 deletions BTrees/tests/test_IOBTree.py

This file was deleted.

46 changes: 0 additions & 46 deletions BTrees/tests/test_IUBTree.py

This file was deleted.

0 comments on commit a6f02b3

Please sign in to comment.