Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

SnapshotValueReader #32

Closed
Trickybrain opened this issue Mar 10, 2024 · 1 comment
Closed

SnapshotValueReader #32

Trickybrain opened this issue Mar 10, 2024 · 1 comment

Comments

@Trickybrain
Copy link
Collaborator

Trickybrain commented Mar 10, 2024

To take step in #7

SnapshotValueReader Test

class SnapshotValueReaderTest {
@Test
fun noEscapingNeeded() {
val reader =
SnapshotValueReader.of(
"""
╔═ 00_empty ═╗
╔═ 01_singleLineString ═╗
this is one line
╔═ 01a_singleLineLeadingSpace ═╗
the leading space is significant
╔═ 01b_singleLineTrailingSpace ═╗
the trailing space is significant
╔═ 02_multiLineStringTrimmed ═╗
Line 1
Line 2
╔═ 03_multiLineStringTrailingNewline ═╗
Line 1
Line 2
╔═ 04_multiLineStringLeadingNewline ═╗
Line 1
Line 2
╔═ 05_notSureHowKotlinMultilineWorks ═╗
"""
.trimIndent())
reader.peekKey() shouldBe "00_empty"
reader.peekKey() shouldBe "00_empty"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "01_singleLineString"
reader.peekKey() shouldBe "01_singleLineString"
reader.nextValue().valueString() shouldBe "this is one line"
reader.peekKey() shouldBe "01a_singleLineLeadingSpace"
reader.nextValue().valueString() shouldBe " the leading space is significant"
reader.peekKey() shouldBe "01b_singleLineTrailingSpace"
reader.nextValue().valueString() shouldBe "the trailing space is significant "
reader.peekKey() 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.nextValue().valueString() shouldBe "Line 1\nLine 2\n"
reader.peekKey() shouldBe "04_multiLineStringLeadingNewline"
reader.nextValue().valueString() shouldBe "\nLine 1\nLine 2"
reader.peekKey() shouldBe "05_notSureHowKotlinMultilineWorks"
reader.nextValue().valueString() shouldBe ""
}
@Test
fun invalidNames() {
shouldThrow<ParseException> { SnapshotValueReader.of("╔═name ═╗").peekKey() }
.let { it.message shouldBe "L1:Expected to start with '╔═ '" }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name═╗").peekKey() }
.let { it.message shouldBe "L1:Expected to contain ' ═╗'" }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name ═╗").peekKey() }
.let { it.message shouldBe "L1:Leading spaces are disallowed: ' name'" }
shouldThrow<ParseException> { SnapshotValueReader.of("╔═ name ═╗").peekKey() }
.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 ═╗ okay ╔═ ═╗ (it's the first ' ═╗' that counts)")
.peekKey() shouldBe "name"
}
@Test
fun escapeCharactersInName() {
val reader =
SnapshotValueReader.of(
"""
╔═ test with \(square brackets\) in name ═╗
╔═ test with \\backslash\\ in name ═╗
╔═ test with\nnewline\nin name ═╗
╔═ test with \ttab\t in name ═╗
╔═ test with \┌\─ ascii art \─\┐ in name ═╗
"""
.trimIndent())
reader.peekKey() shouldBe "test with [square brackets] in name"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe """test with \backslash\ in name"""
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe
"""
test with
newline
in name
"""
.trimIndent()
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "test with \ttab\t in name"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "test with ╔═ ascii art ═╗ in name"
reader.nextValue().valueString() shouldBe ""
}
@Test
fun escapeCharactersInBody() {
val reader =
SnapshotValueReader.of(
"""
╔═ ascii art okay ═╗
╔══╗
╔═ escaped iff on first line ═╗
𐝁══╗
╔═ body escape characters ═╗
𐝃𐝁𐝃𐝃 linear a is dead
"""
.trimIndent())
reader.peekKey() shouldBe "ascii art okay"
reader.nextValue().valueString() shouldBe """ ╔══╗"""
reader.peekKey() shouldBe "escaped iff on first line"
reader.nextValue().valueString() shouldBe """╔══╗"""
reader.peekKey() shouldBe "body escape characters"
reader.nextValue().valueString() shouldBe """𐝁𐝃 linear a is dead"""
}
@Test
fun skipValues() {
val testContent =
"""
╔═ 00_empty ═╗
╔═ 01_singleLineString ═╗
this is one line
╔═ 02_multiLineStringTrimmed ═╗
Line 1
Line 2
╔═ 05_notSureHowKotlinMultilineWorks ═╗
"""
.trimIndent()
assertKeyValueWithSkip(testContent, "00_empty", "")
assertKeyValueWithSkip(testContent, "01_singleLineString", "this is one line")
assertKeyValueWithSkip(testContent, "02_multiLineStringTrimmed", "Line 1\nLine 2")
}
private fun assertKeyValueWithSkip(input: String, key: String, value: String) {
val reader = SnapshotValueReader.of(input)
while (reader.peekKey() != key) {
reader.skipValue()
}
reader.peekKey() shouldBe key
reader.nextValue().valueString() shouldBe value
while (reader.peekKey() != null) {
reader.skipValue()
}
}
@Test
fun binary() {
val reader = SnapshotValueReader.of("""╔═ Apple ═╗ base64 length 3 bytes
c2Fk
""")
reader.peekKey() shouldBe "Apple"
reader.nextValue().valueBinary() shouldBe "sad".toByteArray()
}
}

SnapshotValueReader

class SnapshotValueReader(val lineReader: LineReader) {
var line: String? = null
val unixNewlines = lineReader.unixNewlines()
/** The key of the next value, does not increment anything about the reader's state. */
fun peekKey(): String? {
return nextKey()
}
/** Reads the next value. */
@OptIn(ExperimentalEncodingApi::class)
fun nextValue(): SnapshotValue {
// validate key
nextKey()
val isBase64 = nextLine()!!.contains(FLAG_BASE64)
resetLine()
// read value
val buffer = StringBuilder()
scanValue { line ->
if (line.length >= 2 && line[0] == '\uD801' && line[1] == '\uDF41') { // "\uD801\uDF41" = "𐝁"
buffer.append(KEY_FIRST_CHAR)
buffer.append(line, 2, line.length)
} else {
buffer.append(line)
}
buffer.append('\n')
}
val rawString =
if (buffer.isEmpty()) ""
else {
buffer.setLength(buffer.length - 1)
buffer.toString()
}
return if (isBase64) SnapshotValue.of(Base64.Mime.decode(rawString))
else SnapshotValue.of(bodyEsc.unescape(rawString))
}
/** Same as nextValue, but faster. */
fun skipValue() {
// Ignore key
nextKey()
resetLine()
scanValue {
// ignore it
}
}
private inline fun scanValue(consumer: (String) -> Unit) {
// read next
var nextLine = nextLine()
while (nextLine != null && nextLine.indexOf(KEY_FIRST_CHAR) != 0) {
resetLine()
consumer(nextLine)
// read next
nextLine = nextLine()
}
}
private fun nextKey(): String? {
val line = nextLine() ?: return null
val startIndex = line.indexOf(KEY_START)
val endIndex = line.indexOf(KEY_END)
if (startIndex == -1) {
throw ParseException(lineReader, "Expected to start with '$KEY_START'")
}
if (endIndex == -1) {
throw ParseException(lineReader, "Expected to contain '$KEY_END'")
}
// valid key
val key = line.substring(startIndex + KEY_START.length, endIndex)
return if (key.startsWith(" ")) {
throw ParseException(lineReader, "Leading spaces are disallowed: '$key'")
} else if (key.endsWith(" ")) {
throw ParseException(lineReader, "Trailing spaces are disallowed: '$key'")
} else {
nameEsc.unescape(key)
}
}
private fun nextLine(): String? {
if (line == null) {
line = lineReader.readLine()
}
return line
}
private fun resetLine() {
line = null
}
companion object {
fun of(content: String) = SnapshotValueReader(LineReader.forString(content))
fun of(content: ByteArray) = SnapshotValueReader(LineReader.forBinary(content))
private const val KEY_FIRST_CHAR = ''
private const val KEY_START = "╔═ "
private const val KEY_END = " ═╗"
private const val FLAG_BASE64 = " ═╗ base64"
/**
* https://github.com/diffplug/selfie/blob/main/jvm/selfie-lib/src/commonTest/resources/com/diffplug/selfie/scenarios_and_lenses.ss
*/
internal val nameEsc = PerCharacterEscaper.specifiedEscape("\\\\[(])\nn\tt╔┌╗┐═─")
/** https://github.com/diffplug/selfie/issues/2 */
internal val bodyEsc = PerCharacterEscaper.selfEscape("\uD801\uDF43\uD801\uDF41")
}
}

@nedtwigg
Copy link
Member

Closed by #44

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants