/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.xenei.junit.contract.tooling; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.xenei.classpathutils.ClassPathFilter; import org.xenei.classpathutils.ClassPathUtils; import org.xenei.classpathutils.filter.AndClassFilter; import org.xenei.classpathutils.filter.HasAnnotationClassFilter; import org.xenei.classpathutils.filter.NotClassFilter; import org.xenei.classpathutils.filter.OrClassFilter; import org.xenei.junit.contract.Contract; import org.xenei.junit.contract.ContractImpl; import org.xenei.junit.contract.NoContractTest; import org.xenei.junit.contract.info.ContractTestMap; import org.xenei.junit.contract.info.TestInfo; /** * * Class to produce report data about the state of the contract tests. * */ public class InterfaceReport { /** * A collection of all classes in the package classes. This includes * interfaces, abstract and concrete implementations. */ private final Collection<Class<?>> packageClasses; /** * A filter that describes the classes to skip */ private final ClassPathFilter filter; /** * A map of all interfaces implemented in the packages that have contract * tests to their InterfaceInfo record. */ private Map<Class<?>, InterfaceInfo> interfaceInfoMap; /** * A map of interface to all contract tests for the interface. this includes * classes not in the specified packages. **/ private final ContractTestMap contractTestMap; private final ContractImplMap contractImplMap; private static final Log LOG = LogFactory .getLog(ContractTestMap.class); private static final ClassPathFilter INTERESTING_CLASSES = new AndClassFilter( ClassPathFilter.INTERFACE_CLASS, new NotClassFilter(ClassPathFilter.ANNOTATION_CLASS), new NotClassFilter(new HasAnnotationClassFilter( NoContractTest.class))); private static final Comparator<Class<?>> CLASS_NAME_COMPARATOR = new Comparator<Class<?>>() { @Override public int compare(final Class<?> o1, final Class<?> o2) { return o1.getName().compareTo(o2.getName()); } }; /** * Get a collection of InterfaceInfo objects for the entire test suite. * * @return The colleciton of InterfaceInfo objects. */ public Collection<InterfaceInfo> getInterfaceInfoCollection() { return getInterfaceInfoMap().values(); } /** * Get the interface info map. * * All interfaces implemented in the packages that have contract tests. * * @return */ private Map<Class<?>, InterfaceInfo> getInterfaceInfoMap() { if (interfaceInfoMap == null) { interfaceInfoMap = new HashMap<Class<?>, InterfaceInfo>(); for (final Class<?> c : packageClasses) { if (INTERESTING_CLASSES.accept(c)) { if (!interfaceInfoMap.containsKey(c)) { interfaceInfoMap.put(c, new InterfaceInfo(c)); } } else { final Contract contract = c.getAnnotation(Contract.class); if (contract != null) { InterfaceInfo ii = interfaceInfoMap.get(contract .value()); if (ii == null) { ii = new InterfaceInfo(contract.value()); interfaceInfoMap.put(contract.value(), ii); } ii.add(c); } } } } return interfaceInfoMap; } /** * Constructor. * * If the filter parameter is null it defaults to <code>true()</code> and * all classes are processed. * * @param packages * The list of packages to process. * @param filter * the filter of classes to process. may be null. * @param classLoader * the class loader to use. */ public InterfaceReport(final String[] packages, ClassPathFilter filter, final ClassLoader classLoader) { if (packages.length == 0) { throw new IllegalArgumentException( "At least one package must be specified"); } contractTestMap = new ContractTestMap(); if (filter != null) { this.filter = filter; } else { this.filter = ClassPathFilter.TRUE; } // find all the contract annotated tests on the class path. // this includes classes not in the specified packages packageClasses = new HashSet<Class<?>>(); for (final String p : packages) { packageClasses.addAll(ClassPathUtils.getClasses(classLoader, p, this.filter)); } if (packageClasses.size() == 0) { throw new IllegalArgumentException("No classes found in " + Arrays.asList(packages)); } contractImplMap = new ContractImplMap(packageClasses); } /** * Get the collection of Class objects that were included in the packages. * * @return The collection of Class objects that were included in the * packages. */ public Collection<Class<?>> getPackageClasses() { return packageClasses; } /** * Get the set of errors encountered when discovering contract tests. This * is a list of all errors for all tests. * * @return The list of Throwable objects that represent errors. * */ public List<Throwable> getErrors() { final List<Throwable> retval = new ArrayList<Throwable>(); for (final TestInfo testInfo : contractTestMap.listTestInfo()) { retval.addAll(testInfo.getErrors()); } return retval; } /** * get a set of interfaces that do not have contract tests defined. * * @return The list of interfaces that don't have contract tests defined. */ public Set<Class<?>> getUntestedInterfaces() { final Set<Class<?>> retval = new TreeSet<Class<?>>( CLASS_NAME_COMPARATOR); for (final InterfaceInfo info : getInterfaceInfoMap().values()) { // no test and has methods if (info.getTests().isEmpty() && (info.getName().getDeclaredMethods().length > 0)) { retval.add(info.getName()); } } return retval; } /** * Search for classes that extend interfaces with contract tests but that * don't have an implementation of the test producer. * * @return the set of Classes that do not have contract tests. */ public Set<Class<?>> getUnImplementedTests() { final Set<Class<?>> retval = new TreeSet<Class<?>>( CLASS_NAME_COMPARATOR); // only interested in concrete implementations ClassPathFilter filter = new NotClassFilter(new OrClassFilter( ClassPathFilter.ABSTRACT_CLASS, ClassPathFilter.INTERFACE_CLASS)); for (final Class<?> clazz : filter.filterClasses(packageClasses)) { // we are only interested if there is no contract test for the // class and there are parent tests LOG.debug(String.format("checking %s for contract tests", clazz)); final Set<Class<?>> interfaces = contractTestMap .getAllInterfaces(clazz); if ( !interfaces.isEmpty() ) { final Map<Class<?>, InterfaceInfo> interfaceInfo = getInterfaceInfoMap(); interfaces.retainAll(interfaceInfo.keySet()); } // interfaces contains only contract test interfaces that clazz // implements. if (!interfaces.isEmpty()) { // not empty so we are need to verify that we have a test // for clazz if (!contractImplMap.hasTestFor(clazz)) { retval.add(clazz); } } } return retval; } /** * Get the filter * * @return The filter used to filter classes. */ public ClassPathFilter getClassFilter() { return filter; } /** * A mapping of contracts implementations to tests * * contract implementations are classes annotated with * <code>@ContaractImpl</code> * * tests are the the contract tests being tested by the implementation. * Tests are annotated with <Code>@Contract</code>. * * A test may have more than one implementation. */ private static class ContractImplMap { // the map of the contract tests to their implementations. private final Map<Class<?>, Set<Class<?>>> forwardMap; // the map of an implementation to the contract it tests. private final Map<Class<?>, Class<?>> reverseMap; /** * Constructor. * @param classes The classes for the map. */ public ContractImplMap(final Collection<Class<?>> classes) { forwardMap = new HashMap<Class<?>, Set<Class<?>>>(); reverseMap = new HashMap<Class<?>, Class<?>>(); for (final Class<?> c : classes) { final ContractImpl contractImpl = c .getAnnotation(ContractImpl.class); if (contractImpl != null) { add(c, contractImpl); } } } /** * Add a ContractImpl to the map. * * @param contractTestImplClass * The class annotated with <code>@ContractImpl</code> * @param contractImpl * The ContractImpl annotation. */ private void add(final Class<?> contractTestImplClass, final ContractImpl contractImpl) { Set<Class<?>> set = forwardMap.get(contractImpl.value()); if (set == null) { set = new HashSet<Class<?>>(); forwardMap.put(contractImpl.value(), set); } set.add(contractTestImplClass); reverseMap.put(contractTestImplClass, contractImpl.value()); } /** * Return true if there is a contract test implementation for a specific * contract test. * * @param contractTestImplClass * The class annotated with <code>@ContractImpl</code> * @return */ public boolean hasTestFor(final Class<?> contractTestImplClass) { return forwardMap.containsKey(contractTestImplClass); } } }