diff --git a/src/main/java/com/saucelabs/saucerest/api/PerformanceEndpoint.java b/src/main/java/com/saucelabs/saucerest/api/PerformanceEndpoint.java index c5d8e880..712908da 100644 --- a/src/main/java/com/saucelabs/saucerest/api/PerformanceEndpoint.java +++ b/src/main/java/com/saucelabs/saucerest/api/PerformanceEndpoint.java @@ -1,9 +1,23 @@ package com.saucelabs.saucerest.api; +import com.google.gson.reflect.TypeToken; import com.saucelabs.saucerest.DataCenter; -import com.saucelabs.saucerest.Unfinished; +import com.saucelabs.saucerest.HttpMethod; +import com.saucelabs.saucerest.model.performance.Baseline; +import com.saucelabs.saucerest.model.performance.BaselineParameter; +import com.saucelabs.saucerest.model.performance.BaselineResetStatus; +import com.saucelabs.saucerest.model.performance.DiscardedTests; +import com.saucelabs.saucerest.model.performance.GetPerformanceMetricsParameter; +import com.saucelabs.saucerest.model.performance.MetricAssertion; +import com.saucelabs.saucerest.model.performance.PerformanceMetrics; +import com.saucelabs.saucerest.model.performance.Regime; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; -@Unfinished("This endpoint is not yet completely implemented") public class PerformanceEndpoint extends AbstractEndpoint { public PerformanceEndpoint(DataCenter dataCenter) { super(dataCenter); @@ -20,4 +34,248 @@ public PerformanceEndpoint(String username, String accessKey, DataCenter dataCen public PerformanceEndpoint(String username, String accessKey, String apiServer) { super(username, accessKey, apiServer); } -} \ No newline at end of file + + /** + * Retrieves performance test results and metric values for the requesting account. + * + * @param parameter A {@link GetPerformanceMetricsParameter} object containing the parameters to filter the + * results + * @return A {@link PerformanceMetrics} object + * @throws IOException when the request fails + */ + public PerformanceMetrics getPerformanceResults(GetPerformanceMetricsParameter parameter) throws IOException { + return deserializeJSONObject( + requestWithQueryParameters(getBaseEndpoint(), HttpMethod.GET, parameter.toMap()), PerformanceMetrics.class); + } + + /** + * Retrieves the full results for a specific performance test run. + * + * @param jobID The unique identifier of the test + * @return A {@link PerformanceMetrics} object + * @throws IOException when the request fails + */ + public PerformanceMetrics getPerformanceResults(String jobID) throws IOException { + return getPerformanceResults(jobID, true); + } + + /** + * Retrieves the results for a specific performance test run. + * + * @param jobID The unique identifier of the test + * @param full When {@code false}, only basic data is returned instead of the full metric set + * @return A {@link PerformanceMetrics} object + * @throws IOException when the request fails + */ + public PerformanceMetrics getPerformanceResults(String jobID, boolean full) throws IOException { + Map params = new HashMap<>(); + params.put("full", full); + + return deserializeJSONObject( + requestWithQueryParameters(getBaseEndpoint(jobID), HttpMethod.GET, params), PerformanceMetrics.class); + } + + /** + * Returns information about outlier values in the test for the specified metrics. + * + * @param jobID The unique identifier of the test + * @param metricNames The metrics to evaluate + * @param orderIndex The record number to begin returning results from + * @return A {@link Map} of metric name to {@link MetricAssertion} + * @throws IOException when the request fails + */ + public Map getTestAssertions(String jobID, String[] metricNames, int orderIndex) + throws IOException { + Map params = new HashMap<>(); + params.put("metric_names", metricNames); + params.put("order_index", orderIndex); + + Type type = TypeToken.getParameterized(Map.class, String.class, MetricAssertion.class).getType(); + return deserializeJSON(requestWithQueryParameters(getBaseEndpoint(jobID) + "assert/", HttpMethod.GET, params), type); + } + + /** + * Returns the acceptable upper and lower boundary values for the specified metrics, as determined by the test's + * baseline. + * + * @param jobID The unique identifier of the test + * @param parameter A {@link BaselineParameter} object containing the parameters to filter the results + * @return A {@link Map} of metric name to {@link Baseline} + * @throws IOException when the request fails + */ + public Map getTestBaseline(String jobID, BaselineParameter parameter) throws IOException { + Type type = TypeToken.getParameterized(Map.class, String.class, Baseline.class).getType(); + return deserializeJSON( + requestWithQueryParameters(getBaseEndpoint(jobID) + "baseline/", HttpMethod.GET, parameter.toMap()), type); + } + + /** + * Indicates whether a baseline reset has occurred for the specified test. + * + * @param jobID The unique identifier of the test + * @return A {@link BaselineResetStatus} object + * @throws IOException when the request fails + */ + public BaselineResetStatus getBaselineResetHistory(String jobID) throws IOException { + String url = getBaseEndpoint(jobID) + "baseline/reset/"; + + return deserializeJSONObject(request(url, HttpMethod.GET), BaselineResetStatus.class); + } + + /** + * Resets the baseline calculation point for the specified test; tests run prior to the reset are excluded from + * future baseline calculations. + * + * @param jobID The unique identifier of the test + * @return {@link Response} + * @throws IOException when the request fails + */ + public Response resetBaseline(String jobID) throws IOException { + String url = getBaseEndpoint(jobID) + "baseline/reset/"; + + return request(url, HttpMethod.POST); + } + + /** + * Returns the list of tests that have been discarded from the baseline as outliers. + * + * @param jobID The unique identifier of the test + * @param orderIndex The record number to begin returning results from + * @return A {@link DiscardedTests} object + * @throws IOException when the request fails + */ + public DiscardedTests getDiscardedTests(String jobID, int orderIndex) throws IOException { + return getDiscardedTests(jobID, orderIndex, null); + } + + /** + * Returns the list of tests that have been discarded from the baseline as outliers. + * + * @param jobID The unique identifier of the test + * @param orderIndex The record number to begin returning results from + * @param limit The maximum number of results to return + * @return A {@link DiscardedTests} object + * @throws IOException when the request fails + */ + public DiscardedTests getDiscardedTests(String jobID, int orderIndex, Integer limit) throws IOException { + Map params = new HashMap<>(); + params.put("order_index", orderIndex); + if (limit != null) { + params.put("limit", limit); + } + + return deserializeJSONObject( + requestWithQueryParameters(getBaseEndpoint(jobID) + "discarded/", HttpMethod.GET, params), DiscardedTests.class); + } + + /** + * Excludes outlier results from future baseline calculations, marking them as flaky. + * + * @param jobID The unique identifier of the test + * @param orderIndex The record number to begin discarding results from + * @return {@link Response} + * @throws IOException when the request fails + */ + public Response discardOutliers(String jobID, int orderIndex) throws IOException { + Map params = new HashMap<>(); + params.put("order_index", orderIndex); + + return requestWithQueryParameters(getBaseEndpoint(jobID) + "discarded/", HttpMethod.POST, params); + } + + /** + * Returns the test history for the specified test. + * + * @param jobID The unique identifier of the test + * @param orderIndex The record number to begin returning results from + * @return A {@link PerformanceMetrics} object + * @throws IOException when the request fails + */ + public PerformanceMetrics getTestHistory(String jobID, int orderIndex) throws IOException { + return getTestHistory(jobID, orderIndex, null); + } + + /** + * Returns the test history for the specified test. + * + * @param jobID The unique identifier of the test + * @param orderIndex The record number to begin returning results from + * @param limit The maximum number of results to return + * @return A {@link PerformanceMetrics} object + * @throws IOException when the request fails + */ + public PerformanceMetrics getTestHistory(String jobID, int orderIndex, Integer limit) throws IOException { + Map params = new HashMap<>(); + params.put("order_index", orderIndex); + if (limit != null) { + params.put("limit", limit); + } + + return deserializeJSONObject( + requestWithQueryParameters(getBaseEndpoint(jobID) + "history/", HttpMethod.GET, params), PerformanceMetrics.class); + } + + /** + * Returns the starting and ending job counts in the current regime for each of the specified metrics. + * + * @param jobID The unique identifier of the test + * @param metricNames The metrics to return regime information for + * @param orderIndex The record number to limit results to + * @return A {@link Map} of metric name to a {@link List} of {@link Regime} objects + * @throws IOException when the request fails + */ + public Map> getMetricRegimes(String jobID, String[] metricNames, int orderIndex) + throws IOException { + return getMetricRegimes(jobID, metricNames, orderIndex, null); + } + + /** + * Returns the starting and ending job counts in the current regime for each of the specified metrics. + * + * @param jobID The unique identifier of the test + * @param metricNames The metrics to return regime information for + * @param orderIndex The record number to limit results to + * @param includeBaseline Whether to include baseline values + * @return A {@link Map} of metric name to a {@link List} of {@link Regime} objects + * @throws IOException when the request fails + */ + public Map> getMetricRegimes( + String jobID, String[] metricNames, int orderIndex, Boolean includeBaseline) throws IOException { + Map params = new HashMap<>(); + params.put("metric_names", metricNames); + params.put("order_index", orderIndex); + if (includeBaseline != null) { + params.put("include_baseline", includeBaseline); + } + + Type type = TypeToken.getParameterized( + Map.class, String.class, TypeToken.getParameterized(List.class, Regime.class).getType()).getType(); + return deserializeJSON( + requestWithQueryParameters(getBaseEndpoint(jobID) + "regimes/", HttpMethod.GET, params), type); + } + + /** + * Confirms that the new regime values for the specified test are acceptable. + * + * @param jobID The unique identifier of the test + * @param orderIndex The record number to limit results to + * @return {@link Response} + * @throws IOException when the request fails + */ + public Response acknowledgeRegimes(String jobID, int orderIndex) throws IOException { + Map params = new HashMap<>(); + params.put("order_index", orderIndex); + + return requestWithQueryParameters(getBaseEndpoint(jobID) + "regimes/acknowledge/", HttpMethod.POST, params); + } + + /** The base endpoint of the Performance endpoint APIs. */ + protected String getBaseEndpoint() { + return super.getBaseEndpoint() + "v2/performance/metrics/"; + } + + /** The base endpoint of the Performance endpoint APIs for a specific test. */ + protected String getBaseEndpoint(String jobID) { + return getBaseEndpoint() + jobID + "/"; + } +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/Baseline.java b/src/main/java/com/saucelabs/saucerest/model/performance/Baseline.java new file mode 100644 index 00000000..4397f9dc --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/Baseline.java @@ -0,0 +1,10 @@ +package com.saucelabs.saucerest.model.performance; + +import java.util.List; + +public class Baseline { + public Double upperBoundary; + public Double lowerBoundary; + public Double baseline; + public List values; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/BaselineParameter.java b/src/main/java/com/saucelabs/saucerest/model/performance/BaselineParameter.java new file mode 100644 index 00000000..6eda20dd --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/BaselineParameter.java @@ -0,0 +1,66 @@ +package com.saucelabs.saucerest.model.performance; + +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +/** Query parameters for {@code GET /v2/performance/metrics/{job_id}/baseline/}. */ +public class BaselineParameter { + private final String[] metricNames; + private final int orderIndex; + private final Integer regimeStart; + private final Integer regimeEnd; + + private BaselineParameter(Builder builder) { + metricNames = builder.metricNames; + orderIndex = builder.orderIndex; + regimeStart = builder.regimeStart; + regimeEnd = builder.regimeEnd; + } + + public Map toMap() { + Map parameters = new HashMap<>(); + + Stream.of( + new AbstractMap.SimpleEntry<>("metric_names", metricNames), + new AbstractMap.SimpleEntry<>("order_index", orderIndex), + new AbstractMap.SimpleEntry<>("regime_start", regimeStart), + new AbstractMap.SimpleEntry<>("regime_end", regimeEnd) + ) + .filter(e -> e.getValue() != null) + .forEach(e -> parameters.put(e.getKey(), e.getValue())); + + return parameters; + } + + public static final class Builder { + private final String[] metricNames; + private final int orderIndex; + private Integer regimeStart; + private Integer regimeEnd; + + /** + * @param metricNames The metrics to return baseline information for (required). + * @param orderIndex The record number to begin returning results from (required). + */ + public Builder(String[] metricNames, int orderIndex) { + this.metricNames = metricNames; + this.orderIndex = orderIndex; + } + + public Builder setRegimeStart(Integer val) { + regimeStart = val; + return this; + } + + public Builder setRegimeEnd(Integer val) { + regimeEnd = val; + return this; + } + + public BaselineParameter build() { + return new BaselineParameter(this); + } + } +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/BaselineResetStatus.java b/src/main/java/com/saucelabs/saucerest/model/performance/BaselineResetStatus.java new file mode 100644 index 00000000..9cdd3167 --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/BaselineResetStatus.java @@ -0,0 +1,5 @@ +package com.saucelabs.saucerest.model.performance; + +public class BaselineResetStatus { + public Boolean result; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/BaselineValue.java b/src/main/java/com/saucelabs/saucerest/model/performance/BaselineValue.java new file mode 100644 index 00000000..6f0b5260 --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/BaselineValue.java @@ -0,0 +1,7 @@ +package com.saucelabs.saucerest.model.performance; + +public class BaselineValue { + public Double realValue; + public String datetime; + public String jobId; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/DiscardedTests.java b/src/main/java/com/saucelabs/saucerest/model/performance/DiscardedTests.java new file mode 100644 index 00000000..4adfdfea --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/DiscardedTests.java @@ -0,0 +1,7 @@ +package com.saucelabs.saucerest.model.performance; + +import java.util.List; + +public class DiscardedTests { + public List jobIds; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/GetPerformanceMetricsParameter.java b/src/main/java/com/saucelabs/saucerest/model/performance/GetPerformanceMetricsParameter.java new file mode 100644 index 00000000..5333568b --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/GetPerformanceMetricsParameter.java @@ -0,0 +1,80 @@ +package com.saucelabs.saucerest.model.performance; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +/** Query parameters for {@code GET /v2/performance/metrics/}. */ +public class GetPerformanceMetricsParameter { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + private final String pageUrl; + private final String[] metricNames; + private final String startDate; + private final String endDate; + + private GetPerformanceMetricsParameter(Builder builder) { + pageUrl = builder.pageUrl; + metricNames = builder.metricNames; + startDate = builder.startDate; + endDate = builder.endDate; + } + + public Map toMap() { + Map parameters = new HashMap<>(); + + Stream.of( + new AbstractMap.SimpleEntry<>("page_url", pageUrl), + new AbstractMap.SimpleEntry<>("metric_names", metricNames), + new AbstractMap.SimpleEntry<>("start_date", startDate), + new AbstractMap.SimpleEntry<>("end_date", endDate) + ) + .filter(e -> e.getValue() != null) + .forEach(e -> parameters.put(e.getKey(), e.getValue())); + + return parameters; + } + + public static final class Builder { + private String pageUrl; + private String[] metricNames; + private String startDate; + private String endDate; + + public Builder setPageUrl(String val) { + pageUrl = val; + return this; + } + + public Builder setMetricNames(String[] val) { + metricNames = val; + return this; + } + + public Builder setStartDate(LocalDateTime val) { + startDate = toUtcString(val); + return this; + } + + public Builder setEndDate(LocalDateTime val) { + endDate = toUtcString(val); + return this; + } + + private static String toUtcString(LocalDateTime val) { + ZoneOffset offset = ZoneId.systemDefault().getRules().getOffset(val); + LocalDateTime utcDateTime = val.minusSeconds(offset.getTotalSeconds()); + + return utcDateTime.format(FORMATTER); + } + + public GetPerformanceMetricsParameter build() { + return new GetPerformanceMetricsParameter(this); + } + } +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/Links.java b/src/main/java/com/saucelabs/saucerest/model/performance/Links.java new file mode 100644 index 00000000..22dcebc3 --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/Links.java @@ -0,0 +1,6 @@ +package com.saucelabs.saucerest.model.performance; + +public class Links { + public String next; + public String previous; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/MetricAssertion.java b/src/main/java/com/saucelabs/saucerest/model/performance/MetricAssertion.java new file mode 100644 index 00000000..750aec71 --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/MetricAssertion.java @@ -0,0 +1,12 @@ +package com.saucelabs.saucerest.model.performance; + +public class MetricAssertion { + public Double baseline; + public Double lowerBoundary; + public Double upperBoundary; + public Double realValue; + public String jobId; + public String datetime; + public Integer orderIndex; + public Outlier outlier; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/MetricItem.java b/src/main/java/com/saucelabs/saucerest/model/performance/MetricItem.java new file mode 100644 index 00000000..68e33997 --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/MetricItem.java @@ -0,0 +1,16 @@ +package com.saucelabs.saucerest.model.performance; + +import java.util.Map; + +public class MetricItem { + public String jobId; + public String jobOwner; + public String jobNameHash; + public Map metricData; + public String pageUrl; + public Integer orderIndex; + public String jobCreationTime; + public String loadId; + public String loaderId; + public String error; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/Outlier.java b/src/main/java/com/saucelabs/saucerest/model/performance/Outlier.java new file mode 100644 index 00000000..0df29f31 --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/Outlier.java @@ -0,0 +1,6 @@ +package com.saucelabs.saucerest.model.performance; + +public class Outlier { + public Boolean status; + public String reason; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/PerformanceMetrics.java b/src/main/java/com/saucelabs/saucerest/model/performance/PerformanceMetrics.java new file mode 100644 index 00000000..12c8625c --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/PerformanceMetrics.java @@ -0,0 +1,8 @@ +package com.saucelabs.saucerest.model.performance; + +import java.util.List; + +public class PerformanceMetrics { + public List items; + public Links links; +} diff --git a/src/main/java/com/saucelabs/saucerest/model/performance/Regime.java b/src/main/java/com/saucelabs/saucerest/model/performance/Regime.java new file mode 100644 index 00000000..a4590e09 --- /dev/null +++ b/src/main/java/com/saucelabs/saucerest/model/performance/Regime.java @@ -0,0 +1,8 @@ +package com.saucelabs.saucerest.model.performance; + +public class Regime { + public Integer regimeStart; + public Integer regimeEnd; + public String baselineUrl; + public Boolean active; +} diff --git a/src/test/java/com/saucelabs/saucerest/integration/PerformanceEndpointTest.java b/src/test/java/com/saucelabs/saucerest/integration/PerformanceEndpointTest.java new file mode 100644 index 00000000..d1b18478 --- /dev/null +++ b/src/test/java/com/saucelabs/saucerest/integration/PerformanceEndpointTest.java @@ -0,0 +1,30 @@ +package com.saucelabs.saucerest.integration; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.saucelabs.saucerest.DataCenter; +import com.saucelabs.saucerest.SauceREST; +import com.saucelabs.saucerest.api.PerformanceEndpoint; +import com.saucelabs.saucerest.model.performance.GetPerformanceMetricsParameter; +import com.saucelabs.saucerest.model.performance.PerformanceMetrics; +import java.io.IOException; +import java.time.LocalDateTime; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class PerformanceEndpointTest { + @ParameterizedTest + @EnumSource(DataCenter.class) + public void getPerformanceResultsTest(DataCenter dataCenter) throws IOException { + SauceREST sauceREST = new SauceREST(dataCenter); + PerformanceEndpoint performanceEndpoint = sauceREST.getPerformanceEndpoint(); + + GetPerformanceMetricsParameter parameter = new GetPerformanceMetricsParameter.Builder() + .setStartDate(LocalDateTime.now().minusDays(30)) + .setEndDate(LocalDateTime.now()) + .build(); + PerformanceMetrics performanceMetrics = performanceEndpoint.getPerformanceResults(parameter); + + assertNotNull(performanceMetrics.items); + } +} diff --git a/src/test/java/com/saucelabs/saucerest/unit/BaselineParameterTest.java b/src/test/java/com/saucelabs/saucerest/unit/BaselineParameterTest.java new file mode 100644 index 00000000..bb8a21ee --- /dev/null +++ b/src/test/java/com/saucelabs/saucerest/unit/BaselineParameterTest.java @@ -0,0 +1,35 @@ +package com.saucelabs.saucerest.unit; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.saucelabs.saucerest.model.performance.BaselineParameter; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class BaselineParameterTest { + + @Test + public void testToMap() { + BaselineParameter params = new BaselineParameter.Builder(new String[] {"speedIndex"}, 0) + .setRegimeStart(0) + .setRegimeEnd(5) + .build(); + + Map map = params.toMap(); + assertArrayEquals(new String[] {"speedIndex"}, (String[]) map.get("metric_names")); + assertEquals(0, map.get("order_index")); + assertEquals(0, map.get("regime_start")); + assertEquals(5, map.get("regime_end")); + } + + @Test + public void testToMapWithOnlyRequiredValues() { + BaselineParameter params = new BaselineParameter.Builder(new String[] {"speedIndex"}, 2).build(); + + Map map = params.toMap(); + assertEquals(2, map.size()); + assertArrayEquals(new String[] {"speedIndex"}, (String[]) map.get("metric_names")); + assertEquals(2, map.get("order_index")); + } +} diff --git a/src/test/java/com/saucelabs/saucerest/unit/GetPerformanceMetricsParameterTest.java b/src/test/java/com/saucelabs/saucerest/unit/GetPerformanceMetricsParameterTest.java new file mode 100644 index 00000000..c55a5350 --- /dev/null +++ b/src/test/java/com/saucelabs/saucerest/unit/GetPerformanceMetricsParameterTest.java @@ -0,0 +1,37 @@ +package com.saucelabs.saucerest.unit; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.saucelabs.saucerest.model.performance.GetPerformanceMetricsParameter; +import java.time.LocalDateTime; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class GetPerformanceMetricsParameterTest { + + @Test + public void testToMap() { + GetPerformanceMetricsParameter params = new GetPerformanceMetricsParameter.Builder() + .setPageUrl("https://saucelabs.com/") + .setMetricNames(new String[] {"speedIndex", "load"}) + .setStartDate(LocalDateTime.of(2024, 1, 1, 0, 0, 0)) + .setEndDate(LocalDateTime.of(2024, 1, 2, 0, 0, 0)) + .build(); + + Map map = params.toMap(); + assertEquals("https://saucelabs.com/", map.get("page_url")); + assertArrayEquals(new String[] {"speedIndex", "load"}, (String[]) map.get("metric_names")); + assertTrue(((String) map.get("start_date")).matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); + assertTrue(((String) map.get("end_date")).matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); + } + + @Test + public void testToMapWithNoValues() { + GetPerformanceMetricsParameter params = new GetPerformanceMetricsParameter.Builder().build(); + + Map map = params.toMap(); + assertTrue(map.isEmpty()); + } +}