[llvm] [llvm-lit] Ignore malformed `.lit_test_times` entries (PR #191305)

Jonathan Thackray via llvm-commits llvm-commits at lists.llvm.org
Fri Apr 17 08:31:36 PDT 2026


https://github.com/jthackray updated https://github.com/llvm/llvm-project/pull/191305

>From f2efef36d605c3bc4a375c82b0170f8809b32c6f Mon Sep 17 00:00:00 2001
From: Jonathan Thackray <jonathan.thackray at arm.com>
Date: Thu, 9 Apr 2026 21:58:08 +0100
Subject: [PATCH 1/2] [llvm-lit] Ignore malformed `.lit_test_times` entries

When running `llvm-lit`, I sometimes hit a traceback, because a
`.lit_test_times.txt` file has got corrupted (not sure how).
However, it's non-obvious what the issue is (you just get a traceback),
so I've fixed this as follows:

`read_test_times()` currently assumes every line in .lit_test_times.txt
contains a floating-point time followed by a test path. If the file
contains a blank line, a line without a path, or a non-numeric time,
lit raises during discovery instead of skipping the bad entry.

Fix this by parsing each line defensively. Split once, ignore lines
that do not produce both fields, and ignore entries whose time cannot
be parsed as a float. This keeps the existing behavior for valid lines
while making malformed timing files non-fatal.

Add a new `malformed-test-times.py` regression test and a checked-in
lit_test_times fixture containing both valid and malformed entries. The
test copies that fixture into `.lit_test_times.txt`, checks that lit still
discovers and runs both tests, and verifies that the valid timing entries
still order `b.txt` before `a.txt`.
---
 llvm/utils/lit/lit/TestTimes.py                      | 10 ++++++++--
 .../lit/tests/Inputs/malformed-test-times/a.txt      |  1 +
 .../lit/tests/Inputs/malformed-test-times/b.txt      |  1 +
 .../lit/tests/Inputs/malformed-test-times/lit.cfg    |  7 +++++++
 .../tests/Inputs/malformed-test-times/lit_test_times |  5 +++++
 llvm/utils/lit/tests/malformed-test-times.py         | 12 ++++++++++++
 6 files changed, 34 insertions(+), 2 deletions(-)
 create mode 100644 llvm/utils/lit/tests/Inputs/malformed-test-times/a.txt
 create mode 100644 llvm/utils/lit/tests/Inputs/malformed-test-times/b.txt
 create mode 100644 llvm/utils/lit/tests/Inputs/malformed-test-times/lit.cfg
 create mode 100644 llvm/utils/lit/tests/Inputs/malformed-test-times/lit_test_times
 create mode 100644 llvm/utils/lit/tests/malformed-test-times.py

diff --git a/llvm/utils/lit/lit/TestTimes.py b/llvm/utils/lit/lit/TestTimes.py
index 7ec1a1b97b717..79ac5c3120080 100644
--- a/llvm/utils/lit/lit/TestTimes.py
+++ b/llvm/utils/lit/lit/TestTimes.py
@@ -9,8 +9,14 @@ def read_test_times(suite):
     if os.path.exists(test_times_file):
         with open(test_times_file, "r") as time_file:
             for line in time_file:
-                time, path = line.split(maxsplit=1)
-                test_times[path.strip("\n")] = float(time)
+                fields = line.split(maxsplit=1)
+                if len(fields) != 2:
+                    continue
+                time, path = fields
+                try:
+                    test_times[path.strip("\n")] = float(time)
+                except ValueError:
+                    continue
     return test_times
 
 
diff --git a/llvm/utils/lit/tests/Inputs/malformed-test-times/a.txt b/llvm/utils/lit/tests/Inputs/malformed-test-times/a.txt
new file mode 100644
index 0000000000000..b80b60b7a2794
--- /dev/null
+++ b/llvm/utils/lit/tests/Inputs/malformed-test-times/a.txt
@@ -0,0 +1 @@
+# RUN: true
diff --git a/llvm/utils/lit/tests/Inputs/malformed-test-times/b.txt b/llvm/utils/lit/tests/Inputs/malformed-test-times/b.txt
new file mode 100644
index 0000000000000..b80b60b7a2794
--- /dev/null
+++ b/llvm/utils/lit/tests/Inputs/malformed-test-times/b.txt
@@ -0,0 +1 @@
+# RUN: true
diff --git a/llvm/utils/lit/tests/Inputs/malformed-test-times/lit.cfg b/llvm/utils/lit/tests/Inputs/malformed-test-times/lit.cfg
new file mode 100644
index 0000000000000..0558595a8c305
--- /dev/null
+++ b/llvm/utils/lit/tests/Inputs/malformed-test-times/lit.cfg
@@ -0,0 +1,7 @@
+import lit.formats
+
+config.name = "malformed-test-times"
+config.suffixes = [".txt"]
+config.test_format = lit.formats.ShTest()
+config.test_source_root = None
+config.test_exec_root = None
diff --git a/llvm/utils/lit/tests/Inputs/malformed-test-times/lit_test_times b/llvm/utils/lit/tests/Inputs/malformed-test-times/lit_test_times
new file mode 100644
index 0000000000000..28c0b7da7aba0
--- /dev/null
+++ b/llvm/utils/lit/tests/Inputs/malformed-test-times/lit_test_times
@@ -0,0 +1,5 @@
+4.798174e-03 b.txt
+missing-path
+
+not-a-float ignored.txt
+1.412153e-03 a.txt
diff --git a/llvm/utils/lit/tests/malformed-test-times.py b/llvm/utils/lit/tests/malformed-test-times.py
new file mode 100644
index 0000000000000..9426e4d684401
--- /dev/null
+++ b/llvm/utils/lit/tests/malformed-test-times.py
@@ -0,0 +1,12 @@
+## Check that malformed .lit_test_times.txt lines do not crash discovery.
+##
+## The valid timing entries should still be honored for smart ordering.
+
+# RUN: cp %{inputs}/malformed-test-times/lit_test_times %{inputs}/malformed-test-times/.lit_test_times.txt
+# RUN: %{lit-no-order-opt} %{inputs}/malformed-test-times > %t.out
+# RUN: FileCheck < %t.out %s
+
+# CHECK: -- Testing: 2 tests, 1 workers --
+# CHECK-NEXT: PASS: malformed-test-times :: b.txt
+# CHECK-NEXT: PASS: malformed-test-times :: a.txt
+# CHECK: Passed: 2

>From c3e3c65de781a1f5b62690eb8d73fc342ca49545 Mon Sep 17 00:00:00 2001
From: Jonathan Thackray <jonathan.thackray at arm.com>
Date: Fri, 17 Apr 2026 16:23:30 +0100
Subject: [PATCH 2/2] fixup! Address PR comments

---
 llvm/utils/lit/lit/Test.py                   |  4 ++--
 llvm/utils/lit/lit/TestTimes.py              | 25 +++++++++++++++++---
 llvm/utils/lit/lit/discovery.py              |  2 +-
 llvm/utils/lit/tests/malformed-test-times.py |  6 ++++-
 4 files changed, 30 insertions(+), 7 deletions(-)

diff --git a/llvm/utils/lit/lit/Test.py b/llvm/utils/lit/lit/Test.py
index 23fc61756afcf..daba14a898c4c 100644
--- a/llvm/utils/lit/lit/Test.py
+++ b/llvm/utils/lit/lit/Test.py
@@ -222,14 +222,14 @@ class TestSuite:
     A test suite groups together a set of logically related tests.
     """
 
-    def __init__(self, name, source_root, exec_root, config):
+    def __init__(self, name, source_root, exec_root, config, lit_config=None):
         self.name = name
         self.source_root = source_root
         self.exec_root = exec_root
         # The test suite configuration.
         self.config = config
 
-        self.test_times = read_test_times(self)
+        self.test_times = read_test_times(self, lit_config)
 
     def getSourcePath(self, components):
         return os.path.join(self.source_root, *components)
diff --git a/llvm/utils/lit/lit/TestTimes.py b/llvm/utils/lit/lit/TestTimes.py
index 79ac5c3120080..4e875dba1cca8 100644
--- a/llvm/utils/lit/lit/TestTimes.py
+++ b/llvm/utils/lit/lit/TestTimes.py
@@ -1,22 +1,41 @@
 import os
 
 
-def read_test_times(suite):
-    test_times = {}
+def _get_test_times_path(suite):
     test_times_file = os.path.join(suite.exec_root, ".lit_test_times.txt")
     if not os.path.exists(test_times_file):
         test_times_file = os.path.join(suite.source_root, ".lit_test_times.txt")
+    return test_times_file
+
+
+def read_test_times(suite, lit_config=None):
+    test_times = {}
+    test_times_file = _get_test_times_path(suite)
     if os.path.exists(test_times_file):
+        malformed_data = False
         with open(test_times_file, "r") as time_file:
             for line in time_file:
                 fields = line.split(maxsplit=1)
                 if len(fields) != 2:
+                    malformed_data = True
                     continue
                 time, path = fields
                 try:
                     test_times[path.strip("\n")] = float(time)
                 except ValueError:
+                    malformed_data = True
                     continue
+        if malformed_data and lit_config:
+            warned_files = getattr(lit_config, "_warned_test_times_files", None)
+            if warned_files is None:
+                warned_files = lit_config._warned_test_times_files = set()
+            if test_times_file in warned_files:
+                return test_times
+            warned_files.add(test_times_file)
+            lit_config.warning(
+                "ignoring malformed timing data in %r; remove the file to "
+                "regenerate it" % (test_times_file,)
+            )
     return test_times
 
 
@@ -27,7 +46,7 @@ def record_test_times(tests, lit_config):
         if t.result.elapsed is None:
             continue
         if not t.suite.exec_root in times_by_suite:
-            times_by_suite[t.suite.exec_root] = read_test_times(t.suite)
+            times_by_suite[t.suite.exec_root] = read_test_times(t.suite, lit_config)
 
         # Mark the elapsed time for failed tests as negative so LIT can distingiush failed from
         # successful test runs just based on the time value. For this heuristic to work for tests
diff --git a/llvm/utils/lit/lit/discovery.py b/llvm/utils/lit/lit/discovery.py
index ac06223b45345..bede6300fb031 100644
--- a/llvm/utils/lit/lit/discovery.py
+++ b/llvm/utils/lit/lit/discovery.py
@@ -68,7 +68,7 @@ def search1(path):
         cfg.load_from_path(cfgpath, litConfig)
         source_root = util.abs_path_preserve_drive(cfg.test_source_root or path)
         exec_root = util.abs_path_preserve_drive(cfg.test_exec_root or path)
-        return Test.TestSuite(cfg.name, source_root, exec_root, cfg), ()
+        return Test.TestSuite(cfg.name, source_root, exec_root, cfg, litConfig), ()
 
     def search(path):
         # Check for an already instantiated test suite.
diff --git a/llvm/utils/lit/tests/malformed-test-times.py b/llvm/utils/lit/tests/malformed-test-times.py
index 9426e4d684401..a902ab73c5a54 100644
--- a/llvm/utils/lit/tests/malformed-test-times.py
+++ b/llvm/utils/lit/tests/malformed-test-times.py
@@ -3,10 +3,14 @@
 ## The valid timing entries should still be honored for smart ordering.
 
 # RUN: cp %{inputs}/malformed-test-times/lit_test_times %{inputs}/malformed-test-times/.lit_test_times.txt
-# RUN: %{lit-no-order-opt} %{inputs}/malformed-test-times > %t.out
+# RUN: %{lit-no-order-opt} %{inputs}/malformed-test-times > %t.out 2> %t.err
 # RUN: FileCheck < %t.out %s
+# RUN: FileCheck --check-prefix=ERR < %t.err %s
 
 # CHECK: -- Testing: 2 tests, 1 workers --
 # CHECK-NEXT: PASS: malformed-test-times :: b.txt
 # CHECK-NEXT: PASS: malformed-test-times :: a.txt
 # CHECK: Passed: 2
+
+# ERR: warning: ignoring malformed timing data in
+# ERR-SAME: ; remove the file to regenerate it



More information about the llvm-commits mailing list