From 09059241d4bf6d8070b98eed099c8f5ad01072eb Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 7 Aug 2026 15:21:04 +0100 Subject: [PATCH] [3.15] gh-154948: Fix `test_zipfile` failure when `SOURCE_DATE_EPOCH` is set (GH-155001) (cherry picked from commit 82905dd165f198f1b18625ae0f0ede9367df75a4) Co-authored-by: Stan Ulbrych --- 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 | 36 +++++++--------- 7 files changed, 105 insertions(+), 81 deletions(-) diff --git a/Doc/library/test.rst b/Doc/library/test.rst index 660847ae3fe3c8..54839166e91249 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 daf6060940e97f..e1e2e69cb3d833 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 95dcb4ef9fdc20..9a6ca2be1624c6 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 e4bd850f3514ff..ca2cc045fa674e 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 d8acd1f04356e7..081fd0cf3945e0 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 874c6bb76b1afe..554b1a8ad2c5bb 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -166,21 +166,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 85bc010fe00784..feec688726a803 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -21,14 +21,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 @@ -1883,29 +1884,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