package com.netflix.suro.routing.filter.parser;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.netflix.suro.routing.filter.MessageFilter;
import com.netflix.suro.routing.filter.MessageFilterCompiler;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static com.netflix.suro.routing.filter.parser.FilterPredicate.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(Parameterized.class)
public class SimpleMessageFilterParsingTest {
private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss:SSS";
private final static DateTime now = DateTime.now();
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(
new Object[][] {
{0, stringComp, "root", "!=", "value", "!value", "value"},
{1, stringComp, "//a/b/c", "!=", "value", "!value", "value"},
{2, stringComp, "root", "=", "value", "value", "!value"},
{3, stringComp, "//a/b/c", "=", "value", "value", "!value"},
{4, numberComp, "root", "=", -5, -5, 4},
{5, numberComp, "//a/b/c/f", "!=", 10.11, 10.12, 10.11},
{6, numberComp, "root", ">", 5, 9, 5},
{7, numberComp, "root", ">", 5, 9, 4},
{8, numberComp, "//a/b/c/f", ">=", 134.12, 134.12, 133},
{9, numberComp, "root", ">", 134.12, 150, -100},
{10, numberComp, "//a/b/c/f", ">=", 134.12, 140, 100},
{11, numberComp, "root", "<", 5, 4, 5},
{12, numberComp, "root", "<", 5, 4, 9},
{13, numberComp, "//a/b/c/f", "<=", 134.12, 134.12, 190},
{14, numberComp, "//a/b/c/f", "<=", 134.12, 100, 190},
{15, between, "//a/b/cd", null, new Object[]{6, 10}, 6, 15},
{16, between, "//a/b/cd", null, new Object[]{6, 10}, 8, 15},
{17, between, "//a/b/cd", null, new Object[]{6, 10}, 8, 10},
{18, between, "//a/b/cd", null, new Object[]{6, 10}, 8, 15},
{19, between, "//a/b/cd", null, new Object[]{6, 10}, 8, 5},
{20,
between,
"//a/b/ef",
null,
new Object[]{
timeMillisNHoursAway(-3, TIME_FORMAT),
timeMillisNHoursAway(3, TIME_FORMAT)
},
nHoursAway(0),
nHoursAway(-4)
},
{21,
between,
"//a/b/ef",
null,
new Object[]{
timeMillisNHoursAway(-3, TIME_FORMAT),
timeMillisNHoursAway(3, TIME_FORMAT)
},
nHoursAway(-3),
nHoursAway(3)
},
{22,
between,
"//a/b/ef",
null,
new Object[]{
timeMillisNHoursAway(-3, TIME_FORMAT),
timeMillisNHoursAway(3, TIME_FORMAT)
},
nHoursAway(2),
nHoursAway(4)
},
{23,
between,
"//a/b/ef",
null,
new Object[]{
timeStringNHoursAway(-3, TIME_FORMAT),
timeStringNHoursAway(3, TIME_FORMAT)
},
nHoursAway(0, TIME_FORMAT),
nHoursAway(-4, TIME_FORMAT)
},
{24,
between,
"//a/b/ef",
null,
new Object[]{
timeStringNHoursAway(-3, TIME_FORMAT),
timeStringNHoursAway(3, TIME_FORMAT)
},
nHoursAway(-3, TIME_FORMAT),
nHoursAway(3, TIME_FORMAT)
},
{25,
between,
"//a/b/ef",
null,
new Object[]{
timeStringNHoursAway(-3, TIME_FORMAT),
timeStringNHoursAway(3, TIME_FORMAT)
},
nHoursAway(2, TIME_FORMAT),
nHoursAway(4, TIME_FORMAT)
},
{26,
timeComp,
"//a/b/ef",
">",
timeStringNHoursAway(-3, TIME_FORMAT),
nHoursAway(0, TIME_FORMAT),
nHoursAway(-4, TIME_FORMAT)
},
{27,
timeComp,
"//a/b/ef",
"<",
timeStringNHoursAway(3, TIME_FORMAT),
nHoursAway(0, TIME_FORMAT),
nHoursAway(4, TIME_FORMAT)
},
{28,
timeComp,
"//a/b/ef",
"=",
timeStringNHoursAway(3, TIME_FORMAT),
nHoursAway(3, TIME_FORMAT),
nHoursAway(4, TIME_FORMAT)
},
{29,
timeComp,
"//a/b/ef",
"!=",
timeStringNHoursAway(3, TIME_FORMAT),
nHoursAway(4, TIME_FORMAT),
nHoursAway(3, TIME_FORMAT)
},
{30,
timeComp,
"//a/b/ef",
"<",
timeMillisNHoursAway(3, TIME_FORMAT),
nHoursAway(0),
nHoursAway(4)
},
{31,
timeComp,
"//a/b/ef",
">",
timeMillisNHoursAway(-3, TIME_FORMAT),
nHoursAway(0),
nHoursAway(-4)
},
{32,
timeComp,
"//a/b/ef",
"<",
timeMillisNHoursAway(3, TIME_FORMAT),
nHoursAway(0),
nHoursAway(4)
},
{33,
timeComp,
"//a/b/ef",
"=",
timeMillisNHoursAway(3, TIME_FORMAT),
nHoursAway(3),
nHoursAway(4)
},
{34,
timeComp,
"//a/b/ef",
"!=",
timeMillisNHoursAway(3, TIME_FORMAT),
nHoursAway(0),
nHoursAway(3)
},
{35, isNull, "//a/b/c", null, null, null, "not null value"},
{36, regex, "//a/b/c", null, "[0-9]+", "1234234", "aaaaaaaaaa"},
{37, existsRight, "//a/b/c", "some value", null, "value", "whaever"},
{38, existsLeft, "//a/b/c", "some value", null, "value", "whaever"},
{39, trueValue, "//a/b/c", null, null, null, null},
{40, falseValue, "//a/b", null, null, null, null},
{41,
inPred,
"//a/b/c",
null,
new Object[]{
wrap("abc"),
wrap("def"),
wrap("foo"),
wrap("123"),
wrap("end")
},
"foo",
"bar"
},
{41,
inPred,
"//a/b/c",
null,
new Object[]{
123,
455,
1234.23,
-1324,
23.0
},
23.0,
100
}
}
);
}
private static String wrap(String value){
return String.format("\"%s\"", value);
}
private static long nHoursAway(int n) {
return now.plusHours(n).getMillis();
}
private static String nHoursAway(int n, String format) {
return DateTimeFormat.forPattern(format).print(nHoursAway(n));
}
// Creates a time-millis that is n hours away from now
private static String timeMillisNHoursAway(int n, String format) {
return String.format(
"time-millis(\"%s\", \"%s\")",
format,
nHoursAway(n, format));
}
private static String timeStringNHoursAway(int n, String format) {
return String.format(
"time-string(\"%s\", \"%s\", \"%s\")",
format,
format,
nHoursAway(n, format));
}
@Before
public void setUp() throws Exception {
}
private int index;
private FilterPredicate predicate;
private String predicateString;
private Object expectedValue;
private Object unexpectedValue;
private String xpath;
public SimpleMessageFilterParsingTest(
int index,
FilterPredicate targetPredicate,
String xpath,
String operator,
Object value,
Object expected,
Object unexpected
) {
this.index = index;
this.predicate = targetPredicate;
this.xpath = xpath;
this.predicateString = targetPredicate.create(xpath, operator, value);
System.out.println(String.format("[%d] filter = %s", index, predicateString));
this.expectedValue = expected;
this.unexpectedValue = unexpected;
}
@After
public void tearDown() throws Exception {
}
@Test
public void testPredicate() throws Exception {
try{
MessageFilter filter = MessageFilterCompiler.compile(predicateString);
assertNotNull(filter);
if(predicate != falseValue){
testPositiveMatch(filter);
}
if(predicate == trueValue){
return;
}
if(predicate == existsLeft || predicate == existsRight) {
testNegativeExistsPredicate(filter);
} else {
testNegativeMatch(filter);
}
}catch(Exception e) {
System.err.println(String.format("Failed input for test data [%d]: %s", index, predicateString));
throw e;
}
}
private void testNegativeExistsPredicate(MessageFilter filter) {
Object unexpectedEvent = createEvent("nonpath", unexpectedValue);
assertEquals(
String.format("The object is %s, should not match the filter %s for data set [%d]. Translated filter: %s",
unexpectedEvent, predicateString, index, filter),
false,
filter.apply(unexpectedEvent));
}
private void testNegativeMatch(MessageFilter filter) {
Object unexpectedEvent = createEvent(xpath, unexpectedValue);
assertEquals(
String.format("The object is %s, should not match the filter %s for data set [%d]. Translated filter: %s",
unexpectedEvent, predicateString, index, filter),
false,
filter.apply(unexpectedEvent));
}
private void testPositiveMatch(MessageFilter filter) {
Object expectedEvent = createEvent(xpath, expectedValue);
assertEquals(
String.format("The object is %s, should match the filter %s for data set [%d]. Translated filter: %s ",
expectedEvent, predicateString, index, filter),
true,
filter.apply(expectedEvent));
}
private static Object createEvent(String path, Object value) {
List<String> steps = ImmutableList
.copyOf(Splitter.on('/')
.trimResults()
.omitEmptyStrings()
.split(path));
return createObjectWithPathAndValue(steps, value);
}
// Bypass JXPathContext#createPathAndValue() because it doesn't support context-dependent predicates
private static Map<String, ? extends Object> createObjectWithPathAndValue(List<String> path, Object value) {
if(path.isEmpty()) {
throw new IllegalArgumentException("There should be at least one step in the given path");
}
if(path.size() == 1) {
// We want to allow null values, so we don't use ImmutableMap.of() here.
Map<String, ? super Object> map = Maps.newHashMap();
map.put(path.get(0), value);
return map;
}
Map<String, ? extends Object> child = createObjectWithPathAndValue(path.subList(1, path.size()), value);
return ImmutableMap.of(path.get(0), child);
}
}