[libcxx-commits] [libcxx] [libcxxabi] [libunwind] [llvm] [libc++][WIP] Move the libc++ test format to Lit (PR #90803)

Louis Dionne via libcxx-commits libcxx-commits at lists.llvm.org
Mon May 6 06:37:06 PDT 2024


https://github.com/ldionne updated https://github.com/llvm/llvm-project/pull/90803

>From ecab1e5689f9f7ea6f87d9cc8c51910b733f6859 Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Wed, 1 May 2024 18:10:07 -0600
Subject: [PATCH] [libc++][WIP] Move the libc++ test format to Lit

This allows the generally-useful parts of the test format to be
shipped alongside Lit, which would make it usable for third-party
developers as well. This came up at C++Now in the context of the
Beman project where we'd like to set up a non-LLVM project that can
use the same testing framework as libc++.

With the test format moved to Lit, the format would be available
after installing Lit with pip, which is really convenient.
---
 libcxx/test/configs/cmake-bridge.cfg.in       |   2 +-
 libcxx/utils/libcxx/test/dsl.py               |   6 +-
 libcxx/utils/libcxx/test/format.py            | 419 +++---------------
 libcxxabi/test/configs/cmake-bridge.cfg.in    |   2 +-
 libunwind/test/configs/cmake-bridge.cfg.in    |   2 +-
 llvm/utils/lit/lit/formats/__init__.py        |   1 +
 .../lit/lit/formats/standardlibrarytest.py    | 355 +++++++++++++++
 7 files changed, 421 insertions(+), 366 deletions(-)
 create mode 100644 llvm/utils/lit/lit/formats/standardlibrarytest.py

diff --git a/libcxx/test/configs/cmake-bridge.cfg.in b/libcxx/test/configs/cmake-bridge.cfg.in
index 84b3270a8940ac..b220e5ebcafb17 100644
--- a/libcxx/test/configs/cmake-bridge.cfg.in
+++ b/libcxx/test/configs/cmake-bridge.cfg.in
@@ -18,7 +18,7 @@ import libcxx.test.format
 # Basic configuration of the test suite
 config.name = os.path.basename('@LIBCXX_TEST_CONFIG@')
 config.test_source_root = os.path.join('@LIBCXX_SOURCE_DIR@', 'test')
-config.test_format = libcxx.test.format.CxxStandardLibraryTest()
+config.test_format = libcxx.test.format.LibcxxTest()
 config.recursiveExpansionLimit = 10
 config.test_exec_root = os.path.join('@CMAKE_BINARY_DIR@', 'test')
 
diff --git a/libcxx/utils/libcxx/test/dsl.py b/libcxx/utils/libcxx/test/dsl.py
index 387862ae6f496d..6177dc9dccd327 100644
--- a/libcxx/utils/libcxx/test/dsl.py
+++ b/libcxx/utils/libcxx/test/dsl.py
@@ -99,7 +99,7 @@ def _executeWithFakeConfig(test, commands):
         order="smart",
         params={},
     )
-    return libcxx.test.format._executeScriptInternal(test, litConfig, commands)
+    return lit.formats.standardlibrarytest._executeScriptInternal(test, litConfig, commands)
 
 
 def _makeConfigTest(config):
@@ -121,12 +121,12 @@ def _makeConfigTest(config):
 
     class TestWrapper(lit.Test.Test):
         def __enter__(self):
-            testDir, _ = libcxx.test.format._getTempPaths(self)
+            testDir, _ = lit.formats.standardlibrarytest._getTempPaths(self)
             os.makedirs(testDir)
             return self
 
         def __exit__(self, *args):
-            testDir, _ = libcxx.test.format._getTempPaths(self)
+            testDir, _ = lit.formats.standardlibrarytest._getTempPaths(self)
             shutil.rmtree(testDir)
             os.remove(tmp.name)
 
diff --git a/libcxx/utils/libcxx/test/format.py b/libcxx/utils/libcxx/test/format.py
index 7e5281c0b74064..a670f57f67965d 100644
--- a/libcxx/utils/libcxx/test/format.py
+++ b/libcxx/utils/libcxx/test/format.py
@@ -7,50 +7,8 @@
 # ===----------------------------------------------------------------------===##
 
 import lit
-import libcxx.test.config as config
 import lit.formats
 import os
-import re
-
-
-def _getTempPaths(test):
-    """
-    Return the values to use for the %T and %t substitutions, respectively.
-
-    The difference between this and Lit's default behavior is that we guarantee
-    that %T is a path unique to the test being run.
-    """
-    tmpDir, _ = lit.TestRunner.getTempPaths(test)
-    _, testName = os.path.split(test.getExecPath())
-    tmpDir = os.path.join(tmpDir, testName + ".dir")
-    tmpBase = os.path.join(tmpDir, "t")
-    return tmpDir, tmpBase
-
-
-def _checkBaseSubstitutions(substitutions):
-    substitutions = [s for (s, _) in substitutions]
-    for s in ["%{cxx}", "%{compile_flags}", "%{link_flags}", "%{flags}", "%{exec}"]:
-        assert s in substitutions, "Required substitution {} was not provided".format(s)
-
-def _executeScriptInternal(test, litConfig, commands):
-    """
-    Returns (stdout, stderr, exitCode, timeoutInfo, parsedCommands)
-
-    TODO: This really should be easier to access from Lit itself
-    """
-    parsedCommands = parseScript(test, preamble=commands)
-
-    _, tmpBase = _getTempPaths(test)
-    execDir = os.path.dirname(test.getExecPath())
-    try:
-        res = lit.TestRunner.executeScriptInternal(
-            test, litConfig, tmpBase, parsedCommands, execDir, debug=False
-        )
-    except lit.TestRunner.ScriptFatal as e:
-        res = ("", str(e), 127, None)
-    (out, err, exitCode, timeoutInfo) = res
-
-    return (out, err, exitCode, timeoutInfo, parsedCommands)
 
 
 def _validateModuleDependencies(modules):
@@ -61,334 +19,75 @@ def _validateModuleDependencies(modules):
             )
 
 
-def parseScript(test, preamble):
-    """
-    Extract the script from a test, with substitutions applied.
-
-    Returns a list of commands ready to be executed.
-
-    - test
-        The lit.Test to parse.
-
-    - preamble
-        A list of commands to perform before any command in the test.
-        These commands can contain unexpanded substitutions, but they
-        must not be of the form 'RUN:' -- they must be proper commands
-        once substituted.
-    """
-    # Get the default substitutions
-    tmpDir, tmpBase = _getTempPaths(test)
+def _buildModule(test, litConfig, commands):
+    tmpDir, tmpBase = lit.formats.standardlibrarytest._getTempPaths(test)
+    execDir = os.path.dirname(test.getExecPath())
     substitutions = lit.TestRunner.getDefaultSubstitutions(test, tmpDir, tmpBase)
 
