Skip to content

Commit 6e24222

Browse files
author
ntwigg
committed
Create a reproducer test case for #529
1 parent 2dbfd2d commit 6e24222

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024 DiffPlug
2+
* Copyright (C) 2024-2025 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -249,7 +249,7 @@ private fun <T : Any> toBeDidntMatch(expected: T?, actual: T, format: LiteralFor
249249
}
250250
}
251251
}
252-
private fun assertEqual(expected: Snapshot?, actual: Snapshot, storage: SnapshotSystem) {
252+
internal fun assertEqual(expected: Snapshot?, actual: Snapshot, storage: SnapshotSystem) {
253253
when (expected) {
254254
null -> throw storage.fs.assertFailed(storage.mode.msgSnapshotNotFound())
255255
actual -> return
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (C) 2025 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
17+
18+
import com.diffplug.selfie.guts.FS
19+
import com.diffplug.selfie.guts.SnapshotSystem
20+
import com.diffplug.selfie.guts.TypedPath
21+
import io.kotest.assertions.throwables.shouldThrow
22+
import io.kotest.matchers.shouldBe
23+
import kotlin.test.Test
24+
25+
class SnapshotFacetBugTest {
26+
/**
27+
* This test reproduces the bug where adding a new facet to a snapshot that was already stored on
28+
* disk causes a StringIndexOutOfBoundsException during comparison.
29+
*/
30+
@Test
31+
fun testAddingNewFacetToStoredSnapshot() {
32+
// Create a simple mock FS
33+
val mockFs =
34+
object : FS {
35+
override fun fileExists(typedPath: TypedPath): Boolean = false
36+
override fun <T> fileWalk(typedPath: TypedPath, walk: (Sequence<TypedPath>) -> T): T =
37+
throw UnsupportedOperationException("Not needed for this test")
38+
override fun fileReadBinary(typedPath: TypedPath): ByteArray =
39+
throw UnsupportedOperationException("Not needed for this test")
40+
override fun fileWriteBinary(typedPath: TypedPath, content: ByteArray) {
41+
// Not needed for this test
42+
}
43+
override fun assertFailed(message: String, expected: Any?, actual: Any?): Throwable =
44+
AssertionError(message)
45+
}
46+
47+
// Create a simple mock SnapshotSystem
48+
val mockSystem =
49+
object : SnapshotSystem {
50+
override val fs = mockFs
51+
override val mode = Mode.readonly
52+
override val layout =
53+
object : com.diffplug.selfie.guts.SnapshotFileLayout {
54+
override val rootFolder = TypedPath.ofFolder("/test/")
55+
override val fs = mockFs
56+
override val allowMultipleEquivalentWritesToOneLocation = false
57+
override val javaDontUseTripleQuoteLiterals = false
58+
override fun sourcePathForCall(
59+
call: com.diffplug.selfie.guts.CallLocation
60+
): TypedPath = TypedPath.ofFile("/test/Test.kt")
61+
override fun sourcePathForCallMaybe(
62+
call: com.diffplug.selfie.guts.CallLocation
63+
): TypedPath? = TypedPath.ofFile("/test/Test.kt")
64+
override fun checkForSmuggledError() {
65+
// Not needed for this test
66+
}
67+
}
68+
override fun sourceFileHasWritableComment(
69+
call: com.diffplug.selfie.guts.CallStack
70+
): Boolean = false
71+
override fun writeInline(
72+
literalValue: com.diffplug.selfie.guts.LiteralValue<*>,
73+
call: com.diffplug.selfie.guts.CallStack
74+
) {
75+
// Not needed for this test
76+
}
77+
override fun writeToBeFile(
78+
path: TypedPath,
79+
data: ByteArray,
80+
call: com.diffplug.selfie.guts.CallStack
81+
) {
82+
// Not needed for this test
83+
}
84+
override fun diskThreadLocal() =
85+
throw UnsupportedOperationException("Not needed for this test")
86+
}
87+
88+
// Step 1: Create a simple snapshot (this would be the one stored on disk)
89+
val storedSnapshot = Snapshot.of("")
90+
91+
// Step 2: Create a new snapshot with an added facet
92+
val updatedSnapshot = Snapshot.of("").plusFacet("new-facet", "new-facet-value")
93+
94+
// Step 3: This should throw a StringIndexOutOfBoundsException due to the bug
95+
val exception =
96+
shouldThrow<StringIndexOutOfBoundsException> {
97+
assertEqual(storedSnapshot, updatedSnapshot, mockSystem)
98+
}
99+
100+
// Verify the exception message matches the expected error
101+
exception.message shouldBe "String index out of range: -1"
102+
}
103+
}

0 commit comments

Comments
 (0)