mirror of
https://github.com/davidalves04/Trabalho-Pratico-SD.git
synced 2025-12-08 20:43:32 +00:00
Compare commits
25 Commits
11-convert
...
v0.6.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecb70fa6a2 | ||
| 06f079ce5b | |||
| 72893f87ae | |||
| 6b94d727e2 | |||
| 84cba39597 | |||
| 5dc1b40c88 | |||
| 3117bdf332 | |||
| 1140c3ca48 | |||
|
|
484cba1eee | ||
|
|
0e5526c3f6 | ||
|
|
cf88db4297 | ||
|
|
0960a7a141 | ||
|
|
3b4f968a59 | ||
| 0c256ad6f5 | |||
| 340e436063 | |||
| 1684a6713e | |||
| 22a7081ade | |||
| 3b699556db | |||
|
|
d078808486 | ||
|
|
98581b562d | ||
|
|
4710c96450 | ||
| f9644bd18c | |||
|
|
c6b710ac52 | ||
|
|
d057adeab3 | ||
|
|
be4e7f66d6 |
382
main/src/main/java/sd/ExitNodeProcess.java
Normal file
382
main/src/main/java/sd/ExitNodeProcess.java
Normal file
@@ -0,0 +1,382 @@
|
||||
package sd;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import sd.config.SimulationConfig;
|
||||
import sd.coordinator.SocketClient;
|
||||
import sd.model.Message;
|
||||
import sd.model.MessageType;
|
||||
import sd.model.Vehicle;
|
||||
import sd.model.VehicleType;
|
||||
import sd.protocol.MessageProtocol;
|
||||
import sd.protocol.SocketConnection;
|
||||
import sd.serialization.SerializationException;
|
||||
|
||||
/**
|
||||
* Processo responsável pelo nó de saída do sistema de simulação de tráfego distribuído.
|
||||
*
|
||||
* Este processo representa o ponto final ("S") onde os veículos completam as suas rotas.
|
||||
* As suas principais responsabilidades são:
|
||||
* - Receber veículos que terminam a sua rota vindos das interseções
|
||||
* - Calcular e agregar estatísticas finais dos veículos
|
||||
* - Enviar estatísticas periódicas para o dashboard
|
||||
* - Gerar relatórios finais ao terminar a simulação
|
||||
*/
|
||||
public class ExitNodeProcess {
|
||||
|
||||
private final SimulationConfig config;
|
||||
private ServerSocket serverSocket;
|
||||
private final ExecutorService connectionHandlerPool;
|
||||
|
||||
/** Flag para controlar a execução do processo (volatile para visibilidade entre threads) */
|
||||
private volatile boolean running;
|
||||
|
||||
/** Counter de veículos que completaram a rota */
|
||||
private int totalVehiclesReceived;
|
||||
|
||||
/** Soma dos tempos no sistema de todos os veículos */
|
||||
private double totalSystemTime;
|
||||
|
||||
/** Soma dos tempos de espera de todos os veículos */
|
||||
private double totalWaitingTime;
|
||||
|
||||
/** Soma dos tempos de travessia de todos os veículos */
|
||||
private double totalCrossingTime;
|
||||
|
||||
/** Contagem de veículos por tipo */
|
||||
private final Map<VehicleType, Integer> vehicleTypeCount;
|
||||
|
||||
/** Tempo total de espera acumulado por tipo de veículo */
|
||||
private final Map<VehicleType, Double> vehicleTypeWaitTime;
|
||||
|
||||
/** Socket para comunicação com o dashboard */
|
||||
private SocketClient dashboardClient;
|
||||
|
||||
/**
|
||||
* Método para iniciar o processo
|
||||
*
|
||||
* @param args Argumentos da linha de comandos. Se fornecido, args[0] deve ser
|
||||
* o caminho para um ficheiro de configuração personalizado.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=".repeat(60));
|
||||
System.out.println("EXIT NODE PROCESS");
|
||||
System.out.println("=".repeat(60));
|
||||
|
||||
try {
|
||||
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
|
||||
System.out.println("Loading configuration from: " + configFile);
|
||||
|
||||
SimulationConfig config = new SimulationConfig(configFile);
|
||||
ExitNodeProcess exitNode = new ExitNodeProcess(config);
|
||||
|
||||
System.out.println("\n" + "=".repeat(60));
|
||||
exitNode.initialize();
|
||||
|
||||
System.out.println("\n" + "=".repeat(60));
|
||||
exitNode.start();
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to start exit node: " + e.getMessage());
|
||||
System.exit(1);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Exit node error: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrói um novo processo de nó de saída.
|
||||
*
|
||||
* Inicializa todas as estruturas de dados necessárias para recolher estatísticas
|
||||
* e configura o pool de threads para processar as ligações concorrentes.
|
||||
*
|
||||
* @param config Configuração da simulação contendo portas e endereços dos serviços
|
||||
*/
|
||||
public ExitNodeProcess(SimulationConfig config) {
|
||||
this.config = config;
|
||||
this.connectionHandlerPool = Executors.newCachedThreadPool();
|
||||
this.running = false;
|
||||
|
||||
this.totalVehiclesReceived = 0;
|
||||
this.totalSystemTime = 0.0;
|
||||
this.totalWaitingTime = 0.0;
|
||||
this.totalCrossingTime = 0.0;
|
||||
this.vehicleTypeCount = new HashMap<>();
|
||||
this.vehicleTypeWaitTime = new HashMap<>();
|
||||
|
||||
// Inicializa os counters para cada tipo de veículo
|
||||
for (VehicleType type : VehicleType.values()) {
|
||||
vehicleTypeCount.put(type, 0);
|
||||
vehicleTypeWaitTime.put(type, 0.0);
|
||||
}
|
||||
|
||||
System.out.println("Exit node initialized");
|
||||
System.out.println(" - Exit port: " + config.getExitPort());
|
||||
System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa o processo de ligação ao dashboard.
|
||||
*
|
||||
* Tenta conectar-se ao dashboard. Se a ligação falhar, o processo
|
||||
* continua a funcionar normalmente, mas sem enviar estatísticas.
|
||||
*
|
||||
*/
|
||||
public void initialize() {
|
||||
System.out.println("Connecting to dashboard...");
|
||||
|
||||
try {
|
||||
String host = config.getDashboardHost();
|
||||
int port = config.getDashboardPort();
|
||||
|
||||
dashboardClient = new SocketClient("Dashboard", host, port);
|
||||
dashboardClient.connect();
|
||||
|
||||
System.out.println("Successfully connected to dashboard");
|
||||
} catch (IOException e) {
|
||||
System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
|
||||
System.err.println("Exit node will continue without dashboard connection");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicia o socket e começa a aceitar ligações.
|
||||
*
|
||||
* Este é o loop principal do processo que:
|
||||
* 1. Cria um socket na porta definida
|
||||
* 2. Aguarda pelas ligações das interseções
|
||||
* 3. Delega cada ligação a uma thread da pool para processamento assíncrono
|
||||
*
|
||||
* @throws IOException Se o socket não puder ser criado ou houver erro na aceitação
|
||||
*/
|
||||
public void start() throws IOException {
|
||||
int port = config.getExitPort();
|
||||
serverSocket = new ServerSocket(port);
|
||||
running = true;
|
||||
|
||||
System.out.println("Exit node started on port " + port);
|
||||
System.out.println("Waiting for vehicles...\n");
|
||||
|
||||
while (running) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
System.err.println("Error accepting connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processa uma ligação recebida de uma interseção.
|
||||
*
|
||||
* Mantém a ligação aberta e processa continuamente mensagens do tipo
|
||||
* VEHICLE_TRANSFER. Cada mensagem representa um veículo que chegou ao nó de saída.
|
||||
*
|
||||
* @param clientSocket Socket da ligação estabelecida com a interseção
|
||||
*/
|
||||
private void handleIncomingConnection(Socket clientSocket) {
|
||||
try (SocketConnection connection = new SocketConnection(clientSocket)) {
|
||||
|
||||
System.out.println("New connection accepted from " +
|
||||
clientSocket.getInetAddress().getHostAddress());
|
||||
|
||||
while (running && connection.isConnected()) {
|
||||
try {
|
||||
MessageProtocol message = connection.receiveMessage();
|
||||
|
||||
if (message.getType() == MessageType.VEHICLE_TRANSFER) {
|
||||
Vehicle vehicle = (Vehicle) message.getPayload();
|
||||
processExitingVehicle(vehicle);
|
||||
}
|
||||
|
||||
} catch (ClassNotFoundException e) {
|
||||
System.err.println("Unknown message type received: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
System.err.println("Connection error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processa um veículo que chegou ao nó de saída.
|
||||
*
|
||||
* Método sincronizado para garantir thread-safety ao atualizar as estatísticas.
|
||||
* Calcula as métricas finais do veículo e atualiza:
|
||||
* - Counters globais;
|
||||
* - Estatísticas por tipo de veículo;
|
||||
* - Faz update ao dashboard a cada 10 veículos.
|
||||
*
|
||||
* @param vehicle Veículo que completou a sua rota
|
||||
*/
|
||||
private synchronized void processExitingVehicle(Vehicle vehicle) {
|
||||
totalVehiclesReceived++;
|
||||
|
||||
double systemTime = vehicle.getTotalTravelTime(getCurrentTime());
|
||||
double waitTime = vehicle.getTotalWaitingTime();
|
||||
double crossingTime = vehicle.getTotalCrossingTime();
|
||||
|
||||
totalSystemTime += systemTime;
|
||||
totalWaitingTime += waitTime;
|
||||
totalCrossingTime += crossingTime;
|
||||
|
||||
VehicleType type = vehicle.getType();
|
||||
vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
|
||||
vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);
|
||||
|
||||
System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs)%n",
|
||||
vehicle.getId(), vehicle.getType(), systemTime, waitTime);
|
||||
|
||||
if (totalVehiclesReceived % 10 == 0) {
|
||||
sendStatsToDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém o tempo atual da simulação em segundos.
|
||||
*
|
||||
* @return Tempo atual em segundos desde "epoch"
|
||||
*
|
||||
* "Epoch" é um ponto de referência temporal Unix (1 de janeiro de 1970).
|
||||
* Este método retorna os segundos decorridos desde esse momento.
|
||||
*/
|
||||
private double getCurrentTime() {
|
||||
return System.currentTimeMillis() / 1000.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia as estatísticas para o dashboard.
|
||||
*
|
||||
* Prepara e envia uma mensagem STATS_UPDATE com:
|
||||
* - O total de veículos processados;
|
||||
* - A média dos tempos (sistema, espera, travessia);
|
||||
* - As contagens e médias por cada tipo de veículo.
|
||||
*
|
||||
*/
|
||||
private void sendStatsToDashboard() {
|
||||
if (dashboardClient == null || !dashboardClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("totalVehicles", totalVehiclesReceived);
|
||||
stats.put("avgSystemTime", totalVehiclesReceived > 0 ? totalSystemTime / totalVehiclesReceived : 0.0);
|
||||
stats.put("avgWaitingTime", totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0);
|
||||
stats.put("avgCrossingTime", totalVehiclesReceived > 0 ? totalCrossingTime / totalVehiclesReceived : 0.0);
|
||||
|
||||
Map<String, Integer> typeCounts = new HashMap<>();
|
||||
Map<String, Double> typeAvgWait = new HashMap<>();
|
||||
for (VehicleType type : VehicleType.values()) {
|
||||
int count = vehicleTypeCount.get(type);
|
||||
typeCounts.put(type.name(), count);
|
||||
if (count > 0) {
|
||||
typeAvgWait.put(type.name(), vehicleTypeWaitTime.get(type) / count);
|
||||
}
|
||||
}
|
||||
stats.put("vehicleTypeCounts", typeCounts);
|
||||
stats.put("vehicleTypeAvgWait", typeAvgWait);
|
||||
|
||||
Message message = new Message(MessageType.STATS_UPDATE, "ExitNode", "Dashboard", stats);
|
||||
dashboardClient.send(message);
|
||||
|
||||
System.out.printf("[Exit] Sent stats to dashboard (total=%d, avg_wait=%.2fs)%n",
|
||||
totalVehiclesReceived, totalWaitingTime / totalVehiclesReceived);
|
||||
|
||||
} catch (SerializationException | IOException e) {
|
||||
System.err.println("Failed to send stats to dashboard: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Termina o processo
|
||||
*
|
||||
* Executa a seguinte sequência:
|
||||
* Imprime as estatísticas finais no terminal;
|
||||
* Envia a última atualização de estatísticas ao dashboard;
|
||||
* Fecha o socket;
|
||||
* Aguarda pela finalização das threads;
|
||||
* Fecha a ligação com o dashboard;
|
||||
*/
|
||||
public void shutdown() {
|
||||
System.out.println("\n[Exit] Shutting down...");
|
||||
running = false;
|
||||
|
||||
printFinalStatistics();
|
||||
|
||||
sendStatsToDashboard();
|
||||
|
||||
try {
|
||||
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||
serverSocket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error closing server socket: " + e.getMessage());
|
||||
}
|
||||
|
||||
connectionHandlerPool.shutdown();
|
||||
try {
|
||||
if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
connectionHandlerPool.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
connectionHandlerPool.shutdownNow();
|
||||
}
|
||||
|
||||
if (dashboardClient != null) {
|
||||
dashboardClient.close();
|
||||
}
|
||||
|
||||
System.out.println("[Exit] Shutdown complete.");
|
||||
System.out.println("=".repeat(60));
|
||||
}
|
||||
|
||||
/**
|
||||
* Imprime as estatísticas finais detalhadas no terminal
|
||||
*
|
||||
* Gera um relatório com:
|
||||
* Total de veículos que completaram a rota;
|
||||
* Médias de tempo no sistema, espera e travessia;
|
||||
* Distribuição e médias pelo tipo de veículo (BIKE, LIGHT, HEAVY);
|
||||
*
|
||||
* Este método é chamado durante o shutdown para fornecer um resumo
|
||||
* da simulação antes de terminar o processo.
|
||||
*/
|
||||
private void printFinalStatistics() {
|
||||
System.out.println("\n=== EXIT NODE STATISTICS ===");
|
||||
System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesReceived);
|
||||
|
||||
if (totalVehiclesReceived > 0) {
|
||||
System.out.printf("%nAVERAGE METRICS:%n");
|
||||
System.out.printf(" System Time: %.2f seconds%n", totalSystemTime / totalVehiclesReceived);
|
||||
System.out.printf(" Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesReceived);
|
||||
System.out.printf(" Crossing Time: %.2f seconds%n", totalCrossingTime / totalVehiclesReceived);
|
||||
}
|
||||
|
||||
System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
|
||||
for (VehicleType type : VehicleType.values()) {
|
||||
int count = vehicleTypeCount.get(type);
|
||||
if (count > 0) {
|
||||
double percentage = (count * 100.0) / totalVehiclesReceived;
|
||||
double avgWait = vehicleTypeWaitTime.get(type) / count;
|
||||
System.out.printf(" %s: %d (%.1f%%), Avg Wait: %.2fs%n",
|
||||
type, count, percentage, avgWait);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,19 +8,22 @@ import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import sd.config.SimulationConfig;
|
||||
import sd.engine.TrafficLightThread;
|
||||
import sd.model.Intersection;
|
||||
import sd.model.MessageType;
|
||||
import sd.model.TrafficLight;
|
||||
import sd.model.TrafficLightState;
|
||||
import sd.model.Vehicle;
|
||||
import sd.protocol.MessageProtocol;
|
||||
import sd.protocol.SocketConnection;
|
||||
|
||||
/**
|
||||
* Main class for an Intersection Process in the distributed traffic simulation.
|
||||
* * Each IntersectionProcess runs as an independent Java application (JVM instance)
|
||||
* * Each IntersectionProcess runs as an independent Java application (JVM
|
||||
* instance)
|
||||
* representing one of the five intersections (Cr1-Cr5) in the network.
|
||||
*/
|
||||
public class IntersectionProcess {
|
||||
@@ -39,8 +42,22 @@ public class IntersectionProcess {
|
||||
|
||||
private final ExecutorService trafficLightPool;
|
||||
|
||||
private volatile boolean running; //Quando uma thread escreve um valor volatile, todas as outras
|
||||
//threads veem a mudança imediatamente.
|
||||
private volatile boolean running; // Quando uma thread escreve um valor volatile, todas as outras
|
||||
// threads veem a mudança imediatamente.
|
||||
|
||||
// Traffic Light Coordination
|
||||
/**
|
||||
* Lock to ensure mutual exclusion between traffic lights.
|
||||
* Only one traffic light can be green at any given time within this
|
||||
* intersection.
|
||||
*/
|
||||
private final Lock trafficCoordinationLock;
|
||||
|
||||
/**
|
||||
* Tracks which direction currently has the green light.
|
||||
* null means no direction is currently green (all are red).
|
||||
*/
|
||||
private volatile String currentGreenDirection;
|
||||
|
||||
/**
|
||||
* Constructs a new IntersectionProcess.
|
||||
@@ -57,6 +74,8 @@ public class IntersectionProcess {
|
||||
this.connectionHandlerPool = Executors.newCachedThreadPool();
|
||||
this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions
|
||||
this.running = false;
|
||||
this.trafficCoordinationLock = new ReentrantLock();
|
||||
this.currentGreenDirection = null;
|
||||
|
||||
System.out.println("=".repeat(60));
|
||||
System.out.println("INTERSECTION PROCESS: " + intersectionId);
|
||||
@@ -70,13 +89,12 @@ public class IntersectionProcess {
|
||||
|
||||
configureRouting();
|
||||
|
||||
startTrafficLights();
|
||||
|
||||
System.out.println("[" + intersectionId + "] Initialization complete.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates traffic lights for this intersection based on its physical connections.
|
||||
* Creates traffic lights for this intersection based on its physical
|
||||
* connections.
|
||||
* Each intersection has different number and directions of traffic lights
|
||||
* according to the network topology.
|
||||
*/
|
||||
@@ -86,19 +104,19 @@ public class IntersectionProcess {
|
||||
String[] directions = new String[0];
|
||||
switch (intersectionId) {
|
||||
case "Cr1":
|
||||
directions = new String[]{"East", "South"};
|
||||
directions = new String[] { "East", "South" };
|
||||
break;
|
||||
case "Cr2":
|
||||
directions = new String[]{"West", "East", "South"};
|
||||
directions = new String[] { "West", "East", "South" };
|
||||
break;
|
||||
case "Cr3":
|
||||
directions = new String[]{"West", "South"};
|
||||
directions = new String[] { "West", "South" };
|
||||
break;
|
||||
case "Cr4":
|
||||
directions = new String[]{"East"};
|
||||
directions = new String[] { "East" };
|
||||
break;
|
||||
case "Cr5":
|
||||
directions = new String[]{"East"};
|
||||
directions = new String[] { "East" };
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -107,15 +125,14 @@ public class IntersectionProcess {
|
||||
double redTime = config.getTrafficLightRedTime(intersectionId, direction);
|
||||
|
||||
TrafficLight light = new TrafficLight(
|
||||
intersectionId + "-" + direction,
|
||||
direction,
|
||||
greenTime,
|
||||
redTime
|
||||
);
|
||||
intersectionId + "-" + direction,
|
||||
direction,
|
||||
greenTime,
|
||||
redTime);
|
||||
|
||||
intersection.addTrafficLight(light);
|
||||
System.out.println(" Created traffic light: " + direction +
|
||||
" (Green: " + greenTime + "s, Red: " + redTime + "s)");
|
||||
" (Green: " + greenTime + "s, Red: " + redTime + "s)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +171,29 @@ public class IntersectionProcess {
|
||||
System.out.println(" Routing configured.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permission for a traffic light to turn green.
|
||||
* Blocks until permission is granted (no other light is green).
|
||||
*
|
||||
* @param direction The direction requesting green light
|
||||
*/
|
||||
public void requestGreenLight(String direction) {
|
||||
trafficCoordinationLock.lock();
|
||||
currentGreenDirection = direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the green light permission, allowing another light to turn green.
|
||||
*
|
||||
* @param direction The direction releasing green light
|
||||
*/
|
||||
public void releaseGreenLight(String direction) {
|
||||
if (direction.equals(currentGreenDirection)) {
|
||||
currentGreenDirection = null;
|
||||
trafficCoordinationLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts all traffic light threads.
|
||||
*/
|
||||
@@ -161,107 +201,21 @@ public class IntersectionProcess {
|
||||
System.out.println("\n[" + intersectionId + "] Starting traffic light threads...");
|
||||
|
||||
for (TrafficLight light : intersection.getTrafficLights()) {
|
||||
trafficLightPool.submit(() -> runTrafficLightCycle(light));
|
||||
|
||||
TrafficLightThread lightTask = new TrafficLightThread(light, this, config);
|
||||
|
||||
trafficLightPool.submit(lightTask);
|
||||
|
||||
System.out.println(" Started thread for: " + light.getDirection());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main loop for a traffic light thread.
|
||||
* Continuously cycles between GREEN and RED states.
|
||||
*
|
||||
* @param light The traffic light to control.
|
||||
*/
|
||||
private void runTrafficLightCycle(TrafficLight light) {
|
||||
System.out.println("[" + light.getId() + "] Traffic light thread started.");
|
||||
|
||||
while (running) {
|
||||
try {
|
||||
// Green state
|
||||
light.changeState(TrafficLightState.GREEN);
|
||||
System.out.println("[" + light.getId() + "] State: GREEN");
|
||||
|
||||
// Process vehicles while green
|
||||
processGreenLight(light);
|
||||
|
||||
// Wait for green duration
|
||||
Thread.sleep((long) (light.getGreenTime() * 1000));
|
||||
|
||||
// RED state
|
||||
light.changeState(TrafficLightState.RED);
|
||||
System.out.println("[" + light.getId() + "] State: RED");
|
||||
|
||||
// Wait for red duration
|
||||
Thread.sleep((long) (light.getRedTime() * 1000));
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes vehicles when a traffic light is GREEN.
|
||||
* Dequeues vehicles and sends them to their next destination.
|
||||
*
|
||||
* @param light The traffic light that is currently green.
|
||||
*/
|
||||
private void processGreenLight(TrafficLight light) {
|
||||
while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
|
||||
Vehicle vehicle = light.removeVehicle();
|
||||
|
||||
if (vehicle != null) {
|
||||
// Get crossing time based on vehicle type
|
||||
double crossingTime = getCrossingTimeForVehicle(vehicle);
|
||||
|
||||
// Simulate crossing time
|
||||
try {
|
||||
Thread.sleep((long) (crossingTime * 1000));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
|
||||
// Update vehicle statistics
|
||||
vehicle.addCrossingTime(crossingTime);
|
||||
|
||||
// Update intersection statistics
|
||||
intersection.incrementVehiclesSent();
|
||||
|
||||
// Send vehicle to next destination
|
||||
sendVehicleToNextDestination(vehicle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the crossing time for a vehicle based on its type.
|
||||
*
|
||||
* @param vehicle The vehicle.
|
||||
* @return The crossing time in seconds.
|
||||
*/
|
||||
private double getCrossingTimeForVehicle(Vehicle vehicle) {
|
||||
switch (vehicle.getType()) {
|
||||
case BIKE:
|
||||
return config.getBikeVehicleCrossingTime();
|
||||
case LIGHT:
|
||||
return config.getLightVehicleCrossingTime();
|
||||
case HEAVY:
|
||||
return config.getHeavyVehicleCrossingTime();
|
||||
default:
|
||||
return config.getLightVehicleCrossingTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a vehicle to its next destination via socket connection.
|
||||
*
|
||||
* @param vehicle The vehicle that has crossed this intersection.
|
||||
*/
|
||||
private void sendVehicleToNextDestination(Vehicle vehicle) {
|
||||
public void sendVehicleToNextDestination(Vehicle vehicle) {
|
||||
String nextDestination = vehicle.getCurrentDestination();
|
||||
|
||||
try {
|
||||
@@ -270,21 +224,20 @@ public class IntersectionProcess {
|
||||
|
||||
// Create and send message
|
||||
MessageProtocol message = new VehicleTransferMessage(
|
||||
intersectionId,
|
||||
nextDestination,
|
||||
vehicle
|
||||
);
|
||||
intersectionId,
|
||||
nextDestination,
|
||||
vehicle);
|
||||
|
||||
connection.sendMessage(message);
|
||||
|
||||
System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() +
|
||||
" to " + nextDestination);
|
||||
" to " + nextDestination);
|
||||
|
||||
// Note: vehicle route is advanced when it arrives at the next intersection
|
||||
|
||||
} catch (IOException | InterruptedException e) {
|
||||
System.err.println("[" + intersectionId + "] Failed to send vehicle " +
|
||||
vehicle.getId() + " to " + nextDestination + ": " + e.getMessage());
|
||||
vehicle.getId() + " to " + nextDestination + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +246,7 @@ public class IntersectionProcess {
|
||||
*
|
||||
* @param destinationId The ID of the destination node.
|
||||
* @return The SocketConnection to that destination.
|
||||
* @throws IOException If connection cannot be established.
|
||||
* @throws IOException If connection cannot be established.
|
||||
* @throws InterruptedException If connection attempt is interrupted.
|
||||
*/
|
||||
private synchronized SocketConnection getOrCreateConnection(String destinationId)
|
||||
@@ -304,7 +257,7 @@ public class IntersectionProcess {
|
||||
int port = getPortForDestination(destinationId);
|
||||
|
||||
System.out.println("[" + intersectionId + "] Creating connection to " +
|
||||
destinationId + " at " + host + ":" + port);
|
||||
destinationId + " at " + host + ":" + port);
|
||||
|
||||
SocketConnection connection = new SocketConnection(host, port);
|
||||
outgoingConnections.put(destinationId, connection);
|
||||
@@ -353,6 +306,10 @@ public class IntersectionProcess {
|
||||
running = true;
|
||||
|
||||
System.out.println("\n[" + intersectionId + "] Server started on port " + port);
|
||||
|
||||
// Start traffic light threads when running is true
|
||||
startTrafficLights();
|
||||
|
||||
System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n");
|
||||
|
||||
// Main accept loop
|
||||
@@ -360,14 +317,34 @@ public class IntersectionProcess {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
|
||||
System.out.println("[" + intersectionId + "] New connection accepted from " +
|
||||
clientSocket.getInetAddress().getHostAddress());
|
||||
|
||||
// Check running flag again before handling
|
||||
if (!running) {
|
||||
clientSocket.close();
|
||||
break;
|
||||
}
|
||||
|
||||
// **Set timeout before submitting to handler**
|
||||
try {
|
||||
clientSocket.setSoTimeout(1000);
|
||||
} catch (java.net.SocketException e) {
|
||||
System.err.println("[" + intersectionId + "] Failed to set timeout: " + e.getMessage());
|
||||
clientSocket.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle each connection in a separate thread
|
||||
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
|
||||
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
System.err.println("[" + intersectionId + "] Error accepting connection: " +
|
||||
e.getMessage());
|
||||
// Expected when serverSocket.close() is called during shutdown
|
||||
if (!running) {
|
||||
break; // Normal shutdown
|
||||
}
|
||||
System.err.println("[" + intersectionId + "] Error accepting connection: " +
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,10 +356,18 @@ public class IntersectionProcess {
|
||||
* @param clientSocket The accepted socket connection.
|
||||
*/
|
||||
private void handleIncomingConnection(Socket clientSocket) {
|
||||
try {
|
||||
clientSocket.setSoTimeout(1000); // 1 second timeout
|
||||
|
||||
} catch (java.net.SocketException e) {
|
||||
System.err.println("[" + intersectionId + "] Failed to set socket timeout: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try (SocketConnection connection = new SocketConnection(clientSocket)) {
|
||||
|
||||
System.out.println("[" + intersectionId + "] New connection accepted from " +
|
||||
clientSocket.getInetAddress().getHostAddress());
|
||||
clientSocket.getInetAddress().getHostAddress());
|
||||
|
||||
// Continuously receive messages while connection is active
|
||||
while (running && connection.isConnected()) {
|
||||
@@ -393,15 +378,22 @@ public class IntersectionProcess {
|
||||
Vehicle vehicle = (Vehicle) message.getPayload();
|
||||
|
||||
System.out.println("[" + intersectionId + "] Received vehicle: " +
|
||||
vehicle.getId() + " from " + message.getSourceNode());
|
||||
vehicle.getId() + " from " + message.getSourceNode());
|
||||
|
||||
// Add vehicle to appropriate queue
|
||||
intersection.receiveVehicle(vehicle);
|
||||
}
|
||||
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
// Timeout - check running flag and continue
|
||||
if (!running) {
|
||||
break;
|
||||
}
|
||||
// Continue waiting for next message
|
||||
} catch (ClassNotFoundException e) {
|
||||
System.err.println("[" + intersectionId + "] Unknown message type received: " +
|
||||
e.getMessage());
|
||||
e.getMessage());
|
||||
break; // Invalid message, close connection
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,6 +401,7 @@ public class IntersectionProcess {
|
||||
if (running) {
|
||||
System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage());
|
||||
}
|
||||
// Expected during shutdown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,47 +410,67 @@ public class IntersectionProcess {
|
||||
* Shuts down all threads and closes all connections.
|
||||
*/
|
||||
public void shutdown() {
|
||||
// Check if already shutdown
|
||||
if (!running) {
|
||||
return; // Already shutdown, do nothing
|
||||
}
|
||||
|
||||
System.out.println("\n[" + intersectionId + "] Shutting down...");
|
||||
running = false;
|
||||
|
||||
// Close server socket
|
||||
try {
|
||||
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||
// 1. Close ServerSocket first
|
||||
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||
try {
|
||||
serverSocket.close();
|
||||
} catch (IOException e) {
|
||||
// Expected
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("[" + intersectionId + "] Error closing server socket: " +
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
// Shutdown thread pools
|
||||
trafficLightPool.shutdown();
|
||||
connectionHandlerPool.shutdown();
|
||||
|
||||
try {
|
||||
if (!trafficLightPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
trafficLightPool.shutdownNow();
|
||||
}
|
||||
if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
connectionHandlerPool.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// 2. Shutdown thread pools with force
|
||||
if (trafficLightPool != null && !trafficLightPool.isShutdown()) {
|
||||
trafficLightPool.shutdownNow();
|
||||
}
|
||||
if (connectionHandlerPool != null && !connectionHandlerPool.isShutdown()) {
|
||||
connectionHandlerPool.shutdownNow();
|
||||
}
|
||||
|
||||
// Close all outgoing connections
|
||||
for (Map.Entry<String, SocketConnection> entry : outgoingConnections.entrySet()) {
|
||||
try {
|
||||
entry.getValue().close();
|
||||
} catch (IOException e) {
|
||||
System.err.println("[" + intersectionId + "] Error closing connection to " +
|
||||
entry.getKey() + ": " + e.getMessage());
|
||||
// 3. Wait briefly for termination (don't block forever)
|
||||
try {
|
||||
if (trafficLightPool != null) {
|
||||
trafficLightPool.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
if (connectionHandlerPool != null) {
|
||||
connectionHandlerPool.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
// 4. Close outgoing connections
|
||||
synchronized (outgoingConnections) {
|
||||
for (SocketConnection conn : outgoingConnections.values()) {
|
||||
try {
|
||||
conn.close();
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
outgoingConnections.clear();
|
||||
}
|
||||
|
||||
System.out.println("[" + intersectionId + "] Shutdown complete.");
|
||||
System.out.println("=".repeat(60));
|
||||
System.out.println("============================================================\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Intersection object managed by this process.
|
||||
* Useful for testing and monitoring.
|
||||
*
|
||||
* @return The Intersection object.
|
||||
*/
|
||||
public Intersection getIntersection() {
|
||||
return intersection;
|
||||
}
|
||||
|
||||
// --- Inner class for Vehicle Transfer Messages ---
|
||||
|
||||
204
main/src/main/java/sd/coordinator/CoordinatorProcess.java
Normal file
204
main/src/main/java/sd/coordinator/CoordinatorProcess.java
Normal file
@@ -0,0 +1,204 @@
|
||||
package sd.coordinator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import sd.config.SimulationConfig;
|
||||
import sd.model.Message;
|
||||
import sd.model.MessageType;
|
||||
import sd.model.Vehicle;
|
||||
import sd.serialization.SerializationException;
|
||||
import sd.util.VehicleGenerator;
|
||||
|
||||
/**
|
||||
* Coordinator process responsible for:
|
||||
* 1. Vehicle generation (using VehicleGenerator)
|
||||
* 2. Distributing vehicles to intersection processes via sockets
|
||||
* 3. Managing simulation timing and shutdown
|
||||
*
|
||||
* This is the main entry point for the distributed simulation architecture.
|
||||
*/
|
||||
public class CoordinatorProcess {
|
||||
|
||||
private final SimulationConfig config;
|
||||
private final VehicleGenerator vehicleGenerator;
|
||||
private final Map<String, SocketClient> intersectionClients;
|
||||
private double currentTime;
|
||||
private int vehicleCounter;
|
||||
private boolean running;
|
||||
private double nextGenerationTime;
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=".repeat(60));
|
||||
System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION");
|
||||
System.out.println("=".repeat(60));
|
||||
|
||||
try {
|
||||
// 1. Load configuration
|
||||
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
|
||||
System.out.println("Loading configuration from: " + configFile);
|
||||
|
||||
SimulationConfig config = new SimulationConfig(configFile);
|
||||
CoordinatorProcess coordinator = new CoordinatorProcess(config);
|
||||
|
||||
// 2. Connect to intersection processes
|
||||
System.out.println("\n" + "=".repeat(60));
|
||||
coordinator.initialize();
|
||||
|
||||
// 3. Run the sim
|
||||
System.out.println("\n" + "=".repeat(60));
|
||||
coordinator.run();
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to load configuration: " + e.getMessage());
|
||||
System.exit(1);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Coordinator error: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
public CoordinatorProcess(SimulationConfig config) {
|
||||
this.config = config;
|
||||
this.vehicleGenerator = new VehicleGenerator(config);
|
||||
this.intersectionClients = new HashMap<>();
|
||||
this.currentTime = 0.0;
|
||||
this.vehicleCounter = 0;
|
||||
this.running = false;
|
||||
this.nextGenerationTime = 0.0;
|
||||
|
||||
System.out.println("Coordinator initialized with configuration:");
|
||||
System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s");
|
||||
System.out.println(" - Arrival model: " + config.getArrivalModel());
|
||||
System.out.println(" - Arrival rate: " + config.getArrivalRate() + " vehicles/s");
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
System.out.println("Connecting to intersection processes...");
|
||||
|
||||
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
|
||||
|
||||
for (String intersectionId : intersectionIds) {
|
||||
try {
|
||||
String host = config.getIntersectionHost(intersectionId);
|
||||
int port = config.getIntersectionPort(intersectionId);
|
||||
|
||||
SocketClient client = new SocketClient(intersectionId, host, port);
|
||||
client.connect();
|
||||
intersectionClients.put(intersectionId, client);
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to connect to " + intersectionId + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("Successfully connected to " + intersectionClients.size() + " intersection(s)");
|
||||
|
||||
if (intersectionClients.isEmpty()) {
|
||||
System.err.println("WARNING: No intersections connected. Simulation cannot proceed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
double duration = config.getSimulationDuration();
|
||||
running = true;
|
||||
|
||||
System.out.println("Starting vehicle generation simulation...");
|
||||
System.out.println("Duration: " + duration + " seconds");
|
||||
System.out.println();
|
||||
|
||||
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
|
||||
final double TIME_STEP = 0.1;
|
||||
|
||||
while (running && currentTime < duration) {
|
||||
if (currentTime >= nextGenerationTime) {
|
||||
generateAndSendVehicle();
|
||||
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
|
||||
}
|
||||
currentTime += TIME_STEP;
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("Simulation complete at t=" + String.format("%.2f", currentTime) + "s");
|
||||
System.out.println("Total vehicles generated: " + vehicleCounter);
|
||||
|
||||
shutdown();
|
||||
}
|
||||
|
||||
private void generateAndSendVehicle() {
|
||||
Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime);
|
||||
|
||||
System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
|
||||
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
|
||||
|
||||
if (vehicle.getRoute().isEmpty()) {
|
||||
System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!");
|
||||
return;
|
||||
}
|
||||
|
||||
String entryIntersection = vehicle.getRoute().get(0);
|
||||
sendVehicleToIntersection(vehicle, entryIntersection);
|
||||
}
|
||||
|
||||
private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) {
|
||||
SocketClient client = intersectionClients.get(intersectionId);
|
||||
|
||||
if (client == null || !client.isConnected()) {
|
||||
System.err.println("ERROR: No connection to " + intersectionId + " for vehicle " + vehicle.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Message message = new Message(
|
||||
MessageType.VEHICLE_SPAWN,
|
||||
"COORDINATOR",
|
||||
intersectionId,
|
||||
vehicle
|
||||
);
|
||||
|
||||
client.send(message);
|
||||
System.out.printf("->Sent to %s%n", intersectionId);
|
||||
|
||||
} catch (SerializationException | IOException e) {
|
||||
System.err.println("ERROR: Failed to send vehicle " + vehicle.getId() + " to " + intersectionId);
|
||||
System.err.println("Reason: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
System.out.println();
|
||||
System.out.println("=".repeat(60));
|
||||
System.out.println("Shutting down coordinator...");
|
||||
|
||||
for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) {
|
||||
String intersectionId = entry.getKey();
|
||||
SocketClient client = entry.getValue();
|
||||
|
||||
try {
|
||||
if (client.isConnected()) {
|
||||
Message personalizedShutdown = new Message(
|
||||
MessageType.SHUTDOWN,
|
||||
"COORDINATOR",
|
||||
intersectionId,
|
||||
"Simulation complete"
|
||||
);
|
||||
client.send(personalizedShutdown);
|
||||
System.out.println("Sent shutdown message to " + intersectionId);
|
||||
}
|
||||
} catch (SerializationException | IOException e) {
|
||||
System.err.println("Error sending shutdown to " + intersectionId + ": " + e.getMessage());
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("Coordinator shutdown complete");
|
||||
System.out.println("=".repeat(60));
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
System.out.println("\nStop signal received...");
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
124
main/src/main/java/sd/coordinator/SocketClient.java
Normal file
124
main/src/main/java/sd/coordinator/SocketClient.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package sd.coordinator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import sd.model.Message;
|
||||
import sd.serialization.MessageSerializer;
|
||||
import sd.serialization.SerializationException;
|
||||
import sd.serialization.SerializerFactory;
|
||||
|
||||
/**
|
||||
* Socket client for communication with a single intersection process.
|
||||
*
|
||||
* Handles a persistent TCP connection to one intersection,
|
||||
* providing a simple way to send serialized messages.
|
||||
*/
|
||||
public class SocketClient {
|
||||
|
||||
private final String intersectionId;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private Socket socket;
|
||||
private OutputStream outputStream;
|
||||
private MessageSerializer serializer;
|
||||
|
||||
/**
|
||||
* Creates a new SocketClient for a given intersection.
|
||||
*
|
||||
* @param intersectionId Intersection ID (ex. "Cr1")
|
||||
* @param host Host address (ex. "localhost")
|
||||
* @param port Port number
|
||||
*/
|
||||
public SocketClient(String intersectionId, String host, int port) {
|
||||
this.intersectionId = intersectionId;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.serializer = SerializerFactory.createDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the intersection process via TCP.
|
||||
*
|
||||
* @throws IOException if the connection cannot be established
|
||||
*/
|
||||
|
||||
public void connect() throws IOException {
|
||||
try {
|
||||
socket = new Socket(host, port);
|
||||
outputStream = socket.getOutputStream();
|
||||
System.out.println("Connected to " + intersectionId + " at " + host + ":" + port);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to connect to " + intersectionId + " at " + host + ":" + port);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the connected intersection.
|
||||
* The message is serialized and written over the socket.
|
||||
*
|
||||
* @param message The message to send
|
||||
* @throws SerializationException if serialization fails
|
||||
* @throws IOException if the socket write fails
|
||||
*/
|
||||
public void send(Message message) throws SerializationException, IOException {
|
||||
if (socket == null || socket.isClosed()) {
|
||||
throw new IOException("Socket is not connected to " + intersectionId);
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] data = serializer.serialize(message);
|
||||
|
||||
// Prefix with message length (so receiver knows how much to read)
|
||||
int length = data.length;
|
||||
outputStream.write((length >> 24) & 0xFF);
|
||||
outputStream.write((length >> 16) & 0xFF);
|
||||
outputStream.write((length >> 8) & 0xFF);
|
||||
outputStream.write(length & 0xFF);
|
||||
|
||||
outputStream.write(data);
|
||||
outputStream.flush();
|
||||
|
||||
} catch (SerializationException | IOException e) {
|
||||
System.err.println("Error sending message to " + intersectionId + ": " + e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the socket connection safely.
|
||||
* Calling it multiple times won’t cause issues.
|
||||
*/
|
||||
public void close() {
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
System.out.println("Closed connection to " + intersectionId);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error closing connection to " + intersectionId + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if connected and socket is open, false otherwise
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return socket != null && socket.isConnected() && !socket.isClosed();
|
||||
}
|
||||
|
||||
public String getIntersectionId() {
|
||||
return intersectionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("SocketClient[intersection=%s, host=%s, port=%d, connected=%s]",
|
||||
intersectionId, host, port, isConnected());
|
||||
}
|
||||
}
|
||||
110
main/src/main/java/sd/dashboard/DashboardClientHandler.java
Normal file
110
main/src/main/java/sd/dashboard/DashboardClientHandler.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package sd.dashboard;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
import sd.model.MessageType;
|
||||
import sd.protocol.MessageProtocol;
|
||||
import sd.protocol.SocketConnection;
|
||||
|
||||
/**
|
||||
* Processes statistics messages from a single client connection.
|
||||
* Runs in a separate thread per client.
|
||||
*/
|
||||
public class DashboardClientHandler implements Runnable {
|
||||
|
||||
private final Socket clientSocket;
|
||||
private final DashboardStatistics statistics;
|
||||
|
||||
public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) {
|
||||
this.clientSocket = clientSocket;
|
||||
this.statistics = statistics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort();
|
||||
|
||||
try (SocketConnection connection = new SocketConnection(clientSocket)) {
|
||||
System.out.println("[Handler] Started handling client: " + clientInfo);
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
MessageProtocol message = connection.receiveMessage();
|
||||
|
||||
if (message == null) {
|
||||
System.out.println("[Handler] Client disconnected: " + clientInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
processMessage(message);
|
||||
|
||||
} catch (ClassNotFoundException e) {
|
||||
System.err.println("[Handler] Unknown message class from " + clientInfo + ": " + e.getMessage());
|
||||
} catch (IOException e) {
|
||||
System.out.println("[Handler] Connection error with " + clientInfo + ": " + e.getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("[Handler] Error initializing connection with " + clientInfo + ": " + e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (!clientSocket.isClosed()) {
|
||||
clientSocket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("[Handler] Error closing socket for " + clientInfo + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processMessage(MessageProtocol message) {
|
||||
if (message.getType() != MessageType.STATS_UPDATE) {
|
||||
System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType());
|
||||
return;
|
||||
}
|
||||
|
||||
String senderId = message.getSourceNode();
|
||||
Object payload = message.getPayload();
|
||||
|
||||
System.out.println("[Handler] Received STATS_UPDATE from: " + senderId);
|
||||
|
||||
if (payload instanceof StatsUpdatePayload stats) {
|
||||
updateStatistics(senderId, stats);
|
||||
} else {
|
||||
System.err.println("[Handler] Unknown payload type: " +
|
||||
(payload != null ? payload.getClass().getName() : "null"));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatistics(String senderId, StatsUpdatePayload stats) {
|
||||
if (stats.getTotalVehiclesGenerated() >= 0) {
|
||||
statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated());
|
||||
}
|
||||
|
||||
if (stats.getTotalVehiclesCompleted() >= 0) {
|
||||
statistics.updateVehiclesCompleted(stats.getTotalVehiclesCompleted());
|
||||
}
|
||||
|
||||
if (stats.getTotalSystemTime() >= 0) {
|
||||
statistics.addSystemTime(stats.getTotalSystemTime());
|
||||
}
|
||||
|
||||
if (stats.getTotalWaitingTime() >= 0) {
|
||||
statistics.addWaitingTime(stats.getTotalWaitingTime());
|
||||
}
|
||||
|
||||
if (senderId.startsWith("Cr") || senderId.startsWith("E")) {
|
||||
statistics.updateIntersectionStats(
|
||||
senderId,
|
||||
stats.getIntersectionArrivals(),
|
||||
stats.getIntersectionDepartures(),
|
||||
stats.getIntersectionQueueSize()
|
||||
);
|
||||
}
|
||||
|
||||
System.out.println("[Handler] Successfully updated statistics from: " + senderId);
|
||||
}
|
||||
}
|
||||
148
main/src/main/java/sd/dashboard/DashboardServer.java
Normal file
148
main/src/main/java/sd/dashboard/DashboardServer.java
Normal file
@@ -0,0 +1,148 @@
|
||||
package sd.dashboard;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import sd.config.SimulationConfig;
|
||||
|
||||
/**
|
||||
* Aggregates and displays real-time statistics from all simulation processes.
|
||||
* Uses a thread pool to handle concurrent client connections.
|
||||
*/
|
||||
public class DashboardServer {
|
||||
|
||||
private final int port;
|
||||
private final DashboardStatistics statistics;
|
||||
private final ExecutorService clientHandlerPool;
|
||||
private final AtomicBoolean running;
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=".repeat(60));
|
||||
System.out.println("DASHBOARD SERVER - DISTRIBUTED TRAFFIC SIMULATION");
|
||||
System.out.println("=".repeat(60));
|
||||
|
||||
try {
|
||||
// Load configuration
|
||||
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
|
||||
System.out.println("Loading configuration from: " + configFile);
|
||||
|
||||
SimulationConfig config = new SimulationConfig(configFile);
|
||||
DashboardServer server = new DashboardServer(config);
|
||||
|
||||
// Start the server
|
||||
System.out.println("\n" + "=".repeat(60));
|
||||
server.start();
|
||||
|
||||
// Keep running until interrupted
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
System.out.println("\n\nShutdown signal received...");
|
||||
server.stop();
|
||||
}));
|
||||
|
||||
// Display statistics periodically
|
||||
server.displayLoop();
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to start Dashboard Server: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
public DashboardServer(SimulationConfig config) {
|
||||
this.port = config.getDashboardPort();
|
||||
this.statistics = new DashboardStatistics();
|
||||
this.clientHandlerPool = Executors.newFixedThreadPool(10);
|
||||
this.running = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
public void start() throws IOException {
|
||||
if (running.get()) {
|
||||
System.out.println("Dashboard Server is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
serverSocket = new ServerSocket(port);
|
||||
running.set(true);
|
||||
|
||||
System.out.println("Dashboard Server started on port " + port);
|
||||
System.out.println("Waiting for statistics updates from simulation processes...");
|
||||
System.out.println("=".repeat(60));
|
||||
|
||||
Thread acceptThread = new Thread(this::acceptConnections, "DashboardServer-Accept");
|
||||
acceptThread.setDaemon(false);
|
||||
acceptThread.start();
|
||||
}
|
||||
|
||||
private void acceptConnections() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
System.out.println("[Connection] New client connected: " +
|
||||
clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort());
|
||||
|
||||
clientHandlerPool.execute(new DashboardClientHandler(clientSocket, statistics));
|
||||
|
||||
} catch (IOException e) {
|
||||
if (running.get()) {
|
||||
System.err.println("[Error] Failed to accept client connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BusyWait")
|
||||
private void displayLoop() {
|
||||
final long DISPLAY_INTERVAL_MS = 5000;
|
||||
|
||||
while (running.get()) {
|
||||
try {
|
||||
Thread.sleep(DISPLAY_INTERVAL_MS);
|
||||
displayStatistics();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void displayStatistics() {
|
||||
System.out.println("\n" + "=".repeat(60));
|
||||
System.out.println("REAL-TIME SIMULATION STATISTICS");
|
||||
System.out.println("=".repeat(60));
|
||||
statistics.display();
|
||||
System.out.println("=".repeat(60));
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!running.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("\nStopping Dashboard Server...");
|
||||
running.set(false);
|
||||
|
||||
try {
|
||||
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||
serverSocket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error closing server socket: " + e.getMessage());
|
||||
}
|
||||
|
||||
clientHandlerPool.shutdownNow();
|
||||
System.out.println("Dashboard Server stopped.");
|
||||
}
|
||||
|
||||
public DashboardStatistics getStatistics() {
|
||||
return statistics;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running.get();
|
||||
}
|
||||
}
|
||||
214
main/src/main/java/sd/dashboard/DashboardStatistics.java
Normal file
214
main/src/main/java/sd/dashboard/DashboardStatistics.java
Normal file
@@ -0,0 +1,214 @@
|
||||
package sd.dashboard;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import sd.model.VehicleType;
|
||||
|
||||
/**
|
||||
* Thread-safe storage for aggregated simulation statistics.
|
||||
* Uses atomic types and concurrent collections for lock-free updates.
|
||||
*/
|
||||
public class DashboardStatistics {
|
||||
|
||||
private final AtomicInteger totalVehiclesGenerated;
|
||||
private final AtomicInteger totalVehiclesCompleted;
|
||||
private final AtomicLong totalSystemTime;
|
||||
private final AtomicLong totalWaitingTime;
|
||||
|
||||
private final Map<String, IntersectionStats> intersectionStats;
|
||||
private final Map<VehicleType, AtomicInteger> vehicleTypeCount;
|
||||
private final Map<VehicleType, AtomicLong> vehicleTypeWaitTime;
|
||||
|
||||
private volatile long lastUpdateTime;
|
||||
|
||||
public DashboardStatistics() {
|
||||
this.totalVehiclesGenerated = new AtomicInteger(0);
|
||||
this.totalVehiclesCompleted = new AtomicInteger(0);
|
||||
this.totalSystemTime = new AtomicLong(0);
|
||||
this.totalWaitingTime = new AtomicLong(0);
|
||||
|
||||
this.intersectionStats = new ConcurrentHashMap<>();
|
||||
this.vehicleTypeCount = new ConcurrentHashMap<>();
|
||||
this.vehicleTypeWaitTime = new ConcurrentHashMap<>();
|
||||
|
||||
for (VehicleType type : VehicleType.values()) {
|
||||
vehicleTypeCount.put(type, new AtomicInteger(0));
|
||||
vehicleTypeWaitTime.put(type, new AtomicLong(0));
|
||||
}
|
||||
|
||||
this.lastUpdateTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void updateVehiclesGenerated(int count) {
|
||||
totalVehiclesGenerated.set(count);
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void incrementVehiclesGenerated() {
|
||||
totalVehiclesGenerated.incrementAndGet();
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void updateVehiclesCompleted(int count) {
|
||||
totalVehiclesCompleted.set(count);
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void incrementVehiclesCompleted() {
|
||||
totalVehiclesCompleted.incrementAndGet();
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void addSystemTime(long timeMs) {
|
||||
totalSystemTime.addAndGet(timeMs);
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void addWaitingTime(long timeMs) {
|
||||
totalWaitingTime.addAndGet(timeMs);
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void updateVehicleTypeStats(VehicleType type, int count, long waitTimeMs) {
|
||||
vehicleTypeCount.get(type).set(count);
|
||||
vehicleTypeWaitTime.get(type).set(waitTimeMs);
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void incrementVehicleType(VehicleType type) {
|
||||
vehicleTypeCount.get(type).incrementAndGet();
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
public void updateIntersectionStats(String intersectionId, int arrivals,
|
||||
int departures, int currentQueueSize) {
|
||||
intersectionStats.compute(intersectionId, (id, stats) -> {
|
||||
if (stats == null) {
|
||||
stats = new IntersectionStats(intersectionId);
|
||||
}
|
||||
stats.updateStats(arrivals, departures, currentQueueSize);
|
||||
return stats;
|
||||
});
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
private void updateTimestamp() {
|
||||
lastUpdateTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getTotalVehiclesGenerated() {
|
||||
return totalVehiclesGenerated.get();
|
||||
}
|
||||
|
||||
public int getTotalVehiclesCompleted() {
|
||||
return totalVehiclesCompleted.get();
|
||||
}
|
||||
|
||||
public double getAverageSystemTime() {
|
||||
int completed = totalVehiclesCompleted.get();
|
||||
if (completed == 0) return 0.0;
|
||||
return (double) totalSystemTime.get() / completed;
|
||||
}
|
||||
|
||||
public double getAverageWaitingTime() {
|
||||
int completed = totalVehiclesCompleted.get();
|
||||
if (completed == 0) return 0.0;
|
||||
return (double) totalWaitingTime.get() / completed;
|
||||
}
|
||||
|
||||
public int getVehicleTypeCount(VehicleType type) {
|
||||
return vehicleTypeCount.get(type).get();
|
||||
}
|
||||
|
||||
public double getAverageWaitingTimeByType(VehicleType type) {
|
||||
int count = vehicleTypeCount.get(type).get();
|
||||
if (count == 0) return 0.0;
|
||||
return (double) vehicleTypeWaitTime.get(type).get() / count;
|
||||
}
|
||||
|
||||
public IntersectionStats getIntersectionStats(String intersectionId) {
|
||||
return intersectionStats.get(intersectionId);
|
||||
}
|
||||
|
||||
public Map<String, IntersectionStats> getAllIntersectionStats() {
|
||||
return new HashMap<>(intersectionStats);
|
||||
}
|
||||
|
||||
public long getLastUpdateTime() {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
|
||||
public void display() {
|
||||
System.out.println("\n--- GLOBAL STATISTICS ---");
|
||||
System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated());
|
||||
System.out.printf("Total Vehicles Completed: %d%n", getTotalVehiclesCompleted());
|
||||
System.out.printf("Vehicles In Transit: %d%n",
|
||||
getTotalVehiclesGenerated() - getTotalVehiclesCompleted());
|
||||
System.out.printf("Average System Time: %.2f ms%n", getAverageSystemTime());
|
||||
System.out.printf("Average Waiting Time: %.2f ms%n", getAverageWaitingTime());
|
||||
|
||||
System.out.println("\n--- VEHICLE TYPE STATISTICS ---");
|
||||
for (VehicleType type : VehicleType.values()) {
|
||||
int count = getVehicleTypeCount(type);
|
||||
double avgWait = getAverageWaitingTimeByType(type);
|
||||
System.out.printf("%s: %d vehicles, avg wait: %.2f ms%n",
|
||||
type, count, avgWait);
|
||||
}
|
||||
|
||||
System.out.println("\n--- INTERSECTION STATISTICS ---");
|
||||
if (intersectionStats.isEmpty()) {
|
||||
System.out.println("(No data received yet)");
|
||||
} else {
|
||||
for (IntersectionStats stats : intersectionStats.values()) {
|
||||
stats.display();
|
||||
}
|
||||
}
|
||||
|
||||
System.out.printf("%nLast Update: %tT%n", lastUpdateTime);
|
||||
}
|
||||
|
||||
public static class IntersectionStats {
|
||||
private final String intersectionId;
|
||||
private final AtomicInteger totalArrivals;
|
||||
private final AtomicInteger totalDepartures;
|
||||
private final AtomicInteger currentQueueSize;
|
||||
|
||||
public IntersectionStats(String intersectionId) {
|
||||
this.intersectionId = intersectionId;
|
||||
this.totalArrivals = new AtomicInteger(0);
|
||||
this.totalDepartures = new AtomicInteger(0);
|
||||
this.currentQueueSize = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
public void updateStats(int arrivals, int departures, int queueSize) {
|
||||
this.totalArrivals.set(arrivals);
|
||||
this.totalDepartures.set(departures);
|
||||
this.currentQueueSize.set(queueSize);
|
||||
}
|
||||
|
||||
public String getIntersectionId() {
|
||||
return intersectionId;
|
||||
}
|
||||
|
||||
public int getTotalArrivals() {
|
||||
return totalArrivals.get();
|
||||
}
|
||||
|
||||
public int getTotalDepartures() {
|
||||
return totalDepartures.get();
|
||||
}
|
||||
|
||||
public int getCurrentQueueSize() {
|
||||
return currentQueueSize.get();
|
||||
}
|
||||
|
||||
public void display() {
|
||||
System.out.printf("%s: Arrivals=%d, Departures=%d, Queue=%d%n",
|
||||
intersectionId, getTotalArrivals(), getTotalDepartures(), getCurrentQueueSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
48
main/src/main/java/sd/dashboard/StatsMessage.java
Normal file
48
main/src/main/java/sd/dashboard/StatsMessage.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package sd.dashboard;
|
||||
|
||||
import sd.model.MessageType;
|
||||
import sd.protocol.MessageProtocol;
|
||||
|
||||
/**
|
||||
* Message wrapper for sending statistics to the dashboard.
|
||||
*/
|
||||
public class StatsMessage implements MessageProtocol {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String sourceNode;
|
||||
private final String destinationNode;
|
||||
private final StatsUpdatePayload payload;
|
||||
|
||||
public StatsMessage(String sourceNode, StatsUpdatePayload payload) {
|
||||
this.sourceNode = sourceNode;
|
||||
this.destinationNode = "DashboardServer";
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType getType() {
|
||||
return MessageType.STATS_UPDATE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSourceNode() {
|
||||
return sourceNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDestinationNode() {
|
||||
return destinationNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("StatsMessage[from=%s, to=%s, payload=%s]",
|
||||
sourceNode, destinationNode, payload);
|
||||
}
|
||||
}
|
||||
121
main/src/main/java/sd/dashboard/StatsUpdatePayload.java
Normal file
121
main/src/main/java/sd/dashboard/StatsUpdatePayload.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package sd.dashboard;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import sd.model.VehicleType;
|
||||
|
||||
/**
|
||||
* Data transfer object for statistics updates to the dashboard.
|
||||
* Use -1 for fields not being updated in this message.
|
||||
*/
|
||||
public class StatsUpdatePayload implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int totalVehiclesGenerated = -1;
|
||||
private int totalVehiclesCompleted = -1;
|
||||
private long totalSystemTime = -1;
|
||||
private long totalWaitingTime = -1;
|
||||
|
||||
private int intersectionArrivals = 0;
|
||||
private int intersectionDepartures = 0;
|
||||
private int intersectionQueueSize = 0;
|
||||
|
||||
private Map<VehicleType, Integer> vehicleTypeCounts;
|
||||
private Map<VehicleType, Long> vehicleTypeWaitTimes;
|
||||
|
||||
public StatsUpdatePayload() {
|
||||
this.vehicleTypeCounts = new HashMap<>();
|
||||
this.vehicleTypeWaitTimes = new HashMap<>();
|
||||
}
|
||||
|
||||
public int getTotalVehiclesGenerated() {
|
||||
return totalVehiclesGenerated;
|
||||
}
|
||||
|
||||
public int getTotalVehiclesCompleted() {
|
||||
return totalVehiclesCompleted;
|
||||
}
|
||||
|
||||
public long getTotalSystemTime() {
|
||||
return totalSystemTime;
|
||||
}
|
||||
|
||||
public long getTotalWaitingTime() {
|
||||
return totalWaitingTime;
|
||||
}
|
||||
|
||||
public int getIntersectionArrivals() {
|
||||
return intersectionArrivals;
|
||||
}
|
||||
|
||||
public int getIntersectionDepartures() {
|
||||
return intersectionDepartures;
|
||||
}
|
||||
|
||||
public int getIntersectionQueueSize() {
|
||||
return intersectionQueueSize;
|
||||
}
|
||||
|
||||
public Map<VehicleType, Integer> getVehicleTypeCounts() {
|
||||
return vehicleTypeCounts;
|
||||
}
|
||||
|
||||
public Map<VehicleType, Long> getVehicleTypeWaitTimes() {
|
||||
return vehicleTypeWaitTimes;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setTotalVehiclesGenerated(int totalVehiclesGenerated) {
|
||||
this.totalVehiclesGenerated = totalVehiclesGenerated;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setTotalVehiclesCompleted(int totalVehiclesCompleted) {
|
||||
this.totalVehiclesCompleted = totalVehiclesCompleted;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setTotalSystemTime(long totalSystemTime) {
|
||||
this.totalSystemTime = totalSystemTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setTotalWaitingTime(long totalWaitingTime) {
|
||||
this.totalWaitingTime = totalWaitingTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setIntersectionArrivals(int intersectionArrivals) {
|
||||
this.intersectionArrivals = intersectionArrivals;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setIntersectionDepartures(int intersectionDepartures) {
|
||||
this.intersectionDepartures = intersectionDepartures;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setIntersectionQueueSize(int intersectionQueueSize) {
|
||||
this.intersectionQueueSize = intersectionQueueSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setVehicleTypeCounts(Map<VehicleType, Integer> vehicleTypeCounts) {
|
||||
this.vehicleTypeCounts = vehicleTypeCounts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatsUpdatePayload setVehicleTypeWaitTimes(Map<VehicleType, Long> vehicleTypeWaitTimes) {
|
||||
this.vehicleTypeWaitTimes = vehicleTypeWaitTimes;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("StatsUpdatePayload[generated=%d, completed=%d, arrivals=%d, departures=%d, queueSize=%d]",
|
||||
totalVehiclesGenerated, totalVehiclesCompleted, intersectionArrivals,
|
||||
intersectionDepartures, intersectionQueueSize);
|
||||
}
|
||||
}
|
||||
115
main/src/main/java/sd/engine/TrafficLightThread.java
Normal file
115
main/src/main/java/sd/engine/TrafficLightThread.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package sd.engine;
|
||||
|
||||
import sd.IntersectionProcess;
|
||||
import sd.config.SimulationConfig;
|
||||
import sd.model.TrafficLight;
|
||||
import sd.model.TrafficLightState;
|
||||
import sd.model.Vehicle;
|
||||
|
||||
/**
|
||||
* Implements the control logic for a single TrafficLight
|
||||
* as a Runnable task that runs in its own Thread.
|
||||
*/
|
||||
public class TrafficLightThread implements Runnable {
|
||||
|
||||
private final TrafficLight light;
|
||||
private final IntersectionProcess process;
|
||||
private final SimulationConfig config;
|
||||
private volatile boolean running;
|
||||
|
||||
// Store the thread reference for proper interruption
|
||||
private Thread currentThread;
|
||||
|
||||
public TrafficLightThread(TrafficLight light, IntersectionProcess process, SimulationConfig config) {
|
||||
this.light = light;
|
||||
this.process = process;
|
||||
this.config = config;
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.currentThread = Thread.currentThread();
|
||||
this.running = true;
|
||||
System.out.println("[" + light.getId() + "] Traffic light thread started.");
|
||||
|
||||
try {
|
||||
while (running && !Thread.currentThread().isInterrupted()) {
|
||||
|
||||
// Request permission to turn green (blocks until granted)
|
||||
process.requestGreenLight(light.getDirection());
|
||||
|
||||
try {
|
||||
// --- GREEN Phase ---
|
||||
light.changeState(TrafficLightState.GREEN);
|
||||
System.out.println("[" + light.getId() + "] State: GREEN");
|
||||
|
||||
processGreenLightQueue();
|
||||
|
||||
if (!running || Thread.currentThread().isInterrupted()) break;
|
||||
|
||||
// Wait for green duration
|
||||
Thread.sleep((long) (light.getGreenTime() * 1000));
|
||||
|
||||
if (!running || Thread.currentThread().isInterrupted()) break;
|
||||
|
||||
// --- RED Phase ---
|
||||
light.changeState(TrafficLightState.RED);
|
||||
System.out.println("[" + light.getId() + "] State: RED");
|
||||
|
||||
} finally {
|
||||
// Always release the green light permission
|
||||
process.releaseGreenLight(light.getDirection());
|
||||
}
|
||||
|
||||
// Wait for red duration
|
||||
Thread.sleep((long) (light.getRedTime() * 1000));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
this.running = false;
|
||||
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
private void processGreenLightQueue() throws InterruptedException {
|
||||
while (running && !Thread.currentThread().isInterrupted()
|
||||
&& light.getState() == TrafficLightState.GREEN
|
||||
&& light.getQueueSize() > 0) {
|
||||
|
||||
Vehicle vehicle = light.removeVehicle();
|
||||
|
||||
if (vehicle != null) {
|
||||
double crossingTime = getCrossingTimeForVehicle(vehicle);
|
||||
|
||||
Thread.sleep((long) (crossingTime * 1000));
|
||||
|
||||
vehicle.addCrossingTime(crossingTime);
|
||||
process.getIntersection().incrementVehiclesSent();
|
||||
process.sendVehicleToNextDestination(vehicle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double getCrossingTimeForVehicle(Vehicle vehicle) {
|
||||
return switch (vehicle.getType()) {
|
||||
case BIKE -> config.getBikeVehicleCrossingTime();
|
||||
case LIGHT -> config.getLightVehicleCrossingTime();
|
||||
case HEAVY -> config.getHeavyVehicleCrossingTime();
|
||||
default -> config.getLightVehicleCrossingTime();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the thread to stop gracefully.
|
||||
* Sets the running flag and interrupts the thread to unblock any sleep() calls.
|
||||
*/
|
||||
public void shutdown() {
|
||||
this.running = false;
|
||||
if (currentThread != null && currentThread.isAlive()) {
|
||||
currentThread.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,8 +114,8 @@ public class Intersection {
|
||||
public void receiveVehicle(Vehicle vehicle) {
|
||||
totalVehiclesReceived++;
|
||||
|
||||
// Advance route since vehicle just arrived at this intersection
|
||||
vehicle.advanceRoute();
|
||||
// Note: Route advancement is handled by SimulationEngine.handleVehicleArrival()
|
||||
// before calling this method, so we don't advance here.
|
||||
|
||||
String nextDestination = vehicle.getCurrentDestination();
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package sd.protocol;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ConnectException;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import sd.serialization.MessageSerializer;
|
||||
import sd.serialization.SerializationException;
|
||||
import sd.serialization.SerializerFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper class that simplifies communication via Sockets.
|
||||
* Includes connection retry logic for robustness.
|
||||
@@ -17,8 +25,9 @@ import java.util.concurrent.TimeUnit;
|
||||
public class SocketConnection implements Closeable {
|
||||
|
||||
private final Socket socket;
|
||||
private final ObjectOutputStream outputStream;
|
||||
private final ObjectInputStream inputStream;
|
||||
private final OutputStream outputStream;
|
||||
private final InputStream inputStream;
|
||||
private final MessageSerializer serializer;
|
||||
|
||||
// --- Configuration for Retry Logic ---
|
||||
/** Maximum number of connection attempts. */
|
||||
@@ -88,16 +97,11 @@ public class SocketConnection implements Closeable {
|
||||
|
||||
// If connection was successful, assign to final variable and create streams
|
||||
this.socket = tempSocket;
|
||||
try {
|
||||
// IMPORTANT: The order is crucial. OutputStream first.
|
||||
this.outputStream = new ObjectOutputStream(socket.getOutputStream());
|
||||
this.inputStream = new ObjectInputStream(socket.getInputStream());
|
||||
} catch (IOException e) {
|
||||
// If stream creation fails even after successful socket connection, clean up.
|
||||
System.err.println("[SocketConnection] Failed to create streams after connection: " + e.getMessage());
|
||||
try { socket.close(); } catch (IOException closeEx) { /* ignore */ }
|
||||
throw e; // Re-throw the stream creation exception
|
||||
}
|
||||
|
||||
this.outputStream = socket.getOutputStream();
|
||||
this.inputStream = socket.getInputStream();
|
||||
this.serializer = SerializerFactory.createDefault();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -111,12 +115,10 @@ public class SocketConnection implements Closeable {
|
||||
*/
|
||||
public SocketConnection(Socket acceptedSocket) throws IOException {
|
||||
this.socket = acceptedSocket;
|
||||
this.outputStream = socket.getOutputStream();
|
||||
this.inputStream = socket.getInputStream();
|
||||
this.serializer = SerializerFactory.createDefault();
|
||||
|
||||
// IMPORTANT: The order is crucial. OutputStream first.
|
||||
this.outputStream = new ObjectOutputStream(socket.getOutputStream());
|
||||
this.inputStream = new ObjectInputStream(socket.getInputStream());
|
||||
System.out.printf("[SocketConnection] Connection accepted from %s:%d.%n",
|
||||
acceptedSocket.getInetAddress().getHostAddress(), acceptedSocket.getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,52 +128,66 @@ public class SocketConnection implements Closeable {
|
||||
* @throws IOException If writing to the stream fails or socket is not connected.
|
||||
*/
|
||||
public void sendMessage(MessageProtocol message) throws IOException {
|
||||
if (!isConnected()) {
|
||||
throw new IOException("Socket is not connected.");
|
||||
if (socket == null || !socket.isConnected()) {
|
||||
throw new IOException("Socket is not connected");
|
||||
}
|
||||
synchronized (outputStream) {
|
||||
outputStream.writeObject(message);
|
||||
outputStream.flush(); // Ensures the message is sent immediately.
|
||||
|
||||
try {
|
||||
// Serializa para bytes JSON
|
||||
byte[] data = serializer.serialize(message);
|
||||
|
||||
// Write 4-byte length prefix
|
||||
DataOutputStream dataOut = new DataOutputStream(outputStream);
|
||||
dataOut.writeInt(data.length);
|
||||
dataOut.write(data);
|
||||
dataOut.flush();
|
||||
|
||||
} catch (SerializationException e) {
|
||||
throw new IOException("Failed to serialize message", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to read (deserialize) a MessageProtocol object from the socket.
|
||||
* This call is "blocked" until an object is received.
|
||||
*
|
||||
* @return The "envelope" (MessageProtocol) that was received.
|
||||
* @throws IOException If the connection is lost, the stream is corrupted, or socket is not connected.
|
||||
* @throws ClassNotFoundException If the received object is unknown.
|
||||
*/
|
||||
public MessageProtocol receiveMessage() throws IOException, ClassNotFoundException {
|
||||
if (!isConnected()) {
|
||||
throw new IOException("Socket is not connected.");
|
||||
if (socket == null || !socket.isConnected()) {
|
||||
throw new IOException("Socket is not connected");
|
||||
}
|
||||
synchronized (inputStream) {
|
||||
return (MessageProtocol) inputStream.readObject();
|
||||
|
||||
try {
|
||||
// Lê um prefixo de 4 bytes - indicador de tamanho
|
||||
DataInputStream dataIn = new DataInputStream(inputStream);
|
||||
int length = dataIn.readInt();
|
||||
|
||||
if (length <= 0 || length > 10_000_000) { // Sanity check (10MB max)
|
||||
throw new IOException("Invalid message length: " + length);
|
||||
}
|
||||
|
||||
// Ler dados da mensagem
|
||||
byte[] data = new byte[length];
|
||||
dataIn.readFully(data);
|
||||
|
||||
// Deserialize do JSON
|
||||
return serializer.deserialize(data, MessageProtocol.class);
|
||||
|
||||
} catch (SerializationException e) {
|
||||
throw new IOException("Failed to deserialize message", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the socket and all streams (Input and Output).
|
||||
* It is called automatically if you use 'try-with-resources'.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
System.out.printf("[SocketConnection] Closing connection to %s:%d.%n",
|
||||
socket != null ? socket.getInetAddress().getHostAddress() : "N/A",
|
||||
socket != null ? socket.getPort() : -1);
|
||||
try {
|
||||
if (inputStream != null) inputStream.close();
|
||||
} catch (IOException e) { /* ignore */ }
|
||||
|
||||
try {
|
||||
if (outputStream != null) outputStream.close();
|
||||
} catch (IOException e) { /* ignore */ }
|
||||
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
if (inputStream != null) inputStream.close();
|
||||
if (outputStream != null) outputStream.close();
|
||||
if (socket != null) socket.close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -19,6 +19,7 @@ import sd.IntersectionProcess;
|
||||
import sd.model.MessageType;
|
||||
import sd.model.Vehicle;
|
||||
import sd.model.VehicleType;
|
||||
import sd.protocol.SocketConnection;
|
||||
|
||||
/**
|
||||
* Tests for IntersectionProcess - covers initialization, traffic lights,
|
||||
@@ -96,11 +97,17 @@ public class IntersectionProcessTest {
|
||||
Files.writeString(configFile, configContent);
|
||||
}
|
||||
|
||||
// cleanup after tests
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
if (intersectionProcess != null) {
|
||||
intersectionProcess.shutdown();
|
||||
try {
|
||||
// Only shutdown if still running
|
||||
intersectionProcess.shutdown();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error in tearDown: " + e.getMessage());
|
||||
} finally {
|
||||
intersectionProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +194,7 @@ public class IntersectionProcessTest {
|
||||
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
|
||||
intersectionProcess.initialize();
|
||||
|
||||
// start server in seperate thread
|
||||
// start server in separate thread
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
@@ -197,13 +204,22 @@ public class IntersectionProcessTest {
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
Thread.sleep(500); // wait for server to start
|
||||
|
||||
// try connecting to check if its running
|
||||
try (Socket clientSocket = new Socket("localhost", 18001)) {
|
||||
assertTrue(clientSocket.isConnected());
|
||||
// Wait for server to actually start with retries
|
||||
boolean serverReady = false;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
Thread.sleep(100);
|
||||
try (Socket testSocket = new Socket()) {
|
||||
testSocket.connect(new java.net.InetSocketAddress("localhost", 18001), 500);
|
||||
serverReady = true;
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
// Server not ready yet, continue waiting
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(serverReady, "Server should start and bind to port 18001");
|
||||
|
||||
// Shutdown immediately after confirming server is running
|
||||
intersectionProcess.shutdown();
|
||||
serverThread.join(2000);
|
||||
}
|
||||
@@ -219,11 +235,17 @@ public class IntersectionProcessTest {
|
||||
cr2.initialize();
|
||||
|
||||
Thread thread1 = new Thread(() -> {
|
||||
try { cr1.start(); } catch (IOException e) { }
|
||||
try {
|
||||
cr1.start();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
|
||||
Thread thread2 = new Thread(() -> {
|
||||
try { cr2.start(); } catch (IOException e) { }
|
||||
try {
|
||||
cr2.start();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
|
||||
thread1.start();
|
||||
@@ -233,7 +255,7 @@ public class IntersectionProcessTest {
|
||||
|
||||
// check both are running
|
||||
try (Socket socket1 = new Socket("localhost", 18001);
|
||||
Socket socket2 = new Socket("localhost", 18002)) {
|
||||
Socket socket2 = new Socket("localhost", 18002)) {
|
||||
assertTrue(socket1.isConnected());
|
||||
assertTrue(socket2.isConnected());
|
||||
}
|
||||
@@ -256,29 +278,31 @@ public class IntersectionProcessTest {
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
} catch (IOException e) { }
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
Thread.sleep(500);
|
||||
|
||||
// create test vehicle
|
||||
java.util.List<String> route = Arrays.asList("Cr2", "Cr3", "S");
|
||||
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
||||
try {
|
||||
// create test vehicle - FIXED: use 4-parameter constructor
|
||||
java.util.List<String> route = Arrays.asList("Cr2", "Cr3", "S");
|
||||
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
||||
|
||||
// send vehicle from Cr1 to Cr2
|
||||
try (Socket socket = new Socket("localhost", 18002)) {
|
||||
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
|
||||
// send vehicle from Cr1 to Cr2 - FIXED: use SocketConnection
|
||||
try (Socket socket = new Socket("localhost", 18002);
|
||||
SocketConnection conn = new SocketConnection(socket)) {
|
||||
|
||||
TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
|
||||
out.writeObject(message);
|
||||
out.flush();
|
||||
TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
|
||||
conn.sendMessage(message);
|
||||
|
||||
Thread.sleep(1000); // wait for procesing
|
||||
Thread.sleep(1000); // wait for processing
|
||||
}
|
||||
} finally {
|
||||
intersectionProcess.shutdown();
|
||||
serverThread.join(2000);
|
||||
}
|
||||
|
||||
intersectionProcess.shutdown();
|
||||
serverThread.join(2000);
|
||||
}
|
||||
|
||||
// routing config tests
|
||||
@@ -312,7 +336,8 @@ public class IntersectionProcessTest {
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
} catch (IOException e) { }
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
@@ -330,30 +355,35 @@ public class IntersectionProcessTest {
|
||||
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
|
||||
intersectionProcess.initialize();
|
||||
|
||||
// Start server in separate thread
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
} catch (IOException e) { }
|
||||
} catch (IOException e) {
|
||||
// Expected on shutdown
|
||||
}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
// Wait for server to start
|
||||
Thread.sleep(500);
|
||||
|
||||
// verify server running
|
||||
try (Socket socket = new Socket("localhost", 18001)) {
|
||||
assertTrue(socket.isConnected());
|
||||
}
|
||||
|
||||
// Shutdown
|
||||
intersectionProcess.shutdown();
|
||||
serverThread.join(2000);
|
||||
|
||||
// after shutdown conection should fail
|
||||
Thread.sleep(500);
|
||||
Exception exception = assertThrows(IOException.class, () -> {
|
||||
Socket socket = new Socket("localhost", 18001);
|
||||
socket.close();
|
||||
});
|
||||
assertNotNull(exception);
|
||||
// Give shutdown time to complete
|
||||
Thread.sleep(200);
|
||||
|
||||
// Verify we cannot connect (server socket is closed)
|
||||
boolean connectionFailed = false;
|
||||
try (Socket testSocket = new Socket()) {
|
||||
testSocket.connect(new InetSocketAddress("localhost", 18001), 500);
|
||||
} catch (IOException e) {
|
||||
connectionFailed = true; // Expected - server should be closed
|
||||
}
|
||||
|
||||
assertTrue(connectionFailed, "Server socket should be closed after shutdown");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -365,7 +395,8 @@ public class IntersectionProcessTest {
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
} catch (IOException e) { }
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
@@ -388,45 +419,68 @@ public class IntersectionProcessTest {
|
||||
@Test
|
||||
@Timeout(15)
|
||||
public void testIntegration_TwoIntersectionsVehicleTransfer() throws IOException, InterruptedException {
|
||||
// setup 2 intersections
|
||||
IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString());
|
||||
IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString());
|
||||
IntersectionProcess cr1 = null;
|
||||
IntersectionProcess cr2 = null;
|
||||
Thread thread1 = null;
|
||||
Thread thread2 = null;
|
||||
|
||||
cr1.initialize();
|
||||
cr2.initialize();
|
||||
try {
|
||||
// setup 2 intersections
|
||||
cr1 = new IntersectionProcess("Cr1", configFile.toString());
|
||||
cr2 = new IntersectionProcess("Cr2", configFile.toString());
|
||||
|
||||
// start both
|
||||
Thread thread1 = new Thread(() -> {
|
||||
try { cr1.start(); } catch (IOException e) { }
|
||||
});
|
||||
cr1.initialize();
|
||||
cr2.initialize();
|
||||
|
||||
Thread thread2 = new Thread(() -> {
|
||||
try { cr2.start(); } catch (IOException e) { }
|
||||
});
|
||||
// start both
|
||||
final IntersectionProcess cr1Final = cr1;
|
||||
thread1 = new Thread(() -> {
|
||||
try {
|
||||
cr1Final.start();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
|
||||
thread1.start();
|
||||
thread2.start();
|
||||
final IntersectionProcess cr2Final = cr2;
|
||||
thread2 = new Thread(() -> {
|
||||
try {
|
||||
cr2Final.start();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
});
|
||||
|
||||
Thread.sleep(1000); // wait for servers
|
||||
thread1.start();
|
||||
thread2.start();
|
||||
|
||||
// send vehicle to Cr1 that goes to Cr2
|
||||
java.util.List<String> route = Arrays.asList("Cr1", "Cr2", "S");
|
||||
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
||||
Thread.sleep(1000); // wait for servers
|
||||
|
||||
try (Socket socket = new Socket("localhost", 18001)) {
|
||||
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
|
||||
// send vehicle to Cr1 that goes to Cr2 - FIXED: use 4-parameter constructor
|
||||
java.util.List<String> route = Arrays.asList("Cr1", "Cr2", "S");
|
||||
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
||||
|
||||
TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle);
|
||||
out.writeObject(message);
|
||||
out.flush();
|
||||
// FIXED: use SocketConnection
|
||||
try (Socket socket = new Socket("localhost", 18001);
|
||||
SocketConnection conn = new SocketConnection(socket)) {
|
||||
|
||||
Thread.sleep(2000); // time for processing
|
||||
TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle);
|
||||
conn.sendMessage(message);
|
||||
|
||||
Thread.sleep(2000); // time for processing
|
||||
}
|
||||
} finally {
|
||||
if (cr1 != null) {
|
||||
cr1.shutdown();
|
||||
}
|
||||
if (cr2 != null) {
|
||||
cr2.shutdown();
|
||||
}
|
||||
if (thread1 != null) {
|
||||
thread1.join(2000);
|
||||
}
|
||||
if (thread2 != null) {
|
||||
thread2.join(2000);
|
||||
}
|
||||
}
|
||||
|
||||
cr1.shutdown();
|
||||
cr2.shutdown();
|
||||
thread1.join(2000);
|
||||
thread2.join(2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
327
main/src/test/java/sd/ExitNodeProcessTest.java
Normal file
327
main/src/test/java/sd/ExitNodeProcessTest.java
Normal file
@@ -0,0 +1,327 @@
|
||||
package sd;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import sd.config.SimulationConfig;
|
||||
|
||||
/**
|
||||
* Testes unitários para a classe ExitNodeProcess.
|
||||
*
|
||||
* Esta classe de testes verifica:
|
||||
* - Construção e inicialização do processo
|
||||
* - Criação e aceitação de conexões do servidor socket
|
||||
* - Gestão do ciclo de vida (start/shutdown)
|
||||
* - Processamento concorrente de múltiplas conexões
|
||||
* - Impressão de estatísticas finais
|
||||
*
|
||||
* Os testes utilizam configurações temporárias e portas dedicadas (19001)
|
||||
* para evitar conflitos com outros testes ou processos em execução.
|
||||
*/
|
||||
public class ExitNodeProcessTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private Path configFile;
|
||||
private ExitNodeProcess exitNodeProcess;
|
||||
private Thread exitNodeThread;
|
||||
|
||||
/**
|
||||
* Configura o ambiente de teste antes de cada teste.
|
||||
* Cria um ficheiro de configuração temporário com as definições necessárias.
|
||||
*/
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException {
|
||||
configFile = tempDir.resolve("test-simulation.properties");
|
||||
|
||||
String configContent = """
|
||||
# Test Exit Node Configuration
|
||||
|
||||
# Exit Configuration
|
||||
exit.host=localhost
|
||||
exit.port=19001
|
||||
|
||||
# Dashboard Configuration (will not be running in tests)
|
||||
dashboard.host=localhost
|
||||
dashboard.port=19000
|
||||
|
||||
# Vehicle Crossing Times
|
||||
vehicle.bike.crossingTime=2.0
|
||||
vehicle.light.crossingTime=3.0
|
||||
vehicle.heavy.crossingTime=5.0
|
||||
|
||||
# Simulation Duration
|
||||
simulation.duration=60.0
|
||||
""";
|
||||
|
||||
Files.writeString(configFile, configContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa os recursos após cada teste.
|
||||
* Garante que o processo e threads são terminados corretamente.
|
||||
*/
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
if (exitNodeProcess != null) {
|
||||
exitNodeProcess.shutdown();
|
||||
}
|
||||
if (exitNodeThread != null && exitNodeThread.isAlive()) {
|
||||
exitNodeThread.interrupt();
|
||||
try {
|
||||
exitNodeThread.join(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa a construção bem-sucedida do ExitNodeProcess com configuração válida.
|
||||
*/
|
||||
@Test
|
||||
public void testConstructor_Success() throws IOException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
assertNotNull(exitNodeProcess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa que uma exceção é lançada quando a configuração é inválida.
|
||||
*/
|
||||
@Test
|
||||
public void testConstructor_InvalidConfig() {
|
||||
Exception exception = assertThrows(IOException.class, () -> {
|
||||
new SimulationConfig("non-existent-config.properties");
|
||||
});
|
||||
assertNotNull(exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa a inicialização sem dashboard disponível.
|
||||
* Verifica que o processo continua a funcionar mesmo sem conexão ao dashboard.
|
||||
*/
|
||||
@Test
|
||||
public void testInitialize_WithoutDashboard() throws IOException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
assertDoesNotThrow(() -> exitNodeProcess.initialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa que o servidor socket é criado corretamente na porta configurada.
|
||||
* Verifica que é possível estabelecer uma conexão ao socket do servidor.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||
public void testStart_ServerSocketCreated() throws IOException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
exitNodeProcess.initialize();
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
exitNodeThread = new Thread(() -> {
|
||||
try {
|
||||
latch.countDown();
|
||||
exitNodeProcess.start();
|
||||
} catch (IOException e) {
|
||||
// expected when shutdown
|
||||
}
|
||||
});
|
||||
|
||||
exitNodeThread.start();
|
||||
|
||||
try {
|
||||
assertTrue(latch.await(2, TimeUnit.SECONDS), "Exit node should start within timeout");
|
||||
Thread.sleep(100);
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
try (Socket testSocket = new Socket("localhost", 19001)) {
|
||||
assertTrue(testSocket.isConnected());
|
||||
}
|
||||
});
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa que o servidor aceita conexões de clientes.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||
public void testStart_AcceptsConnection() throws IOException, InterruptedException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
exitNodeProcess.initialize();
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
exitNodeThread = new Thread(() -> {
|
||||
try {
|
||||
latch.countDown();
|
||||
exitNodeProcess.start();
|
||||
} catch (IOException e) {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
|
||||
exitNodeThread.start();
|
||||
|
||||
assertTrue(latch.await(2, TimeUnit.SECONDS));
|
||||
Thread.sleep(200);
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
try (Socket socket = new Socket("localhost", 19001)) {
|
||||
assertTrue(socket.isConnected());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa múltiplas inicializações e encerramentos do processo.
|
||||
* Verifica que o processo pode ser iniciado e parado múltiplas vezes,
|
||||
* permitindo reutilização da porta.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||
public void testMultipleStartStop() throws IOException, InterruptedException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
exitNodeProcess.initialize();
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
exitNodeThread = new Thread(() -> {
|
||||
try {
|
||||
latch.countDown();
|
||||
exitNodeProcess.start();
|
||||
} catch (IOException e) {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
|
||||
exitNodeThread.start();
|
||||
assertTrue(latch.await(2, TimeUnit.SECONDS));
|
||||
Thread.sleep(100);
|
||||
|
||||
exitNodeProcess.shutdown();
|
||||
Thread.sleep(100);
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
SimulationConfig config2 = new SimulationConfig(configFile.toString());
|
||||
ExitNodeProcess exitNode2 = new ExitNodeProcess(config2);
|
||||
exitNode2.initialize();
|
||||
exitNode2.shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa que o shutdown fecha corretamente o servidor socket.
|
||||
* Após o shutdown, novas conexões ao socket devem falhar.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||
public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
exitNodeProcess.initialize();
|
||||
|
||||
CountDownLatch startLatch = new CountDownLatch(1);
|
||||
|
||||
exitNodeThread = new Thread(() -> {
|
||||
try {
|
||||
startLatch.countDown();
|
||||
exitNodeProcess.start();
|
||||
} catch (IOException e) {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
|
||||
exitNodeThread.start();
|
||||
assertTrue(startLatch.await(2, TimeUnit.SECONDS));
|
||||
Thread.sleep(200);
|
||||
|
||||
exitNodeProcess.shutdown();
|
||||
Thread.sleep(200);
|
||||
|
||||
assertThrows(IOException.class, () -> {
|
||||
Socket socket = new Socket("localhost", 19001);
|
||||
socket.close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa que as estatísticas finais são impressas corretamente durante o shutdown.
|
||||
* Verifica que o método não lança exceções mesmo sem dados processados.
|
||||
*/
|
||||
@Test
|
||||
public void testPrintFinalStatistics() throws IOException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
exitNodeProcess.initialize();
|
||||
|
||||
assertDoesNotThrow(() -> exitNodeProcess.shutdown());
|
||||
}
|
||||
|
||||
/**
|
||||
* Testa o processamento de múltiplas conexões concorrentes.
|
||||
* Verifica que o servidor consegue lidar com vários clientes simultaneamente
|
||||
* usando o pool de threads.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||
public void testMultipleConcurrentConnections() throws IOException, InterruptedException {
|
||||
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||
exitNodeProcess = new ExitNodeProcess(config);
|
||||
exitNodeProcess.initialize();
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
exitNodeThread = new Thread(() -> {
|
||||
try {
|
||||
latch.countDown();
|
||||
exitNodeProcess.start();
|
||||
} catch (IOException e) {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
|
||||
exitNodeThread.start();
|
||||
assertTrue(latch.await(2, TimeUnit.SECONDS));
|
||||
Thread.sleep(200);
|
||||
|
||||
Thread[] clients = new Thread[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
clients[i] = new Thread(() -> {
|
||||
try (Socket socket = new Socket("localhost", 19001)) {
|
||||
assertTrue(socket.isConnected());
|
||||
Thread.sleep(100);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
clients[i].start();
|
||||
}
|
||||
|
||||
for (Thread client : clients) {
|
||||
client.join(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
main/src/test/java/sd/TrafficLightCoordinationTest.java
Normal file
206
main/src/test/java/sd/TrafficLightCoordinationTest.java
Normal file
@@ -0,0 +1,206 @@
|
||||
package sd;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import sd.model.TrafficLight;
|
||||
import sd.model.TrafficLightState;
|
||||
|
||||
/**
|
||||
* Test class to verify traffic light coordination within an intersection.
|
||||
* Ensures that only ONE traffic light can be GREEN at any given time.
|
||||
*/
|
||||
public class TrafficLightCoordinationTest {
|
||||
|
||||
private IntersectionProcess intersectionProcess;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException {
|
||||
// Create an intersection with multiple traffic lights
|
||||
intersectionProcess = new IntersectionProcess("Cr2", "src/main/resources/simulation.properties");
|
||||
intersectionProcess.initialize();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() throws InterruptedException {
|
||||
if (intersectionProcess != null) {
|
||||
intersectionProcess.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that verifies mutual exclusion between traffic lights.
|
||||
* Monitors all traffic lights for 10 seconds and ensures that
|
||||
* at most ONE light is GREEN at any point in time.
|
||||
*/
|
||||
@Test
|
||||
public void testOnlyOneGreenLightAtATime() throws InterruptedException {
|
||||
System.out.println("\n=== Testing Traffic Light Mutual Exclusion ===");
|
||||
|
||||
// Start the intersection
|
||||
Thread intersectionThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
intersectionThread.start();
|
||||
|
||||
// Monitor traffic lights for violations
|
||||
AtomicInteger maxGreenSimultaneously = new AtomicInteger(0);
|
||||
AtomicInteger violationCount = new AtomicInteger(0);
|
||||
List<String> violations = new ArrayList<>();
|
||||
|
||||
// Monitor for 10 seconds
|
||||
long endTime = System.currentTimeMillis() + 10000;
|
||||
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
int greenCount = 0;
|
||||
StringBuilder currentState = new StringBuilder("States: ");
|
||||
|
||||
for (TrafficLight light : intersectionProcess.getIntersection().getTrafficLights()) {
|
||||
TrafficLightState state = light.getState();
|
||||
currentState.append(light.getDirection()).append("=").append(state).append(" ");
|
||||
|
||||
if (state == TrafficLightState.GREEN) {
|
||||
greenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update maximum simultaneous green lights
|
||||
if (greenCount > maxGreenSimultaneously.get()) {
|
||||
maxGreenSimultaneously.set(greenCount);
|
||||
}
|
||||
|
||||
// Check for violations (more than one green)
|
||||
if (greenCount > 1) {
|
||||
violationCount.incrementAndGet();
|
||||
String violation = String.format("[VIOLATION] %d lights GREEN simultaneously: %s",
|
||||
greenCount, currentState.toString());
|
||||
violations.add(violation);
|
||||
System.err.println(violation);
|
||||
}
|
||||
|
||||
Thread.sleep(50); // Check every 50ms
|
||||
}
|
||||
|
||||
System.out.println("\n=== Test Results ===");
|
||||
System.out.println("Maximum simultaneous GREEN lights: " + maxGreenSimultaneously.get());
|
||||
System.out.println("Total violations detected: " + violationCount.get());
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
System.err.println("\nViolation details:");
|
||||
violations.forEach(System.err::println);
|
||||
}
|
||||
|
||||
// Assert that we never had more than one green light
|
||||
assertEquals(0, violationCount.get(),
|
||||
"Traffic light coordination violated! Multiple lights were GREEN simultaneously.");
|
||||
assertTrue(maxGreenSimultaneously.get() <= 1,
|
||||
"At most ONE light should be GREEN at any time. Found: " + maxGreenSimultaneously.get());
|
||||
|
||||
System.out.println("\nTraffic light coordination working correctly!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that verifies all traffic lights get a chance to be GREEN.
|
||||
* Ensures fairness in the coordination mechanism.
|
||||
*/
|
||||
@Test
|
||||
public void testAllLightsGetGreenTime() throws InterruptedException {
|
||||
System.out.println("\n=== Testing Traffic Light Fairness ===");
|
||||
|
||||
// Start the intersection
|
||||
Thread intersectionThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
intersectionThread.start();
|
||||
|
||||
// Track which lights have been green
|
||||
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
|
||||
boolean[] hasBeenGreen = new boolean[lights.size()];
|
||||
|
||||
// Monitor for 15 seconds (enough time for all lights to cycle)
|
||||
long endTime = System.currentTimeMillis() + 15000;
|
||||
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
for (int i = 0; i < lights.size(); i++) {
|
||||
if (lights.get(i).getState() == TrafficLightState.GREEN) {
|
||||
hasBeenGreen[i] = true;
|
||||
System.out.println("✓ " + lights.get(i).getDirection() + " has been GREEN");
|
||||
}
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
|
||||
// Check if all lights got green time
|
||||
int greenCount = 0;
|
||||
System.out.println("\n=== Fairness Results ===");
|
||||
for (int i = 0; i < lights.size(); i++) {
|
||||
String status = hasBeenGreen[i] ? "✓ YES" : "✗ NO";
|
||||
System.out.println(lights.get(i).getDirection() + " got GREEN time: " + status);
|
||||
if (hasBeenGreen[i]) greenCount++;
|
||||
}
|
||||
|
||||
assertTrue(greenCount > 0, "At least one light should have been GREEN during the test");
|
||||
System.out.println("\n" + greenCount + "/" + lights.size() + " lights were GREEN during test period");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that verifies the state transitions are consistent.
|
||||
*/
|
||||
@Test
|
||||
public void testStateTransitionsAreConsistent() throws InterruptedException {
|
||||
System.out.println("\n=== Testing State Transition Consistency ===");
|
||||
|
||||
Thread intersectionThread = new Thread(() -> {
|
||||
try {
|
||||
intersectionProcess.start();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
intersectionThread.start();
|
||||
|
||||
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
|
||||
TrafficLightState[] previousStates = new TrafficLightState[lights.size()];
|
||||
|
||||
// Initialize previous states
|
||||
for (int i = 0; i < lights.size(); i++) {
|
||||
previousStates[i] = lights.get(i).getState();
|
||||
}
|
||||
|
||||
int transitionCount = 0;
|
||||
long endTime = System.currentTimeMillis() + 8000;
|
||||
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
for (int i = 0; i < lights.size(); i++) {
|
||||
TrafficLightState currentState = lights.get(i).getState();
|
||||
|
||||
if (currentState != previousStates[i]) {
|
||||
transitionCount++;
|
||||
System.out.println(lights.get(i).getDirection() + " transitioned: " +
|
||||
previousStates[i] + " → " + currentState);
|
||||
previousStates[i] = currentState;
|
||||
}
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
|
||||
System.out.println("\nTotal state transitions observed: " + transitionCount);
|
||||
assertTrue(transitionCount > 0, "There should be state transitions during the test period");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package sd.coordinator;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
|
||||
import sd.model.Message;
|
||||
import sd.model.MessageType;
|
||||
import sd.model.Vehicle;
|
||||
import sd.serialization.MessageSerializer;
|
||||
import sd.serialization.SerializerFactory;
|
||||
|
||||
/**
|
||||
* Integration tests for the Coordinator-side networking.
|
||||
*
|
||||
* What we’re checking here:
|
||||
* 1. A SocketClient can actually connect to something listening
|
||||
* 2. Messages go over the wire and can be deserialized
|
||||
* 3. Vehicle payloads survive the trip
|
||||
* 4. Shutdown messages can be broadcast to multiple intersections
|
||||
*
|
||||
* We do this by spinning up a tiny mock intersection server in-process.
|
||||
*/
|
||||
class CoordinatorIntegrationTest {
|
||||
|
||||
private List<MockIntersectionServer> mockServers;
|
||||
private static final int BASE_PORT = 9001; // keep clear of real ports
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockServers = new ArrayList<>();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// Stop all mock servers
|
||||
for (MockIntersectionServer server : mockServers) {
|
||||
server.stop();
|
||||
}
|
||||
mockServers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the client open a TCP connection to our fake intersection?
|
||||
*/
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void testSocketClientConnection() throws IOException, InterruptedException {
|
||||
MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT);
|
||||
server.start();
|
||||
mockServers.add(server);
|
||||
|
||||
// tiny pause to let the server bind
|
||||
Thread.sleep(100);
|
||||
|
||||
SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT);
|
||||
client.connect();
|
||||
|
||||
assertTrue(client.isConnected(), "Client should be connected to mock intersection");
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* End-to-end: send a message, make sure the server actually receives it.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void testMessageTransmission() throws Exception {
|
||||
MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT);
|
||||
server.start();
|
||||
mockServers.add(server);
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT);
|
||||
client.connect();
|
||||
|
||||
Message testMessage = new Message(
|
||||
MessageType.VEHICLE_SPAWN,
|
||||
"COORDINATOR",
|
||||
"Cr1",
|
||||
"Test payload"
|
||||
);
|
||||
|
||||
client.send(testMessage);
|
||||
|
||||
// give the server a moment to read and deserialize
|
||||
Thread.sleep(200);
|
||||
|
||||
assertFalse(
|
||||
server.getReceivedMessages().isEmpty(),
|
||||
"Mock server should have received at least one message"
|
||||
);
|
||||
|
||||
Message receivedMsg = server.getReceivedMessages().poll();
|
||||
assertNotNull(receivedMsg, "Server should have actually received a message");
|
||||
assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message type should match what we sent");
|
||||
assertEquals("COORDINATOR", receivedMsg.getSenderId(), "Sender ID should be preserved");
|
||||
assertEquals("Cr1", receivedMsg.getDestinationId(), "Destination ID should be preserved");
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure vehicle payloads survive the trip and arrive non-null.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void testVehicleSpawnMessage() throws Exception {
|
||||
MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT);
|
||||
server.start();
|
||||
mockServers.add(server);
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT);
|
||||
client.connect();
|
||||
|
||||
// fake a vehicle like the coordinator would send
|
||||
List<String> route = List.of("Cr1", "Cr4", "Cr5", "S");
|
||||
Vehicle vehicle = new Vehicle("V1", sd.model.VehicleType.LIGHT, 0.0, route);
|
||||
|
||||
Message spawnMessage = new Message(
|
||||
MessageType.VEHICLE_SPAWN,
|
||||
"COORDINATOR",
|
||||
"Cr1",
|
||||
vehicle
|
||||
);
|
||||
|
||||
client.send(spawnMessage);
|
||||
|
||||
Thread.sleep(200);
|
||||
|
||||
Message receivedMsg = server.getReceivedMessages().poll();
|
||||
assertNotNull(receivedMsg, "Mock server should receive the spawn message");
|
||||
assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message should be of type VEHICLE_SPAWN");
|
||||
assertNotNull(receivedMsg.getPayload(), "Payload should not be null (vehicle must arrive)");
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast shutdown to multiple mock intersections and see if all of them get it.
|
||||
*/
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void testShutdownMessageBroadcast() throws Exception {
|
||||
// Start a couple of fake intersections
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
MockIntersectionServer server = new MockIntersectionServer("Cr" + i, BASE_PORT + i - 1);
|
||||
server.start();
|
||||
mockServers.add(server);
|
||||
}
|
||||
|
||||
Thread.sleep(200);
|
||||
|
||||
// Connect to all of them
|
||||
List<SocketClient> clients = new ArrayList<>();
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
SocketClient client = new SocketClient("Cr" + i, "localhost", BASE_PORT + i - 1);
|
||||
client.connect();
|
||||
clients.add(client);
|
||||
}
|
||||
|
||||
Message shutdownMessage = new Message(
|
||||
MessageType.SHUTDOWN,
|
||||
"COORDINATOR",
|
||||
"ALL",
|
||||
"Simulation complete"
|
||||
);
|
||||
|
||||
for (SocketClient client : clients) {
|
||||
client.send(shutdownMessage);
|
||||
}
|
||||
|
||||
Thread.sleep(200);
|
||||
|
||||
for (MockIntersectionServer server : mockServers) {
|
||||
assertFalse(
|
||||
server.getReceivedMessages().isEmpty(),
|
||||
"Server " + server.getIntersectionId() + " should have received the shutdown message"
|
||||
);
|
||||
|
||||
Message msg = server.getReceivedMessages().poll();
|
||||
assertEquals(MessageType.SHUTDOWN, msg.getType(), "Server should receive a SHUTDOWN message");
|
||||
}
|
||||
|
||||
for (SocketClient client : clients) {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiny TCP server that pretends to be an intersection.
|
||||
* It:
|
||||
* - listens on a port
|
||||
* - accepts connections
|
||||
* - reads length-prefixed messages
|
||||
* - deserializes them and stores them for the test to inspect
|
||||
*/
|
||||
private static class MockIntersectionServer {
|
||||
private final String intersectionId;
|
||||
private final int port;
|
||||
private ServerSocket serverSocket;
|
||||
private Thread serverThread;
|
||||
private volatile boolean running;
|
||||
private final ConcurrentLinkedQueue<Message> receivedMessages;
|
||||
private final MessageSerializer serializer;
|
||||
|
||||
public MockIntersectionServer(String intersectionId, int port) {
|
||||
this.intersectionId = intersectionId;
|
||||
this.port = port;
|
||||
this.receivedMessages = new ConcurrentLinkedQueue<>();
|
||||
this.serializer = SerializerFactory.createDefault();
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
public void start() throws IOException {
|
||||
serverSocket = new ServerSocket(port);
|
||||
running = true;
|
||||
|
||||
System.out.printf("Mock %s listening on port %d%n", intersectionId, port);
|
||||
|
||||
serverThread = new Thread(() -> {
|
||||
try {
|
||||
while (running) {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
handleClient(clientSocket);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
System.err.println("Mock " + intersectionId + " server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}, "mock-" + intersectionId + "-listener");
|
||||
|
||||
serverThread.start();
|
||||
}
|
||||
|
||||
private void handleClient(Socket clientSocket) {
|
||||
new Thread(() -> {
|
||||
try (DataInputStream input = new DataInputStream(clientSocket.getInputStream())) {
|
||||
while (running) {
|
||||
// Read length prefix (4 bytes, big-endian)
|
||||
int length = input.readInt();
|
||||
byte[] data = new byte[length];
|
||||
input.readFully(data);
|
||||
|
||||
Message message = serializer.deserialize(data, Message.class);
|
||||
receivedMessages.offer(message);
|
||||
|
||||
System.out.println("Mock " + intersectionId + " received: " + message.getType());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
System.err.println("Mock " + intersectionId + " client handler error: " + e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Mock " + intersectionId + " deserialization error: " + e.getMessage());
|
||||
}
|
||||
}, "mock-" + intersectionId + "-client").start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
try {
|
||||
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||
serverSocket.close();
|
||||
}
|
||||
if (serverThread != null) {
|
||||
serverThread.interrupt();
|
||||
serverThread.join(1000);
|
||||
}
|
||||
System.out.printf("Mock %s stopped%n", intersectionId);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
System.err.println("Error stopping mock server " + intersectionId + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public ConcurrentLinkedQueue<Message> getReceivedMessages() {
|
||||
return receivedMessages;
|
||||
}
|
||||
|
||||
public String getIntersectionId() {
|
||||
return intersectionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
194
main/src/test/java/sd/coordinator/CoordinatorProcessTest.java
Normal file
194
main/src/test/java/sd/coordinator/CoordinatorProcessTest.java
Normal file
@@ -0,0 +1,194 @@
|
||||
package sd.coordinator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import sd.config.SimulationConfig;
|
||||
import sd.model.Vehicle;
|
||||
import sd.util.VehicleGenerator;
|
||||
|
||||
/**
|
||||
* Tests for the Coordinator/vehicle-generation layer.
|
||||
*
|
||||
* What we’re checking here:
|
||||
* 1. Coordinator can be created with a valid config
|
||||
* 2. Vehicle arrival times are monotonic and sane
|
||||
* 3. Vehicle IDs are created in the format we expect (V1, V2, ...)
|
||||
* 4. Generated vehicles have proper routes (start at CrX, end at S)
|
||||
* 5. Config actually has intersection info
|
||||
* 6. Duration in config is not something crazy
|
||||
*/
|
||||
class CoordinatorProcessTest {
|
||||
|
||||
private SimulationConfig config;
|
||||
private static final String TEST_CONFIG = "src/main/resources/simulation.properties";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
config = new SimulationConfig(TEST_CONFIG);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
config = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic smoke test: can we build a coordinator with this config?
|
||||
*/
|
||||
@Test
|
||||
void testCoordinatorInitialization() {
|
||||
CoordinatorProcess coordinator = new CoordinatorProcess(config);
|
||||
assertNotNull(coordinator, "Coordinator should be created with a valid config");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the VehicleGenerator is giving us increasing arrival times,
|
||||
* i.e. time doesn’t go backwards and intervals look reasonable.
|
||||
*/
|
||||
@Test
|
||||
void testVehicleGenerationTiming() {
|
||||
VehicleGenerator generator = new VehicleGenerator(config);
|
||||
|
||||
double currentTime = 0.0;
|
||||
List<Double> arrivalTimes = new ArrayList<>();
|
||||
|
||||
// generate a small batch to inspect
|
||||
for (int i = 0; i < 10; i++) {
|
||||
double nextArrival = generator.getNextArrivalTime(currentTime);
|
||||
arrivalTimes.add(nextArrival);
|
||||
currentTime = nextArrival;
|
||||
}
|
||||
|
||||
// times should strictly increase
|
||||
for (int i = 1; i < arrivalTimes.size(); i++) {
|
||||
assertTrue(
|
||||
arrivalTimes.get(i) > arrivalTimes.get(i - 1),
|
||||
"Arrival times must increase — got " + arrivalTimes.get(i - 1) + " then " + arrivalTimes.get(i)
|
||||
);
|
||||
}
|
||||
|
||||
// and they shouldn't be nonsense
|
||||
for (double time : arrivalTimes) {
|
||||
assertTrue(time >= 0, "Arrival time should not be negative (got " + time + ")");
|
||||
assertTrue(time < 1000, "Arrival time looks suspiciously large: " + time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We generate V1..V5 manually and make sure the IDs are exactly those.
|
||||
*/
|
||||
@Test
|
||||
void testVehicleIdGeneration() {
|
||||
VehicleGenerator generator = new VehicleGenerator(config);
|
||||
|
||||
List<Vehicle> vehicles = new ArrayList<>();
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
Vehicle v = generator.generateVehicle("V" + i, 0.0);
|
||||
vehicles.add(v);
|
||||
assertEquals("V" + i, v.getId(), "Vehicle ID should be 'V" + i + "' but got " + v.getId());
|
||||
}
|
||||
|
||||
// just to be safe, no duplicates in that small set
|
||||
long distinctCount = vehicles.stream().map(Vehicle::getId).distinct().count();
|
||||
assertEquals(5, distinctCount, "Vehicle IDs in this batch should all be unique");
|
||||
}
|
||||
|
||||
/**
|
||||
* A generated vehicle should:
|
||||
* - have a non-empty route
|
||||
* - start in a known intersection (Cr1..Cr5)
|
||||
* - end in S (exit)
|
||||
*/
|
||||
@Test
|
||||
void testVehicleRouteValidity() {
|
||||
VehicleGenerator generator = new VehicleGenerator(config);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
Vehicle vehicle = generator.generateVehicle("V" + i, 0.0);
|
||||
|
||||
assertNotNull(vehicle.getRoute(), "Vehicle route should not be null");
|
||||
assertFalse(vehicle.getRoute().isEmpty(), "Vehicle route should not be empty");
|
||||
|
||||
String firstHop = vehicle.getRoute().get(0);
|
||||
assertTrue(
|
||||
firstHop.matches("Cr[1-5]"),
|
||||
"First hop should be a valid intersection (Cr1..Cr5), got: " + firstHop
|
||||
);
|
||||
|
||||
String lastHop = vehicle.getRoute().get(vehicle.getRoute().size() - 1);
|
||||
assertEquals("S", lastHop, "Last hop should be exit 'S' but got: " + lastHop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whatever is in simulation.properties should give us a sane duration.
|
||||
*/
|
||||
@Test
|
||||
void testSimulationDuration() {
|
||||
double duration = config.getSimulationDuration();
|
||||
assertTrue(duration > 0, "Simulation duration must be positive");
|
||||
assertTrue(duration >= 1.0, "Simulation should run at least 1 second (got " + duration + ")");
|
||||
assertTrue(duration <= 86400.0, "Simulation should not run more than a day (got " + duration + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the 5 intersections defined in the architecture
|
||||
* actually exist in the config and have valid network data.
|
||||
*/
|
||||
@Test
|
||||
void testIntersectionConfiguration() {
|
||||
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
|
||||
|
||||
for (String id : intersectionIds) {
|
||||
String host = config.getIntersectionHost(id);
|
||||
int port = config.getIntersectionPort(id);
|
||||
|
||||
assertNotNull(host, "Host should not be null for " + id);
|
||||
assertFalse(host.isEmpty(), "Host should not be empty for " + id);
|
||||
assertTrue(port > 0, "Port should be > 0 for " + id + " (got " + port + ")");
|
||||
assertTrue(port < 65536, "Port should be a valid TCP port for " + id + " (got " + port + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick sanity check: over a bunch of generated vehicles,
|
||||
* we should eventually see the different vehicle types appear.
|
||||
*
|
||||
* Note: this is probabilistic, so we're not being super strict.
|
||||
*/
|
||||
@Test
|
||||
void testVehicleTypeDistribution() {
|
||||
VehicleGenerator generator = new VehicleGenerator(config);
|
||||
|
||||
boolean hasBike = false;
|
||||
boolean hasLight = false;
|
||||
boolean hasHeavy = false;
|
||||
|
||||
// 50 is enough for a "we're probably fine" test
|
||||
for (int i = 0; i < 50; i++) {
|
||||
Vehicle vehicle = generator.generateVehicle("V" + i, 0.0);
|
||||
|
||||
switch (vehicle.getType()) {
|
||||
case BIKE -> hasBike = true;
|
||||
case LIGHT -> hasLight = true;
|
||||
case HEAVY -> hasHeavy = true;
|
||||
}
|
||||
}
|
||||
|
||||
// at least one of them should have shown up — if not, RNG is cursed
|
||||
assertTrue(
|
||||
hasBike || hasLight || hasHeavy,
|
||||
"Expected to see at least one vehicle type after 50 generations"
|
||||
);
|
||||
}
|
||||
}
|
||||
164
main/src/test/java/sd/dashboard/DashboardTest.java
Normal file
164
main/src/test/java/sd/dashboard/DashboardTest.java
Normal file
@@ -0,0 +1,164 @@
|
||||
package sd.dashboard;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import sd.config.SimulationConfig;
|
||||
import sd.model.VehicleType;
|
||||
|
||||
/**
|
||||
* Unit tests for Dashboard Server components.
|
||||
*/
|
||||
class DashboardTest {
|
||||
|
||||
private DashboardStatistics statistics;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
statistics = new DashboardStatistics();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
statistics = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitialStatistics() {
|
||||
assertEquals(0, statistics.getTotalVehiclesGenerated(),
|
||||
"Initial vehicles generated should be 0");
|
||||
assertEquals(0, statistics.getTotalVehiclesCompleted(),
|
||||
"Initial vehicles completed should be 0");
|
||||
assertEquals(0.0, statistics.getAverageSystemTime(),
|
||||
"Initial average system time should be 0.0");
|
||||
assertEquals(0.0, statistics.getAverageWaitingTime(),
|
||||
"Initial average waiting time should be 0.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVehicleCounters() {
|
||||
statistics.incrementVehiclesGenerated();
|
||||
assertEquals(1, statistics.getTotalVehiclesGenerated());
|
||||
|
||||
statistics.updateVehiclesGenerated(10);
|
||||
assertEquals(10, statistics.getTotalVehiclesGenerated());
|
||||
|
||||
statistics.incrementVehiclesCompleted();
|
||||
assertEquals(1, statistics.getTotalVehiclesCompleted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAverageCalculations() {
|
||||
// Add 3 completed vehicles with known times
|
||||
statistics.updateVehiclesCompleted(3);
|
||||
statistics.addSystemTime(3000); // 3000ms total
|
||||
statistics.addWaitingTime(1500); // 1500ms total
|
||||
|
||||
assertEquals(1000.0, statistics.getAverageSystemTime(), 0.01,
|
||||
"Average system time should be 1000ms");
|
||||
assertEquals(500.0, statistics.getAverageWaitingTime(), 0.01,
|
||||
"Average waiting time should be 500ms");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVehicleTypeStatistics() {
|
||||
statistics.incrementVehicleType(VehicleType.LIGHT);
|
||||
statistics.incrementVehicleType(VehicleType.LIGHT);
|
||||
statistics.incrementVehicleType(VehicleType.HEAVY);
|
||||
|
||||
assertEquals(2, statistics.getVehicleTypeCount(VehicleType.LIGHT));
|
||||
assertEquals(1, statistics.getVehicleTypeCount(VehicleType.HEAVY));
|
||||
assertEquals(0, statistics.getVehicleTypeCount(VehicleType.BIKE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIntersectionStatistics() {
|
||||
statistics.updateIntersectionStats("Cr1", 10, 8, 2);
|
||||
|
||||
DashboardStatistics.IntersectionStats stats =
|
||||
statistics.getIntersectionStats("Cr1");
|
||||
|
||||
assertNotNull(stats, "Intersection stats should not be null");
|
||||
assertEquals("Cr1", stats.getIntersectionId());
|
||||
assertEquals(10, stats.getTotalArrivals());
|
||||
assertEquals(8, stats.getTotalDepartures());
|
||||
assertEquals(2, stats.getCurrentQueueSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleIntersections() {
|
||||
statistics.updateIntersectionStats("Cr1", 10, 8, 2);
|
||||
statistics.updateIntersectionStats("Cr2", 15, 12, 3);
|
||||
statistics.updateIntersectionStats("Cr3", 5, 5, 0);
|
||||
|
||||
assertEquals(3, statistics.getAllIntersectionStats().size(),
|
||||
"Should have 3 intersections");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStatsUpdatePayload() {
|
||||
StatsUpdatePayload payload = new StatsUpdatePayload()
|
||||
.setTotalVehiclesGenerated(50)
|
||||
.setTotalVehiclesCompleted(20)
|
||||
.setIntersectionArrivals(30)
|
||||
.setIntersectionDepartures(25)
|
||||
.setIntersectionQueueSize(5);
|
||||
|
||||
assertEquals(50, payload.getTotalVehiclesGenerated());
|
||||
assertEquals(20, payload.getTotalVehiclesCompleted());
|
||||
assertEquals(30, payload.getIntersectionArrivals());
|
||||
assertEquals(25, payload.getIntersectionDepartures());
|
||||
assertEquals(5, payload.getIntersectionQueueSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStatsMessage() {
|
||||
StatsUpdatePayload payload = new StatsUpdatePayload()
|
||||
.setIntersectionArrivals(10);
|
||||
|
||||
StatsMessage message = new StatsMessage("Cr1", payload);
|
||||
|
||||
assertEquals("Cr1", message.getSourceNode());
|
||||
assertEquals("DashboardServer", message.getDestinationNode());
|
||||
assertEquals(sd.model.MessageType.STATS_UPDATE, message.getType());
|
||||
assertNotNull(message.getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testThreadSafety() throws InterruptedException {
|
||||
// Test concurrent updates
|
||||
Thread t1 = new Thread(() -> {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
statistics.incrementVehiclesGenerated();
|
||||
}
|
||||
});
|
||||
|
||||
Thread t2 = new Thread(() -> {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
statistics.incrementVehiclesGenerated();
|
||||
}
|
||||
});
|
||||
|
||||
t1.start();
|
||||
t2.start();
|
||||
t1.join();
|
||||
t2.join();
|
||||
|
||||
assertEquals(200, statistics.getTotalVehiclesGenerated(),
|
||||
"Concurrent increments should total 200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDashboardServerCreation() throws Exception {
|
||||
SimulationConfig config = new SimulationConfig("simulation.properties");
|
||||
DashboardServer server = new DashboardServer(config);
|
||||
|
||||
assertNotNull(server, "Server should be created successfully");
|
||||
assertNotNull(server.getStatistics(), "Statistics should be initialized");
|
||||
assertFalse(server.isRunning(), "Server should not be running initially");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user