Skip to content

Commit f039cae

Browse files
committed
[libc++] Add support for generated tests in the libc++ test format
A recurring problem recently has been that libc++ has several generated tests which all need to be re-generated before committing a change. This creates noise during code reviews and friction for contributors. Furthermore, the way we generated most of these tests resulted in extremely bad compilation times when using modules, because we defined a macro before compiling each file. This commit introduces a new kind of test called a '.gen' test. These tests are normal shell tests, however the Lit test format will run the test to discover the actual Lit tests it should run. This basically allows generating a Lit test suite on the fly using arbitrary code, which can be used in the future to generate tests like our __verbose_abort tests and several others. Differential Revision: https://reviews.llvm.org/D151258
1 parent 65c7893 commit f039cae

File tree

5 files changed

+126
-6
lines changed

5 files changed

+126
-6
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// Make sure we can generate no tests at all
10+
11+
// RUN: true
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// Make sure we can generate one test
10+
11+
// RUN: echo "//--- test1.compile.pass.cpp"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// Make sure we can generate two tests
10+
11+
// RUN: echo "//--- test1.compile.pass.cpp"
12+
// RUN: echo "//--- test2.compile.pass.cpp"

libcxx/utils/libcxx/test/dsl.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,7 @@ def programOutput(config, program, args=None):
179179
"Failed to run program, cmd:\n{}\nstderr is:\n{}".format(runcmd, err)
180180
)
181181

182-
actualOut = re.search("# command output:\n(.+)\n$", out, flags=re.DOTALL)
183-
actualOut = actualOut.group(1) if actualOut else ""
184-
return actualOut
182+
return libcxx.test.format._parseLitOutput(out)
185183

186184

