org.ensembl.healthcheck.ConfigurableTestRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.ensembl.healthcheck.ConfigurableTestRunner.java

Source

/*
 * Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
 * Copyright [2016-2018] 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.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.StringUtils;
import org.ensembl.healthcheck.ReporterFactory.ReporterType;
import org.ensembl.healthcheck.TestRegistryFactory.TestRegistryType;
import org.ensembl.healthcheck.configuration.ConfigurationUserParameters;
import org.ensembl.healthcheck.configuration.ConfigureConfiguration;
import org.ensembl.healthcheck.configuration.ConfigureHealthcheckDatabase;
import org.ensembl.healthcheck.configuration.ConfigureHost;
import org.ensembl.healthcheck.configuration.ConfigureTestGroups;
import org.ensembl.healthcheck.configurationmanager.ConfigurationDumper;
import org.ensembl.healthcheck.configurationmanager.ConfigurationException;
import org.ensembl.healthcheck.configurationmanager.ConfigurationFactory;
import org.ensembl.healthcheck.configurationmanager.ConfigurationFactory.ConfigurationType;
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.ConnectionBasedSqlTemplateImpl;
import org.ensembl.healthcheck.util.CreateHealthCheckDB;
import org.ensembl.healthcheck.util.DBUtils;
import org.ensembl.healthcheck.util.SqlTemplate;

/**
 * 
 * Runs test which can be configured on the command line and stores them in a
 * database.
 * 
 */
public class ConfigurableTestRunner extends TestRunner {

    /**
     * 
     */
    private static final String GET_DIVISION_DBS = "select division_db.db_name from division_db "
            + "join division using (division_id) where division_db.is_current=1 and (division.name=? or division.shortname=?)";

    /**
     * 
     */
    private static final String GET_DIVISION_SPECIES_DBS = "select "
            + "concat(species.db_name,'_',db.db_type,'_',db.db_release,'_',db.db_assembly) from db "
            + "join species using (species_id) join division_species using (species_id) "
            + "join division using (division_id) where db.is_current=1 and species.is_current=1 and (division.name=? or division.shortname=?)";

    //   static final Logger log = Logger.getLogger(ConfigurableTestRunner.class
    //         .getCanonicalName());

    /**
     * Name of a properties file from which parameters can be taken, if they
     * have not been set by the user anywhere.
     */
    protected final static String DEFAULT_PROPERTIES_FILE = "database.defaults.properties";

    /**
     * The configuration object from which configuration information is
     * retrieved.
     */
    protected final ConfigurationUserParameters configuration;

    /**
     * The type of reporter used.
     */
    protected final ReporterType reporterType;

    /**
     * The type of test registry used.
     */
    protected final TestRegistryType testRegistryType;

    protected final SystemPropertySetter systemPropertySetter;

    protected final TestRegistry testRegistry;
    protected final Reporter reporter;

    /**
     * @param configuration
     *            - A configuration object of type ConfigurationUserParameters
     * 
     *            Creates a ConfigurableTestRunner using the parameters from the
     *            configuration object.
     * 
     */
    public ConfigurableTestRunner(ConfigurationUserParameters configuration) {

        logger.config("Using classpath: \n\n" + Debug.classpathToString());

        this.configuration = configuration;
        this.systemPropertySetter = new SystemPropertySetter(configuration);
        this.testRegistryType = getTestRegistryType(configuration.getTestRegistryType());
        this.reporterType = getReporterType(configuration.getReporterType());
        this.testRegistry = getTestRegistry(this.testRegistryType, configuration);
        this.reporter = getReporter(this.reporterType);

        String repair = configuration.getRepair().toLowerCase();
        this.doRepair = (repair.equals("do") || repair.equals("1") || repair.equals("yes"));
        this.showRepair = (repair.equals("show"));

        /* configuration is cast to a lesser interface to compartmentalise options for
        * different packages.
        * */
        DBUtils.setHostConfiguration((ConfigureHost) configuration);
    }

