diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..344c385
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -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
diff --git a/STEP2_SUMMARY.md b/STEP2_SUMMARY.md
new file mode 100644
index 0000000..b234c44
--- /dev/null
+++ b/STEP2_SUMMARY.md
@@ -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 (Cr1–Cr5) 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 E1–E3
+* **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 ✅
+Everything’s working — traffic lights, routing, vehicles, stats.
+Ready to go distributed next.
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
index 001c208..97d1982 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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
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.
-- **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.
-- **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.
\ No newline at end of file
+- **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.
diff --git a/main/pom.xml b/main/pom.xml
index 0adc5f4..56ce74f 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -42,6 +42,26 @@
sd.Entry
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.2
+
+
+ package
+
+ shade
+
+
+
+
+ sd.Entry
+
+
+
+
+
+
diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java
new file mode 100644
index 0000000..11db78d
--- /dev/null
+++ b/main/src/main/java/sd/IntersectionProcess.java
@@ -0,0 +1,501 @@
+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.model.Intersection;
+import sd.model.MessageType;
+import sd.model.TrafficLight;
+import sd.model.TrafficLightState;
+import sd.model.Vehicle;
+import sd.protocol.MessageProtocol;
+import sd.protocol.SocketConnection;
+
+/**
+ * Main class for an Intersection Process in the distributed traffic simulation.
+ * * Each IntersectionProcess runs as an independent Java application (JVM instance)
+ * 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 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.
+
+ /**
+ * 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;
+
+ 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();
+
+ startTrafficLights();
+
+ 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.");
+ }
+
+ /**
+ * Starts all traffic light threads.
+ */
+ private void startTrafficLights() {
+ System.out.println("\n[" + intersectionId + "] Starting traffic light threads...");
+
+ for (TrafficLight light : intersection.getTrafficLights()) {
+ trafficLightPool.submit(() -> runTrafficLightCycle(light));
+ System.out.println(" Started thread for: " + light.getDirection());
+ }
+ }
+
+ /**
+ * The main loop for a traffic light thread.
+ * Continuously cycles between GREEN and RED states.
+ *
+ * @param light The traffic light to control.
+ */
+ private void runTrafficLightCycle(TrafficLight light) {
+ System.out.println("[" + light.getId() + "] Traffic light thread started.");
+
+ while (running) {
+ try {
+ // Green state
+ light.changeState(TrafficLightState.GREEN);
+ System.out.println("[" + light.getId() + "] State: GREEN");
+
+ // Process vehicles while green
+ processGreenLight(light);
+
+ // Wait for green duration
+ Thread.sleep((long) (light.getGreenTime() * 1000));
+
+ // RED state
+ light.changeState(TrafficLightState.RED);
+ System.out.println("[" + light.getId() + "] State: RED");
+
+ // Wait for red duration
+ Thread.sleep((long) (light.getRedTime() * 1000));
+
+ } catch (InterruptedException e) {
+ System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
+ break;
+ }
+ }
+
+ System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
+ }
+
+ /**
+ * Processes vehicles when a traffic light is GREEN.
+ * Dequeues vehicles and sends them to their next destination.
+ *
+ * @param light The traffic light that is currently green.
+ */
+ private void processGreenLight(TrafficLight light) {
+ while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
+ Vehicle vehicle = light.removeVehicle();
+
+ if (vehicle != null) {
+ // Get crossing time based on vehicle type
+ double crossingTime = getCrossingTimeForVehicle(vehicle);
+
+ // Simulate crossing time
+ try {
+ Thread.sleep((long) (crossingTime * 1000));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+
+ // Update vehicle statistics
+ vehicle.addCrossingTime(crossingTime);
+
+ // Update intersection statistics
+ intersection.incrementVehiclesSent();
+
+ // Send vehicle to next destination
+ sendVehicleToNextDestination(vehicle);
+ }
+ }
+ }
+
+ /**
+ * Gets the crossing time for a vehicle based on its type.
+ *
+ * @param vehicle The vehicle.
+ * @return The crossing time in seconds.
+ */
+ private double getCrossingTimeForVehicle(Vehicle vehicle) {
+ switch (vehicle.getType()) {
+ case BIKE:
+ return config.getBikeVehicleCrossingTime();
+ case LIGHT:
+ return config.getLightVehicleCrossingTime();
+ case HEAVY:
+ return config.getHeavyVehicleCrossingTime();
+ default:
+ return config.getLightVehicleCrossingTime();
+ }
+ }
+
+ /**
+ * Sends a vehicle to its next destination via socket connection.
+ *
+ * @param vehicle The vehicle that has crossed this intersection.
+ */
+ private void sendVehicleToNextDestination(Vehicle vehicle) {
+ 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);
+ System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n");
+
+ // Main accept loop
+ while (running) {
+ try {
+ Socket clientSocket = serverSocket.accept();
+
+ // Handle each connection in a separate thread
+ connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
+
+ } catch (IOException e) {
+ if (running) {
+ System.err.println("[" + intersectionId + "] Error accepting connection: " +
+ e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 (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 (ClassNotFoundException e) {
+ System.err.println("[" + intersectionId + "] Unknown message type received: " +
+ e.getMessage());
+ }
+ }
+
+ } catch (IOException e) {
+ if (running) {
+ System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Stops the intersection process gracefully.
+ * Shuts down all threads and closes all connections.
+ */
+ public void shutdown() {
+ System.out.println("\n[" + intersectionId + "] Shutting down...");
+ running = false;
+
+ // Close server socket
+ try {
+ if (serverSocket != null && !serverSocket.isClosed()) {
+ serverSocket.close();
+ }
+ } catch (IOException e) {
+ System.err.println("[" + intersectionId + "] Error closing server socket: " +
+ e.getMessage());
+ }
+
+ // Shutdown thread pools
+ trafficLightPool.shutdown();
+ connectionHandlerPool.shutdown();
+
+ try {
+ if (!trafficLightPool.awaitTermination(5, TimeUnit.SECONDS)) {
+ trafficLightPool.shutdownNow();
+ }
+ if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
+ connectionHandlerPool.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ trafficLightPool.shutdownNow();
+ connectionHandlerPool.shutdownNow();
+ }
+
+ // Close all outgoing connections
+ for (Map.Entry entry : outgoingConnections.entrySet()) {
+ try {
+ entry.getValue().close();
+ } catch (IOException e) {
+ System.err.println("[" + intersectionId + "] Error closing connection to " +
+ entry.getKey() + ": " + e.getMessage());
+ }
+ }
+
+ System.out.println("[" + intersectionId + "] Shutdown complete.");
+ System.out.println("=".repeat(60));
+ }
+
+ // --- 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;
+ }
+ }
+}
diff --git a/main/src/main/java/sd/config/SimulationConfig.java b/main/src/main/java/sd/config/SimulationConfig.java
index 4a3fe89..d11ed42 100644
--- a/main/src/main/java/sd/config/SimulationConfig.java
+++ b/main/src/main/java/sd/config/SimulationConfig.java
@@ -31,7 +31,7 @@ public class SimulationConfig {
* (por exemplo quando executado a partir do classpath/jar),
* 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 (InputStream input = new FileInputStream(filePath)) {
diff --git a/main/src/main/java/sd/engine/SimulationEngine.java b/main/src/main/java/sd/engine/SimulationEngine.java
index 94793f7..484ae80 100644
--- a/main/src/main/java/sd/engine/SimulationEngine.java
+++ b/main/src/main/java/sd/engine/SimulationEngine.java
@@ -264,32 +264,19 @@ public class SimulationEngine {
*/
private void processEvent(Event event) {
switch (event.getType()) {
- case VEHICLE_GENERATION:
- handleVehicleGeneration();
- break;
+ case VEHICLE_GENERATION -> handleVehicleGeneration();
- case VEHICLE_ARRIVAL:
- handleVehicleArrival(event);
- break;
+ case VEHICLE_ARRIVAL -> handleVehicleArrival(event);
- case TRAFFIC_LIGHT_CHANGE:
- handleTrafficLightChange(event);
- break;
+ case TRAFFIC_LIGHT_CHANGE -> handleTrafficLightChange(event);
- case CROSSING_START:
- handleCrossingStart(event);
- break;
+ case CROSSING_START -> handleCrossingStart(event);
- case CROSSING_END:
- handleCrossingEnd(event);
- break;
+ case CROSSING_END -> handleCrossingEnd(event);
- case STATISTICS_UPDATE:
- handleStatisticsUpdate();
- break;
+ case STATISTICS_UPDATE -> handleStatisticsUpdate();
- default:
- System.err.println("Unknown event type: " + event.getType());
+ default -> System.err.println("Unknown event type: " + event.getType());
}
}
@@ -386,7 +373,7 @@ public class SimulationEngine {
* @param vehicle The vehicle to process.
* @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
// 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
@@ -591,16 +578,12 @@ public class SimulationEngine {
* @return The crossing time in seconds.
*/
private double getCrossingTime(VehicleType type) {
- switch (type) {
- case BIKE:
- return config.getBikeVehicleCrossingTime();
- case LIGHT:
- return config.getLightVehicleCrossingTime();
- case HEAVY:
- return config.getHeavyVehicleCrossingTime();
- default:
- return 2.0; // Default fallback
- }
+ return switch (type) {
+ case BIKE -> config.getBikeVehicleCrossingTime();
+ case LIGHT -> config.getLightVehicleCrossingTime();
+ case HEAVY -> config.getHeavyVehicleCrossingTime();
+ default -> 2.0;
+ }; // Default fallback
}
/**
diff --git a/main/src/main/java/sd/model/Intersection.java b/main/src/main/java/sd/model/Intersection.java
index 718c98c..4475fc3 100644
--- a/main/src/main/java/sd/model/Intersection.java
+++ b/main/src/main/java/sd/model/Intersection.java
@@ -104,16 +104,28 @@ public class Intersection {
* Accepts an incoming vehicle and places it in the correct queue.
* * This method:
* 1. Increments the {@link #totalVehiclesReceived} counter.
- * 2. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
- * 3. Uses the {@link #routing} map to find the correct *direction* for that destination.
- * 4. Adds the vehicle to the queue of the {@link TrafficLight} for that direction.
+ * 2. Advances the vehicle's route (since it just arrived here)
+ * 3. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
+ * 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.
*/
public void receiveVehicle(Vehicle vehicle) {
totalVehiclesReceived++;
+ // Advance route since vehicle just arrived at this intersection
+ vehicle.advanceRoute();
+
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);
if (direction != null && trafficLights.containsKey(direction)) {
diff --git a/main/src/test/java/IntersectionProcessTest.java b/main/src/test/java/IntersectionProcessTest.java
new file mode 100644
index 0000000..90de4f1
--- /dev/null
+++ b/main/src/test/java/IntersectionProcessTest.java
@@ -0,0 +1,473 @@
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+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;
+
+/**
+ * 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);
+ }
+
+ // cleanup after tests
+ @AfterEach
+ public void tearDown() {
+ if (intersectionProcess != null) {
+ intersectionProcess.shutdown();
+ }
+ }
+
+ // ==================== 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 seperate thread
+ Thread serverThread = new Thread(() -> {
+ try {
+ intersectionProcess.start();
+ } catch (IOException e) {
+ // expected on shutdown
+ }
+ });
+ serverThread.start();
+
+ Thread.sleep(500); // wait for server to start
+
+ // try connecting to check if its running
+ try (Socket clientSocket = new Socket("localhost", 18001)) {
+ assertTrue(clientSocket.isConnected());
+ }
+
+ 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);
+
+ // create test vehicle
+ java.util.List route = Arrays.asList("Cr2", "Cr3", "S");
+ Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
+
+ // send vehicle from Cr1 to Cr2
+ try (Socket socket = new Socket("localhost", 18002)) {
+ ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
+
+ TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
+ out.writeObject(message);
+ out.flush();
+
+ Thread.sleep(1000); // wait for procesing
+ }
+
+ 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();
+
+ Thread serverThread = new Thread(() -> {
+ try {
+ intersectionProcess.start();
+ } catch (IOException e) { }
+ });
+ serverThread.start();
+
+ Thread.sleep(500);
+
+ // verify server running
+ try (Socket socket = new Socket("localhost", 18001)) {
+ assertTrue(socket.isConnected());
+ }
+
+ intersectionProcess.shutdown();
+ serverThread.join(2000);
+
+ // after shutdown conection should fail
+ Thread.sleep(500);
+ Exception exception = assertThrows(IOException.class, () -> {
+ Socket socket = new Socket("localhost", 18001);
+ socket.close();
+ });
+ assertNotNull(exception);
+ }
+
+ @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 {
+ // setup 2 intersections
+ IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString());
+ IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString());
+
+ cr1.initialize();
+ cr2.initialize();
+
+ // start both
+ 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(1000); // wait for servers
+
+ // send vehicle to Cr1 that goes to Cr2
+ java.util.List route = Arrays.asList("Cr1", "Cr2", "S");
+ Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
+
+ try (Socket socket = new Socket("localhost", 18001)) {
+ ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
+
+ TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle);
+ out.writeObject(message);
+ out.flush();
+
+ Thread.sleep(2000); // time for processing
+ }
+
+ cr1.shutdown();
+ cr2.shutdown();
+ thread1.join(2000);
+ 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;
+ }
+ }
+}
diff --git a/main/src/test/java/SimulationTest.java b/main/src/test/java/SimulationTest.java
index 7172a1a..b3a49df 100644
--- a/main/src/test/java/SimulationTest.java
+++ b/main/src/test/java/SimulationTest.java
@@ -43,7 +43,7 @@ class SimulationTest {
assertEquals("TEST1", vehicle.getId());
assertNotNull(vehicle.getType());
assertNotNull(vehicle.getRoute());
- assertTrue(vehicle.getRoute().size() > 0);
+ assertTrue(!vehicle.getRoute().isEmpty());
}
@Test