Skip to content
This repository was archived by the owner on Apr 4, 2024. It is now read-only.

Commit 60a48c8

Browse files
committed
Merge branch 'main' into SnapshotValueReader-Edwin
2 parents b1532bd + e6c5905 commit 60a48c8

File tree

31 files changed

+850
-151
lines changed

31 files changed

+850
-151
lines changed

jvm/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## [Unreleased]
1414

15+
## [2.0.2] - 2024-03-20
16+
### Fixed
17+
- `toBeFile` now checks for duplicate writes and throws a more helpful error message if the file doesn't exist. ([#277](https://github.com/diffplug/selfie/pull/277))
18+
1519
## [2.0.1] - 2024-02-24
1620
### Fixed
1721
- The `coroutines` methods used to eagerly throw an exception if they were ever called from anywhere besides a Kotest method. Now they wait until `toMatchDisk()` is called, because they can work just fine anywhere if you use `toBe`. ([#247](https://github.com/diffplug/selfie/pull/247))

jvm/example-junit5/build.gradle

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ repositories {
55
mavenCentral()
66
}
77
dependencies {
8-
implementation 'io.jooby:jooby:3.0.7'
9-
implementation 'io.jooby:jooby-netty:3.0.7'
10-
implementation 'jakarta.mail:jakarta.mail-api:2.1.2'
11-
testImplementation 'io.jooby:jooby-test:3.0.7'
8+
implementation 'io.jooby:jooby:3.0.9'
9+
implementation 'io.jooby:jooby-netty:3.0.9'
10+
implementation 'jakarta.mail:jakarta.mail-api:2.1.3'
11+
testImplementation 'io.jooby:jooby-test:3.0.9'
1212
testImplementation "org.junit.jupiter:junit-jupiter:$ver_JUNIT_USE"
1313
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
1414
testImplementation 'io.rest-assured:rest-assured:5.4.0'

jvm/gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ org.gradle.configuration-cache=true
77
ver_JUNIT_USE=5.10.2
88
ver_JUNIT_PIONEER=2.2.0
99
ver_OKIO=3.8.0
10-
ver_KOTLIN_TEST=1.9.22
10+
ver_KOTLIN_TEST=1.9.23
1111
ver_KOTLIN_SERIALIZATION=1.6.3
1212
# Kotest 5.4.0 is the oldest that we support
1313
ver_KOTEST=5.4.0

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,18 @@ class CacheSelfieBinary<T>(
7474
if (isTodo) {
7575
Selfie.system.writeInline(TodoStub.toBeFile.createLiteral(), call)
7676
}
77-
Selfie.system.fs.fileWriteBinary(resolvePath(subpath), roundtrip.serialize(actual))
77+
Selfie.system.writeToBeFile(resolvePath(subpath), roundtrip.serialize(actual), call)
7878
return actual
7979
} else {
8080
if (isTodo) {
8181
throw Selfie.system.fs.assertFailed("Can't call `toBeFile_TODO` in ${Mode.readonly} mode!")
8282
} else {
83-
return roundtrip.parse(Selfie.system.fs.fileReadBinary(resolvePath(subpath)))
83+
val path = resolvePath(subpath)
84+
if (!Selfie.system.fs.fileExists(path)) {
85+
throw Selfie.system.fs.assertFailed(
86+
Selfie.system.mode.msgSnapshotNotFoundNoSuchFile(path))
87+
}
88+
return roundtrip.parse(Selfie.system.fs.fileReadBinary(path))
8489
}
8590
}
8691
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.diffplug.selfie
1818
import com.diffplug.selfie.guts.CallStack
1919
import com.diffplug.selfie.guts.CommentTracker
2020
import com.diffplug.selfie.guts.SnapshotSystem
21+
import com.diffplug.selfie.guts.TypedPath
2122

2223
enum class Mode {
2324
interactive,
@@ -39,6 +40,8 @@ enum class Mode {
3940
overwrite -> true
4041
}
4142
internal fun msgSnapshotNotFound() = msg("Snapshot not found")
43+
internal fun msgSnapshotNotFoundNoSuchFile(file: TypedPath) =
44+
msg("Snapshot not found: no such file $file")
4245
internal fun msgSnapshotMismatch() = msg("Snapshot mismatch")
4346
private fun msg(headline: String) =
4447
when (this) {

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,18 @@ class BinarySelfie(actual: Snapshot, disk: DiskStorage, private val onlyFacet: S
132132
if (isTodo) {
133133
Selfie.system.writeInline(TodoStub.toBeFile.createLiteral(), call)
134134
}
135-
Selfie.system.fs.fileWriteBinary(resolvePath(subpath), actualBytes)
135+
Selfie.system.writeToBeFile(resolvePath(subpath), actualBytes, call)
136136
return actualBytes
137137
} else {
138138
if (isTodo) {
139139
throw Selfie.system.fs.assertFailed("Can't call `toBeFile_TODO` in ${Mode.readonly} mode!")
140140
} else {
141-
val expected = Selfie.system.fs.fileReadBinary(resolvePath(subpath))
141+
val path = resolvePath(subpath)
142+
if (!Selfie.system.fs.fileExists(path)) {
143+
throw Selfie.system.fs.assertFailed(
144+
Selfie.system.mode.msgSnapshotNotFoundNoSuchFile(path))
145+
}
146+
val expected = Selfie.system.fs.fileReadBinary(path)
142147
if (expected.contentEquals(actualBytes)) {
143148
return actualBytes
144149
} else {

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,18 @@ class CacheSelfieBinarySuspend<T>(
7878
if (isTodo) {
7979
Selfie.system.writeInline(TodoStub.toBeFile.createLiteral(), call)
8080
}
81-
Selfie.system.fs.fileWriteBinary(resolvePath(subpath), roundtrip.serialize(actual))
81+
Selfie.system.writeToBeFile(resolvePath(subpath), roundtrip.serialize(actual), call)
8282
return actual
8383
} else {
8484
if (isTodo) {
8585
throw Selfie.system.fs.assertFailed("Can't call `toBeFile_TODO` in ${Mode.readonly} mode!")
8686
} else {
87-
return roundtrip.parse(Selfie.system.fs.fileReadBinary(resolvePath(subpath)))
87+
val path = resolvePath(subpath)
88+
if (!Selfie.system.fs.fileExists(path)) {
89+
throw Selfie.system.fs.assertFailed(
90+
Selfie.system.mode.msgSnapshotNotFoundNoSuchFile(path))
91+
}
92+
return roundtrip.parse(Selfie.system.fs.fileReadBinary(path))
8893
}
8994
}
9095
}

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

+7
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ data class TypedPath(val absolutePath: String) : Comparable<TypedPath> {
8080
}
8181

8282
interface FS {
83+
/**
84+
* Returns true if the given path exists *and is a file*, false if it doesn't or if it is a
85+
* folder.
86+
*/
87+
fun fileExists(typedPath: TypedPath): Boolean
8388
/** Walks the files (not directories) which are children and grandchildren of the given path. */
8489
fun <T> fileWalk(typedPath: TypedPath, walk: (Sequence<TypedPath>) -> T): T
8590
fun fileRead(typedPath: TypedPath) = fileReadBinary(typedPath).decodeToString()
@@ -100,6 +105,8 @@ interface SnapshotSystem {
100105
fun sourceFileHasWritableComment(call: CallStack): Boolean
101106
/** Indicates that the following value should be written into test sourcecode. */
102107
fun writeInline(literalValue: LiteralValue<*>, call: CallStack)
108+
/** Writes the given bytes to the given file, checking for duplicate writes. */
109+
fun writeToBeFile(path: TypedPath, data: ByteArray, call: CallStack)
103110
/** Returns the DiskStorage for the test associated with this thread, else error. */
104111
fun diskThreadLocal(): DiskStorage
105112
}

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

+42-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ sealed class WriteTracker<K : Comparable<K>, V> {
8080
expectSelfie(underTest).toBe("bash$")
8181
}
8282
"""
83-
.trimIndent()
83+
is ToBeFileWriteTracker ->
84+
"You can fix this with `.toBeFile(String filename)` and pass a unique filename for each code path."
8485
}
8586
if (existing.snapshot != snapshot) {
8687
throw layout.fs.assertFailed(
@@ -102,6 +103,46 @@ class DiskWriteTracker : WriteTracker<String, Snapshot>() {
102103
}
103104
}
104105

106+
class ToBeFileWriteTracker : WriteTracker<TypedPath, ToBeFileLazyBytes>() {
107+
fun writeToDisk(
108+
key: TypedPath,
109+
snapshot: ByteArray,
110+
call: CallStack,
111+
layout: SnapshotFileLayout
112+
) {
113+
val lazyBytes = ToBeFileLazyBytes(key, layout, snapshot)
114+
recordInternal(key, lazyBytes, call, layout)
115+
// recordInternal will throw an exception on a duplicate write, so we can safely write to disk
116+
lazyBytes.writeToDisk()
117+
// and because we are doing duplicate checks, `ToBeFileLazyBytes` can allow its in-memory
118+
// data to be garbage collected, because it can safely read from disk in the future
119+
}
120+
}
121+
122+
class ToBeFileLazyBytes(val location: TypedPath, val layout: SnapshotFileLayout, data: ByteArray) {
123+
/** When constructed, we always have the data. */
124+
var data: ByteArray? = data
125+
/**
126+
* Shortly after being construted, this data is written to disk, and we can stop holding it in
127+
* memory.
128+
*/
129+
internal fun writeToDisk() {
130+
data?.let { layout.fs.fileWriteBinary(location, it) }
131+
?: throw IllegalStateException("Data has already been written to disk!")
132+
data = null
133+
}
134+
/**
135+
* If we need to read our data, we do it from memory if it's still there, or from disk if it
136+
* isn't.
137+
*/
138+
private fun readData(): ByteArray = data ?: layout.fs.fileReadBinary(location)
139+
/** We calculate equality based on this data. */
140+
override fun equals(other: Any?): Boolean =
141+
if (this === other) true
142+
else if (other is ToBeFileLazyBytes) readData().contentEquals(other.readData()) else false
143+
override fun hashCode(): Int = readData().contentHashCode()
144+
}
145+
105146
class InlineWriteTracker : WriteTracker<CallLocation, LiteralValue<*>>() {
106147
fun record(call: CallStack, literalValue: LiteralValue<*>, layout: SnapshotFileLayout) {
107148
recordInternal(call.location, literalValue, call, layout)

jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotSystemJUnit5.kt

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.diffplug.selfie.guts.LiteralValue
2626
import com.diffplug.selfie.guts.SnapshotFileLayout
2727
import com.diffplug.selfie.guts.SnapshotSystem
2828
import com.diffplug.selfie.guts.SourceFile
29+
import com.diffplug.selfie.guts.ToBeFileWriteTracker
2930
import com.diffplug.selfie.guts.TypedPath
3031
import com.diffplug.selfie.guts.WithinTestGC
3132
import com.diffplug.selfie.guts.atomic
@@ -41,6 +42,7 @@ import org.opentest4j.AssertionFailedError
4142
internal fun TypedPath.toPath(): java.nio.file.Path = java.nio.file.Path.of(absolutePath)
4243

4344
internal object FSJava : FS {
45+
override fun fileExists(typedPath: TypedPath): Boolean = Files.isRegularFile(typedPath.toPath())
4446
override fun fileWriteBinary(typedPath: TypedPath, content: ByteArray) =
4547
typedPath.toPath().writeBytes(content)
4648
override fun fileReadBinary(typedPath: TypedPath) = typedPath.toPath().readBytes()
@@ -65,6 +67,7 @@ internal object SnapshotSystemJUnit5 : SnapshotSystem {
6567
override val layout = SnapshotFileLayoutJUnit5(SelfieSettingsAPI.initialize(), fs)
6668
private val commentTracker = CommentTracker()
6769
private val inlineWriteTracker = InlineWriteTracker()
70+
private val toBeFileWriteTracker = ToBeFileWriteTracker()
6871
private val progressPerClass = atomic(ArrayMap.empty<String, SnapshotFileProgress>())
6972
fun forClass(className: String): SnapshotFileProgress {
7073
// optimize for reads
@@ -90,6 +93,9 @@ internal object SnapshotSystemJUnit5 : SnapshotSystem {
9093
override fun writeInline(literalValue: LiteralValue<*>, call: CallStack) {
9194
inlineWriteTracker.record(call, literalValue, layout)
9295
}
96+
override fun writeToBeFile(path: TypedPath, data: ByteArray, call: CallStack) {
97+
toBeFileWriteTracker.writeToDisk(path, data, call, layout)
98+
}
9399
internal val testListenerRunning = AtomicBoolean(false)
94100
fun finishedAllTests() {
95101
val snapshotsFilesWrittenToDisk =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (C) 2023-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.junit5
17+
18+
import io.kotest.matchers.shouldBe
19+
import io.kotest.matchers.string.shouldStartWith
20+
import kotlin.test.Test
21+
import org.junit.jupiter.api.MethodOrderer
22+
import org.junit.jupiter.api.Order
23+
import org.junit.jupiter.api.TestMethodOrder
24+
import org.junitpioneer.jupiter.DisableIfTestFails
25+
26+
/** Simplest test for verifying read/write of a snapshot. */
27+
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
28+
@DisableIfTestFails
29+
class DuplicateWriteToBeFileTest : HarnessJUnit() {
30+
@Test @Order(1)
31+
fun noSelfie() {
32+
ut_snapshot().deleteIfExists()
33+
ut_snapshot().assertDoesNotExist()
34+
}
35+
36+
@Test @Order(2)
37+
fun cannot_write_multiple_things_to_one_snapshot() {
38+
ut_mirrorKt().linesFrom("fun shouldFail()").toFirst("}").uncomment()
39+
ut_mirrorKt().linesFrom("fun shouldPass()").toFirst("}").commentOut()
40+
gradlew("test", "-PunderTest=true", "-Pselfie=overwrite")!!.message shouldStartWith
41+
"Snapshot was set to multiple values"
42+
}
43+
44+
@Test @Order(3)
45+
fun can_write_one_thing_multiple_times_to_one_snapshot() {
46+
ut_mirrorKt().linesFrom("fun shouldFail()").toFirst("}").commentOut()
47+
ut_mirrorKt().linesFrom("fun shouldPass()").toFirst("}").uncomment()
48+
gradlew("test", "-PunderTest=true", "-Pselfie=overwrite") shouldBe null
49+
}
50+
51+
@Test @Order(4)
52+
fun can_read_one_thing_multiple_times_from_one_snapshot() {
53+
ut_mirrorKt().linesFrom("fun shouldFail()").toFirst("}").commentOut()
54+
ut_mirrorKt().linesFrom("fun shouldPass()").toFirst("}").uncomment()
55+
gradlew("test", "-PunderTest=true", "-Pselfie=readonly") shouldBe null
56+
}
57+
58+
@Test @Order(5)
59+
fun writeonce_mode() {
60+
ut_mirrorKt().linesFrom("fun shouldFail()").toFirst("}").commentOut()
61+
ut_mirrorKt().linesFrom("fun shouldPass()").toFirst("}").uncomment()
62+
gradlew(
63+
"test",
64+
"-PunderTest=true",
65+
"-Pselfie=overwrite",
66+
"-Pselfie.settings=undertest.junit5.SelfieWriteOnce")!!
67+
.message shouldStartWith "Snapshot was set to the same value multiple times"
68+
}
69+
}

jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ expect internal val FS_SYSTEM: FileSystem
2828
internal fun TypedPath.toPath(): okio.Path = absolutePath.toPath()
2929

3030
internal object FSOkio : FS {
31+
override fun fileExists(typedPath: TypedPath): Boolean =
32+
FS_SYSTEM.metadataOrNull(typedPath.toPath())?.isRegularFile ?: false
3133
/** Walks the files (not directories) which are children and grandchildren of the given path. */
3234
override fun <T> fileWalk(typedPath: TypedPath, walk: (Sequence<TypedPath>) -> T): T =
3335
walk(

jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SnapshotSystemKotest.kt

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.diffplug.selfie.guts.LiteralValue
3030
import com.diffplug.selfie.guts.SnapshotFileLayout
3131
import com.diffplug.selfie.guts.SnapshotSystem
3232
import com.diffplug.selfie.guts.SourceFile
33+
import com.diffplug.selfie.guts.ToBeFileWriteTracker
3334
import com.diffplug.selfie.guts.TypedPath
3435
import com.diffplug.selfie.guts.WithinTestGC
3536
import com.diffplug.selfie.guts.atomic
@@ -40,6 +41,7 @@ internal class SnapshotSystemKotest(settings: SelfieSettingsAPI) : SnapshotSyste
4041
override val layout = SnapshotFileLayoutKotest(settings, fs)
4142
private val commentTracker = CommentTracker()
4243
private val inlineWriteTracker = InlineWriteTracker()
44+
private val toBeFileWriteTracker = ToBeFileWriteTracker()
4345
private val progressPerFile = atomic(ArrayMap.empty<String, SnapshotFileProgress>())
4446
fun forClassOrFilename(classOrFilename: String): SnapshotFileProgress {
4547
// optimize for reads
@@ -66,6 +68,9 @@ internal class SnapshotSystemKotest(settings: SelfieSettingsAPI) : SnapshotSyste
6668
override fun writeInline(literalValue: LiteralValue<*>, call: CallStack) {
6769
inlineWriteTracker.record(call, literalValue, layout)
6870
}
71+
override fun writeToBeFile(path: TypedPath, data: ByteArray, call: CallStack) {
72+
toBeFileWriteTracker.writeToDisk(path, data, call, layout)
73+
}
6974
override fun diskThreadLocal(): DiskStorage =
7075
throw fs.assertFailed(
7176
"""

jvm/settings.gradle

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ plugins {
1818
// https://github.com/gradle-nexus/publish-plugin/releases
1919
id 'io.github.gradle-nexus.publish-plugin' version '2.0.0-rc-2' apply false
2020
// https://plugins.gradle.org/plugin/org.jetbrains.dokka
21-
id 'org.jetbrains.dokka' version '1.9.10' apply false
21+
id 'org.jetbrains.dokka' version '1.9.20' apply false
2222
// https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm
23-
id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false
23+
id 'org.jetbrains.kotlin.jvm' version '1.9.23' apply false
2424
// https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.serialization
25-
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false
25+
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23' apply false
2626
// https://plugins.gradle.org/plugin/org.jetbrains.kotlin.multiplatform
27-
id 'org.jetbrains.kotlin.multiplatform' version '1.9.22' apply false
27+
id 'org.jetbrains.kotlin.multiplatform' version '1.9.23' apply false
2828
// https://github.com/adamko-dev/dokkatoo/releases
2929
id 'dev.adamko.dokkatoo-html' version '2.0.0' apply false
3030
}

jvm/undertest-junit5-kotest/harness/settings.gradle

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ plugins {
1818
// https://github.com/gradle-nexus/publish-plugin/releases
1919
id 'io.github.gradle-nexus.publish-plugin' version '2.0.0-rc-2' apply false
2020
// https://plugins.gradle.org/plugin/org.jetbrains.dokka
21-
id 'org.jetbrains.dokka' version '1.9.10' apply false
21+
id 'org.jetbrains.dokka' version '1.9.20' apply false
2222
// https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm
23-
id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false
23+
id 'org.jetbrains.kotlin.jvm' version '1.9.23' apply false
2424
// https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.serialization
25-
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false
25+
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23' apply false
2626
// https://plugins.gradle.org/plugin/org.jetbrains.kotlin.multiplatform
27-
id 'org.jetbrains.kotlin.multiplatform' version '1.9.22' apply false
27+
id 'org.jetbrains.kotlin.multiplatform' version '1.9.23' apply false
2828
// https://github.com/adamko-dev/dokkatoo/releases
2929
id 'dev.adamko.dokkatoo-html' version '2.0.0' apply false
3030
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
twins
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package undertest.junit5
2+
3+
import com.diffplug.selfie.Selfie.expectSelfie
4+
import org.junit.jupiter.api.Test
5+
6+
class UT_DuplicateWriteToBeFileTest {
7+
// @Test fun shouldFail() {
8+
// expectSelfie("apples".toByteArray()).toBeFile("duplicate_tobefile.data")
9+
// expectSelfie("oranges".toByteArray()).toBeFile("duplicate_tobefile.data")
10+
// }
11+
@Test fun shouldPass() {
12+
expectSelfie("twins".toByteArray()).toBeFile("duplicate_tobefile.data")
13+
expectSelfie("twins".toByteArray()).toBeFile("duplicate_tobefile.data")
14+
}
15+
}

0 commit comments

Comments
 (0)