blob: 9ab340edf55ccd9503f51cc3d8ad337f79fb072e [file] [log] [blame]
// Copyright 2017 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.common.options;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.truth.Correspondence;
import com.google.devtools.common.options.OptionsParser.ConstructionException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link IsolatedOptionsData} and {@link OptionsData}. */
@RunWith(JUnit4.class)
public class OptionsDataTest {
private static IsolatedOptionsData construct(Class<? extends OptionsBase> optionsClass)
throws OptionsParser.ConstructionException {
return IsolatedOptionsData.from(ImmutableList.<Class<? extends OptionsBase>>of(optionsClass));
}
private static IsolatedOptionsData construct(
Class<? extends OptionsBase> optionsClass1,
Class<? extends OptionsBase> optionsClass2)
throws OptionsParser.ConstructionException {
return IsolatedOptionsData.from(
ImmutableList.<Class<? extends OptionsBase>>of(optionsClass1, optionsClass2));
}
private static IsolatedOptionsData construct(
Class<? extends OptionsBase> optionsClass1,
Class<? extends OptionsBase> optionsClass2,
Class<? extends OptionsBase> optionsClass3)
throws OptionsParser.ConstructionException {
return IsolatedOptionsData.from(
ImmutableList.<Class<? extends OptionsBase>>of(
optionsClass1, optionsClass2, optionsClass3));
}
/** Dummy options class. */
public static class ExampleNameConflictOptions extends OptionsBase {
@Option(
name = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "1"
)
public int foo;
@Option(
name = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "I should conflict with foo"
)
public String anotherFoo;
}
@Test
public void testNameConflictInSingleClass() {
ConstructionException e =
assertThrows(
"foo should conflict with the previous flag foo",
ConstructionException.class,
() -> construct(ExampleNameConflictOptions.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains("Duplicate option name, due to option name collision: --foo");
}
/** Dummy options class. */
public static class ExampleIntegerFooOptions extends OptionsBase {
@Option(
name = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "5"
)
public int foo;
}
/** Dummy options class. */
public static class ExampleBooleanFooOptions extends OptionsBase {
@Option(
name = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "false"
)
public boolean foo;
}
@Test
public void testNameConflictInTwoClasses() {
ConstructionException e =
assertThrows(
"foo should conflict with the previous flag foo",
ConstructionException.class,
() -> construct(ExampleIntegerFooOptions.class, ExampleBooleanFooOptions.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains("Duplicate option name, due to option name collision: --foo");
}
/** Dummy options class. */
public static class ExamplePrefixedFooOptions extends OptionsBase {
@Option(
name = "nofoo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "false"
)
public boolean noFoo;
}
@Test
public void testBooleanPrefixNameConflict() {
// Try the same test in both orders, the parser should fail if the overlapping flag is defined
// before or after the boolean flag introduces the alias.
ConstructionException e =
assertThrows(
"nofoo should conflict with the previous flag foo, "
+ "since foo, as a boolean flag, can be written as --nofoo",
ConstructionException.class,
() -> construct(ExampleBooleanFooOptions.class, ExamplePrefixedFooOptions.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains(
"Duplicate option name, due to option --nofoo, it "
+ "conflicts with a negating alias for boolean flag --foo");
e =
assertThrows(
"option nofoo should conflict with the previous flag foo, "
+ "since foo, as a boolean flag, can be written as --nofoo",
ConstructionException.class,
() -> construct(ExamplePrefixedFooOptions.class, ExampleBooleanFooOptions.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains("Duplicate option name, due to boolean option alias: --nofoo");
}
/** Dummy options class. */
public static class ExampleBarWasNamedFooOption extends OptionsBase {
@Option(
name = "bar",
oldName = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "false"
)
public boolean bar;
}
@Test
public void testBooleanAliasWithOldNameConflict() {
// Try the same test in both orders, the parser should fail if the overlapping flag is defined
// before or after the boolean flag introduces the alias.
ConstructionException e =
assertThrows(
"bar has old name foo, which is a boolean flag and can be named as nofoo, so it "
+ "should conflict with the previous option --nofoo",
ConstructionException.class,
() -> construct(ExamplePrefixedFooOptions.class, ExampleBarWasNamedFooOption.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains("Duplicate option name, due to boolean option alias: --nofoo");
e =
assertThrows(
"nofoo should conflict with the previous flag bar that has old name foo, "
+ "since foo, as a boolean flag, can be written as --nofoo",
ConstructionException.class,
() -> construct(ExampleBarWasNamedFooOption.class, ExamplePrefixedFooOptions.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains(
"Duplicate option name, due to option --nofoo, it conflicts with a negating "
+ "alias for boolean flag --foo");
}
/** Dummy options class. */
public static class ExampleBarWasNamedNoFooOption extends OptionsBase {
@Option(
name = "bar",
oldName = "nofoo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "false"
)
public boolean bar;
}
@Test
public void testBooleanWithOldNameAsAliasOfBooleanConflict() {
// Try the same test in both orders, the parser should fail if the overlapping flag is defined
// before or after the boolean flag introduces the alias.
ConstructionException e =
assertThrows(
"nofoo, the old name for bar, should conflict with the previous flag foo, "
+ "since foo, as a boolean flag, can be written as --nofoo",
ConstructionException.class,
() -> construct(ExampleBooleanFooOptions.class, ExampleBarWasNamedNoFooOption.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains(
"Duplicate option name, due to old option name --nofoo, it conflicts with a "
+ "negating alias for boolean flag --foo");
e =
assertThrows(
"foo, as a boolean flag, can be written as --nofoo and should conflict with the "
+ "previous option bar that has old name nofoo",
ConstructionException.class,
() -> construct(ExampleBarWasNamedNoFooOption.class, ExampleBooleanFooOptions.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains("Duplicate option name, due to boolean option alias: --nofoo");
}
/** Dummy options class. */
public static class ExampleFooBooleanConflictsWithOwnOldName extends OptionsBase {
@Option(
name = "nofoo",
oldName = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "false"
)
public boolean foo;
}
@Test
public void testSelfConflictBooleanAliases() {
// Try the same test in both orders, the parser should fail if the overlapping flag is defined
// before or after the boolean flag introduces the alias.
ConstructionException e =
assertThrows(
"foo, the old name for boolean option nofoo, should conflict with its own new name.",
ConstructionException.class,
() -> construct(ExampleFooBooleanConflictsWithOwnOldName.class));
assertThat(e).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(e)
.hasMessageThat()
.contains("Duplicate option name, due to boolean option alias: --nofoo");
}
/** Dummy options class. */
public static class OldNameToCanonicalNameConflictExample extends OptionsBase {
@Option(
name = "new_name",
oldName = "old_name",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "defaultValue"
)
public String flag1;
@Option(
name = "old_name",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "defaultValue"
)
public String flag2;
}
@Test
public void testOldNameToCanonicalNameConflict() {
ConstructionException expected =
assertThrows(
"old_name should conflict with the flag already named old_name",
ConstructionException.class,
() -> construct(OldNameToCanonicalNameConflictExample.class));
assertThat(expected).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(expected)
.hasMessageThat()
.contains(
"Duplicate option name, due to option name collision with another option's old name:"
+ " --old_name");
}
/** Dummy options class. */
public static class OldNameConflictExample extends OptionsBase {
@Option(
name = "new_name",
oldName = "old_name",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "defaultValue"
)
public String flag1;
@Option(
name = "another_name",
oldName = "old_name",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "defaultValue"
)
public String flag2;
}
@Test
public void testOldNameToOldNameConflict() {
ConstructionException expected =
assertThrows(
"old_name should conflict with the flag already named old_name",
ConstructionException.class,
() -> construct(OldNameConflictExample.class));
assertThat(expected).hasCauseThat().isInstanceOf(DuplicateOptionDeclarationException.class);
assertThat(expected)
.hasMessageThat()
.contains(
"Duplicate option name, due to old option name collision with another "
+ "old option name: --old_name");
}
/** Dummy options class. */
public static class StringConverter implements Converter<String> {
@Override
public String convert(String input) {
return input;
}
@Override
public String getTypeDescription() {
return "a string";
}
}
/**
* Dummy options class.
*
* <p>Option name order is different from field name order.
*
* <p>There are four fields to increase the likelihood of a non-deterministic order being noticed.
*/
public static class FieldNamesDifferOptions extends OptionsBase {
@Option(
name = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int aFoo;
@Option(
name = "bar",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int bBar;
@Option(
name = "baz",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int cBaz;
@Option(
name = "qux",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int dQux;
}
/** Dummy options class. */
public static class EndOfAlphabetOptions extends OptionsBase {
@Option(
name = "X",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int x;
@Option(
name = "Y",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int y;
}
/** Dummy options class. */
public static class ReverseOrderedOptions extends OptionsBase {
@Option(
name = "C",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int c;
@Option(
name = "B",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int b;
@Option(
name = "A",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "0"
)
public int a;
}
@Test
public void optionsClassesIsOrdered() throws Exception {
IsolatedOptionsData data = construct(
FieldNamesDifferOptions.class,
EndOfAlphabetOptions.class,
ReverseOrderedOptions.class);
assertThat(data.getOptionsClasses()).containsExactly(
FieldNamesDifferOptions.class,
EndOfAlphabetOptions.class,
ReverseOrderedOptions.class).inOrder();
}
@Test
public void getAllNamedFieldsIsOrdered() throws Exception {
IsolatedOptionsData data = construct(
FieldNamesDifferOptions.class,
EndOfAlphabetOptions.class,
ReverseOrderedOptions.class);
ArrayList<String> names = new ArrayList<>();
for (Map.Entry<String, OptionDefinition> entry : data.getAllOptionDefinitions()) {
names.add(entry.getKey());
}
assertThat(names).containsExactly(
"bar", "baz", "foo", "qux", "X", "Y", "A", "B", "C").inOrder();
}
private List<String> getOptionNames(Class<? extends OptionsBase> optionsBase) {
ArrayList<String> result = new ArrayList<>();
for (OptionDefinition optionDefinition :
OptionsData.getAllOptionDefinitionsForClass(optionsBase)) {
result.add(optionDefinition.getOptionName());
}
return result;
}
@Test
public void getFieldsForClassIsOrdered() throws Exception {
assertThat(getOptionNames(FieldNamesDifferOptions.class))
.containsExactly("bar", "baz", "foo", "qux")
.inOrder();
assertThat(getOptionNames(EndOfAlphabetOptions.class)).containsExactly("X", "Y").inOrder();
assertThat(getOptionNames(ReverseOrderedOptions.class))
.containsExactly("A", "B", "C")
.inOrder();
}
@Test
public void optionsDefinitionsAreSharedBetweenOptionsBases() throws Exception {
Class<FieldNamesDifferOptions> class1 = FieldNamesDifferOptions.class;
Class<EndOfAlphabetOptions> class2 = EndOfAlphabetOptions.class;
Class<ReverseOrderedOptions> class3 = ReverseOrderedOptions.class;
// Construct the definitions once and accumulate them so we can test that these are not
// recomputed during the construction of the options data.
ImmutableList<OptionDefinition> optionDefinitions =
new ImmutableList.Builder<OptionDefinition>()
.addAll(OptionsData.getAllOptionDefinitionsForClass(class1))
.addAll(OptionsData.getAllOptionDefinitionsForClass(class2))
.addAll(OptionsData.getAllOptionDefinitionsForClass(class3))
.build();
// Construct the data all together.
IsolatedOptionsData data = construct(class1, class2, class3);
ArrayList<OptionDefinition> optionDefinitionsFromData =
new ArrayList<>(optionDefinitions.size());
data.getAllOptionDefinitions()
.forEach(entry -> optionDefinitionsFromData.add(entry.getValue()));
Correspondence<Object, Object> referenceEquality =
Correspondence.from((obj1, obj2) -> obj1 == obj2, "is the same object as");
assertThat(optionDefinitionsFromData)
.comparingElementsUsing(referenceEquality)
.containsAtLeastElementsIn(optionDefinitions);
// Construct options data for each class separately, and check again.
IsolatedOptionsData data1 = construct(class1);
IsolatedOptionsData data2 = construct(class2);
IsolatedOptionsData data3 = construct(class3);
ArrayList<OptionDefinition> optionDefinitionsFromGroupedData =
new ArrayList<>(optionDefinitions.size());
data1
.getAllOptionDefinitions()
.forEach(entry -> optionDefinitionsFromGroupedData.add(entry.getValue()));
data2
.getAllOptionDefinitions()
.forEach(entry -> optionDefinitionsFromGroupedData.add(entry.getValue()));
data3
.getAllOptionDefinitions()
.forEach(entry -> optionDefinitionsFromGroupedData.add(entry.getValue()));
assertThat(optionDefinitionsFromGroupedData)
.comparingElementsUsing(referenceEquality)
.containsAtLeastElementsIn(optionDefinitions);
}
/** Dummy options class. */
public static class ValidExpansionOptions extends OptionsBase {
@Option(
name = "foo",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "1"
)
public int foo;
@Option(
name = "bar",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "null",
expansion = {"--foo=42"}
)
public Void bar;
}
@Test
public void staticExpansionOptionsCanBeVoidType() {
construct(ValidExpansionOptions.class);
}
}