blob: e7917ed9f1d76a53165ffd8ac4a74654fe8853e7 [file] [log] [blame]
// Copyright 2020 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.query2.engine;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryTaskFuture;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests of parser and pretty-printer. */
@RunWith(JUnit4.class)
public final class QueryParserTest {
private static class MockFunction implements QueryFunction {
private final String name;
private final int mandatoryArguments;
private final List<ArgumentType> arguments;
private MockFunction(String name, int mandatoryArguments, ArgumentType... arguments) {
this.name = name;
this.mandatoryArguments = mandatoryArguments;
this.arguments = ImmutableList.copyOf(arguments);
}
@Override
public String getName() {
return name;
}
@Override
public int getMandatoryArguments() {
return mandatoryArguments;
}
@Override
public List<ArgumentType> getArgumentTypes() {
return arguments;
}
@Override
public <T> QueryTaskFuture<Void> eval(
QueryEnvironment<T> env,
QueryExpressionContext<T> context,
QueryExpression expression,
List<Argument> args,
Callback<T> callback) {
throw new IllegalStateException();
}
}
private static QueryEnvironment<?> mockEnvironment() {
ImmutableList.Builder<QueryFunction> functions = ImmutableList.builder();
functions.addAll(QueryEnvironment.DEFAULT_QUERY_FUNCTIONS);
functions.add(new MockFunction("opt", 2,
ArgumentType.WORD, ArgumentType.WORD, ArgumentType.WORD));
QueryEnvironment<?> result = mock(QueryEnvironment.class);
when(result.getFunctions()).thenReturn(functions.build());
return result;
}
// Asserts that 'query' parses, and that when pretty-printed, yields 'query'.
private static String checkPrettyPrint(String query) throws Exception {
return checkPrettyPrint(query, query);
}
// Asserts that 'query' parses, and that when pretty-printed, yields
// 'expectedPrettyPrintOutput'.
private static String checkPrettyPrint(String expectedPrettyPrintOutput, String query)
throws Exception {
assertThat(QueryExpression.parse(query, mockEnvironment()).toString())
.isEqualTo(expectedPrettyPrintOutput);
return expectedPrettyPrintOutput;
}
public static void checkParseFails(String query, String expectedError) {
QueryException e =
assertThrows(QueryException.class, () -> QueryExpression.parse(query, mockEnvironment()));
assertThat(e).hasMessageThat().isEqualTo(expectedError);
}
@Test
public void testOptionalArguments() throws Exception {
checkPrettyPrint("opt('foo', 'bar')");
checkPrettyPrint("opt('foo', 'bar', 'qux')");
checkParseFails("opt('foo', 'bar', 'qux', 'zyc')", "syntax error at ', zyc )'");
checkParseFails("opt('foo')", "syntax error at ')'");
}
@Test
public void testTargetLiterals() throws Exception {
checkPrettyPrint("x");
checkPrettyPrint("//x");
checkPrettyPrint("//x:y");
checkPrettyPrint("x/...:all-targets");
checkPrettyPrint("\"set\""); // reserved word
checkPrettyPrint("\"\"");
}
@Test
public void checkParseErrors() {
checkParseFails("foo(a)", "syntax error at '( a )'");
checkParseFails("deps(", "premature end of input");
checkParseFails("deps(a, ", "premature end of input");
checkParseFails("deps(a, b, c, d)", "expected an integer literal: 'b'");
checkParseFails("set(a, ", "syntax error at ','");
checkParseFails("set(a b", "premature end of input");
}
@Test
public void testBinaryOperators() throws Exception {
checkParseFails("foo intersect", "premature end of input");
checkPrettyPrint("(a - b)", "a - b");
checkPrettyPrint("(a intersect b)", "a intersect b");
checkPrettyPrint("(a intersect b intersect c)", "a intersect b intersect c");
checkPrettyPrint("(a union b)", "a union b");
checkPrettyPrint("(a union b union c)", "a union b union c");
checkPrettyPrint("(a except b)", "a except b");
checkPrettyPrint("(a except b except c)", "a except b except c");
checkPrettyPrint("((a union b) except c)", "a union b except c");
checkPrettyPrint("((a except b) union c)", "a except b union c");
}
@Test
public void testOperators() throws Exception {
checkPrettyPrint("some(x)");
checkPrettyPrint("somepath(x, y)");
checkPrettyPrint("allpaths(x, y)");
checkPrettyPrint("deps(x)");
checkPrettyPrint("deps(x, 1)");
checkPrettyPrint("rdeps(x, y)");
checkPrettyPrint("rdeps(x, y, 1)");
checkPrettyPrint("kind('rule', x)", "kind(rule, x)");
checkPrettyPrint("kind('source file', x)");
checkPrettyPrint("kind('.*', x)");
checkPrettyPrint("attr('linkshared', '1', x)", "attr(linkshared,1,x)");
checkPrettyPrint("filter('jar$', x)", "filter(jar$, x)");
checkPrettyPrint("let x = e1 in e2");
checkPrettyPrint("labels('srcs', x)");
checkPrettyPrint("tests(x)");
checkPrettyPrint("set()");
checkPrettyPrint("set(//a)");
checkPrettyPrint("set(//a //b)");
}
@Test
public void testMultipleOperatorParsing() throws Exception {
checkPrettyPrint(checkPrettyPrint("kind('rule', x)", "kind(rule, x)"));
checkPrettyPrint(checkPrettyPrint("attr('linkshared', '1', x)", "attr(linkshared,1,x)"));
checkPrettyPrint(checkPrettyPrint("filter('jar$', x)", "filter(jar$, x)"));
}
@Test
public void testMultipleBinaryOperatorParsing() throws Exception {
checkPrettyPrint(checkPrettyPrint("((a union b) except c)", "a union b except c"));
checkPrettyPrint(checkPrettyPrint("(a intersect b intersect c)", "a intersect b intersect c"));
checkPrettyPrint(checkPrettyPrint("(a union b union c)", "a union b union c"));
checkPrettyPrint(
checkPrettyPrint(
"((((a union b) intersect c) except d) intersect e)",
"a union b intersect c except d intersect e"));
}
@Test
public void testMultipleTargetLiteralParsing() throws Exception {
checkPrettyPrint(checkPrettyPrint("//foo:.*@4", "\"//foo:.*@4\""));
checkPrettyPrint(checkPrettyPrint("set(//foo)", "set(\"//foo\")"));
checkPrettyPrint("\"set(//foo)\"");
checkPrettyPrint("\"set('//foo')\"");
}
@Test
public void testQuotedAndUnquotedMetacharacters() throws Exception {
checkPrettyPrint("\"//foo:xx+xx\"");
checkPrettyPrint(checkPrettyPrint("(//foo:xx + xx)", "//foo:xx+xx"));
checkPrettyPrint("\"//foo:xx=xx\"");
checkParseFails("//foo:xx=xx", "unexpected token '=' after query expression '//foo:xx'");
}
@Test
public void testQuotedSpecialCharacters() throws Exception {
checkPrettyPrint("\"foo[]^$asd.|asd?*+{})_asd()2\"", "'foo[]^$asd.|asd?*+{})_asd()2'");
checkPrettyPrint("\"foo[]^$asd.|asd?*+{})_asd()2\"");
checkPrettyPrint("\" #&()+,;<=>?[]{|}\"");
}
@Test
public void testUnquotedSpecialCharacters() throws Exception {
// All special characters in the Lexer#scanWord ./@_:~-*$
checkPrettyPrint("a.b");
checkPrettyPrint("a/b");
checkPrettyPrint("a@b");
checkPrettyPrint("a_b");
checkPrettyPrint("a:b");
checkPrettyPrint("a~b");
checkPrettyPrint("a-b");
checkPrettyPrint("a*b");
checkPrettyPrint("a$b");
}
@Test
public void testPreserveQuoting() throws Exception {
checkPrettyPrint(checkPrettyPrint("\"a+b\""));
// this should preserve quoting without being quoted in TargetLiteral#toString
checkPrettyPrint(checkPrettyPrint("aaa", "\"aaa\""));
}
@Test
public void testQuotedIllegalCharacters() throws Exception {
checkParseFails("\"-x\"", "target literal must not begin with (-): -x");
checkParseFails("\"*x\"", "target literal must not begin with (*): *x");
}
@Test
public void testIllegalQuoting() throws Exception {
checkParseFails("\"a", "unclosed quotation");
checkParseFails("\'a", "unclosed quotation");
checkParseFails("a\"a", "unclosed quotation");
checkParseFails("a\'a", "unclosed quotation");
checkParseFails("a\'\"a", "unclosed quotation");
checkParseFails("a\"\'a", "unclosed quotation");
checkParseFails("\'a\"\'a\'", "unclosed quotation");
checkParseFails("\"a\'\"a\"", "unclosed quotation");
checkParseFails(
"\'\"a\" + \'a\'\'", "unexpected token 'a' after query expression ''\"a\" + ''");
checkParseFails(
"\"\'a\' + \"a\"\"", "unexpected token 'a' after query expression '\"'a' + \"'");
checkParseFails(
"\"set(\"//foo\" + \"bar\")\"",
"unexpected token '//foo' after query expression '\"set(\"'");
checkParseFails(
"'set('//foo' + 'bar')'", "unexpected token '//foo' after query expression '\"set(\"'");
}
@Test
public void testUsingCorrectQuotingInTargetLiteralToString() throws Exception {
// These tests all fall into the needsQuoting == true use case in TargetLiteral#toString
checkPrettyPrint("'set(\"//foo\" + \"bar\")'");
checkPrettyPrint("\"set('//foo' + 'bar')\"");
checkPrettyPrint("\"a'a\"");
checkPrettyPrint("\'a\"a\'");
}
}