From 9fbae21aae934c373153aa542973be96ba4bc433 Mon Sep 17 00:00:00 2001 From: sksamuel Date: Tue, 20 Jan 2026 13:23:49 -0600 Subject: [PATCH 1/5] Update to Kotest 6.1.0 and refactor to align with updated API changes. --- jvm/example-kotest/build.gradle.kts | 1 + .../test/kotlin/com/example/kotest/KotestConfig.kt | 2 +- jvm/gradle.properties | 4 ++-- .../com/diffplug/selfie/junit5/SelfieExtension.kt | 12 ++++++------ .../kotlin/com/diffplug/selfie/kotest/FSOkio.kt | 14 ++++++++------ .../com/diffplug/selfie/kotest/SelfieExtension.kt | 10 +++++----- jvm/undertest-junit5-kotest/build.gradle | 1 + .../harness/gradle.properties | 2 +- .../undertest/junit5/JunitKotestProjectConfig.kt | 2 +- .../junit5/UT_KotestConcurrencyStressTest.kt | 5 ++++- 10 files changed, 30 insertions(+), 23 deletions(-) diff --git a/jvm/example-kotest/build.gradle.kts b/jvm/example-kotest/build.gradle.kts index 248917e0a..178f67c28 100644 --- a/jvm/example-kotest/build.gradle.kts +++ b/jvm/example-kotest/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { } tasks.test { useJUnitPlatform() + systemProperty("kotest.framework.config.fqn", "com.example.kotest.KotestConfig") environment(properties.filter { it.key == "selfie" || it.key == "OPENAI_API_KEY" }) inputs.files(fileTree("src/test") { include("**/*.ss") diff --git a/jvm/example-kotest/src/test/kotlin/com/example/kotest/KotestConfig.kt b/jvm/example-kotest/src/test/kotlin/com/example/kotest/KotestConfig.kt index c16584b60..901a2248b 100644 --- a/jvm/example-kotest/src/test/kotlin/com/example/kotest/KotestConfig.kt +++ b/jvm/example-kotest/src/test/kotlin/com/example/kotest/KotestConfig.kt @@ -3,5 +3,5 @@ package com.example.kotest import com.diffplug.selfie.kotest.SelfieExtension class KotestConfig : io.kotest.core.config.AbstractProjectConfig() { - override fun extensions() = listOf(SelfieExtension(this)) + override val extensions = listOf(SelfieExtension(this)) } \ No newline at end of file diff --git a/jvm/gradle.properties b/jvm/gradle.properties index 83dfda6f3..557243c9a 100644 --- a/jvm/gradle.properties +++ b/jvm/gradle.properties @@ -9,7 +9,7 @@ ver_JUNIT_PIONEER=2.3.0 ver_OKIO=3.16.0 ver_KOTLIN_TEST=2.0.0 ver_KOTLIN_SERIALIZATION=1.9.0 -# Kotest 5.6.0 is the oldest that we support -ver_KOTEST=5.6.0 +# Kotest 6.0.0 is the oldest that we support +ver_KOTEST=6.1.0 ver_JVM_TARGET=11 \ No newline at end of file diff --git a/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt b/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt index aed9aee40..062e5d7a9 100644 --- a/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt +++ b/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt @@ -24,7 +24,7 @@ import io.kotest.core.listeners.BeforeSpecListener import io.kotest.core.listeners.FinalizeSpecListener import io.kotest.core.spec.Spec import io.kotest.core.test.TestCase -import io.kotest.core.test.TestResult +import io.kotest.engine.test.TestResult import kotlin.reflect.KClass import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.withContext @@ -43,11 +43,11 @@ class SelfieExtension(projectConfig: AbstractProjectConfig) : execute: suspend (TestCase) -> TestResult ): TestResult { val file = SnapshotSystemJUnit5.forClass(testCase.spec::class.java.name) - val coroutineLocal = CoroutineDiskStorage(DiskStorageJUnit5(file, testCase.name.testName)) + val coroutineLocal = CoroutineDiskStorage(DiskStorageJUnit5(file, testCase.name.name)) return withContext(currentCoroutineContext() + coroutineLocal) { - file.startTest(testCase.name.testName, false) + file.startTest(testCase.name.name, false) val result = execute(testCase) - file.finishedTestWithSuccess(testCase.name.testName, false, result.isSuccess) + file.finishedTestWithSuccess(testCase.name.name, false, result.isSuccess) result } } @@ -58,8 +58,8 @@ class SelfieExtension(projectConfig: AbstractProjectConfig) : val file = SnapshotSystemJUnit5.forClass(kclass.java.name) results.entries.forEach { if (it.value.isIgnored) { - file.startTest(it.key.name.testName, false) - file.finishedTestWithSuccess(it.key.name.testName, false, false) + file.startTest(it.key.name.name, false) + file.finishedTestWithSuccess(it.key.name.name, false, false) } } SnapshotSystemJUnit5.forClass(kclass.java.name) diff --git a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt index 366fc526a..41187509f 100644 --- a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt +++ b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt @@ -18,13 +18,13 @@ package com.diffplug.selfie.kotest import com.diffplug.selfie.guts.FS import com.diffplug.selfie.guts.TypedPath import io.kotest.assertions.Actual -import io.kotest.assertions.Exceptions +import io.kotest.assertions.AssertionErrorBuilder import io.kotest.assertions.Expected import io.kotest.assertions.print.Printed import okio.FileSystem import okio.Path.Companion.toPath -expect internal val FS_SYSTEM: FileSystem +internal expect val FS_SYSTEM: FileSystem internal fun TypedPath.toPath(): okio.Path = absolutePath.toPath() internal object FSOkio : FS { @@ -42,7 +42,7 @@ internal object FSOkio : FS { FS_SYSTEM.write(typedPath.toPath()) { write(content) } /** Creates an assertion failed exception to throw. */ override fun assertFailed(message: String, expected: Any?, actual: Any?): Throwable = - if (expected == null && actual == null) Exceptions.createAssertionError(message, null) + if (expected == null && actual == null) AssertionErrorBuilder.create().withMessage(message).build() else { val expectedStr = nullableToString(expected, "") val actualStr = nullableToString(actual, "") @@ -56,9 +56,11 @@ internal object FSOkio : FS { } } private fun nullableToString(any: Any?, onNull: String): String = - any?.let { it.toString() } ?: onNull + any?.toString() ?: onNull private fun comparisonAssertion(message: String, expected: String, actual: String): Throwable { - return Exceptions.createAssertionError( - message, null, Expected(Printed((expected))), Actual(Printed((actual)))) + return AssertionErrorBuilder.create() + .withMessage(message) + .withValues(Expected(Printed((expected))), Actual(Printed((actual)))) + .build() } } diff --git a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt index ffd5fb653..01436e9df 100644 --- a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt +++ b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt @@ -26,7 +26,7 @@ import io.kotest.core.listeners.FinalizeSpecListener import io.kotest.core.source.SourceRef import io.kotest.core.spec.Spec import io.kotest.core.test.TestCase -import io.kotest.core.test.TestResult +import io.kotest.engine.test.TestResult import kotlin.jvm.JvmStatic import kotlin.reflect.KClass import kotlinx.coroutines.currentCoroutineContext @@ -62,7 +62,7 @@ class SelfieExtension( val classOrFilename: String = when (val source = testCase.source) { is SourceRef.ClassSource -> source.fqn - is SourceRef.FileSource -> source.fileName + is SourceRef.ClassLineSource -> source.fqn is SourceRef.None -> TODO("Handle SourceRef.None") } return system.forClassOrFilename(classOrFilename) @@ -73,7 +73,7 @@ class SelfieExtension( execute: suspend (TestCase) -> TestResult ): TestResult { val file = snapshotFileFor(testCase) - val testName = testCase.name.testName + val testName = testCase.name.name val coroutineLocal = CoroutineDiskStorage(DiskStorageKotest(file, testName)) return withContext(currentCoroutineContext() + coroutineLocal) { file.startTest(testName) @@ -89,8 +89,8 @@ class SelfieExtension( val file = results.keys.map { snapshotFileFor(it) }.firstOrNull() ?: return results.entries.forEach { if (it.value.isIgnored) { - file.startTest(it.key.name.testName) - file.finishedTestWithSuccess(it.key.name.testName, false) + file.startTest(it.key.name.name) + file.finishedTestWithSuccess(it.key.name.name, false) } } file.finishedClassWithSuccess(results.entries.all { it.value.isSuccess }) diff --git a/jvm/undertest-junit5-kotest/build.gradle b/jvm/undertest-junit5-kotest/build.gradle index e45f5e401..43fc8f05d 100644 --- a/jvm/undertest-junit5-kotest/build.gradle +++ b/jvm/undertest-junit5-kotest/build.gradle @@ -39,4 +39,5 @@ test { // defaults to 'write' systemProperty 'selfie', findProperty('selfie') systemProperty 'selfie.settings', findProperty('selfie.settings') + systemProperty 'kotest.framework.config.fqn', 'undertest.junit5.JunitKotestProjectConfig' } \ No newline at end of file diff --git a/jvm/undertest-junit5-kotest/harness/gradle.properties b/jvm/undertest-junit5-kotest/harness/gradle.properties index 520a835d1..b48cfaa78 100644 --- a/jvm/undertest-junit5-kotest/harness/gradle.properties +++ b/jvm/undertest-junit5-kotest/harness/gradle.properties @@ -9,4 +9,4 @@ ver_JUNIT_PIONEER=2.2.0 ver_OKIO=3.7.0 ver_KOTLIN_TEST=1.9.22 ver_KOTLIN_SERIALIZATION=1.6.3 -ver_KOTEST=5.8.0 \ No newline at end of file +ver_KOTEST=6.1.0 \ No newline at end of file diff --git a/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/JunitKotestProjectConfig.kt b/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/JunitKotestProjectConfig.kt index 574982605..6750745d9 100644 --- a/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/JunitKotestProjectConfig.kt +++ b/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/JunitKotestProjectConfig.kt @@ -4,5 +4,5 @@ import com.diffplug.selfie.junit5.SelfieExtension import io.kotest.core.config.AbstractProjectConfig class JunitKotestProjectConfig : AbstractProjectConfig() { - override fun extensions() = listOf(SelfieExtension(this)) + override val extensions = listOf(SelfieExtension(this)) } diff --git a/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/UT_KotestConcurrencyStressTest.kt b/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/UT_KotestConcurrencyStressTest.kt index bdabe2565..540b9ddaf 100644 --- a/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/UT_KotestConcurrencyStressTest.kt +++ b/jvm/undertest-junit5-kotest/src/test/kotlin/undertest/junit5/UT_KotestConcurrencyStressTest.kt @@ -1,12 +1,15 @@ package undertest.junit5 import com.diffplug.selfie.coroutines.expectSelfie +import io.kotest.common.ExperimentalKotest import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.concurrency.TestExecutionMode import kotlinx.coroutines.delay +@OptIn(ExperimentalKotest::class) class UT_KotestConcurrencyStressTest : FunSpec({ - concurrency = 100 + testExecutionMode = TestExecutionMode.LimitedConcurrency(100) for (d in 1..1000) { val digit = d test(String.format("test %04d", digit)) { From 0cbea499aa64567bce70fa605060b9869126b5fc Mon Sep 17 00:00:00 2001 From: sksamuel Date: Tue, 20 Jan 2026 16:06:21 -0600 Subject: [PATCH 2/5] Fixed spotless warning --- .../main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt b/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt index 062e5d7a9..b15fb563e 100644 --- a/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt +++ b/jvm/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieExtension.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 75942b02448f67edb4b0ab853395d1cd2b986b62 Mon Sep 17 00:00:00 2001 From: sksamuel Date: Tue, 20 Jan 2026 20:16:34 -0600 Subject: [PATCH 3/5] Spotless apply --- .../kotlin/com/diffplug/selfie/kotest/FSOkio.kt | 16 +++++++++------- .../diffplug/selfie/kotest/SelfieExtension.kt | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt index 41187509f..5bdac324e 100644 --- a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt +++ b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/FSOkio.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ internal fun TypedPath.toPath(): okio.Path = absolutePath.toPath() internal object FSOkio : FS { override fun fileExists(typedPath: TypedPath): Boolean = FS_SYSTEM.metadataOrNull(typedPath.toPath())?.isRegularFile ?: false + /** Walks the files (not directories) which are children and grandchildren of the given path. */ override fun fileWalk(typedPath: TypedPath, walk: (Sequence) -> T): T = walk( @@ -40,9 +41,11 @@ internal object FSOkio : FS { FS_SYSTEM.read(typedPath.toPath()) { readByteArray() } override fun fileWriteBinary(typedPath: TypedPath, content: ByteArray): Unit = FS_SYSTEM.write(typedPath.toPath()) { write(content) } + /** Creates an assertion failed exception to throw. */ override fun assertFailed(message: String, expected: Any?, actual: Any?): Throwable = - if (expected == null && actual == null) AssertionErrorBuilder.create().withMessage(message).build() + if (expected == null && actual == null) + AssertionErrorBuilder.create().withMessage(message).build() else { val expectedStr = nullableToString(expected, "") val actualStr = nullableToString(actual, "") @@ -55,12 +58,11 @@ internal object FSOkio : FS { comparisonAssertion(message, expectedStr, actualStr) } } - private fun nullableToString(any: Any?, onNull: String): String = - any?.toString() ?: onNull + private fun nullableToString(any: Any?, onNull: String): String = any?.toString() ?: onNull private fun comparisonAssertion(message: String, expected: String, actual: String): Throwable { return AssertionErrorBuilder.create() - .withMessage(message) - .withValues(Expected(Printed((expected))), Actual(Printed((actual)))) - .build() + .withMessage(message) + .withValues(Expected(Printed((expected))), Actual(Printed((actual)))) + .build() } } diff --git a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt index 01436e9df..3ee9a74b5 100644 --- a/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt +++ b/jvm/selfie-runner-kotest/src/commonMain/kotlin/com/diffplug/selfie/kotest/SelfieExtension.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 DiffPlug + * Copyright (C) 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ class SelfieExtension( } return system.forClassOrFilename(classOrFilename) } + /** Called for every test method. */ override suspend fun intercept( testCase: TestCase, From 45f88c9e95c71a3ba716fa0582593966a30a92b1 Mon Sep 17 00:00:00 2001 From: sksamuel Date: Tue, 20 Jan 2026 20:21:04 -0600 Subject: [PATCH 4/5] docs --- selfie.dev/src/pages/jvm/kotest.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfie.dev/src/pages/jvm/kotest.mdx b/selfie.dev/src/pages/jvm/kotest.mdx index b748affb1..8dcc5c81f 100644 --- a/selfie.dev/src/pages/jvm/kotest.mdx +++ b/selfie.dev/src/pages/jvm/kotest.mdx @@ -40,6 +40,9 @@ class ProjectConfig : AbstractProjectConfig() { } ``` +Note that your `AbstractProjectConfig` must either have the fully qualified name of `io.kotest.provided.ProjectConfig` or the system property `kotest.framework.config.fqn` must be set. +See [Kotest docs](https://kotest.io/docs/framework/project-config.html) for more info. + ## Selfie and coroutines In a regular JUnit 5 test, you call `Selfie.expectSelfie(...)`, like so From 5ee91c50b7b814b88596b67470c68ab8a273c6c9 Mon Sep 17 00:00:00 2001 From: sksamuel Date: Tue, 20 Jan 2026 22:36:56 -0600 Subject: [PATCH 5/5] tests --- .../kotlin/undertest/kotest/UT_ConcurrencyStressTest.kt | 5 ++++- .../src/jvmTest/kotlin/kotest/KotestProjectConfig.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jvm/undertest-kotest/src/commonTest/kotlin/undertest/kotest/UT_ConcurrencyStressTest.kt b/jvm/undertest-kotest/src/commonTest/kotlin/undertest/kotest/UT_ConcurrencyStressTest.kt index 995718021..65760c78f 100644 --- a/jvm/undertest-kotest/src/commonTest/kotlin/undertest/kotest/UT_ConcurrencyStressTest.kt +++ b/jvm/undertest-kotest/src/commonTest/kotlin/undertest/kotest/UT_ConcurrencyStressTest.kt @@ -1,12 +1,15 @@ package undertest.kotest import com.diffplug.selfie.coroutines.expectSelfie +import io.kotest.common.ExperimentalKotest import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.concurrency.TestExecutionMode import kotlinx.coroutines.delay +@OptIn(ExperimentalKotest::class) class UT_ConcurrencyStressTest : FunSpec({ - concurrency = 100 + testExecutionMode = TestExecutionMode.LimitedConcurrency(100) for (d in 1..1000) { val digit = d test(String.format("test %04d", digit)) { diff --git a/jvm/undertest-kotest/src/jvmTest/kotlin/kotest/KotestProjectConfig.kt b/jvm/undertest-kotest/src/jvmTest/kotlin/kotest/KotestProjectConfig.kt index c15b38e8b..9632653e2 100644 --- a/jvm/undertest-kotest/src/jvmTest/kotlin/kotest/KotestProjectConfig.kt +++ b/jvm/undertest-kotest/src/jvmTest/kotlin/kotest/KotestProjectConfig.kt @@ -4,5 +4,5 @@ import com.diffplug.selfie.kotest.SelfieExtension import io.kotest.core.config.AbstractProjectConfig object KotestProjectConfig : AbstractProjectConfig() { - override fun extensions() = listOf(SelfieExtension(this)) + override val extensions = listOf(SelfieExtension(this)) }