From 211ea25ca594089bea7afc54bdf7cbad8e6258da Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Wed, 22 Oct 2025 23:36:41 +0100 Subject: [PATCH 01/15] Step 2 - Finishing touches --- .../main/java/sd/config/SimulationConfig.java | 2 +- .../main/java/sd/engine/SimulationEngine.java | 45 ++++++------------- main/src/test/java/SimulationTest.java | 2 +- 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/main/src/main/java/sd/config/SimulationConfig.java b/main/src/main/java/sd/config/SimulationConfig.java index 4a3fe89..d11ed42 100644 --- a/main/src/main/java/sd/config/SimulationConfig.java +++ b/main/src/main/java/sd/config/SimulationConfig.java @@ -31,7 +31,7 @@ public class SimulationConfig { * (por exemplo quando executado a partir do classpath/jar), * faz fallback para carregar a partir do classpath usando o ClassLoader. */ - IOException lastException = null; + IOException lastException = null; //FIXME: melhorar esta parte para reportar erros de forma mais clara try { try (InputStream input = new FileInputStream(filePath)) { diff --git a/main/src/main/java/sd/engine/SimulationEngine.java b/main/src/main/java/sd/engine/SimulationEngine.java index 94793f7..484ae80 100644 --- a/main/src/main/java/sd/engine/SimulationEngine.java +++ b/main/src/main/java/sd/engine/SimulationEngine.java @@ -264,32 +264,19 @@ public class SimulationEngine { */ private void processEvent(Event event) { switch (event.getType()) { - case VEHICLE_GENERATION: - handleVehicleGeneration(); - break; + case VEHICLE_GENERATION -> handleVehicleGeneration(); - case VEHICLE_ARRIVAL: - handleVehicleArrival(event); - break; + case VEHICLE_ARRIVAL -> handleVehicleArrival(event); - case TRAFFIC_LIGHT_CHANGE: - handleTrafficLightChange(event); - break; + case TRAFFIC_LIGHT_CHANGE -> handleTrafficLightChange(event); - case CROSSING_START: - handleCrossingStart(event); - break; + case CROSSING_START -> handleCrossingStart(event); - case CROSSING_END: - handleCrossingEnd(event); - break; + case CROSSING_END -> handleCrossingEnd(event); - case STATISTICS_UPDATE: - handleStatisticsUpdate(); - break; + case STATISTICS_UPDATE -> handleStatisticsUpdate(); - default: - System.err.println("Unknown event type: " + event.getType()); + default -> System.err.println("Unknown event type: " + event.getType()); } } @@ -386,7 +373,7 @@ public class SimulationEngine { * @param vehicle The vehicle to process. * @param intersection The intersection where the vehicle is. */ - private void tryProcessVehicle(Vehicle vehicle, Intersection intersection) { + private void tryProcessVehicle(Vehicle vehicle, Intersection intersection) { //FIXME // Find the direction (and light) this vehicle is queued at // This logic is a bit flawed: it just finds the *first* non-empty queue // A better approach would be to get the light from the vehicle's route @@ -591,16 +578,12 @@ public class SimulationEngine { * @return The crossing time in seconds. */ private double getCrossingTime(VehicleType type) { - switch (type) { - case BIKE: - return config.getBikeVehicleCrossingTime(); - case LIGHT: - return config.getLightVehicleCrossingTime(); - case HEAVY: - return config.getHeavyVehicleCrossingTime(); - default: - return 2.0; // Default fallback - } + return switch (type) { + case BIKE -> config.getBikeVehicleCrossingTime(); + case LIGHT -> config.getLightVehicleCrossingTime(); + case HEAVY -> config.getHeavyVehicleCrossingTime(); + default -> 2.0; + }; // Default fallback } /** diff --git a/main/src/test/java/SimulationTest.java b/main/src/test/java/SimulationTest.java index 7172a1a..b3a49df 100644 --- a/main/src/test/java/SimulationTest.java +++ b/main/src/test/java/SimulationTest.java @@ -43,7 +43,7 @@ class SimulationTest { assertEquals("TEST1", vehicle.getId()); assertNotNull(vehicle.getType()); assertNotNull(vehicle.getRoute()); - assertTrue(vehicle.getRoute().size() > 0); + assertTrue(!vehicle.getRoute().isEmpty()); } @Test From 1216089e80782aa4ba0266c211f8c00ea064e5a0 Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Wed, 22 Oct 2025 23:37:27 +0100 Subject: [PATCH 02/15] Step 2 - Finishing touches --- STEP2_SUMMARY.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++ TODO.md | 23 ++++++++ 2 files changed, 157 insertions(+) create mode 100644 STEP2_SUMMARY.md diff --git a/STEP2_SUMMARY.md b/STEP2_SUMMARY.md new file mode 100644 index 0000000..b234c44 --- /dev/null +++ b/STEP2_SUMMARY.md @@ -0,0 +1,134 @@ +# 🏁 Single-Process Prototype — Implementation Summary + +**Status:** ✅ Complete +**Date:** October 22, 2025 +**Branch:** `8-single-process-prototype` + +--- + +## Overview + +The single-process prototype implements a **discrete event simulation (DES)** of a 3×3 urban grid with five intersections, realistic vehicle behavior, and fully synchronized traffic lights. Everything runs under one process, laying the groundwork for the distributed architecture in Phase 3. + +--- + +## Core Architecture + +### **SimulationEngine** + +Drives the DES loop with a priority queue of timestamped events — vehicles, lights, crossings, and periodic stats updates. Handles five intersections (Cr1–Cr5) and six event types. + +**Main loop:** + +``` +while (events && time < duration): + event = nextEvent() + time = event.timestamp + handle(event) +``` + +### **VehicleGenerator** + +Spawns vehicles via: + +* **Poisson arrivals** (λ = 0.5 veh/s) or fixed intervals +* **Probabilistic routes** from E1–E3 +* **Type distribution**: 20% BIKE, 60% LIGHT, 20% HEAVY + +### **StatisticsCollector** + +Tracks system-wide and per-type metrics: throughput, avg. wait, queue sizes, light cycles — updated every 10 s and at simulation end. + +--- + +## Model Highlights + +* **Vehicle** – type, route, timings, lifecycle. +* **Intersection** – routing tables, traffic lights, queues. +* **TrafficLight** – red/green cycles with FIFO queues. +* **Event** – timestamped, comparable; 6 types for all DES actions. + +--- + +## Configuration (`simulation.properties`) + +```properties +simulation.duration=60.0 +simulation.arrival.model=POISSON +simulation.arrival.rate=0.5 + +vehicle.bike.crossingTime=1.5 +vehicle.light.crossingTime=2.0 +vehicle.heavy.crossingTime=4.0 + +statistics.update.interval=10.0 +``` + +**Speed logic:** +`t_bike = 0.5×t_car`, `t_heavy = 2×t_car`. + +--- + +## Topology + +``` +E1→Cr1→Cr4→Cr5→S +E2→Cr2→Cr5→S +E3→Cr3→S +Bi-dir: Cr1↔Cr2, Cr2↔Cr3 +``` + +--- + +## Results + +**Unit Tests:** 7/7 ✅ +**60-Second Simulation:** + +* Generated: 22 vehicles +* Completed: 5 (22.7%) +* Avg system time: 15.47 s +* Throughput: 0.08 veh/s +* All lights & intersections operational + +**Performance:** +~0.03 s real-time run (≈2000× speed-up), < 50 MB RAM. + +--- + +## Code Structure + +``` +sd/ +├── engine/SimulationEngine.java +├── model/{Vehicle,Intersection,TrafficLight,Event}.java +├── util/{VehicleGenerator,StatisticsCollector}.java +└── config/SimulationConfig.java +``` + +--- + +## Key Flow + +1. Initialize intersections, lights, first events. +2. Process events chronologically. +3. Vehicles follow routes → queue → cross → exit. +4. Lights toggle, queues drain, stats update. +5. Print summary and performance metrics. + +--- + +## Next Steps — Phase 3 + +* Split intersections into independent **processes**. +* Add **socket-based communication**. +* Run **traffic lights as threads**. +* Enable **distributed synchronization** and fault handling. + +--- + +## TL;DR + +Solid single-process DES ✅ +Everything’s working — traffic lights, routing, vehicles, stats. +Ready to go distributed next. \ No newline at end of file diff --git a/TODO.md b/TODO.md index 001c208..d323ab6 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,26 @@ +## ✅ SINGLE-PROCESS PROTOTYPE - COMPLETED + +### Phase 2 Status: DONE ✅ + +All components for the single-process prototype have been successfully implemented and tested: + +- ✅ **SimulationEngine** - Priority queue-based discrete event simulation +- ✅ **VehicleGenerator** - Poisson and Fixed arrival models +- ✅ **StatisticsCollector** - Comprehensive metrics tracking +- ✅ **Entry point** - Main simulation runner +- ✅ **60s test simulation** - Successfully validated event processing and routing + +### Test Results: +- All 7 unit tests passing +- 60-second simulation completed successfully +- Generated 22 vehicles with 5 completing their routes +- Traffic light state changes working correctly +- Vehicle routing through intersections validated + +--- + +## NEXT: Distributed Architecture Implementation + ### Compreender os Conceitos Fundamentais Primeiro, as tecnologias e paradigmas chave necessários para este projeto devem ser totalmente compreendidos. From f0dbdb551d3070a3e4f99ae5df4915fc5aef47bd Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:01:55 +0100 Subject: [PATCH 03/15] Add GitHub Actions workflow for Java CI with Maven This workflow builds a Java project using Maven, caches dependencies, and updates the dependency graph for improved Dependabot alerts. --- .github/workflows/maven.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/maven.yml diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..06b6aa0 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,35 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 From bb18c1119ec99bf3e7086e6bc374880b05b977d3 Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:02:55 +0100 Subject: [PATCH 04/15] Update Maven build file path to main/pom.xml --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 06b6aa0..c68fc22 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -28,7 +28,7 @@ jobs: distribution: 'temurin' cache: maven - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B package --file main/pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph From 3689f7a207db14d8d010cd10b78d45ef3ff002ed Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:08:16 +0100 Subject: [PATCH 05/15] Set working directory for dependency graph update Specify working directory for dependency graph update --- .github/workflows/maven.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c68fc22..5472be2 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -33,3 +33,5 @@ jobs: # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + with: + working-directory: main From c30aa25de0368e5a92265e3f2b6266eebdb03a5c Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:14:34 +0100 Subject: [PATCH 06/15] Update Maven workflow to use JDK 17 and improve steps --- .github/workflows/maven.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 5472be2..6fa7075 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,17 +21,25 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: maven - - name: Build with Maven - run: mvn -B package --file main/pom.xml - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + - name: Build with Maven + run: mvn -B package + working-directory: main + + # Generate dependency graph explicitly (run step supports -f) + - name: Generate dependency graph + run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph + + # Upload the generated dependency files so you can inspect them in the workflow run + - name: Upload dependency graph artifact + uses: actions/upload-artifact@v4 with: - working-directory: main + name: dependency-graph + path: main/target/** From 12b7aabe872a217ecc96b7913e1050e4edaffa32 Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:21:36 +0100 Subject: [PATCH 07/15] Enhance CI workflow with security and dependency checks Added security scan and dependency review jobs to the workflow. --- .github/workflows/maven.yml | 97 ++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 11 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 6fa7075..acc3264 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,11 +1,6 @@ # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Java CI with Maven on: @@ -16,9 +11,7 @@ on: jobs: build: - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -33,13 +26,95 @@ jobs: run: mvn -B package working-directory: main - # Generate dependency graph explicitly (run step supports -f) + # Generate dependency graph explicitly - name: Generate dependency graph - run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph + run: mvn -B com.github.ferstl:depgraph-maven-plugin:4.0.1:graph + working-directory: main - # Upload the generated dependency files so you can inspect them in the workflow run + # Upload the packaged JAR file as an artifact + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: package + path: main/target/*.jar # Upload only the JAR + + # Upload the generated dependency graph so you can inspect it - name: Upload dependency graph artifact uses: actions/upload-artifact@v4 with: name: dependency-graph - path: main/target/** + path: main/target/dependency-graph.dot # Upload the specific graph file + + test-and-coverage: + runs-on: ubuntu-latest + needs: build # Make it dependent on the build job to ensure code compiles + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Run tests and generate coverage report + # 'verify' runs all tests. 'jacoco:report' generates the coverage report. + # This assumes you have the JaCoCo plugin configured in your pom.xml + run: mvn -B verify jacoco:report + working-directory: main + + - name: Upload coverage report + # This uploads the HTML coverage report as an artifact + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: main/target/site/jacoco/ # Path to the JaCoCo HTML report + + security-scan: + # NEW JOB: This job runs GitHub's CodeQL to find security vulnerabilities. + # It runs in parallel with other jobs. + runs-on: ubuntu-latest + + # Required permissions for CodeQL to write results + permissions: + security-events: write # for github/codeql-action/analyze + actions: read # for github/codeql-action/init + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: 'java' # Specify the language to analyze + + - name: Build project for CodeQL + # CodeQL needs to monitor the build process. + # We skip tests here (-DskipTests) because we only need the compiled code + # for static analysis, and tests are run in the 'test-and-coverage' job. + run: mvn -B clean package -DskipTests + working-directory: main + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + dependency-review: + # NEW JOB: This job checks for vulnerable dependencies on Pull Requests. + # It prevents merging PRs that introduce known vulnerabilities. + runs-on: ubuntu-latest + + # This job only needs to run on pull requests + if: github.event_name == 'pull_request' + + permissions: + contents: read # To read dependency files + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 From 9093b13c5d43f5a278f3106e11c4e1090c5f79a3 Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:27:37 +0100 Subject: [PATCH 08/15] Rollback Oops --- .github/workflows/maven.yml | 92 +++---------------------------------- 1 file changed, 6 insertions(+), 86 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index acc3264..6819189 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -11,7 +11,9 @@ on: jobs: build: + runs-on: ubuntu-latest + steps: - uses: actions/checkout@v4 @@ -26,95 +28,13 @@ jobs: run: mvn -B package working-directory: main - # Generate dependency graph explicitly + # Generate dependency graph explicitly (run step supports -f) - name: Generate dependency graph - run: mvn -B com.github.ferstl:depgraph-maven-plugin:4.0.1:graph - working-directory: main + run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph - # Upload the packaged JAR file as an artifact - - name: Upload package artifact - uses: actions/upload-artifact@v4 - with: - name: package - path: main/target/*.jar # Upload only the JAR - - # Upload the generated dependency graph so you can inspect it + # Upload the generated dependency files so you can inspect them in the workflow run - name: Upload dependency graph artifact uses: actions/upload-artifact@v4 with: name: dependency-graph - path: main/target/dependency-graph.dot # Upload the specific graph file - - test-and-coverage: - runs-on: ubuntu-latest - needs: build # Make it dependent on the build job to ensure code compiles - - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Run tests and generate coverage report - # 'verify' runs all tests. 'jacoco:report' generates the coverage report. - # This assumes you have the JaCoCo plugin configured in your pom.xml - run: mvn -B verify jacoco:report - working-directory: main - - - name: Upload coverage report - # This uploads the HTML coverage report as an artifact - uses: actions/upload-artifact@v4 - with: - name: code-coverage-report - path: main/target/site/jacoco/ # Path to the JaCoCo HTML report - - security-scan: - # NEW JOB: This job runs GitHub's CodeQL to find security vulnerabilities. - # It runs in parallel with other jobs. - runs-on: ubuntu-latest - - # Required permissions for CodeQL to write results - permissions: - security-events: write # for github/codeql-action/analyze - actions: read # for github/codeql-action/init - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: 'java' # Specify the language to analyze - - - name: Build project for CodeQL - # CodeQL needs to monitor the build process. - # We skip tests here (-DskipTests) because we only need the compiled code - # for static analysis, and tests are run in the 'test-and-coverage' job. - run: mvn -B clean package -DskipTests - working-directory: main - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - - dependency-review: - # NEW JOB: This job checks for vulnerable dependencies on Pull Requests. - # It prevents merging PRs that introduce known vulnerabilities. - runs-on: ubuntu-latest - - # This job only needs to run on pull requests - if: github.event_name == 'pull_request' - - permissions: - contents: read # To read dependency files - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Dependency Review - uses: actions/dependency-review-action@v4 + path: main/target/** From 33ed84b0c2eb9ffcd7a8d31940056e6dbdc50c59 Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:36:13 +0100 Subject: [PATCH 09/15] Enhance Maven workflow with release publishing Added a publish-release job to create a GitHub release with the built JAR file when a tag is pushed. --- .github/workflows/maven.yml | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 6819189..344c385 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,17 +1,15 @@ -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - name: Java CI with Maven on: push: branches: [ "main" ] + tags: + - 'v*.*.*' pull_request: branches: [ "main" ] jobs: build: - runs-on: ubuntu-latest steps: @@ -28,13 +26,36 @@ jobs: run: mvn -B package working-directory: main - # Generate dependency graph explicitly (run step supports -f) + - name: Upload built JAR + uses: actions/upload-artifact@v4 + with: + name: package + path: main/target/*.jar + - name: Generate dependency graph run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph - # Upload the generated dependency files so you can inspect them in the workflow run - name: Upload dependency graph artifact uses: actions/upload-artifact@v4 with: name: dependency-graph path: main/target/** + + publish-release: + runs-on: ubuntu-latest + needs: [build] + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + + steps: + - name: Download built JAR + uses: actions/download-artifact@v4 + with: + name: package + path: main/target/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: main/target/*.jar From 8e95bc4c01a51f0a2816cfaae3d945cc2a23e77b Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Thu, 23 Oct 2025 00:43:57 +0100 Subject: [PATCH 10/15] Testing job --- TODO.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index d323ab6..97d1982 100644 --- a/TODO.md +++ b/TODO.md @@ -39,7 +39,7 @@ Primeiro, as tecnologias e paradigmas chave necessários para este projeto devem - Uma **lista de eventos** central, frequentemente uma fila de prioridades, será necessária para armazenar eventos futuros, ordenados pelo seu timestamp. O ciclo principal da simulação retira o próximo evento da lista, processa-o e adiciona quaisquer novos eventos que resultem dele. -- **Processo de Poisson:** Para o modelo "mais realista" de chegadas de veículos, é especificado um processo de Poisson. A principal conclusão é que o tempo _entre_ chegadas consecutivas de veículos segue uma **distribuição exponencial**. Em Java, este intervalo pode ser gerado usando `Math.log(1 - Math.random()) / -lambda`, onde `lambda` (λi​) é a taxa de chegada especificada. +- **Processo de Poisson:** Para o modelo 'mais realista' de chegadas de veículos, é especificado um processo de Poisson. A principal conclusão é que o tempo _entre_ chegadas consecutivas de veículos segue uma **distribuição exponencial**. Em Java, este intervalo pode ser gerado usando `Math.log(1 - Math.random()) / -lambda`, onde `lambda` (λi​) é a taxa de chegada especificada. --- @@ -195,4 +195,4 @@ Assim que o sistema completo estiver a funcionar, as experiências exigidas pela - **Debugging:** Debugging de sistemas distribuídos podem ser difíceis. Uma framework de logging (como Log4j 2 ou SLF4J) pode ser usada para registar eventos//alterações de estado nos diferentes processos. -- **Configuração:** Valores como endereços IP, números de porta ou parâmetros da simulação não devem ser "hardcoded". Um ficheiro de configuração (ex: um ficheiro `.properties` ou `.json`) torna a aplicação mais fácil de executar e testar. \ No newline at end of file +- **Configuração:** Valores como endereços IP, números de porta ou parâmetros da simulação não devem ser "hardcoded". Um ficheiro de configuração (ex: um ficheiro `.properties` ou `.json`) torna a aplicação mais fácil de executar e testar. From 23f7a74798a753b19e3aa1d82c1e30f5d406e80d Mon Sep 17 00:00:00 2001 From: Leandro Afonso Date: Fri, 24 Oct 2025 20:20:15 +0100 Subject: [PATCH 11/15] Add dependency build to CI job --- main/pom.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/main/pom.xml b/main/pom.xml index 0adc5f4..56ce74f 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -42,6 +42,26 @@ sd.Entry + + org.apache.maven.plugins + maven-shade-plugin + 3.5.2 + + + package + + shade + + + + + sd.Entry + + + + + + From 684fb408efb14251d3f9c5a9ab8dd0ea9b13a682 Mon Sep 17 00:00:00 2001 From: David Alves Date: Mon, 27 Oct 2025 22:53:37 +0000 Subject: [PATCH 12/15] Create IntersectionProcess main class --- .../src/main/java/sd/IntersectionProcess.java | 565 ++++++++++++++++++ 1 file changed, 565 insertions(+) create mode 100644 main/src/main/java/sd/IntersectionProcess.java diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java new file mode 100644 index 0000000..cbe7cef --- /dev/null +++ b/main/src/main/java/sd/IntersectionProcess.java @@ -0,0 +1,565 @@ +package sd; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import sd.config.SimulationConfig; +import sd.model.Intersection; +import sd.model.MessageType; +import sd.model.TrafficLight; +import sd.model.TrafficLightState; +import sd.model.Vehicle; +import sd.protocol.MessageProtocol; +import sd.protocol.SocketConnection; + +/** + * Main class for an Intersection Process in the distributed traffic simulation. + * * Each IntersectionProcess runs as an independent Java application (JVM instance) + * representing one of the five intersections (Cr1-Cr5) in the network. + */ +public class IntersectionProcess { + + private final String intersectionId; + + private final SimulationConfig config; + + private final Intersection intersection; + + private ServerSocket serverSocket; + + private final Map outgoingConnections; + + private final ExecutorService connectionHandlerPool; + + private final ExecutorService trafficLightPool; + + private volatile boolean running; //Quando uma thread escreve um valor volatile, todas as outras + //threads veem a mudança imediatamente. + + /** + * Constructs a new IntersectionProcess. + * + * @param intersectionId The ID of this intersection (e.g., "Cr1"). + * @param configFilePath Path to the simulation.properties file. + * @throws IOException If configuration cannot be loaded. + */ + public IntersectionProcess(String intersectionId, String configFilePath) throws IOException { + this.intersectionId = intersectionId; + this.config = new SimulationConfig(configFilePath); + this.intersection = new Intersection(intersectionId); + this.outgoingConnections = new HashMap<>(); + this.connectionHandlerPool = Executors.newCachedThreadPool(); + this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions + this.running = false; + + System.out.println("=".repeat(60)); + System.out.println("INTERSECTION PROCESS: " + intersectionId); + System.out.println("=".repeat(60)); + } + + public void initialize() { + System.out.println("\n[" + intersectionId + "] Initializing intersection..."); + + createTrafficLights(); + + configureRouting(); + + startTrafficLights(); + + System.out.println("[" + intersectionId + "] Initialization complete."); + } + + /** + * Creates traffic lights for this intersection based on its physical connections. + * Each intersection has different number and directions of traffic lights + * according to the network topology. + */ + private void createTrafficLights() { + System.out.println("\n[" + intersectionId + "] Creating traffic lights..."); + + // Define directions based on the actual network topology + String[] directions; + switch (intersectionId) { + case "Cr1": + // Cr1: East (to Cr2), South (to Cr4), West (from Cr2) + directions = new String[]{"East", "South", "West"}; + 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"}; + 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) { + double greenTime = config.getTrafficLightGreenTime(intersectionId, direction); + double redTime = config.getTrafficLightRedTime(intersectionId, direction); + + TrafficLight light = new TrafficLight( + intersectionId + "-" + direction, + direction, + greenTime, + redTime + ); + + intersection.addTrafficLight(light); + System.out.println(" Created traffic light: " + direction + + " (Green: " + greenTime + "s, Red: " + redTime + "s)"); + } + } + + private void configureRouting() { + System.out.println("\n[" + intersectionId + "] Configuring routing..."); + + 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 + 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 + 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 + break; + + case "Cr4": + // Cr4 connections: → Cr5 (East) + intersection.configureRoute("Cr5", "East"); // Go to Cr5 + intersection.configureRoute("S", "East"); // S via Cr5 + 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 + break; + + default: + System.err.println(" Warning: Unknown intersection ID: " + intersectionId); + } + + System.out.println(" Routing configured."); + } + + /** + * Starts all traffic light threads. + */ + private void startTrafficLights() { + System.out.println("\n[" + intersectionId + "] Starting traffic light threads..."); + + for (TrafficLight light : intersection.getTrafficLights()) { + trafficLightPool.submit(() -> runTrafficLightCycle(light)); + System.out.println(" Started thread for: " + light.getDirection()); + } + } + + /** + * The main loop for a traffic light thread. + * Continuously cycles between GREEN and RED states. + * + * @param light The traffic light to control. + */ + private void runTrafficLightCycle(TrafficLight light) { + System.out.println("[" + light.getId() + "] Traffic light thread started."); + + while (running) { + try { + // GREEN phase + light.changeState(TrafficLightState.GREEN); + System.out.println("[" + light.getId() + "] State: GREEN"); + + // Process vehicles while green + processGreenLight(light); + + // Wait for green duration + Thread.sleep((long) (light.getGreenTime() * 1000)); + + // RED phase + light.changeState(TrafficLightState.RED); + System.out.println("[" + light.getId() + "] State: RED"); + + // Wait for red duration + Thread.sleep((long) (light.getRedTime() * 1000)); + + } catch (InterruptedException e) { + System.out.println("[" + light.getId() + "] Traffic light thread interrupted."); + break; + } + } + + System.out.println("[" + light.getId() + "] Traffic light thread stopped."); + } + + /** + * Processes vehicles when a traffic light is GREEN. + * Dequeues vehicles and sends them to their next destination. + * + * @param light The traffic light that is currently green. + */ + private void processGreenLight(TrafficLight light) { + while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) { + Vehicle vehicle = light.removeVehicle(); + + if (vehicle != null) { + // Get crossing time based on vehicle type + double crossingTime = getCrossingTimeForVehicle(vehicle); + + // Simulate crossing time + try { + Thread.sleep((long) (crossingTime * 1000)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + + // Update vehicle statistics + vehicle.addCrossingTime(crossingTime); + + // Update intersection statistics + intersection.incrementVehiclesSent(); + + // Send vehicle to next destination + sendVehicleToNextDestination(vehicle); + } + } + } + + /** + * Gets the crossing time for a vehicle based on its type. + * + * @param vehicle The vehicle. + * @return The crossing time in seconds. + */ + private double getCrossingTimeForVehicle(Vehicle vehicle) { + switch (vehicle.getType()) { + case BIKE: + return config.getBikeVehicleCrossingTime(); + case LIGHT: + return config.getLightVehicleCrossingTime(); + case HEAVY: + return config.getHeavyVehicleCrossingTime(); + default: + return config.getLightVehicleCrossingTime(); + } + } + + /** + * Sends a vehicle to its next destination via socket connection. + * + * @param vehicle The vehicle that has crossed this intersection. + */ + private void sendVehicleToNextDestination(Vehicle vehicle) { + String nextDestination = vehicle.getCurrentDestination(); + + try { + // Get or create connection to next destination + SocketConnection connection = getOrCreateConnection(nextDestination); + + // Create and send message + MessageProtocol message = new VehicleTransferMessage( + intersectionId, + nextDestination, + vehicle + ); + + connection.sendMessage(message); + + System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() + + " to " + nextDestination); + + // Update vehicle's path - advance to next destination in route + vehicle.advanceRoute(); + + } catch (IOException | InterruptedException e) { + System.err.println("[" + intersectionId + "] Failed to send vehicle " + + vehicle.getId() + " to " + nextDestination + ": " + e.getMessage()); + } + } + + /** + * Gets an existing connection to a destination or creates a new one. + * + * @param destinationId The ID of the destination node. + * @return The SocketConnection to that destination. + * @throws IOException If connection cannot be established. + * @throws InterruptedException If connection attempt is interrupted. + */ + private synchronized SocketConnection getOrCreateConnection(String destinationId) + throws IOException, InterruptedException { + + if (!outgoingConnections.containsKey(destinationId)) { + String host = getHostForDestination(destinationId); + int port = getPortForDestination(destinationId); + + System.out.println("[" + intersectionId + "] Creating connection to " + + destinationId + " at " + host + ":" + port); + + SocketConnection connection = new SocketConnection(host, port); + outgoingConnections.put(destinationId, connection); + } + + return outgoingConnections.get(destinationId); + } + + /** + * Gets the host address for a destination node from configuration. + * + * @param destinationId The destination node ID. + * @return The host address. + */ + 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(); + } + } + + /** + * Gets the port number for a destination node from configuration. + * + * @param destinationId The destination node ID. + * @return The port number. + */ + 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(); + } + } + + /** + * Starts the server socket and begins accepting incoming connections. + * This is the main listening loop of the process. + * + * @throws IOException If the server socket cannot be created. + */ + public void start() throws IOException { + int port = config.getIntersectionPort(intersectionId); + serverSocket = new ServerSocket(port); + running = true; + + System.out.println("\n[" + intersectionId + "] Server started on port " + port); + System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n"); + + // Main accept loop + while (running) { + try { + Socket clientSocket = serverSocket.accept(); + + // Handle each connection in a separate thread + connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket)); + + } catch (IOException e) { + if (running) { + System.err.println("[" + intersectionId + "] Error accepting connection: " + + e.getMessage()); + } + } + } + } + + /** + * Handles an incoming connection from another process. + * Continuously listens for vehicle transfer messages. + * + * @param clientSocket The accepted socket connection. + */ + private void handleIncomingConnection(Socket clientSocket) { + try (SocketConnection connection = new SocketConnection(clientSocket)) { + + System.out.println("[" + intersectionId + "] New connection accepted from " + + clientSocket.getInetAddress().getHostAddress()); + + // Continuously receive messages while connection is active + while (running && connection.isConnected()) { + try { + MessageProtocol message = connection.receiveMessage(); + + if (message.getType() == MessageType.VEHICLE_TRANSFER) { + Vehicle vehicle = (Vehicle) message.getPayload(); + + System.out.println("[" + intersectionId + "] Received vehicle: " + + vehicle.getId() + " from " + message.getSourceNode()); + + // Add vehicle to appropriate queue + intersection.receiveVehicle(vehicle); + } + + } catch (ClassNotFoundException e) { + System.err.println("[" + intersectionId + "] Unknown message type received: " + + e.getMessage()); + } + } + + } catch (IOException e) { + if (running) { + System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage()); + } + } + } + + /** + * Stops the intersection process gracefully. + * Shuts down all threads and closes all connections. + */ + public void shutdown() { + System.out.println("\n[" + intersectionId + "] Shutting down..."); + running = false; + + // Close server socket + try { + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + } + } catch (IOException e) { + System.err.println("[" + intersectionId + "] Error closing server socket: " + + e.getMessage()); + } + + // Shutdown thread pools + trafficLightPool.shutdown(); + connectionHandlerPool.shutdown(); + + try { + if (!trafficLightPool.awaitTermination(5, TimeUnit.SECONDS)) { + trafficLightPool.shutdownNow(); + } + if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) { + connectionHandlerPool.shutdownNow(); + } + } catch (InterruptedException e) { + trafficLightPool.shutdownNow(); + connectionHandlerPool.shutdownNow(); + } + + // Close all outgoing connections + for (Map.Entry entry : outgoingConnections.entrySet()) { + try { + entry.getValue().close(); + } catch (IOException e) { + System.err.println("[" + intersectionId + "] Error closing connection to " + + entry.getKey() + ": " + e.getMessage()); + } + } + + System.out.println("[" + intersectionId + "] Shutdown complete."); + 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 --- + + /** + * Implementation of MessageProtocol for vehicle transfers between processes. + */ + private static class VehicleTransferMessage implements MessageProtocol { + private static final long serialVersionUID = 1L; + + private final String sourceNode; + private final String destinationNode; + private final Vehicle payload; + + public VehicleTransferMessage(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; + } + } +} From dab0651dbde763aa32eb7c7867ca59e68042973b Mon Sep 17 00:00:00 2001 From: David Alves Date: Wed, 29 Oct 2025 22:36:58 +0000 Subject: [PATCH 13/15] Corrected directions --- .../src/main/java/sd/IntersectionProcess.java | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java index cbe7cef..606c5ac 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."); From db5e01021ac1b791ed9eb4235eefa531603e4592 Mon Sep 17 00:00:00 2001 From: David Alves Date: Thu, 30 Oct 2025 10:41:17 +0000 Subject: [PATCH 14/15] Refactor IntersectionProcess and add unit tests --- .../src/main/java/sd/IntersectionProcess.java | 51 +- .../test/java/IntersectionProcessTest.java | 473 ++++++++++++++++++ 2 files changed, 477 insertions(+), 47 deletions(-) create mode 100644 main/src/test/java/IntersectionProcessTest.java diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java index 606c5ac..66d55c8 100644 --- a/main/src/main/java/sd/IntersectionProcess.java +++ b/main/src/main/java/sd/IntersectionProcess.java @@ -177,7 +177,7 @@ public class IntersectionProcess { while (running) { try { - // GREEN phase + // Green state light.changeState(TrafficLightState.GREEN); System.out.println("[" + light.getId() + "] State: GREEN"); @@ -187,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"); @@ -323,10 +323,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); } } @@ -339,10 +337,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); } } @@ -465,45 +461,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/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; + } + } +} From dc4f567e1f7de0db4c45cecb2de5c8cd707bb746 Mon Sep 17 00:00:00 2001 From: David Alves Date: Thu, 30 Oct 2025 15:57:58 +0000 Subject: [PATCH 15/15] Move vehicle route advancement to intersection arrival --- main/src/main/java/sd/IntersectionProcess.java | 3 +-- main/src/main/java/sd/model/Intersection.java | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java index 66d55c8..11db78d 100644 --- a/main/src/main/java/sd/IntersectionProcess.java +++ b/main/src/main/java/sd/IntersectionProcess.java @@ -280,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 " + 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)) {