Skip to content

Commit 13d9205

Browse files
bpo-45629: Add a test for the "freeze" tool. (gh-29222)
The "freeze" tool has been part of the repo for a long time. However, it hasn't had any tests in the test suite to guard against regressions. We add such a test here. This is especially important as there has been a lot of change recently related to frozen modules, with more to come. Note that as part of the test we build Python out-of-tree and install it in a temp dir. https://bugs.python.org/issue45629
1 parent 7f61d9d commit 13d9205

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ Tools/unicode/data/
120120
Tools/msi/obj
121121
Tools/ssl/amd64
122122
Tools/ssl/win32
123+
Tools/freeze/test/outdir
123124

124125
# The frozen modules are always generated by the build so we don't
125126
# keep them in the repo. Also see Tools/scripts/freeze_modules.py.

Lib/test/support/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,17 @@ def wrapper(*args, **kw):
372372
return decorator
373373

374374

375+
def skip_if_buildbot(reason=None):
376+
"""Decorator raising SkipTest if running on a buildbot."""
377+
if not reason:
378+
reason = 'not suitable for buildbots'
379+
if sys.platform == 'win32':
380+
isbuildbot = os.environ.get('USERNAME') == 'Buildbot'
381+
else:
382+
isbuildbot = os.environ.get('USER') == 'buildbot'
383+
return unittest.skipIf(isbuildbot, reason)
384+
385+
375386
def system_must_validate_cert(f):
376387
"""Skip the test on TLS certificate validation failures."""
377388
@functools.wraps(f)

