/* * Copyright 2000-2013 Vaadin Ltd. * * 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.vaadin.tests.tb3; import java.io.File; import java.io.IOException; import java.lang.reflect.Modifier; import java.net.JarURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.jar.JarEntry; import org.junit.runners.Suite; import org.junit.runners.model.InitializationError; /** * Test suite which consists of all the TB3 tests passed in the constructor. * Runs the tests in parallel using a {@link ParallelScheduler} * * @author Vaadin Ltd */ public class TB3TestSuite extends Suite { /** * This only restricts the number of test suites running concurrently. The * number of tests to run concurrently are configured in {@link TB3Runner}. */ private static final int MAX_CONCURRENT_TEST_SUITES = 20; /** * This is static so it is shared by all test suites running concurrently on * the same machine and thus can limit the number of threads in use. */ private final ExecutorService service = Executors .newFixedThreadPool(MAX_CONCURRENT_TEST_SUITES); public TB3TestSuite(Class klass, Class baseClass, String basePackage, String[] ignorePackages) throws InitializationError { super(klass, findTests(baseClass, basePackage, ignorePackages)); setScheduler(new ParallelScheduler(service)); } /** * Traverses the directory on the classpath (inside or outside a Jar file) * specified by 'basePackage'. Collects all classes inside the location * which can be assigned to 'baseClass' except for classes inside packages * listed in 'ignoredPackages'. * * @param baseClass * @param basePackage * @param ignorePackages * @return */ private static Class[] findTests( Class baseClass, String basePackage, String[] ignorePackages) { try { List l = findClasses(baseClass, basePackage, ignorePackages); return l.toArray(new Class[] {}); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * Traverses the directory on the classpath (inside or outside a Jar file) * specified by 'basePackage'. Collects all classes inside the location * which can be assigned to 'baseClass' except for classes inside packages * listed in 'ignoredPackages'. * * @param baseClass * @param basePackage * @param ignoredPackages * @return * @throws IOException */ private static List> findClasses(Class baseClass, String basePackage, String[] ignoredPackages) throws IOException { List> classes = new ArrayList>(); String basePackageDirName = "/" + basePackage.replace('.', '/'); URL location = baseClass.getResource(basePackageDirName); if (location.getProtocol().equals("file")) { try { File f = new File(location.toURI()); if (!f.exists()) { throw new IOException("Directory " + f.toString() + " does not exist"); } findPackages(f, basePackage, baseClass, classes, ignoredPackages); } catch (URISyntaxException e) { throw new IOException(e.getMessage()); } } else if (location.getProtocol().equals("jar")) { JarURLConnection juc = (JarURLConnection) location.openConnection(); findClassesInJar(juc, basePackage, baseClass, classes); } Collections.sort(classes, new Comparator>() { @Override public int compare(Class o1, Class o2) { return o1.getName().compareTo(o2.getName()); } }); return classes; } /** * Traverses the given directory and collects all classes which are inside * the given 'javaPackage' and can be assigned to the given 'baseClass'. The * found classes are added to 'result'. * * @param parent * The directory to traverse * @param javaPackage * The java package which 'parent' contains * @param baseClass * The class which the target classes extend * @param result * The collection to which found classes are added * @param ignoredPackages * A collection of packages (including sub packages) to ignore */ private static void findPackages(File parent, String javaPackage, Class baseClass, Collection> result, String[] ignoredPackages) { for (String ignoredPackage : ignoredPackages) { if (javaPackage.equals(ignoredPackage)) { return; } } for (File file : parent.listFiles()) { if (file.isDirectory()) { findPackages(file, javaPackage + "." + file.getName(), baseClass, result, ignoredPackages); } else if (file.getName().endsWith(".class")) { String fullyQualifiedClassName = javaPackage + "." + file.getName().replace(".class", ""); addClassIfMatches(result, fullyQualifiedClassName, baseClass); } } } /** * Traverses a Jar file using the given connection and collects all classes * which are inside the given 'javaPackage' and can be assigned to the given * 'baseClass'. The found classes are added to 'result'. * * @param javaPackage * The java package containing the classes (classes may be in a * sub package) * @param baseClass * The class which the target classes extend * @param result * The collection to which found classes are added * @throws IOException */ private static void findClassesInJar(JarURLConnection juc, String javaPackage, Class baseClass, Collection> result) throws IOException { String javaPackageDir = javaPackage.replace('.', '/'); Enumeration ent = juc.getJarFile().entries(); while (ent.hasMoreElements()) { JarEntry e = ent.nextElement(); if (e.getName().endsWith(".class") && e.getName().startsWith(javaPackageDir)) { String fullyQualifiedClassName = e.getName().replace('/', '.') .replace(".class", ""); addClassIfMatches(result, fullyQualifiedClassName, baseClass); } } } /** * Verifies that the class represented by 'fullyQualifiedClassName' can be * loaded, assigned to 'baseClass' and is not an abstract or anonymous * class. * * @param result * The collection to add to * @param fullyQualifiedClassName * The candidate class * @param baseClass * The class 'fullyQualifiedClassName' should be assignable to */ @SuppressWarnings("unchecked") private static void addClassIfMatches( Collection> result, String fullyQualifiedClassName, Class baseClass) { try { // Try to load the class Class c = Class.forName(fullyQualifiedClassName); if (!baseClass.isAssignableFrom(c)) { return; } if (!includeInSuite(c)) { return; } if (!Modifier.isAbstract(c.getModifiers()) && !c.isAnonymousClass()) { result.add((Class) c); } } catch (Exception e) { // Could ignore that class cannot be loaded e.printStackTrace(); } catch (LinkageError e) { // Ignore. Client side classes will at least throw LinkageErrors } } /** * @return true if the class should be included in the suite, false if not */ private static boolean includeInSuite(Class c) { if (c.getAnnotation(ExcludeFromSuite.class) != null) { return false; } return true; } }