]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-122311: Add more tests for error messages in pickle (GH-122373)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 29 Jul 2024 10:40:16 +0000 (13:40 +0300)
committerGitHub <noreply@github.com>
Mon, 29 Jul 2024 10:40:16 +0000 (13:40 +0300)
Lib/test/pickletester.py

index a2b49e6c92a7b3d4799be82eeec46059e49304e7..3c936b3bc4029e81b58562ea474faec7cc64c01b 100644 (file)
@@ -1229,24 +1229,38 @@ class AbstractUnpickleTests:
         self.assertIs(unpickler.find_class('os.path', 'join'), os.path.join)
 
         self.assertIs(unpickler4.find_class('builtins', 'str.upper'), str.upper)
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                r"module 'builtins' has no attribute 'str\.upper'|"
+                r"Can't get attribute 'str\.upper' on <module 'builtins'"):
             unpickler.find_class('builtins', 'str.upper')
 
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                "module 'math' has no attribute 'spam'|"
+                "Can't get attribute 'spam' on <module 'math'"):
             unpickler.find_class('math', 'spam')
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                "Can't get attribute 'spam' on <module 'math'"):
             unpickler4.find_class('math', 'spam')
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                r"module 'math' has no attribute 'log\.spam'|"
+                r"Can't get attribute 'log\.spam' on <module 'math'"):
             unpickler.find_class('math', 'log.spam')
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                r"Can't get attribute 'log\.spam' on <module 'math'"):
             unpickler4.find_class('math', 'log.spam')
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                r"module 'math' has no attribute 'log\.<locals>\.spam'|"
+                r"Can't get attribute 'log\.<locals>\.spam' on <module 'math'"):
             unpickler.find_class('math', 'log.<locals>.spam')
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                r"Can't get local attribute 'log\.<locals>\.spam' on <module 'math'"):
             unpickler4.find_class('math', 'log.<locals>.spam')
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                "module 'math' has no attribute ''|"
+                "Can't get attribute '' on <module 'math'"):
             unpickler.find_class('math', '')
-        with self.assertRaises(AttributeError):
+        with self.assertRaisesRegex(AttributeError,
+                "Can't get attribute '' on <module 'math'"):
             unpickler4.find_class('math', '')
         self.assertRaises(ModuleNotFoundError, unpickler.find_class, 'spam', 'log')
         self.assertRaises(ValueError, unpickler.find_class, '', 'log')
@@ -1568,27 +1582,39 @@ class AbstractPicklingErrorTests:
         obj = REX([print, ()])
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f'{obj.__reduce_ex__!r} must return string or tuple',
+                    '__reduce__ must return a string or tuple'})
 
         obj = REX((print,))
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f'Tuple returned by {obj.__reduce_ex__!r} must have two to six elements',
+                    'tuple returned by __reduce__ must contain 2 through 6 elements'})
 
         obj = REX((print, (), None, None, None, None, None))
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f'Tuple returned by {obj.__reduce_ex__!r} must have two to six elements',
+                    'tuple returned by __reduce__ must contain 2 through 6 elements'})
 
     def test_bad_reconstructor(self):
         obj = REX((42, ()))
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'func from save_reduce() must be callable',
+                    'first item of the tuple returned by __reduce__ must be callable'})
 
     def test_unpickleable_reconstructor(self):
         obj = REX((UnpickleableCallable(), ()))
@@ -1601,8 +1627,11 @@ class AbstractPicklingErrorTests:
         obj = REX((print, []))
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'args from save_reduce() must be a tuple',
+                    'second item of the tuple returned by __reduce__ must be a tuple'})
 
     def test_unpickleable_reconstructor_args(self):
         obj = REX((print, (1, 2, UNPICKLEABLE)))
@@ -1617,26 +1646,37 @@ class AbstractPicklingErrorTests:
             with self.subTest(proto=proto):
                 with self.assertRaises((IndexError, pickle.PicklingError)) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'tuple index out of range',
+                    '__newobj__ arglist is empty'})
 
         obj = REX((copyreg.__newobj__, [REX]))
         for proto in protocols[2:]:
             with self.subTest(proto=proto):
