diff --git a/app-desktop/build.gradle.kts b/app-desktop/build.gradle.kts index 5d4bd25c..ba0a336b 100644 --- a/app-desktop/build.gradle.kts +++ b/app-desktop/build.gradle.kts @@ -66,11 +66,14 @@ val generateStrings by tasks.registering(JavaExec::class) { mainClass.set("kst4contest.i18n.GeneratorMain") val input = layout.projectDirectory.dir("src/main/i18n") + // A local copy: the provider below must not capture the script-level val, which + // drags the build script object into the configuration cache and fails to serialize. + val output = generatedI18nDirectory inputs.dir(input) - outputs.dir(generatedI18nDirectory) + outputs.dir(output) argumentProviders.add(CommandLineArgumentProvider { - listOf(input.asFile.absolutePath, generatedI18nDirectory.get().asFile.absolutePath) + listOf(input.asFile.absolutePath, output.get().asFile.absolutePath) }) } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/DrawingLayerRegressionTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/DrawingLayerRegressionTest.kt index 3c7bd4c1..96ae82af 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/DrawingLayerRegressionTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/DrawingLayerRegressionTest.kt @@ -20,6 +20,8 @@ import kst4contest.view.compose.tabs.MessageHandlingTabState import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path /** * The four ways the drawing layer went wrong while the build was green. @@ -41,7 +43,7 @@ import org.junit.jupiter.api.Test * these were written, because the spec left it open and a net that only works on a * developer's desk is worth saying so about. */ -class DrawingLayerRegressionTest { +class DrawingLayerRegressionTest(@TempDir private val tempDir: Path) { private fun themed(content: @Composable () -> Unit): @Composable () -> Unit = { Kst4ContestTheme(darkMode = false, baseFontSizeSp = 12f) { content() } @@ -52,7 +54,7 @@ class DrawingLayerRegressionTest { @OptIn(ExperimentalTestApi::class) @Test fun aTypedSettingAppearsOnScreenAndNotOnlyInThePreferences() = runComposeUiTest { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.setMessageHandling_autoAnswerTextMainCat("") val state = MessageHandlingTabState(prefs) diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowStateTest.kt index f1288a96..5666414e 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowStateTest.kt @@ -6,6 +6,8 @@ import kst4contest.model.ChatPreferences import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path /** * The shortcut and snippet rosters are ChatPreferences.SimpleRoster, which notifies @@ -13,11 +15,11 @@ import org.junit.jupiter.api.Test * above the send line follow the settings window — the reason ShortcutsTabState has been * calling refreshShortcutButtons into thin air since Etappe 3b. */ -class MainWindowStateRosterTest { +class MainWindowStateRosterTest(@TempDir private val tempDir: Path) { @Test fun `the shortcuts start out as what the profile holds`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getLst_txtShortCutBtnList().setAll(listOf("MYQRG", "pse sked")) val mirror = RosterMirror(prefs.getLst_txtShortCutBtnList()) @@ -31,7 +33,7 @@ class MainWindowStateRosterTest { */ @Test fun `a roster change reaches the mirror when it is refreshed`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getLst_txtShortCutBtnList().setAll(listOf("MYQRG")) val mirror = RosterMirror(prefs.getLst_txtShortCutBtnList()) @@ -44,7 +46,7 @@ class MainWindowStateRosterTest { @Test fun `an emptied roster empties the mirror`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getLst_txtShortCutBtnList().setAll(listOf("MYQRG")) val mirror = RosterMirror(prefs.getLst_txtShortCutBtnList()) @@ -60,7 +62,7 @@ class MainWindowStateRosterTest { */ @Test fun `drawing the buttons subscribes to the mirror`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getLst_txtShortCutBtnList().setAll(listOf("MYQRG")) val mirror = RosterMirror(prefs.getLst_txtShortCutBtnList()) @@ -79,9 +81,9 @@ class MainWindowStateRosterTest { * A mismatch here is silent and total: SplitterState falls back to an even split, so * every operator would lose the layout they arranged without any error saying so. */ -class MainWindowSplitterDefaultsTest { +class MainWindowSplitterDefaultsTest(@TempDir private val tempDir: Path) { - private val prefs = ChatPreferences() + private val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) @Test fun `the message column has as many dividers as the profile stores positions`() { diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/ObservedValueTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/ObservedValueTest.kt index 97e74adc..d0a1966e 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/ObservedValueTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/ObservedValueTest.kt @@ -5,6 +5,8 @@ import kst4contest.model.ChatPreferences import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path /** * ChatPreferences hands out MutableValue, which notifies its own listeners and nothing @@ -15,11 +17,11 @@ import org.junit.jupiter.api.Test * The JavaFX window solved it with attachQtfFollower() and attachOwnQrgFollower(); this is * the same idea, ending in Compose state instead of a TextField. */ -class ObservedValueTest { +class ObservedValueTest(@TempDir private val tempDir: Path) { @Test fun `the mirror starts at the stored value`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getMYQRGFirstCat().set("144.370") val observed = ObservedValue(prefs.getMYQRGFirstCat()) @@ -30,7 +32,7 @@ class ObservedValueTest { /** The rotator moving must reach the screen without anything asking it to. */ @Test fun `a change to the underlying value reaches the mirror by itself`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getActualQTF().set(90.0) val observed = ObservedValue(prefs.getActualQTF()) @@ -41,7 +43,7 @@ class ObservedValueTest { @Test fun `a null value is carried through rather than turned into a default`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val observed = ObservedValue(prefs.getMYQRGSecondCat()) prefs.getMYQRGSecondCat().set(null) @@ -55,7 +57,7 @@ class ObservedValueTest { */ @Test fun `drawing from the mirror subscribes to it`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getMYQRGFirstCat().set("144.370") val observed = ObservedValue(prefs.getMYQRGFirstCat()) @@ -73,7 +75,7 @@ class ObservedValueTest { */ @Test fun `releasing the mirror stops it following`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.getMYQRGFirstCat().set("144.370") val observed = ObservedValue(prefs.getMYQRGFirstCat()) @@ -85,7 +87,7 @@ class ObservedValueTest { @Test fun `releasing twice is harmless`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val observed = ObservedValue(prefs.getMYQRGFirstCat()) observed.release() observed.release() diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/PaletteStoreFactoryTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/PaletteStoreFactoryTest.kt index 4ff99d7e..58bd0230 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/PaletteStoreFactoryTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/PaletteStoreFactoryTest.kt @@ -7,6 +7,8 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.io.File +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path /** * The factory binds the store to one profile's preferences and one profile's stylesheet. @@ -15,7 +17,7 @@ import java.io.File * another's colours. The stylesheet path is injected here rather than derived inside, so the * test does not have to write into the operator's home directory to check which file is read. */ -class PaletteStoreFactoryTest { +class PaletteStoreFactoryTest(@TempDir private val tempDir: Path) { private val daylight = OperatorProfilePaletteFiles( dayFile = File("/nonexistent/day.css"), @@ -24,7 +26,7 @@ class PaletteStoreFactoryTest { @Test fun aChangeLandsInTheDesignsOwnPreference() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val store = PaletteStoreFactory.create(prefs, daylight) store.set(darkMode = false, role = PaletteRole.ACCENT, colour = "#FF0000") @@ -35,7 +37,7 @@ class PaletteStoreFactoryTest { @Test fun anEveningChangeLandsInTheEveningPreference() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val store = PaletteStoreFactory.create(prefs, daylight) store.set(darkMode = true, role = PaletteRole.ACCENT, colour = "#FF0000") @@ -47,7 +49,7 @@ class PaletteStoreFactoryTest { @Test fun whatIsAlreadyStoredIsInForceFromTheStart() { // The store is built at startup; a colour saved last session has to be there at once. - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.guiOptions_paletteOverridesDay = "accent=#FF0000" val store = PaletteStoreFactory.create(prefs, daylight) @@ -57,7 +59,7 @@ class PaletteStoreFactoryTest { @Test fun aMissingStylesheetLeavesEveryRoleWithTheShippedSheet() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val store = PaletteStoreFactory.create(prefs, daylight) @@ -73,7 +75,7 @@ class PaletteStoreFactoryTest { file.writeText(".root { -fx-accent: #123456; }") val store = PaletteStoreFactory.create( - ChatPreferences(), + ChatPreferences(tempDir.resolve("preferences.xml")), OperatorProfilePaletteFiles(dayFile = file, eveningFile = file), ) diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/AirscoutTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/AirscoutTabStateTest.kt index 5a9c8e5e..9010ffba 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/AirscoutTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/AirscoutTabStateTest.kt @@ -5,11 +5,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class AirscoutTabStateTest { +class AirscoutTabStateTest(@TempDir private val tempDir: Path) { - private class Session { - val prefs = ChatPreferences() + private inner class Session { + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state by lazy { AirscoutTabState(prefs) } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/BeaconTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/BeaconTabStateTest.kt index c531ab33..88725471 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/BeaconTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/BeaconTabStateTest.kt @@ -8,11 +8,13 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class BeaconTabStateTest { +class BeaconTabStateTest(@TempDir private val tempDir: Path) { - private class Session(private val templateRefusal: String? = null) { - val prefs = ChatPreferences() + private inner class Session(private val templateRefusal: String? = null) { + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) var timerRestarts = 0 var validated = mutableListOf() diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/GuiOptionsTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/GuiOptionsTabStateTest.kt index 9e146fdd..30933653 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/GuiOptionsTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/GuiOptionsTabStateTest.kt @@ -9,8 +9,10 @@ import java.util.Locale import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class GuiOptionsTabStateTest { +class GuiOptionsTabStateTest(@TempDir private val tempDir: Path) { /* * Over lambdas rather than over the preferences, so switching the language in a test does @@ -33,7 +35,7 @@ class GuiOptionsTabStateTest { * that gets saved, and it has to do so through the store, which is what repaints the * open windows. A store wired to anything else would pass a weaker test. */ - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state = GuiOptionsTabState(prefs, LanguageStoreFactory.create(prefs, Locale.UK)) state.language = "de" @@ -45,7 +47,7 @@ class GuiOptionsTabStateTest { @Test fun `reading comes straight from the preferences`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.setGUI_darkModeActiveByDefault(true) assertTrue(GuiOptionsTabState(prefs, languageStore()).gUI_darkModeActiveByDefault) @@ -53,7 +55,7 @@ class GuiOptionsTabStateTest { @Test fun `writing reaches the preferences at once`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state = GuiOptionsTabState(prefs, languageStore()) state.guiOptions_defaultFilterPublicMsgs = true @@ -68,7 +70,7 @@ class GuiOptionsTabStateTest { // The JavaFX tab offered them as four separate checkboxes, not a choice of // one. Turning them into a radio group would take a combination away that // an operator may have configured. - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state = GuiOptionsTabState(prefs, languageStore()) state.guiOptions_defaultFilterPublicMsgs = true diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/LogSynchTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/LogSynchTabStateTest.kt index 7e5afc61..cff3a617 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/LogSynchTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/LogSynchTabStateTest.kt @@ -5,11 +5,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class LogSynchTabStateTest { +class LogSynchTabStateTest(@TempDir private val tempDir: Path) { - private class Session(detected: String? = null) { - val prefs = ChatPreferences() + private inner class Session(detected: String? = null) { + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) var restarts = 0 var stops = 0 val detectedBroadcast = detected diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/MessageHandlingTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/MessageHandlingTabStateTest.kt index 4e6ea1a6..45682fbb 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/MessageHandlingTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/MessageHandlingTabStateTest.kt @@ -5,12 +5,14 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class MessageHandlingTabStateTest { +class MessageHandlingTabStateTest(@TempDir private val tempDir: Path) { @Test fun `the state starts from the stored preferences`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) prefs.setMessageHandling_autoAnswerEnabled(true) prefs.setMessageHandling_autoAnswerToQRGRequestEnabled(true) prefs.setMessageHandling_autoAnswerTextMainCat("qrv 144.300") @@ -26,7 +28,7 @@ class MessageHandlingTabStateTest { fun `one checkbox drives both category flags`() { // The JavaFX window offered a single checkbox and wrote both preferences from // it; keeping only the main one would leave the second chat on its old value. - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state = MessageHandlingTabState(prefs) state.autoAnswerEnabled = true @@ -42,7 +44,7 @@ class MessageHandlingTabStateTest { @Test fun `one text field drives both category texts`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state = MessageHandlingTabState(prefs) state.autoAnswerText = "qrv 432.200" @@ -53,7 +55,7 @@ class MessageHandlingTabStateTest { @Test fun `the QRG reply flag writes through on its own`() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state = MessageHandlingTabState(prefs) state.autoAnswerToQRGRequestEnabled = true diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/NotificationTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/NotificationTabStateTest.kt index 4b561158..f8e4a7c0 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/NotificationTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/NotificationTabStateTest.kt @@ -11,11 +11,13 @@ import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class NotificationTabStateTest { +class NotificationTabStateTest(@TempDir private val tempDir: Path) { - private class Session { - val prefs = ChatPreferences() + private inner class Session { + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val monitored = SimpleRoster() var serverEnabledApplied: Boolean? = null var portApplied: Int? = null diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/ShortcutsTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/ShortcutsTabStateTest.kt index ac19a9e3..b0621482 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/ShortcutsTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/ShortcutsTabStateTest.kt @@ -5,11 +5,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class ShortcutsTabStateTest { +class ShortcutsTabStateTest(@TempDir private val tempDir: Path) { - private class Session { - val prefs = ChatPreferences() + private inner class Session { + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) var shortcutRefreshes = 0 var snippetRefreshes = 0 diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/StationTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/StationTabStateTest.kt index 80f0cf86..169db403 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/StationTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/StationTabStateTest.kt @@ -6,11 +6,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class StationTabStateTest { +class StationTabStateTest(@TempDir private val tempDir: Path) { - private class Session { - val prefs = ChatPreferences() + private inner class Session { + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) var mainApplied: ChatCategory? = null var secondApplied: ChatCategory? = null var secondWasApplied = false @@ -87,7 +89,7 @@ class StationTabStateTest { fun `the second chat switch lives with the login settings`() { // It moved here from the GUI tab: the JavaFX checkbox stood on the Station tab // next to the category pickers, and it decides what the Connect button does. - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val state = StationTabState(prefs, { }, { }) state.loginToSecondChatEnabled = true diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/TrxSynchTabStateTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/TrxSynchTabStateTest.kt index a70cb9ca..f42d11b0 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/TrxSynchTabStateTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/TrxSynchTabStateTest.kt @@ -5,11 +5,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path -class TrxSynchTabStateTest { +class TrxSynchTabStateTest(@TempDir private val tempDir: Path) { - private class Session { - val prefs = ChatPreferences() + private inner class Session { + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val followerCalls = mutableListOf() val state by lazy { TrxSynchTabState(prefs) { followerCalls.add(it) } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/i18n/TranslationBoundaryTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/i18n/TranslationBoundaryTest.kt index b134bfef..1e8dbae8 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/i18n/TranslationBoundaryTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/i18n/TranslationBoundaryTest.kt @@ -5,6 +5,8 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.lang.reflect.Modifier +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path /** * Nothing that goes to the server or to another radio amateur has got into the texts. @@ -17,7 +19,7 @@ import java.lang.reflect.Modifier * deliberately not: a partial file is the normal state of a contribution in progress, and a * check over every language would turn the arrival of one into a red build. */ -class TranslationBoundaryTest { +class TranslationBoundaryTest(@TempDir private val tempDir: Path) { private val everyText: List = Translations.BY_LANGUAGE.values.flatMap { it.values } @@ -63,7 +65,7 @@ class TranslationBoundaryTest { @Test fun noTextIsOneOfThePreferenceDefaultsThatGoToOtherRadioAmateurs() { - val prefs = ChatPreferences() + val prefs = ChatPreferences(tempDir.resolve("preferences.xml")) val defaults = ChatPreferences::class.java.declaredFields .filter { it.type == String::class.java && !Modifier.isStatic(it.modifiers) } diff --git a/build.gradle.kts b/build.gradle.kts index 9832afd0..636abeb8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -68,8 +68,80 @@ subprojects { } } + /* + * Tests never see the operator's real home directory. ApplicationFileUtils resolves the + * application folder below user.home, and the no-argument ChatPreferences constructor + * seeds preferences.xml there when it is missing; a test that slipped through would + * create or rewrite files in ~/.praktiKST of whoever runs the build. Each test task's JVM + * gets its own scratch home, build/test-user-home/, emptied before the task + * runs, so one test task can never wipe another's. Tests that need their own profile + * directory still set user.home themselves and restore it afterwards. + * + * The redirection alone would hide the mistake, so every test task is finalized by + * checkTestUserHome, which fails the build if a preferences.xml was left in any scratch + * home: tests are expected to use ChatPreferences(Path) with a @TempDir. It is a + * finalizer and not a doLast because a doLast is skipped when a test fails, and a + * failing test must not hide the stray file. The check stays deliberately narrow: other + * files land in the scratch .praktiKST by design, for example the sound files + * PlayAudioUtils seeds from the bundled resources, and absorbing those is what the + * redirection is for. Only preferences.xml marks a test that should have used its own file. + * + * The opt-in ON4KST practice tests, which deliberately read the real profile, need + * -Don4kst.live=true, which the test task does not forward; they skip here either way + * and are meant to be run from an IDE. + */ + val testUserHomeRoot = layout.buildDirectory.dir("test-user-home").get().asFile + val checkTestUserHome = tasks.register("checkTestUserHome") { + description = "Fails if a test wrote preferences.xml into its scratch user.home" + group = "verification" + doLast { + val strayPreferences = testUserHomeRoot.listFiles().orEmpty() + .map { it.resolve(".praktiKST/preferences.xml") } + .filter { it.exists() } + if (strayPreferences.isNotEmpty()) { + throw GradleException( + "A test wrote ${strayPreferences.joinToString()}: it used the user " + + "profile instead of ChatPreferences(Path) with a @TempDir" + ) + } + } + } + tasks.withType().configureEach { useJUnitPlatform() + + val testUserHome = testUserHomeRoot.resolve(name) + /* + * The scratch home is a declared output. Test tasks are cacheable, and a cache hit + * skips doFirst; as an output the directory is restored together with the cached + * result, so checkTestUserHome always judges the files that belong to these inputs + * and never a leftover from another run. + * + * Skiko unpacks its native library (about 30 MB) below user.home unless told + * otherwise. It is kept beside the scratch home instead, so it neither bloats every + * cache entry nor is unpacked again on each run. + * + * Both paths are passed through an argument provider rather than systemProperty(): + * a system property is a task input, and an absolute path there would make the + * cache key differ between checkouts. + */ + val skikoDataPath = layout.buildDirectory.dir("skiko-data").get().asFile + outputs.dir(testUserHome) + jvmArgumentProviders.add(CommandLineArgumentProvider { + listOf( + "-Duser.home=${testUserHome.absolutePath}", + "-Dskiko.data.path=${skikoDataPath.absolutePath}" + ) + }) + doFirst { + // deleteRecursively() reports failure instead of throwing. A file left over + // from an earlier run would later be blamed on this one, so stop here. + if (!testUserHome.deleteRecursively()) { + throw GradleException("Could not empty the test user.home $testUserHome") + } + testUserHome.mkdirs() + } + finalizedBy(checkTestUserHome) } // XML for CI to read, HTML for people. Gradle does not wire the report after the test diff --git a/core/src/main/java/kst4contest/controller/DXClusterThreadPooledServer.java b/core/src/main/java/kst4contest/controller/DXClusterThreadPooledServer.java index e776a1ed..9c283d8f 100644 --- a/core/src/main/java/kst4contest/controller/DXClusterThreadPooledServer.java +++ b/core/src/main/java/kst4contest/controller/DXClusterThreadPooledServer.java @@ -40,7 +40,8 @@ public class DXClusterThreadPooledServer implements Runnable { Executors.newSingleThreadScheduledExecutor(); private volatile boolean stopped; - private ServerSocket serverSocket; + /** Assigned by run() on the server thread and closed by stop() from another one. */ + private volatile ServerSocket serverSocket; public DXClusterThreadPooledServer( int port, diff --git a/core/src/main/java/kst4contest/controller/WinTestSkedSender.java b/core/src/main/java/kst4contest/controller/WinTestSkedSender.java index 0f8a08b1..db10545b 100644 --- a/core/src/main/java/kst4contest/controller/WinTestSkedSender.java +++ b/core/src/main/java/kst4contest/controller/WinTestSkedSender.java @@ -178,7 +178,6 @@ public class WinTestSkedSender { private void sendUdp(WinTestMessage msg) throws Exception { try (DatagramSocket socket = new DatagramSocket()) { socket.setBroadcast(true); - socket.setReuseAddress(true); byte[] bytes = msg.toBytes(); DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcastAddress, port); diff --git a/core/src/test/java/kst4contest/controller/On4KstConnectionProbeTest.java b/core/src/test/java/kst4contest/controller/On4KstConnectionProbeTest.java index 28946a69..eaf13612 100644 --- a/core/src/test/java/kst4contest/controller/On4KstConnectionProbeTest.java +++ b/core/src/test/java/kst4contest/controller/On4KstConnectionProbeTest.java @@ -22,15 +22,20 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.Timeout; import kst4contest.model.ChatCategory; import kst4contest.model.ChatMessage; import kst4contest.model.ChatPreferences; import kst4contest.testing.ManualScheduler; +import java.nio.file.Path; class On4KstConnectionProbeTest { + @TempDir + Path tempDir; + @Test void separatesClientProbeServerProbeAndInternalResponses() { assertEquals("CK|", On4KstProtocol.clientLivenessProbe()); @@ -357,7 +362,7 @@ class On4KstConnectionProbeTest { } private ChatPreferences localPreferences() { - ChatPreferences preferences = new ChatPreferences(); + ChatPreferences preferences = new ChatPreferences(tempDir.resolve("preferences.xml")); preferences.setStn_on4kstServersDns("127.0.0.1"); preferences.setStn_loginCallSign("DL1ABC"); preferences.setStn_loginPassword("test-password"); diff --git a/core/src/test/java/kst4contest/controller/On4KstProtocolTest.java b/core/src/test/java/kst4contest/controller/On4KstProtocolTest.java index 5b91a72b..98b17906 100644 --- a/core/src/test/java/kst4contest/controller/On4KstProtocolTest.java +++ b/core/src/test/java/kst4contest/controller/On4KstProtocolTest.java @@ -5,13 +5,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.time.LocalDateTime; import java.time.ZoneOffset; import kst4contest.model.ChatPreferences; +import java.nio.file.Path; class On4KstProtocolTest { + + @TempDir + Path tempDir; + @Test void buildsLoginWithReplayOverlap() { assertEquals( @@ -72,7 +78,7 @@ class On4KstProtocolTest { @Test void resolvesBeaconVariablesBeforeApplyingProtocolValidation() { - ChatPreferences preferences = new ChatPreferences(); + ChatPreferences preferences = new ChatPreferences(tempDir.resolve("preferences.xml")); preferences.setMYQRGFirstCat("144.300"); ChatController controller = new ChatController(); @@ -92,7 +98,7 @@ class On4KstProtocolTest { @Test void acceptsTemporarilyUnresolvedVariableOnlyBeaconTemplate() { - ChatPreferences preferences = new ChatPreferences(); + ChatPreferences preferences = new ChatPreferences(tempDir.resolve("preferences.xml")); preferences.setMYQRGFirstCat(""); ChatController controller = new ChatController(); @@ -110,7 +116,7 @@ class On4KstProtocolTest { @Test void rejectsEmptyOverlongAndProtocolBreakingBeaconText() { - ChatPreferences preferences = new ChatPreferences(); + ChatPreferences preferences = new ChatPreferences(tempDir.resolve("preferences.xml")); ChatController controller = new ChatController(); controller.setChatPreferences(preferences); diff --git a/core/src/test/java/kst4contest/model/ChatPreferencesLanguageTest.java b/core/src/test/java/kst4contest/model/ChatPreferencesLanguageTest.java index 2343c6c8..e81c2975 100644 --- a/core/src/test/java/kst4contest/model/ChatPreferencesLanguageTest.java +++ b/core/src/test/java/kst4contest/model/ChatPreferencesLanguageTest.java @@ -1,21 +1,26 @@ package kst4contest.model; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.nio.file.Path; /** The interface language is stored per profile, like every other GUI preference. */ class ChatPreferencesLanguageTest { + @TempDir + Path tempDir; + @Test void theDefaultIsTheSystemLanguage() { // Empty means "follow the system", which is what the spec asks for on a first start. - assertEquals("", new ChatPreferences().getGuiOptions_language()); + assertEquals("", new ChatPreferences(tempDir.resolve("preferences.xml")).getGuiOptions_language()); } @Test void aChosenLanguageIsKept() { - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); prefs.setGuiOptions_language("de"); @@ -24,7 +29,7 @@ class ChatPreferencesLanguageTest { @Test void aNullSetterValueBecomesEmpty() { - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); prefs.setGuiOptions_language(null); diff --git a/core/src/test/java/kst4contest/model/ChatPreferencesLocaleTest.java b/core/src/test/java/kst4contest/model/ChatPreferencesLocaleTest.java index 7ae47efb..7e301ded 100644 --- a/core/src/test/java/kst4contest/model/ChatPreferencesLocaleTest.java +++ b/core/src/test/java/kst4contest/model/ChatPreferencesLocaleTest.java @@ -1,6 +1,7 @@ package kst4contest.model; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -10,6 +11,7 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Path; /** * No default that goes to another radio amateur changes with the interface language. @@ -22,11 +24,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; */ class ChatPreferencesLocaleTest { + @TempDir + Path tempDir; + private Map stringFieldsUnder(final Locale locale) throws Exception { Locale previous = Locale.getDefault(); try { Locale.setDefault(locale); - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); Map values = new LinkedHashMap<>(); for (Field field : ChatPreferences.class.getDeclaredFields()) { diff --git a/core/src/test/java/kst4contest/model/ChatPreferencesObservableTest.java b/core/src/test/java/kst4contest/model/ChatPreferencesObservableTest.java index b7ad4b9b..0cba89a7 100644 --- a/core/src/test/java/kst4contest/model/ChatPreferencesObservableTest.java +++ b/core/src/test/java/kst4contest/model/ChatPreferencesObservableTest.java @@ -1,16 +1,21 @@ package kst4contest.model; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Path; class ChatPreferencesObservableTest { + @TempDir + Path tempDir; + @Test void theDefaultsMatchTheFormerJavaFxProperties() { - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); assertEquals(360.0, prefs.getActualQTF().get(), 0.0001, "actualQTF defaulted to 360 as a SimpleDoubleProperty"); @@ -20,7 +25,7 @@ class ChatPreferencesObservableTest { @Test void theQrgValuesStartUnavailable() { - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); assertNull(prefs.getMYQRGFirstCat().get(), "an unset own QRG must stay unavailable rather than becoming a fixed band"); @@ -29,7 +34,7 @@ class ChatPreferencesObservableTest { @Test void theListSettingsStartEmpty() { - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); assertTrue(prefs.getLstNotify_QSOSniffer_sniffedCallSignList().snapshot().isEmpty()); assertTrue(prefs.getLstNotify_QSOSniffer_sniffedPrefixLocList().snapshot().isEmpty()); diff --git a/core/src/test/java/kst4contest/model/ChatPreferencesPaletteTest.java b/core/src/test/java/kst4contest/model/ChatPreferencesPaletteTest.java index 24739277..2ace6345 100644 --- a/core/src/test/java/kst4contest/model/ChatPreferencesPaletteTest.java +++ b/core/src/test/java/kst4contest/model/ChatPreferencesPaletteTest.java @@ -1,8 +1,10 @@ package kst4contest.model; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.nio.file.Path; /** * The two designs' colour overrides are stored independently. @@ -13,9 +15,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; */ class ChatPreferencesPaletteTest { + @TempDir + Path tempDir; + @Test void theTwoDesignsAreStoredIndependently() { - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); prefs.setGuiOptions_paletteOverridesDay("surface=#FFFFFF"); prefs.setGuiOptions_paletteOverridesEvening("surface=#000000"); @@ -27,7 +32,7 @@ class ChatPreferencesPaletteTest { @Test void anUnsetOverrideReadsAsEmptyAndNotAsNull() { // A preferences file from an older release has no such element at all. - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); assertEquals("", prefs.getGuiOptions_paletteOverridesDay()); assertEquals("", prefs.getGuiOptions_paletteOverridesEvening()); @@ -35,7 +40,7 @@ class ChatPreferencesPaletteTest { @Test void aNullSetterValueBecomesEmpty() { - ChatPreferences prefs = new ChatPreferences(); + ChatPreferences prefs = new ChatPreferences(tempDir.resolve("preferences.xml")); prefs.setGuiOptions_paletteOverridesDay(null); prefs.setGuiOptions_paletteOverridesEvening(null); diff --git a/core/src/test/java/kst4contest/test/ChatPreferencesStationMapVisibilityTest.java b/core/src/test/java/kst4contest/test/ChatPreferencesStationMapVisibilityTest.java index 23f82912..d433867b 100644 --- a/core/src/test/java/kst4contest/test/ChatPreferencesStationMapVisibilityTest.java +++ b/core/src/test/java/kst4contest/test/ChatPreferencesStationMapVisibilityTest.java @@ -18,7 +18,8 @@ class ChatPreferencesStationMapVisibilityTest { @Test void pathAnalysisIsVisibleByDefault() { - ChatPreferences preferences = new ChatPreferences(); + ChatPreferences preferences = new ChatPreferences( + temporaryDirectory.resolve("preferences.xml")); assertTrue(preferences.isGUIstationMapPathAnalysisVisible()); } @@ -27,9 +28,7 @@ class ChatPreferencesStationMapVisibilityTest { void hiddenPathAnalysisStateSurvivesXmlRoundTrip() throws IOException { Path preferencesFile = temporaryDirectory.resolve("preferences.xml"); - ChatPreferences writtenPreferences = new ChatPreferences(); - writtenPreferences.setStoreAndRestorePreferencesFileName( - preferencesFile.toString()); + ChatPreferences writtenPreferences = new ChatPreferences(preferencesFile); writtenPreferences.setGUIstationMapPathAnalysisVisible(false); writtenPreferences.writePreferencesToXmlFile(); @@ -38,9 +37,7 @@ class ChatPreferencesStationMapVisibilityTest { "false" + "")); - ChatPreferences restoredPreferences = new ChatPreferences(); - restoredPreferences.setStoreAndRestorePreferencesFileName( - preferencesFile.toString()); + ChatPreferences restoredPreferences = new ChatPreferences(preferencesFile); restoredPreferences.readPreferencesFromXmlFile(); assertFalse(restoredPreferences.isGUIstationMapPathAnalysisVisible()); @@ -63,9 +60,8 @@ class ChatPreferencesStationMapVisibilityTest { """); - ChatPreferences restoredPreferences = new ChatPreferences(); - restoredPreferences.setStoreAndRestorePreferencesFileName( - legacyPreferencesFile.toString()); + ChatPreferences restoredPreferences = + new ChatPreferences(legacyPreferencesFile); restoredPreferences.readPreferencesFromXmlFile(); assertTrue(restoredPreferences.isGUIstationMapPathAnalysisVisible());