Merge branch 'dev' into 10-create-network-communication-classes

This commit is contained in:
2025-11-02 22:39:26 +00:00
committed by GitHub
10 changed files with 1245 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.

25
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
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.
---

View File

@@ -42,6 +42,26 @@
<mainClass>sd.Entry</mainClass>
</configuration>
</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>
</build>

View File

@@ -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<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.
/**
* 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<String, SocketConnection> 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;
}
}
}

View File

@@ -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)) {

View File

@@ -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
}
/**

View File

@@ -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)) {

View File

@@ -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<String> route = Arrays.asList("Cr2", "Cr3", "S");
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
// send vehicle from Cr1 to Cr2
try (Socket socket = new Socket("localhost", 18002)) {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
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<String> 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;
}
}
}

View File

@@ -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