mirror of
https://github.com/davidalves04/Trabalho-Pratico-SD.git
synced 2025-12-08 12:33:31 +00:00
Compare commits
11 Commits
11-convert
...
12-impleme
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c256ad6f5 | |||
| 340e436063 | |||
| 1684a6713e | |||
| 22a7081ade | |||
| 3b699556db | |||
|
|
d078808486 | ||
|
|
98581b562d | ||
| f9644bd18c | |||
|
|
c6b710ac52 | ||
|
|
d057adeab3 | ||
|
|
be4e7f66d6 |
204
main/src/main/java/sd/coordinator/CoordinatorProcess.java
Normal file
204
main/src/main/java/sd/coordinator/CoordinatorProcess.java
Normal file
@@ -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<String, SocketClient> 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<String, SocketClient> 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;
|
||||
}
|
||||
}
|
||||
124
main/src/main/java/sd/coordinator/SocketClient.java
Normal file
124
main/src/main/java/sd/coordinator/SocketClient.java
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
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 +25,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 +97,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 +115,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 +128,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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<MockIntersectionServer> 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<String> 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<SocketClient> 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<Message> 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<Message> getReceivedMessages() {
|
||||
return receivedMessages;
|
||||
}
|
||||
|
||||
public String getIntersectionId() {
|
||||
return intersectionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
194
main/src/test/java/sd/coordinator/CoordinatorProcessTest.java
Normal file
194
main/src/test/java/sd/coordinator/CoordinatorProcessTest.java
Normal file
@@ -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<Double> 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<Vehicle> 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user