52 Commits

Author SHA1 Message Date
David Alves
ecb70fa6a2 Merge pull request #33 from davidalves04/17-create-dashboardserver-process
Dashboard Server Implementation
2025-11-19 19:16:50 +00:00
06f079ce5b fix intersections starting independently with no coordination 2025-11-18 14:29:11 +00:00
72893f87ae added dashboard server and built an example implementation for the message protocol 2025-11-14 02:01:51 +00:00
6b94d727e2 shutdown and teardown fixes + incoming connection handler 2025-11-11 17:28:44 +00:00
84cba39597 bullshit fixes 2025-11-06 20:31:59 +00:00
5dc1b40c88 Merge pull request #32 from davidalves04/14-create-trafficlightthread-class
14 create trafficlightthread class
2025-11-06 13:53:12 +00:00
3117bdf332 Merge branch 'dev' into 14-create-trafficlightthread-class 2025-11-06 13:53:01 +00:00
1140c3ca48 Merge pull request #30 from davidalves04/13-create-exit-node-process
13 create exit node process
2025-11-06 13:49:21 +00:00
Gaa56
484cba1eee Update TrafficLightThread 2025-11-05 13:21:10 +00:00
Gaa56
0e5526c3f6 Merge pull request #31 from davidalves04/dev
Dev
2025-11-05 12:37:48 +00:00
David Alves
cf88db4297 Add traffic light coordination and tests
Sorry to add this on this branch ahah
2025-11-05 12:09:32 +00:00
David Alves
0960a7a141 Add ExitNodeProcess and unit tests 2025-11-05 11:54:34 +00:00
David Alves
3b4f968a59 Merge pull request #29 from davidalves04/12-implement-coordinatorgenerator-process
Coordinator Process Implementation
2025-11-03 00:02:56 +00:00
0c256ad6f5 Fix Intersection Destination - Doubled Advance 2025-11-02 23:56:54 +00:00
340e436063 Merge branch 'dev' into 12-implement-coordinatorgenerator-process 2025-11-02 23:21:36 +00:00
1684a6713e Implementation of the Coordinator Process 2025-11-02 23:17:15 +00:00
22a7081ade Merge pull request #28 from davidalves04/10-create-network-communication-classes
Fix Serialization
2025-11-02 22:39:38 +00:00
3b699556db Merge branch 'dev' into 10-create-network-communication-classes 2025-11-02 22:39:26 +00:00
Gaa56
d078808486 Update SocketConnection 2025-10-30 19:25:27 +00:00
Gaa56
98581b562d Merge pull request #27 from davidalves04/9-design-message-protocol-specification
#10 Req
2025-10-30 18:44:54 +00:00
Gaa56
4710c96450 Create TrafficLightThread Class 2025-10-30 18:06:02 +00:00
f9644bd18c Merge pull request #26 from davidalves04/dev
#12 Req.
2025-10-30 16:09:04 +00:00
David Alves
c6b710ac52 Merge pull request #25 from davidalves04/11-convert-intersection-to-standalone-process
11 convert intersection to standalone process
2025-10-30 16:00:05 +00:00
David Alves
dc4f567e1f Move vehicle route advancement to intersection arrival 2025-10-30 15:57:58 +00:00
David Alves
db5e01021a Refactor IntersectionProcess and add unit tests 2025-10-30 10:41:17 +00:00
David Alves
dab0651dbd Corrected directions 2025-10-29 22:36:58 +00:00
David Alves
4772add574 Merge pull request #24 from davidalves04/dev
Dev
2025-10-27 23:04:15 +00:00
David Alves
ae27115791 Merge pull request #23 from davidalves04/11-convert-intersection-to-standalone-process
Create IntersectionProcess main class
2025-10-27 22:58:55 +00:00
David Alves
684fb408ef Create IntersectionProcess main class 2025-10-27 22:53:37 +00:00
David Alves
d057adeab3 Revert "Enunciado uploaded"
This reverts commit be4e7f66d6.
2025-10-27 22:52:19 +00:00
David Alves
be4e7f66d6 Enunciado uploaded 2025-10-27 18:03:17 +00:00
fd26063f6e Merge pull request #22 from davidalves04/10-create-network-communication-classes
Create network communication classes
2025-10-27 12:29:22 +00:00
Gaa56
d8b59cc502 Deleted MessageSerializer 2025-10-27 09:18:33 +00:00
Gaa56
06c34a198a Removed MessageSerializer 2025-10-27 09:15:33 +00:00
Gaa56
1524188b29 Add connection retry logic 2025-10-26 17:00:34 +00:00
Gaa56
bc1a8da160 Create MessageSerializer utility 2025-10-25 18:00:58 +01:00
Gaa56
96903e4b7c SocketConnection 2025-10-25 17:43:25 +01:00
Gaa56
6c5eab0e72 Create SocketConnection wrapper class 2025-10-25 17:41:55 +01:00
23f7a74798 Add dependency build to CI job 2025-10-24 20:20:15 +01:00
d7dec0d73e Merge pull request #21 from davidalves04/9-design-message-protocol-specification
Mmessage protocol specification
2025-10-24 20:12:18 +01:00
8e95bc4c01 Testing job 2025-10-23 00:46:52 +01:00
33ed84b0c2 Enhance Maven workflow with release publishing
Added a publish-release job to create a GitHub release with the built JAR file when a tag is pushed.
2025-10-23 00:36:13 +01:00
9093b13c5d Rollback
Oops
2025-10-23 00:27:37 +01:00
12b7aabe87 Enhance CI workflow with security and dependency checks
Added security scan and dependency review jobs to the workflow.
2025-10-23 00:21:36 +01:00
c30aa25de0 Update Maven workflow to use JDK 17 and improve steps 2025-10-23 00:14:34 +01:00
3689f7a207 Set working directory for dependency graph update
Specify working directory for dependency graph update
2025-10-23 00:08:16 +01:00
bb18c1119e Update Maven build file path to main/pom.xml 2025-10-23 00:02:55 +01:00
f0dbdb551d Add GitHub Actions workflow for Java CI with Maven
This workflow builds a Java project using Maven, caches dependencies, and updates the dependency graph for improved Dependabot alerts.
2025-10-23 00:01:55 +01:00
f519c9aba7 Merge pull request #20 from davidalves04/8-single-process-prototype
Step 2: Single-Process Prototype
2025-10-22 23:51:19 +01:00
1216089e80 Step 2 - Finishing touches 2025-10-22 23:37:27 +01:00
211ea25ca5 Step 2 - Finishing touches 2025-10-22 23:36:41 +01:00
Gaa56
3fe467a2a3 Create MessageProtocol interface 2025-10-22 19:19:28 +01:00
26 changed files with 4211 additions and 38 deletions

