[lldb] [llvm] [lldb] Introduce the require decorator (PR #212753)
Charles Zablit via llvm-commits
llvm-commits at lists.llvm.org
Fri Jul 31 05:03:56 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/212753
>From 21f517edcb842901e073f0c279855cf4168a2d6c Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Fri, 31 Jul 2026 14:03:40 +0200
Subject: [PATCH] [lldb] Introduce the require decorator
---
lldb/docs/resources/test.md | 29 +++++
.../Python/lldbsuite/test/decorators.py | 122 ++++++++++++++++++
lldb/packages/Python/lldbsuite/test/dotest.py | 6 +
.../Python/lldbsuite/test/skip_reason.py | 17 +++
.../Python/lldbsuite/test/test_result.py | 21 ++-
lldb/test/API/lldbtest.py | 13 +-
lldb/test/API/windows/conpty/TestConPTY.py | 8 +-
.../debug-heap/TestWindowsDebugHeap.py | 2 +-
.../launch/missing-dll/TestMissingDll.py | 2 +-
.../launch/replace-dll/TestReplaceDLL.py | 2 +-
.../windows/msvcrt/TestMSVCRTCException.py | 2 +-
.../test/API/windows/thread/TestThreadName.py | 2 +-
llvm/utils/lit/lit/reports.py | 2 +-
13 files changed, 213 insertions(+), 15 deletions(-)
create mode 100644 lldb/packages/Python/lldbsuite/test/skip_reason.py
diff --git a/lldb/docs/resources/test.md b/lldb/docs/resources/test.md
index e3c02c25553f0..1277a7a5abb8c 100644
--- a/lldb/docs/resources/test.md
+++ b/lldb/docs/resources/test.md
@@ -137,6 +137,35 @@ the test should be run or not.
@skipTestIfFn(checking_function_name)
```
+### Skipped versus unsupported
+
+A test that doesn't run does so for one of two very different reasons, and the
+decorator you pick says which:
+
+* The test **can never** run in this configuration. A test for Mach-O debug
+ maps has nothing to say on Linux; a test that calls `fork()` has nothing to
+ say on Windows. Use the `require*` family. These are reported as
+ **UNSUPPORTED**.
+
+* The test **ought to** run in this configuration but doesn't work yet. Use the
+ `skipIf*` / `skipUnless*` family. These are reported as **SKIPPED**, which
+ keeps them visible as work still to be done.
+
+```python
+ at requireDarwin # inherently Darwin-only: reported UNSUPPORTED elsewhere
+ at skipIfWindows # ought to work on Windows, currently broken: reported SKIPPED
+```
+
+The `require*` decorators mirror the `skip*` ones one-for-one:
+`requireDarwin` / `requireNotDarwin`, `requireLinux` / `requireNotLinux`,
+`requireWindows` / `requireNotWindows`, plus `requirePOSIX`, `requireSignals`,
+`requireNotWasm`, `requireDarwinHost`, and the general
+`requirePlatform(oslist)` / `requireNotPlatform(oslist)`.
+
+Reach for `require*` when the test is tied to a platform-specific file format,
+API, or OS feature. If the test is merely untested or broken somewhere, keep
+`skipIf*` so nobody mistakes a bug for a design decision.
+
In addition to providing a lot more flexibility when it comes to writing the
test, the API test also allow for much more complex scenarios when it comes to
building inferiors. Every test has its own `Makefile`, most of them only a
diff --git a/lldb/packages/Python/lldbsuite/test/decorators.py b/lldb/packages/Python/lldbsuite/test/decorators.py
index 50eec24120c4c..906ade7d63fa3 100644
--- a/lldb/packages/Python/lldbsuite/test/decorators.py
+++ b/lldb/packages/Python/lldbsuite/test/decorators.py
@@ -30,6 +30,7 @@
from lldbsuite.test import lldbplatform
from lldbsuite.test import lldbplatformutil
from lldbsuite.test.cpu_feature import CPUFeature
+from lldbsuite.test.skip_reason import UnsupportedReason
class DecorateMode:
@@ -1123,6 +1124,127 @@ def skipUnlessPlatform(oslist):
)
+##############################################################################
+# Platform *requirement* decorators.
+#
+# These express "this test can only ever run here", as opposed to the skipIf /
+# skipUnless family which means "this test ought to run here but is broken".
+# Tests turned off by a `require*` decorator are reported as UNSUPPORTED; tests
+# turned off by a `skip*` decorator are reported as SKIPPED. See
+# `lldbsuite.test.skip_reason` for how the distinction is carried.
+#
+# Reach for these when the test is inherently tied to a platform: it debugs a
+# platform-specific file format, drives a platform-specific API, or exercises
+# an OS feature that simply doesn't exist elsewhere. If the test is merely
+# untested or broken somewhere, keep using skipIf / skipUnless so it stays
+# visible as work to be done.
+##############################################################################
+
+
+def requirePlatform(oslist):
+ """Mark the item as runnable only on the listed target platforms.
+
+ Unlike `skipUnlessPlatform`, other platforms are reported as UNSUPPORTED
+ rather than SKIPPED.
+ """
+ return unittest.skipUnless(
+ lldbplatformutil.getPlatform() in oslist,
+ UnsupportedReason("requires one of %s" % (", ".join(oslist))),
+ )
+
+
+def requireNotPlatform(oslist):
+ """Mark the item as inherently inapplicable to the listed target platforms.
+
+ Unlike `skipIfPlatform`, the listed platforms are reported as UNSUPPORTED
+ rather than SKIPPED.
+ """
+ return unittest.skipIf(
+ lldbplatformutil.getPlatform() in oslist,
+ UnsupportedReason("unsupported on %s" % (", ".join(oslist))),
+ )
+
+
+def requireDarwin(func):
+ """Mark the item as inherently Darwin-only (Mach-O, debug maps, Darwin
+ kernel/runtime APIs, ...). Non-Darwin targets report UNSUPPORTED."""
+ return requirePlatform(lldbplatform.translate(lldbplatform.darwin_all))(func)
+
+
+def requireNotDarwin(func):
+ """Mark the item as inherently inapplicable to Darwin targets."""
+ return requireNotPlatform(lldbplatform.translate(lldbplatform.darwin_all))(func)
+
+
+def requireLinux(func):
+ """Mark the item as inherently Linux-only (procfs, Linux-specific syscalls,
+ ...). Other targets report UNSUPPORTED."""
+ return requirePlatform(["linux"])(func)
+
+
+def requireNotLinux(func):
+ """Mark the item as inherently inapplicable to Linux targets."""
+ return requireNotPlatform(["linux"])(func)
+
+
+def requireWindows(func):
+ """Mark the item as inherently Windows-only (PE/COFF, Win32 APIs, ...).
+ Other targets report UNSUPPORTED."""
+ return requirePlatform(["windows"])(func)
+
+
+def requireNotWindows(func):
+ """Mark the item as inherently inapplicable to Windows targets.
+
+ Use this for tests built on POSIX-only concepts: fork/exec semantics,
+ POSIX signals, ptrace, ELF/Mach-O specifics, shell pipelines, and so on.
+ """
+ return requireNotPlatform(["windows"])(func)
+
+
+def requirePOSIX(func):
+ """Mark the item as requiring a POSIX target.
+
+ A shorthand for `requireNotWindows` that reads better on tests whose
+ dependency is POSIX semantics generally rather than anything about
+ Windows specifically.
+ """
+ return requireNotPlatform(["windows"])(func)
+
+
+def requireSignals(func):
+ """Mark the item as requiring POSIX signal support on the target."""
+ return requireNotPlatform(["windows", "wasip1", "wasi"])(func)
+
+
+def requireNotWasm(func):
+ """Mark the item as inherently inapplicable to WebAssembly targets.
+
+ WebAssembly has no processes, no signals, no shared libraries and no
+ ptrace-style debugging, so a large amount of the test suite can never
+ apply to it.
+ """
+ return requireNotPlatform(["wasip1", "wasi"])(func)
+
+
+def requireHostPlatform(oslist):
+ """Mark the item as runnable only on the listed *host* platforms."""
+ return unittest.skipUnless(
+ lldbplatformutil.getHostPlatform() in oslist,
+ UnsupportedReason("requires one of %s as host" % (", ".join(oslist))),
+ )
+
+
+def requireDarwinHost(func):
+ """Mark the item as requiring a Darwin host, regardless of target.
+
+ Use for tests that drive host-side Darwin facilities: `xcrun`, the
+ simulator runtimes, dsymutil, the LLDB.framework layout, and so on.
+ """
+ return requireHostPlatform(lldbplatform.translate(lldbplatform.darwin_all))(func)
+
+
+
def skipIfTargetDoesNotSupportThreads():
"""Skip tests that require thread support (e.g. pthreads)."""
platform = lldbplatformutil.getPlatform()
diff --git a/lldb/packages/Python/lldbsuite/test/dotest.py b/lldb/packages/Python/lldbsuite/test/dotest.py
index 604fb98b1e2e5..2bd1d085f6ceb 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest.py
@@ -1255,6 +1255,12 @@ def run_suite():
configuration.failed = not result.wasSuccessful()
+ if getattr(result, "skipped", None):
+ sys.stderr.write(
+ "Skip breakdown (unsupported=%d, skipped=%d)\n"
+ % (result.countUnsupported(), result.countSkipped())
+ )
+
if configuration.sdir_has_content and configuration.verbose:
sys.stderr.write(
"Session logs for test failures/errors/unexpected successes"
diff --git a/lldb/packages/Python/lldbsuite/test/skip_reason.py b/lldb/packages/Python/lldbsuite/test/skip_reason.py
new file mode 100644
index 0000000000000..b1aece572f4f3
--- /dev/null
+++ b/lldb/packages/Python/lldbsuite/test/skip_reason.py
@@ -0,0 +1,17 @@
+"""
+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
+
+Distinguishes the two reasons a test can end up not running.
+"""
+
+
+class UnsupportedReason(str):
+ """A skip reason meaning "this test can never run here", not "this test is
+ broken here". Reported as UNSUPPORTED rather than SKIPPED."""
+
+
+def is_unsupported(reason):
+ """Return True if *reason* marks a test as unsupported rather than skipped."""
+ return isinstance(reason, UnsupportedReason)
diff --git a/lldb/packages/Python/lldbsuite/test/test_result.py b/lldb/packages/Python/lldbsuite/test/test_result.py
index 383135d5f67c9..cba6f7206f8da 100644
--- a/lldb/packages/Python/lldbsuite/test/test_result.py
+++ b/lldb/packages/Python/lldbsuite/test/test_result.py
@@ -16,6 +16,7 @@
# LLDB Modules
from . import configuration
+from .skip_reason import UnsupportedReason, is_unsupported
from lldbsuite.test_event import build_exception
@@ -162,10 +163,18 @@ def hardMarkAsSkipped(self, test):
getattr(test, test._testMethodName).__func__.__unittest_skip__ = True
getattr(
test, test._testMethodName
- ).__func__.__unittest_skip_why__ = (
+ ).__func__.__unittest_skip_why__ = UnsupportedReason(
"test case does not fall in any category of interest for this run"
)
+ def countUnsupported(self):
+ """Number of skipped tests that can never run in this configuration."""
+ return sum(1 for _, reason in self.skipped if is_unsupported(reason))
+
+ def countSkipped(self):
+ """Number of skipped tests that ought to run here but don't work yet."""
+ return len(self.skipped) - self.countUnsupported()
+
def checkExclusion(self, exclusion_list, name):
if exclusion_list:
import re
@@ -282,9 +291,13 @@ def addSkip(self, test, reason):
method = getattr(test, "markSkippedTest", None)
if method:
method()
+ # A test turned off by a `require*` decorator can never run in this
+ # configuration, so report it as UNSUPPORTED. Anything else is a test
+ # that ought to run here but doesn't work yet: report it as SKIPPED.
+ status = "UNSUPPORTED" if is_unsupported(reason) else "SKIPPED"
self.stream.write(
- "UNSUPPORTED: LLDB (%s) :: %s (%s) \n"
- % (self._config_string(test), str(test), reason)
+ "%s: LLDB (%s) :: %s (%s) \n"
+ % (status, self._config_string(test), str(test), reason)
)
def addUnexpectedSuccess(self, test):
@@ -302,4 +315,4 @@ def stopTest(self, test):
dumpSessionInfo = getattr(test, "dumpSessionInfo", None)
if dumpSessionInfo:
dumpSessionInfo()
- super().stopTest(test)
+ super().stopTest(test)
\ No newline at end of file
diff --git a/lldb/test/API/lldbtest.py b/lldb/test/API/lldbtest.py
index 6dad5a412a5a5..62952b67bb64f 100644
--- a/lldb/test/API/lldbtest.py
+++ b/lldb/test/API/lldbtest.py
@@ -127,6 +127,15 @@ def execute(self, test, litConfig):
expected_failures = parsed_details["expected failures"]
unexpected_successes = parsed_details["unexpected successes"]
+ only_skipped = 0
+ breakdown = re.search(
+ r"^Skip breakdown \(unsupported=(\d+), skipped=(\d+)\)\r?$",
+ err,
+ re.MULTILINE,
+ )
+ if breakdown:
+ only_skipped = int(breakdown.group(2))
+
non_pass = (
failures + errors + skipped + expected_failures + unexpected_successes
)
@@ -142,9 +151,11 @@ def execute(self, test, litConfig):
return lit.Test.XPASS, output
else:
# Aggregate the tests results with the following precedence:
- # PASS > XFAIL > UNSUPPORTED
+ # PASS > XFAIL > SKIPPED > UNSUPPORTED
if passes > 0:
return lit.Test.PASS, output
if expected_failures > 0:
return lit.Test.XFAIL, output
+ if only_skipped > 0:
+ return lit.Test.SKIPPED, output
return lit.Test.UNSUPPORTED, output
diff --git a/lldb/test/API/windows/conpty/TestConPTY.py b/lldb/test/API/windows/conpty/TestConPTY.py
index f192367a801f6..455263eb0d962 100644
--- a/lldb/test/API/windows/conpty/TestConPTY.py
+++ b/lldb/test/API/windows/conpty/TestConPTY.py
@@ -61,7 +61,7 @@ def _run_to_exit(self, mode):
return process.GetSTDOUT(1 << 20)
- @skipUnlessWindows
+ @requireWindows
@skipUnlessWindowsConPTY2022
@skipIf(oslist=["windows"], archs=["aarch64"], bugnumber="#194069")
def test_stdout_delivery(self):
@@ -72,7 +72,7 @@ def test_stdout_delivery(self):
output = self._strip_output(output)
self.assertIn("Hello from ConPTY\r\n", output)
- @skipUnlessWindows
+ @requireWindows
@skipUnlessWindowsConPTY2022
@skipIf(oslist=["windows"], archs=["aarch64"], bugnumber="#194069")
def test_large_output(self):
@@ -90,7 +90,7 @@ def test_large_output(self):
for i, line in enumerate(output_lines):
self.assertEqual("line %04d" % i, line)
- @skipUnlessWindows
+ @requireWindows
@skipUnlessWindowsConPTY
@skipIf(oslist=["windows"], archs=["aarch64"], bugnumber="#194069")
def test_basic_output_without_vt_check(self):
@@ -107,7 +107,7 @@ def test_basic_output_without_vt_check(self):
stripped = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", output)
self.assertIn("Hello from ConPTY", stripped)
- @skipUnlessWindows
+ @requireWindows
@skipUnlessWindowsConPTY2022
@skipIf(oslist=["windows"], archs=["aarch64"], bugnumber="#194069")
def test_no_screen_clear_on_init(self):
diff --git a/lldb/test/API/windows/debug-heap/TestWindowsDebugHeap.py b/lldb/test/API/windows/debug-heap/TestWindowsDebugHeap.py
index 0d5ba5dc40f13..69bb22da366ea 100644
--- a/lldb/test/API/windows/debug-heap/TestWindowsDebugHeap.py
+++ b/lldb/test/API/windows/debug-heap/TestWindowsDebugHeap.py
@@ -8,7 +8,7 @@
from typing import List
- at skipUnlessWindows
+ at requireWindows
class DebugHeapTestCase(TestBase):
NO_DEBUG_INFO_TESTCASE = True
diff --git a/lldb/test/API/windows/launch/missing-dll/TestMissingDll.py b/lldb/test/API/windows/launch/missing-dll/TestMissingDll.py
index d1e972c7f0bcb..5d51e0bd6b1ee 100644
--- a/lldb/test/API/windows/launch/missing-dll/TestMissingDll.py
+++ b/lldb/test/API/windows/launch/missing-dll/TestMissingDll.py
@@ -6,7 +6,7 @@
@skipIfTargetDoesNotSupportSharedLibraries()
class MissingDllTestCase(TestBase):
- @skipUnlessWindows
+ @requireWindows
def test(self):
"""
Test that lldb reports the application's exit code (STATUS_DLL_NOT_FOUND),
diff --git a/lldb/test/API/windows/launch/replace-dll/TestReplaceDLL.py b/lldb/test/API/windows/launch/replace-dll/TestReplaceDLL.py
index 91111702169ae..83250f1700666 100644
--- a/lldb/test/API/windows/launch/replace-dll/TestReplaceDLL.py
+++ b/lldb/test/API/windows/launch/replace-dll/TestReplaceDLL.py
@@ -10,7 +10,7 @@
class ReplaceDllTestCase(TestBase):
SHARED_BUILD_TESTCASE = False
- @skipUnlessWindows
+ @requireWindows
def test(self):
"""
Test that LLDB unlocks module files once all references are released.
diff --git a/lldb/test/API/windows/msvcrt/TestMSVCRTCException.py b/lldb/test/API/windows/msvcrt/TestMSVCRTCException.py
index 63727c37b1ae3..b379e50d48cbd 100644
--- a/lldb/test/API/windows/msvcrt/TestMSVCRTCException.py
+++ b/lldb/test/API/windows/msvcrt/TestMSVCRTCException.py
@@ -9,7 +9,7 @@
class TestMSVCRTCException(TestBase):
NO_DEBUG_INFO_TESTCASE = True
- @skipUnlessPlatform(["windows"])
+ @requireWindows
@skipUnlessMSVC
def test_msvc_runtime_checks(self):
"""Test that lldb prints MSVC's runtime checks exceptions as stop reasons."""
diff --git a/lldb/test/API/windows/thread/TestThreadName.py b/lldb/test/API/windows/thread/TestThreadName.py
index 590b936191a22..7680790a6d49f 100644
--- a/lldb/test/API/windows/thread/TestThreadName.py
+++ b/lldb/test/API/windows/thread/TestThreadName.py
@@ -9,7 +9,7 @@
class TestThreadName(TestBase):
- @skipUnlessWindows
+ @requireWindows
@skipIfWindows(windows_version=["<", "10.0.14393"])
def test_with_thread_description(self):
"""SBThread.GetName() reflects SetThreadDescription on Windows."""
diff --git a/llvm/utils/lit/lit/reports.py b/llvm/utils/lit/lit/reports.py
index c5b09fe81bb21..4a166e16c500a 100755
--- a/llvm/utils/lit/lit/reports.py
+++ b/llvm/utils/lit/lit/reports.py
@@ -183,7 +183,7 @@ def _get_skip_reason(self, test):
if code == lit.Test.EXCLUDED:
return "Test not selected (--filter, --max-tests)"
if code == lit.Test.SKIPPED:
- return "User interrupt"
+ return "Skipped"
assert code == lit.Test.UNSUPPORTED
features = test.getMissingRequiredFeatures()
More information about the llvm-commits
mailing list