package sd; import java.io.IOException; import java.net.Socket; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; 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.config.SimulationConfig; /** * Testes unitários para a classe ExitNodeProcess. * * Esta classe de testes verifica: * - Construção e inicialização do processo * - Criação e aceitação de conexões do servidor socket * - Gestão do ciclo de vida (start/shutdown) * - Processamento concorrente de múltiplas conexões * - Impressão de estatísticas finais * * Os testes utilizam configurações temporárias e portas dedicadas (19001) * para evitar conflitos com outros testes ou processos em execução. */ public class ExitNodeProcessTest { @TempDir Path tempDir; private Path configFile; private ExitNodeProcess exitNodeProcess; private Thread exitNodeThread; /** * Configura o ambiente de teste antes de cada teste. * Cria um ficheiro de configuração temporário com as definições necessárias. */ @BeforeEach public void setUp() throws IOException { configFile = tempDir.resolve("test-simulation.properties"); String configContent = """ # Test Exit Node Configuration # Exit Configuration exit.host=localhost exit.port=19001 # Dashboard Configuration (will not be running in tests) dashboard.host=localhost dashboard.port=19000 # Vehicle Crossing Times vehicle.bike.crossingTime=2.0 vehicle.light.crossingTime=3.0 vehicle.heavy.crossingTime=5.0 # Simulation Duration simulation.duration=60.0 """; Files.writeString(configFile, configContent); } /** * Limpa os recursos após cada teste. * Garante que o processo e threads são terminados corretamente. */ @AfterEach public void tearDown() { if (exitNodeProcess != null) { exitNodeProcess.shutdown(); } if (exitNodeThread != null && exitNodeThread.isAlive()) { exitNodeThread.interrupt(); try { exitNodeThread.join(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } /** * Testa a construção bem-sucedida do ExitNodeProcess com configuração válida. */ @Test public void testConstructor_Success() throws IOException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); assertNotNull(exitNodeProcess); } /** * Testa que uma exceção é lançada quando a configuração é inválida. */ @Test public void testConstructor_InvalidConfig() { Exception exception = assertThrows(IOException.class, () -> { new SimulationConfig("non-existent-config.properties"); }); assertNotNull(exception); } /** * Testa a inicialização sem dashboard disponível. * Verifica que o processo continua a funcionar mesmo sem conexão ao dashboard. */ @Test public void testInitialize_WithoutDashboard() throws IOException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); assertDoesNotThrow(() -> exitNodeProcess.initialize()); } /** * Testa que o servidor socket é criado corretamente na porta configurada. * Verifica que é possível estabelecer uma conexão ao socket do servidor. */ @Test @Timeout(value = 3, unit = TimeUnit.SECONDS) public void testStart_ServerSocketCreated() throws IOException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); exitNodeProcess.initialize(); CountDownLatch latch = new CountDownLatch(1); exitNodeThread = new Thread(() -> { try { latch.countDown(); exitNodeProcess.start(); } catch (IOException e) { // expected when shutdown } }); exitNodeThread.start(); try { assertTrue(latch.await(2, TimeUnit.SECONDS), "Exit node should start within timeout"); Thread.sleep(100); assertDoesNotThrow(() -> { try (Socket testSocket = new Socket("localhost", 19001)) { assertTrue(testSocket.isConnected()); } }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } /** * Testa que o servidor aceita conexões de clientes. */ @Test @Timeout(value = 3, unit = TimeUnit.SECONDS) public void testStart_AcceptsConnection() throws IOException, InterruptedException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); exitNodeProcess.initialize(); CountDownLatch latch = new CountDownLatch(1); exitNodeThread = new Thread(() -> { try { latch.countDown(); exitNodeProcess.start(); } catch (IOException e) { // expected } }); exitNodeThread.start(); assertTrue(latch.await(2, TimeUnit.SECONDS)); Thread.sleep(200); assertDoesNotThrow(() -> { try (Socket socket = new Socket("localhost", 19001)) { assertTrue(socket.isConnected()); } }); } /** * Testa múltiplas inicializações e encerramentos do processo. * Verifica que o processo pode ser iniciado e parado múltiplas vezes, * permitindo reutilização da porta. */ @Test @Timeout(value = 3, unit = TimeUnit.SECONDS) public void testMultipleStartStop() throws IOException, InterruptedException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); exitNodeProcess.initialize(); CountDownLatch latch = new CountDownLatch(1); exitNodeThread = new Thread(() -> { try { latch.countDown(); exitNodeProcess.start(); } catch (IOException e) { // expected } }); exitNodeThread.start(); assertTrue(latch.await(2, TimeUnit.SECONDS)); Thread.sleep(100); exitNodeProcess.shutdown(); Thread.sleep(100); assertDoesNotThrow(() -> { SimulationConfig config2 = new SimulationConfig(configFile.toString()); ExitNodeProcess exitNode2 = new ExitNodeProcess(config2); exitNode2.initialize(); exitNode2.shutdown(); }); } /** * Testa que o shutdown fecha corretamente o servidor socket. * Após o shutdown, novas conexões ao socket devem falhar. */ @Test @Timeout(value = 3, unit = TimeUnit.SECONDS) public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); exitNodeProcess.initialize(); CountDownLatch startLatch = new CountDownLatch(1); exitNodeThread = new Thread(() -> { try { startLatch.countDown(); exitNodeProcess.start(); } catch (IOException e) { // expected } }); exitNodeThread.start(); assertTrue(startLatch.await(2, TimeUnit.SECONDS)); Thread.sleep(200); exitNodeProcess.shutdown(); Thread.sleep(200); assertThrows(IOException.class, () -> { Socket socket = new Socket("localhost", 19001); socket.close(); }); } /** * Testa que as estatísticas finais são impressas corretamente durante o shutdown. * Verifica que o método não lança exceções mesmo sem dados processados. */ @Test public void testPrintFinalStatistics() throws IOException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); exitNodeProcess.initialize(); assertDoesNotThrow(() -> exitNodeProcess.shutdown()); } /** * Testa o processamento de múltiplas conexões concorrentes. * Verifica que o servidor consegue lidar com vários clientes simultaneamente * usando o pool de threads. */ @Test @Timeout(value = 3, unit = TimeUnit.SECONDS) public void testMultipleConcurrentConnections() throws IOException, InterruptedException { SimulationConfig config = new SimulationConfig(configFile.toString()); exitNodeProcess = new ExitNodeProcess(config); exitNodeProcess.initialize(); CountDownLatch latch = new CountDownLatch(1); exitNodeThread = new Thread(() -> { try { latch.countDown(); exitNodeProcess.start(); } catch (IOException e) { // expected } }); exitNodeThread.start(); assertTrue(latch.await(2, TimeUnit.SECONDS)); Thread.sleep(200); Thread[] clients = new Thread[3]; for (int i = 0; i < 3; i++) { clients[i] = new Thread(() -> { try (Socket socket = new Socket("localhost", 19001)) { assertTrue(socket.isConnected()); Thread.sleep(100); } catch (IOException | InterruptedException e) { // ignore } }); clients[i].start(); } for (Thread client : clients) { client.join(1000); } } }