blob: 5090fab1d4e37efaf835c7ac759868239d6236c8 [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 org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
import com.google.devtools.common.options.OptionsParser.ConstructionException;
import java.lang.reflect.Field;
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() {
try {
construct(ExampleNameConflictOptions.class);
fail("foo should conflict with the previous flag foo");
} catch (DuplicateOptionDeclarationException e) {
assertThat(e).hasMessageThat().contains("Duplicate option name, due to option: --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() {
try {
construct(ExampleIntegerFooOptions.class, ExampleBooleanFooOptions.class);
fail("foo should conflict with the previous flag foo");
} catch (DuplicateOptionDeclarationException e) {
assertThat(e).hasMessageThat().contains("Duplicate option name, due to option: --foo");
}
}
/** Dummy options class. */
public static class ExamplePrefixFooOptions 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.
try {
construct(ExampleBooleanFooOptions.class, ExamplePrefixFooOptions.class);
fail("nofoo should conflict with the previous flag foo, "
+ "since foo, as a boolean flag, can be written as --nofoo");
} catch (DuplicateOptionDeclarationException e) {
assertThat(e)
.hasMessageThat()
.contains(
"Duplicate option name, due to option --nofoo, it "
+ "conflicts with a negating alias for boolean flag --foo");
}
try {
construct(ExamplePrefixFooOptions.class, ExampleBooleanFooOptions.class);
fail("nofoo should conflict with the previous flag foo, "
+ "since foo, as a boolean flag, can be written as --nofoo");
} catch (DuplicateOptionDeclarationException e) {
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.
try {
construct(ExamplePrefixFooOptions.class, ExampleBarWasNamedFooOption.class);
fail("nofoo should conflict with the previous flag foo, "
+ "since foo, as a boolean flag, can be written as --nofoo");
} catch (DuplicateOptionDeclarationException e) {
assertThat(e)
.hasMessageThat()
.contains("Duplicate option name, due to boolean option alias: --nofoo");
}
}
/** 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.
try {
construct(ExampleBooleanFooOptions.class, ExampleBarWasNamedNoFooOption.class);
fail("nofoo, the old name for bar, should conflict with the previous flag foo, "
+ "since foo, as a boolean flag, can be written as --nofoo");
} catch (DuplicateOptionDeclarationException e) {
assertThat(e)
.hasMessageThat()
.contains(
"Duplicate option name, due to old option name --nofoo, it conflicts with a "
+ "negating alias for boolean flag --foo");
}
}
/** 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 = "old_name",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "defaultValue"
)
public String flag2;
}
@Test
public void testOldNameConflict() {
try {
construct(OldNameConflictExample.class);
fail("old_name should conflict with the flag already named old_name");
} catch (DuplicateOptionDeclarationException expected) {
}
}
/** 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. */
public static class InvalidOptionConverter extends OptionsBase {
@Option(
name = "foo",
converter = StringConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "1"
)
public Integer foo;
}
@Test
public void errorForInvalidOptionConverter() throws Exception {
try {
construct(InvalidOptionConverter.class);
} catch (ConstructionException e) {
// Expected exception
return;
}
fail();
}
/** Dummy options class. */
public static class InvalidListOptionConverter extends OptionsBase {
@Option(
name = "foo",
converter = StringConverter.class,
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "1",
allowMultiple = true
)
public List<Integer> foo;
}
@Test
public void errorForInvalidListOptionConverter() throws Exception {
try {
construct(InvalidListOptionConverter.class);
} catch (ConstructionException e) {
// Expected exception
return;
}
fail();
}
/** Dummy options class using deprecated category. */
public static class InvalidUndocumentedCategory extends OptionsBase {
@Option(
name = "experimental_foo",
category = "undocumented",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.NO_OP},
defaultValue = "true"
)
public boolean experimentalFoo;
}
@Test
public void invalidUndocumentedCategoryFails() {
try {
construct(InvalidUndocumentedCategory.class);
fail();
} catch (ConstructionException e) {
// Expected exception
assertThat(e).hasMessageThat().contains(
"Documentation level is no longer read from the option category.");
assertThat(e).hasMessageThat().contains("undocumented");
assertThat(e).hasMessageThat().contains("experimental_foo");
}
}
/**
* 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, Field> entry : data.getAllNamedFields()) {
names.add(entry.getKey());
}
assertThat(names).containsExactly(
"bar", "baz", "foo", "qux", "X", "Y", "A", "B", "C").inOrder();
}
private List<String> getOptionNames(Iterable<Field> fields) {
ArrayList<String> result = new ArrayList<>();
for (Field field : fields) {
result.add(field.getAnnotation(Option.class).name());
}
return result;
}
@Test
public void getFieldsForClassIsOrdered() throws Exception {
IsolatedOptionsData data = construct(
FieldNamesDifferOptions.class,
EndOfAlphabetOptions.class,
ReverseOrderedOptions.class);
assertThat(getOptionNames(data.getFieldsForClass(FieldNamesDifferOptions.class)))
.containsExactly("bar", "baz", "foo", "qux").inOrder();
assertThat(getOptionNames(data.getFieldsForClass(EndOfAlphabetOptions.class)))
.containsExactly("X", "Y").inOrder();
assertThat(getOptionNames(data.getFieldsForClass(ReverseOrderedOptions.class)))
.containsExactly("A", "B", "C").inOrder();
}
/** Dummy options class. */
public static class InvalidExpansionOptions 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 = "1",
expansion = {"--foo=42"}
)
public int bar;
}
@Test
public void staticExpansionOptionsShouldNotHaveValues() {
try {
construct(InvalidExpansionOptions.class);
fail();
} catch (ConstructionException e) {
// Expected exception
assertThat(e).hasMessageThat().contains(
"Option bar is an expansion flag with a static expansion, but does not have Void type.");
}
}
/** 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);
}
}