Skip to content

Commit

Permalink
Improve callstack capture and IDE-compatible printing (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Dec 17, 2023
2 parents db1bec2 + 6931934 commit 7de1aa4
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,48 @@ package com.diffplug.selfie.junit5

import com.diffplug.selfie.RW
import com.diffplug.selfie.Snapshot
import java.nio.file.Files
import java.nio.file.Paths
import java.util.stream.Collectors
import kotlin.io.path.name

/** Represents the line at which user code called into Selfie. */
data class CallLocation(val subpath: String, val line: Int) : Comparable<CallLocation> {
override fun compareTo(other: CallLocation): Int {
val subpathCompare = subpath.compareTo(other.subpath)
return if (subpathCompare != 0) subpathCompare else line.compareTo(other.line)
data class CallLocation(val clazz: String, val method: String, val file: String?, val line: Int) :
Comparable<CallLocation> {
override fun compareTo(other: CallLocation): Int =
compareValuesBy(this, other, { it.clazz }, { it.method }, { it.file }, { it.line })

/**
* If the runtime didn't give us the filename, guess it from the class, and try to find the source
* file by walking the CWD. If we don't find it, report it as a `.class` file.
*/
private fun findFileIfAbsent(): String {
if (file != null) {
return file
}
val fileWithoutExtension = clazz.substringAfterLast('.').substringBefore('$')
val likelyExtensions = listOf("kt", "java", "scala", "groovy", "clj", "cljc")
val filenames = likelyExtensions.map { "$fileWithoutExtension.$it" }.toSet()
val firstPath = Files.walk(Paths.get("")).use { it.filter { it.name in filenames }.findFirst() }
return if (firstPath.isEmpty) "${clazz.substringAfterLast('.')}.class" else firstPath.get().name
}
override fun toString(): String = "$subpath:$line"

/** A `toString` which an IDE will render as a clickable link. */
override fun toString(): String = "$clazz.$method(${findFileIfAbsent()}:$line)"
}
/** Represents the callstack above a given CallLocation. */
class CallStack(val location: CallLocation, val restOfStack: List<CallLocation>) {
override fun toString(): String = "$location"
override fun toString(): String {
return location.toString()
}
}
/** Generates a CallLocation and the CallStack behind it. */
fun recordCall(): CallStack {
val calls =
StackWalker.getInstance().walk { frames ->
frames
.skip(1)
.map { CallLocation(it.className.replace('.', '/') + ".kt", it.lineNumber) }
.dropWhile { it.className.startsWith("com.diffplug.selfie") }
.map { CallLocation(it.className, it.methodName, it.fileName, it.lineNumber) }
.collect(Collectors.toList())
}
return CallStack(calls.removeAt(0), calls)
Expand All @@ -53,7 +74,7 @@ internal open class WriteTracker<K : Comparable<K>, V> {
if (existing != null) {
if (existing.snapshot != snapshot) {
throw org.opentest4j.AssertionFailedError(
"Snapshot was set to multiple values:\nfirst time:${existing.callStack}\n\nthis time:${call}",
"Snapshot was set to multiple values!\n first time: ${existing.callStack}\n this time: ${call}",
existing.snapshot,
snapshot)
} else if (RW.isWriteOnce) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class RecordCallTest {
@Test
fun testRecordCall() {
val stack = recordCall()
stack.location.toString() shouldBe "testpkg/RecordCallTest.kt:26"
// shows as clickable link in IDE
stack.location.toString() shouldBe "testpkg.RecordCallTest.testRecordCall(RecordCallTest.kt:26)"
stack.restOfStack.size shouldBeGreaterThan 0
}
}

0 comments on commit 7de1aa4

Please sign in to comment.