Skip to content

Commit f90a8cc

Browse files
committed
WIP: implement useUnixNewLines
- redo jvm line reader to keep incremental reads and figure it out useUnixNewLines - still got some failures, but want to get feedback about the line reader
1 parent 05a3112 commit f90a8cc

File tree

5 files changed

+150
-6
lines changed

5 files changed

+150
-6
lines changed

selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/SnapshotFile.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,7 @@ class SnapshotFile {
153153
fun parse(valueReader: SnapshotValueReader): SnapshotFile {
154154
try {
155155
val result = SnapshotFile()
156-
result.unixNewlines =
157-
TODO("""add internal method to SnapshotValueReader to query if newline is \n or \r\n""")
156+
result.unixNewlines = valueReader.unixNewlines
158157
val reader = SnapshotReader(valueReader)
159158
// only if the first value starts with 📷
160159
if (reader.peekKey()?.startsWith(HEADER_PREFIX) == true) {
@@ -220,6 +219,7 @@ class SnapshotReader(val valueReader: SnapshotValueReader) {
220219
/** Provides the ability to parse a snapshot file incrementally. */
221220
class SnapshotValueReader(val lineReader: LineReader) {
222221
var line: String? = null
222+
val unixNewlines = lineReader.unixNewlines()
223223

224224
/** The key of the next value, does not increment anything about the reader's state. */
225225
fun peekKey(): String? {
@@ -324,6 +324,7 @@ class SnapshotValueReader(val lineReader: LineReader) {
324324
expect class LineReader {
325325
fun getLineNumber(): Int
326326
fun readLine(): String?
327+
fun unixNewlines(): Boolean
327328

328329
companion object {
329330
fun forString(content: String): LineReader

selfie-lib/src/jsMain/kotlin/com/diffplug/selfie/SnapshotFile.js.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ actual class LineReader {
2222
}
2323
actual fun getLineNumber(): Int = TODO()
2424
actual fun readLine(): String? = TODO()
25+
actual fun unixNewlines(): Boolean = TODO()
2526
}

selfie-lib/src/jvmMain/kotlin/com/diffplug/selfie/SnapshotFile.jvm.kt

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,74 @@
1515
*/
1616
package com.diffplug.selfie
1717

18+
import java.io.BufferedReader
1819
import java.io.InputStreamReader
1920
import java.io.LineNumberReader
2021
import java.io.Reader
2122
import java.io.StringReader
23+
import java.nio.CharBuffer
2224
import java.nio.charset.StandardCharsets
2325

24-
actual class LineReader(reader: Reader) : LineNumberReader(reader) {
26+
actual class LineReader(reader: Reader) {
27+
private val reader = LineTerminatorAware(LineTerminatorReader(reader))
28+
2529
actual companion object {
2630
actual fun forString(content: String) = LineReader(StringReader(content))
2731
actual fun forBinary(content: ByteArray) =
2832
LineReader(InputStreamReader(content.inputStream(), StandardCharsets.UTF_8))
2933
}
34+
actual fun getLineNumber(): Int = reader.lineNumber
35+
actual fun readLine(): String? = reader.readLine()
36+
actual fun unixNewlines(): Boolean = reader.lineTerminator.unixNewlines()
37+
}
38+
39+
/**
40+
* Keep track of carriage return char to figure it out if we need unix new line or not. The first
41+
* line is kept in memory until we require the next line.
42+
*/
43+
private open class LineTerminatorAware(val lineTerminator: LineTerminatorReader) :
44+
LineNumberReader(lineTerminator) {
45+
/** First line is initialized as soon as possible. */
46+
private var firstLine: String? = super.readLine()
47+
override fun readLine(): String? {
48+
if (this.firstLine != null) {
49+
val result = this.firstLine
50+
this.firstLine = null
51+
return result
52+
}
53+
return super.readLine()
54+
}
55+
}
56+
57+
/**
58+
* Override all read operations to find the carriage return. We want to keep lazy/incremental reads.
59+
*/
60+
private class LineTerminatorReader(reader: Reader) : BufferedReader(reader) {
61+
private val CR: Int = '\r'.code
62+
private var unixNewlines = System.lineSeparator().equals("\n")
63+
override fun read(cbuf: CharArray): Int {
64+
val result = super.read(cbuf)
65+
unixNewlines = cbuf.indexOf(CR.toChar()) == -1
66+
return result
67+
}
68+
override fun read(target: CharBuffer): Int {
69+
val result = super.read(target)
70+
unixNewlines = target.indexOf(CR.toChar()) == -1
71+
return result
72+
}
73+
override fun read(cbuf: CharArray, off: Int, len: Int): Int {
74+
val result = super.read(cbuf, off, len)
75+
unixNewlines = cbuf.indexOf(CR.toChar()) == -1
76+
return result
77+
}
78+
override fun read(): Int {
79+
val ch = super.read()
80+
if (ch == CR) {
81+
unixNewlines = false
82+
}
83+
return ch
84+
}
85+
fun unixNewlines(): Boolean {
86+
return unixNewlines
87+
}
3088
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (C) 2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.selfie
17+
18+
import io.kotest.matchers.shouldBe
19+
import kotlin.test.Test
20+
21+
class LineReaderTest {
22+
23+
@Test
24+
fun shouldFindUnixSeparatorFromBinary() {
25+
val reader = LineReader.forBinary("This is a new line\n".encodeToByteArray())
26+
reader.unixNewlines() shouldBe true
27+
reader.readLine() shouldBe "This is a new line"
28+
}
29+
30+
@Test
31+
fun shouldFindWindowsSeparatorFromBinary() {
32+
val reader = LineReader.forBinary("This is a new line\r\n".encodeToByteArray())
33+
reader.unixNewlines() shouldBe false
34+
reader.readLine() shouldBe "This is a new line"
35+
}
36+
37+
@Test
38+
fun shouldFindUnixSeparatorFromString() {
39+
val reader = LineReader.forString("This is a new line\n")
40+
reader.unixNewlines() shouldBe true
41+
reader.readLine() shouldBe "This is a new line"
42+
}
43+
44+
@Test
45+
fun shouldFindWindowsSeparatorFromString() {
46+
val reader = LineReader.forString("This is a new line\r\n")
47+
reader.unixNewlines() shouldBe false
48+
reader.readLine() shouldBe "This is a new line"
49+
}
50+
51+
@Test
52+
fun shouldGetOSLineSeparatorWhenThereIsNone() {
53+
val reader = LineReader.forBinary("This is a new line".encodeToByteArray())
54+
reader.unixNewlines() shouldBe System.lineSeparator().equals("\n")
55+
reader.readLine() shouldBe "This is a new line"
56+
}
57+
58+
@Test
59+
fun shouldReadNextLineWithoutProblem() {
60+
val reader = LineReader.forBinary("First\r\nSecond\r\n".encodeToByteArray())
61+
reader.unixNewlines() shouldBe false
62+
reader.readLine() shouldBe "First"
63+
reader.unixNewlines() shouldBe false
64+
reader.readLine() shouldBe "Second"
65+
reader.unixNewlines() shouldBe false
66+
}
67+
68+
@Test
69+
fun shouldUseFirstLineSeparatorAndIgnoreNext() {
70+
val reader = LineReader.forBinary("First\r\nAnother separator\n".encodeToByteArray())
71+
reader.unixNewlines() shouldBe false
72+
reader.readLine() shouldBe "First"
73+
reader.unixNewlines() shouldBe false
74+
reader.readLine() shouldBe "Another separator"
75+
reader.unixNewlines() shouldBe false
76+
}
77+
}

selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieConfig.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,16 @@ internal class SnapshotFileLayout(
7575
val snapshotRootFolder = rootFolder(properties.getProperty("output-dir"))
7676
// it's pretty easy to preserve the line endings of existing snapshot files, but it's
7777
// a bit harder to create a fresh snapshot file with the correct line endings.
78-
val unixNewlines: Boolean =
79-
TODO("find the first file in the snapshot folder and check if it has unix newlines")
80-
return SnapshotFileLayout(snapshotRootFolder, snapshotFolderName, unixNewlines)
78+
val candidate =
79+
snapshotRootFolder
80+
.resolve(snapshotFolderName!!)
81+
.toFile()
82+
.walkTopDown()
83+
.maxDepth(1)
84+
.filter { it.isFile }
85+
.firstOrNull()
86+
val cr = candidate?.readText()?.contains('\r') ?: System.lineSeparator().equals("\r\n")
87+
return SnapshotFileLayout(snapshotRootFolder, snapshotFolderName, !cr)
8188
}
8289
private fun snapshotFolderName(snapshotDir: String?): String? {
8390
if (snapshotDir == null) {

0 commit comments

Comments
 (0)