// 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 = 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); } } }