com.greenpepper.maven.plugin.SpecificationRunnerMojo.java Source code

Java tutorial

Introduction

Here is the source code for com.greenpepper.maven.plugin.SpecificationRunnerMojo.java

Source

/*
 * Copyright (c) 2007 Pyxis Technologies inc.
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA,
 * or see the FSF site: http://www.fsf.org.
 */

package com.greenpepper.maven.plugin;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.greenpepper.Statistics;
import com.greenpepper.document.GreenPepperInterpreterSelector;
import com.greenpepper.maven.plugin.runner.Runner;
import com.greenpepper.maven.plugin.runner.RunnerTask;
import com.greenpepper.maven.plugin.utils.NullStatisticsListener;
import com.greenpepper.maven.plugin.utils.ProjectsIndex;
import com.greenpepper.maven.plugin.utils.RepositoryIndex;
import com.greenpepper.maven.plugin.utils.TestResultsIndex;
import com.greenpepper.repository.DocumentRepository;
import com.greenpepper.repository.FileSystemRepository;
import com.greenpepper.runner.CompositeSpecificationRunnerMonitor;
import com.greenpepper.runner.RecorderMonitor;
import com.greenpepper.runner.RunnerStatistics;
import com.greenpepper.runner.SpecificationRunnerMonitor;
import com.greenpepper.runner.repository.DocumentNeverImplementedException;
import com.greenpepper.server.domain.*;
import com.greenpepper.util.IOUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.jsoup.Jsoup;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.concurrent.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import static com.greenpepper.maven.plugin.runner.Runner.DEFAULT_RUNNER_NAME;
import static com.greenpepper.util.URIUtil.escapeFileSystemForbiddenCharacters;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static org.apache.commons.io.FileUtils.*;
import static org.apache.commons.lang3.StringUtils.*;

/**
 * Run the specification testing.
 *
 * @goal run
 * @phase integration-test
 * @requiresDependencyResolution test
 * @description Runs GreenPepper specifications
 * @author oaouattara
 * @version $Id: $Id
 */
@SuppressWarnings("JavaDoc")
public class SpecificationRunnerMojo extends SpecificationNavigatorMojo {

    private static final Logger LOGGER = LoggerFactory.getLogger(SpecificationRunnerMojo.class);

    private static final String HTML_EXTENSION = ".html";

    private class RunnerResult {
        private final Future<?> future;
        private final RunnerTask runnerTask;

        RunnerResult(RunnerTask runnerTask, Future<?> future) {
            this.runnerTask = runnerTask;
            this.future = future;
        }
    }

    /**
     * Set this to 'true' to bypass greenpepper tests entirely.
     * Its use is NOT RECOMMENDED, but quite convenient on occasion.
     *
     * @parameter property="maven.greenpepper.test.skip" default-value="false"
     */
    @SuppressWarnings("unused")
    private boolean skip;

    /**
     * Project fixture classpath.
     * @parameter property="project.runtimeClasspathElements"
     * @required
     * @readonly
     */
    List<String> classpathElements;

    /**
     * The directory where compiled fixture classes go.
     *
     * @parameter default-value="${project.build.directory}/fixture-test-classes"
     * @required
     */
    @SuppressWarnings("unused")
    private File fixtureOutputDirectory;

    /**
     * The SystemUnderDevelopment class to use
     * @parameter default-value="com.greenpepper.systemunderdevelopment.DefaultSystemUnderDevelopment"
     * @required
     */
    String systemUnderDevelopment;

    /**
     * The {@link com.greenpepper.systemunderdevelopment.SystemUnderDevelopment} constructor args.
     * This parameter is optionnal and can be achieved by appending them to the systemUnderDevelopment parameter.
     * @parameter
     */
    String systemUnderDevelopmentArgs;

    /**
     * @parameter property="plugin.artifacts"
     * @required
     * @readonly
     */
    List<Artifact> pluginDependencies;

    /**
     * Set this to 'true' to stop the execution on a failure.
     * @parameter property="maven.greenpepper.test.stop" default-value="false"
     */
    @SuppressWarnings("unused")
    private boolean stopOnFirstFailure;

    /**
     * Set the locale for the execution.
     * @parameter property="maven.greenpepper.locale"
     */
    String locale;

