Skip to content

Fix facet parsing #518

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions jvm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -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()

Expand Down Expand Up @@ -321,7 +323,7 @@ class SnapshotValueReader(val lineReader: LineReader) {
/** Same as nextValue, but faster. */
fun skipValue() {
// Ignore key
nextKey()
nextKeyRaw()
resetLine()

scanValue {
Expand All @@ -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)
Expand All @@ -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? {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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"
}"""
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<ParseException> { SnapshotValueReader.of("╔═name ═╗").peekKey() }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═name ═╗").peekKeyRaw() }
.let { it.message shouldBe "L1:Expected to start with '╔═ '" }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name═╗").peekKey() }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name═╗").peekKeyRaw() }
.let { it.message shouldBe "L1:Expected to contain ' ═╗'" }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name ═╗").peekKey() }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name ═╗").peekKeyRaw() }
.let { it.message shouldBe "L1:Leading spaces are disallowed: ' name'" }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name ═╗").peekKey() }
shouldThrow<ParseException> { 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
Expand All @@ -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 ""
}

Expand All @@ -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"""
}

Expand All @@ -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()
}
}
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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")) {
Expand Down
Loading