script_helper.py 9.11 KB
Newer Older
Nick Coghlan's avatar
Nick Coghlan committed
1 2 3
# Common utility functions used by various script execution tests
#  e.g. test_cmd_line, test_cmd_line_script and test_runpy

4
import importlib
Nick Coghlan's avatar
Nick Coghlan committed
5 6 7 8 9 10 11 12 13 14
import sys
import os
import os.path
import tempfile
import subprocess
import py_compile
import contextlib
import shutil
import zipfile

15
from importlib.util import source_from_cache
16
from test.support import make_legacy_pyc, strip_python_stderr, temp_dir
Barry Warsaw's avatar
Barry Warsaw committed
17

18 19 20 21

# Cached result of the expensive test performed in the function below.
__cached_interp_requires_environment = None

22
def interpreter_requires_environment():
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
    """
    Returns True if our sys.executable interpreter requires environment
    variables in order to be able to run at all.

    This is designed to be used with @unittest.skipIf() to annotate tests
    that need to use an assert_python*() function to launch an isolated
    mode (-I) or no environment mode (-E) sub-interpreter process.

    A normal build & test does not run into this situation but it can happen
    when trying to run the standard library test suite from an interpreter that
    doesn't have an obvious home with Python's current home finding logic.

    Setting PYTHONHOME is one way to get most of the testsuite to run in that
    situation.  PYTHONPATH or PYTHONUSERSITE are other common envirnonment
    variables that might impact whether or not the interpreter can start.
    """
    global __cached_interp_requires_environment
    if __cached_interp_requires_environment is None:
        # Try running an interpreter with -E to see if it works or not.
        try:
            subprocess.check_call([sys.executable, '-E',
                                   '-c', 'import sys; sys.exit(0)'])
        except subprocess.CalledProcessError:
            __cached_interp_requires_environment = True
        else:
            __cached_interp_requires_environment = False

    return __cached_interp_requires_environment


Nick Coghlan's avatar
Nick Coghlan committed
53
# Executing the interpreter in a subprocess
54
def _assert_python(expected_success, *args, **env_vars):
55
    env_required = interpreter_requires_environment()
56 57 58
    if '__isolated' in env_vars:
        isolated = env_vars.pop('__isolated')
    else:
59
        isolated = not env_vars and not env_required
60
    cmd_line = [sys.executable, '-X', 'faulthandler']
61 62 63 64
    if isolated:
        # isolated mode: ignore Python environment variables, ignore user
        # site-packages, and don't add the current directory to sys.path
        cmd_line.append('-I')
65
    elif not env_vars and not env_required:
66
        # ignore Python environment variables
67
        cmd_line.append('-E')
68 69 70
    # Need to preserve the original environment, for in-place testing of
    # shared library builds.
    env = os.environ.copy()
71 72 73 74
    # But a special flag that can be set to override -- in this case, the
    # caller is responsible to pass the full environment.
    if env_vars.pop('__cleanenv', None):
        env = {}
75
    env.update(env_vars)
76
    cmd_line.extend(args)
77
    p = subprocess.Popen(cmd_line, stdin=subprocess.PIPE,
78 79
                         stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                         env=env)
80 81 82 83
    try:
        out, err = p.communicate()
    finally:
        subprocess._cleanup()
84 85
        p.stdout.close()
        p.stderr.close()
86
    rc = p.returncode
87
    err = strip_python_stderr(err)
88
    if (rc and expected_success) or (not rc and not expected_success):
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
        # Limit to 80 lines to ASCII characters
        maxlen = 80 * 100
        if len(out) > maxlen:
            out = b'(... truncated stdout ...)' + out[-maxlen:]
        if len(err) > maxlen:
            err = b'(... truncated stderr ...)' + err[-maxlen:]
        out = out.decode('ascii', 'replace').rstrip()
        err = err.decode('ascii', 'replace').rstrip()
        raise AssertionError("Process return code is %d\n"
                             "command line: %r\n"
                             "\n"
                             "stdout:\n"
                             "---\n"
                             "%s\n"
                             "---\n"
                             "\n"
                             "stderr:\n"
                             "---\n"
                             "%s\n"
                             "---"
                             % (rc, cmd_line,
                                out,
                                err))
112 113
    return rc, out, err

114 115 116
def assert_python_ok(*args, **env_vars):
    """
    Assert that running the interpreter with `args` and optional environment
117 118
    variables `env_vars` succeeds (rc == 0) and return a (return code, stdout,
    stderr) tuple.
119 120 121 122 123

    If the __cleanenv keyword is set, env_vars is used a fresh environment.

    Python is started in isolated mode (command line option -I),
    except if the __isolated keyword is set to False.
124 125
    """
    return _assert_python(True, *args, **env_vars)
126

127 128 129
def assert_python_failure(*args, **env_vars):
    """
    Assert that running the interpreter with `args` and optional environment
130 131
    variables `env_vars` fails (rc != 0) and return a (return code, stdout,
    stderr) tuple.
132 133

    See assert_python_ok() for more options.
134 135
    """
    return _assert_python(False, *args, **env_vars)
Nick Coghlan's avatar
Nick Coghlan committed
136

137
def spawn_python(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw):
138 139 140 141 142
    """Run a Python subprocess with the given arguments.

    kw is extra keyword args to pass to subprocess.Popen. Returns a Popen
    object.
    """
Nick Coghlan's avatar
Nick Coghlan committed
143 144
    cmd_line = [sys.executable, '-E']
    cmd_line.extend(args)
