Skip to content
8 changes: 6 additions & 2 deletions scripts/ci/test_reds.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")

FAILED = (
re.compile(r"✘ Test (.+?\)) (?:recorded an issue|failed)"), # Swift Testing
# Swift Testing: `name()`, or a display name (`@Test("A capped plan …")` prints `✘ Test "A capped plan …"`).
re.compile(r"✘ Test (\"[^\"\n]*\"|.+?\)) (?:recorded an issue|failed)"),
re.compile(r"Test Case '-\[(\S+ \S+)\]' failed"), # XCTest
re.compile(r"^test (\S+) \.\.\. FAILED$"), # cargo test
re.compile(r"^(?:FAIL|ERROR): (\S+ \([^)]+\))"), # unittest
Expand Down Expand Up @@ -293,7 +294,10 @@ def same_error(head: str | None, base: str | None) -> bool:

def log_lines(log: str) -> list[str]:
"""Every line of a job log with its timestamp and colours removed, the runner's step echoes included."""
return [STAMP.sub("", ANSI.sub("", line)).rstrip() for line in log.splitlines()]
# Swift Testing prefixes some event lines with a zero-width space (U+200B) when another writer's output
# shares the stream ("\u200b✔ Suite \"x\" passed"); left in, the anchored suite patterns miss the line and
# a suite that passed reads as never finished (https://github.com/manaflow-ai/cmux/pull/35296).
return [STAMP.sub("", ANSI.sub("", line)).lstrip("\u200b").rstrip() for line in log.splitlines()]


def lines_of(log: str) -> list[str]:
Expand Down
19 changes: 19 additions & 0 deletions tests/test_test_reds.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,17 @@ def test_a_new_failing_test_is_the_heads_own(self):
self.assertTrue(verdict.own)
self.assertIn("pass or do not run on the base: c()", verdict.reasons[0])

def test_display_named_swift_testing_reds_are_compared_by_name(self):
# PR 35296's shard 2: `@Test("…")` names print quoted, with no `()`.
shared = '✘ Test "A capped plan shows used/limit" recorded an issue at X.swift:25:9: Expectation failed'
param = ('✘ Test "Hovering shows help" recorded an issue with 2 arguments activeCount → 1, help → "x" '
'at X.swift:1:2: Expectation failed')
self.assertEqual(reds.read(log(shared, param, SUMMARY)).failed,
{'"A capped plan shows used/limit"', '"Hovering shows help"'})
self.assertFalse(reds.classify(log(shared, SUMMARY), log(shared, SUMMARY)).own)
new = log(shared, '✘ Test "Another" recorded an issue at X.swift:3:4: Expectation failed', SUMMARY)
self.assertIn('"Another"', reds.classify(new, log(shared, SUMMARY)).reasons[0])

def test_a_crash_on_both_sides_is_still_the_heads_own(self):
both = log(failed("a()"), ABORT, "*** Terminating app due to uncaught exception 'NSInternalInconsistencyException'")
verdict = reds.classify(both, both)
Expand Down Expand Up @@ -335,6 +346,14 @@ def test_a_whole_parameterized_run_is_not_cut_off(self):
"✘ Test a() failed after 0.1 seconds with 1 issue.", SUMMARY)
self.assertEqual(reds.read(whole).truncated, [])

def test_a_suite_end_after_a_zero_width_space_is_whole(self):
# PR 35296's app-host shard 2: Swift Testing printed "\u200b✔ Suite ... passed" after interleaved app logs.
whole = log('◇ Suite "Cloud sidebar recorded interaction" started.',
'\u200b✔ Suite "Cloud sidebar recorded interaction" passed after 0.001 seconds.', SUMMARY)
self.assertEqual(reds.read(whole).truncated, [])
cut = log('\u200b◇ Suite "s" started.', SUMMARY)
self.assertIn('1 suite(s) started and never finished: "s"', reds.read(cut).truncated)

def test_a_run_cut_off_between_tests_is_the_heads_own(self):
cut = log("◇ Test run started.", "◇ Suite S started.", "◇ Test a() started.", failed("a()"),
"✘ Test a() failed after 0.1 seconds with 1 issue.")
Expand Down
Loading