de.pixida.logtest.buildserver.RunIntegrationTests.java Source code

Java tutorial

Introduction

Here is the source code for de.pixida.logtest.buildserver.RunIntegrationTests.java

Source

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright (c) 2016 Pixida GmbH
 */

package de.pixida.logtest.buildserver;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.log4j.Level;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.pixida.logtest.automatondefinitions.JsonAutomatonDefinition;
import de.pixida.logtest.logreaders.GenericLogReader;
import de.pixida.logtest.logreaders.ILogReader;
import de.pixida.logtest.processing.EvaluationResult;
import de.pixida.logtest.processing.Job;
import de.pixida.logtest.processing.JobExecutor;
import de.pixida.logtest.processing.LogSink;
import de.pixida.logtest.reporting.ConsoleSummaryReportGenerator;
import de.pixida.logtest.reporting.ReportsGenerator;
import de.pixida.logtest.reporting.XUnitReportGenerator;

public class RunIntegrationTests {
    private static final String AUTOMATON_DIRECTORY_SWITCH = "automatonDirectory";
    private static final String TRACE_LOG_DIRECTORY_SWITCH = "traceLogDirectory";
    private static final String VERBOSITY_SWITCH = "verbose";
    private static final String REPORT_SWITCH = "reportFile";
    private static final String LOG_READER_CONFIG_SWITCH = "logReaderConfig";
    private static final String LOG_READER_CONFIG_FILE_SWITCH = "logReaderConfigFile";
    private static final String DEFAULT_PARAMETER_FILE_SWITCH = "defaultParameterFile";
    private static final String HELP_SWITCH = "help";

    private static final Logger LOG = LoggerFactory.getLogger(RunIntegrationTests.class);

    static class ExitWithFailureException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        ExitWithFailureException() {
            // Empty constructor needed by checkstyle
        }

