blob: 1110ded979679eb4b166e0a0c75d443bdb963847 [file] [log] [blame]
#===----------------------------------------------------------------------===##
#
# 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.newformat
import lit
import lit.util
import os
import pipes
import platform
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
"""
class FakeLitConfig(object):
def __init__(self):
self.isWindows = platform.system() == 'Windows'
self.maxIndividualTestTime = 0
litConfig = FakeLitConfig()
_, tmpBase = lit.TestRunner.getTempPaths(test)
execDir = os.path.dirname(test.getExecPath())
if not os.path.exists(execDir):
os.makedirs(execDir)
res = lit.TestRunner.executeScriptInternal(test, litConfig, tmpBase, commands, execDir)
if isinstance(res, lit.Test.Result):
res = ('', '', 127, None)
return res
def _makeConfigTest(config):
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)
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 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:
commands = ["%{{cxx}} -xc++ {} -Werror -fsyntax-only %{{flags}} %{{compile_flags}} {}".format(os.devnull, flag)]
commands = libcxx.test.newformat.parseScript(test, preamble=commands, fileDependencies=[])
out, err, exitCode, timeoutInfo = _executeScriptInternal(test, commands)
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.
"""
with _makeConfigTest(config) as test:
with open(test.getSourcePath(), 'w') as source:
source.write("""
#include <locale.h>
int main(int, char** argv) {
if (::setlocale(LC_ALL, argv[1]) != NULL) return 0;
else return 1;
}
""")
commands = [
"mkdir -p %T",
"%{cxx} -xc++ %s %{flags} %{compile_flags} %{link_flags} -o %t.exe",
"%{{exec}} %t.exe {}".format(pipes.quote(locale)),
]
commands = libcxx.test.newformat.parseScript(test, preamble=commands, fileDependencies=['%t.exe'])
out, err, exitCode, timeoutInfo = _executeScriptInternal(test, commands)
cleanup = libcxx.test.newformat.parseScript(test, preamble=['rm %t.exe'], fileDependencies=[])
_executeScriptInternal(test, cleanup)
return exitCode == 0
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:
commands = ["%{{cxx}} -xc++ {} -dM -E %{{flags}} %{{compile_flags}} {}".format(os.devnull, flags)]
commands = libcxx.test.newformat.parseScript(test, preamble=commands, fileDependencies=[])
unparsedOutput, err, exitCode, timeoutInfo = _executeScriptInternal(test, commands)
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 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)
name = self._name(config) if callable(self._name) else self._name
config.available_features.add(name)
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, more specifically by passing `--param <KEY>=<VALUE>`
when running Lit. Parameters have multiple possible values, and they can
have a default value when left unspecified.
Parameters can 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 = litParams.get(self.name, None)
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".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)