Skip to content

bpo-45629: Add a test for the "freeze" tool. #29222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a2e4494
Ignore the tests outdir.
ericsnowcurrently Oct 26, 2021
9675b69
Add test helpers for the "freeze" tool.
ericsnowcurrently Oct 26, 2021
a34dca4
Add a test for the "freeze" tool.
ericsnowcurrently Oct 26, 2021
77f334d
Create the outdir.
ericsnowcurrently Oct 26, 2021
561f8e1
Build in the source tree for tests.
ericsnowcurrently Oct 26, 2021
bd54ddd
Copy the repo during tests.
ericsnowcurrently Oct 26, 2021
5d0ec9e
Force the git pull.
ericsnowcurrently Oct 26, 2021
b879a3c
Show the commands.
ericsnowcurrently Oct 26, 2021
5639939
Fix the relfile.
ericsnowcurrently Oct 26, 2021
172a259
Only print some of the run commands.
ericsnowcurrently Oct 26, 2021
875cbe3
Actually print the config var.
ericsnowcurrently Oct 26, 2021
8e4e52e
Fix a kwarg.
ericsnowcurrently Oct 26, 2021
bf95b31
Fall back to Makefile if sysconfig fails.
ericsnowcurrently Oct 26, 2021
a8e2cf0
Do not clean up first if there is not Makefile.
ericsnowcurrently Oct 26, 2021
fab3dad
Clean up get_config_var().
ericsnowcurrently Oct 26, 2021
ef579ac
Do not duplicate configure args.
ericsnowcurrently Oct 26, 2021
7338d60
Clean up the copied repo first.
ericsnowcurrently Oct 26, 2021
f6a8755
Hide error text in get_config_var().
ericsnowcurrently Oct 26, 2021
b12477e
Skip the freeze tests if the tool is missing.
ericsnowcurrently Oct 26, 2021
2dd674d
Be explicit about the -j option.
ericsnowcurrently Oct 26, 2021
cc68b86
Do not use the -C option with git.
ericsnowcurrently Oct 26, 2021
9b6aee8
Simplify.
ericsnowcurrently Oct 26, 2021
4b315b0
Skip the test if running with "-u -cpu".
ericsnowcurrently Oct 27, 2021
aa56cfc
Do not run the test on buildbots.
ericsnowcurrently Oct 27, 2021
4c18103
Add test.support.skip_if_buildbot().
ericsnowcurrently Oct 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])