Lib/test/test_tools/test_freeze.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Sanity-check tests for the "freeze" tool."""
2+
3+
import sys
4+
import textwrap
5+
import unittest
6+
7+
from test import support
8+
9+
from . import imports_under_tool, skip_if_missing
10+
skip_if_missing('freeze')
11+
with imports_under_tool('freeze', 'test'):
12+
import freeze as helper
13+
14+
15+
@unittest.skipIf(sys.platform.startswith('win'), 'not supported on Windows')
16+
@support.skip_if_buildbot('not all buildbots have enough space')
17+
class TestFreeze(unittest.TestCase):
18+
19+
def test_freeze_simple_script(self):
20+
script = textwrap.dedent("""
21+
import sys
22+
print('running...')
23+
sys.exit(0)
24+
""")
25+
outdir, scriptfile, python = helper.prepare(script)
26+
27+
executable = helper.freeze(python, scriptfile, outdir)
28+
text = helper.run(executable)
29+
self.assertEqual(text, 'running...')

Tools/freeze/test/freeze.py

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import os
2+
import os.path
3+
import re
4+
import shlex
5+
import shutil
6+
import subprocess
7+
8+
9+
TESTS_DIR = os.path.dirname(__file__)
10+
TOOL_ROOT = os.path.dirname(TESTS_DIR)
11+
SRCDIR = os.path.dirname(os.path.dirname(TOOL_ROOT))
12+
13+
MAKE = shutil.which('make')
14+
GIT = shutil.which('git')
15+
FREEZE = os.path.join(TOOL_ROOT, 'freeze.py')
16+
OUTDIR = os.path.join(TESTS_DIR, 'outdir')
17+
18+
19+
class UnsupportedError(Exception):
20+
"""The operation isn't supported."""
21+
22+
23+
def _run_quiet(cmd, cwd=None):
24+
#print(f'# {" ".join(shlex.quote(a) for a in cmd)}')
25+
return subprocess.run(
26+
cmd,
27+
cwd=cwd,
28+
capture_output=True,
29+
text=True,
30+
check=True,
31+
)
32+
33+
34+
def _run_stdout(cmd, cwd=None):
35+
proc = _run_quiet(cmd, cwd)
36+
return proc.stdout.strip()
37+
38+
39+
def find_opt(args, name):
40+
opt = f'--{name}'
41+
optstart = f'{opt}='
42+
for i, arg in enumerate(args):
43+
if arg == opt or arg.startswith(optstart):
44+
return i
45+
return -1
46+
47+
48+
def ensure_opt(args, name, value):
49+
opt = f'--{name}'
50+
pos = find_opt(args, name)
51+
if value is None:
52+
if pos < 0:
53+
args.append(opt)
54+
else:
55+
args[pos] = opt
56+
elif pos < 0:
57+
args.extend([opt, value])
58+
else:
59+
arg = args[pos]
60+
if arg == opt:
61+
if pos == len(args) - 1:
62+
raise NotImplementedError((args, opt))
63+
args[pos + 1] = value
64+
else:
65+
args[pos] = f'{opt}={value}'
66+
67+
68+
def git_copy_repo(newroot, oldroot):
69+
if not GIT:
70+
raise UnsupportedError('git')
71+
72+
if os.path.exists(newroot):
73+
print(f'updating copied repo {newroot}...')
74+
if newroot == SRCDIR:
75+
raise Exception('this probably isn\'t what you wanted')
76+
_run_quiet([GIT, 'clean', '-d', '-f'], newroot)
77+
_run_quiet([GIT, 'reset'], newroot)
78+
_run_quiet([GIT, 'checkout', '.'], newroot)
79+
_run_quiet([GIT, 'pull', '-f', oldroot], newroot)
80+
else:
81+
print(f'copying repo into {newroot}...')
82+
_run_quiet([GIT, 'clone', oldroot, newroot])
83+
84+
# Copy over any uncommited files.
85+
text = _run_stdout([GIT, 'status', '-s'], oldroot)
86+
for line in text.splitlines():
87+
_, _, relfile = line.strip().partition(' ')
88+
relfile = relfile.strip()
89+
isdir = relfile.endswith(os.path.sep)
90+
relfile = relfile.rstrip(os.path.sep)
91+
srcfile = os.path.join(oldroot, relfile)
92+
dstfile = os.path.join(newroot, relfile)
93+
os.makedirs(os.path.dirname(dstfile), exist_ok=True)
94+
if isdir:
95+
shutil.copytree(srcfile, dstfile, dirs_exist_ok=True)
96+
else:
97+
shutil.copy2(srcfile, dstfile)
98+
99+
100+
def get_makefile_var(builddir, name):
101+
regex = re.compile(rf'^{name} *=\s*(.*?)\s*$')
102+
filename = os.path.join(builddir, 'Makefile')
103+
try:
104+
infile = open(filename)
105+
except FileNotFoundError:
106+
return None
107+
with infile:
108+
for line in infile:
109+
m = regex.match(line)
110+
if m:
111+
value, = m.groups()
112+
return value or ''
113+
return None
114+
115+
116+
def get_config_var(builddir, name):
117+
python = os.path.join(builddir, 'python')
118+
if os.path.isfile(python):
119+
cmd = [python, '-c',
120+
f'import sysconfig; print(sysconfig.get_config_var("{name}"))']
121+
try:
122+
return _run_stdout(cmd)
123+
except subprocess.CalledProcessError:
124+
pass
125+
return get_makefile_var(builddir, name)
126+
127+
128+
##################################
129+
# freezing
130+
131+
def prepare(script=None, outdir=None):
132+
if not outdir:
133+
outdir = OUTDIR
134+
os.makedirs(outdir, exist_ok=True)
135+
136+
# Write the script to disk.
137+
if script:
138+
scriptfile = os.path.join(outdir, 'app.py')
139+
with open(scriptfile, 'w') as outfile:
140+
outfile.write(script)
141+
142+
# Make a copy of the repo to avoid affecting the current build.
143+
srcdir = os.path.join(outdir, 'cpython')
144+
git_copy_repo(srcdir, SRCDIR)
145+
146+
# We use an out-of-tree build (instead of srcdir).
147+
builddir = os.path.join(outdir, 'python-build')
148+
os.makedirs(builddir, exist_ok=True)
149+
150+
# Run configure.
151+
print(f'configuring python in {builddir}...')
152+
cmd = [
153+
os.path.join(srcdir, 'configure'),
154+
*shlex.split(get_config_var(builddir, 'CONFIG_ARGS') or ''),
155+
]
156+
ensure_opt(cmd, 'cache-file', os.path.join(outdir, 'python-config.cache'))
157+
prefix = os.path.join(outdir, 'python-installation')
158+
ensure_opt(cmd, 'prefix', prefix)
159+
_run_quiet(cmd, builddir)
160+
161+
if not MAKE:
162+
raise UnsupportedError('make')
163+
164+
# Build python.
165+
print('building python...')
166+
if os.path.exists(os.path.join(srcdir, 'Makefile')):
167+
# Out-of-tree builds require a clean srcdir.
168+
_run_quiet([MAKE, '-C', srcdir, 'clean'])
169+
_run_quiet([MAKE, '-C', builddir, '-j8'])
170+
171+
# Install the build.
172+
print(f'installing python into {prefix}...')
173+
_run_quiet([MAKE, '-C', builddir, '-j8', 'install'])
174+
python = os.path.join(prefix, 'bin', 'python3')
175+
176+
return outdir, scriptfile, python
177+
178+
179+
def freeze(python, scriptfile, outdir):
180+
if not MAKE:
181+
raise UnsupportedError('make')
182+
183+
print(f'freezing {scriptfile}...')
184+
os.makedirs(outdir, exist_ok=True)
185+
_run_quiet([python, FREEZE, '-o', outdir, scriptfile], outdir)
186+
_run_quiet([MAKE, '-C', os.path.dirname(scriptfile)])
187+
188+
name = os.path.basename(scriptfile).rpartition('.')[0]
189+
executable = os.path.join(outdir, name)
190+
return executable
191+
192+
193+
def run(executable):
194+
return _run_stdout([executable])

0 commit comments

Comments
 (0)