mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-24 08:06:25 +02:00
fix: address Compose UI regressions
This commit is contained in:
1 parent
936efdd8dd
commit
971fb35109
66 files changed
+4436
-766
No files matched your search
@@ -2901,7 +2901,7 @@ private SimpleRoster<String>
|
||||
|
||||
chatPreferences = new ChatPreferences(preferencesRelativeFileName);
|
||||
chatPreferences.readPreferencesFromXmlFile();
|
||||
Path historyFile = Path.of(ApplicationFileUtils.getFilePath(
|
||||
Path historyFile = Path.of(ApplicationFileUtils.getDebugFilePath(
|
||||
ApplicationConstants.APPLICATION_NAME,
|
||||
MessageHistoryRecorder.CURRENT_FILE_NAME));
|
||||
messageHistoryRecorder = new MessageHistoryRecorder(
|
||||
|
||||
@@ -758,8 +758,15 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
// Built only now: the receiver field has always been read after the sender.
|
||||
InboundChatFrame frame = InboundChatFrame.of(fields);
|
||||
boolean publicMessage = frame.receiverField().equals("0");
|
||||
newMessageArrived.setMessageDirectedToCommunity(publicMessage);
|
||||
newMessageArrived.setMessageDirectedToMe(
|
||||
publicMessage
|
||||
&& isPublicMessageExplicitlyAddressedTo(
|
||||
frame.text(),
|
||||
prefs.getStn_loginCallSign()));
|
||||
|
||||
if (frame.receiverField().equals("0")) {
|
||||
if (publicMessage) {
|
||||
// message is not directed to anyone, move it to the cq messages.
|
||||
newMessageArrived.setReceiver(createAllReceiver());
|
||||
|
||||
@@ -778,6 +785,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
newMessageArrived.getSender().getCallSign(),
|
||||
newMessageArrived.getReceiver().getCallSign(),
|
||||
prefs.getStn_loginCallSign());
|
||||
newMessageArrived.setMessageDirectedToMe(route == ChatRoute.TO_ME);
|
||||
|
||||
DirectionalOpportunity opportunity = null;
|
||||
boolean senderAlreadyInAngleAndRange = false;
|
||||
@@ -1714,6 +1722,33 @@ public class MessageBusManagementThread extends Thread {
|
||||
return ChatRoute.BETWEEN_OTHERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognizes the structured address prefix produced by ON4KST for a public
|
||||
* {@code /cq CALL ...} message.
|
||||
*
|
||||
* <p>The address must be the first non-whitespace token and occupy the complete
|
||||
* parenthesized prefix. A callsign occurring later in ordinary prose is therefore
|
||||
* not treated as an address.</p>
|
||||
*/
|
||||
static boolean isPublicMessageExplicitlyAddressedTo(String messageText, String ownCall) {
|
||||
if (messageText == null || ownCall == null || ownCall.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String text = messageText.stripLeading();
|
||||
if (!text.startsWith("(")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int closingParenthesis = text.indexOf(')');
|
||||
if (closingParenthesis <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String addressedCall = text.substring(1, closingParenthesis).trim();
|
||||
return addressedCall.equalsIgnoreCase(ownCall.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the short tick sound for a new directional opportunity is
|
||||
* played: only with simple sounds enabled and only on the transition into
|
||||
|
||||
@@ -425,6 +425,7 @@ public class ChatPreferences {
|
||||
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
|
||||
private boolean GUIstationMapPathAnalysisVisible = true;
|
||||
private boolean GUIstationMapClusteringEnabled = true;
|
||||
private boolean GUIstationMapAlwaysOnTop = false;
|
||||
private final Map<String, Double> tableColumnWidths = new LinkedHashMap<>();
|
||||
|
||||
private static final String TAG_TABLE_COLUMN_WIDTH = "tableColumnWidth";
|
||||
@@ -803,6 +804,14 @@ public class ChatPreferences {
|
||||
this.GUIstationMapClusteringEnabled = GUIstationMapClusteringEnabled;
|
||||
}
|
||||
|
||||
public boolean isGUIstationMapAlwaysOnTop() {
|
||||
return GUIstationMapAlwaysOnTop;
|
||||
}
|
||||
|
||||
public void setGUIstationMapAlwaysOnTop(boolean GUIstationMapAlwaysOnTop) {
|
||||
this.GUIstationMapAlwaysOnTop = GUIstationMapAlwaysOnTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stored width for one stable table/leaf-column identity.
|
||||
*
|
||||
@@ -2395,6 +2404,12 @@ public class ChatPreferences {
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapClusteringEnabled);
|
||||
|
||||
Element GUIstationMapAlwaysOnTop = doc.createElement("GUIstationMapAlwaysOnTop");
|
||||
GUIstationMapAlwaysOnTop.setTextContent(
|
||||
String.valueOf(this.isGUIstationMapAlwaysOnTop())
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapAlwaysOnTop);
|
||||
|
||||
appendTableColumnWidths(doc, guiOptions);
|
||||
appendWindowPositions(doc, guiOptions);
|
||||
|
||||
@@ -2484,6 +2499,8 @@ public class ChatPreferences {
|
||||
getGUIstationMapStagePositionXY()[0] + ";" + getGUIstationMapStagePositionXY()[1]);
|
||||
upsertDirectChildText(document, guiOptions, "GUIstationMapClusteringEnabled",
|
||||
String.valueOf(isGUIstationMapClusteringEnabled()));
|
||||
upsertDirectChildText(document, guiOptions, "GUIstationMapAlwaysOnTop",
|
||||
String.valueOf(isGUIstationMapAlwaysOnTop()));
|
||||
}
|
||||
|
||||
private void appendTableColumnWidths(Document document, Element guiOptions) {
|
||||
@@ -3223,6 +3240,7 @@ public class ChatPreferences {
|
||||
*
|
||||
***********************************************/
|
||||
this.setGUIstationMapClusteringEnabled(true);
|
||||
this.setGUIstationMapAlwaysOnTop(false);
|
||||
list = doc.getElementsByTagName("guiOptions");
|
||||
if (list.getLength() != 0) {
|
||||
|
||||
@@ -3272,6 +3290,12 @@ public class ChatPreferences {
|
||||
"GUIstationMapClusteringEnabled"
|
||||
));
|
||||
|
||||
this.setGUIstationMapAlwaysOnTop(getBooleanOrDefault(
|
||||
element,
|
||||
false,
|
||||
"GUIstationMapAlwaysOnTop"
|
||||
));
|
||||
|
||||
// Splitpane divider positions
|
||||
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
|
||||
if (s1 != null) {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package kst4contest.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.FileHandler;
|
||||
import java.util.logging.ErrorManager;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
@@ -20,6 +24,10 @@ import java.util.logging.Logger;
|
||||
public final class ApplicationFileLogging implements AutoCloseable {
|
||||
/** Logger namespace owned by the application. */
|
||||
private static final String APP_LOGGER_NAME = "kst4contest";
|
||||
/** Maximum size of the active error log before it is archived. */
|
||||
private static final long DEFAULT_SIZE_LIMIT_BYTES = 20L * 1024L * 1024L;
|
||||
/** Active error log plus four archived generations. */
|
||||
private static final int DEFAULT_FILE_COUNT = 5;
|
||||
|
||||
/** Runtime switch for additional application diagnostics. */
|
||||
private final AtomicBoolean debugEnabled = new AtomicBoolean();
|
||||
@@ -44,6 +52,28 @@ public final class ApplicationFileLogging implements AutoCloseable {
|
||||
final boolean debugEnabled
|
||||
)
|
||||
throws IOException {
|
||||
this(logFile, debugEnabled,
|
||||
DEFAULT_SIZE_LIMIT_BYTES, DEFAULT_FILE_COUNT);
|
||||
}
|
||||
|
||||
/** Test seam for exercising rotation without writing multi-megabyte files. */
|
||||
ApplicationFileLogging(
|
||||
final Path logFile,
|
||||
final boolean debugEnabled,
|
||||
final long sizeLimitBytes,
|
||||
final int fileCount
|
||||
) throws IOException {
|
||||
this(logFile, debugEnabled, sizeLimitBytes, fileCount, ignored -> { });
|
||||
}
|
||||
|
||||
/** Test seam for a transient file-system error after the active stream closes. */
|
||||
ApplicationFileLogging(
|
||||
final Path logFile,
|
||||
final boolean debugEnabled,
|
||||
final long sizeLimitBytes,
|
||||
final int fileCount,
|
||||
final RotationFaultInjector rotationFaultInjector
|
||||
) throws IOException {
|
||||
final Path parent = logFile.toAbsolutePath().getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
@@ -51,15 +81,175 @@ public final class ApplicationFileLogging implements AutoCloseable {
|
||||
|
||||
this.debugEnabled.set(debugEnabled);
|
||||
previousAppLevel = appLogger.getLevel();
|
||||
appLogger.setLevel(Level.FINE);
|
||||
|
||||
final FileHandler fileHandler =
|
||||
new FileHandler(logFile.toString(), true);
|
||||
final Handler fileHandler = new BoundedRotatingFileHandler(
|
||||
logFile, sizeLimitBytes, fileCount, rotationFaultInjector);
|
||||
fileHandler.setLevel(Level.ALL);
|
||||
fileHandler.setFilter(this::isLoggable);
|
||||
fileHandler.setFormatter(new UtcLogFormatter());
|
||||
|
||||
try {
|
||||
appLogger.setLevel(Level.FINE);
|
||||
rootLogger.addHandler(fileHandler);
|
||||
} catch (RuntimeException exception) {
|
||||
fileHandler.close();
|
||||
try {
|
||||
appLogger.setLevel(previousAppLevel);
|
||||
} catch (RuntimeException restoreFailure) {
|
||||
exception.addSuppressed(restoreFailure);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
handler = fileHandler;
|
||||
rootLogger.addHandler(handler);
|
||||
}
|
||||
|
||||
/** Injects a test-only failure at the point where archive operations begin. */
|
||||
@FunctionalInterface
|
||||
interface RotationFaultInjector {
|
||||
void beforeArchive(Path logFile) throws IOException;
|
||||
}
|
||||
|
||||
/** Keeps the active canonical file name while retaining bounded generations. */
|
||||
private static final class BoundedRotatingFileHandler extends Handler {
|
||||
private final Path logFile;
|
||||
private final long sizeLimitBytes;
|
||||
private final int fileCount;
|
||||
private final RotationFaultInjector rotationFaultInjector;
|
||||
private OutputStream output;
|
||||
private long currentSize;
|
||||
|
||||
private BoundedRotatingFileHandler(
|
||||
final Path logFile,
|
||||
final long sizeLimitBytes,
|
||||
final int fileCount,
|
||||
final RotationFaultInjector rotationFaultInjector
|
||||
) throws IOException {
|
||||
if (sizeLimitBytes <= 0L) {
|
||||
throw new IllegalArgumentException("sizeLimitBytes must be positive");
|
||||
}
|
||||
if (fileCount <= 0) {
|
||||
throw new IllegalArgumentException("fileCount must be positive");
|
||||
}
|
||||
this.logFile = logFile;
|
||||
this.sizeLimitBytes = sizeLimitBytes;
|
||||
this.fileCount = fileCount;
|
||||
this.rotationFaultInjector = rotationFaultInjector;
|
||||
open(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void publish(final LogRecord record) {
|
||||
if (!isLoggable(record)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final byte[] encoded = getFormatter().format(record)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
try {
|
||||
if (output == null) {
|
||||
open(true);
|
||||
}
|
||||
if (currentSize > 0L
|
||||
&& currentSize + encoded.length > sizeLimitBytes) {
|
||||
try {
|
||||
rotate();
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
reportError("Could not rotate " + logFile,
|
||||
exception, ErrorManager.WRITE_FAILURE);
|
||||
if (output == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
output.write(encoded);
|
||||
output.flush();
|
||||
currentSize += encoded.length;
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
reportError("Could not write or rotate " + logFile,
|
||||
exception, ErrorManager.WRITE_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void flush() {
|
||||
if (output == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
output.flush();
|
||||
} catch (IOException exception) {
|
||||
reportError("Could not flush " + logFile,
|
||||
exception, ErrorManager.FLUSH_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
closeOutput();
|
||||
}
|
||||
|
||||
private void rotate() throws IOException {
|
||||
closeOutput();
|
||||
try {
|
||||
rotationFaultInjector.beforeArchive(logFile);
|
||||
if (fileCount > 1) {
|
||||
Files.deleteIfExists(generation(fileCount - 1));
|
||||
for (int generation = fileCount - 2;
|
||||
generation >= 1;
|
||||
generation--) {
|
||||
Path source = generation(generation);
|
||||
if (Files.exists(source)) {
|
||||
Files.move(source, generation(generation + 1),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
if (Files.exists(logFile)) {
|
||||
Files.move(logFile, generation(1),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
open(false);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
open(true);
|
||||
} catch (IOException | RuntimeException recoveryFailure) {
|
||||
exception.addSuppressed(recoveryFailure);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private Path generation(final int generation) {
|
||||
return logFile.resolveSibling(
|
||||
logFile.getFileName() + "." + generation);
|
||||
}
|
||||
|
||||
private void open(final boolean append) throws IOException {
|
||||
output = append
|
||||
? Files.newOutputStream(logFile,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE,
|
||||
StandardOpenOption.APPEND)
|
||||
: Files.newOutputStream(logFile,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
currentSize = Files.size(logFile);
|
||||
}
|
||||
|
||||
private void closeOutput() {
|
||||
if (output == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException exception) {
|
||||
reportError("Could not close " + logFile,
|
||||
exception, ErrorManager.CLOSE_FAILURE);
|
||||
} finally {
|
||||
output = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies the runtime debug preference to subsequent file log records. */
|
||||
|
||||
@@ -29,6 +29,21 @@ public class ApplicationFileUtils {
|
||||
fileName).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path of a diagnostic file below the application's debug directory.
|
||||
*
|
||||
* @param applicationName application directory name
|
||||
* @param fileName diagnostic file name
|
||||
* @return full path below {@code .applicationName/debug}
|
||||
*/
|
||||
public static String getDebugFilePath(
|
||||
final String applicationName,
|
||||
final String fileName
|
||||
) {
|
||||
return Path.of(getFilePath(applicationName, "debug"), fileName)
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a resource to the application folder if required.
|
||||
* @param applicationName Name of the application.
|
||||
|
||||
+35
@@ -146,6 +146,41 @@ class MessageBusManagementThreadChatFrameTest {
|
||||
verifyNoInteractions(playAudioUtils);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10)
|
||||
void genuinePrivateMessageCarriesTheAuthoritativeRouteFlags() throws InterruptedException {
|
||||
runThrough(chatFrame("CH", REMOTE_CALL, "pse ur qrg", MY_CALL));
|
||||
|
||||
ChatMessage published = singlePublished();
|
||||
assertEquals(MY_CALL, published.getReceiver().getCallSign());
|
||||
assertTrue(published.isMessageDirectedToMe());
|
||||
assertFalse(published.isMessageDirectedToCommunity());
|
||||
assertEquals(ChatCategory.VUHF, published.getChatCategory().getCategoryNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10)
|
||||
void publicMessageWithLeadingAddressIsMarkedAsExplicitlyAddressedToMe()
|
||||
throws InterruptedException {
|
||||
runThrough(chatFrame("CH", REMOTE_CALL, "(" + MY_CALL + ") pse ur qrg", "0"));
|
||||
|
||||
ChatMessage published = singlePublished();
|
||||
assertEquals("ALL", published.getReceiver().getCallSign());
|
||||
assertTrue(published.isMessageDirectedToCommunity());
|
||||
assertTrue(published.isMessageDirectedToMe());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10)
|
||||
void arbitraryPublicMentionIsNotMarkedAsExplicitlyAddressedToMe()
|
||||
throws InterruptedException {
|
||||
runThrough(chatFrame("CH", REMOTE_CALL, "ask " + MY_CALL + " about the sked", "0"));
|
||||
|
||||
ChatMessage published = singlePublished();
|
||||
assertTrue(published.isMessageDirectedToCommunity());
|
||||
assertFalse(published.isMessageDirectedToMe());
|
||||
}
|
||||
|
||||
// --- sent by me ---------------------------------------------------------
|
||||
|
||||
@Test
|
||||
|
||||
@@ -146,6 +146,25 @@ class MessageHistoryRecorderTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void dedicatedDebugDirectoryIsCreatedWithoutMovingLegacyHistory()
|
||||
throws Exception {
|
||||
Path legacyHistory = historyFile();
|
||||
Files.writeString(legacyHistory, "legacy history\n");
|
||||
Path debugDirectory = temporaryDirectory.resolve("debug");
|
||||
|
||||
try (MessageHistoryRecorder recorder = new MessageHistoryRecorder(
|
||||
debugDirectory,
|
||||
true,
|
||||
Clock.fixed(FIXED_INSTANT, ZoneOffset.UTC))) {
|
||||
recorder.recordRx("OK|");
|
||||
}
|
||||
|
||||
assertEquals("legacy history\n", Files.readString(legacyHistory));
|
||||
assertTrue(Files.isRegularFile(
|
||||
debugDirectory.resolve("Messagehistory.raw")));
|
||||
}
|
||||
|
||||
private MessageHistoryRecorder recorder(boolean enabled) {
|
||||
return new MessageHistoryRecorder(
|
||||
temporaryDirectory,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ChatPreferencesStationMapAlwaysOnTopTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void alwaysOnTopIsDisabledByDefault() {
|
||||
assertFalse(new ChatPreferences(temporaryDirectory.resolve("unused.xml"))
|
||||
.isGUIstationMapAlwaysOnTop());
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledAlwaysOnTopSurvivesFullRoundTrip() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
|
||||
ChatPreferences written = new ChatPreferences(preferencesFile);
|
||||
written.setGUIstationMapAlwaysOnTop(true);
|
||||
|
||||
assertTrue(written.writePreferencesToXmlFile());
|
||||
assertTrue(Files.readString(preferencesFile).contains(
|
||||
"<GUIstationMapAlwaysOnTop>true</GUIstationMapAlwaysOnTop>"));
|
||||
|
||||
ChatPreferences restored = new ChatPreferences(preferencesFile);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
assertTrue(restored.isGUIstationMapAlwaysOnTop());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingOrMalformedValueKeepsAlwaysOnTopDisabled() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("legacy.xml");
|
||||
Files.writeString(preferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<configVersion>8</configVersion>
|
||||
<guiOptions>
|
||||
<GUIstationMapAlwaysOnTop>sometimes</GUIstationMapAlwaysOnTop>
|
||||
</guiOptions>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences restored = new ChatPreferences(preferencesFile);
|
||||
restored.setGUIstationMapAlwaysOnTop(true);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
assertFalse(restored.isGUIstationMapAlwaysOnTop());
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectiveLayoutWritePersistsAlwaysOnTopWithoutFunctionalSettings() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("selective.xml");
|
||||
Files.writeString(preferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<configVersion>8</configVersion>
|
||||
<station><LoginCallSign>SAVED-CALL</LoginCallSign></station>
|
||||
<guiOptions/>
|
||||
</praktiKST>
|
||||
""");
|
||||
ChatPreferences preferences = new ChatPreferences(preferencesFile);
|
||||
assertTrue(preferences.readPreferencesFromXmlFile());
|
||||
preferences.setStn_loginCallSign("UNSAVED-CALL");
|
||||
preferences.setGUIstationMapAlwaysOnTop(true);
|
||||
|
||||
assertTrue(preferences.writeLayoutPreferencesToXmlFile());
|
||||
|
||||
String xml = Files.readString(preferencesFile);
|
||||
assertTrue(xml.contains("<LoginCallSign>SAVED-CALL</LoginCallSign>"));
|
||||
assertFalse(xml.contains("UNSAVED-CALL"));
|
||||
assertTrue(xml.contains("<GUIstationMapAlwaysOnTop>true</GUIstationMapAlwaysOnTop>"));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package kst4contest.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -47,4 +51,87 @@ class ApplicationFileLoggingTest {
|
||||
assertTrue(log.contains("error while enabled"));
|
||||
assertTrue(log.contains("Z\tWARNING\t"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsTheDebugDirectoryOnDemand() throws Exception {
|
||||
Path logFile = temporaryDirectory.resolve("debug/kst4contest-errors.log");
|
||||
Logger logger = Logger.getLogger(
|
||||
"kst4contest.test.filelogging." + UUID.randomUUID());
|
||||
|
||||
try (ApplicationFileLogging logging =
|
||||
new ApplicationFileLogging(logFile, false)) {
|
||||
logger.warning("directory test");
|
||||
}
|
||||
|
||||
assertTrue(Files.isRegularFile(logFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rotatesAtTheConfiguredLimitAndKeepsFiveFiles() throws Exception {
|
||||
Path logFile = temporaryDirectory.resolve("debug/kst4contest-errors.log");
|
||||
Logger logger = Logger.getLogger(
|
||||
"kst4contest.test.filelogging." + UUID.randomUUID());
|
||||
|
||||
try (ApplicationFileLogging logging =
|
||||
new ApplicationFileLogging(logFile, true, 320L, 5)) {
|
||||
for (int index = 0; index < 40; index++) {
|
||||
logger.info("rotation entry " + index + " " + "x".repeat(120));
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(Files.isRegularFile(logFile));
|
||||
for (int generation = 1; generation < 5; generation++) {
|
||||
assertTrue(Files.isRegularFile(
|
||||
logFile.resolveSibling(logFile.getFileName() + "." + generation)));
|
||||
}
|
||||
try (var files = Files.list(logFile.getParent())) {
|
||||
assertEquals(5L, files.filter(path -> path.getFileName().toString()
|
||||
.startsWith("kst4contest-errors.log")).count());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void anUnusableLogFileIsReportedWithoutChangingTheLoggerLevel() throws Exception {
|
||||
Path logFile = temporaryDirectory.resolve("debug/kst4contest-errors.log");
|
||||
Files.createDirectories(logFile);
|
||||
|
||||
Logger appLogger = Logger.getLogger("kst4contest");
|
||||
Level previousLevel = appLogger.getLevel();
|
||||
appLogger.setLevel(Level.SEVERE);
|
||||
|
||||
try {
|
||||
assertThrows(IOException.class, () -> new ApplicationFileLogging(
|
||||
logFile, false));
|
||||
assertEquals(Level.SEVERE, appLogger.getLevel());
|
||||
} finally {
|
||||
appLogger.setLevel(previousLevel);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void continuesWritingAfterATransientRotationFailure() throws Exception {
|
||||
Path logFile = temporaryDirectory.resolve("debug/kst4contest-errors.log");
|
||||
Logger logger = Logger.getLogger(
|
||||
"kst4contest.test.filelogging." + UUID.randomUUID());
|
||||
AtomicBoolean failNextRotation = new AtomicBoolean(true);
|
||||
|
||||
try (ApplicationFileLogging logging = new ApplicationFileLogging(
|
||||
logFile, true, 320L, 5,
|
||||
ignored -> {
|
||||
if (failNextRotation.getAndSet(false)) {
|
||||
throw new IOException("simulated transient rotation failure");
|
||||
}
|
||||
})) {
|
||||
logger.info("before rotation " + "x".repeat(180));
|
||||
logger.info("written despite failed rotation " + "x".repeat(180));
|
||||
logger.info("written after rotation recovery " + "x".repeat(180));
|
||||
}
|
||||
|
||||
Path firstGeneration = logFile.resolveSibling(
|
||||
logFile.getFileName() + ".1");
|
||||
assertTrue(Files.readString(firstGeneration)
|
||||
.contains("written despite failed rotation"));
|
||||
assertTrue(Files.readString(logFile)
|
||||
.contains("written after rotation recovery"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package kst4contest.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ApplicationFileUtilsTest {
|
||||
|
||||
@Test
|
||||
void debugFilesResolveBelowTheDedicatedApplicationDirectory() {
|
||||
assertEquals(
|
||||
Path.of(ApplicationFileUtils.getFilePath("praktiKST", "debug"),
|
||||
"kst4contest-errors.log").toString(),
|
||||
ApplicationFileUtils.getDebugFilePath(
|
||||
"praktiKST", "kst4contest-errors.log"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user