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. */
ConverterTester(Class<? extends Converter<?>> converterClass)39   public ConverterTester(Class<? extends Converter<?>> converterClass) {
40     this.converterClass = converterClass;
41     this.converter = createConverter();
42   }
43 
createConverter()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. */
getConverterClass()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    */
hasTestForInput(String input)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    */
addEqualityGroup(String... inputs)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    */
testConvert()113   public ConverterTester testConvert() {
114     tester.testEquals();
115     testItems();
116     return this;
117   }
118 
testItems()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 
WrappedItem(String argument, Object wrapped)175     private WrappedItem(String argument, Object wrapped) {
176       this.argument = argument;
177       this.wrapped = wrapped;
178     }
179 
180     @Override
toString()181     public String toString() {
182       return String.format("Converted input \"%s\" => [%s]", argument, wrapped);
183     }
184 
185     @Override
hashCode()186     public int hashCode() {
187       return wrapped.hashCode();
188     }
189 
190     @Override
equals(Object other)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