From 04326474347008569e8396be1d2bc0f03b71783b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 31 Jul 2026 20:01:14 +0200 Subject: [PATCH] Fix `test_zipfile` failure when `SOURCE_DATE_EPOCH` is set Additionally, move the `SOURCE_DATE_EPOCH` test helpers from `test_py_compile` to `test.support.os_helper`. --- Doc/library/test.rst | 26 +++++++++++ Lib/test/support/os_helper.py | 43 +++++++++++++++++++ Lib/test/test_compileall.py | 4 +- .../test_importlib/source/test_file_loader.py | 9 ++-- Lib/test/test_py_compile.py | 39 +---------------- Lib/test/test_regrtest.py | 29 ++++++------- Lib/test/test_zipfile/test_core.py | 40 ++++++++--------- 7 files changed, 109 insertions(+), 81 deletions(-) diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 660847ae3fe3c85..54839166e912493 100644 --- a/Doc/library/test.rst +++ b/Doc/library/test.rst @@ -1643,6 +1643,32 @@ The :mod:`!test.support.os_helper` module provides support for os tests. wrapped with a wait loop that checks for the existence of the file. +.. decorator:: with_source_date_epoch(*, epoch=123456789) + + A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH` + environment variable set to *epoch*. + + +.. decorator:: without_source_date_epoch + + A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH` + environment variable unset. + + +.. class:: SourceDateEpochTestMeta + + Metaclass wrapping all test methods of the class with + :func:`with_source_date_epoch` if the *source_date_epoch* keyword class + argument is true, or with :func:`without_source_date_epoch` otherwise. + For example:: + + class TestsWithSourceEpoch(Tests, + metaclass=SourceDateEpochTestMeta, + source_date_epoch=True): + pass + + + :mod:`!test.support.import_helper` --- Utilities for import tests ================================================================= diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py index daf6060940e97f0..e1e2e69cb3d8334 100644 --- a/Lib/test/support/os_helper.py +++ b/Lib/test/support/os_helper.py @@ -1,6 +1,7 @@ import collections.abc import contextlib import errno +import functools import logging import os import re @@ -806,6 +807,48 @@ def __exit__(self, *ignore_exc): os.environ = self._environ +def without_source_date_epoch(fxn): + """Runs function with SOURCE_DATE_EPOCH unset.""" + @functools.wraps(fxn) + def wrapper(*args, **kwargs): + with EnvironmentVarGuard() as env: + env.unset('SOURCE_DATE_EPOCH') + return fxn(*args, **kwargs) + return wrapper + + +_MISSING = sentinel("MISSING") + +def with_source_date_epoch(fxn=_MISSING, *, epoch=123456789): + """Runs function with SOURCE_DATE_EPOCH set to *epoch*.""" + if fxn is _MISSING: + return functools.partial(with_source_date_epoch, epoch=epoch) + + @functools.wraps(fxn) + def wrapper(*args, **kwargs): + with EnvironmentVarGuard() as env: + env['SOURCE_DATE_EPOCH'] = str(epoch) + return fxn(*args, **kwargs) + return wrapper + + +# Run tests with SOURCE_DATE_EPOCH set or unset explicitly. +class SourceDateEpochTestMeta(type(unittest.TestCase)): + def __new__(mcls, name, bases, dct, *, source_date_epoch): + cls = super().__new__(mcls, name, bases, dct) + + for attr in dir(cls): + if attr.startswith('test_'): + meth = getattr(cls, attr) + if source_date_epoch: + wrapper = with_source_date_epoch(meth) + else: + wrapper = without_source_date_epoch(meth) + setattr(cls, attr, wrapper) + + return cls + + try: if support.MS_WINDOWS: import ctypes diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py index 95dcb4ef9fdc202..9a6ca2be1624c6c 100644 --- a/Lib/test/test_compileall.py +++ b/Lib/test/test_compileall.py @@ -28,8 +28,8 @@ from test import support from test.support import os_helper from test.support import script_helper -from test.test_py_compile import without_source_date_epoch -from test.test_py_compile import SourceDateEpochTestMeta +from test.support.os_helper import without_source_date_epoch +from test.support.os_helper import SourceDateEpochTestMeta from test.support.os_helper import FakePath diff --git a/Lib/test/test_importlib/source/test_file_loader.py b/Lib/test/test_importlib/source/test_file_loader.py index e4bd850f3514ff1..ca2cc045fa674e1 100644 --- a/Lib/test/test_importlib/source/test_file_loader.py +++ b/Lib/test/test_importlib/source/test_file_loader.py @@ -5,21 +5,18 @@ machinery = util.import_importlib('importlib.machinery') importlib_util = util.import_importlib('importlib.util') -import errno import marshal import os import py_compile -import shutil import stat import sys import types import unittest -import warnings -from test.support.import_helper import make_legacy_pyc, unload +from test.support.import_helper import make_legacy_pyc -from test.test_py_compile import without_source_date_epoch -from test.test_py_compile import SourceDateEpochTestMeta +from test.support.os_helper import without_source_date_epoch +from test.support.os_helper import SourceDateEpochTestMeta class SimpleTest: diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index b4265e6a0b458db..616e5d0c7cf971c 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -1,4 +1,3 @@ -import functools import importlib.util import os import py_compile @@ -11,43 +10,7 @@ from test import support from test.support import os_helper, script_helper - - -def without_source_date_epoch(fxn): - """Runs function with SOURCE_DATE_EPOCH unset.""" - @functools.wraps(fxn) - def wrapper(*args, **kwargs): - with os_helper.EnvironmentVarGuard() as env: - env.unset('SOURCE_DATE_EPOCH') - return fxn(*args, **kwargs) - return wrapper - - -def with_source_date_epoch(fxn): - """Runs function with SOURCE_DATE_EPOCH set.""" - @functools.wraps(fxn) - def wrapper(*args, **kwargs): - with os_helper.EnvironmentVarGuard() as env: - env['SOURCE_DATE_EPOCH'] = '123456789' - return fxn(*args, **kwargs) - return wrapper - - -# Run tests with SOURCE_DATE_EPOCH set or unset explicitly. -class SourceDateEpochTestMeta(type(unittest.TestCase)): - def __new__(mcls, name, bases, dct, *, source_date_epoch): - cls = super().__new__(mcls, name, bases, dct) - - for attr in dir(cls): - if attr.startswith('test_'): - meth = getattr(cls, attr) - if source_date_epoch: - wrapper = with_source_date_epoch(meth) - else: - wrapper = without_source_date_epoch(meth) - setattr(cls, attr, wrapper) - - return cls +from test.support.os_helper import SourceDateEpochTestMeta class PyCompileTestsBase: diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 6d30d267cd5ad29..6ba440053089161 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -170,21 +170,20 @@ def test_randomize(self): ns = self.parse_args([opt]) self.assertTrue(ns.randomize) - with os_helper.EnvironmentVarGuard() as env: - # with SOURCE_DATE_EPOCH - env['SOURCE_DATE_EPOCH'] = '1697839080' - ns = self.parse_args(['--randomize']) - regrtest = main.Regrtest(ns) - self.assertFalse(regrtest.randomize) - self.assertIsInstance(regrtest.random_seed, str) - self.assertEqual(regrtest.random_seed, '1697839080') - - # without SOURCE_DATE_EPOCH - del env['SOURCE_DATE_EPOCH'] - ns = self.parse_args(['--randomize']) - regrtest = main.Regrtest(ns) - self.assertTrue(regrtest.randomize) - self.assertIsInstance(regrtest.random_seed, int) + @os_helper.with_source_date_epoch(epoch=1697839080) + def test_randomize_with_source_date_epoch(self): + ns = self.parse_args(['--randomize']) + regrtest = main.Regrtest(ns) + self.assertFalse(regrtest.randomize) + self.assertIsInstance(regrtest.random_seed, str) + self.assertEqual(regrtest.random_seed, '1697839080') + + @os_helper.without_source_date_epoch + def test_randomize_without_source_date_epoch(self): + ns = self.parse_args(['--randomize']) + regrtest = main.Regrtest(ns) + self.assertTrue(regrtest.randomize) + self.assertIsInstance(regrtest.random_seed, int) def test_no_randomize(self): ns = self.parse_args([]) diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index cd498ba13e6f461..5e384fa74e8c16e 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -22,14 +22,15 @@ from random import randint, random, randbytes from test import archiver_tests -from test.support import script_helper, os_helper +from test.support import script_helper from test.support import ( findfile, requires_zlib, requires_bz2, requires_lzma, requires_zstd, captured_stdout, captured_stderr, requires_subprocess, cpython_only ) from test.support.os_helper import ( - TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath + TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath, + with_source_date_epoch, without_source_date_epoch, ) from test.support.import_helper import ensure_lazy_imports @@ -1960,6 +1961,7 @@ def test_repack_file_entry_before_first_file(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # it would override the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_bytes_before_removed_files(self): """Should preserve if there are bytes before stale local file entries.""" @@ -2004,6 +2006,7 @@ def test_repack_bytes_before_removed_files(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # it would override the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_bytes_after_removed_files(self): """Should keep extra bytes if there are bytes after stale local file entries.""" @@ -2047,6 +2050,7 @@ def test_repack_bytes_after_removed_files(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # it would override the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_bytes_between_removed_files(self): """Should strip only local file entries before random bytes.""" @@ -2251,6 +2255,7 @@ def test_repack_removed_partial(self): with zipfile.ZipFile(TESTFN) as zh: self.assertIsNone(zh.testzip()) + @without_source_date_epoch # it would override the time mock below @mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr() def test_repack_removed_bytes_between_files(self): """Should not remove bytes between local file entries.""" @@ -4003,29 +4008,24 @@ def test_writestr_extended_local_header_issue1202(self): zinfo.flag_bits |= zipfile._MASK_USE_DATA_DESCRIPTOR # Include an extended local header. orig_zip.writestr(zinfo, data) + @with_source_date_epoch(epoch=1735715999) def test_write_with_source_date_epoch(self): - with os_helper.EnvironmentVarGuard() as env: - # Set the SOURCE_DATE_EPOCH environment variable to a specific timestamp - env['SOURCE_DATE_EPOCH'] = "1735715999" - - with zipfile.ZipFile(TESTFN, "w") as zf: - zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH") - with zipfile.ZipFile(TESTFN, "r") as zf: - zip_info = zf.getinfo("test_source_date_epoch.txt") - expected_utc = (2025, 1, 1, 7, 19, 58) - self.assertEqual(zip_info.date_time, expected_utc) + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_source_date_epoch.txt") + expected_utc = (2025, 1, 1, 7, 19, 58) + self.assertEqual(zip_info.date_time, expected_utc) + @without_source_date_epoch def test_write_without_source_date_epoch(self): - with os_helper.EnvironmentVarGuard() as env: - del env['SOURCE_DATE_EPOCH'] - - with zipfile.ZipFile(TESTFN, "w") as zf: - zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH") + with zipfile.ZipFile(TESTFN, "w") as zf: + zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH") - with zipfile.ZipFile(TESTFN, "r") as zf: - zip_info = zf.getinfo("test_no_source_date_epoch.txt") - self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2) + with zipfile.ZipFile(TESTFN, "r") as zf: + zip_info = zf.getinfo("test_no_source_date_epoch.txt") + self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2) def assertTimestampAlmostEqual(self, time1, time2, tolerance): import datetime