    /**
     * Set the Selector class.
     * @parameter property="maven.greenpepper.selector"
     *            default-value="com.greenpepper.document.GreenPepperInterpreterSelector"
     */
    String selector;

    /**
     * Set the Debug mode.
     *
     * @parameter property="maven.greenpepper.debug" default-value="false"
     */
    boolean debug;

    /**
     * Set this to true to ignore a failure during testing.
     * Its use is NOT RECOMMENDED, but quite convenient on occasion.
     *
     * @parameter property="maven.greenpepper.test.failure.ignore" default-value="false"
     */
    @SuppressWarnings("unused")
    private boolean testFailureIgnore;

    /**
     * Set this to true to output the logs only in the log file and not in the console.
     *
     * @parameter property="maven.greenpepper.redirect.output" default-value="false"
     */
    @SuppressWarnings("unused")
    private boolean redirectOutputToFile;

    /**
     * Set this property to true to launch only new specifications + failed ones.
     *
     * @parameter property="maven.greenpepper.resume" default-value="false"
     */
    boolean resume;

    /**
     * Set this to a Specification name to run only this test.
     * The test is searched inside the default repository.
     *
     * @parameter property="gp.test"
     */
    String testSpecification;

    /**
     * Set this to a Specification name to run only this test.
     * The test is searched inside the default repository.
     *
     * @parameter property="gp.testOutput"
     */
    String testSpecificationOutput;

    /**
     * Set this to a Repository name defined in the pom.xml.
     * This option is only used in case <code>-Dgp.test</code> is used.
     *
     * @parameter property="gp.repo"
     */
    String selectedRepository;

    /**
     * Launch the test in the Maven process if false. Or fork a java process if true.
     *
     * @parameter property="maven.greenpepper.fork" default-value="false"
     */
    boolean fork;

    /**
     * The maximum number of default runner processes that needs to be spawn;
     *
     * @parameter property="maven.greenpepper.forkcount" default=1
     */
    Integer forkCount;

    /**
     * The Java Virtual Machine path to use for the default runner in fork mode.
     *
     * @parameter property="maven.greenpepper.jvm" default-value="java"
     */
    @SuppressWarnings("FieldCanBeLocal")
    private String jvm = "java";

    /**
     * Additionnal JAVA Options to be added to the java command in fork mode.
     *
     * <strong>This is only used in FORK mode and for the default runner.</strong>
     *
     * @parameter property="maven.greenpepper.javaoptions"
     */
    String javaOptions;

    /**
     * When launching the tests in a fork, we create a default runner. You can exclude this default runner from the
     * testing process if you want to configure your owns.
     *
     * @parameter property="maven.greenpepper.excludedefaultrunner" default-value=false
     */
    boolean excludeDefaultRunner;

    /**
     * The list of runners that can be associated to repositories for testing.
     *
     * @parameter
     */
    List<Runner> runners;

    /**
     * @component
     */
    protected MavenProject project;

    private HashMap<String, ExecutorService> executorMap = new HashMap<String, ExecutorService>();
    HashMap<String, Runner> runnerMap = new HashMap<String, Runner>();
    private LinkedHashSet<RunnerResult> runnerResults = new LinkedHashSet<RunnerResult>();

    RunnerStatistics runnerStatistics;

    private boolean testFailed;
    private boolean exceptionOccured;
    HashMap<String, TestResultsIndex> testResultsIndexes = new HashMap<String, TestResultsIndex>();

    /**
     * <p>Constructor for SpecificationRunnerMojo.</p>
     */
    public SpecificationRunnerMojo() {
        this.runnerStatistics = new RunnerStatistics();
    }

