blob: 774452d74acc510dd47ca9bc0d8b93f5e756549a [file] [log] [blame]
// Copyright 2018 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.starlark;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable;
import com.google.devtools.build.lib.pkgcache.TargetParsingCompleteEvent;
import com.google.devtools.build.lib.starlark.util.StarlarkOptionsTestCase;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsParsingResult;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import net.starlark.java.eval.StarlarkInt;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit test for the {@code StarlarkOptionsParser}. */
@RunWith(JUnit4.class)
public class StarlarkOptionsParsingTest extends StarlarkOptionsTestCase {
private List<Postable> postedEvents;
@Before
public void addPostableEventHandler() {
postedEvents = new ArrayList<>();
reporter.addHandler(
new ExtendedEventHandler() {
@Override
public void post(Postable obj) {
postedEvents.add(obj);
}
@Override
public void handle(Event event) {}
});
}
/** Returns only the posted events of the given class. */
private List<Postable> eventsOfType(Class<? extends Postable> clazz) {
return postedEvents.stream()
.filter(event -> event.getClass().equals(clazz))
.collect(Collectors.toList());
}
// test --flag=value
@Test
public void testFlagEqualsValueForm() throws Exception {
writeBasicIntFlag();
OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting=666");
assertThat(result.getStarlarkOptions()).hasSize(1);
assertThat(result.getStarlarkOptions().get("//test:my_int_setting"))
.isEqualTo(StarlarkInt.of(666));
assertThat(result.getResidue()).isEmpty();
}
// test --@main_workspace//flag=value parses out to //flag=value
// test --@other_workspace//flag=value parses out to @other_workspace//flag=value
@Test
public void testFlagNameWithExternalRepo() throws Exception {
writeBasicIntFlag();
scratch.file("test/repo2/MODULE.bazel", "module(name = 'repo2')");
scratch.file(
"test/repo2/defs.bzl",
"""
def _impl(ctx):
pass
my_flag = rule(
implementation = _impl,
build_setting = config.int(flag = True),
)
""");
scratch.file(
"test/repo2/BUILD",
"""
load(":defs.bzl", "my_flag")
my_flag(
name = "flag2",
build_setting_default = 2,
)
""");
rewriteModuleDotBazel(
"module(name='starlark_options_test')",
"bazel_dep(name='repo2')",
"local_path_override(",
" module_name = 'repo2',",
" path = 'test/repo2',",
")");
OptionsParsingResult result =
parseStarlarkOptions(
"--@starlark_options_test//test:my_int_setting=666 --@repo2//:flag2=222",
/* onlyStarlarkParser= */ true);
assertThat(result.getStarlarkOptions()).hasSize(2);
assertThat(result.getStarlarkOptions().get("//test:my_int_setting"))
.isEqualTo(StarlarkInt.of(666));
assertThat(result.getStarlarkOptions().get("@@repo2+//:flag2")).isEqualTo(StarlarkInt.of(222));
assertThat(result.getResidue()).isEmpty();
}
// test --fake_flag=value
@Test
public void testBadFlag_equalsForm() throws Exception {
scratch.file("test/BUILD");
reporter.removeHandler(failFastHandler);
OptionsParsingException e =
assertThrows(
OptionsParsingException.class,
() -> parseStarlarkOptions("--//fake_flag=blahblahblah"));
assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
assertThat(e.getInvalidArgument()).isEqualTo("//fake_flag");
}
// test --fake_flag
@Test
public void testBadFlag_boolForm() throws Exception {
scratch.file("test/BUILD");
reporter.removeHandler(failFastHandler);
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//fake_flag"));
assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
assertThat(e.getInvalidArgument()).isEqualTo("//fake_flag");
}
@Test
public void testBadFlag_keepGoing() throws Exception {
optionsParser.parse("--keep_going");
scratch.file("test/BUILD");
reporter.removeHandler(failFastHandler);
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//fake_flag"));
assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
assertThat(e.getInvalidArgument()).isEqualTo("//fake_flag");
}
@Test
public void testSingleDash_notAllowed() throws Exception {
writeBasicIntFlag();
OptionsParsingException e =
assertThrows(
OptionsParsingException.class,
() ->
parseStarlarkOptions("-//test:my_int_setting=666", /* onlyStarlarkParser= */ true));
assertThat(e).hasMessageThat().isEqualTo("Invalid options syntax: -//test:my_int_setting=666");
}
// test --non_flag_setting=value
@Test
public void testNonFlagParsing() throws Exception {
scratch.file(
"test/build_setting.bzl",
"""
def _build_setting_impl(ctx):
return []
int_flag = rule(
implementation = _build_setting_impl,
build_setting = config.int(flag = False),
)
""");
scratch.file(
"test/BUILD",
"""
load("//test:build_setting.bzl", "int_flag")
int_flag(
name = "my_int_setting",
build_setting_default = 42,
)
""");
OptionsParsingException e =
assertThrows(
OptionsParsingException.class,
() -> parseStarlarkOptions("--//test:my_int_setting=666"));
assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:my_int_setting=666");
}
// test --bool_flag
@Test
public void testBooleanFlag() throws Exception {
writeBasicBoolFlag();
OptionsParsingResult result = parseStarlarkOptions("--//test:my_bool_setting=false");
assertThat(result.getStarlarkOptions()).hasSize(1);
assertThat(result.getStarlarkOptions().get("//test:my_bool_setting")).isEqualTo(false);
assertThat(result.getResidue()).isEmpty();
}
// test --nobool_flag
@Test
public void testNoPrefixedBooleanFlag() throws Exception {
writeBasicBoolFlag();
OptionsParsingResult result = parseStarlarkOptions("--no//test:my_bool_setting");
assertThat(result.getStarlarkOptions()).hasSize(1);
assertThat(result.getStarlarkOptions().get("//test:my_bool_setting")).isEqualTo(false);
assertThat(result.getResidue()).isEmpty();
}
// test --no@main_workspace//:bool_flag
@Test
public void testNoPrefixedBooleanFlag_withWorkspace() throws Exception {
writeBasicBoolFlag();
OptionsParsingResult result = parseStarlarkOptions("--no@//test:my_bool_setting");
assertThat(result.getStarlarkOptions()).hasSize(1);
assertThat(result.getStarlarkOptions().get("//test:my_bool_setting")).isEqualTo(false);
assertThat(result.getResidue()).isEmpty();
}
// test --noint_flag
@Test
public void testNoPrefixedNonBooleanFlag() throws Exception {
writeBasicIntFlag();
OptionsParsingException e =
assertThrows(
OptionsParsingException.class, () -> parseStarlarkOptions("--no//test:my_int_setting"));
assertThat(e)
.hasMessageThat()
.isEqualTo("Illegal use of 'no' prefix on non-boolean option: //test:my_int_setting");
}
// test --int_flag
@Test
public void testFlagWithoutValue() throws Exception {
writeBasicIntFlag();
OptionsParsingException e =
assertThrows(
OptionsParsingException.class, () -> parseStarlarkOptions("--//test:my_int_setting"));
assertThat(e).hasMessageThat().isEqualTo("Expected value after --//test:my_int_setting");
}
// test --flag --flag
@Test
public void testRepeatFlagLastOneWins() throws Exception {
writeBasicIntFlag();
OptionsParsingResult result =
parseStarlarkOptions("--//test:my_int_setting=4 --//test:my_int_setting=7");
assertThat(result.getStarlarkOptions()).hasSize(1);
assertThat(result.getStarlarkOptions().get("//test:my_int_setting"))
.isEqualTo(StarlarkInt.of(7));
assertThat(result.getResidue()).isEmpty();
}
// test --flagA=valueA --flagB=valueB
@Test
public void testMultipleFlags() throws Exception {
scratch.file(
"test/build_setting.bzl",
"""
def _build_setting_impl(ctx):
return []
int_flag = rule(
implementation = _build_setting_impl,
build_setting = config.int(flag = True),
)
""");
scratch.file(
"test/BUILD",
"""
load("//test:build_setting.bzl", "int_flag")
int_flag(
name = "my_int_setting",
build_setting_default = 42,
)
int_flag(
name = "my_other_int_setting",
build_setting_default = 77,
)
""");
OptionsParsingResult result =
parseStarlarkOptions("--//test:my_int_setting=0 --//test:my_other_int_setting=0");
assertThat(result.getResidue()).isEmpty();
assertThat(result.getStarlarkOptions()).hasSize(2);
assertThat(result.getStarlarkOptions().get("//test:my_int_setting"))
.isEqualTo(StarlarkInt.of(0));
assertThat(result.getStarlarkOptions().get("//test:my_other_int_setting"))
.isEqualTo(StarlarkInt.of(0));
}
// test --non_build_setting
@Test
public void testNonBuildSetting() throws Exception {
scratch.file(
"test/rules.bzl",
"""
def _impl(ctx):
return []
my_rule = rule(
implementation = _impl,
)
""");
scratch.file(
"test/BUILD",
"""
load("//test:rules.bzl", "my_rule")
my_rule(name = "my_rule")
""");
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:my_rule"));
assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:my_rule");
}
// test --non_rule_configured_target
@Test
public void testNonRuleConfiguredTarget() throws Exception {
scratch.file(
"test/BUILD",
"""
genrule(
name = "my_gen",
srcs = ["x.in"],
outs = ["x.cc"],
cmd = "$(locations :tool) $< >$@",
tools = [":tool"],
)
cc_library(name = "tool-dep")
""");
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:x.in"));
assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:x.in");
}
// test --int_flag=non_int_value
@Test
public void testWrongValueType_int() throws Exception {
writeBasicIntFlag();
OptionsParsingException e =
assertThrows(
OptionsParsingException.class,
() -> parseStarlarkOptions("--//test:my_int_setting=woohoo"));
assertThat(e)
.hasMessageThat()
.isEqualTo("While parsing option //test:my_int_setting=woohoo: 'woohoo' is not a int");
}
// test --bool_flag=non_bool_value
@Test
public void testWrongValueType_bool() throws Exception {
writeBasicBoolFlag();
OptionsParsingException e =
assertThrows(
OptionsParsingException.class,
() -> parseStarlarkOptions("--//test:my_bool_setting=woohoo"));
assertThat(e)
.hasMessageThat()
.isEqualTo("While parsing option //test:my_bool_setting=woohoo: 'woohoo' is not a boolean");
}
// test --int-flag=same value as default
@Test
public void testDontStoreDefaultValue() throws Exception {
// build_setting_default = 42
writeBasicIntFlag();
OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting=42");
assertThat(result.getStarlarkOptions()).isEmpty();
}
@Test
public void testOptionsAreParsedWithBuildTestsOnly() throws Exception {
writeBasicIntFlag();
optionsParser.parse("--build_tests_only");
OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting=15");
assertThat(result.getStarlarkOptions().get("//test:my_int_setting"))
.isEqualTo(StarlarkInt.of(15));
}
/**
* When Starlark flags are only set as flags, they shouldn't produce {@link
* TargetParsingCompleteEvent}s. That's intended to communicate (to the build event protocol)
* which of the targets in {@code blaze build //foo:all //bar:all} were built.
*/
@Test
public void testExpectedBuildEventOutput_asFlag() throws Exception {
writeBasicIntFlag();
scratch.file("blah/BUILD", "cc_library(name = 'mylib')");
useConfiguration("--//test:my_int_setting=15");
update(
ImmutableList.of("//blah:mylib"),
/*keepGoing=*/ false,
/*loadingPhaseThreads=*/ LOADING_PHASE_THREADS,
/*doAnalysis*/ true,
eventBus);
List<Postable> targetParsingCompleteEvents = eventsOfType(TargetParsingCompleteEvent.class);
assertThat(targetParsingCompleteEvents).hasSize(1);
assertThat(
((TargetParsingCompleteEvent) targetParsingCompleteEvents.get(0))
.getOriginalTargetPattern())
.containsExactly("//blah:mylib");
}
/**
* But Starlark are also targets. When they're requested as normal build targets they should
* produce {@link TargetParsingCompleteEvent} just like any other target.
*/
@Test
public void testExpectedBuildEventOutput_asTarget() throws Exception {
writeBasicIntFlag();
scratch.file("blah/BUILD", "cc_library(name = 'mylib')");
useConfiguration("--//test:my_int_setting=15");
update(
ImmutableList.of("//blah:mylib", "//test:my_int_setting"),
/*keepGoing=*/ false,
/*loadingPhaseThreads=*/ LOADING_PHASE_THREADS,
/*doAnalysis*/ true,
eventBus);
List<Postable> targetParsingCompleteEvents = eventsOfType(TargetParsingCompleteEvent.class);
assertThat(targetParsingCompleteEvents).hasSize(1);
assertThat(
((TargetParsingCompleteEvent) targetParsingCompleteEvents.get(0))
.getOriginalTargetPattern())
.containsExactly("//blah:mylib", "//test:my_int_setting");
}
@Test
@SuppressWarnings("unchecked")
public void testAllowMultipleStringFlag() throws Exception {
scratch.file(
"test/build_setting.bzl",
"""
def _build_setting_impl(ctx):
return []
allow_multiple_flag = rule(
implementation = _build_setting_impl,
build_setting = config.string(flag = True, allow_multiple = True),
)
""");
scratch.file(
"test/BUILD",
"""
load("//test:build_setting.bzl", "allow_multiple_flag")
allow_multiple_flag(
name = "cats",
build_setting_default = "tabby",
)
""");
OptionsParsingResult result = parseStarlarkOptions("--//test:cats=calico --//test:cats=bengal");
assertThat(result.getStarlarkOptions().keySet()).containsExactly("//test:cats");
assertThat((List<String>) result.getStarlarkOptions().get("//test:cats"))
.containsExactly("calico", "bengal");
}
@Test
@SuppressWarnings("unchecked")
public void testRepeatedStringListFlag() throws Exception {
scratch.file(
"test/build_setting.bzl",
"""
def _build_setting_impl(ctx):
return []
repeated_flag = rule(
implementation = _build_setting_impl,
build_setting = config.string_list(flag = True, repeatable = True),
)
""");
scratch.file(
"test/BUILD",
"""
load("//test:build_setting.bzl", "repeated_flag")
repeated_flag(
name = "cats",
build_setting_default = ["tabby"],
)
""");
OptionsParsingResult result = parseStarlarkOptions("--//test:cats=calico --//test:cats=bengal");
assertThat(result.getStarlarkOptions().keySet()).containsExactly("//test:cats");
assertThat((List<String>) result.getStarlarkOptions().get("//test:cats"))
.containsExactly("calico", "bengal");
}
@Test
public void flagReferencesExactlyOneTarget() throws Exception {
scratch.file(
"test/build_setting.bzl",
"""
string_flag = rule(
implementation = lambda ctx, attr: [],
build_setting = config.string(flag = True),
)
""");
scratch.file(
"test/BUILD",
"""
load("//test:build_setting.bzl", "string_flag")
string_flag(
name = "one",
build_setting_default = "",
)
string_flag(
name = "two",
build_setting_default = "",
)
""");
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:all"));
assertThat(e)
.hasMessageThat()
.contains("//test:all: user-defined flags must reference exactly one target");
}
@Test
public void flagIsAlias() throws Exception {
scratch.file(
"test/build_setting.bzl",
"""
string_flag = rule(
implementation = lambda ctx: [],
build_setting = config.string(flag = True),
)
""");
scratch.file(
"test/BUILD",
"""
load("//test:build_setting.bzl", "string_flag")
alias(
name = "one",
actual = "//test/pkg:two",
)
string_flag(
name = "three",
build_setting_default = "",
)
""");
scratch.file(
"test/pkg/BUILD",
"""
alias(
name = "two",
actual = "//test:three",
)
""");
OptionsParsingResult result = parseStarlarkOptions("--//test:one=one --//test/pkg:two=two");
assertThat(result.getStarlarkOptions()).containsExactly("//test:three", "two");
}
@Test
public void flagIsAlias_cycle() throws Exception {
scratch.file(
"test/BUILD",
"""
alias(
name = "one",
actual = "//test/pkg:two",
)
alias(
name = "three",
actual = ":one",
)
""");
scratch.file(
"test/pkg/BUILD",
"""
alias(
name = "two",
actual = "//test:three",
)
""");
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:one=one"));
assertThat(e)
.hasMessageThat()
.isEqualTo(
"Failed to load build setting '//test:one' due to a cycle in alias chain: //test:one"
+ " -> //test/pkg:two -> //test:three -> //test:one");
}
@Test
public void flagIsAlias_usesSelect() throws Exception {
scratch.file(
"test/BUILD",
"""
alias(
name = "one",
actual = "//test/pkg:two",
)
alias(
name = "three",
actual = ":one",
)
""");
scratch.file(
"test/pkg/BUILD",
"""
# Needed to avoid select() being eliminated as trivial.
config_setting(
name = "config",
values = {"define": "pi=3"},
)
alias(
name = "two",
actual = select({
":config": "//test:three",
"//conditions:default": "//test:three",
}),
)
""");
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:one=one"));
assertThat(e)
.hasMessageThat()
.isEqualTo(
"Failed to load build setting '//test:one' as it resolves to an alias with an actual"
+ " value that uses select(): //test:one -> //test/pkg:two. This is not supported"
+ " as build settings are needed to determine the configuration the select is"
+ " evaluated in.");
}
@Test
public void flagIsAlias_resolvesToNonBuildSettingTarget() throws Exception {
scratch.file(
"test/BUILD",
"""
alias(
name = "one",
actual = "//test/pkg:two",
)
genrule(
name = "three",
outs = ["out"],
cmd = "echo hello > $@",
)
""");
scratch.file(
"test/pkg/BUILD",
"""
alias(
name = "two",
actual = "//test:three",
)
""");
OptionsParsingException e =
assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:one=one"));
assertThat(e)
.hasMessageThat()
.isEqualTo("Unrecognized option: //test:one -> //test/pkg:two -> //test:three");
}
}