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.collect.ImmutableListMultimap; 21 import com.google.devtools.common.options.Converter; 22 import com.google.devtools.common.options.Option; 23 import com.google.devtools.common.options.OptionsBase; 24 import java.lang.reflect.Field; 25 import java.lang.reflect.Modifier; 26 27 /** 28 * A tester to validate certain useful properties of OptionsBase subclasses. These are not required 29 * for parsing options in these classes, but can be helpful for e.g. ensuring that equality is not 30 * violated. 31 */ 32 public final class OptionsTester { 33 34 private final Class<? extends OptionsBase> optionsClass; 35 OptionsTester(Class<? extends OptionsBase> optionsClass)36 public OptionsTester(Class<? extends OptionsBase> optionsClass) { 37 this.optionsClass = optionsClass; 38 } 39 getAllFields(Class<? extends OptionsBase> optionsClass)40 private static ImmutableList<Field> getAllFields(Class<? extends OptionsBase> optionsClass) { 41 ImmutableList.Builder<Field> builder = ImmutableList.builder(); 42 Class<? extends OptionsBase> current = optionsClass; 43 while (!OptionsBase.class.equals(current)) { 44 builder.add(current.getDeclaredFields()); 45 // the input extends OptionsBase and we haven't seen OptionsBase yet, so this must also extend 46 // (or be) OptionsBase 47 @SuppressWarnings("unchecked") 48 Class<? extends OptionsBase> superclass = 49 (Class<? extends OptionsBase>) current.getSuperclass(); 50 current = superclass; 51 } 52 return builder.build(); 53 } 54 55 /** 56 * Tests that there are no non-Option instance fields. Fields not annotated with @Option will not 57 * be considered for equality. 58 */ testAllInstanceFieldsAnnotatedWithOption()59 public OptionsTester testAllInstanceFieldsAnnotatedWithOption() { 60 for (Field field : getAllFields(optionsClass)) { 61 if (!Modifier.isStatic(field.getModifiers())) { 62 assertWithMessage( 63 field 64 + " is missing an @Option annotation; it will not be considered for equality.") 65 .that(field.getAnnotation(Option.class)) 66 .isNotNull(); 67 } 68 } 69 return this; 70 } 71 72 /** 73 * Tests that the default values of this class were part of the test data for the appropriate 74 * ConverterTester, ensuring that the defaults at least obey proper equality semantics. 75 * 76 * <p>The default converters are not tested in this way. 77 * 78 * <p>Note that testConvert is not actually run on the ConverterTesters; it is expected that they 79 * are run elsewhere. 80 */ testAllDefaultValuesTestedBy(ConverterTesterMap testers)81 public OptionsTester testAllDefaultValuesTestedBy(ConverterTesterMap testers) { 82 ImmutableListMultimap.Builder<Class<? extends Converter<?>>, Field> converterClassesBuilder = 83 ImmutableListMultimap.builder(); 84 for (Field field : getAllFields(optionsClass)) { 85 Option option = field.getAnnotation(Option.class); 86 if (option != null && !Converter.class.equals(option.converter())) { 87 @SuppressWarnings("unchecked") // converter is rawtyped; see comment on Option.converter() 88 Class<? extends Converter<?>> converter = 89 (Class<? extends Converter<?>>) option.converter(); 90 converterClassesBuilder.put(converter, field); 91 } 92 } 93 ImmutableListMultimap<Class<? extends Converter<?>>, Field> converterClasses = 94 converterClassesBuilder.build(); 95 for (Class<? extends Converter<?>> converter : converterClasses.keySet()) { 96 assertWithMessage( 97 "Converter " + converter.getCanonicalName() + " has no corresponding ConverterTester") 98 .that(testers) 99 .containsKey(converter); 100 for (Field field : converterClasses.get(converter)) { 101 Option option = field.getAnnotation(Option.class); 102 if (!option.allowMultiple() && !"null".equals(option.defaultValue())) { 103 assertWithMessage( 104 "Default value \"" 105 + option.defaultValue() 106 + "\" on " 107 + field 108 + " is not tested in the corresponding ConverterTester for " 109 + converter.getCanonicalName()) 110 .that(testers.get(converter).hasTestForInput(option.defaultValue())) 111 .isTrue(); 112 } 113 } 114 } 115 return this; 116 } 117 } 118