[llvm] [lit] Add --per-test-output-subdir to isolate test_exec_root across concurrent runs (PR #206826)
David Young via llvm-commits
llvm-commits at lists.llvm.org
Sun Jul 19 07:38:05 PDT 2026
https://github.com/youngd007 updated https://github.com/llvm/llvm-project/pull/206826
>From dfb82b2bd2045ba9ded2316bc67053682f4094b9 Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Tue, 30 Jun 2026 13:18:15 -0700
Subject: [PATCH 1/6] [lit] Add --per-process-output-dir to isolate
test_exec_root across concurrent runs
---
llvm/docs/CommandGuide/lit.rst | 9 ++++++++
llvm/utils/lit/lit/LitConfig.py | 2 ++
llvm/utils/lit/lit/TestingConfig.py | 13 +++++++++++
llvm/utils/lit/lit/cl_arguments.py | 10 +++++++++
llvm/utils/lit/lit/main.py | 1 +
.../Inputs/per-process-output-dir/lit.cfg | 9 ++++++++
.../Inputs/per-process-output-dir/test.txt | 2 ++
.../utils/lit/tests/per-process-output-dir.py | 22 +++++++++++++++++++
8 files changed, 68 insertions(+)
create mode 100644 llvm/utils/lit/tests/Inputs/per-process-output-dir/lit.cfg
create mode 100644 llvm/utils/lit/tests/Inputs/per-process-output-dir/test.txt
create mode 100644 llvm/utils/lit/tests/per-process-output-dir.py
diff --git a/llvm/docs/CommandGuide/lit.rst b/llvm/docs/CommandGuide/lit.rst
index 8b58011f8ec37..6a0e931199629 100644
--- a/llvm/docs/CommandGuide/lit.rst
+++ b/llvm/docs/CommandGuide/lit.rst
@@ -246,6 +246,15 @@ EXECUTION OPTIONS
setting a unique value to LLVM_PROFILE_FILE for each RUN). The coverage
data files will be emitted in the directory specified by ``config.test_exec_root``.
+.. option:: --per-process-output-dir
+
+ Isolate each lit process's writable output by appending a unique per-process
+ subdirectory (``pid-<pid>``) to each test suite's ``config.test_exec_root``.
+ This causes ``%t``, ``%T`` and the per-test ``Output`` directory to resolve
+ under a per-process path, so multiple lit processes can run the same tests
+ concurrently against a single build tree (for example, test stress runs)
+ without clobbering each other's temporary files. Off by default.
+
.. option:: --ignore-fail
Exit with status zero even if some tests fail.
diff --git a/llvm/utils/lit/lit/LitConfig.py b/llvm/utils/lit/lit/LitConfig.py
index 331320cf8ebc8..5622ee9ee29ab 100644
--- a/llvm/utils/lit/lit/LitConfig.py
+++ b/llvm/utils/lit/lit/LitConfig.py
@@ -41,6 +41,7 @@ def __init__(
per_test_coverage=False,
gtest_sharding=True,
update_tests=False,
+ per_process_output_dir=False,
):
# The name of the test runner.
self.progname = progname
@@ -94,6 +95,7 @@ def __init__(
self.per_test_coverage = per_test_coverage
self.gtest_sharding = bool(gtest_sharding)
self.update_tests = update_tests
+ self.per_process_output_dir = bool(per_process_output_dir)
self.test_updaters = [diff_test_updater]
@property
diff --git a/llvm/utils/lit/lit/TestingConfig.py b/llvm/utils/lit/lit/TestingConfig.py
index 1508b1d71caf2..f3e85e62eade4 100644
--- a/llvm/utils/lit/lit/TestingConfig.py
+++ b/llvm/utils/lit/lit/TestingConfig.py
@@ -239,6 +239,19 @@ def finish(self, litConfig):
# FIXME: This should really only be suite in test suite config
# files. Should we distinguish them?
self.test_exec_root = str(self.test_exec_root)
+ # Optionally isolate this lit process's writable output tree so that
+ # multiple lit processes can run the same tests concurrently against
+ # one build tree without clobbering each other's %t/%T/Output files.
+ # The guard makes this idempotent: local configs are deep-copied from
+ # their parent (which is already isolated) and then finish()ed again,
+ # which must not append a second component.
+ if litConfig.per_process_output_dir and not getattr(
+ self, "_per_process_output_dir_applied", False
+ ):
+ self.test_exec_root = os.path.join(
+ self.test_exec_root, "pid-%d" % os.getpid()
+ )
+ self._per_process_output_dir_applied = True
if self.test_source_root is not None:
# FIXME: This should really only be suite in test suite config
# files. Should we distinguish them?
diff --git a/llvm/utils/lit/lit/cl_arguments.py b/llvm/utils/lit/lit/cl_arguments.py
index 6225ac57abfd0..3a36aef53e5f0 100644
--- a/llvm/utils/lit/lit/cl_arguments.py
+++ b/llvm/utils/lit/lit/cl_arguments.py
@@ -363,6 +363,16 @@ def parse_args():
action="store_true",
help="Enable individual test case coverage",
)
+ execution_group.add_argument(
+ "--per-process-output-dir",
+ dest="per_process_output_dir",
+ action="store_true",
+ help="Isolate each lit process's writable output by appending a unique "
+ "per-process subdirectory (pid-<pid>) to each test suite's "
+ "test_exec_root. This lets multiple lit processes run the same tests "
+ "concurrently against a single build tree (e.g. test stress runs) "
+ "without clobbering each other's %%t/%%T/Output files. [Default: Off]",
+ )
execution_group.add_argument(
"--ignore-fail",
dest="ignoreFail",
diff --git a/llvm/utils/lit/lit/main.py b/llvm/utils/lit/lit/main.py
index a3bd153040a69..74b5609655772 100755
--- a/llvm/utils/lit/lit/main.py
+++ b/llvm/utils/lit/lit/main.py
@@ -41,6 +41,7 @@ def main(builtin_params={}):
params=params,
config_prefix=opts.configPrefix,
per_test_coverage=opts.per_test_coverage,
+ per_process_output_dir=opts.per_process_output_dir,
gtest_sharding=opts.gtest_sharding,
maxRetriesPerTest=opts.maxRetriesPerTest,
update_tests=opts.update_tests,
diff --git a/llvm/utils/lit/tests/Inputs/per-process-output-dir/lit.cfg b/llvm/utils/lit/tests/Inputs/per-process-output-dir/lit.cfg
new file mode 100644
index 0000000000000..43fa67116e27b
--- /dev/null
+++ b/llvm/utils/lit/tests/Inputs/per-process-output-dir/lit.cfg
@@ -0,0 +1,9 @@
+import lit.formats
+
+config.name = "per-process-output-dir"
+config.suffixes = [".txt"]
+config.test_format = lit.formats.ShTest()
+config.test_source_root = os.path.dirname(__file__)
+# The outer test passes a writable exec root so that --per-process-output-dir
+# has a non-None test_exec_root to splice the per-process component into.
+config.test_exec_root = lit_config.params["exec_root"]
diff --git a/llvm/utils/lit/tests/Inputs/per-process-output-dir/test.txt b/llvm/utils/lit/tests/Inputs/per-process-output-dir/test.txt
new file mode 100644
index 0000000000000..d2b85b1fbf71e
--- /dev/null
+++ b/llvm/utils/lit/tests/Inputs/per-process-output-dir/test.txt
@@ -0,0 +1,2 @@
+# Print the resolved per-test temp path so the outer test can inspect it.
+# RUN: echo "TEMP_PATH=%t"
diff --git a/llvm/utils/lit/tests/per-process-output-dir.py b/llvm/utils/lit/tests/per-process-output-dir.py
new file mode 100644
index 0000000000000..3c6f27f9f93e3
--- /dev/null
+++ b/llvm/utils/lit/tests/per-process-output-dir.py
@@ -0,0 +1,22 @@
+# Check that --per-process-output-dir isolates each lit process's writable
+# output tree by splicing a unique "pid-<pid>" component into test_exec_root,
+# so that %t/%T/Output resolve under a per-process directory. This lets multiple
+# lit processes run the same tests concurrently against one build tree without
+# clobbering each other.
+
+# With the flag, the temp path is nested under a pid-<pid> directory.
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: %{lit} -a --per-process-output-dir -Dexec_root=%t \
+# RUN: %{inputs}/per-process-output-dir | \
+# RUN: FileCheck --check-prefix=ISOLATED %s
+
+# Without the flag, the temp path is not nested under a pid- directory.
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: %{lit} -a -Dexec_root=%t \
+# RUN: %{inputs}/per-process-output-dir | \
+# RUN: FileCheck --check-prefix=SHARED %s
+
+# ISOLATED: TEMP_PATH={{.*}}/pid-{{[0-9]+}}/{{.*}}Output
+
+# SHARED: TEMP_PATH=
+# SHARED-NOT: /pid-
>From ceea9b13427bebab352c1550d987834d43b6498c Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Wed, 1 Jul 2026 04:40:46 -0700
Subject: [PATCH 2/6] [lit] Rework isolation flag into
--per-test-output-subdir=ID
Address PR feedback: rather than a boolean --per-process-output-dir that
derives the isolation directory from the PID, take a caller-supplied ID
and append it as a subdirectory to each suite's config.test_exec_root
(%t/%T/Output resolve under test_exec_root/<ID>).
Making the ID caller-chosen decouples it from the process, so it can be a
PID, a CI job id, etc. Reusing an ID reuses that output tree, preserving
artifacts such as .lit_test_times.txt across runs (retaining test-ordering
benefits when a build tree is reused). Passing --per-test-output-subdir=pid-$$
recovers the original per-process behavior.
---
llvm/docs/CommandGuide/lit.rst | 18 ++++++++-------
llvm/utils/lit/lit/LitConfig.py | 4 ++--
llvm/utils/lit/lit/TestingConfig.py | 17 ++++++++------
llvm/utils/lit/lit/cl_arguments.py | 20 ++++++++++-------
llvm/utils/lit/lit/main.py | 2 +-
.../lit.cfg | 6 ++---
.../test.txt | 0
.../utils/lit/tests/per-process-output-dir.py | 22 -------------------
.../utils/lit/tests/per-test-output-subdir.py | 22 +++++++++++++++++++
9 files changed, 60 insertions(+), 51 deletions(-)
rename llvm/utils/lit/tests/Inputs/{per-process-output-dir => per-test-output-subdir}/lit.cfg (50%)
rename llvm/utils/lit/tests/Inputs/{per-process-output-dir => per-test-output-subdir}/test.txt (100%)
delete mode 100644 llvm/utils/lit/tests/per-process-output-dir.py
create mode 100644 llvm/utils/lit/tests/per-test-output-subdir.py
diff --git a/llvm/docs/CommandGuide/lit.rst b/llvm/docs/CommandGuide/lit.rst
index 6a0e931199629..57ce95730f00c 100644
--- a/llvm/docs/CommandGuide/lit.rst
+++ b/llvm/docs/CommandGuide/lit.rst
@@ -246,14 +246,16 @@ EXECUTION OPTIONS
setting a unique value to LLVM_PROFILE_FILE for each RUN). The coverage
data files will be emitted in the directory specified by ``config.test_exec_root``.
-.. option:: --per-process-output-dir
-
- Isolate each lit process's writable output by appending a unique per-process
- subdirectory (``pid-<pid>``) to each test suite's ``config.test_exec_root``.
- This causes ``%t``, ``%T`` and the per-test ``Output`` directory to resolve
- under a per-process path, so multiple lit processes can run the same tests
- concurrently against a single build tree (for example, test stress runs)
- without clobbering each other's temporary files. Off by default.
+.. option:: --per-test-output-subdir ID
+
+ Isolate this run's writable output by appending ``ID`` as a subdirectory to
+ each test suite's ``config.test_exec_root``. This causes ``%t``, ``%T`` and the
+ per-test ``Output`` directory to resolve under ``test_exec_root/<ID>``, so
+ multiple lit runs can use the same tests concurrently against a single build
+ tree (for example, test stress runs) without clobbering each other's temporary
+ files. Give each concurrent run a distinct ``ID`` (a process id, CI job id,
+ etc.); reusing an ``ID`` reuses that output tree, preserving artifacts such as
+ ``.lit_test_times.txt`` across runs. Disabled by default.
.. option:: --ignore-fail
diff --git a/llvm/utils/lit/lit/LitConfig.py b/llvm/utils/lit/lit/LitConfig.py
index 5622ee9ee29ab..e9cebd85a1719 100644
--- a/llvm/utils/lit/lit/LitConfig.py
+++ b/llvm/utils/lit/lit/LitConfig.py
@@ -41,7 +41,7 @@ def __init__(
per_test_coverage=False,
gtest_sharding=True,
update_tests=False,
- per_process_output_dir=False,
+ per_test_output_subdir=None,
):
# The name of the test runner.
self.progname = progname
@@ -95,7 +95,7 @@ def __init__(
self.per_test_coverage = per_test_coverage
self.gtest_sharding = bool(gtest_sharding)
self.update_tests = update_tests
- self.per_process_output_dir = bool(per_process_output_dir)
+ self.per_test_output_subdir = per_test_output_subdir
self.test_updaters = [diff_test_updater]
@property
diff --git a/llvm/utils/lit/lit/TestingConfig.py b/llvm/utils/lit/lit/TestingConfig.py
index f3e85e62eade4..e90b50452e908 100644
--- a/llvm/utils/lit/lit/TestingConfig.py
+++ b/llvm/utils/lit/lit/TestingConfig.py
@@ -239,19 +239,22 @@ def finish(self, litConfig):
# FIXME: This should really only be suite in test suite config
# files. Should we distinguish them?
self.test_exec_root = str(self.test_exec_root)
- # Optionally isolate this lit process's writable output tree so that
- # multiple lit processes can run the same tests concurrently against
- # one build tree without clobbering each other's %t/%T/Output files.
+ # Optionally isolate this run's writable output tree so that
+ # multiple lit runs can use the same tests concurrently against one
+ # build tree without clobbering each other's %t/%T/Output files.
+ # The caller-supplied ID names the subdirectory, so it need not be
+ # tied to the process (a PID, CI job id, etc.); reusing an ID reuses
+ # the tree, preserving artifacts like .lit_test_times.txt.
# The guard makes this idempotent: local configs are deep-copied from
# their parent (which is already isolated) and then finish()ed again,
# which must not append a second component.
- if litConfig.per_process_output_dir and not getattr(
- self, "_per_process_output_dir_applied", False
+ if litConfig.per_test_output_subdir and not getattr(
+ self, "_per_test_output_subdir_applied", False
):
self.test_exec_root = os.path.join(
- self.test_exec_root, "pid-%d" % os.getpid()
+ self.test_exec_root, litConfig.per_test_output_subdir
)
- self._per_process_output_dir_applied = True
+ self._per_test_output_subdir_applied = True
if self.test_source_root is not None:
# FIXME: This should really only be suite in test suite config
# files. Should we distinguish them?
diff --git a/llvm/utils/lit/lit/cl_arguments.py b/llvm/utils/lit/lit/cl_arguments.py
index 3a36aef53e5f0..e309df96f209e 100644
--- a/llvm/utils/lit/lit/cl_arguments.py
+++ b/llvm/utils/lit/lit/cl_arguments.py
@@ -364,14 +364,18 @@ def parse_args():
help="Enable individual test case coverage",
)
execution_group.add_argument(
- "--per-process-output-dir",
- dest="per_process_output_dir",
- action="store_true",
- help="Isolate each lit process's writable output by appending a unique "
- "per-process subdirectory (pid-<pid>) to each test suite's "
- "test_exec_root. This lets multiple lit processes run the same tests "
- "concurrently against a single build tree (e.g. test stress runs) "
- "without clobbering each other's %%t/%%T/Output files. [Default: Off]",
+ "--per-test-output-subdir",
+ dest="per_test_output_subdir",
+ metavar="ID",
+ default=None,
+ help="Isolate this run's writable output by appending the given ID as a "
+ "subdirectory to each test suite's test_exec_root, so %%t/%%T/Output "
+ "resolve under test_exec_root/<ID>. This lets multiple lit runs use the "
+ "same tests concurrently against a single build tree (e.g. test stress "
+ "runs) without clobbering each other's output; give each run a distinct "
+ "ID (a PID, CI job id, etc.). Reusing an ID reuses that output tree, "
+ "preserving artifacts like .lit_test_times.txt across runs. [Default: "
+ "disabled]",
)
execution_group.add_argument(
"--ignore-fail",
diff --git a/llvm/utils/lit/lit/main.py b/llvm/utils/lit/lit/main.py
index 74b5609655772..8d2dc0219d539 100755
--- a/llvm/utils/lit/lit/main.py
+++ b/llvm/utils/lit/lit/main.py
@@ -41,7 +41,7 @@ def main(builtin_params={}):
params=params,
config_prefix=opts.configPrefix,
per_test_coverage=opts.per_test_coverage,
- per_process_output_dir=opts.per_process_output_dir,
+ per_test_output_subdir=opts.per_test_output_subdir,
gtest_sharding=opts.gtest_sharding,
maxRetriesPerTest=opts.maxRetriesPerTest,
update_tests=opts.update_tests,
diff --git a/llvm/utils/lit/tests/Inputs/per-process-output-dir/lit.cfg b/llvm/utils/lit/tests/Inputs/per-test-output-subdir/lit.cfg
similarity index 50%
rename from llvm/utils/lit/tests/Inputs/per-process-output-dir/lit.cfg
rename to llvm/utils/lit/tests/Inputs/per-test-output-subdir/lit.cfg
index 43fa67116e27b..cbe2ffb1c973c 100644
--- a/llvm/utils/lit/tests/Inputs/per-process-output-dir/lit.cfg
+++ b/llvm/utils/lit/tests/Inputs/per-test-output-subdir/lit.cfg
@@ -1,9 +1,9 @@
import lit.formats
-config.name = "per-process-output-dir"
+config.name = "per-test-output-subdir"
config.suffixes = [".txt"]
config.test_format = lit.formats.ShTest()
config.test_source_root = os.path.dirname(__file__)
-# The outer test passes a writable exec root so that --per-process-output-dir
-# has a non-None test_exec_root to splice the per-process component into.
+# The outer test passes a writable exec root so that --per-test-output-subdir
+# has a non-None test_exec_root to splice the per-run subdirectory into.
config.test_exec_root = lit_config.params["exec_root"]
diff --git a/llvm/utils/lit/tests/Inputs/per-process-output-dir/test.txt b/llvm/utils/lit/tests/Inputs/per-test-output-subdir/test.txt
similarity index 100%
rename from llvm/utils/lit/tests/Inputs/per-process-output-dir/test.txt
rename to llvm/utils/lit/tests/Inputs/per-test-output-subdir/test.txt
diff --git a/llvm/utils/lit/tests/per-process-output-dir.py b/llvm/utils/lit/tests/per-process-output-dir.py
deleted file mode 100644
index 3c6f27f9f93e3..0000000000000
--- a/llvm/utils/lit/tests/per-process-output-dir.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Check that --per-process-output-dir isolates each lit process's writable
-# output tree by splicing a unique "pid-<pid>" component into test_exec_root,
-# so that %t/%T/Output resolve under a per-process directory. This lets multiple
-# lit processes run the same tests concurrently against one build tree without
-# clobbering each other.
-
-# With the flag, the temp path is nested under a pid-<pid> directory.
-# RUN: rm -rf %t && mkdir -p %t
-# RUN: %{lit} -a --per-process-output-dir -Dexec_root=%t \
-# RUN: %{inputs}/per-process-output-dir | \
-# RUN: FileCheck --check-prefix=ISOLATED %s
-
-# Without the flag, the temp path is not nested under a pid- directory.
-# RUN: rm -rf %t && mkdir -p %t
-# RUN: %{lit} -a -Dexec_root=%t \
-# RUN: %{inputs}/per-process-output-dir | \
-# RUN: FileCheck --check-prefix=SHARED %s
-
-# ISOLATED: TEMP_PATH={{.*}}/pid-{{[0-9]+}}/{{.*}}Output
-
-# SHARED: TEMP_PATH=
-# SHARED-NOT: /pid-
diff --git a/llvm/utils/lit/tests/per-test-output-subdir.py b/llvm/utils/lit/tests/per-test-output-subdir.py
new file mode 100644
index 0000000000000..cd0dde42c2992
--- /dev/null
+++ b/llvm/utils/lit/tests/per-test-output-subdir.py
@@ -0,0 +1,22 @@
+# Check that --per-test-output-subdir isolates a run's writable output tree by
+# splicing the caller-supplied ID as a component of test_exec_root, so that
+# %t/%T/Output resolve under test_exec_root/<ID>. This lets multiple lit runs
+# use the same tests concurrently against one build tree without clobbering each
+# other, while reusing an ID reuses the tree.
+
+# With the option, the temp path is nested under the given ID directory.
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: %{lit} -a --per-test-output-subdir run-abc123 -Dexec_root=%t \
+# RUN: %{inputs}/per-test-output-subdir | \
+# RUN: FileCheck --check-prefix=ISOLATED %s
+
+# Without the option, the temp path is not nested under the ID directory.
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: %{lit} -a -Dexec_root=%t \
+# RUN: %{inputs}/per-test-output-subdir | \
+# RUN: FileCheck --check-prefix=SHARED %s
+
+# ISOLATED: TEMP_PATH={{.*}}/run-abc123/{{.*}}Output
+
+# SHARED: TEMP_PATH=
+# SHARED-NOT: /run-abc123
>From 2ef328ba7e7f231f18f4d471c15f32fc5be9ef0a Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Wed, 1 Jul 2026 08:01:40 -0700
Subject: [PATCH 3/6] [lit] Replace --per-test-output-subdir with full-path
--test-output-root
Per PR feedback, generalize the output-isolation option: instead of
--per-test-output-subdir=ID (which appended an ID subdirectory to each
suite's existing build-tree test_exec_root), take --test-output-root=DIR,
a full path to the root of the test output tree.
Each suite's exec root becomes DIR/<suite-name> (the suite name is
sanitized to a single path component so multiple suites in one run don't
collide), so %t/%T/Output and .lit_test_times.txt resolve under DIR. This:
- lets test output live entirely outside the build tree (DIR can be
anywhere), and
- lets multiple lit runs share one build tree without clobbering each
other -- give each concurrent run a distinct DIR (from a PID, CI job id,
per-run temp dir, etc.); reusing a DIR reuses the tree, preserving
test-run ordering (.lit_test_times.txt) across runs.
A full path is also easier to wire from build systems (buck2/bazel/CI),
which already hand each invocation a unique output/temp directory, than a
PID that must be synthesized and shell-expanded.
Applied even when a suite sets no test_exec_root, since the intent is to
force the output location. Rename the lit test accordingly.
---
llvm/docs/CommandGuide/lit.rst | 23 ++++++-----
llvm/utils/lit/lit/LitConfig.py | 4 +-
llvm/utils/lit/lit/TestingConfig.py | 38 +++++++++++--------
llvm/utils/lit/lit/cl_arguments.py | 24 ++++++------
llvm/utils/lit/lit/main.py | 2 +-
.../lit.cfg | 6 +--
.../test.txt | 0
.../utils/lit/tests/per-test-output-subdir.py | 22 -----------
llvm/utils/lit/tests/test-output-root.py | 23 +++++++++++
9 files changed, 77 insertions(+), 65 deletions(-)
rename llvm/utils/lit/tests/Inputs/{per-test-output-subdir => test-output-root}/lit.cfg (50%)
rename llvm/utils/lit/tests/Inputs/{per-test-output-subdir => test-output-root}/test.txt (100%)
delete mode 100644 llvm/utils/lit/tests/per-test-output-subdir.py
create mode 100644 llvm/utils/lit/tests/test-output-root.py
diff --git a/llvm/docs/CommandGuide/lit.rst b/llvm/docs/CommandGuide/lit.rst
index 57ce95730f00c..12d035dac874f 100644
--- a/llvm/docs/CommandGuide/lit.rst
+++ b/llvm/docs/CommandGuide/lit.rst
@@ -246,16 +246,19 @@ EXECUTION OPTIONS
setting a unique value to LLVM_PROFILE_FILE for each RUN). The coverage
data files will be emitted in the directory specified by ``config.test_exec_root``.
-.. option:: --per-test-output-subdir ID
-
- Isolate this run's writable output by appending ``ID`` as a subdirectory to
- each test suite's ``config.test_exec_root``. This causes ``%t``, ``%T`` and the
- per-test ``Output`` directory to resolve under ``test_exec_root/<ID>``, so
- multiple lit runs can use the same tests concurrently against a single build
- tree (for example, test stress runs) without clobbering each other's temporary
- files. Give each concurrent run a distinct ``ID`` (a process id, CI job id,
- etc.); reusing an ``ID`` reuses that output tree, preserving artifacts such as
- ``.lit_test_times.txt`` across runs. Disabled by default.
+.. option:: --test-output-root DIR
+
+ Write all test output under ``DIR`` instead of each test suite's default
+ ``config.test_exec_root``. Each suite's exec root becomes ``DIR/<suite-name>``,
+ so ``%t``, ``%T``, the per-test ``Output`` directory and ``.lit_test_times.txt``
+ resolve under ``DIR`` (a relative ``DIR`` is resolved against the current
+ directory). This lets test output live outside the build tree, and lets
+ multiple lit runs use the same tests concurrently against a single build tree
+ (for example, test stress runs) without clobbering each other's temporary
+ files. Give each concurrent run a distinct ``DIR`` (derived from a process id,
+ CI job id, per-run temp dir, etc.); reusing a ``DIR`` reuses that output tree,
+ preserving artifacts such as ``.lit_test_times.txt`` across runs. Disabled by
+ default.
.. option:: --ignore-fail
diff --git a/llvm/utils/lit/lit/LitConfig.py b/llvm/utils/lit/lit/LitConfig.py
index e9cebd85a1719..ab6216f778dbc 100644
--- a/llvm/utils/lit/lit/LitConfig.py
+++ b/llvm/utils/lit/lit/LitConfig.py
@@ -41,7 +41,7 @@ def __init__(
per_test_coverage=False,
gtest_sharding=True,
update_tests=False,
- per_test_output_subdir=None,
+ test_output_root=None,
):
# The name of the test runner.
self.progname = progname
@@ -95,7 +95,7 @@ def __init__(
self.per_test_coverage = per_test_coverage
self.gtest_sharding = bool(gtest_sharding)
self.update_tests = update_tests
- self.per_test_output_subdir = per_test_output_subdir
+ self.test_output_root = test_output_root
self.test_updaters = [diff_test_updater]
@property
diff --git a/llvm/utils/lit/lit/TestingConfig.py b/llvm/utils/lit/lit/TestingConfig.py
index e90b50452e908..a2728e3ea44be 100644
--- a/llvm/utils/lit/lit/TestingConfig.py
+++ b/llvm/utils/lit/lit/TestingConfig.py
@@ -1,4 +1,5 @@
import os
+import re
import sys
@@ -239,22 +240,27 @@ def finish(self, litConfig):
# FIXME: This should really only be suite in test suite config
# files. Should we distinguish them?
self.test_exec_root = str(self.test_exec_root)
- # Optionally isolate this run's writable output tree so that
- # multiple lit runs can use the same tests concurrently against one
- # build tree without clobbering each other's %t/%T/Output files.
- # The caller-supplied ID names the subdirectory, so it need not be
- # tied to the process (a PID, CI job id, etc.); reusing an ID reuses
- # the tree, preserving artifacts like .lit_test_times.txt.
- # The guard makes this idempotent: local configs are deep-copied from
- # their parent (which is already isolated) and then finish()ed again,
- # which must not append a second component.
- if litConfig.per_test_output_subdir and not getattr(
- self, "_per_test_output_subdir_applied", False
- ):
- self.test_exec_root = os.path.join(
- self.test_exec_root, litConfig.per_test_output_subdir
- )
- self._per_test_output_subdir_applied = True
+ # Optionally relocate this suite's writable output tree under a
+ # caller-specified root, so %t/%T/Output and .lit_test_times.txt resolve
+ # under <root>/<suite-name> instead of the build tree. This lets output
+ # live outside the build dir, and lets multiple lit runs use the same
+ # tests concurrently against one build tree without clobbering each
+ # other; reusing a root reuses the tree (preserving .lit_test_times.txt).
+ # Applied even when test_exec_root is None, since the point is to force
+ # the output location. The per-suite <suite-name> component keeps
+ # multiple suites in one run from colliding under a shared root. The
+ # guard makes this idempotent: local configs are deep-copied from their
+ # parent (already relocated) and finish()ed again, which must not relocate
+ # a second time.
+ if litConfig.test_output_root and not getattr(
+ self, "_test_output_root_applied", False
+ ):
+ # Sanitize the suite name into a single safe path component.
+ suite_dir = re.sub(r"[^\w.-]", "_", self.name)
+ self.test_exec_root = os.path.join(
+ os.path.abspath(litConfig.test_output_root), suite_dir
+ )
+ self._test_output_root_applied = True
if self.test_source_root is not None:
# FIXME: This should really only be suite in test suite config
# files. Should we distinguish them?
diff --git a/llvm/utils/lit/lit/cl_arguments.py b/llvm/utils/lit/lit/cl_arguments.py
index e309df96f209e..ea6272660347a 100644
--- a/llvm/utils/lit/lit/cl_arguments.py
+++ b/llvm/utils/lit/lit/cl_arguments.py
@@ -364,18 +364,20 @@ def parse_args():
help="Enable individual test case coverage",
)
execution_group.add_argument(
- "--per-test-output-subdir",
- dest="per_test_output_subdir",
- metavar="ID",
+ "--test-output-root",
+ dest="test_output_root",
+ metavar="DIR",
default=None,
- help="Isolate this run's writable output by appending the given ID as a "
- "subdirectory to each test suite's test_exec_root, so %%t/%%T/Output "
- "resolve under test_exec_root/<ID>. This lets multiple lit runs use the "
- "same tests concurrently against a single build tree (e.g. test stress "
- "runs) without clobbering each other's output; give each run a distinct "
- "ID (a PID, CI job id, etc.). Reusing an ID reuses that output tree, "
- "preserving artifacts like .lit_test_times.txt across runs. [Default: "
- "disabled]",
+ help="Write all test output under DIR instead of each test suite's "
+ "default test_exec_root. Each suite's exec root becomes DIR/<suite-name>, "
+ "so %%t/%%T/Output and .lit_test_times.txt resolve under DIR (a relative "
+ "DIR is resolved against the current directory). This lets test output "
+ "live outside the build tree, and lets multiple lit runs use the same "
+ "tests concurrently against a single build tree (e.g. test stress runs) "
+ "without clobbering each other -- give each concurrent run a distinct DIR "
+ "(from a PID, CI job id, per-run temp dir, etc.). Reusing a DIR reuses "
+ "that output tree, preserving artifacts like .lit_test_times.txt across "
+ "runs. [Default: disabled]",
)
execution_group.add_argument(
"--ignore-fail",
diff --git a/llvm/utils/lit/lit/main.py b/llvm/utils/lit/lit/main.py
index 8d2dc0219d539..936c3dfa23308 100755
--- a/llvm/utils/lit/lit/main.py
+++ b/llvm/utils/lit/lit/main.py
@@ -41,7 +41,7 @@ def main(builtin_params={}):
params=params,
config_prefix=opts.configPrefix,
per_test_coverage=opts.per_test_coverage,
- per_test_output_subdir=opts.per_test_output_subdir,
+ test_output_root=opts.test_output_root,
gtest_sharding=opts.gtest_sharding,
maxRetriesPerTest=opts.maxRetriesPerTest,
update_tests=opts.update_tests,
diff --git a/llvm/utils/lit/tests/Inputs/per-test-output-subdir/lit.cfg b/llvm/utils/lit/tests/Inputs/test-output-root/lit.cfg
similarity index 50%
rename from llvm/utils/lit/tests/Inputs/per-test-output-subdir/lit.cfg
rename to llvm/utils/lit/tests/Inputs/test-output-root/lit.cfg
index cbe2ffb1c973c..85021508f7efe 100644
--- a/llvm/utils/lit/tests/Inputs/per-test-output-subdir/lit.cfg
+++ b/llvm/utils/lit/tests/Inputs/test-output-root/lit.cfg
@@ -1,9 +1,9 @@
import lit.formats
-config.name = "per-test-output-subdir"
+config.name = "output-root-suite"
config.suffixes = [".txt"]
config.test_format = lit.formats.ShTest()
config.test_source_root = os.path.dirname(__file__)
-# The outer test passes a writable exec root so that --per-test-output-subdir
-# has a non-None test_exec_root to splice the per-run subdirectory into.
+# The outer test passes a build exec root here; --test-output-root should
+# override it and relocate output under <root>/<config.name>.
config.test_exec_root = lit_config.params["exec_root"]
diff --git a/llvm/utils/lit/tests/Inputs/per-test-output-subdir/test.txt b/llvm/utils/lit/tests/Inputs/test-output-root/test.txt
similarity index 100%
rename from llvm/utils/lit/tests/Inputs/per-test-output-subdir/test.txt
rename to llvm/utils/lit/tests/Inputs/test-output-root/test.txt
diff --git a/llvm/utils/lit/tests/per-test-output-subdir.py b/llvm/utils/lit/tests/per-test-output-subdir.py
deleted file mode 100644
index cd0dde42c2992..0000000000000
--- a/llvm/utils/lit/tests/per-test-output-subdir.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Check that --per-test-output-subdir isolates a run's writable output tree by
-# splicing the caller-supplied ID as a component of test_exec_root, so that
-# %t/%T/Output resolve under test_exec_root/<ID>. This lets multiple lit runs
-# use the same tests concurrently against one build tree without clobbering each
-# other, while reusing an ID reuses the tree.
-
-# With the option, the temp path is nested under the given ID directory.
-# RUN: rm -rf %t && mkdir -p %t
-# RUN: %{lit} -a --per-test-output-subdir run-abc123 -Dexec_root=%t \
-# RUN: %{inputs}/per-test-output-subdir | \
-# RUN: FileCheck --check-prefix=ISOLATED %s
-
-# Without the option, the temp path is not nested under the ID directory.
-# RUN: rm -rf %t && mkdir -p %t
-# RUN: %{lit} -a -Dexec_root=%t \
-# RUN: %{inputs}/per-test-output-subdir | \
-# RUN: FileCheck --check-prefix=SHARED %s
-
-# ISOLATED: TEMP_PATH={{.*}}/run-abc123/{{.*}}Output
-
-# SHARED: TEMP_PATH=
-# SHARED-NOT: /run-abc123
diff --git a/llvm/utils/lit/tests/test-output-root.py b/llvm/utils/lit/tests/test-output-root.py
new file mode 100644
index 0000000000000..5c2fb41f8ea1b
--- /dev/null
+++ b/llvm/utils/lit/tests/test-output-root.py
@@ -0,0 +1,23 @@
+# Check that --test-output-root relocates each suite's writable output tree to
+# <root>/<suite-name>, so %t/%T/Output resolve under the given root directory
+# instead of the suite's build test_exec_root. This lets test output live
+# outside the build tree and lets multiple lit runs share one build tree without
+# clobbering each other; reusing a root reuses the tree.
+
+# With the option, the temp path is under <root>/<suite-name>, not the build dir.
+# RUN: rm -rf %t && mkdir -p %t/build %t/out
+# RUN: %{lit} -a --test-output-root %t/out -Dexec_root=%t/build \
+# RUN: %{inputs}/test-output-root | \
+# RUN: FileCheck --check-prefix=ROOTED %s
+
+# Without the option, the temp path stays under the build test_exec_root.
+# RUN: rm -rf %t && mkdir -p %t/build
+# RUN: %{lit} -a -Dexec_root=%t/build \
+# RUN: %{inputs}/test-output-root | \
+# RUN: FileCheck --check-prefix=DEFAULT %s
+
+# ROOTED: TEMP_PATH={{.*}}/out/output-root-suite/{{.*}}Output
+# ROOTED-NOT: TEMP_PATH={{.*}}/build/
+
+# DEFAULT: TEMP_PATH={{.*}}/build/{{.*}}Output
+# DEFAULT-NOT: output-root-suite
>From 3348f3d32ea53cb21722f1e260f16ae2e714f076 Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Mon, 6 Jul 2026 18:12:29 -0700
Subject: [PATCH 4/6] [lit] Fix test-output-root.py ROOTED-NOT false positive
when build tree contains /build/
The ROOTED-NOT check asserted the temp path is not placed under the
passed exec root by matching TEMP_PATH={{.*}}/build/. But when the lit
test tree itself lives under a build directory (e.g. CI runs from
llvm-project/build/utils/lit/tests/...), %t already contains a /build/
segment, so the pattern matched the ambient path and failed spuriously.
Use a distinctive 'execroot' directory name for the passed exec root so
the negative check can only match the exec root, not an ambient /build/
segment in %t.
---
llvm/utils/lit/tests/test-output-root.py | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/llvm/utils/lit/tests/test-output-root.py b/llvm/utils/lit/tests/test-output-root.py
index 5c2fb41f8ea1b..cc81c40be098d 100644
--- a/llvm/utils/lit/tests/test-output-root.py
+++ b/llvm/utils/lit/tests/test-output-root.py
@@ -3,21 +3,25 @@
# instead of the suite's build test_exec_root. This lets test output live
# outside the build tree and lets multiple lit runs share one build tree without
# clobbering each other; reusing a root reuses the tree.
+#
+# The passed exec root uses a distinctive "execroot" directory name (rather than
+# "build") so the ROOTED-NOT check below can't be fooled by an ambient "/build/"
+# segment in %t (e.g. when the lit test tree itself lives under a build dir).
-# With the option, the temp path is under <root>/<suite-name>, not the build dir.
-# RUN: rm -rf %t && mkdir -p %t/build %t/out
-# RUN: %{lit} -a --test-output-root %t/out -Dexec_root=%t/build \
+# With the option, the temp path is under <root>/<suite-name>, not the exec root.
+# RUN: rm -rf %t && mkdir -p %t/execroot %t/out
+# RUN: %{lit} -a --test-output-root %t/out -Dexec_root=%t/execroot \
# RUN: %{inputs}/test-output-root | \
# RUN: FileCheck --check-prefix=ROOTED %s
-# Without the option, the temp path stays under the build test_exec_root.
-# RUN: rm -rf %t && mkdir -p %t/build
-# RUN: %{lit} -a -Dexec_root=%t/build \
+# Without the option, the temp path stays under the exec root.
+# RUN: rm -rf %t && mkdir -p %t/execroot
+# RUN: %{lit} -a -Dexec_root=%t/execroot \
# RUN: %{inputs}/test-output-root | \
# RUN: FileCheck --check-prefix=DEFAULT %s
# ROOTED: TEMP_PATH={{.*}}/out/output-root-suite/{{.*}}Output
-# ROOTED-NOT: TEMP_PATH={{.*}}/build/
+# ROOTED-NOT: TEMP_PATH={{.*}}/execroot/
-# DEFAULT: TEMP_PATH={{.*}}/build/{{.*}}Output
+# DEFAULT: TEMP_PATH={{.*}}/execroot/{{.*}}Output
# DEFAULT-NOT: output-root-suite
>From 2f0cb154ba36ba7b0efa69cef441de53c87157be Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Tue, 14 Jul 2026 11:52:02 -0700
Subject: [PATCH 5/6] Account for feedback to shorten comments, harden test,
and remove reference to deprecated %T
---
llvm/docs/CommandGuide/lit.rst | 12 ++++--------
llvm/utils/lit/lit/TestingConfig.py | 13 ++-----------
llvm/utils/lit/lit/cl_arguments.py | 10 +---------
llvm/utils/lit/tests/test-output-root.py | 21 ++++++++-------------
4 files changed, 15 insertions(+), 41 deletions(-)
diff --git a/llvm/docs/CommandGuide/lit.rst b/llvm/docs/CommandGuide/lit.rst
index d4e6e7cf84c42..cbb427f5979b1 100644
--- a/llvm/docs/CommandGuide/lit.rst
+++ b/llvm/docs/CommandGuide/lit.rst
@@ -250,15 +250,11 @@ EXECUTION OPTIONS
Write all test output under ``DIR`` instead of each test suite's default
``config.test_exec_root``. Each suite's exec root becomes ``DIR/<suite-name>``,
- so ``%t``, ``%T``, the per-test ``Output`` directory and ``.lit_test_times.txt``
- resolve under ``DIR`` (a relative ``DIR`` is resolved against the current
- directory). This lets test output live outside the build tree, and lets
- multiple lit runs use the same tests concurrently against a single build tree
- (for example, test stress runs) without clobbering each other's temporary
- files. Give each concurrent run a distinct ``DIR`` (derived from a process id,
+ so ``%t``, the per-test ``Output`` directory, ``.lit_test_times.txt``, etc.
+ resolve under ``DIR``. This lets test output live outside the build tree.
+ Parallel concurrent runs can have a distinct ``DIR`` (derived from a process id,
CI job id, per-run temp dir, etc.); reusing a ``DIR`` reuses that output tree,
- preserving artifacts such as ``.lit_test_times.txt`` across runs. Disabled by
- default.
+ preserving artifacts such as ``.lit_test_times.txt`` across runs.
.. option:: --ignore-fail
diff --git a/llvm/utils/lit/lit/TestingConfig.py b/llvm/utils/lit/lit/TestingConfig.py
index a2728e3ea44be..9b2a752ada47f 100644
--- a/llvm/utils/lit/lit/TestingConfig.py
+++ b/llvm/utils/lit/lit/TestingConfig.py
@@ -241,17 +241,8 @@ def finish(self, litConfig):
# files. Should we distinguish them?
self.test_exec_root = str(self.test_exec_root)
# Optionally relocate this suite's writable output tree under a
- # caller-specified root, so %t/%T/Output and .lit_test_times.txt resolve
- # under <root>/<suite-name> instead of the build tree. This lets output
- # live outside the build dir, and lets multiple lit runs use the same
- # tests concurrently against one build tree without clobbering each
- # other; reusing a root reuses the tree (preserving .lit_test_times.txt).
- # Applied even when test_exec_root is None, since the point is to force
- # the output location. The per-suite <suite-name> component keeps
- # multiple suites in one run from colliding under a shared root. The
- # guard makes this idempotent: local configs are deep-copied from their
- # parent (already relocated) and finish()ed again, which must not relocate
- # a second time.
+ # caller-specified root, so %t/Output/.lit_test_times.txt/etc. resolve
+ # under <root>/<suite-name> instead of the build tree.
if litConfig.test_output_root and not getattr(
self, "_test_output_root_applied", False
):
diff --git a/llvm/utils/lit/lit/cl_arguments.py b/llvm/utils/lit/lit/cl_arguments.py
index fd4acc15dd086..11672e3d9eb39 100644
--- a/llvm/utils/lit/lit/cl_arguments.py
+++ b/llvm/utils/lit/lit/cl_arguments.py
@@ -373,15 +373,7 @@ def parse_args():
metavar="DIR",
default=None,
help="Write all test output under DIR instead of each test suite's "
- "default test_exec_root. Each suite's exec root becomes DIR/<suite-name>, "
- "so %%t/%%T/Output and .lit_test_times.txt resolve under DIR (a relative "
- "DIR is resolved against the current directory). This lets test output "
- "live outside the build tree, and lets multiple lit runs use the same "
- "tests concurrently against a single build tree (e.g. test stress runs) "
- "without clobbering each other -- give each concurrent run a distinct DIR "
- "(from a PID, CI job id, per-run temp dir, etc.). Reusing a DIR reuses "
- "that output tree, preserving artifacts like .lit_test_times.txt across "
- "runs. [Default: disabled]",
+ "default test_exec_root.",
)
execution_group.add_argument(
"--ignore-fail",
diff --git a/llvm/utils/lit/tests/test-output-root.py b/llvm/utils/lit/tests/test-output-root.py
index cc81c40be098d..dff63f9b1bf0d 100644
--- a/llvm/utils/lit/tests/test-output-root.py
+++ b/llvm/utils/lit/tests/test-output-root.py
@@ -1,27 +1,22 @@
# Check that --test-output-root relocates each suite's writable output tree to
-# <root>/<suite-name>, so %t/%T/Output resolve under the given root directory
-# instead of the suite's build test_exec_root. This lets test output live
-# outside the build tree and lets multiple lit runs share one build tree without
-# clobbering each other; reusing a root reuses the tree.
+# <root>/<suite-name>, so %t/Output/etc. resolve under the given root directory
+# instead of the suite's build test_exec_root.
#
-# The passed exec root uses a distinctive "execroot" directory name (rather than
-# "build") so the ROOTED-NOT check below can't be fooled by an ambient "/build/"
-# segment in %t (e.g. when the lit test tree itself lives under a build dir).
+# Both cases capture the outer lit's %t as TESTDIR and match the inner temp path
+# against it exactly, so each check fully pins where the output landed.
# With the option, the temp path is under <root>/<suite-name>, not the exec root.
# RUN: rm -rf %t && mkdir -p %t/execroot %t/out
# RUN: %{lit} -a --test-output-root %t/out -Dexec_root=%t/execroot \
# RUN: %{inputs}/test-output-root | \
-# RUN: FileCheck --check-prefix=ROOTED %s
+# RUN: FileCheck --check-prefix=ROOTED %s -DTESTDIR=%t
# Without the option, the temp path stays under the exec root.
# RUN: rm -rf %t && mkdir -p %t/execroot
# RUN: %{lit} -a -Dexec_root=%t/execroot \
# RUN: %{inputs}/test-output-root | \
-# RUN: FileCheck --check-prefix=DEFAULT %s
+# RUN: FileCheck --check-prefix=DEFAULT %s -DTESTDIR=%t
-# ROOTED: TEMP_PATH={{.*}}/out/output-root-suite/{{.*}}Output
-# ROOTED-NOT: TEMP_PATH={{.*}}/execroot/
+# ROOTED: TEMP_PATH=[[TESTDIR]]/out/output-root-suite/{{.*}}Output
-# DEFAULT: TEMP_PATH={{.*}}/execroot/{{.*}}Output
-# DEFAULT-NOT: output-root-suite
+# DEFAULT: TEMP_PATH=[[TESTDIR]]/execroot/{{.*}}Output
>From 06d8655d596ddd45efe2c289507da59602f7a55a Mon Sep 17 00:00:00 2001
From: David Young <davidayoung at meta.com>
Date: Sun, 19 Jul 2026 07:27:47 -0700
Subject: [PATCH 6/6] [lit] Fix test-output-root.py Windows path-separator
mismatch
The ROOTED/DEFAULT FileCheck patterns hardcoded '/' separators, but the
inner test's %t is built via os.path.join/os.path.abspath, which emit
backslashes on Windows (use_normalized_slashes=0). This made FileCheck
fail on the Windows premerge builder. Use the separator-agnostic
{{[\\/]}} regex (as in use-llvm-tool.py) so the checks match both
'/' and '\'.
---
llvm/utils/lit/tests/test-output-root.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/llvm/utils/lit/tests/test-output-root.py b/llvm/utils/lit/tests/test-output-root.py
index dff63f9b1bf0d..fbcb7e52ccec5 100644
--- a/llvm/utils/lit/tests/test-output-root.py
+++ b/llvm/utils/lit/tests/test-output-root.py
@@ -17,6 +17,6 @@
# RUN: %{inputs}/test-output-root | \
# RUN: FileCheck --check-prefix=DEFAULT %s -DTESTDIR=%t
-# ROOTED: TEMP_PATH=[[TESTDIR]]/out/output-root-suite/{{.*}}Output
+# ROOTED: TEMP_PATH=[[TESTDIR]]{{[\\/]}}out{{[\\/]}}output-root-suite{{[\\/]}}{{.*}}Output
-# DEFAULT: TEMP_PATH=[[TESTDIR]]/execroot/{{.*}}Output
+# DEFAULT: TEMP_PATH=[[TESTDIR]]{{[\\/]}}execroot{{[\\/]}}{{.*}}Output
More information about the llvm-commits
mailing list