There's a unit test "test_mutants" which I don't understand. If anyone remembers what it's doing, please contact me -- after ripping out dictionary ordering in Py3k, it stops working. In particular, the code in test_one() requires changes, but I don't know how... Please help! -- --Guido van Rossum (home page: http://www.python.org/~guido/)
[Guido]
There's a unit test "test_mutants" which I don't understand. If anyone remembers what it's doing, please contact me -- after ripping out dictionary ordering in Py3k,
Is any form of dictionary comparison still supported, and, if so, what does "dict1 cmp_op dict2" mean now?
it stops working.
Traceback?
In particular, the code in test_one() requires changes, but I don't know how... Please help!
The keys and values of dict1 and dict2 are filled with objects of a user-defined class whose __cmp__ method randomly mutates dict1 and dict2. dict1 and dict2 are initially forced to have the same number of elements, so in current Python: c = cmp(dict1, dict2) triggers a world of pain, with the internal dict code doing fancy stuff comparing keys and values. However, every key and value comparison /may/ mutate the dicts in arbitrary ways, so this is testing whether the dict comparison implementation blows up (segfaults, etc) when the dicts it's comparing mutate during comparison. If it's only ordering comparisons that have gone away for dicts, then, e.g., replacing c = cmp(dict1, dict2) with c = dict1 == dict2 instead will still meet the test's intent. No particular /result/ is expected. The test passes if and only if Python doesn't crash. When the test was introduced, it uncovered at least six distinct failure (crashing) modes across the first 20 times it was run, so it's well worth keeping around in some form.
On 8/24/06, Tim Peters
[Guido]
There's a unit test "test_mutants" which I don't understand. If anyone remembers what it's doing, please contact me -- after ripping out dictionary ordering in Py3k,
Is any form of dictionary comparison still supported, and, if so, what does "dict1 cmp_op dict2" mean now?
Only == and != are supported between dicts. All the work is done by dict_equal().
it stops working.
Traceback?
Not particularly interesting: without changes, the code immediately bombs like this: trying w/ lengths 90 90 . Traceback (most recent call last): File "../Lib/test/test_mutants.py", line 152, in <module> test(100) File "../Lib/test/test_mutants.py", line 149, in test test_one(random.randrange(1, 100)) File "../Lib/test/test_mutants.py", line 135, in test_one c = cmp(dict1, dict2) TypeError: unorderable types: dict() > dict()
In particular, the code in test_one() requires changes, but I don't know how... Please help!
The keys and values of dict1 and dict2 are filled with objects of a user-defined class whose __cmp__ method randomly mutates dict1 and dict2. dict1 and dict2 are initially forced to have the same number of elements, so in current Python:
c = cmp(dict1, dict2)
triggers a world of pain, with the internal dict code doing fancy stuff comparing keys and values. However, every key and value comparison /may/ mutate the dicts in arbitrary ways, so this is testing whether the dict comparison implementation blows up (segfaults, etc) when the dicts it's comparing mutate during comparison.
If it's only ordering comparisons that have gone away for dicts, then, e.g., replacing
c = cmp(dict1, dict2)
with
c = dict1 == dict2
instead will still meet the test's intent.
I made that change, and changed class Horrid to define __eq__ instead of __cmp__. Since dict_equal() only invokes PyObject_RichCompareBool() with op==Py_EQ that should be all that's needed. Now when I run it, it spits out an apaprently infinite number of dots. Putting a print in that __eq__ method suggests it is never called. Do you understand this? If I change Horrid.__hash__ to always return 42, I get output like this: trying w/ lengths 12 14 trying w/ lengths 48 52 trying w/ lengths 19 18 trying w/ lengths 10 9 trying w/ lengths 48 46 trying w/ lengths 58 55 trying w/ lengths 50 48 trying w/ lengths 45 50 trying w/ lengths 19 19 . Traceback (most recent call last): File "../Lib/test/test_mutants.py", line 158, in <module> test(100) File "../Lib/test/test_mutants.py", line 155, in test test_one(random.randrange(1, 100)) File "../Lib/test/test_mutants.py", line 141, in test_one c = dict1 == dict2 File "../Lib/test/test_mutants.py", line 99, in __eq__ return self.i == other.i AttributeError: 'Horrid' object has no attribute 'i' Segmentation fault But it doesn't always end with a segfault -- most of the time, the AttributeError is the last thing printed.
No particular /result/ is expected. The test passes if and only if Python doesn't crash. When the test was introduced, it uncovered at least six distinct failure (crashing) modes across the first 20 times it was run, so it's well worth keeping around in some form.
Well, it looks like it did provoke another crash, so I'll play with it some more. Thanks! -- --Guido van Rossum (home page: http://www.python.org/~guido/)
On 8/24/06, Guido van Rossum
I made that change, and changed class Horrid to define __eq__ instead of __cmp__. Since dict_equal() only invokes PyObject_RichCompareBool() with op==Py_EQ that should be all that's needed.
Now when I run it, it spits out an apaprently infinite number of dots. Putting a print in that __eq__ method suggests it is never called. Do you understand this?
If I change Horrid.__hash__ to always return 42, I get output like this:
trying w/ lengths 12 14 trying w/ lengths 48 52 trying w/ lengths 19 18 trying w/ lengths 10 9 trying w/ lengths 48 46 trying w/ lengths 58 55 trying w/ lengths 50 48 trying w/ lengths 45 50 trying w/ lengths 19 19 . Traceback (most recent call last): File "../Lib/test/test_mutants.py", line 158, in <module> test(100) File "../Lib/test/test_mutants.py", line 155, in test test_one(random.randrange(1, 100)) File "../Lib/test/test_mutants.py", line 141, in test_one c = dict1 == dict2 File "../Lib/test/test_mutants.py", line 99, in __eq__ return self.i == other.i AttributeError: 'Horrid' object has no attribute 'i' Segmentation fault
But it doesn't always end with a segfault -- most of the time, the AttributeError is the last thing printed.
As a follow up to this story line, this appeared to be a refcount bug in dict_equal(). I believe the same bug is probably present in 2.5; it isn't triggered by test_mutants.py because that only exercises dict_compare, not dict_richcompare, and only the latter can call dict_equal (when op==Py_EQ or op==Py_NE). The bug is here: PyObject *key = a->ma_table[i].me_key; /* temporarily bump aval's refcount to ensure it stays alive until we're done with it */ Py_INCREF(aval); bval = PyDict_GetItem((PyObject *)b, key); The fix is to put Py_INCREF(key) // Py_DECREF(key) around the call to PyDict_GetItem(). Apparently what can happen is that the only reference to the key is in the dict, and the evil mutation from the comparison delete the object before the comparison is completely done with it. Should I attempt to reproduce this bug in 2.5 and fix it? -- --Guido van Rossum (home page: http://www.python.org/~guido/)
On 8/24/06, Guido van Rossum
Should I attempt to reproduce this bug in 2.5 and fix it?
Couldn't help myself. The fix is python.org/sf/1546288 . I set the priority to 8 which means Neal and Anthony will look at it. It's probably okay to reduce the priority to 7 and fix it in 2.5.1. I suspect pre-2.5 versions may have the same bug, as long as they have dict_equal(). -- --Guido van Rossum (home page: http://www.python.org/~guido/)
participants (2)
-
Guido van Rossum
-
Tim Peters