-                with self.assertRaises((IndexError, pickle.PicklingError)):
+                with self.assertRaises((IndexError, pickle.PicklingError)) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'args from save_reduce() must be a tuple',
+                    'second item of the tuple returned by __reduce__ must be a tuple'})
 
     def test_bad_newobj_class(self):
         obj = REX((copyreg.__newobj__, (NoNew(),)))
         for proto in protocols[2:]:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'args[0] from __newobj__ args has no __new__',
+                    'args[0] from __newobj__ args is not a type'})
 
     def test_wrong_newobj_class(self):
         obj = REX((copyreg.__newobj__, (str,)))
         for proto in protocols[2:]:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertEqual(str(cm.exception),
+                    'args[0] from __newobj__ args has the wrong class')
 
     def test_unpickleable_newobj_class(self):
         class LocalREX(REX): pass
@@ -1657,34 +1697,62 @@ class AbstractPicklingErrorTests:
         obj = REX((copyreg.__newobj_ex__, ()))
         for proto in protocols[2:]:
             with self.subTest(proto=proto):
-                with self.assertRaises((ValueError, pickle.PicklingError)):
+                with self.assertRaises((ValueError, pickle.PicklingError)) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'not enough values to unpack (expected 3, got 0)',
+                    'length of the NEWOBJ_EX argument tuple must be exactly 3, not 0'})
 
         obj = REX((copyreg.__newobj_ex__, 42))
         for proto in protocols[2:]:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'args from save_reduce() must be a tuple',
+                    'second item of the tuple returned by __reduce__ must be a tuple'})
 
         obj = REX((copyreg.__newobj_ex__, (REX, 42, {})))
-        is_py = self.pickler is pickle._Pickler
-        for proto in protocols[2:4] if is_py else protocols[2:]:
-            with self.subTest(proto=proto):
-                with self.assertRaises((TypeError, pickle.PicklingError)):
-                    self.dumps(obj, proto)
+        if self.pickler is pickle._Pickler:
+            for proto in protocols[2:4]:
+                with self.subTest(proto=proto):
+                    with self.assertRaises(TypeError) as cm:
+                        self.dumps(obj, proto)
+                    self.assertEqual(str(cm.exception),
+                        'Value after * must be an iterable, not int')
+        else:
+            for proto in protocols[2:]:
+                with self.subTest(proto=proto):
+                    with self.assertRaises(pickle.PicklingError) as cm:
+                        self.dumps(obj, proto)
+                    self.assertEqual(str(cm.exception),
+                        'second item from NEWOBJ_EX argument tuple must be a tuple, not int')
 
         obj = REX((copyreg.__newobj_ex__, (REX, (), [])))
-        for proto in protocols[2:4] if is_py else protocols[2:]:
-            with self.subTest(proto=proto):
-                with self.assertRaises((TypeError, pickle.PicklingError)):
-                    self.dumps(obj, proto)
+        if self.pickler is pickle._Pickler:
+            for proto in protocols[2:4]:
+                with self.subTest(proto=proto):
+                    with self.assertRaises(TypeError) as cm:
+                        self.dumps(obj, proto)
+                    self.assertEqual(str(cm.exception),
+                        'functools.partial() argument after ** must be a mapping, not list')
+        else:
+            for proto in protocols[2:]:
+                with self.subTest(proto=proto):
+                    with self.assertRaises(pickle.PicklingError) as cm:
+                        self.dumps(obj, proto)
+                    self.assertEqual(str(cm.exception),
+                        'third item from NEWOBJ_EX argument tuple must be a dict, not list')
 
     def test_bad_newobj_ex__class(self):
         obj = REX((copyreg.__newobj_ex__, (NoNew(), (), {})))
         for proto in protocols[2:]:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'args[0] from __newobj_ex__ args has no __new__',
+                    'first item from NEWOBJ_EX argument tuple must be a class, not NoNew'})
 
     def test_wrong_newobj_ex_class(self):
         if self.pickler is not pickle._Pickler:
@@ -1692,8 +1760,10 @@ class AbstractPicklingErrorTests:
         obj = REX((copyreg.__newobj_ex__, (str, (), {})))
         for proto in protocols[2:]:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertEqual(str(cm.exception),
+                    'args[0] from __newobj_ex__ args has the wrong class')
 
     def test_unpickleable_newobj_ex_class(self):
         class LocalREX(REX): pass
