timeout-minutes: 10
outputs:
run_tests: ${{ steps.check.outputs.run_tests }}
+ run_hypothesis: ${{ steps.check.outputs.run_hypothesis }}
steps:
- uses: actions/checkout@v3
- name: Check for source changes
git diff --name-only origin/$GITHUB_BASE_REF.. | grep -qvE '(\.rst$|^Doc|^Misc)' && echo "run_tests=true" >> $GITHUB_OUTPUT || true
fi
+ # Check if we should run hypothesis tests
+ GIT_BRANCH=${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}
+ echo $GIT_BRANCH
+ if $(echo "$GIT_BRANCH" | grep -q -w '3\.\(8\|9\|10\|11\)'); then
+ echo "Branch too old for hypothesis tests"
+ echo "run_hypothesis=false" >> $GITHUB_OUTPUT
+ else
+ echo "Run hypothesis tests"
+ echo "run_hypothesis=true" >> $GITHUB_OUTPUT
+ fi
+
check_generated_files:
name: 'Check if generated files are up to date'
runs-on: ubuntu-latest
- name: SSL tests
run: ./python Lib/test/ssltests.py
+ test_hypothesis:
+ name: "Hypothesis Tests on Ubuntu"
+ runs-on: ubuntu-20.04
+ timeout-minutes: 60
+ needs: check_source
+ if: needs.check_source.outputs.run_tests == 'true' && needs.check_source.outputs.run_hypothesis == 'true'
+ env:
+ OPENSSL_VER: 1.1.1t
+ PYTHONSTRICTEXTENSIONBUILD: 1
+ steps:
+ - uses: actions/checkout@v3
+ - name: Register gcc problem matcher
+ run: echo "::add-matcher::.github/problem-matchers/gcc.json"
+ - name: Install Dependencies
+ run: sudo ./.github/workflows/posix-deps-apt.sh
+ - name: Configure OpenSSL env vars
+ run: |
+ echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> $GITHUB_ENV
+ echo "OPENSSL_DIR=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}" >> $GITHUB_ENV
+ echo "LD_LIBRARY_PATH=${GITHUB_WORKSPACE}/multissl/openssl/${OPENSSL_VER}/lib" >> $GITHUB_ENV
+ - name: 'Restore OpenSSL build'
+ id: cache-openssl
+ uses: actions/cache@v3
+ with:
+ path: ./multissl/openssl/${{ env.OPENSSL_VER }}
+ key: ${{ runner.os }}-multissl-openssl-${{ env.OPENSSL_VER }}
+ - name: Install OpenSSL
+ if: steps.cache-openssl.outputs.cache-hit != 'true'
+ run: python3 Tools/ssl/multissltests.py --steps=library --base-directory $MULTISSL_DIR --openssl $OPENSSL_VER --system Linux
+ - name: Add ccache to PATH
+ run: |
+ echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV
+ - name: Configure ccache action
+ uses: hendrikmuhs/ccache-action@v1.2
+ - name: Setup directory envs for out-of-tree builds
+ run: |
+ echo "CPYTHON_RO_SRCDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-ro-srcdir)" >> $GITHUB_ENV
+ echo "CPYTHON_BUILDDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-builddir)" >> $GITHUB_ENV
+ - name: Create directories for read-only out-of-tree builds
+ run: mkdir -p $CPYTHON_RO_SRCDIR $CPYTHON_BUILDDIR
+ - name: Bind mount sources read-only
+ run: sudo mount --bind -o ro $GITHUB_WORKSPACE $CPYTHON_RO_SRCDIR
+ - name: Configure CPython out-of-tree
+ working-directory: ${{ env.CPYTHON_BUILDDIR }}
+ run: ../cpython-ro-srcdir/configure --with-pydebug --with-openssl=$OPENSSL_DIR
+ - name: Build CPython out-of-tree
+ working-directory: ${{ env.CPYTHON_BUILDDIR }}
+ run: make -j4
+ - name: Display build info
+ working-directory: ${{ env.CPYTHON_BUILDDIR }}
+ run: make pythoninfo
+ - name: Remount sources writable for tests
+ # some tests write to srcdir, lack of pyc files slows down testing
+ run: sudo mount $CPYTHON_RO_SRCDIR -oremount,rw
+ - name: Setup directory envs for out-of-tree builds
+ run: |
+ echo "CPYTHON_BUILDDIR=$(realpath -m ${GITHUB_WORKSPACE}/../cpython-builddir)" >> $GITHUB_ENV
+ - name: "Create hypothesis venv"
+ working-directory: ${{ env.CPYTHON_BUILDDIR }}
+ run: |
+ VENV_LOC=$(realpath -m .)/hypovenv
+ VENV_PYTHON=$VENV_LOC/bin/python
+ echo "HYPOVENV=${VENV_LOC}" >> $GITHUB_ENV
+ echo "VENV_PYTHON=${VENV_PYTHON}" >> $GITHUB_ENV
+ ./python -m venv $VENV_LOC && $VENV_PYTHON -m pip install -U hypothesis
+ - name: "Run tests"
+ working-directory: ${{ env.CPYTHON_BUILDDIR }}
+ run: |
+ # Most of the excluded tests are slow test suites with no property tests
+ #
+ # (GH-104097) test_sysconfig is skipped because it has tests that are
+ # failing when executed from inside a virtual environment.
+ ${{ env.VENV_PYTHON }} -m test \
+ -W \
+ -x test_asyncio \
+ -x test_multiprocessing_fork \
+ -x test_multiprocessing_forkserver \
+ -x test_multiprocessing_spawn \
+ -x test_concurrent_futures \
+ -x test_socket \
+ -x test_subprocess \
+ -x test_signal \
+ -x test_sysconfig
+
build_asan:
name: 'Address sanitizer'
--- /dev/null
+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)
+
+
+KEY_EXAMPLES = [
+ "Africa/Abidjan",
+ "Africa/Casablanca",
+ "America/Los_Angeles",
+ "America/Santiago",
+ "Asia/Tokyo",
+ "Australia/Sydney",
+ "Europe/Dublin",
+ "Europe/Lisbon",
+ "Europe/London",
+ "Pacific/Kiritimati",
+ "UTC",
+]
+
+
+def add_key_examples(f):
+ for key in KEY_EXAMPLES:
+ f = hypothesis.example(key)(f)
+ return f
+
+
+class ZoneInfoTest(ZoneInfoTestBase):
+ module = py_zoneinfo
+
+ @hypothesis.given(key=valid_keys())
+ @add_key_examples
+ def test_str(self, key):
+ zi = self.klass(key)
+ self.assertEqual(str(zi), key)
+
+ @hypothesis.given(key=valid_keys())
+ @add_key_examples
+ 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()
+ )
+ )
+ @hypothesis.example(dt=datetime.datetime.min)
+ @hypothesis.example(dt=datetime.datetime.max)
+ @hypothesis.example(dt=datetime.datetime(1970, 1, 1))
+ @hypothesis.example(dt=datetime.datetime(2039, 1, 1))
+ @hypothesis.example(dt=datetime.time(0))
+ @hypothesis.example(dt=datetime.time(12, 0))
+ @hypothesis.example(dt=datetime.time(23, 59, 59, 999999))
+ 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())
+ @add_key_examples
+ 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())
+ @add_key_examples
+ 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())
+ @add_key_examples
+ 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())
+ @add_key_examples
+ 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())
+ @add_key_examples
+ 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())
+ @add_key_examples
+ 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())
+ @hypothesis.example(dt=datetime.datetime.min, key="America/New_York")
+ @hypothesis.example(dt=datetime.datetime.max, key="America/New_York")
+ @hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="America/New_York")
+ @hypothesis.example(dt=datetime.datetime(2020, 1, 1), key="Europe/Paris")
+ @hypothesis.example(dt=datetime.datetime(2020, 6, 1), key="Europe/Paris")
+ 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())
+ @hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="America/New_York")
+ @hypothesis.example(dt=datetime.datetime(2020, 2, 5), key="America/New_York")
+ @hypothesis.example(dt=datetime.datetime(2020, 8, 12), key="America/New_York")
+ @hypothesis.example(dt=datetime.datetime(2040, 1, 1), key="Africa/Casablanca")
+ @hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="Europe/Paris")
+ @hypothesis.example(dt=datetime.datetime(2040, 1, 1), key="Europe/Paris")
+ @hypothesis.example(dt=datetime.datetime.min, key="Asia/Tokyo")
+ @hypothesis.example(dt=datetime.datetime.max, key="Asia/Tokyo")
+ 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())
+ @add_key_examples
+ 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)