package com.spotify.heroic.grammar; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.spotify.heroic.filter.AndFilter; import com.spotify.heroic.filter.FalseFilter; import com.spotify.heroic.filter.Filter; import com.spotify.heroic.filter.HasTagFilter; import com.spotify.heroic.filter.MatchKeyFilter; import com.spotify.heroic.filter.MatchTagFilter; import com.spotify.heroic.filter.NotFilter; import com.spotify.heroic.filter.OrFilter; import com.spotify.heroic.filter.RegexFilter; import com.spotify.heroic.filter.StartsWithFilter; import com.spotify.heroic.filter.TrueFilter; import com.spotify.heroic.metric.MetricType; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.tuple.Pair; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import static com.spotify.heroic.grammar.Expression.duration; import static com.spotify.heroic.grammar.Expression.empty; import static com.spotify.heroic.grammar.Expression.function; import static com.spotify.heroic.grammar.Expression.integer; import static com.spotify.heroic.grammar.Expression.let; import static com.spotify.heroic.grammar.Expression.list; import static com.spotify.heroic.grammar.Expression.range; import static com.spotify.heroic.grammar.Expression.reference; import static com.spotify.heroic.grammar.Expression.string; import static org.junit.Assert.assertEquals; @RunWith(MockitoJUnitRunner.class) public class QueryParserTest { @Rule public final ExpectedException exception = ExpectedException.none(); private final CoreQueryParser parser = new CoreQueryParser(); @Test public void testString() { assertEquals(string(cols(0, 2), "foo"), parseExpression("foo")); assertEquals(string(cols(0, 4), "foo"), parseExpression("\"foo\"")); assertEquals(string(cols(0, 2), "\u4040"), parseExpression("\"\u4040\"")); assertEquals(string(cols(0, 1), "1a"), parseExpression("1a")); // test all possible escape sequences assertEquals(string(cols(0, 17), "\b\t\n\f\r\"\'\\"), parseExpression("\"\\b\\t\\n\\f\\r\\\"\\'\\\\\"")); } @Test public void testStringEscapes() { final List<Pair<String, String>> tests = ImmutableList.of(Pair.of("b", "\b"), Pair.of("t", "\t"), Pair.of("n", "\n"), Pair.of("f", "\f"), Pair.of("r", "\r"), Pair.of("\"", "\""), Pair.of("\'", "'"), Pair.of("\\", "\\")); for (final Pair<String, String> test : tests) { final String input = "\"\\" + test.getLeft() + "\""; final String ref = test.getRight(); assertEquals(string(cols(0, input.length() - 1), ref), parseExpression(input)); } } @Test public void testStringUnicodeEscape() { for (char c = 0; c < Character.MAX_VALUE; c++) { final String input = "\"" + String.format("\\u%04x", (int) c) + "\""; assertEquals(string(cols(0, input.length() - 1), Character.toString(c)), parseExpression(input)); } } @Test public void testList() { final Expression ref = list(cols(0, 8), integer(cols(1, 1), 1), integer(cols(4, 4), 2), integer(cols(7, 7), 3)); assertEquals(ref, parseExpression("[1, 2, 3]")); assertEquals(ref, parseExpression("{1, 2, 3}")); } @Test public void testFunction1() { assertEquals(function(cols(0, 11), "average", duration(cols(8, 10), TimeUnit.HOURS, 30)), parseFunction("average(30H)")); assertEquals(function(cols(0, 7), "sum", duration(cols(4, 6), TimeUnit.HOURS, 30)), parseFunction("sum(30H)")); } @Test public void testFunction2a() { final FunctionExpression chain = function(cols(0, 43), "chain", function(cols(6, 32), "group", list(cols(12, 17), string(cols(13, 16), "host")), function(cols(20, 31), "average", duration(cols(28, 30), TimeUnit.HOURS, 30))), function(cols(35, 42), "sum", duration(cols(39, 41), TimeUnit.HOURS, 30))); assertEquals(chain, parseFunction("chain(group([host], average(30H)), sum(30H))")); } @Test public void testFunction2b() { final FunctionExpression chain = function(cols(0, 30), "chain", function(cols(0, 19), "group", string(cols(16, 19), "host"), function(cols(0, 11), "average", duration(cols(8, 10), TimeUnit.HOURS, 30))), function(cols(23, 30), "sum", duration(cols(27, 29), TimeUnit.HOURS, 30))); assertEquals(chain, parseFunction("average(30H) by host | sum(30H)")); } @Test public void testFunction3() { final FunctionExpression arg1 = function(cols(0, 14), "group", string(cols(11, 14), "host"), function(cols(0, 6), "average")); final FunctionExpression arg2 = function(cols(18, 28), "group", string(cols(25, 28), "site"), function(cols(18, 20), "sum")); assertEquals(function(cols(0, 28), "chain", arg1, arg2), parseFunction("average by host | sum by site")); } @Test public void testFunction4() { final FunctionExpression parsed = parseFunction("(average by host | sum) by site"); final Expression arg1 = string(cols(27, 30), "site"); final Expression arg2 = function(cols(1, 21), "chain", function(cols(1, 15), "group", string(cols(12, 15), "host"), function(cols(1, 7), "average")), string(cols(19, 21), "sum")); // test grouping assertEquals(function(cols(0, 30), "group", arg1, arg2), parsed); } @Test public void testByAll() { final FunctionExpression reference = function(cols(0, 11), "group", empty(cols(0, 11)), function(cols(0, 6), "average")); assertEquals(reference, parseExpression("average by *")); } @Test public void testFilter() { final MatchTagFilter f1 = new MatchTagFilter("a", "a"); final MatchTagFilter f2 = new MatchTagFilter("a", "b"); final MatchTagFilter f3 = new MatchTagFilter("a", "c"); final MatchTagFilter f4 = new MatchTagFilter("b", "b"); assertEquals(f1, parseFilter("a = a")); assertEquals(OrFilter.of(f1, f2, f3), parseFilter("a in [a, b, c]")); assertEquals(AndFilter.of(f1, f4), parseFilter("a = a and b = b")); assertEquals(OrFilter.of(f1, f4), parseFilter("a = a or b = b")); assertEquals(new RegexFilter("a", "b"), parseFilter("a ~ b")); assertEquals(new NotFilter(f1), parseFilter("a != a")); assertEquals(new NotFilter(f1), parseFilter("!(a = a)")); assertEquals(new StartsWithFilter("a", "a"), parseFilter("a ^ a")); assertEquals(new HasTagFilter("a"), parseFilter("+a")); assertEquals(new MatchKeyFilter("a"), parseFilter("$key = a")); assertEquals(TrueFilter.get(), parseFilter("true")); assertEquals(FalseFilter.get(), parseFilter("false")); } @Test public void testUnterminatedString() { exception.expect(ParseException.class); exception.expectMessage("unterminated string"); parser.parseExpression("\"open"); } @Test public void testInvalidSelect() { exception.expect(ParseException.class); parser.parseExpression("%1"); } @Test public void testInvalidGrammar() { exception.expect(ParseException.class); exception.expectMessage("unexpected token: ~"); parser.parseQuery("~ from points"); } @Test public void testParseDateTime() { final Expression.Scope scope = new DefaultScope(0L); final Expression e = parser.parseExpression("{2014-01-01 00:00:00.000} + {00:01}").eval(scope); final InstantExpression expected = new InstantExpression(cols(0, 34), Instant.parse("2014-01-01T00:01:00.000Z")); assertEquals(expected, e); } @Test public void testArithmetics() { final DefaultScope scope = new DefaultScope(10000); // numbers assertEquals(3L, parseExpression("1 + 2 + 3 - 3").eval(scope).cast(IntegerExpression.class).getValue()); // two strings assertEquals("foobar", parseExpression("foo + bar").eval(scope).cast(StringExpression.class).getString()); // two lists assertEquals(list(cols(0, 12), string(cols(1, 3), "foo"), string(cols(9, 11), "bar")), parseExpression("[foo] + [bar]").eval(scope).cast(ListExpression.class)); // durations assertEquals(duration(cols(0, 6), TimeUnit.MINUTES, 55), parseExpression("1H - 5m").eval(scope)); assertEquals(duration(cols(0, 6), TimeUnit.HOURS, 7), parseExpression("3H + 4H").eval(scope)); assertEquals(duration(cols(0, 8), TimeUnit.MINUTES, 59), parseExpression("119m - 1H").eval(scope)); assertEquals(duration(cols(0, 17), TimeUnit.MINUTES, 60 * 11), parseExpression("1H + 1m - 1m + 10H").eval(scope)); } @Test public void testFrom() { final DefaultScope scope = new DefaultScope(10000); checkFrom(MetricType.POINT, Optional.empty(), parseFrom("from points")); checkFrom(MetricType.EVENT, Optional.empty(), parseFrom("from events")); final Optional<Expression> r1 = Optional.of(range(cols(11, 24), integer(col(12), 0), integer(cols(15, 23), 1000))); // absolute checkFrom(MetricType.POINT, r1, parseFrom("from points(0, 400 + 600)").eval(scope)); final Optional<Expression> r2 = Optional.of( range(cols(11, 18), integer(cols(11, 18), 9000), integer(cols(11, 18), 10000))); // relative checkFrom(MetricType.POINT, r2, parseFrom("from points(1000ms)").eval(scope)); } @Test public void testLetStatement() { final Expression expected = let(cols(0, 9), reference(cols(4, 5), "a"), query(col(9)).build()); assertEquals(ImmutableList.of(expected), parser.parse("let $a = *")); } Context col(int col) { return new Context(0, col, 0, col); } Context cols(int start, int end) { return new Context(0, start, 0, end); } Expression parseExpression(String input) { return parser.parseExpression(input); } FunctionExpression parseFunction(String input) { return parseExpression(input).visit(new Expression.Visitor<FunctionExpression>() { @Override public FunctionExpression visitFunction(final FunctionExpression e) { return e; } }); } Filter parseFilter(final String input) { return parser.parseFilter(input); } void checkFrom( MetricType source, Optional<Expression> range, CoreQueryParser.FromDSL result ) { assertEquals(source, result.getSource()); assertEquals(range, result.getRange()); } CoreQueryParser.FromDSL parseFrom(String input) { return parser.parseFrom(input); } static QueryBuilder query(Context ctx) { return new QueryBuilder(ctx); } @RequiredArgsConstructor static class QueryBuilder { private final Context context; private Optional<Expression> select = Optional.empty(); private Optional<MetricType> source = Optional.empty(); private Optional<RangeExpression> range = Optional.empty(); private Optional<Filter> filter = Optional.empty(); private Map<String, Expression> with = ImmutableMap.of(); private Map<String, Expression> as = ImmutableMap.of(); public QueryBuilder select(final Expression select) { this.select = Optional.of(select); return this; } public QueryBuilder source(final MetricType source) { this.source = Optional.of(source); return this; } public QueryBuilder range(final RangeExpression range) { this.range = Optional.of(range); return this; } public QueryBuilder filter(final Filter filter) { this.filter = Optional.of(filter); return this; } public QueryBuilder with(final Map<String, Expression> with) { this.with = with; return this; } public QueryBuilder as(final Map<String, Expression> as) { this.as = as; return this; } public QueryExpression build() { return new QueryExpression(context, select, source, range, filter, with, as); } } }