cb424d7448
verify-patch-sanity.py validates every active recipe .patch has internally- consistent hunk line counts — catching the 'malformed patch at line N' failure at commit/CI/preflight time instead of hours into a cook. This cycle hit that class three times (qtwaylandscanner, sddm, xwayland), each only discovered when cookbook tried to apply the patch. Running it across the repo found 29 latent malformed patches (validated against GNU patch: e.g. relibc/P3-sysv-ipc reproduces 'malformed patch at line 22'). They were harmless only because they sit in vendored recipes (baked, not re- applied) — but would fail on any version-bump re-derivation. --fix recounts the hunk headers (body untouched) and repaired all 29. Wired into build-preflight.sh (Phase 1.0D) and redbear-ci.yml, with a unit test (test-patch-sanity.sh). Skips archived/legacy trees and unvalidatable formats (empty placeholders, bare-@@ git hunks).
153 lines
5.6 KiB
Python
153 lines
5.6 KiB
Python
import collections
|
|
import os
|
|
import re
|
|
import operator
|
|
|
|
import lit.Test
|
|
import lit.TestRunner
|
|
import lit.util
|
|
from lit.formats.base import TestFormat
|
|
|
|
|
|
class LLDBTest(TestFormat):
|
|
def __init__(self, dotest_cmd):
|
|
self.dotest_cmd = dotest_cmd
|
|
|
|
def getTestsInDirectory(self, testSuite, path_in_suite, litConfig, localConfig):
|
|
source_path = testSuite.getSourcePath(path_in_suite)
|
|
for filename in os.listdir(source_path):
|
|
# Ignore dot files and excluded tests.
|
|
if filename.startswith(".") or filename in localConfig.excludes:
|
|
continue
|
|
|
|
# Ignore files that don't start with 'Test'.
|
|
if not filename.startswith("Test"):
|
|
continue
|
|
|
|
filepath = os.path.join(source_path, filename)
|
|
if not os.path.isdir(filepath):
|
|
base, ext = os.path.splitext(filename)
|
|
if ext in localConfig.suffixes:
|
|
yield lit.Test.Test(
|
|
testSuite, path_in_suite + (filename,), localConfig
|
|
)
|
|
|
|
def execute(self, test, litConfig):
|
|
if litConfig.noExecute:
|
|
return lit.Test.PASS, ""
|
|
|
|
if not getattr(test.config, "lldb_enable_python", False):
|
|
return (lit.Test.UNSUPPORTED, "Python module disabled")
|
|
|
|
if test.config.unsupported:
|
|
return (lit.Test.UNSUPPORTED, "Test is unsupported")
|
|
|
|
testPath, testFile = os.path.split(test.getSourcePath())
|
|
|
|
# The Python used to run lit can be different from the Python LLDB was
|
|
# build with.
|
|
executable = test.config.python_executable
|
|
|
|
isLuaTest = testFile == test.config.lua_test_entry
|
|
|
|
# On Windows, the system does not always correctly interpret
|
|
# shebang lines. To make sure we can execute the tests, add
|
|
# python exe as the first parameter of the command.
|
|
cmd = [executable] + self.dotest_cmd + [testPath, "-p", testFile]
|
|
|
|
if isLuaTest:
|
|
cmd.extend(["--env", "LUA_EXECUTABLE=%s" % test.config.lua_executable])
|
|
cmd.extend(["--env", "LLDB_LUA_CPATH=%s" % test.config.lldb_lua_cpath])
|
|
|
|
timeoutInfo = None
|
|
try:
|
|
out, err, exitCode = lit.util.executeCommand(
|
|
cmd,
|
|
env=test.config.environment,
|
|
timeout=litConfig.maxIndividualTestTime,
|
|
)
|
|
except lit.util.ExecuteCommandTimeoutException as e:
|
|
out = e.out
|
|
err = e.err
|
|
exitCode = e.exitCode
|
|
timeoutInfo = "Reached timeout of {} seconds".format(
|
|
litConfig.maxIndividualTestTime
|
|
)
|
|
|
|
output = """Script:\n--\n%s\n--\nExit Code: %d\n""" % (" ".join(cmd), exitCode)
|
|
if timeoutInfo is not None:
|
|
output += """Timeout: %s\n""" % (timeoutInfo,)
|
|
output += "\n"
|
|
|
|
if out:
|
|
output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
|
|
if err:
|
|
output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
|
|
|
|
if timeoutInfo:
|
|
return lit.Test.TIMEOUT, output
|
|
|
|
# Parse the dotest output from stderr. First get the # of total tests, in order to infer the # of passes.
|
|
# Example: "Ran 5 tests in 0.042s"
|
|
num_ran_regex = r"^Ran (\d+) tests? in "
|
|
num_ran_results = re.search(num_ran_regex, err, re.MULTILINE)
|
|
|
|
# If parsing fails mark this test as unresolved.
|
|
if not num_ran_results:
|
|
return lit.Test.UNRESOLVED, output
|
|
num_ran = int(num_ran_results.group(1))
|
|
|
|
# Then look for a detailed summary, which is OK or FAILED followed by optional details.
|
|
# Example: "OK (skipped=1, expected failures=1)"
|
|
# Example: "FAILED (failures=3)"
|
|
# Example: "OK"
|
|
result_regex = r"^(?:OK|FAILED)(?: \((.*)\))?\r?$"
|
|
results = re.search(result_regex, err, re.MULTILINE)
|
|
|
|
# If parsing fails mark this test as unresolved.
|
|
if not results:
|
|
return lit.Test.UNRESOLVED, output
|
|
|
|
details = results.group(1)
|
|
parsed_details = collections.defaultdict(int)
|
|
if details:
|
|
for detail in details.split(", "):
|
|
detail_parts = detail.split("=")
|
|
if len(detail_parts) != 2:
|
|
return lit.Test.UNRESOLVED, output
|
|
parsed_details[detail_parts[0]] = int(detail_parts[1])
|
|
|
|
failures = parsed_details["failures"]
|
|
errors = parsed_details["errors"]
|
|
skipped = parsed_details["skipped"]
|
|
expected_failures = parsed_details["expected failures"]
|
|
unexpected_successes = parsed_details["unexpected successes"]
|
|
|
|
non_pass = (
|
|
failures + errors + skipped + expected_failures + unexpected_successes
|
|
)
|
|
passes = num_ran - non_pass
|
|
|
|
if exitCode:
|
|
# Mark this test as FAIL if at least one test failed.
|
|
if failures > 0:
|
|
return lit.Test.FAIL, output
|
|
lit_results = [
|
|
(failures, lit.Test.FAIL),
|
|
(errors, lit.Test.UNRESOLVED),
|
|
(unexpected_successes, lit.Test.XPASS),
|
|
]
|
|
else:
|
|
# Mark this test as PASS if at least one test passed.
|
|
if passes > 0:
|
|
return lit.Test.PASS, output
|
|
lit_results = [
|
|
(passes, lit.Test.PASS),
|
|
(skipped, lit.Test.UNSUPPORTED),
|
|
(expected_failures, lit.Test.XFAIL),
|
|
]
|
|
|
|
# Return the lit result code with the maximum occurrence. Only look at
|
|
# the first element and rely on the original order to break ties.
|
|
return max(lit_results, key=operator.itemgetter(0))[1], output
|