Home | History | Annotate | Download | only in testing
      1 // Copyright 2017 The Bazel Authors. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //    http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package com.google.devtools.common.options.testing;
     16 
     17 import static com.google.common.truth.Truth.assertWithMessage;
     18 
     19 import com.google.common.collect.ImmutableList;
     20 import com.google.common.testing.EqualsTester;
     21 import com.google.devtools.common.options.Converter;
     22 import com.google.devtools.common.options.OptionsParsingException;
     23 import java.util.ArrayList;
     24 import java.util.LinkedHashSet;
     25 
     26 /**
     27  * A tester to confirm that {@link Converter} instances produce equal results on multiple calls with
     28  * the same input.
     29  */
     30 public final class ConverterTester {
     31 
     32   private final Converter<?> converter;
     33   private final Class<? extends Converter<?>> converterClass;
     34   private final EqualsTester tester = new EqualsTester();
     35   private final LinkedHashSet<String> testedInputs = new LinkedHashSet<>();
     36   private final ArrayList<ImmutableList<String>> inputLists = new ArrayList<>();
     37 
     38   /** Creates a new ConverterTester which will test the given Converter class. */
     39   public ConverterTester(Class<? extends Converter<?>> converterClass) {
     40     this.converterClass = converterClass;
     41     this.converter = createConverter();
     42   }
     43 
     44   private Converter<?> createConverter() {
     45     try {
     46       return converterClass.getDeclaredConstructor().newInstance();
     47     } catch (ReflectiveOperationException ex) {
     48       throw new AssertionError("Failed to create converter", ex);
     49     }
     50   }
     51 
     52   /** Returns the class this ConverterTester is testing. */
     53   public Class<? extends Converter<?>> getConverterClass() {
     54     return converterClass;
     55   }
     56 
     57   /**
     58    * Returns whether this ConverterTester has a test for the given input, i.e., addEqualityGroup
     59    * was called with the given string.
     60    */
     61   public boolean hasTestForInput(String input) {
     62     return testedInputs.contains(input);
     63   }
     64 
     65   /**
     66    * Adds a set of valid inputs which are expected to convert to equal values.
     67    *
     68    * <p>The inputs added here will be converted to values using the Converter class passed to the
     69    * constructor of this instance; the resulting values must be equal (and have equal hashCodes):
     70    *
     71    * <ul>
     72    * <li>to themselves
     73    * <li>to another copy of themselves generated from the same Converter instance
     74    * <li>to another copy of themselves generated from a different Converter instance
     75    * <li>to the other values converted from inputs in the same addEqualityGroup call
     76    * </ul>
     77    *
     78    * <p>They must NOT be equal:
     79    *
     80    * <ul>
     81    * <li>to null
     82    * <li>to an instance of an arbitrary class
     83    * <li>to any values converted from inputs in a different addEqualityGroup call
     84    * </ul>
     85    *
     86    * @throws AssertionError if an {@link OptionsParsingException} is thrown from the
     87    *     {@link Converter#convert} method when converting any of the inputs.
     88    * @see EqualsTester#addEqualityGroup
     89    */
     90   public ConverterTester addEqualityGroup(String... inputs) {
     91     ImmutableList.Builder<WrappedItem> wrapped = ImmutableList.builder();
     92     ImmutableList<String> inputList = ImmutableList.copyOf(inputs);
     93     inputLists.add(inputList);
     94     for (String input : inputList) {
     95       testedInputs.add(input);
     96       try {
     97         wrapped.add(new WrappedItem(input, converter.convert(input)));
     98       } catch (OptionsParsingException ex) {
     99         throw new AssertionError("Failed to parse input: \"" + input + "\"", ex);
    100       }
    101     }
    102     tester.addEqualityGroup(wrapped.build().toArray());
    103     return this;
    104   }
    105 
    106   /**
    107    * Tests the convert method of the wrapped Converter class, verifying the properties listed in the
    108    * Javadoc listed for {@link #addEqualityGroup}.
    109    *
    110    * @throws AssertionError if one of the expected properties did not hold up
    111    * @see EqualsTester#testEquals
    112    */
    113   public ConverterTester testConvert() {
    114     tester.testEquals();
    115     testItems();
    116     return this;
    117   }
    118 
    119   private void testItems() {
    120     for (ImmutableList<String> inputList : inputLists) {
    121       for (String input : inputList) {
    122         Converter<?> converter = createConverter();
    123         Converter<?> converter2 = createConverter();
    124 
    125         Object converted;
    126         Object convertedAgain;
    127         Object convertedDifferentConverterInstance;
    128         try {
    129           converted = converter.convert(input);
    130           convertedAgain = converter.convert(input);
    131           convertedDifferentConverterInstance = converter2.convert(input);
    132         } catch (OptionsParsingException ex) {
    133           throw new AssertionError("Failed to parse input: \"" + input + "\"", ex);
    134         }
    135 
    136         assertWithMessage(
    137                 "Input \""
    138                     + input
    139                     + "\" was not equal to itself when converted twice by the same Converter")
    140             .that(convertedAgain)
    141             .isEqualTo(converted);
    142         assertWithMessage(
    143                 "Input \""
    144                     + input
    145                     + "\" did not have a consistent hashCode when converted twice "
    146                     + "by the same Converter")
    147             .that(convertedAgain.hashCode())
    148             .isEqualTo(converted.hashCode());
    149         assertWithMessage(
    150             "Input \""
    151                 + input
    152                 + "\" was not equal to itself when converted twice by a different Converter")
    153             .that(convertedDifferentConverterInstance)
    154             .isEqualTo(converted);
    155         assertWithMessage(
    156             "Input \""
    157                 + input
    158                 + "\" did not have a consistent hashCode when converted twice "
    159                 + "by a different Converter")
    160             .that(convertedDifferentConverterInstance.hashCode())
    161             .isEqualTo(converted.hashCode());
    162       }
    163     }
    164   }
    165 
    166   /**
    167    * A wrapper around the objects passed to EqualsTester to give them a more useful toString() so
    168    * that the mapping between the input text which actually appears in the source file and the
    169    * object produced from parsing it is more obvious.
    170    */
    171   private static final class WrappedItem {
    172     private final String argument;
    173     private final Object wrapped;
    174 
    175     private WrappedItem(String argument, Object wrapped) {
    176       this.argument = argument;
    177       this.wrapped = wrapped;
    178     }
    179 
    180     @Override
    181     public String toString() {
    182       return String.format("Converted input \"%s\" => [%s]", argument, wrapped);
    183     }
    184 
    185     @Override
    186     public int hashCode() {
    187       return wrapped.hashCode();
    188     }
    189 
    190     @Override
    191     public boolean equals(Object other) {
    192       if (other instanceof WrappedItem) {
    193         return this.wrapped.equals(((WrappedItem) other).wrapped);
    194       }
    195       return this.wrapped.equals(other);
    196     }
    197   }
    198 }
    199