mirror of https://github.com/python/cpython
687 lines
20 KiB
Python
687 lines
20 KiB
Python
from collections import namedtuple
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import os.path
|
|
#import select
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from textwrap import dedent
|
|
import threading
|
|
import types
|
|
import unittest
|
|
import warnings
|
|
|
|
from test import support
|
|
|
|
# We would use test.support.import_helper.import_module(),
|
|
# but the indirect import of test.support.os_helper causes refleaks.
|
|
try:
|
|
import _interpreters
|
|
except ImportError as exc:
|
|
raise unittest.SkipTest(str(exc))
|
|
from test.support import interpreters
|
|
|
|
|
|
try:
|
|
import _testinternalcapi
|
|
import _testcapi
|
|
except ImportError:
|
|
_testinternalcapi = None
|
|
_testcapi = None
|
|
|
|
def requires_test_modules(func):
|
|
return unittest.skipIf(_testinternalcapi is None, "test requires _testinternalcapi module")(func)
|
|
|
|
|
|
def _dump_script(text):
|
|
lines = text.splitlines()
|
|
print()
|
|
print('-' * 20)
|
|
for i, line in enumerate(lines, 1):
|
|
print(f' {i:>{len(str(len(lines)))}} {line}')
|
|
print('-' * 20)
|
|
|
|
|
|
def _close_file(file):
|
|
try:
|
|
if hasattr(file, 'close'):
|
|
file.close()
|
|
else:
|
|
os.close(file)
|
|
except OSError as exc:
|
|
if exc.errno != 9:
|
|
raise # re-raise
|
|
# It was closed already.
|
|
|
|
|
|
def pack_exception(exc=None):
|
|
captured = _interpreters.capture_exception(exc)
|
|
data = dict(captured.__dict__)
|
|
data['type'] = dict(captured.type.__dict__)
|
|
return json.dumps(data)
|
|
|
|
|
|
def unpack_exception(packed):
|
|
try:
|
|
data = json.loads(packed)
|
|
except json.decoder.JSONDecodeError:
|
|
warnings.warn('incomplete exception data', RuntimeWarning)
|
|
print(packed if isinstance(packed, str) else packed.decode('utf-8'))
|
|
return None
|
|
exc = types.SimpleNamespace(**data)
|
|
exc.type = types.SimpleNamespace(**exc.type)
|
|
return exc;
|
|
|
|
|
|
class CapturingResults:
|
|
|
|
STDIO = dedent("""\
|
|
with open({w_pipe}, 'wb', buffering=0) as _spipe_{stream}:
|
|
_captured_std{stream} = io.StringIO()
|
|
with contextlib.redirect_std{stream}(_captured_std{stream}):
|
|
#########################
|
|
# begin wrapped script
|
|
|
|
{indented}
|
|
|
|
# end wrapped script
|
|
#########################
|
|
text = _captured_std{stream}.getvalue()
|
|
_spipe_{stream}.write(text.encode('utf-8'))
|
|
""")[:-1]
|
|
EXC = dedent("""\
|
|
with open({w_pipe}, 'wb', buffering=0) as _spipe_exc:
|
|
try:
|
|
#########################
|
|
# begin wrapped script
|
|
|
|
{indented}
|
|
|
|
# end wrapped script
|
|
#########################
|
|
except Exception as exc:
|
|
text = _interp_utils.pack_exception(exc)
|
|
_spipe_exc.write(text.encode('utf-8'))
|
|
""")[:-1]
|
|
|
|
@classmethod
|
|
def wrap_script(cls, script, *, stdout=True, stderr=False, exc=False):
|
|
script = dedent(script).strip(os.linesep)
|
|
imports = [
|
|
f'import {__name__} as _interp_utils',
|
|
]
|
|
wrapped = script
|
|
|
|
# Handle exc.
|
|
if exc:
|
|
exc = os.pipe()
|
|
r_exc, w_exc = exc
|
|
indented = wrapped.replace('\n', '\n ')
|
|
wrapped = cls.EXC.format(
|
|
w_pipe=w_exc,
|
|
indented=indented,
|
|
)
|
|
else:
|
|
exc = None
|
|
|
|
# Handle stdout.
|
|
if stdout:
|
|
imports.extend([
|
|
'import contextlib, io',
|
|
])
|
|
stdout = os.pipe()
|
|
r_out, w_out = stdout
|
|
indented = wrapped.replace('\n', '\n ')
|
|
wrapped = cls.STDIO.format(
|
|
w_pipe=w_out,
|
|
indented=indented,
|
|
stream='out',
|
|
)
|
|
else:
|
|
stdout = None
|
|
|
|
# Handle stderr.
|
|
if stderr == 'stdout':
|
|
stderr = None
|
|
elif stderr:
|
|
if not stdout:
|
|
imports.extend([
|
|
'import contextlib, io',
|
|
])
|
|
stderr = os.pipe()
|
|
r_err, w_err = stderr
|
|
indented = wrapped.replace('\n', '\n ')
|
|
wrapped = cls.STDIO.format(
|
|
w_pipe=w_err,
|
|
indented=indented,
|
|
stream='err',
|
|
)
|
|
else:
|
|
stderr = None
|
|
|
|
if wrapped == script:
|
|
raise NotImplementedError
|
|
else:
|
|
for line in imports:
|
|
wrapped = f'{line}{os.linesep}{wrapped}'
|
|
|
|
results = cls(stdout, stderr, exc)
|
|
return wrapped, results
|
|
|
|
def __init__(self, out, err, exc):
|
|
self._rf_out = None
|
|
self._rf_err = None
|
|
self._rf_exc = None
|
|
self._w_out = None
|
|
self._w_err = None
|
|
self._w_exc = None
|
|
|
|
if out is not None:
|
|
r_out, w_out = out
|
|
self._rf_out = open(r_out, 'rb', buffering=0)
|
|
self._w_out = w_out
|
|
|
|
if err is not None:
|
|
r_err, w_err = err
|
|
self._rf_err = open(r_err, 'rb', buffering=0)
|
|
self._w_err = w_err
|
|
|
|
if exc is not None:
|
|
r_exc, w_exc = exc
|
|
self._rf_exc = open(r_exc, 'rb', buffering=0)
|
|
self._w_exc = w_exc
|
|
|
|
self._buf_out = b''
|
|
self._buf_err = b''
|
|
self._buf_exc = b''
|
|
self._exc = None
|
|
|
|
self._closed = False
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
self.close()
|
|
|
|
@property
|
|
def closed(self):
|
|
return self._closed
|
|
|
|
def close(self):
|
|
if self._closed:
|
|
return
|
|
self._closed = True
|
|
|
|
if self._w_out is not None:
|
|
_close_file(self._w_out)
|
|
self._w_out = None
|
|
if self._w_err is not None:
|
|
_close_file(self._w_err)
|
|
self._w_err = None
|
|
if self._w_exc is not None:
|
|
_close_file(self._w_exc)
|
|
self._w_exc = None
|
|
|
|
self._capture()
|
|
|
|
if self._rf_out is not None:
|
|
_close_file(self._rf_out)
|
|
self._rf_out = None
|
|
if self._rf_err is not None:
|
|
_close_file(self._rf_err)
|
|
self._rf_err = None
|
|
if self._rf_exc is not None:
|
|
_close_file(self._rf_exc)
|
|
self._rf_exc = None
|
|
|
|
def _capture(self):
|
|
# Ideally this is called only after the script finishes
|
|
# (and thus has closed the write end of the pipe.
|
|
if self._rf_out is not None:
|
|
chunk = self._rf_out.read(100)
|
|
while chunk:
|
|
self._buf_out += chunk
|
|
chunk = self._rf_out.read(100)
|
|
if self._rf_err is not None:
|
|
chunk = self._rf_err.read(100)
|
|
while chunk:
|
|
self._buf_err += chunk
|
|
chunk = self._rf_err.read(100)
|
|
if self._rf_exc is not None:
|
|
chunk = self._rf_exc.read(100)
|
|
while chunk:
|
|
self._buf_exc += chunk
|
|
chunk = self._rf_exc.read(100)
|
|
|
|
def _unpack_stdout(self):
|
|
return self._buf_out.decode('utf-8')
|
|
|
|
def _unpack_stderr(self):
|
|
return self._buf_err.decode('utf-8')
|
|
|
|
def _unpack_exc(self):
|
|
if self._exc is not None:
|
|
return self._exc
|
|
if not self._buf_exc:
|
|
return None
|
|
self._exc = unpack_exception(self._buf_exc)
|
|
return self._exc
|
|
|
|
def stdout(self):
|
|
if self.closed:
|
|
return self.final().stdout
|
|
self._capture()
|
|
return self._unpack_stdout()
|
|
|
|
def stderr(self):
|
|
if self.closed:
|
|
return self.final().stderr
|
|
self._capture()
|
|
return self._unpack_stderr()
|
|
|
|
def exc(self):
|
|
if self.closed:
|
|
return self.final().exc
|
|
self._capture()
|
|
return self._unpack_exc()
|
|
|
|
def final(self, *, force=False):
|
|
try:
|
|
return self._final
|
|
except AttributeError:
|
|
if not self._closed:
|
|
if not force:
|
|
raise Exception('no final results available yet')
|
|
else:
|
|
return CapturedResults.Proxy(self)
|
|
self._final = CapturedResults(
|
|
self._unpack_stdout(),
|
|
self._unpack_stderr(),
|
|
self._unpack_exc(),
|
|
)
|
|
return self._final
|
|
|
|
|
|
class CapturedResults(namedtuple('CapturedResults', 'stdout stderr exc')):
|
|
|
|
class Proxy:
|
|
def __init__(self, capturing):
|
|
self._capturing = capturing
|
|
def _finish(self):
|
|
if self._capturing is None:
|
|
return
|
|
self._final = self._capturing.final()
|
|
self._capturing = None
|
|
def __iter__(self):
|
|
self._finish()
|
|
yield from self._final
|
|
def __len__(self):
|
|
self._finish()
|
|
return len(self._final)
|
|
def __getattr__(self, name):
|
|
self._finish()
|
|
if name.startswith('_'):
|
|
raise AttributeError(name)
|
|
return getattr(self._final, name)
|
|
|
|
def raise_if_failed(self):
|
|
if self.exc is not None:
|
|
raise interpreters.ExecutionFailed(self.exc)
|
|
|
|
|
|
def _captured_script(script, *, stdout=True, stderr=False, exc=False):
|
|
return CapturingResults.wrap_script(
|
|
script,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
exc=exc,
|
|
)
|
|
|
|
|
|
def clean_up_interpreters():
|
|
for interp in interpreters.list_all():
|
|
if interp.id == 0: # main
|
|
continue
|
|
try:
|
|
interp.close()
|
|
except _interpreters.InterpreterError:
|
|
pass # already destroyed
|
|
|
|
|
|
def _run_output(interp, request, init=None):
|
|
script, results = _captured_script(request)
|
|
with results:
|
|
if init:
|
|
interp.prepare_main(init)
|
|
interp.exec(script)
|
|
return results.stdout()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _running(interp):
|
|
r, w = os.pipe()
|
|
def run():
|
|
interp.exec(dedent(f"""
|
|
# wait for "signal"
|
|
with open({r}) as rpipe:
|
|
rpipe.read()
|
|
"""))
|
|
|
|
t = threading.Thread(target=run)
|
|
t.start()
|
|
|
|
yield
|
|
|
|
with open(w, 'w') as spipe:
|
|
spipe.write('done')
|
|
t.join()
|
|
|
|
|
|
class TestBase(unittest.TestCase):
|
|
|
|
def tearDown(self):
|
|
clean_up_interpreters()
|
|
|
|
def pipe(self):
|
|
def ensure_closed(fd):
|
|
try:
|
|
os.close(fd)
|
|
except OSError:
|
|
pass
|
|
r, w = os.pipe()
|
|
self.addCleanup(lambda: ensure_closed(r))
|
|
self.addCleanup(lambda: ensure_closed(w))
|
|
return r, w
|
|
|
|
def temp_dir(self):
|
|
tempdir = tempfile.mkdtemp()
|
|
tempdir = os.path.realpath(tempdir)
|
|
from test.support import os_helper
|
|
self.addCleanup(lambda: os_helper.rmtree(tempdir))
|
|
return tempdir
|
|
|
|
@contextlib.contextmanager
|
|
def captured_thread_exception(self):
|
|
ctx = types.SimpleNamespace(caught=None)
|
|
def excepthook(args):
|
|
ctx.caught = args
|
|
orig_excepthook = threading.excepthook
|
|
threading.excepthook = excepthook
|
|
try:
|
|
yield ctx
|
|
finally:
|
|
threading.excepthook = orig_excepthook
|
|
|
|
def make_script(self, filename, dirname=None, text=None):
|
|
if text:
|
|
text = dedent(text)
|
|
if dirname is None:
|
|
dirname = self.temp_dir()
|
|
filename = os.path.join(dirname, filename)
|
|
|
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
|
with open(filename, 'w', encoding='utf-8') as outfile:
|
|
outfile.write(text or '')
|
|
return filename
|
|
|
|
def make_module(self, name, pathentry=None, text=None):
|
|
if text:
|
|
text = dedent(text)
|
|
if pathentry is None:
|
|
pathentry = self.temp_dir()
|
|
else:
|
|
os.makedirs(pathentry, exist_ok=True)
|
|
*subnames, basename = name.split('.')
|
|
|
|
dirname = pathentry
|
|
for subname in subnames:
|
|
dirname = os.path.join(dirname, subname)
|
|
if os.path.isdir(dirname):
|
|
pass
|
|
elif os.path.exists(dirname):
|
|
raise Exception(dirname)
|
|
else:
|
|
os.mkdir(dirname)
|
|
initfile = os.path.join(dirname, '__init__.py')
|
|
if not os.path.exists(initfile):
|
|
with open(initfile, 'w'):
|
|
pass
|
|
filename = os.path.join(dirname, basename + '.py')
|
|
|
|
with open(filename, 'w', encoding='utf-8') as outfile:
|
|
outfile.write(text or '')
|
|
return filename
|
|
|
|
@support.requires_subprocess()
|
|
def run_python(self, *argv):
|
|
proc = subprocess.run(
|
|
[sys.executable, *argv],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return proc.returncode, proc.stdout, proc.stderr
|
|
|
|
def assert_python_ok(self, *argv):
|
|
exitcode, stdout, stderr = self.run_python(*argv)
|
|
self.assertNotEqual(exitcode, 1)
|
|
return stdout, stderr
|
|
|
|
def assert_python_failure(self, *argv):
|
|
exitcode, stdout, stderr = self.run_python(*argv)
|
|
self.assertNotEqual(exitcode, 0)
|
|
return stdout, stderr
|
|
|
|
def assert_ns_equal(self, ns1, ns2, msg=None):
|
|
# This is mostly copied from TestCase.assertDictEqual.
|
|
self.assertEqual(type(ns1), type(ns2))
|
|
if ns1 == ns2:
|
|
return
|
|
|
|
import difflib
|
|
import pprint
|
|
from unittest.util import _common_shorten_repr
|
|
standardMsg = '%s != %s' % _common_shorten_repr(ns1, ns2)
|
|
diff = ('\n' + '\n'.join(difflib.ndiff(
|
|
pprint.pformat(vars(ns1)).splitlines(),
|
|
pprint.pformat(vars(ns2)).splitlines())))
|
|
diff = f'namespace({diff})'
|
|
standardMsg = self._truncateMessage(standardMsg, diff)
|
|
self.fail(self._formatMessage(msg, standardMsg))
|
|
|
|
def _run_string(self, interp, script):
|
|
wrapped, results = _captured_script(script, exc=False)
|
|
#_dump_script(wrapped)
|
|
with results:
|
|
if isinstance(interp, interpreters.Interpreter):
|
|
interp.exec(script)
|
|
else:
|
|
err = _interpreters.run_string(interp, wrapped)
|
|
if err is not None:
|
|
return None, err
|
|
return results.stdout(), None
|
|
|
|
def run_and_capture(self, interp, script):
|
|
text, err = self._run_string(interp, script)
|
|
if err is not None:
|
|
raise interpreters.ExecutionFailed(err)
|
|
else:
|
|
return text
|
|
|
|
def interp_exists(self, interpid):
|
|
try:
|
|
_interpreters.whence(interpid)
|
|
except _interpreters.InterpreterNotFoundError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
@requires_test_modules
|
|
@contextlib.contextmanager
|
|
def interpreter_from_capi(self, config=None, whence=None):
|
|
if config is False:
|
|
if whence is None:
|
|
whence = _interpreters.WHENCE_LEGACY_CAPI
|
|
else:
|
|
assert whence in (_interpreters.WHENCE_LEGACY_CAPI,
|
|
_interpreters.WHENCE_UNKNOWN), repr(whence)
|
|
config = None
|
|
elif config is True:
|
|
config = _interpreters.new_config('default')
|
|
elif config is None:
|
|
if whence not in (
|
|
_interpreters.WHENCE_LEGACY_CAPI,
|
|
_interpreters.WHENCE_UNKNOWN,
|
|
):
|
|
config = _interpreters.new_config('legacy')
|
|
elif isinstance(config, str):
|
|
config = _interpreters.new_config(config)
|
|
|
|
if whence is None:
|
|
whence = _interpreters.WHENCE_XI
|
|
|
|
interpid = _testinternalcapi.create_interpreter(config, whence=whence)
|
|
try:
|
|
yield interpid
|
|
finally:
|
|
try:
|
|
_testinternalcapi.destroy_interpreter(interpid)
|
|
except _interpreters.InterpreterNotFoundError:
|
|
pass
|
|
|
|
@contextlib.contextmanager
|
|
def interpreter_obj_from_capi(self, config='legacy'):
|
|
with self.interpreter_from_capi(config) as interpid:
|
|
interp = interpreters.Interpreter(
|
|
interpid,
|
|
_whence=_interpreters.WHENCE_CAPI,
|
|
_ownsref=False,
|
|
)
|
|
yield interp, interpid
|
|
|
|
@contextlib.contextmanager
|
|
def capturing(self, script):
|
|
wrapped, capturing = _captured_script(script, stdout=True, exc=True)
|
|
#_dump_script(wrapped)
|
|
with capturing:
|
|
yield wrapped, capturing.final(force=True)
|
|
|
|
@requires_test_modules
|
|
def run_from_capi(self, interpid, script, *, main=False):
|
|
with self.capturing(script) as (wrapped, results):
|
|
rc = _testinternalcapi.exec_interpreter(interpid, wrapped, main=main)
|
|
assert rc == 0, rc
|
|
results.raise_if_failed()
|
|
return results.stdout
|
|
|
|
@contextlib.contextmanager
|
|
def _running(self, run_interp, exec_interp):
|
|
token = b'\0'
|
|
r_in, w_in = self.pipe()
|
|
r_out, w_out = self.pipe()
|
|
|
|
def close():
|
|
_close_file(r_in)
|
|
_close_file(w_in)
|
|
_close_file(r_out)
|
|
_close_file(w_out)
|
|
|
|
# Start running (and wait).
|
|
script = dedent(f"""
|
|
import os
|
|
try:
|
|
# handshake
|
|
token = os.read({r_in}, 1)
|
|
os.write({w_out}, token)
|
|
# Wait for the "done" message.
|
|
os.read({r_in}, 1)
|
|
except BrokenPipeError:
|
|
pass
|
|
except OSError as exc:
|
|
if exc.errno != 9:
|
|
raise # re-raise
|
|
# It was closed already.
|
|
""")
|
|
failed = None
|
|
def run():
|
|
nonlocal failed
|
|
try:
|
|
run_interp(script)
|
|
except Exception as exc:
|
|
failed = exc
|
|
close()
|
|
t = threading.Thread(target=run)
|
|
t.start()
|
|
|
|
# handshake
|
|
try:
|
|
os.write(w_in, token)
|
|
token2 = os.read(r_out, 1)
|
|
assert token2 == token, (token2, token)
|
|
except OSError:
|
|
t.join()
|
|
if failed is not None:
|
|
raise failed
|
|
|
|
# CM __exit__()
|
|
try:
|
|
try:
|
|
yield
|
|
finally:
|
|
# Send "done".
|
|
os.write(w_in, b'\0')
|
|
finally:
|
|
close()
|
|
t.join()
|
|
if failed is not None:
|
|
raise failed
|
|
|
|
@contextlib.contextmanager
|
|
def running(self, interp):
|
|
if isinstance(interp, int):
|
|
interpid = interp
|
|
def exec_interp(script):
|
|
exc = _interpreters.exec(interpid, script)
|
|
assert exc is None, exc
|
|
run_interp = exec_interp
|
|
else:
|
|
def run_interp(script):
|
|
text = self.run_and_capture(interp, script)
|
|
assert text == '', repr(text)
|
|
def exec_interp(script):
|
|
interp.exec(script)
|
|
with self._running(run_interp, exec_interp):
|
|
yield
|
|
|
|
@requires_test_modules
|
|
@contextlib.contextmanager
|
|
def running_from_capi(self, interpid, *, main=False):
|
|
def run_interp(script):
|
|
text = self.run_from_capi(interpid, script, main=main)
|
|
assert text == '', repr(text)
|
|
def exec_interp(script):
|
|
rc = _testinternalcapi.exec_interpreter(interpid, script)
|
|
assert rc == 0, rc
|
|
with self._running(run_interp, exec_interp):
|
|
yield
|
|
|
|
@requires_test_modules
|
|
def run_temp_from_capi(self, script, config='legacy'):
|
|
if config is False:
|
|
# Force using Py_NewInterpreter().
|
|
run_in_interp = (lambda s, c: _testcapi.run_in_subinterp(s))
|
|
config = None
|
|
else:
|
|
run_in_interp = _testinternalcapi.run_in_subinterp_with_config
|
|
if config is True:
|
|
config = 'default'
|
|
if isinstance(config, str):
|
|
config = _interpreters.new_config(config)
|
|
with self.capturing(script) as (wrapped, results):
|
|
rc = run_in_interp(wrapped, config)
|
|
assert rc == 0, rc
|
|
results.raise_if_failed()
|
|
return results.stdout
|