Merge pull request #33 from davidalves04/17-create-dashboardserver-process

Dashboard Server Implementation
This commit is contained in:
David Alves
2025-11-19 19:16:50 +00:00
committed by GitHub
9 changed files with 860 additions and 25 deletions

View File

@@ -171,6 +171,29 @@ public class IntersectionProcess {
System.out.println(" Routing configured."); 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. * Starts all traffic light threads.
*/ */

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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<String, IntersectionStats> intersectionStats;
private final Map<VehicleType, AtomicInteger> vehicleTypeCount;
private final Map<VehicleType, AtomicLong> 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<String, IntersectionStats> 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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<VehicleType, Integer> vehicleTypeCounts;
private Map<VehicleType, Long> 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<VehicleType, Integer> getVehicleTypeCounts() {
return vehicleTypeCounts;
}
public Map<VehicleType, Long> 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<VehicleType, Integer> vehicleTypeCounts) {
this.vehicleTypeCounts = vehicleTypeCounts;
return this;
}
public StatsUpdatePayload setVehicleTypeWaitTimes(Map<VehicleType, Long> 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);
}
}

View File

@@ -29,7 +29,6 @@ public class TrafficLightThread implements Runnable {
@Override @Override
public void run() { public void run() {
// Capture the current thread reference
this.currentThread = Thread.currentThread(); this.currentThread = Thread.currentThread();
this.running = true; this.running = true;
System.out.println("[" + light.getId() + "] Traffic light thread started."); System.out.println("[" + light.getId() + "] Traffic light thread started.");
@@ -37,6 +36,10 @@ public class TrafficLightThread implements Runnable {
try { try {
while (running && !Thread.currentThread().isInterrupted()) { while (running && !Thread.currentThread().isInterrupted()) {
// Request permission to turn green (blocks until granted)
process.requestGreenLight(light.getDirection());
try {
// --- GREEN Phase --- // --- GREEN Phase ---
light.changeState(TrafficLightState.GREEN); light.changeState(TrafficLightState.GREEN);
System.out.println("[" + light.getId() + "] State: GREEN"); System.out.println("[" + light.getId() + "] State: GREEN");
@@ -54,12 +57,16 @@ public class TrafficLightThread implements Runnable {
light.changeState(TrafficLightState.RED); light.changeState(TrafficLightState.RED);
System.out.println("[" + light.getId() + "] State: RED"); System.out.println("[" + light.getId() + "] State: RED");
} finally {
// Always release the green light permission
process.releaseGreenLight(light.getDirection());
}
// Wait for red duration // Wait for red duration
Thread.sleep((long) (light.getRedTime() * 1000)); Thread.sleep((long) (light.getRedTime() * 1000));
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
System.out.println("[" + light.getId() + "] Traffic light thread interrupted."); System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
// Restore interrupt status
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} finally { } finally {
this.running = false; this.running = false;

View File

@@ -1,18 +1,18 @@
package sd; 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.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; 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. * Test class to verify traffic light coordination within an intersection.
@@ -108,7 +108,7 @@ public class TrafficLightCoordinationTest {
assertTrue(maxGreenSimultaneously.get() <= 1, assertTrue(maxGreenSimultaneously.get() <= 1,
"At most ONE light should be GREEN at any time. Found: " + maxGreenSimultaneously.get()); "At most ONE light should be GREEN at any time. Found: " + maxGreenSimultaneously.get());
System.out.println("\nTraffic light coordination working correctly!"); System.out.println("\nTraffic light coordination working correctly!");
} }
/** /**

View File

@@ -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");
}
}