test: expand service coverage and modernize test infrastructure

- replace legacy mockito-all with mockito-junit-jupiter
- add tests for AppState, MQTT service, process manager, and animation file service
- rewrite service tests for stronger edge-case coverage
- refactor services for testability (MQTT client/process launcher/output path injection)
- fix duplicate payload ID handling in BinaryEventService
- update README and docs testing/source-map sections
This commit is contained in:
2026-03-06 14:16:40 +01:00
parent 4eca8d6e91
commit 9fed2cd420
17 changed files with 956 additions and 237 deletions
@@ -6,15 +6,29 @@ import vassistent.util.Logger;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.websocket.*;
/**
* Writes the current problem level as animation state JSON for Unreal integration.
*/
public class AnimationFileService {
private static final String PATH = "C:\\Users\\Student\\Documents\\Dannick\\Prototyp1\\Saved\\animation.json";
private final String outputPath;
/**
* Creates the animation file service with default output path.
*/
public AnimationFileService() {
this(PATH);
}
/**
* Creates the animation file service with an explicit output path.
*
* @param outputPath target JSON file path
*/
AnimationFileService(String outputPath) {
this.outputPath = outputPath;
}
/**
* Writes the mapped animation state for the given level to the configured JSON file.
@@ -25,10 +39,15 @@ public class AnimationFileService {
String animation = mapLevelToAnimation(level);
File file = new File(PATH);
File file = new File(outputPath);
try {
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
String json = """
{
"animation": "%s"
@@ -52,7 +52,7 @@ public class BinaryEventService {
if (lastId != null && lastId == id) {
Logger.warn("EVENT", "Payload ID bereits verarbeitet: " + id);
//return;
return;
}
lastId = id;
@@ -30,29 +30,55 @@ public class MqttClientService implements MqttCallback {
* @param appState shared state updated with connection status
*/
public MqttClientService(AppState appState) {
this(appState, null, true);
}
/**
* Creates an MQTT service with an optionally injected client (primarily for tests).
*
* @param appState shared state updated with connection status
* @param injectedClient externally provided MQTT client, or {@code null} to create a default client
* @param connectOnStart whether the service should connect immediately
*/
MqttClientService(AppState appState, MqttClient injectedClient, boolean connectOnStart) {
this.appState = appState;
try {
client = new MqttClient(
client = injectedClient != null
? injectedClient
: new MqttClient(
BROKER_URL,
CLIENT_ID,
new MemoryPersistence()
);
client.setCallback(this);
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setAutomaticReconnect(true);
Logger.info("MQTT", "Verbinde mit Broker " + BROKER_URL);
client.connect(options);
appState.setMqttConnected(true);
Logger.info("MQTT", "Verbindung hergestellt");
if (connectOnStart) {
connectClient();
}
} catch (MqttException e) {
Logger.error("MQTT", "Fehler beim Verbinden", e);
appState.setMqttConnected(false);
}
}
/**
* Connects the configured MQTT client with default options.
*
* @throws MqttException if connection fails
*/
private void connectClient() throws MqttException {
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setAutomaticReconnect(true);
Logger.info("MQTT", "Verbinde mit Broker " + BROKER_URL);
client.connect(options);
appState.setMqttConnected(true);
Logger.info("MQTT", "Verbindung hergestellt");
}
/**
* Disconnects the MQTT client when connected.
*/
@@ -6,6 +6,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Properties;
/**
@@ -13,7 +14,21 @@ import java.util.Properties;
*/
public class ProcessManagerService {
@FunctionalInterface
interface ProcessLauncher {
Process launch(ProcessBuilder processBuilder) throws IOException;
}
private static final String DEFAULT_UNREAL_PID_FILE =
"C:\\Users\\Student\\Documents\\Dannick\\avatar\\unreal.pid";
private static final String DEFAULT_SIGNALLING_PID_FILE =
"C:\\Users\\Student\\Documents\\Dannick\\avatar\\signalling.pid";
private final Properties config;
private final ProcessLauncher processLauncher;
private final String unrealPidFile;
private final String signallingPidFile;
private Process pythonProcess;
private Process unrealProcess;
@@ -25,7 +40,32 @@ public class ProcessManagerService {
* @param config loaded application properties
*/
public ProcessManagerService(Properties config) {
this.config = config;
this(
config,
ProcessBuilder::start,
DEFAULT_UNREAL_PID_FILE,
DEFAULT_SIGNALLING_PID_FILE
);
}
/**
* Creates a process manager with injectable process launcher and PID file paths.
*
* @param config loaded application properties
* @param processLauncher strategy used to launch process builders
* @param unrealPidFile PID file path for Unreal process cleanup
* @param signallingPidFile PID file path for signalling process cleanup
*/
ProcessManagerService(
Properties config,
ProcessLauncher processLauncher,
String unrealPidFile,
String signallingPidFile
) {
this.config = Objects.requireNonNull(config);
this.processLauncher = Objects.requireNonNull(processLauncher);
this.unrealPidFile = Objects.requireNonNull(unrealPidFile);
this.signallingPidFile = Objects.requireNonNull(signallingPidFile);
}
/**
@@ -55,7 +95,7 @@ public class ProcessManagerService {
ProcessBuilder pb = new ProcessBuilder(python, script);
pb.redirectErrorStream(true);
pythonProcess = pb.start();
pythonProcess = processLauncher.launch(pb);
Logger.info("PROCESS", "Mqtt Simulator gestartet");
} catch (IOException e) {
@@ -123,7 +163,7 @@ public class ProcessManagerService {
unrealPsScript
);
unrealProcess = pb.start();
unrealProcess = processLauncher.launch(pb);
//pb.directory(new File(exe).getParentFile());
@@ -144,13 +184,8 @@ public class ProcessManagerService {
terminateProcess(unrealProcess);
terminateProcess(unrealSignallingProcess);
killProcessFromPidFile(
"C:\\Users\\Student\\Documents\\Dannick\\avatar\\unreal.pid"
);
killProcessFromPidFile(
"C:\\Users\\Student\\Documents\\Dannick\\avatar\\signalling.pid"
);
killProcessFromPidFile(unrealPidFile);
killProcessFromPidFile(signallingPidFile);
Logger.info("PROCESS", "Externe Prozesse beendet");
}
@@ -177,7 +212,7 @@ public class ProcessManagerService {
"/F"
);
pb.start().waitFor();
processLauncher.launch(pb).waitFor();
Logger.info("PROCESS",
"Process Tree beendet → PID " + pid);
@@ -209,10 +244,37 @@ public class ProcessManagerService {
"/F"
);
pb.start().waitFor();
processLauncher.launch(pb).waitFor();
} catch (Exception e) {
Logger.error("PROCESS", "PID Kill fehlgeschlagen", e);
}
}
/**
* Returns the currently tracked Python simulator process.
*
* @return Python process or {@code null}
*/
Process getPythonProcess() {
return pythonProcess;
}
/**
* Returns the currently tracked Unreal process.
*
* @return Unreal process or {@code null}
*/
Process getUnrealProcess() {
return unrealProcess;
}
/**
* Returns the currently tracked Unreal signalling process.
*
* @return signalling process or {@code null}
*/
Process getUnrealSignallingProcess() {
return unrealSignallingProcess;
}
}