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:
parent
4eca8d6e91
commit
9fed2cd420
@ -28,15 +28,7 @@ Impact:
|
||||
|
||||
- log output may indicate startup intent, but process is not actually launched via that path
|
||||
|
||||
## 4. Legacy Mockito Dependency
|
||||
|
||||
Mockito-based tests fail on Java 17 due to old `mockito-all:2.0.2-beta`.
|
||||
|
||||
Impact:
|
||||
|
||||
- full CI-style green test run is blocked until dependency modernization
|
||||
|
||||
## 5. Encoding Artifacts in Logs/Strings
|
||||
## 4. Encoding Artifacts in Logs/Strings
|
||||
|
||||
Some source/log text contains mojibake characters in comments/messages.
|
||||
|
||||
|
||||
@ -51,9 +51,16 @@ Package-level map of all Java source files in `src/main/java` and tests in `src/
|
||||
|
||||
## Test Sources
|
||||
|
||||
### `vassistent.model`
|
||||
|
||||
- `AppStateTest.java`
|
||||
|
||||
### `vassistent.service`
|
||||
|
||||
- `AnimationFileServiceTest.java`
|
||||
- `BinaryEventServiceTest.java`
|
||||
- `DataPersistenceServiceTest.java`
|
||||
- `EvaluationServiceTest.java`
|
||||
- `MqttClientServiceTest.java`
|
||||
- `ProcessManagerServiceTest.java`
|
||||
- `StatisticsServiceTest.java`
|
||||
|
||||
@ -8,32 +8,36 @@ mvn test
|
||||
|
||||
## Current Test Suite
|
||||
|
||||
Located in `src/test/java/vassistent/service`:
|
||||
Located in:
|
||||
|
||||
- `BinaryEventServiceTest`
|
||||
- `DataPersistenceServiceTest`
|
||||
- `EvaluationServiceTest`
|
||||
- `StatisticsServiceTest`
|
||||
- `src/test/java/vassistent/model/AppStateTest`
|
||||
- `src/test/java/vassistent/service/AnimationFileServiceTest`
|
||||
- `src/test/java/vassistent/service/BinaryEventServiceTest`
|
||||
- `src/test/java/vassistent/service/DataPersistenceServiceTest`
|
||||
- `src/test/java/vassistent/service/EvaluationServiceTest`
|
||||
- `src/test/java/vassistent/service/MqttClientServiceTest`
|
||||
- `src/test/java/vassistent/service/ProcessManagerServiceTest`
|
||||
- `src/test/java/vassistent/service/StatisticsServiceTest`
|
||||
|
||||
Coverage focus:
|
||||
|
||||
- state change notifications
|
||||
- payload validation and processing flow
|
||||
- level evaluation thresholds
|
||||
- MQTT lifecycle and topic routing
|
||||
- process launch/shutdown command sequencing
|
||||
- animation file output mapping
|
||||
- SQLite persistence behavior
|
||||
- rolling averages and ratio calculations
|
||||
|
||||
## Current Status (Java 17)
|
||||
|
||||
The suite does not fully pass on Java 17 because of legacy Mockito dependency:
|
||||
- full suite passes with `mvn test`
|
||||
- test stack:
|
||||
- `org.junit.jupiter:junit-jupiter`
|
||||
- `org.mockito:mockito-junit-jupiter`
|
||||
|
||||
- project uses `mockito-all:2.0.2-beta`
|
||||
- this version relies on CGLIB behavior that conflicts with modern Java module access
|
||||
- result: Mockito-based tests fail at initialization (`InaccessibleObjectException`)
|
||||
## Notes
|
||||
|
||||
Tests not depending on Mockito are expected to run successfully.
|
||||
|
||||
## Recommended Fix
|
||||
|
||||
Replace legacy dependency with modern Mockito stack, for example:
|
||||
|
||||
- `org.mockito:mockito-core` (or `mockito-junit-jupiter`)
|
||||
- ensure JUnit Jupiter engine is configured by Surefire
|
||||
|
||||
After dependency update, rerun:
|
||||
|
||||
```powershell
|
||||
mvn test
|
||||
```
|
||||
- some tests are integration-style and use temporary SQLite databases
|
||||
- test output may include logger lines because services log during execution
|
||||
|
||||
21
pom.xml
21
pom.xml
@ -26,14 +26,14 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.10.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-all</artifactId>
|
||||
<version>2.0.2-beta</version>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>5.12.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -59,4 +59,17 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<configuration>
|
||||
<useModulePath>false</useModulePath>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@ -127,8 +127,9 @@ mvn test
|
||||
|
||||
Current state in this repository on Java 17:
|
||||
|
||||
- `DataPersistenceServiceTest` and `StatisticsServiceTest` execute.
|
||||
- Mockito-based tests fail because dependency is `mockito-all:2.0.2-beta` (legacy CGLIB + Java module access issue on modern JDKs).
|
||||
- unit and integration tests are available for state, event processing, evaluation, persistence, and statistics
|
||||
- test stack uses JUnit 5 + Mockito JUnit Jupiter
|
||||
- full suite runs successfully with `mvn test`
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
82
src/test/java/vassistent/model/AppStateTest.java
Normal file
82
src/test/java/vassistent/model/AppStateTest.java
Normal file
@ -0,0 +1,82 @@
|
||||
package vassistent.model;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AppState}.
|
||||
*/
|
||||
class AppStateTest {
|
||||
|
||||
private AppState appState;
|
||||
|
||||
/**
|
||||
* Creates a fresh app state instance for each test.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
appState = new AppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies problem level defaults to NONE.
|
||||
*/
|
||||
@Test
|
||||
void defaultProblemLevel_isNone() {
|
||||
assertEquals(ProblemLevel.NONE, appState.getProblemLevel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies MQTT status defaults to disconnected.
|
||||
*/
|
||||
@Test
|
||||
void defaultMqttState_isDisconnected() {
|
||||
assertFalse(appState.isMqttConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies listeners are notified when problem level changes.
|
||||
*/
|
||||
@Test
|
||||
void setProblemLevel_notifiesListeners() {
|
||||
AtomicInteger notifications = new AtomicInteger(0);
|
||||
appState.addListener(state -> notifications.incrementAndGet());
|
||||
|
||||
appState.setProblemLevel(ProblemLevel.HIGH);
|
||||
|
||||
assertEquals(1, notifications.get());
|
||||
assertEquals(ProblemLevel.HIGH, appState.getProblemLevel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies MQTT listener notifications happen only when value changes.
|
||||
*/
|
||||
@Test
|
||||
void setMqttConnected_notifiesOnlyOnChange() {
|
||||
AtomicInteger notifications = new AtomicInteger(0);
|
||||
appState.addListener(state -> notifications.incrementAndGet());
|
||||
|
||||
appState.setMqttConnected(true);
|
||||
appState.setMqttConnected(true);
|
||||
appState.setMqttConnected(false);
|
||||
|
||||
assertEquals(2, notifications.get());
|
||||
assertFalse(appState.isMqttConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies setting MQTT state to true updates the state value.
|
||||
*/
|
||||
@Test
|
||||
void setMqttConnected_updatesValue() {
|
||||
appState.setMqttConnected(true);
|
||||
|
||||
assertTrue(appState.isMqttConnected());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package vassistent.service;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import vassistent.model.ProblemLevel;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link AnimationFileService}.
|
||||
*/
|
||||
class AnimationFileServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
/**
|
||||
* Verifies level-to-animation mapping is correctly serialized into JSON output.
|
||||
*
|
||||
* @param level problem level name
|
||||
* @param expectedAnimation expected serialized animation value
|
||||
* @throws Exception if file access fails
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"NONE, no",
|
||||
"WARNING, warning",
|
||||
"HIGH, high",
|
||||
"DISASTER, disaster"
|
||||
})
|
||||
void wirteAnimationState_writesMappedAnimation(String level, String expectedAnimation) throws Exception {
|
||||
Path outputFile = tempDir.resolve("animation.json");
|
||||
AnimationFileService service = new AnimationFileService(outputFile.toString());
|
||||
|
||||
service.wirteAnimationState(ProblemLevel.valueOf(level));
|
||||
|
||||
assertTrue(Files.exists(outputFile));
|
||||
String content = Files.readString(outputFile);
|
||||
assertTrue(content.contains("\"animation\": \"" + expectedAnimation + "\""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies parent directories are created automatically when missing.
|
||||
*
|
||||
* @throws Exception if file access fails
|
||||
*/
|
||||
@Test
|
||||
void wirteAnimationState_createsParentDirectory() throws Exception {
|
||||
Path nestedFile = tempDir.resolve("nested/path/animation.json");
|
||||
AnimationFileService service = new AnimationFileService(nestedFile.toString());
|
||||
|
||||
service.wirteAnimationState(ProblemLevel.WARNING);
|
||||
|
||||
assertTrue(Files.exists(nestedFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies subsequent writes overwrite prior animation state.
|
||||
*
|
||||
* @throws Exception if file access fails
|
||||
*/
|
||||
@Test
|
||||
void wirteAnimationState_overwritesPreviousContent() throws Exception {
|
||||
Path outputFile = tempDir.resolve("animation.json");
|
||||
AnimationFileService service = new AnimationFileService(outputFile.toString());
|
||||
|
||||
service.wirteAnimationState(ProblemLevel.NONE);
|
||||
service.wirteAnimationState(ProblemLevel.DISASTER);
|
||||
|
||||
String content = Files.readString(outputFile);
|
||||
assertTrue(content.contains("\"animation\": \"disaster\""));
|
||||
assertTrue(!content.contains("\"animation\": \"no\""));
|
||||
}
|
||||
}
|
||||
@ -1,67 +1,116 @@
|
||||
package vassistent.service;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for payload validation and processing behavior in {@link BinaryEventService}.
|
||||
* Unit tests for {@link BinaryEventService}.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BinaryEventServiceTest {
|
||||
|
||||
@Mock
|
||||
private DataPersistenceService persistenceService;
|
||||
|
||||
@Mock
|
||||
private EvaluationService evaluationService;
|
||||
|
||||
private BinaryEventService binaryEventService;
|
||||
|
||||
/**
|
||||
* Creates mocked dependencies and the service under test.
|
||||
* Creates the service under test with mocked dependencies.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
persistenceService = mock(DataPersistenceService.class);
|
||||
evaluationService = mock(EvaluationService.class);
|
||||
binaryEventService = new BinaryEventService(persistenceService, evaluationService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears references after each test run.
|
||||
*/
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
persistenceService = null;
|
||||
evaluationService = null;
|
||||
binaryEventService = null;
|
||||
binaryEventService = new BinaryEventService(
|
||||
persistenceService,
|
||||
evaluationService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a valid payload triggers persistence and evaluation.
|
||||
*/
|
||||
@Test
|
||||
void handlePayloadValid() {
|
||||
binaryEventService.handlePayload("1");
|
||||
verify(persistenceService).store(anyInt());
|
||||
void handlePayload_validPayload_persistsAndEvaluates() {
|
||||
String payload = "{\"valid\":true,\"_id\":1,\"prediction\":1}";
|
||||
|
||||
binaryEventService.handlePayload(payload);
|
||||
|
||||
verify(persistenceService).store(1);
|
||||
verify(evaluationService).evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an invalid numeric payload value is ignored.
|
||||
* Verifies that an invalid "valid" flag prevents processing.
|
||||
*/
|
||||
@Test
|
||||
void handlePayloadInvalidValue() {
|
||||
binaryEventService.handlePayload("5");
|
||||
verify(persistenceService, never()).store(anyInt());
|
||||
void handlePayload_invalidFlag_doesNotProcess() {
|
||||
String payload = "{\"valid\":false,\"_id\":1,\"prediction\":1}";
|
||||
|
||||
binaryEventService.handlePayload(payload);
|
||||
|
||||
verify(persistenceService, never()).store(org.mockito.ArgumentMatchers.anyInt());
|
||||
verify(evaluationService, never()).evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a non-numeric payload does not trigger downstream actions.
|
||||
* Verifies that payloads missing required fields are ignored.
|
||||
*/
|
||||
@Test
|
||||
void handlePayloadNonNumeric() {
|
||||
binaryEventService.handlePayload("abc");
|
||||
verify(persistenceService, never()).store(anyInt());
|
||||
void handlePayload_missingPrediction_doesNotProcess() {
|
||||
String payload = "{\"valid\":true,\"_id\":1}";
|
||||
|
||||
binaryEventService.handlePayload(payload);
|
||||
|
||||
verify(persistenceService, never()).store(org.mockito.ArgumentMatchers.anyInt());
|
||||
verify(evaluationService, never()).evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that predictions outside the binary domain are ignored.
|
||||
*/
|
||||
@Test
|
||||
void handlePayload_invalidPrediction_doesNotProcess() {
|
||||
String payload = "{\"valid\":true,\"_id\":1,\"prediction\":3}";
|
||||
|
||||
binaryEventService.handlePayload(payload);
|
||||
|
||||
verify(persistenceService, never()).store(org.mockito.ArgumentMatchers.anyInt());
|
||||
verify(evaluationService, never()).evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that malformed JSON payloads do not trigger downstream processing.
|
||||
*/
|
||||
@Test
|
||||
void handlePayload_malformedJson_doesNotProcess() {
|
||||
String payload = "not-json";
|
||||
|
||||
binaryEventService.handlePayload(payload);
|
||||
|
||||
verify(persistenceService, never()).store(org.mockito.ArgumentMatchers.anyInt());
|
||||
verify(evaluationService, never()).evaluate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies current behavior for duplicate IDs.
|
||||
*/
|
||||
@Test
|
||||
void handlePayload_duplicateId_isIgnoredAfterFirstMessage() {
|
||||
String payload = "{\"valid\":true,\"_id\":7,\"prediction\":0}";
|
||||
|
||||
binaryEventService.handlePayload(payload);
|
||||
binaryEventService.handlePayload(payload);
|
||||
|
||||
verify(persistenceService, times(1)).store(0);
|
||||
verify(evaluationService, times(1)).evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,89 +1,88 @@
|
||||
package vassistent.service;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import vassistent.model.DatabaseEntry;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests basic database initialization and persistence operations.
|
||||
* Integration tests for {@link DataPersistenceService} using an isolated temp SQLite database.
|
||||
*/
|
||||
class DataPersistenceServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private DataPersistenceService service;
|
||||
private static final String TEST_DB = "jdbc:sqlite:data/test_health.db";
|
||||
private static final String DB_FILE = "data/test_health.db";
|
||||
private String dbUrl;
|
||||
|
||||
/**
|
||||
* Initializes a persistence service backed by a test database.
|
||||
* Creates an isolated test database for each test.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
service = new DataPersistenceService(TEST_DB);
|
||||
void setUp() {
|
||||
dbUrl = "jdbc:sqlite:" + tempDir.resolve("health-test.db").toAbsolutePath();
|
||||
service = new DataPersistenceService(dbUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes test database artifacts after each test.
|
||||
* Verifies the configured JDBC URL is exposed unchanged.
|
||||
*/
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
deleteTestDatabase();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the test database file when present.
|
||||
*/
|
||||
private void deleteTestDatabase() {
|
||||
File db = new File(DB_FILE);
|
||||
if (db.exists()) {
|
||||
assertTrue(db.delete(), "Testdatenbank konnte nicht gelöscht werden");
|
||||
}
|
||||
@Test
|
||||
void getDbUrl_returnsConfiguredUrl() {
|
||||
assertEquals(dbUrl, service.getDbUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the database folder exists after service initialization.
|
||||
*/
|
||||
@org.junit.jupiter.api.Test
|
||||
void createDatabaseFolder() {
|
||||
File folder = new File("data");
|
||||
|
||||
assertTrue(folder.exists(), "Datenbankordner sollte existieren");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that storing a value inserts at least one row.
|
||||
* Verifies that stored entries can be read back in chronological order.
|
||||
*
|
||||
* @throws Exception if direct JDBC validation fails
|
||||
* @throws InterruptedException if the sleep used to separate timestamps is interrupted
|
||||
*/
|
||||
@org.junit.jupiter.api.Test
|
||||
void store() throws Exception {
|
||||
@Test
|
||||
void store_andGetLastEntries_returnsChronologicalEntries() throws InterruptedException {
|
||||
service.store(1);
|
||||
Thread.sleep(5);
|
||||
service.store(0);
|
||||
|
||||
List<DatabaseEntry> entries = service.getLastEntries(10);
|
||||
|
||||
assertEquals(2, entries.size());
|
||||
assertEquals(1.0, entries.get(0).getValue());
|
||||
assertEquals(0.0, entries.get(1).getValue());
|
||||
assertNotNull(entries.get(0).getTimestamp());
|
||||
assertNotNull(entries.get(1).getTimestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that entry retrieval respects the requested limit.
|
||||
*/
|
||||
@Test
|
||||
void getLastEntries_respectsLimit() throws InterruptedException {
|
||||
service.store(1);
|
||||
Thread.sleep(5);
|
||||
service.store(0);
|
||||
Thread.sleep(5);
|
||||
service.store(1);
|
||||
|
||||
try (Connection c = DriverManager.getConnection(TEST_DB);
|
||||
Statement s = c.createStatement();
|
||||
ResultSet rs = s.executeQuery(
|
||||
"SELECT COUNT(*) FROM binary_event")) {
|
||||
List<DatabaseEntry> entries = service.getLastEntries(2);
|
||||
|
||||
assertTrue(rs.next());
|
||||
int count = rs.getInt(1);
|
||||
|
||||
assertTrue(count > 0,
|
||||
"Es sollte mindestens ein Eintrag existieren");
|
||||
}
|
||||
assertEquals(2, entries.size());
|
||||
assertFalse(entries.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the service returns its configured JDBC URL.
|
||||
* Verifies that initializing the service ensures the data folder exists.
|
||||
*/
|
||||
@org.junit.jupiter.api.Test
|
||||
void getDbUrl() {
|
||||
assertEquals(TEST_DB, service.getDbUrl());
|
||||
@Test
|
||||
void constructor_createsDataFolderIfMissing() {
|
||||
assertTrue(java.nio.file.Files.exists(Path.of("data")));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,64 +1,83 @@
|
||||
package vassistent.service;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import vassistent.model.AppState;
|
||||
import vassistent.model.ProblemLevel;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests mapping of computed ratios to {@link ProblemLevel} values.
|
||||
* Unit tests for {@link EvaluationService}.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EvaluationServiceTest {
|
||||
|
||||
@Mock
|
||||
private StatisticsService statisticsService;
|
||||
|
||||
@Mock
|
||||
private AnimationFileService animationFileService;
|
||||
|
||||
private AppState appState;
|
||||
private AnimationFileService unrealService;
|
||||
private EvaluationService evaluationService;
|
||||
|
||||
/**
|
||||
* Creates mocks and initializes the evaluation service under test.
|
||||
* Creates the service under test with mocked dependencies.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
statisticsService = mock(StatisticsService.class);
|
||||
appState = new AppState();
|
||||
unrealService = mock(AnimationFileService.class);
|
||||
evaluationService = new EvaluationService(statisticsService, appState, unrealService);
|
||||
evaluationService = new EvaluationService(
|
||||
statisticsService,
|
||||
appState,
|
||||
animationFileService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears test references.
|
||||
* Verifies threshold mapping and animation side-effect for multiple ratio values.
|
||||
*
|
||||
* @param ratio ratio returned by statistics service
|
||||
* @param expectedLevel expected mapped problem level name
|
||||
*/
|
||||
@org.junit.jupiter.api.Test
|
||||
void tearDown() {
|
||||
statisticsService = null;
|
||||
appState = null;
|
||||
unrealService = null;
|
||||
evaluationService = null;
|
||||
}
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"0.00, NONE",
|
||||
"0.49, NONE",
|
||||
"0.50, WARNING",
|
||||
"0.79, WARNING",
|
||||
"0.80, HIGH",
|
||||
"0.89, HIGH",
|
||||
"0.90, DISASTER",
|
||||
"1.00, DISASTER"
|
||||
})
|
||||
void evaluate_mapsThresholdsAndWritesAnimation(double ratio, String expectedLevel) {
|
||||
ProblemLevel level = ProblemLevel.valueOf(expectedLevel);
|
||||
when(statisticsService.getRatio(20)).thenReturn(ratio);
|
||||
|
||||
/**
|
||||
* Verifies that high ratios map to {@link ProblemLevel#DISASTER}.
|
||||
*/
|
||||
@org.junit.jupiter.api.Test
|
||||
void testEvaluateDisasterLevel() {
|
||||
when(statisticsService.getRatio(anyInt())).thenReturn(0.95);
|
||||
evaluationService.evaluate();
|
||||
Assertions.assertEquals(ProblemLevel.DISASTER, appState.getProblemLevel());
|
||||
//verify(unrealService).speak("DISASTER");
|
||||
|
||||
assertEquals(level, appState.getProblemLevel());
|
||||
verify(animationFileService).wirteAnimationState(level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that low ratios keep the state at {@link ProblemLevel#NONE}.
|
||||
* Verifies that evaluation always queries the ratio with a fixed window size of 20.
|
||||
*/
|
||||
@Test
|
||||
void testEvaluateNoChange() {
|
||||
appState.setProblemLevel(ProblemLevel.NONE);
|
||||
when(statisticsService.getRatio(anyInt())).thenReturn(0.1);
|
||||
void evaluate_usesFixedRatioWindow() {
|
||||
when(statisticsService.getRatio(20)).thenReturn(0.1);
|
||||
|
||||
evaluationService.evaluate();
|
||||
//verify(unrealService, never()).speak(anyString());
|
||||
|
||||
verify(statisticsService).getRatio(20);
|
||||
}
|
||||
}
|
||||
|
||||
155
src/test/java/vassistent/service/MqttClientServiceTest.java
Normal file
155
src/test/java/vassistent/service/MqttClientServiceTest.java
Normal file
@ -0,0 +1,155 @@
|
||||
package vassistent.service;
|
||||
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import vassistent.model.AppState;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MqttClientService}.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MqttClientServiceTest {
|
||||
|
||||
@Mock
|
||||
private MqttClient mqttClient;
|
||||
|
||||
private AppState appState;
|
||||
|
||||
/**
|
||||
* Creates a fresh app state per test.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
appState = new AppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies constructor behavior when auto-connect is enabled.
|
||||
*
|
||||
* @throws Exception if mock interaction setup fails
|
||||
*/
|
||||
@Test
|
||||
void constructor_withConnectOnStart_connectsAndSetsState() throws Exception {
|
||||
MqttClientService service = new MqttClientService(appState, mqttClient, true);
|
||||
|
||||
verify(mqttClient).setCallback(service);
|
||||
verify(mqttClient).connect(any());
|
||||
assertTrue(appState.isMqttConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies constructor handles connection failures without throwing.
|
||||
*
|
||||
* @throws Exception if mock interaction setup fails
|
||||
*/
|
||||
@Test
|
||||
void constructor_connectFailure_keepsStateDisconnected() throws Exception {
|
||||
doThrow(new MqttException(0)).when(mqttClient).connect(any());
|
||||
|
||||
MqttClientService service = new MqttClientService(appState, mqttClient, true);
|
||||
|
||||
verify(mqttClient).setCallback(service);
|
||||
verify(mqttClient).connect(any());
|
||||
assertFalse(appState.isMqttConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies subscribe registers the listener and routes arriving payloads.
|
||||
*
|
||||
* @throws Exception if callback invocation fails
|
||||
*/
|
||||
@Test
|
||||
void subscribe_thenMessageArrived_routesPayloadToListener() throws Exception {
|
||||
MqttClientService service = new MqttClientService(appState, mqttClient, false);
|
||||
List<String> payloads = new ArrayList<>();
|
||||
|
||||
service.subscribe("PREDICTION", payloads::add);
|
||||
service.messageArrived("PREDICTION", new MqttMessage("data".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
verify(mqttClient).subscribe("PREDICTION");
|
||||
assertEquals(List.of("data"), payloads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies publish forwards a message with the configured QoS and payload.
|
||||
*
|
||||
* @throws Exception if mock interaction setup fails
|
||||
*/
|
||||
@Test
|
||||
void publish_forwardsMessageWithQos() throws Exception {
|
||||
MqttClientService service = new MqttClientService(appState, mqttClient, false);
|
||||
ArgumentCaptor<MqttMessage> captor = ArgumentCaptor.forClass(MqttMessage.class);
|
||||
|
||||
service.publish("PREDICTION", "hello", 1);
|
||||
|
||||
verify(mqttClient).publish(org.mockito.ArgumentMatchers.eq("PREDICTION"), captor.capture());
|
||||
MqttMessage published = captor.getValue();
|
||||
assertEquals("hello", new String(published.getPayload(), StandardCharsets.UTF_8));
|
||||
assertEquals(1, published.getQos());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies disconnect only calls client disconnect when connection is active.
|
||||
*
|
||||
* @throws Exception if mock interaction setup fails
|
||||
*/
|
||||
@Test
|
||||
void disconnect_onlyWhenConnected() throws Exception {
|
||||
MqttClientService service = new MqttClientService(appState, mqttClient, false);
|
||||
|
||||
when(mqttClient.isConnected()).thenReturn(false);
|
||||
service.disconnect();
|
||||
verify(mqttClient, never()).disconnect();
|
||||
|
||||
when(mqttClient.isConnected()).thenReturn(true);
|
||||
service.disconnect();
|
||||
verify(mqttClient).disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies connection loss updates app state to disconnected.
|
||||
*/
|
||||
@Test
|
||||
void connectionLost_setsMqttStateFalse() {
|
||||
appState.setMqttConnected(true);
|
||||
MqttClientService service = new MqttClientService(appState, mqttClient, false);
|
||||
|
||||
service.connectionLost(new RuntimeException("lost"));
|
||||
|
||||
assertFalse(appState.isMqttConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies unknown topics do not fail when a message arrives.
|
||||
*
|
||||
* @throws Exception if callback invocation fails
|
||||
*/
|
||||
@Test
|
||||
void messageArrived_unknownTopic_doesNotThrow() throws Exception {
|
||||
MqttClientService service = new MqttClientService(appState, mqttClient, false);
|
||||
|
||||
service.messageArrived("UNKNOWN", new MqttMessage("x".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
verify(mqttClient, never()).subscribe("UNKNOWN");
|
||||
}
|
||||
}
|
||||
199
src/test/java/vassistent/service/ProcessManagerServiceTest.java
Normal file
199
src/test/java/vassistent/service/ProcessManagerServiceTest.java
Normal file
@ -0,0 +1,199 @@
|
||||
package vassistent.service;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProcessManagerService} using an injected process launcher.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProcessManagerServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Mock
|
||||
private Process process;
|
||||
|
||||
private Properties config;
|
||||
private List<List<String>> launchedCommands;
|
||||
|
||||
/**
|
||||
* Initializes test configuration and command capture.
|
||||
*
|
||||
* @throws Exception if process mock setup fails
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
config = new Properties();
|
||||
launchedCommands = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a service instance with command capture and test PID files.
|
||||
*
|
||||
* @return configured process manager service
|
||||
*/
|
||||
private ProcessManagerService createService() {
|
||||
ProcessManagerService.ProcessLauncher launcher = processBuilder -> {
|
||||
launchedCommands.add(new ArrayList<>(processBuilder.command()));
|
||||
return process;
|
||||
};
|
||||
|
||||
return new ProcessManagerService(
|
||||
config,
|
||||
launcher,
|
||||
tempDir.resolve("unreal.pid").toString(),
|
||||
tempDir.resolve("signalling.pid").toString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stubs process wait behavior for taskkill command execution.
|
||||
*
|
||||
* @throws Exception if mock stubbing fails
|
||||
*/
|
||||
private void stubProcessWaitFor() throws Exception {
|
||||
when(process.waitFor()).thenReturn(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stubs process metadata needed by shutdown of tracked processes.
|
||||
*
|
||||
* @throws Exception if mock stubbing fails
|
||||
*/
|
||||
private void stubProcessForTrackedShutdown() throws Exception {
|
||||
stubProcessWaitFor();
|
||||
when(process.pid()).thenReturn(123L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that no processes are launched when all features are disabled.
|
||||
*/
|
||||
@Test
|
||||
void startProcesses_whenDisabled_launchesNothing() {
|
||||
config.setProperty("mqtt_sim.enabled", "false");
|
||||
config.setProperty("unreal.enabled", "false");
|
||||
|
||||
ProcessManagerService service = createService();
|
||||
service.startProcesses();
|
||||
|
||||
assertTrue(launchedCommands.isEmpty());
|
||||
assertNull(service.getPythonProcess());
|
||||
assertNull(service.getUnrealProcess());
|
||||
assertNull(service.getUnrealSignallingProcess());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies MQTT simulator launch command when simulator is enabled.
|
||||
*/
|
||||
@Test
|
||||
void startProcesses_mqttEnabled_launchesPythonSimulator() {
|
||||
config.setProperty("mqtt_sim.enabled", "true");
|
||||
config.setProperty("mqtt_sim.script", "sim.py");
|
||||
config.setProperty("python.path", "python");
|
||||
config.setProperty("unreal.enabled", "false");
|
||||
|
||||
ProcessManagerService service = createService();
|
||||
service.startProcesses();
|
||||
|
||||
assertEquals(1, launchedCommands.size());
|
||||
assertEquals(List.of("python", "sim.py"), launchedCommands.get(0));
|
||||
assertSame(process, service.getPythonProcess());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies Unreal launch command when Unreal integration is enabled.
|
||||
*/
|
||||
@Test
|
||||
void startProcesses_unrealEnabled_launchesPowerShell() {
|
||||
config.setProperty("mqtt_sim.enabled", "false");
|
||||
config.setProperty("unreal.enabled", "true");
|
||||
config.setProperty("unreal.executable", "avatar.ps1");
|
||||
config.setProperty("unreal.signalling_server.script", "start_signal.bat");
|
||||
|
||||
ProcessManagerService service = createService();
|
||||
service.startProcesses();
|
||||
|
||||
assertEquals(1, launchedCommands.size());
|
||||
assertEquals(
|
||||
List.of("powershell.exe", "-ExecutionPolicy", "Bypass", "-File", "avatar.ps1"),
|
||||
launchedCommands.get(0)
|
||||
);
|
||||
assertSame(process, service.getUnrealProcess());
|
||||
assertNull(service.getUnrealSignallingProcess());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies shutdown issues taskkill commands for tracked process handles.
|
||||
*/
|
||||
@Test
|
||||
void shutdown_withTrackedProcesses_issuesTaskkillCommands() throws Exception {
|
||||
stubProcessForTrackedShutdown();
|
||||
|
||||
config.setProperty("mqtt_sim.enabled", "true");
|
||||
config.setProperty("mqtt_sim.script", "sim.py");
|
||||
config.setProperty("python.path", "python");
|
||||
config.setProperty("unreal.enabled", "true");
|
||||
config.setProperty("unreal.executable", "avatar.ps1");
|
||||
config.setProperty("unreal.signalling_server.script", "start_signal.bat");
|
||||
|
||||
ProcessManagerService service = createService();
|
||||
service.startProcesses();
|
||||
service.shutdown();
|
||||
|
||||
long taskkillCount = launchedCommands.stream()
|
||||
.filter(command -> !command.isEmpty() && "taskkill".equals(command.get(0)))
|
||||
.count();
|
||||
|
||||
assertEquals(2, taskkillCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies shutdown uses PID files when present.
|
||||
*
|
||||
* @throws Exception if temporary PID files cannot be written
|
||||
*/
|
||||
@Test
|
||||
void shutdown_withPidFiles_issuesPidTaskkillCommands() throws Exception {
|
||||
stubProcessWaitFor();
|
||||
|
||||
Path unrealPid = tempDir.resolve("unreal.pid");
|
||||
Path signallingPid = tempDir.resolve("signalling.pid");
|
||||
Files.writeString(unrealPid, "777");
|
||||
Files.writeString(signallingPid, "888");
|
||||
|
||||
ProcessManagerService.ProcessLauncher launcher = processBuilder -> {
|
||||
launchedCommands.add(new ArrayList<>(processBuilder.command()));
|
||||
return process;
|
||||
};
|
||||
|
||||
ProcessManagerService service = new ProcessManagerService(
|
||||
config,
|
||||
launcher,
|
||||
unrealPid.toString(),
|
||||
signallingPid.toString()
|
||||
);
|
||||
|
||||
service.shutdown();
|
||||
|
||||
assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("777")));
|
||||
assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("888")));
|
||||
}
|
||||
}
|
||||
@ -1,97 +1,110 @@
|
||||
package vassistent.service;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import vassistent.model.DatabaseEntry;
|
||||
import vassistent.model.RatioPoint;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.SQLException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests ratio calculations over persisted binary events.
|
||||
* Unit and integration tests for {@link StatisticsService}.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StatisticsServiceTest {
|
||||
|
||||
private String DB_URL;
|
||||
private DataPersistenceService persistenceService;
|
||||
private StatisticsService statisticsService;
|
||||
private File tempDbFile;
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Mock
|
||||
private DataPersistenceService mockedPersistenceService;
|
||||
|
||||
private StatisticsService unitService;
|
||||
|
||||
/**
|
||||
* Creates an isolated temporary SQLite database for each test.
|
||||
* Creates unit-level service instance with mocked persistence dependency.
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
try {
|
||||
tempDbFile = File.createTempFile("testdb", ".db");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Kann temporäre DB-Datei nicht erstellen", e);
|
||||
}
|
||||
|
||||
DB_URL = "jdbc:sqlite:" + tempDbFile.getAbsolutePath();
|
||||
persistenceService = new DataPersistenceService(DB_URL);
|
||||
statisticsService = new StatisticsService(persistenceService);
|
||||
unitService = new StatisticsService(mockedPersistenceService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the temporary test database file.
|
||||
*/
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (tempDbFile != null && tempDbFile.exists()) {
|
||||
tempDbFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies ratio behavior when no events are available.
|
||||
* Verifies rolling average output for a deterministic input sequence.
|
||||
*/
|
||||
@Test
|
||||
void testGetRatioNoData() {
|
||||
double ratio = statisticsService.getRatio(10);
|
||||
assertEquals(0.0, ratio, "Ratio sollte 0 sein, wenn keine Daten vorhanden");
|
||||
void getLastNAverages_computesExpectedRollingValues() {
|
||||
LocalDateTime t0 = LocalDateTime.now();
|
||||
List<DatabaseEntry> entries = List.of(
|
||||
new DatabaseEntry(t0, 1),
|
||||
new DatabaseEntry(t0.plusSeconds(1), 0),
|
||||
new DatabaseEntry(t0.plusSeconds(2), 1)
|
||||
);
|
||||
when(mockedPersistenceService.getLastEntries(3)).thenReturn(entries);
|
||||
|
||||
List<RatioPoint> result = unitService.getLastNAverages(2);
|
||||
|
||||
assertEquals(3, result.size());
|
||||
assertEquals(0.0, result.get(0).getRatio(), 0.0001);
|
||||
assertEquals(1.0, result.get(1).getRatio(), 0.0001);
|
||||
assertEquals(0.5, result.get(2).getRatio(), 0.0001);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies ratio behavior with exactly one stored value.
|
||||
* Verifies that empty persistence result yields an empty chart result.
|
||||
*/
|
||||
@Test
|
||||
void getLastNAverages_withNoEntries_returnsEmptyList() {
|
||||
when(mockedPersistenceService.getLastEntries(6)).thenReturn(List.of());
|
||||
|
||||
List<RatioPoint> result = unitService.getLastNAverages(5);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies average ratio over newest values using real SQLite persistence.
|
||||
*
|
||||
* @throws SQLException retained for compatibility with current method signature
|
||||
* @throws InterruptedException if sleeps used to separate timestamps are interrupted
|
||||
*/
|
||||
@Test
|
||||
void testGetRatioSingleValue() throws SQLException {
|
||||
persistenceService.store(1);
|
||||
void getRatio_returnsAverageOfLastN() throws InterruptedException {
|
||||
String dbUrl = "jdbc:sqlite:" + tempDir.resolve("ratio-test.db").toAbsolutePath();
|
||||
DataPersistenceService integrationPersistence = new DataPersistenceService(dbUrl);
|
||||
StatisticsService integrationService = new StatisticsService(integrationPersistence);
|
||||
|
||||
double ratio = statisticsService.getRatio(1);
|
||||
assertEquals(1.0, ratio, "Ratio sollte 1 sein, wenn nur 1 gespeichert wurde");
|
||||
integrationPersistence.store(1);
|
||||
Thread.sleep(5);
|
||||
integrationPersistence.store(0);
|
||||
Thread.sleep(5);
|
||||
integrationPersistence.store(1);
|
||||
|
||||
double ratio = integrationService.getRatio(3);
|
||||
|
||||
assertEquals(2.0 / 3.0, ratio, 0.0001);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies ratio behavior across multiple stored values.
|
||||
* Verifies that requesting a ratio on an empty dataset returns zero.
|
||||
*/
|
||||
@Test
|
||||
void testGetRatioMultipleValues() {
|
||||
persistenceService.store(1);
|
||||
persistenceService.store(0);
|
||||
persistenceService.store(1);
|
||||
void getRatio_withNoData_returnsZero() {
|
||||
String dbUrl = "jdbc:sqlite:" + tempDir.resolve("ratio-empty.db").toAbsolutePath();
|
||||
DataPersistenceService integrationPersistence = new DataPersistenceService(dbUrl);
|
||||
StatisticsService integrationService = new StatisticsService(integrationPersistence);
|
||||
|
||||
double ratio = statisticsService.getRatio(10);
|
||||
assertEquals((1 + 0 + 1) / 3.0, ratio, 0.0001, "Ratio sollte Durchschnitt der letzten Werte sein");
|
||||
}
|
||||
double ratio = integrationService.getRatio(20);
|
||||
|
||||
/**
|
||||
* Verifies that ratio calculation only considers the latest N values.
|
||||
*/
|
||||
@Test
|
||||
void testGetRatioLimitLastN() {
|
||||
persistenceService.store(1);
|
||||
persistenceService.store(1);
|
||||
persistenceService.store(0);
|
||||
persistenceService.store(0);
|
||||
persistenceService.store(1);
|
||||
|
||||
double ratio = statisticsService.getRatio(3);
|
||||
assertEquals((1 + 0 + 0) / 3.0, ratio, 0.0001, "Ratio sollte Durchschnitt der letzten Werte sein");
|
||||
assertEquals(0.0, ratio, 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user