61
.github/workflows/maven.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Java CI with Maven
on:
push:
branches: [ "main" ]
tags:
- 'v*.*.*'
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package
working-directory: main
- name: Upload built JAR
uses: actions/upload-artifact@v4
with:
name: package
path: main/target/*.jar
- name: Generate dependency graph
run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph
- name: Upload dependency graph artifact
uses: actions/upload-artifact@v4
with:
name: dependency-graph
path: main/target/**
publish-release:
runs-on: ubuntu-latest
needs: [build]
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download built JAR
uses: actions/download-artifact@v4
with:
name: package
path: main/target/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: main/target/*.jar

134
STEP2_SUMMARY.md Normal file
View File

@@ -0,0 +1,134 @@
# 🏁 Single-Process Prototype — Implementation Summary
**Status:** ✅ Complete
**Date:** October 22, 2025
**Branch:** `8-single-process-prototype`
---
## Overview
The single-process prototype implements a **discrete event simulation (DES)** of a 3×3 urban grid with five intersections, realistic vehicle behavior, and fully synchronized traffic lights. Everything runs under one process, laying the groundwork for the distributed architecture in Phase 3.
---
## Core Architecture
### **SimulationEngine**
Drives the DES loop with a priority queue of timestamped events — vehicles, lights, crossings, and periodic stats updates. Handles five intersections (Cr1Cr5) and six event types.
**Main loop:**
```
while (events && time < duration):
event = nextEvent()
time = event.timestamp
handle(event)
```
### **VehicleGenerator**
Spawns vehicles via:
* **Poisson arrivals** (λ = 0.5 veh/s) or fixed intervals
* **Probabilistic routes** from E1E3
* **Type distribution**: 20% BIKE, 60% LIGHT, 20% HEAVY
### **StatisticsCollector**
Tracks system-wide and per-type metrics: throughput, avg. wait, queue sizes, light cycles — updated every 10 s and at simulation end.
---
## Model Highlights
* **Vehicle** type, route, timings, lifecycle.
* **Intersection** routing tables, traffic lights, queues.
* **TrafficLight** red/green cycles with FIFO queues.
* **Event** timestamped, comparable; 6 types for all DES actions.
---
## Configuration (`simulation.properties`)
```properties
simulation.duration=60.0
simulation.arrival.model=POISSON
simulation.arrival.rate=0.5
vehicle.bike.crossingTime=1.5
vehicle.light.crossingTime=2.0
vehicle.heavy.crossingTime=4.0
statistics.update.interval=10.0
```
**Speed logic:**
`t_bike = 0.5×t_car`, `t_heavy = 2×t_car`.
---
## Topology
```
E1→Cr1→Cr4→Cr5→S
E2→Cr2→Cr5→S
E3→Cr3→S
Bi-dir: Cr1↔Cr2, Cr2↔Cr3
```
---
## Results
**Unit Tests:** 7/7 ✅
**60-Second Simulation:**
* Generated: 22 vehicles
* Completed: 5 (22.7%)
* Avg system time: 15.47 s
* Throughput: 0.08 veh/s
* All lights & intersections operational
**Performance:**
~0.03 s real-time run (≈2000× speed-up), < 50 MB RAM.
---
## Code Structure
```
sd/
├── engine/SimulationEngine.java
├── model/{Vehicle,Intersection,TrafficLight,Event}.java
├── util/{VehicleGenerator,StatisticsCollector}.java
└── config/SimulationConfig.java
```
---
## Key Flow
1. Initialize intersections, lights, first events.
2. Process events chronologically.
3. Vehicles follow routes queue cross exit.
4. Lights toggle, queues drain, stats update.
5. Print summary and performance metrics.
---
## Next Steps — Phase 3
* Split intersections into independent **processes**.
* Add **socket-based communication**.
* Run **traffic lights as threads**.
* Enable **distributed synchronization** and fault handling.
---
## TL;DR
Solid single-process DES
Everythings working traffic lights, routing, vehicles, stats.
Ready to go distributed next.

27
TODO.md
View File

@@ -1,3 +1,26 @@
## ✅ SINGLE-PROCESS PROTOTYPE - COMPLETED
### Phase 2 Status: DONE ✅
All components for the single-process prototype have been successfully implemented and tested:
-**SimulationEngine** - Priority queue-based discrete event simulation
-**VehicleGenerator** - Poisson and Fixed arrival models
-**StatisticsCollector** - Comprehensive metrics tracking
-**Entry point** - Main simulation runner
-**60s test simulation** - Successfully validated event processing and routing
### Test Results:
- All 7 unit tests passing
- 60-second simulation completed successfully
- Generated 22 vehicles with 5 completing their routes
- Traffic light state changes working correctly
- Vehicle routing through intersections validated
---
## NEXT: Distributed Architecture Implementation
### Compreender os Conceitos Fundamentais ### Compreender os Conceitos Fundamentais
Primeiro, as tecnologias e paradigmas chave necessários para este projeto devem ser totalmente compreendidos. Primeiro, as tecnologias e paradigmas chave necessários para este projeto devem ser totalmente compreendidos.
@@ -16,7 +39,7 @@ Primeiro, as tecnologias e paradigmas chave necessários para este projeto devem
- Uma **lista de eventos** central, frequentemente uma fila de prioridades, será necessária para armazenar eventos futuros, ordenados pelo seu timestamp. O ciclo principal da simulação retira o próximo evento da lista, processa-o e adiciona quaisquer novos eventos que resultem dele. - Uma **lista de eventos** central, frequentemente uma fila de prioridades, será necessária para armazenar eventos futuros, ordenados pelo seu timestamp. O ciclo principal da simulação retira o próximo evento da lista, processa-o e adiciona quaisquer novos eventos que resultem dele.
- **Processo de Poisson:** Para o modelo "mais realista" de chegadas de veículos, é especificado um processo de Poisson. A principal conclusão é que o tempo _entre_ chegadas consecutivas de veículos segue uma **distribuição exponencial**. Em Java, este intervalo pode ser gerado usando `Math.log(1 - Math.random()) / -lambda`, onde `lambda` (λi) é a taxa de chegada especificada. - **Processo de Poisson:** Para o modelo 'mais realista' de chegadas de veículos, é especificado um processo de Poisson. A principal conclusão é que o tempo _entre_ chegadas consecutivas de veículos segue uma **distribuição exponencial**. Em Java, este intervalo pode ser gerado usando `Math.log(1 - Math.random()) / -lambda`, onde `lambda` (λi) é a taxa de chegada especificada.
--- ---
@@ -172,4 +195,4 @@ Assim que o sistema completo estiver a funcionar, as experiências exigidas pela
- **Debugging:** Debugging de sistemas distribuídos podem ser difíceis. Uma framework de logging (como Log4j 2 ou SLF4J) pode ser usada para registar eventos//alterações de estado nos diferentes processos. - **Debugging:** Debugging de sistemas distribuídos podem ser difíceis. Uma framework de logging (como Log4j 2 ou SLF4J) pode ser usada para registar eventos//alterações de estado nos diferentes processos.
- **Configuração:** Valores como endereços IP, números de porta ou parâmetros da simulação não devem ser "hardcoded". Um ficheiro de configuração (ex: um ficheiro `.properties` ou `.json`) torna a aplicação mais fácil de executar e testar. - **Configuração:** Valores como endereços IP, números de porta ou parâmetros da simulação não devem ser "hardcoded". Um ficheiro de configuração (ex: um ficheiro `.properties` ou `.json`) torna a aplicação mais fácil de executar e testar.

View File

@@ -42,6 +42,26 @@
<mainClass>sd.Entry</mainClass> <mainClass>sd.Entry</mainClass>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>sd.Entry</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>

View 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);
}
}
}
}

View File

@@ -0,0 +1,514 @@
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 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.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)
* representing one of the five intersections (Cr1-Cr5) in the network.
*/
public class IntersectionProcess {
private final String intersectionId;
private final SimulationConfig config;
private final Intersection intersection;
private ServerSocket serverSocket;
private final Map<String, SocketConnection> outgoingConnections;
private final ExecutorService connectionHandlerPool;
private final ExecutorService trafficLightPool;
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.
*
* @param intersectionId The ID of this intersection (e.g., "Cr1").
* @param configFilePath Path to the simulation.properties file.
* @throws IOException If configuration cannot be loaded.
*/
public IntersectionProcess(String intersectionId, String configFilePath) throws IOException {
this.intersectionId = intersectionId;
this.config = new SimulationConfig(configFilePath);
this.intersection = new Intersection(intersectionId);
this.outgoingConnections = new HashMap<>();
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);
System.out.println("=".repeat(60));
}
public void initialize() {
System.out.println("\n[" + intersectionId + "] Initializing intersection...");
createTrafficLights();
configureRouting();
System.out.println("[" + intersectionId + "] Initialization complete.");
}
/**
* 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.
*/
private void createTrafficLights() {
System.out.println("\n[" + intersectionId + "] Creating traffic lights...");
String[] directions = new String[0];
switch (intersectionId) {
case "Cr1":
directions = new String[] { "East", "South" };
break;
case "Cr2":
directions = new String[] { "West", "East", "South" };
break;
case "Cr3":
directions = new String[] { "West", "South" };
break;
case "Cr4":
directions = new String[] { "East" };
break;
case "Cr5":
directions = new String[] { "East" };
break;
}
for (String direction : directions) {
double greenTime = config.getTrafficLightGreenTime(intersectionId, direction);
double redTime = config.getTrafficLightRedTime(intersectionId, direction);
TrafficLight light = new TrafficLight(
intersectionId + "-" + direction,
direction,
greenTime,
redTime);
intersection.addTrafficLight(light);
System.out.println(" Created traffic light: " + direction +
" (Green: " + greenTime + "s, Red: " + redTime + "s)");
}
}
private void configureRouting() {
System.out.println("\n[" + intersectionId + "] Configuring routing...");
switch (intersectionId) {
case "Cr1":
intersection.configureRoute("Cr2", "East");
intersection.configureRoute("Cr4", "South");
break;
case "Cr2":
intersection.configureRoute("Cr1", "West");
intersection.configureRoute("Cr3", "East");
intersection.configureRoute("Cr5", "South");
break;
case "Cr3":
intersection.configureRoute("Cr2", "West");
intersection.configureRoute("S", "South");
break;
case "Cr4":
intersection.configureRoute("Cr5", "East");
break;
case "Cr5":
intersection.configureRoute("S", "East");
break;
default:
System.err.println(" Error: unknown intersection ID: " + intersectionId);
}
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.
*/
private void startTrafficLights() {
System.out.println("\n[" + intersectionId + "] Starting traffic light threads...");
for (TrafficLight light : intersection.getTrafficLights()) {
TrafficLightThread lightTask = new TrafficLightThread(light, this, config);
trafficLightPool.submit(lightTask);
System.out.println(" Started thread for: " + light.getDirection());
}
}
/**
* Sends a vehicle to its next destination via socket connection.
*
* @param vehicle The vehicle that has crossed this intersection.
*/
public void sendVehicleToNextDestination(Vehicle vehicle) {
String nextDestination = vehicle.getCurrentDestination();
try {
// Get or create connection to next destination
SocketConnection connection = getOrCreateConnection(nextDestination);
// Create and send message
MessageProtocol message = new VehicleTransferMessage(
intersectionId,
nextDestination,
vehicle);
connection.sendMessage(message);
System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() +
" 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());
}
}
/**
* Gets an existing connection to a destination or creates a new one.
*
* @param destinationId The ID of the destination node.
* @return The SocketConnection to that destination.
* @throws IOException If connection cannot be established.
* @throws InterruptedException If connection attempt is interrupted.
*/
private synchronized SocketConnection getOrCreateConnection(String destinationId)
throws IOException, InterruptedException {
if (!outgoingConnections.containsKey(destinationId)) {
String host = getHostForDestination(destinationId);
int port = getPortForDestination(destinationId);
System.out.println("[" + intersectionId + "] Creating connection to " +
destinationId + " at " + host + ":" + port);
SocketConnection connection = new SocketConnection(host, port);
outgoingConnections.put(destinationId, connection);
}
return outgoingConnections.get(destinationId);
}
/**
* Gets the host address for a destination node from configuration.
*
* @param destinationId The destination node ID.
* @return The host address.
*/
private String getHostForDestination(String destinationId) {
if (destinationId.equals("S")) {
return config.getExitHost();
} else {
return config.getIntersectionHost(destinationId);
}
}
/**
* Gets the port number for a destination node from configuration.
*
* @param destinationId The destination node ID.
* @return The port number.
*/
private int getPortForDestination(String destinationId) {
if (destinationId.equals("S")) {
return config.getExitPort();
} else {
return config.getIntersectionPort(destinationId);
}
}
/**
* Starts the server socket and begins accepting incoming connections.
* This is the main listening loop of the process.
*
* @throws IOException If the server socket cannot be created.
*/
public void start() throws IOException {
int port = config.getIntersectionPort(intersectionId);
serverSocket = new ServerSocket(port);
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
while (running) {
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) {
// Expected when serverSocket.close() is called during shutdown
if (!running) {
break; // Normal shutdown
}
System.err.println("[" + intersectionId + "] Error accepting connection: " +
e.getMessage());
}
}
}
/**
* Handles an incoming connection from another process.
* Continuously listens for vehicle transfer messages.
*
* @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());
// Continuously receive messages while connection is active
while (running && connection.isConnected()) {
try {
MessageProtocol message = connection.receiveMessage();
if (message.getType() == MessageType.VEHICLE_TRANSFER) {
Vehicle vehicle = (Vehicle) message.getPayload();
System.out.println("[" + intersectionId + "] Received vehicle: " +
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());
break; // Invalid message, close connection
}
}
} catch (IOException e) {
if (running) {
System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage());
}
// Expected during shutdown
}
}
/**
* Stops the intersection process gracefully.
* 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;
// 1. Close ServerSocket first
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException e) {
// Expected
}
}
// 2. Shutdown thread pools with force
if (trafficLightPool != null && !trafficLightPool.isShutdown()) {
trafficLightPool.shutdownNow();
}
if (connectionHandlerPool != null && !connectionHandlerPool.isShutdown()) {
connectionHandlerPool.shutdownNow();
}
// 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("============================================================\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 ---
/**
* Implementation of MessageProtocol for vehicle transfers between processes.
*/
private static class VehicleTransferMessage implements MessageProtocol {
private static final long serialVersionUID = 1L;
private final String sourceNode;
private final String destinationNode;
private final Vehicle payload;
public VehicleTransferMessage(String sourceNode, String destinationNode, Vehicle vehicle) {
this.sourceNode = sourceNode;
this.destinationNode = destinationNode;
this.payload = vehicle;
}
@Override
public MessageType getType() {
return MessageType.VEHICLE_TRANSFER;
}
@Override
public Object getPayload() {
return payload;
}
@Override
public String getSourceNode() {
return sourceNode;
}
@Override
public String getDestinationNode() {
return destinationNode;
}
}
}

