-
-
Notifications
You must be signed in to change notification settings - Fork 33.5k
GH-86275: Implementation of hypothesis stubs for property-based tests, with zoneinfo tests #22863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
36049cf
Add stubs for hypothesis tests
pganssle 9bb7f5c
Add property tests for the zoneinfo module
pganssle 840aea1
Add examples to zoneinfo hypothesis tests
pganssle 87c6bdb
Enable settings to operate as a decorator
pganssle c97ec97
Add Phase enum
pganssle cd3ddd7
Make reprs more accurate
pganssle 57a357b
Hard-code ignoring hypothesis files in libregrtest
pganssle 176cc6b
Add news entry
pganssle fd4391c
Add Azure Pipelines CI and PR jobs for hypothesis
pganssle 46cbf7d
Add GHA job to run Hypothesis tests
pganssle 728f5e0
Use independent build stages for hypothesis GHA job
pganssle a305d29
Revert "Add Azure Pipelines CI and PR jobs for hypothesis"
pganssle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
- Loading branch information
commit 9bb7f5c02c55c6b0e86b2ea44c45ff07e4de059e
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| from .test_zoneinfo import * | ||
| from .test_zoneinfo_property import * |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,318 @@ | ||
| import contextlib | ||
| import datetime | ||
| import os | ||
| import pickle | ||
| import unittest | ||
| import zoneinfo | ||
|
|
||
| from test.support.hypothesis_helper import hypothesis | ||
|
|
||
| import test.test_zoneinfo._support as test_support | ||
|
|
||
| ZoneInfoTestBase = test_support.ZoneInfoTestBase | ||
|
|
||
| py_zoneinfo, c_zoneinfo = test_support.get_modules() | ||
|
|
||
| UTC = datetime.timezone.utc | ||
| MIN_UTC = datetime.datetime.min.replace(tzinfo=UTC) | ||
| MAX_UTC = datetime.datetime.max.replace(tzinfo=UTC) | ||
| ZERO = datetime.timedelta(0) | ||
|
|
||
|
|
||
| def _valid_keys(): | ||
| """Get available time zones, including posix/ and right/ directories.""" | ||
| from importlib import resources | ||
|
|
||
| available_zones = sorted(zoneinfo.available_timezones()) | ||
| TZPATH = zoneinfo.TZPATH | ||
|
|
||
| def valid_key(key): | ||
| for root in TZPATH: | ||
| key_file = os.path.join(root, key) | ||
| if os.path.exists(key_file): | ||
| return True | ||
|
|
||
| components = key.split("/") | ||
| package_name = ".".join(["tzdata.zoneinfo"] + components[:-1]) | ||
| resource_name = components[-1] | ||
|
|
||
| try: | ||
| return resources.files(package_name).joinpath(resource_name).is_file() | ||
| except ModuleNotFoundError: | ||
| return False | ||
|
|
||
| # This relies on the fact that dictionaries maintain insertion order — for | ||
| # shrinking purposes, it is preferable to start with the standard version, | ||
| # then move to the posix/ version, then to the right/ version. | ||
| out_zones = {"": available_zones} | ||
| for prefix in ["posix", "right"]: | ||
| prefix_out = [] | ||
| for key in available_zones: | ||
| prefix_key = f"{prefix}/{key}" | ||
| if valid_key(prefix_key): | ||
| prefix_out.append(prefix_key) | ||
|
|
||
| out_zones[prefix] = prefix_out | ||
|
|
||
| output = [] | ||
| for keys in out_zones.values(): | ||
| output.extend(keys) | ||
|
|
||
| return output | ||
|
|
||
|
|
||
| VALID_KEYS = _valid_keys() | ||
| if not VALID_KEYS: | ||
| raise unittest.SkipTest("No time zone data available") | ||
|
|
||
|
|
||
| def valid_keys(): | ||
| return hypothesis.strategies.sampled_from(VALID_KEYS) | ||
|
|
||
|
|
||
| class ZoneInfoTest(ZoneInfoTestBase): | ||
| module = py_zoneinfo | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_str(self, key): | ||
| zi = self.klass(key) | ||
| self.assertEqual(str(zi), key) | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_key(self, key): | ||
| zi = self.klass(key) | ||
|
|
||
| self.assertEqual(zi.key, key) | ||
|
|
||
| @hypothesis.given( | ||
| dt=hypothesis.strategies.one_of( | ||
| hypothesis.strategies.datetimes(), hypothesis.strategies.times() | ||
| ) | ||
| ) | ||
| def test_utc(self, dt): | ||
| zi = self.klass("UTC") | ||
| dt_zi = dt.replace(tzinfo=zi) | ||
|
|
||
| self.assertEqual(dt_zi.utcoffset(), ZERO) | ||
| self.assertEqual(dt_zi.dst(), ZERO) | ||
| self.assertEqual(dt_zi.tzname(), "UTC") | ||
|
|
||
|
|
||
| class CZoneInfoTest(ZoneInfoTest): | ||
| module = c_zoneinfo | ||
|
|
||
|
|
||
| class ZoneInfoPickleTest(ZoneInfoTestBase): | ||
| module = py_zoneinfo | ||
|
|
||
| def setUp(self): | ||
| with contextlib.ExitStack() as stack: | ||
| stack.enter_context(test_support.set_zoneinfo_module(self.module)) | ||
| self.addCleanup(stack.pop_all().close) | ||
|
|
||
| super().setUp() | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_pickle_unpickle_cache(self, key): | ||
| zi = self.klass(key) | ||
| pkl_str = pickle.dumps(zi) | ||
| zi_rt = pickle.loads(pkl_str) | ||
|
|
||
| self.assertIs(zi, zi_rt) | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_pickle_unpickle_no_cache(self, key): | ||
| zi = self.klass.no_cache(key) | ||
| pkl_str = pickle.dumps(zi) | ||
| zi_rt = pickle.loads(pkl_str) | ||
|
|
||
| self.assertIsNot(zi, zi_rt) | ||
| self.assertEqual(str(zi), str(zi_rt)) | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_pickle_unpickle_cache_multiple_rounds(self, key): | ||
| """Test that pickle/unpickle is idempotent.""" | ||
| zi_0 = self.klass(key) | ||
| pkl_str_0 = pickle.dumps(zi_0) | ||
| zi_1 = pickle.loads(pkl_str_0) | ||
| pkl_str_1 = pickle.dumps(zi_1) | ||
| zi_2 = pickle.loads(pkl_str_1) | ||
| pkl_str_2 = pickle.dumps(zi_2) | ||
|
|
||
| self.assertEqual(pkl_str_0, pkl_str_1) | ||
| self.assertEqual(pkl_str_1, pkl_str_2) | ||
|
|
||
| self.assertIs(zi_0, zi_1) | ||
| self.assertIs(zi_0, zi_2) | ||
| self.assertIs(zi_1, zi_2) | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_pickle_unpickle_no_cache_multiple_rounds(self, key): | ||
| """Test that pickle/unpickle is idempotent.""" | ||
| zi_cache = self.klass(key) | ||
|
|
||
| zi_0 = self.klass.no_cache(key) | ||
| pkl_str_0 = pickle.dumps(zi_0) | ||
| zi_1 = pickle.loads(pkl_str_0) | ||
| pkl_str_1 = pickle.dumps(zi_1) | ||
| zi_2 = pickle.loads(pkl_str_1) | ||
| pkl_str_2 = pickle.dumps(zi_2) | ||
|
|
||
| self.assertEqual(pkl_str_0, pkl_str_1) | ||
| self.assertEqual(pkl_str_1, pkl_str_2) | ||
|
|
||
| self.assertIsNot(zi_0, zi_1) | ||
| self.assertIsNot(zi_0, zi_2) | ||
| self.assertIsNot(zi_1, zi_2) | ||
|
|
||
| self.assertIsNot(zi_0, zi_cache) | ||
| self.assertIsNot(zi_1, zi_cache) | ||
| self.assertIsNot(zi_2, zi_cache) | ||
|
|
||
|
|
||
| class CZoneInfoPickleTest(ZoneInfoPickleTest): | ||
| module = c_zoneinfo | ||
|
|
||
|
|
||
| class ZoneInfoCacheTest(ZoneInfoTestBase): | ||
| module = py_zoneinfo | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_cache(self, key): | ||
| zi_0 = self.klass(key) | ||
| zi_1 = self.klass(key) | ||
|
|
||
| self.assertIs(zi_0, zi_1) | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_no_cache(self, key): | ||
| zi_0 = self.klass.no_cache(key) | ||
| zi_1 = self.klass.no_cache(key) | ||
|
|
||
| self.assertIsNot(zi_0, zi_1) | ||
|
|
||
|
|
||
| class CZoneInfoCacheTest(ZoneInfoCacheTest): | ||
| klass = c_zoneinfo.ZoneInfo | ||
|
|
||
|
|
||
| class PythonCConsistencyTest(unittest.TestCase): | ||
| """Tests that the C and Python versions do the same thing.""" | ||
|
|
||
| def _is_ambiguous(self, dt): | ||
| return dt.replace(fold=not dt.fold).utcoffset() == dt.utcoffset() | ||
|
|
||
| @hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys()) | ||
| def test_same_str(self, dt, key): | ||
| py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key)) | ||
| c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key)) | ||
|
|
||
| self.assertEqual(str(py_dt), str(c_dt)) | ||
|
|
||
| @hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys()) | ||
| def test_same_offsets_and_names(self, dt, key): | ||
| py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key)) | ||
| c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key)) | ||
|
|
||
| self.assertEqual(py_dt.tzname(), c_dt.tzname()) | ||
| self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset()) | ||
| self.assertEqual(py_dt.dst(), c_dt.dst()) | ||
|
|
||
| @hypothesis.given( | ||
| dt=hypothesis.strategies.datetimes(timezones=hypothesis.strategies.just(UTC)), | ||
| key=valid_keys(), | ||
| ) | ||
| @hypothesis.example(dt=MIN_UTC, key="Asia/Tokyo") | ||
| @hypothesis.example(dt=MAX_UTC, key="Asia/Tokyo") | ||
| @hypothesis.example(dt=MIN_UTC, key="America/New_York") | ||
| @hypothesis.example(dt=MAX_UTC, key="America/New_York") | ||
| @hypothesis.example( | ||
| dt=datetime.datetime(2006, 10, 29, 5, 15, tzinfo=UTC), | ||
| key="America/New_York", | ||
| ) | ||
| def test_same_from_utc(self, dt, key): | ||
| py_zi = py_zoneinfo.ZoneInfo(key) | ||
| c_zi = c_zoneinfo.ZoneInfo(key) | ||
|
|
||
| # Convert to UTC: This can overflow, but we just care about consistency | ||
| py_overflow_exc = None | ||
| c_overflow_exc = None | ||
| try: | ||
| py_dt = dt.astimezone(py_zi) | ||
| except OverflowError as e: | ||
| py_overflow_exc = e | ||
|
|
||
| try: | ||
| c_dt = dt.astimezone(c_zi) | ||
| except OverflowError as e: | ||
| c_overflow_exc = e | ||
|
|
||
| if (py_overflow_exc is not None) != (c_overflow_exc is not None): | ||
| raise py_overflow_exc or c_overflow_exc # pragma: nocover | ||
|
|
||
| if py_overflow_exc is not None: | ||
| return # Consistently raises the same exception | ||
|
|
||
| # PEP 495 says that an inter-zone comparison between ambiguous | ||
| # datetimes is always False. | ||
| if py_dt != c_dt: | ||
| self.assertEqual( | ||
| self._is_ambiguous(py_dt), | ||
| self._is_ambiguous(c_dt), | ||
| (py_dt, c_dt), | ||
| ) | ||
|
|
||
| self.assertEqual(py_dt.tzname(), c_dt.tzname()) | ||
| self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset()) | ||
| self.assertEqual(py_dt.dst(), c_dt.dst()) | ||
|
|
||
| @hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys()) | ||
| @hypothesis.example(dt=datetime.datetime.max, key="America/New_York") | ||
| @hypothesis.example(dt=datetime.datetime.min, key="America/New_York") | ||
| @hypothesis.example(dt=datetime.datetime.min, key="Asia/Tokyo") | ||
| @hypothesis.example(dt=datetime.datetime.max, key="Asia/Tokyo") | ||
| def test_same_to_utc(self, dt, key): | ||
| py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key)) | ||
| c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key)) | ||
|
|
||
| # Convert from UTC: Overflow OK if it happens in both implementations | ||
| py_overflow_exc = None | ||
| c_overflow_exc = None | ||
| try: | ||
| py_utc = py_dt.astimezone(UTC) | ||
| except OverflowError as e: | ||
| py_overflow_exc = e | ||
|
|
||
| try: | ||
| c_utc = c_dt.astimezone(UTC) | ||
| except OverflowError as e: | ||
| c_overflow_exc = e | ||
|
|
||
| if (py_overflow_exc is not None) != (c_overflow_exc is not None): | ||
| raise py_overflow_exc or c_overflow_exc # pragma: nocover | ||
|
|
||
| if py_overflow_exc is not None: | ||
| return # Consistently raises the same exception | ||
|
|
||
| self.assertEqual(py_utc, c_utc) | ||
|
|
||
| @hypothesis.given(key=valid_keys()) | ||
| def test_cross_module_pickle(self, key): | ||
| py_zi = py_zoneinfo.ZoneInfo(key) | ||
| c_zi = c_zoneinfo.ZoneInfo(key) | ||
|
|
||
| with test_support.set_zoneinfo_module(py_zoneinfo): | ||
| py_pkl = pickle.dumps(py_zi) | ||
|
|
||
| with test_support.set_zoneinfo_module(c_zoneinfo): | ||
| c_pkl = pickle.dumps(c_zi) | ||
|
|
||
| with test_support.set_zoneinfo_module(c_zoneinfo): | ||
| # Python → C | ||
| py_to_c_zi = pickle.loads(py_pkl) | ||
| self.assertIs(py_to_c_zi, c_zi) | ||
|
|
||
| with test_support.set_zoneinfo_module(py_zoneinfo): | ||
| # C → Python | ||
| c_to_py_zi = pickle.loads(c_pkl) | ||
| self.assertIs(c_to_py_zi, py_zi) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.