diff --git a/.claude/skills/fix-issue/scripts/lib/platform-apple.sh b/.claude/skills/fix-issue/scripts/lib/platform-apple.sh index 581b1e4160..20c9b1320a 100644 --- a/.claude/skills/fix-issue/scripts/lib/platform-apple.sh +++ b/.claude/skills/fix-issue/scripts/lib/platform-apple.sh @@ -223,7 +223,7 @@ apply_tally() { # Build errors with their file and line, which the result bundle keeps even when the log interleaves them. report_build_errors() { local errors - errors="$(python3 "$XCRESULT_CASES" errors "$1" | head -12)" + errors="$(python3 "$XCRESULT_CASES" errors --limit 12 "$1")" [ -n "$errors" ] || return 1 note "errors:" note "$(sed 's/^/ /' <<< "$errors")" diff --git a/scripts/lib/test_xcresult_cases.py b/scripts/lib/test_xcresult_cases.py index 3e0148175f..85caeb2182 100644 --- a/scripts/lib/test_xcresult_cases.py +++ b/scripts/lib/test_xcresult_cases.py @@ -175,6 +175,34 @@ def test_no_build_results(self): self.assertEqual(list(parser.build_errors(None)), []) +class ErrorsCommandTests(unittest.TestCase): + def setUp(self): + self.original = parser.xcresulttool + parser.xcresulttool = lambda bundle, *arguments: BROKEN_BUILD if arguments == ("build-results",) else None + + def tearDown(self): + parser.xcresulttool = self.original + + def errors(self, *arguments): + with redirect_stdout(io.StringIO()) as out: + code = parser.main(["errors", *arguments]) + return code, out.getvalue().splitlines() + + def test_prints_file_name_line_and_message(self): + self.assertEqual(self.errors("Build.xcresult"), (0, [ + "Broken.swift:4: Cannot convert value of type 'String' to specified type 'Int'", + "Linker command failed with exit code 1", + ])) + + def test_limit_stops_after_n_errors(self): + self.assertEqual(self.errors("--limit", "1", "Build.xcresult"), + (0, ["Broken.swift:4: Cannot convert value of type 'String' to specified type 'Int'"])) + + def test_a_bad_limit_is_a_usage_error(self): + with redirect_stderr(io.StringIO()): + self.assertEqual(self.errors("--limit", "x", "Build.xcresult")[0], 2) + + class XunitTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory() diff --git a/scripts/lib/xcresult-cases.py b/scripts/lib/xcresult-cases.py index 8004a2f832..14283a4410 100755 --- a/scripts/lib/xcresult-cases.py +++ b/scripts/lib/xcresult-cases.py @@ -4,8 +4,8 @@ Usage: xcresult-cases.py tally [--filters ] ... verify.sh's notes, then " " - xcresult-cases.py errors - build errors as ":: " + xcresult-cases.py errors [--limit ] + the first n build errors as ":: " xcresult-cases.py summary ... the CI step summary; exits 1 when a bundle or a swift test run executed no test, which xcodebuild and swift test both call success @@ -21,6 +21,7 @@ import urllib.parse import xml.etree.ElementTree as ET from collections import Counter, namedtuple +from itertools import islice from pathlib import Path Case = namedtuple("Case", "target suite name label result retried seconds message") @@ -252,10 +253,15 @@ def main(arguments): filters, rest = read_filters(rest[1]), rest[2:] print("\n".join(tally([case for argument in rest for case in cases_of(argument)], filters))) return 0 - if command == "errors" and len(rest) == 1: - for where, message in build_errors(xcresulttool(Path(rest[0]), "build-results")): - print(f"{Path(where).name}: {message}" if where else message) - return 0 + if command == "errors": + limit = None + if rest[:1] == ["--limit"] and len(rest) > 1 and rest[1].isdigit(): + limit, rest = int(rest[1]), rest[2:] + if len(rest) == 1: + errors = islice(build_errors(xcresulttool(Path(rest[0]), "build-results")), limit) + for where, message in errors: + print(f"{Path(where).name}: {message}" if where else message) + return 0 if command == "summary": destination = os.environ.get("GITHUB_STEP_SUMMARY") if not destination: