From 81b16ad4c5fd5739704fb3138875adf667bf3ec8 Mon Sep 17 00:00:00 2001 From: Philipp Wagner Date: Fri, 16 Oct 2026 10:48:33 +0200 Subject: [PATCH] Keep tests out of the user profile; small socket fixes (#127) * Keep tests out of the user profile The no-argument ChatPreferences constructor seeds preferences.xml in ~/.praktiKST when it is missing, so 21 older test files could create a profile file for whoever ran the build. They now use ChatPreferences(Path) with a @TempDir. None of them reads the file: their assertions are on the field initialisers, which both constructors share, so no assertion changes meaning. ChatPreferencesStationMapVisibilityTest passes its file to the constructor instead of redirecting it with the setter afterwards. The two opt-in ON4KST practice tests keep the no-argument constructor: they deliberately read the operator's real profile for live credentials. As a net, the test tasks run with user.home pointed at build/test-user-home, emptied before each run, and fail if a preferences.xml appears there. This also keeps the sound files, the map tile cache and the Skiko native library that tests unpacked into the real home directory out of it. Co-Authored-By: Claude Opus 5.5 * Make DXClusterThreadPooledServer.serverSocket volatile run() assigns the server socket on the server thread, and stop() closes it from another thread. closeServerSocket() is synchronized, but the write in run() is not, so nothing guaranteed that stop() saw the socket run() had opened; it could find null and leave the socket listening. Co-Authored-By: Claude Opus 5.5 * Drop the ineffective SO_REUSEADDR from the Win-Test sked socket WinTestSkedSender.sendUdp() set reuse address on a socket the constructor had already bound, where it has no effect, so it was never in effect in any shipped version. The socket binds an ephemeral port and only sends, which gains nothing from it, and enabling it for real would change UDP multi-bind semantics on Windows. Removing the call keeps the behaviour exactly as shipped and stops the code from claiming an option it does not have. Co-Authored-By: Claude Opus 5.5 * Check the test user.home in a finalizer task The stray-preferences check ran as a doLast of the test task, which Gradle skips once a test fails, so a failing run could hide the file. It is now a separate task, checkTestUserHome, that every test task is finalized by; finalizers run when the test task fails too. It captures only a File, so it stays configuration-cache safe. Each test task now gets its own scratch home, build/test-user-home/, so a second test task cannot wipe another's. Emptying it checks the result of deleteRecursively(), which reports failure instead of throwing, and stops the task, so a leftover file is never blamed on the current run. The check stays limited to preferences.xml. Other files land in the scratch .praktiKST by design, such as the sound files PlayAudioUtils seeds from the bundled resources; absorbing those is the redirection's job, and a wider check would need an exclusion list that grows with every seeded file. Co-Authored-By: Claude Opus 5.5 * Keep generateStrings configuration-cache safe The argument provider of generateStrings referenced the script-level generatedI18nDirectory, which captures the build script object, so storing the configuration cache failed for every build that included app-desktop. The task now works on a local copy of the provider. Co-Authored-By: Claude Opus 5.5 * Declare the test user.home as a test task output Test tasks are cacheable, and a cache hit skips doFirst, so the scratch home was neither emptied nor rewritten and checkTestUserHome judged whatever an earlier run had left there: a false alarm after a violating run, or a hidden violation when a passing result for violating inputs came from the cache. The scratch home is now a declared output, restored and replaced together with the cached result, so the check always sees the files belonging to the inputs. The doFirst wipe stays for real executions. Skiko unpacked its native library, about 30 MB, into the scratch home, which would now land in every app-desktop cache entry. skiko.data.path points it at build/skiko-data instead, outside the output and kept between runs. user.home and skiko.data.path are passed through an argument provider: as system properties they would be task inputs, and their absolute paths would make the cache key differ between checkouts. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- app-desktop/build.gradle.kts | 7 +- .../compose/DrawingLayerRegressionTest.kt | 6 +- .../view/compose/MainWindowStateTest.kt | 16 +++-- .../view/compose/ObservedValueTest.kt | 16 +++-- .../view/compose/PaletteStoreFactoryTest.kt | 14 ++-- .../view/compose/tabs/AirscoutTabStateTest.kt | 8 ++- .../view/compose/tabs/BeaconTabStateTest.kt | 8 ++- .../compose/tabs/GuiOptionsTabStateTest.kt | 12 ++-- .../view/compose/tabs/LogSynchTabStateTest.kt | 8 ++- .../tabs/MessageHandlingTabStateTest.kt | 12 ++-- .../compose/tabs/NotificationTabStateTest.kt | 8 ++- .../compose/tabs/ShortcutsTabStateTest.kt | 8 ++- .../view/compose/tabs/StationTabStateTest.kt | 10 +-- .../view/compose/tabs/TrxSynchTabStateTest.kt | 8 ++- .../view/i18n/TranslationBoundaryTest.kt | 6 +- build.gradle.kts | 72 +++++++++++++++++++ .../DXClusterThreadPooledServer.java | 3 +- .../controller/WinTestSkedSender.java | 1 - .../controller/On4KstConnectionProbeTest.java | 7 +- .../controller/On4KstProtocolTest.java | 12 +++- .../model/ChatPreferencesLanguageTest.java | 11 ++- .../model/ChatPreferencesLocaleTest.java | 7 +- .../model/ChatPreferencesObservableTest.java | 11 ++- .../model/ChatPreferencesPaletteTest.java | 11 ++- ...atPreferencesStationMapVisibilityTest.java | 16 ++--- 25 files changed, 214 insertions(+), 84 deletions(-) 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());