/
dsl.py
390 lines (334 loc) · 15.2 KB
/
dsl.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
#===----------------------------------------------------------------------===##
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
#===----------------------------------------------------------------------===##
import libcxx.test.format
import lit
import lit.util
import os
import pipes
import platform
import re
import tempfile
def _memoize(f):
cache = dict()
def memoized(x):
if x not in cache:
cache[x] = f(x)
return cache[x]
return memoized
def _executeScriptInternal(test, commands):
"""
Returns (stdout, stderr, exitCode, timeoutInfo)
TODO: This really should be easier to access from Lit itself
"""
parsedCommands = libcxx.test.format.parseScript(test, preamble=commands)
litConfig = lit.LitConfig.LitConfig(
progname='lit',
path=[],
quiet=False,
useValgrind=False,
valgrindLeakCheck=False,
valgrindArgs=[],
noExecute=False,
debug=False,
isWindows=platform.system() == 'Windows',
params={})
_, tmpBase = libcxx.test.format._getTempPaths(test)
execDir = os.path.dirname(test.getExecPath())
for d in (execDir, os.path.dirname(tmpBase)):
if not os.path.exists(d):
os.makedirs(d)
res = lit.TestRunner.executeScriptInternal(test, litConfig, tmpBase, parsedCommands, execDir)
if isinstance(res, lit.Test.Result):
res = ('', '', 127, None)
return res
def _makeConfigTest(config, testPrefix=None):
sourceRoot = os.path.join(config.test_exec_root, '__config_src__')
execRoot = os.path.join(config.test_exec_root, '__config_exec__')
suite = lit.Test.TestSuite('__config__', sourceRoot, execRoot, config)
if not os.path.exists(sourceRoot):
os.makedirs(sourceRoot)
tmp = tempfile.NamedTemporaryFile(dir=sourceRoot, delete=False, suffix='.cpp',
prefix=testPrefix)
tmp.close()
pathInSuite = [os.path.relpath(tmp.name, sourceRoot)]
class TestWrapper(lit.Test.Test):
def __enter__(self): return self
def __exit__(self, *args): os.remove(tmp.name)
return TestWrapper(suite, pathInSuite, config)
def sourceBuilds(config, source):
"""
Return whether the program in the given string builds successfully.
This is done by compiling and linking a program that consists of the given
source with the %{cxx} substitution, and seeing whether that succeeds.
"""
with _makeConfigTest(config) as test:
with open(test.getSourcePath(), 'w') as sourceFile:
sourceFile.write(source)
out, err, exitCode, timeoutInfo = _executeScriptInternal(test, [
"%{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe"
])
_executeScriptInternal(test, ['rm %t.exe'])
return exitCode == 0
def programOutput(config, program, args=[], testPrefix=None):
"""
Compiles a program for the test target, run it on the test target and return
the output.
If the program fails to compile or run, None is returned instead. Note that
execution of the program is done through the %{exec} substitution, which means
that the program may be run on a remote host depending on what %{exec} does.
"""
with _makeConfigTest(config, testPrefix=testPrefix) as test:
with open(test.getSourcePath(), 'w') as source:
source.write(program)
try:
_, _, exitCode, _ = _executeScriptInternal(test, [
"%{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe",
])
if exitCode != 0:
return None
out, err, exitCode, _ = _executeScriptInternal(test, [
"%{{exec}} %t.exe {}".format(' '.join(args))
])
if exitCode != 0:
return None
actualOut = re.search("command output:\n(.+)\n$", out, flags=re.DOTALL)
actualOut = actualOut.group(1) if actualOut else ""
return actualOut
finally:
_executeScriptInternal(test, ['rm %t.exe'])
def hasCompileFlag(config, flag):
"""
Return whether the compiler in the configuration supports a given compiler flag.
This is done by executing the %{cxx} substitution with the given flag and
checking whether that succeeds.
"""
with _makeConfigTest(config) as test:
out, err, exitCode, timeoutInfo = _executeScriptInternal(test, [
"%{{cxx}} -xc++ {} -Werror -fsyntax-only %{{flags}} %{{compile_flags}} {}".format(os.devnull, flag)
])
return exitCode == 0
def hasLocale(config, locale):
"""
Return whether the runtime execution environment supports a given locale.
This is done by executing a program that tries to set the given locale using
%{exec} -- this means that the command may be executed on a remote host
depending on the %{exec} substitution.
"""
program = """
#include <locale.h>
int main(int, char** argv) {
if (::setlocale(LC_ALL, argv[1]) != NULL) return 0;
else return 1;
}
"""
return programOutput(config, program, args=[pipes.quote(locale)],
testPrefix="check_locale_" + locale) is not None
def compilerMacros(config, flags=''):
"""
Return a dictionary of predefined compiler macros.
The keys are strings representing macros, and the values are strings
representing what each macro is defined to.
If the optional `flags` argument (a string) is provided, these flags will
be added to the compiler invocation when generating the macros.
"""
with _makeConfigTest(config) as test:
unparsedOutput, err, exitCode, timeoutInfo = _executeScriptInternal(test, [
"%{{cxx}} -xc++ {} -dM -E %{{flags}} %{{compile_flags}} {}".format(os.devnull, flags)
])
parsedMacros = dict()
defines = (l.strip() for l in unparsedOutput.split('\n') if l.startswith('#define '))
for line in defines:
line = line[len('#define '):]
macro, _, value = line.partition(' ')
parsedMacros[macro] = value
return parsedMacros
def featureTestMacros(config, flags=''):
"""
Return a dictionary of feature test macros.
The keys are strings representing feature test macros, and the values are
integers representing the value of the macro.
"""
allMacros = compilerMacros(config, flags)
return {m: int(v.rstrip('LlUu')) for (m, v) in allMacros.items() if m.startswith('__cpp_')}
class Feature(object):
"""
Represents a Lit available feature that is enabled whenever it is supported.
A feature like this informs the test suite about a capability of the compiler,
platform, etc. Unlike Parameters, it does not make sense to explicitly
control whether a Feature is enabled -- it should be enabled whenever it
is supported.
"""
def __init__(self, name, compileFlag=None, linkFlag=None, when=lambda _: True):
"""
Create a Lit feature for consumption by a test suite.
- name
The name of the feature. This is what will end up in Lit's available
features if the feature is enabled. This can be either a string or a
callable, in which case it is passed the TestingConfig and should
generate a string representing the name of the feature.
- compileFlag
An optional compile flag to add when this feature is added to a
TestingConfig. If provided, this must be a string representing a
compile flag that will be appended to the end of the %{compile_flags}
substitution of the TestingConfig.
- linkFlag
An optional link flag to add when this feature is added to a
TestingConfig. If provided, this must be a string representing a
link flag that will be appended to the end of the %{link_flags}
substitution of the TestingConfig.
- when
A callable that gets passed a TestingConfig and should return a
boolean representing whether the feature is supported in that
configuration. For example, this can use `hasCompileFlag` to
check whether the compiler supports the flag that the feature
represents. If omitted, the feature will always be considered
supported.
"""
self._name = name
self._compileFlag = compileFlag
self._linkFlag = linkFlag
self._isSupported = when
def isSupported(self, config):
"""
Return whether the feature is supported by the given TestingConfig.
"""
return self._isSupported(config)
def getName(self, config):
"""
Return the name of the feature.
It is an error to call `f.getName(cfg)` if the feature `f` is not supported.
"""
assert self.isSupported(config), \
"Trying to get the name of a feature that is not supported in the given configuration"
name = self._name(config) if callable(self._name) else self._name
if not isinstance(name, str):
raise ValueError("Feature did not resolve to a name that's a string, got {}".format(name))
return name
def enableIn(self, config):
"""
Enable a feature in a TestingConfig.
The name of the feature is added to the set of available features of
`config`, and any compile or link flags provided upon construction of
the Feature are added to the end of the corresponding substitution in
the config.
It is an error to call `f.enableIn(cfg)` if the feature `f` is not
supported in that TestingConfig (i.e. if `not f.isSupported(cfg)`).
"""
assert self.isSupported(config), \
"Trying to enable feature {} that is not supported in the given configuration".format(self._name)
addTo = lambda subs, sub, flag: [(s, x + ' ' + flag) if s == sub else (s, x) for (s, x) in subs]
if self._compileFlag:
compileFlag = self._compileFlag(config) if callable(self._compileFlag) else self._compileFlag
config.substitutions = addTo(config.substitutions, '%{compile_flags}', compileFlag)
if self._linkFlag:
linkFlag = self._linkFlag(config) if callable(self._linkFlag) else self._linkFlag
config.substitutions = addTo(config.substitutions, '%{link_flags}', linkFlag)
config.available_features.add(self.getName(config))
def _str_to_bool(s):
"""
Convert a string value to a boolean.
True values are "y", "yes", "t", "true", "on" and "1", regardless of capitalization.
False values are "n", "no", "f", "false", "off" and "0", regardless of capitalization.
"""
trueVals = ["y", "yes", "t", "true", "on", "1"]
falseVals = ["n", "no", "f", "false", "off", "0"]
lower = s.lower()
if lower in trueVals:
return True
elif lower in falseVals:
return False
else:
raise ValueError("Got string '{}', which isn't a valid boolean".format(s))
class Parameter(object):
"""
Represents a parameter of a Lit test suite.
Parameters are used to customize the behavior of test suites in a user
controllable way. There are two ways of setting the value of a Parameter.
The first one is to pass `--param <KEY>=<VALUE>` when running Lit (or
equivalenlty to set `litConfig.params[KEY] = VALUE` somewhere in the
Lit configuration files. This method will set the parameter globally for
all test suites being run.
The second method is to set `config.KEY = VALUE` somewhere in the Lit
configuration files, which sets the parameter only for the test suite(s)
that use that `config` object.
Parameters can have multiple possible values, and they can have a default
value when left unspecified. They can also have a Feature associated to them,
in which case the Feature is added to the TestingConfig if the parameter is
enabled. It is an error if the Parameter is enabled but the Feature associated
to it is not supported, for example trying to set the compilation standard to
C++17 when `-std=c++17` is not supported by the compiler.
One important point is that Parameters customize the behavior of the test
suite in a bounded way, i.e. there should be a finite set of possible choices
for `<VALUE>`. While this may appear to be an aggressive restriction, this
is actually a very important constraint that ensures that the set of
configurations supported by a test suite is finite. Otherwise, a test
suite could have an unbounded number of supported configurations, and
nobody wants to be stuck maintaining that. If it's not possible for an
option to have a finite set of possible values (e.g. the path to the
compiler), it can be handled in the `lit.cfg`, but it shouldn't be
represented with a Parameter.
"""
def __init__(self, name, choices, type, help, feature, default=None):
"""
Create a Lit parameter to customize the behavior of a test suite.
- name
The name of the parameter that can be used to set it on the command-line.
On the command-line, the parameter can be set using `--param <name>=<value>`
when running Lit. This must be non-empty.
- choices
A non-empty set of possible values for this parameter. This must be
anything that can be iterated. It is an error if the parameter is
given a value that is not in that set, whether explicitly or through
a default value.
- type
A callable that can be used to parse the value of the parameter given
on the command-line. As a special case, using the type `bool` also
allows parsing strings with boolean-like contents.
- help
A string explaining the parameter, for documentation purposes.
TODO: We should be able to surface those from the Lit command-line.
- feature
A callable that gets passed the parsed value of the parameter (either
the one passed on the command-line or the default one), and that returns
either None or a Feature.
- default
An optional default value to use for the parameter when no value is
provided on the command-line. If the default value is a callable, it
is called with the TestingConfig and should return the default value
for the parameter. Whether the default value is computed or specified
directly, it must be in the 'choices' provided for that Parameter.
"""
self._name = name
if len(self._name) == 0:
raise ValueError("Parameter name must not be the empty string")
self._choices = list(choices) # should be finite
if len(self._choices) == 0:
raise ValueError("Parameter '{}' must be given at least one possible value".format(self._name))
self._parse = lambda x: (_str_to_bool(x) if type is bool and isinstance(x, str)
else type(x))
self._help = help
self._feature = feature
self._default = default
@property
def name(self):
"""
Return the name of the parameter.
This is the name that can be used to set the parameter on the command-line
when running Lit.
"""
return self._name
def getFeature(self, config, litParams):
param = getattr(config, self.name, None)
param = litParams.get(self.name, param)
if param is None and self._default is None:
raise ValueError("Parameter {} doesn't have a default value, but it was not specified in the Lit parameters or in the Lit config".format(self.name))
getDefault = lambda: self._default(config) if callable(self._default) else self._default
value = self._parse(param) if param is not None else getDefault()
if value not in self._choices:
raise ValueError("Got value '{}' for parameter '{}', which is not in the provided set of possible choices: {}".format(value, self.name, self._choices))
return self._feature(value)