[lit] Recursively expand substitutions

This allows defining substitutions in terms of other substitutions. For
example, a %build substitution could be defined in terms of a %cxx
substitution as '%cxx %s -o %t.exe' and the script would be properly
expanded.

Differential Revision: https://reviews.llvm.org/D76178
This commit is contained in:
Louis Dionne 2020-03-14 12:09:52 -04:00
parent 401a324c51
commit faf415a1de
17 changed files with 214 additions and 9 deletions

View File

@ -410,11 +410,12 @@ be used to define subdirectories of optional tests, or to change other
configuration parameters --- for example, to change the test format, or the configuration parameters --- for example, to change the test format, or the
suffixes which identify test files. suffixes which identify test files.
PRE-DEFINED SUBSTITUTIONS SUBSTITUTIONS
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~
:program:`lit` provides various patterns that can be used with the RUN command. :program:`lit` allows patterns to be substituted inside RUN commands. It also
These are defined in TestRunner.py. The base set of substitutions are: provides the following base set of substitutions, which are defined in
TestRunner.py:
======================= ============== ======================= ==============
Macro Substitution Macro Substitution
@ -453,6 +454,14 @@ Other substitutions are provided that are variations on this base set and
further substitution patterns can be defined by each test module. See the further substitution patterns can be defined by each test module. See the
modules :ref:`local-configuration-files`. modules :ref:`local-configuration-files`.
By default, substitutions are expanded exactly once, so that if e.g. a
substitution ``%build`` is defined in top of another substitution ``%cxx``,
``%build`` will expand to ``%cxx`` textually, not to what ``%cxx`` expands to.
However, if the ``recursiveExpansionLimit`` property of the ``LitConfig`` is
set to a non-negative integer, substitutions will be expanded recursively until
that limit is reached. It is an error if the limit is reached and expanding
substitutions again would yield a different result.
More detailed information on substitutions can be found in the More detailed information on substitutions can be found in the
:doc:`../TestingGuide`. :doc:`../TestingGuide`.

View File

@ -66,6 +66,19 @@ class LitConfig(object):
self.maxIndividualTestTime = maxIndividualTestTime self.maxIndividualTestTime = maxIndividualTestTime
self.parallelism_groups = parallelism_groups self.parallelism_groups = parallelism_groups
self.echo_all_commands = echo_all_commands self.echo_all_commands = echo_all_commands
self._recursiveExpansionLimit = None
@property
def recursiveExpansionLimit(self):
return self._recursiveExpansionLimit
@recursiveExpansionLimit.setter
def recursiveExpansionLimit(self, value):
if value is not None and not isinstance(value, int):
self.fatal('recursiveExpansionLimit must be either None or an integer (got <{}>)'.format(value))
if isinstance(value, int) and value < 0:
self.fatal('recursiveExpansionLimit must be a non-negative integer (got <{}>)'.format(value))
self._recursiveExpansionLimit = value
@property @property
def maxIndividualTestTime(self): def maxIndividualTestTime(self):

View File

