diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java index ee95058..57c658f 100644 --- a/main/src/main/java/sd/IntersectionProcess.java +++ b/main/src/main/java/sd/IntersectionProcess.java @@ -171,6 +171,29 @@ public class IntersectionProcess { System.out.println(" Routing configured."); } + /** + * Requests permission for a traffic light to turn green. + * Blocks until permission is granted (no other light is green). + * + * @param direction The direction requesting green light + */ + public void requestGreenLight(String direction) { + trafficCoordinationLock.lock(); + currentGreenDirection = direction; + } + + /** + * Releases the green light permission, allowing another light to turn green. + * + * @param direction The direction releasing green light + */ + public void releaseGreenLight(String direction) { + if (direction.equals(currentGreenDirection)) { + currentGreenDirection = null; + trafficCoordinationLock.unlock(); + } + } + /** * Starts all traffic light threads. */ diff --git a/main/src/main/java/sd/dashboard/DashboardClientHandler.java b/main/src/main/java/sd/dashboard/DashboardClientHandler.java new file mode 100644 index 0000000..11ccb17 --- /dev/null +++ b/main/src/main/java/sd/dashboard/DashboardClientHandler.java @@ -0,0 +1,110 @@ +package sd.dashboard; + +import java.io.IOException; +import java.net.Socket; + +import sd.model.MessageType; +import sd.protocol.MessageProtocol; +import sd.protocol.SocketConnection; + +/** + * Processes statistics messages from a single client connection. + * Runs in a separate thread per client. + */ +public class DashboardClientHandler implements Runnable { + + private final Socket clientSocket; + private final DashboardStatistics statistics; + + public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) { + this.clientSocket = clientSocket; + this.statistics = statistics; + } + + @Override + public void run() { + String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort(); + + try (SocketConnection connection = new SocketConnection(clientSocket)) { + System.out.println("[Handler] Started handling client: " + clientInfo); + + while (!Thread.currentThread().isInterrupted()) { + try { + MessageProtocol message = connection.receiveMessage(); + + if (message == null) { + System.out.println("[Handler] Client disconnected: " + clientInfo); + break; + } + + processMessage(message); + + } catch (ClassNotFoundException e) { + System.err.println("[Handler] Unknown message class from " + clientInfo + ": " + e.getMessage()); + } catch (IOException e) { + System.out.println("[Handler] Connection error with " + clientInfo + ": " + e.getMessage()); + break; + } + } + + } catch (IOException e) { + System.err.println("[Handler] Error initializing connection with " + clientInfo + ": " + e.getMessage()); + } finally { + try { + if (!clientSocket.isClosed()) { + clientSocket.close(); + } + } catch (IOException e) { + System.err.println("[Handler] Error closing socket for " + clientInfo + ": " + e.getMessage()); + } + } + } + + private void processMessage(MessageProtocol message) { + if (message.getType() != MessageType.STATS_UPDATE) { + System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType()); + return; + } + + String senderId = message.getSourceNode(); + Object payload = message.getPayload(); + + System.out.println("[Handler] Received STATS_UPDATE from: " + senderId); + + if (payload instanceof StatsUpdatePayload stats) { + updateStatistics(senderId, stats); + } else { + System.err.println("[Handler] Unknown payload type: " + + (payload != null ? payload.getClass().getName() : "null")); + } + } + + private void updateStatistics(String senderId, StatsUpdatePayload stats) { + if (stats.getTotalVehiclesGenerated() >= 0) { + statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated()); + } + + if (stats.getTotalVehiclesCompleted() >= 0) { + statistics.updateVehiclesCompleted(stats.getTotalVehiclesCompleted()); + } + + if (stats.getTotalSystemTime() >= 0) { + statistics.addSystemTime(stats.getTotalSystemTime()); + } + + if (stats.getTotalWaitingTime() >= 0) { + statistics.addWaitingTime(stats.getTotalWaitingTime()); + } + + if (senderId.startsWith("Cr") || senderId.startsWith("E")) { + statistics.updateIntersectionStats( + senderId, + stats.getIntersectionArrivals(), + stats.getIntersectionDepartures(), + stats.getIntersectionQueueSize() + ); + } + + System.out.println("[Handler] Successfully updated statistics from: " + senderId); + } +} diff --git a/main/src/main/java/sd/dashboard/DashboardServer.java b/main/src/main/java/sd/dashboard/DashboardServer.java new file mode 100644 index 0000000..9299d0a --- /dev/null +++ b/main/src/main/java/sd/dashboard/DashboardServer.java @@ -0,0 +1,148 @@ +package sd.dashboard; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import sd.config.SimulationConfig; + +/** + * Aggregates and displays real-time statistics from all simulation processes. + * Uses a thread pool to handle concurrent client connections. + */ +public class DashboardServer { + + private final int port; + private final DashboardStatistics statistics; + private final ExecutorService clientHandlerPool; + private final AtomicBoolean running; + private ServerSocket serverSocket; + + public static void main(String[] args) { + System.out.println("=".repeat(60)); + System.out.println("DASHBOARD SERVER - DISTRIBUTED TRAFFIC SIMULATION"); + System.out.println("=".repeat(60)); + + try { + // 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); + DashboardServer server = new DashboardServer(config); + + // Start the server + System.out.println("\n" + "=".repeat(60)); + server.start(); + + // Keep running until interrupted + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\n\nShutdown signal received..."); + server.stop(); + })); + + // Display statistics periodically + server.displayLoop(); + + } catch (IOException e) { + System.err.println("Failed to start Dashboard Server: " + e.getMessage()); + System.exit(1); + } + } + + public DashboardServer(SimulationConfig config) { + this.port = config.getDashboardPort(); + this.statistics = new DashboardStatistics(); + this.clientHandlerPool = Executors.newFixedThreadPool(10); + this.running = new AtomicBoolean(false); + } + + public void start() throws IOException { + if (running.get()) { + System.out.println("Dashboard Server is already running."); + return; + } + + serverSocket = new ServerSocket(port); + running.set(true); + + System.out.println("Dashboard Server started on port " + port); + System.out.println("Waiting for statistics updates from simulation processes..."); + System.out.println("=".repeat(60)); + + Thread acceptThread = new Thread(this::acceptConnections, "DashboardServer-Accept"); + acceptThread.setDaemon(false); + acceptThread.start(); + } + + private void acceptConnections() { + while (running.get()) { + try { + Socket clientSocket = serverSocket.accept(); + System.out.println("[Connection] New client connected: " + + clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort()); + + clientHandlerPool.execute(new DashboardClientHandler(clientSocket, statistics)); + + } catch (IOException e) { + if (running.get()) { + System.err.println("[Error] Failed to accept client connection: " + e.getMessage()); + } + } + } + } + + @SuppressWarnings("BusyWait") + private void displayLoop() { + final long DISPLAY_INTERVAL_MS = 5000; + + while (running.get()) { + try { + Thread.sleep(DISPLAY_INTERVAL_MS); + displayStatistics(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + public void displayStatistics() { + System.out.println("\n" + "=".repeat(60)); + System.out.println("REAL-TIME SIMULATION STATISTICS"); + System.out.println("=".repeat(60)); + statistics.display(); + System.out.println("=".repeat(60)); + } + + public void stop() { + if (!running.get()) { + return; + } + + System.out.println("\nStopping Dashboard Server..."); + running.set(false); + + try { + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + } + } catch (IOException e) { + System.err.println("Error closing server socket: " + e.getMessage()); + } + + clientHandlerPool.shutdownNow(); + System.out.println("Dashboard Server stopped."); + } + + public DashboardStatistics getStatistics() { + return statistics; + } + + public boolean isRunning() { + return running.get(); + } +} diff --git a/main/src/main/java/sd/dashboard/DashboardStatistics.java b/main/src/main/java/sd/dashboard/DashboardStatistics.java new file mode 100644 index 0000000..56f227c --- /dev/null +++ b/main/src/main/java/sd/dashboard/DashboardStatistics.java @@ -0,0 +1,214 @@ +package sd.dashboard; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import sd.model.VehicleType; + +/** + * Thread-safe storage for aggregated simulation statistics. + * Uses atomic types and concurrent collections for lock-free updates. + */ +public class DashboardStatistics { + + private final AtomicInteger totalVehiclesGenerated; + private final AtomicInteger totalVehiclesCompleted; + private final AtomicLong totalSystemTime; + private final AtomicLong totalWaitingTime; + + private final Map intersectionStats; + private final Map vehicleTypeCount; + private final Map vehicleTypeWaitTime; + + private volatile long lastUpdateTime; + + public DashboardStatistics() { + this.totalVehiclesGenerated = new AtomicInteger(0); + this.totalVehiclesCompleted = new AtomicInteger(0); + this.totalSystemTime = new AtomicLong(0); + this.totalWaitingTime = new AtomicLong(0); + + this.intersectionStats = new ConcurrentHashMap<>(); + this.vehicleTypeCount = new ConcurrentHashMap<>(); + this.vehicleTypeWaitTime = new ConcurrentHashMap<>(); + + for (VehicleType type : VehicleType.values()) { + vehicleTypeCount.put(type, new AtomicInteger(0)); + vehicleTypeWaitTime.put(type, new AtomicLong(0)); + } + + this.lastUpdateTime = System.currentTimeMillis(); + } + + public void updateVehiclesGenerated(int count) { + totalVehiclesGenerated.set(count); + updateTimestamp(); + } + + public void incrementVehiclesGenerated() { + totalVehiclesGenerated.incrementAndGet(); + updateTimestamp(); + } + + public void updateVehiclesCompleted(int count) { + totalVehiclesCompleted.set(count); + updateTimestamp(); + } + + public void incrementVehiclesCompleted() { + totalVehiclesCompleted.incrementAndGet(); + updateTimestamp(); + } + + public void addSystemTime(long timeMs) { + totalSystemTime.addAndGet(timeMs); + updateTimestamp(); + } + + public void addWaitingTime(long timeMs) { + totalWaitingTime.addAndGet(timeMs); + updateTimestamp(); + } + + public void updateVehicleTypeStats(VehicleType type, int count, long waitTimeMs) { + vehicleTypeCount.get(type).set(count); + vehicleTypeWaitTime.get(type).set(waitTimeMs); + updateTimestamp(); + } + + public void incrementVehicleType(VehicleType type) { + vehicleTypeCount.get(type).incrementAndGet(); + updateTimestamp(); + } + + public void updateIntersectionStats(String intersectionId, int arrivals, + int departures, int currentQueueSize) { + intersectionStats.compute(intersectionId, (id, stats) -> { + if (stats == null) { + stats = new IntersectionStats(intersectionId); + } + stats.updateStats(arrivals, departures, currentQueueSize); + return stats; + }); + updateTimestamp(); + } + + private void updateTimestamp() { + lastUpdateTime = System.currentTimeMillis(); + } + + public int getTotalVehiclesGenerated() { + return totalVehiclesGenerated.get(); + } + + public int getTotalVehiclesCompleted() { + return totalVehiclesCompleted.get(); + } + + public double getAverageSystemTime() { + int completed = totalVehiclesCompleted.get(); + if (completed == 0) return 0.0; + return (double) totalSystemTime.get() / completed; + } + + public double getAverageWaitingTime() { + int completed = totalVehiclesCompleted.get(); + if (completed == 0) return 0.0; + return (double) totalWaitingTime.get() / completed; + } + + public int getVehicleTypeCount(VehicleType type) { + return vehicleTypeCount.get(type).get(); + } + + public double getAverageWaitingTimeByType(VehicleType type) { + int count = vehicleTypeCount.get(type).get(); + if (count == 0) return 0.0; + return (double) vehicleTypeWaitTime.get(type).get() / count; + } + + public IntersectionStats getIntersectionStats(String intersectionId) { + return intersectionStats.get(intersectionId); + } + + public Map getAllIntersectionStats() { + return new HashMap<>(intersectionStats); + } + + public long getLastUpdateTime() { + return lastUpdateTime; + } + + public void display() { + System.out.println("\n--- GLOBAL STATISTICS ---"); + System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated()); + System.out.printf("Total Vehicles Completed: %d%n", getTotalVehiclesCompleted()); + System.out.printf("Vehicles In Transit: %d%n", + getTotalVehiclesGenerated() - getTotalVehiclesCompleted()); + System.out.printf("Average System Time: %.2f ms%n", getAverageSystemTime()); + System.out.printf("Average Waiting Time: %.2f ms%n", getAverageWaitingTime()); + + System.out.println("\n--- VEHICLE TYPE STATISTICS ---"); + for (VehicleType type : VehicleType.values()) { + int count = getVehicleTypeCount(type); + double avgWait = getAverageWaitingTimeByType(type); + System.out.printf("%s: %d vehicles, avg wait: %.2f ms%n", + type, count, avgWait); + } + + System.out.println("\n--- INTERSECTION STATISTICS ---"); + if (intersectionStats.isEmpty()) { + System.out.println("(No data received yet)"); + } else { + for (IntersectionStats stats : intersectionStats.values()) { + stats.display(); + } + } + + System.out.printf("%nLast Update: %tT%n", lastUpdateTime); + } + + public static class IntersectionStats { + private final String intersectionId; + private final AtomicInteger totalArrivals; + private final AtomicInteger totalDepartures; + private final AtomicInteger currentQueueSize; + + public IntersectionStats(String intersectionId) { + this.intersectionId = intersectionId; + this.totalArrivals = new AtomicInteger(0); + this.totalDepartures = new AtomicInteger(0); + this.currentQueueSize = new AtomicInteger(0); + } + + public void updateStats(int arrivals, int departures, int queueSize) { + this.totalArrivals.set(arrivals); + this.totalDepartures.set(departures); + this.currentQueueSize.set(queueSize); + } + + public String getIntersectionId() { + return intersectionId; + } + + public int getTotalArrivals() { + return totalArrivals.get(); + } + + public int getTotalDepartures() { + return totalDepartures.get(); + } + + public int getCurrentQueueSize() { + return currentQueueSize.get(); + } + + public void display() { + System.out.printf("%s: Arrivals=%d, Departures=%d, Queue=%d%n", + intersectionId, getTotalArrivals(), getTotalDepartures(), getCurrentQueueSize()); + } + } +} diff --git a/main/src/main/java/sd/dashboard/StatsMessage.java b/main/src/main/java/sd/dashboard/StatsMessage.java new file mode 100644 index 0000000..7209130 --- /dev/null +++ b/main/src/main/java/sd/dashboard/StatsMessage.java @@ -0,0 +1,48 @@ +package sd.dashboard; + +import sd.model.MessageType; +import sd.protocol.MessageProtocol; + +/** + * Message wrapper for sending statistics to the dashboard. + */ +public class StatsMessage implements MessageProtocol { + + private static final long serialVersionUID = 1L; + + private final String sourceNode; + private final String destinationNode; + private final StatsUpdatePayload payload; + + public StatsMessage(String sourceNode, StatsUpdatePayload payload) { + this.sourceNode = sourceNode; + this.destinationNode = "DashboardServer"; + this.payload = payload; + } + + @Override + public MessageType getType() { + return MessageType.STATS_UPDATE; + } + + @Override + public Object getPayload() { + return payload; + } + + @Override + public String getSourceNode() { + return sourceNode; + } + + @Override + public String getDestinationNode() { + return destinationNode; + } + + @Override + public String toString() { + return String.format("StatsMessage[from=%s, to=%s, payload=%s]", + sourceNode, destinationNode, payload); + } +} diff --git a/main/src/main/java/sd/dashboard/StatsUpdatePayload.java b/main/src/main/java/sd/dashboard/StatsUpdatePayload.java new file mode 100644 index 0000000..a84760b --- /dev/null +++ b/main/src/main/java/sd/dashboard/StatsUpdatePayload.java @@ -0,0 +1,121 @@ +package sd.dashboard; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import sd.model.VehicleType; + +/** + * Data transfer object for statistics updates to the dashboard. + * Use -1 for fields not being updated in this message. + */ +public class StatsUpdatePayload implements Serializable { + + private static final long serialVersionUID = 1L; + + private int totalVehiclesGenerated = -1; + private int totalVehiclesCompleted = -1; + private long totalSystemTime = -1; + private long totalWaitingTime = -1; + + private int intersectionArrivals = 0; + private int intersectionDepartures = 0; + private int intersectionQueueSize = 0; + + private Map vehicleTypeCounts; + private Map vehicleTypeWaitTimes; + + public StatsUpdatePayload() { + this.vehicleTypeCounts = new HashMap<>(); + this.vehicleTypeWaitTimes = new HashMap<>(); + } + + public int getTotalVehiclesGenerated() { + return totalVehiclesGenerated; + } + + public int getTotalVehiclesCompleted() { + return totalVehiclesCompleted; + } + + public long getTotalSystemTime() { + return totalSystemTime; + } + + public long getTotalWaitingTime() { + return totalWaitingTime; + } + + public int getIntersectionArrivals() { + return intersectionArrivals; + } + + public int getIntersectionDepartures() { + return intersectionDepartures; + } + + public int getIntersectionQueueSize() { + return intersectionQueueSize; + } + + public Map getVehicleTypeCounts() { + return vehicleTypeCounts; + } + + public Map getVehicleTypeWaitTimes() { + return vehicleTypeWaitTimes; + } + + public StatsUpdatePayload setTotalVehiclesGenerated(int totalVehiclesGenerated) { + this.totalVehiclesGenerated = totalVehiclesGenerated; + return this; + } + + public StatsUpdatePayload setTotalVehiclesCompleted(int totalVehiclesCompleted) { + this.totalVehiclesCompleted = totalVehiclesCompleted; + return this; + } + + public StatsUpdatePayload setTotalSystemTime(long totalSystemTime) { + this.totalSystemTime = totalSystemTime; + return this; + } + + public StatsUpdatePayload setTotalWaitingTime(long totalWaitingTime) { + this.totalWaitingTime = totalWaitingTime; + return this; + } + + public StatsUpdatePayload setIntersectionArrivals(int intersectionArrivals) { + this.intersectionArrivals = intersectionArrivals; + return this; + } + + public StatsUpdatePayload setIntersectionDepartures(int intersectionDepartures) { + this.intersectionDepartures = intersectionDepartures; + return this; + } + + public StatsUpdatePayload setIntersectionQueueSize(int intersectionQueueSize) { + this.intersectionQueueSize = intersectionQueueSize; + return this; + } + + public StatsUpdatePayload setVehicleTypeCounts(Map vehicleTypeCounts) { + this.vehicleTypeCounts = vehicleTypeCounts; + return this; + } + + public StatsUpdatePayload setVehicleTypeWaitTimes(Map vehicleTypeWaitTimes) { + this.vehicleTypeWaitTimes = vehicleTypeWaitTimes; + return this; + } + + @Override + public String toString() { + return String.format("StatsUpdatePayload[generated=%d, completed=%d, arrivals=%d, departures=%d, queueSize=%d]", + totalVehiclesGenerated, totalVehiclesCompleted, intersectionArrivals, + intersectionDepartures, intersectionQueueSize); + } +} diff --git a/main/src/main/java/sd/engine/TrafficLightThread.java b/main/src/main/java/sd/engine/TrafficLightThread.java index f3951df..1f1c197 100644 --- a/main/src/main/java/sd/engine/TrafficLightThread.java +++ b/main/src/main/java/sd/engine/TrafficLightThread.java @@ -29,7 +29,6 @@ public class TrafficLightThread implements Runnable { @Override public void run() { - // Capture the current thread reference this.currentThread = Thread.currentThread(); this.running = true; System.out.println("[" + light.getId() + "] Traffic light thread started."); @@ -37,29 +36,37 @@ public class TrafficLightThread implements Runnable { try { while (running && !Thread.currentThread().isInterrupted()) { - // --- GREEN Phase --- - light.changeState(TrafficLightState.GREEN); - System.out.println("[" + light.getId() + "] State: GREEN"); + // Request permission to turn green (blocks until granted) + process.requestGreenLight(light.getDirection()); - processGreenLightQueue(); - - if (!running || Thread.currentThread().isInterrupted()) break; - - // Wait for green duration - Thread.sleep((long) (light.getGreenTime() * 1000)); - - if (!running || Thread.currentThread().isInterrupted()) break; + try { + // --- GREEN Phase --- + light.changeState(TrafficLightState.GREEN); + System.out.println("[" + light.getId() + "] State: GREEN"); + + processGreenLightQueue(); + + if (!running || Thread.currentThread().isInterrupted()) break; + + // Wait for green duration + Thread.sleep((long) (light.getGreenTime() * 1000)); + + if (!running || Thread.currentThread().isInterrupted()) break; - // --- RED Phase --- - light.changeState(TrafficLightState.RED); - System.out.println("[" + light.getId() + "] State: RED"); + // --- RED Phase --- + light.changeState(TrafficLightState.RED); + System.out.println("[" + light.getId() + "] State: RED"); + + } finally { + // Always release the green light permission + process.releaseGreenLight(light.getDirection()); + } // Wait for red duration Thread.sleep((long) (light.getRedTime() * 1000)); } } catch (InterruptedException e) { System.out.println("[" + light.getId() + "] Traffic light thread interrupted."); - // Restore interrupt status Thread.currentThread().interrupt(); } finally { this.running = false; diff --git a/main/src/test/java/sd/TrafficLightCoordinationTest.java b/main/src/test/java/sd/TrafficLightCoordinationTest.java index 1fe51a3..347864d 100644 --- a/main/src/test/java/sd/TrafficLightCoordinationTest.java +++ b/main/src/test/java/sd/TrafficLightCoordinationTest.java @@ -1,18 +1,18 @@ package sd; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; - -import sd.model.TrafficLight; -import sd.model.TrafficLightState; - import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import sd.model.TrafficLight; +import sd.model.TrafficLightState; /** * Test class to verify traffic light coordination within an intersection. @@ -108,7 +108,7 @@ public class TrafficLightCoordinationTest { assertTrue(maxGreenSimultaneously.get() <= 1, "At most ONE light should be GREEN at any time. Found: " + maxGreenSimultaneously.get()); - System.out.println("\n✅ Traffic light coordination working correctly!"); + System.out.println("\nTraffic light coordination working correctly!"); } /** diff --git a/main/src/test/java/sd/dashboard/DashboardTest.java b/main/src/test/java/sd/dashboard/DashboardTest.java new file mode 100644 index 0000000..bcc72bb --- /dev/null +++ b/main/src/test/java/sd/dashboard/DashboardTest.java @@ -0,0 +1,164 @@ +package sd.dashboard; + +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import sd.config.SimulationConfig; +import sd.model.VehicleType; + +/** + * Unit tests for Dashboard Server components. + */ +class DashboardTest { + + private DashboardStatistics statistics; + + @BeforeEach + void setUp() { + statistics = new DashboardStatistics(); + } + + @AfterEach + void tearDown() { + statistics = null; + } + + @Test + void testInitialStatistics() { + assertEquals(0, statistics.getTotalVehiclesGenerated(), + "Initial vehicles generated should be 0"); + assertEquals(0, statistics.getTotalVehiclesCompleted(), + "Initial vehicles completed should be 0"); + assertEquals(0.0, statistics.getAverageSystemTime(), + "Initial average system time should be 0.0"); + assertEquals(0.0, statistics.getAverageWaitingTime(), + "Initial average waiting time should be 0.0"); + } + + @Test + void testVehicleCounters() { + statistics.incrementVehiclesGenerated(); + assertEquals(1, statistics.getTotalVehiclesGenerated()); + + statistics.updateVehiclesGenerated(10); + assertEquals(10, statistics.getTotalVehiclesGenerated()); + + statistics.incrementVehiclesCompleted(); + assertEquals(1, statistics.getTotalVehiclesCompleted()); + } + + @Test + void testAverageCalculations() { + // Add 3 completed vehicles with known times + statistics.updateVehiclesCompleted(3); + statistics.addSystemTime(3000); // 3000ms total + statistics.addWaitingTime(1500); // 1500ms total + + assertEquals(1000.0, statistics.getAverageSystemTime(), 0.01, + "Average system time should be 1000ms"); + assertEquals(500.0, statistics.getAverageWaitingTime(), 0.01, + "Average waiting time should be 500ms"); + } + + @Test + void testVehicleTypeStatistics() { + statistics.incrementVehicleType(VehicleType.LIGHT); + statistics.incrementVehicleType(VehicleType.LIGHT); + statistics.incrementVehicleType(VehicleType.HEAVY); + + assertEquals(2, statistics.getVehicleTypeCount(VehicleType.LIGHT)); + assertEquals(1, statistics.getVehicleTypeCount(VehicleType.HEAVY)); + assertEquals(0, statistics.getVehicleTypeCount(VehicleType.BIKE)); + } + + @Test + void testIntersectionStatistics() { + statistics.updateIntersectionStats("Cr1", 10, 8, 2); + + DashboardStatistics.IntersectionStats stats = + statistics.getIntersectionStats("Cr1"); + + assertNotNull(stats, "Intersection stats should not be null"); + assertEquals("Cr1", stats.getIntersectionId()); + assertEquals(10, stats.getTotalArrivals()); + assertEquals(8, stats.getTotalDepartures()); + assertEquals(2, stats.getCurrentQueueSize()); + } + + @Test + void testMultipleIntersections() { + statistics.updateIntersectionStats("Cr1", 10, 8, 2); + statistics.updateIntersectionStats("Cr2", 15, 12, 3); + statistics.updateIntersectionStats("Cr3", 5, 5, 0); + + assertEquals(3, statistics.getAllIntersectionStats().size(), + "Should have 3 intersections"); + } + + @Test + void testStatsUpdatePayload() { + StatsUpdatePayload payload = new StatsUpdatePayload() + .setTotalVehiclesGenerated(50) + .setTotalVehiclesCompleted(20) + .setIntersectionArrivals(30) + .setIntersectionDepartures(25) + .setIntersectionQueueSize(5); + + assertEquals(50, payload.getTotalVehiclesGenerated()); + assertEquals(20, payload.getTotalVehiclesCompleted()); + assertEquals(30, payload.getIntersectionArrivals()); + assertEquals(25, payload.getIntersectionDepartures()); + assertEquals(5, payload.getIntersectionQueueSize()); + } + + @Test + void testStatsMessage() { + StatsUpdatePayload payload = new StatsUpdatePayload() + .setIntersectionArrivals(10); + + StatsMessage message = new StatsMessage("Cr1", payload); + + assertEquals("Cr1", message.getSourceNode()); + assertEquals("DashboardServer", message.getDestinationNode()); + assertEquals(sd.model.MessageType.STATS_UPDATE, message.getType()); + assertNotNull(message.getPayload()); + } + + @Test + void testThreadSafety() throws InterruptedException { + // Test concurrent updates + Thread t1 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + statistics.incrementVehiclesGenerated(); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + statistics.incrementVehiclesGenerated(); + } + }); + + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + assertEquals(200, statistics.getTotalVehiclesGenerated(), + "Concurrent increments should total 200"); + } + + @Test + void testDashboardServerCreation() throws Exception { + SimulationConfig config = new SimulationConfig("simulation.properties"); + DashboardServer server = new DashboardServer(config); + + assertNotNull(server, "Server should be created successfully"); + assertNotNull(server.getStatistics(), "Statistics should be initialized"); + assertFalse(server.isRunning(), "Server should not be running initially"); + } +}