Starup Report added and StatisticsService rolling avgs optimized to O(n)
This commit is contained in:
parent
6478e046b4
commit
c7e9bce1d3
@ -98,8 +98,15 @@ public class ApplicationInitializer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
context.setProcessManagerService(new ProcessManagerService(config));
|
context.setProcessManagerService(new ProcessManagerService(config));
|
||||||
|
|
||||||
|
ProcessManagerService.StartupReport startupReport =
|
||||||
context.getProcessManagerService().startProcesses();
|
context.getProcessManagerService().startProcesses();
|
||||||
|
|
||||||
|
if (startupReport.hasFailures()) {
|
||||||
|
context.getProcessManagerService().shutdown();
|
||||||
|
throw new IllegalStateException(startupReport.buildFailureMessage());
|
||||||
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,7 +107,7 @@ public class DataPersistenceService {
|
|||||||
String query = """
|
String query = """
|
||||||
SELECT value, timestamp
|
SELECT value, timestamp
|
||||||
FROM binary_event
|
FROM binary_event
|
||||||
ORDER BY timestamp DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""";
|
""";
|
||||||
try (Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement(query)) {
|
try (Connection c = DriverManager.getConnection(DB_URL); PreparedStatement ps = c.prepareStatement(query)) {
|
||||||
|
|||||||
@ -13,6 +13,205 @@ import java.util.Properties;
|
|||||||
*/
|
*/
|
||||||
public class ProcessManagerService {
|
public class ProcessManagerService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents startup status for a single managed process component.
|
||||||
|
*/
|
||||||
|
public static final class ProcessStartStatus {
|
||||||
|
|
||||||
|
private final String componentName;
|
||||||
|
private final boolean enabled;
|
||||||
|
private final boolean started;
|
||||||
|
private final String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an immutable process start status instance.
|
||||||
|
*
|
||||||
|
* @param componentName logical component name
|
||||||
|
* @param enabled whether component startup was enabled
|
||||||
|
* @param started whether startup completed without launch error
|
||||||
|
* @param errorMessage optional startup error details
|
||||||
|
*/
|
||||||
|
private ProcessStartStatus(
|
||||||
|
String componentName,
|
||||||
|
boolean enabled,
|
||||||
|
boolean started,
|
||||||
|
String errorMessage
|
||||||
|
) {
|
||||||
|
this.componentName = componentName;
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.started = started;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a disabled status for a component.
|
||||||
|
*
|
||||||
|
* @param componentName logical component name
|
||||||
|
* @return disabled status instance
|
||||||
|
*/
|
||||||
|
static ProcessStartStatus disabled(String componentName) {
|
||||||
|
return new ProcessStartStatus(componentName, false, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a successful startup status for a component.
|
||||||
|
*
|
||||||
|
* @param componentName logical component name
|
||||||
|
* @return successful status instance
|
||||||
|
*/
|
||||||
|
static ProcessStartStatus started(String componentName) {
|
||||||
|
return new ProcessStartStatus(componentName, true, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a failed startup status for a component.
|
||||||
|
*
|
||||||
|
* @param componentName logical component name
|
||||||
|
* @param errorMessage startup failure details
|
||||||
|
* @return failure status instance
|
||||||
|
*/
|
||||||
|
static ProcessStartStatus failed(String componentName, String errorMessage) {
|
||||||
|
return new ProcessStartStatus(componentName, true, false, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether startup failed for an enabled component.
|
||||||
|
*
|
||||||
|
* @return {@code true} when startup failed
|
||||||
|
*/
|
||||||
|
public boolean hasFailed() {
|
||||||
|
return enabled && !started;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns logical component name.
|
||||||
|
*
|
||||||
|
* @return component name
|
||||||
|
*/
|
||||||
|
public String getComponentName() {
|
||||||
|
return componentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether startup was enabled.
|
||||||
|
*
|
||||||
|
* @return enabled flag
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether startup completed without launch error.
|
||||||
|
*
|
||||||
|
* @return started flag
|
||||||
|
*/
|
||||||
|
public boolean isStarted() {
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns startup error details when available.
|
||||||
|
*
|
||||||
|
* @return error details or {@code null}
|
||||||
|
*/
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates startup status for all managed process components.
|
||||||
|
*/
|
||||||
|
public static final class StartupReport {
|
||||||
|
|
||||||
|
private final ProcessStartStatus mqttSimulator;
|
||||||
|
private final ProcessStartStatus unreal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a startup report.
|
||||||
|
*
|
||||||
|
* @param mqttSimulator startup status of MQTT simulator component
|
||||||
|
* @param unreal startup status of Unreal launcher component
|
||||||
|
*/
|
||||||
|
StartupReport(
|
||||||
|
ProcessStartStatus mqttSimulator,
|
||||||
|
ProcessStartStatus unreal
|
||||||
|
) {
|
||||||
|
this.mqttSimulator = Objects.requireNonNull(mqttSimulator);
|
||||||
|
this.unreal = Objects.requireNonNull(unreal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns startup status of MQTT simulator component.
|
||||||
|
*
|
||||||
|
* @return MQTT simulator startup status
|
||||||
|
*/
|
||||||
|
public ProcessStartStatus getMqttSimulator() {
|
||||||
|
return mqttSimulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns startup status of Unreal launcher component.
|
||||||
|
*
|
||||||
|
* @return Unreal startup status
|
||||||
|
*/
|
||||||
|
public ProcessStartStatus getUnreal() {
|
||||||
|
return unreal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether at least one enabled process failed to launch.
|
||||||
|
*
|
||||||
|
* @return {@code true} when startup has failures
|
||||||
|
*/
|
||||||
|
public boolean hasFailures() {
|
||||||
|
return mqttSimulator.hasFailed() || unreal.hasFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a compact startup failure message suitable for exceptions.
|
||||||
|
*
|
||||||
|
* @return failure message
|
||||||
|
*/
|
||||||
|
public String buildFailureMessage() {
|
||||||
|
StringBuilder message =
|
||||||
|
new StringBuilder("External process startup failed:");
|
||||||
|
|
||||||
|
appendFailure(message, mqttSimulator);
|
||||||
|
appendFailure(message, unreal);
|
||||||
|
|
||||||
|
return message.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends one failed component summary to a message builder.
|
||||||
|
*
|
||||||
|
* @param message target message builder
|
||||||
|
* @param status component startup status
|
||||||
|
*/
|
||||||
|
private static void appendFailure(
|
||||||
|
StringBuilder message,
|
||||||
|
ProcessStartStatus status
|
||||||
|
) {
|
||||||
|
if (!status.hasFailed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.append(System.lineSeparator())
|
||||||
|
.append("- ")
|
||||||
|
.append(status.getComponentName())
|
||||||
|
.append(": ");
|
||||||
|
|
||||||
|
String error = status.getErrorMessage();
|
||||||
|
if (error == null || error.isBlank()) {
|
||||||
|
message.append("unknown startup error");
|
||||||
|
} else {
|
||||||
|
message.append(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
interface ProcessLauncher {
|
interface ProcessLauncher {
|
||||||
Process launch(ProcessBuilder processBuilder) throws IOException;
|
Process launch(ProcessBuilder processBuilder) throws IOException;
|
||||||
@ -78,23 +277,31 @@ public class ProcessManagerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts optional helper processes according to configuration flags.
|
* Starts optional helper processes according to configuration flags.
|
||||||
|
*
|
||||||
|
* @return startup status report for all managed components
|
||||||
*/
|
*/
|
||||||
public void startProcesses() {
|
public StartupReport startProcesses() {
|
||||||
|
|
||||||
startPythonIfEnabled();
|
ProcessStartStatus mqttSimulator = startPythonIfEnabled();
|
||||||
startUnrealIfEnabled();
|
ProcessStartStatus unreal = startUnrealIfEnabled();
|
||||||
|
|
||||||
|
return new StartupReport(mqttSimulator, unreal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the Python MQTT simulator if enabled.
|
* Starts the Python MQTT simulator if enabled.
|
||||||
|
*
|
||||||
|
* @return startup status for the simulator component
|
||||||
*/
|
*/
|
||||||
private void startPythonIfEnabled() {
|
private ProcessStartStatus startPythonIfEnabled() {
|
||||||
|
|
||||||
boolean enabled = Boolean.parseBoolean(
|
boolean enabled = Boolean.parseBoolean(
|
||||||
config.getProperty("mqtt_sim.enabled", "false")
|
config.getProperty("mqtt_sim.enabled", "false")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!enabled) return;
|
if (!enabled) {
|
||||||
|
return ProcessStartStatus.disabled("mqtt_sim");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String script = cleanConfigValue(config.getProperty("mqtt_sim.script"));
|
String script = cleanConfigValue(config.getProperty("mqtt_sim.script"));
|
||||||
@ -107,25 +314,39 @@ public class ProcessManagerService {
|
|||||||
pythonProcess = processLauncher.launch(pb);
|
pythonProcess = processLauncher.launch(pb);
|
||||||
|
|
||||||
Logger.info("PROCESS", "Mqtt Simulator gestartet");
|
Logger.info("PROCESS", "Mqtt Simulator gestartet");
|
||||||
} catch (IOException e) {
|
return ProcessStartStatus.started("mqtt_sim");
|
||||||
|
} catch (Exception e) {
|
||||||
Logger.error("PROCESS", "Mqtt Simulator Start fehlgeschlagen", e);
|
Logger.error("PROCESS", "Mqtt Simulator Start fehlgeschlagen", e);
|
||||||
|
return ProcessStartStatus.failed(
|
||||||
|
"mqtt_sim",
|
||||||
|
exceptionMessage(e)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts Unreal-related processes if enabled.
|
* Starts Unreal-related processes if enabled.
|
||||||
|
*
|
||||||
|
* @return startup status for the Unreal component
|
||||||
*/
|
*/
|
||||||
private void startUnrealIfEnabled() {
|
private ProcessStartStatus startUnrealIfEnabled() {
|
||||||
|
|
||||||
boolean enabled = Boolean.parseBoolean(config.getProperty("unreal.enabled", "false"));
|
boolean enabled = Boolean.parseBoolean(config.getProperty("unreal.enabled", "false"));
|
||||||
|
|
||||||
if (!enabled) return;
|
if (!enabled) {
|
||||||
|
return ProcessStartStatus.disabled("unreal");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startUnrealEngine();
|
startUnrealEngine();
|
||||||
|
return ProcessStartStatus.started("unreal");
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
Logger.error("PROCESS", "Unreal Start fehlgeschlagen", e);
|
Logger.error("PROCESS", "Unreal Start fehlgeschlagen", e);
|
||||||
|
return ProcessStartStatus.failed(
|
||||||
|
"unreal",
|
||||||
|
exceptionMessage(e)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,6 +494,23 @@ public class ProcessManagerService {
|
|||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a compact message for startup exception reporting.
|
||||||
|
*
|
||||||
|
* @param exception startup exception
|
||||||
|
* @return compact exception details
|
||||||
|
*/
|
||||||
|
private static String exceptionMessage(Exception exception) {
|
||||||
|
String type = exception.getClass().getSimpleName();
|
||||||
|
String message = exception.getMessage();
|
||||||
|
|
||||||
|
if (message == null || message.isBlank()) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return type + ": " + message;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passes optional simulator settings from app config to the Python process environment.
|
* Passes optional simulator settings from app config to the Python process environment.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import vassistent.model.RatioPoint;
|
|||||||
import vassistent.util.Logger;
|
import vassistent.util.Logger;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -31,6 +30,10 @@ public class StatisticsService {
|
|||||||
* @return ratio in range {@code [0, 1]} or {@code 0.0} when unavailable
|
* @return ratio in range {@code [0, 1]} or {@code 0.0} when unavailable
|
||||||
*/
|
*/
|
||||||
public double getRatio(int lastN) {
|
public double getRatio(int lastN) {
|
||||||
|
if (lastN <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
String query = """
|
String query = """
|
||||||
SELECT AVG(value)
|
SELECT AVG(value)
|
||||||
FROM (
|
FROM (
|
||||||
@ -58,30 +61,40 @@ public class StatisticsService {
|
|||||||
/**
|
/**
|
||||||
* Computes rolling averages for chart rendering.
|
* Computes rolling averages for chart rendering.
|
||||||
*
|
*
|
||||||
* @param windowSize number of previous entries considered for each point
|
* The rolling window includes the current entry and up to {@code windowSize - 1}
|
||||||
|
* previous entries.
|
||||||
|
*
|
||||||
|
* @param windowSize number of entries considered per rolling average window
|
||||||
* @return chronological list of ratio points
|
* @return chronological list of ratio points
|
||||||
*/
|
*/
|
||||||
public List<RatioPoint> getLastNAverages(int windowSize) {
|
public List<RatioPoint> getLastNAverages(int windowSize) {
|
||||||
|
|
||||||
|
if (windowSize <= 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
List<DatabaseEntry> entries = persistenceService.getLastEntries(windowSize + 1);
|
List<DatabaseEntry> entries = persistenceService.getLastEntries(windowSize + 1);
|
||||||
|
|
||||||
List<RatioPoint> result = new ArrayList<>();
|
List<RatioPoint> result = new ArrayList<>();
|
||||||
|
|
||||||
if (entries.isEmpty())
|
if (entries.isEmpty()) {
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
for (int i = 0; i < entries.size(); i++) {
|
|
||||||
int start = Math.max(0, i - windowSize);
|
|
||||||
|
|
||||||
double sum = 0;
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
for (int j = start; j < i; j++) {
|
|
||||||
sum += entries.get(j).getValue();
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double avg = count == 0 ? 0 : sum / count;
|
double rollingSum = 0.0;
|
||||||
|
int windowStart = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < entries.size(); i++) {
|
||||||
|
rollingSum += entries.get(i).getValue();
|
||||||
|
|
||||||
|
if (i - windowStart + 1 > windowSize) {
|
||||||
|
rollingSum -= entries.get(windowStart).getValue();
|
||||||
|
windowStart++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int windowCount = i - windowStart + 1;
|
||||||
|
double avg = rollingSum / windowCount;
|
||||||
|
|
||||||
|
|
||||||
result.add(new RatioPoint(
|
result.add(new RatioPoint(
|
||||||
entries.get(i).getTimestamp(),
|
entries.get(i).getTimestamp(),
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import java.util.List;
|
|||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@ -92,11 +93,14 @@ class ProcessManagerServiceTest {
|
|||||||
config.setProperty("unreal.enabled", "false");
|
config.setProperty("unreal.enabled", "false");
|
||||||
|
|
||||||
ProcessManagerService service = createService();
|
ProcessManagerService service = createService();
|
||||||
service.startProcesses();
|
ProcessManagerService.StartupReport report = service.startProcesses();
|
||||||
|
|
||||||
assertTrue(launchedCommands.isEmpty());
|
assertTrue(launchedCommands.isEmpty());
|
||||||
assertNull(service.getPythonProcess());
|
assertNull(service.getPythonProcess());
|
||||||
assertNull(service.getUnrealProcess());
|
assertNull(service.getUnrealProcess());
|
||||||
|
assertFalse(report.hasFailures());
|
||||||
|
assertFalse(report.getMqttSimulator().isEnabled());
|
||||||
|
assertFalse(report.getUnreal().isEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,11 +114,14 @@ class ProcessManagerServiceTest {
|
|||||||
config.setProperty("unreal.enabled", "false");
|
config.setProperty("unreal.enabled", "false");
|
||||||
|
|
||||||
ProcessManagerService service = createService();
|
ProcessManagerService service = createService();
|
||||||
service.startProcesses();
|
ProcessManagerService.StartupReport report = service.startProcesses();
|
||||||
|
|
||||||
assertEquals(1, launchedCommands.size());
|
assertEquals(1, launchedCommands.size());
|
||||||
assertEquals(List.of("python", "sim.py"), launchedCommands.get(0));
|
assertEquals(List.of("python", "sim.py"), launchedCommands.get(0));
|
||||||
assertSame(process, service.getPythonProcess());
|
assertSame(process, service.getPythonProcess());
|
||||||
|
assertFalse(report.hasFailures());
|
||||||
|
assertTrue(report.getMqttSimulator().isStarted());
|
||||||
|
assertFalse(report.getUnreal().isEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,7 +135,7 @@ class ProcessManagerServiceTest {
|
|||||||
config.setProperty("unreal.signalling_server.script", "start_signal.bat");
|
config.setProperty("unreal.signalling_server.script", "start_signal.bat");
|
||||||
|
|
||||||
ProcessManagerService service = createService();
|
ProcessManagerService service = createService();
|
||||||
service.startProcesses();
|
ProcessManagerService.StartupReport report = service.startProcesses();
|
||||||
|
|
||||||
assertEquals(1, launchedCommands.size());
|
assertEquals(1, launchedCommands.size());
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@ -140,6 +147,9 @@ class ProcessManagerServiceTest {
|
|||||||
.noneMatch(command -> !command.isEmpty() && "cmd".equalsIgnoreCase(command.get(0)))
|
.noneMatch(command -> !command.isEmpty() && "cmd".equalsIgnoreCase(command.get(0)))
|
||||||
);
|
);
|
||||||
assertSame(process, service.getUnrealProcess());
|
assertSame(process, service.getUnrealProcess());
|
||||||
|
assertFalse(report.hasFailures());
|
||||||
|
assertTrue(report.getUnreal().isStarted());
|
||||||
|
assertFalse(report.getMqttSimulator().isEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,7 +167,7 @@ class ProcessManagerServiceTest {
|
|||||||
config.setProperty("unreal.signalling_server.script", "start_signal.bat");
|
config.setProperty("unreal.signalling_server.script", "start_signal.bat");
|
||||||
|
|
||||||
ProcessManagerService service = createService();
|
ProcessManagerService service = createService();
|
||||||
service.startProcesses();
|
ProcessManagerService.StartupReport report = service.startProcesses();
|
||||||
service.shutdown();
|
service.shutdown();
|
||||||
|
|
||||||
long taskkillCount = launchedCommands.stream()
|
long taskkillCount = launchedCommands.stream()
|
||||||
@ -165,6 +175,7 @@ class ProcessManagerServiceTest {
|
|||||||
.count();
|
.count();
|
||||||
|
|
||||||
assertEquals(2, taskkillCount);
|
assertEquals(2, taskkillCount);
|
||||||
|
assertFalse(report.hasFailures());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,4 +209,61 @@ class ProcessManagerServiceTest {
|
|||||||
assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("777")));
|
assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("777")));
|
||||||
assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("888")));
|
assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("888")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a failed simulator launch is reported as startup failure.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void startProcesses_mqttLaunchFails_reportsFailure() {
|
||||||
|
config.setProperty("mqtt_sim.enabled", "true");
|
||||||
|
config.setProperty("mqtt_sim.script", "sim.py");
|
||||||
|
config.setProperty("python.path", "python");
|
||||||
|
config.setProperty("unreal.enabled", "false");
|
||||||
|
|
||||||
|
ProcessManagerService.ProcessLauncher failingLauncher = processBuilder -> {
|
||||||
|
throw new java.io.IOException("python not found");
|
||||||
|
};
|
||||||
|
|
||||||
|
ProcessManagerService service = new ProcessManagerService(
|
||||||
|
config,
|
||||||
|
failingLauncher,
|
||||||
|
tempDir.resolve("unreal.pid").toString(),
|
||||||
|
tempDir.resolve("signalling.pid").toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
ProcessManagerService.StartupReport report = service.startProcesses();
|
||||||
|
|
||||||
|
assertTrue(report.hasFailures());
|
||||||
|
assertTrue(report.getMqttSimulator().hasFailed());
|
||||||
|
assertTrue(report.buildFailureMessage().contains("mqtt_sim"));
|
||||||
|
assertTrue(report.buildFailureMessage().contains("python not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a failed Unreal launch is reported as startup failure.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void startProcesses_unrealLaunchFails_reportsFailure() {
|
||||||
|
config.setProperty("mqtt_sim.enabled", "false");
|
||||||
|
config.setProperty("unreal.enabled", "true");
|
||||||
|
config.setProperty("unreal.executable", "avatar.ps1");
|
||||||
|
|
||||||
|
ProcessManagerService.ProcessLauncher failingLauncher = processBuilder -> {
|
||||||
|
throw new java.io.IOException("powershell unavailable");
|
||||||
|
};
|
||||||
|
|
||||||
|
ProcessManagerService service = new ProcessManagerService(
|
||||||
|
config,
|
||||||
|
failingLauncher,
|
||||||
|
tempDir.resolve("unreal.pid").toString(),
|
||||||
|
tempDir.resolve("signalling.pid").toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
ProcessManagerService.StartupReport report = service.startProcesses();
|
||||||
|
|
||||||
|
assertTrue(report.hasFailures());
|
||||||
|
assertTrue(report.getUnreal().hasFailed());
|
||||||
|
assertTrue(report.buildFailureMessage().contains("unreal"));
|
||||||
|
assertTrue(report.buildFailureMessage().contains("powershell unavailable"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ -55,8 +56,8 @@ class StatisticsServiceTest {
|
|||||||
List<RatioPoint> result = unitService.getLastNAverages(2);
|
List<RatioPoint> result = unitService.getLastNAverages(2);
|
||||||
|
|
||||||
assertEquals(3, result.size());
|
assertEquals(3, result.size());
|
||||||
assertEquals(0.0, result.get(0).getRatio(), 0.0001);
|
assertEquals(1.0, result.get(0).getRatio(), 0.0001);
|
||||||
assertEquals(1.0, result.get(1).getRatio(), 0.0001);
|
assertEquals(0.5, result.get(1).getRatio(), 0.0001);
|
||||||
assertEquals(0.5, result.get(2).getRatio(), 0.0001);
|
assertEquals(0.5, result.get(2).getRatio(), 0.0001);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +73,16 @@ class StatisticsServiceTest {
|
|||||||
assertTrue(result.isEmpty());
|
assertTrue(result.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that non-positive window sizes yield an empty result.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void getLastNAverages_withNonPositiveWindow_returnsEmptyList() {
|
||||||
|
List<RatioPoint> result = unitService.getLastNAverages(0);
|
||||||
|
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies average ratio over newest values using real SQLite persistence.
|
* Verifies average ratio over newest values using real SQLite persistence.
|
||||||
*
|
*
|
||||||
@ -107,4 +118,28 @@ class StatisticsServiceTest {
|
|||||||
|
|
||||||
assertEquals(0.0, ratio, 0.0001);
|
assertEquals(0.0, ratio, 0.0001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the newest rolling average matches {@link StatisticsService#getRatio(int)}
|
||||||
|
* for the same window size.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void getLastNAverages_lastPointMatchesGetRatio() {
|
||||||
|
String dbUrl = "jdbc:sqlite:" + tempDir.resolve("ratio-rolling.db").toAbsolutePath();
|
||||||
|
DataPersistenceService integrationPersistence = new DataPersistenceService(dbUrl);
|
||||||
|
StatisticsService integrationService = new StatisticsService(integrationPersistence);
|
||||||
|
|
||||||
|
integrationPersistence.store(1);
|
||||||
|
integrationPersistence.store(0);
|
||||||
|
integrationPersistence.store(1);
|
||||||
|
integrationPersistence.store(1);
|
||||||
|
|
||||||
|
int window = 3;
|
||||||
|
|
||||||
|
List<RatioPoint> rolling = integrationService.getLastNAverages(window);
|
||||||
|
double ratio = integrationService.getRatio(window);
|
||||||
|
|
||||||
|
assertFalse(rolling.isEmpty());
|
||||||
|
assertEquals(ratio, rolling.get(rolling.size() - 1).getRatio(), 0.0001);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user