[3.13] gh-119004: fix a crash in equality testing between OrderedDict (GH-121329) (#124507)

gh-119004: fix a crash in equality testing between `OrderedDict` (GH-121329)
(cherry picked from commit 38a887dc3e)

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
This commit is contained in:
Miss Islington (bot) 2024-09-27 20:34:48 +02:00 committed by GitHub
parent dff48e1804
commit c6c3d970ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 145 additions and 11 deletions

View file

@ -2,7 +2,9 @@ import builtins
import contextlib
import copy
import gc
import operator
import pickle
import re
from random import randrange, shuffle
import struct
import sys
@ -740,11 +742,44 @@ class OrderedDictTests:
# when it's mutated and returned from __next__:
self.assertTrue(gc.is_tracked(next(it)))
class _TriggerSideEffectOnEqual:
count = 0 # number of calls to __eq__
trigger = 1 # count value when to trigger side effect
def __eq__(self, other):
if self.__class__.count == self.__class__.trigger:
self.side_effect()
self.__class__.count += 1
return True
def __hash__(self):
# all instances represent the same key
return -1
def side_effect(self):
raise NotImplementedError
class PurePythonOrderedDictTests(OrderedDictTests, unittest.TestCase):
module = py_coll
OrderedDict = py_coll.OrderedDict
def test_issue119004_attribute_error(self):
class Key(_TriggerSideEffectOnEqual):
def side_effect(self):
del dict1[TODEL]
TODEL = Key()
dict1 = self.OrderedDict(dict.fromkeys((0, TODEL, 4.2)))
dict2 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
# This causes an AttributeError due to the linked list being changed
msg = re.escape("'NoneType' object has no attribute 'key'")
self.assertRaisesRegex(AttributeError, msg, operator.eq, dict1, dict2)
self.assertEqual(Key.count, 2)
self.assertDictEqual(dict1, dict.fromkeys((0, 4.2)))
self.assertDictEqual(dict2, dict.fromkeys((0, Key(), 4.2)))
class CPythonBuiltinDictTests(unittest.TestCase):
"""Builtin dict preserves insertion order.
@ -765,8 +800,85 @@ for method in (
del method
class CPythonOrderedDictSideEffects:
def check_runtime_error_issue119004(self, dict1, dict2):
msg = re.escape("OrderedDict mutated during iteration")
self.assertRaisesRegex(RuntimeError, msg, operator.eq, dict1, dict2)
def test_issue119004_change_size_by_clear(self):
class Key(_TriggerSideEffectOnEqual):
def side_effect(self):
dict1.clear()
dict1 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
dict2 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
self.check_runtime_error_issue119004(dict1, dict2)
self.assertEqual(Key.count, 2)
self.assertDictEqual(dict1, {})
self.assertDictEqual(dict2, dict.fromkeys((0, Key(), 4.2)))
def test_issue119004_change_size_by_delete_key(self):
class Key(_TriggerSideEffectOnEqual):
def side_effect(self):
del dict1[TODEL]
TODEL = Key()
dict1 = self.OrderedDict(dict.fromkeys((0, TODEL, 4.2)))
dict2 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
self.check_runtime_error_issue119004(dict1, dict2)
self.assertEqual(Key.count, 2)
self.assertDictEqual(dict1, dict.fromkeys((0, 4.2)))
self.assertDictEqual(dict2, dict.fromkeys((0, Key(), 4.2)))
def test_issue119004_change_linked_list_by_clear(self):
class Key(_TriggerSideEffectOnEqual):
def side_effect(self):
dict1.clear()
dict1['a'] = dict1['b'] = 'c'
dict1 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
dict2 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
self.check_runtime_error_issue119004(dict1, dict2)
self.assertEqual(Key.count, 2)
self.assertDictEqual(dict1, dict.fromkeys(('a', 'b'), 'c'))
self.assertDictEqual(dict2, dict.fromkeys((0, Key(), 4.2)))
def test_issue119004_change_linked_list_by_delete_key(self):
class Key(_TriggerSideEffectOnEqual):
def side_effect(self):
del dict1[TODEL]
dict1['a'] = 'c'
TODEL = Key()
dict1 = self.OrderedDict(dict.fromkeys((0, TODEL, 4.2)))
dict2 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
self.check_runtime_error_issue119004(dict1, dict2)
self.assertEqual(Key.count, 2)
self.assertDictEqual(dict1, {0: None, 'a': 'c', 4.2: None})
self.assertDictEqual(dict2, dict.fromkeys((0, Key(), 4.2)))
def test_issue119004_change_size_by_delete_key_in_dict_eq(self):
class Key(_TriggerSideEffectOnEqual):
trigger = 0
def side_effect(self):
del dict1[TODEL]
TODEL = Key()
dict1 = self.OrderedDict(dict.fromkeys((0, TODEL, 4.2)))
dict2 = self.OrderedDict(dict.fromkeys((0, Key(), 4.2)))
self.assertEqual(Key.count, 0)
# the side effect is in dict.__eq__ and modifies the length
self.assertNotEqual(dict1, dict2)
self.assertEqual(Key.count, 2)
self.assertDictEqual(dict1, dict.fromkeys((0, 4.2)))
self.assertDictEqual(dict2, dict.fromkeys((0, Key(), 4.2)))
@unittest.skipUnless(c_coll, 'requires the C version of the collections module')
class CPythonOrderedDictTests(OrderedDictTests, unittest.TestCase):
class CPythonOrderedDictTests(OrderedDictTests,
CPythonOrderedDictSideEffects,
unittest.TestCase):
module = c_coll
OrderedDict = c_coll.OrderedDict