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