Skip to content

Commit

Permalink
bpo-45629: Add a test for the "freeze" tool. (gh-29222)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ericsnowcurrently authored Oct 28, 2021
1 parent 7f61d9d commit 13d9205
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Tools/unicode/data/
Tools/msi/obj
Tools/ssl/amd64
Tools/ssl/win32
Tools/freeze/test/outdir

# The frozen modules are always generated by the build so we don't
# keep them in the repo. Also see Tools/scripts/freeze_modules.py.
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,17 @@ def wrapper(*args, **kw):
return decorator


def skip_if_buildbot(reason=None):
"""Decorator raising SkipTest if running on a buildbot."""
if not reason:
reason = 'not suitable for buildbots'
if sys.platform == 'win32':
isbuildbot = os.environ.get('USERNAME') == 'Buildbot'
else:
isbuildbot = os.environ.get('USER') == 'buildbot'
return unittest.skipIf(isbuildbot, reason)


def system_must_validate_cert(f):
"""Skip the test on TLS certificate validation failures."""
@functools.wraps(f)
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_tools/test_freeze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Sanity-check tests for the "freeze" tool."""

import sys
import textwrap
import unittest

from test import support

from . import imports_under_tool, skip_if_missing
skip_if_missing('freeze')
with imports_under_tool('freeze', 'test'):
import freeze as helper


@unittest.skipIf(sys.platform.startswith('win'), 'not supported on Windows')
@support.skip_if_buildbot('not all buildbots have enough space')
class TestFreeze(unittest.TestCase):

def test_freeze_simple_script(self):
script = textwrap.dedent("""
import sys
print('running...')
sys.exit(0)
""")
outdir, scriptfile, python = helper.prepare(script)

executable = helper.freeze(python, scriptfile, outdir)
text = helper.run(executable)
self.assertEqual(text, 'running...')
194 changes: 194 additions & 0 deletions Tools/freeze/test/freeze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import os
import os.path
import re
import shlex
import shutil
import subprocess


TESTS_DIR = os.path.dirname(__file__)
TOOL_ROOT = os.path.dirname(TESTS_DIR)
SRCDIR = os.path.dirname(os.path.dirname(TOOL_ROOT))

MAKE = shutil.which('make')
GIT = shutil.which('git')
FREEZE = os.path.join(TOOL_ROOT, 'freeze.py')
OUTDIR = os.path.join(TESTS_DIR, 'outdir')


class UnsupportedError(Exception):
"""The operation isn't supported."""


def _run_quiet(cmd, cwd=None):
#print(f'# {" ".join(shlex.quote(a) for a in cmd)}')
return subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
check=True,
)


def _run_stdout(cmd, cwd=None):
proc = _run_quiet(cmd, cwd)
return proc.stdout.strip()


def find_opt(args, name):
opt = f'--{name}'
optstart = f'{opt}='
for i, arg in enumerate(args):
if arg == opt or arg.startswith(optstart):
return i
return -1


def ensure_opt(args, name, value):
opt = f'--{name}'
pos = find_opt(args, name)
if value is None:
if pos < 0:
args.append(opt)
else:
args[pos] = opt
elif pos < 0:
args.extend([opt, value])
else:
arg = args[pos]
if arg == opt:
if pos == len(args) - 1:
raise NotImplementedError((args, opt))
args[pos + 1] = value
else:
args[pos] = f'{opt}={value}'


def git_copy_repo(newroot, oldroot):
if not GIT:
raise UnsupportedError('git')

if os.path.exists(newroot):
print(f'updating copied repo {newroot}...')
if newroot == SRCDIR:
raise Exception('this probably isn\'t what you wanted')
_run_quiet([GIT, 'clean', '-d', '-f'], newroot)
_run_quiet([GIT, 'reset'], newroot)
_run_quiet([GIT, 'checkout', '.'], newroot)
_run_quiet([GIT, 'pull', '-f', oldroot], newroot)
else:
print(f'copying repo into {newroot}...')
_run_quiet([GIT, 'clone', oldroot, newroot])

# Copy over any uncommited files.
text = _run_stdout([GIT, 'status', '-s'], oldroot)
for line in text.splitlines():
_, _, relfile = line.strip().partition(' ')
relfile = relfile.strip()
isdir = relfile.endswith(os.path.sep)
relfile = relfile.rstrip(os.path.sep)
srcfile = os.path.join(oldroot, relfile)
dstfile = os.path.join(newroot, relfile)
os.makedirs(os.path.dirname(dstfile), exist_ok=True)
if isdir:
shutil.copytree(srcfile, dstfile, dirs_exist_ok=True)
else:
shutil.copy2(srcfile, dstfile)


def get_makefile_var(builddir, name):
regex = re.compile(rf'^{name} *=\s*(.*?)\s*$')
filename = os.path.join(builddir, 'Makefile')
try:
infile = open(filename)
except FileNotFoundError:
return None
with infile:
for line in infile:
m = regex.match(line)
if m:
value, = m.groups()
return value or ''
return None


def get_config_var(builddir, name):
python = os.path.join(builddir, 'python')
if os.path.isfile(python):
cmd = [python, '-c',
f'import sysconfig; print(sysconfig.get_config_var("{name}"))']
try:
return _run_stdout(cmd)
except subprocess.CalledProcessError:
pass
return get_makefile_var(builddir, name)


##################################
# freezing

def prepare(script=None, outdir=None):
if not outdir:
outdir = OUTDIR
os.makedirs(outdir, exist_ok=True)

# Write the script to disk.
if script:
scriptfile = os.path.join(outdir, 'app.py')
with open(scriptfile, 'w') as outfile:
outfile.write(script)

# Make a copy of the repo to avoid affecting the current build.
srcdir = os.path.join(outdir, 'cpython')
git_copy_repo(srcdir, SRCDIR)

# We use an out-of-tree build (instead of srcdir).
builddir = os.path.join(outdir, 'python-build')
os.makedirs(builddir, exist_ok=True)

# Run configure.
print(f'configuring python in {builddir}...')
cmd = [
os.path.join(srcdir, 'configure'),
*shlex.split(get_config_var(builddir, 'CONFIG_ARGS') or ''),
]
ensure_opt(cmd, 'cache-file', os.path.join(outdir, 'python-config.cache'))
prefix = os.path.join(outdir, 'python-installation')
ensure_opt(cmd, 'prefix', prefix)
_run_quiet(cmd, builddir)

if not MAKE:
raise UnsupportedError('make')

# Build python.
print('building python...')
if os.path.exists(os.path.join(srcdir, 'Makefile')):
# Out-of-tree builds require a clean srcdir.
_run_quiet([MAKE, '-C', srcdir, 'clean'])
_run_quiet([MAKE, '-C', builddir, '-j8'])

# Install the build.
print(f'installing python into {prefix}...')
_run_quiet([MAKE, '-C', builddir, '-j8', 'install'])
python = os.path.join(prefix, 'bin', 'python3')

return outdir, scriptfile, python


def freeze(python, scriptfile, outdir):
if not MAKE:
raise UnsupportedError('make')

print(f'freezing {scriptfile}...')
os.makedirs(outdir, exist_ok=True)
_run_quiet([python, FREEZE, '-o', outdir, scriptfile], outdir)
_run_quiet([MAKE, '-C', os.path.dirname(scriptfile)])

name = os.path.basename(scriptfile).rpartition('.')[0]
executable = os.path.join(outdir, name)
return executable


def run(executable):
return _run_stdout([executable])

0 comments on commit 13d9205

Please sign in to comment.