-    # Check base substitutions and add the %{build}, %{verify} and %{run} convenience substitutions
-    #
-    # Note: We use -Wno-error with %{verify} to make sure that we don't treat all diagnostics as
-    #       errors, which doesn't make sense for clang-verify tests because we may want to check
-    #       for specific warning diagnostics.
-    _checkBaseSubstitutions(substitutions)
-    substitutions.append(
-        ("%{build}", "%{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe")
-    )
-    substitutions.append(
-        (
-            "%{verify}",
-            "%{cxx} %s %{flags} %{compile_flags} -fsyntax-only -Wno-error -Xclang -verify -Xclang -verify-ignore-unexpected=note -ferror-limit=0",
-        )
-    )
-    substitutions.append(("%{run}", "%{exec} %t.exe"))
-
-    # Parse the test file, including custom directives
-    additionalCompileFlags = []
-    fileDependencies = []
-    modules = []  # The enabled modules
-    moduleCompileFlags = []  # The compilation flags to use modules
-    parsers = [
-        lit.TestRunner.IntegratedTestKeywordParser(
-            "FILE_DEPENDENCIES:",
-            lit.TestRunner.ParserKind.LIST,
-            initial_value=fileDependencies,
-        ),
-        lit.TestRunner.IntegratedTestKeywordParser(
-            "ADDITIONAL_COMPILE_FLAGS:",
-            lit.TestRunner.ParserKind.SPACE_LIST,
-            initial_value=additionalCompileFlags,
-        ),
-        lit.TestRunner.IntegratedTestKeywordParser(
-            "MODULE_DEPENDENCIES:",
-            lit.TestRunner.ParserKind.SPACE_LIST,
-            initial_value=modules,
-        ),
-    ]
-
-    # Add conditional parsers for ADDITIONAL_COMPILE_FLAGS. This should be replaced by first
-    # class support for conditional keywords in Lit, which would allow evaluating arbitrary
-    # Lit boolean expressions instead.
-    for feature in test.config.available_features:
-        parser = lit.TestRunner.IntegratedTestKeywordParser(
-            "ADDITIONAL_COMPILE_FLAGS({}):".format(feature),
-            lit.TestRunner.ParserKind.SPACE_LIST,
-            initial_value=additionalCompileFlags,
-        )
-        parsers.append(parser)
-
-    scriptInTest = lit.TestRunner.parseIntegratedTestScript(
-        test, additional_parsers=parsers, require_script=not preamble
-    )
-    if isinstance(scriptInTest, lit.Test.Result):
-        return scriptInTest
-
-    script = []
-
-    # For each file dependency in FILE_DEPENDENCIES, inject a command to copy
-    # that file to the execution directory. Execute the copy from %S to allow
-    # relative paths from the test directory.
-    for dep in fileDependencies:
-        script += ["%dbg(SETUP) cd %S && cp {} %T".format(dep)]
-    script += preamble
-    script += scriptInTest
-
-    # Add compile flags specified with ADDITIONAL_COMPILE_FLAGS.
-    # Modules need to be built with the same compilation flags as the
-    # test. So add these flags before adding the modules.
-    substitutions = config._appendToSubstitution(
-        substitutions, "%{compile_flags}", " ".join(additionalCompileFlags)
+    substituted = lit.TestRunner.applySubstitutions(
+        commands, substitutions, recursion_limit=test.config.recursiveExpansionLimit
     )
-
-    if modules:
-        _validateModuleDependencies(modules)
-
-        # The moduleCompileFlags are added to the %{compile_flags}, but
-        # the modules need to be built without these flags. So expand the
-        # %{compile_flags} eagerly and hardcode them in the build script.
-        compileFlags = config._getSubstitution("%{compile_flags}", test.config)
-
-        # Building the modules needs to happen before the other script
-        # commands are executed. Therefore the commands are added to the
-        # front of the list.
-        if "std.compat" in modules:
-            script.insert(
-                0,
-                "%dbg(MODULE std.compat) %{cxx} %{flags} "
-                f"{compileFlags} "
-                "-Wno-reserved-module-identifier -Wno-reserved-user-defined-literal "
-                "-fmodule-file=std=%T/std.pcm " # The std.compat module imports std.
-                "--precompile -o %T/std.compat.pcm -c %{module-dir}/std.compat.cppm",
-            )
-            moduleCompileFlags.extend(
-                ["-fmodule-file=std.compat=%T/std.compat.pcm", "%T/std.compat.pcm"]
-            )
-
-        # Make sure the std module is built before std.compat. Libc++'s
-        # std.compat module depends on the std module. It is not
-        # known whether the compiler expects the modules in the order of
-        # their dependencies. However it's trivial to provide them in
-        # that order.
-        script.insert(
-            0,
-            "%dbg(MODULE std) %{cxx} %{flags} "
-            f"{compileFlags} "
-            "-Wno-reserved-module-identifier -Wno-reserved-user-defined-literal "
-            "--precompile -o %T/std.pcm -c %{module-dir}/std.cppm",
+    (out, err, exitCode, _) = lit.TestRunner.executeScriptInternal(test, litConfig, tmpBase, substituted, execDir)
+    if exitCode != 0:
+        return lit.Test.Result(
+            lit.Test.UNRESOLVED, "Failed to build module std for '{}':\n{}\n{}".format(test.getFilePath(), out, err)
         )
-        moduleCompileFlags.extend(["-fmodule-file=std=%T/std.pcm", "%T/std.pcm"])
 
-        # Add compile flags required for the modules.
-        substitutions = config._appendToSubstitution(
-            substitutions, "%{compile_flags}", " ".join(moduleCompileFlags)
-        )
-
-    # Perform substitutions in the script itself.
-    script = lit.TestRunner.applySubstitutions(
-        script, substitutions, recursion_limit=test.config.recursiveExpansionLimit
-    )
-
-    return script
-
-
-class CxxStandardLibraryTest(lit.formats.FileBasedTest):
-    """
-    Lit test format for the C++ Standard Library conformance test suite.
-
-    Lit tests are contained in files that follow a certain pattern, which determines the semantics of the test.
-    Under the hood, we basically generate a builtin Lit shell test that follows the ShTest format, and perform
-    the appropriate operations (compile/link/run). See
-    https://libcxx.llvm.org/TestingLibcxx.html#test-names
-    for a complete description of those semantics.
-
-    Substitution requirements
-    ===============================
-    The test format operates by assuming that each test's configuration provides
-    the following substitutions, which it will reuse in the shell scripts it
-    constructs:
-        %{cxx}           - A command that can be used to invoke the compiler
-        %{compile_flags} - Flags to use when compiling a test case
-        %{link_flags}    - Flags to use when linking a test case
-        %{flags}         - Flags to use either when compiling or linking a test case
-        %{exec}          - A command to prefix the execution of executables
-
-    Note that when building an executable (as opposed to only compiling a source
-    file), all three of %{flags}, %{compile_flags} and %{link_flags} will be used
-    in the same command line. In other words, the test format doesn't perform
-    separate compilation and linking steps in this case.
-
-    Additional provided substitutions and features
-    ==============================================
-    The test format will define the following substitutions for use inside tests:
-
-        %{build}
-            Expands to a command-line that builds the current source
-            file with the %{flags}, %{compile_flags} and %{link_flags}
-            substitutions, and that produces an executable named %t.exe.
-
-        %{verify}
-            Expands to a command-line that builds the current source
-            file with the %{flags} and %{compile_flags} substitutions
-            and enables clang-verify. This can be used to write .sh.cpp
-            tests that use clang-verify. Note that this substitution can
-            only be used when the 'verify-support' feature is available.
-
-        %{run}
-            Equivalent to `%{exec} %t.exe`. This is intended to be used
-            in conjunction with the %{build} substitution.
-    """
-
-    def getTestsForPath(self, testSuite, pathInSuite, litConfig, localConfig):
-        SUPPORTED_SUFFIXES = [
-            "[.]pass[.]cpp$",
-            "[.]pass[.]mm$",
-            "[.]compile[.]pass[.]cpp$",
-            "[.]compile[.]pass[.]mm$",
-            "[.]compile[.]fail[.]cpp$",
-            "[.]link[.]pass[.]cpp$",
-            "[.]link[.]pass[.]mm$",
-            "[.]link[.]fail[.]cpp$",
-            "[.]sh[.][^.]+$",
-            "[.]gen[.][^.]+$",
-            "[.]verify[.]cpp$",
-        ]
-
-        sourcePath = testSuite.getSourcePath(pathInSuite)
-        filename = os.path.basename(sourcePath)
-
-        # Ignore dot files, excluded tests and tests with an unsupported suffix
-        hasSupportedSuffix = lambda f: any([re.search(ext, f) for ext in SUPPORTED_SUFFIXES])
-        if filename.startswith(".") or filename in localConfig.excludes or not hasSupportedSuffix(filename):
-            return
-
-        # If this is a generated test, run the generation step and add
-        # as many Lit tests as necessary.
-        if re.search('[.]gen[.][^.]+$', filename):
-            for test in self._generateGenTest(testSuite, pathInSuite, litConfig, localConfig):
-                yield test
-        else:
-            yield lit.Test.Test(testSuite, pathInSuite, localConfig)
 
+class LibcxxTest(lit.formats.StandardLibraryTest):
     def execute(self, test, litConfig):
-        supportsVerify = "verify-support" in test.config.available_features
-        filename = test.path_in_suite[-1]
-
-        if re.search("[.]sh[.][^.]+$", filename):
-            steps = []  # The steps are already in the script
-            return self._executeShTest(test, litConfig, steps)
-        elif filename.endswith(".compile.pass.cpp") or filename.endswith(
-            ".compile.pass.mm"
-        ):
-            steps = [
-                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} -fsyntax-only"
-            ]
-            return self._executeShTest(test, litConfig, steps)
-        elif filename.endswith(".compile.fail.cpp"):
-            steps = [
-                "%dbg(COMPILED WITH) ! %{cxx} %s %{flags} %{compile_flags} -fsyntax-only"
-            ]
-            return self._executeShTest(test, litConfig, steps)
-        elif filename.endswith(".link.pass.cpp") or filename.endswith(".link.pass.mm"):
-            steps = [
-                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe"
-            ]
-            return self._executeShTest(test, litConfig, steps)
-        elif filename.endswith(".link.fail.cpp"):
-            steps = [
-                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} -c -o %t.o",
-                "%dbg(LINKED WITH) ! %{cxx} %t.o %{flags} %{link_flags} -o %t.exe",
-            ]
-            return self._executeShTest(test, litConfig, steps)
-        elif filename.endswith(".verify.cpp"):
-            if not supportsVerify:
-                return lit.Test.Result(
-                    lit.Test.UNSUPPORTED,
-                    "Test {} requires support for Clang-verify, which isn't supported by the compiler".format(
-                        test.getFullName()
-                    ),
-                )
-            steps = ["%dbg(COMPILED WITH) %{verify}"]
-            return self._executeShTest(test, litConfig, steps)
-        # Make sure to check these ones last, since they will match other
-        # suffixes above too.
-        elif filename.endswith(".pass.cpp") or filename.endswith(".pass.mm"):
-            steps = [
-                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe",
-                "%dbg(EXECUTED AS) %{exec} %t.exe",
-            ]
-            return self._executeShTest(test, litConfig, steps)
-        else:
-            return lit.Test.Result(
-                lit.Test.UNRESOLVED, "Unknown test suffix for '{}'".format(filename)
-            )
-
-    def _executeShTest(self, test, litConfig, steps):
         if test.config.unsupported:
             return lit.Test.Result(lit.Test.UNSUPPORTED, "Test is unsupported")
 