    /**
     * <p>execute.</p>
     *
     * @throws org.apache.maven.plugin.MojoExecutionException if any.
     * @throws org.apache.maven.plugin.MojoFailureException if any.
     */
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (skip) {
            getLog().info("Not executing specifications.");
        } else {
            prepareReportsDir();
            prepareRunnerExecutors();
            printBanner();
            try {
                runAllTests();
            } finally {
                printFooter();
                for (TestResultsIndex testResultsIndex : testResultsIndexes.values()) {
                    testResultsIndex.dump();
                }
            }
            checkTestsResults();
        }
    }

    private void prepareRunnerExecutors() {
        if (runners == null || runners.isEmpty()) {
            runners = new ArrayList<Runner>();
            if (!excludeDefaultRunner) {
                runners.add(getDefaultRunner());
            }
        }
        for (Runner runner : runners) {
            ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(runner.getName() + "-%d")
                    .build();
            ExecutorService executorService = Executors.newFixedThreadPool(runner.getForkCount(), threadFactory);
            executorMap.put(runner.getName(), executorService);
            runnerMap.put(runner.getName(), runner);
            if (fork) {
                runner.setRedirectOutputToFile(true);
            }
        }
    }

    private void checkTestsResults() throws MojoExecutionException, MojoFailureException {
        if (exceptionOccured)
            notifyExceptionsOccured();
        if (testFailed)
            notifyTestsFailed();
    }

    private void notifyExceptionsOccured() throws MojoExecutionException {
        if (testFailureIgnore) {
            getLog().error("Some greenpepper tests did not run\n");
        } else {
            throw new MojoExecutionException("Some greenpepper tests did not run");
        }
    }

    private void notifyTestsFailed() throws MojoFailureException {
        if (!testResultsIndexes.isEmpty()) {
            System.out.println("List of failed tests");
            for (Map.Entry<String, TestResultsIndex> indexEntry : testResultsIndexes.entrySet()) {
                String repoName = indexEntry.getKey();
                TestResultsIndex testResultsIndex = indexEntry.getValue();
                for (Map.Entry<String, TestResultsIndex.TestResults> entry : testResultsIndex.getNameToInfo()
                        .entrySet()) {
                    Statistics statistics = entry.getValue().getStatistics();
                    if (statistics.hasFailed()) {
                        System.out.println(
                                format("\t %s (Repository: %s) (%s)", entry.getKey(), repoName, statistics));
                    }
                }
            }
            System.out.println();
            System.out.println(
                    "You can run the failed tests using the resume option '-Dmaven.greenpepper.resume=true'");
            System.out.println();
        }

        if (testFailureIgnore) {
            getLog().error("There were greenpepper tests failures\n");
        } else {
            throw new MojoFailureException("There were greenpepper tests failures");
        }
    }

    private void printBanner() {
        System.out.println();
        System.out.println("-----------------------------------------------------");
        System.out.println(" G R E E N  P E P P E R  S P E C I F I C A T I O N S ");
        System.out.println("-----------------------------------------------------");
        System.out.println();
    }

    private void runAllTests() throws MojoExecutionException, MojoFailureException {
        if (StringUtils.isNotEmpty(testSpecification)) {
            // Locate default repository
            Repository defaultRepository = null;
            if (repositories.size() == 1) {
                defaultRepository = repositories.get(0);
            } else {
                boolean repositorySelected = StringUtils.isNotEmpty(selectedRepository);
                if (repositorySelected) {
                    defaultRepository = extractSelectedRepository();
                } else {
                    for (Repository repository : repositories) {
                        if (repository.isDefault()) {
                            defaultRepository = repository;
                            break;
                        }
                    }
                }
            }
            if (defaultRepository == null) {
                throw new MojoExecutionException(
                        "A default repository should be set when using '-Dgp.test='. Use '-Dgp.repo=' or specify it in the pom.xml");
            }

            // Run the test
            runSingleTest(defaultRepository, testSpecification);

            checkAsynchTasks();

        } else {
            boolean repositorySelected = StringUtils.isNotEmpty(selectedRepository);
            if (repositorySelected) {
                runAllIn(extractSelectedRepository());
            } else {
                for (Repository repository : repositories) {
                    if (shouldStop()) {
                        break;
                    }

                    runAllIn(repository);
                }
            }
        }

    }

    private Repository extractSelectedRepository() throws MojoExecutionException {
        for (Repository repository : repositories) {
            if (StringUtils.equalsIgnoreCase(selectedRepository, repository.getName())) {
                return repository;
            }
        }
        throw new MojoExecutionException(
                format("Repository '%s' not found in the list of repository.", selectedRepository));
    }

    private void checkAsynchTasks() {
        for (RunnerResult runnerResult : runnerResults) {
            try {
                // This will wait for this Task completion
                runnerResult.future.get();
                RecorderMonitor recorder = runnerResult.runnerTask.getRecorder();

                exceptionOccured |= recorder.hasException();
                testFailed |= recorder.hasTestFailures();
                registerStatistics(recorder);
            } catch (InterruptedException e) {
                getLog().error(format("The task [%s] has been interrupted",
                        runnerResult.runnerTask.getSpecification().getName()));
            } catch (ExecutionException e) {
                getLog().error(
                        format("The task [%s] has failed", runnerResult.runnerTask.getSpecification().getName()),
                        e);
            }
        }
        runnerResults.clear();
    }

    private void runAllIn(Repository repository) throws MojoExecutionException, MojoFailureException {
        List<String> repositorySpecifications;
        try {
            repositorySpecifications = listRepositorySpecifications(repository);
        } catch (Exception e) {
            throw new MojoExecutionException(
                    format("Couldn't list repository '%s' specifications", repository.getName()), e);
        }

        try {
            extractHtmlReportSummary();
            prepareProjectIndex(repository);
            prepareTestResultsIndex(repository);
        } catch (URISyntaxException e) {
            throw new MojoExecutionException(
                    format("Couldn't prepare the report for repository '%s'", repository.getName()), e);
        } catch (IOException e) {
            throw new MojoExecutionException(
                    format("Couldn't prepare the report for repository '%s'", repository.getName()), e);
        }

        repository.getTests().clear();
        repository.getTests().addAll(repositorySpecifications);
        runTestsIn(repository);

        checkAsynchTasks();

        System.out.println();
        System.out.println(format("See the report at %s",
                getFile(reportsDirectory, "index" + HTML_EXTENSION).getAbsolutePath()));
        System.out.println();
    }

    private void prepareProjectIndex(Repository repository) throws IOException {
        File storage = new File(reportsDirectory, "index.json");
        ProjectsIndex projectsIndex = new ProjectsIndex(storage);
        try {
            projectsIndex.load();
        } catch (IOException e) {
            getLog().warn(format("index.json is corrupted. Start a new one. Cause: %s", e.getMessage()));
            FileUtils.moveFile(storage, new File(reportsDirectory, "index.json.orig"));
        }
        ProjectsIndex.ProjectInfo projectInfo = projectsIndex.getNameToInfo().get(repository.getName());
        if (projectInfo == null) {
            projectInfo = new ProjectsIndex.ProjectInfo();
            projectInfo.projectName = repository.getProjectName();
            projectInfo.repoName = repository.getName();
            projectInfo.repoId = getRepositoryMetaName(repository);
            projectInfo.systemUnderTest = repository.getSystemUnderTest();
            projectInfo.startDate = ProjectsIndex.ProjectInfo.SIMPLE_DATE_FORMAT.format(new Date());
            projectsIndex.getNameToInfo().put(repository.getName(), projectInfo);
        } else {
            projectInfo.startDate = ProjectsIndex.ProjectInfo.SIMPLE_DATE_FORMAT.format(new Date());
        }
        projectsIndex.dump();
    }

    private void prepareTestResultsIndex(Repository repository) throws IOException {
        File storage = getResultsIndexFile(repository);
        TestResultsIndex testResultsIndex = TestResultsIndex.newInstance(storage);
        testResultsIndexes.put(repository.getName(), testResultsIndex);
    }

    private void extractHtmlReportSummary() throws IOException, URISyntaxException {
        final String path = "html-summary-report";
        final File jarFile = new File(getClass().getProtectionDomain().getCodeSource().getLocation().getPath());

        forceMkdir(reportsDirectory);
        if (jarFile.isFile()) { // Run with JAR file
            JarFile jar = new JarFile(jarFile);
            Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String name = jarEntry.getName();
                if (name.startsWith(path)) { //filter according to the path
                    File file = getFile(reportsDirectory, substringAfter(name, path));
                    if (jarEntry.isDirectory()) {
                        forceMkdir(file);
                    } else {
                        forceMkdir(file.getParentFile());
                        if (!file.exists()) {
                            copyInputStreamToFile(jar.getInputStream(jarEntry), file);
                        }
                    }
                }
            }
            jar.close();
        } else { // Run with IDE
            URL url = getClass().getResource("/" + path);
            if (url != null) {
                File apps = FileUtils.toFile(url);
                if (apps.isDirectory()) {
                    copyDirectory(apps, reportsDirectory);
                } else {
                    throw new IllegalStateException(
                            format("Internal resource '%s' should be a directory.", apps.getAbsolutePath()));
                }
            } else {
                throw new IllegalStateException(format("Internal resource '/%s' should be here.", path));
            }
        }
    }

    File getResultsIndexFile(Repository repository) throws UnsupportedEncodingException {
        return new File(reportsDirectory, getRepositoryMetaName(repository) + ".results");
    }

    private void runTestsIn(Repository repository) throws MojoExecutionException, MojoFailureException {
        if (!resume) {
            TestResultsIndex testResultsIndex = testResultsIndexes.get(repository.getName());
            if (testResultsIndex != null) {
                testResultsIndex.getNameToInfo().clear();
            }
        }

        for (String test : repository.getTests()) {
            if (shouldStop()) {
                break;
            }

            runSingleTest(repository, test);
        }
    }

    private void runSingleTest(Repository repository, String test)
            throws MojoExecutionException, MojoFailureException {

        if (resume) {
            // Resume set, we will run the test only if it's new or failing;
            TestResultsIndex testResultsIndex = testResultsIndexes.get(repository.getName());
            if (testResultsIndex != null) {
                Map<String, TestResultsIndex.TestResults> results = testResultsIndex.getNameToInfo();
                if (results.containsKey(test) && !results.get(test).getStatistics().hasFailed()) {
                    getLog().info(format("Skipping %s due to 'resume' option set", test));
                    return;
                }
            }
        }

        String repoCmdOption;
        boolean managingFileSystem;
        try {
            DocumentRepository documentRepository = repository.getDocumentRepository();
            managingFileSystem = FileSystemRepository.class.isAssignableFrom(documentRepository.getClass());
        } catch (Exception e) {
            throw new MojoFailureException("Unable to get the document repository", e);
        }
        if (managingFileSystem) {
            File projectBasedir = project.getBasedir();
            repoCmdOption = repository.getType() + ";";
            if (repository.getRoot() != null) {
                File relativeRoot = new File(repository.getRoot());
                File absoluteDir;
                if (relativeRoot.getAbsoluteFile().compareTo(relativeRoot) == 0) {
                    absoluteDir = relativeRoot;
                } else {
                    absoluteDir = new File(projectBasedir, repository.getRoot());
                }
                repoCmdOption += absoluteDir.getAbsolutePath();
            } else {
                repoCmdOption += projectBasedir.getAbsolutePath();
            }
        } else {
            repoCmdOption = repository.getType() + (repository.getRoot() != null ? ";" + repository.getRoot() : "");
        }

        File repositoryReportsFolder = new File(reportsDirectory, repository.getName());

        runnerStatistics.addToTotal(1);

        if (fork || isNotBlank(repository.getRunnerName())) {
            // If the repository specified a runner
            runInForkedRunner(repository, test, repositoryReportsFolder);
        } else {
            runInEmbeddedRunner(repository, test, repoCmdOption, repositoryReportsFolder);
        }

    }

    private void runInForkedRunner(Repository repository, String test, File repositoryReportsFolder)
            throws MojoExecutionException {
        File outputFile = new File(repositoryReportsFolder, test);
        SystemUnderTest systemUnderTest = new SystemUnderTest();
        systemUnderTest.setName(repository.getSystemUnderTest());
        systemUnderTest.setProject(Project.newInstance(repository.getProjectName()));

        Specification specification = Specification.newInstance(test);
        com.greenpepper.server.domain.Repository repositoryRunner = com.greenpepper.server.domain.Repository
                .newInstance(repository.getName());
        RepositoryType repositoryType = RepositoryType.newInstance("FILE");
        repositoryType.setRepositoryClass(repository.getType());
        EnvironmentType java = EnvironmentType.newInstance("JAVA");
        repositoryType.registerClassForEnvironment(repository.getType(), java);
        repositoryRunner.setBaseTestUrl(repository.getRoot());

        repositoryRunner.setType(repositoryType);
        specification.setRepository(repositoryRunner);
        systemUnderTest.setFixtureFactory(systemUnderDevelopment);
        systemUnderTest.setFixtureFactoryArgs(systemUnderDevelopmentArgs);

        specification.setDialectClass(repository.getDialect());

        String runnerName = repository.getRunnerName();
        if (runnerName == null) {
            if (runnerMap.size() == 1) {
                runnerName = runnerMap.keySet().iterator().next();
            } else {
                runnerName = DEFAULT_RUNNER_NAME;
            }
        }

        Runner defaultRunner = runnerMap.get(runnerName);
        ExecutorService executorService = executorMap.get(runnerName);

        if (defaultRunner != null && executorService != null) {
            if (defaultRunner.getForkCount() == 1) {
                defaultRunner.setRedirectOutputToFile(redirectOutputToFile);
            }

            // We will try to get the external-link after the test (for version of Greepepper < 4.1 )
            defaultRunner.setRepositoryIndex(this.repositoryIndexes.get(repository.getName()));
            TestResultsIndex testResultsIndex = testResultsIndexes.get(repository.getName());
            RunnerTask runnerTask = new RunnerTask(specification, systemUnderTest, outputFile.getAbsolutePath(),
                    defaultRunner, getLog(), testResultsIndex);
            if (defaultRunner.isIncludeProjectClasspath()) {
                TreeSet<String> classpath = new TreeSet<String>();
                for (URL url : createClasspath()) {
                    classpath.add(FileUtils.toFile(url).getAbsolutePath());
                }
                systemUnderTest.setSutClasspaths(classpath);
            }

            Future<?> future = executorService.submit(runnerTask);
            RunnerResult runnerResult = new RunnerResult(runnerTask, future);
            runnerResults.add(runnerResult);
        } else {
            getLog().warn(format(
                    "No runner found for executing %s in repository %s. Runner '%s' was specified (or falled back to)",
                    specification.getName(), repository.getName(), runnerName));
        }
    }

    private Runner getDefaultRunner() {
        List<String> optionsList = new ArrayList<String>();
        appendOptionsList(optionsList);
        Runner defaultRunner = Runner.createDefault(jvm, javaOptions, optionsList);
        if (forkCount != null) {
            defaultRunner.setForkCount(forkCount);
        }
        return defaultRunner;
    }

    private void runInEmbeddedRunner(Repository repository, String test, String repoCmdOption,
            File repositoryReportsFolder) throws MojoExecutionException, MojoFailureException {
        String outputDir = repositoryReportsFolder.getAbsolutePath();

        String systemUnderDevelopmentWithArgs = getFixtureFactoryWithArgs();

        List<String> args = new ArrayList<String>();
        args.addAll(asList("-f", systemUnderDevelopmentWithArgs, "-r", repoCmdOption, "-o", outputDir));
        if (isNotBlank(repository.getDialect())) {
            List<String> dialectOption = asList("-d", repository.getDialect());
            args.addAll(dialectOption);
        }

        appendOptionsList(args);

        args.add(test);

        // Add the target output (which might have the same name as the test
        String output = null;
        if (StringUtils.isNoneEmpty(testSpecification, testSpecificationOutput)) {
            output = testSpecificationOutput;
        } else if (endsWithIgnoreCase(test, HTML_EXTENSION)) {
            output = test;
        }
        if (isNotBlank(output)) {
            args.add(output);
        }

        // try to define the output file
        File outputFile;
        if (isNotBlank(output)) {
            outputFile = getFile(outputDir, escapeFileSystemForbiddenCharacters(output));
        } else {
            if (!endsWithIgnoreCase(test, HTML_EXTENSION)) {
                outputFile = getFile(outputDir, escapeFileSystemForbiddenCharacters(test) + HTML_EXTENSION);
            } else {
                outputFile = getFile(outputDir, escapeFileSystemForbiddenCharacters(test));
            }
        }

        File outLogFile = new File(outputFile.getAbsolutePath() + "-output.log");
        File errLogFile = new File(outputFile.getAbsolutePath() + "-err.log");
        LogWriterMonitor logWriterMonitor = new LogWriterMonitor(outLogFile, errLogFile);

        TestMonitor testMonitor = new TestMonitor(getLog(), new NullStatisticsListener());

        RecorderMonitor recorderMonitor = run(args, logWriterMonitor, testMonitor);

        TestResultsIndex testResultsIndex = testResultsIndexes.get(repository.getName());
        if (testResultsIndex != null) {
            testResultsIndex.notify(test, recorderMonitor.getStatistics(), testMonitor.duration);
        }

        RepositoryIndex repositoryIndex = repositoryIndexes.get(repository.getName());
        if (repositoryIndex != null && repositoryIndex.getNameToInfo().containsKey(test)
                && isBlank(repositoryIndex.getNameToInfo().get(test).getLink())) {
            if (outputFile.isFile() && outputFile.canRead()) {
                try {
                    String outputHTML = readFileToString(outputFile);
                    recoverLinkInResult(test, outputHTML, repositoryIndex);
                } catch (Exception e) {
                    getLog().debug(format("Could not read the output file '%s'. Cause : %s",
                            outputFile.getAbsolutePath(), e.getMessage()));
                    LOGGER.trace("full trace: ", e);
                }
            }
        }

    }

    private String getFixtureFactoryWithArgs() {
        String systemUnderDevelopmentWithArgs = systemUnderDevelopment;
        if (isNotBlank(systemUnderDevelopmentArgs)) {
            systemUnderDevelopmentWithArgs = systemUnderDevelopment + ";" + systemUnderDevelopmentArgs;
        }
        return systemUnderDevelopmentWithArgs;
    }

    public static void recoverLinkInResult(String specification, String htmlString, RepositoryIndex repositoryIndex)
            throws IOException {
        RepositoryIndex.SpecificationInfo specificationInfo = repositoryIndex.getNameToInfo().get(specification);
        if (isBlank(specificationInfo.getLink()) && isNotBlank(htmlString)) {
            LOGGER.trace("got new missing link in index for '{}'. trying to find it in the result output",
                    specification);
            org.jsoup.nodes.Document resultOutput = Jsoup.parse(htmlString);
            Elements metaTags = resultOutput.head().getElementsByTag("meta");
            String link = metaTags.select("[name=\"external-link\"]").attr("content");
            if (isNotBlank(link)) {
                LOGGER.trace("Found {}", link);
                specificationInfo.setLink(link);
                repositoryIndex.dump();
            }
        }
    }

    private RecorderMonitor run(List<String> args, SpecificationRunnerMonitor... testMonitors)
            throws MojoExecutionException, MojoFailureException {
        DynamicCoreInvoker runner = new DynamicCoreInvoker(createClassLoader());
        CompositeSpecificationRunnerMonitor monitors = new CompositeSpecificationRunnerMonitor();
        for (SpecificationRunnerMonitor specificationRunnerMonitor : testMonitors) {
            monitors.add(specificationRunnerMonitor);
        }
        RecorderMonitor recorder = new RecorderMonitor();
        monitors.add(recorder);
        runner.setMonitor(monitors);

        try {
            runner.run(toArray(args));
        } catch (DocumentNeverImplementedException e) {
            LOGGER.info(DocumentRepository.THIS_SPECIFICATION_WAS_NEVER_SET_AS_IMPLEMENTED);
        } catch (Exception e) {
            exceptionOccured = true;
            throw new MojoExecutionException("Unable to run tests", e);
        }

        exceptionOccured |= recorder.hasException();
        testFailed |= recorder.hasTestFailures();
        registerStatistics(recorder);
        return recorder;
    }

    private void registerStatistics(RecorderMonitor recorder) {
        runnerStatistics.tally(recorder.getStatistics());
    }

    private void printFooter() {
        System.out.println();
        System.out.println(runnerStatistics);
        System.out.println();
    }

    private ClassLoader createClassLoader() throws MojoExecutionException {
        URL[] classpath = createClasspath();

        return new URLClassLoader(classpath, ClassLoader.getSystemClassLoader());
    }

    private URL[] createClasspath() throws MojoExecutionException {
        List<URL> urls = new ArrayList<URL>();
        if (classpathElements != null) {
            for (String classpathElement : classpathElements) {
                urls.add(toURL(new File(classpathElement)));
            }
        }

        urls.add(toURL(fixtureOutputDirectory));

        if (!containsGreenPepperCore(urls)) {
            urls.add(getDependencyURL("greenpepper-core"));
        }

        urls.add(getDependencyURL("greenpepper-extensions-java"));
        urls.add(getDependencyURL("slf4j-api"));
        urls.add(getDependencyURL("jcl-over-slf4j"));

        return urls.toArray(new URL[urls.size()]);
    }

    private URL getDependencyURL(String name) throws MojoExecutionException {
        if (pluginDependencies != null && !pluginDependencies.isEmpty()) {
            for (Artifact artifact : pluginDependencies) {
                if (artifact.getArtifactId().equals(name) && artifact.getType().equals("jar"))
                    return toURL(artifact.getFile());
            }
        }
        throw new MojoExecutionException("Dependency not found: " + name);
    }

    private URL toURL(File f) throws MojoExecutionException {
        try {
            return f.toURI().toURL();
        } catch (MalformedURLException e) {
            throw new MojoExecutionException("Invalid dependency: " + f.getAbsolutePath(), e);
        }
    }

    private boolean containsGreenPepperCore(List<URL> urls) {

        for (URL url : urls) {

            if (url.getFile().contains("greenpepper-core") && url.getFile().endsWith(".jar")) {
                return true;
            }
        }

        return false;
    }

    private void prepareReportsDir() throws MojoExecutionException {
        if (StringUtils.isAnyEmpty(testSpecification, testSpecificationOutput)) {
            try {
                IOUtil.createDirectoryTree(reportsDirectory);
            } catch (IOException e) {
                throw new MojoExecutionException(
                        "Could not create reports directory: " + reportsDirectory.getAbsolutePath());
            }
        }

    }

    private boolean shouldStop() {
        return stopOnFirstFailure && runnerStatistics.hasFailure();
    }

    private void appendOptionsList(List<String> arguments) {
        if (!StringUtils.isEmpty(locale)) {
            arguments.add("--locale");
            arguments.add(locale);
        }

        if (!StringUtils.isEmpty(selector) && !GreenPepperInterpreterSelector.class.getName().equals(selector)) {
            arguments.add("--selector");
            arguments.add(selector);
        }

        if (stopOnFirstFailure) {
            arguments.add("--stop");
        }

        if (debug) {
            arguments.add("--debug");
        }
    }

    private String[] toArray(List<String> args) {
        String[] arguments = new String[args.size()];
        args.toArray(arguments);
        return arguments;
    }

    private class LogWriterMonitor implements SpecificationRunnerMonitor {

        private final File outLogFile;
        private final File errLogFile;
        private PrintStream sysout;
        private PrintStream syserr;

        LogWriterMonitor(File outLogFile, File errLogFile) {
            this.outLogFile = outLogFile;
            this.errLogFile = errLogFile;
        }

        @Override
        public void testRunning(String location) {
            getLog().debug(format("Creating Log files (by redirecting sysout and stderr) for: %s", location));
            sysout = System.out;
            syserr = System.err;
            syserr.flush();
            sysout.flush();
            try {
                FileOutputStream output = openOutputStream(outLogFile);
                FileOutputStream err = openOutputStream(errLogFile);
                if (redirectOutputToFile) {
                    System.setOut(new PrintStream(output));
                    System.setErr(new PrintStream(err));
                } else {
                    TeePrintStream teeoutput = new TeePrintStream(output, sysout);
                    System.setOut(teeoutput);
                    TeePrintStream teeErr = new TeePrintStream(err, syserr);
                    System.setErr(teeErr);
                }
            } catch (IOException e) {
                System.setOut(sysout);
                System.setErr(syserr);
                getLog().warn(format("Could not create the log files. Cause: %s", e.getMessage()));
                getLog().debug("Could not create the log files.", e);
            }
        }

        @Override
        public void testDone(int rightCount, int wrongCount, int exceptionCount, int ignoreCount) {
            getLog().debug("Restoring stdout and stderr");
            System.setOut(sysout);
            System.setErr(syserr);
        }

        @Override
        public void exceptionOccured(Throwable t) {

        }
    }

    private class TeePrintStream extends PrintStream {
        private final PrintStream second;

        TeePrintStream(OutputStream main, PrintStream second) {
            super(main);
            this.second = second;
        }

        /**
         * Closes the main stream.
         * The second stream is just flushed but <b>not</b> closed.
         * @see java.io.PrintStream#close()
         */
        @Override
        public void close() {
            // just for documentation
            super.close();
        }

        @Override
        public void flush() {
            super.flush();
            second.flush();
        }

        @SuppressWarnings("NullableProblems")
        @Override
        public void write(byte[] buf, int off, int len) {
            super.write(buf, off, len);
            second.write(buf, off, len);
        }

        @Override
        public void write(int b) {
            super.write(b);
            second.write(b);
        }

        @SuppressWarnings("NullableProblems")
        @Override
        public void write(byte[] b) throws IOException {
            super.write(b);
            second.write(b);
        }
    }
}