Skip to content

Commit 82905dd

Browse files
gh-154948: Fix test_zipfile failure when SOURCE_DATE_EPOCH is set (#155001)
1 parent 298bef3 commit 82905dd

7 files changed

Lines changed: 109 additions & 81 deletions

File tree

Doc/library/test.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,32 @@ The :mod:`!test.support.os_helper` module provides support for os tests.
16561656
wrapped with a wait loop that checks for the existence of the file.
16571657

16581658

1659+
.. decorator:: with_source_date_epoch(*, epoch=123456789)
1660+
1661+
A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH`
1662+
environment variable set to *epoch*.
1663+
1664+
1665+
.. decorator:: without_source_date_epoch
1666+
1667+
A decorator for running tests with the :envvar:`SOURCE_DATE_EPOCH`
1668+
environment variable unset.
1669+
1670+
1671+
.. class:: SourceDateEpochTestMeta
1672+
1673+
Metaclass wrapping all test methods of the class with
1674+
:func:`with_source_date_epoch` if the *source_date_epoch* keyword class
1675+
argument is true, or with :func:`without_source_date_epoch` otherwise.
1676+
For example::
1677+
1678+
class TestsWithSourceEpoch(Tests,
1679+
metaclass=SourceDateEpochTestMeta,
1680+
source_date_epoch=True):
1681+
pass
1682+
1683+
1684+
16591685
:mod:`!test.support.import_helper` --- Utilities for import tests
16601686
=================================================================
16611687

Lib/test/support/os_helper.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import collections.abc
22
import contextlib
33
import errno
4+
import functools
45
import logging
56
import os
67
import re
@@ -806,6 +807,48 @@ def __exit__(self, *ignore_exc):
806807
os.environ = self._environ
807808

808809

810+
def without_source_date_epoch(fxn):
811+
"""Runs function with SOURCE_DATE_EPOCH unset."""
812+
@functools.wraps(fxn)
813+
def wrapper(*args, **kwargs):
814+
with EnvironmentVarGuard() as env:
815+
env.unset('SOURCE_DATE_EPOCH')
816+
return fxn(*args, **kwargs)
817+
return wrapper
818+
819+
820+
_MISSING = sentinel("MISSING")
821+
822+
def with_source_date_epoch(fxn=_MISSING, *, epoch=123456789):
823+
"""Runs function with SOURCE_DATE_EPOCH set to *epoch*."""
824+
if fxn is _MISSING:
825+
return functools.partial(with_source_date_epoch, epoch=epoch)
826+
827+
@functools.wraps(fxn)
828+
def wrapper(*args, **kwargs):
829+
with EnvironmentVarGuard() as env:
830+
env['SOURCE_DATE_EPOCH'] = str(epoch)
831+
return fxn(*args, **kwargs)
832+
return wrapper
833+
834+
835+
# Run tests with SOURCE_DATE_EPOCH set or unset explicitly.
836+
class SourceDateEpochTestMeta(type(unittest.TestCase)):
837+
def __new__(mcls, name, bases, dct, *, source_date_epoch):
838+
cls = super().__new__(mcls, name, bases, dct)
839+
840+
for attr in dir(cls):
841+
if attr.startswith('test_'):
842+
meth = getattr(cls, attr)
843+
if source_date_epoch:
844+
wrapper = with_source_date_epoch(meth)
845+
else:
846+
wrapper = without_source_date_epoch(meth)
847+
setattr(cls, attr, wrapper)
848+
849+
return cls
850+
851+
809852
try:
810853
if support.MS_WINDOWS:
811854
import ctypes

Lib/test/test_compileall.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
from test import support
2929
from test.support import os_helper
3030
from test.support import script_helper
31-
from test.test_py_compile import without_source_date_epoch
32-
from test.test_py_compile import SourceDateEpochTestMeta
31+
from test.support.os_helper import without_source_date_epoch
32+
from test.support.os_helper import SourceDateEpochTestMeta
3333
from test.support.os_helper import FakePath
3434

3535

Lib/test/test_importlib/source/test_file_loader.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,18 @@
55
machinery = util.import_importlib('importlib.machinery')
66
importlib_util = util.import_importlib('importlib.util')
77

8-
import errno
98
import marshal
109
import os
1110
import py_compile
12-
import shutil
1311
import stat
1412
import sys
1513
import types
1614
import unittest
17-
import warnings
1815

19-
from test.support.import_helper import make_legacy_pyc, unload
16+
from test.support.import_helper import make_legacy_pyc
2017

21-
from test.test_py_compile import without_source_date_epoch
22-
from test.test_py_compile import SourceDateEpochTestMeta
18+
from test.support.os_helper import without_source_date_epoch
19+
from test.support.os_helper import SourceDateEpochTestMeta
2320

2421

2522
class SimpleTest:

Lib/test/test_py_compile.py

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import functools
21
import importlib.util
32
import os
43
import py_compile
@@ -11,43 +10,7 @@
1110

1211
from test import support
1312
from test.support import os_helper, script_helper
14-
15-
16-
def without_source_date_epoch(fxn):
17-
"""Runs function with SOURCE_DATE_EPOCH unset."""
18-
@functools.wraps(fxn)
19-
def wrapper(*args, **kwargs):
20-
with os_helper.EnvironmentVarGuard() as env:
21-
env.unset('SOURCE_DATE_EPOCH')
22-
return fxn(*args, **kwargs)
23-
return wrapper
24-
25-
26-
def with_source_date_epoch(fxn):
27-
"""Runs function with SOURCE_DATE_EPOCH set."""
28-
@functools.wraps(fxn)
29-
def wrapper(*args, **kwargs):
30-
with os_helper.EnvironmentVarGuard() as env:
31-
env['SOURCE_DATE_EPOCH'] = '123456789'
32-
return fxn(*args, **kwargs)
33-
return wrapper
34-
35-
36-
# Run tests with SOURCE_DATE_EPOCH set or unset explicitly.
37-
class SourceDateEpochTestMeta(type(unittest.TestCase)):
38-
def __new__(mcls, name, bases, dct, *, source_date_epoch):
39-
cls = super().__new__(mcls, name, bases, dct)
40-
41-
for attr in dir(cls):
42-
if attr.startswith('test_'):
43-
meth = getattr(cls, attr)
44-
if source_date_epoch:
45-
wrapper = with_source_date_epoch(meth)
46-
else:
47-
wrapper = without_source_date_epoch(meth)
48-
setattr(cls, attr, wrapper)
49-
50-
return cls
13+
from test.support.os_helper import SourceDateEpochTestMeta
5114

5215

5316
class PyCompileTestsBase:

Lib/test/test_regrtest.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -170,21 +170,20 @@ def test_randomize(self):
170170
ns = self.parse_args([opt])
171171
self.assertTrue(ns.randomize)
172172

173-
with os_helper.EnvironmentVarGuard() as env:
174-
# with SOURCE_DATE_EPOCH
175-
env['SOURCE_DATE_EPOCH'] = '1697839080'
176-
ns = self.parse_args(['--randomize'])
177-
regrtest = main.Regrtest(ns)
178-
self.assertFalse(regrtest.randomize)
179-
self.assertIsInstance(regrtest.random_seed, str)
180-
self.assertEqual(regrtest.random_seed, '1697839080')
181-
182-
# without SOURCE_DATE_EPOCH
183-
del env['SOURCE_DATE_EPOCH']
184-
ns = self.parse_args(['--randomize'])
185-
regrtest = main.Regrtest(ns)
186-
self.assertTrue(regrtest.randomize)
187-
self.assertIsInstance(regrtest.random_seed, int)
173+
@os_helper.with_source_date_epoch(epoch=1697839080)
174+
def test_randomize_with_source_date_epoch(self):
175+
ns = self.parse_args(['--randomize'])
176+
regrtest = main.Regrtest(ns)
177+
self.assertFalse(regrtest.randomize)
178+
self.assertIsInstance(regrtest.random_seed, str)
179+
self.assertEqual(regrtest.random_seed, '1697839080')
180+
181+
@os_helper.without_source_date_epoch
182+
def test_randomize_without_source_date_epoch(self):
183+
ns = self.parse_args(['--randomize'])
184+
regrtest = main.Regrtest(ns)
185+
self.assertTrue(regrtest.randomize)
186+
self.assertIsInstance(regrtest.random_seed, int)
188187

189188
def test_no_randomize(self):
190189
ns = self.parse_args([])

Lib/test/test_zipfile/test_core.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
from random import randint, random, randbytes
2323

2424
from test import archiver_tests
25-
from test.support import script_helper, os_helper
25+
from test.support import script_helper
2626
from test.support import (
2727
findfile, requires_zlib, requires_bz2, requires_lzma,
2828
requires_zstd, captured_stdout, captured_stderr, requires_subprocess,
2929
cpython_only, gc_collect
3030
)
3131
from test.support.os_helper import (
32-
TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath
32+
TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath,
33+
with_source_date_epoch, without_source_date_epoch,
3334
)
3435
from test.support.import_helper import ensure_lazy_imports
3536
from test.support.warnings_helper import check_no_resource_warning
@@ -1961,6 +1962,7 @@ def test_repack_file_entry_before_first_file(self):
19611962
with zipfile.ZipFile(TESTFN) as zh:
19621963
self.assertIsNone(zh.testzip())
19631964

1965+
@without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below
19641966
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr()
19651967
def test_repack_bytes_before_removed_files(self):
19661968
"""Should preserve if there are bytes before stale local file entries."""
@@ -2005,6 +2007,7 @@ def test_repack_bytes_before_removed_files(self):
20052007
with zipfile.ZipFile(TESTFN) as zh:
20062008
self.assertIsNone(zh.testzip())
20072009

2010+
@without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below
20082011
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr()
20092012
def test_repack_bytes_after_removed_files(self):
20102013
"""Should keep extra bytes if there are bytes after stale local file entries."""
@@ -2048,6 +2051,7 @@ def test_repack_bytes_after_removed_files(self):
20482051
with zipfile.ZipFile(TESTFN) as zh:
20492052
self.assertIsNone(zh.testzip())
20502053

2054+
@without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below
20512055
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr()
20522056
def test_repack_bytes_between_removed_files(self):
20532057
"""Should strip only local file entries before random bytes."""
@@ -2252,6 +2256,7 @@ def test_repack_removed_partial(self):
22522256
with zipfile.ZipFile(TESTFN) as zh:
22532257
self.assertIsNone(zh.testzip())
22542258

2259+
@without_source_date_epoch # SOURCE_DATE_EPOCH would bypass the time mock below
22552260
@mock.patch.object(time, 'time', new=lambda: 315590400) # fix time for ZipFile.writestr()
22562261
def test_repack_removed_bytes_between_files(self):
22572262
"""Should not remove bytes between local file entries."""
@@ -4004,29 +4009,24 @@ def test_writestr_extended_local_header_issue1202(self):
40044009
zinfo.flag_bits |= zipfile._MASK_USE_DATA_DESCRIPTOR # Include an extended local header.
40054010
orig_zip.writestr(zinfo, data)
40064011

4012+
@with_source_date_epoch(epoch=1735715999)
40074013
def test_write_with_source_date_epoch(self):
4008-
with os_helper.EnvironmentVarGuard() as env:
4009-
# Set the SOURCE_DATE_EPOCH environment variable to a specific timestamp
4010-
env['SOURCE_DATE_EPOCH'] = "1735715999"
4011-
4012-
with zipfile.ZipFile(TESTFN, "w") as zf:
4013-
zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH")
4014+
with zipfile.ZipFile(TESTFN, "w") as zf:
4015+
zf.writestr("test_source_date_epoch.txt", "Testing SOURCE_DATE_EPOCH")
40144016

4015-
with zipfile.ZipFile(TESTFN, "r") as zf:
4016-
zip_info = zf.getinfo("test_source_date_epoch.txt")
4017-
expected_utc = (2025, 1, 1, 7, 19, 58)
4018-
self.assertEqual(zip_info.date_time, expected_utc)
4017+
with zipfile.ZipFile(TESTFN, "r") as zf:
4018+
zip_info = zf.getinfo("test_source_date_epoch.txt")
4019+
expected_utc = (2025, 1, 1, 7, 19, 58)
4020+
self.assertEqual(zip_info.date_time, expected_utc)
40194021

4022+
@without_source_date_epoch
40204023
def test_write_without_source_date_epoch(self):
4021-
with os_helper.EnvironmentVarGuard() as env:
4022-
del env['SOURCE_DATE_EPOCH']
4023-
4024-
with zipfile.ZipFile(TESTFN, "w") as zf:
4025-
zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH")
4024+
with zipfile.ZipFile(TESTFN, "w") as zf:
4025+
zf.writestr("test_no_source_date_epoch.txt", "Testing without SOURCE_DATE_EPOCH")
40264026

4027-
with zipfile.ZipFile(TESTFN, "r") as zf:
4028-
zip_info = zf.getinfo("test_no_source_date_epoch.txt")
4029-
self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2)
4027+
with zipfile.ZipFile(TESTFN, "r") as zf:
4028+
zip_info = zf.getinfo("test_no_source_date_epoch.txt")
4029+
self.assertTimestampAlmostEqual(time.localtime(), zip_info.date_time, tolerance=2)
40304030

40314031
def assertTimestampAlmostEqual(self, time1, time2, tolerance):
40324032
import datetime

0 commit comments

Comments
 (0)