aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/common/options/testing/ConverterTester.java
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/main/java/com/google/devtools/common/options/testing/ConverterTester.java
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/main/java/com/google/devtools/common/options/testing/ConverterTester.java')
-rw-r--r--src/main/java/com/google/devtools/common/options/testing/ConverterTester.java198
1 files changed, 198 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/common/options/testing/ConverterTester.java b/src/main/java/com/google/devtools/common/options/testing/ConverterTester.java
new file mode 100644
index 0000000000..7d0b8d3974
--- /dev/null
+++ b/src/main/java/com/google/devtools/common/options/testing/ConverterTester.java
@@ -0,0 +1,198 @@
+// 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.assertWithMessage;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.OptionsParsingException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+
+/**
+ * A tester to confirm that {@link Converter} instances produce equal results on multiple calls with
+ * the same input.
+ */
+public final class ConverterTester {
+
+ private final Converter<?> converter;
+ private final Class<? extends Converter<?>> converterClass;
+ private final EqualsTester tester = new EqualsTester();
+ private final LinkedHashSet<String> testedInputs = new LinkedHashSet<>();
+ private final ArrayList<ImmutableList<String>> inputLists = new ArrayList<>();
+
+ /** Creates a new ConverterTester which will test the given Converter class. */
+ public ConverterTester(Class<? extends Converter<?>> converterClass) {
+ this.converterClass = converterClass;
+ this.converter = createConverter();
+ }
+
+ private Converter<?> createConverter() {
+ try {
+ return converterClass.getDeclaredConstructor().newInstance();
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError("Failed to create converter", ex);
+ }
+ }
+
+ /** Returns the class this ConverterTester is testing. */
+ public Class<? extends Converter<?>> getConverterClass() {
+ return converterClass;
+ }
+
+ /**
+ * Returns whether this ConverterTester has a test for the given input, i.e., addEqualityGroup
+ * was called with the given string.
+ */
+ public boolean hasTestForInput(String input) {
+ return testedInputs.contains(input);
+ }
+
+ /**
+ * Adds a set of valid inputs which are expected to convert to equal values.
+ *
+ * <p>The inputs added here will be converted to values using the Converter class passed to the
+ * constructor of this instance; the resulting values must be equal (and have equal hashCodes):
+ *
+ * <ul>
+ * <li>to themselves
+ * <li>to another copy of themselves generated from the same Converter instance
+ * <li>to another copy of themselves generated from a different Converter instance
+ * <li>to the other values converted from inputs in the same addEqualityGroup call
+ * </ul>
+ *
+ * <p>They must NOT be equal:
+ *
+ * <ul>
+ * <li>to null
+ * <li>to an instance of an arbitrary class
+ * <li>to any values converted from inputs in a different addEqualityGroup call
+ * </ul>
+ *
+ * @throws AssertionError if an {@link OptionsParsingException} is thrown from the
+ * {@link Converter#convert} method when converting any of the inputs.
+ * @see EqualsTester#addEqualityGroup
+ */
+ public ConverterTester addEqualityGroup(String... inputs) {
+ ImmutableList.Builder<WrappedItem> wrapped = new ImmutableList.Builder<>();
+ ImmutableList<String> inputList = ImmutableList.copyOf(inputs);
+ inputLists.add(inputList);
+ for (String input : inputList) {
+ testedInputs.add(input);
+ try {
+ wrapped.add(new WrappedItem(input, converter.convert(input)));
+ } catch (OptionsParsingException ex) {
+ throw new AssertionError("Failed to parse input: \"" + input + "\"", ex);
+ }
+ }
+ tester.addEqualityGroup(wrapped.build().toArray());
+ return this;
+ }
+
+ /**
+ * Tests the convert method of the wrapped Converter class, verifying the properties listed in the
+ * Javadoc listed for {@link #addEqualityGroup}.
+ *
+ * @throws AssertionError if one of the expected properties did not hold up
+ * @see EqualsTester#testEquals
+ */
+ public ConverterTester testConvert() {
+ tester.testEquals();
+ testItems();
+ return this;
+ }
+
+ private void testItems() {
+ for (ImmutableList<String> inputList : inputLists) {
+ for (String input : inputList) {
+ Converter<?> converter = createConverter();
+ Converter<?> converter2 = createConverter();
+
+ Object converted;
+ Object convertedAgain;
+ Object convertedDifferentConverterInstance;
+ try {
+ converted = converter.convert(input);
+ convertedAgain = converter.convert(input);
+ convertedDifferentConverterInstance = converter2.convert(input);
+ } catch (OptionsParsingException ex) {
+ throw new AssertionError("Failed to parse input: \"" + input + "\"", ex);
+ }
+
+ assertWithMessage(
+ "Input \""
+ + input
+ + "\" was not equal to itself when converted twice by the same Converter")
+ .that(convertedAgain)
+ .isEqualTo(converted);
+ assertWithMessage(
+ "Input \""
+ + input
+ + "\" did not have a consistent hashCode when converted twice "
+ + "by the same Converter")
+ .that(convertedAgain.hashCode())
+ .isEqualTo(converted.hashCode());
+ assertWithMessage(
+ "Input \""
+ + input
+ + "\" was not equal to itself when converted twice by a different Converter")
+ .that(convertedDifferentConverterInstance)
+ .isEqualTo(converted);
+ assertWithMessage(
+ "Input \""
+ + input
+ + "\" did not have a consistent hashCode when converted twice "
+ + "by a different Converter")
+ .that(convertedDifferentConverterInstance.hashCode())
+ .isEqualTo(converted.hashCode());
+ }
+ }
+ }
+
+ /**
+ * A wrapper around the objects passed to EqualsTester to give them a more useful toString() so
+ * that the mapping between the input text which actually appears in the source file and the
+ * object produced from parsing it is more obvious.
+ */
+ private static final class WrappedItem {
+ private final String argument;
+ private final Object wrapped;
+
+ private WrappedItem(String argument, Object wrapped) {
+ this.argument = argument;
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Converted input \"%s\" => [%s]", argument, wrapped);
+ }
+
+ @Override
+ public int hashCode() {
+ return wrapped.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof WrappedItem) {
+ return this.wrapped.equals(((WrappedItem) other).wrapped);
+ }
+ return this.wrapped.equals(other);
+ }
+ }
+}