Skip to content

Fix test name comparison for names containing newlines#735

Open
pdelagrave wants to merge 2 commits intoEnricoMi:masterfrom
pdelagrave:fix/newline-in-test-names
Open

Fix test name comparison for names containing newlines#735
pdelagrave wants to merge 2 commits intoEnricoMi:masterfrom
pdelagrave:fix/newline-in-test-names

Conversation

@pdelagrave
Copy link

Problem

Parameterized tests (e.g. JUnit 5 @ValueSource with multi-line strings, or @CsvSource with embedded \n) produce test names containing literal newline characters in the JUnit XML <testcase name="..."> attribute.

These names are serialized into check run annotations by joining with '\n' (get_test_list_annotation) and deserialized by splitting on '\n' (get_test_list_from_annotation). When a test name itself contains a newline, the split produces phantom fragments that don't match real test names, causing spurious "removes X and adds Y tests" counts in PR comments -- even when no tests actually changed.

For example, a JUnit parameterized test with name [1] pattern=/core/*\n!/core/*/ gets split into two fragments: [1] pattern=/core/* and !/core/*/, both counted as separate "tests" in the comparison.

Solution

Escape newlines (as \n) and backslashes (as \\) in test names before serialization, and unescape after deserialization. This is backwards compatible: old annotations without escaping deserialize correctly since unescape_test_name is a no-op on strings that don't contain escape sequences.

Changes

  • python/publish/__init__.py: Added escape_test_name() / unescape_test_name() and applied escaping in get_test_list_annotation()
  • python/publish/publisher.py: Applied unescaping in get_test_list_from_annotation()
  • Tests: Added unit tests for escape/unescape, annotation serialization with newlines, and a round-trip test that serializes and deserializes test names containing newlines

Parameterized tests (e.g. JUnit 5 @valuesource with multi-line strings)
can produce test names containing literal newlines. These names were
serialized into annotations by joining with '\n' and deserialized by
splitting on '\n', which broke multi-line names into fragments and
caused spurious added/removed test counts in PR comments.

Escape newlines (and backslashes) in test names before serialization,
and unescape after deserialization. This preserves backwards
compatibility: old annotations without escaping deserialize correctly
since unescape is a no-op on strings without escape sequences.
Copy link
Owner

@EnricoMi EnricoMi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this. What will happen if the fixed action reads in existing annotations that already contain an escaped newline?

Comment on lines +951 to +969
"""Reverse escape_test_name, handling escape sequences in a single pass."""
result = []
i = 0
while i < len(name):
if name[i] == '\\' and i + 1 < len(name):
c = name[i + 1]
if c == 'n':
result.append('\n')
i += 2
elif c == '\\':
result.append('\\')
i += 2
else:
result.append('\\')
i += 1
else:
result.append(name[i])
i += 1
return ''.join(result)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simply reverse using replace again?

Suggested change
"""Reverse escape_test_name, handling escape sequences in a single pass."""
result = []
i = 0
while i < len(name):
if name[i] == '\\' and i + 1 < len(name):
c = name[i + 1]
if c == 'n':
result.append('\n')
i += 2
elif c == '\\':
result.append('\\')
i += 2
else:
result.append('\\')
i += 1
else:
result.append(name[i])
i += 1
return ''.join(result)
"""Reverse escape_test_name."""
return name.replace('\\n', '\n').replace('\\\\', '\\')


def escape_test_name(name: str) -> str:
"""Escape newlines in test names so they survive newline-delimited serialization."""
return name.replace('\\', '\\\\').replace('\n', '\\n')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may also escape "\r" while we are here.

Old annotations stored test names without escaping, so a test name
containing literal backslash-n (e.g. from a Java string "path\\nname")
would be incorrectly unescaped into a real newline by the new reader.

Prepend a "v2" header line to raw_details when writing. On read, only
unescape if the header is present; otherwise treat as legacy format
and split without unescaping. This preserves exact backwards
compatibility with old annotations.

Also account for the header size in chunk budget calculations.
@pdelagrave
Copy link
Author

Thanks for fixing this. What will happen if the fixed action reads in existing annotations that already contain an escaped newline?

Addressed in commit 8d06be0

@github-actions
Copy link

Test Results (reference)

      168 files  ±    0        168 suites  ±0   1h 4m 36s ⏱️ + 1m 18s
      451 tests +    7        451 ✅ +    7    0 💤 ±0  0 ❌ ±0 
1 223 880 runs  +1 176  1 223 208 ✅ +1 176  672 💤 ±0  0 ❌ ±0 

Results for commit 8d06be0. ± Comparison against base commit f3a4c8e.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants