From 1684a6713eec8f45c19e24fff0b2a71977eac78d Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Sun, 2 Nov 2025 23:17:15 +0000 Subject: [PATCH 1/2] Implementation of the Coordinator Process --- .../sd/coordinator/CoordinatorProcess.java | 204 ++++++++++++ .../java/sd/coordinator/SocketClient.java | 124 +++++++ .../java/sd/protocol/SocketConnection.java | 105 +++--- .../CoordinatorIntegrationTest.java | 302 ++++++++++++++++++ .../coordinator/CoordinatorProcessTest.java | 194 +++++++++++ 5 files changed, 884 insertions(+), 45 deletions(-) create mode 100644 main/src/main/java/sd/coordinator/CoordinatorProcess.java create mode 100644 main/src/main/java/sd/coordinator/SocketClient.java create mode 100644 main/src/test/java/sd/coordinator/CoordinatorIntegrationTest.java create mode 100644 main/src/test/java/sd/coordinator/CoordinatorProcessTest.java diff --git a/main/src/main/java/sd/coordinator/CoordinatorProcess.java b/main/src/main/java/sd/coordinator/CoordinatorProcess.java new file mode 100644 index 0000000..9d25ee0 --- /dev/null +++ b/main/src/main/java/sd/coordinator/CoordinatorProcess.java @@ -0,0 +1,204 @@ +package sd.coordinator; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import sd.config.SimulationConfig; +import sd.model.Message; +import sd.model.MessageType; +import sd.model.Vehicle; +import sd.serialization.SerializationException; +import sd.util.VehicleGenerator; + +/** + * Coordinator process responsible for: + * 1. Vehicle generation (using VehicleGenerator) + * 2. Distributing vehicles to intersection processes via sockets + * 3. Managing simulation timing and shutdown + * + * This is the main entry point for the distributed simulation architecture. + */ +public class CoordinatorProcess { + + private final SimulationConfig config; + private final VehicleGenerator vehicleGenerator; + private final Map intersectionClients; + private double currentTime; + private int vehicleCounter; + private boolean running; + private double nextGenerationTime; + + public static void main(String[] args) { + System.out.println("=".repeat(60)); + System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION"); + System.out.println("=".repeat(60)); + + try { + // 1. Load configuration + String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties"; + System.out.println("Loading configuration from: " + configFile); + + SimulationConfig config = new SimulationConfig(configFile); + CoordinatorProcess coordinator = new CoordinatorProcess(config); + + // 2. Connect to intersection processes + System.out.println("\n" + "=".repeat(60)); + coordinator.initialize(); + + // 3. Run the sim + System.out.println("\n" + "=".repeat(60)); + coordinator.run(); + + } catch (IOException e) { + System.err.println("Failed to load configuration: " + e.getMessage()); + System.exit(1); + } catch (Exception e) { + System.err.println("Coordinator error: " + e.getMessage()); + System.exit(1); + } + } + + public CoordinatorProcess(SimulationConfig config) { + this.config = config; + this.vehicleGenerator = new VehicleGenerator(config); + this.intersectionClients = new HashMap<>(); + this.currentTime = 0.0; + this.vehicleCounter = 0; + this.running = false; + this.nextGenerationTime = 0.0; + + System.out.println("Coordinator initialized with configuration:"); + System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s"); + System.out.println(" - Arrival model: " + config.getArrivalModel()); + System.out.println(" - Arrival rate: " + config.getArrivalRate() + " vehicles/s"); + } + + public void initialize() { + System.out.println("Connecting to intersection processes..."); + + String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"}; + + for (String intersectionId : intersectionIds) { + try { + String host = config.getIntersectionHost(intersectionId); + int port = config.getIntersectionPort(intersectionId); + + SocketClient client = new SocketClient(intersectionId, host, port); + client.connect(); + intersectionClients.put(intersectionId, client); + + } catch (IOException e) { + System.err.println("Failed to connect to " + intersectionId + ": " + e.getMessage()); + } + } + + System.out.println("Successfully connected to " + intersectionClients.size() + " intersection(s)"); + + if (intersectionClients.isEmpty()) { + System.err.println("WARNING: No intersections connected. Simulation cannot proceed."); + } + } + + public void run() { + double duration = config.getSimulationDuration(); + running = true; + + System.out.println("Starting vehicle generation simulation..."); + System.out.println("Duration: " + duration + " seconds"); + System.out.println(); + + nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime); + final double TIME_STEP = 0.1; + + while (running && currentTime < duration) { + if (currentTime >= nextGenerationTime) { + generateAndSendVehicle(); + nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime); + } + currentTime += TIME_STEP; + } + + System.out.println(); + System.out.println("Simulation complete at t=" + String.format("%.2f", currentTime) + "s"); + System.out.println("Total vehicles generated: " + vehicleCounter); + + shutdown(); + } + + private void generateAndSendVehicle() { + Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime); + + System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n", + currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute()); + + if (vehicle.getRoute().isEmpty()) { + System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!"); + return; + } + + String entryIntersection = vehicle.getRoute().get(0); + sendVehicleToIntersection(vehicle, entryIntersection); + } + + private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) { + SocketClient client = intersectionClients.get(intersectionId); + + if (client == null || !client.isConnected()) { + System.err.println("ERROR: No connection to " + intersectionId + " for vehicle " + vehicle.getId()); + return; + } + + try { + Message message = new Message( + MessageType.VEHICLE_SPAWN, + "COORDINATOR", + intersectionId, + vehicle + ); + + client.send(message); + System.out.printf("->Sent to %s%n", intersectionId); + + } catch (SerializationException | IOException e) { + System.err.println("ERROR: Failed to send vehicle " + vehicle.getId() + " to " + intersectionId); + System.err.println("Reason: " + e.getMessage()); + } + } + + public void shutdown() { + System.out.println(); + System.out.println("=".repeat(60)); + System.out.println("Shutting down coordinator..."); + + for (Map.Entry entry : intersectionClients.entrySet()) { + String intersectionId = entry.getKey(); + SocketClient client = entry.getValue(); + + try { + if (client.isConnected()) { + Message personalizedShutdown = new Message( + MessageType.SHUTDOWN, + "COORDINATOR", + intersectionId, + "Simulation complete" + ); + client.send(personalizedShutdown); + System.out.println("Sent shutdown message to " + intersectionId); + } + } catch (SerializationException | IOException e) { + System.err.println("Error sending shutdown to " + intersectionId + ": " + e.getMessage()); + } finally { + client.close(); + } + } + + System.out.println("Coordinator shutdown complete"); + System.out.println("=".repeat(60)); + } + + public void stop() { + System.out.println("\nStop signal received..."); + running = false; + } +} diff --git a/main/src/main/java/sd/coordinator/SocketClient.java b/main/src/main/java/sd/coordinator/SocketClient.java new file mode 100644 index 0000000..88d75b2 --- /dev/null +++ b/main/src/main/java/sd/coordinator/SocketClient.java @@ -0,0 +1,124 @@ +package sd.coordinator; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; + +import sd.model.Message; +import sd.serialization.MessageSerializer; +import sd.serialization.SerializationException; +import sd.serialization.SerializerFactory; + +/** + * Socket client for communication with a single intersection process. + * + * Handles a persistent TCP connection to one intersection, + * providing a simple way to send serialized messages. + */ +public class SocketClient { + + private final String intersectionId; + private final String host; + private final int port; + private Socket socket; + private OutputStream outputStream; + private MessageSerializer serializer; + + /** + * Creates a new SocketClient for a given intersection. + * + * @param intersectionId Intersection ID (ex. "Cr1") + * @param host Host address (ex. "localhost") + * @param port Port number + */ + public SocketClient(String intersectionId, String host, int port) { + this.intersectionId = intersectionId; + this.host = host; + this.port = port; + this.serializer = SerializerFactory.createDefault(); + } + + /** + * Connects to the intersection process via TCP. + * + * @throws IOException if the connection cannot be established + */ + + public void connect() throws IOException { + try { + socket = new Socket(host, port); + outputStream = socket.getOutputStream(); + System.out.println("Connected to " + intersectionId + " at " + host + ":" + port); + } catch (IOException e) { + System.err.println("Failed to connect to " + intersectionId + " at " + host + ":" + port); + throw e; + } + } + + /** + * Sends a message to the connected intersection. + * The message is serialized and written over the socket. + * + * @param message The message to send + * @throws SerializationException if serialization fails + * @throws IOException if the socket write fails + */ + public void send(Message message) throws SerializationException, IOException { + if (socket == null || socket.isClosed()) { + throw new IOException("Socket is not connected to " + intersectionId); + } + + try { + byte[] data = serializer.serialize(message); + + // Prefix with message length (so receiver knows how much to read) + int length = data.length; + outputStream.write((length >> 24) & 0xFF); + outputStream.write((length >> 16) & 0xFF); + outputStream.write((length >> 8) & 0xFF); + outputStream.write(length & 0xFF); + + outputStream.write(data); + outputStream.flush(); + + } catch (SerializationException | IOException e) { + System.err.println("Error sending message to " + intersectionId + ": " + e.getMessage()); + throw e; + } + } + + /** + * Closes the socket connection safely. + * Calling it multiple times won’t cause issues. + */ + public void close() { + try { + if (outputStream != null) { + outputStream.close(); + } + if (socket != null && !socket.isClosed()) { + socket.close(); + System.out.println("Closed connection to " + intersectionId); + } + } catch (IOException e) { + System.err.println("Error closing connection to " + intersectionId + ": " + e.getMessage()); + } + } + + /** + * @return true if connected and socket is open, false otherwise + */ + public boolean isConnected() { + return socket != null && socket.isConnected() && !socket.isClosed(); + } + + public String getIntersectionId() { + return intersectionId; + } + + @Override + public String toString() { + return String.format("SocketClient[intersection=%s, host=%s, port=%d, connected=%s]", + intersectionId, host, port, isConnected()); + } +} diff --git a/main/src/main/java/sd/protocol/SocketConnection.java b/main/src/main/java/sd/protocol/SocketConnection.java index f6392a4..2a30641 100644 --- a/main/src/main/java/sd/protocol/SocketConnection.java +++ b/main/src/main/java/sd/protocol/SocketConnection.java @@ -1,15 +1,22 @@ package sd.protocol; import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; +import java.io.InputStream; +import java.io.OutputStream; import java.net.ConnectException; import java.net.Socket; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.concurrent.TimeUnit; +import sd.serialization.MessageSerializer; +import sd.serialization.SerializationException; +import sd.serialization.SerializerFactory; + + /** * Wrapper class that simplifies communication via Sockets. * Includes connection retry logic for robustness. @@ -17,8 +24,9 @@ import java.util.concurrent.TimeUnit; public class SocketConnection implements Closeable { private final Socket socket; - private final ObjectOutputStream outputStream; - private final ObjectInputStream inputStream; + private final OutputStream outputStream; + private final InputStream inputStream; + private final MessageSerializer serializer; // --- Configuration for Retry Logic --- /** Maximum number of connection attempts. */ @@ -88,16 +96,11 @@ public class SocketConnection implements Closeable { // 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 - } + + this.outputStream = socket.getOutputStream(); + this.inputStream = socket.getInputStream(); + this.serializer = SerializerFactory.createDefault(); + } @@ -111,12 +114,10 @@ public class SocketConnection implements Closeable { */ 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()); + this.outputStream = socket.getOutputStream(); + this.inputStream = socket.getInputStream(); + this.serializer = SerializerFactory.createDefault(); + } /** @@ -126,52 +127,66 @@ public class SocketConnection implements Closeable { * @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."); + if (socket == null || !socket.isConnected()) { + throw new IOException("Socket is not connected"); } - synchronized (outputStream) { - outputStream.writeObject(message); - outputStream.flush(); // Ensures the message is sent immediately. + + try { + // Serializa para bytes JSON + byte[] data = serializer.serialize(message); + + // Write 4-byte length prefix + DataOutputStream dataOut = new DataOutputStream(outputStream); + dataOut.writeInt(data.length); + dataOut.write(data); + dataOut.flush(); + + } catch (SerializationException e) { + throw new IOException("Failed to serialize message", e); } } /** * Tries to read (deserialize) a MessageProtocol object from the socket. - * 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."); + if (socket == null || !socket.isConnected()) { + throw new IOException("Socket is not connected"); } - synchronized (inputStream) { - return (MessageProtocol) inputStream.readObject(); + + try { + // Lê um prefixo de 4 bytes - indicador de tamanho + DataInputStream dataIn = new DataInputStream(inputStream); + int length = dataIn.readInt(); + + if (length <= 0 || length > 10_000_000) { // Sanity check (10MB max) + throw new IOException("Invalid message length: " + length); + } + + // Ler dados da mensagem + byte[] data = new byte[length]; + dataIn.readFully(data); + + // Deserialize do JSON + return serializer.deserialize(data, MessageProtocol.class); + + } catch (SerializationException e) { + throw new IOException("Failed to deserialize message", e); } } /** * Closes the socket and all streams (Input and Output). - * 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(); - } + if (inputStream != null) inputStream.close(); + if (outputStream != null) outputStream.close(); + if (socket != null) socket.close(); } /** diff --git a/main/src/test/java/sd/coordinator/CoordinatorIntegrationTest.java b/main/src/test/java/sd/coordinator/CoordinatorIntegrationTest.java new file mode 100644 index 0000000..7264f87 --- /dev/null +++ b/main/src/test/java/sd/coordinator/CoordinatorIntegrationTest.java @@ -0,0 +1,302 @@ +package sd.coordinator; + +import java.io.DataInputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import sd.model.Message; +import sd.model.MessageType; +import sd.model.Vehicle; +import sd.serialization.MessageSerializer; +import sd.serialization.SerializerFactory; + +/** + * Integration tests for the Coordinator-side networking. + * + * What we’re checking here: + * 1. A SocketClient can actually connect to something listening + * 2. Messages go over the wire and can be deserialized + * 3. Vehicle payloads survive the trip + * 4. Shutdown messages can be broadcast to multiple intersections + * + * We do this by spinning up a tiny mock intersection server in-process. + */ +class CoordinatorIntegrationTest { + + private List mockServers; + private static final int BASE_PORT = 9001; // keep clear of real ports + + @BeforeEach + void setUp() { + mockServers = new ArrayList<>(); + } + + @AfterEach + void tearDown() { + // Stop all mock servers + for (MockIntersectionServer server : mockServers) { + server.stop(); + } + mockServers.clear(); + } + + /** + * Can the client open a TCP connection to our fake intersection? + */ + @Test + @Timeout(5) + void testSocketClientConnection() throws IOException, InterruptedException { + MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT); + server.start(); + mockServers.add(server); + + // tiny pause to let the server bind + Thread.sleep(100); + + SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT); + client.connect(); + + assertTrue(client.isConnected(), "Client should be connected to mock intersection"); + + client.close(); + } + + /** + * End-to-end: send a message, make sure the server actually receives it. + */ + @Test + @Timeout(5) + void testMessageTransmission() throws Exception { + MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT); + server.start(); + mockServers.add(server); + + Thread.sleep(100); + + SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT); + client.connect(); + + Message testMessage = new Message( + MessageType.VEHICLE_SPAWN, + "COORDINATOR", + "Cr1", + "Test payload" + ); + + client.send(testMessage); + + // give the server a moment to read and deserialize + Thread.sleep(200); + + assertFalse( + server.getReceivedMessages().isEmpty(), + "Mock server should have received at least one message" + ); + + Message receivedMsg = server.getReceivedMessages().poll(); + assertNotNull(receivedMsg, "Server should have actually received a message"); + assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message type should match what we sent"); + assertEquals("COORDINATOR", receivedMsg.getSenderId(), "Sender ID should be preserved"); + assertEquals("Cr1", receivedMsg.getDestinationId(), "Destination ID should be preserved"); + + client.close(); + } + + /** + * Make sure vehicle payloads survive the trip and arrive non-null. + */ + @Test + @Timeout(5) + void testVehicleSpawnMessage() throws Exception { + MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT); + server.start(); + mockServers.add(server); + + Thread.sleep(100); + + SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT); + client.connect(); + + // fake a vehicle like the coordinator would send + List route = List.of("Cr1", "Cr4", "Cr5", "S"); + Vehicle vehicle = new Vehicle("V1", sd.model.VehicleType.LIGHT, 0.0, route); + + Message spawnMessage = new Message( + MessageType.VEHICLE_SPAWN, + "COORDINATOR", + "Cr1", + vehicle + ); + + client.send(spawnMessage); + + Thread.sleep(200); + + Message receivedMsg = server.getReceivedMessages().poll(); + assertNotNull(receivedMsg, "Mock server should receive the spawn message"); + assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message should be of type VEHICLE_SPAWN"); + assertNotNull(receivedMsg.getPayload(), "Payload should not be null (vehicle must arrive)"); + + client.close(); + } + + /** + * Broadcast shutdown to multiple mock intersections and see if all of them get it. + */ + @Test + @Timeout(5) + void testShutdownMessageBroadcast() throws Exception { + // Start a couple of fake intersections + for (int i = 1; i <= 3; i++) { + MockIntersectionServer server = new MockIntersectionServer("Cr" + i, BASE_PORT + i - 1); + server.start(); + mockServers.add(server); + } + + Thread.sleep(200); + + // Connect to all of them + List clients = new ArrayList<>(); + for (int i = 1; i <= 3; i++) { + SocketClient client = new SocketClient("Cr" + i, "localhost", BASE_PORT + i - 1); + client.connect(); + clients.add(client); + } + + Message shutdownMessage = new Message( + MessageType.SHUTDOWN, + "COORDINATOR", + "ALL", + "Simulation complete" + ); + + for (SocketClient client : clients) { + client.send(shutdownMessage); + } + + Thread.sleep(200); + + for (MockIntersectionServer server : mockServers) { + assertFalse( + server.getReceivedMessages().isEmpty(), + "Server " + server.getIntersectionId() + " should have received the shutdown message" + ); + + Message msg = server.getReceivedMessages().poll(); + assertEquals(MessageType.SHUTDOWN, msg.getType(), "Server should receive a SHUTDOWN message"); + } + + for (SocketClient client : clients) { + client.close(); + } + } + + /** + * Tiny TCP server that pretends to be an intersection. + * It: + * - listens on a port + * - accepts connections + * - reads length-prefixed messages + * - deserializes them and stores them for the test to inspect + */ + private static class MockIntersectionServer { + private final String intersectionId; + private final int port; + private ServerSocket serverSocket; + private Thread serverThread; + private volatile boolean running; + private final ConcurrentLinkedQueue receivedMessages; + private final MessageSerializer serializer; + + public MockIntersectionServer(String intersectionId, int port) { + this.intersectionId = intersectionId; + this.port = port; + this.receivedMessages = new ConcurrentLinkedQueue<>(); + this.serializer = SerializerFactory.createDefault(); + this.running = false; + } + + public void start() throws IOException { + serverSocket = new ServerSocket(port); + running = true; + + System.out.printf("Mock %s listening on port %d%n", intersectionId, port); + + serverThread = new Thread(() -> { + try { + while (running) { + Socket clientSocket = serverSocket.accept(); + handleClient(clientSocket); + } + } catch (IOException e) { + if (running) { + System.err.println("Mock " + intersectionId + " server error: " + e.getMessage()); + } + } + }, "mock-" + intersectionId + "-listener"); + + serverThread.start(); + } + + private void handleClient(Socket clientSocket) { + new Thread(() -> { + try (DataInputStream input = new DataInputStream(clientSocket.getInputStream())) { + while (running) { + // Read length prefix (4 bytes, big-endian) + int length = input.readInt(); + byte[] data = new byte[length]; + input.readFully(data); + + Message message = serializer.deserialize(data, Message.class); + receivedMessages.offer(message); + + System.out.println("Mock " + intersectionId + " received: " + message.getType()); + } + } catch (IOException e) { + if (running) { + System.err.println("Mock " + intersectionId + " client handler error: " + e.getMessage()); + } + } catch (Exception e) { + System.err.println("Mock " + intersectionId + " deserialization error: " + e.getMessage()); + } + }, "mock-" + intersectionId + "-client").start(); + } + + public void stop() { + running = false; + try { + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + } + if (serverThread != null) { + serverThread.interrupt(); + serverThread.join(1000); + } + System.out.printf("Mock %s stopped%n", intersectionId); + } catch (IOException | InterruptedException e) { + System.err.println("Error stopping mock server " + intersectionId + ": " + e.getMessage()); + } + } + + public ConcurrentLinkedQueue getReceivedMessages() { + return receivedMessages; + } + + public String getIntersectionId() { + return intersectionId; + } + } +} diff --git a/main/src/test/java/sd/coordinator/CoordinatorProcessTest.java b/main/src/test/java/sd/coordinator/CoordinatorProcessTest.java new file mode 100644 index 0000000..f334d90 --- /dev/null +++ b/main/src/test/java/sd/coordinator/CoordinatorProcessTest.java @@ -0,0 +1,194 @@ +package sd.coordinator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import sd.config.SimulationConfig; +import sd.model.Vehicle; +import sd.util.VehicleGenerator; + +/** + * Tests for the Coordinator/vehicle-generation layer. + * + * What we’re checking here: + * 1. Coordinator can be created with a valid config + * 2. Vehicle arrival times are monotonic and sane + * 3. Vehicle IDs are created in the format we expect (V1, V2, ...) + * 4. Generated vehicles have proper routes (start at CrX, end at S) + * 5. Config actually has intersection info + * 6. Duration in config is not something crazy + */ +class CoordinatorProcessTest { + + private SimulationConfig config; + private static final String TEST_CONFIG = "src/main/resources/simulation.properties"; + + @BeforeEach + void setUp() throws IOException { + config = new SimulationConfig(TEST_CONFIG); + } + + @AfterEach + void tearDown() { + config = null; + } + + /** + * Basic smoke test: can we build a coordinator with this config? + */ + @Test + void testCoordinatorInitialization() { + CoordinatorProcess coordinator = new CoordinatorProcess(config); + assertNotNull(coordinator, "Coordinator should be created with a valid config"); + } + + /** + * Make sure the VehicleGenerator is giving us increasing arrival times, + * i.e. time doesn’t go backwards and intervals look reasonable. + */ + @Test + void testVehicleGenerationTiming() { + VehicleGenerator generator = new VehicleGenerator(config); + + double currentTime = 0.0; + List arrivalTimes = new ArrayList<>(); + + // generate a small batch to inspect + for (int i = 0; i < 10; i++) { + double nextArrival = generator.getNextArrivalTime(currentTime); + arrivalTimes.add(nextArrival); + currentTime = nextArrival; + } + + // times should strictly increase + for (int i = 1; i < arrivalTimes.size(); i++) { + assertTrue( + arrivalTimes.get(i) > arrivalTimes.get(i - 1), + "Arrival times must increase — got " + arrivalTimes.get(i - 1) + " then " + arrivalTimes.get(i) + ); + } + + // and they shouldn't be nonsense + for (double time : arrivalTimes) { + assertTrue(time >= 0, "Arrival time should not be negative (got " + time + ")"); + assertTrue(time < 1000, "Arrival time looks suspiciously large: " + time); + } + } + + /** + * We generate V1..V5 manually and make sure the IDs are exactly those. + */ + @Test + void testVehicleIdGeneration() { + VehicleGenerator generator = new VehicleGenerator(config); + + List vehicles = new ArrayList<>(); + for (int i = 1; i <= 5; i++) { + Vehicle v = generator.generateVehicle("V" + i, 0.0); + vehicles.add(v); + assertEquals("V" + i, v.getId(), "Vehicle ID should be 'V" + i + "' but got " + v.getId()); + } + + // just to be safe, no duplicates in that small set + long distinctCount = vehicles.stream().map(Vehicle::getId).distinct().count(); + assertEquals(5, distinctCount, "Vehicle IDs in this batch should all be unique"); + } + + /** + * A generated vehicle should: + * - have a non-empty route + * - start in a known intersection (Cr1..Cr5) + * - end in S (exit) + */ + @Test + void testVehicleRouteValidity() { + VehicleGenerator generator = new VehicleGenerator(config); + + for (int i = 0; i < 20; i++) { + Vehicle vehicle = generator.generateVehicle("V" + i, 0.0); + + assertNotNull(vehicle.getRoute(), "Vehicle route should not be null"); + assertFalse(vehicle.getRoute().isEmpty(), "Vehicle route should not be empty"); + + String firstHop = vehicle.getRoute().get(0); + assertTrue( + firstHop.matches("Cr[1-5]"), + "First hop should be a valid intersection (Cr1..Cr5), got: " + firstHop + ); + + String lastHop = vehicle.getRoute().get(vehicle.getRoute().size() - 1); + assertEquals("S", lastHop, "Last hop should be exit 'S' but got: " + lastHop); + } + } + + /** + * Whatever is in simulation.properties should give us a sane duration. + */ + @Test + void testSimulationDuration() { + double duration = config.getSimulationDuration(); + assertTrue(duration > 0, "Simulation duration must be positive"); + assertTrue(duration >= 1.0, "Simulation should run at least 1 second (got " + duration + ")"); + assertTrue(duration <= 86400.0, "Simulation should not run more than a day (got " + duration + ")"); + } + + /** + * Check that the 5 intersections defined in the architecture + * actually exist in the config and have valid network data. + */ + @Test + void testIntersectionConfiguration() { + String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"}; + + for (String id : intersectionIds) { + String host = config.getIntersectionHost(id); + int port = config.getIntersectionPort(id); + + assertNotNull(host, "Host should not be null for " + id); + assertFalse(host.isEmpty(), "Host should not be empty for " + id); + assertTrue(port > 0, "Port should be > 0 for " + id + " (got " + port + ")"); + assertTrue(port < 65536, "Port should be a valid TCP port for " + id + " (got " + port + ")"); + } + } + + /** + * Quick sanity check: over a bunch of generated vehicles, + * we should eventually see the different vehicle types appear. + * + * Note: this is probabilistic, so we're not being super strict. + */ + @Test + void testVehicleTypeDistribution() { + VehicleGenerator generator = new VehicleGenerator(config); + + boolean hasBike = false; + boolean hasLight = false; + boolean hasHeavy = false; + + // 50 is enough for a "we're probably fine" test + for (int i = 0; i < 50; i++) { + Vehicle vehicle = generator.generateVehicle("V" + i, 0.0); + + switch (vehicle.getType()) { + case BIKE -> hasBike = true; + case LIGHT -> hasLight = true; + case HEAVY -> hasHeavy = true; + } + } + + // at least one of them should have shown up — if not, RNG is cursed + assertTrue( + hasBike || hasLight || hasHeavy, + "Expected to see at least one vehicle type after 50 generations" + ); + } +} From 0c256ad6f5ccd9373b94e7c82fed50ccedf579df Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Sun, 2 Nov 2025 23:55:37 +0000 Subject: [PATCH 2/2] Fix Intersection Destination - Doubled Advance --- main/src/main/java/sd/model/Intersection.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/src/main/java/sd/model/Intersection.java b/main/src/main/java/sd/model/Intersection.java index 4475fc3..bc8dea7 100644 --- a/main/src/main/java/sd/model/Intersection.java +++ b/main/src/main/java/sd/model/Intersection.java @@ -114,8 +114,8 @@ public class Intersection { public void receiveVehicle(Vehicle vehicle) { totalVehiclesReceived++; - // Advance route since vehicle just arrived at this intersection - vehicle.advanceRoute(); + // Note: Route advancement is handled by SimulationEngine.handleVehicleArrival() + // before calling this method, so we don't advance here. String nextDestination = vehicle.getCurrentDestination();