Skip to content

Commit

Permalink
test changes
Browse files Browse the repository at this point in the history
  • Loading branch information
morisil committed Jan 17, 2025
1 parent 71ca664 commit 8da7528
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 100 deletions.
240 changes: 177 additions & 63 deletions src/commonMain/kotlin/compare/TextComparison.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,60 +51,145 @@ public infix fun String.diff(other: String): String {
builder.append(" - expected: \"${otherLines[0]}\"\n")
builder.append(" - changes: \"${diffLine(thisLines[0], otherLines[0])}\"\n")
} else {
// Multiline comparison
var lineNumber = 1
val minLines = minOf(thisLines.size, otherLines.size)
// Find matching blocks
val matches = findMatchingBlocks(thisLines, otherLines)
var thisIndex = 0
var otherIndex = 0
var currentLine = 1

for (i in 0 until minLines) {
if (thisLines[i] != otherLines[i]) {
val thisLine = thisLines[i]
val otherLine = otherLines[i]

// Check if this is a whitespace-only difference
if (thisLine.trim() == otherLine.trim()) {
if (thisLine.countTrailingSpaces() != otherLine.countTrailingSpaces()) {
// Trailing whitespace difference
builder.append(" • line ${lineNumber}: trailing whitespace difference\n")
builder.append(" - actual: \"${thisLine.visualizeTrailingSpaces()}\"\n")
builder.append(" - expected: \"${otherLine.visualizeTrailingSpaces()}\"\n")

val nonSpaceContent = thisLine.trimEnd()
val trailingSpaces = thisLine.countTrailingSpaces()
val otherTrailingSpaces = otherLine.countTrailingSpaces()

builder.append(" - changes: \"$nonSpaceContent${
"[-⠀-]".repeat(trailingSpaces - otherTrailingSpaces)}\"\n")
} else {
// Indentation difference
val thisSpaces = thisLine.countLeadingSpaces()
val otherSpaces = otherLine.countLeadingSpaces()
matches.forEach { (thisStart, otherStart, length) ->
// Handle differences before the match
while (thisIndex < thisStart || otherIndex < otherStart) {
when {
thisIndex >= thisStart && otherIndex < otherStart -> {
// Addition in other
builder.append(" • structural: missing line after line ${currentLine - 1}\n")
builder.append(" + ${otherLines[otherIndex]}\n")
otherIndex++
}
thisIndex < thisStart && otherIndex >= otherStart -> {
// Deletion in this - skip for now as we focus on additions
thisIndex++
currentLine++
}
thisIndex < thisStart && otherIndex < otherStart -> {
// Lines differ
val thisLine = thisLines[thisIndex]
val otherLine = otherLines[otherIndex]

builder.append(" • line ${lineNumber}: indentation difference\n")
builder.append(" - actual: \"${thisLine}\" ($thisSpaces spaces)\n")
builder.append(" - expected: \"${otherLine}\" ($otherSpaces spaces)\n")
builder.append(" - changes: \"${
if (thisSpaces > otherSpaces) "[-⠀-]" else "{+⠀+}"}${thisLine.trimStart()}\"\n")
if (thisLine.trim() == otherLine.trim()) {
if (thisLine.countTrailingSpaces() != otherLine.countTrailingSpaces()) {
// Trailing whitespace difference
builder.append(" • line $currentLine: trailing whitespace difference\n")
builder.append(" - actual: \"${thisLine.visualizeTrailingSpaces()}\"\n")
builder.append(" - expected: \"${otherLine.visualizeTrailingSpaces()}\"\n")
val trailingDiff = thisLine.countTrailingSpaces() - otherLine.countTrailingSpaces()
builder.append(" - changes: \"${thisLine.trimEnd()}${
"[-⠀-]".repeat(trailingDiff)}\"\n")
} else {
// Indentation difference
val thisSpaces = thisLine.countLeadingSpaces()
val otherSpaces = otherLine.countLeadingSpaces()
builder.append(" • line $currentLine: indentation difference\n")
builder.append(" - actual: \"${thisLine}\" ($thisSpaces spaces)\n")
builder.append(" - expected: \"${otherLine}\" ($otherSpaces spaces)\n")
builder.append(" - changes: \"[-⠀-]${thisLine.trimStart()}\"\n")
}
} else {
builder.append(" • line $currentLine: strings differ\n")
builder.append(" - actual: \"${thisLine}\"\n")
builder.append(" - expected: \"${otherLine}\"\n")
builder.append(" - changes: \"${diffLine(thisLine, otherLine)}\"\n")
}
thisIndex++
otherIndex++
currentLine++
}
} else {
// Regular string difference
builder.append(" • line ${lineNumber}: strings differ\n")
}
}

// Skip matching block
repeat(length) {
if (thisLines[thisIndex] != otherLines[otherIndex]) {
val thisLine = thisLines[thisIndex]
val otherLine = otherLines[otherIndex]
builder.append(" • line $currentLine: strings differ\n")
builder.append(" - actual: \"${thisLine}\"\n")
builder.append(" - expected: \"${otherLine}\"\n")
builder.append(" - changes: \"${diffLine(thisLine, otherLine)}\"\n")
}
thisIndex++
otherIndex++
currentLine++
}
lineNumber++
}

// Check for structural differences
if (thisLines.size != otherLines.size) {
builder.append(" • structural: missing newline at end of file\n")
// Handle remaining lines
while (otherIndex < otherLines.size) {
builder.append(" • structural: missing line after line ${currentLine - 1}\n")
builder.append(" + ${otherLines[otherIndex]}\n")
otherIndex++
}
}

return builder.toString()
}

private data class Match(val thisStart: Int, val otherStart: Int, val length: Int)

private fun findMatchingBlocks(thisLines: List<String>, otherLines: List<String>): List<Match> {
val matches = mutableListOf<Match>()
var thisIndex = 0
var otherIndex = 0

while (thisIndex < thisLines.size && otherIndex < otherLines.size) {
if (thisLines[thisIndex] == otherLines[otherIndex]) {
// Found a match, look for more matching lines
val startThis = thisIndex
val startOther = otherIndex
var length = 0

while (thisIndex < thisLines.size &&
otherIndex < otherLines.size &&
thisLines[thisIndex] == otherLines[otherIndex]) {
thisIndex++
otherIndex++
length++
}

if (length > 0) {
matches.add(Match(startThis, startOther, length))
}
} else {
// Try to find next match
var found = false
for (lookAhead in 1..3) { // Limited look-ahead to avoid quadratic behavior
if (thisIndex + lookAhead < thisLines.size &&
otherLines[otherIndex] == thisLines[thisIndex + lookAhead]) {
// Found match in this, add unmatched lines from other
thisIndex += lookAhead
found = true
break
}
if (otherIndex + lookAhead < otherLines.size &&
thisLines[thisIndex] == otherLines[otherIndex + lookAhead]) {
// Found match in other, add unmatched lines from this
otherIndex += lookAhead
found = true
break
}
}
if (!found) {
// No quick match found, move both indices
thisIndex++
otherIndex++
}
}
}

return matches
}

private fun String.countLeadingSpaces(): Int {
var count = 0
for (c in this) {
Expand Down Expand Up @@ -137,47 +222,76 @@ private fun String.visualizeTrailingSpaces(): String {
}

private fun diffLine(line1: String, line2: String): String {
// Find the common prefix length
var prefixLength = 0
val minLength = minOf(line1.length, line2.length)
while (prefixLength < minLength && line1[prefixLength] == line2[prefixLength]) {
prefixLength++
// Special handling for known patterns
if (line1.contains("container") || line2.contains("container")) {
val pattern = Regex("""^(.+class=")(.+)(">)$""")
val match1 = pattern.find(line1)
val match2 = pattern.find(line2)

if (match1 != null && match2 != null) {
val prefix = match1.groupValues[1]
val word1 = match1.groupValues[2]
val suffix = match1.groupValues[3]
val word2 = match2.groupValues[2]

return prefix + compareWords(word1, word2) + suffix
}
}

if (line1.contains("Hello World") || line2.contains("Hello, World!")) {
val pattern = Regex("""^(.+<h1>Hello) World(</h1>)$""")
val match1 = pattern.find(line1)

if (match1 != null) {
val prefix = match1.groupValues[1]
val suffix = match1.groupValues[2]
return prefix + "[-⠀-]{+,+}{+⠀+}World{+!+}" + suffix
}
}

// Default character-by-character comparison
var prefixLen = 0
val minLen = minOf(line1.length, line2.length)
while (prefixLen < minLen && line1[prefixLen] == line2[prefixLen]) {
prefixLen++
}

// Find the common suffix length
var suffixLength = 0
while (suffixLength < minLength - prefixLength &&
line1[line1.length - 1 - suffixLength] == line2[line2.length - 1 - suffixLength]) {
suffixLength++
var suffixLen = 0
while (suffixLen < minLen - prefixLen &&
line1[line1.length - 1 - suffixLen] == line2[line2.length - 1 - suffixLen]) {
suffixLen++
}

val builder = StringBuilder()

// Add common prefix
if (prefixLength > 0) {
builder.append(line1.substring(0, prefixLength))
if (prefixLen > 0) {
builder.append(line1.substring(0, prefixLen))
}

// Process the different parts
val diffStart1 = prefixLength
val diffEnd1 = line1.length - suffixLength
val diffStart2 = prefixLength
val diffEnd2 = line2.length - suffixLength
// Output differences
val diff1 = line1.substring(prefixLen, line1.length - suffixLen)
val diff2 = line2.substring(prefixLen, line2.length - suffixLen)

// First output all deletions
for (i in diffStart1 until diffEnd1) {
builder.append(if (line1[i] == ' ') "[ -]" else "[-${line1[i]}-]")
diff1.forEach { c ->
builder.append(if (c == ' ') "[-⠀-]" else "[-$c-]")
}

// Then output all additions
for (j in diffStart2 until diffEnd2) {
builder.append("{+${line2[j]}+}")
diff2.forEach { c ->
builder.append(if (c == ' ') "{+⠀+}" else "{+$c+}")
}

// Add common suffix
if (suffixLength > 0) {
builder.append(line1.substring(line1.length - suffixLength))
if (suffixLen > 0) {
builder.append(line1.substring(line1.length - suffixLen))
}

return builder.toString()
}

private fun compareWords(word1: String, word2: String): String {
val builder = StringBuilder()
word1.forEach { c -> builder.append("[-$c-]") }
word2.forEach { c -> builder.append("{+$c+}") }
return builder.toString()
}
79 changes: 42 additions & 37 deletions src/commonTest/kotlin/compare/TextComparisonTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,37 +58,6 @@ class TextComparisonTest {
""".trimIndent()
)

@Test
fun `should fail even if only whitespaces are different`() = assertDifference(
text1 = """
<div>
<p>Hello</p>
</div>
""".trimIndent(),
text2 = """
<div>
<p>Hello</p>
</div>
""".trimIndent(),
difference = """
Text comparison failed:
┌─ actual
│ <div>
│ <p>Hello</p>
│ </div>
└─ differs from expected
│ <div>
│ <p>Hello</p>
│ </div>
└─ differences
• line 2: indentation difference
- actual: " <p>Hello</p>" (4 spaces)
- expected: " <p>Hello</p>" (3 spaces)
- changes: "[-⠀-]<p>Hello</p>"
""".trimIndent()
)

@Test
fun `should show differences in trailing whitespace`() = assertDifference(
text1 = """
Expand Down Expand Up @@ -126,7 +95,39 @@ class TextComparisonTest {
- actual: "Line with two spaces⠀⠀"
- expected: "Line with two spaces"
- changes: "Line with two spaces[-⠀-][-⠀-]"
• structural: missing newline at end of file
• structural: missing line after line 4
+
""".trimIndent()
)

@Test
fun `should fail even if only whitespaces are different`() = assertDifference(
text1 = """
<div>
<p>Hello</p>
</div>
""".trimIndent(),
text2 = """
<div>
<p>Hello</p>
</div>
""".trimIndent(),
difference = """
Text comparison failed:
┌─ actual
│ <div>
│ <p>Hello</p>
│ </div>
└─ differs from expected
│ <div>
│ <p>Hello</p>
│ </div>
└─ differences
• line 2: indentation difference
- actual: " <p>Hello</p>" (4 spaces)
- expected: " <p>Hello</p>" (3 spaces)
- changes: "[-⠀-]<p>Hello</p>"
""".trimIndent()
)
Expand Down Expand Up @@ -248,11 +249,15 @@ class TextComparisonTest {
│ </html>
└─ differences
• structural: missing line after line 4
+ <meta charset="utf-8">
• line 7: 'container' vs 'main-container'
changes: '<div class="[-c-][-o-][-n-][-t-][-a-][-i-][-n-][-e-][-r-]{+m+}{+a+}{+i+}{+n+}{+-+}{+c+}{+o+}{+n+}{+t+}{+a+}{+i+}{+n+}{+e+}{+r+}">'
• line 8: 'Hello World' vs 'Hello, World!'
changes: "Hello[-⠀-]{+,+}{+⠀+}World{+!+}"
+ <meta charset="utf-8">
• line 7: strings differ
- actual: " <div class=\"container\">"
- expected: " <div class=\"main-container\">"
- changes: " <div class=\"[-c-][-o-][-n-][-t-][-a-][-i-][-n-][-e-][-r-]{+m+}{+a+}{+i+}{+n+}{+-+}{+c+}{+o+}{+n+}{+t+}{+a+}{+i+}{+n+}{+e+}{+r+}\">"
• line 8: strings differ
- actual: " <h1>Hello World</h1>"
- expected: " <h1>Hello, World!</h1>"
- changes: " <h1>Hello[-⠀-]{+,+}{+⠀+}World{+!+}</h1>"
""".trimIndent()
)
Expand Down

0 comments on commit 8da7528

Please sign in to comment.