[libcxx-commits] [libcxx] [libcxx][test] Add support for LLVM-style split tests (PR #188283)

Michael Buch via libcxx-commits libcxx-commits at lists.llvm.org
Wed Mar 25 03:11:45 PDT 2026


https://github.com/Michael137 updated https://github.com/llvm/llvm-project/pull/188283

>From 0173673b94485f249b627023c83cbf26cb50d65d Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Tue, 24 Mar 2026 16:14:53 +0000
Subject: [PATCH 1/2] [libcxx][test] Add support for LLVM-style split tests

This patch adds support for a new kind of test (whose names have
`.split.` in them). An example of such test is as follows:
```
   // RUN: use %{temp}/main.cpp
   // RUN: use %{temp}/inputs.txt

   //--- main.cpp
   int main() { return 0; }

   //--- input.txt
   some inputs
```

This patch creates the subfiles in the directory pointed to by the `%{temp}` substitution. Then it runs the original test file just like a regular shell LIT test.

The LLVM `split-file` utility would do this for us already, but it's not available as a Python package yet. So in the meantime we piggyback off of the hand-rolled `_splitFile` that's used for the `.gen.` tests.

This is useful for the upcoming LLDB data-formatter tests (see https://github.com/llvm/llvm-project/pull/187677).
---
 libcxx/test/selftest/split/test.split.cpp | 24 ++++++++++++
 libcxx/test/selftest/split/test.split.sh  | 24 ++++++++++++
 libcxx/utils/libcxx/test/format.py        | 45 ++++++++++++++++++++++-
 3 files changed, 91 insertions(+), 2 deletions(-)
 create mode 100644 libcxx/test/selftest/split/test.split.cpp
 create mode 100644 libcxx/test/selftest/split/test.split.sh

diff --git a/libcxx/test/selftest/split/test.split.cpp b/libcxx/test/selftest/split/test.split.cpp
new file mode 100644
index 0000000000000..38e638569e3d6
--- /dev/null
+++ b/libcxx/test/selftest/split/test.split.cpp
@@ -0,0 +1,24 @@
+// Pre-delimiter comment.
+
+// RUN: grep 'int main' %{temp}/main.cpp
+// RUN: grep 'return 0' %{temp}/main.cpp
+// RUN: not grep -c 'Pre-delimiter' %{temp}/main.cpp
+// RUN: not grep -c 'foo' %{temp}/main.cpp
+// RUN: not grep -c '//---' %{temp}/main.cpp
+
+// RUN: grep foo %{temp}/input.txt
+// RUN: grep bar %{temp}/input.txt
+// RUN: not grep -c 'Pre-delimiter' %{temp}/input.txt
+// RUN: not grep -c 'int main' %{temp}/input.txt
+// RUN: not grep -c '//---' %{temp}/input.txt
+
+//--- main.cpp
+
+int main() {
+  return 0;
+}
+
+//--- input.txt
+
+foo
+bar
diff --git a/libcxx/test/selftest/split/test.split.sh b/libcxx/test/selftest/split/test.split.sh
new file mode 100644
index 0000000000000..a72e3aadfc501
--- /dev/null
+++ b/libcxx/test/selftest/split/test.split.sh
@@ -0,0 +1,24 @@
+# Pre-delimiter comment.
+
+# RUN: grep 'int main' %{temp}/main.cpp
+# RUN: grep 'return 0' %{temp}/main.cpp
+# RUN: not grep -c 'Pre-delimiter' %{temp}/main.cpp
+# RUN: not grep -c 'foo' %{temp}/main.cpp
+# RUN: not grep -c '//---' %{temp}/main.cpp
+
+# RUN: grep foo %{temp}/input.txt
+# RUN: grep bar %{temp}/input.txt
+# RUN: not grep -c 'Pre-delimiter' %{temp}/input.txt
+# RUN: not grep -c 'int main' %{temp}/input.txt
+# RUN: not grep -c '//---' %{temp}/input.txt
+
+#--- main.cpp
+
+int main() {
+  return 0;
+}
+
+#--- input.txt
+
+foo
+bar
diff --git a/libcxx/utils/libcxx/test/format.py b/libcxx/utils/libcxx/test/format.py
index 49cbe8a8db618..49186d2832757 100644
--- a/libcxx/utils/libcxx/test/format.py
+++ b/libcxx/utils/libcxx/test/format.py
@@ -281,6 +281,7 @@ def getTestsForPath(self, testSuite, pathInSuite, litConfig, localConfig):
             "[.]sh[.][^.]+$",
             "[.]gen[.][^.]+$",
             "[.]verify[.]cpp$",
+            "[.]split[.][^.]+$",
         ]
 
         sourcePath = testSuite.getSourcePath(pathInSuite)
@@ -365,6 +366,8 @@ def execute(self, test, litConfig):
             return self._executeShTest(test, litConfig, steps)
         elif re.search('[.]gen[.][^.]+$', filename): # This only happens when a generator test is not supported
             return self._executeShTest(test, litConfig, [])
+        elif re.search('[.]split[.][^.]+$', filename):
+            return self._executeSplitTest(test, litConfig)
         else:
             return lit.Test.Result(
                 lit.Test.UNRESOLVED, "Unknown test suffix for '{}'".format(filename)
@@ -418,6 +421,36 @@ def _generateGenTest(self, testSuite, pathInSuite, litConfig, localConfig):
                 f.write(content)
             yield lit.Test.Test(testSuite, (generatedFile,), localConfig)
 
+    # Split tests have following structure:
+    #
+    #    // RUN: use %{temp}/main.cpp
+    #    // RUN: use %{temp}/inputs.txt
+    #
+    #    //--- main.cpp
+    #    int main() { return 0; }
+    #
+    #    //--- input.txt
+    #    some inputs
+    #
+    # This function takes such test and creates the subfiles in the directory
+    # pointed to by the %{temp} substitution. Then it runs the original test file
+    # just like a regular shell LIT test.
+    def _executeSplitTest(self, test, litConfig):
+            with open(test.getSourcePath(), 'r') as f:
+                content_to_split = f.read()
+                for subfile, content in self._splitFile(content_to_split):
+                    # Write split content into respective subfile in the temporary
+                    # directory (pointed to by the substitution %t substitution).
+                    tempDir, _ = _getTempPaths(test)
+                    subfile_path = os.path.join(tempDir, subfile)
+                    os.makedirs(os.path.dirname(subfile_path), exist_ok=True)
+                    with open(subfile_path, 'w') as sf:
+                        sf.write(content)
+
+            # Just as for regular .sh tests, the steps are already in the script.
+            steps = []
+            return self._executeShTest(test, litConfig, steps)
+
     def _splitFile(self, input):
         DELIM = r'^(//|#)---(.+)'
         lines = input.splitlines()
@@ -430,7 +463,15 @@ def _splitFile(self, input):
                     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)
+
+            # Anything before the first match line is disregarded.
+            # E.g., .split. tests put all the RUN lines before any
+            # split delimiters.
+            if currentFile is None:
+                continue
+
+            # Don't put the delimiter itself into the split content.
+            if not match:
+                thisFileContent.append(line)
         if currentFile is not None:
             yield (currentFile, '\n'.join(thisFileContent))

>From b515b157216106fdabdc6542d487bb51ee230e8e Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Wed, 25 Mar 2026 10:11:29 +0000
Subject: [PATCH 2/2] fixup! format

---
 libcxx/test/selftest/split/test.split.cpp | 24 -----------------
 libcxx/utils/libcxx/test/format.py        | 32 +++++++++++------------
 2 files changed, 16 insertions(+), 40 deletions(-)
 delete mode 100644 libcxx/test/selftest/split/test.split.cpp

diff --git a/libcxx/test/selftest/split/test.split.cpp b/libcxx/test/selftest/split/test.split.cpp
deleted file mode 100644
index 38e638569e3d6..0000000000000
--- a/libcxx/test/selftest/split/test.split.cpp
+++ /dev/null
@@ -1,24 +0,0 @@
-// Pre-delimiter comment.
-
-// RUN: grep 'int main' %{temp}/main.cpp
-// RUN: grep 'return 0' %{temp}/main.cpp
-// RUN: not grep -c 'Pre-delimiter' %{temp}/main.cpp
-// RUN: not grep -c 'foo' %{temp}/main.cpp
-// RUN: not grep -c '//---' %{temp}/main.cpp
-
-// RUN: grep foo %{temp}/input.txt
-// RUN: grep bar %{temp}/input.txt
-// RUN: not grep -c 'Pre-delimiter' %{temp}/input.txt
-// RUN: not grep -c 'int main' %{temp}/input.txt
-// RUN: not grep -c '//---' %{temp}/input.txt
-
-//--- main.cpp
-
-int main() {
-  return 0;
-}
-
-//--- input.txt
-
-foo
-bar
diff --git a/libcxx/utils/libcxx/test/format.py b/libcxx/utils/libcxx/test/format.py
index 49186d2832757..f3197aaec0dc9 100644
--- a/libcxx/utils/libcxx/test/format.py
+++ b/libcxx/utils/libcxx/test/format.py
@@ -281,7 +281,7 @@ def getTestsForPath(self, testSuite, pathInSuite, litConfig, localConfig):
             "[.]sh[.][^.]+$",
             "[.]gen[.][^.]+$",
             "[.]verify[.]cpp$",
-            "[.]split[.][^.]+$",
+            "[.]split[.]sh$",
         ]
 
         sourcePath = testSuite.getSourcePath(pathInSuite)