    /**
     * @param args
     *            - The command line arguments
     * 
     *            Creates a ConfigurableTestRunner using command line arguments.
     *            The configuration parameters are taken from the command line
     *            and from the properties files that can be specifies on the
     *            command line with the --conf option. The
     *            DEFAULT_PROPERTIES_FILE is added as well.
     * 
     */
    public ConfigurableTestRunner(String[] args) {

        this(createConfigurationObj(args));
    }

    /**
     * Used for created layered constructors.
     */
    protected static ReporterType getReporterType(String reporterType) {

        ReporterType r;

        try {
            r = ReporterType.valueOf(reporterType.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new ConfigurationException(
                    "Parameter reporterType has been set to an illegal value: " + reporterType);
        }

        return r;
    }

    /**
     * Used for created layered constructors.
     */
    protected static TestRegistryType getTestRegistryType(String testRegistryType) {

        TestRegistryType t;

        try {
            t = TestRegistryType.valueOf(testRegistryType);
        } catch (IllegalArgumentException e) {
            throw new ConfigurationException(
                    "Parameter reportertype has been set to an illegal value: " + testRegistryType);
        }

        return t;
    }

    /**
     * @return TestRegistry
     * 
     *         Creates a TestRegistry. The type of TestRegistry is determined by
     *         the configuration object.
     * 
     */
    protected TestRegistry getTestRegistry(TestRegistryType testRegistryType, ConfigureTestGroups configuration) {

        TestRegistryFactory testRegistryFactory = new TestRegistryFactory(

                // Downcast of configuration, the TestRegistryFactory and the
                // Registries it produces from it only have access to the
                // params in the ConfigureTestGroups interface
                //
                (ConfigureTestGroups) configuration);

        logger.config("Using test registry of type: " + testRegistryType);

        TestRegistry testRegistry = null;

        try {
            testRegistry = testRegistryFactory.getTestRegistry(testRegistryType);
        } catch (TestRegistryCreationException e) {
            throw new ConfigurationException(e);
        }

        logger.config("Using testregistry with this configuration:\n" + testRegistry.toString());

        return testRegistry;
    }

    /**
     * @return Reporter
     * 
     *         Creates a reporter. The type of reporter is determined by the
     *         configuration object.
     * 
     */
    protected Reporter getReporter(ReporterType reporterType) {

        Reporter reporter = new ReporterFactory().getTestReporter(reporterType);

        logger.config("Using reporter of type: " + reporterType);

        return reporter;
    }

    /**
     * Used for creating layered constructors.
     */
    protected static ConfigurationUserParameters createConfigurationObj(String[] args) {

        Logger logger = Logger.getLogger("ConfigurationUserParameters");
        // A temporary configuration object for accessing the command line
        // parameters in which the user configures where the configuration
        // files are located. Since only this information is of interest at
        // this point the configuration object is subcast to the
        // ConfigureConfiguration interface.
        //
        ConfigureConfiguration conf = new ConfigurationFactory<ConfigurationUserParameters>(
                ConfigurationUserParameters.class, (Object[]) args).getConfiguration(ConfigurationType.Commandline);

        // Get the list of property files the user specified and add the
        // default property file to it.
        //
        List<File> propertyFileNames = new ArrayList<File>();

        // The user is not required to provide a property file. In this case
        // only the
        // command line arguments and the default properties file will be used.
        //
        if (conf.isConf()) {
            propertyFileNames.addAll(conf.getConf());
        }
        propertyFileNames.add(new File(DEFAULT_PROPERTIES_FILE));

        // Use this to create the final configuration object.
        ConfigurationFactory<ConfigurationUserParameters> confFact = new ConfigurationFactory(
                ConfigurationUserParameters.class, args, propertyFileNames);

        // Users may want to know what went into the the configuration of this,
        // So some information on where the configuration information came
        // from is compiled here and sent to the logger.
        //
        StringBuffer msg = new StringBuffer();

        //
        // Create a few logging messages about the configuration information
        // that that will be used. Useful for debugging.
        //

        // Information about the command line arguments and which property files
        // are used
        msg.append("Creating configuration for this run.\n\n");
        msg.append("The following arguments were specified on the command line:\n");

        for (String arg : args) {
            if (arg.startsWith("-")) {
                msg.append("\n");
            }
            msg.append(" " + arg);
        }

        msg.append("\n\nThe following property files will be used:\n\n");

        for (File propertyFileName : propertyFileNames) {
            msg.append("  - " + propertyFileName.getName() + "\n");
        }

        logger.config(msg.toString());

        // Finally create the configuration object.
        ConfigurationUserParameters configuration = confFact.getConfiguration(ConfigurationType.Cascading);

        // Show user the final configuration settings that will be used.
        logger.config("The following settings will be used:\n\n"
                + new ConfigurationDumper<ConfigurationUserParameters>().dump(configuration));

        return configuration;
    }

    public static void main(String[] args) {

        ConfigurableTestRunner configurableTestRunner = new ConfigurableTestRunner(args);
        try {

            configurableTestRunner.run();

        } catch (ConfigurationException e) {

            configurableTestRunner.logger.log(Level.SEVERE, e.getMessage());
        }
    }

    protected DatabaseServer connectToDatabase(ConfigureHost conf) {

        DatabaseServer ds = new DatabaseServer(conf.getHost(), conf.getPort(), conf.getUser(), conf.getPassword(),
                conf.getDriver());
        return ds;
    }

    List<Class<? extends EnsTestCase>> getAllRegisteredTestClasses(TestRegistry testRegistry) {

        List<Class<? extends EnsTestCase>> registeredClasses = new ArrayList<Class<? extends EnsTestCase>>();

        for (EnsTestCase currentTestCase : testRegistry.getAll()) {
            registeredClasses.add(currentTestCase.getClass());
        }
        return registeredClasses;
    }

    protected void run() {

        TestRegistry testRegistry = this.testRegistry;
        Reporter reporter = this.reporter;
        String outputLevelString = configuration.getOutputLevel();
        setOutputLevel(outputLevelString);
        ReportManager.setOutputLevel(outputLevel);

        ReportManager.setReporter(reporter);

        DatabaseServer ds = connectToDatabase(configuration);

        if (this.reporterType == ReporterType.DATABASE && configuration.isEndSession()) {
            logger.info("Finishing reporter session");
            systemPropertySetter.setPropertiesForReportManager_connectToOutputDatabase();
            ReportManager.connectToOutputDatabase();
            ReportManager.setSessionID(Long.valueOf(configuration.getEndSession()));
            ReportManager.endDatabaseSession();
            logger.info("Finished reporter session");
            return;
        }

        List<String> testDatabases = new ArrayList<String>(getTestDatabases());
        Species globalSpecies = null;

        if (configuration.isSpecies()) {
            globalSpecies = Species.resolveAlias(configuration.getSpecies());
            if (globalSpecies != Species.UNKNOWN) {
                logger.info("Will override guessed species with " + globalSpecies + " for all databases");
            } else {
                String msg = "Argument " + configuration.getSpecies() + " to -species not recognised";
                logger.severe(msg);
                throw new ConfigurationException(msg);
            }
        }

        DatabaseType globalDatabaseType = null;

        if (configuration.isDbType()) {
            globalDatabaseType = DatabaseType.resolveAlias(configuration.getDbType());
            if (globalDatabaseType != DatabaseType.UNKNOWN) {
                logger.info(
                        "Will override guessed database types with " + globalDatabaseType + " for all databases");
            } else {
                String msg = "Argument " + configuration.getDbType() + " to -type not recognised";
                logger.severe(msg);
                throw new ConfigurationException(msg);
            }
        }

        DatabaseRegistry databasesToTestRegistry = new DatabaseRegistry(testDatabases, globalDatabaseType,
                globalSpecies, false);

        if (databasesToTestRegistry.getAll().length == 0) {
            logger.warning("Warning: no databases configured!");
            throw new RuntimeException("No databases configured");
        }

        if (this.reporterType == ReporterType.DATABASE) {

            // Create the database to which tests will be written
            CreateHealthCheckDB c = new CreateHealthCheckDB((ConfigureHealthcheckDatabase) configuration);

            boolean reportDatabaseExistsAlready = c.databaseExists(configuration.getOutputDatabase());

            if (reportDatabaseExistsAlready) {
                logger.info(
                        "Reporting database " + configuration.getOutputDatabase() + " already exists, will reuse.");
            } else {
                logger.info("Reporting database " + configuration.getOutputDatabase()
                        + " does not exist, will create.");
                c.run();
            }

            systemPropertySetter.setPropertiesForReportManager_connectToOutputDatabase();
            ReportManager.connectToOutputDatabase();

            systemPropertySetter.setPropertiesForReportManager_createDatabaseSession();
            if (configuration.isSessionID()) {
                ReportManager.reuseDatabaseSession(Long.valueOf(configuration.getSessionID()));
            } else {
                ReportManager.createDatabaseSession();
            }
        }

        // When writing to a database, this must only be run after calling
        //
        // ReportManager.connectToOutputDatabase()
        //
        // Otherwise reporting problems won't work.
        //
        complainAboutDatabasesNotFound(databasesToTestRegistry, testDatabases);

        systemPropertySetter.setPropertiesForHealthchecks();

        logger.info("Running tests");
        List<Class<? extends EnsTestCase>> testsThrowingAnException = new ArrayList<Class<? extends EnsTestCase>>();
        List<Class<? extends EnsTestCase>> testsSkippedLongRunning = new ArrayList<Class<? extends EnsTestCase>>();
        List<Class<? extends EnsTestCase>> testsSkippedForUnknownReason = new ArrayList<Class<? extends EnsTestCase>>();

        List<Class<? extends EnsTestCase>> testsApplyingToNoDb = findTestsApplyingToNoDb(testRegistry,
                databasesToTestRegistry);

        if (!testsApplyingToNoDb.isEmpty()) {

            ReportManager.problem(new TestRunnerSelfCheck(), "Skipped tests",
                    "These tests apply to none of the databases that will be tested:\n"
                            + testListToBulletPoints(testsApplyingToNoDb));
        }

        Map<Class<? extends EnsTestCase>, List<DatabaseRegistryEntry>> exceptionToDb;

        try {
            TestRunStats accounting = runAllTestsWithAccounting(databasesToTestRegistry, testRegistry, false);

            exceptionToDb = accounting.getExceptionToDb();

            if (!exceptionToDb.isEmpty()) {

                for (Class<? extends EnsTestCase> currentTestClass : exceptionToDb.keySet()) {

                    ReportManager.problem(new TestRunnerSelfCheck(), "Skipped tests",
                            "The following test died with an exception: " + currentTestClass.getName() + "\n"
                                    + "on the following databases: \n"
                                    + dbreListToBulletPoints(exceptionToDb.get(currentTestClass)));
                }

            }

            for (Class<? extends EnsTestCase> currentTestCase : accounting.getTrackCompletionStatus().keySet()) {

                if (accounting.getTrackCompletionStatus().get(currentTestCase)
                        .equals(TestRunStats.CompletionStatus.DIED_WITH_EXCEPTION)) {
                    testsThrowingAnException.add(currentTestCase);
                }
                if (accounting.getTrackCompletionStatus().get(currentTestCase)
                        .equals(TestRunStats.CompletionStatus.SKIPPED_LONG_RUNNING)) {
                    testsSkippedLongRunning.add(currentTestCase);
                }
            }

            testsSkippedForUnknownReason = getAllRegisteredTestClasses(testRegistry);
            testsSkippedForUnknownReason.removeAll(accounting.getTestsRun());
            testsSkippedForUnknownReason.removeAll(testsThrowingAnException);
            testsSkippedForUnknownReason.removeAll(testsSkippedLongRunning);
            testsSkippedForUnknownReason.removeAll(testsApplyingToNoDb);

        } catch (Throwable e) {
            logger.severe("Execution of tests failed: " + e.getMessage());
            logger.log(Level.FINE, "Execution of tests failed: " + e.getMessage(), e);
        }

        if (!testsSkippedForUnknownReason.isEmpty()) {

            ReportManager.problem(new TestRunnerSelfCheck(), "Skipped tests",
                    "The following tests were skipped for no known reason:\n"
                            + testListToBulletPoints(testsSkippedForUnknownReason));
        }

        if (!testsSkippedLongRunning.isEmpty()) {

            ReportManager.correct(new TestRunnerSelfCheck(), "Skipped tests",
                    "The following tests were not run, because they are long running and the run was configured to skip these:\n"
                            + testListToBulletPoints(testsSkippedLongRunning));
        }

        logger.info("Done running tests\n\n");

        boolean printFailureText = true;

        if (this.reporterType == ReporterType.DATABASE) {
            if (!configuration.isSessionID()) {
                logger.info("Finishing reporter session");
                ReportManager.endDatabaseSession();
                logger.info("Finished reporter session");
            }
        } else {
            logger.info("Printing output by test");
            printReportsByTest(outputLevel, printFailureText);
        }
    }

    public static String getDefaultPropertiesFile() {
        return DEFAULT_PROPERTIES_FILE;
    }

    /**
     * <p>
     * Generates a Collection<String> of the databases to be tested.
     * </p>
     * 
     * <p>
     * The databases to be tested are the ones specified explicitly via the
     * testDatabases parameter and the ones specified by the division 
     * parameter.
     * </p> 
     * 
     * <p>
     * If a division parameter has been set, this method will try to query the 
     * production database. If a production database has not been configured,
     * a ConfigurationException is thrown.
     * </p>
     * 
     */
    protected Collection<String> getTestDatabases() {
        Collection<String> dbs = new HashSet<String>();
        if (configuration.isTestDatabases() && configuration.getTestDatabases().size() > 0) {
            for (String db : configuration.getTestDatabases()) {
                if (!StringUtils.isEmpty(db)) {
                    dbs.add(db);
                }
            }
        }
        if (configuration.isDivisions() && configuration.getDivisions().size() > 0) {
            if (!configuration.isProductionDatabase() && !configuration.isOutputHost()) {
                throw new ConfigurationException(
                        "Parameters production.database and output.host etc. must be set to use test_divisions");
            } else {
                // don't want to have to do this, but need to query production
                // separately
                Connection conn = null;
                SqlTemplate template = null;
                try {
                    conn = DBUtils.openConnection(configuration.getOutputDriver(), configuration.getOutputHost(),
                            configuration.getOutputPort(), configuration.getOutputUser(),
                            configuration.getOutputPassword(), configuration.getProductionDatabase());
                    template = new ConnectionBasedSqlTemplateImpl(conn);
                    for (String division : configuration.getDivisions()) {
                        dbs.addAll(template.queryForDefaultObjectList(GET_DIVISION_SPECIES_DBS, String.class,
                                division, division));
                        dbs.addAll(template.queryForDefaultObjectList(GET_DIVISION_DBS, String.class, division,
                                division));
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(
                            "Could not open connection to production db " + configuration.getProductionDatabase(),
                            e);
                } finally {
                    DBUtils.closeQuietly(conn);
                }
            }
        }
        if (dbs.isEmpty()) {
            logger.warning(
                    "No test databases found - Parameters test_databases or test_divisions have not been set - testing all databases...");
        }
        return dbs;
    }

    /**
     * Run appropriate tests against databases. Also run show/repair methods if
     * the test implements the Repair interface and the appropriate flags are
     * set.
     * 
     * @param databaseRegistry
     *            The DatabaseRegistry to use.
     * @param testRegistry
     *            The TestRegistry to use.
     * @param skipSlow
     *            If true, skip long-running tests.
     */
    protected TestRunStats runAllTestsWithAccounting(DatabaseRegistry databaseRegistry, TestRegistry testRegistry,
            boolean skipSlow) {

        logger.info("Running all tests with accounting");

        int numberOfTestsRun = 0;

        HashSet<Class<? extends EnsTestCase>> testsRun = new HashSet<Class<? extends EnsTestCase>>();
        Map<Class<? extends EnsTestCase>, TestRunStats.CompletionStatus> trackCompletionStatus = new HashMap<Class<? extends EnsTestCase>, TestRunStats.CompletionStatus>();
        Map<Class<? extends EnsTestCase>, List<DatabaseRegistryEntry>> exceptionToDb = new HashMap<Class<? extends EnsTestCase>, List<DatabaseRegistryEntry>>();

        // --------------------------------
        // Single-database tests

        // run the appropriate tests on each of them
        for (DatabaseRegistryEntry database : databaseRegistry.getAll()) {
            logger.info("Processing database " + database.getName());
            for (SingleDatabaseTestCase testCase : testRegistry.getAllSingle(groupsToRun, database.getType())) {

                if (!testCase.isLongRunning() || (testCase.isLongRunning() && !skipSlow)) {

                    try {
                        logger.info("Executing " + testCase.getTestName() + " on " + database.getName());
                        ReportManager.startTestCase(testCase, database);

                        testCase.types();

                        boolean result = testCase.run(database);

                        testsRun.add(testCase.getClass());
                        trackCompletionStatus.put(testCase.getClass(), TestRunStats.CompletionStatus.COMPLETED);

                        ReportManager.finishTestCase(testCase, result, database);

                        checkRepair(testCase, database);
                        numberOfTestsRun++;
                        logger.info("Completed executing " + testCase.getTestName() + " on " + database.getName());

                    } catch (Throwable e) {

                        trackCompletionStatus.put(testCase.getClass(),
                                TestRunStats.CompletionStatus.DIED_WITH_EXCEPTION);

                        if (!exceptionToDb.containsKey(testCase.getClass())) {
                            exceptionToDb.put(testCase.getClass(), new ArrayList<DatabaseRegistryEntry>());
                        }

                        exceptionToDb.get(testCase.getClass()).add(database);

                        String msg = "Could not execute test " + testCase.getName() + " on " + database.getName()
                                + ": " + e.getMessage();
                        logger.log(Level.WARNING, msg, e);
                    }

                } else {
                    logger.info("Skipping long-running test " + testCase.getName());
                    trackCompletionStatus.put(testCase.getClass(),
                            TestRunStats.CompletionStatus.SKIPPED_LONG_RUNNING);

                }

            } // foreach test

        } // foreach DB

        // --------------------------------
        // Multi-database tests

        // here we just pass the whole DatabaseRegistry to each test
        // and let the test decide what to do

        for (MultiDatabaseTestCase testCase : testRegistry.getAllMulti(groupsToRun)) {

            if (!testCase.isLongRunning() || (testCase.isLongRunning() && !skipSlow)) {
                try {
                    ReportManager.startTestCase(testCase, null);

                    logger.info("Starting test " + testCase.getName() + " ");

                    testCase.types();
                    boolean result = testCase.run(databaseRegistry);
                    testsRun.add(testCase.getClass());
                    trackCompletionStatus.put(testCase.getClass(), TestRunStats.CompletionStatus.COMPLETED);

                    ReportManager.finishTestCase(testCase, result, null);
                    logger.info(testCase.getName() + " " + (result ? "PASSED" : "FAILED"));

                    numberOfTestsRun++;
                } catch (Throwable e) {
                    //TODO If we had a throwable then we should mark the test as failed 
                    String msg = "Could not execute test " + testCase.getName() + ": " + e.getMessage();
                    logger.log(Level.WARNING, msg, e);
                    trackCompletionStatus.put(testCase.getClass(),
                            TestRunStats.CompletionStatus.DIED_WITH_EXCEPTION);
                }
            } else {

                logger.info("Skipping long-running test " + testCase.getName());
                trackCompletionStatus.put(testCase.getClass(), TestRunStats.CompletionStatus.SKIPPED_LONG_RUNNING);

            }

        } // foreach test

        // --------------------------------
        // Ordered database tests

        // getAll() should give back databases in the order they were specified
        // on the command line
        DatabaseRegistryEntry[] orderedDatabases = databaseRegistry.getAll();

        for (OrderedDatabaseTestCase testCase : testRegistry.getAllOrdered(groupsToRun)) {

            ReportManager.startTestCase(testCase, null);

            try {
                boolean result = testCase.run(orderedDatabases);
                testsRun.add(testCase.getClass());

                ReportManager.finishTestCase(testCase, result, null);
                logger.info(testCase.getName() + " " + (result ? "PASSED" : "FAILED"));
            } catch (Throwable e) {
                //TODO If we had a throwable then we should mark the test as failed
                String msg = "Could not execute test " + testCase.getName() + ": " + e.getMessage();
                logger.log(Level.WARNING, msg, e);
            }

            numberOfTestsRun++;

        } // foreach test

        // --------------------------------

        if (numberOfTestsRun == 0) {
            logger.warning("Warning: no tests were run.");
        }

        return new TestRunStats(testsRun, trackCompletionStatus, exceptionToDb);
    } // runAllTests

    /**
     * <p>
     * Users specify the exact names of databases and these are used to 
     * initialise the DatabaseRegistry object.
     * </p>
     * 
     * <p>
     * The DatabaseRegistry however uses them as regular expressions to which
     * databases may match or not. If no matching database was found, it does
     * not complain about it. The database will appear as if it has passed.
     * </p>
     * 
     * <p>
     * This is not the desired behaviour in the configurable testrunner. If 
     * users misspell a database name, this should flag as an error.
     * </p>
     * 
     * @param databasesToTestRegistry
     * @param testDatabases
     */
    void complainAboutDatabasesNotFound(DatabaseRegistry databasesToTestRegistry, List<String> testDatabases) {

        HashSet<String> namesOfDbsFound = new HashSet<String>();
        for (DatabaseRegistryEntry currentDRE : databasesToTestRegistry.getAll()) {
            namesOfDbsFound.add(currentDRE.getName());
        }
        for (String dbName : testDatabases) {
            if (!namesOfDbsFound.contains(dbName)) {
                ReportManager.problem(new TestRunnerSelfCheck(), "Configuration problem", "Database " + dbName
                        + " has been specified for testing, but it doesn't exist on the server!");
            }
        }
    }

    /**
     * <p>
     *    Convert a list of tests into a string in which the names are listed
     * in bullet points. The list is sorted by class names.
     * </p>
     * 
     * <p>
     * Useful for printing.
     * </p>
     * 
     * @param listOfTests
     * @return Stringified version of the list as bullet points
     */
    protected String testListToBulletPoints(List<Class<? extends EnsTestCase>> listOfTests) {

        StringBuffer missingTestToString = new StringBuffer();

        // Sort list before printing. Otherwise they will appear in an
        // arbitrary order and appear as new every day on the admin site.
        //
        Collections.sort(listOfTests, new Comparator<Class<? extends EnsTestCase>>() {
            @Override
            public int compare(Class<? extends EnsTestCase> o1, Class<? extends EnsTestCase> o2) {

                return o1.getName().compareTo(o2.getName());
            }
        });

        for (Class<? extends EnsTestCase> currentMissingTest : listOfTests) {
            missingTestToString.append("  - " + currentMissingTest.getName() + "\n");
        }
        return missingTestToString.toString();
    }

    /**
     * <p>
     *    Convert a list of DatabaseRegistryEntry into a string in which the 
     * names are listed in bullet points. The list is sorted using the 
     * {@link DatabaseRegistryEntry#compareTo(DatabaseRegistryEntry)} method.
     * </p>
     * 
     * <p>
     * Useful for printing.
     * </p>
     * 
     * @return Stringified version of the list as bullet points
     */
    protected String dbreListToBulletPoints(List<DatabaseRegistryEntry> listOfDbres) {

        // Sort list before printing. Otherwise they will appear in an
        // arbitrary order and appear as new every day on the admin site.
        //
        Collections.sort(listOfDbres, new Comparator<DatabaseRegistryEntry>() {
            @Override
            public int compare(DatabaseRegistryEntry o1, DatabaseRegistryEntry o2) {

                return o1.compareTo(o2);
            }
        });

        StringBuffer listOfDbresToString = new StringBuffer();

        for (DatabaseRegistryEntry currentDbre : listOfDbres) {
            listOfDbresToString.append("  - " + currentDbre.getName() + "\n");
        }
        return listOfDbresToString.toString();
    }

    List<Class<? extends EnsTestCase>> findTestsApplyingToNoDb(TestRegistry testRegistry,
            DatabaseRegistry databasesToTestRegistry) {

        HashSet<DatabaseType> databaseTypesRegistered = new HashSet<DatabaseType>();

        for (DatabaseRegistryEntry dbre : databasesToTestRegistry.getAll()) {
            databaseTypesRegistered.add(dbre.getType());
        }

        List<Class<? extends EnsTestCase>> testsApplyingToNoDb = new ArrayList<Class<? extends EnsTestCase>>();

        for (Class<? extends EnsTestCase> currentTest : getAllRegisteredTestClasses(testRegistry)) {

            DatabaseType[] dbT;

            /*
             * Instead of instantiating a test directly, a group of this one 
             * test is created and then retrieved from it.
             * 
             * The reason is that "getTests" has method calls to set the types
             * of databases to which this test can be applied.
             * 
             * By instantiating the test this way, the testcase is initialised
             * the same way as it will be when it is run by the testrunner.
             * 
             */
            GroupOfTests g = new GroupOfTests();
            g.addTest(currentTest);
            dbT = g.getTests().iterator().next().getAppliesToTypes();

            boolean currentTestAppliesToADb = false;

            for (DatabaseType currentDbt : dbT) {
                if (databaseTypesRegistered.contains(currentDbt)) {
                    currentTestAppliesToADb = true;
                    if (currentTestAppliesToADb) {
                        break;
                    }
                }
            }
            if (!currentTestAppliesToADb) {
                testsApplyingToNoDb.add(currentTest);
            }
        }
        return testsApplyingToNoDb;
    }
}

class TestRunnerSelfCheck extends EnsTestCase {
    public TestRunnerSelfCheck() {
        setTeamResponsible(Team.RELEASE_COORDINATOR);
    }
};

class TestRunStats {

    protected enum CompletionStatus {
        COMPLETED, SKIPPED_LONG_RUNNING, DIED_WITH_EXCEPTION
    }

    public HashSet<Class<? extends EnsTestCase>> getTestsRun() {
        return testsRun;
    }

    public Map<Class<? extends EnsTestCase>, CompletionStatus> getTrackCompletionStatus() {
        return trackCompletionStatus;
    }

    protected final HashSet<Class<? extends EnsTestCase>> testsRun;
    protected final Map<Class<? extends EnsTestCase>, CompletionStatus> trackCompletionStatus;
    protected final Map<Class<? extends EnsTestCase>, List<DatabaseRegistryEntry>> exceptionToDb;

    public Map<Class<? extends EnsTestCase>, List<DatabaseRegistryEntry>> getExceptionToDb() {
        return exceptionToDb;
    }

    public TestRunStats(HashSet<Class<? extends EnsTestCase>> testsRun,
            Map<Class<? extends EnsTestCase>, CompletionStatus> trackCompletionStatus,
            Map<Class<? extends EnsTestCase>, List<DatabaseRegistryEntry>> exceptionToDb) {
        this.testsRun = testsRun;
        this.trackCompletionStatus = trackCompletionStatus;
        this.exceptionToDb = exceptionToDb;
    }
}