145 146 147 148 149 150
    # Under Fedora (?), GNU readline can output junk on stderr when initialized,
    # depending on the TERM setting.  Setting TERM=vt100 is supposed to disable
    # that.  References:
    # - http://reinout.vanrees.org/weblog/2009/08/14/readline-invisible-character-hack.html
    # - http://stackoverflow.com/questions/15760712/python-readline-module-prints-escape-character-during-import
    # - http://lists.gnu.org/archive/html/bug-readline/2007-08/msg00004.html
151 152
    env = kw.setdefault('env', dict(os.environ))
    env['TERM'] = 'vt100'
Nick Coghlan's avatar
Nick Coghlan committed
153
    return subprocess.Popen(cmd_line, stdin=subprocess.PIPE,
154
                            stdout=stdout, stderr=stderr,
155
                            **kw)
Nick Coghlan's avatar
Nick Coghlan committed
156 157

def kill_python(p):
158
    """Run the given Popen process until completion and return stdout."""
Nick Coghlan's avatar
Nick Coghlan committed
159 160 161 162
    p.stdin.close()
    data = p.stdout.read()
    p.stdout.close()
    # try to cleanup the child so we don't appear to leak when running
163 164
    # with regrtest -R.
    p.wait()
Nick Coghlan's avatar
Nick Coghlan committed
165 166 167
    subprocess._cleanup()
    return data

168 169 170 171
def make_script(script_dir, script_basename, source, omit_suffix=False):
    script_filename = script_basename
    if not omit_suffix:
        script_filename += os.extsep + 'py'
Nick Coghlan's avatar
Nick Coghlan committed
172
    script_name = os.path.join(script_dir, script_filename)
173 174
    # The script should be encoded to UTF-8, the default string encoding
    script_file = open(script_name, 'w', encoding='utf-8')
Nick Coghlan's avatar
Nick Coghlan committed
175 176
    script_file.write(source)
    script_file.close()
177
    importlib.invalidate_caches()
Nick Coghlan's avatar
Nick Coghlan committed
178 179 180 181 182 183 184
    return script_name

def make_zip_script(zip_dir, zip_basename, script_name, name_in_zip=None):
    zip_filename = zip_basename+os.extsep+'zip'
    zip_name = os.path.join(zip_dir, zip_filename)
    zip_file = zipfile.ZipFile(zip_name, 'w')
    if name_in_zip is None:
Barry Warsaw's avatar
Barry Warsaw committed
185 186 187 188 189 190 191
        parts = script_name.split(os.sep)
        if len(parts) >= 2 and parts[-2] == '__pycache__':
            legacy_pyc = make_legacy_pyc(source_from_cache(script_name))
            name_in_zip = os.path.basename(legacy_pyc)
            script_name = legacy_pyc
        else:
            name_in_zip = os.path.basename(script_name)
Nick Coghlan's avatar
Nick Coghlan committed
192 193
    zip_file.write(script_name, name_in_zip)
    zip_file.close()
Florent Xicluna's avatar
Florent Xicluna committed
194
    #if test.support.verbose:
Nick Coghlan's avatar
Nick Coghlan committed
195 196 197 198 199 200
    #    zip_file = zipfile.ZipFile(zip_name, 'r')
    #    print 'Contents of %r:' % zip_name
    #    zip_file.printdir()
    #    zip_file.close()
    return zip_name, os.path.join(zip_name, name_in_zip)

201
def make_pkg(pkg_dir, init_source=''):
Nick Coghlan's avatar
Nick Coghlan committed
202
    os.mkdir(pkg_dir)
203
    make_script(pkg_dir, '__init__', init_source)
Nick Coghlan's avatar
Nick Coghlan committed
204 205 206 207 208 209 210 211 212 213

def make_zip_pkg(zip_dir, zip_basename, pkg_name, script_basename,
                 source, depth=1, compiled=False):
    unlink = []
    init_name = make_script(zip_dir, '__init__', '')
    unlink.append(init_name)
    init_basename = os.path.basename(init_name)
    script_name = make_script(zip_dir, script_basename, source)
    unlink.append(script_name)
    if compiled:
214 215
        init_name = py_compile.compile(init_name, doraise=True)
        script_name = py_compile.compile(script_name, doraise=True)
Nick Coghlan's avatar
Nick Coghlan committed
216 217 218 219 220 221 222 223 224 225 226 227 228
        unlink.extend((init_name, script_name))
    pkg_names = [os.sep.join([pkg_name]*i) for i in range(1, depth+1)]
    script_name_in_zip = os.path.join(pkg_names[-1], os.path.basename(script_name))
    zip_filename = zip_basename+os.extsep+'zip'
    zip_name = os.path.join(zip_dir, zip_filename)
    zip_file = zipfile.ZipFile(zip_name, 'w')
    for name in pkg_names:
        init_name_in_zip = os.path.join(name, init_basename)
        zip_file.write(init_name, init_name_in_zip)
    zip_file.write(script_name, script_name_in_zip)
    zip_file.close()
    for name in unlink:
        os.unlink(name)
Florent Xicluna's avatar
Florent Xicluna committed
229
    #if test.support.verbose:
Nick Coghlan's avatar
Nick Coghlan committed
230 231 232 233 234
    #    zip_file = zipfile.ZipFile(zip_name, 'r')
    #    print 'Contents of %r:' % zip_name
    #    zip_file.printdir()
    #    zip_file.close()
    return zip_name, os.path.join(zip_name, script_name_in_zip)