From 1f8920e7c3323435ffa1f68be94b855a8d4a1918 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 Dec 2023 15:17:01 -0800 Subject: [PATCH 1/3] Keep track of when a snapshot file is written, so that we notice and notify when a just-created snapshot file is going to be deleted as stale. --- .../junit5/SelfieTestExecutionListener.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieTestExecutionListener.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieTestExecutionListener.kt index b707a3f8..ccad5b72 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieTestExecutionListener.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieTestExecutionListener.kt @@ -21,6 +21,8 @@ import com.diffplug.selfie.RW import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path +import java.util.concurrent.ConcurrentSkipListSet +import java.util.concurrent.atomic.AtomicReference import org.junit.platform.engine.TestExecutionResult import org.junit.platform.engine.support.descriptor.ClassSource import org.junit.platform.engine.support.descriptor.MethodSource @@ -121,6 +123,7 @@ internal class ClassProgress(val parent: Progress, val className: String) { if (file!!.snapshots.isEmpty()) { deleteFileAndParentDirIfEmpty(snapshotPath) } else { + parent.markPathAsWritten(parent.layout.snapshotPathForClass(className)) Files.createDirectories(snapshotPath.parent) Files.newBufferedWriter(snapshotPath, StandardCharsets.UTF_8).use { writer -> file!!.serialize(writer::write) @@ -227,10 +230,27 @@ internal class Progress { } } } + + private var checkForInvalidStale: AtomicReference?> = + AtomicReference(ConcurrentSkipListSet()) + internal fun markPathAsWritten(path: Path) { + val written = + checkForInvalidStale.get() + ?: throw AssertionError("Snapshot file is being written after all tests were finished.") + written.add(path) + } fun finishedAllTests() { + val written = + checkForInvalidStale.getAndSet(null) + ?: throw AssertionError("finishedAllTests() was called more than once.") for (stale in findStaleSnapshotFiles(layout)) { val path = layout.snapshotPathForClass(stale) - deleteFileAndParentDirIfEmpty(path) + if (written.contains(path)) { + throw AssertionError( + "Selfie wrote a snapshot and then marked it stale for deletion it in the same run: $path\nSelfie will delete this snapshot on the next run, which is bad! Why is Selfie marking this snapshot as stale?") + } else { + deleteFileAndParentDirIfEmpty(path) + } } } } From 538fe50978acd115c6df1fb1544e37488939b5de Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 Dec 2023 16:22:10 -0800 Subject: [PATCH 2/3] Add a test for `junit-vintage`, failing. --- .../junitvintage/ReadWriteVintageTest.kt | 85 +++++++++++++++++++ settings.gradle | 1 + undertest-junit-vintage/build.gradle | 48 +++++++++++ .../junit5/UT_ReadWriteVintageTest.kt | 11 +++ .../__snapshots__/UT_ReadWriteVintageTest.ss | 3 + undertest-junit5/build.gradle | 2 + 6 files changed, 150 insertions(+) create mode 100644 selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junitvintage/ReadWriteVintageTest.kt create mode 100644 undertest-junit-vintage/build.gradle create mode 100644 undertest-junit-vintage/src/test/kotlin/undertest/junit5/UT_ReadWriteVintageTest.kt create mode 100644 undertest-junit-vintage/src/test/kotlin/undertest/junit5/__snapshots__/UT_ReadWriteVintageTest.ss diff --git a/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junitvintage/ReadWriteVintageTest.kt b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junitvintage/ReadWriteVintageTest.kt new file mode 100644 index 00000000..46574ce5 --- /dev/null +++ b/selfie-runner-junit5/src/test/kotlin/com/diffplug/selfie/junitvintage/ReadWriteVintageTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.selfie.junitvintage + +import com.diffplug.selfie.junit5.Harness +import kotlin.test.Test +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.TestMethodOrder +import org.junitpioneer.jupiter.DisableIfTestFails + +/** Simplest test for verifying read/write of a snapshot. */ +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +@DisableIfTestFails +class ReadWriteVintageTest : Harness("undertest-junit-vintage") { + @Test @Order(1) + fun noSelfie() { + ut_snapshot().deleteIfExists() + ut_snapshot().assertDoesNotExist() + } + + @Test @Order(2) + fun writeApple() { + ut_mirror().lineWith("apple").uncomment() + ut_mirror().lineWith("orange").commentOut() + gradleWriteSS() + ut_snapshot() + .assertContent( + """ + ╔═ selfie ═╗ + apple + ╔═ [end of file] ═╗ + + """ + .trimIndent()) + } + + @Test @Order(3) + fun assertApplePasses() { + gradleReadSS() + } + + @Test @Order(4) + fun assertOrangeFails() { + ut_mirror().lineWith("apple").commentOut() + ut_mirror().lineWith("orange").uncomment() + gradleReadSSFail() + ut_snapshot() + .assertContent( + """ + ╔═ selfie ═╗ + apple + ╔═ [end of file] ═╗ + + """ + .trimIndent()) + } + + @Test @Order(5) + fun writeOrange() { + gradleWriteSS() + ut_snapshot() + .assertContent( + """ + ╔═ selfie ═╗ + orange + ╔═ [end of file] ═╗ + + """ + .trimIndent()) + } +} diff --git a/settings.gradle b/settings.gradle index 7843bb2d..a8528ed0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -42,4 +42,5 @@ blowdryerSetup { include 'selfie-lib' include 'selfie-runner-junit5' include 'undertest-junit5' +include 'undertest-junit-vintage' rootProject.name = 'selfie' diff --git a/undertest-junit-vintage/build.gradle b/undertest-junit-vintage/build.gradle new file mode 100644 index 00000000..a7deca57 --- /dev/null +++ b/undertest-junit-vintage/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' +} +repositories { + mavenCentral() +} +apply plugin: 'com.diffplug.spotless' +spotless { + kotlin { + target 'src/**/*.kt' + toggleOffOn() + licenseHeader '' + ktfmt() + replaceRegex("test one-liner", "@Test\n(\\s*)fun ", "@Test fun ") + replaceRegex("test harness comments", "\n(\\s)*//", "\n//") + } +} + +dependencies { + testImplementation project(':selfie-runner-junit5') + testCompileOnly "junit:junit:4.13.2" + testImplementation "org.junit.jupiter:junit-jupiter-api:${ver_JUNIT_USE}" + testImplementation "org.junit.vintage:junit-vintage-engine:${ver_JUNIT_USE}" +} +// this project is just a test environment for a different project +test { + enabled = false +} +tasks.register('underTest', Test) { + useJUnitPlatform() + testClassesDirs = testing.suites.test.sources.output.classesDirs + classpath = testing.suites.test.sources.runtimeClasspath + testLogging.showStandardStreams = true + // the snapshots are both output and input, for this harness best if the test just always runs + outputs.upToDateWhen { false } + // defaults to 'write' + systemProperty 'selfie', findProperty('selfie') +} +tasks.register('underTestRead', Test) { + useJUnitPlatform() + testClassesDirs = testing.suites.test.sources.output.classesDirs + classpath = testing.suites.test.sources.runtimeClasspath + testLogging.showStandardStreams = true + // the snapshots are both output and input, for this harness best if the test just always runs + outputs.upToDateWhen { false } + // read-only + systemProperty 'selfie', 'read' +} diff --git a/undertest-junit-vintage/src/test/kotlin/undertest/junit5/UT_ReadWriteVintageTest.kt b/undertest-junit-vintage/src/test/kotlin/undertest/junit5/UT_ReadWriteVintageTest.kt new file mode 100644 index 00000000..4ef54996 --- /dev/null +++ b/undertest-junit-vintage/src/test/kotlin/undertest/junit5/UT_ReadWriteVintageTest.kt @@ -0,0 +1,11 @@ +package undertest.junit5 + +import com.diffplug.selfie.Selfie.expectSelfie +import org.junit.Test + +class UT_ReadWriteVintageTest { + @Test fun selfie() { +// expectSelfie("apple").toMatchDisk() + expectSelfie("orange").toMatchDisk() + } +} diff --git a/undertest-junit-vintage/src/test/kotlin/undertest/junit5/__snapshots__/UT_ReadWriteVintageTest.ss b/undertest-junit-vintage/src/test/kotlin/undertest/junit5/__snapshots__/UT_ReadWriteVintageTest.ss new file mode 100644 index 00000000..4ca144c6 --- /dev/null +++ b/undertest-junit-vintage/src/test/kotlin/undertest/junit5/__snapshots__/UT_ReadWriteVintageTest.ss @@ -0,0 +1,3 @@ +╔═ selfie ═╗ +orange +╔═ [end of file] ═╗ diff --git a/undertest-junit5/build.gradle b/undertest-junit5/build.gradle index 5cd65e34..77477a97 100644 --- a/undertest-junit5/build.gradle +++ b/undertest-junit5/build.gradle @@ -29,6 +29,7 @@ tasks.register('underTest', Test) { useJUnitPlatform() testClassesDirs = testing.suites.test.sources.output.classesDirs classpath = testing.suites.test.sources.runtimeClasspath + testLogging.showStandardStreams = true // the snapshots are both output and input, for this harness best if the test just always runs outputs.upToDateWhen { false } // defaults to 'write' @@ -38,6 +39,7 @@ tasks.register('underTestRead', Test) { useJUnitPlatform() testClassesDirs = testing.suites.test.sources.output.classesDirs classpath = testing.suites.test.sources.runtimeClasspath + testLogging.showStandardStreams = true // the snapshots are both output and input, for this harness best if the test just always runs outputs.upToDateWhen { false } // read-only From 17aa070b1d1b2d2ad6873a1789ae5830a35fcb8b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 Dec 2023 16:26:21 -0800 Subject: [PATCH 3/3] SelfieGC now has a list of annotations which it supports for marking a test as existing. --- .../com/diffplug/selfie/junit5/SelfieGC.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieGC.kt b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieGC.kt index 6359ff49..4e3cde92 100644 --- a/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieGC.kt +++ b/selfie-runner-junit5/src/main/kotlin/com/diffplug/selfie/junit5/SelfieGC.kt @@ -22,6 +22,20 @@ import java.nio.file.Files import kotlin.io.path.name import org.junit.jupiter.api.Test +/** Search for any test annotation classes which are present on the classpath. */ +private val testAnnotations = + listOf( + "org.junit.jupiter.api.Test", // junit5, + "org.junit.Test" // junit4 + ) + .mapNotNull { + try { + Class.forName(it).asSubclass(Annotation::class.java) + } catch (e: ClassNotFoundException) { + null + } + } + /** * Searches the whole snapshot directory, finds all the `.ss` files, and prunes any which don't have * matching test files anymore. @@ -41,7 +55,9 @@ internal fun findStaleSnapshotFiles(layout: SnapshotFileLayout): List { } private fun classExistsAndHasTests(key: String): Boolean { return try { - Class.forName(key).declaredMethods.any { it.isAnnotationPresent(Test::class.java) } + Class.forName(key).methods.any { method -> + testAnnotations.any { method.isAnnotationPresent(it) } + } } catch (e: ClassNotFoundException) { false }