import java.io.IOException; import java.net.InetSocketAddress; 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; import sd.protocol.SocketConnection; /** * 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); } @AfterEach public void tearDown() { if (intersectionProcess != null) { try { // Only shutdown if still running intersectionProcess.shutdown(); } catch (Exception e) { System.err.println("Error in tearDown: " + e.getMessage()); } finally { intersectionProcess = null; } } } // ==================== 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 separate thread Thread serverThread = new Thread(() -> { try { intersectionProcess.start(); } catch (IOException e) { // expected on shutdown } }); serverThread.start(); // Wait for server to actually start with retries boolean serverReady = false; for (int i = 0; i < 20; i++) { Thread.sleep(100); try (Socket testSocket = new Socket()) { testSocket.connect(new java.net.InetSocketAddress("localhost", 18001), 500); serverReady = true; break; } catch (IOException e) { // Server not ready yet, continue waiting } } assertTrue(serverReady, "Server should start and bind to port 18001"); // Shutdown immediately after confirming server is running 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); try { // create test vehicle - FIXED: use 4-parameter constructor 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 - FIXED: use SocketConnection try (Socket socket = new Socket("localhost", 18002); SocketConnection conn = new SocketConnection(socket)) { TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle); conn.sendMessage(message); Thread.sleep(1000); // wait for processing } } finally { 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(); // Start server in separate thread Thread serverThread = new Thread(() -> { try { intersectionProcess.start(); } catch (IOException e) { // Expected on shutdown } }); serverThread.start(); // Wait for server to start Thread.sleep(500); // Shutdown intersectionProcess.shutdown(); serverThread.join(2000); // Give shutdown time to complete Thread.sleep(200); // Verify we cannot connect (server socket is closed) boolean connectionFailed = false; try (Socket testSocket = new Socket()) { testSocket.connect(new InetSocketAddress("localhost", 18001), 500); } catch (IOException e) { connectionFailed = true; // Expected - server should be closed } assertTrue(connectionFailed, "Server socket should be closed after shutdown"); } @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 { IntersectionProcess cr1 = null; IntersectionProcess cr2 = null; Thread thread1 = null; Thread thread2 = null; try { // setup 2 intersections cr1 = new IntersectionProcess("Cr1", configFile.toString()); cr2 = new IntersectionProcess("Cr2", configFile.toString()); cr1.initialize(); cr2.initialize(); // start both final IntersectionProcess cr1Final = cr1; thread1 = new Thread(() -> { try { cr1Final.start(); } catch (IOException e) { } }); final IntersectionProcess cr2Final = cr2; thread2 = new Thread(() -> { try { cr2Final.start(); } catch (IOException e) { } }); thread1.start(); thread2.start(); Thread.sleep(1000); // wait for servers // send vehicle to Cr1 that goes to Cr2 - FIXED: use 4-parameter constructor java.util.List route = Arrays.asList("Cr1", "Cr2", "S"); Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route); // FIXED: use SocketConnection try (Socket socket = new Socket("localhost", 18001); SocketConnection conn = new SocketConnection(socket)) { TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle); conn.sendMessage(message); Thread.sleep(2000); // time for processing } } finally { if (cr1 != null) { cr1.shutdown(); } if (cr2 != null) { cr2.shutdown(); } if (thread1 != null) { thread1.join(2000); } if (thread2 != null) { 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; } } }