mirror of
https://github.com/davidalves04/Trabalho-Pratico-SD.git
synced 2025-12-08 20:43:32 +00:00
Implementation of the Coordinator Process
This commit is contained in:
@@ -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