blob: 665c7f06a6f14351a0fe432f7e0711a5d19b8ddd [file] [log] [blame]
// Copyright 2019 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.bazel.rules.ninja;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.bazel.rules.ninja.file.ByteBufferFragment;
import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
import com.google.devtools.build.lib.bazel.rules.ninja.lexer.NinjaLexer;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaFileParseResult;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaParserStep;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaRule;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaRuleVariable;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaScope;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaTarget;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaVariableValue;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link NinjaParserStep}. */
@RunWith(JUnit4.class)
public class NinjaParserStepTest {
@Test
public void testSimpleVariable() throws Exception {
doTestSimpleVariable("a=b", "a", "b");
doTestSimpleVariable("a=b\nc", "a", "b");
doTestSimpleVariable("a=b # comment", "a", "b");
doTestSimpleVariable("a.b.c = some long: value", "a.b.c", "some long: value");
doTestSimpleVariable("a_11_24-rt.15= ^&%=#@", "a_11_24-rt.15", "^&%=#@");
}
@Test
public void testVariableParsingException() {
doTestVariableParsingException(" ", "Expected identifier, but got indent in fragment:\n \n");
doTestVariableParsingException("a", "Expected = after 'a' in fragment:\na\n");
doTestVariableParsingException(
"^a=",
"Expected identifier, but got error: 'Symbol '^' is not allowed in the identifier, "
+ "the text fragment with the symbol:\n^a=\n' in fragment:\n^a=\n");
}
private static void doTestVariableParsingException(String text, String message) {
GenericParsingException exception =
assertThrows(GenericParsingException.class, () -> createParser(text).parseVariable());
assertThat(exception).hasMessageThat().isEqualTo(message);
}
@Test
public void testNoValue() throws Exception {
doTestNoValue("a=");
doTestNoValue("a=\u000018");
doTestNoValue("a = ");
doTestNoValue("a =\nm");
doTestNoValue("a = # 123");
}
@Test
public void testWithVariablesInValue() throws Exception {
doTestWithVariablesInValue("a=$a $b", "a", "${a} ${b}", ImmutableSortedSet.of("a", "b"));
doTestWithVariablesInValue("a=a_$b_c", "a", "a_${b_c}", ImmutableSortedSet.of("b_c"));
doTestWithVariablesInValue("a=$b a c", "a", "${b} a c", ImmutableSortedSet.of("b"));
doTestWithVariablesInValue("a=a_$b c", "a", "a_${b} c", ImmutableSortedSet.of("b"));
doTestWithVariablesInValue("a=a_${b.d}c", "a", "a_${b.d}c", ImmutableSortedSet.of("b.d"));
doTestWithVariablesInValue(
"e=a$b*c${ d }*18", "e", "a${b}*c${d}*18", ImmutableSortedSet.of("b", "d"));
doTestWithVariablesInValue("e=a$b*${ b }", "e", "a${b}*${b}", ImmutableSortedSet.of("b"));
}
@Test
public void testNormalizeVariableName() {
assertThat(NinjaParserStep.normalizeVariableName("$a")).isEqualTo("a");
assertThat(NinjaParserStep.normalizeVariableName("$a-b-c")).isEqualTo("a-b-c");
assertThat(NinjaParserStep.normalizeVariableName("${abc_de-7}")).isEqualTo("abc_de-7");
assertThat(NinjaParserStep.normalizeVariableName("${ a1.5}")).isEqualTo("a1.5");
assertThat(NinjaParserStep.normalizeVariableName("${a1.5 }")).isEqualTo("a1.5");
}
@Test
public void testInclude() throws Exception {
NinjaVariableValue value1 = createParser("include x/multi words/z").parseIncludeStatement();
assertThat(value1.getRawText()).isEqualTo("x/multi words/z");
NinjaVariableValue value2 = createParser("subninja ${x}.ninja").parseSubNinjaStatement();
assertThat(value2.getRawText()).isEqualTo("${x}.ninja");
MockValueExpander expander = new MockValueExpander("###");
assertThat(value2.getExpandedValue(expander)).isEqualTo("###x.ninja");
assertThat(expander.getRequestedVariables()).containsExactly("x");
}
@Test
public void testIncludeErrors() {
GenericParsingException exception1 =
assertThrows(
GenericParsingException.class,
() -> createParser("include x $").parseIncludeStatement());
assertThat(exception1)
.hasMessageThat()
.isEqualTo(
"Expected newline, but got error: "
+ "'Bad $-escape (literal $ must be written as $$)' in fragment:\ninclude x $\n");
GenericParsingException exception2 =
assertThrows(
GenericParsingException.class, () -> createParser("include").parseIncludeStatement());
assertThat(exception2).hasMessageThat().isEqualTo("include statement has no path.");
GenericParsingException exception3 =
assertThrows(
GenericParsingException.class,
() -> createParser("subninja \nm").parseSubNinjaStatement());
assertThat(exception3).hasMessageThat().isEqualTo("subninja statement has no path.");
}
@Test
public void testNinjaRule() throws Exception {
// Additionally test the situation when we get more line separators in the end.
NinjaParserStep parser =
createParser(
"rule testRule \n"
+ " command = executable --flag $TARGET $out && $POST_BUILD\n"
+ " description = Test rule for $TARGET\n"
+ " rspfile = $TARGET.in\n"
+ " deps = ${abc} $\n"
+ " ${cde}\n\n\n");
NinjaRule ninjaRule = parser.parseNinjaRule();
ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> variables = ninjaRule.getVariables();
assertThat(variables.keySet())
.containsExactly(
NinjaRuleVariable.NAME,
NinjaRuleVariable.COMMAND,
NinjaRuleVariable.DESCRIPTION,
NinjaRuleVariable.RSPFILE,
NinjaRuleVariable.DEPS);
assertThat(variables.get(NinjaRuleVariable.NAME).getRawText()).isEqualTo("testRule");
assertThat(variables.get(NinjaRuleVariable.DEPS).getRawText()).isEqualTo("${abc} $\n ${cde}");
MockValueExpander expander = new MockValueExpander("###");
assertThat(variables.get(NinjaRuleVariable.DEPS).getExpandedValue(expander))
.isEqualTo("###abc $\n ###cde");
assertThat(expander.getRequestedVariables()).containsExactly("abc", "cde");
}
@Test
public void testNinjaRuleWithHash() throws Exception {
// Additionally test the situation when we get more line separators in the end.
NinjaParserStep parser =
createParser(
"rule testRule \n"
+ " command = executable --flag $TARGET $out && sed -e 's/#.*$$//' -e '/^$$/d'\n"
+ " description = Test rule for $TARGET");
NinjaRule ninjaRule = parser.parseNinjaRule();
assertThat(ninjaRule.getVariables().get(NinjaRuleVariable.COMMAND).getRawText())
// Variables are wrapped with {} by print function, $$ escape sequence is unescaped.
.isEqualTo("executable --flag ${TARGET} ${out} && sed -e 's/#.*$//' -e '/^$/d'");
}
@Test
public void testVariableWithoutValue() throws Exception {
NinjaParserStep parser =
createParser(
"rule testRule \n"
+ " command = executable --flag $TARGET $out && $POST_BUILD\n"
+ " description =\n");
NinjaRule ninjaRule = parser.parseNinjaRule();
ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> variables = ninjaRule.getVariables();
assertThat(variables.keySet())
.containsExactly(
NinjaRuleVariable.NAME, NinjaRuleVariable.COMMAND, NinjaRuleVariable.DESCRIPTION);
assertThat(variables.get(NinjaRuleVariable.NAME).getRawText()).isEqualTo("testRule");
assertThat(variables.get(NinjaRuleVariable.DESCRIPTION).getRawText()).isEmpty();
}
@Test
public void testNinjaRuleParsingException() {
doTestNinjaRuleParsingException(
"rule testRule extra-word\n",
String.join(
"\n",
"Expected newline, but got identifier in fragment:",
"rule testRule extra-word",
"",
""));
doTestNinjaRuleParsingException(
"rule testRule\ncommand =",
String.join(
"\n",
"Expected indent, but got identifier in fragment:",
"rule testRule",
"command =",
""));
doTestNinjaRuleParsingException(
"rule testRule\n ^custom = a",
String.join(
"\n",
"Expected identifier, but got error: 'Symbol '^' is not allowed in the identifier, "
+ "the text fragment with the symbol:",
"rule testRule",
" ^custom = a",
"' in fragment:",
"rule testRule",
" ^custom = a",
""));
doTestNinjaRuleParsingException("rule testRule\n custom = a", "Unexpected variable 'custom'");
}
@Test
public void testNinjaTargets() throws Exception {
// Additionally test the situation when the target does not have the variables section and
// we get more line separators in the end.
NinjaTarget target = parseNinjaTarget("build output: command input\n\n");
assertThat(target.getRuleName()).isEqualTo("command");
assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
assertThat(target.getUsualInputs()).containsExactly(PathFragment.create("input"));
NinjaTarget target1 =
parseNinjaTarget("build o1 o2 | io1 io2: command i1 i2 | ii1 ii2 || ooi1 ooi2");
assertThat(target1.getRuleName()).isEqualTo("command");
assertThat(target1.getOutputs())
.containsExactly(PathFragment.create("o1"), PathFragment.create("o2"));
assertThat(target1.getImplicitOutputs())
.containsExactly(PathFragment.create("io1"), PathFragment.create("io2"));
assertThat(target1.getUsualInputs())
.containsExactly(PathFragment.create("i1"), PathFragment.create("i2"));
assertThat(target1.getImplicitInputs())
.containsExactly(PathFragment.create("ii1"), PathFragment.create("ii2"));
assertThat(target1.getOrderOnlyInputs())
.containsExactly(PathFragment.create("ooi1"), PathFragment.create("ooi2"));
NinjaTarget target2 = parseNinjaTarget("build output: phony");
assertThat(target2.getRuleName()).isEqualTo("phony");
assertThat(target2.getOutputs()).containsExactly(PathFragment.create("output"));
NinjaTarget target3 = parseNinjaTarget("build output: command $\n || order-only-input");
assertThat(target3.getRuleName()).isEqualTo("command");
assertThat(target3.getOutputs()).containsExactly(PathFragment.create("output"));
assertThat(target3.getOrderOnlyInputs())
.containsExactly(PathFragment.create("order-only-input"));
}
@Test
public void testNinjaTargetParsingErrors() {
testNinjaTargetParsingError("build xxx", "Unexpected end of target");
testNinjaTargetParsingError("build xxx yyy:", "Expected rule name");
testNinjaTargetParsingError("build xxx || yyy: command", "Unexpected token: PIPE2");
testNinjaTargetParsingError("build xxx: command :", "Unexpected token: COLON");
testNinjaTargetParsingError("build xxx: command | || a", "Expected paths sequence");
}
@Test
public void testNinjaTargetsWithVariables() throws Exception {
NinjaFileParseResult parseResult = new NinjaFileParseResult();
parseResult.addVariable("output", 1, NinjaVariableValue.createPlainText("out123"));
parseResult.addVariable("input", 2, NinjaVariableValue.createPlainText("in123"));
NinjaScope scope = new NinjaScope();
parseResult.expandIntoScope(scope, Maps.newHashMap());
// Variables, defined inside build statement, are used for input and output paths,
// but not for the values of the other variables.
// Test it.
NinjaTarget target =
createParser(
"build $output : command $input $dir/abcde\n"
+ " dir = def$input\n empty = '$dir'")
.parseNinjaTarget(scope, 5);
assertThat(target.getRuleName()).isEqualTo("command");
assertThat(target.getOutputs()).containsExactly(PathFragment.create("out123"));
assertThat(target.getUsualInputs())
.containsExactly(PathFragment.create("in123"), PathFragment.create("defin123/abcde"));
assertThat(target.getVariables())
.containsExactlyEntriesIn(ImmutableSortedMap.of("dir", "defin123", "empty", "''"));
}
@Test
public void testPseudoCyclesOfVariables() throws Exception {
NinjaFileParseResult parseResult = new NinjaFileParseResult();
parseResult.addVariable(
"output", 1, NinjaVariableValue.builder().addText("'out'").addVariable("input").build());
parseResult.addVariable(
"input", 2, NinjaVariableValue.builder().addText("'in'").addVariable("output").build());
NinjaScope scope = new NinjaScope();
parseResult.expandIntoScope(scope, Maps.newHashMap());
assertThat(scope.findExpandedVariable(3, "input")).isEqualTo("'in''out'");
assertThat(scope.findExpandedVariable(3, "output")).isEqualTo("'out'");
}
@Test
public void testNinjaTargetsPathWithEscapedSpace() throws Exception {
NinjaTarget target = parseNinjaTarget("build output : command input$ with$ space other");
assertThat(target.getRuleName()).isEqualTo("command");
assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
assertThat(target.getUsualInputs())
.containsExactly(PathFragment.create("input with space"), PathFragment.create("other"));
}
@Test
public void testNinjaTargetWithScope() throws Exception {
NinjaTarget target = parseNinjaTarget("build output : command input\n pool = abc\n");
assertThat(target.getRuleName()).isEqualTo("command");
assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
assertThat(target.getUsualInputs()).containsExactly(PathFragment.create("input"));
}
private static void testNinjaTargetParsingError(String text, String error) {
GenericParsingException exception =
assertThrows(GenericParsingException.class, () -> parseNinjaTarget(text));
assertThat(exception).hasMessageThat().isEqualTo(error);
}
private static NinjaTarget parseNinjaTarget(String text) throws Exception {
NinjaScope fileScope = new NinjaScope();
return createParser(text).parseNinjaTarget(fileScope, 0);
}
private static void doTestNinjaRuleParsingException(String text, String message) {
GenericParsingException exception =
assertThrows(GenericParsingException.class, () -> createParser(text).parseNinjaRule());
assertThat(exception).hasMessageThat().isEqualTo(message);
}
private static void doTestSimpleVariable(String text, String name, String value)
throws Exception {
NinjaParserStep parser = createParser(text);
Pair<String, NinjaVariableValue> variable = parser.parseVariable();
assertThat(variable.getFirst()).isEqualTo(name);
assertThat(variable.getSecond()).isNotNull();
assertThat(variable.getSecond().getRawText()).isEqualTo(value);
MockValueExpander expander = new MockValueExpander("###");
assertThat(variable.getSecond().getExpandedValue(expander)).isEqualTo(value);
assertThat(expander.getRequestedVariables()).isEmpty();
}
private static void doTestNoValue(String text) throws Exception {
NinjaParserStep parser = createParser(text);
NinjaVariableValue value = parser.parseVariable().getSecond();
assertThat(value).isNotNull();
assertThat(value.getRawText()).isEmpty();
}
private static void doTestWithVariablesInValue(
String text, String name, String value, ImmutableSortedSet<String> expectedVars)
throws Exception {
NinjaParserStep parser = createParser(text);
Pair<String, NinjaVariableValue> variable = parser.parseVariable();
assertThat(variable.getFirst()).isEqualTo(name);
assertThat(variable.getSecond()).isNotNull();
assertThat(variable.getSecond().getRawText()).isEqualTo(value);
MockValueExpander expander = new MockValueExpander("###");
assertThat(variable.getSecond().getExpandedValue(expander)).contains("###");
assertThat(expander.getRequestedVariables()).containsExactlyElementsIn(expectedVars);
}
private static NinjaParserStep createParser(String text) {
ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.ISO_8859_1));
NinjaLexer lexer = new NinjaLexer(new ByteBufferFragment(buffer, 0, buffer.limit()));
return new NinjaParserStep(lexer);
}
private static class MockValueExpander implements Function<String, String> {
private final ImmutableSortedSet.Builder<String> setBuilder;
private final String prefix;
private MockValueExpander(String prefix) {
this.prefix = prefix;
setBuilder = ImmutableSortedSet.naturalOrder();
}
@Override
public String apply(String s) {
setBuilder.add(s);
return prefix + s;
}
public ImmutableSortedSet<String> getRequestedVariables() {
return setBuilder.build();
}
}
}