Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve cache docs #238

Merged
merged 13 commits into from
Feb 20, 2024
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
# Selfie: snapshot testing for Java, Kotlin, and the JVM
# Selfie: snapshot testing and [memoizing](https://selfie.dev/jvm/cache) for Java, Kotlin, and the JVM

![gif demo of selfie in action](https://docs.diffplug.com/selfie/selfie-demo.gif)

## Key features

- Just [add a test dependency](https://selfie.dev/jvm/get-started#installation), zero setup, zero config.
- Snapshots can be [inline literals](https://selfie.dev/jvm#literal) or [on disk](https://selfie.dev/jvm#like-a-filesystem).
- Use `expectSelfie` for testing or `cacheSelfie` for [memoizing expensive API calls](https://selfie.dev/jvm/cache).
- Disk snapshots are automatically [garbage collected](https://github.com/diffplug/selfie/blob/main/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieGC.kt) when the test class or test method is removed.
- Snapshots are **just strings**. Use html, json, markdown, whatever. No [magic serializers](https://selfie.dev/jvm/advanced#typed-snapshots).
- Snapshots are **just strings**. Use html, json, markdown, whatever. No [magic serializers](https://selfie.dev/jvm/facets#typed-snapshots).
- Record **multiple facets** of the entity under test, e.g. for a web request...
- store the HTML as one facet
- store HTML-rendered-to-markdown as another facet
- store cookies in another facet
- **assert some facets on disk, others inline**
- see gif above for live demo, detailed example [here](https://selfie.dev/jvm/advanced)

JVM only for now, other platforms on the way: [js](https://github.com/diffplug/selfie/issues/84), [py, go, ...](https://github.com/diffplug/selfie/issues/85)
JVM only for now, [python](https://github.com/diffplug/selfie/issues/170) is in progress, other platforms on the way: [js](https://github.com/diffplug/selfie/issues/84), [.NET, go, ...](https://github.com/diffplug/selfie/issues/85)

## Documentation

Expand Down
8 changes: 4 additions & 4 deletions jvm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Memoization** ([#219](https://github.com/diffplug/selfie/pull/219) implements [#215](https://github.com/diffplug/selfie/issues/215))
- like `expectSelfie`, all are available as `Selfie.memoize` or as `suspend fun` in `com.diffplug.selfie.coroutines`.
```kotlin
val cachedResult: ByteArray = Selfie.memoizeBinary { dalleJpeg() }.toBeFile("example.jpg")
val cachedResult: String = Selfie.memoize { someString() }.toBe("what it was earlier")
val cachedResult: T = Selfie.memoizeAsJson { anyKotlinxSerializable() }.toBe("""{"key": "value"}""")
val cachedResult: T = Selfie.memoizeBinarySerializable { anyJavaIoSerializable() }.toMatchDisk()
val cachedResult: ByteArray = Selfie.cacheSelfieBinary { dalleJpeg() }.toBeFile("example.jpg")
val cachedResult: String = Selfie.cacheSelfie { someString() }.toBe("what it was earlier")
val cachedResult: T = Selfie.cacheSelfieJson { anyKotlinxSerializable() }.toBe("""{"key": "value"}""")
val cachedResult: T = Selfie.cacheSelfieBinarySerializable { anyJavaIoSerializable() }.toMatchDisk()
```
- `toBeBase64` and `toBeFile` for true binary comparison of binary snapshots and facets. ([#224](https://github.com/diffplug/selfie/pull/224))
### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.diffplug.selfie.guts.recordCall
class CacheSelfie<T>(
private val disk: DiskStorage,
private val roundtrip: Roundtrip<T, String>,
private val generator: () -> T
private val generator: Cacheable<T>
) {
fun toMatchDisk(sub: String = ""): T {
return toMatchDiskImpl(sub, false)
Expand All @@ -35,7 +35,7 @@ class CacheSelfie<T>(
private fun toMatchDiskImpl(sub: String, isTodo: Boolean): T {
val call = recordCall(false)
if (Selfie.system.mode.canWrite(isTodo, call, Selfie.system)) {
val actual = generator()
val actual = generator.get()
disk.writeDisk(Snapshot.of(roundtrip.serialize(actual)), sub, call)
if (isTodo) {
Selfie.system.writeInline(TodoStub.toMatchDisk.createLiteral(), call)
Expand Down Expand Up @@ -67,7 +67,7 @@ class CacheSelfie<T>(
val call = recordCall(false)
val writable = Selfie.system.mode.canWrite(snapshot == null, call, Selfie.system)
if (writable) {
val actual = generator()
val actual = generator.get()
Selfie.system.writeInline(
LiteralValue(snapshot, roundtrip.serialize(actual), LiteralString), call)
return actual
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
class CacheSelfieBinary<T>(
private val disk: DiskStorage,
private val roundtrip: Roundtrip<T, ByteArray>,
private val generator: () -> T
private val generator: Cacheable<T>
) {
fun toMatchDisk(sub: String = ""): T {
return toMatchDiskImpl(sub, false)
Expand All @@ -37,7 +37,7 @@ class CacheSelfieBinary<T>(
private fun toMatchDiskImpl(sub: String, isTodo: Boolean): T {
val call = recordCall(false)
if (Selfie.system.mode.canWrite(isTodo, call, Selfie.system)) {
val actual = generator()
val actual = generator.get()
disk.writeDisk(Snapshot.of(roundtrip.serialize(actual)), sub, call)
if (isTodo) {
Selfie.system.writeInline(TodoStub.toMatchDisk.createLiteral(), call)
Expand Down Expand Up @@ -70,7 +70,7 @@ class CacheSelfieBinary<T>(
val call = recordCall(false)
val writable = Selfie.system.mode.canWrite(isTodo, call, Selfie.system)
if (writable) {
val actual = generator()
val actual = generator.get()
if (isTodo) {
Selfie.system.writeInline(TodoStub.toBeFile.createLiteral(), call)
}
Expand All @@ -96,7 +96,7 @@ class CacheSelfieBinary<T>(
val call = recordCall(false)
val writable = Selfie.system.mode.canWrite(snapshot == null, call, Selfie.system)
if (writable) {
val actual = generator()
val actual = generator.get()
val base64 = Base64.Mime.encode(roundtrip.serialize(actual)).replace("\r", "")
Selfie.system.writeInline(LiteralValue(snapshot, base64, LiteralString), call)
return actual
Expand Down
25 changes: 16 additions & 9 deletions jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Selfie.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import com.diffplug.selfie.guts.SnapshotSystem
import com.diffplug.selfie.guts.initSnapshotSystem
import kotlin.jvm.JvmStatic

/** A getter which may or may not be run. */
fun interface Cacheable<T> {
@Throws(Throwable::class) fun get(): T
}

/** Static methods for creating snapshots. */
object Selfie {
internal val system: SnapshotSystem = initSnapshotSystem()
Expand Down Expand Up @@ -57,23 +62,25 @@ object Selfie {
@JvmStatic fun expectSelfie(actual: Long) = LongSelfie(actual)
@JvmStatic fun expectSelfie(actual: Int) = IntSelfie(actual)
@JvmStatic fun expectSelfie(actual: Boolean) = BooleanSelfie(actual)
@JvmStatic fun cacheSelfie(toMemoize: () -> String) = cacheSelfie(Roundtrip.identity(), toMemoize)

@JvmStatic
fun <T> cacheSelfie(roundtrip: Roundtrip<T, String>, toMemoize: () -> T) =
CacheSelfie(deferredDiskStorage, roundtrip, toMemoize)
fun cacheSelfie(toCache: Cacheable<String>) = cacheSelfie(Roundtrip.identity(), toCache)

@JvmStatic
fun <T> cacheSelfie(roundtrip: Roundtrip<T, String>, toCache: Cacheable<T>) =
CacheSelfie(deferredDiskStorage, roundtrip, toCache)
/**
* Memoizes any type which is marked with `@kotlinx.serialization.Serializable` as pretty-printed
* json.
*/
inline fun <reified T> cacheSelfieJson(noinline toMemoize: () -> T) =
cacheSelfie(RoundtripJson.of<T>(), toMemoize)
inline fun <reified T> cacheSelfieJson(noinline toCache: () -> T) =
cacheSelfie(RoundtripJson.of<T>(), toCache)

@JvmStatic
fun cacheSelfieBinary(toMemoize: () -> ByteArray) =
cacheSelfieBinary(Roundtrip.identity(), toMemoize)
fun cacheSelfieBinary(toCache: Cacheable<ByteArray>) =
cacheSelfieBinary(Roundtrip.identity(), toCache)

@JvmStatic
fun <T> cacheSelfieBinary(roundtrip: Roundtrip<T, ByteArray>, toMemoize: () -> T) =
CacheSelfieBinary<T>(deferredDiskStorage, roundtrip, toMemoize)
fun <T> cacheSelfieBinary(roundtrip: Roundtrip<T, ByteArray>, toCache: Cacheable<T>) =
CacheSelfieBinary<T>(deferredDiskStorage, roundtrip, toCache)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,16 @@ suspend fun preserveSelfiesOnDisk(vararg subsToKeep: String) {
subsToKeep.forEach { disk.keep(it) }
}
}
suspend fun cacheSelfie(toMemoize: suspend () -> String) =
cacheSelfie(Roundtrip.identity(), toMemoize)
suspend fun <T> cacheSelfie(roundtrip: Roundtrip<T, String>, toMemoize: suspend () -> T) =
CacheSelfieSuspend(disk(), roundtrip, toMemoize)
suspend fun cacheSelfie(toCache: suspend () -> String) = cacheSelfie(Roundtrip.identity(), toCache)
suspend fun <T> cacheSelfie(roundtrip: Roundtrip<T, String>, toCache: suspend () -> T) =
CacheSelfieSuspend(disk(), roundtrip, toCache)
/**
* Memoizes any type which is marked with `@kotlinx.serialization.Serializable` as pretty-printed
* json.
*/
suspend inline fun <reified T> cacheSelfieJson(noinline toMemoize: suspend () -> T) =
cacheSelfie(RoundtripJson.of<T>(), toMemoize)
suspend fun cacheSelfieBinary(toMemoize: suspend () -> ByteArray) =
cacheSelfieBinary(Roundtrip.identity(), toMemoize)
suspend fun <T> cacheSelfieBinary(roundtrip: Roundtrip<T, ByteArray>, toMemoize: suspend () -> T) =
CacheSelfieBinarySuspend(disk(), roundtrip, toMemoize)
suspend inline fun <reified T> cacheSelfieJson(noinline toCache: suspend () -> T) =
cacheSelfie(RoundtripJson.of<T>(), toCache)
suspend fun cacheSelfieBinary(toCache: suspend () -> ByteArray) =
cacheSelfieBinary(Roundtrip.identity(), toCache)
suspend fun <T> cacheSelfieBinary(roundtrip: Roundtrip<T, ByteArray>, toCache: suspend () -> T) =
CacheSelfieBinarySuspend(disk(), roundtrip, toCache)
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import java.io.ByteArrayOutputStream
import java.io.ObjectOutputStream
import java.io.Serializable
fun <T : Serializable> Selfie.cacheSelfieBinarySerializable(
toMemoize: () -> T
toCache: Cacheable<T>
): CacheSelfieBinary<T> =
cacheSelfieBinary(SerializableRoundtrip as Roundtrip<T, ByteArray>, toMemoize)
cacheSelfieBinary(SerializableRoundtrip as Roundtrip<T, ByteArray>, toCache)

internal object SerializableRoundtrip : Roundtrip<Serializable, ByteArray> {
override fun serialize(value: Serializable): ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ import java.io.Serializable

/** Memoizes any [java.io.Serializable] type as a binary blob. */
suspend fun <T : Serializable> cacheSelfieBinarySerializable(
toMemoize: suspend () -> T
toCache: suspend () -> T
): CacheSelfieBinarySuspend<T> =
cacheSelfieBinary(SerializableRoundtrip as Roundtrip<T, ByteArray>, toMemoize)
cacheSelfieBinary(SerializableRoundtrip as Roundtrip<T, ByteArray>, toCache)
3 changes: 2 additions & 1 deletion selfie.dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ To develop locally, run `npm run dev` and you'll get a local dev server with hot

- JVM homepage, `/jvm` -> [`src/pages/jvm/index.mdx`](src/pages/jvm/index.mdx)
- JVM get started, `/jvm/get-started` -> [`src/pages/jvm/get-started.mdx`](src/pages/jvm/get-started.mdx)
- JVM advanced, `/jvm/advanced` edit [`src/pages/jvm/advanced.mdx`](src/pages/jvm/advanced.mdx)
- JVM facets, `/jvm/facets` edit [`src/pages/jvm/facets.mdx`](src/pages/jvm/facets.mdx)
- JVM cache, `/jvm/cache` edit [`src/pages/jvm/cache.mdx`](src/pages/jvm/cache.mdx)

### Deeper interventions

Expand Down
1 change: 1 addition & 0 deletions selfie.dev/public/_redirects
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/advanced /facets 301
/js https://github.com/diffplug/selfie/issues/84
/js/* https://github.com/diffplug/selfie/issues/84
/py https://github.com/diffplug/selfie/issues/85
Expand Down
Binary file added selfie.dev/public/cache.webp
Binary file not shown.
Binary file added selfie.dev/public/dalle-3.webp
Binary file not shown.
14 changes: 9 additions & 5 deletions selfie.dev/public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://selfie.dev/jvm</loc>
<lastmod>2024-02-11</lastmod>
<lastmod>2024-02-19</lastmod>
</url>
<url>
<loc>https://selfie.dev/jvm/get-started</loc>
<lastmod>2024-02-11</lastmod>
<lastmod>2024-02-19</lastmod>
</url>
<url>
<loc>https://selfie.dev/jvm/kotest</loc>
<lastmod>2024-02-11</lastmod>
<lastmod>2024-02-19</lastmod>
</url>
<url>
<loc>https://selfie.dev/jvm/advanced</loc>
<lastmod>2024-01-21</lastmod>
<loc>https://selfie.dev/jvm/facets</loc>
<lastmod>2024-02-19</lastmod>
</url>
<url>
<loc>https://selfie.dev/jvm/cache</loc>
<lastmod>2024-02-19</lastmod>
</url>
</urlset>
11 changes: 8 additions & 3 deletions selfie.dev/src/components/Navigation/SubNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ const subNavButtonList: SubNavButton[] = [
isPressed: (subpathName) => subpathName === "get-started",
},
{
text: "advanced",
hrefSubpath: "/advanced",
isPressed: (subpathName) => subpathName === "advanced",
text: "facets",
hrefSubpath: "/facets",
isPressed: (subpathName) => subpathName === "facets",
},
{
text: "cache",
hrefSubpath: "/cache",
isPressed: (subpathName) => subpathName === "cache",
},
];

Expand Down
2 changes: 1 addition & 1 deletion selfie.dev/src/lib/languageFromPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type LanguageSlug = keyof typeof languageSlugsToLabels;

export type PathParts = {
language: LanguageSlug;
subpath: "" | "get-started" | "advanced";
subpath: "" | "get-started" | "facets" | "cache";
is404: boolean;
};

Expand Down
Loading
Loading