Skip to content

Commit af4961b

Browse files
committed
Add property tests for the zoneinfo module
This migrates the tests from https://github.com/Zac-HD/stdlib-property-tests into the standard library, using the hypothesis stubs.
1 parent 615d54f commit af4961b

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed

Lib/test/test_zoneinfo/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .test_zoneinfo import *
2+
from .test_zoneinfo_property import *
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import contextlib
2+
import datetime
3+
import os
4+
import pickle
5+
import unittest
6+
import zoneinfo
7+
8+
from test.support.hypothesis_helper import hypothesis
9+
10+
import test.test_zoneinfo._support as test_support
11+
12+
ZoneInfoTestBase = test_support.ZoneInfoTestBase
13+
14+
py_zoneinfo, c_zoneinfo = test_support.get_modules()
15+
16+
UTC = datetime.timezone.utc
17+
MIN_UTC = datetime.datetime.min.replace(tzinfo=UTC)
18+
MAX_UTC = datetime.datetime.max.replace(tzinfo=UTC)
19+
ZERO = datetime.timedelta(0)
20+
21+
22+
def _valid_keys():
23+
"""Get available time zones, including posix/ and right/ directories."""
24+
from importlib import resources
25+
26+
available_zones = sorted(zoneinfo.available_timezones())
27+
TZPATH = zoneinfo.TZPATH
28+
29+
def valid_key(key):
30+
for root in TZPATH:
31+
key_file = os.path.join(root, key)
32+
if os.path.exists(key_file):
33+
return True
34+
35+
components = key.split("/")
36+
package_name = ".".join(["tzdata.zoneinfo"] + components[:-1])
37+
resource_name = components[-1]
38+
39+
try:
40+
return resources.is_resource(package_name, resource_name)
41+
except ModuleNotFoundError:
42+
return False
43+
44+
# This relies on the fact that dictionaries maintain insertion order — for
45+
# shrinking purposes, it is preferable to start with the standard version,
46+
# then move to the posix/ version, then to the right/ version.
47+
out_zones = {"": available_zones}
48+
for prefix in ["posix", "right"]:
49+
prefix_out = []
50+
for key in available_zones:
51+
prefix_key = f"{prefix}/{key}"
52+
if valid_key(prefix_key):
53+
prefix_out.append(prefix_key)
54+
55+
out_zones[prefix] = prefix_out
56+
57+
output = []
58+
for keys in out_zones.values():
59+
output.extend(keys)
60+
61+
return output
62+
63+
64+
VALID_KEYS = _valid_keys()
65+
if not VALID_KEYS:
66+
raise unittest.SkipTest("No time zone data available")
67+
68+
69+
def valid_keys():
70+
return hypothesis.strategies.sampled_from(VALID_KEYS)
71+
72+
73+
class ZoneInfoTest(ZoneInfoTestBase):
74+
module = py_zoneinfo
75+
76+
@hypothesis.given(key=valid_keys())
77+
def test_str(self, key):
78+
zi = self.klass(key)
79+
self.assertEqual(str(zi), key)
80+
81+
@hypothesis.given(key=valid_keys())
82+
def test_key(self, key):
83+
zi = self.klass(key)
84+
85+
self.assertEqual(zi.key, key)
86+
87+
@hypothesis.given(
88+
dt=hypothesis.strategies.one_of(
89+
hypothesis.strategies.datetimes(), hypothesis.strategies.times()
90+
)
91+
)
92+
def test_utc(self, dt):
93+
zi = self.klass("UTC")
94+
dt_zi = dt.replace(tzinfo=zi)
95+
96+
self.assertEqual(dt_zi.utcoffset(), ZERO)
97+
self.assertEqual(dt_zi.dst(), ZERO)
98+
self.assertEqual(dt_zi.tzname(), "UTC")
99+
100+
101+
class CZoneInfoTest(ZoneInfoTest):
102+
module = c_zoneinfo
103+
104+
105+
class ZoneInfoPickleTest(ZoneInfoTestBase):
106+
module = py_zoneinfo
107+
108+
def setUp(self):
109+
with contextlib.ExitStack() as stack:
110+
stack.enter_context(test_support.set_zoneinfo_module(self.module))
111+
self.addCleanup(stack.pop_all().close)
112+
113+
super().setUp()
114+
115+
@hypothesis.given(key=valid_keys())
116+
def test_pickle_unpickle_cache(self, key):
117+
zi = self.klass(key)
118+
pkl_str = pickle.dumps(zi)
119+
zi_rt = pickle.loads(pkl_str)
120+
121+
self.assertIs(zi, zi_rt)
122+
123+
@hypothesis.given(key=valid_keys())
124+
def test_pickle_unpickle_no_cache(self, key):
125+
zi = self.klass.no_cache(key)
126+
pkl_str = pickle.dumps(zi)
127+
zi_rt = pickle.loads(pkl_str)
128+
129+
self.assertIsNot(zi, zi_rt)
130+
self.assertEqual(str(zi), str(zi_rt))
131+
132+
@hypothesis.given(key=valid_keys())
133+
def test_pickle_unpickle_cache_multiple_rounds(self, key):
134+
"""Test that pickle/unpickle is idempotent."""
135+
zi_0 = self.klass(key)
136+
pkl_str_0 = pickle.dumps(zi_0)
137+
zi_1 = pickle.loads(pkl_str_0)
138+
pkl_str_1 = pickle.dumps(zi_1)
139+
zi_2 = pickle.loads(pkl_str_1)
140+
pkl_str_2 = pickle.dumps(zi_2)
141+
142+
self.assertEqual(pkl_str_0, pkl_str_1)
143+
self.assertEqual(pkl_str_1, pkl_str_2)
144+
145+
self.assertIs(zi_0, zi_1)
146+
self.assertIs(zi_0, zi_2)
147+
self.assertIs(zi_1, zi_2)
148+
149+
@hypothesis.given(key=valid_keys())
150+
def test_pickle_unpickle_no_cache_multiple_rounds(self, key):
151+
"""Test that pickle/unpickle is idempotent."""
152+
zi_cache = self.klass(key)
153+
154+
zi_0 = self.klass.no_cache(key)
155+
pkl_str_0 = pickle.dumps(zi_0)
156+
zi_1 = pickle.loads(pkl_str_0)
157+
pkl_str_1 = pickle.dumps(zi_1)
158+
zi_2 = pickle.loads(pkl_str_1)
159+
pkl_str_2 = pickle.dumps(zi_2)
160+
161+
self.assertEqual(pkl_str_0, pkl_str_1)
162+
self.assertEqual(pkl_str_1, pkl_str_2)
163+
164+
self.assertIsNot(zi_0, zi_1)
165+
self.assertIsNot(zi_0, zi_2)
166+
self.assertIsNot(zi_1, zi_2)
167+
168+
self.assertIsNot(zi_0, zi_cache)
169+
self.assertIsNot(zi_1, zi_cache)
170+
self.assertIsNot(zi_2, zi_cache)
171+
172+
173+
class CZoneInfoPickleTest(ZoneInfoPickleTest):
174+
module = c_zoneinfo
175+
176+
177+
class ZoneInfoCacheTest(ZoneInfoTestBase):
178+
module = py_zoneinfo
179+
180+
@hypothesis.given(key=valid_keys())
181+
def test_cache(self, key):
182+
zi_0 = self.klass(key)
183+
zi_1 = self.klass(key)
184+
185+
self.assertIs(zi_0, zi_1)
186+
187+
@hypothesis.given(key=valid_keys())
188+
def test_no_cache(self, key):
189+
zi_0 = self.klass.no_cache(key)
190+
zi_1 = self.klass.no_cache(key)
191+
192+
self.assertIsNot(zi_0, zi_1)
193+
194+
195+
class CZoneInfoCacheTest(ZoneInfoCacheTest):
196+
klass = c_zoneinfo.ZoneInfo
197+
198+
199+
class PythonCConsistencyTest(unittest.TestCase):
200+
"""Tests that the C and Python versions do the same thing."""
201+
202+
def _is_ambiguous(self, dt):
203+
return dt.replace(fold=not dt.fold).utcoffset() == dt.utcoffset()
204+
205+
@hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
206+
def test_same_str(self, dt, key):
207+
py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
208+
c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
209+
210+
self.assertEqual(str(py_dt), str(c_dt))
211+
212+
@hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
213+
def test_same_offsets_and_names(self, dt, key):
214+
py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
215+
c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
216+
217+
self.assertEqual(py_dt.tzname(), c_dt.tzname())
218+
self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset())
219+
self.assertEqual(py_dt.dst(), c_dt.dst())
220+
221+
@hypothesis.given(
222+
dt=hypothesis.strategies.datetimes(timezones=hypothesis.strategies.just(UTC)),
223+
key=valid_keys(),
224+
)
225+
@hypothesis.example(dt=MIN_UTC, key="Asia/Tokyo")
226+
@hypothesis.example(dt=MAX_UTC, key="Asia/Tokyo")
227+
@hypothesis.example(dt=MIN_UTC, key="America/New_York")
228+
@hypothesis.example(dt=MAX_UTC, key="America/New_York")
229+
@hypothesis.example(
230+
dt=datetime.datetime(2006, 10, 29, 5, 15, tzinfo=UTC), key="America/New_York",
231+
)
232+
def test_same_from_utc(self, dt, key):
233+
py_zi = py_zoneinfo.ZoneInfo(key)
234+
c_zi = c_zoneinfo.ZoneInfo(key)
235+
236+
# Convert to UTC: This can overflow, but we just care about consistency
237+
py_overflow_exc = None
238+
c_overflow_exc = None
239+
try:
240+
py_dt = dt.astimezone(py_zi)
241+
except OverflowError as e:
242+
py_overflow_exc = e
243+
244+
try:
245+
c_dt = dt.astimezone(c_zi)
246+
except OverflowError as e:
247+
c_overflow_exc = e
248+
249+
if (py_overflow_exc is not None) != (c_overflow_exc is not None):
250+
raise py_overflow_exc or c_overflow_exc # pragma: nocover
251+
252+
if py_overflow_exc is not None:
253+
return # Consistently raises the same exception
254+
255+
# PEP 495 says that an inter-zone comparison between ambiguous
256+
# datetimes is always False.
257+
if py_dt != c_dt:
258+
self.assertEqual(
259+
self._is_ambiguous(py_dt), self._is_ambiguous(c_dt), (py_dt, c_dt),
260+
)
261+
262+
self.assertEqual(py_dt.tzname(), c_dt.tzname())
263+
self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset())
264+
self.assertEqual(py_dt.dst(), c_dt.dst())
265+
266+
@hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
267+
@hypothesis.example(dt=datetime.datetime.max, key="America/New_York")
268+
@hypothesis.example(dt=datetime.datetime.min, key="America/New_York")
269+
@hypothesis.example(dt=datetime.datetime.min, key="Asia/Tokyo")
270+
@hypothesis.example(dt=datetime.datetime.max, key="Asia/Tokyo")
271+
def test_same_to_utc(self, dt, key):
272+
py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
273+
c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
274+
275+
# Convert from UTC: Overflow OK if it happens in both implementations
276+
py_overflow_exc = None
277+
c_overflow_exc = None
278+
try:
279+
py_utc = py_dt.astimezone(UTC)
280+
except OverflowError as e:
281+
py_overflow_exc = e
282+
283+
try:
284+
c_utc = c_dt.astimezone(UTC)
285+
except OverflowError as e:
286+
c_overflow_exc = e
287+
288+
if (py_overflow_exc is not None) != (c_overflow_exc is not None):
289+
raise py_overflow_exc or c_overflow_exc # pragma: nocover
290+
291+
if py_overflow_exc is not None:
292+
return # Consistently raises the same exception
293+
294+
self.assertEqual(py_utc, c_utc)
295+
296+
@hypothesis.given(key=valid_keys())
297+
def test_cross_module_pickle(self, key):
298+
py_zi = py_zoneinfo.ZoneInfo(key)
299+
c_zi = c_zoneinfo.ZoneInfo(key)
300+
301+
with test_support.set_zoneinfo_module(py_zoneinfo):
302+
py_pkl = pickle.dumps(py_zi)
303+
304+
with test_support.set_zoneinfo_module(c_zoneinfo):
305+
c_pkl = pickle.dumps(c_zi)
306+
307+
with test_support.set_zoneinfo_module(c_zoneinfo):
308+
# Python → C
309+
py_to_c_zi = pickle.loads(py_pkl)
310+
self.assertIs(py_to_c_zi, c_zi)
311+
312+
with test_support.set_zoneinfo_module(py_zoneinfo):
313+
# C → Python
314+
c_to_py_zi = pickle.loads(c_pkl)
315+
self.assertIs(c_to_py_zi, py_zi)

0 commit comments

Comments
 (0)