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

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