package com.linkedin.thirdeye.client.timeseries;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.joda.time.DateTime;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.linkedin.thirdeye.api.DimensionKey;
import com.linkedin.thirdeye.api.MetricSchema;
import com.linkedin.thirdeye.api.MetricTimeSeries;
import com.linkedin.thirdeye.api.MetricType;
import com.linkedin.thirdeye.client.MetricFunction;
import com.linkedin.thirdeye.client.timeseries.TimeSeriesRow.Builder;
import com.linkedin.thirdeye.client.timeseries.TimeSeriesRow.TimeSeriesMetric;
import com.linkedin.thirdeye.constant.MetricAggFunction;
public class TestTimeSeriesResponseUtils {
@Test(dataProvider = "toMapProvider")
public void toMap(String testName, TimeSeriesResponse response, List<String> schemaDimensions,
Map<DimensionKey, MetricTimeSeries> expected) {
Map<DimensionKey, MetricTimeSeries> actual = TimeSeriesResponseConverter.toMap(response, schemaDimensions);
Assert.assertEquals(actual, expected);
}
@DataProvider(name = "toMapProvider")
public Object[][] toMapProvider() {
Object[] noGroupByArgsOneMetricPerRow =
createMapProviderArgs("noGroupByArgsOneMetricPerRow", false, false); // no dimension
// grouping, each
// MetricFunction
// appears on a
// separate row
// from others in
// the same time +
// dimension key
Object[] noGroupByArgsAllMetricsInRow =
createMapProviderArgs("noGroupByArgsAllMetricsInRow", false, true); // no dimension
// grouping, metric
// functions for the
// same time +
// dimension key
// will appear in
// the same row.
Object[] dimensionGroupByArgsOneMetricPerRow =
createMapProviderArgs("dimensionGroupByArgsOneMetricPerRow", true, false); // dimension
// grouping,
// each
// metric function appears
// on a separate row from
// others in the same time +
// dimension key.
Object[] dimensionGroupByWithAllMetricsInRow =
createMapProviderArgs("dimensionGroupByWithAllMetricsInRow", true, true); // dimension
// grouping,
// metric
// functions
// for the
// same time +
// dimension
// key appear
// in one row.
return new Object[][] {
noGroupByArgsOneMetricPerRow, noGroupByArgsAllMetricsInRow,
dimensionGroupByArgsOneMetricPerRow, dimensionGroupByWithAllMetricsInRow
};
}
/**
* return type: String testName, TimeSeriesResponse, List<String> dimensions, Map<DimensionKey,
* MetricTimeSeries>.
*/
public Object[] createMapProviderArgs(String testName, boolean groupByDimension,
boolean groupTimeSeriesMetricsIntoRow) {
List<String> dimensions = Arrays.asList("dim1", "dim2", "dim3", "all");
List<String> dimensionValueSuffixes = Arrays.asList("_a", "_b", "_c"); // appended to each
// dimension
List<MetricFunction> metricFunctions = createSumFunctions("m1", "m2", "m3");
ConversionDataGenerator dataGenerator =
new ConversionDataGenerator(dimensions, metricFunctions);
for (long hoursSinceEpoch = 0; hoursSinceEpoch < 50; hoursSinceEpoch++) {
DateTime start = new DateTime(hoursSinceEpoch);
DateTime end = start.plusHours(1);
for (String dimension : (groupByDimension ? dimensions
: Collections.<String> singleton("all"))) {
for (String dimensionValueSuffix : (groupByDimension ? dimensionValueSuffixes
: Collections.<String> singleton("all"))) {
String dimensionValue;
if (groupByDimension) {
dimensionValue = dimension + dimensionValueSuffix;
} else {
dimensionValue = "all";
}
List<TimeSeriesMetric> timeSeriesMetrics = new ArrayList<>();
for (MetricFunction metricFunction : metricFunctions) {
Double value = (double) (Objects.hash(start, end, dimension, dimensionValue,
metricFunction.toString()) % 1000); // doesn't matter, the test is that values are
// consistent between data
// structures.
TimeSeriesMetric timeSeriesMetric =
new TimeSeriesMetric(metricFunction.getMetricName(), value);
timeSeriesMetrics.add(timeSeriesMetric);
}
if (groupTimeSeriesMetricsIntoRow) {
// add them all at once
dataGenerator.addEntry(Arrays.asList(dimension), Arrays.asList(dimensionValue), start, end,
timeSeriesMetrics.toArray(new TimeSeriesMetric[timeSeriesMetrics.size()]));
} else {
// add them individually (one metric per row)
for (TimeSeriesMetric timeSeriesMetric : timeSeriesMetrics) {
dataGenerator.addEntry(Arrays.asList(dimension), Arrays.asList(dimensionValue), start, end, timeSeriesMetric);
}
}
}
}
}
Object[] dimensionGroupByArgs = new Object[] {
testName, dataGenerator.getResponse(), dimensions, dataGenerator.getMap()
};
return dimensionGroupByArgs;
}
private List<MetricFunction> createSumFunctions(String... metricNames) {
List<MetricFunction> result = new ArrayList<>();
for (String metricName : metricNames) {
result.add(new MetricFunction(MetricAggFunction.SUM, metricName, 0L, "dataset", null, null));
}
return result;
}
/**
* Helper class to test converting from TimeSeriesResponse to Map<DK,MTS>. This class will
* simultaneously build corresponding TimeSeriesRow objects and populate a Map<DimensionKey,
* MetricTimeSeries> with the provided addEntry method.
*/
private class ConversionDataGenerator {
private final List<TimeSeriesRow> timeSeriesRows = new ArrayList<>();
private final Map<DimensionKey, MetricTimeSeries> map = new HashMap<>();
private final List<String> dimensions;
private final List<MetricFunction> metricFunctions;
private final List<String> metricNames;
private final MetricSchema metricSchema;
ConversionDataGenerator(List<String> dimensions, List<MetricFunction> metricFunctions) {
this.dimensions = dimensions;
this.metricFunctions = metricFunctions;
List<String> metricNames = new ArrayList<>();
for (MetricFunction metricFunction : metricFunctions) {
metricNames.add(metricFunction.getMetricName());
}
this.metricNames = metricNames;
this.metricSchema =
new MetricSchema(metricNames, Collections.nCopies(metricNames.size(), MetricType.DOUBLE));
}
void addEntry(List<String> dimensionNames, List<String> dimensionValues, DateTime start, DateTime end,
TimeSeriesMetric... timeSeriesMetrics) {
validateArgs(dimensionNames, dimensionValues, start, end, timeSeriesMetrics);
timeSeriesRows.add(createRow(dimensionNames, dimensionValues, start, end, timeSeriesMetrics));
String[] dimensionKeyArr = new String[dimensions.size()];
Arrays.fill(dimensionKeyArr, "*");
if (dimensionNames != null && dimensionNames.size() != 0) {
for (int idx = 0; idx < dimensionNames.size(); ++idx) {
dimensionKeyArr[dimensions.indexOf(dimensionNames.get(idx))] = dimensionValues.get(idx);
}
}
DimensionKey dimensionKey = new DimensionKey(dimensionKeyArr);
if (!map.containsKey(dimensionKey)) {
map.put(dimensionKey, new MetricTimeSeries(metricSchema));
}
incrementMetricData(map.get(dimensionKey), start, end, timeSeriesMetrics);
}
private void validateArgs(List<String> dimensionNames, List<String> dimensionValue, DateTime start,
DateTime end, TimeSeriesMetric[] timeSeriesMetrics) {
if (dimensionNames != null && dimensionNames.size() != 0) {
for (String dimensionName : dimensionNames) {
Assert.assertTrue(dimensions.contains(dimensionName));
}
}
for (TimeSeriesMetric metric : timeSeriesMetrics) {
Assert.assertTrue(metricNames.contains(metric.getMetricName()));
}
}
private TimeSeriesRow createRow(List<String> dimensionNames, List<String> dimensionValues, DateTime start,
DateTime end, TimeSeriesMetric... timeSeriesMetrics) {
Builder builder = new TimeSeriesRow.Builder();
builder.setDimensionNames(dimensionNames);
builder.setDimensionValues(dimensionValues);
builder.setStart(start);
builder.setEnd(end);
builder.addMetrics(timeSeriesMetrics);
return builder.build();
}
private void incrementMetricData(MetricTimeSeries metricTimeSeries, DateTime start,
DateTime end, TimeSeriesMetric... timeSeriesMetrics) {
long timeWindow = start.getMillis();
for (TimeSeriesMetric metric : timeSeriesMetrics) {
metricTimeSeries.increment(timeWindow, metric.getMetricName(), metric.getValue());
}
}
private TimeSeriesResponse getResponse() {
return new TimeSeriesResponse(timeSeriesRows);
}
private Map<DimensionKey, MetricTimeSeries> getMap() {
return map;
}
}
}