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

Commit 09b3efa

Browse files
authored
Better binary facets (diffplug#224)
2 parents e3e43c3 + 3a334da commit 09b3efa

File tree

15 files changed

+264
-107
lines changed

15 files changed

+264
-107
lines changed

jvm/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- **Memoization** ([#219](https://github.com/diffplug/selfie/pull/219) implements [#215](https://github.com/diffplug/selfie/issues/215))
1616
- like `expectSelfie`, all are available as `Selfie.memoize` or as `suspend fun` in `com.diffplug.selfie.coroutines`.
1717
```kotlin
18-
val cachedResult: ByteArray = Selfie.memoizeBinary { dalleJpeg() }.toBePath("example.jpg")
18+
val cachedResult: ByteArray = Selfie.memoizeBinary { dalleJpeg() }.toBeFile("example.jpg")
1919
val cachedResult: String = Selfie.memoize { someString() }.toBe("what it was earlier")
2020
val cachedResult: T = Selfie.memoizeAsJson { anyKotlinxSerializable() }.toBe("""{"key": "value"}""")
2121
val cachedResult: T = Selfie.memoizeBinarySerializable { anyJavaIoSerializable() }.toMatchDisk()
2222
```
23+
- `toBeBase64` and `toBeFile` for true binary comparison of binary snapshots and facets. ([#224](https://github.com/diffplug/selfie/pull/224))
24+
### Changed
25+
- **BREAKING** reordered a class hierarchy for better binary support. ([#221](https://github.com/diffplug/selfie/issues/221))
26+
- most users won't need to make any changes at all
27+
- only exception is that `expectSelfie(byte[]).toBe` is now a compile error, must do `toBeBase64`
2328

2429
## [1.2.0] - 2024-02-12
2530
### Added

jvm/example-junit5/src/test/java/selfie/SelfieSettings.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import com.diffplug.selfie.Camera;
44
import com.diffplug.selfie.CompoundLens;
5-
import com.diffplug.selfie.DiskSelfie;
65
import com.diffplug.selfie.Lens;
76
import com.diffplug.selfie.Selfie;
87
import com.diffplug.selfie.Snapshot;
8+
import com.diffplug.selfie.StringSelfie;
99
import com.diffplug.selfie.junit5.SelfieSettingsAPI;
1010
import com.example.EmailDev;
1111
import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
@@ -33,11 +33,11 @@ private static Snapshot cameraEmail(EmailDev email) {
3333
"metadata", "subject=" + email.subject + "\nto=" + email.to + "\nfrom=" + email.from);
3434
}
3535

36-
public static DiskSelfie expectSelfie(EmailDev email) {
36+
public static StringSelfie expectSelfie(EmailDev email) {
3737
return Selfie.expectSelfie(email, EMAIL_CAMERA);
3838
}
3939

40-
public static DiskSelfie expectSelfie(Response response) {
40+
public static StringSelfie expectSelfie(Response response) {
4141
return Selfie.expectSelfie(response, RESPONSE_CAMERA.withLens(htmlClean));
4242
}
4343

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
*/
1616
package com.diffplug.selfie
1717

18-
import com.diffplug.selfie.guts.DiskSnapshotTodo
1918
import com.diffplug.selfie.guts.DiskStorage
20-
import com.diffplug.selfie.guts.ToBeFileTodo
19+
import com.diffplug.selfie.guts.LiteralString
20+
import com.diffplug.selfie.guts.LiteralValue
21+
import com.diffplug.selfie.guts.TodoStub
2122
import com.diffplug.selfie.guts.recordCall
23+
import kotlin.io.encoding.Base64
24+
import kotlin.io.encoding.ExperimentalEncodingApi
2225

2326
class MemoBinary<T>(
2427
private val disk: DiskStorage,
@@ -37,7 +40,7 @@ class MemoBinary<T>(
3740
val actual = generator()
3841
disk.writeDisk(Snapshot.of(roundtrip.serialize(actual)), sub, call)
3942
if (isTodo) {
40-
Selfie.system.writeInline(DiskSnapshotTodo.createLiteral(), call)
43+
Selfie.system.writeInline(TodoStub.toMatchDisk.createLiteral(), call)
4144
}
4245
return actual
4346
} else {
@@ -69,7 +72,7 @@ class MemoBinary<T>(
6972
if (writable) {
7073
val actual = generator()
7174
if (isTodo) {
72-
Selfie.system.writeInline(ToBeFileTodo.createLiteral(), call)
75+
Selfie.system.writeInline(TodoStub.toBeFile.createLiteral(), call)
7376
}
7477
Selfie.system.fs.fileWriteBinary(resolvePath(subpath), roundtrip.serialize(actual))
7578
return actual
@@ -81,4 +84,28 @@ class MemoBinary<T>(
8184
}
8285
}
8386
}
87+
fun toBeBase64_TODO(unusedArg: Any? = null): T {
88+
return toBeBase64Impl(null)
89+
}
90+
fun toBeBase64(snapshot: String): T {
91+
return toBeBase64Impl(snapshot)
92+
}
93+
94+
@OptIn(ExperimentalEncodingApi::class)
95+
private fun toBeBase64Impl(snapshot: String?): T {
96+
val call = recordCall(false)
97+
val writable = Selfie.system.mode.canWrite(snapshot == null, call, Selfie.system)
98+
if (writable) {
99+
val actual = generator()
100+
val base64 = Base64.Mime.encode(roundtrip.serialize(actual)).replace("\r", "")
101+
Selfie.system.writeInline(LiteralValue(snapshot, base64, LiteralString), call)
102+
return actual
103+
} else {
104+
if (snapshot == null) {
105+
throw Selfie.system.fs.assertFailed("Can't call `toBe_TODO` in ${Mode.readonly} mode!")
106+
} else {
107+
return roundtrip.parse(Base64.Mime.decode(snapshot))
108+
}
109+
}
110+
}
84111
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
*/
1616
package com.diffplug.selfie
1717

18-
import com.diffplug.selfie.guts.DiskSnapshotTodo
1918
import com.diffplug.selfie.guts.DiskStorage
2019
import com.diffplug.selfie.guts.LiteralString
2120
import com.diffplug.selfie.guts.LiteralValue
21+
import com.diffplug.selfie.guts.TodoStub
2222
import com.diffplug.selfie.guts.recordCall
2323

2424
class MemoString<T>(
@@ -38,7 +38,7 @@ class MemoString<T>(
3838
val actual = generator()
3939
disk.writeDisk(Snapshot.of(roundtrip.serialize(actual)), sub, call)
4040
if (isTodo) {
41-
Selfie.system.writeInline(DiskSnapshotTodo.createLiteral(), call)
41+
Selfie.system.writeInline(TodoStub.toMatchDisk.createLiteral(), call)
4242
}
4343
return actual
4444
} else {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ object Selfie {
4949

5050
@JvmStatic
5151
fun <T> expectSelfie(actual: T, camera: Camera<T>) = expectSelfie(camera.snapshot(actual))
52+
53+
@JvmStatic
54+
fun expectSelfie(actual: ByteArray) = BinarySelfie(Snapshot.of(actual), deferredDiskStorage, "")
5255
@JvmStatic fun expectSelfie(actual: String) = expectSelfie(Snapshot.of(actual))
53-
@JvmStatic fun expectSelfie(actual: ByteArray) = expectSelfie(Snapshot.of(actual))
54-
@JvmStatic fun expectSelfie(actual: Snapshot) = DiskSelfie(actual, deferredDiskStorage)
56+
@JvmStatic fun expectSelfie(actual: Snapshot) = StringSelfie(actual, deferredDiskStorage)
5557
@JvmStatic fun expectSelfie(actual: Long) = LongSelfie(actual)
5658
@JvmStatic fun expectSelfie(actual: Int) = IntSelfie(actual)
5759
@JvmStatic fun expectSelfie(actual: Boolean) = BooleanSelfie(actual)

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

Lines changed: 136 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package com.diffplug.selfie
1717

18-
import com.diffplug.selfie.guts.DiskSnapshotTodo
1918
import com.diffplug.selfie.guts.DiskStorage
2019
import com.diffplug.selfie.guts.LiteralBoolean
2120
import com.diffplug.selfie.guts.LiteralFormat
@@ -24,16 +23,138 @@ import com.diffplug.selfie.guts.LiteralLong
2423
import com.diffplug.selfie.guts.LiteralString
2524
import com.diffplug.selfie.guts.LiteralValue
2625
import com.diffplug.selfie.guts.SnapshotSystem
26+
import com.diffplug.selfie.guts.TodoStub
2727
import com.diffplug.selfie.guts.recordCall
2828
import kotlin.io.encoding.Base64
2929
import kotlin.io.encoding.ExperimentalEncodingApi
3030
import kotlin.jvm.JvmOverloads
3131

32-
open class LiteralStringSelfie
33-
internal constructor(
34-
protected val actual: Snapshot,
32+
/** A selfie which can be stored into a selfie-managed file. */
33+
open class DiskSelfie
34+
internal constructor(protected val actual: Snapshot, protected val disk: DiskStorage) :
35+
FluentFacet {
36+
@JvmOverloads
37+
open fun toMatchDisk(sub: String = ""): DiskSelfie {
38+
val call = recordCall(false)
39+
if (Selfie.system.mode.canWrite(false, call, Selfie.system)) {
40+
disk.writeDisk(actual, sub, call)
41+
} else {
42+
assertEqual(disk.readDisk(sub, call), actual, Selfie.system)
43+
}
44+
return this
45+
}
46+
47+
@JvmOverloads
48+
open fun toMatchDisk_TODO(sub: String = ""): DiskSelfie {
49+
val call = recordCall(false)
50+
if (Selfie.system.mode.canWrite(true, call, Selfie.system)) {
51+
disk.writeDisk(actual, sub, call)
52+
Selfie.system.writeInline(TodoStub.toMatchDisk.createLiteral(), call)
53+
return this
54+
} else {
55+
throw Selfie.system.fs.assertFailed("Can't call `toMatchDisk_TODO` in ${Mode.readonly} mode!")
56+
}
57+
}
58+
override fun facet(facet: String): StringFacet = StringSelfie(actual, disk, listOf(facet))
59+
override fun facets(vararg facets: String): StringFacet =
60+
StringSelfie(actual, disk, facets.toList())
61+
override fun facetBinary(facet: String) = BinarySelfie(actual, disk, facet)
62+
}
63+
64+
interface FluentFacet {
65+
/** Extract a single facet from a snapshot in order to do an inline snapshot. */
66+
fun facet(facet: String): StringFacet
67+
/** Extract multiple facets from a snapshot in order to do an inline snapshot. */
68+
fun facets(vararg facets: String): StringFacet
69+
fun facetBinary(facet: String): BinaryFacet
70+
}
71+
72+
interface StringFacet : FluentFacet {
73+
fun toBe(expected: String): String
74+
fun toBe_TODO(unusedArg: Any?): String = toBe_TODO()
75+
fun toBe_TODO(): String
76+
}
77+
78+
interface BinaryFacet : FluentFacet {
79+
fun toBeBase64(expected: String): ByteArray
80+
fun toBeBase64_TODO(unusedArg: Any?): ByteArray = toBeBase64_TODO()
81+
fun toBeBase64_TODO(): ByteArray
82+
fun toBeFile(subpath: String): ByteArray
83+
fun toBeFile_TODO(subpath: String): ByteArray
84+
}
85+
86+
class BinarySelfie(actual: Snapshot, disk: DiskStorage, private val onlyFacet: String) :
87+
DiskSelfie(actual, disk), BinaryFacet {
88+
init {
89+
check(actual.subjectOrFacetMaybe(onlyFacet)?.isBinary == true) {
90+
"The facet $onlyFacet was not found in the snapshot, or it was not a binary facet."
91+
}
92+
}
93+
private fun actualBytes() = actual.subjectOrFacet(onlyFacet).valueBinary()
94+
override fun toMatchDisk(sub: String): BinarySelfie {
95+
super.toMatchDisk(sub)
96+
return this
97+
}
98+
override fun toMatchDisk_TODO(sub: String): BinarySelfie {
99+
super.toMatchDisk_TODO(sub)
100+
return this
101+
}
102+
103+
@OptIn(ExperimentalEncodingApi::class)
104+
private fun actualString(): String = Base64.Mime.encode(actualBytes()).replace("\r", "")
105+
override fun toBeBase64_TODO(): ByteArray {
106+
toBeDidntMatch(null, actualString(), LiteralString)
107+
return actualBytes()
108+
}
109+
110+
@OptIn(ExperimentalEncodingApi::class)
111+
override fun toBeBase64(expected: String): ByteArray {
112+
val expectedBytes = Base64.Mime.decode(expected)
113+
val actualBytes = actualBytes()
114+
return if (expectedBytes.contentEquals(actualBytes)) Selfie.system.checkSrc(actualBytes)
115+
else {
116+
toBeDidntMatch(expected, actualString(), LiteralString)
117+
return actualBytes()
118+
}
119+
}
120+
override fun toBeFile_TODO(subpath: String): ByteArray {
121+
return toBeFileImpl(subpath, true)
122+
}
123+
override fun toBeFile(subpath: String): ByteArray {
124+
return toBeFileImpl(subpath, false)
125+
}
126+
private fun resolvePath(subpath: String) = Selfie.system.layout.rootFolder.resolveFile(subpath)
127+
private fun toBeFileImpl(subpath: String, isTodo: Boolean): ByteArray {
128+
val call = recordCall(false)
129+
val writable = Selfie.system.mode.canWrite(isTodo, call, Selfie.system)
130+
val actualBytes = actualBytes()
131+
if (writable) {
132+
if (isTodo) {
133+
Selfie.system.writeInline(TodoStub.toBeFile.createLiteral(), call)
134+
}
135+
Selfie.system.fs.fileWriteBinary(resolvePath(subpath), actualBytes)
136+
return actualBytes
137+
} else {
138+
if (isTodo) {
139+
throw Selfie.system.fs.assertFailed("Can't call `toBeFile_TODO` in ${Mode.readonly} mode!")
140+
} else {
141+
val expected = Selfie.system.fs.fileReadBinary(resolvePath(subpath))
142+
if (expected.contentEquals(actualBytes)) {
143+
return actualBytes
144+
} else {
145+
throw Selfie.system.fs.assertFailed(
146+
Selfie.system.mode.msgSnapshotMismatch(), expected, actualBytes)
147+
}
148+
}
149+
}
150+
}
151+
}
152+
153+
class StringSelfie(
154+
actual: Snapshot,
155+
disk: DiskStorage,
35156
private val onlyFacets: Collection<String>? = null
36-
) {
157+
) : DiskSelfie(actual, disk), StringFacet {
37158
init {
38159
if (onlyFacets != null) {
39160
check(onlyFacets.all { it == "" || actual.facets.containsKey(it) }) {
@@ -47,10 +168,14 @@ internal constructor(
47168
}
48169
}
49170
}
50-
/** Extract a single facet from a snapshot in order to do an inline snapshot. */
51-
fun facet(facet: String) = LiteralStringSelfie(actual, listOf(facet))
52-
/** Extract a multiple facets from a snapshot in order to do an inline snapshot. */
53-
fun facets(vararg facets: String) = LiteralStringSelfie(actual, facets.toList())
171+
override fun toMatchDisk(sub: String): StringSelfie {
172+
super.toMatchDisk(sub)
173+
return this
174+
}
175+
override fun toMatchDisk_TODO(sub: String): StringSelfie {
176+
super.toMatchDisk_TODO(sub)
177+
return this
178+
}
54179

55180
@OptIn(ExperimentalEncodingApi::class)
56181
private fun actualString(): String {
@@ -70,42 +195,14 @@ internal constructor(
70195
})
71196
}
72197
}
73-
74-
@JvmOverloads
75-
fun toBe_TODO(unusedArg: Any? = null) = toBeDidntMatch(null, actualString(), LiteralString)
76-
fun toBe(expected: String): String {
198+
override fun toBe_TODO(): String = toBeDidntMatch(null, actualString(), LiteralString)
199+
override fun toBe(expected: String): String {
77200
val actualString = actualString()
78201
return if (actualString == expected) Selfie.system.checkSrc(actualString)
79202
else toBeDidntMatch(expected, actualString, LiteralString)
80203
}
81204
}
82205

83-
class DiskSelfie internal constructor(actual: Snapshot, val disk: DiskStorage) :
84-
LiteralStringSelfie(actual) {
85-
@JvmOverloads
86-
fun toMatchDisk(sub: String = ""): DiskSelfie {
87-
val call = recordCall(false)
88-
if (Selfie.system.mode.canWrite(false, call, Selfie.system)) {
89-
disk.writeDisk(actual, sub, call)
90-
} else {
91-
assertEqual(disk.readDisk(sub, call), actual, Selfie.system)
92-
}
93-
return this
94-
}
95-
96-
@JvmOverloads
97-
fun toMatchDisk_TODO(sub: String = ""): DiskSelfie {
98-
val call = recordCall(false)
99-
if (Selfie.system.mode.canWrite(true, call, Selfie.system)) {
100-
disk.writeDisk(actual, sub, call)
101-
Selfie.system.writeInline(DiskSnapshotTodo.createLiteral(), call)
102-
return this
103-
} else {
104-
throw Selfie.system.fs.assertFailed("Can't call `toMatchDisk_TODO` in ${Mode.readonly} mode!")
105-
}
106-
}
107-
}
108-
109206
/**
110207
* Returns a serialized form of only the given facets if they are available, silently omits missing
111208
* facets.

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
*/
1616
package com.diffplug.selfie.coroutines
1717

18+
import com.diffplug.selfie.BinarySelfie
1819
import com.diffplug.selfie.Camera
19-
import com.diffplug.selfie.DiskSelfie
2020
import com.diffplug.selfie.Roundtrip
2121
import com.diffplug.selfie.RoundtripJson
2222
import com.diffplug.selfie.Snapshot
23+
import com.diffplug.selfie.StringSelfie
2324
import com.diffplug.selfie.guts.CoroutineDiskStorage
2425
import kotlin.coroutines.coroutineContext
2526

@@ -45,8 +46,8 @@ private suspend fun disk() =
4546
.trimIndent())
4647
suspend fun <T> expectSelfie(actual: T, camera: Camera<T>) = expectSelfie(camera.snapshot(actual))
4748
suspend fun expectSelfie(actual: String) = expectSelfie(Snapshot.of(actual))
48-
suspend fun expectSelfie(actual: ByteArray) = expectSelfie(Snapshot.of(actual))
49-
suspend fun expectSelfie(actual: Snapshot) = DiskSelfie(actual, disk())
49+
suspend fun expectSelfie(actual: ByteArray) = BinarySelfie(Snapshot.of(actual), disk(), "")
50+
suspend fun expectSelfie(actual: Snapshot) = StringSelfie(actual, disk())
5051
suspend fun preserveSelfiesOnDisk(vararg subsToKeep: String) {
5152
val disk = disk()
5253
if (subsToKeep.isEmpty()) {

0 commit comments

Comments
 (0)