xref: /llvm-project/llvm/utils/lit/lit/LitConfig.py (revision 44d4b30ca30b11f29fdb7819e94dcc54ac8a979a)
1from __future__ import absolute_import
2import inspect
3import os
4import platform
5import sys
6
7import lit.Test
8import lit.formats
9import lit.TestingConfig
10import lit.util
11
12# LitConfig must be a new style class for properties to work
13class LitConfig(object):
14    """LitConfig - Configuration data for a 'lit' test runner instance, shared
15    across all tests.
16
17    The LitConfig object is also used to communicate with client configuration
18    files, it is always passed in as the global variable 'lit' so that
19    configuration files can access common functionality and internal components
20    easily.
21    """
22
23    def __init__(
24        self,
25        progname,
26        path,
27        quiet,
28        useValgrind,
29        valgrindLeakCheck,
30        valgrindArgs,
31        noExecute,
32        debug,
33        isWindows,
34        order,
35        params,
36        config_prefix=None,
37        maxIndividualTestTime=0,
38        parallelism_groups={},
39        per_test_coverage=False,
40        gtest_sharding=True,
41    ):
42        # The name of the test runner.
43        self.progname = progname
44        # The items to add to the PATH environment variable.
45        self.path = [str(p) for p in path]
46        self.quiet = bool(quiet)
47        self.useValgrind = bool(useValgrind)
48        self.valgrindLeakCheck = bool(valgrindLeakCheck)
49        self.valgrindUserArgs = list(valgrindArgs)
50        self.noExecute = noExecute
51        self.debug = debug
52        self.isWindows = bool(isWindows)
53        self.order = order
54        self.params = dict(params)
55        self.bashPath = None
56
57        # Configuration files to look for when discovering test suites.
58        self.config_prefix = config_prefix or "lit"
59        self.suffixes = ["cfg.py", "cfg"]
60        self.config_names = ["%s.%s" % (self.config_prefix, x) for x in self.suffixes]
61        self.site_config_names = [
62            "%s.site.%s" % (self.config_prefix, x) for x in self.suffixes
63        ]
64        self.local_config_names = [
65            "%s.local.%s" % (self.config_prefix, x) for x in self.suffixes
66        ]
67
68        self.numErrors = 0
69        self.numWarnings = 0
70
71        self.valgrindArgs = []
72        if self.useValgrind:
73            self.valgrindArgs = [
74                "valgrind",
75                "-q",
76                "--run-libc-freeres=no",
77                "--tool=memcheck",
78                "--trace-children=yes",
79                "--error-exitcode=123",
80            ]
81            if self.valgrindLeakCheck:
82                self.valgrindArgs.append("--leak-check=full")
83            else:
84                # The default is 'summary'.
85                self.valgrindArgs.append("--leak-check=no")
86            self.valgrindArgs.extend(self.valgrindUserArgs)
87
88        self.maxIndividualTestTime = maxIndividualTestTime
89        self.parallelism_groups = parallelism_groups
90        self.per_test_coverage = per_test_coverage
91        self.gtest_sharding = bool(gtest_sharding)
92
93    @property
94    def maxIndividualTestTime(self):
95        """
96        Interface for getting maximum time to spend executing
97        a single test
98        """
99        return self._maxIndividualTestTime
100
101    @property
102    def maxIndividualTestTimeIsSupported(self):
103        """
104        Returns a tuple (<supported> , <error message>)
105        where
106        `<supported>` is True if setting maxIndividualTestTime is supported
107            on the current host, returns False otherwise.
108        `<error message>` is an empty string if `<supported>` is True,
109            otherwise is contains a string describing why setting
110            maxIndividualTestTime is not supported.
111        """
112        return lit.util.killProcessAndChildrenIsSupported()
113
114    @maxIndividualTestTime.setter
115    def maxIndividualTestTime(self, value):
116        """
117        Interface for setting maximum time to spend executing
118        a single test
119        """
120        if not isinstance(value, int):
121            self.fatal("maxIndividualTestTime must set to a value of type int.")
122        self._maxIndividualTestTime = value
123        if self.maxIndividualTestTime > 0:
124            # The current implementation needs psutil on some platforms to set
125            # a timeout per test. Check it's available.
126            # See lit.util.killProcessAndChildren()
127            supported, errormsg = self.maxIndividualTestTimeIsSupported
128            if not supported:
129                self.fatal("Setting a timeout per test not supported. " + errormsg)
130        elif self.maxIndividualTestTime < 0:
131            self.fatal("The timeout per test must be >= 0 seconds")
132
133    @property
134    def per_test_coverage(self):
135        """
136        Interface for getting the per_test_coverage value
137        """
138        return self._per_test_coverage
139
140    @per_test_coverage.setter
141    def per_test_coverage(self, value):
142        """
143        Interface for setting the per_test_coverage value
144        """
145        if not isinstance(value, bool):
146            self.fatal("per_test_coverage must set to a value of type bool.")
147        self._per_test_coverage = value
148
149    def load_config(self, config, path):
150        """load_config(config, path) - Load a config object from an alternate
151        path."""
152        if self.debug:
153            self.note("load_config from %r" % path)
154        config.load_from_path(path, self)
155        return config
156
157    def getBashPath(self):
158        """getBashPath - Get the path to 'bash'"""
159        if self.bashPath is not None:
160            return self.bashPath
161
162        self.bashPath = lit.util.which("bash", os.pathsep.join(self.path))
163        if self.bashPath is None:
164            self.bashPath = lit.util.which("bash")
165
166        if self.bashPath is None:
167            self.bashPath = ""
168
169        # Check whether the found version of bash is able to cope with paths in
170        # the host path format. If not, don't return it as it can't be used to
171        # run scripts. For example, WSL's bash.exe requires '/mnt/c/foo' rather
172        # than 'C:\\foo' or 'C:/foo'.
173        if self.isWindows and self.bashPath:
174            command = [
175                self.bashPath,
176                "-c",
177                '[[ -f "%s" ]]' % self.bashPath.replace("\\", "\\\\"),
178            ]
179            _, _, exitCode = lit.util.executeCommand(command)
180            if exitCode:
181                self.note(
182                    "bash command failed: %s" % (" ".join('"%s"' % c for c in command))
183                )
184                self.bashPath = ""
185
186        if not self.bashPath:
187            self.warning("Unable to find a usable version of bash.")
188
189        return self.bashPath
190
191    def getToolsPath(self, dir, paths, tools):
192        if dir is not None and os.path.isabs(dir) and os.path.isdir(dir):
193            if not lit.util.checkToolsPath(dir, tools):
194                return None
195        else:
196            dir = lit.util.whichTools(tools, paths)
197
198        # bash
199        self.bashPath = lit.util.which("bash", dir)
200        if self.bashPath is None:
201            self.bashPath = ""
202
203        return dir
204
205    def _write_message(self, kind, message):
206        # Get the file/line where this message was generated.
207        f = inspect.currentframe()
208        # Step out of _write_message, and then out of wrapper.
209        f = f.f_back.f_back
210        file = os.path.abspath(inspect.getsourcefile(f))
211        line = inspect.getlineno(f)
212        sys.stderr.write(
213            "%s: %s:%d: %s: %s\n" % (self.progname, file, line, kind, message)
214        )
215        if self.isWindows:
216            # In a git bash terminal, the writes to sys.stderr aren't visible
217            # on screen immediately. Flush them here to avoid broken/misoredered
218            # output.
219            sys.stderr.flush()
220
221    def substitute(self, string):
222        """substitute - Interpolate params into a string"""
223        try:
224            return string % self.params
225        except KeyError as e:
226            (key,) = e.args
227            self.fatal(
228                "unable to find %r parameter, use '--param=%s=VALUE'" % (key, key)
229            )
230
231    def note(self, message):
232        if not self.quiet:
233            self._write_message("note", message)
234
235    def warning(self, message):
236        if not self.quiet:
237            self._write_message("warning", message)
238        self.numWarnings += 1
239
240    def error(self, message):
241        self._write_message("error", message)
242        self.numErrors += 1
243
244    def fatal(self, message):
245        self._write_message("fatal", message)
246        sys.exit(2)
247