@ -1147,10 +1147,18 @@ def _memoize(f):
def _caching_re_compile(r): def _caching_re_compile(r):
return re.compile(r) return re.compile(r)
def applySubstitutions(script, substitutions): def applySubstitutions(script, substitutions, recursion_limit=None):
"""Apply substitutions to the script. Allow full regular expression syntax. """
Apply substitutions to the script. Allow full regular expression syntax.
Replace each matching occurrence of regular expression pattern a with Replace each matching occurrence of regular expression pattern a with
substitution b in line ln.""" substitution b in line ln.
If a substitution expands into another substitution, it is expanded
recursively until the line has no more expandable substitutions. If
the line can still can be substituted after being substituted
`recursion_limit` times, it is an error. If the `recursion_limit` is
`None` (the default), no recursive substitution is performed at all.
"""
def processLine(ln): def processLine(ln):
# Apply substitutions # Apply substitutions
for a,b in substitutions: for a,b in substitutions:
@ -1167,9 +1175,28 @@ def applySubstitutions(script, substitutions):
# Strip the trailing newline and any extra whitespace. # Strip the trailing newline and any extra whitespace.
return ln.strip() return ln.strip()
def processLineToFixedPoint(ln):
assert isinstance(recursion_limit, int) and recursion_limit >= 0
origLine = ln
steps = 0
processed = processLine(ln)
while processed != ln and steps < recursion_limit:
ln = processed
processed = processLine(ln)
steps += 1
if processed != ln:
raise ValueError("Recursive substitution of '%s' did not complete "
"in the provided recursion limit (%s)" % \
(origLine, recursion_limit))
return processed
# Note Python 3 map() gives an iterator rather than a list so explicitly # Note Python 3 map() gives an iterator rather than a list so explicitly
# convert to list before returning. # convert to list before returning.
return list(map(processLine, script)) process = processLine if recursion_limit is None else processLineToFixedPoint
return list(map(process, script))
class ParserKind(object): class ParserKind(object):
@ -1506,7 +1533,8 @@ def executeShTest(test, litConfig, useExternalSh,
substitutions = list(extra_substitutions) substitutions = list(extra_substitutions)
substitutions += getDefaultSubstitutions(test, tmpDir, tmpBase, substitutions += getDefaultSubstitutions(test, tmpDir, tmpBase,
normalize_slashes=useExternalSh) normalize_slashes=useExternalSh)
script = applySubstitutions(script, substitutions) script = applySubstitutions(script, substitutions,
recursion_limit=litConfig.recursiveExpansionLimit)
# Re-run failed tests up to test.allowed_retries times. # Re-run failed tests up to test.allowed_retries times.
attempts = test.allowed_retries + 1 attempts = test.allowed_retries + 1

View File

@ -0,0 +1,10 @@
import lit.formats
config.name = 'does-not-substitute-no-limit'
config.suffixes = ['.py']
config.test_format = lit.formats.ShTest()
config.test_source_root = None
config.test_exec_root = None
config.substitutions = [("%rec1", "STOP"), ("%rec2", "%rec1"),
("%rec3", "%rec2"), ("%rec4", "%rec3"),
("%rec5", "%rec4")]

View File

@ -0,0 +1,12 @@
import lit.formats
config.name = 'does-not-substitute-within-limit'
config.suffixes = ['.py']
config.test_format = lit.formats.ShTest()
config.test_source_root = None
config.test_exec_root = None
config.substitutions = [("%rec1", "STOP"), ("%rec2", "%rec1"),
("%rec3", "%rec2"), ("%rec4", "%rec3"),
("%rec5", "%rec4")]
lit_config.recursiveExpansionLimit = 2

View File

@ -0,0 +1,8 @@
import lit.formats
config.name = 'negative-integer'
config.suffixes = ['.py']
config.test_format = lit.formats.ShTest()
config.test_source_root = None
config.test_exec_root = None
lit_config.recursiveExpansionLimit = -4

View File

@ -0,0 +1,8 @@
import lit.formats
config.name = 'not-an-integer'
config.suffixes = ['.py']
config.test_format = lit.formats.ShTest()
config.test_source_root = None
config.test_exec_root = None
lit_config.recursiveExpansionLimit = "not-an-integer"

View File

@ -0,0 +1,8 @@
import lit.formats
config.name = 'set-to-none'
config.suffixes = ['.py']
config.test_format = lit.formats.ShTest()
config.test_source_root = None
config.test_exec_root = None
lit_config.recursiveExpansionLimit = None

View File

@ -0,0 +1 @@
# RUN: true

View File

@ -0,0 +1,12 @@
import lit.formats
config.name = 'substitutes-within-limit'
config.suffixes = ['.py']
config.test_format = lit.formats.ShTest()
config.test_source_root = None
config.test_exec_root = None
config.substitutions = [("%rec1", "STOP"), ("%rec2", "%rec1"),
("%rec3", "%rec2"), ("%rec4", "%rec3"),
("%rec5", "%rec4")]
lit_config.recursiveExpansionLimit = 5

View File

@ -0,0 +1,23 @@
# Check that the config.recursiveExpansionLimit is picked up and will cause
# lit substitutions to be expanded recursively.
# RUN: %{lit} -j 1 %{inputs}/shtest-recursive-substitution/substitutes-within-limit --show-all | FileCheck --check-prefix=CHECK-TEST1 %s
# CHECK-TEST1: PASS: substitutes-within-limit :: test.py
# CHECK-TEST1: $ "echo" "STOP"
# RUN: not %{lit} -j 1 %{inputs}/shtest-recursive-substitution/does-not-substitute-within-limit --show-all | FileCheck --check-prefix=CHECK-TEST2 %s
# CHECK-TEST2: UNRESOLVED: does-not-substitute-within-limit :: test.py
# CHECK-TEST2: ValueError: Recursive substitution of
# RUN: %{lit} -j 1 %{inputs}/shtest-recursive-substitution/does-not-substitute-no-limit --show-all | FileCheck --check-prefix=CHECK-TEST3 %s
# CHECK-TEST3: PASS: does-not-substitute-no-limit :: test.py
# CHECK-TEST3: $ "echo" "%rec4"
# RUN: not %{lit} -j 1 %{inputs}/shtest-recursive-substitution/not-an-integer --show-all 2>&1 | FileCheck --check-prefix=CHECK-TEST4 %s
# CHECK-TEST4: recursiveExpansionLimit must be either None or an integer
# RUN: not %{lit} -j 1 %{inputs}/shtest-recursive-substitution/negative-integer --show-all 2>&1 | FileCheck --check-prefix=CHECK-TEST5 %s
# CHECK-TEST5: recursiveExpansionLimit must be a non-negative integer
# RUN: %{lit} -j 1 %{inputs}/shtest-recursive-substitution/set-to-none --show-all | FileCheck --check-prefix=CHECK-TEST6 %s
# CHECK-TEST6: PASS: set-to-none :: test.py

View File

@ -199,6 +199,74 @@ class TestIntegratedTestKeywordParser(unittest.TestCase):
except BaseException as e: except BaseException as e:
self.fail("CUSTOM_NO_PARSER: raised the wrong exception: %r" % e) self.fail("CUSTOM_NO_PARSER: raised the wrong exception: %r" % e)
class TestApplySubtitutions(unittest.TestCase):
def test_simple(self):
script = ["echo %bar"]
substitutions = [("%bar", "hello")]
result = lit.TestRunner.applySubstitutions(script, substitutions)
self.assertEqual(result, ["echo hello"])
def test_multiple_substitutions(self):
script = ["echo %bar %baz"]
substitutions = [("%bar", "hello"),
("%baz", "world"),
("%useless", "shouldnt expand")]
result = lit.TestRunner.applySubstitutions(script, substitutions)
self.assertEqual(result, ["echo hello world"])
def test_multiple_script_lines(self):
script = ["%cxx %compile_flags -c -o %t.o",
"%cxx %link_flags %t.o -o %t.exe"]
substitutions = [("%cxx", "clang++"),
("%compile_flags", "-std=c++11 -O3"),
("%link_flags", "-lc++")]
result = lit.TestRunner.applySubstitutions(script, substitutions)
self.assertEqual(result, ["clang++ -std=c++11 -O3 -c -o %t.o",
"clang++ -lc++ %t.o -o %t.exe"])
def test_recursive_substitution_real(self):
script = ["%build %s"]
substitutions = [("%cxx", "clang++"),
("%compile_flags", "-std=c++11 -O3"),
("%link_flags", "-lc++"),
("%build", "%cxx %compile_flags %link_flags %s -o %t.exe")]
result = lit.TestRunner.applySubstitutions(script, substitutions, recursion_limit=3)
self.assertEqual(result, ["clang++ -std=c++11 -O3 -lc++ %s -o %t.exe %s"])
def test_recursive_substitution_limit(self):
script = ["%rec5"]
# Make sure the substitutions are not in an order where the global
# substitution would appear to be recursive just because they are
# processed in the right order.
substitutions = [("%rec1", "STOP"), ("%rec2", "%rec1"),
("%rec3", "%rec2"), ("%rec4", "%rec3"), ("%rec5", "%rec4")]
for limit in [5, 6, 7]:
result = lit.TestRunner.applySubstitutions(script, substitutions, recursion_limit=limit)
self.assertEqual(result, ["STOP"])
def test_recursive_substitution_limit_exceeded(self):
script = ["%rec5"]
substitutions = [("%rec1", "STOP"), ("%rec2", "%rec1"),
("%rec3", "%rec2"), ("%rec4", "%rec3"), ("%rec5", "%rec4")]
for limit in [0, 1, 2, 3, 4]:
try:
lit.TestRunner.applySubstitutions(script, substitutions, recursion_limit=limit)
self.fail("applySubstitutions should have raised an exception")
except ValueError:
pass
def test_recursive_substitution_invalid_value(self):
script = ["%rec5"]
substitutions = [("%rec1", "STOP"), ("%rec2", "%rec1"),
("%rec3", "%rec2"), ("%rec4", "%rec3"), ("%rec5", "%rec4")]
for limit in [-1, -2, -3, "foo"]:
try:
lit.TestRunner.applySubstitutions(script, substitutions, recursion_limit=limit)
self.fail("applySubstitutions should have raised an exception")
except AssertionError:
pass
if __name__ == '__main__': if __name__ == '__main__':
TestIntegratedTestKeywordParser.load_keyword_parser_lit_tests() TestIntegratedTestKeywordParser.load_keyword_parser_lit_tests()
unittest.main(verbosity=2) unittest.main(verbosity=2)