// 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.collect.ImmutableListMultimap; import com.google.devtools.common.options.Converter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionsBase; import java.lang.reflect.Field; import java.lang.reflect.Modifier; /** * A tester to validate certain useful properties of OptionsBase subclasses. These are not required * for parsing options in these classes, but can be helpful for e.g. ensuring that equality is not * violated. */ public final class OptionsTester { private final Class<? extends OptionsBase> optionsClass; public OptionsTester(Class<? extends OptionsBase> optionsClass) { this.optionsClass = optionsClass; } private static ImmutableList<Field> getAllFields(Class<? extends OptionsBase> optionsClass) { ImmutableList.Builder<Field> builder = ImmutableList.builder(); Class<? extends OptionsBase> current = optionsClass; while (!OptionsBase.class.equals(current)) { builder.add(current.getDeclaredFields()); // the input extends OptionsBase and we haven't seen OptionsBase yet, so this must also extend // (or be) OptionsBase @SuppressWarnings("unchecked") Class<? extends OptionsBase> superclass = (Class<? extends OptionsBase>) current.getSuperclass(); current = superclass; } return builder.build(); } /** * Tests that there are no non-Option instance fields. Fields not annotated with @Option will not * be considered for equality. */ public OptionsTester testAllInstanceFieldsAnnotatedWithOption() { for (Field field : getAllFields(optionsClass)) { if (!Modifier.isStatic(field.getModifiers())) { assertWithMessage( field + " is missing an @Option annotation; it will not be considered for equality.") .that(field.getAnnotation(Option.class)) .isNotNull(); } } return this; } /** * Tests that the default values of this class were part of the test data for the appropriate * ConverterTester, ensuring that the defaults at least obey proper equality semantics. * * <p>The default converters are not tested in this way. * * <p>Note that testConvert is not actually run on the ConverterTesters; it is expected that they * are run elsewhere. */ public OptionsTester testAllDefaultValuesTestedBy(ConverterTesterMap testers) { ImmutableListMultimap.Builder<Class<? extends Converter<?>>, Field> converterClassesBuilder = ImmutableListMultimap.builder(); for (Field field : getAllFields(optionsClass)) { Option option = field.getAnnotation(Option.class); if (option != null && !Converter.class.equals(option.converter())) { @SuppressWarnings("unchecked") // converter is rawtyped; see comment on Option.converter() Class<? extends Converter<?>> converter = (Class<? extends Converter<?>>) option.converter(); converterClassesBuilder.put(converter, field); } } ImmutableListMultimap<Class<? extends Converter<?>>, Field> converterClasses = converterClassesBuilder.build(); for (Class<? extends Converter<?>> converter : converterClasses.keySet()) { assertWithMessage( "Converter " + converter.getCanonicalName() + " has no corresponding ConverterTester") .that(testers) .containsKey(converter); for (Field field : converterClasses.get(converter)) { Option option = field.getAnnotation(Option.class); if (!option.allowMultiple() && !"null".equals(option.defaultValue())) { assertWithMessage( "Default value \"" + option.defaultValue() + "\" on " + field + " is not tested in the corresponding ConverterTester for " + converter.getCanonicalName()) .that(testers.get(converter).hasTestForInput(option.defaultValue())) .isTrue(); } } } return this; } }