| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- """Test cases for building an C extension and running it."""
- from __future__ import annotations
- import ast
- import contextlib
- import glob
- import os.path
- import re
- import shutil
- import subprocess
- import sys
- import time
- from typing import Any, Iterator
- from mypy import build
- from mypy.errors import CompileError
- from mypy.options import TYPE_VAR_TUPLE, UNPACK, Options
- from mypy.test.config import test_temp_dir
- from mypy.test.data import DataDrivenTestCase
- from mypy.test.helpers import assert_module_equivalence, perform_file_operations
- from mypyc.build import construct_groups
- from mypyc.codegen import emitmodule
- from mypyc.errors import Errors
- from mypyc.options import CompilerOptions
- from mypyc.test.test_serialization import check_serialization_roundtrip
- from mypyc.test.testutil import (
- ICODE_GEN_BUILTINS,
- TESTUTIL_PATH,
- MypycDataSuite,
- assert_test_output,
- fudge_dir_mtimes,
- show_c,
- use_custom_builtins,
- )
- files = [
- "run-async.test",
- "run-misc.test",
- "run-functions.test",
- "run-integers.test",
- "run-i64.test",
- "run-i32.test",
- "run-i16.test",
- "run-u8.test",
- "run-floats.test",
- "run-math.test",
- "run-bools.test",
- "run-strings.test",
- "run-bytes.test",
- "run-tuples.test",
- "run-lists.test",
- "run-dicts.test",
- "run-sets.test",
- "run-primitives.test",
- "run-loops.test",
- "run-exceptions.test",
- "run-imports.test",
- "run-classes.test",
- "run-traits.test",
- "run-generators.test",
- "run-multimodule.test",
- "run-bench.test",
- "run-mypy-sim.test",
- "run-dunders.test",
- "run-singledispatch.test",
- "run-attrs.test",
- "run-python37.test",
- "run-python38.test",
- ]
- if sys.version_info >= (3, 10):
- files.append("run-match.test")
- setup_format = """\
- from setuptools import setup
- from mypyc.build import mypycify
- setup(name='test_run_output',
- ext_modules=mypycify({}, separate={}, skip_cgen_input={!r}, strip_asserts=False,
- multi_file={}, opt_level='{}'),
- )
- """
- WORKDIR = "build"
- def run_setup(script_name: str, script_args: list[str]) -> bool:
- """Run a setup script in a somewhat controlled environment.
- This is adapted from code in distutils and our goal here is that is
- faster to not need to spin up a python interpreter to run it.
- We had to fork it because the real run_setup swallows errors
- and KeyboardInterrupt with no way to recover them (!).
- The real version has some extra features that we removed since
- we weren't using them.
- Returns whether the setup succeeded.
- """
- save_argv = sys.argv.copy()
- g = {"__file__": script_name}
- try:
- try:
- sys.argv[0] = script_name
- sys.argv[1:] = script_args
- with open(script_name, "rb") as f:
- exec(f.read(), g)
- finally:
- sys.argv = save_argv
- except SystemExit as e:
- # distutils converts KeyboardInterrupt into a SystemExit with
- # "interrupted" as the argument. Convert it back so that
- # pytest will exit instead of just failing the test.
- if e.code == "interrupted":
- raise KeyboardInterrupt from e
- return e.code == 0 or e.code is None
- return True
- @contextlib.contextmanager
- def chdir_manager(target: str) -> Iterator[None]:
- dir = os.getcwd()
- os.chdir(target)
- try:
- yield
- finally:
- os.chdir(dir)
- class TestRun(MypycDataSuite):
- """Test cases that build a C extension and run code."""
- files = files
- base_path = test_temp_dir
- optional_out = True
- multi_file = False
- separate = False # If True, using separate (incremental) compilation
- def run_case(self, testcase: DataDrivenTestCase) -> None:
- # setup.py wants to be run from the root directory of the package, which we accommodate
- # by chdiring into tmp/
- with use_custom_builtins(
- os.path.join(self.data_prefix, ICODE_GEN_BUILTINS), testcase
- ), chdir_manager("tmp"):
- self.run_case_inner(testcase)
- def run_case_inner(self, testcase: DataDrivenTestCase) -> None:
- if not os.path.isdir(WORKDIR): # (one test puts something in build...)
- os.mkdir(WORKDIR)
- text = "\n".join(testcase.input)
- with open("native.py", "w", encoding="utf-8") as f:
- f.write(text)
- with open("interpreted.py", "w", encoding="utf-8") as f:
- f.write(text)
- shutil.copyfile(TESTUTIL_PATH, "testutil.py")
- step = 1
- self.run_case_step(testcase, step)
- steps = testcase.find_steps()
- if steps == [[]]:
- steps = []
- for operations in steps:
- # To make sure that any new changes get picked up as being
- # new by distutils, shift the mtime of all of the
- # generated artifacts back by a second.
- fudge_dir_mtimes(WORKDIR, -1)
- # On Ubuntu, changing the mtime doesn't work reliably. As
- # a workaround, sleep.
- #
- # TODO: Figure out a better approach, since this slows down tests.
- if sys.platform == "linux":
- time.sleep(1.0)
- step += 1
- with chdir_manager(".."):
- perform_file_operations(operations)
- self.run_case_step(testcase, step)
- def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) -> None:
- bench = testcase.config.getoption("--bench", False) and "Benchmark" in testcase.name
- options = Options()
- options.use_builtins_fixtures = True
- options.show_traceback = True
- options.strict_optional = True
- options.python_version = sys.version_info[:2]
- options.export_types = True
- options.preserve_asts = True
- options.allow_empty_bodies = True
- options.incremental = self.separate
- options.enable_incomplete_feature = [TYPE_VAR_TUPLE, UNPACK]
- # Avoid checking modules/packages named 'unchecked', to provide a way
- # to test interacting with code we don't have types for.
- options.per_module_options["unchecked.*"] = {"follow_imports": "error"}
- source = build.BuildSource("native.py", "native", None)
- sources = [source]
- module_names = ["native"]
- module_paths = ["native.py"]
- # Hard code another module name to compile in the same compilation unit.
- to_delete = []
- for fn, text in testcase.files:
- fn = os.path.relpath(fn, test_temp_dir)
- if os.path.basename(fn).startswith("other") and fn.endswith(".py"):
- name = fn.split(".")[0].replace(os.sep, ".")
- module_names.append(name)
- sources.append(build.BuildSource(fn, name, None))
- to_delete.append(fn)
- module_paths.append(fn)
- shutil.copyfile(fn, os.path.join(os.path.dirname(fn), name + "_interpreted.py"))
- for source in sources:
- options.per_module_options.setdefault(source.module, {})["mypyc"] = True
- separate = (
- self.get_separate("\n".join(testcase.input), incremental_step)
- if self.separate
- else False
- )
- groups = construct_groups(sources, separate, len(module_names) > 1)
- try:
- compiler_options = CompilerOptions(multi_file=self.multi_file, separate=self.separate)
- result = emitmodule.parse_and_typecheck(
- sources=sources,
- options=options,
- compiler_options=compiler_options,
- groups=groups,
- alt_lib_path=".",
- )
- errors = Errors(options)
- ir, cfiles = emitmodule.compile_modules_to_c(
- result, compiler_options=compiler_options, errors=errors, groups=groups
- )
- if errors.num_errors:
- errors.flush_errors()
- assert False, "Compile error"
- except CompileError as e:
- for line in e.messages:
- print(fix_native_line_number(line, testcase.file, testcase.line))
- assert False, "Compile error"
- # Check that serialization works on this IR. (Only on the first
- # step because the returned ir only includes updated code.)
- if incremental_step == 1:
- check_serialization_roundtrip(ir)
- opt_level = int(os.environ.get("MYPYC_OPT_LEVEL", 0))
- debug_level = int(os.environ.get("MYPYC_DEBUG_LEVEL", 0))
- setup_file = os.path.abspath(os.path.join(WORKDIR, "setup.py"))
- # We pass the C file information to the build script via setup.py unfortunately
- with open(setup_file, "w", encoding="utf-8") as f:
- f.write(
- setup_format.format(
- module_paths, separate, cfiles, self.multi_file, opt_level, debug_level
- )
- )
- if not run_setup(setup_file, ["build_ext", "--inplace"]):
- if testcase.config.getoption("--mypyc-showc"):
- show_c(cfiles)
- assert False, "Compilation failed"
- # Assert that an output file got created
- suffix = "pyd" if sys.platform == "win32" else "so"
- assert glob.glob(f"native.*.{suffix}") or glob.glob(f"native.{suffix}")
- driver_path = "driver.py"
- if not os.path.isfile(driver_path):
- # No driver.py provided by test case. Use the default one
- # (mypyc/test-data/driver/driver.py) that calls each
- # function named test_*.
- default_driver = os.path.join(
- os.path.dirname(__file__), "..", "test-data", "driver", "driver.py"
- )
- shutil.copy(default_driver, driver_path)
- env = os.environ.copy()
- env["MYPYC_RUN_BENCH"] = "1" if bench else "0"
- debugger = testcase.config.getoption("debugger")
- if debugger:
- if debugger == "lldb":
- subprocess.check_call(["lldb", "--", sys.executable, driver_path], env=env)
- elif debugger == "gdb":
- subprocess.check_call(["gdb", "--args", sys.executable, driver_path], env=env)
- else:
- assert False, "Unsupported debugger"
- # TODO: find a way to automatically disable capturing
- # stdin/stdout when in debugging mode
- assert False, (
- "Test can't pass in debugging mode. "
- "(Make sure to pass -s to pytest to interact with the debugger)"
- )
- proc = subprocess.Popen(
- [sys.executable, driver_path],
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- env=env,
- )
- if sys.version_info >= (3, 12):
- # TODO: testDecorators1 hangs on 3.12, remove this once fixed
- proc.wait(timeout=30)
- output = proc.communicate()[0].decode("utf8")
- outlines = output.splitlines()
- if testcase.config.getoption("--mypyc-showc"):
- show_c(cfiles)
- if proc.returncode != 0:
- print()
- print("*** Exit status: %d" % proc.returncode)
- # Verify output.
- if bench:
- print("Test output:")
- print(output)
- else:
- if incremental_step == 1:
- msg = "Invalid output"
- expected = testcase.output
- else:
- msg = f"Invalid output (step {incremental_step})"
- expected = testcase.output2.get(incremental_step, [])
- if not expected:
- # Tweak some line numbers, but only if the expected output is empty,
- # as tweaked output might not match expected output.
- outlines = [
- fix_native_line_number(line, testcase.file, testcase.line) for line in outlines
- ]
- assert_test_output(testcase, outlines, msg, expected)
- if incremental_step > 1 and options.incremental:
- suffix = "" if incremental_step == 2 else str(incremental_step - 1)
- expected_rechecked = testcase.expected_rechecked_modules.get(incremental_step - 1)
- if expected_rechecked is not None:
- assert_module_equivalence(
- "rechecked" + suffix, expected_rechecked, result.manager.rechecked_modules
- )
- expected_stale = testcase.expected_stale_modules.get(incremental_step - 1)
- if expected_stale is not None:
- assert_module_equivalence(
- "stale" + suffix, expected_stale, result.manager.stale_modules
- )
- assert proc.returncode == 0
- def get_separate(self, program_text: str, incremental_step: int) -> Any:
- template = r"# separate{}: (\[.*\])$"
- m = re.search(template.format(incremental_step), program_text, flags=re.MULTILINE)
- if not m:
- m = re.search(template.format(""), program_text, flags=re.MULTILINE)
- if m:
- return ast.literal_eval(m.group(1))
- else:
- return True
- class TestRunMultiFile(TestRun):
- """Run the main multi-module tests in multi-file compilation mode.
- In multi-file mode each module gets compiled into a separate C file,
- but all modules (C files) are compiled together.
- """
- multi_file = True
- test_name_suffix = "_multi"
- files = ["run-multimodule.test", "run-mypy-sim.test"]
- class TestRunSeparate(TestRun):
- """Run the main multi-module tests in separate compilation mode.
- In this mode there are multiple compilation groups, which are compiled
- incrementally. Each group is compiled to a separate C file, and these C
- files are compiled separately.
- Each compiled module is placed into a separate compilation group, unless
- overridden by a special comment. Consider this example:
- # separate: [(["other.py", "other_b.py"], "stuff")]
- This puts other.py and other_b.py into a compilation group named "stuff".
- Any files not mentioned in the comment will get single-file groups.
- """
- separate = True
- test_name_suffix = "_separate"
- files = ["run-multimodule.test", "run-mypy-sim.test"]
- def fix_native_line_number(message: str, fnam: str, delta: int) -> str:
- """Update code locations in test case output to point to the .test file.
- The description of the test case is written to native.py, and line numbers
- in test case output often are relative to native.py. This translates the
- line numbers to be relative to the .test file that contains the test case
- description, and also updates the file name to the .test file name.
- Args:
- message: message to update
- fnam: path of the .test file
- delta: line number of the beginning of the test case in the .test file
- Returns updated message (or original message if we couldn't find anything).
- """
- fnam = os.path.basename(fnam)
- message = re.sub(
- r"native\.py:([0-9]+):", lambda m: "%s:%d:" % (fnam, int(m.group(1)) + delta), message
- )
- message = re.sub(
- r'"native.py", line ([0-9]+),',
- lambda m: '"%s", line %d,' % (fnam, int(m.group(1)) + delta),
- message,
- )
- return message
|