aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test/java/com/google/devtools/common/options/testing
diff options
context:
space:
mode:
authorGravatar mstaib <mstaib@google.com>2017-07-19 21:50:20 +0200
committerGravatar Klaus Aehlig <aehlig@google.com>2017-07-20 10:29:13 +0200
commit2cb56d53efb3964f1bd3ab3cb19f43ae7a2fdce0 (patch)
treea3fd9cbdf4ae538ccef5bd34c36c4fdf64bed7b9 /src/test/java/com/google/devtools/common/options/testing
parentd1e564bbe72c9de5f22e5b6dc8af26ce7520bbc8 (diff)
Add test framework for OptionsBase classes and their Converters.
Because OptionsBase implements equals() as a final method, subclasses can only add fields in certain ways for OptionsBase to properly obey equals() semantics. Specifically, all fields must be public and @Option annotated. The OptionsTester checks for these two things. Additionally, Converters must make sure to always return equals() values on equals() (or equivalent) input. The OptionsTester includes a check that all Converters named by the OptionsBase subclass being tested have matching ConverterTesters, and if valid default values are specified (i.e., on Options which are not multi-valued or default null), that these defaults are among the values tested by the ConverterTesters. The ConverterTesters themselves are wrapped EqualsTesters, testing that the output of a Converter obeys equals() as expected for the same input (or equivalent ones), and is consistent across calls to the same Converter instance or different Converter instances. Between these two, OptionsBase subclasses can have reasonable certainty that two instances of themselves which were parsed equally - or underwent equivalent transformations - will be equal. This does not actually test any OptionsBase subclasses or Converter implementations; it merely adds a framework. Future changes will cover automatically testing all of the OptionsBase subclasses in a RuleClassProvider, but naturally, this requires writing test data for each Converter in the Bazel codebase first. RELNOTES: None. PiperOrigin-RevId: 162522445
Diffstat (limited to 'src/test/java/com/google/devtools/common/options/testing')
-rw-r--r--src/test/java/com/google/devtools/common/options/testing/AllTests.java25
-rw-r--r--src/test/java/com/google/devtools/common/options/testing/BUILD38
-rw-r--r--src/test/java/com/google/devtools/common/options/testing/ConverterTesterMapTest.java120
-rw-r--r--src/test/java/com/google/devtools/common/options/testing/ConverterTesterTest.java407
-rw-r--r--src/test/java/com/google/devtools/common/options/testing/OptionsTesterTest.java434
5 files changed, 1024 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/common/options/testing/AllTests.java b/src/test/java/com/google/devtools/common/options/testing/AllTests.java
new file mode 100644
index 0000000000..4edbfda878
--- /dev/null
+++ b/src/test/java/com/google/devtools/common/options/testing/AllTests.java
@@ -0,0 +1,25 @@
+// Copyright 2014 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.testing;
+
+import com.google.devtools.build.lib.testutil.ClasspathSuite;
+
+import org.junit.runner.RunWith;
+
+/**
+ * Test suite for options parsing framework.
+ */
+@RunWith(ClasspathSuite.class)
+public class AllTests {
+}
diff --git a/src/test/java/com/google/devtools/common/options/testing/BUILD b/src/test/java/com/google/devtools/common/options/testing/BUILD
new file mode 100644
index 0000000000..0e2d3e15cc
--- /dev/null
+++ b/src/test/java/com/google/devtools/common/options/testing/BUILD
@@ -0,0 +1,38 @@
+# Description:
+# Tests of the testing tools for the devtools-common options parser.
+package(
+ default_testonly = 1,
+ default_visibility = ["//visibility:private"],
+)
+
+licenses(["notice"]) # Apache 2.0
+
+filegroup(
+ name = "srcs",
+ testonly = 0,
+ srcs = glob(
+ ["**"],
+ ),
+ visibility = ["//src/test/java/com/google/devtools/common/options:__pkg__"],
+)
+
+java_library(
+ name = "OptionsTesterTest_lib",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/main/java/com/google/devtools/common/options",
+ "//src/main/java/com/google/devtools/common/options/testing",
+ "//src/main/protobuf:option_filters_java_proto",
+ "//src/test/java/com/google/devtools/build/lib:testutil",
+ "//third_party:guava",
+ "//third_party:guava-testlib",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
+java_test(
+ name = "OptionsTesterTest",
+ test_class = "com.google.devtools.common.options.testing.AllTests",
+ runtime_deps = [":OptionsTesterTest_lib"],
+)
diff --git a/src/test/java/com/google/devtools/common/options/testing/ConverterTesterMapTest.java b/src/test/java/com/google/devtools/common/options/testing/ConverterTesterMapTest.java
new file mode 100644
index 0000000000..0265f13b6a
--- /dev/null
+++ b/src/test/java/com/google/devtools/common/options/testing/ConverterTesterMapTest.java
@@ -0,0 +1,120 @@
+// 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.testing;
+
+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.Converters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the ConverterTesterMap map builder. */
+@RunWith(JUnit4.class)
+public final class ConverterTesterMapTest {
+
+ @Test
+ public void add_mapsTestedConverterClassToTester() throws Exception {
+ ConverterTester stringTester = new ConverterTester(Converters.StringConverter.class);
+ ConverterTester intTester = new ConverterTester(Converters.IntegerConverter.class);
+ ConverterTester doubleTester = new ConverterTester(Converters.DoubleConverter.class);
+ ConverterTester booleanTester = new ConverterTester(Converters.BooleanConverter.class);
+ ConverterTesterMap map =
+ new ConverterTesterMap.Builder()
+ .add(stringTester)
+ .add(intTester)
+ .add(doubleTester)
+ .add(booleanTester)
+ .build();
+ assertThat(map)
+ .containsExactly(
+ Converters.StringConverter.class,
+ stringTester,
+ Converters.IntegerConverter.class,
+ intTester,
+ Converters.DoubleConverter.class,
+ doubleTester,
+ Converters.BooleanConverter.class,
+ booleanTester);
+ }
+
+ @Test
+ public void addAll_mapsTestedConverterClassesToTester() throws Exception {
+ ConverterTester stringTester = new ConverterTester(Converters.StringConverter.class);
+ ConverterTester intTester = new ConverterTester(Converters.IntegerConverter.class);
+ ConverterTester doubleTester = new ConverterTester(Converters.DoubleConverter.class);
+ ConverterTester booleanTester = new ConverterTester(Converters.BooleanConverter.class);
+ ConverterTesterMap map =
+ new ConverterTesterMap.Builder()
+ .addAll(ImmutableList.of(stringTester, intTester, doubleTester, booleanTester))
+ .build();
+ assertThat(map)
+ .containsExactly(
+ Converters.StringConverter.class,
+ stringTester,
+ Converters.IntegerConverter.class,
+ intTester,
+ Converters.DoubleConverter.class,
+ doubleTester,
+ Converters.BooleanConverter.class,
+ booleanTester);
+ }
+
+ @Test
+ public void addAll_dumpsConverterTesterMapIntoNewMap() throws Exception {
+ ConverterTester stringTester = new ConverterTester(Converters.StringConverter.class);
+ ConverterTester intTester = new ConverterTester(Converters.IntegerConverter.class);
+ ConverterTester doubleTester = new ConverterTester(Converters.DoubleConverter.class);
+ ConverterTester booleanTester = new ConverterTester(Converters.BooleanConverter.class);
+ ConverterTesterMap baseMap =
+ new ConverterTesterMap.Builder()
+ .addAll(ImmutableList.of(stringTester, intTester, doubleTester))
+ .build();
+ ConverterTesterMap map =
+ new ConverterTesterMap.Builder().addAll(baseMap).add(booleanTester).build();
+ assertThat(map)
+ .containsExactly(
+ Converters.StringConverter.class,
+ stringTester,
+ Converters.IntegerConverter.class,
+ intTester,
+ Converters.DoubleConverter.class,
+ doubleTester,
+ Converters.BooleanConverter.class,
+ booleanTester);
+ }
+
+ @Test
+ public void build_forbidsDuplicates() throws Exception {
+ ConverterTesterMap.Builder builder =
+ new ConverterTesterMap.Builder()
+ .add(new ConverterTester(Converters.StringConverter.class))
+ .add(new ConverterTester(Converters.IntegerConverter.class))
+ .add(new ConverterTester(Converters.DoubleConverter.class))
+ .add(new ConverterTester(Converters.BooleanConverter.class))
+ .add(new ConverterTester(Converters.BooleanConverter.class));
+
+ try {
+ builder.build();
+ fail("expected build() with duplicate to fail");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .contains(Converters.BooleanConverter.class.getSimpleName());
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/common/options/testing/ConverterTesterTest.java b/src/test/java/com/google/devtools/common/options/testing/ConverterTesterTest.java
new file mode 100644
index 0000000000..947791dc87
--- /dev/null
+++ b/src/test/java/com/google/devtools/common/options/testing/ConverterTesterTest.java
@@ -0,0 +1,407 @@
+// 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.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters;
+import com.google.devtools.common.options.OptionsParsingException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests to exercise the functionality of {@link ConverterTester}. */
+@RunWith(JUnit4.class)
+public final class ConverterTesterTest {
+
+ @Test
+ public void construction_throwsAssertionErrorIfConverterCreationFails() throws Exception {
+ try {
+ new ConverterTester(UnconstructableConverter.class);
+ } catch (AssertionError expected) {
+ assertThat(expected) // AssertionError
+ .hasCauseThat() // InvocationTargetException
+ .hasCauseThat() // UnsupportedOperationException
+ .hasMessageThat()
+ .contains("YOU CAN'T MAKE ME!");
+ return;
+ }
+ fail("expected tester creation to fail");
+ }
+
+ /** Test converter for construction_throwsAssertionErrorIfConverterCreationFails. */
+ public static final class UnconstructableConverter implements Converter<String> {
+ public UnconstructableConverter() {
+ throw new UnsupportedOperationException("YOU CAN'T MAKE ME!");
+ }
+
+ @Override
+ public String convert(String input) throws OptionsParsingException {
+ return input;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "anything, if you can get an instance";
+ }
+ }
+
+ @Test
+ public void getConverterClass_returnsConstructorArg() throws Exception {
+ ConverterTester tester = new ConverterTester(Converters.BooleanConverter.class);
+ assertThat(tester.getConverterClass()).isEqualTo(Converters.BooleanConverter.class);
+ }
+
+ @Test
+ public void hasTestForInput_returnsTrueIffInputPassedToAddEqualityGroup() throws Exception {
+ ConverterTester tester =
+ new ConverterTester(Converters.DoubleConverter.class)
+ .addEqualityGroup("1.0", "1", "1.00")
+ .addEqualityGroup("2");
+
+ assertThat(tester.hasTestForInput("1.0")).isTrue();
+ assertThat(tester.hasTestForInput("1")).isTrue();
+ assertThat(tester.hasTestForInput("1.00")).isTrue();
+ assertThat(tester.hasTestForInput("2")).isTrue();
+
+ assertThat(tester.hasTestForInput("3")).isFalse();
+ assertThat(tester.hasTestForInput("1.000")).isFalse();
+ assertThat(tester.hasTestForInput("not a double")).isFalse();
+ }
+
+ @Test
+ public void addEqualityGroup_throwsIfConversionFails() throws Exception {
+ ConverterTester tester =
+ new ConverterTester(ThrowingConverter.class)
+ .addEqualityGroup("okay")
+ .addEqualityGroup("also okay", "pretty fine");
+ try {
+ tester.addEqualityGroup("wrong");
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"wrong\"");
+ assertThat(expected).hasCauseThat().hasMessageThat().contains("HOW DARE YOU");
+ return;
+ }
+ fail("expected addEqualityGroup to fail");
+ }
+
+ /** Test converter for addEqualityGroup_throwsIfConversionFails. */
+ public static final class ThrowingConverter implements Converter<String> {
+ @Override
+ public String convert(String input) throws OptionsParsingException {
+ if ("wrong".equals(input)) {
+ throw new OptionsParsingException("HOW DARE YOU");
+ }
+ return input;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "just don't give the wrong answer";
+ }
+ }
+
+ @Test
+ public void testConvert_passesWhenAllInstancesObeyEqualsAndItemsOnlyEqualToOthersInSameGroup() {
+ new ConverterTester(Converters.DoubleConverter.class)
+ .addEqualityGroup("1.0", "1", "1.00")
+ .addEqualityGroup("2", "2", "2.0000", "2.0", "+2")
+ .addEqualityGroup("3")
+ .addEqualityGroup("3.1415")
+ .testConvert();
+ }
+
+ @Test
+ public void testConvert_testsHashCodeConsistencyForConvertedInstance() {
+ ConverterTester tester =
+ new ConverterTester(InconsistentHashCodeConverter.class)
+ .addEqualityGroup("input doesn't matter");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"input doesn't matter\"");
+ assertThat(expected).hasMessageThat().contains("hashCode");
+ assertThat(expected).hasMessageThat().contains("must be consistent");
+ return;
+ }
+ fail("expected the tester to notice the bad hash code implementation");
+ }
+
+ /** A class with a badly implemented hashCode which is not consistent across calls. */
+ public static final class InconsistentHashCode {
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof InconsistentHashCode;
+ }
+
+ private int howManyTimesHaveIBeenHashedAlready = 0;
+
+ @Override
+ public int hashCode() {
+ howManyTimesHaveIBeenHashedAlready += 1;
+ return howManyTimesHaveIBeenHashedAlready;
+ }
+ }
+
+ /** Test converter for testConvert_testsHashCodeConsistencyForConvertedInstance. */
+ public static final class InconsistentHashCodeConverter
+ implements Converter<InconsistentHashCode> {
+ @Override
+ public InconsistentHashCode convert(String input) throws OptionsParsingException {
+ return new InconsistentHashCode();
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "anything, I don't even look at it";
+ }
+ }
+
+ @Test
+ public void testConvert_testsHashCodeConsistencyForSameConverter() {
+ ConverterTester tester =
+ new ConverterTester(IncrementingHashCodeConverter.class)
+ .addEqualityGroup("meaningless input");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"meaningless input\"");
+ assertThat(expected).hasMessageThat().contains("consistent hashCode");
+ assertThat(expected).hasMessageThat().contains("same Converter");
+ return;
+ }
+ fail("expected the tester to notice the mismatched hash codes");
+ }
+
+ /** A class with a configurable hashCode set in the constructor. */
+ public static final class SettableHashCode {
+ private final int hashCode;
+
+ public SettableHashCode(int hashCode) {
+ this.hashCode = hashCode;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof SettableHashCode;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+ }
+
+ /** Test converter for testConvert_testsHashCodeConsistencyForSameConverter. */
+ public static final class IncrementingHashCodeConverter
+ implements Converter<SettableHashCode> {
+ private int howManyInstancesHaveIMadeAlready = 0;
+
+ @Override
+ public SettableHashCode convert(String input) throws OptionsParsingException {
+ howManyInstancesHaveIMadeAlready += 1;
+ return new SettableHashCode(howManyInstancesHaveIMadeAlready);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "whatever, I'm pretty much just going to ignore it";
+ }
+ }
+
+ @Test
+ public void testConvert_testsHashCodeConsistencyForDifferentConverters() {
+ ConverterTester tester =
+ new ConverterTester(StaticIncrementingHashCodeConverter.class)
+ .addEqualityGroup("some kind of input");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"some kind of input\"");
+ assertThat(expected).hasMessageThat().contains("consistent hashCode");
+ assertThat(expected).hasMessageThat().contains("different Converter");
+ return;
+ }
+ fail("expected the tester to notice the mismatched hash codes");
+ }
+
+ /** Test converter for testConvert_testsHashCodeConsistencyForDifferentConverters. */
+ public static final class StaticIncrementingHashCodeConverter
+ implements Converter<SettableHashCode> {
+ private static int howManyInstancesHaveIMadeAlready = 0;
+
+ private final int hashCode;
+
+ public StaticIncrementingHashCodeConverter() {
+ howManyInstancesHaveIMadeAlready += 1;
+ this.hashCode = howManyInstancesHaveIMadeAlready;
+ }
+
+ @Override
+ public SettableHashCode convert(String input) throws OptionsParsingException {
+ return new SettableHashCode(hashCode);
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a string or null, I'm easy";
+ }
+ }
+
+
+ @Test
+ public void testConvert_testsSelfEqualityForConvertedInstance() {
+ ConverterTester tester =
+ new ConverterTester(SelfLoathingConverter.class)
+ .addEqualityGroup("self-loathing");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"self-loathing\"");
+ assertThat(expected).hasMessageThat().contains("must be Object#equals to itself");
+ return;
+ }
+ fail("expected the tester to notice the bad equals implementation");
+ }
+
+ /** A class which is equal to every instance of its class except itself. */
+ public static final class SelfLoathingObject {
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof SelfLoathingObject && other != this;
+ }
+
+ @Override
+ public int hashCode() {
+ return 4; // chosen by fair hashing algorithm
+ }
+ }
+
+ /** Test converter for testConvert_testsSelfEqualityForConvertedInstance. */
+ public static final class SelfLoathingConverter implements Converter<SelfLoathingObject> {
+ @Override
+ public SelfLoathingObject convert(String input) throws OptionsParsingException {
+ return new SelfLoathingObject();
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "whatever... why even ask me to convert anything..............";
+ }
+ }
+
+ @Test
+ public void testConvert_testsEqualityForSameConverter() {
+ ConverterTester tester =
+ new ConverterTester(CountingConverter.class)
+ .addEqualityGroup("countables");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"countables\"");
+ assertThat(expected).hasMessageThat().contains("equal to itself");
+ assertThat(expected).hasMessageThat().contains("same Converter");
+ return;
+ }
+ fail("expected the tester to notice the converter giving unequal objects");
+ }
+
+ /** Test converter for testConvert_testsEqualityForSameConverter. */
+ public static final class CountingConverter implements Converter<Integer> {
+ private int howManyInstancesHaveIMadeAlready = 0;
+
+ @Override
+ public Integer convert(String input) throws OptionsParsingException {
+ howManyInstancesHaveIMadeAlready += 1;
+ return howManyInstancesHaveIMadeAlready;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "I can count anything!";
+ }
+ }
+
+ @Test
+ public void testConvert_testsEqualityForDifferentConverters() {
+ ConverterTester tester =
+ new ConverterTester(StaticCountingConverter.class)
+ .addEqualityGroup("words I like");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"words I like\"");
+ assertThat(expected).hasMessageThat().contains("equal to itself");
+ assertThat(expected).hasMessageThat().contains("different Converter");
+ return;
+ }
+ fail("expected the tester to notice the converters giving unequal objects");
+ }
+
+ /** Test converter for testConvert_testsEqualityForDifferentConverters. */
+ public static final class StaticCountingConverter implements Converter<Integer> {
+ private static int howManyInstancesHaveIMadeAlready = 0;
+
+ private final int output;
+
+ public StaticCountingConverter() {
+ howManyInstancesHaveIMadeAlready += 1;
+ this.output = howManyInstancesHaveIMadeAlready;
+ }
+
+ @Override
+ public Integer convert(String input) throws OptionsParsingException {
+ return output;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "your favorite text";
+ }
+ }
+
+ @Test
+ public void testConvert_testsEqualityForItemsInSameGroup() {
+ ConverterTester tester =
+ new ConverterTester(Converters.DoubleConverter.class)
+ .addEqualityGroup("+1.000", "2.30");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"+1.000\"");
+ assertThat(expected).hasMessageThat().contains("\"2.30\"");
+ return;
+ }
+ fail("expected the tester to notice the two non-equal conversion results in the same group");
+ }
+
+ @Test
+ public void testConvert_testsNonEqualityForItemsInDifferentGroups() {
+ ConverterTester tester =
+ new ConverterTester(Converters.DoubleConverter.class)
+ .addEqualityGroup("+1.000")
+ .addEqualityGroup("1.0");
+ try {
+ tester.testConvert();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("\"+1.000\"");
+ assertThat(expected).hasMessageThat().contains("\"1.0\"");
+ return;
+ }
+ fail("expected the tester to notice the two equal conversion results in different groups");
+ }
+}
diff --git a/src/test/java/com/google/devtools/common/options/testing/OptionsTesterTest.java b/src/test/java/com/google/devtools/common/options/testing/OptionsTesterTest.java
new file mode 100644
index 0000000000..6c350ee9d1
--- /dev/null
+++ b/src/test/java/com/google/devtools/common/options/testing/OptionsTesterTest.java
@@ -0,0 +1,434 @@
+// 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.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the OptionsTester. */
+@RunWith(JUnit4.class)
+public final class OptionsTesterTest {
+
+ @Test
+ public void optionAnnotationCheck_PassesWhenAllFieldsAnnotated() throws Exception {
+ new OptionsTester(OptionAnnotationCheckAllFieldsAnnotated.class)
+ .testAllInstanceFieldsAnnotatedWithOption();
+ }
+
+ /** Test options class for optionAnnotationCheck_PassesWhenAllFieldsAnnotated. */
+ public static class BaseAllFieldsAnnotated extends OptionsBase {
+ @Option(
+ name = "public inherited field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ public String inheritedField;
+ }
+
+ /** Test options class for optionAnnotationCheck_PassesWhenAllFieldsAnnotated. */
+ public static final class OptionAnnotationCheckAllFieldsAnnotated extends BaseAllFieldsAnnotated {
+ @Option(
+ name = "public declared field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ public String publicField;
+
+ @Option(
+ name = "private declared field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ @SuppressWarnings("unused")
+ private String privateField;
+ }
+
+ @Test
+ public void optionAnnotationCheck_AllowsUnAnnotatedStaticFields() throws Exception {
+ new OptionsTester(OptionAnnotationCheckUnAnnotatedStaticField.class)
+ .testAllInstanceFieldsAnnotatedWithOption();
+ }
+
+ /** Test options class for optionAnnotationCheck_AllowsUnAnnotatedStaticFields. */
+ public static class BaseUnAnnotatedStaticField extends OptionsBase {
+ public static String parentClassUnAnnotatedStaticField;
+ }
+
+ /** Test options class for optionAnnotationCheck_AllowsUnAnnotatedStaticFields. */
+ public static final class OptionAnnotationCheckUnAnnotatedStaticField
+ extends BaseUnAnnotatedStaticField {
+ @SuppressWarnings("unused")
+ private static String privateDeclaredUnAnnotatedStaticField;
+ }
+
+ @Test
+ public void optionAnnotationCheck_FailsWhenFieldNotAnnotated() throws Exception {
+ try {
+ new OptionsTester(OptionAnnotationCheckUnAnnotatedField.class)
+ .testAllInstanceFieldsAnnotatedWithOption();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("unAnnotatedField");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for optionAnnotationCheck_FailsWhenFieldNotAnnotated. */
+ public static class OptionAnnotationCheckUnAnnotatedField extends OptionsBase {
+ public String unAnnotatedField;
+ }
+
+ @Test
+ public void optionAnnotationCheck_FailsForUnAnnotatedFieldsInSuperclass() throws Exception {
+ try {
+ new OptionsTester(OptionAnnotationCheckInheritedUnAnnotatedField.class)
+ .testAllInstanceFieldsAnnotatedWithOption();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("unAnnotatedField");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for optionAnnotationCheck_FailsForUnAnnotatedFieldsInSuperclass. */
+ public static final class OptionAnnotationCheckInheritedUnAnnotatedField
+ extends OptionAnnotationCheckUnAnnotatedField {}
+
+ @Test
+ public void optionAnnotationCheck_FailsForPrivateUnAnnotatedField() throws Exception {
+ try {
+ new OptionsTester(OptionAnnotationCheckPrivateUnAnnotatedField.class)
+ .testAllInstanceFieldsAnnotatedWithOption();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("privateUnAnnotatedField");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for optionAnnotationCheck_FailsForPrivateUnAnnotatedField. */
+ public static final class OptionAnnotationCheckPrivateUnAnnotatedField extends OptionsBase {
+ @SuppressWarnings("unused")
+ private String privateUnAnnotatedField;
+ }
+
+ @Test
+ public void optionAccessCheck_PassesWhenAllFieldsPublicNotStaticNotFinal() throws Exception {
+ new OptionsTester(OptionAccessCheckAllFieldsPublicNotStaticNotFinal.class)
+ .testAllOptionFieldsPublic();
+ }
+
+ /** Test options class for optionAccessCheck_PassesWhenAllFieldsPublicNotStaticNotFinal. */
+ public static class BaseAllFieldsPublicNotStaticNotFinal extends OptionsBase {
+ @Option(
+ name = "public inherited field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ public String inheritedField;
+ }
+
+ /** Test options class for optionAccessCheck_PassesWhenAllFieldsPublicNotStaticNotFinal. */
+ public static final class OptionAccessCheckAllFieldsPublicNotStaticNotFinal
+ extends BaseAllFieldsPublicNotStaticNotFinal {
+ @Option(
+ name = "public declared field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ public String annotatedField;
+ }
+
+ @Test
+ public void optionAccessCheck_IgnoresNonAnnotatedFields() throws Exception {
+ new OptionsTester(OptionAccessCheckNonAnnotatedFields.class).testAllOptionFieldsPublic();
+ }
+
+ /** Test options class for optionAccessCheck_IgnoresNonAnnotatedFields. */
+ public static class BaseNonAnnotatedFields extends OptionsBase {
+ protected static String parentClassUnAnnotatedStaticField;
+ final String parentClassUnAnnotatedFinalField = "";
+ }
+
+ /** Test options class for optionAccessCheck_IgnoresNonAnnotatedFields. */
+ public static final class OptionAccessCheckNonAnnotatedFields extends BaseNonAnnotatedFields {
+ @SuppressWarnings("unused")
+ private static String privateDeclaredUnAnnotatedStaticFinalField;
+
+ public static final String PUBLIC_DECLARED_UN_ANNOTATED_STATIC_FIELD = "";
+ protected String protectedDeclaredField;
+ }
+
+ @Test
+ public void optionAccessCheck_FailsForNonPublicFields() throws Exception {
+ try {
+ new OptionsTester(OptionAccessCheckNonPublicStaticField.class).testAllOptionFieldsPublic();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("protectedField");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for optionAccessCheck_FailsForNonPublicFields. */
+ public static class OptionAccessCheckNonPublicStaticField extends OptionsBase {
+ @Option(
+ name = "protected declared field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ protected String protectedField;
+ }
+
+ @Test
+ public void optionAccessCheck_FailsForNonPublicFieldsInSuperclass() throws Exception {
+ try {
+ new OptionsTester(OptionAccessCheckInheritedUnAnnotatedField.class)
+ .testAllOptionFieldsPublic();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("protectedField");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for optionAccessCheck_FailsForNonPublicFieldsInSuperclass. */
+ public static final class OptionAccessCheckInheritedUnAnnotatedField
+ extends OptionAccessCheckNonPublicStaticField {}
+
+ @Test
+ public void optionAccessCheck_FailsForStaticFields() throws Exception {
+ try {
+ new OptionsTester(OptionAccessCheckPublicStaticField.class).testAllOptionFieldsPublic();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("staticField");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for optionAccessCheck_FailsForStaticFields. */
+ public static final class OptionAccessCheckPublicStaticField extends OptionsBase {
+ @Option(
+ name = "public static declared field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ public static String staticField;
+ }
+
+ @Test
+ public void optionAccessCheck_FailsForFinalFields() throws Exception {
+ try {
+ new OptionsTester(OptionAccessCheckPublicFinalField.class).testAllOptionFieldsPublic();
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("finalField");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for optionAccessCheck_FailsForStaticFields. */
+ public static final class OptionAccessCheckPublicFinalField extends OptionsBase {
+ @Option(
+ name = "public final declared field with annotation",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "defaultFoo"
+ )
+ public final String finalField = "";
+ }
+
+ /** Test converter class for testing testAllDefaultValuesTestedBy. */
+ public static final class TestConverter implements Converter<String> {
+ @Override
+ public String convert(String input) {
+ return input;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a string";
+ }
+ }
+
+ @Test
+ public void defaultTestCheck_PassesIfAllDefaultsTestedIgnoringNullAndAllowMultiple() {
+ new OptionsTester(DefaultTestCheck.class)
+ .testAllDefaultValuesTestedBy(
+ new ConverterTesterMap.Builder()
+ .add(
+ new ConverterTester(TestConverter.class)
+ .addEqualityGroup("testedDefault", "otherTestedDefault"))
+ .build());
+ }
+
+ /** Test options class for defaultTestCheck_PassesIfAllDefaultsTested. */
+ public static final class DefaultTestCheck extends OptionsBase {
+ @Option(
+ name = "tested field",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ converter = TestConverter.class,
+ defaultValue = "testedDefault"
+ )
+ public String testedField;
+
+ @Option(
+ name = "field implicitly using default converter",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ defaultValue = "implicitConverterDefault"
+ )
+ public String implicitConverterField;
+
+ @Option(
+ name = "other tested field",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ converter = TestConverter.class,
+ defaultValue = "otherTestedDefault"
+ )
+ public String otherTestedField;
+
+ @Option(
+ name = "field with null default",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ converter = TestConverter.class,
+ defaultValue = "null"
+ )
+ public String nullDefaultField;
+
+ @Option(
+ name = "allowMultiple field",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ converter = TestConverter.class,
+ defaultValue = "allowMultipleDefault",
+ allowMultiple = true
+ )
+ public List<String> allowMultipleField;
+ }
+
+ @Test
+ public void defaultTestCheck_FailsIfTesterIsPresentButValueIsNotTested() {
+ try {
+ new OptionsTester(DefaultTestCheckUntestedField.class)
+ .testAllDefaultValuesTestedBy(
+ new ConverterTesterMap.Builder()
+ .add(new ConverterTester(TestConverter.class).addEqualityGroup("testedDefault"))
+ .build());
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("untestedField");
+ assertThat(expected).hasMessageThat().contains("untestedDefault");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ @Test
+ public void defaultTestCheck_FailsIfTesterIsAbsent() {
+ try {
+ new OptionsTester(DefaultTestCheckUntestedField.class)
+ .testAllDefaultValuesTestedBy(new ConverterTesterMap.Builder().build());
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("TestConverter");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /**
+ * Test options class for defaultTestCheck_FailsIfTesterIsPresentButValueIsNotTested and
+ * defaultTestCheck_FailsIfTesterIsAbsent.
+ */
+ public static final class DefaultTestCheckUntestedField extends OptionsBase {
+ @Option(
+ name = "untested field",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ converter = TestConverter.class,
+ defaultValue = "untestedDefault"
+ )
+ public String untestedField;
+ }
+
+ @Test
+ public void defaultTestCheck_FailsIfTesterIsAbsentEvenForNullDefault() {
+ try {
+ new OptionsTester(DefaultTestCheckUntestedNullField.class)
+ .testAllDefaultValuesTestedBy(new ConverterTesterMap.Builder().build());
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("TestConverter");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for defaultTestCheck_FailsIfTesterIsAbsentEvenForNullDefault. */
+ public static final class DefaultTestCheckUntestedNullField extends OptionsBase {
+ @Option(
+ name = "untested field",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ converter = TestConverter.class,
+ defaultValue = "null"
+ )
+ public String untestedNullField;
+ }
+
+ @Test
+ public void defaultTestCheck_FailsIfTesterIsAbsentEvenForAllowMultiple() {
+ try {
+ new OptionsTester(DefaultTestCheckUntestedMultipleField.class)
+ .testAllDefaultValuesTestedBy(new ConverterTesterMap.Builder().build());
+ } catch (AssertionError expected) {
+ assertThat(expected).hasMessageThat().contains("TestConverter");
+ return;
+ }
+ fail("test is expected to have failed");
+ }
+
+ /** Test options class for defaultTestCheck_FailsIfTesterIsAbsentEvenForAllowMultiple. */
+ public static final class DefaultTestCheckUntestedMultipleField extends OptionsBase {
+ @Option(
+ name = "untested field",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.NO_OP},
+ converter = TestConverter.class,
+ defaultValue = "untestedDefault",
+ allowMultiple = true
+ )
+ public List<String> untestedMultipleField;
+ }
+}