Skip to content

Commit 5c0c779

Browse files
authored
Better error messages (#477)
2 parents 91a7865 + 97de81d commit 5c0c779

File tree

6 files changed

+213
-13
lines changed

6 files changed

+213
-13
lines changed

jvm/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1111
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1212

1313
## [Unreleased]
14+
### Added
15+
- Snapshot mismatch error messages now show a diff of the first mismatched line. ([#477](https://github.com/diffplug/selfie/pull/477))
16+
- before
17+
```
18+
Snapshot mismatch
19+
- update this snapshot by adding `_TODO` to the function name
20+
- update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`
21+
```
22+
- after
23+
```
24+
Snapshot mismatch at L7:C9
25+
-Testing 123
26+
+Testing ABC
27+
‣ update this snapshot by adding `_TODO` to the function name
28+
‣ update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`
29+
```
1430
1531
## [2.3.0] - 2024-07-11
1632
### Added

jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Mode.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.diffplug.selfie
1717

1818
import com.diffplug.selfie.guts.CallStack
1919
import com.diffplug.selfie.guts.CommentTracker
20+
import com.diffplug.selfie.guts.SnapshotNotEqualErrorMsg
2021
import com.diffplug.selfie.guts.SnapshotSystem
2122
import com.diffplug.selfie.guts.TypedPath
2223

@@ -42,13 +43,28 @@ enum class Mode {
4243
internal fun msgSnapshotNotFound() = msg("Snapshot not found")
4344
internal fun msgSnapshotNotFoundNoSuchFile(file: TypedPath) =
4445
msg("Snapshot not found: no such file $file")
45-
internal fun msgSnapshotMismatch() = msg("Snapshot mismatch")
46+
internal fun msgSnapshotMismatch(expected: String, actual: String) =
47+
msg(SnapshotNotEqualErrorMsg.forUnequalStrings(expected, actual))
48+
internal fun msgSnapshotMismatchBinary(expected: ByteArray, actual: ByteArray) =
49+
msgSnapshotMismatch(expected.toQuotedPrintable(), actual.toQuotedPrintable())
50+
private fun ByteArray.toQuotedPrintable(): String {
51+
val sb = StringBuilder()
52+
for (byte in this) {
53+
val b = byte.toInt() and 0xFF // Make sure byte is treated as unsigned
54+
if (b in 33..126 && b != 61) { // Printable ASCII, except '='
55+
sb.append(b.toChar())
56+
} else {
57+
sb.append("=").append(b.toString(16).uppercase().padStart(2, '0')) // Convert to hex and pad
58+
}
59+
}
60+
return sb.toString()
61+
}
4662
private fun msg(headline: String) =
4763
when (this) {
4864
interactive ->
4965
"$headline\n" +
50-
"- update this snapshot by adding `_TODO` to the function name\n" +
51-
"- update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`"
66+
" update this snapshot by adding `_TODO` to the function name\n" +
67+
" update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`"
5268
readonly -> headline
5369
overwrite -> "$headline\n(didn't expect this to ever happen in overwrite mode)"
5470
}

jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/SelfieImplementations.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ class BinarySelfie(actual: Snapshot, disk: DiskStorage, private val onlyFacet: S
146146
return actualBytes
147147
} else {
148148
throw Selfie.system.fs.assertFailed(
149-
Selfie.system.mode.msgSnapshotMismatch(), expected, actualBytes)
149+
Selfie.system.mode.msgSnapshotMismatchBinary(expected, actualBytes),
150+
expected,
151+
actualBytes)
150152
}
151153
}
152154
}
@@ -241,7 +243,9 @@ private fun <T : Any> toBeDidntMatch(expected: T?, actual: T, format: LiteralFor
241243
throw Selfie.system.fs.assertFailed("Can't call `toBe_TODO` in ${Mode.readonly} mode!")
242244
} else {
243245
throw Selfie.system.fs.assertFailed(
244-
Selfie.system.mode.msgSnapshotMismatch(), expected, actual)
246+
Selfie.system.mode.msgSnapshotMismatch(expected.toString(), actual.toString()),
247+
expected,
248+
actual)
245249
}
246250
}
247251
}
@@ -263,10 +267,12 @@ private fun assertEqual(expected: Snapshot?, actual: Snapshot, storage: Snapshot
263267
.filter { expected.subjectOrFacetMaybe(it) != actual.subjectOrFacetMaybe(it) }
264268
.toList()
265269
.sorted()
270+
val expectedFacets = serializeOnlyFacets(expected, mismatchedKeys)
271+
val actualFacets = serializeOnlyFacets(actual, mismatchedKeys)
266272
throw storage.fs.assertFailed(
267-
storage.mode.msgSnapshotMismatch(),
268-
serializeOnlyFacets(expected, mismatchedKeys),
269-
serializeOnlyFacets(actual, mismatchedKeys))
273+
storage.mode.msgSnapshotMismatch(expectedFacets, actualFacets),
274+
expectedFacets,
275+
actualFacets)
270276
}
271277
}
272278
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.selfie.guts
17+
18+
object SnapshotNotEqualErrorMsg {
19+
const val REASONABLE_LINE_LENGTH = 120
20+
fun forUnequalStrings(expected: String, actual: String): String {
21+
var lineNumber = 1
22+
var columnNumber = 1
23+
var index = 0
24+
25+
while (index < expected.length && index < actual.length) {
26+
val expectedChar = expected[index]
27+
val actualChar = actual[index]
28+
if (expectedChar != actualChar) {
29+
val endOfLineExpected =
30+
expected.indexOf('\n', index).let { if (it == -1) expected.length else it }
31+
val endOfLineActual =
32+
actual.indexOf('\n', index).let { if (it == -1) actual.length else it }
33+
val expectedLine = expected.substring(index - columnNumber + 1, endOfLineExpected)
34+
val actualLine = actual.substring(index - columnNumber + 1, endOfLineActual)
35+
return "Snapshot mismatch at L$lineNumber:C$columnNumber\n-$expectedLine\n+$actualLine"
36+
}
37+
if (expectedChar == '\n') {
38+
lineNumber++
39+
columnNumber = 1
40+
} else {
41+
columnNumber++
42+
}
43+
index++
44+
}
45+
val endOfLineExpected =
46+
expected.indexOf('\n', index).let { if (it == -1) expected.length else it }
47+
val endOfLineActual = actual.indexOf('\n', index).let { if (it == -1) actual.length else it }
48+
49+
if (endOfLineActual == endOfLineExpected) {
50+
// it ended at a line break
51+
val longer = if (actual.length > expected.length) actual else expected
52+
val added = if (actual.length > expected.length) "+" else "-"
53+
val endIdx =
54+
longer.indexOf('\n', endOfLineActual + 1).let { if (it == -1) longer.length else it }
55+
val line = longer.substring(endOfLineActual + 1, endIdx)
56+
return "Snapshot mismatch at L${lineNumber+1}:C1 - line(s) ${if (added == "+") "added" else "removed"}\n${added}$line"
57+
} else {
58+
val expectedLine = expected.substring(index - columnNumber + 1, endOfLineExpected)
59+
val actualLine = actual.substring(index - columnNumber + 1, endOfLineActual)
60+
return "Snapshot mismatch at L$lineNumber:C$columnNumber\n-$expectedLine\n+$actualLine"
61+
}
62+
}
63+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (C) 2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.selfie.guts
17+
18+
import io.kotest.matchers.shouldBe
19+
import kotlin.test.Test
20+
21+
class SnapshotNotEqualErrorMsgTest {
22+
@Test
23+
fun errorLine1() {
24+
SnapshotNotEqualErrorMsg.forUnequalStrings("Testing 123", "Testing ABC") shouldBe
25+
"""Snapshot mismatch at L1:C9
26+
-Testing 123
27+
+Testing ABC"""
28+
29+
SnapshotNotEqualErrorMsg.forUnequalStrings("123 Testing", "ABC Testing") shouldBe
30+
"""Snapshot mismatch at L1:C1
31+
-123 Testing
32+
+ABC Testing"""
33+
}
34+
35+
@Test
36+
fun errorLine2() {
37+
SnapshotNotEqualErrorMsg.forUnequalStrings("Line\nTesting 123", "Line\nTesting ABC") shouldBe
38+
"""Snapshot mismatch at L2:C9
39+
-Testing 123
40+
+Testing ABC"""
41+
42+
SnapshotNotEqualErrorMsg.forUnequalStrings("Line\n123 Testing", "Line\nABC Testing") shouldBe
43+
"""Snapshot mismatch at L2:C1
44+
-123 Testing
45+
+ABC Testing"""
46+
}
47+
48+
@Test
49+
fun extraLine1() {
50+
SnapshotNotEqualErrorMsg.forUnequalStrings("123", "123ABC") shouldBe
51+
"""Snapshot mismatch at L1:C4
52+
-123
53+
+123ABC"""
54+
SnapshotNotEqualErrorMsg.forUnequalStrings("123ABC", "123") shouldBe
55+
"""Snapshot mismatch at L1:C4
56+
-123ABC
57+
+123"""
58+
}
59+
60+
@Test
61+
fun extraLine2() {
62+
SnapshotNotEqualErrorMsg.forUnequalStrings("line\n123", "line\n123ABC") shouldBe
63+
"""Snapshot mismatch at L2:C4
64+
-123
65+
+123ABC"""
66+
SnapshotNotEqualErrorMsg.forUnequalStrings("line\n123ABC", "line\n123") shouldBe
67+
"""Snapshot mismatch at L2:C4
68+
-123ABC
69+
+123"""
70+
}
71+
72+
@Test
73+
fun extraLine() {
74+
SnapshotNotEqualErrorMsg.forUnequalStrings("line", "line\nnext") shouldBe
75+
"""Snapshot mismatch at L2:C1 - line(s) added
76+
+next"""
77+
SnapshotNotEqualErrorMsg.forUnequalStrings("line\nnext", "line") shouldBe
78+
"""Snapshot mismatch at L2:C1 - line(s) removed
79+
-next"""
80+
}
81+
82+
@Test
83+
fun extraNewline() {
84+
SnapshotNotEqualErrorMsg.forUnequalStrings("line", "line\n") shouldBe
85+
"""Snapshot mismatch at L2:C1 - line(s) added
86+
+"""
87+
SnapshotNotEqualErrorMsg.forUnequalStrings("line\n", "line") shouldBe
88+
"""Snapshot mismatch at L2:C1 - line(s) removed
89+
-"""
90+
SnapshotNotEqualErrorMsg.forUnequalStrings("", "\n") shouldBe
91+
"""Snapshot mismatch at L2:C1 - line(s) added
92+
+"""
93+
SnapshotNotEqualErrorMsg.forUnequalStrings("\n", "") shouldBe
94+
"""Snapshot mismatch at L2:C1 - line(s) removed
95+
-"""
96+
}
97+
}

jvm/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/InteractiveTest.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ class InteractiveTest : HarnessJUnit() {
3737
fun inlineMismatch() {
3838
ut_mirrorKt().lineWith("expectSelfie(").setContent(" expectSelfie(5).toBe(10)")
3939
gradleInteractiveFail().message shouldBe
40-
"Snapshot mismatch\n" +
41-
"- update this snapshot by adding `_TODO` to the function name\n" +
42-
"- update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`"
40+
"""Snapshot mismatch at L1:C1
41+
-10
42+
+5
43+
‣ update this snapshot by adding `_TODO` to the function name
44+
‣ update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`"""
4345
}
4446

4547
@Test @Order(3)
@@ -72,8 +74,8 @@ class InteractiveTest : HarnessJUnit() {
7274
ut_mirrorKt().lineWith("expectSelfie(").setContent(" expectSelfie(\"5\").toMatchDisk()")
7375
gradleInteractiveFail().message shouldBe
7476
"Snapshot not found\n" +
75-
"- update this snapshot by adding `_TODO` to the function name\n" +
76-
"- update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`"
77+
" update this snapshot by adding `_TODO` to the function name\n" +
78+
" update all snapshots in this file by adding `//selfieonce` or `//SELFIEWRITE`"
7779
}
7880

7981
@Test @Order(7)

0 commit comments

Comments
 (0)