diff --git a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/BenchmarkEnums.java b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/BenchmarkEnums.java index 4c5864912f..d281cad0e4 100644 --- a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/BenchmarkEnums.java +++ b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/BenchmarkEnums.java @@ -7,8 +7,12 @@ public class BenchmarkEnums { // Constant for preference public static final String PREFERENCE = "preference"; + public static final String METRICS = "metrics"; + public static final String DURATION = "duration"; // Constant for recommended public static final String RECOMMENDED = "recommended"; + public static final String OPTIONS = "options"; + public enum TestUnit { DISTANCE, @@ -47,8 +51,8 @@ public Map getRequestParams() { return switch (this) { case ALGO_CH -> Map.of(PREFERENCE, RECOMMENDED); case ALGO_CORE -> Map.of(PREFERENCE, - RECOMMENDED, "options", Map.of("avoid_features", List.of("ferries"))); - case ALGO_LM_ASTAR -> Map.of(PREFERENCE, RECOMMENDED, "options", + RECOMMENDED, OPTIONS, Map.of("avoid_features", List.of("ferries"))); + case ALGO_LM_ASTAR -> Map.of(PREFERENCE, RECOMMENDED, OPTIONS, Map.of("avoid_polygons", Map.of("type", "Polygon", "coordinates", List.of(List.of(List.of(100.0, 100.0), List.of(100.001, 100.0), @@ -73,4 +77,48 @@ public String getValue() { } } + /** + * Enum representing different matrix modes for benchmarking. + * getRequestParams() provides the parameters to trigger each matrix algorithm. + */ + public enum MatrixModes { + ALGO_DIJKSTRA_MATRIX, + ALGO_CORE_MATRIX, + ALGO_RPHAST_MATRIX; + + public static MatrixModes fromString(String value) { + return switch (value.toLowerCase()) { + case "algodijkstra" -> ALGO_DIJKSTRA_MATRIX; + case "algocore" -> ALGO_CORE_MATRIX; + case "algorphast" -> ALGO_RPHAST_MATRIX; + default -> throw new IllegalArgumentException("Invalid matrix mode: " + value); + }; + } + + public List getProfiles() { + return switch (this) { + case ALGO_DIJKSTRA_MATRIX, ALGO_CORE_MATRIX, ALGO_RPHAST_MATRIX -> List.of("driving-car"); + }; + } + /** + * Returns the request parameters for the matrix algorithm. + * These parameters are used to trigger the specific matrix algorithm. + * This is not great as we have to maintain this in multiple places, + * at the moment this is the only way to trigger the algorithms. + * What would be better is to have a common interface for the algorithms, + * but that would require a larger refactor of the codebase. + * @return a map of request parameters + */ + public Map getRequestParams() { + return switch (this) { + case ALGO_RPHAST_MATRIX -> Map.of(METRICS, List.of(DURATION)); + case ALGO_CORE_MATRIX -> Map.of(METRICS, + List.of(DURATION), OPTIONS, Map.of("dynamic_speeds", "true")); + case ALGO_DIJKSTRA_MATRIX -> Map.of(METRICS, + List.of(DURATION), OPTIONS, Map.of("dynamic_speeds", "false", "avoid_features", List.of("ferries"))); + + }; + } + } + } diff --git a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/Config.java b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/Config.java index fdae9d2070..278dfdb806 100644 --- a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/Config.java +++ b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/Config.java @@ -1,6 +1,7 @@ package org.heigit.ors.benchmark; import org.heigit.ors.benchmark.BenchmarkEnums.DirectionsModes; +import org.heigit.ors.benchmark.BenchmarkEnums.MatrixModes; import org.heigit.ors.benchmark.BenchmarkEnums.TestUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,6 +10,7 @@ import java.util.List; import static org.heigit.ors.benchmark.BenchmarkEnums.DirectionsModes.*; +import static org.heigit.ors.benchmark.BenchmarkEnums.MatrixModes.*; public class Config { private static final Logger logger = LoggerFactory.getLogger(Config.class); @@ -28,7 +30,8 @@ public class Config { private final boolean parallelExecution; private final TestUnit testUnit; private final List sourceFiles; - private final List modes; + private final List directionsModes; + private final List matrixModes; private final List ranges; public Config() { @@ -48,8 +51,8 @@ public Config() { this.testUnit = TestUnit.fromString(getSystemProperty("test_unit", "distance")); this.sourceFiles = parseCommaSeparatedStringToStrings(getSystemProperty("source_files", "")); this.ranges = parseCommaSeparatedStringToInts(this.range); - this.modes = parseCommaSeparatedStringToStrings(getSystemProperty("modes", "")); - } + this.directionsModes = parseCommaSeparatedStringToStrings(getSystemProperty("modes", "")); + this.matrixModes = parseCommaSeparatedStringToStrings(getSystemProperty("matrix_modes", ""));} private String getSystemProperty(String key, String defaultValue) { String value = System.getProperty(key) != null ? System.getProperty(key) : defaultValue; @@ -145,9 +148,16 @@ public List getRanges() { } public List getDirectionsModes() { - return modes.isEmpty() ? List.of(ALGO_CH, ALGO_CORE, ALGO_LM_ASTAR) - : modes.stream() + return directionsModes.isEmpty() ? List.of(ALGO_CH, ALGO_CORE, ALGO_LM_ASTAR) + : directionsModes.stream() .map(DirectionsModes::fromString) .toList(); } + + public List getMatrixModes() { + return matrixModes.isEmpty() ? List.of(ALGO_DIJKSTRA_MATRIX, ALGO_CORE_MATRIX, ALGO_RPHAST_MATRIX) + : matrixModes.stream() + .map(MatrixModes::fromString) + .toList(); + } } diff --git a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/MatrixAlgorithmLoadTest.java b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/MatrixAlgorithmLoadTest.java index 768645a968..e6645934d0 100644 --- a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/MatrixAlgorithmLoadTest.java +++ b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/MatrixAlgorithmLoadTest.java @@ -1,16 +1,17 @@ package org.heigit.ors.benchmark; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import io.gatling.javaapi.core.PopulationBuilder; import io.gatling.javaapi.core.ScenarioBuilder; import io.gatling.javaapi.core.Session; import io.gatling.javaapi.http.HttpRequestActionBuilder; -import org.heigit.ors.benchmark.BenchmarkEnums.DirectionsModes; +import org.heigit.ors.benchmark.BenchmarkEnums.MatrixModes; import org.heigit.ors.benchmark.exceptions.RequestBodyCreationException; import org.heigit.ors.util.SourceUtils; import org.slf4j.LoggerFactory; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -20,16 +21,38 @@ import static io.gatling.javaapi.http.HttpDsl.http; import static io.gatling.javaapi.http.HttpDsl.status; +/** + * Load test implementation for OpenRouteService Matrix API using Gatling + * framework. + * + * This class performs load testing on the matrix endpoint by: + * - Reading matrix test data from CSV files containing coordinates, sources, + * and destinations + * - Creating HTTP requests to the /v2/matrix/{profile} endpoint + * - Testing different matrix calculation modes and routing profiles + * - Measuring response times and throughput under concurrent load + * + * The test data is expected to be in CSV format with columns: + * coordinates, sources, destinations, distances, profile + */ public class MatrixAlgorithmLoadTest extends AbstractLoadTest { static { logger = LoggerFactory.getLogger(MatrixAlgorithmLoadTest.class); } + /** + * Constructs a new MatrixAlgorithmLoadTest instance. + * Initializes the load test with configuration from the parent class. + */ public MatrixAlgorithmLoadTest() { super(); } + /** + * Logs configuration information specific to matrix load testing. + * Displays source files, concurrent users, and execution mode. + */ @Override protected void logConfigInfo() { logger.info("Initializing MatrixAlgorithmLoadTest:"); @@ -38,33 +61,68 @@ protected void logConfigInfo() { logger.info("- Execution mode: {}", config.isParallelExecution() ? "parallel" : "sequential"); } + /** + * Logs the type of test being performed. + */ @Override protected void logTestTypeInfo() { logger.info("Testing matrix"); } + /** + * Creates test scenarios for all combinations of matrix modes, source files, + * and profiles. + * + * @param isParallel whether scenarios should be executed in parallel + * @return stream of PopulationBuilder instances for each test scenario + */ @Override protected Stream createScenarios(boolean isParallel) { - return config.getDirectionsModes().stream() + return config.getMatrixModes().stream() .flatMap(mode -> config.getSourceFiles().stream() .flatMap(sourceFile -> mode.getProfiles().stream() .map(profile -> createScenarioWithInjection(sourceFile, isParallel, mode, profile)))); } - private PopulationBuilder createScenarioWithInjection(String sourceFile, boolean isParallel, DirectionsModes mode, + /** + * Creates a single test scenario with user injection configuration. + * + * @param sourceFile path to the CSV file containing test data + * @param isParallel whether the scenario runs in parallel mode + * @param mode the matrix calculation mode to test + * @param profile the routing profile to test + * @return PopulationBuilder configured with the specified parameters + */ + private PopulationBuilder createScenarioWithInjection(String sourceFile, boolean isParallel, MatrixModes mode, String profile) { String scenarioName = formatScenarioName(mode, profile, isParallel); - return createMatrixScenario(scenarioName, sourceFile, config, mode, profile) + return createMatrixScenario(scenarioName, sourceFile, mode, profile) .injectOpen(atOnceUsers(config.getNumConcurrentUsers())); } - private String formatScenarioName(DirectionsModes mode, String profile, boolean isParallel) { + /** + * Formats a descriptive name for the test scenario. + * + * @param mode the matrix calculation mode + * @param profile the routing profile + * @param isParallel whether the scenario runs in parallel + * @return formatted scenario name string + */ + private String formatScenarioName(MatrixModes mode, String profile, boolean isParallel) { return String.format("%s - %s - %s", isParallel ? "Parallel" : "Sequential", mode, profile); } - private static ScenarioBuilder createMatrixScenario(String name, String sourceFile, Config config, - DirectionsModes mode, String profile) { - + /** + * Creates a Gatling scenario for matrix load testing. + * + * @param name descriptive name for the scenario + * @param sourceFile path to CSV file containing test coordinates + * @param mode matrix calculation mode to test + * @param profile routing profile to test + * @return ScenarioBuilder configured for matrix testing + */ + private static ScenarioBuilder createMatrixScenario(String name, String sourceFile, + MatrixModes mode, String profile) { try { List> records = csv(sourceFile).readRecords(); List> targetRecords = SourceUtils.getRecordsByProfile(records, profile); @@ -77,7 +135,7 @@ private static ScenarioBuilder createMatrixScenario(String name, String sourceFi return scenario(name) .feed(targetRecords.iterator(), 1) .asLongAs(session -> remainingRecords.decrementAndGet() >= 0) - .on(exec(createRequest(name, config, mode, profile))); + .on(exec(createRequest(name, mode, profile))); } catch (IllegalStateException e) { logger.error("Error building scenario: ", e); @@ -86,43 +144,90 @@ private static ScenarioBuilder createMatrixScenario(String name, String sourceFi } } - private static HttpRequestActionBuilder createRequest(String name, Config config, DirectionsModes mode, + /** + * Creates an HTTP request action for the matrix API endpoint. + * + * @param name request name for identification in test results + * @param mode matrix calculation mode + * @param profile routing profile + * @return HttpRequestActionBuilder configured for matrix API calls + */ + private static HttpRequestActionBuilder createRequest(String name, MatrixModes mode, String profile) { return http(name) .post("/v2/matrix/" + profile) - .body(StringBody(session -> createRequestBody(session, config, mode))) + .header("Accept", "application/json;charset=UTF-8") + .body(StringBody(session -> createRequestBody(session, mode))) .asJson() .check(status().is(200)); } - static String createRequestBody(Session session, Config config, DirectionsModes mode) { + /** + * Creates the JSON request body for matrix API calls from CSV session data. + * + * @param session Gatling session containing CSV row data + * @param mode matrix calculation mode providing additional parameters + * @return JSON string representation of the request body + * @throws RequestBodyCreationException if JSON serialization fails or data is missing + */ + static String createRequestBody(Session session, MatrixModes mode) { try { - Map requestBody = new java.util.HashMap<>(Map.of( - "locations", createLocationsListFromArrays(session, config), - "sources", List.of(0), - "destinations", List.of(1))); + // 1) Retrieve the raw feeder values. Gatling will give us a List for each column. + @SuppressWarnings("unchecked") + List coordsList = (List) session.get("coordinates"); + @SuppressWarnings("unchecked") + List sourcesList = (List) session.get("sources"); + @SuppressWarnings("unchecked") + List destsList = (List) session.get("destinations"); + + // 2) Fail fast if any column is missing or empty + if (coordsList == null || coordsList.isEmpty()) { + throw new RequestBodyCreationException("'coordinates' field is missing or empty in session"); + } + if (sourcesList == null || sourcesList.isEmpty()) { + throw new RequestBodyCreationException("'sources' field is missing or empty in session"); + } + if (destsList == null || destsList.isEmpty()) { + throw new RequestBodyCreationException("'destinations' field is missing or empty in session"); + } + + // 3) The first element of each List is the actual JSON‐style value. + String coordinatesJson = coordsList.get(0); + String sourcesJson = sourcesList.get(0); + String destsJson = destsList.get(0); + + logger.debug( + "Raw CSV values → coordinatesJson: {}, sourcesJson: {}, destsJson: {}", + coordinatesJson, sourcesJson, destsJson + ); + + // 4) Let Jackson parse "[[lon, lat], [lon, lat], …]" into List> + List> locations = objectMapper.readValue( + coordinatesJson, new TypeReference>>() {} + ); + + // 5) Similarly parse "[0, 1, 2]" into List + List sources = objectMapper.readValue( + sourcesJson, new TypeReference>() {} + ); + List destinations = objectMapper.readValue( + destsJson, new TypeReference>() {} + ); + + // 6) Build the request body map and merge in any extra params from MatrixModes + Map requestBody = new HashMap<>(Map.of( + "locations", locations, + "sources", sources, + "destinations", destinations + )); requestBody.putAll(mode.getRequestParams()); + + // 7) Serialize to JSON and return return objectMapper.writeValueAsString(requestBody); - } catch (JsonProcessingException e) { - throw new RequestBodyCreationException("Failed to create request body", e); - } - } - static List> createLocationsListFromArrays(Session session, Config config) { - List> locations = new ArrayList<>(); - try { - Double startLon = Double.valueOf((String) session.getList(config.getFieldStartLon()).get(0)); - Double startLat = Double.valueOf((String) session.getList(config.getFieldStartLat()).get(0)); - locations.add(List.of(startLon, startLat)); - Double endLon = Double.valueOf((String) session.getList(config.getFieldEndLon()).get(0)); - Double endLat = Double.valueOf((String) session.getList(config.getFieldEndLat()).get(0)); - locations.add(List.of(endLon, endLat)); - } catch (NumberFormatException e) { - String errorMessage = String.format( - "Failed to parse coordinate values in locations list at index %d. Original value could not be converted to double", - locations.size()); - throw new RequestBodyCreationException("Error processing coordinates: " + errorMessage, e); + } catch (JsonProcessingException e) { + // Jackson failed to parse or serialize + throw new RequestBodyCreationException("Failed to serialize request body to JSON", e); } - return locations; } } diff --git a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/exceptions/RequestBodyCreationException.java b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/exceptions/RequestBodyCreationException.java index d3550d93d1..d01a9eb149 100644 --- a/ors-benchmark/src/main/java/org/heigit/ors/benchmark/exceptions/RequestBodyCreationException.java +++ b/ors-benchmark/src/main/java/org/heigit/ors/benchmark/exceptions/RequestBodyCreationException.java @@ -4,4 +4,8 @@ public class RequestBodyCreationException extends RuntimeException { public RequestBodyCreationException(String message, Throwable cause) { super(message, cause); } + + public RequestBodyCreationException(String message) { + super(message); + } } diff --git a/ors-benchmark/src/main/java/org/heigit/ors/coordinates_generator/generators/CoordinateGeneratorMatrix.java b/ors-benchmark/src/main/java/org/heigit/ors/coordinates_generator/generators/CoordinateGeneratorMatrix.java index 2253e51668..766bcafe36 100644 --- a/ors-benchmark/src/main/java/org/heigit/ors/coordinates_generator/generators/CoordinateGeneratorMatrix.java +++ b/ors-benchmark/src/main/java/org/heigit/ors/coordinates_generator/generators/CoordinateGeneratorMatrix.java @@ -329,12 +329,15 @@ private boolean generateMatricesForProfile() { int currRetry = 0; List snappedCoordinates = new ArrayList<>(); + //First, generate a center coordinate for a matrix extent + double[] randomCoordinate = CoordinateGeneratorHelper.generateRandomPoint(extent); + double[] localExtent = createExtentAroundPoint(randomCoordinate, effectiveMaxDistance); while (snappedCoordinates.size() < targetNum && currRetry < maxRetries) { - List randomCoordinate = CoordinateGeneratorHelper.randomCoordinatesInExtent(1, - extent); + List matrixCoordinate = CoordinateGeneratorHelper.randomCoordinatesInExtent(1, + localExtent); // Snap the coordinates to the road network - List snappedCoordinate = coordinateSnapper.snapCoordinates(randomCoordinate, profile); + List snappedCoordinate = coordinateSnapper.snapCoordinates(matrixCoordinate, profile); snappedCoordinates.addAll(snappedCoordinate); currRetry++; } @@ -347,6 +350,30 @@ private boolean generateMatricesForProfile() { return processSnappedCoordinates(snappedCoordinates, effectiveMaxDistance); } + public static double[] createExtentAroundPoint(double[] coord, double maxExtentMeters) { + // Mean Earth radius in meters + final double R = 6_371_008.8; + + // Half the desired side length + double halfSide = maxExtentMeters / 2.0; + + // Convert center latitude to radians for the longitude calculation + double latRad = Math.toRadians(coord[1]); + + // Δ latitude in degrees: (distance / radius) × (180/π) + double deltaLat = Math.toDegrees(halfSide / R); + + // Δ longitude in degrees: (distance / (radius × cos(lat))) × (180/π) + double deltaLon = Math.toDegrees(halfSide / (R * Math.cos(latRad))); + + double minLat = coord[1] - deltaLat; + double maxLat = coord[1] + deltaLat; + double minLon = coord[0] - deltaLon; + double maxLon = coord[0] + deltaLon; + + return new double[] { minLon, minLat, maxLon, maxLat }; + } + private boolean processSnappedCoordinates(List snappedCoordinates, double maxDistance) { boolean addedNewMatrix = false; try { @@ -409,11 +436,11 @@ private boolean hasNearbyPoints(List row, List col, double m for (double[] rowCoord : row) { for (double[] colCoord : col) { double distance = CoordinateGeneratorHelper.calculateHaversineDistance(rowCoord, colCoord); - if (distance <= maxDistance) - return true; + if (distance > maxDistance) + return false; } } - return false; + return true; } /** @@ -453,7 +480,7 @@ private boolean rowsColsRouteable(List row, List col) { */ private boolean computeAndProcessMatrix(List snappedCoordinates) { int[] sources = java.util.stream.IntStream.range(0, numRows).toArray(); - int[] destinations = java.util.stream.IntStream.range(0, numCols).toArray(); + int[] destinations = java.util.stream.IntStream.range(numRows, numRows + numCols).toArray(); Optional fullMatrixResult = matrixCalculator.calculateAsymmetricMatrix( snappedCoordinates, sources, destinations, profile); diff --git a/ors-benchmark/src/test/java/org/heigit/ors/benchmark/BenchmarkEnumsTest.java b/ors-benchmark/src/test/java/org/heigit/ors/benchmark/BenchmarkEnumsTest.java index a243611701..63b6d03215 100644 --- a/ors-benchmark/src/test/java/org/heigit/ors/benchmark/BenchmarkEnumsTest.java +++ b/ors-benchmark/src/test/java/org/heigit/ors/benchmark/BenchmarkEnumsTest.java @@ -52,4 +52,36 @@ void testRangeTypeGetValue() { assertEquals("time", BenchmarkEnums.RangeType.TIME.getValue()); assertEquals("distance", BenchmarkEnums.RangeType.DISTANCE.getValue()); } + + @Test + void testMatrixModesFromString() { + assertEquals(BenchmarkEnums.MatrixModes.ALGO_DIJKSTRA_MATRIX, BenchmarkEnums.MatrixModes.fromString("algodijkstra")); + assertEquals(BenchmarkEnums.MatrixModes.ALGO_CORE_MATRIX, BenchmarkEnums.MatrixModes.fromString("algocore")); + assertEquals(BenchmarkEnums.MatrixModes.ALGO_RPHAST_MATRIX, BenchmarkEnums.MatrixModes.fromString("algorphast")); + + Throwable exception = assertThrows(IllegalArgumentException.class, () -> BenchmarkEnums.MatrixModes.fromString("invalid")); + assertTrue(exception instanceof IllegalArgumentException); + } + + @Test + void testMatrixModesGetDefaultProfiles() { + List dijkstraProfiles = BenchmarkEnums.MatrixModes.ALGO_DIJKSTRA_MATRIX.getProfiles(); + assertTrue(dijkstraProfiles.contains("driving-car")); + assertEquals(1, dijkstraProfiles.size()); + } + + @Test + void testMatrixModesGetRequestParams() { + Map rphastParams = BenchmarkEnums.MatrixModes.ALGO_RPHAST_MATRIX.getRequestParams(); + assertEquals("recommended", rphastParams.get("preference")); + assertEquals(1, rphastParams.size()); + + Map coreParams = BenchmarkEnums.MatrixModes.ALGO_CORE_MATRIX.getRequestParams(); + assertEquals("recommended", coreParams.get("preference")); + assertTrue(coreParams.get("options") instanceof Map); + + Map dijkstraParams = BenchmarkEnums.MatrixModes.ALGO_DIJKSTRA_MATRIX.getRequestParams(); + assertEquals("recommended", dijkstraParams.get("preference")); + assertTrue(dijkstraParams.get("options") instanceof List); + } } diff --git a/ors-benchmark/src/test/java/org/heigit/ors/benchmark/MatrixAlgorithmLoadTestTest.java b/ors-benchmark/src/test/java/org/heigit/ors/benchmark/MatrixAlgorithmLoadTestTest.java new file mode 100644 index 0000000000..9da93422b1 --- /dev/null +++ b/ors-benchmark/src/test/java/org/heigit/ors/benchmark/MatrixAlgorithmLoadTestTest.java @@ -0,0 +1,269 @@ +package org.heigit.ors.benchmark; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.gatling.javaapi.core.Session; +import org.heigit.ors.benchmark.BenchmarkEnums.MatrixModes; +import org.heigit.ors.benchmark.exceptions.RequestBodyCreationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class MatrixAlgorithmLoadTestTest { + private ObjectMapper objectMapper; + private Session mockSession; + private MatrixModes mockMode; + private static final List VALID_COORDINATES = + Arrays.asList( + "[[8.695556, 49.392701], [8.684623, 49.398284], [8.705916, 49.406309]]" + ); + private static final List VALID_SOURCES = + Arrays.asList("[0, 1]"); + private static final List VALID_DESTINATIONS = + Arrays.asList("[2]"); + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + mockSession = mock(Session.class); + mockMode = mock(MatrixModes.class); + + // Mock CSV data as it would appear in the session after CSV feeder + // Each CSV column becomes a List in the session + when(mockSession.get("coordinates")) + .thenReturn(VALID_COORDINATES); + when(mockSession.get("sources")).thenReturn(VALID_SOURCES); + when(mockSession.get("destinations")).thenReturn(VALID_DESTINATIONS); + when(mockMode.getRequestParams()).thenReturn(Map.of("preference", "recommended")); + } + + @Test + void createRequestBody_ShouldCreateValidJson() throws JsonProcessingException { + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + + // then + JsonNode json = objectMapper.readTree(result); + assertThat(json.get("locations")).isNotNull(); + assertThat(json.get("sources")).isNotNull(); + assertThat(json.get("destinations")).isNotNull(); + assertThat(json.get("preference").asText()).isEqualTo("recommended"); + } + + @Test + void createRequestBody_ShouldIncludeCorrectLocations() throws JsonProcessingException { + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + JsonNode locations = json.get("locations"); + assertEquals(3, locations.size()); + assertEquals(8.695556, locations.get(0).get(0).asDouble(), 0.000001); + assertEquals(49.392701, locations.get(0).get(1).asDouble(), 0.000001); + assertEquals(8.684623, locations.get(1).get(0).asDouble(), 0.000001); + assertEquals(49.398284, locations.get(1).get(1).asDouble(), 0.000001); + assertEquals(8.705916, locations.get(2).get(0).asDouble(), 0.000001); + assertEquals(49.406309, locations.get(2).get(1).asDouble(), 0.000001); + } + + @Test + void createRequestBody_ShouldIncludeCorrectSourcesAndDestinations() throws JsonProcessingException { + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + JsonNode sources = json.get("sources"); + JsonNode destinations = json.get("destinations"); + + assertEquals(2, sources.size()); + assertEquals(0, sources.get(0).asInt()); + assertEquals(1, sources.get(1).asInt()); + + assertEquals(1, destinations.size()); + assertEquals(2, destinations.get(0).asInt()); + } + + @ParameterizedTest(name = "createRequestBody throws if '{0}' list is empty") + @ValueSource(strings = { "coordinates", "sources", "destinations" }) + void createRequestBody_ShouldThrowExceptionForEmptyList(String emptyKey) { + // only override the one under test to be empty + when(mockSession.get(emptyKey)).thenReturn(List.of()); + + // verify that empty‐list triggers the exception + assertThrows( + RequestBodyCreationException.class, + () -> MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode) + ); + } + + @ParameterizedTest(name = "createRequestBody should throw when '{0}' is missing") + @ValueSource(strings = { "coordinates", "sources", "destinations" }) + void createRequestBody_ShouldThrowExceptionForMissingRequiredSessionAttributes(String missingAttribute) { + when(mockSession.get(missingAttribute)).thenReturn(null); + assertThrows(RequestBodyCreationException.class, + () -> MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode)); + } + + @ParameterizedTest(name = "createRequestBody throws when '{0}' contains invalid JSON") + @ValueSource(strings = { "coordinates", "sources", "destinations" }) + void createRequestBody_ShouldThrowExceptionForInvalidJson(String invalidKey) { + // override the one under test to invalid JSON + when(mockSession.get(invalidKey)).thenReturn(List.of("invalid json")); + + assertThrows( + RequestBodyCreationException.class, + () -> MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode) + ); + } + + @Test + void createRequestBody_WithDifferentMatrixMode() throws JsonProcessingException { + // given + when(mockMode.getRequestParams()).thenReturn(Map.of( + "preference", "fastest", + "options", Map.of("avoid_features", Arrays.asList("highways")))); + + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + assertEquals("fastest", json.get("preference").asText()); + assertThat(json.get("options")).isNotNull(); + } + + @Test + void createRequestBody_ShouldHandleComplexSourcesAndDestinations() throws JsonProcessingException { + // given + when(mockSession.get("sources")).thenReturn(Arrays.asList("[0, 1, 2, 3]")); + when(mockSession.get("destinations")).thenReturn(Arrays.asList("[4, 5, 6, 7, 8]")); + + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + JsonNode sources = json.get("sources"); + JsonNode destinations = json.get("destinations"); + + assertEquals(4, sources.size()); + assertEquals(5, destinations.size()); + assertEquals(0, sources.get(0).asInt()); + assertEquals(3, sources.get(3).asInt()); + assertEquals(4, destinations.get(0).asInt()); + assertEquals(8, destinations.get(4).asInt()); + } + + @Test + void createRequestBody_ShouldHandleSingleCoordinate() throws JsonProcessingException { + // given + when(mockSession.get("coordinates")).thenReturn(Arrays.asList("[[8.695556, 49.392701]]")); + when(mockSession.get("sources")).thenReturn(Arrays.asList("[0]")); + when(mockSession.get("destinations")).thenReturn(Arrays.asList("[0]")); + + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + JsonNode locations = json.get("locations"); + assertEquals(1, locations.size()); + assertEquals(8.695556, locations.get(0).get(0).asDouble(), 0.000001); + assertEquals(49.392701, locations.get(0).get(1).asDouble(), 0.000001); + } + + @Test + void createRequestBody_ShouldHandleLargeCoordinateArray() throws JsonProcessingException { + // given + when(mockSession.get("coordinates")).thenReturn(Arrays.asList( + "[[8.695556, 49.392701], [8.684623, 49.398284], [8.705916, 49.406309], [8.689981, 49.394522], [8.681502, 49.394791]]")); + when(mockSession.get("sources")).thenReturn(Arrays.asList("[0, 1, 2]")); + when(mockSession.get("destinations")).thenReturn(Arrays.asList("[3, 4]")); + + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + JsonNode locations = json.get("locations"); + assertEquals(5, locations.size()); + assertEquals(8.695556, locations.get(0).get(0).asDouble(), 0.000001); + assertEquals(49.392701, locations.get(0).get(1).asDouble(), 0.000001); + assertEquals(8.681502, locations.get(4).get(0).asDouble(), 0.000001); + assertEquals(49.394791, locations.get(4).get(1).asDouble(), 0.000001); + } + + @Test + void createRequestBody_ShouldHandleEmptyArrays() throws JsonProcessingException { + // given + when(mockSession.get("coordinates")).thenReturn(Arrays.asList("[]")); + when(mockSession.get("sources")).thenReturn(Arrays.asList("[]")); + when(mockSession.get("destinations")).thenReturn(Arrays.asList("[]")); + + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + JsonNode locations = json.get("locations"); + JsonNode sources = json.get("sources"); + JsonNode destinations = json.get("destinations"); + + assertEquals(0, locations.size()); + assertEquals(0, sources.size()); + assertEquals(0, destinations.size()); + } + + @Test + void createRequestBody_ShouldMergeMatrixModeParameters() throws JsonProcessingException { + // given + when(mockMode.getRequestParams()).thenReturn(Map.of( + "preference", "recommended", + "units", "m", + "metrics", Arrays.asList("distance", "duration"))); + + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + assertEquals("recommended", json.get("preference").asText()); + assertEquals("m", json.get("units").asText()); + assertThat(json.get("metrics")).isNotNull(); + assertEquals(2, json.get("metrics").size()); + assertEquals("distance", json.get("metrics").get(0).asText()); + assertEquals("duration", json.get("metrics").get(1).asText()); + } + + @Test + void createRequestBody_ShouldHandleNestedCoordinates() throws JsonProcessingException { + // given - coordinates with high precision + when(mockSession.get("coordinates")).thenReturn(Arrays.asList( + "[[8.695556789, 49.392701123], [8.684623456, 49.398284789]]")); + + // when + String result = MatrixAlgorithmLoadTest.createRequestBody(mockSession, mockMode); + JsonNode json = objectMapper.readTree(result); + + // then + JsonNode locations = json.get("locations"); + assertEquals(2, locations.size()); + assertEquals(8.695556789, locations.get(0).get(0).asDouble(), 0.000000001); + assertEquals(49.392701123, locations.get(0).get(1).asDouble(), 0.000000001); + assertEquals(8.684623456, locations.get(1).get(0).asDouble(), 0.000000001); + assertEquals(49.398284789, locations.get(1).get(1).asDouble(), 0.000000001); + } +} \ No newline at end of file