View File

@@ -31,7 +31,7 @@ public class SimulationConfig {
* (por exemplo quando executado a partir do classpath/jar), * (por exemplo quando executado a partir do classpath/jar),
* faz fallback para carregar a partir do classpath usando o ClassLoader. * faz fallback para carregar a partir do classpath usando o ClassLoader.
*/ */
IOException lastException = null; IOException lastException = null; //FIXME: melhorar esta parte para reportar erros de forma mais clara
try { try {
try (InputStream input = new FileInputStream(filePath)) { try (InputStream input = new FileInputStream(filePath)) {

View 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;
}
}

View 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 wont 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());
}
}

View 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);
}
}

View 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();
}
}

View 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());
}
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -264,32 +264,19 @@ public class SimulationEngine {
*/ */
private void processEvent(Event event) { private void processEvent(Event event) {
switch (event.getType()) { switch (event.getType()) {
case VEHICLE_GENERATION: case VEHICLE_GENERATION -> handleVehicleGeneration();
handleVehicleGeneration();
break;
case VEHICLE_ARRIVAL: case VEHICLE_ARRIVAL -> handleVehicleArrival(event);
handleVehicleArrival(event);
break;
case TRAFFIC_LIGHT_CHANGE: case TRAFFIC_LIGHT_CHANGE -> handleTrafficLightChange(event);
handleTrafficLightChange(event);
break;
case CROSSING_START: case CROSSING_START -> handleCrossingStart(event);
handleCrossingStart(event);
break;
case CROSSING_END: case CROSSING_END -> handleCrossingEnd(event);
handleCrossingEnd(event);
break;
case STATISTICS_UPDATE: case STATISTICS_UPDATE -> handleStatisticsUpdate();
handleStatisticsUpdate();
break;
default: default -> System.err.println("Unknown event type: " + event.getType());
System.err.println("Unknown event type: " + event.getType());
} }
} }
@@ -386,7 +373,7 @@ public class SimulationEngine {
* @param vehicle The vehicle to process. * @param vehicle The vehicle to process.
* @param intersection The intersection where the vehicle is. * @param intersection The intersection where the vehicle is.
*/ */
private void tryProcessVehicle(Vehicle vehicle, Intersection intersection) { private void tryProcessVehicle(Vehicle vehicle, Intersection intersection) { //FIXME
// Find the direction (and light) this vehicle is queued at // Find the direction (and light) this vehicle is queued at
// This logic is a bit flawed: it just finds the *first* non-empty queue // This logic is a bit flawed: it just finds the *first* non-empty queue
// A better approach would be to get the light from the vehicle's route // A better approach would be to get the light from the vehicle's route
@@ -591,16 +578,12 @@ public class SimulationEngine {
* @return The crossing time in seconds. * @return The crossing time in seconds.
*/ */
private double getCrossingTime(VehicleType type) { private double getCrossingTime(VehicleType type) {
switch (type) { return switch (type) {
case BIKE: case BIKE -> config.getBikeVehicleCrossingTime();
return config.getBikeVehicleCrossingTime(); case LIGHT -> config.getLightVehicleCrossingTime();
case LIGHT: case HEAVY -> config.getHeavyVehicleCrossingTime();
return config.getLightVehicleCrossingTime(); default -> 2.0;
case HEAVY: }; // Default fallback
return config.getHeavyVehicleCrossingTime();
default:
return 2.0; // Default fallback
}
} }
/** /**

View 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();
}
}
}

View File

@@ -104,16 +104,28 @@ public class Intersection {
* Accepts an incoming vehicle and places it in the correct queue. * Accepts an incoming vehicle and places it in the correct queue.
* * This method: * * This method:
* 1. Increments the {@link #totalVehiclesReceived} counter. * 1. Increments the {@link #totalVehiclesReceived} counter.
* 2. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}). * 2. Advances the vehicle's route (since it just arrived here)
* 3. Uses the {@link #routing} map to find the correct *direction* for that destination. * 3. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
* 4. Adds the vehicle to the queue of the {@link TrafficLight} for that direction. * 4. Uses the {@link #routing} map to find the correct *direction* for that destination.
* 5. Adds the vehicle to the queue of the {@link TrafficLight} for that direction.
* *
* @param vehicle The {@link Vehicle} arriving at the intersection. * @param vehicle The {@link Vehicle} arriving at the intersection.
*/ */
public void receiveVehicle(Vehicle vehicle) { public void receiveVehicle(Vehicle vehicle) {
totalVehiclesReceived++; totalVehiclesReceived++;
// Note: Route advancement is handled by SimulationEngine.handleVehicleArrival()
// before calling this method, so we don't advance here.
String nextDestination = vehicle.getCurrentDestination(); String nextDestination = vehicle.getCurrentDestination();
// Check if vehicle reached final destination
if (nextDestination == null) {
System.out.printf("[%s] Vehicle %s reached final destination%n",
this.id, vehicle.getId());
return;
}
String direction = routing.get(nextDestination); String direction = routing.get(nextDestination);
if (direction != null && trafficLights.containsKey(direction)) { if (direction != null && trafficLights.containsKey(direction)) {

View File

@@ -0,0 +1,41 @@
package sd.protocol;
import java.io.Serializable;
import sd.model.MessageType; // Assuming MessageType is in sd.model or sd.protocol
/**
* Interface defining the contract for all messages exchanged in the simulator.
* Ensures that any message can be identified and routed.
* * This interface extends Serializable to allow objects that implement it
* to be sent over Sockets (ObjectOutputStream).
*
*/
public interface MessageProtocol extends Serializable {
/**
* Returns the type of the message, indicating its purpose.
* @return The MessageType (e.g., VEHICLE_TRANSFER, STATS_UPDATE).
*/
MessageType getType();
/**
* Returns the data object (payload) that this message carries.
* The type of object will depend on the MessageType.
* * - If getType() == VEHICLE_TRANSFER, the payload will be a {@link sd.model.Vehicle} object.
* - If getType() == STATS_UPDATE, the payload will be a statistics object.
* * @return The data object (payload), which must also be Serializable.
*/
Object getPayload();
/**
* Returns the ID of the node (Process) that sent this message.
* @return String (e.g., "Cr1", "Cr5", "S").
*/
String getSourceNode();
/**
* Returns the ID of the destination node (Process) for this message.
* @return String (e.g., "Cr2", "DashboardServer").
*/
String getDestinationNode();
}

View File

@@ -0,0 +1,199 @@
package sd.protocol;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
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.
*/
public class SocketConnection implements Closeable {
private final Socket socket;
private final OutputStream outputStream;
private final InputStream inputStream;
private final MessageSerializer serializer;
// --- Configuration for Retry Logic ---
/** Maximum number of connection attempts. */
private static final int MAX_RETRIES = 5;
/** Delay between retry attempts in milliseconds. */
private static final long RETRY_DELAY_MS = 1000;
/**
* Constructor for the "Client" (who initiates the connection).
* Tries to connect to a process that is already listening (Server).
* Includes retry logic in case of initial connection failure.
*
* @param host The host address (e.g., "localhost" from your simulation.properties)
* @param port The port (e.g., 8001 from your simulation.properties)
* @throws IOException If connection fails after all retries.
* @throws UnknownHostException If the host is not found (this error usually doesn't need retry).
* @throws InterruptedException If the thread is interrupted while waiting between retries.
*/
public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException {
Socket tempSocket = null;
IOException lastException = null;
System.out.printf("[SocketConnection] Attempting to connect to %s:%d...%n", host, port);
// --- Retry Loop ---
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
// Try to establish the connection
tempSocket = new Socket(host, port);
// If successful, break out of the retry loop
System.out.printf("[SocketConnection] Connected successfully on attempt %d.%n", attempt);
lastException = null; // Clear last error on success
break;
} catch (ConnectException | SocketTimeoutException e) {
// These are common errors indicating the server might not be ready.
lastException = e;
System.out.printf("[SocketConnection] Attempt %d/%d failed: %s. Retrying in %d ms...%n",
attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
if (attempt < MAX_RETRIES) {
// Wait before the next attempt
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
}
} catch (IOException e) {
// Other IOExceptions might be more permanent, but we retry anyway.
lastException = e;
System.out.printf("[SocketConnection] Attempt %d/%d failed with IOException: %s. Retrying in %d ms...%n",
attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
if (attempt < MAX_RETRIES) {
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
}
}
} // --- End of Retry Loop ---
// If after all retries tempSocket is still null, it means connection failed permanently.
if (tempSocket == null) {
System.err.printf("[SocketConnection] Failed to connect to %s:%d after %d attempts.%n", host, port, MAX_RETRIES);
if (lastException != null) {
throw lastException; // Throw the last exception encountered
} else {
// Should not happen if loop ran, but as a fallback
throw new IOException("Failed to connect after " + MAX_RETRIES + " attempts, reason unknown.");
}
}
// If connection was successful, assign to final variable and create streams
this.socket = tempSocket;
this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream();
this.serializer = SerializerFactory.createDefault();
}
/**
* Constructor for the "Server" (who accepts the connection).
* Receives a Socket that has already been accepted by a ServerSocket.
* No retry logic needed here as the connection is already established.
*
* @param acceptedSocket The Socket returned by serverSocket.accept().
* @throws IOException If stream creation fails.
*/
public SocketConnection(Socket acceptedSocket) throws IOException {
this.socket = acceptedSocket;
this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream();
this.serializer = SerializerFactory.createDefault();
}
/**
* Sends (serializes) a MessageProtocol object over the socket.
*
* @param message The "envelope" (which contains the Vehicle) to be sent.
* @throws IOException If writing to the stream fails or socket is not connected.
*/
public void sendMessage(MessageProtocol message) throws IOException {
if (socket == null || !socket.isConnected()) {
throw new IOException("Socket is not connected");
}
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.
*
* @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 (socket == null || !socket.isConnected()) {
throw new IOException("Socket is not connected");
}
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).
*/
@Override
public void close() throws IOException {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
if (socket != null) socket.close();
}
/**
* @return true if the socket is still connected and not closed.
*/
public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed();
}
}

View File

@@ -0,0 +1,527 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
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.IntersectionProcess;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.protocol.SocketConnection;
/**
* Tests for IntersectionProcess - covers initialization, traffic lights,
* vehicle transfer and network stuff
*/
public class IntersectionProcessTest {
@TempDir
Path tempDir;
private Path configFile;
private IntersectionProcess intersectionProcess;
// setup test config before each test
@BeforeEach
public void setUp() throws IOException {
// create temp config file
configFile = tempDir.resolve("test-simulation.properties");
String configContent = """
# Test Simulation Configuration
# Intersection Network Configuration
intersection.Cr1.host=localhost
intersection.Cr1.port=18001
intersection.Cr2.host=localhost
intersection.Cr2.port=18002
intersection.Cr3.host=localhost
intersection.Cr3.port=18003
intersection.Cr4.host=localhost
intersection.Cr4.port=18004
intersection.Cr5.host=localhost
intersection.Cr5.port=18005
# Exit Configuration
exit.host=localhost
exit.port=18099
# Dashboard Configuration
dashboard.host=localhost
dashboard.port=18100
# Traffic Light Timing (seconds)
trafficLight.Cr1.East.greenTime=5.0
trafficLight.Cr1.East.redTime=5.0
trafficLight.Cr1.South.greenTime=5.0
trafficLight.Cr1.South.redTime=5.0
trafficLight.Cr1.West.greenTime=5.0
trafficLight.Cr1.West.redTime=5.0
trafficLight.Cr2.West.greenTime=4.0
trafficLight.Cr2.West.redTime=6.0
trafficLight.Cr2.East.greenTime=4.0
trafficLight.Cr2.East.redTime=6.0
trafficLight.Cr2.South.greenTime=4.0
trafficLight.Cr2.South.redTime=6.0
trafficLight.Cr3.West.greenTime=3.0
trafficLight.Cr3.West.redTime=7.0
trafficLight.Cr3.East.greenTime=3.0
trafficLight.Cr3.East.redTime=7.0
trafficLight.Cr4.East.greenTime=6.0
trafficLight.Cr4.East.redTime=4.0
trafficLight.Cr5.East.greenTime=5.0
trafficLight.Cr5.East.redTime=5.0
# Vehicle Crossing Times (seconds)
vehicle.bike.crossingTime=2.0
vehicle.light.crossingTime=3.0
vehicle.heavy.crossingTime=5.0
""";
Files.writeString(configFile, configContent);
}
@AfterEach
public void tearDown() {
if (intersectionProcess != null) {
try {
// Only shutdown if still running
intersectionProcess.shutdown();
} catch (Exception e) {
System.err.println("Error in tearDown: " + e.getMessage());
} finally {
intersectionProcess = null;
}
}
}
// ==================== Initialization Tests ====================
@Test
public void testConstructor_Success() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
assertNotNull(intersectionProcess);
}
@Test
public void testConstructor_InvalidConfig() {
Exception exception = assertThrows(IOException.class, () -> {
new IntersectionProcess("Cr1", "non-existent-config.properties");
});
assertNotNull(exception);
}
@Test
public void testInitialize_Cr1() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr2() throws IOException {
intersectionProcess = new IntersectionProcess("Cr2", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr3() throws IOException {
intersectionProcess = new IntersectionProcess("Cr3", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr4() throws IOException {
intersectionProcess = new IntersectionProcess("Cr4", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
@Test
public void testInitialize_Cr5() throws IOException {
intersectionProcess = new IntersectionProcess("Cr5", configFile.toString());
assertDoesNotThrow(() -> intersectionProcess.initialize());
}
// traffic light creation tests
@Test
public void testTrafficLightCreation_Cr1_HasCorrectDirections() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
// cant access private fields but initialization succeds
assertNotNull(intersectionProcess);
}
@Test
public void testTrafficLightCreation_Cr3_HasCorrectDirections() throws IOException {
intersectionProcess = new IntersectionProcess("Cr3", configFile.toString());
intersectionProcess.initialize();
// Cr3 has west and south only
assertNotNull(intersectionProcess);
}
@Test
public void testTrafficLightCreation_Cr4_HasSingleDirection() throws IOException {
intersectionProcess = new IntersectionProcess("Cr4", configFile.toString());
intersectionProcess.initialize();
// Cr4 only has east direction
assertNotNull(intersectionProcess);
}
// server startup tests
@Test
@Timeout(5)
public void testServerStart_BindsToCorrectPort() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
// start server in separate thread
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
// expected on shutdown
}
});
serverThread.start();
// 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);
}
@Test
@Timeout(5)
public void testServerStart_MultipleIntersections() throws IOException, InterruptedException {
// test 2 intersections on diferent ports
IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString());
IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString());
cr1.initialize();
cr2.initialize();
Thread thread1 = new Thread(() -> {
try {
cr1.start();
} catch (IOException e) {
}
});
Thread thread2 = new Thread(() -> {
try {
cr2.start();
} catch (IOException e) {
}
});
thread1.start();
thread2.start();
Thread.sleep(500);
// check both are running
try (Socket socket1 = new Socket("localhost", 18001);
Socket socket2 = new Socket("localhost", 18002)) {
assertTrue(socket1.isConnected());
assertTrue(socket2.isConnected());
}
cr1.shutdown();
cr2.shutdown();
thread1.join(2000);
thread2.join(2000);
}
// vehicle transfer tests
@Test
@Timeout(10)
public void testVehicleTransfer_ReceiveVehicle() throws IOException, InterruptedException {
// setup reciever intersection
intersectionProcess = new IntersectionProcess("Cr2", configFile.toString());
intersectionProcess.initialize();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
}
});
serverThread.start();
Thread.sleep(500);
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 - FIXED: use SocketConnection
try (Socket socket = new Socket("localhost", 18002);
SocketConnection conn = new SocketConnection(socket)) {
TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
conn.sendMessage(message);
Thread.sleep(1000); // wait for processing
}
} finally {
intersectionProcess.shutdown();
serverThread.join(2000);
}
}
// routing config tests
@Test
public void testRoutingConfiguration_Cr1() throws IOException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
// indirect test - if init works routing should be ok
assertNotNull(intersectionProcess);
}
@Test
public void testRoutingConfiguration_Cr5() throws IOException {
intersectionProcess = new IntersectionProcess("Cr5", configFile.toString());
intersectionProcess.initialize();
// Cr5 routes to exit
assertNotNull(intersectionProcess);
}
// shutdown tests
@Test
@Timeout(5)
public void testShutdown_GracefulTermination() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
}
});
serverThread.start();
Thread.sleep(500);
// shutdown should be fast
assertDoesNotThrow(() -> intersectionProcess.shutdown());
serverThread.join(2000);
}
@Test
@Timeout(5)
public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
// Start server in separate thread
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
// Expected on shutdown
}
});
serverThread.start();
// Wait for server to start
Thread.sleep(500);
// Shutdown
intersectionProcess.shutdown();
serverThread.join(2000);
// 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
@Timeout(5)
public void testShutdown_StopsTrafficLightThreads() throws IOException, InterruptedException {
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
intersectionProcess.initialize();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
}
});
serverThread.start();
Thread.sleep(500);
int threadCountBefore = Thread.activeCount();
intersectionProcess.shutdown();
serverThread.join(2000);
Thread.sleep(500); // wait for threads to die
// thread count should decrese (traffic light threads stop)
int threadCountAfter = Thread.activeCount();
assertTrue(threadCountAfter <= threadCountBefore);
}
// integration tests
@Test
@Timeout(15)
public void testIntegration_TwoIntersectionsVehicleTransfer() throws IOException, InterruptedException {
IntersectionProcess cr1 = null;
IntersectionProcess cr2 = null;
Thread thread1 = null;
Thread thread2 = null;
try {
// setup 2 intersections
cr1 = new IntersectionProcess("Cr1", configFile.toString());
cr2 = new IntersectionProcess("Cr2", configFile.toString());
cr1.initialize();
cr2.initialize();
// start both
final IntersectionProcess cr1Final = cr1;
thread1 = new Thread(() -> {
try {
cr1Final.start();
} catch (IOException e) {
}
});
final IntersectionProcess cr2Final = cr2;
thread2 = new Thread(() -> {
try {
cr2Final.start();
} catch (IOException e) {
}
});
thread1.start();
thread2.start();
Thread.sleep(1000); // wait for servers
// 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);
// FIXED: use SocketConnection
try (Socket socket = new Socket("localhost", 18001);
SocketConnection conn = new SocketConnection(socket)) {
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);
}
}
}
@Test
public void testMain_MissingArguments() {
// main needs intersection ID as argument
// cant test System.exit easily in modern java
assertTrue(true, "Main method expects intersection ID as first argument");
}
// helper class for testing vehicle messages
private static class TestVehicleMessage implements sd.protocol.MessageProtocol {
private static final long serialVersionUID = 1L;
private final String sourceNode;
private final String destinationNode;
private final Vehicle payload;
public TestVehicleMessage(String sourceNode, String destinationNode, Vehicle vehicle) {
this.sourceNode = sourceNode;
this.destinationNode = destinationNode;
this.payload = vehicle;
}
@Override
public MessageType getType() {
return MessageType.VEHICLE_TRANSFER;
}
@Override
public Object getPayload() {
return payload;
}
@Override
public String getSourceNode() {
return sourceNode;
}
@Override
public String getDestinationNode() {
return destinationNode;
}
}
}

View File

@@ -43,7 +43,7 @@ class SimulationTest {
assertEquals("TEST1", vehicle.getId()); assertEquals("TEST1", vehicle.getId());
assertNotNull(vehicle.getType()); assertNotNull(vehicle.getType());
assertNotNull(vehicle.getRoute()); assertNotNull(vehicle.getRoute());
assertTrue(vehicle.getRoute().size() > 0); assertTrue(!vehicle.getRoute().isEmpty());
} }
@Test @Test

View 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);
}
}
}

View 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");
}
}

View File

@@ -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 were 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;
}
}
}

View 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 were 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 doesnt 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"
);
}
}

View 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");
}
}