@@ -366,7 +366,7 @@ def execute(self, test, litConfig):
             return self._executeShTest(test, litConfig, steps)
         elif re.search('[.]gen[.][^.]+$', filename): # This only happens when a generator test is not supported
             return self._executeShTest(test, litConfig, [])
-        elif re.search('[.]split[.][^.]+$', filename):
+        elif re.search("[.]split[.]sh$", filename):
             return self._executeSplitTest(test, litConfig)
         else:
             return lit.Test.Result(
@@ -436,20 +436,20 @@ def _generateGenTest(self, testSuite, pathInSuite, litConfig, localConfig):
     # pointed to by the %{temp} substitution. Then it runs the original test file
     # just like a regular shell LIT test.
     def _executeSplitTest(self, test, litConfig):
-            with open(test.getSourcePath(), 'r') as f:
-                content_to_split = f.read()
-                for subfile, content in self._splitFile(content_to_split):
-                    # Write split content into respective subfile in the temporary
-                    # directory (pointed to by the substitution %t substitution).
-                    tempDir, _ = _getTempPaths(test)
-                    subfile_path = os.path.join(tempDir, subfile)
-                    os.makedirs(os.path.dirname(subfile_path), exist_ok=True)
-                    with open(subfile_path, 'w') as sf:
-                        sf.write(content)
-
-            # Just as for regular .sh tests, the steps are already in the script.
-            steps = []
-            return self._executeShTest(test, litConfig, steps)
+        with open(test.getSourcePath(), "r") as f:
+            content_to_split = f.read()
+            for subfile, content in self._splitFile(content_to_split):
+                # Write split content into respective subfile in the temporary
+                # directory (pointed to by the substitution %t substitution).
+                tempDir, _ = _getTempPaths(test)
+                subfile_path = os.path.join(tempDir, subfile)
+                os.makedirs(os.path.dirname(subfile_path), exist_ok=True)
+                with open(subfile_path, "w") as sf:
+                    sf.write(content)
+
+        # Just as for regular .sh tests, the steps are already in the script.
+        steps = []
+        return self._executeShTest(test, litConfig, steps)
 
     def _splitFile(self, input):
         DELIM = r'^(//|#)---(.+)'



More information about the libcxx-commits mailing list