From a150da70a53aedd0eeb4ae25b4666f7b0caf0017 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 17 Dec 2023 07:43:31 -0800 Subject: [PATCH 1/2] `WriteTracker` now captures the stack at the entrypoint into Selfie. --- .../main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt index ba481592..c78c0edf 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt @@ -31,12 +31,15 @@ data class CallLocation(val subpath: String, val line: Int) : Comparable) { override fun toString(): String = "$location" } -/** Generates a CallLocation and the CallStack behind it. */ +/** + * Generates a CallLocation and the CallStack behind it, starting at the entrypoint into the Selfie + * codebase. + */ fun recordCall(): CallStack { val calls = StackWalker.getInstance().walk { frames -> frames - .skip(1) + .dropWhile { it.className.startsWith("com.diffplug.selfie") } .map { CallLocation(it.className.replace('.', '/') + ".kt", it.lineNumber) } .collect(Collectors.toList()) } From 69319348934ee3b424fe647a67f7bb5f1cdf8163 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 17 Dec 2023 07:53:22 -0800 Subject: [PATCH 2/2] Update `WriteTracker` so that it captures everything needed to generate a clickable link inside an IDE. --- .../diffplug/selfie/junit5/WriteTracker.kt | 42 +++++++++++++------ .../src/test/kotlin/testpkg/RecordCallTest.kt | 3 +- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt index c78c0edf..3fcd886b 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/WriteTracker.kt @@ -17,30 +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 { - 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 { + 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) { - override fun toString(): String = "$location" + override fun toString(): String { + return location.toString() + } } -/** - * Generates a CallLocation and the CallStack behind it, starting at the entrypoint into the Selfie - * codebase. - */ +/** Generates a CallLocation and the CallStack behind it. */ fun recordCall(): CallStack { val calls = StackWalker.getInstance().walk { frames -> frames .dropWhile { it.className.startsWith("com.diffplug.selfie") } - .map { CallLocation(it.className.replace('.', '/') + ".kt", it.lineNumber) } + .map { CallLocation(it.className, it.methodName, it.fileName, it.lineNumber) } .collect(Collectors.toList()) } return CallStack(calls.removeAt(0), calls) @@ -56,7 +74,7 @@ internal open class WriteTracker, 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) { diff --git a/selfie-runner-junit5/src/test/kotlin/testpkg/RecordCallTest.kt b/selfie-runner-junit5/src/test/kotlin/testpkg/RecordCallTest.kt index 26d756a5..e212c8d3 100644 --- a/selfie-runner-junit5/src/test/kotlin/testpkg/RecordCallTest.kt +++ b/selfie-runner-junit5/src/test/kotlin/testpkg/RecordCallTest.kt @@ -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 } }