From 77e6054927796e2faab26a53320dab7d6f78363d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 8 Jan 2024 16:27:07 -0800 Subject: [PATCH 01/20] Improve the tests for our existing single-line code. --- .../com/diffplug/selfie/guts/Literals.kt | 4 +- .../diffplug/selfie/guts/LiteralStringTest.kt | 44 ++++++------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index 12a8b6c3..a2cd4f49 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -108,7 +108,7 @@ internal object LiteralString : LiteralFormat { override fun parse(str: String, language: Language): String { return singleLineJavaFromSource(str) } - private fun singleLineJavaToSource(value: String): String { + fun singleLineJavaToSource(value: String): String { val source = StringBuilder() source.append("\"") for (char in value) { @@ -134,7 +134,7 @@ internal object LiteralString : LiteralFormat { private fun isControlChar(c: Char): Boolean { return c in '\u0000'..'\u001F' || c == '\u007F' } - private fun singleLineJavaFromSource(source: String): String { + fun singleLineJavaFromSource(source: String): String { val value = StringBuilder() var i = 0 while (i < source.length) { diff --git a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt index ed5cc5b7..4c2953d5 100644 --- a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt +++ b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt @@ -20,42 +20,24 @@ import kotlin.test.Test class LiteralStringTest { @Test - fun encode() { - encode( - "1", - """ - "1" - """ - .trimIndent()) - encode( - "1\n\tABC", - """ - "1\n\tABC" - """ - .trimIndent()) + fun singleLineJavaToSource() { + singleLineJavaToSource("1", "'1'") + singleLineJavaToSource("\\", "'\\\\'") + singleLineJavaToSource("1\n\tABC", "'1\\n\\tABC'") } - private fun encode(value: String, expected: String) { - val actual = LiteralString.encode(value, Language.JAVA) - actual shouldBe expected + private fun singleLineJavaToSource(value: String, expected: String) { + val actual = LiteralString.singleLineJavaToSource(value) + actual shouldBe expected.replace("'", "\"") } @Test - fun decode() { - decode( - """ - "1" - """ - .trimIndent(), - "1") - decode( - """ - "1\n\tABC" - """ - .trimIndent(), - "1\n\tABC") + fun singleLineJavaFromSource() { + singleLineJavaFromSource("'1'", "1") + singleLineJavaFromSource("'\\\\'", "\\") + singleLineJavaFromSource("'1\\n\\tABC'", "1\n\tABC") } - private fun decode(value: String, expected: String) { - val actual = LiteralString.parse(value, Language.JAVA) + private fun singleLineJavaFromSource(value: String, expected: String) { + val actual = LiteralString.singleLineJavaFromSource(value.replace("'", "\"")) actual shouldBe expected } } From 3dc7f9375c810b0b2da5aed885eece064ff563d8 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 8 Jan 2024 16:59:13 -0800 Subject: [PATCH 02/20] We can now read java multiline strings. --- .../com/diffplug/selfie/guts/Literals.kt | 36 ++++++++++++++++--- .../diffplug/selfie/guts/LiteralStringTest.kt | 19 ++++++++-- .../src/test/java/JavaMultilineString.java | 35 ++++++++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 selfie-runner-junit5/src/test/java/JavaMultilineString.java diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index a2cd4f49..fd1549cd 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -135,33 +135,61 @@ internal object LiteralString : LiteralFormat { return c in '\u0000'..'\u001F' || c == '\u007F' } fun singleLineJavaFromSource(source: String): String { + val firstEscape = source.indexOf('\\') + if (firstEscape == -1) { + return source + } val value = StringBuilder() - var i = 0 + value.append(source.substring(0, firstEscape)) + var i = firstEscape while (i < source.length) { var c = source[i] if (c == '\\') { i++ c = source[i] when (c) { + '\"' -> value.append('\"') + '\\' -> value.append('\\') 'b' -> value.append('\b') + 'f' -> value.append('\u000c') 'n' -> value.append('\n') 'r' -> value.append('\r') + 's' -> value.append(' ') '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 != '\"') { + } else { value.append(c) } i++ } return value.toString() } + fun multiLineJavaFromSource(source: String): String { + val linesRaw = source.lines() + require(linesRaw[0].isBlank()) + val lines = linesRaw.drop(1) + val commonPrefix = + lines + .mapNotNull { line -> + if (line.isNotBlank()) line.takeWhile { it.isWhitespace() } else null + } + .minOrNull() + return lines.joinToString("\n") { line -> + if (line.isBlank()) { + "" + } else { + val removedPrefix = commonPrefix?.let { line.removePrefix(it) } ?: line + val removeTrailingWhitespace = removedPrefix.trimEnd() + val handleEscapeSequences = singleLineJavaFromSource(removeTrailingWhitespace) + handleEscapeSequences + } + } + } } internal object LiteralBoolean : LiteralFormat { diff --git a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt index 4c2953d5..2ede8c86 100644 --- a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt +++ b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt @@ -32,12 +32,25 @@ class LiteralStringTest { @Test fun singleLineJavaFromSource() { - singleLineJavaFromSource("'1'", "1") - singleLineJavaFromSource("'\\\\'", "\\") - singleLineJavaFromSource("'1\\n\\tABC'", "1\n\tABC") + singleLineJavaFromSource("1", "1") + singleLineJavaFromSource("\\\\", "\\") + singleLineJavaFromSource("1\\n\\tABC", "1\n\tABC") } private fun singleLineJavaFromSource(value: String, expected: String) { val actual = LiteralString.singleLineJavaFromSource(value.replace("'", "\"")) actual shouldBe expected } + + @Test + fun multiLineJavaFromSource() { + multiLineJavaFromSource("\n123\nabc", "123\nabc") + multiLineJavaFromSource("\n 123\n abc", "123\nabc") + multiLineJavaFromSource("\n 123 \n abc\t", "123\nabc") + multiLineJavaFromSource("\n 123 \n abc\t", "123\nabc") + multiLineJavaFromSource("\n 123 \\s\n abc\t\\s", "123 \nabc\t ") + } + private fun multiLineJavaFromSource(value: String, expected: String) { + val actual = LiteralString.multiLineJavaFromSource(value.replace("'", "\"")) + actual shouldBe expected + } } diff --git a/selfie-runner-junit5/src/test/java/JavaMultilineString.java b/selfie-runner-junit5/src/test/java/JavaMultilineString.java new file mode 100644 index 00000000..43a790bd --- /dev/null +++ b/selfie-runner-junit5/src/test/java/JavaMultilineString.java @@ -0,0 +1,35 @@ +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class JavaMultilineString { + @Test + public void newlines() { + Assertions.assertEquals("", """ + """); + Assertions.assertEquals("\n", """ + + """); + Assertions.assertEquals("\n\n", """ + + + """); + // this is illegal, there can be no text after the triple quotes + // Assertions.assertEquals("", """a + // """); + } + + @Test + public void indentation() { + Assertions.assertEquals("a", """ + a"""); + Assertions.assertEquals("a\n", """ + a + """); + Assertions.assertEquals("a\n", """ + a + """); + Assertions.assertEquals(" a\n", """ + a + """); + } +} \ No newline at end of file From 8704a1a41ff6b5f400094488f3d9144645e98ce4 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 8 Jan 2024 20:08:47 -0800 Subject: [PATCH 03/20] Make `LiteralFormat`'s methods internal so that we can change their implementation over time. --- .../com/diffplug/selfie/guts/Literals.kt | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index fd1549cd..624e8596 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -45,9 +45,9 @@ enum class Language { class LiteralValue(val expected: T?, val actual: T, val format: LiteralFormat) -interface LiteralFormat { - fun encode(value: T, language: Language): String - fun parse(str: String, language: Language): T +abstract class LiteralFormat { + internal abstract fun encode(value: T, language: Language): String + internal abstract fun parse(str: String, language: Language): T } private const val MAX_RAW_NUMBER = 1000 @@ -75,7 +75,7 @@ private fun encodeUnderscores( } } -internal object LiteralInt : LiteralFormat { +internal object LiteralInt : LiteralFormat() { override fun encode(value: Int, language: Language): String { return encodeUnderscores(StringBuilder(), value.toLong(), language).toString() } @@ -84,7 +84,7 @@ internal object LiteralInt : LiteralFormat { } } -internal object LiteralLong : LiteralFormat { +internal object LiteralLong : LiteralFormat() { override fun encode(value: Long, language: Language): String { val buffer = encodeUnderscores(StringBuilder(), value.toLong(), language) if (language != Language.CLOJURE) { @@ -101,7 +101,9 @@ internal object LiteralLong : LiteralFormat { } } -internal object LiteralString : LiteralFormat { +private const val TRIPLE_QUOTE = "\"\"\"" + +internal object LiteralString : LiteralFormat() { override fun encode(value: String, language: Language): String { return singleLineJavaToSource(value) } @@ -134,6 +136,27 @@ internal object LiteralString : LiteralFormat { private fun isControlChar(c: Char): Boolean { return c in '\u0000'..'\u001F' || c == '\u007F' } + fun multiLineJavaToSource(arg: String): String { + val escapeBackslashes = arg.replace("\\", "\\\\") + val escapeTripleQuotes = escapeBackslashes.replace(TRIPLE_QUOTE, "\\\"\\\"\\\"") + val protectWhitespace = + escapeTripleQuotes.lines().joinToString("\n") { line -> + val protectTrailingWhitespace = + if (line.endsWith(" ")) { + line.dropLast(1) + "\\s" + } else if (line.endsWith("\t")) { + line.dropLast(1) + "\\t" + } else line + val protectLeadingWhitespace = + if (protectTrailingWhitespace.startsWith(" ")) { + "\\s" + protectTrailingWhitespace.drop(1) + } else if (protectTrailingWhitespace.startsWith("\t")) { + "\\t" + protectTrailingWhitespace.drop(1) + } else protectTrailingWhitespace + protectLeadingWhitespace + } + return "$TRIPLE_QUOTE\n$protectWhitespace$TRIPLE_QUOTE" + } fun singleLineJavaFromSource(source: String): String { val firstEscape = source.indexOf('\\') if (firstEscape == -1) { @@ -192,7 +215,7 @@ internal object LiteralString : LiteralFormat { } } -internal object LiteralBoolean : LiteralFormat { +internal object LiteralBoolean : LiteralFormat() { override fun encode(value: Boolean, language: Language): String { return value.toString() } @@ -201,7 +224,7 @@ internal object LiteralBoolean : LiteralFormat { } } -internal object DiskSnapshotTodo : LiteralFormat { +internal object DiskSnapshotTodo : LiteralFormat() { override fun encode(value: Unit, language: Language) = throw UnsupportedOperationException() override fun parse(str: String, language: Language) = throw UnsupportedOperationException() fun createLiteral() = LiteralValue(null, Unit, DiskSnapshotTodo) From 17e307d0913ea7922a51dab76a83d6ab9ca26781 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 8 Jan 2024 20:09:16 -0800 Subject: [PATCH 04/20] Add tests for multiline java strings. --- .../com/diffplug/selfie/guts/LiteralStringTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt index 2ede8c86..d1456215 100644 --- a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt +++ b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt @@ -30,6 +30,17 @@ class LiteralStringTest { actual shouldBe expected.replace("'", "\"") } + @Test + fun multiLineJavaToSource() { + multiLineJavaToSource("1", "'''\n1'''") + multiLineJavaToSource("\\", "'''\n\\\\'''") + multiLineJavaToSource(" leading\ntrailing ", "'''\n" + "\\s leading\n" + "trailing \\s'''") + } + private fun multiLineJavaToSource(value: String, expected: String) { + val actual = LiteralString.multiLineJavaToSource(value) + actual shouldBe expected.replace("'", "\"") + } + @Test fun singleLineJavaFromSource() { singleLineJavaFromSource("1", "1") From 0cde50e3081ac4105da1aa0df053578c114e6895 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 8 Jan 2024 22:40:23 -0800 Subject: [PATCH 05/20] Keep `"` and `"""` included as part of the string literal. --- .../com/diffplug/selfie/guts/Literals.kt | 25 ++++++++++++------ .../com/diffplug/selfie/guts/SourceFile.kt | 26 +++++-------------- .../diffplug/selfie/guts/LiteralStringTest.kt | 4 +-- .../selfie/guts/SourceFileToBeTest.kt | 20 +++++++------- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index 624e8596..649b802b 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -157,7 +157,12 @@ internal object LiteralString : LiteralFormat() { } return "$TRIPLE_QUOTE\n$protectWhitespace$TRIPLE_QUOTE" } - fun singleLineJavaFromSource(source: String): String { + fun singleLineJavaFromSource(sourceWithQuotes: String): String { + check(sourceWithQuotes.startsWith('"')) + check(sourceWithQuotes.endsWith('"')) + return unescapeJava(sourceWithQuotes.substring(1, sourceWithQuotes.length - 1)) + } + private fun unescapeJava(source: String): String { val firstEscape = source.indexOf('\\') if (firstEscape == -1) { return source @@ -184,6 +189,7 @@ internal object LiteralString : LiteralFormat() { value.append(code.toChar()) i += 4 } + else -> throw IllegalArgumentException("Unknown escape sequence $c") } } else { value.append(c) @@ -192,23 +198,26 @@ internal object LiteralString : LiteralFormat() { } return value.toString() } - fun multiLineJavaFromSource(source: String): String { - val linesRaw = source.lines() - require(linesRaw[0].isBlank()) - val lines = linesRaw.drop(1) + fun multiLineJavaFromSource(sourceWithQuotes: String): String { + check(sourceWithQuotes.startsWith("$TRIPLE_QUOTE\n")) + check(sourceWithQuotes.endsWith(TRIPLE_QUOTE)) + val source = + sourceWithQuotes.substring( + TRIPLE_QUOTE.length + 1, sourceWithQuotes.length - TRIPLE_QUOTE.length) + val lines = source.lines() val commonPrefix = lines .mapNotNull { line -> if (line.isNotBlank()) line.takeWhile { it.isWhitespace() } else null } - .minOrNull() + .minOrNull() ?: "" return lines.joinToString("\n") { line -> if (line.isBlank()) { "" } else { - val removedPrefix = commonPrefix?.let { line.removePrefix(it) } ?: line + val removedPrefix = if (commonPrefix.isEmpty()) line else line.removePrefix(commonPrefix) val removeTrailingWhitespace = removedPrefix.trimEnd() - val handleEscapeSequences = singleLineJavaFromSource(removeTrailingWhitespace) + val handleEscapeSequences = unescapeJava(removeTrailingWhitespace) handleEscapeSequences } } diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SourceFile.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SourceFile.kt index 50fd617f..7c42ca36 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SourceFile.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SourceFile.kt @@ -36,12 +36,6 @@ class SourceFile(filename: String, content: String) { val asString: String get() = contentSlice.toString().let { if (unixNewlines) it else it.replace("\n", "\r\n") } - internal enum class ToBeArg { - NUMERIC, - STRING_SINGLEQUOTE, - STRING_TRIPLEQUOTE - } - /** * Represents a section of the sourcecode which is a `.toBe(LITERAL)` call. It might also be * `.toBe_TODO()` or ` toBe LITERAL` (infix notation). @@ -50,7 +44,6 @@ class SourceFile(filename: String, content: String) { internal constructor( internal val functionCallPlusArg: Slice, internal val arg: Slice, - internal val argType: ToBeArg ) { /** * Modifies the parent [SourceFile] to set the value within the `toBe` call, and returns the net @@ -139,22 +132,18 @@ class SourceFile(filename: String, content: String) { // argStart is now the first non-whitespace character after the opening paren var endArg = -1 var endParen: Int - val argType: ToBeArg if (contentSlice[argStart] == '"') { if (contentSlice.subSequence(argStart, contentSlice.length).startsWith(TRIPLE_QUOTE)) { - argType = ToBeArg.STRING_TRIPLEQUOTE - argStart += TRIPLE_QUOTE.length - endArg = contentSlice.indexOf(TRIPLE_QUOTE, argStart) + endArg = contentSlice.indexOf(TRIPLE_QUOTE, argStart + TRIPLE_QUOTE.length) if (endArg == -1) { throw AssertionError( "Appears to be an unclosed multiline string literal `${TRIPLE_QUOTE}` on line $lineOneIndexed") } else { - endParen = endArg + TRIPLE_QUOTE.length + endArg += TRIPLE_QUOTE.length + endParen = endArg } } else { - argType = ToBeArg.STRING_SINGLEQUOTE - argStart += 1 - endArg = argStart + endArg = argStart + 1 while (contentSlice[endArg] != '"' || contentSlice[endArg - 1] == '\\') { ++endArg if (endArg == contentSlice.length) { @@ -162,10 +151,10 @@ class SourceFile(filename: String, content: String) { "Appears to be an unclosed string literal `\"` on line $lineOneIndexed") } } - endParen = endArg + 1 + endArg += 1 + endParen = endArg } } else { - argType = ToBeArg.NUMERIC endArg = argStart while (!contentSlice[endArg].isWhitespace()) { if (contentSlice[endArg] == ')') { @@ -191,7 +180,6 @@ class SourceFile(filename: String, content: String) { } return ToBeLiteral( contentSlice.subSequence(dotFunctionCall, endParen + 1), - contentSlice.subSequence(argStart, endArg), - argType) + contentSlice.subSequence(argStart, endArg)) } } diff --git a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt index d1456215..019860a3 100644 --- a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt +++ b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt @@ -48,7 +48,7 @@ class LiteralStringTest { singleLineJavaFromSource("1\\n\\tABC", "1\n\tABC") } private fun singleLineJavaFromSource(value: String, expected: String) { - val actual = LiteralString.singleLineJavaFromSource(value.replace("'", "\"")) + val actual = LiteralString.singleLineJavaFromSource("\"${value.replace("'", "\"")}\"") actual shouldBe expected } @@ -61,7 +61,7 @@ class LiteralStringTest { multiLineJavaFromSource("\n 123 \\s\n abc\t\\s", "123 \nabc\t ") } private fun multiLineJavaFromSource(value: String, expected: String) { - val actual = LiteralString.multiLineJavaFromSource(value.replace("'", "\"")) + val actual = LiteralString.multiLineJavaFromSource("\"\"\"${value.replace("'", "\"")}\"\"\"") actual shouldBe expected } } diff --git a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/SourceFileToBeTest.kt b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/SourceFileToBeTest.kt index 2b301afd..6b1685ae 100644 --- a/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/SourceFileToBeTest.kt +++ b/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/SourceFileToBeTest.kt @@ -41,20 +41,20 @@ class SourceFileToBeTest { @Test fun singleLineString() { - javaTest(".toBe('7')", "7") - javaTest(".toBe('')", "") - javaTest(".toBe( '' )", "") - javaTest(".toBe( \n '' \n )", "") - javaTest(".toBe( \n '78' \n )", "78") - javaTest(".toBe('\\'')", "\\'") + javaTest(".toBe('7')", "'7'") + javaTest(".toBe('')", "''") + javaTest(".toBe( '' )", "''") + javaTest(".toBe( \n '' \n )", "''") + javaTest(".toBe( \n '78' \n )", "'78'") + javaTest(".toBe('\\'')", "'\\''") } @Test fun multiLineString() { - javaTest(".toBe('''7''')", "7") - javaTest(".toBe(''' 7 ''')", " 7 ") - javaTest(".toBe('''\n7\n''')", "\n7\n") - javaTest(".toBe(''' ' '' ' ''')", " ' '' ' ") + javaTest(".toBe('''7''')", "'''7'''") + javaTest(".toBe(''' 7 ''')", "''' 7 '''") + javaTest(".toBe('''\n7\n''')", "'''\n7\n'''") + javaTest(".toBe(''' ' '' ' ''')", "''' ' '' ' '''") } @Test From 7cd0789fa0677c9aa34e600bd015b27de413ce33 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 8 Jan 2024 22:51:29 -0800 Subject: [PATCH 06/20] Add a test for various challenging string literals. --- .../junit5/UT_JavaStringLiteralsTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 undertest-junit5/src/test/java/undertest/junit5/UT_JavaStringLiteralsTest.java diff --git a/undertest-junit5/src/test/java/undertest/junit5/UT_JavaStringLiteralsTest.java b/undertest-junit5/src/test/java/undertest/junit5/UT_JavaStringLiteralsTest.java new file mode 100644 index 00000000..f0011e95 --- /dev/null +++ b/undertest-junit5/src/test/java/undertest/junit5/UT_JavaStringLiteralsTest.java @@ -0,0 +1,35 @@ +package undertest.junit5; + +import org.junit.jupiter.api.Test; +import static com.diffplug.selfie.Selfie.expectSelfie; + +public class UT_JavaStringLiteralsTest { + @Test + public void empty() { + expectSelfie("").toBe_TODO(); + } + + @Test + public void tabs() { + expectSelfie("\t\t\t").toBe_TODO(); + } + + @Test + public void spaces() { + expectSelfie(" ").toBe_TODO(); + } + + @Test + public void newlines() { + expectSelfie("\n").toBe_TODO(); + expectSelfie("\n\n").toBe_TODO(); + expectSelfie("\n\n\n").toBe_TODO(); + } + + @Test + public void allOfIt() { + expectSelfie(" a\n" + + "a \n" + + "\t a \t\n").toBe_TODO(); + } +} From 5ce90cfeec34d3ab0aea49b9f59e293db7807004 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 8 Jan 2024 23:16:15 -0800 Subject: [PATCH 07/20] Wire up the java string literals and use them in an integration test. --- .../com/diffplug/selfie/guts/Literals.kt | 11 ++--- .../selfie/junit5/JavaStringLiteralsTest.kt | 46 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index 649b802b..2317ee12 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -104,12 +104,11 @@ internal object LiteralLong : LiteralFormat() { private const val TRIPLE_QUOTE = "\"\"\"" internal object LiteralString : LiteralFormat() { - override fun encode(value: String, language: Language): String { - return singleLineJavaToSource(value) - } - override fun parse(str: String, language: Language): String { - return singleLineJavaFromSource(str) - } + override fun encode(value: String, language: Language): String = + if (value.contains("\n")) multiLineJavaToSource(value) else singleLineJavaToSource(value) + override fun parse(str: String, language: Language): String = + if (str.startsWith(TRIPLE_QUOTE)) multiLineJavaFromSource(str) + else singleLineJavaFromSource(str) fun singleLineJavaToSource(value: String): String { val source = StringBuilder() source.append("\"") diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt new file mode 100644 index 00000000..6044d5cf --- /dev/null +++ b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 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 kotlin.test.Test +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.TestMethodOrder +import org.junitpioneer.jupiter.DisableIfTestFails + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +@DisableIfTestFails +class JavaStringLiteralsTest : Harness("undertest-junit5") { + @Test @Order(1) + fun readFailsBecauseTodo() { + gradleReadSSFail() + } + + @Test @Order(2) + fun writeSucceeds() { + gradleWriteSS() + } + + @Test @Order(3) + fun nowReadSucceeds() { + gradleReadSS() + } + + @Test @Order(4) + fun cleanup() { + ut_mirror().restoreFromGit() + } +} From 01abf8ef3dd1922574d51b0723f2c1675e28a39c Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 00:34:53 -0800 Subject: [PATCH 08/20] Fix test cleanup. --- .../kotlin/com/diffplug/selfie/junit5/Harness.kt | 12 ++++++++++++ .../diffplug/selfie/junit5/JavaStringLiteralsTest.kt | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt index 31b460cd..6b884a81 100644 --- a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt +++ b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt @@ -24,12 +24,14 @@ import javax.xml.xpath.XPathFactory import okio.FileSystem import okio.Path import okio.Path.Companion.toPath +import okio.internal.commonToUtf8String import org.gradle.tooling.BuildException import org.gradle.tooling.GradleConnector import org.gradle.tooling.TestExecutionException import org.opentest4j.AssertionFailedError import org.w3c.dom.NodeList import org.xml.sax.InputSource +import java.nio.charset.StandardCharsets open class Harness(subproject: String) { // not sure why, but it doesn't work in this project @@ -51,6 +53,7 @@ open class Harness(subproject: String) { "The subproject folder $subproject must exist" } } + protected fun ut_mirrorJava() = file("UT_${javaClass.simpleName}.java") protected fun ut_mirror() = file("UT_${javaClass.simpleName}.kt") protected fun ut_snapshot() = file("UT_${javaClass.simpleName}.ss") fun file(nameOrSubpath: String): FileHarness { @@ -74,6 +77,15 @@ open class Harness(subproject: String) { } inner class FileHarness(val subpath: String) { + fun restoreFromGit() { + val absolute = subprojectFolder.resolve(subpath) + val pwd = Runtime.getRuntime().exec("pwd", arrayOf(), subprojectFolder.toFile()).inputStream.readAllBytes().toString(StandardCharsets.UTF_8) + val git = Runtime.getRuntime().exec("git checkout **/${absolute.toFile().name}", arrayOf(), subprojectFolder.toFile()) + println(pwd) + println(git.inputStream.readAllBytes().toString(StandardCharsets.UTF_8)) + println(git.errorStream.readAllBytes().toString(StandardCharsets.UTF_8)) + println("done") + } fun assertDoesNotExist() { if (FileSystem.SYSTEM.exists(subprojectFolder.resolve(subpath))) { throw AssertionError("Expected $subpath to not exist, but it does") diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt index 6044d5cf..7dfaaf1b 100644 --- a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt +++ b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt @@ -41,6 +41,6 @@ class JavaStringLiteralsTest : Harness("undertest-junit5") { @Test @Order(4) fun cleanup() { - ut_mirror().restoreFromGit() + ut_mirrorJava().restoreFromGit() } } From 1a0886ed4e5157d3a612a8288bccf0b292ae453f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 09:53:44 -0800 Subject: [PATCH 09/20] Only use multiline string literals when appropriate. --- .../kotlin/com/diffplug/selfie/guts/Literals.kt | 11 ++++++++++- .../kotlin/com/diffplug/selfie/junit5/Harness.kt | 12 +++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index 2317ee12..74cd2bf4 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -105,7 +105,16 @@ private const val TRIPLE_QUOTE = "\"\"\"" internal object LiteralString : LiteralFormat() { override fun encode(value: String, language: Language): String = - if (value.contains("\n")) multiLineJavaToSource(value) else singleLineJavaToSource(value) + if (value.indexOf('\n') != -1) singleLineJavaToSource(value) + else + when (language) { + Language.GROOVY, + Language.SCALA, + Language.CLOJURE, + Language.JAVA_PRE15 -> singleLineJavaToSource(value) + Language.JAVA -> multiLineJavaToSource(value) + Language.KOTLIN -> multiLineJavaToSource(value) + } override fun parse(str: String, language: Language): String = if (str.startsWith(TRIPLE_QUOTE)) multiLineJavaFromSource(str) else singleLineJavaFromSource(str) diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt index 6b884a81..fd6ec88e 100644 --- a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt +++ b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/Harness.kt @@ -24,14 +24,12 @@ import javax.xml.xpath.XPathFactory import okio.FileSystem import okio.Path import okio.Path.Companion.toPath -import okio.internal.commonToUtf8String import org.gradle.tooling.BuildException import org.gradle.tooling.GradleConnector import org.gradle.tooling.TestExecutionException import org.opentest4j.AssertionFailedError import org.w3c.dom.NodeList import org.xml.sax.InputSource -import java.nio.charset.StandardCharsets open class Harness(subproject: String) { // not sure why, but it doesn't work in this project @@ -78,13 +76,9 @@ open class Harness(subproject: String) { inner class FileHarness(val subpath: String) { fun restoreFromGit() { - val absolute = subprojectFolder.resolve(subpath) - val pwd = Runtime.getRuntime().exec("pwd", arrayOf(), subprojectFolder.toFile()).inputStream.readAllBytes().toString(StandardCharsets.UTF_8) - val git = Runtime.getRuntime().exec("git checkout **/${absolute.toFile().name}", arrayOf(), subprojectFolder.toFile()) - println(pwd) - println(git.inputStream.readAllBytes().toString(StandardCharsets.UTF_8)) - println(git.errorStream.readAllBytes().toString(StandardCharsets.UTF_8)) - println("done") + val path = subprojectFolder.resolve(subpath) + Runtime.getRuntime() + .exec("git checkout **/${path.toFile().name}", arrayOf(), subprojectFolder.toFile()) } fun assertDoesNotExist() { if (FileSystem.SYSTEM.exists(subprojectFolder.resolve(subpath))) { From c2ac8ff4d0a1df42e39605ded31ae0a2a0186371 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 09:57:02 -0800 Subject: [PATCH 10/20] Remove `InlineStringTest` since it duplicates JavaStringLiteralsTest. --- .../selfie/junit5/InlineStringTest.kt | 48 ------------------- .../undertest/junit5/UT_InlineStringTest.kt | 13 ----- 2 files changed, 61 deletions(-) delete mode 100644 selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/InlineStringTest.kt delete mode 100644 undertest-junit5/src/test/kotlin/undertest/junit5/UT_InlineStringTest.kt diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/InlineStringTest.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/InlineStringTest.kt deleted file mode 100644 index 7f2ecd6e..00000000 --- a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/InlineStringTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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") - } -} diff --git a/undertest-junit5/src/test/kotlin/undertest/junit5/UT_InlineStringTest.kt b/undertest-junit5/src/test/kotlin/undertest/junit5/UT_InlineStringTest.kt deleted file mode 100644 index f1e6fe03..00000000 --- a/undertest-junit5/src/test/kotlin/undertest/junit5/UT_InlineStringTest.kt +++ /dev/null @@ -1,13 +0,0 @@ -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() - } -} From 8393d1701d601176ffcc80f8eca53627b3684f59 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 09:59:22 -0800 Subject: [PATCH 11/20] Rename `JavaStringLiteralsTest` to `StringLiteralsJavaTest`. --- .../{JavaStringLiteralsTest.kt => StringLiteralsJavaTest.kt} | 0 ...vaStringLiteralsTest.java => UT_StringLiteralsJavaTest.java} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/{JavaStringLiteralsTest.kt => StringLiteralsJavaTest.kt} (100%) rename undertest-junit5/src/test/java/undertest/junit5/{UT_JavaStringLiteralsTest.java => UT_StringLiteralsJavaTest.java} (94%) diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/StringLiteralsJavaTest.kt similarity index 100% rename from selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/JavaStringLiteralsTest.kt rename to selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/StringLiteralsJavaTest.kt diff --git a/undertest-junit5/src/test/java/undertest/junit5/UT_JavaStringLiteralsTest.java b/undertest-junit5/src/test/java/undertest/junit5/UT_StringLiteralsJavaTest.java similarity index 94% rename from undertest-junit5/src/test/java/undertest/junit5/UT_JavaStringLiteralsTest.java rename to undertest-junit5/src/test/java/undertest/junit5/UT_StringLiteralsJavaTest.java index f0011e95..44d2059d 100644 --- a/undertest-junit5/src/test/java/undertest/junit5/UT_JavaStringLiteralsTest.java +++ b/undertest-junit5/src/test/java/undertest/junit5/UT_StringLiteralsJavaTest.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import static com.diffplug.selfie.Selfie.expectSelfie; -public class UT_JavaStringLiteralsTest { +public class UT_StringLiteralsJavaTest { @Test public void empty() { expectSelfie("").toBe_TODO(); From 240219278a7b2f4c3f41c9f3b467c42607786f1d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 11:51:38 -0800 Subject: [PATCH 12/20] Fix silly mistake in LiteralString. --- .../src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index 74cd2bf4..9a955757 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -105,7 +105,7 @@ private const val TRIPLE_QUOTE = "\"\"\"" internal object LiteralString : LiteralFormat() { override fun encode(value: String, language: Language): String = - if (value.indexOf('\n') != -1) singleLineJavaToSource(value) + if (value.indexOf('\n') == -1) singleLineJavaToSource(value) else when (language) { Language.GROOVY, From fac76b0a85dfde09d7d2d5f144fc1f7045dd01b5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 11:55:48 -0800 Subject: [PATCH 13/20] Fix bad rename. --- .../kotlin/com/diffplug/selfie/junit5/StringLiteralsJavaTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/StringLiteralsJavaTest.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/StringLiteralsJavaTest.kt index 7dfaaf1b..85231e0d 100644 --- a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/StringLiteralsJavaTest.kt +++ b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junit5/StringLiteralsJavaTest.kt @@ -23,7 +23,7 @@ import org.junitpioneer.jupiter.DisableIfTestFails @TestMethodOrder(MethodOrderer.OrderAnnotation::class) @DisableIfTestFails -class JavaStringLiteralsTest : Harness("undertest-junit5") { +class StringLiteralsJavaTest : Harness("undertest-junit5") { @Test @Order(1) fun readFailsBecauseTodo() { gradleReadSSFail() From b51a639bbb0ef6e9cff4630210c8ee3db6426978 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 12:24:42 -0800 Subject: [PATCH 14/20] Better null safety and error messages for `sourcePathForCall`. --- .../src/commonMain/kotlin/com/diffplug/selfie/Mode.kt | 2 +- .../kotlin/com/diffplug/selfie/guts/SnapshotStorage.kt | 3 ++- .../kotlin/com/diffplug/selfie/guts/WriteTracker.kt | 4 +--- .../kotlin/com/diffplug/selfie/guts/WriteTracker.jvm.kt | 2 +- .../diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt | 9 ++++++++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Mode.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Mode.kt index 0337ce73..cc962204 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Mode.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/Mode.kt @@ -29,7 +29,7 @@ enum class Mode { readonly -> { if (storage.sourceFileHasWritableComment(call)) { val layout = storage.layout - val path = layout.sourcePathForCall(call.location)!! + val path = layout.sourcePathForCall(call.location) val (comment, line) = CommentTracker.commentString(path, storage.fs) throw storage.fs.assertFailed( "Selfie is in readonly mode, so `$comment` is illegal at ${call.location.withLine(line).ideLink(layout)}") diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SnapshotStorage.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SnapshotStorage.kt index bd4d21ad..442cbd1c 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SnapshotStorage.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/SnapshotStorage.kt @@ -57,5 +57,6 @@ interface SnapshotFileLayout { val rootFolder: Path val fs: FS val allowMultipleEquivalentWritesToOneLocation: Boolean - fun sourcePathForCall(call: CallLocation): Path? + fun sourcePathForCall(call: CallLocation): Path + fun sourcePathForCallMaybe(call: CallLocation): Path? } diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/WriteTracker.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/WriteTracker.kt index bbfd072b..ba5e1ec4 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/WriteTracker.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/WriteTracker.kt @@ -83,9 +83,7 @@ class InlineWriteTracker : WriteTracker>() { recordInternal(call.location, literalValue, call, layout) // assert that the value passed at runtime matches the value we parse at compile time // because if that assert fails, we've got no business modifying test code - val file = - layout.sourcePathForCall(call.location) - ?: throw Error("Unable to find source file for ${call.location.ideLink(layout)}") + val file = layout.sourcePathForCall(call.location) if (literalValue.expected != null) { // if expected == null, it's a `toBe_TODO()`, so there's nothing to check val content = SourceFile(layout.fs.name(file), layout.fs.fileRead(file)) diff --git a/selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/WriteTracker.jvm.kt b/selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/WriteTracker.jvm.kt index d60c2237..60791878 100644 --- a/selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/WriteTracker.jvm.kt +++ b/selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/WriteTracker.jvm.kt @@ -40,7 +40,7 @@ actual data class CallLocation( if (fileName != null) { return fileName } - return layout.sourcePathForCall(this)?.let { layout.fs.name(it) } + return layout.sourcePathForCallMaybe(this)?.let { layout.fs.name(it) } ?: "${clazz.substringAfterLast('.')}.class" } diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt index aefd7a28..c02e3b8e 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt @@ -29,7 +29,14 @@ class SnapshotFileLayoutJUnit5(settings: SelfieSettingsAPI, override val fs: FS) internal val unixNewlines = inferDefaultLineEndingIsUnix(settings.rootFolder, fs) val extension: String = ".ss" private val cache = ThreadLocal?>() - override fun sourcePathForCall(call: CallLocation): Path? { + override fun sourcePathForCall(call: CallLocation): Path { + val nonNull = + sourcePathForCallMaybe(call) + ?: throw fs.assertFailed( + "Couldn't find source file for $call, looked in $rootFolder, maybe there are other source roots?") + return nonNull + } + override fun sourcePathForCallMaybe(call: CallLocation): Path? { val cached = cache.get() if (cached?.first?.samePathAs(call) == true) { return cached.second From 6c47e303fdb797739a7884818ab2fb47d3ea84cc Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 12:30:53 -0800 Subject: [PATCH 15/20] Support multiple test source roots. --- .../selfie/junit5/SelfieSettingsAPI.kt | 18 ++++++++++++++++++ .../selfie/junit5/SnapshotFileLayoutJUnit5.kt | 13 ++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieSettingsAPI.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieSettingsAPI.kt index 51e51372..94e9de11 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieSettingsAPI.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieSettingsAPI.kt @@ -50,6 +50,24 @@ open class SelfieSettingsAPI { "Could not find a standard test directory, 'user.dir' is equal to $userDir, looked in $STANDARD_DIRS") } + /** + * If Selfie should look for test sourcecode in places other than the rootFolder, you can specify + * them here. + */ + open val otherSourceRoots: List + get() { + return buildList { + val rootDir = rootFolder + val userDir = File(System.getProperty("user.dir")) + for (standardDir in STANDARD_DIRS) { + val candidate = userDir.resolve(standardDir) + if (candidate.isDirectory && candidate != rootDir) { + add(candidate) + } + } + } + } + internal companion object { private val STANDARD_DIRS = listOf( diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt index c02e3b8e..03b6f41f 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt @@ -23,6 +23,7 @@ import com.diffplug.selfie.guts.SnapshotFileLayout class SnapshotFileLayoutJUnit5(settings: SelfieSettingsAPI, override val fs: FS) : SnapshotFileLayout { override val rootFolder = settings.rootFolder + private val otherSourceRoots = settings.otherSourceRoots override val allowMultipleEquivalentWritesToOneLocation = settings.allowMultipleEquivalentWritesToOneLocation val snapshotFolderName = settings.snapshotFolderName @@ -48,16 +49,22 @@ class SnapshotFileLayoutJUnit5(settings: SelfieSettingsAPI, override val fs: FS) path } } - private fun computePathForCall(call: CallLocation): Path? { + private fun computePathForCall(call: CallLocation): Path? = + sequence { + yield(rootFolder) + yieldAll(otherSourceRoots) + } + .firstNotNullOfOrNull { computePathForCall(it, call) } + private fun computePathForCall(folder: Path, call: CallLocation): Path? { if (call.fileName != null) { - return fs.fileWalk(rootFolder) { walk -> + return fs.fileWalk(folder) { walk -> walk.filter { fs.name(it) == call.fileName }.firstOrNull() } } val fileWithoutExtension = call.clazz.substringAfterLast('.').substringBefore('$') val likelyExtensions = listOf("kt", "java", "scala", "groovy", "clj", "cljc") val possibleNames = likelyExtensions.map { "$fileWithoutExtension.$it" }.toSet() - return fs.fileWalk(rootFolder) { walk -> + return fs.fileWalk(folder) { walk -> walk.filter { fs.name(it) in possibleNames }.firstOrNull() } } From 5f2cf2746eeeb19bab3e89224484bea71cc35a1e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 12:43:30 -0800 Subject: [PATCH 16/20] Better error message. --- .../com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt index 03b6f41f..8185bd58 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SnapshotFileLayoutJUnit5.kt @@ -34,7 +34,7 @@ class SnapshotFileLayoutJUnit5(settings: SelfieSettingsAPI, override val fs: FS) val nonNull = sourcePathForCallMaybe(call) ?: throw fs.assertFailed( - "Couldn't find source file for $call, looked in $rootFolder, maybe there are other source roots?") + "Couldn't find source file for $call, looked in $rootFolder and $otherSourceRoots, maybe there are other source roots?") return nonNull } override fun sourcePathForCallMaybe(call: CallLocation): Path? { From 05d63199e724b4e56e1717091e2adaa8ad2245b9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 12:43:50 -0800 Subject: [PATCH 17/20] Move snapshot files now that we have a `src/test/java` folder where they can go. --- .../{kotlin => java}/undertest/junit5/UT_CarriageReturnTest.ss | 0 .../{kotlin => java}/undertest/junit5/UT_DuplicateWriteTest.ss | 0 .../{kotlin => java}/undertest/junit5/UT_WithinMethodGCTest.ss | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename undertest-junit5/src/test/{kotlin => java}/undertest/junit5/UT_CarriageReturnTest.ss (100%) rename undertest-junit5/src/test/{kotlin => java}/undertest/junit5/UT_DuplicateWriteTest.ss (100%) rename undertest-junit5/src/test/{kotlin => java}/undertest/junit5/UT_WithinMethodGCTest.ss (100%) diff --git a/undertest-junit5/src/test/kotlin/undertest/junit5/UT_CarriageReturnTest.ss b/undertest-junit5/src/test/java/undertest/junit5/UT_CarriageReturnTest.ss similarity index 100% rename from undertest-junit5/src/test/kotlin/undertest/junit5/UT_CarriageReturnTest.ss rename to undertest-junit5/src/test/java/undertest/junit5/UT_CarriageReturnTest.ss diff --git a/undertest-junit5/src/test/kotlin/undertest/junit5/UT_DuplicateWriteTest.ss b/undertest-junit5/src/test/java/undertest/junit5/UT_DuplicateWriteTest.ss similarity index 100% rename from undertest-junit5/src/test/kotlin/undertest/junit5/UT_DuplicateWriteTest.ss rename to undertest-junit5/src/test/java/undertest/junit5/UT_DuplicateWriteTest.ss diff --git a/undertest-junit5/src/test/kotlin/undertest/junit5/UT_WithinMethodGCTest.ss b/undertest-junit5/src/test/java/undertest/junit5/UT_WithinMethodGCTest.ss similarity index 100% rename from undertest-junit5/src/test/kotlin/undertest/junit5/UT_WithinMethodGCTest.ss rename to undertest-junit5/src/test/java/undertest/junit5/UT_WithinMethodGCTest.ss From 990790ac1fd1dd55cad670254717b3c3b2c23650 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 12:50:25 -0800 Subject: [PATCH 18/20] Fix compile error. --- selfie-lib/src/jvmTest/kotlin/testpkg/RecordCallTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/selfie-lib/src/jvmTest/kotlin/testpkg/RecordCallTest.kt b/selfie-lib/src/jvmTest/kotlin/testpkg/RecordCallTest.kt index b6821644..9b75ba7c 100644 --- a/selfie-lib/src/jvmTest/kotlin/testpkg/RecordCallTest.kt +++ b/selfie-lib/src/jvmTest/kotlin/testpkg/RecordCallTest.kt @@ -37,6 +37,7 @@ class RecordCallTest { override val allowMultipleEquivalentWritesToOneLocation: Boolean get() = TODO() override fun sourcePathForCall(call: CallLocation) = Path("testpkg/RecordCallTest.kt") + override fun sourcePathForCallMaybe(call: CallLocation): Path? = sourcePathForCall(call) } stack.location.ideLink(layout) shouldBe "testpkg.RecordCallTest.testRecordCall(RecordCallTest.kt:30)" From 6c6fa728e8e6767ce9059e0183a16f2ddfd9474f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 12:44:49 -0800 Subject: [PATCH 19/20] Detect the actual running JVM. --- .../com/diffplug/selfie/guts/Literals.kt | 4 ++- .../com/diffplug/selfie/guts/Literals.js.kt | 22 ++++++++++++++++ .../com/diffplug/selfie/guts/Literals.jvm.kt | 25 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 selfie-lib/src/jsMain/kotlin/com/diffplug/selfie/guts/Literals.js.kt create mode 100644 selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/Literals.jvm.kt diff --git a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt index 9a955757..84aedd45 100644 --- a/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt +++ b/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt @@ -17,6 +17,8 @@ package com.diffplug.selfie.guts import kotlin.math.abs +internal expect fun jreVersion(): Int + enum class Language { JAVA, JAVA_PRE15, @@ -28,7 +30,7 @@ enum class Language { companion object { fun fromFilename(filename: String): Language { return when (filename.substringAfterLast('.')) { - "java" -> JAVA_PRE15 // TODO: detect JRE and use JAVA if JVM >= 15 + "java" -> if (jreVersion() < 15) JAVA_PRE15 else JAVA "kt" -> KOTLIN "groovy", "gvy", diff --git a/selfie-lib/src/jsMain/kotlin/com/diffplug/selfie/guts/Literals.js.kt b/selfie-lib/src/jsMain/kotlin/com/diffplug/selfie/guts/Literals.js.kt new file mode 100644 index 00000000..b054cd52 --- /dev/null +++ b/selfie-lib/src/jsMain/kotlin/com/diffplug/selfie/guts/Literals.js.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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.guts + +/** + * If the user is hip enough to run javascript, they're probably hip enough for multiline string + * literals. + */ +internal actual fun jreVersion(): Int = 15 diff --git a/selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/Literals.jvm.kt b/selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/Literals.jvm.kt new file mode 100644 index 00000000..ef6c10b4 --- /dev/null +++ b/selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/guts/Literals.jvm.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.guts + +internal actual fun jreVersion(): Int { + val versionStr = System.getProperty("java.version") + return if (versionStr.startsWith("1.")) { + if (versionStr.startsWith("1.8")) 8 else throw Error("Unsupported java version: $versionStr") + } else { + versionStr.substringBefore('.').toInt() + } +} From b48b6a1e972702204d53da990314cd6f38d54f5b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 9 Jan 2024 12:54:37 -0800 Subject: [PATCH 20/20] Remove this test because it only compiles for Java 11, and we are implicitly testing these in StringLiteralsJavaTest now anyway. --- .../src/test/java/JavaMultilineString.java | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 selfie-runner-junit5/src/test/java/JavaMultilineString.java diff --git a/selfie-runner-junit5/src/test/java/JavaMultilineString.java b/selfie-runner-junit5/src/test/java/JavaMultilineString.java deleted file mode 100644 index 43a790bd..00000000 --- a/selfie-runner-junit5/src/test/java/JavaMultilineString.java +++ /dev/null @@ -1,35 +0,0 @@ -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class JavaMultilineString { - @Test - public void newlines() { - Assertions.assertEquals("", """ - """); - Assertions.assertEquals("\n", """ - - """); - Assertions.assertEquals("\n\n", """ - - - """); - // this is illegal, there can be no text after the triple quotes - // Assertions.assertEquals("", """a - // """); - } - - @Test - public void indentation() { - Assertions.assertEquals("a", """ - a"""); - Assertions.assertEquals("a\n", """ - a - """); - Assertions.assertEquals("a\n", """ - a - """); - Assertions.assertEquals(" a\n", """ - a - """); - } -} \ No newline at end of file