187185
@_memoizeExpensiveOperation(

libcxx/utils/libcxx/test/format.py

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#
77
# ===----------------------------------------------------------------------===##
88

9+
import contextlib
10+
import io
911
import lit
1012
import lit.formats
1113
import os
@@ -33,6 +35,38 @@ def _checkBaseSubstitutions(substitutions):
3335
for s in ["%{cxx}", "%{compile_flags}", "%{link_flags}", "%{flags}", "%{exec}"]:
3436
assert s in substitutions, "Required substitution {} was not provided".format(s)
3537

38+
def _parseLitOutput(fullOutput):
39+
"""
40+
Parse output of a Lit ShTest to extract the actual output of the contained commands.
41+
42+
This takes output of the form
43+
44+
$ ":" "RUN: at line 11"
45+
$ "echo" "OUTPUT1"
46+
# command output:
47+
OUTPUT1
48+
49+
$ ":" "RUN: at line 12"
50+
$ "echo" "OUTPUT2"
51+
# command output:
52+
OUTPUT2
53+
54+
and returns a string containing
55+
56+
OUTPUT1
57+
OUTPUT2
58+
59+
as-if the commands had been run directly. This is a workaround for the fact
60+
that Lit doesn't let us execute ShTest and retrieve the raw output without
61+
injecting additional Lit output around it.
62+
"""
63+
parsed = ''
64+
for output in re.split('[$]\s*":"\s*"RUN: at line \d+"', fullOutput):
65+
if output: # skip blank lines
66+
commandOutput = re.search("# command output:\n(.+)\n$", output, flags=re.DOTALL)
67+
if commandOutput:
68+
parsed += commandOutput.group(1)
69+
return parsed
3670

3771
def _executeScriptInternal(test, litConfig, commands):
3872
"""
@@ -170,6 +204,16 @@ class CxxStandardLibraryTest(lit.formats.TestFormat):
170204
171205
FOO.sh.<anything> - A builtin Lit Shell test
172206
207+
FOO.gen.<anything> - A .sh test that generates one or more Lit tests on the
208+
fly. Executing this test must generate one or more files
209+
as expected by LLVM split-file, and each generated file
210+
leads to a separate Lit test that runs that file as
211+
defined by the test format. This can be used to generate
212+
multiple Lit tests from a single source file, which is
213+
useful for testing repetitive properties in the library.
214+
Be careful not to abuse this since this is not a replacement
215+
for usual code reuse techniques.
216+
173217
FOO.verify.cpp - Compiles with clang-verify. This type of test is
174218
automatically marked as UNSUPPORTED if the compiler
175219
does not support Clang-verify.
@@ -245,6 +289,7 @@ def getTestsInDirectory(self, testSuite, pathInSuite, litConfig, localConfig):
245289
"[.]link[.]pass[.]mm$",
246290
"[.]link[.]fail[.]cpp$",
247291
"[.]sh[.][^.]+$",
292+
"[.]gen[.][^.]+$",
248293
"[.]verify[.]cpp$",
249294
"[.]fail[.]cpp$",
250295
]
@@ -257,9 +302,13 @@ def getTestsInDirectory(self, testSuite, pathInSuite, litConfig, localConfig):
257302
filepath = os.path.join(sourcePath, filename)
258303
if not os.path.isdir(filepath):
259304
if any([re.search(ext, filename) for ext in SUPPORTED_SUFFIXES]):
260-
yield lit.Test.Test(
261-
testSuite, pathInSuite + (filename,), localConfig
262-
)
305+
# If this is a generated test, run the generation step and add
306+
# as many Lit tests as necessary.
307+
if re.search('[.]gen[.][^.]+$', filename):
308+
for test in self._generateGenTest(testSuite, pathInSuite + (filename,), litConfig, localConfig):
309+
yield test
310+
else:
311+
yield lit.Test.Test(testSuite, pathInSuite + (filename,), localConfig)
263312

264313
def execute(self, test, litConfig):
265314
VERIFY_FLAGS = (
@@ -356,3 +405,42 @@ def _executeShTest(self, test, litConfig, steps):
356405
return lit.TestRunner._runShTest(
357406
test, litConfig, useExternalSh, script, tmpBase
358407
)
408+
409+
def _generateGenTest(self, testSuite, pathInSuite, litConfig, localConfig):
410+
generator = lit.Test.Test(testSuite, pathInSuite, localConfig)
411+
412+
# Make sure we have a directory to execute the generator test in
413+
generatorExecDir = os.path.dirname(testSuite.getExecPath(pathInSuite))
414+
os.makedirs(generatorExecDir, exist_ok=True)
415+
416+
# Run the generator test
417+
steps = [] # Steps must already be in the script
418+
(out, err, exitCode, _, _) = _executeScriptInternal(generator, litConfig, steps)
419+
if exitCode != 0:
420+
raise RuntimeError(f"Error while trying to generate gen test\nstdout:\n{out}\n\nstderr:\n{err}")
421+
422+
# Split the generated output into multiple files and generate one test for each file
423+
parsed = _parseLitOutput(out)
424+
for (subfile, content) in self._splitFile(parsed):
425+
generatedFile = testSuite.getExecPath(pathInSuite + (subfile, ))
426+
os.makedirs(os.path.dirname(generatedFile), exist_ok=True)
427+
with open(generatedFile, 'w') as f:
428+
f.write(content)
429+
yield lit.Test.Test(testSuite, (generatedFile,), localConfig)
430+
431+
def _splitFile(self, input):
432+
DELIM = r'^(//|#)---(.+)'
433+
lines = input.splitlines()
434+
currentFile = None
435+
thisFileContent = []
436+
for line in lines:
437+
match = re.match(DELIM, line)
438+
if match:
439+
if currentFile is not None:
440+
yield (currentFile, '\n'.join(thisFileContent))
441+
currentFile = match.group(2).strip()
442+
thisFileContent = []
443+
assert currentFile is not None, f"Some input to split-file doesn't belong to any file, input was:\n{input}"
444+
thisFileContent.append(line)
445+
if currentFile is not None:
446+
yield (currentFile, '\n'.join(thisFileContent))

0 commit comments

Comments
 (0)