diff --git a/jvm/CHANGELOG.md b/jvm/CHANGELOG.md index c161c6f4..52aa3248 100644 --- a/jvm/CHANGELOG.md +++ b/jvm/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Fixed a bug when saving facets containing keys with the `]` character ([#518](https://github.com/diffplug/selfie/pull/518)) ## [2.4.2] - 2025-01-01 ### Fixed diff --git a/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/SnapshotFile.kt b/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/SnapshotFile.kt index e4c470e1..a11fe650 100644 --- a/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/SnapshotFile.kt +++ b/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/SnapshotFile.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 DiffPlug + * Copyright (C) 2023-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -242,20 +242,20 @@ class SnapshotFile { class SnapshotReader(val valueReader: SnapshotValueReader) { fun peekKey(): String? { - val next = valueReader.peekKey() ?: return null + val next = valueReader.peekKeyRaw() ?: return null if (next == SnapshotFile.END_OF_FILE) { return null } require(next.indexOf('[') == -1) { "Missing root snapshot, square brackets not allowed: '$next'" } - return next + return SnapshotValueReader.nameEsc.unescape(next) } fun nextSnapshot(): Snapshot { val rootName = peekKey() var snapshot = Snapshot.of(valueReader.nextValue()) while (true) { - val nextKey = valueReader.peekKey() ?: return snapshot + val nextKey = valueReader.peekKeyRaw() ?: return snapshot val facetIdx = nextKey.indexOf('[') if (facetIdx == -1 || (facetIdx == 0 && nextKey == SnapshotFile.END_OF_FILE)) { return snapshot @@ -267,7 +267,9 @@ class SnapshotReader(val valueReader: SnapshotValueReader) { val facetEndIdx = nextKey.indexOf(']', facetIdx + 1) require(facetEndIdx != -1) { "Missing ] in $nextKey" } val facetName = nextKey.substring(facetIdx + 1, facetEndIdx) - snapshot = snapshot.plusFacet(facetName, valueReader.nextValue()) + snapshot = + snapshot.plusFacet( + SnapshotValueReader.nameEsc.unescape(facetName), valueReader.nextValue()) } } fun skipSnapshot() { @@ -285,15 +287,15 @@ class SnapshotValueReader(val lineReader: LineReader) { val unixNewlines = lineReader.unixNewlines() /** The key of the next value, does not increment anything about the reader's state. */ - fun peekKey(): String? { - return nextKey() + fun peekKeyRaw(): String? { + return nextKeyRaw() } /** Reads the next value. */ @OptIn(ExperimentalEncodingApi::class) fun nextValue(): SnapshotValue { // validate key - nextKey() + nextKeyRaw() val isBase64 = nextLine()!!.contains(FLAG_BASE64) resetLine() @@ -321,7 +323,7 @@ class SnapshotValueReader(val lineReader: LineReader) { /** Same as nextValue, but faster. */ fun skipValue() { // Ignore key - nextKey() + nextKeyRaw() resetLine() scanValue { @@ -340,7 +342,7 @@ class SnapshotValueReader(val lineReader: LineReader) { nextLine = nextLine() } } - private fun nextKey(): String? { + private fun nextKeyRaw(): String? { val line = nextLine() ?: return null val startIndex = line.indexOf(KEY_START) val endIndex = line.indexOf(KEY_END) @@ -357,7 +359,7 @@ class SnapshotValueReader(val lineReader: LineReader) { } else if (key.endsWith(" ")) { throw ParseException(lineReader, "Trailing spaces are disallowed: '$key'") } else { - nameEsc.unescape(key) + key } } private fun nextLine(): String? { diff --git a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotFileTest.kt b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotFileTest.kt index 0a0a82c7..4517824c 100644 --- a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotFileTest.kt +++ b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotFileTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 DiffPlug + * Copyright (C) 2023-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,4 +91,33 @@ class SnapshotFileTest { """ .trimIndent() } + + @Test + fun escapingBug() { + val file = + SnapshotFile.parse( + SnapshotValueReader.of( + """ +╔═ trialStarted/stripe ═╗ + +╔═ trialStarted/stripe[«1»{\n "params": {\n "line_items": "line_items=\({quantity=1, price=price_xxxx}\)"\n },\n "apiMode": "V1"\n}] ═╗ +{} +╔═ [end of file] ═╗ + +""" + .trimIndent())) + val keys = file.snapshots.keys.toList() + keys.size shouldBe 1 + keys[0] shouldBe "trialStarted/stripe" + val snapshot = file.snapshots.get(keys[0])!! + + snapshot.facets.keys.size shouldBe 1 + snapshot.facets.keys.first() shouldBe + """«1»{ + "params": { + "line_items": "line_items=[{quantity=1, price=price_xxxx}]" + }, + "apiMode": "V1" +}""" + } } diff --git a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotValueReaderTest.kt b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotValueReaderTest.kt index 4aa9fc3c..d93773ee 100644 --- a/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotValueReaderTest.kt +++ b/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/SnapshotValueReaderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 DiffPlug + * Copyright (C) 2023-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,42 +46,42 @@ class SnapshotValueReaderTest { ╔═ 05_notSureHowKotlinMultilineWorks ═╗ """ .trimIndent()) - reader.peekKey() shouldBe "00_empty" - reader.peekKey() shouldBe "00_empty" + reader.peekKeyRaw() shouldBe "00_empty" + reader.peekKeyRaw() shouldBe "00_empty" reader.nextValue().valueString() shouldBe "" - reader.peekKey() shouldBe "01_singleLineString" - reader.peekKey() shouldBe "01_singleLineString" + reader.peekKeyRaw() shouldBe "01_singleLineString" + reader.peekKeyRaw() shouldBe "01_singleLineString" reader.nextValue().valueString() shouldBe "this is one line" - reader.peekKey() shouldBe "01a_singleLineLeadingSpace" + reader.peekKeyRaw() shouldBe "01a_singleLineLeadingSpace" reader.nextValue().valueString() shouldBe " the leading space is significant" - reader.peekKey() shouldBe "01b_singleLineTrailingSpace" + reader.peekKeyRaw() shouldBe "01b_singleLineTrailingSpace" reader.nextValue().valueString() shouldBe "the trailing space is significant " - reader.peekKey() shouldBe "02_multiLineStringTrimmed" + reader.peekKeyRaw() shouldBe "02_multiLineStringTrimmed" reader.nextValue().valueString() shouldBe "Line 1\nLine 2" // note that leading and trailing newlines in the snapshots are significant // this is critical so that snapshots can accurately capture the exact number of newlines - reader.peekKey() shouldBe "03_multiLineStringTrailingNewline" + reader.peekKeyRaw() shouldBe "03_multiLineStringTrailingNewline" reader.nextValue().valueString() shouldBe "Line 1\nLine 2\n" - reader.peekKey() shouldBe "04_multiLineStringLeadingNewline" + reader.peekKeyRaw() shouldBe "04_multiLineStringLeadingNewline" reader.nextValue().valueString() shouldBe "\nLine 1\nLine 2" - reader.peekKey() shouldBe "05_notSureHowKotlinMultilineWorks" + reader.peekKeyRaw() shouldBe "05_notSureHowKotlinMultilineWorks" reader.nextValue().valueString() shouldBe "" } @Test fun invalidNames() { - shouldThrow { SnapshotValueReader.of("╔═name ═╗").peekKey() } + shouldThrow { SnapshotValueReader.of("╔═name ═╗").peekKeyRaw() } .let { it.message shouldBe "L1:Expected to start with '╔═ '" } - shouldThrow { SnapshotValueReader.of("╔═ name═╗").peekKey() } + shouldThrow { SnapshotValueReader.of("╔═ name═╗").peekKeyRaw() } .let { it.message shouldBe "L1:Expected to contain ' ═╗'" } - shouldThrow { SnapshotValueReader.of("╔═ name ═╗").peekKey() } + shouldThrow { SnapshotValueReader.of("╔═ name ═╗").peekKeyRaw() } .let { it.message shouldBe "L1:Leading spaces are disallowed: ' name'" } - shouldThrow { SnapshotValueReader.of("╔═ name ═╗").peekKey() } + shouldThrow { SnapshotValueReader.of("╔═ name ═╗").peekKeyRaw() } .let { it.message shouldBe "L1:Trailing spaces are disallowed: 'name '" } - SnapshotValueReader.of("╔═ name ═╗ comment okay").peekKey() shouldBe "name" - SnapshotValueReader.of("╔═ name ═╗okay here too").peekKey() shouldBe "name" + SnapshotValueReader.of("╔═ name ═╗ comment okay").peekKeyRaw() shouldBe "name" + SnapshotValueReader.of("╔═ name ═╗okay here too").peekKeyRaw() shouldBe "name" SnapshotValueReader.of("╔═ name ═╗ okay ╔═ ═╗ (it's the first ' ═╗' that counts)") - .peekKey() shouldBe "name" + .peekKeyRaw() shouldBe "name" } @Test @@ -96,21 +96,15 @@ class SnapshotValueReaderTest { ╔═ test with \┌\─ ascii art \─\┐ in name ═╗ """ .trimIndent()) - reader.peekKey() shouldBe "test with [square brackets] in name" + reader.peekKeyRaw() shouldBe "test with \\(square brackets\\) in name" reader.nextValue().valueString() shouldBe "" - reader.peekKey() shouldBe """test with \backslash\ in name""" + reader.peekKeyRaw() shouldBe """test with \\backslash\\ in name""" reader.nextValue().valueString() shouldBe "" - reader.peekKey() shouldBe - """ - test with - newline - in name - """ - .trimIndent() + reader.peekKeyRaw() shouldBe "test with\\nnewline\\nin name" reader.nextValue().valueString() shouldBe "" - reader.peekKey() shouldBe "test with \ttab\t in name" + reader.peekKeyRaw() shouldBe "test with \\ttab\\t in name" reader.nextValue().valueString() shouldBe "" - reader.peekKey() shouldBe "test with ╔═ ascii art ═╗ in name" + reader.peekKeyRaw() shouldBe "test with \\┌\\─ ascii art \\─\\┐ in name" reader.nextValue().valueString() shouldBe "" } @@ -127,11 +121,11 @@ class SnapshotValueReaderTest { 𐝃𐝁𐝃𐝃 linear a is dead """ .trimIndent()) - reader.peekKey() shouldBe "ascii art okay" + reader.peekKeyRaw() shouldBe "ascii art okay" reader.nextValue().valueString() shouldBe """ ╔══╗""" - reader.peekKey() shouldBe "escaped iff on first line" + reader.peekKeyRaw() shouldBe "escaped iff on first line" reader.nextValue().valueString() shouldBe """╔══╗""" - reader.peekKey() shouldBe "body escape characters" + reader.peekKeyRaw() shouldBe "body escape characters" reader.nextValue().valueString() shouldBe """𐝁𐝃 linear a is dead""" } @@ -154,12 +148,12 @@ class SnapshotValueReaderTest { } private fun assertKeyValueWithSkip(input: String, key: String, value: String) { val reader = SnapshotValueReader.of(input) - while (reader.peekKey() != key) { + while (reader.peekKeyRaw() != key) { reader.skipValue() } - reader.peekKey() shouldBe key + reader.peekKeyRaw() shouldBe key reader.nextValue().valueString() shouldBe value - while (reader.peekKey() != null) { + while (reader.peekKeyRaw() != null) { reader.skipValue() } } @@ -169,7 +163,7 @@ class SnapshotValueReaderTest { val reader = SnapshotValueReader.of("""╔═ Apple ═╗ base64 length 3 bytes c2Fk """) - reader.peekKey() shouldBe "Apple" + reader.peekKeyRaw() shouldBe "Apple" reader.nextValue().valueBinary() shouldBe "sad".encodeToByteArray() } } diff --git a/jvm/selfie-runner-kotest/src/commonTest/kotlin/com/diffplug/selfie/kotest/HarnessKotest.kt b/jvm/selfie-runner-kotest/src/commonTest/kotlin/com/diffplug/selfie/kotest/HarnessKotest.kt index a7db55f0..ae9c7c67 100644 --- a/jvm/selfie-runner-kotest/src/commonTest/kotlin/com/diffplug/selfie/kotest/HarnessKotest.kt +++ b/jvm/selfie-runner-kotest/src/commonTest/kotlin/com/diffplug/selfie/kotest/HarnessKotest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 DiffPlug + * Copyright (C) 2023-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -206,7 +206,7 @@ open class HarnessKotest() : FunSpec() { argList.add("-c") } argList.add( - "${if (IS_WINDOWS) "" else "./"}gradlew :undertest-kotest:$actualTask --configuration-cache ${args.joinToString(" ")}") + "${if (IS_WINDOWS) "" else "./"}gradlew :undertest-kotest:$actualTask --configuration-cache --console=plain ${args.joinToString(" ")}") val output = exec(TypedPath.ofFolder(subprojectFolder.parent.toString()), *argList.toTypedArray()) if (output.contains("BUILD SUCCESSFUL")) {