/**
* Copyright (C) 2014-2016 LinkedIn Corp. (pinot-core@linkedin.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.linkedin.pinot.util;
import com.linkedin.pinot.common.data.Schema;
import com.linkedin.pinot.common.response.broker.GroupByResult;
import com.linkedin.pinot.core.data.GenericRow;
import com.linkedin.pinot.core.data.readers.RecordReader;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.mutable.MutableLong;
import org.json.JSONArray;
import org.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
/**
* Various utilities for unit tests.
*
*/
public class TestUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(TestUtils.class);
private static final int testThreshold = 1000;
public static final double hllEstimationThreshold = 0.5;
public static final double digestEstimationThreshold = 0.1;
public static String getFileFromResourceUrl(@Nonnull URL resourceUrl) {
// For maven cross package use case, we need to extract the resource from jar to a temporary directory.
String resourceUrlStr = resourceUrl.toString();
if (resourceUrlStr.contains("jar!")) {
try {
String extension = resourceUrlStr.substring(resourceUrlStr.lastIndexOf('.'));
File tempFile = File.createTempFile("pinot-test-temp", extension);
String tempFilePath = tempFile.getAbsolutePath();
LOGGER.info("Extracting from " + resourceUrlStr + " to " + tempFilePath);
FileUtils.copyURLToFile(resourceUrl, tempFile);
return tempFilePath;
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
return resourceUrl.getFile();
}
}
/**
* assert estimation in error range
* @param estimate
* @param actual
*/
public static void assertApproximation(double estimate, double actual, double precision) {
estimate = Math.abs(estimate);
actual = Math.abs(actual);
if (estimate < testThreshold && actual < testThreshold) {
double errorDiff = Math.abs(actual - estimate);
LOGGER.debug("estimate: " + estimate + " actual: " + actual + " error (in difference): " + errorDiff);
LOGGER.debug("small value comparison ignored!");
//Assert.assertEquals(error < 3, true);
} else {
double errorRate = 1;
if (actual > 0) {
errorRate = Math.abs((actual - estimate) / actual);
}
LOGGER.debug("estimate: " + estimate + " actual: " + actual + " error (in rate): " + errorRate);
if (errorRate >= precision) {
System.out.println("Found it: " + actual + " " + estimate);
}
Assert.assertTrue(errorRate < precision);
}
}
public static void assertGroupByResultsApproximation(
List<GroupByResult> estimateValues, List<GroupByResult> actualValues, double precision) {
LOGGER.info("====== assertGroupByResultsApproximation ======");
// estimation should not affect number of groups formed
Assert.assertEquals(estimateValues.size(), actualValues.size());
Map<List<String>, Double> mapEstimate = new HashMap<>();
Map<List<String>, Double> mapActual = new HashMap<>();
for (GroupByResult gby: estimateValues) {
mapEstimate.put(gby.getGroup(), Double.parseDouble(gby.getValue().toString()));
}
for (GroupByResult gby: actualValues) {
mapActual.put(gby.getGroup(), Double.parseDouble(gby.getValue().toString()));
}
LOGGER.info("estimate: " + mapEstimate.keySet());
LOGGER.info("actual: " + mapActual.keySet());
int cnt = 0;
for (List<String> key: mapEstimate.keySet()) {
// Not strictly enforced, since in quantile, top 100 groups from accurate maybe not be top 100 from estimate
// Assert.assertEquals(mapActual.keySet().contains(key), true);
if (mapActual.keySet().contains(key)) {
assertApproximation(mapEstimate.get(key), mapActual.get(key), precision);
cnt += 1;
}
}
LOGGER.info("group overlap rate: " + (cnt+0.0)/mapEstimate.keySet().size());
}
public static void assertJSONArrayApproximation(JSONArray jsonArrayEstimate, JSONArray jsonArrayActual, double precision) {
LOGGER.info("====== assertJSONArrayApproximation ======");
try {
HashMap<String, Double> mapEstimate = genMapFromJSONArray(jsonArrayEstimate);
HashMap<String, Double> mapActual = genMapFromJSONArray(jsonArrayActual);
// estimation should not affect number of groups formed
Assert.assertEquals(mapEstimate.keySet().size(), mapActual.keySet().size());
LOGGER.info("estimate: " + mapEstimate.keySet());
LOGGER.info("actual: " + mapActual.keySet());
int cnt = 0;
for (String key: mapEstimate.keySet()) {
// Not strictly enforced, since in quantile, top 100 groups from accurate maybe not be top 100 from estimate
// Assert.assertEquals(mapActual.keySet().contains(key), true);
if (mapActual.keySet().contains(key)) {
assertApproximation(mapEstimate.get(key), mapActual.get(key), precision);
cnt += 1;
}
}
LOGGER.info("group overlap rate: " + (cnt+0.0)/mapEstimate.keySet().size());
} catch (JSONException e) {
e.printStackTrace();
}
}
private static HashMap<String, Double> genMapFromJSONArray(JSONArray array) throws JSONException {
HashMap<String, Double> map = new HashMap<String, Double>();
for (int i = 0; i < array.length(); ++i) {
map.put(array.getJSONObject(i).getJSONArray("group").getString(0),
array.getJSONObject(i).getDouble("value"));
}
return map;
}
/**
* Utility class for reading generic row records
*/
public static class GenericRowRecordReader implements RecordReader {
private final Schema _schema;
private final List<GenericRow> _data;
int counter = 0;
// Constructor for the class.
public GenericRowRecordReader(final Schema schema, final List<GenericRow> data) {
_schema = schema;
_data = data;
}
@Override
public void rewind()
throws Exception {
counter = 0;
}
@Override
public GenericRow next() {
return _data.get(counter++);
}
@Override
public GenericRow next(GenericRow row) {
return next();
}
@Override
public void init()
throws Exception {
}
@Override
public boolean hasNext() {
return counter < _data.size();
}
@Override
public Schema getSchema() {
return _schema;
}
@Override
public Map<String, MutableLong> getNullCountMap() {
return null;
}
@Override
public void close()
throws Exception {
}
}
}