Tipragot
628be439b8
Cela permet de ne pas avoir de problèmes de compatibilité car python est dans le git.
284 lines
9.2 KiB
Python
284 lines
9.2 KiB
Python
"""Helpers for writing tests"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import os.path
|
|
import re
|
|
import shutil
|
|
from typing import Callable, Iterator
|
|
|
|
from mypy import build
|
|
from mypy.errors import CompileError
|
|
from mypy.options import Options
|
|
from mypy.test.config import test_temp_dir
|
|
from mypy.test.data import DataDrivenTestCase, DataSuite
|
|
from mypy.test.helpers import assert_string_arrays_equal
|
|
from mypyc.analysis.ircheck import assert_func_ir_valid
|
|
from mypyc.common import IS_32_BIT_PLATFORM, PLATFORM_SIZE
|
|
from mypyc.errors import Errors
|
|
from mypyc.ir.func_ir import FuncIR
|
|
from mypyc.ir.module_ir import ModuleIR
|
|
from mypyc.irbuild.main import build_ir
|
|
from mypyc.irbuild.mapper import Mapper
|
|
from mypyc.options import CompilerOptions
|
|
from mypyc.test.config import test_data_prefix
|
|
|
|
# The builtins stub used during icode generation test cases.
|
|
ICODE_GEN_BUILTINS = os.path.join(test_data_prefix, "fixtures/ir.py")
|
|
# The testutil support library
|
|
TESTUTIL_PATH = os.path.join(test_data_prefix, "fixtures/testutil.py")
|
|
|
|
|
|
class MypycDataSuite(DataSuite):
|
|
# Need to list no files, since this will be picked up as a suite of tests
|
|
files: list[str] = []
|
|
data_prefix = test_data_prefix
|
|
|
|
|
|
def builtins_wrapper(
|
|
func: Callable[[DataDrivenTestCase], None], path: str
|
|
) -> Callable[[DataDrivenTestCase], None]:
|
|
"""Decorate a function that implements a data-driven test case to copy an
|
|
alternative builtins module implementation in place before performing the
|
|
test case. Clean up after executing the test case.
|
|
"""
|
|
return lambda testcase: perform_test(func, path, testcase)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def use_custom_builtins(builtins_path: str, testcase: DataDrivenTestCase) -> Iterator[None]:
|
|
for path, _ in testcase.files:
|
|
if os.path.basename(path) == "builtins.pyi":
|
|
default_builtins = False
|
|
break
|
|
else:
|
|
# Use default builtins.
|
|
builtins = os.path.abspath(os.path.join(test_temp_dir, "builtins.pyi"))
|
|
shutil.copyfile(builtins_path, builtins)
|
|
default_builtins = True
|
|
|
|
# Actually perform the test case.
|
|
try:
|
|
yield None
|
|
finally:
|
|
if default_builtins:
|
|
# Clean up.
|
|
os.remove(builtins)
|
|
|
|
|
|
def perform_test(
|
|
func: Callable[[DataDrivenTestCase], None], builtins_path: str, testcase: DataDrivenTestCase
|
|
) -> None:
|
|
for path, _ in testcase.files:
|
|
if os.path.basename(path) == "builtins.py":
|
|
default_builtins = False
|
|
break
|
|
else:
|
|
# Use default builtins.
|
|
builtins = os.path.join(test_temp_dir, "builtins.py")
|
|
shutil.copyfile(builtins_path, builtins)
|
|
default_builtins = True
|
|
|
|
# Actually perform the test case.
|
|
func(testcase)
|
|
|
|
if default_builtins:
|
|
# Clean up.
|
|
os.remove(builtins)
|
|
|
|
|
|
def build_ir_for_single_file(
|
|
input_lines: list[str], compiler_options: CompilerOptions | None = None
|
|
) -> list[FuncIR]:
|
|
return build_ir_for_single_file2(input_lines, compiler_options).functions
|
|
|
|
|
|
def build_ir_for_single_file2(
|
|
input_lines: list[str], compiler_options: CompilerOptions | None = None
|
|
) -> ModuleIR:
|
|
program_text = "\n".join(input_lines)
|
|
|
|
# By default generate IR compatible with the earliest supported Python C API.
|
|
# If a test needs more recent API features, this should be overridden.
|
|
compiler_options = compiler_options or CompilerOptions(capi_version=(3, 7))
|
|
options = Options()
|
|
options.show_traceback = True
|
|
options.hide_error_codes = True
|
|
options.use_builtins_fixtures = True
|
|
options.strict_optional = True
|
|
options.python_version = compiler_options.python_version or (3, 6)
|
|
options.export_types = True
|
|
options.preserve_asts = True
|
|
options.allow_empty_bodies = True
|
|
options.per_module_options["__main__"] = {"mypyc": True}
|
|
|
|
source = build.BuildSource("main", "__main__", program_text)
|
|
# Construct input as a single single.
|
|
# Parse and type check the input program.
|
|
result = build.build(sources=[source], options=options, alt_lib_path=test_temp_dir)
|
|
if result.errors:
|
|
raise CompileError(result.errors)
|
|
|
|
errors = Errors(options)
|
|
modules = build_ir(
|
|
[result.files["__main__"]],
|
|
result.graph,
|
|
result.types,
|
|
Mapper({"__main__": None}),
|
|
compiler_options,
|
|
errors,
|
|
)
|
|
if errors.num_errors:
|
|
raise CompileError(errors.new_messages())
|
|
|
|
module = list(modules.values())[0]
|
|
for fn in module.functions:
|
|
assert_func_ir_valid(fn)
|
|
return module
|
|
|
|
|
|
def update_testcase_output(testcase: DataDrivenTestCase, output: list[str]) -> None:
|
|
# TODO: backport this to mypy
|
|
assert testcase.old_cwd is not None, "test was not properly set up"
|
|
testcase_path = os.path.join(testcase.old_cwd, testcase.file)
|
|
with open(testcase_path) as f:
|
|
data_lines = f.read().splitlines()
|
|
|
|
# We can't rely on the test line numbers to *find* the test, since
|
|
# we might fix multiple tests in a run. So find it by the case
|
|
# header. Give up if there are multiple tests with the same name.
|
|
test_slug = f"[case {testcase.name}]"
|
|
if data_lines.count(test_slug) != 1:
|
|
return
|
|
start_idx = data_lines.index(test_slug)
|
|
stop_idx = start_idx + 11
|
|
while stop_idx < len(data_lines) and not data_lines[stop_idx].startswith("[case "):
|
|
stop_idx += 1
|
|
|
|
test = data_lines[start_idx:stop_idx]
|
|
out_start = test.index("[out]")
|
|
test[out_start + 1 :] = output
|
|
data_lines[start_idx:stop_idx] = test + [""]
|
|
data = "\n".join(data_lines)
|
|
|
|
with open(testcase_path, "w") as f:
|
|
print(data, file=f)
|
|
|
|
|
|
def assert_test_output(
|
|
testcase: DataDrivenTestCase,
|
|
actual: list[str],
|
|
message: str,
|
|
expected: list[str] | None = None,
|
|
formatted: list[str] | None = None,
|
|
) -> None:
|
|
__tracebackhide__ = True
|
|
|
|
expected_output = expected if expected is not None else testcase.output
|
|
if expected_output != actual and testcase.config.getoption("--update-data", False):
|
|
update_testcase_output(testcase, actual)
|
|
|
|
assert_string_arrays_equal(
|
|
expected_output, actual, f"{message} ({testcase.file}, line {testcase.line})"
|
|
)
|
|
|
|
|
|
def get_func_names(expected: list[str]) -> list[str]:
|
|
res = []
|
|
for s in expected:
|
|
m = re.match(r"def ([_a-zA-Z0-9.*$]+)\(", s)
|
|
if m:
|
|
res.append(m.group(1))
|
|
return res
|
|
|
|
|
|
def remove_comment_lines(a: list[str]) -> list[str]:
|
|
"""Return a copy of array with comments removed.
|
|
|
|
Lines starting with '--' (but not with '---') are removed.
|
|
"""
|
|
r = []
|
|
for s in a:
|
|
if s.strip().startswith("--") and not s.strip().startswith("---"):
|
|
pass
|
|
else:
|
|
r.append(s)
|
|
return r
|
|
|
|
|
|
def print_with_line_numbers(s: str) -> None:
|
|
lines = s.splitlines()
|
|
for i, line in enumerate(lines):
|
|
print("%-4d %s" % (i + 1, line))
|
|
|
|
|
|
def heading(text: str) -> None:
|
|
print("=" * 20 + " " + text + " " + "=" * 20)
|
|
|
|
|
|
def show_c(cfiles: list[list[tuple[str, str]]]) -> None:
|
|
heading("Generated C")
|
|
for group in cfiles:
|
|
for cfile, ctext in group:
|
|
print(f"== {cfile} ==")
|
|
print_with_line_numbers(ctext)
|
|
heading("End C")
|
|
|
|
|
|
def fudge_dir_mtimes(dir: str, delta: int) -> None:
|
|
for dirpath, _, filenames in os.walk(dir):
|
|
for name in filenames:
|
|
path = os.path.join(dirpath, name)
|
|
new_mtime = os.stat(path).st_mtime + delta
|
|
os.utime(path, times=(new_mtime, new_mtime))
|
|
|
|
|
|
def replace_word_size(text: list[str]) -> list[str]:
|
|
"""Replace WORDSIZE with platform specific word sizes"""
|
|
result = []
|
|
for line in text:
|
|
index = line.find("WORD_SIZE")
|
|
if index != -1:
|
|
# get 'WORDSIZE*n' token
|
|
word_size_token = line[index:].split()[0]
|
|
n = int(word_size_token[10:])
|
|
replace_str = str(PLATFORM_SIZE * n)
|
|
result.append(line.replace(word_size_token, replace_str))
|
|
else:
|
|
result.append(line)
|
|
return result
|
|
|
|
|
|
def infer_ir_build_options_from_test_name(name: str) -> CompilerOptions | None:
|
|
"""Look for magic substrings in test case name to set compiler options.
|
|
|
|
Return None if the test case should be skipped (always pass).
|
|
|
|
Supported naming conventions:
|
|
|
|
*_64bit*:
|
|
Run test case only on 64-bit platforms
|
|
*_32bit*:
|
|
Run test caseonly on 32-bit platforms
|
|
*_python3_8* (or for any Python version):
|
|
Use Python 3.8+ C API features (default: lowest supported version)
|
|
*StripAssert*:
|
|
Don't generate code for assert statements
|
|
"""
|
|
# If this is specific to some bit width, always pass if platform doesn't match.
|
|
if "_64bit" in name and IS_32_BIT_PLATFORM:
|
|
return None
|
|
if "_32bit" in name and not IS_32_BIT_PLATFORM:
|
|
return None
|
|
options = CompilerOptions(strip_asserts="StripAssert" in name, capi_version=(3, 7))
|
|
# A suffix like _python3.8 is used to set the target C API version.
|
|
m = re.search(r"_python([3-9]+)_([0-9]+)(_|\b)", name)
|
|
if m:
|
|
options.capi_version = (int(m.group(1)), int(m.group(2)))
|
|
options.python_version = options.capi_version
|
|
elif "_py" in name or "_Python" in name:
|
|
assert False, f"Invalid _py* suffix (should be _pythonX_Y): {name}"
|
|
return options
|