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/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/main/java/sd/protocol/MessageProtocol.java b/main/src/main/java/sd/protocol/MessageProtocol.java new file mode 100644 index 0000000..47975be --- /dev/null +++ b/main/src/main/java/sd/protocol/MessageProtocol.java @@ -0,0 +1,41 @@ +package sd.protocol; + +import java.io.Serializable; +import sd.model.MessageType; // Assuming MessageType is in sd.model or sd.protocol + +/** + * Interface defining the contract for all messages exchanged in the simulator. + * Ensures that any message can be identified and routed. + * * This interface extends Serializable to allow objects that implement it + * to be sent over Sockets (ObjectOutputStream). + * + */ +public interface MessageProtocol extends Serializable { + + /** + * Returns the type of the message, indicating its purpose. + * @return The MessageType (e.g., VEHICLE_TRANSFER, STATS_UPDATE). + */ + MessageType getType(); + + /** + * Returns the data object (payload) that this message carries. + * The type of object will depend on the MessageType. + * * - If getType() == VEHICLE_TRANSFER, the payload will be a {@link sd.model.Vehicle} object. + * - If getType() == STATS_UPDATE, the payload will be a statistics object. + * * @return The data object (payload), which must also be Serializable. + */ + Object getPayload(); + + /** + * Returns the ID of the node (Process) that sent this message. + * @return String (e.g., "Cr1", "Cr5", "S"). + */ + String getSourceNode(); + + /** + * Returns the ID of the destination node (Process) for this message. + * @return String (e.g., "Cr2", "DashboardServer"). + */ + String getDestinationNode(); +} \ No newline at end of file diff --git a/main/src/main/java/sd/protocol/SocketConnection.java b/main/src/main/java/sd/protocol/SocketConnection.java new file mode 100644 index 0000000..f6392a4 --- /dev/null +++ b/main/src/main/java/sd/protocol/SocketConnection.java @@ -0,0 +1,183 @@ +package sd.protocol; + +import java.io.Closeable; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.ConnectException; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.concurrent.TimeUnit; + +/** + * Wrapper class that simplifies communication via Sockets. + * Includes connection retry logic for robustness. + */ +public class SocketConnection implements Closeable { + + private final Socket socket; + private final ObjectOutputStream outputStream; + private final ObjectInputStream inputStream; + + // --- Configuration for Retry Logic --- + /** Maximum number of connection attempts. */ + private static final int MAX_RETRIES = 5; + /** Delay between retry attempts in milliseconds. */ + private static final long RETRY_DELAY_MS = 1000; + + /** + * Constructor for the "Client" (who initiates the connection). + * Tries to connect to a process that is already listening (Server). + * Includes retry logic in case of initial connection failure. + * + * @param host The host address (e.g., "localhost" from your simulation.properties) + * @param port The port (e.g., 8001 from your simulation.properties) + * @throws IOException If connection fails after all retries. + * @throws UnknownHostException If the host is not found (this error usually doesn't need retry). + * @throws InterruptedException If the thread is interrupted while waiting between retries. + */ + public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException { + Socket tempSocket = null; + IOException lastException = null; + + System.out.printf("[SocketConnection] Attempting to connect to %s:%d...%n", host, port); + + // --- Retry Loop --- + for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + // Try to establish the connection + tempSocket = new Socket(host, port); + + // If successful, break out of the retry loop + System.out.printf("[SocketConnection] Connected successfully on attempt %d.%n", attempt); + lastException = null; // Clear last error on success + break; + + } catch (ConnectException | SocketTimeoutException e) { + // These are common errors indicating the server might not be ready. + lastException = e; + System.out.printf("[SocketConnection] Attempt %d/%d failed: %s. Retrying in %d ms...%n", + attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS); + + if (attempt < MAX_RETRIES) { + // Wait before the next attempt + TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS); + } + } catch (IOException e) { + // Other IOExceptions might be more permanent, but we retry anyway. + lastException = e; + System.out.printf("[SocketConnection] Attempt %d/%d failed with IOException: %s. Retrying in %d ms...%n", + attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS); + if (attempt < MAX_RETRIES) { + TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS); + } + } + } // --- End of Retry Loop --- + + // If after all retries tempSocket is still null, it means connection failed permanently. + if (tempSocket == null) { + System.err.printf("[SocketConnection] Failed to connect to %s:%d after %d attempts.%n", host, port, MAX_RETRIES); + if (lastException != null) { + throw lastException; // Throw the last exception encountered + } else { + // Should not happen if loop ran, but as a fallback + throw new IOException("Failed to connect after " + MAX_RETRIES + " attempts, reason unknown."); + } + } + + // If connection was successful, assign to final variable and create streams + this.socket = tempSocket; + try { + // IMPORTANT: The order is crucial. OutputStream first. + this.outputStream = new ObjectOutputStream(socket.getOutputStream()); + this.inputStream = new ObjectInputStream(socket.getInputStream()); + } catch (IOException e) { + // If stream creation fails even after successful socket connection, clean up. + System.err.println("[SocketConnection] Failed to create streams after connection: " + e.getMessage()); + try { socket.close(); } catch (IOException closeEx) { /* ignore */ } + throw e; // Re-throw the stream creation exception + } + } + + + /** + * Constructor for the "Server" (who accepts the connection). + * Receives a Socket that has already been accepted by a ServerSocket. + * No retry logic needed here as the connection is already established. + * + * @param acceptedSocket The Socket returned by serverSocket.accept(). + * @throws IOException If stream creation fails. + */ + public SocketConnection(Socket acceptedSocket) throws IOException { + this.socket = acceptedSocket; + + // IMPORTANT: The order is crucial. OutputStream first. + this.outputStream = new ObjectOutputStream(socket.getOutputStream()); + this.inputStream = new ObjectInputStream(socket.getInputStream()); + System.out.printf("[SocketConnection] Connection accepted from %s:%d.%n", + acceptedSocket.getInetAddress().getHostAddress(), acceptedSocket.getPort()); + } + + /** + * Sends (serializes) a MessageProtocol object over the socket. + * + * @param message The "envelope" (which contains the Vehicle) to be sent. + * @throws IOException If writing to the stream fails or socket is not connected. + */ + public void sendMessage(MessageProtocol message) throws IOException { + if (!isConnected()) { + throw new IOException("Socket is not connected."); + } + synchronized (outputStream) { + outputStream.writeObject(message); + outputStream.flush(); // Ensures the message is sent immediately. + } + } + + /** + * Tries to read (deserialize) a MessageProtocol object from the socket. + * This call is "blocked" until an object is received. + * + * @return The "envelope" (MessageProtocol) that was received. + * @throws IOException If the connection is lost, the stream is corrupted, or socket is not connected. + * @throws ClassNotFoundException If the received object is unknown. + */ + public MessageProtocol receiveMessage() throws IOException, ClassNotFoundException { + if (!isConnected()) { + throw new IOException("Socket is not connected."); + } + synchronized (inputStream) { + return (MessageProtocol) inputStream.readObject(); + } + } + + /** + * Closes the socket and all streams (Input and Output). + * It is called automatically if you use 'try-with-resources'. + */ + @Override + public void close() throws IOException { + System.out.printf("[SocketConnection] Closing connection to %s:%d.%n", + socket != null ? socket.getInetAddress().getHostAddress() : "N/A", + socket != null ? socket.getPort() : -1); + try { + if (inputStream != null) inputStream.close(); + } catch (IOException e) { /* ignore */ } + + try { + if (outputStream != null) outputStream.close(); + } catch (IOException e) { /* ignore */ } + + if (socket != null && !socket.isClosed()) { + socket.close(); + } + } + + /** + * @return true if the socket is still connected and not closed. + */ + public boolean isConnected() { + return socket != null && socket.isConnected() && !socket.isClosed(); + } +} \ No newline at end of file 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; + } + } +}