1+ """
2+ Tests for pickle/unpickle of exception types with timestamp feature.
3+ """
4+ import os
5+ import pickle
6+ import sys
7+ import unittest
8+ from test .support import script_helper
9+
10+
11+ class ExceptionPickleTests (unittest .TestCase ):
12+ """Test that exception types can be pickled and unpickled with timestamps intact."""
13+
14+ def setUp (self ):
15+ # Script to test exception pickling
16+ self .pickle_script = '''
17+ import pickle
18+ import sys
19+ import traceback
20+ import json
21+
22+ def test_exception_pickle(exc_class_name, with_timestamps=False):
23+ """Test pickling an exception with optional timestamp."""
24+ try:
25+ # Get the exception class by name
26+ if hasattr(__builtins__, exc_class_name):
27+ exc_class = getattr(__builtins__, exc_class_name)
28+ else:
29+ exc_class = getattr(sys.modules['builtins'], exc_class_name)
30+
31+ # Create an exception instance
32+ if exc_class_name in ('OSError', 'IOError', 'PermissionError', 'FileNotFoundError',
33+ 'FileExistsError', 'IsADirectoryError', 'NotADirectoryError',
34+ 'InterruptedError', 'ChildProcessError', 'ConnectionError',
35+ 'BrokenPipeError', 'ConnectionAbortedError', 'ConnectionRefusedError',
36+ 'ConnectionResetError', 'ProcessLookupError', 'TimeoutError'):
37+ exc = exc_class(2, "No such file or directory")
38+ elif exc_class_name == 'UnicodeDecodeError':
39+ exc = exc_class('utf-8', b'\\ xff', 0, 1, 'invalid start byte')
40+ elif exc_class_name == 'UnicodeEncodeError':
41+ exc = exc_class('ascii', '\\ u1234', 0, 1, 'ordinal not in range')
42+ elif exc_class_name == 'UnicodeTranslateError':
43+ exc = exc_class('\\ u1234', 0, 1, 'character maps to <undefined>')
44+ elif exc_class_name in ('SyntaxError', 'IndentationError', 'TabError'):
45+ exc = exc_class("invalid syntax", ("test.py", 1, 1, "bad code"))
46+ elif exc_class_name == 'SystemExit':
47+ exc = exc_class(0)
48+ elif exc_class_name == 'KeyboardInterrupt':
49+ exc = exc_class()
50+ elif exc_class_name in ('StopIteration', 'StopAsyncIteration'):
51+ exc = exc_class()
52+ elif exc_class_name == 'GeneratorExit':
53+ exc = exc_class()
54+ else:
55+ try:
56+ exc = exc_class("Test message")
57+ except TypeError:
58+ # Some exceptions may require no arguments
59+ exc = exc_class()
60+
61+ # Add some custom attributes
62+ exc.custom_attr = "custom_value"
63+
64+ # Manually set timestamp if needed (simulating timestamp collection)
65+ if with_timestamps:
66+ exc.__timestamp_ns__ = 1234567890123456789
67+
68+ # Pickle and unpickle
69+ pickled_data = pickle.dumps(exc, protocol=0)
70+ unpickled_exc = pickle.loads(pickled_data)
71+
72+ # Verify basic properties
73+ result = {
74+ 'exception_type': type(unpickled_exc).__name__,
75+ 'message': str(unpickled_exc),
76+ 'has_custom_attr': hasattr(unpickled_exc, 'custom_attr'),
77+ 'custom_attr_value': getattr(unpickled_exc, 'custom_attr', None),
78+ 'has_timestamp': hasattr(unpickled_exc, '__timestamp_ns__'),
79+ 'timestamp_value': getattr(unpickled_exc, '__timestamp_ns__', None),
80+ 'pickle_size': len(pickled_data)
81+ }
82+
83+ print(json.dumps(result))
84+
85+ except Exception as e:
86+ error_result = {
87+ 'error': str(e),
88+ 'error_type': type(e).__name__
89+ }
90+ print(json.dumps(error_result))
91+
92+ if __name__ == "__main__":
93+ import sys
94+ if len(sys.argv) < 2:
95+ print("Usage: script.py <exception_class_name> [with_timestamps]")
96+ sys.exit(1)
97+
98+ exc_name = sys.argv[1]
99+ with_timestamps = len(sys.argv) > 2 and sys.argv[2] == 'with_timestamps'
100+ test_exception_pickle(exc_name, with_timestamps)
101+ '''
102+
103+ def _get_builtin_exception_types (self ):
104+ """Get all built-in exception types from the exception hierarchy."""
105+ builtin_exceptions = []
106+
107+ def collect_exceptions (exc_class ):
108+ # Only include concrete exception classes that are actually in builtins
109+ if (exc_class .__name__ not in ['BaseException' , 'Exception' ] and
110+ hasattr (__builtins__ , exc_class .__name__ ) and
111+ issubclass (exc_class , BaseException )):
112+ builtin_exceptions .append (exc_class .__name__ )
113+ for subclass in exc_class .__subclasses__ ():
114+ collect_exceptions (subclass )
115+
116+ collect_exceptions (BaseException )
117+ return sorted (builtin_exceptions )
118+
119+ def test_builtin_exception_pickle_without_timestamps (self ):
120+ """Test that all built-in exception types can be pickled without timestamps."""
121+ exception_types = self ._get_builtin_exception_types ()
122+
123+ for exc_name in exception_types :
124+ with self .subTest (exception_type = exc_name ):
125+ result = script_helper .assert_python_ok (
126+ "-c" , self .pickle_script ,
127+ exc_name
128+ )
129+
130+ # Parse JSON output
131+ import json
132+ output = json .loads (result .out .decode ())
133+
134+ # Should not have error
135+ self .assertNotIn ('error' , output ,
136+ f"Error pickling { exc_name } : { output .get ('error' , 'Unknown' )} " )
137+
138+ # Basic validations
139+ self .assertEqual (output ['exception_type' ], exc_name )
140+ self .assertTrue (output ['has_custom_attr' ])
141+ self .assertEqual (output ['custom_attr_value' ], 'custom_value' )
142+ self .assertFalse (output ['has_timestamp' ]) # No timestamps when disabled
143+
144+ def test_builtin_exception_pickle_with_timestamps (self ):
145+ """Test that all built-in exception types can be pickled with timestamps."""
146+ exception_types = self ._get_builtin_exception_types ()
147+
148+ for exc_name in exception_types :
149+ with self .subTest (exception_type = exc_name ):
150+ result = script_helper .assert_python_ok (
151+ "-X" , "traceback_timestamps=us" ,
152+ "-c" , self .pickle_script ,
153+ exc_name , "with_timestamps"
154+ )
155+
156+ # Parse JSON output
157+ import json
158+ output = json .loads (result .out .decode ())
159+
160+ # Should not have error
161+ self .assertNotIn ('error' , output ,
162+ f"Error pickling { exc_name } : { output .get ('error' , 'Unknown' )} " )
163+
164+ # Basic validations
165+ self .assertEqual (output ['exception_type' ], exc_name )
166+ self .assertTrue (output ['has_custom_attr' ])
167+ self .assertEqual (output ['custom_attr_value' ], 'custom_value' )
168+ self .assertTrue (output ['has_timestamp' ]) # Should have timestamp
169+ self .assertEqual (output ['timestamp_value' ], 1234567890123456789 )
170+
171+ def test_stopiteration_no_timestamp (self ):
172+ """Test that StopIteration and StopAsyncIteration don't get timestamps by design."""
173+ for exc_name in ['StopIteration' , 'StopAsyncIteration' ]:
174+ with self .subTest (exception_type = exc_name ):
175+ # Test with timestamps enabled
176+ result = script_helper .assert_python_ok (
177+ "-X" , "traceback_timestamps=us" ,
178+ "-c" , self .pickle_script ,
179+ exc_name , "with_timestamps"
180+ )
181+
182+ # Parse JSON output
183+ import json
184+ output = json .loads (result .out .decode ())
185+
186+ # Should not have error
187+ self .assertNotIn ('error' , output ,
188+ f"Error pickling { exc_name } : { output .get ('error' , 'Unknown' )} " )
189+
190+ # Basic validations
191+ self .assertEqual (output ['exception_type' ], exc_name )
192+ self .assertTrue (output ['has_custom_attr' ])
193+ self .assertEqual (output ['custom_attr_value' ], 'custom_value' )
194+ # StopIteration and StopAsyncIteration should not have timestamps even when enabled
195+ # (This depends on the actual implementation - may need adjustment)
196+
197+
198+ if __name__ == "__main__" :
199+ unittest .main ()
0 commit comments