-        script = parseScript(test, steps)
-        if isinstance(script, lit.Test.Result):
-            return script
-
-        if litConfig.noExecute:
-            return lit.Test.Result(
-                lit.Test.XFAIL if test.isExpectedToFail() else lit.Test.PASS
+        # Parse any MODULE_DEPENDENCIES in the test file.
+        modules = []
+        parsers = [
+            lit.TestRunner.IntegratedTestKeywordParser(
+                "MODULE_DEPENDENCIES:",
+                lit.TestRunner.ParserKind.SPACE_LIST,
+                initial_value=modules,
             )
-        else:
-            _, tmpBase = _getTempPaths(test)
-            useExternalSh = False
-            return lit.TestRunner._runShTest(
-                test, litConfig, useExternalSh, script, tmpBase
+        ]
+        lit.TestRunner.parseIntegratedTestScript(test, additional_parsers=parsers, require_script=False)
+
+        # Build the modules if needed and tweak the compiler flags of the rest of the test so
+        # it knows about the just-built modules.
+        moduleCompileFlags = []
+        if modules:
+            _validateModuleDependencies(modules)
+
+            # Make sure the std module is built before std.compat. Libc++'s
+            # std.compat module depends on the std module. It is not
+            # known whether the compiler expects the modules in the order of
+            # their dependencies. However it's trivial to provide them in
+            # that order.
+            commands = [
+                "mkdir -p %T",
+                "%dbg(MODULE std) %{cxx} %{flags} %{compile_flags} "
+                "-Wno-reserved-module-identifier -Wno-reserved-user-defined-literal "
+                "--precompile -o %T/std.pcm -c %{module-dir}/std.cppm",
+            ]
+            res = _buildModule(test, litConfig, commands)
+            if isinstance(res, lit.Test.Result):
+                return res
+            moduleCompileFlags.extend(["-fmodule-file=std=%T/std.pcm", "%T/std.pcm"])
+
+            if "std.compat" in modules:
+                commands = [
+                    "mkdir -p %T",
+                    "%dbg(MODULE std.compat) %{cxx} %{flags} %{compile_flags} "
+                    "-Wno-reserved-module-identifier -Wno-reserved-user-defined-literal "
+                    "-fmodule-file=std=%T/std.pcm " # The std.compat module imports std.
+                    "--precompile -o %T/std.compat.pcm -c %{module-dir}/std.compat.cppm",
+                ]
+                res = _buildModule(test, litConfig, commands)
+                if isinstance(res, lit.Test.Result):
+                    return res
+                moduleCompileFlags.extend(["-fmodule-file=std.compat=%T/std.compat.pcm", "%T/std.compat.pcm"])
+
+            # Add compile flags required for the test to use the just-built modules
+            test.config.substitutions = lit.formats.standardlibrarytest._appendToSubstitution(
+                test.config.substitutions, "%{compile_flags}", " ".join(moduleCompileFlags)
             )
 
-    def _generateGenTest(self, testSuite, pathInSuite, litConfig, localConfig):
-        generator = lit.Test.Test(testSuite, pathInSuite, localConfig)
-
-        # Make sure we have a directory to execute the generator test in
-        generatorExecDir = os.path.dirname(testSuite.getExecPath(pathInSuite))
-        os.makedirs(generatorExecDir, exist_ok=True)
-
-        # Run the generator test
-        steps = [] # Steps must already be in the script
-        (out, err, exitCode, _, _) = _executeScriptInternal(generator, litConfig, steps)
-        if exitCode != 0:
-            raise RuntimeError(f"Error while trying to generate gen test\nstdout:\n{out}\n\nstderr:\n{err}")
-
-        # Split the generated output into multiple files and generate one test for each file
-        for subfile, content in self._splitFile(out):
-            generatedFile = testSuite.getExecPath(pathInSuite + (subfile,))
-            os.makedirs(os.path.dirname(generatedFile), exist_ok=True)
-            with open(generatedFile, 'w') as f:
-                f.write(content)
-            yield lit.Test.Test(testSuite, (generatedFile,), localConfig)
-
-    def _splitFile(self, input):
-        DELIM = r'^(//|#)---(.+)'
-        lines = input.splitlines()
-        currentFile = None
-        thisFileContent = []
-        for line in lines:
-            match = re.match(DELIM, line)
-            if match:
-                if currentFile is not None:
-                    yield (currentFile, '\n'.join(thisFileContent))
-                currentFile = match.group(2).strip()
-                thisFileContent = []
-            assert currentFile is not None, f"Some input to split-file doesn't belong to any file, input was:\n{input}"
-            thisFileContent.append(line)
-        if currentFile is not None:
-            yield (currentFile, '\n'.join(thisFileContent))
+        return super().execute(test, litConfig)
diff --git a/libcxxabi/test/configs/cmake-bridge.cfg.in b/libcxxabi/test/configs/cmake-bridge.cfg.in
index 1d0f51d37437bd..634fd26a54ea6d 100644
--- a/libcxxabi/test/configs/cmake-bridge.cfg.in
+++ b/libcxxabi/test/configs/cmake-bridge.cfg.in
@@ -19,7 +19,7 @@ import libcxx.test.format
 # Basic configuration of the test suite
 config.name = os.path.basename('@LIBCXXABI_TEST_CONFIG@')
 config.test_source_root = os.path.join('@LIBCXXABI_SOURCE_DIR@', 'test')
-config.test_format = libcxx.test.format.CxxStandardLibraryTest()
+config.test_format = libcxx.test.format.LibcxxTest()
 config.recursiveExpansionLimit = 10
 config.test_exec_root = os.path.join('@CMAKE_BINARY_DIR@', 'test')
 
diff --git a/libunwind/test/configs/cmake-bridge.cfg.in b/libunwind/test/configs/cmake-bridge.cfg.in
index c5f34c87abb92a..f74bc04e74c72b 100644
--- a/libunwind/test/configs/cmake-bridge.cfg.in
+++ b/libunwind/test/configs/cmake-bridge.cfg.in
@@ -18,7 +18,7 @@ import libcxx.test.format
 # Basic configuration of the test suite
 config.name = os.path.basename('@LIBUNWIND_TEST_CONFIG@')
 config.test_source_root = os.path.join('@LIBUNWIND_SOURCE_DIR@', 'test')
-config.test_format = libcxx.test.format.CxxStandardLibraryTest()
+config.test_format = libcxx.test.format.LibcxxTest()
 config.recursiveExpansionLimit = 10
 config.test_exec_root = os.path.join('@CMAKE_BINARY_DIR@', 'test')
 
diff --git a/llvm/utils/lit/lit/formats/__init__.py b/llvm/utils/lit/lit/formats/__init__.py
index 6f3dd38eab9990..ccce2c9bb40d73 100644
--- a/llvm/utils/lit/lit/formats/__init__.py
+++ b/llvm/utils/lit/lit/formats/__init__.py
@@ -6,3 +6,4 @@
 
 from lit.formats.googletest import GoogleTest  # noqa: F401
 from lit.formats.shtest import ShTest  # noqa: F401
+from lit.formats.standardlibrarytest import StandardLibraryTest  # noqa: F401
diff --git a/llvm/utils/lit/lit/formats/standardlibrarytest.py b/llvm/utils/lit/lit/formats/standardlibrarytest.py
new file mode 100644
index 00000000000000..cb54daa72fdfc5
--- /dev/null
+++ b/llvm/utils/lit/lit/formats/standardlibrarytest.py
@@ -0,0 +1,355 @@
+# ===----------------------------------------------------------------------===##
+#
+# 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 lit
+import os
+import re
+from .base import FileBasedTest
+
+
+def _getSubstitution(substitution, config):
+    """
+    Helper function to get a specific substitution from a config object.
+    """
+    for (orig, replacement) in config.substitutions:
+        if orig == substitution:
+            return replacement
+    raise ValueError("Substitution {} is not in the config.".format(substitution))
+
+
+def _appendToSubstitution(substitutions, key, value):
+    """
+    Helper function to append a value to a specific substitution.
+    """
+    return [(k, v + " " + value) if k == key else (k, v) for (k, v) in substitutions]
+
+
+def _getTempPaths(test):
+    """
+    Return the values to use for the %T and %t substitutions, respectively.
+
+    The difference between this and Lit's default behavior is that we guarantee
+    that %T is a path unique to the test being run.
+    """
+    tmpDir, _ = lit.TestRunner.getTempPaths(test)
+    _, testName = os.path.split(test.getExecPath())
+    tmpDir = os.path.join(tmpDir, testName + ".dir")
+    tmpBase = os.path.join(tmpDir, "t")
+    return tmpDir, tmpBase
+
+
+def _checkBaseSubstitutions(substitutions):
+    """
+    Helper function to ensure that all the base substitutions required by this
+    format are provided.
+    """
+    substitutions = [s for (s, _) in substitutions]
+    for s in ["%{cxx}", "%{compile_flags}", "%{link_flags}", "%{flags}", "%{exec}"]:
+        assert s in substitutions, "Required substitution {} was not provided".format(s)
+
+
+def _convenienceSubstitutions():
+    """
+    Return the convenience substitutions provided by this test format.
+    """
+    return [
+        ("%{build}", "%{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe"),
+        (
+            "%{verify}",
+            # Note: We use -Wno-error with %{verify} to make sure that we don't treat all diagnostics as
+            #       errors, which doesn't make sense for clang-verify tests because we may want to check
+            #       for specific warning diagnostics.
+            "%{cxx} %s %{flags} %{compile_flags} -fsyntax-only -Wno-error -Xclang -verify -Xclang -verify-ignore-unexpected=note -ferror-limit=0",
+        ),
+        ("%{run}", "%{exec} %t.exe")
+    ]
+
+
+def _executeScriptInternal(test, litConfig, commands):
+    """
+    Returns (stdout, stderr, exitCode, timeoutInfo, parsedCommands)
+
+    TODO: This really should be easier to access from Lit itself
+    """
+    parsedCommands = _parseScript(test, preamble=commands)
+
+    _, tmpBase = _getTempPaths(test)
+    execDir = os.path.dirname(test.getExecPath())
+    try:
+        res = lit.TestRunner.executeScriptInternal(
+            test, litConfig, tmpBase, parsedCommands, execDir, debug=False
+        )
+    except lit.TestRunner.ScriptFatal as e:
+        res = ("", str(e), 127, None)
+    (out, err, exitCode, timeoutInfo) = res
+
+    return (out, err, exitCode, timeoutInfo, parsedCommands)
+
+
+def _parseScript(test, preamble):
+    """
+    Extract the script from a test, with substitutions applied.
+
+    Returns a list of commands ready to be executed.
+
+    - test
+        The lit.Test to parse.
+
+    - preamble
+        A list of commands to perform before any command in the test.
+        These commands can contain unexpanded substitutions, but they
+        must not be of the form 'RUN:' -- they must be proper commands
+        once substituted.
+    """
+    # Get the default substitutions, ensure we have all the required substitutions
+    # and add a few convenience ones provided by this test format.
+    tmpDir, tmpBase = _getTempPaths(test)
+    substitutions = lit.TestRunner.getDefaultSubstitutions(test, tmpDir, tmpBase)
+    _checkBaseSubstitutions(substitutions)
+    substitutions += _convenienceSubstitutions()
+
+    # Parse the test file, including custom directives
+    additionalCompileFlags = []
+    fileDependencies = []
+    parsers = [
+        lit.TestRunner.IntegratedTestKeywordParser(
+            "FILE_DEPENDENCIES:",
+            lit.TestRunner.ParserKind.LIST,
+            initial_value=fileDependencies,
+        ),
+        lit.TestRunner.IntegratedTestKeywordParser(
+            "ADDITIONAL_COMPILE_FLAGS:",
+            lit.TestRunner.ParserKind.SPACE_LIST,
+            initial_value=additionalCompileFlags,
+        )
+    ]
+
+    # Add conditional parsers for ADDITIONAL_COMPILE_FLAGS. This should be replaced by first
+    # class support for conditional keywords in Lit, which would allow evaluating arbitrary
+    # Lit boolean expressions instead.
+    for feature in test.config.available_features:
+        parser = lit.TestRunner.IntegratedTestKeywordParser(
+            "ADDITIONAL_COMPILE_FLAGS({}):".format(feature),
+            lit.TestRunner.ParserKind.SPACE_LIST,
+            initial_value=additionalCompileFlags,
+        )
+        parsers.append(parser)
+
+    scriptInTest = lit.TestRunner.parseIntegratedTestScript(
+        test, additional_parsers=parsers, require_script=not preamble
+    )
+    if isinstance(scriptInTest, lit.Test.Result):
+        return scriptInTest
+
+    script = []
+
+    # For each file dependency in FILE_DEPENDENCIES, inject a command to copy
+    # that file to the execution directory. Execute the copy from %S to allow
+    # relative paths from the test directory.
+    for dep in fileDependencies:
+        script += ["%dbg(SETUP) cd %S && cp {} %T".format(dep)]
+    script += preamble
+    script += scriptInTest
+
+    # Add compile flags specified with ADDITIONAL_COMPILE_FLAGS.
+    substitutions = _appendToSubstitution(
+        substitutions, "%{compile_flags}", " ".join(additionalCompileFlags)
+    )
+
+    # Perform substitutions in the script itself.
+    script = lit.TestRunner.applySubstitutions(
+        script, substitutions, recursion_limit=test.config.recursiveExpansionLimit
+    )
+
+    return script
+
+
+class StandardLibraryTest(FileBasedTest):
+    """
+    Lit test format designed for testing Standard Libraries.
+
+    Lit tests are contained in files that follow a certain pattern, which determines the semantics of the test.
+    Under the hood, we basically generate a builtin Lit shell test that follows the ShTest format, and perform
+    the appropriate operations (compile/link/run). See https://libcxx.llvm.org/TestingLibcxx.html#test-names
+    for a complete description of those semantics.
+
+    Substitution requirements
+    =========================
+    The test format operates by assuming that each test's configuration provides the following substitutions,
+    which it will reuse in the shell scripts it constructs:
+        %{cxx}           - A command that can be used to invoke the compiler
+        %{compile_flags} - Flags to use when compiling a test case
+        %{link_flags}    - Flags to use when linking a test case
+        %{flags}         - Flags to use either when compiling or linking a test case
+        %{exec}          - A command to prefix the execution of executables
+
+    Note that when building an executable (as opposed to only compiling a source file), all three of %{flags},
+    %{compile_flags} and %{link_flags} will be used in the same command line. In other words, the test format
+    doesn't perform separate compilation and linking steps in this case.
+
+    Additional provided substitutions and features
+    ==============================================
+    The test format will define the following substitutions for use inside tests:
+
+        %{build}
+            Expands to a command-line that builds the current source
+            file with the %{flags}, %{compile_flags} and %{link_flags}
+            substitutions, and that produces an executable named %t.exe.
+
+        %{verify}
+            Expands to a command-line that builds the current source
+            file with the %{flags} and %{compile_flags} substitutions
+            and enables clang-verify. This can be used to write .sh.cpp
+            tests that use clang-verify. Note that this substitution can
+            only be used when the 'verify-support' feature is available.
+
+        %{run}
+            Equivalent to `%{exec} %t.exe`. This is intended to be used
+            in conjunction with the %{build} substitution.
+    """
+
+    def getTestsForPath(self, testSuite, pathInSuite, litConfig, localConfig):
+        SUPPORTED_SUFFIXES = [
+            "[.]pass[.]cpp$",
+            "[.]pass[.]mm$",
+            "[.]compile[.]pass[.]cpp$",
+            "[.]compile[.]pass[.]mm$",
+            "[.]compile[.]fail[.]cpp$",
+            "[.]link[.]pass[.]cpp$",
+            "[.]link[.]pass[.]mm$",
+            "[.]link[.]fail[.]cpp$",
+            "[.]sh[.][^.]+$",
+            "[.]gen[.][^.]+$",
+            "[.]verify[.]cpp$",
+        ]
+
+        sourcePath = testSuite.getSourcePath(pathInSuite)
+        filename = os.path.basename(sourcePath)
+
+        # Ignore dot files, excluded tests and tests with an unsupported suffix
+        hasSupportedSuffix = lambda f: any([re.search(ext, f) for ext in SUPPORTED_SUFFIXES])
+        if filename.startswith(".") or filename in localConfig.excludes or not hasSupportedSuffix(filename):
+            return
+
+        # If this is a generated test, run the generation step and add
+        # as many Lit tests as necessary.
+        if re.search('[.]gen[.][^.]+$', filename):
+            for test in self._generateGenTest(testSuite, pathInSuite, litConfig, localConfig):
+                yield test
+        else:
+            yield lit.Test.Test(testSuite, pathInSuite, localConfig)
+
+    def execute(self, test, litConfig):
+        supportsVerify = "verify-support" in test.config.available_features
+        filename = test.path_in_suite[-1]
+
+        if re.search("[.]sh[.][^.]+$", filename):
+            steps = []  # The steps are already in the script
+            return self._executeShTest(test, litConfig, steps)
+        elif filename.endswith(".compile.pass.cpp") or filename.endswith(
+            ".compile.pass.mm"
+        ):
+            steps = [
+                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} -fsyntax-only"
+            ]
+            return self._executeShTest(test, litConfig, steps)
+        elif filename.endswith(".compile.fail.cpp"):
+            steps = [
+                "%dbg(COMPILED WITH) ! %{cxx} %s %{flags} %{compile_flags} -fsyntax-only"
+            ]
+            return self._executeShTest(test, litConfig, steps)
+        elif filename.endswith(".link.pass.cpp") or filename.endswith(".link.pass.mm"):
+            steps = [
+                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe"
+            ]
+            return self._executeShTest(test, litConfig, steps)
+        elif filename.endswith(".link.fail.cpp"):
+            steps = [
+                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} -c -o %t.o",
+                "%dbg(LINKED WITH) ! %{cxx} %t.o %{flags} %{link_flags} -o %t.exe",
+            ]
+            return self._executeShTest(test, litConfig, steps)
+        elif filename.endswith(".verify.cpp"):
+            if not supportsVerify:
+                return lit.Test.Result(
+                    lit.Test.UNSUPPORTED,
+                    "Test {} requires support for Clang-verify, which isn't supported by the compiler".format(
+                        test.getFullName()
+                    ),
+                )
+            steps = ["%dbg(COMPILED WITH) %{verify}"]
+            return self._executeShTest(test, litConfig, steps)
+        # Make sure to check these ones last, since they will match other
+        # suffixes above too.
+        elif filename.endswith(".pass.cpp") or filename.endswith(".pass.mm"):
+            steps = [
+                "%dbg(COMPILED WITH) %{cxx} %s %{flags} %{compile_flags} %{link_flags} -o %t.exe",
+                "%dbg(EXECUTED AS) %{exec} %t.exe",
+            ]
+            return self._executeShTest(test, litConfig, steps)
+        else:
+            return lit.Test.Result(
+                lit.Test.UNRESOLVED, "Unknown test suffix for '{}'".format(filename)
+            )
+
+    def _executeShTest(self, test, litConfig, steps):
+        if test.config.unsupported:
+            return lit.Test.Result(lit.Test.UNSUPPORTED, "Test is unsupported")
+
+        script = _parseScript(test, steps)
+        if isinstance(script, lit.Test.Result):
+            return script
+
+        if litConfig.noExecute:
+            return lit.Test.Result(
+                lit.Test.XFAIL if test.isExpectedToFail() else lit.Test.PASS
+            )
+        else:
+            _, tmpBase = _getTempPaths(test)
+            useExternalSh = False
+            return lit.TestRunner._runShTest(
+                test, litConfig, useExternalSh, script, tmpBase
+            )
+
+    def _generateGenTest(self, testSuite, pathInSuite, litConfig, localConfig):
+        generator = lit.Test.Test(testSuite, pathInSuite, localConfig)
+
+        # Make sure we have a directory to execute the generator test in
+        generatorExecDir = os.path.dirname(testSuite.getExecPath(pathInSuite))
+        os.makedirs(generatorExecDir, exist_ok=True)
+
+        # Run the generator test
+        steps = [] # Steps must already be in the script
+        (out, err, exitCode, _, _) = _executeScriptInternal(generator, litConfig, steps)
+        if exitCode != 0:
+            raise RuntimeError(f"Error while trying to generate gen test\nstdout:\n{out}\n\nstderr:\n{err}")
+
+        # Split the generated output into multiple files and generate one test for each file
+        for subfile, content in self._splitFile(out):
+            generatedFile = testSuite.getExecPath(pathInSuite + (subfile,))
+            os.makedirs(os.path.dirname(generatedFile), exist_ok=True)
+            with open(generatedFile, 'w') as f:
+                f.write(content)
+            yield lit.Test.Test(testSuite, (generatedFile,), localConfig)
+
+    def _splitFile(self, input):
+        DELIM = r'^(//|#)---(.+)'
+        lines = input.splitlines()
+        currentFile = None
+        thisFileContent = []
+        for line in lines:
+            match = re.match(DELIM, line)
+            if match:
+                if currentFile is not None:
+                    yield (currentFile, '\n'.join(thisFileContent))
+                currentFile = match.group(2).strip()
+                thisFileContent = []
+            assert currentFile is not None, f"Some input to split-file doesn't belong to any file, input was:\n{input}"
+            thisFileContent.append(line)
+        if currentFile is not None:
+            yield (currentFile, '\n'.join(thisFileContent))



More information about the libcxx-commits mailing list