        int getExitCode() {
            return 1;
        }
    }

    private boolean verbose = false;
    private File jUnitReportTarget = null;
    private Map<File, List<Pair<File, Map<String, String>>>> configuredExecutions;
    private List<List<EvaluationResult>> results;
    private List<Job> jobs;
    private final StopWatch stopWatch = new StopWatch();
    private List<Long> jobExecutionTimesMs;
    private JSONObject logReaderConfigFromCommandLine;
    private JSONObject logReaderConfigFromFile;
    private final Map<String, String> defaultParameters = new HashMap<>();

    public RunIntegrationTests() {
        // Empty constructor needed by checkstyle
    }

    public static void main(final String[] args) {
        try {
            final RunIntegrationTests runner = new RunIntegrationTests();
            final boolean run = runner.parseCommandLine(args);
            if (run) {
                runner.createAndRunJobs();
                runner.printResults();
            }
        } catch (final ExitWithFailureException ee) {
            LOG.debug("Finished with exit code: " + ee.getExitCode());
            System.exit(ee.getExitCode());
        } catch (final Exception re) {
            LOG.debug("Abording with errors", re);
            System.exit(1); // Exit with error
        }
    }

    boolean parseCommandLine(final String[] args) {
        final Options options = createOptions();
        final CommandLineParser parser = new DefaultParser();
        try {
            final CommandLine params = parser.parse(options, args);
            if (params.hasOption(HELP_SWITCH)) {
                printHelp(options);
                return false;
            }

            this.applyVerbositySwitch(params);

            this.loadDefaultParameters(params);

            this.configuredExecutions = this.groupAutomatonsByTraceFile(params);

            final String param = params.getOptionValue(REPORT_SWITCH);
            if (param != null) {
                this.jUnitReportTarget = new File(param);
            }

            try {
                if (params.hasOption(LOG_READER_CONFIG_SWITCH)) {
                    this.logReaderConfigFromCommandLine = new JSONObject(
                            params.getOptionValue(LOG_READER_CONFIG_SWITCH));
                }
            } catch (final JSONException jsonEx) {
                throw new ParseException("Failed to parse log reader configuration JSON data from command line: "
                        + jsonEx.getMessage());
            }

            File logReaderConfigurationFile = null;
            try {
                if (params.hasOption(LOG_READER_CONFIG_FILE_SWITCH)) {
                    logReaderConfigurationFile = new File(params.getOptionValue(LOG_READER_CONFIG_FILE_SWITCH));
                    this.logReaderConfigFromFile = new JSONObject(
                            IOUtils.toString(logReaderConfigurationFile.toURI(), StandardCharsets.UTF_8));
                    LOG.debug("Using log reader configuration file '{}'",
                            logReaderConfigurationFile.getCanonicalPath());
                } else {
                    LOG.debug("No log reader configuration file set.");
                }
            } catch (final JSONException jsonEx) {
                throw new ParseException(
                        "Failed to parse log reader configuration JSON data from file: " + jsonEx.getMessage());
            } catch (final IOException e) {
                throw new ParseException("Failed to read log reader configuration file '"
                        + logReaderConfigurationFile.getAbsolutePath() + "': " + e.getMessage());
            }
        } catch (final ParseException e) {
            // CHECKSTYLE:OFF We intentionally print to STDERR here
            System.err.println(e.getMessage());
            System.err.println();
            printHelp(options);
            // CHECKSTYLE:ON

            // Abort with failure - build server job must not succeed if the calling convention is erroneous
            throw new ExitWithFailureException();
        }

        return true;
    }

    boolean getIsVerbose() {
        return this.verbose;
    }

    void createAndRunJobs() {
        this.jobs = this.createJobs(this.configuredExecutions);
        LOG.info("Starting integration tests");
        this.stopWatch.start();
        final JobExecutor executor = new JobExecutor(this.jobs);
        this.results = executor.getResults();
        this.jobExecutionTimesMs = executor.getJobExecutionTimesMs();
        this.stopWatch.stop();
        LOG.info("Integration tests finished");
    }

    void printResults() {
        final ReportsGenerator reportsGenerator = new ReportsGenerator();
        reportsGenerator.setJobs(this.jobs);
        reportsGenerator.setResults(this.results);
        reportsGenerator.addReportGenerator(new ConsoleSummaryReportGenerator(this.stopWatch.getTime()));
        reportsGenerator.setJobExecutionTimes(this.jobExecutionTimesMs);
        if (this.jUnitReportTarget != null) {
            reportsGenerator
                    .addReportGenerator(new XUnitReportGenerator(this.jUnitReportTarget, this.stopWatch.getTime()));
        }
        reportsGenerator.generateReports();
        final long numFailedExecutions = this.results.stream()
                .mapToLong(result -> result.stream().filter(er -> !er.isSuccess()).count()).sum();

        if (numFailedExecutions == 1) {
            throw new ExitWithFailureException();
        }
    }

    private void loadDefaultParameters(final CommandLine params) throws ParseException {
        File defaultParameterFile = null;
        try {
            if (params.hasOption(DEFAULT_PARAMETER_FILE_SWITCH)) {
                defaultParameterFile = new File(params.getOptionValue(DEFAULT_PARAMETER_FILE_SWITCH));
                final JSONObject root = new JSONObject(
                        IOUtils.toString(defaultParameterFile.toURI(), StandardCharsets.UTF_8));
                for (final String k : root.keySet()) {
                    final String v = root.getString(k);
                    this.defaultParameters.put(k, v);
                }
                LOG.debug("Loaded '{}' default parameters from file '{}'", this.defaultParameters.size(),
                        defaultParameterFile.getCanonicalPath());
                LOG.trace("Using default parameters: {}", this.defaultParameters);
            } else {
                LOG.debug("No default parameters set.");
            }
        } catch (final JSONException jsonEx) {
            throw new ParseException(
                    "Failed to parse default parameter JSON data from file: " + jsonEx.getMessage());
        } catch (final IOException e) {
            throw new ParseException("Failed to read default parameter file '"
                    + defaultParameterFile.getAbsolutePath() + "': " + e.getMessage());
        }
    }

    private List<Job> createJobs(final Map<File, List<Pair<File, Map<String, String>>>> pairsWithParams) {
        final List<Job> result = new ArrayList<>();
        for (final Entry<File, List<Pair<File, Map<String, String>>>> pair : pairsWithParams.entrySet()) {
            final List<LogSink> sinks = new ArrayList<>();
            for (final Pair<File, Map<String, String>> sinkDef : pair.getValue()) {
                final LogSink newSink = new LogSink();
                newSink.setAutomaton(new JsonAutomatonDefinition(sinkDef.getLeft()));
                newSink.setParameters(sinkDef.getRight());
                sinks.add(newSink);
            }

            final Job newJob = new Job();
            newJob.setLogReader(this.createAndConfigureLogReader(pair.getKey()));
            newJob.setSinks(sinks);
            result.add(newJob);
        }
        return result;
    }

    private void applyVerbositySwitch(final CommandLine params) {
        if (params.hasOption(VERBOSITY_SWITCH)) {
            if (org.apache.log4j.Logger.getRootLogger().getLevel().isGreaterOrEqual(Level.DEBUG)) // Don't turn TRACE into DEBUG
            {
                org.apache.log4j.Logger.getRootLogger().setLevel(Level.DEBUG);
                this.verbose = true;
                LOG.debug("Verbose mode enabled");
            }
        }
    }

    private static Options createOptions() {
        final Options options = new Options();
        final Option traceLogDirectory = Option.builder("t").longOpt(TRACE_LOG_DIRECTORY_SWITCH)
                .desc("Trace logs location").hasArg().argName("folder").build();
        final Option automatonDirectory = Option.builder("a").longOpt(AUTOMATON_DIRECTORY_SWITCH)
                .desc("Automatons location").hasArg().argName("folder").build();
        final Option reportFile = Option.builder("r").longOpt(REPORT_SWITCH).desc("Write XUnit report to file")
                .hasArg().argName("file").build();
        final Option verbosity = Option.builder("v").longOpt(VERBOSITY_SWITCH).desc("Enable debug output").build();
        final Option logReaderConfigSwitch = Option.builder("lrcfg").longOpt(LOG_READER_CONFIG_SWITCH)
                .desc("Log reader configuration (JSON)").hasArg().argName("json-object").build();
        final Option logReaderConfigFileSwitch = Option.builder("lrcfgf").longOpt(LOG_READER_CONFIG_FILE_SWITCH)
                .desc("Log reader configuration file").hasArg().argName("file").build();
        final Option defaultParameterFileSwitch = Option.builder("dpf").longOpt(DEFAULT_PARAMETER_FILE_SWITCH)
                .desc("Default parameter file").hasArg().argName("file").build();
        final Option helpSwitch = Option.builder("h").longOpt(HELP_SWITCH).desc("Show (this) help only").build();
        options.addOption(traceLogDirectory);
        options.addOption(automatonDirectory);
        options.addOption(logReaderConfigSwitch);
        options.addOption(logReaderConfigFileSwitch);
        options.addOption(defaultParameterFileSwitch);
        options.addOption(reportFile);
        options.addOption(verbosity);
        options.addOption(helpSwitch);
        return options;
    }

    private ILogReader createAndConfigureLogReader(final File logFile) {
        final GenericLogReader logReader = new GenericLogReader(logFile);

        // Settings from configuration file
        if (this.logReaderConfigFromFile != null) {
            logReader.overwriteCurrentSettingsWithSettingsInConfigurationFile(this.logReaderConfigFromFile);
        }

        // Settings from command line
        if (this.logReaderConfigFromCommandLine != null) {
            logReader.overwriteCurrentSettingsWithSettingsInConfigurationFile(this.logReaderConfigFromCommandLine);
        }

        return logReader;
    }

    private Map<File, List<Pair<File, Map<String, String>>>> groupAutomatonsByTraceFile(final CommandLine params)
            throws ParseException {
        final File logFolder = new File(commandLineParamOrCurrentDirectory(params, TRACE_LOG_DIRECTORY_SWITCH));
        final File automatonsFolder = new File(
                commandLineParamOrCurrentDirectory(params, AUTOMATON_DIRECTORY_SWITCH));
        LOG.debug("Using log folder: {}", logFolder.getAbsolutePath());
        LOG.debug("Using automatons folder: {}", automatonsFolder.getAbsolutePath());

        final Map<File, List<Pair<File, Map<String, String>>>> result = new HashMap<>();
        for (final String arg : params.getArgList()) {
            final int numComponentsLogFileAndAutomaton = 2;
            final int numComponentsLogFileAndAutomatonAndParameter = 3;
            final String[] components = arg.split(":", numComponentsLogFileAndAutomatonAndParameter);
            if (components.length < numComponentsLogFileAndAutomaton
                    || components.length > numComponentsLogFileAndAutomatonAndParameter) {
                throw new ParseException(
                        "Invalid execution entry on command line. Format must be <logfile>:<automaton>[:<parameters>]: "
                                + arg);
            }
            final File traceLog = new File(logFolder, components[0]);
            List<Pair<File, Map<String, String>>> automatons = result.get(traceLog);
            if (automatons == null) {
                automatons = new ArrayList<>();
                result.put(traceLog, automatons);
            }
            Map<String, String> parameters = null;
            if (components.length >= numComponentsLogFileAndAutomatonAndParameter) {
                parameters = this
                        .parseAutomatonParameters(components[numComponentsLogFileAndAutomatonAndParameter - 1]);
            }
            if (parameters == null) {
                parameters = this.parseAutomatonParameters("");
            }

            automatons.add(Pair.of(new File(automatonsFolder, components[1]), parameters));
        }
        return result;
    }

    private static String commandLineParamOrCurrentDirectory(final CommandLine params, final String paramName) {
        return params.hasOption(paramName) ? params.getOptionValue(paramName) : ".";
    }

    private Map<String, String> parseAutomatonParameters(final String string) throws ParseException {
        final Map<String, String> result = new HashMap<>();

        // Add default parameters
        result.putAll(this.defaultParameters);

        if (string.length() > 0) {
            // Separate by ','
            final String[] params = string.split(",");

            // Separate by '='
            for (final String param : params) {
                final int kvLen = 2;
                final String[] kv = param.split("=", kvLen);
                if (kv.length != kvLen) {
                    throw new ParseException("A parameter entry must be a key=value pair.");
                }
                result.put(kv[0], kv[1]);
            }
        }

        return result;
    }

    private static void printHelp(final Options options) {
        final HelpFormatter formatter = new HelpFormatter();
        final int assumedConsoleWidth = 150;
        formatter.setWidth(assumedConsoleWidth);
        formatter.printHelp("java -jar logtest-buildserver-app.jar [OPTIONS]... [EXECUTIONS]...\n"
                + "An EXECUTION is a triple <scenario-filename>:<automaton-filename>[:<parameters, comma separated key=value pairs...>,<...>]"
                + " e.g. tracelog.txt:checkSystemStartupSucceeds.json:waitForNetIO=yes,timeout=30" + "\n", // Separate help text from description of parameters with an empty line
                options);
    }

    Map<File, List<Pair<File, Map<String, String>>>> getConfiguredExecutions() {
        return this.configuredExecutions;
    }
}