/*
* Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
* Copyright [2016-2017] EMBL-European Bioinformatics Institute
*
* 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 org.ensembl.healthcheck;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.ensembl.healthcheck.testcase.EnsTestCase;
import org.ensembl.healthcheck.testcase.MultiDatabaseTestCase;
import org.ensembl.healthcheck.testcase.OrderedDatabaseTestCase;
import org.ensembl.healthcheck.testcase.SingleDatabaseTestCase;
import org.ensembl.healthcheck.util.ClassFileFilenameFilter;
import org.ensembl.healthcheck.util.Utils;
/**
* Hold information about tests. Can also find tests in a particular location.
*/
public class DiscoveryBasedTestRegistry implements TestRegistry {
private static Logger logger = Logger.getLogger("HealthCheckLogger");
private List allTests; // a list of EnsTestCase objects
private static final String BASE_TESTCASE_PACKAGE = "org.ensembl.healthcheck.testcase";
// -----------------------------------------------------------------
/**
* Create a new TestRegistry.
*/
public DiscoveryBasedTestRegistry() {
allTests = findAllTests();
}
// -----------------------------------------------------------------
/**
* @return All the currently defined (single and multiple database) tests.
*/
public List getAll() {
return allTests;
} // getAll
// -----------------------------------------------------------------
/**
* @return All the single-database tests.
*/
public List getAllSingle() {
List allSingle = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
Object test = it.next();
if (test instanceof SingleDatabaseTestCase) {
allSingle.add(test);
}
}
return allSingle;
} // getAllSingle
// -----------------------------------------------------------------
/**
* @return All the multi-database tests.
*/
public List getAllMulti() {
List allMulti = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
Object test = it.next();
if (test instanceof MultiDatabaseTestCase) {
allMulti.add(test);
}
}
return allMulti;
} // getAllMulti
// -----------------------------------------------------------------
/**
* @return All the ordered database tests.
*/
public List getAllOrdered() {
List allOrdered = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
Object test = it.next();
if (test instanceof OrderedDatabaseTestCase) {
allOrdered.add(test);
}
}
return allOrdered;
} // getAllOrdered
// -----------------------------------------------------------------
/**
* Get a list of all the single-database test cases that match certain conditions.
*
* @param groups
* A list of test case groups
* @param type
* The type of databases the result tests should apply to.
* @return All the single-database tests that are in at least one of groups, and apply to type.
*/
public List getAllSingle(List groups, DatabaseType type) {
List result = new ArrayList();
Iterator it = getAllSingle().iterator();
while (it.hasNext()) {
SingleDatabaseTestCase test = (SingleDatabaseTestCase) it.next();
if (test.inGroups(groups) && test.appliesToType(type)) {
result.add(test);
}
}
return result;
}
// -----------------------------------------------------------------
/**
* Get a list of all the multi-database test cases that match certain conditions. Note the database type that the test applies to
* is /not/ checked here.
*
* @param groups
* A list of test case groups
* @return All the multi-database tests that are in at least one of groups.
*/
public List getAllMulti(List groups) {
List result = new ArrayList();
Iterator it = getAllMulti().iterator();
while (it.hasNext()) {
MultiDatabaseTestCase test = (MultiDatabaseTestCase) it.next();
if (test.inGroups(groups)) {
result.add(test);
}
}
return result;
}
// -----------------------------------------------------------------
/**
* Get a list of all the ordered database test cases that match certain conditions. Note the database type that the test applies
* to is /not/ checked here.
*
* @param groups
* A list of test case groups
* @return All the ordered-database tests that are in at least one of groups.
*/
public List getAllOrdered(List groups) {
List result = new ArrayList();
Iterator it = getAllOrdered().iterator();
while (it.hasNext()) {
OrderedDatabaseTestCase test = (OrderedDatabaseTestCase) it.next();
if (test.inGroups(groups)) {
result.add(test);
}
}
return result;
}
// -----------------------------------------------------------------
/**
* Finds all tests.
*
* A test case is a class that extends EnsTestCase. Test case classes found in more than one location are only added once.
*
* @return A List containing objects of the test case classes found.
*/
public List findAllTests() {
allTests = new ArrayList();
// --------------------------------------
// Look for class files located in the appropriate package in the build/ directory.
// find all subdirectories
String startDir = System.getProperty("user.dir") + File.separator + "target" + File.separator + "build" + File.separator + BASE_TESTCASE_PACKAGE.replace('.', File.separatorChar);
String[] subdirs = Utils.getSubDirs(startDir);
// look for tests in each
for (int i = 0; i < subdirs.length; i++) {
String subdir = subdirs[i];
// check dir corresponds to a known database type
if (!subdir.equals("multi") && subdir.equalsIgnoreCase("generic") || DatabaseType.resolveAlias(subdir) != DatabaseType.UNKNOWN) {
String directoryName = startDir + File.separator + subdir;
String packageName = BASE_TESTCASE_PACKAGE + "." + subdir;
addUniqueTests(allTests, findTestsInDirectory(directoryName, packageName));
} else {
logger.warning("Subdirectory " + subdir + " cannot be related to a database type");
}
} // foreach subdir
// --------------------------------------
// Look inside lib/ensj-healthcheck.jar
// This is done second as if there is a class file for this test case in the build dir
// then that should be used instead of the one in the jar file.
// (addUniqueTests doesn't add a test if it's already in the list)
String jarFileName = System.getProperty("user.dir") + File.separator + "target" + File.separator + "dist" + File.separator + "ensj-healthcheck.jar";
if ((new File(jarFileName)).exists()) {
addUniqueTests(allTests, findTestsInJar(jarFileName, BASE_TESTCASE_PACKAGE));
}
// --------------------------------------
logger.finer("Found " + allTests.size() + " unique test case class" + (allTests.size() > 1 ? "es" : ""));
return allTests;
} // findAllTests
// -------------------------------------------------------------------------
/**
* Find all the tests (ie classes that extend EnsTestCase) in a directory.
*
* @param dir
* The base directory to look in.
* @param packageName
* The package name to look for.
* @return A list of tests in dir.
*/
public List<EnsTestCase> findTestsInDirectory(String dir, String packageName) {
logger.finest("Looking for tests in " + dir);
List<EnsTestCase> tests = new ArrayList<EnsTestCase>();
File f = new File(dir);
// find all classes that extend org.ensembl.healthcheck.EnsTestCase
ClassFileFilenameFilter cnff = new ClassFileFilenameFilter();
File[] classFiles = f.listFiles(cnff);
if (classFiles.length > 0) {
logger.finer("Examining " + classFiles.length + " class file" + (classFiles.length > 1 ? "s" : "") + " in " + dir);
}
Object obj = null;
for (File classFile : classFiles) {
String baseClassName = classFile.getName().substring(0, classFile.getName().lastIndexOf("."));
if (baseClassName.indexOf("$") > 0) {
logger.finest("Skipping " + baseClassName + " since it appears to be an auto-generated anonymous inner class");
continue;
}
try {
Class<?> newClass = Class.forName(packageName + "." + baseClassName);
boolean isEnsTestCase = EnsTestCase.class.isAssignableFrom(newClass);
boolean isAbstract = Modifier.isAbstract(newClass.getModifiers());
if (! isAbstract) {
if(isEnsTestCase) {
obj = newClass.newInstance();
}
else {
logger.fine("The class "+baseClassName+" is in the test package but appears not to implement "+EnsTestCase.class);
}
}
} catch (IllegalAccessException ie) {
// EG: Catch and log reflection warnings
logger.log(Level.WARNING, baseClassName + " has an issue when trying to create an instance", ie);
} catch (InstantiationException ie) {
logger.log(Level.WARNING, baseClassName + " has an issue when trying to create an instance", ie);
} catch (Exception e) {
e.printStackTrace();
}
if (obj != null && !tests.contains(obj)) {
// set the test's type based upon the directory, if required
EnsTestCase testCase = (EnsTestCase) obj;
String[] bits = testCase.getName().split("\\.");
String dirName = bits[bits.length - 2];
testCase.setTypeFromDirName(dirName);
// call the test's type method
testCase.types();
// store the test instance
tests.add(testCase);
}
} // for classFiles
return tests;
} // findTestsInDirectory
// -------------------------------------------------------------------------
/**
* Find tests in a jar file.
*
* @param jarFileName
* The name of the jar file to search.
* @param packageName
* The package name of the tests.
* @return The list of tests in the jar file.
*/
public List findTestsInJar(String jarFileName, String packageName) {
logger.finest("Looking for tests in " + jarFileName);
ArrayList tests = new ArrayList();
try {
JarFile jarFile = new JarFile(jarFileName);
for (Enumeration en = jarFile.entries(); en.hasMoreElements();) {
JarEntry entry = (JarEntry) en.nextElement();
String entryName = entry.getName().replace(File.separatorChar, '.');
Object obj = null;
// if entryName matches base package name, extract test name and subdir
if (!entry.isDirectory() && entryName.indexOf(packageName) > -1) {
String[] bits = entryName.split("\\.");
String className = bits[bits.length - 2];
String dirName = bits[bits.length - 3];
String extension = bits[bits.length - 1];
if (className.indexOf("$") > 0) {
logger.finest("Skipping " + className + " since it appears to be an auto-generated anonymous inner class");
continue;
}
if (extension.equalsIgnoreCase("class") && !dirName.equals("testcase")) {
try {
Class<?> newClass = Class.forName(packageName + "." + dirName + "." + className);
boolean isEnsTestCase = EnsTestCase.class.isAssignableFrom(newClass);
boolean isAbstract = Modifier.isAbstract(newClass.getModifiers());
if (! isAbstract) {
if(isEnsTestCase) {
obj = newClass.newInstance();
}
else {
logger.fine("The class "+className+" is in the test package but appears not to implement "+EnsTestCase.class);
}
}
} catch (IllegalAccessException ie) {
// EG: Catch and log reflection warnings
logger.log(Level.WARNING, className + " had an issue whilst trying to create an instance", ie);
} catch (InstantiationException ie) {
logger.log(Level.WARNING, className + " has an issue whilst trying to create an instance", ie);
} catch (Exception e) {
e.printStackTrace();
}
if (obj != null && !tests.contains(obj)) {
// set the test's type based upon the directory, if required
EnsTestCase testCase = (EnsTestCase) obj;
testCase.setTypeFromDirName(dirName);
// store the test instance
tests.add(testCase);
}
}
}
}
} catch (IOException ioe) {
ioe.printStackTrace(System.err);
}
return tests;
} // findTestsInJar
// -------------------------------------------------------------------------
/**
* Add all tests in subList to mainList, <em>unless</em> the test is already a member of mainList.
*
* @param mainList
* The list to add to.
* @param subList
* The list to be added.
*/
public void addUniqueTests(List mainList, List subList) {
Iterator it = subList.iterator();
while (it.hasNext()) {
EnsTestCase test = (EnsTestCase) it.next();
// can't really use List.contains() as the lists store objects which may be different
if (!testInList(test, mainList)) {
mainList.add(test);
logger.fine("Added " + test.getShortTestName() + " to the list of tests");
} else {
logger.fine("Skipped " + test.getShortTestName() + " as it is already in the list of tests");
}
}
} // addUniqueTests
// -------------------------------------------------------------------------
/**
* Check if a particular test is in a list of tests. The check is done by test name.
*
* @param test
* The test case to check.
* @param list
* The list to search.
* @return true if test is in list.
*/
public boolean testInList(EnsTestCase test, List list) {
boolean inList = false;
Iterator it = list.iterator();
while (it.hasNext()) {
EnsTestCase thisTest = (EnsTestCase) it.next();
if (thisTest.getTestName().equals(test.getTestName())) {
inList = true;
}
}
return inList;
} // testInList
// -----------------------------------------------------------------
/**
* Get a list of the union of types of databases that all the tests apply to.
*
* @return An array containing each DatabaseType found in the registry.
*/
public DatabaseType[] getTypes() {
List types = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
EnsTestCase test = (EnsTestCase) it.next();
DatabaseType[] testTypes = test.getAppliesToTypes();
for (int i = 0; i < testTypes.length; i++) {
if (!types.contains(testTypes[i])) {
types.add(testTypes[i]);
}
}
}
return (DatabaseType[]) types.toArray(new DatabaseType[types.size()]);
}
// -------------------------------------------------------------------------
/**
* Get a list of the union of groups of all the tests.
*
* @return An array of all the group names.
*/
public String[] getGroups() {
List groups = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
EnsTestCase test = (EnsTestCase) it.next();
List testGroups = test.getGroups();
Iterator it2 = testGroups.iterator();
while (it2.hasNext()) {
String group = (String) it.next();
// filter out test names
if (!isTestName(group) && !groups.contains(group)) {
groups.add(group);
}
}
}
return (String[]) groups.toArray(new String[groups.size()]);
}
// -------------------------------------------------------------------------
/**
* Get all the tests in a particular group.
*
* @param group
* the group to run.
* @return The array of tests in group.
*/
public EnsTestCase[] getTestsInGroup(String group) {
List result = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
EnsTestCase test = (EnsTestCase) it.next();
if (test.inGroup(group)) {
result.add(test);
}
}
return (EnsTestCase[]) result.toArray(new EnsTestCase[result.size()]);
}
// -------------------------------------------------------------------------
/**
* Get a list of the union of groups of all the tests that apply to a particular type of database.
*
* @param type
* The type to check.
* @return The union of groups of all the tests that apply to type.
*/
public String[] getGroups(DatabaseType type) {
List groups = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
EnsTestCase test = (EnsTestCase) it.next();
if (test.appliesToType(type)) {
List testGroups = test.getGroups();
Iterator it2 = testGroups.iterator();
while (it2.hasNext()) {
String group = (String) it2.next();
// filter out test names
if (!isTestName(group) && !groups.contains(group)) {
groups.add(group);
}
}
}
}
return (String[]) groups.toArray(new String[groups.size()]);
}
// -------------------------------------------------------------------------
/**
* Get all the tests in a particular group that apply to a particular type of database.
*
* @param group
* The group to check.
* @param type
* The type to check.
* @return All the tests in group that apply to type.
*/
public EnsTestCase[] getTestsInGroup(String group, DatabaseType type) {
List result = new ArrayList();
Iterator it = allTests.iterator();
while (it.hasNext()) {
EnsTestCase test = (EnsTestCase) it.next();
if (test.inGroup(group) && test.appliesToType(type)) {
result.add(test);
}
}
return (EnsTestCase[]) result.toArray(new EnsTestCase[result.size()]);
}
// -------------------------------------------------------------------------
/**
* Check if a string (e.g. the name of a group) is actually the name of a known test case.
*/
private boolean isTestName(String s) {
Iterator it = allTests.iterator();
while (it.hasNext()) {
EnsTestCase test = (EnsTestCase) it.next();
if (test.getShortTestName().equals(s)) {
return true;
}
}
return false;
}
// -------------------------------------------------------------------------
} // TestRegistry