cpython/Lib/test/test_mutants.py

292 lines
8.3 KiB
Python
Raw Normal View History

from test.test_support import verbose, TESTFN
import random
import os
# From SF bug #422121: Insecurities in dict comparison.
2001-05-10 16:40:30 -03:00
# Safety of code doing comparisons has been an historical Python weak spot.
# The problem is that comparison of structures written in C *naturally*
# wants to hold on to things like the size of the container, or "the
# biggest" containee so far, across a traversal of the container; but
# code to do containee comparisons can call back into Python and mutate
# the container in arbitrary ways while the C loop is in midstream. If the
# C code isn't extremely paranoid about digging things out of memory on
# each trip, and artificially boosting refcounts for the duration, anything
# from infinite loops to OS crashes can result (yes, I use Windows <wink>).
#
# The other problem is that code designed to provoke a weakness is usually
# white-box code, and so catches only the particular vulnerabilities the
# author knew to protect against. For example, Python's list.sort() code
# went thru many iterations as one "new" vulnerability after another was
# discovered.
#
# So the dict comparison test here uses a black-box approach instead,
# generating dicts of various sizes at random, and performing random
# mutations on them at random times. This proved very effective,
# triggering at least six distinct failure modes the first 20 times I
# ran it. Indeed, at the start, the driver never got beyond 6 iterations
# before the test died.
# The dicts are global to make it easy to mutate tham from within functions.
dict1 = {}
dict2 = {}
# The current set of keys in dict1 and dict2. These are materialized as
# lists to make it easy to pick a dict key at random.
dict1keys = []
dict2keys = []
# Global flag telling maybe_mutate() whether to *consider* mutating.
mutate = 0
# If global mutate is true, consider mutating a dict. May or may not
# mutate a dict even if mutate is true. If it does decide to mutate a
# dict, it picks one of {dict1, dict2} at random, and deletes a random
# entry from it; or, more rarely, adds a random element.
def maybe_mutate():
global mutate
if not mutate:
return
if random.random() < 0.5:
return
if random.random() < 0.5:
target, keys = dict1, dict1keys
else:
target, keys = dict2, dict2keys
if random.random() < 0.2:
# Insert a new key.
mutate = 0 # disable mutation until key inserted
while 1:
newkey = Horrid(random.randrange(100))
if newkey not in target:
break
target[newkey] = Horrid(random.randrange(100))
keys.append(newkey)
mutate = 1
elif keys:
# Delete a key at random.
mutate = 0 # disable mutation until key deleted
i = random.randrange(len(keys))
key = keys[i]
del target[key]
del keys[i]
mutate = 1
# A horrid class that triggers random mutations of dict1 and dict2 when
# instances are compared.
class Horrid:
def __init__(self, i):
# Comparison outcomes are determined by the value of i.
self.i = i
# An artificial hashcode is selected at random so that we don't
2001-05-10 16:40:30 -03:00
# have any systematic relationship between comparison outcomes
# (based on self.i and other.i) and relative position within the
2001-05-10 16:40:30 -03:00
# hash vector (based on hashcode).
# XXX This is no longer effective.
##self.hashcode = random.randrange(1000000000)
def __hash__(self):
##return self.hashcode
return 42
Restructure comparison dramatically. There is no longer a default *ordering* between objects; there is only a default equality test (defined by an object being equal to itself only). Read the comment in object.c. The current implementation never uses a three-way comparison to compute a rich comparison, but it does use a rich comparison to compute a three-way comparison. I'm not quite done ripping out all the calls to PyObject_Compare/Cmp, or replacing tp_compare implementations with tp_richcompare implementations; but much of that has happened (to make most unit tests pass). The following tests still fail, because I need help deciding or understanding: test_codeop -- depends on comparing code objects test_datetime -- need Tim Peters' opinion test_marshal -- depends on comparing code objects test_mutants -- need help understanding it The problem with test_codeop and test_marshal is this: these tests compare two different code objects and expect them to be equal. Is that still a feature we'd like to support? I've temporarily removed the comparison and hash code from code objects, so they use the default (equality by pointer only) comparison. For the other two tests, run them to see for yourself. (There may be more failing test with "-u all".) A general problem with getting lots of these tests to pass is the reality that for object types that have a natural total ordering, implementing __cmp__ is much more convenient than implementing __eq__, __ne__, __lt__, and so on. Should we go back to allowing __cmp__ to provide a total ordering? Should we provide some other way to implement rich comparison with a single method override? Alex proposed a __key__() method; I've considered a __richcmp__() method. Or perhaps __cmp__() just shouldn't be killed off...
2006-08-23 21:41:19 -03:00
def __eq__(self, other):
maybe_mutate() # The point of the test.
Restructure comparison dramatically. There is no longer a default *ordering* between objects; there is only a default equality test (defined by an object being equal to itself only). Read the comment in object.c. The current implementation never uses a three-way comparison to compute a rich comparison, but it does use a rich comparison to compute a three-way comparison. I'm not quite done ripping out all the calls to PyObject_Compare/Cmp, or replacing tp_compare implementations with tp_richcompare implementations; but much of that has happened (to make most unit tests pass). The following tests still fail, because I need help deciding or understanding: test_codeop -- depends on comparing code objects test_datetime -- need Tim Peters' opinion test_marshal -- depends on comparing code objects test_mutants -- need help understanding it The problem with test_codeop and test_marshal is this: these tests compare two different code objects and expect them to be equal. Is that still a feature we'd like to support? I've temporarily removed the comparison and hash code from code objects, so they use the default (equality by pointer only) comparison. For the other two tests, run them to see for yourself. (There may be more failing test with "-u all".) A general problem with getting lots of these tests to pass is the reality that for object types that have a natural total ordering, implementing __cmp__ is much more convenient than implementing __eq__, __ne__, __lt__, and so on. Should we go back to allowing __cmp__ to provide a total ordering? Should we provide some other way to implement rich comparison with a single method override? Alex proposed a __key__() method; I've considered a __richcmp__() method. Or perhaps __cmp__() just shouldn't be killed off...
2006-08-23 21:41:19 -03:00
return self.i == other.i
def __ne__(self, other):
raise RuntimeError("I didn't expect some kind of Spanish inquisition!")
__lt__ = __le__ = __gt__ = __ge__ = __ne__
def __repr__(self):
return "Horrid(%d)" % self.i
# Fill dict d with numentries (Horrid(i), Horrid(j)) key-value pairs,
# where i and j are selected at random from the candidates list.
# Return d.keys() after filling.
def fill_dict(d, candidates, numentries):
d.clear()
for i in xrange(numentries):
d[Horrid(random.choice(candidates))] = \
Horrid(random.choice(candidates))
return d.keys()
# Test one pair of randomly generated dicts, each with n entries.
# Note that dict comparison is trivial if they don't have the same number
# of entires (then the "shorter" dict is instantly considered to be the
# smaller one, without even looking at the entries).
def test_one(n):
global mutate, dict1, dict2, dict1keys, dict2keys
# Fill the dicts without mutating them.
mutate = 0
dict1keys = fill_dict(dict1, range(n), n)
dict2keys = fill_dict(dict2, range(n), n)
# Enable mutation, then compare the dicts so long as they have the
# same size.
mutate = 1
if verbose:
print "trying w/ lengths", len(dict1), len(dict2),
while dict1 and len(dict1) == len(dict2):
if verbose:
print ".",
Restructure comparison dramatically. There is no longer a default *ordering* between objects; there is only a default equality test (defined by an object being equal to itself only). Read the comment in object.c. The current implementation never uses a three-way comparison to compute a rich comparison, but it does use a rich comparison to compute a three-way comparison. I'm not quite done ripping out all the calls to PyObject_Compare/Cmp, or replacing tp_compare implementations with tp_richcompare implementations; but much of that has happened (to make most unit tests pass). The following tests still fail, because I need help deciding or understanding: test_codeop -- depends on comparing code objects test_datetime -- need Tim Peters' opinion test_marshal -- depends on comparing code objects test_mutants -- need help understanding it The problem with test_codeop and test_marshal is this: these tests compare two different code objects and expect them to be equal. Is that still a feature we'd like to support? I've temporarily removed the comparison and hash code from code objects, so they use the default (equality by pointer only) comparison. For the other two tests, run them to see for yourself. (There may be more failing test with "-u all".) A general problem with getting lots of these tests to pass is the reality that for object types that have a natural total ordering, implementing __cmp__ is much more convenient than implementing __eq__, __ne__, __lt__, and so on. Should we go back to allowing __cmp__ to provide a total ordering? Should we provide some other way to implement rich comparison with a single method override? Alex proposed a __key__() method; I've considered a __richcmp__() method. Or perhaps __cmp__() just shouldn't be killed off...
2006-08-23 21:41:19 -03:00
c = dict1 == dict2
if verbose:
print
# Run test_one n times. At the start (before the bugs were fixed), 20
# consecutive runs of this test each blew up on or before the sixth time
# test_one was run. So n doesn't have to be large to get an interesting
# test.
# OTOH, calling with large n is also interesting, to ensure that the fixed
# code doesn't hold on to refcounts *too* long (in which case memory would
# leak).
def test(n):
for i in xrange(n):
test_one(random.randrange(1, 100))
# See last comment block for clues about good values for n.
test(100)
##########################################################################
2001-06-02 05:18:58 -03:00
# Another segfault bug, distilled by Michael Hudson from a c.l.py post.
class Child:
def __init__(self, parent):
self.__dict__['parent'] = parent
def __getattr__(self, attr):
self.parent.a = 1
self.parent.b = 1
self.parent.c = 1
self.parent.d = 1
self.parent.e = 1
self.parent.f = 1
self.parent.g = 1
self.parent.h = 1
self.parent.i = 1
return getattr(self.parent, attr)
class Parent:
def __init__(self):
self.a = Child(self)
# Hard to say what this will print! May vary from time to time. But
# we're specifically trying to test the tp_print slot here, and this is
# the clearest way to do it. We print the result to a temp file so that
# the expected-output file doesn't need to change.
f = open(TESTFN, "w")
print >> f, Parent().__dict__
f.close()
os.unlink(TESTFN)
##########################################################################
# And another core-dumper from Michael Hudson.
dict = {}
# Force dict to malloc its table.
for i in range(1, 10):
dict[i] = i
f = open(TESTFN, "w")
class Machiavelli:
def __repr__(self):
dict.clear()
# Michael sez: "doesn't crash without this. don't know why."
# Tim sez: "luck of the draw; crashes with or without for me."
print >> f
2006-08-24 23:59:59 -03:00
return repr("machiavelli")
def __hash__(self):
return 0
dict[Machiavelli()] = Machiavelli()
print >> f, str(dict)
f.close()
os.unlink(TESTFN)
del f, dict
##########################################################################
# And another core-dumper from Michael Hudson.
dict = {}
# let's force dict to malloc its table
for i in range(1, 10):
dict[i] = i
class Machiavelli2:
def __eq__(self, other):
dict.clear()
return 1
def __hash__(self):
return 0
dict[Machiavelli2()] = Machiavelli2()
try:
dict[Machiavelli2()]
except KeyError:
pass
del dict
##########################################################################
# And another core-dumper from Michael Hudson.
dict = {}
# let's force dict to malloc its table
for i in range(1, 10):
dict[i] = i
class Machiavelli3:
def __init__(self, id):
self.id = id
def __eq__(self, other):
if self.id == other.id:
dict.clear()
return 1
else:
return 0
def __repr__(self):
return "%s(%s)"%(self.__class__.__name__, self.id)
def __hash__(self):
return 0
dict[Machiavelli3(1)] = Machiavelli3(0)
dict[Machiavelli3(2)] = Machiavelli3(0)
f = open(TESTFN, "w")
try:
try:
print >> f, dict[Machiavelli3(2)]
except KeyError:
pass
finally:
f.close()
os.unlink(TESTFN)
del dict
del dict1, dict2, dict1keys, dict2keys