Skip to content

Commit

Permalink
Rename to selfie (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg committed Aug 23, 2023
2 parents d92efc9 + a4ba0f3 commit a0aaadc
Show file tree
Hide file tree
Showing 26 changed files with 273 additions and 94 deletions.
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
# spotless-snapshot
# Selfie

- Precise snapshot testing for the JVM and Javascript (Native TODO)
- Precise snapshot testing for the JVM and Javascript (others TBD)
- Supports text and binary data.
- Friendly to humans and version control.
- In-place [literal snapshots](#literal-snapshots).
- Allows [lenses](#lenses) to verify multiple aspects of an object under test.
- e.g. pretty-printed JSON, or only the rendered plaintext of an HTML document, etc.

The example below uses the JUnit 5 runner, but we also support TODO.

Here is a very simple test which snapshots the HTML served at various URLs.
ere is a very simple test which snapshots the HTML served at various URLs.

```java
@Test public void gzipFavicon(Expect expect) {
expect.toMatchBinarySnapshot(get("localhost:8080/favicon.ico", ContentEncoding.GZIP));
@Test public void gzipFavicon() {
expectSelfie(get("/favicon.ico", ContentEncoding.GZIP)).toMatchDisk();
}
@Test public void orderFlow(Expect expect) {
expect.scenario("initial").toMatchSnapshot(get("localhost:8080/orders"));
@Test public void orderFlow() {
expectSelfie(get("/orders")).toMatchDisk("initial");
postOrder();
expect.scenario("ordered").toMatchSnapshot(get("localhost:8080/orders"));
expectSelfie(get("/orders")).toMatchDisk("ordered");
}
```

Expand All @@ -43,12 +41,12 @@ H4sIAAAAAAAA/8pIzcnJVyjPL8pJUQQAlQYXAAAA

### Literal snapshots

A great thing about snapshots is that they are fast to write and update. A downside is that the asserted value is opaque. But it doesn't have to be! Just swap `toMatchSnapshot` for `toMatchLiteral`.
A great thing about snapshots is that they are fast to write and update. A downside is that the asserted value is opaque. But it doesn't have to be! Just swap `toMatchDisk` for `toBe`.

```java
@Test public void preventCssBloat(Expect expect) {
// spotless-snapshot can update this literal value for you ▼
int size = expect.toMatchLiteral(get("/index.css").length, 5_236);
@Test public void preventCssBloat() {
// selfie can update this literal value for you ▼
int size = expectSelfie(get("/index.css").length).toBe(5_236);
if (size > 100_000) {
Assert.fail("CSS has gotten too big!");
}
Expand All @@ -62,7 +60,7 @@ Now we can see at a glance how a PR has affected the bundled size of our CSS. We
When snapshotting HTML, you might want to look at only the rendered text, ignoring tags, classes, and all that.

```java
public SnapshotConfig extends SpotlessSnapshotConfig {
public SelfieConfig extends com.diffplug.selfie.SelfieConfig {
@Override public @Nullable Snapshot intercept(Class<?> className, String testName, Snapshot snapshot) {
if (!snapshot.getValue().isBinary()) {
String content = snapshot.getValue().valueString();
Expand Down Expand Up @@ -92,9 +90,9 @@ Order information
Tracking #ABC123
```

Lenses can make PRs easier to review, by putting the focus on various aspects of the snapshot that are relevant to the change.
Lenses can make PRs easier to review, by putting the focus on whichever aspect of the snapshot is relevant to the change.

### Acknowledgements

- JUnit test runner heavily inspired by [origin-energy's java-snapshot-testing](https://github.com/origin-energy/java-snapshot-testing).
- Heavily inspired by [origin-energy's java-snapshot-testing](https://github.com/origin-energy/java-snapshot-testing).
- Which in turn is heavily inspired by [Facebook's jest-snapshot](https://jestjs.io/docs/snapshot-testing).
2 changes: 1 addition & 1 deletion gradle/spotless.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ spotless {
target 'src/**/*.kt'
licenseHeaderFile 干.file("spotless/license-${license}.java")
ktfmt()
for (modifier in ['', 'override ', 'public ', 'protected ', 'private ', 'internal ', 'expected ', 'actual ']) {
for (modifier in ['', 'override ', 'public ', 'protected ', 'private ', 'internal ', 'infix ', 'expected ', 'actual ']) {
for (key in ['inline', 'fun', 'val', 'override']) {
String toCheck = "$modifier$key"
replaceRegex("dense $toCheck", "\n\n(\\s*)$toCheck ", "\n\$1$toCheck ")
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

import kotlin.collections.binarySearch

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

expect class PerCharacterEscaper {
fun escape(input: String): String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

class ParseException(val line: Int, message: String) : IllegalArgumentException(message) {
constructor(lineReader: LineReader, message: String) : this(lineReader.getLineNumber(), message)
Expand Down Expand Up @@ -47,11 +47,6 @@ internal data class SnapshotValueString(val value: String) : SnapshotValue {
override fun valueString(): String = value
}

internal data class SnapshotValueEmptyString(val value: String) : SnapshotValue {
override fun valueBinary() = throw UnsupportedOperationException("This is an empty string value.")
override fun valueString() = value
}

data class Snapshot(
val value: SnapshotValue,
private val lensData: ArrayMap<String, SnapshotValue>
Expand All @@ -73,8 +68,31 @@ data class Snapshot(
interface Snapshotter<T> {
fun snapshot(value: T): Snapshot
}
fun parseSS(valueReader: SnapshotValueReader): ArrayMap<String, Snapshot> = TODO()
fun serializeSS(valueWriter: StringWriter, snapshots: ArrayMap<String, Snapshot>): Unit = TODO()

class SnapshotFile {
// this will probably become `<String, JsonObject>` we'll cross that bridge when we get to it
var metadata: Map.Entry<String, String>? = null
var snapshots = ArrayMap.empty<String, Snapshot>()
fun serialize(valueWriter: StringWriter): Unit = TODO()

companion object {
private val HEADER_PREFIX = """📷 """
fun parse(valueReader: SnapshotValueReader): SnapshotFile {
val result = SnapshotFile()
val reader = SnapshotReader(valueReader)
// only if the first value starts with 📷
if (reader.peekKey()?.startsWith(HEADER_PREFIX) == true) {
val metadataName = reader.peekKey()!!.substring(HEADER_PREFIX.length)
val metadataValue = reader.valueReader.nextValue().valueString()
result.metadata = entry(metadataName, metadataValue)
}
while (reader.peekKey() != null) {
result.snapshots = result.snapshots.plus(reader.peekKey()!!, reader.nextSnapshot())
}
return result
}
}
}

class SnapshotReader(val valueReader: SnapshotValueReader) {
fun peekKey(): String? = TODO()
Expand Down Expand Up @@ -176,11 +194,11 @@ class SnapshotValueReader(val lineReader: LineReader) {
private val headerEnd = " ═╗"

/**
* https://github.com/diffplug/spotless-snapshot/blob/f63192a84390901a3d3543066d095ea23bf81d21/snapshot-lib/src/commonTest/resources/com/diffplug/snapshot/scenarios_and_lenses.ss#L11-L29
* https://github.com/diffplug/selfie/blob/f63192a84390901a3d3543066d095ea23bf81d21/snapshot-lib/src/commonTest/resources/com/diffplug/snapshot/scenarios_and_lenses.ss#L11-L29
*/
private val nameEsc = PerCharacterEscaper.specifiedEscape("\\\\/∕[(])\nn\tt╔┌╗┐═─")

/** https://github.com/diffplug/spotless-snapshot/issues/2 */
/** https://github.com/diffplug/selfie/issues/2 */
private val bodyEsc = PerCharacterEscaper.selfEscape("\uD801\uDF43\uD801\uDF41")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

internal actual fun <K, V> entry(key: K, value: V): Map.Entry<K, V> = E(key, value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

actual class PerCharacterEscaper {
actual fun escape(input: String): String = TODO()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

actual class LineReader {
actual companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

internal actual fun <K, V> entry(key: K, value: V): Map.Entry<K, V> =
java.util.Map.entry(key, value)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

/**
* If your escape policy is "'123", it means this:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

import java.io.InputStreamReader
import java.io.LineNumberReader
Expand All @@ -24,7 +24,6 @@ import java.nio.charset.StandardCharsets
actual class LineReader(reader: Reader) : LineNumberReader(reader) {
actual companion object {
actual fun forString(content: String) = LineReader(StringReader(content))

actual fun forBinary(content: ByteArray) =
LineReader(InputStreamReader(content.inputStream(), StandardCharsets.UTF_8))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

import com.diffplug.snapshot.PerCharacterEscaper.Companion.selfEscape
import com.diffplug.snapshot.PerCharacterEscaper.Companion.specifiedEscape
import com.diffplug.selfie.PerCharacterEscaper.Companion.selfEscape
import com.diffplug.selfie.PerCharacterEscaper.Companion.specifiedEscape
import java.util.function.Consumer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot
package com.diffplug.selfie

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
Expand Down
25 changes: 25 additions & 0 deletions selfie-runner-junit5/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
}
repositories {
mavenCentral()
}
apply from: rootProject.file('gradle/spotless.gradle')
apply plugin: 'java-library'
dependencies {
implementation project(':selfie-lib')
// User supplied Junit5 version
String JUNIT_API = '5.0.0'
compileOnly "org.junit.jupiter:junit-jupiter-api:$JUNIT_API"
compileOnly "org.junit.jupiter:junit-jupiter-engine:$JUNIT_API"

testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5:1.9.0'
testImplementation 'io.kotest:kotest-assertions-core:5.6.2'

String JUNIT_IMPL = '5.10.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:$JUNIT_IMPL"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$JUNIT_IMPL"
}
test {
useJUnitPlatform()
}
90 changes: 90 additions & 0 deletions selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/Selfie.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.selfie

object SelfieRouting {
var isWriting: Boolean = true
var currentFile: SnapshotFile? = null
var currentDiskPrefix: String? = null
private fun assertInitializedProperly() {
if (currentFile == null || currentDiskPrefix == null) {
throw AssertionError("You called `toMatchDisk` without setting up snapshots.")
}
}
fun onDiskRightNow(scenario: String?): Snapshot? {
assertInitializedProperly()
val snapshotSuffix = scenario?.let { "/$scenario" } ?: ""
val snapshotName = "${currentDiskPrefix!!}${snapshotSuffix}"
return currentFile!!.snapshots.get(snapshotName)
}
}

open class DiskSelfie internal constructor(private val actual: Snapshot) {
fun toMatchDisk(scenario: String? = null): Snapshot {
val snapshot = SelfieRouting.onDiskRightNow(scenario)
if (actual != snapshot) {
if (SelfieRouting.isWriting) {
TODO("write snapshot")
} else {
throw AssertionError()
}
}
return actual
}
}
fun <T> expectSelfie(actual: T, snapshotter: Snapshotter<T>) =
DiskSelfie(snapshotter.snapshot(actual))

class StringSelfie(private val actual: String) : DiskSelfie(Snapshot.of(actual)) {
fun toBe(expected: String): String = TODO()
fun toBe_TODO(): String = TODO()
}
fun expectSelfie(actual: String) = StringSelfie(actual)

class BinarySelfie(private val actual: ByteArray) : DiskSelfie(Snapshot.of(actual)) {
fun toBeBase64(expected: String): ByteArray = TODO()
fun toBeBase64_TODO(): ByteArray = TODO()
}
fun expectSelfie(actual: ByteArray) = BinarySelfie(actual)

class IntSelfie(private val actual: Int) {
fun toBe(expected: Int): Int = TODO()
fun toBe_TODO(): Int = TODO()
}
fun expectSelfie(actual: Int) = IntSelfie(actual)

class LongSelfie(private val actual: Long) {
fun toBe(expected: Long): Long = TODO()
fun toBe_TODO(): Long = TODO()
}
fun expectSelfie(actual: Long) = LongSelfie(actual)

class BooleanSelfie(private val actual: Boolean) {
fun toBe(expected: Boolean): Boolean = TODO()
fun toBe_TODO(): Boolean = TODO()
}
fun expectSelfie(actual: Boolean) = BooleanSelfie(actual)

/** Sometimes a selfie doesn't get used. */
fun preserveDiskSelfies(vararg names: String): Unit = TODO()

// infix versions for the inline methods, consistent with Kotest's API
infix fun String.shouldBeSelfie(expected: String): String = expectSelfie(this).toBe(expected)
infix fun ByteArray.shouldBeSelfieBase64(expected: String): ByteArray =
expectSelfie(this).toBeBase64(expected)
infix fun Int.shouldBeSelfie(expected: Int): Int = expectSelfie(this).toBe(expected)
infix fun Long.shouldBeSelfie(expected: Long): Long = expectSelfie(this).toBe(expected)
infix fun Boolean.shouldBeSelfie(expected: Boolean): Boolean = expectSelfie(this).toBe(expected)
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.snapshot.jest
package com.diffplug.selfie.alternative_api

import com.diffplug.snapshot.Snapshot
import com.diffplug.snapshot.Snapshotter
import com.diffplug.selfie.Snapshot
import com.diffplug.selfie.Snapshotter
fun preserveScenarios(vararg names: String): Unit = TODO()

class Expect(private val actual: Snapshot) {
Expand Down

0 comments on commit a0aaadc

Please sign in to comment.