Skip to content

Commit 994e487

Browse files
morisilclaude
andauthored
fix: detect CRLF-only string differences and report clear error message (#74)
* fix: detect CRLF-only string differences and report clear error message When two strings differ only in line endings (CRLF vs LF), the diff normalization produces zero hunks, resulting in a confusing empty diff. Now sameAs detects this case early and fails with a descriptive message indicating which side uses CRLF vs LF. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make all CRLF detection branches explicit and tested Split the catch-all else branch into two specific cases: - standalone \r characters (neither string contains \r\n) - mixed CRLF/LF line endings (both strings contain \r\n) Each branch now has a descriptive message and a dedicated test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 41117a6 commit 994e487

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Both developers and AI agents are expected to add entries as they encounter surp
1313

1414
## Known gotchas
1515

16+
## Testing conventions
17+
18+
- Tests use `// given`, `// when`, `// then` comments to structure test cases. Use existing test cases as a reference.
19+
1620
## Anti-patterns to avoid
1721

1822
- Do not add content to this file that is already discoverable by reading the source or build scripts — that inflates context without adding signal, reducing AI agent task success rates (see [arxiv 2602.11988](https://arxiv.org/abs/2602.11988)).

src/commonMain/kotlin/SameAs.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ public infix fun String?.sameAs(expected: String) {
3434
fail("The string is null, but expected to be: $expected")
3535
}
3636

37+
// Detect strings that differ only in line endings (CRLF vs LF).
38+
// The diff normalizes \r away, which would produce zero hunks — confusing the developer.
39+
if (this.replace("\r", "") == expected.replace("\r", "")) {
40+
val expectedCrlf = "\r\n" in expected
41+
val actualCrlf = "\r\n" in this
42+
val detail = when {
43+
expectedCrlf && !actualCrlf -> "expected uses CRLF (\\r\\n), actual uses LF (\\n)"
44+
!expectedCrlf && actualCrlf -> "expected uses LF (\\n), actual uses CRLF (\\r\\n)"
45+
!expectedCrlf && !actualCrlf -> "strings contain standalone carriage return (\\r) characters"
46+
else -> "strings have mixed CRLF (\\r\\n) and LF (\\n) line endings"
47+
}
48+
fail("Strings differ only in line endings: $detail")
49+
}
50+
3751
val diff = generateUnifiedDiff(expected, this)
3852
fail(diff)
3953
}

src/commonTest/kotlin/SameAsTest.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,70 @@ class SameAsTest {
11371137
"line1\nline2\n" sameAs "line1\nline2\n"
11381138
}
11391139

1140+
@Test
1141+
fun `should fail with clear message when actual uses CRLF and expected uses LF`() {
1142+
// given
1143+
val expected = "line1\nline2\n"
1144+
val actual = "line1\r\nline2\r\n"
1145+
1146+
// when
1147+
val error = assertFailsWith<AssertionError> {
1148+
actual sameAs expected
1149+
}
1150+
1151+
// then
1152+
error.message sameAs
1153+
"Strings differ only in line endings: expected uses LF (\\n), actual uses CRLF (\\r\\n)"
1154+
}
1155+
1156+
@Test
1157+
fun `should fail with clear message when expected uses CRLF and actual uses LF`() {
1158+
// given
1159+
val expected = "line1\r\nline2\r\n"
1160+
val actual = "line1\nline2\n"
1161+
1162+
// when
1163+
val error = assertFailsWith<AssertionError> {
1164+
actual sameAs expected
1165+
}
1166+
1167+
// then
1168+
error.message sameAs
1169+
"Strings differ only in line endings: expected uses CRLF (\\r\\n), actual uses LF (\\n)"
1170+
}
1171+
1172+
@Test
1173+
fun `should fail with clear message when strings contain standalone carriage returns`() {
1174+
// given
1175+
val expected = "ab"
1176+
val actual = "a\rb"
1177+
1178+
// when
1179+
val error = assertFailsWith<AssertionError> {
1180+
actual sameAs expected
1181+
}
1182+
1183+
// then
1184+
error.message sameAs
1185+
"Strings differ only in line endings: strings contain standalone carriage return (\\r) characters"
1186+
}
1187+
1188+
@Test
1189+
fun `should fail with clear message when strings have mixed CRLF and LF line endings`() {
1190+
// given
1191+
val expected = "line1\r\nline2\nline3\n"
1192+
val actual = "line1\nline2\r\nline3\n"
1193+
1194+
// when
1195+
val error = assertFailsWith<AssertionError> {
1196+
actual sameAs expected
1197+
}
1198+
1199+
// then
1200+
error.message sameAs
1201+
"Strings differ only in line endings: strings have mixed CRLF (\\r\\n) and LF (\\n) line endings"
1202+
}
1203+
11401204
@Test
11411205
fun `should fail and report difference with control characters`() {
11421206
// given

0 commit comments

Comments
 (0)