diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java index cbe7cef..11db78d 100644 --- a/main/src/main/java/sd/IntersectionProcess.java +++ b/main/src/main/java/sd/IntersectionProcess.java @@ -83,33 +83,23 @@ public class IntersectionProcess { private void createTrafficLights() { System.out.println("\n[" + intersectionId + "] Creating traffic lights..."); - // Define directions based on the actual network topology - String[] directions; + String[] directions = new String[0]; switch (intersectionId) { case "Cr1": - // Cr1: East (to Cr2), South (to Cr4), West (from Cr2) - directions = new String[]{"East", "South", "West"}; + directions = new String[]{"East", "South"}; break; case "Cr2": - // Cr2: West (to Cr1), East (to Cr3), South (to Cr5) - // Plus receiving from Cr1 and Cr3 directions = new String[]{"West", "East", "South"}; break; case "Cr3": - // Cr3: West (to Cr2), East (to S) - directions = new String[]{"West", "East"}; + directions = new String[]{"West", "South"}; break; case "Cr4": - // Cr4: East (to Cr5), plus pedestrian crossing directions = new String[]{"East"}; break; case "Cr5": - // Cr5: East (to S), receives from Cr2 and Cr4 directions = new String[]{"East"}; break; - default: - // Fallback to all directions - directions = new String[]{"North", "South", "East", "West"}; } for (String direction : directions) { @@ -134,41 +124,31 @@ public class IntersectionProcess { switch (intersectionId) { case "Cr1": - // Cr1 connections: → Cr2 (East), → Cr4 (South), ← Cr2 (West) - intersection.configureRoute("Cr2", "East"); // Go to Cr2 - intersection.configureRoute("Cr4", "South"); // Go to Cr4 - // Routes through other intersections to reach S - intersection.configureRoute("S", "East"); // S via Cr2 + intersection.configureRoute("Cr2", "East"); + intersection.configureRoute("Cr4", "South"); break; case "Cr2": - // Cr2 connections: ↔ Cr1 (West/East), ↔ Cr3 (East/West), → Cr5 (South) - intersection.configureRoute("Cr1", "West"); // Go to Cr1 - intersection.configureRoute("Cr3", "East"); // Go to Cr3 - intersection.configureRoute("Cr5", "South"); // Go to Cr5 - intersection.configureRoute("S", "South"); // S via Cr5 or direct + intersection.configureRoute("Cr1", "West"); + intersection.configureRoute("Cr3", "East"); + intersection.configureRoute("Cr5", "South"); break; case "Cr3": - // Cr3 connections: ← Cr2 (West), → S (South/East) - intersection.configureRoute("Cr2", "West"); // Go back to Cr2 - intersection.configureRoute("S", "East"); // Go to exit S + intersection.configureRoute("Cr2", "West"); + intersection.configureRoute("S", "South"); break; case "Cr4": - // Cr4 connections: → Cr5 (East) - intersection.configureRoute("Cr5", "East"); // Go to Cr5 - intersection.configureRoute("S", "East"); // S via Cr5 + intersection.configureRoute("Cr5", "East"); break; case "Cr5": - // Cr5 connections: → S (East/South) - intersection.configureRoute("S", "East"); // Go to exit S - // Cr5 might also receive from Cr2 and Cr4 but doesn't route back + intersection.configureRoute("S", "East"); break; default: - System.err.println(" Warning: Unknown intersection ID: " + intersectionId); + System.err.println(" Error: unknown intersection ID: " + intersectionId); } System.out.println(" Routing configured."); @@ -197,7 +177,7 @@ public class IntersectionProcess { while (running) { try { - // GREEN phase + // Green state light.changeState(TrafficLightState.GREEN); System.out.println("[" + light.getId() + "] State: GREEN"); @@ -207,7 +187,7 @@ public class IntersectionProcess { // Wait for green duration Thread.sleep((long) (light.getGreenTime() * 1000)); - // RED phase + // RED state light.changeState(TrafficLightState.RED); System.out.println("[" + light.getId() + "] State: RED"); @@ -300,8 +280,7 @@ public class IntersectionProcess { System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() + " to " + nextDestination); - // Update vehicle's path - advance to next destination in route - vehicle.advanceRoute(); + // Note: vehicle route is advanced when it arrives at the next intersection } catch (IOException | InterruptedException e) { System.err.println("[" + intersectionId + "] Failed to send vehicle " + @@ -343,10 +322,8 @@ public class IntersectionProcess { private String getHostForDestination(String destinationId) { if (destinationId.equals("S")) { return config.getExitHost(); - } else if (destinationId.startsWith("Cr")) { - return config.getIntersectionHost(destinationId); } else { - return config.getDashboardHost(); + return config.getIntersectionHost(destinationId); } } @@ -359,10 +336,8 @@ public class IntersectionProcess { private int getPortForDestination(String destinationId) { if (destinationId.equals("S")) { return config.getExitPort(); - } else if (destinationId.startsWith("Cr")) { - return config.getIntersectionPort(destinationId); } else { - return config.getDashboardPort(); + return config.getIntersectionPort(destinationId); } } @@ -485,45 +460,6 @@ public class IntersectionProcess { System.out.println("=".repeat(60)); } - /** - * Main method to start an intersection process. - * - * @param args Command-line arguments: - * args[0] - Intersection ID (required, e.g., "Cr1") - * args[1] - Config file path (optional, defaults to "simulation.properties") - */ - public static void main(String[] args) { - if (args.length < 1) { - System.err.println("Usage: java IntersectionProcess [configFile]"); - System.err.println("Example: java IntersectionProcess Cr1"); - System.exit(1); - } - - String intersectionId = args[0]; - String configFile = args.length > 1 ? args[1] : "simulation.properties"; - - IntersectionProcess process = null; - - try { - process = new IntersectionProcess(intersectionId, configFile); - process.initialize(); - - // Add shutdown hook for graceful termination - final IntersectionProcess finalProcess = process; - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - finalProcess.shutdown(); - })); - - // Start the process - process.start(); - - } catch (IOException e) { - System.err.println("Error starting intersection process: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); - } - } - // --- Inner class for Vehicle Transfer Messages --- /** diff --git a/main/src/main/java/sd/model/Intersection.java b/main/src/main/java/sd/model/Intersection.java index 718c98c..4475fc3 100644 --- a/main/src/main/java/sd/model/Intersection.java +++ b/main/src/main/java/sd/model/Intersection.java @@ -104,16 +104,28 @@ public class Intersection { * Accepts an incoming vehicle and places it in the correct queue. * * This method: * 1. Increments the {@link #totalVehiclesReceived} counter. - * 2. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}). - * 3. Uses the {@link #routing} map to find the correct *direction* for that destination. - * 4. Adds the vehicle to the queue of the {@link TrafficLight} for that direction. + * 2. Advances the vehicle's route (since it just arrived here) + * 3. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}). + * 4. Uses the {@link #routing} map to find the correct *direction* for that destination. + * 5. Adds the vehicle to the queue of the {@link TrafficLight} for that direction. * * @param vehicle The {@link Vehicle} arriving at the intersection. */ public void receiveVehicle(Vehicle vehicle) { totalVehiclesReceived++; + // Advance route since vehicle just arrived at this intersection + vehicle.advanceRoute(); + String nextDestination = vehicle.getCurrentDestination(); + + // Check if vehicle reached final destination + if (nextDestination == null) { + System.out.printf("[%s] Vehicle %s reached final destination%n", + this.id, vehicle.getId()); + return; + } + String direction = routing.get(nextDestination); if (direction != null && trafficLights.containsKey(direction)) { diff --git a/main/src/test/java/IntersectionProcessTest.java b/main/src/test/java/IntersectionProcessTest.java new file mode 100644 index 0000000..90de4f1 --- /dev/null +++ b/main/src/test/java/IntersectionProcessTest.java @@ -0,0 +1,473 @@ +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +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 org.junit.jupiter.api.io.TempDir; + +import sd.IntersectionProcess; +import sd.model.MessageType; +import sd.model.Vehicle; +import sd.model.VehicleType; + +/** + * Tests for IntersectionProcess - covers initialization, traffic lights, + * vehicle transfer and network stuff + */ +public class IntersectionProcessTest { + + @TempDir + Path tempDir; + + private Path configFile; + private IntersectionProcess intersectionProcess; + + // setup test config before each test + @BeforeEach + public void setUp() throws IOException { + // create temp config file + configFile = tempDir.resolve("test-simulation.properties"); + + String configContent = """ + # Test Simulation Configuration + + # Intersection Network Configuration + intersection.Cr1.host=localhost + intersection.Cr1.port=18001 + intersection.Cr2.host=localhost + intersection.Cr2.port=18002 + intersection.Cr3.host=localhost + intersection.Cr3.port=18003 + intersection.Cr4.host=localhost + intersection.Cr4.port=18004 + intersection.Cr5.host=localhost + intersection.Cr5.port=18005 + + # Exit Configuration + exit.host=localhost + exit.port=18099 + + # Dashboard Configuration + dashboard.host=localhost + dashboard.port=18100 + + # Traffic Light Timing (seconds) + trafficLight.Cr1.East.greenTime=5.0 + trafficLight.Cr1.East.redTime=5.0 + trafficLight.Cr1.South.greenTime=5.0 + trafficLight.Cr1.South.redTime=5.0 + trafficLight.Cr1.West.greenTime=5.0 + trafficLight.Cr1.West.redTime=5.0 + + trafficLight.Cr2.West.greenTime=4.0 + trafficLight.Cr2.West.redTime=6.0 + trafficLight.Cr2.East.greenTime=4.0 + trafficLight.Cr2.East.redTime=6.0 + trafficLight.Cr2.South.greenTime=4.0 + trafficLight.Cr2.South.redTime=6.0 + + trafficLight.Cr3.West.greenTime=3.0 + trafficLight.Cr3.West.redTime=7.0 + trafficLight.Cr3.East.greenTime=3.0 + trafficLight.Cr3.East.redTime=7.0 + + trafficLight.Cr4.East.greenTime=6.0 + trafficLight.Cr4.East.redTime=4.0 + + trafficLight.Cr5.East.greenTime=5.0 + trafficLight.Cr5.East.redTime=5.0 + + # Vehicle Crossing Times (seconds) + vehicle.bike.crossingTime=2.0 + vehicle.light.crossingTime=3.0 + vehicle.heavy.crossingTime=5.0 + """; + + Files.writeString(configFile, configContent); + } + + // cleanup after tests + @AfterEach + public void tearDown() { + if (intersectionProcess != null) { + intersectionProcess.shutdown(); + } + } + + // ==================== Initialization Tests ==================== + + @Test + public void testConstructor_Success() throws IOException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + assertNotNull(intersectionProcess); + } + + @Test + public void testConstructor_InvalidConfig() { + Exception exception = assertThrows(IOException.class, () -> { + new IntersectionProcess("Cr1", "non-existent-config.properties"); + }); + assertNotNull(exception); + } + + @Test + public void testInitialize_Cr1() throws IOException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + assertDoesNotThrow(() -> intersectionProcess.initialize()); + } + + @Test + public void testInitialize_Cr2() throws IOException { + intersectionProcess = new IntersectionProcess("Cr2", configFile.toString()); + assertDoesNotThrow(() -> intersectionProcess.initialize()); + } + + @Test + public void testInitialize_Cr3() throws IOException { + intersectionProcess = new IntersectionProcess("Cr3", configFile.toString()); + assertDoesNotThrow(() -> intersectionProcess.initialize()); + } + + @Test + public void testInitialize_Cr4() throws IOException { + intersectionProcess = new IntersectionProcess("Cr4", configFile.toString()); + assertDoesNotThrow(() -> intersectionProcess.initialize()); + } + + @Test + public void testInitialize_Cr5() throws IOException { + intersectionProcess = new IntersectionProcess("Cr5", configFile.toString()); + assertDoesNotThrow(() -> intersectionProcess.initialize()); + } + + // traffic light creation tests + + @Test + public void testTrafficLightCreation_Cr1_HasCorrectDirections() throws IOException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + intersectionProcess.initialize(); + + // cant access private fields but initialization succeds + assertNotNull(intersectionProcess); + } + + @Test + public void testTrafficLightCreation_Cr3_HasCorrectDirections() throws IOException { + intersectionProcess = new IntersectionProcess("Cr3", configFile.toString()); + intersectionProcess.initialize(); + + // Cr3 has west and south only + assertNotNull(intersectionProcess); + } + + @Test + public void testTrafficLightCreation_Cr4_HasSingleDirection() throws IOException { + intersectionProcess = new IntersectionProcess("Cr4", configFile.toString()); + intersectionProcess.initialize(); + + // Cr4 only has east direction + assertNotNull(intersectionProcess); + } + + // server startup tests + + @Test + @Timeout(5) + public void testServerStart_BindsToCorrectPort() throws IOException, InterruptedException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + intersectionProcess.initialize(); + + // start server in seperate thread + Thread serverThread = new Thread(() -> { + try { + intersectionProcess.start(); + } catch (IOException e) { + // expected on shutdown + } + }); + serverThread.start(); + + Thread.sleep(500); // wait for server to start + + // try connecting to check if its running + try (Socket clientSocket = new Socket("localhost", 18001)) { + assertTrue(clientSocket.isConnected()); + } + + intersectionProcess.shutdown(); + serverThread.join(2000); + } + + @Test + @Timeout(5) + public void testServerStart_MultipleIntersections() throws IOException, InterruptedException { + // test 2 intersections on diferent ports + IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString()); + IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString()); + + cr1.initialize(); + cr2.initialize(); + + Thread thread1 = new Thread(() -> { + try { cr1.start(); } catch (IOException e) { } + }); + + Thread thread2 = new Thread(() -> { + try { cr2.start(); } catch (IOException e) { } + }); + + thread1.start(); + thread2.start(); + + Thread.sleep(500); + + // check both are running + try (Socket socket1 = new Socket("localhost", 18001); + Socket socket2 = new Socket("localhost", 18002)) { + assertTrue(socket1.isConnected()); + assertTrue(socket2.isConnected()); + } + + cr1.shutdown(); + cr2.shutdown(); + thread1.join(2000); + thread2.join(2000); + } + + // vehicle transfer tests + + @Test + @Timeout(10) + public void testVehicleTransfer_ReceiveVehicle() throws IOException, InterruptedException { + // setup reciever intersection + intersectionProcess = new IntersectionProcess("Cr2", configFile.toString()); + intersectionProcess.initialize(); + + Thread serverThread = new Thread(() -> { + try { + intersectionProcess.start(); + } catch (IOException e) { } + }); + serverThread.start(); + + Thread.sleep(500); + + // create test vehicle + java.util.List route = Arrays.asList("Cr2", "Cr3", "S"); + Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route); + + // send vehicle from Cr1 to Cr2 + try (Socket socket = new Socket("localhost", 18002)) { + ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); + + TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle); + out.writeObject(message); + out.flush(); + + Thread.sleep(1000); // wait for procesing + } + + intersectionProcess.shutdown(); + serverThread.join(2000); + } + + // routing config tests + + @Test + public void testRoutingConfiguration_Cr1() throws IOException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + intersectionProcess.initialize(); + + // indirect test - if init works routing should be ok + assertNotNull(intersectionProcess); + } + + @Test + public void testRoutingConfiguration_Cr5() throws IOException { + intersectionProcess = new IntersectionProcess("Cr5", configFile.toString()); + intersectionProcess.initialize(); + + // Cr5 routes to exit + assertNotNull(intersectionProcess); + } + + // shutdown tests + + @Test + @Timeout(5) + public void testShutdown_GracefulTermination() throws IOException, InterruptedException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + intersectionProcess.initialize(); + + Thread serverThread = new Thread(() -> { + try { + intersectionProcess.start(); + } catch (IOException e) { } + }); + serverThread.start(); + + Thread.sleep(500); + + // shutdown should be fast + assertDoesNotThrow(() -> intersectionProcess.shutdown()); + + serverThread.join(2000); + } + + @Test + @Timeout(5) + public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + intersectionProcess.initialize(); + + Thread serverThread = new Thread(() -> { + try { + intersectionProcess.start(); + } catch (IOException e) { } + }); + serverThread.start(); + + Thread.sleep(500); + + // verify server running + try (Socket socket = new Socket("localhost", 18001)) { + assertTrue(socket.isConnected()); + } + + intersectionProcess.shutdown(); + serverThread.join(2000); + + // after shutdown conection should fail + Thread.sleep(500); + Exception exception = assertThrows(IOException.class, () -> { + Socket socket = new Socket("localhost", 18001); + socket.close(); + }); + assertNotNull(exception); + } + + @Test + @Timeout(5) + public void testShutdown_StopsTrafficLightThreads() throws IOException, InterruptedException { + intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); + intersectionProcess.initialize(); + + Thread serverThread = new Thread(() -> { + try { + intersectionProcess.start(); + } catch (IOException e) { } + }); + serverThread.start(); + + Thread.sleep(500); + + int threadCountBefore = Thread.activeCount(); + + intersectionProcess.shutdown(); + serverThread.join(2000); + + Thread.sleep(500); // wait for threads to die + + // thread count should decrese (traffic light threads stop) + int threadCountAfter = Thread.activeCount(); + assertTrue(threadCountAfter <= threadCountBefore); + } + + // integration tests + + @Test + @Timeout(15) + public void testIntegration_TwoIntersectionsVehicleTransfer() throws IOException, InterruptedException { + // setup 2 intersections + IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString()); + IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString()); + + cr1.initialize(); + cr2.initialize(); + + // start both + Thread thread1 = new Thread(() -> { + try { cr1.start(); } catch (IOException e) { } + }); + + Thread thread2 = new Thread(() -> { + try { cr2.start(); } catch (IOException e) { } + }); + + thread1.start(); + thread2.start(); + + Thread.sleep(1000); // wait for servers + + // send vehicle to Cr1 that goes to Cr2 + java.util.List route = Arrays.asList("Cr1", "Cr2", "S"); + Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route); + + try (Socket socket = new Socket("localhost", 18001)) { + ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); + + TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle); + out.writeObject(message); + out.flush(); + + Thread.sleep(2000); // time for processing + } + + cr1.shutdown(); + cr2.shutdown(); + thread1.join(2000); + thread2.join(2000); + } + + @Test + public void testMain_MissingArguments() { + // main needs intersection ID as argument + // cant test System.exit easily in modern java + assertTrue(true, "Main method expects intersection ID as first argument"); + } + + // helper class for testing vehicle messages + private static class TestVehicleMessage implements sd.protocol.MessageProtocol { + private static final long serialVersionUID = 1L; + + private final String sourceNode; + private final String destinationNode; + private final Vehicle payload; + + public TestVehicleMessage(String sourceNode, String destinationNode, Vehicle vehicle) { + this.sourceNode = sourceNode; + this.destinationNode = destinationNode; + this.payload = vehicle; + } + + @Override + public MessageType getType() { + return MessageType.VEHICLE_TRANSFER; + } + + @Override + public Object getPayload() { + return payload; + } + + @Override + public String getSourceNode() { + return sourceNode; + } + + @Override + public String getDestinationNode() { + return destinationNode; + } + } +}