Skip to content

Commit

Permalink
Inline string literals (single quoted line) (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Dec 20, 2023
2 parents 06f1eae + d4ab210 commit 739d716
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 30 deletions.
64 changes: 56 additions & 8 deletions selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Literals.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,63 @@ object LiteralInt : LiteralFormat<Int> {

object LiteralString : LiteralFormat<String> {
override fun encode(value: String, language: Language): String {
if (!value.contains("\n")) {
// TODO: replace \t, maybe others...
return "\"" + value.replace("\"", "\\\"") + "\""
} else {
// TODO: test! It's okay to assume Java 15+ for now
return "\"\"\"\n" + value + "\"\"\""
}
return singleLineJavaToSource(value)
}
override fun parse(str: String, language: Language): String {
TODO()
return singleLineJavaFromSource(str)
}
private fun singleLineJavaToSource(value: String): String {
val source = StringBuilder()
source.append("\"")
for (char in value) {
when (char) {
'\b' -> source.append("\\b")
'\n' -> source.append("\\n")
'\r' -> source.append("\\r")
'\t' -> source.append("\\t")
'\"' -> source.append("\\\"")
'\\' -> source.append("\\\\")
else ->
if (isControlChar(char)) {
source.append("\\u")
source.append(char.code.toString(16).padStart(4, '0'))
} else {
source.append(char)
}
}
}
source.append("\"")
return source.toString()
}
private fun isControlChar(c: Char): Boolean {
return c in '\u0000'..'\u001F' || c == '\u007F'
}
private fun singleLineJavaFromSource(source: String): String {
val value = StringBuilder()
var i = 0
while (i < source.length) {
var c = source[i]
if (c == '\\') {
i++
c = source[i]
when (c) {
'b' -> value.append('\b')
'n' -> value.append('\n')
'r' -> value.append('\r')
't' -> value.append('\t')
'\"' -> value.append('\"')
'\\' -> value.append('\\')
'u' -> {
val code = source.substring(i + 1, i + 5).toInt(16)
value.append(code.toChar())
i += 4
}
}
} else if (c != '\"') {
value.append(c)
}
i++
}
return value.toString()
}
}
19 changes: 19 additions & 0 deletions selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/SourceFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ class SourceFile(filename: String, content: String) {
*/
fun <T : Any> setLiteralAndGetNewlineDelta(literalValue: LiteralValue<T>): Int {
val encoded = literalValue.format.encode(literalValue.actual, language)
val roundTripped = literalValue.format.parse(encoded, language) // sanity check
if (roundTripped != literalValue.actual) {
throw Error(
"There is an error in " +
literalValue.format::class.simpleName +
", the following value isn't roundtripping.\n" +
"Please this error and the data below at https://github.com/diffplug/selfie/issues/new\n" +
"```\n" +
"ORIGINAL\n" +
literalValue.actual +
"\n" +
"ROUNDTRIPPED\n" +
roundTripped +
"\n" +
"ENCODED ORIGINAL\n" +
encoded +
"\n" +
"```\n")
}
val existingNewlines = slice.count { it == '\n' }
val newNewlines = encoded.count { it == '\n' }
contentSlice = Slice(slice.replaceSelfWith(".toBe($encoded)"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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

import io.kotest.matchers.shouldBe
import kotlin.test.Test

class LiteralStringTest {
@Test
fun encode() {
encode(
"1",
"""
"1"
"""
.trimIndent())
encode(
"1\n\tABC",
"""
"1\n\tABC"
"""
.trimIndent())
}
private fun encode(value: String, expected: String) {
val actual = LiteralString.encode(value, Language.JAVA)
actual shouldBe expected
}

@Test
fun decode() {
decode(
"""
"1"
"""
.trimIndent(),
"1")
decode(
"""
"1\n\tABC"
"""
.trimIndent(),
"1\n\tABC")
}
private fun decode(value: String, expected: String) {
val actual = LiteralString.parse(value, Language.JAVA)
actual shouldBe expected
}
}
44 changes: 22 additions & 22 deletions selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/Selfie.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,26 @@ object Selfie {
@JvmStatic
fun <T> expectSelfie(actual: T, camera: Camera<T>) = DiskSelfie(camera.snapshot(actual))

/** Implements the inline snapshot whenever a match fails. */
private fun <T : Any> toBeDidntMatch(expected: T?, actual: T, format: LiteralFormat<T>): T {
if (RW.isWrite) {
Router.writeInline(recordCall(), LiteralValue(expected, actual, format))
return actual
} else {
if (expected == null) {
throw AssertionFailedError(
"`.toBe_TODO()` was called in `read` mode, try again with selfie in write mode")
} else {
throw AssertionFailedError(
"Inline literal did not match the actual value", expected, actual)
}
}
}

class StringSelfie(private val actual: String) : DiskSelfie(Snapshot.of(actual)) {
fun toBe(expected: String): String = TODO()
fun toBe_TODO(): String = TODO()
fun toBe_TODO() = toBeDidntMatch(null, actual, LiteralString)
infix fun toBe(expected: String) =
if (actual == expected) expected else toBeDidntMatch(expected, actual, LiteralString)
}

@JvmStatic fun expectSelfie(actual: String) = StringSelfie(actual)
Expand All @@ -62,26 +79,9 @@ object Selfie {
@JvmStatic fun expectSelfie(actual: ByteArray) = BinarySelfie(actual)

class IntSelfie(private val actual: Int) {
fun toBe_TODO(): Int = toBeDidntMatch(null)
infix fun toBe(expected: Int): Int =
if (actual == expected) expected
else {
toBeDidntMatch(expected)
}
private fun toBeDidntMatch(expected: Int?): Int {
if (RW.isWrite) {
Router.writeInline(recordCall(), LiteralValue(expected, actual, LiteralInt))
return actual
} else {
if (expected == null) {
throw AssertionFailedError(
"`.toBe_TODO()` was called in `read` mode, try again with selfie in write mode")
} else {
throw AssertionFailedError(
"Inline literal did not match the actual value", expected, actual)
}
}
}
fun toBe_TODO() = toBeDidntMatch(null, actual, LiteralInt)
infix fun toBe(expected: Int) =
if (actual == expected) expected else toBeDidntMatch(expected, actual, LiteralInt)
}

@JvmStatic fun expectSelfie(actual: Int) = IntSelfie(actual)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.junit5

import io.kotest.matchers.shouldBe
import kotlin.test.Test
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.TestMethodOrder

/** Write-only test which asserts adding and removing snapshots results in same-class GC. */
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
// @DisableIfTestFails don't disable if test fails because we *have* to run cleanup
class InlineStringTest : Harness("undertest-junit5") {
@Test @Order(1)
fun toBe_TODO() {
ut_mirror().lineWith("@Ignore").setContent("//@Ignore")
ut_mirror().lineWith("expectSelfie").setContent(" expectSelfie(\"Hello world\").toBe_TODO()")
gradleReadSSFail()
}

@Test @Order(2)
fun toBe_write() {
gradleWriteSS()
ut_mirror().lineWith("expectSelfie").content() shouldBe
" expectSelfie(\"Hello world\").toBe(\"Hello world\")"
gradleReadSS()
}

@Test @Order(3)
fun cleanup() {
ut_mirror().lineWith("expectSelfie").setContent(" expectSelfie(\"Hello world\").toBe_TODO()")
ut_mirror().lineWith("//@Ignore").setContent("@Ignore")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package undertest.junit5
// spotless:off
import com.diffplug.selfie.Selfie.expectSelfie
import kotlin.test.Ignore
import kotlin.test.Test
// spotless:on

@Ignore
class UT_InlineStringTest {
@Test fun singleLine() {
expectSelfie("Hello world").toBe_TODO()
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
╔═ selfie ═╗
apple
╔═ selfie[count] ═╗
5
╔═ [end of file] ═╗

0 comments on commit 739d716

Please sign in to comment.