@@ -1730,8 +1800,10 @@ class AbstractPicklingErrorTests:
         obj = REX((print, (), 'state', None, None, 42))
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertEqual(str(cm.exception),
+                    'sixth element of the tuple returned by __reduce__ must be a function, not int')
 
     def test_unpickleable_state_setter(self):
         obj = REX((print, (), 'state', None, None, UnpickleableCallable()))
@@ -1753,8 +1825,11 @@ class AbstractPicklingErrorTests:
         obj = REX((list, (), None, 42))
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises((TypeError, pickle.PicklingError)):
+                with self.assertRaises((TypeError, pickle.PicklingError)) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    "'int' object is not iterable",
+                    'fourth element of the tuple returned by __reduce__ must be an iterator, not int'})
 
         if self.pickler is not pickle._Pickler:
             # Python implementation is less strict and also accepts iterables.
@@ -1763,6 +1838,9 @@ class AbstractPicklingErrorTests:
                 with self.subTest(proto=proto):
                     with self.assertRaises((TypeError, pickle.PicklingError)):
                         self.dumps(obj, proto)
+                    self.assertIn(str(cm.exception), {
+                        "'int' object is not iterable",
+                        'fourth element of the tuple returned by __reduce__ must be an iterator, not int'})
 
     def test_unpickleable_object_list_items(self):
         obj = REX_six([1, 2, UNPICKLEABLE])
@@ -1777,14 +1855,20 @@ class AbstractPicklingErrorTests:
         obj = REX((dict, (), None, None, 42))
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises((TypeError, pickle.PicklingError)):
+                with self.assertRaises((TypeError, pickle.PicklingError)) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    "'int' object is not iterable",
+                    'fifth element of the tuple returned by __reduce__ must be an iterator, not int'})
 
         for proto in protocols:
             obj = REX((dict, (), None, None, iter([('a',)])))
             with self.subTest(proto=proto):
-                with self.assertRaises((ValueError, TypeError)):
+                with self.assertRaises((ValueError, TypeError)) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'not enough values to unpack (expected 2, got 1)',
+                    'dict items iterator must return 2-tuples'})
 
         if self.pickler is not pickle._Pickler:
             # Python implementation is less strict and also accepts iterables.
@@ -1793,6 +1877,8 @@ class AbstractPicklingErrorTests:
                 with self.subTest(proto=proto):
                     with self.assertRaises((TypeError, pickle.PicklingError)):
                         self.dumps(obj, proto)
+                    self.assertEqual(str(cm.exception),
+                        'dict items iterator must return 2-tuples')
 
     def test_unpickleable_object_dict_items(self):
         obj = REX_seven({'a': UNPICKLEABLE})
@@ -1860,26 +1946,38 @@ class AbstractPicklingErrorTests:
         obj.__module__ = __name__
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {obj!r}: it's not found as {__name__}.spam",
+                    f"Can't pickle {obj!r}: attribute lookup spam on {__name__} failed"})
 
         obj.__module__ = 'nonexisting'
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {obj!r}: it's not found as nonexisting.spam",
+                    f"Can't pickle {obj!r}: import of module 'nonexisting' failed"})
 
         obj.__module__ = ''
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises((ValueError, pickle.PicklingError)):
+                with self.assertRaises((ValueError, pickle.PicklingError)) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    'Empty module name',
+                    f"Can't pickle {obj!r}: import of module '' failed"})
 
         obj.__module__ = None
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {obj!r}: it's not found as __main__.spam",
+                    f"Can't pickle {obj!r}: attribute lookup spam on __main__ failed"})
 
     def test_nonencodable_global_name_error(self):
         for proto in protocols[:4]:
@@ -1888,8 +1986,15 @@ class AbstractPicklingErrorTests:
                 obj = REX(name)
                 obj.__module__ = __name__
                 with support.swap_item(globals(), name, obj):
-                    with self.assertRaises((UnicodeEncodeError, pickle.PicklingError)):
-                        self.dumps(obj, proto)
+                    if proto == 3 and self.pickler is pickle._Pickler:
+                        with self.assertRaises(UnicodeEncodeError):
+                            self.dumps(obj, proto)
+                    else:
+                        with self.assertRaises(pickle.PicklingError) as cm:
+                            self.dumps(obj, proto)
+                        self.assertIn(str(cm.exception), {
+                            f"can't pickle global identifier '{__name__}.{name}' using pickle protocol {proto}",
+                            f"can't pickle global identifier '{name}' using pickle protocol {proto}"})
 
     def test_nonencodable_module_name_error(self):
         for proto in protocols[:4]:
@@ -1899,8 +2004,15 @@ class AbstractPicklingErrorTests:
                 obj.__module__ = name
                 mod = types.SimpleNamespace(test=obj)
                 with support.swap_item(sys.modules, name, mod):
-                    with self.assertRaises((UnicodeEncodeError, pickle.PicklingError)):
-                        self.dumps(obj, proto)
+                    if proto == 3 and self.pickler is pickle._Pickler:
+                        with self.assertRaises(UnicodeEncodeError):
+                            self.dumps(obj, proto)
+                    else:
+                        with self.assertRaises(pickle.PicklingError) as cm:
+                            self.dumps(obj, proto)
+                        self.assertIn(str(cm.exception), {
+                            f"can't pickle global identifier '{name}.test' using pickle protocol {proto}",
+                            f"can't pickle module identifier '{name}' using pickle protocol {proto}"})
 
     def test_nested_lookup_error(self):
         # Nested name does not exist
@@ -1908,14 +2020,20 @@ class AbstractPicklingErrorTests:
         obj.__module__ = __name__
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {obj!r}: it's not found as {__name__}.AbstractPickleTests.spam",
+                    f"Can't pickle {obj!r}: attribute lookup AbstractPickleTests.spam on {__name__} failed"})
 
         obj.__module__ = None
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {obj!r}: it's not found as __main__.AbstractPickleTests.spam",
+                    f"Can't pickle {obj!r}: attribute lookup AbstractPickleTests.spam on __main__ failed"})
 
     def test_wrong_object_lookup_error(self):
         # Name is bound to different object
@@ -1924,14 +2042,19 @@ class AbstractPicklingErrorTests:
         AbstractPickleTests.ham = []
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertEqual(str(cm.exception),
+                    f"Can't pickle {obj!r}: it's not the same object as {__name__}.AbstractPickleTests")
 
         obj.__module__ = None
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     self.dumps(obj, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {obj!r}: it's not found as __main__.AbstractPickleTests",
+                    f"Can't pickle {obj!r}: attribute lookup AbstractPickleTests on __main__ failed"})
 
     def test_local_lookup_error(self):
         # Test that whichmodule() errors out cleanly when looking up
@@ -1941,21 +2064,30 @@ class AbstractPicklingErrorTests:
         # Since the function is local, lookup will fail
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises((AttributeError, pickle.PicklingError)):
+                with self.assertRaises((AttributeError, pickle.PicklingError)) as cm:
                     self.dumps(f, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {f!r}: it's not found as {__name__}.{f.__qualname__}",
+                    f"Can't get local object {f.__qualname__!r}"})
         # Same without a __module__ attribute (exercises a different path
         # in _pickle.c).
         del f.__module__
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises((AttributeError, pickle.PicklingError)):
+                with self.assertRaises((AttributeError, pickle.PicklingError)) as cm:
                     self.dumps(f, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {f!r}: it's not found as __main__.{f.__qualname__}",
+                    f"Can't get local object {f.__qualname__!r}"})
         # Yet a different path.
         f.__name__ = f.__qualname__
         for proto in protocols:
             with self.subTest(proto=proto):
-                with self.assertRaises((AttributeError, pickle.PicklingError)):
+                with self.assertRaises((AttributeError, pickle.PicklingError)) as cm:
                     self.dumps(f, proto)
+                self.assertIn(str(cm.exception), {
+                    f"Can't pickle {f!r}: it's not found as __main__.{f.__qualname__}",
+                    f"Can't get local object {f.__qualname__!r}"})
 
     def test_reduce_ex_None(self):
         c = REX_None()
@@ -4543,8 +4675,11 @@ class AbstractHookTests:
                 # NotImplemented
                 self.assertIs(math_log, math.log)
 
-                with self.assertRaises(pickle.PicklingError):
+                with self.assertRaises(pickle.PicklingError) as cm:
                     p.dump(g)
+                self.assertRegex(str(cm.exception),
+                    r'(__reduce__|<bound method .*reducer_override.*>)'
+                    r' must return (a )?string or tuple')
 
                 with self.assertRaisesRegex(
                         ValueError, 'The reducer just failed'):