mirror of
https://github.com/AdaCore/langkit.git
synced 2026-02-12 12:28:12 -08:00
Introduce the testsuite `--with-langkit-python` option so that it is possible to have: * one interpreter with the e3 distribution (to run the testsuite), * one interpreter with Langkit installed (to run lkm and the various scripts using Langkit modules), * one bare interpreter to import Langkit-generated libraries.
373 lines
12 KiB
Python
373 lines
12 KiB
Python
import glob
|
|
import os
|
|
import os.path
|
|
import sys
|
|
|
|
from e3.fs import sync_tree
|
|
from e3.testsuite.control import YAMLTestControlCreator
|
|
from e3.testsuite.driver.classic import TestAbortWithError
|
|
from e3.testsuite.driver.diff import (
|
|
DiffTestDriver,
|
|
OutputRefiner,
|
|
PatternSubstitute,
|
|
Substitute,
|
|
)
|
|
|
|
from drivers.valgrind import valgrind_cmd
|
|
|
|
|
|
class PythonTracebackCollapser(OutputRefiner[str]):
|
|
"""Remove quoted lines of code from Python tracebacks.
|
|
|
|
This is useful to cope with variations depending on Python interpreter
|
|
versions.
|
|
"""
|
|
|
|
# Sub-refiner to hide line numbers from stack frames
|
|
linenos_refiner = PatternSubstitute(r" line \d+, ", " line XXX, ")
|
|
|
|
def __init__(self, hide_linenos: bool):
|
|
self.hide_linenos = hide_linenos
|
|
|
|
def refine(self, output: str) -> str:
|
|
result = []
|
|
|
|
# To collapse source code quoted in tracebacks, proceed line by line...
|
|
|
|
# Are we in the middle of a Python traceback?
|
|
in_traceback = False
|
|
|
|
# Are we in the middle of quoted source code?
|
|
in_source_code = False
|
|
|
|
for l in output.splitlines():
|
|
if l.startswith("Traceback (most recent call last):"):
|
|
# This marks the beginning of a Python traceback
|
|
in_traceback = True
|
|
|
|
elif in_traceback:
|
|
# In tracebacks, quoted source code starts with 4 spaces while
|
|
# frame locations ("File FFF, line LLL, in FFF") start with
|
|
# 2 spaces.
|
|
if not l.startswith(" "):
|
|
# Less than 4 spaces of indentation: this is not quoted
|
|
# source code anymore.
|
|
in_source_code = False
|
|
if not l.startswith(" "):
|
|
# Not even 2 spaces of indentation: we are now out of
|
|
# the traceback.
|
|
in_traceback = False
|
|
elif self.hide_linenos:
|
|
l = self.linenos_refiner.refine(l)
|
|
|
|
elif not in_source_code:
|
|
# We do have a 4-spaces indentation. This is the first line
|
|
# of quoted source code: add a single "<source code>"
|
|
# marker for this quoted block.
|
|
l = " <source code>"
|
|
in_source_code = True
|
|
else:
|
|
# This is the continuation of quoted source code: just skip
|
|
# it.
|
|
continue
|
|
result.append(l + "\n")
|
|
return "".join(result)
|
|
|
|
|
|
class BaseDriver(DiffTestDriver):
|
|
"""
|
|
Base class to provide common test driver helpers.
|
|
"""
|
|
|
|
@property
|
|
def test_control_creator(self):
|
|
return YAMLTestControlCreator(self.env.control_condition_env)
|
|
|
|
def set_up(self):
|
|
super().set_up()
|
|
|
|
# Allow tests to clone directories in their own working dir, such
|
|
# as common dependencies between multiple tests.
|
|
for path in self.test_env.get("sync_trees", []):
|
|
sync_tree(self.test_dir(path), self.working_dir(), delete=False)
|
|
|
|
def tear_down(self):
|
|
# Allow test drivers to create "*.log" files in their working space
|
|
# just for logging purposes, and so forward them to the test result,
|
|
# for post-mortem investigation.
|
|
for log_file in sorted(glob.glob(self.working_dir("*.log"))):
|
|
with open(log_file) as f:
|
|
log_content = f.read()
|
|
|
|
self.result.log += (
|
|
f"== Content of {os.path.basename(log_file)} =="
|
|
f"\n\n{log_content}"
|
|
"\n\n== END =="
|
|
)
|
|
|
|
super().tear_down()
|
|
|
|
@property
|
|
def output_refiners(self):
|
|
result = super().output_refiners
|
|
|
|
# Replace references to the working directory with a placeholder, so
|
|
# that baselines are not influenced by where the testsuite is run.
|
|
pattern = self.working_dir()
|
|
repl = "<working-dir>"
|
|
if self.default_encoding == "binary":
|
|
pattern = pattern.encode("ascii")
|
|
repl = repl.encode("ascii")
|
|
result.append(Substitute(pattern, repl))
|
|
|
|
# If requested, canonicalize Windows-style directory separators to
|
|
# Unix-style.
|
|
if self.test_env.get("canonicalize_directory_separators", False):
|
|
result.append(Substitute("\\", "/"))
|
|
|
|
# If requested, collapse Python tracebacks and hide line numbers
|
|
if self.test_env.get("collapse_python_tracebacks"):
|
|
result.append(
|
|
PythonTracebackCollapser(
|
|
hide_linenos=self.test_env.get(
|
|
"hide_python_traceback_linenos", False
|
|
)
|
|
)
|
|
)
|
|
|
|
return result + [
|
|
# Hide platform-specific details from Python traceback
|
|
PatternSubstitute(
|
|
r'File "[^"]*[/\\]([^"/\\]+)"',
|
|
r'File ".../\1"',
|
|
),
|
|
]
|
|
|
|
def read_file(self, filename):
|
|
"""Return the content of `filename`."""
|
|
with open(filename, "r") as f:
|
|
return f.read()
|
|
|
|
# Convenience path builders
|
|
|
|
@property
|
|
def langkit_root_dir(self):
|
|
"""Return the absolute path to the repository root directory."""
|
|
return os.path.abspath(os.path.join(self.testsuite_dir, ".."))
|
|
|
|
@property
|
|
def testsuite_dir(self):
|
|
"""Return the absolute path to the testsuite root directory."""
|
|
result = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
|
return os.path.abspath(result)
|
|
|
|
@property
|
|
def coverage_enabled(self):
|
|
return self.env.options.coverage
|
|
|
|
def coverage_file(self, ext):
|
|
"""
|
|
Return the name of a coverage data file (or trace file) for the current
|
|
test.
|
|
|
|
:param str ext: File extension for this file.
|
|
:rtype: str
|
|
"""
|
|
return os.path.join(
|
|
self.env.coverage_dir, self.test_env["test_name"] + "." + ext
|
|
)
|
|
|
|
@property
|
|
def valgrind_enabled(self):
|
|
return self.env.options.valgrind
|
|
|
|
#
|
|
# Tear up helpers
|
|
#
|
|
|
|
@property
|
|
def langkit_python_interpreter(self):
|
|
"""
|
|
See the documentation for --with-langkit-python.
|
|
"""
|
|
return self.env.options.with_langkit_python or sys.executable
|
|
|
|
@property
|
|
def python_interpreter(self):
|
|
"""
|
|
See the documentation for --with-python.
|
|
"""
|
|
return self.env.options.with_python or "python"
|
|
|
|
def check_file(self, filename):
|
|
"""
|
|
Check file presence.
|
|
|
|
If the file does not exist test is aborted.
|
|
"""
|
|
if not os.path.isfile(self.test_dir(filename)):
|
|
raise TestAbortWithError(
|
|
"Missing mandatory file: {}".format(filename)
|
|
)
|
|
|
|
def check_file_list(self, what, file_list, can_be_empty=True):
|
|
"""Raise a SetupError if `file_list` is not a list of existing files.
|
|
|
|
Also raise an error if it is an empty list while `can_be_empty` is
|
|
False.
|
|
"""
|
|
# First check we have a list of strings
|
|
if (
|
|
not isinstance(file_list, list)
|
|
or (not can_be_empty and len(file_list) == 0)
|
|
or not all(isinstance(fn, str) for fn in file_list)
|
|
):
|
|
empty_msg = "non-empty "
|
|
raise TestAbortWithError(
|
|
"{} must be a {}list of strings".format(what, empty_msg)
|
|
)
|
|
|
|
# Then check that these are existing files
|
|
for filename in file_list:
|
|
self.check_file(filename)
|
|
|
|
def add_path(self, env, env_var, path):
|
|
"""
|
|
Add a path to some environment variable.
|
|
|
|
:param dict[str, str] env: Environment to modify.
|
|
:param str env_var: Name of the environment variable to define/extend.
|
|
:param str path: Path to prepend.
|
|
"""
|
|
assert isinstance(env_var, str)
|
|
assert isinstance(path, str)
|
|
path_list = env.get(env_var, "")
|
|
assert isinstance(path_list, str)
|
|
if path_list:
|
|
path_list = "{}{}{}".format(path, os.path.pathsep, path_list)
|
|
else:
|
|
path_list = path
|
|
|
|
env[env_var] = path_list
|
|
|
|
#
|
|
# Run helpers
|
|
#
|
|
|
|
def run_and_check(
|
|
self,
|
|
argv,
|
|
env=None,
|
|
for_coverage=False,
|
|
memcheck=False,
|
|
analyze_output=True,
|
|
):
|
|
"""
|
|
Run a subprocess with `argv` and check it completes with status code 0.
|
|
|
|
In case of failure, the test output is appended to the actual output
|
|
and a TestError is raised.
|
|
|
|
:param list[str] argv: List of arguments to pass to the subprocess.
|
|
:param None|dict[str, str] env: If provided, environment variables to
|
|
pass to the subprocess.
|
|
:param bool for_coverage: If true and if coverage is enabled, produce a
|
|
trace file.
|
|
:param bool memcheck: If true and if Valgrind runs are requested, run
|
|
this process under Valgrind. If there are memory issues, they be
|
|
reported on the testcase output and the process will return
|
|
non-zero.
|
|
:param bool analyze_output: See
|
|
e3.testsuite.driver.classic.ClassicTestDriver.shell.
|
|
"""
|
|
if for_coverage and self.coverage_enabled:
|
|
trace_file = self.coverage_file("trace")
|
|
argv = ["gnatcov", "run", "-o", trace_file] + argv
|
|
|
|
if memcheck and self.valgrind_enabled:
|
|
argv = valgrind_cmd(argv)
|
|
|
|
self.shell(argv, env=env, analyze_output=analyze_output)
|
|
|
|
@property
|
|
def memcheck_for_lkt(self) -> bool:
|
|
"""
|
|
Return whether Lkt programs can be run under Valgrind.
|
|
|
|
We always build Langkit for 64-bit platforms, so when we test it to
|
|
target 32-bit, we cannot use Valgrind (32-bit) to memcheck Langkit
|
|
binaries (64-bit).
|
|
"""
|
|
return self.env.build.cpu.bits == 64
|
|
|
|
def create_project_file(self, project_file, mains):
|
|
"""
|
|
Create a project file for the given main source files.
|
|
|
|
The project file is created in the working directory. It gathers the
|
|
Ada source files in the working directory.
|
|
|
|
:param str project_file: Project file name to create.
|
|
:param list[str] mains: List of main source files.
|
|
"""
|
|
|
|
def format_string_list(strings):
|
|
return ", ".join('"{}"'.format(s) for s in strings)
|
|
|
|
cargs = ["-O0", "-g", "-gnata", "-gnatwae"]
|
|
if self.coverage_enabled:
|
|
cargs += ["-fdump-scos", "-fpreserve-control-flow"]
|
|
|
|
with open(self.working_dir(project_file), "w") as f:
|
|
f.write(
|
|
"""
|
|
with "langkit_support";
|
|
|
|
project P is
|
|
for Languages use ("Ada");
|
|
for Source_Dirs use (".");
|
|
for Object_Dir use "obj";
|
|
for Main use ({mains});
|
|
|
|
package Compiler is
|
|
for Default_Switches ("Ada") use ({cargs});
|
|
end Compiler;
|
|
end P;
|
|
""".format(
|
|
mains=", ".join('"{}"'.format(m) for m in mains),
|
|
cargs=format_string_list(cargs),
|
|
)
|
|
)
|
|
|
|
def gprbuild(self, project_file):
|
|
"""
|
|
Run GPRbuild on the given project file.
|
|
|
|
:param str project_file: Project file name.
|
|
"""
|
|
argv = [
|
|
"gprbuild",
|
|
f"-P{project_file}",
|
|
"-p",
|
|
f"-j{self.env.inner_jobs}",
|
|
]
|
|
if self.coverage_enabled:
|
|
argv.append("--subdirs=gnatcov")
|
|
self.run_and_check(argv, analyze_output=False)
|
|
|
|
def program_path(self, main_source_file):
|
|
"""
|
|
Return the path to the program corresponding to the given main file.
|
|
|
|
:param str main_source_file: File name for the main source file from
|
|
which the program has been built.
|
|
:rtype: str
|
|
"""
|
|
assert main_source_file.endswith(".adb")
|
|
program_name = main_source_file[:-4]
|
|
return (
|
|
self.working_dir("obj", "gnatcov", program_name)
|
|
if self.coverage_enabled
|
|
else self.working_dir("obj", program_name)
|
|
)
|