io.cloudslang.lang.tools.build.SlangBuildMain.java Source code

Java tutorial

Introduction

Here is the source code for io.cloudslang.lang.tools.build.SlangBuildMain.java

Source

/*******************************************************************************
 * (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License v2.0 which accompany this distribution.
 *
 * The Apache License is available at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 *******************************************************************************/
package io.cloudslang.lang.tools.build;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import io.cloudslang.lang.api.Slang;
import io.cloudslang.lang.commons.services.api.UserConfigurationService;
import io.cloudslang.lang.commons.services.impl.UserConfigurationServiceImpl;
import io.cloudslang.lang.logging.LoggingService;
import io.cloudslang.lang.logging.LoggingServiceImpl;
import io.cloudslang.lang.tools.build.commands.ApplicationArgs;
import io.cloudslang.lang.tools.build.tester.IRunTestResults;
import io.cloudslang.lang.tools.build.tester.TestRun;
import io.cloudslang.lang.tools.build.tester.parallel.report.SlangTestCaseRunReportGeneratorService;
import io.cloudslang.lang.tools.build.tester.parse.SlangTestCase;
import io.cloudslang.lang.tools.build.tester.runconfiguration.TestRunInfoService;
import io.cloudslang.score.events.ScoreEvent;
import io.cloudslang.score.events.ScoreEventListener;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import static io.cloudslang.lang.tools.build.ArgumentProcessorUtils.getBooleanFromPropertiesWithDefault;
import static io.cloudslang.lang.tools.build.ArgumentProcessorUtils.getEnumInstanceFromPropertiesWithDefault;
import static io.cloudslang.lang.tools.build.ArgumentProcessorUtils.getIntFromPropertiesWithDefaultAndRange;
import static io.cloudslang.lang.tools.build.ArgumentProcessorUtils.getListForPrint;
import static io.cloudslang.lang.tools.build.SlangBuildMain.BulkRunMode.ALL_PARALLEL;
import static io.cloudslang.lang.tools.build.SlangBuildMain.BulkRunMode.ALL_SEQUENTIAL;
import static io.cloudslang.lang.tools.build.SlangBuildMain.BulkRunMode.POSSIBLY_MIXED;
import static io.cloudslang.lang.tools.build.SlangBuildMain.RunConfigurationProperties.TEST_COVERAGE;
import static io.cloudslang.lang.tools.build.SlangBuildMain.RunConfigurationProperties.TEST_PARALLEL_THREAD_COUNT;
import static io.cloudslang.lang.tools.build.SlangBuildMain.RunConfigurationProperties.TEST_SUITES_PARALLEL;
import static io.cloudslang.lang.tools.build.SlangBuildMain.RunConfigurationProperties.TEST_SUITES_RUN_UNSPECIFIED;
import static io.cloudslang.lang.tools.build.SlangBuildMain.RunConfigurationProperties.TEST_SUITES_SEQUENTIAL;
import static io.cloudslang.lang.tools.build.SlangBuildMain.RunConfigurationProperties.TEST_SUITES_TO_RUN;
import static io.cloudslang.lang.tools.build.tester.SlangTestRunner.MAX_TIME_PER_TESTCASE_IN_MINUTES;
import static io.cloudslang.lang.tools.build.tester.SlangTestRunner.TEST_CASE_TIMEOUT_IN_MINUTES_KEY;
import static io.cloudslang.lang.tools.build.tester.parallel.services.ParallelTestCaseExecutorService.SLANG_TEST_RUNNER_THREAD_COUNT;
import static java.lang.Integer.parseInt;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.lang.System.getProperty;
import static java.lang.System.setProperty;
import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.isRegularFile;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.Paths.get;
import static java.util.Locale.ENGLISH;
import static org.apache.commons.collections4.ListUtils.removeAll;
import static org.apache.commons.collections4.ListUtils.union;
import static org.apache.commons.collections4.MapUtils.isNotEmpty;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase;
import static org.apache.commons.lang3.StringUtils.isEmpty;

public class SlangBuildMain {
    public static final String DEFAULT_TESTS = "default";

    private static final String TEST_CASE_REPORT_LOCATION = "cloudslang.test.case.report.location";
    private static final String CONTENT_DIR = File.separator + "content";
    private static final String TEST_DIR = File.separator + "test";

    private static final Logger log = Logger.getLogger(SlangBuildMain.class);
    private static final int MAX_THREADS_TEST_RUNNER = 32;
    private static final String MESSAGE_NOT_SCHEDULED_FOR_RUN_RULES = "Rules '%s' defined in '%s' key "
            + "are not scheduled for run.";

    private static final String MESSAGE_TEST_SUITES_WITH_UNSPECIFIED_MAPPING = "Test suites '%s' have "
            + "unspecified mapping. They will run in '%s' mode.";
    private static final String PROPERTIES_FILE_EXTENSION = "properties";

    private static final String DID_NOT_DETECT_RUN_CONFIGURATION_PROPERTIES_FILE = "Did not detect run "
            + "configuration properties file at path '%s'. "
            + "Check that the path you are using is an absolute path. "
            + "Check that the path separator is '\\\\' for Windows, or '/' for Linux.";
    private static final String NEW_LINE = System.lineSeparator();
    private static final String MESSAGE_BOTH_PARALLEL_AND_SEQUENTIAL_EXECUTION = "The '%s' suites are configured for "
            + "both parallel and sequential execution."
            + " Each test suite must have only one execution mode (parallel or sequential).";
    private static final String MESSAGE_ERROR_LOADING_SMART_MODE_CONFIG_FILE = "Error loading smart "
            + "mode configuration file:";
    private static final String LOG4J_CONFIGURATION_KEY = "log4j.configuration";
    private static final String LOG4J_ERROR_PREFIX = "log4j: error loading log4j properties file.";
    private static final String LOG4J_ERROR_SUFFIX = "Using default configuration.";
    private static final String APP_HOME_KEY = "app.home";

    // This class is a used in the interaction with the run configuration property file
    static class RunConfigurationProperties {
        static final String TEST_COVERAGE = "test.coverage";
        static final String TEST_PARALLEL_THREAD_COUNT = "test.parallel.thread.count";
        static final String TEST_SUITES_TO_RUN = "test.suites.active";
        static final String TEST_SUITES_PARALLEL = "test.suites.parallel";
        static final String TEST_SUITES_SEQUENTIAL = "test.suites.sequential";
        static final String TEST_SUITES_RUN_UNSPECIFIED = "test.suites.run.mode.unspecified";
    }

    // The possible ways to execute a test case
    public enum TestCaseRunMode {
        PARALLEL, SEQUENTIAL
    }

    // The typical configuration on how to configure the run of all tests as a bulk
    public enum BulkRunMode {
        ALL_PARALLEL, ALL_SEQUENTIAL, POSSIBLY_MIXED
    }

    // The possible ways to run tests: everything or the tests affected by current changelist
    public enum BuildMode {
        BASIC, CHANGED
    }

    public static void main(String[] args) {
        loadUserProperties();
        configureLog4j();

        ApplicationArgs appArgs = new ApplicationArgs();
        parseArgs(args, appArgs);
        String projectPath = parseProjectPathArg(appArgs);
        final String contentPath = defaultIfEmpty(appArgs.getContentRoot(), projectPath + CONTENT_DIR);
        final String testsPath = defaultIfEmpty(appArgs.getTestRoot(), projectPath + TEST_DIR);
        List<String> testSuites = parseTestSuites(appArgs);
        boolean shouldPrintCoverageData = appArgs.shouldOutputCoverage();
        boolean runTestsInParallel = appArgs.isParallel();
        int threadCount = parseThreadCountArg(appArgs, runTestsInParallel);
        String testCaseTimeout = parseTestTimeout(appArgs);
        setProperty(TEST_CASE_TIMEOUT_IN_MINUTES_KEY, valueOf(testCaseTimeout));
        final boolean shouldValidateDescription = appArgs.shouldValidateDescription();
        final boolean shouldValidateCheckstyle = appArgs.shouldValidateCheckstyle();
        String runConfigPath = FilenameUtils.normalize(appArgs.getRunConfigPath());

        BuildMode buildMode = null;
        Set<String> changedFiles = null;
        try {
            String smartModePath = appArgs.getChangesOnlyConfigPath();
            if (StringUtils.isEmpty(smartModePath)) {
                buildMode = BuildMode.BASIC;
                changedFiles = new HashSet<>();
                printBuildModeInfo(buildMode);
            } else {
                buildMode = BuildMode.CHANGED;
                changedFiles = readChangedFilesFromSource(smartModePath);
                printBuildModeInfo(buildMode);
            }
        } catch (Exception ex) {
            log.error("Exception: " + ex.getMessage());
            System.exit(1);
        }

        // Override with the values from the file if configured
        List<String> testSuitesParallel = new ArrayList<>();
        List<String> testSuitesSequential = new ArrayList<>();
        BulkRunMode bulkRunMode = runTestsInParallel ? ALL_PARALLEL : ALL_SEQUENTIAL;

        TestCaseRunMode unspecifiedTestSuiteRunMode = runTestsInParallel ? TestCaseRunMode.PARALLEL
                : TestCaseRunMode.SEQUENTIAL;
        if (get(runConfigPath).isAbsolute() && isRegularFile(get(runConfigPath), NOFOLLOW_LINKS)
                && equalsIgnoreCase(PROPERTIES_FILE_EXTENSION, FilenameUtils.getExtension(runConfigPath))) {
            Properties runConfigurationProperties = ArgumentProcessorUtils.getPropertiesFromFile(runConfigPath);
            shouldPrintCoverageData = getBooleanFromPropertiesWithDefault(TEST_COVERAGE, shouldPrintCoverageData,
                    runConfigurationProperties);
            threadCount = getIntFromPropertiesWithDefaultAndRange(TEST_PARALLEL_THREAD_COUNT,
                    Runtime.getRuntime().availableProcessors(), runConfigurationProperties, 1,
                    MAX_THREADS_TEST_RUNNER + 1);
            testSuites = getTestSuitesForKey(runConfigurationProperties, TEST_SUITES_TO_RUN);
            testSuitesParallel = getTestSuitesForKey(runConfigurationProperties, TEST_SUITES_PARALLEL);
            testSuitesSequential = getTestSuitesForKey(runConfigurationProperties, TEST_SUITES_SEQUENTIAL);
            addErrorIfSameTestSuiteIsInBothParallelOrSequential(testSuitesParallel, testSuitesSequential);
            unspecifiedTestSuiteRunMode = getEnumInstanceFromPropertiesWithDefault(TEST_SUITES_RUN_UNSPECIFIED,
                    unspecifiedTestSuiteRunMode, runConfigurationProperties);
            addWarningsForMisconfiguredTestSuites(unspecifiedTestSuiteRunMode, testSuites, testSuitesSequential,
                    testSuitesParallel);
            bulkRunMode = POSSIBLY_MIXED;
        } else { // Warn when file is misconfigured, relative path, file does not exist or is not a properties file
            log.info(format(DID_NOT_DETECT_RUN_CONFIGURATION_PROPERTIES_FILE, runConfigPath));
        }

        String testCaseReportLocation = getProperty(TEST_CASE_REPORT_LOCATION);
        if (StringUtils.isBlank(testCaseReportLocation)) {
            log.info("Test case report location property [" + TEST_CASE_REPORT_LOCATION
                    + "] is not defined. Report will be skipped.");
        }

        // Setting thread count for visibility in ParallelTestCaseExecutorService
        setProperty(SLANG_TEST_RUNNER_THREAD_COUNT, valueOf(threadCount));

        log.info(NEW_LINE + "------------------------------------------------------------");
        log.info("Building project: " + projectPath);
        log.info("Content root is at: " + contentPath);
        log.info("Test root is at: " + testsPath);
        log.info("Active test suites are: " + getListForPrint(testSuites));
        log.info("Parallel run mode is configured for test suites: " + getListForPrint(testSuitesParallel));
        log.info("Sequential run mode is configured for test suites: " + getListForPrint(testSuitesSequential));
        log.info("Default run mode '" + unspecifiedTestSuiteRunMode.name().toLowerCase()
                + "' is configured for test suites: " + getListForPrint(
                        getDefaultRunModeTestSuites(testSuites, testSuitesParallel, testSuitesSequential)));

        log.info("Bulk run mode for tests: " + getBulkModeForPrint(bulkRunMode));

        log.info("Print coverage data: " + valueOf(shouldPrintCoverageData));
        log.info("Validate description: " + valueOf(shouldValidateDescription));
        log.info("Validate checkstyle: " + valueOf(shouldValidateCheckstyle));
        log.info("Thread count: " + threadCount);
        log.info("Test case timeout in minutes: "
                + (isEmpty(testCaseTimeout) ? valueOf(MAX_TIME_PER_TESTCASE_IN_MINUTES) : testCaseTimeout));

        log.info(NEW_LINE + "Loading...");

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/testRunnerContext.xml");
        context.registerShutdownHook();
        SlangBuilder slangBuilder = context.getBean(SlangBuilder.class);
        LoggingService loggingService = context.getBean(LoggingServiceImpl.class);
        Slang slang = context.getBean(Slang.class);

        try {

            updateTestSuiteMappings(context.getBean(TestRunInfoService.class), testSuitesParallel,
                    testSuitesSequential, testSuites, unspecifiedTestSuiteRunMode);

            registerEventHandlers(slang);

            List<RuntimeException> exceptions = new ArrayList<>();

            SlangBuildResults buildResults = slangBuilder.buildSlangContent(projectPath, contentPath, testsPath,
                    testSuites, shouldValidateDescription, shouldValidateCheckstyle, bulkRunMode, buildMode,
                    changedFiles);
            exceptions.addAll(buildResults.getCompilationExceptions());
            if (exceptions.size() > 0) {
                logErrors(exceptions, projectPath, loggingService);
            }
            IRunTestResults runTestsResults = buildResults.getRunTestsResults();
            Map<String, TestRun> skippedTests = runTestsResults.getSkippedTests();

            if (isNotEmpty(skippedTests)) {
                printSkippedTestsSummary(skippedTests, loggingService);
            }
            printPassedTests(runTestsResults, loggingService);
            if (shouldPrintCoverageData) {
                printTestCoverageData(runTestsResults, loggingService);
            }

            if (isNotEmpty(runTestsResults.getFailedTests())) {
                printBuildFailureSummary(projectPath, runTestsResults, loggingService);
            } else {
                printBuildSuccessSummary(contentPath, buildResults, runTestsResults, loggingService);
            }
            loggingService.waitForAllLogTasksToFinish();

            generateTestCaseReport(context.getBean(SlangTestCaseRunReportGeneratorService.class), runTestsResults,
                    testCaseReportLocation);
            System.exit(isNotEmpty(runTestsResults.getFailedTests()) ? 1 : 0);

        } catch (Throwable e) {
            logErrorsPrefix(loggingService);
            loggingService.logEvent(Level.ERROR, "Exception: " + e.getMessage());
            logErrorsSuffix(projectPath, loggingService);
            loggingService.waitForAllLogTasksToFinish();
            System.exit(1);
        }
    }

    private static void configureLog4j() {
        String configFilename = System.getProperty(LOG4J_CONFIGURATION_KEY);
        String errorMessage = null;

        try {
            if (StringUtils.isEmpty(configFilename)) {
                errorMessage = "Config file name is empty.";
            } else {
                String normalizedPath = FilenameUtils.normalize(configFilename);
                if (normalizedPath == null) {
                    errorMessage = "Normalized config file path is null.";
                } else if (!isUnderAppHome(normalizedPath, getNormalizedApplicationHome())) {
                    errorMessage = "Normalized config file path[" + normalizedPath + "] "
                            + "is not under application home directory";
                } else {
                    if (!isRegularFile(get(normalizedPath), NOFOLLOW_LINKS)) {
                        errorMessage = "Normalized config file path[" + normalizedPath + "]"
                                + " does not lead to a regular file.";
                    } else {
                        Properties log4jProperties = new Properties();
                        try (InputStream log4jInputStream = SlangBuildMain.class
                                .getResourceAsStream(normalizedPath)) {
                            log4jProperties.load(log4jInputStream);
                            PropertyConfigurator.configure(log4jProperties);
                        }
                    }
                }
            }
        } catch (IOException | RuntimeException ex) {
            errorMessage = ex.getMessage();
        }

        if (StringUtils.isNotEmpty(errorMessage)) {
            System.out.printf("%s%n\t%s%n\t%s%n", LOG4J_ERROR_PREFIX, errorMessage, LOG4J_ERROR_SUFFIX);
        }
    }

    private static boolean isUnderAppHome(String normalizedFilePath, String normalizedAppHome) {
        return normalizedFilePath.startsWith(normalizedAppHome);
    }

    private static String getNormalizedApplicationHome() {
        String appHome = System.getProperty(APP_HOME_KEY);
        if (StringUtils.isEmpty(appHome)) {
            throw new RuntimeException(APP_HOME_KEY + " system property is empty");
        }
        String normalizedAppHome = FilenameUtils.normalize(appHome);
        if (normalizedAppHome == null) {
            throw new RuntimeException("Normalized app home path is null.");
        }
        return normalizedAppHome;
    }

    private static void printBuildModeInfo(BuildMode buildMode) {
        log.info("Build mode set to: " + buildMode);
    }

    private static Set<String> readChangedFilesFromSource(String filePath) throws IOException {
        String normalizedPath = FilenameUtils.normalize(filePath);
        if (!get(normalizedPath).isAbsolute()) {
            throw new RuntimeException(MESSAGE_ERROR_LOADING_SMART_MODE_CONFIG_FILE + " Path[" + normalizedPath
                    + "] is not an absolute path.");
        }
        if (!isRegularFile(get(normalizedPath), NOFOLLOW_LINKS)) {
            throw new RuntimeException(MESSAGE_ERROR_LOADING_SMART_MODE_CONFIG_FILE + " Path[" + normalizedPath
                    + "] does not lead to a regular file.");
        }
        return ArgumentProcessorUtils.loadChangedItems(normalizedPath);
    }

    private static void addErrorIfSameTestSuiteIsInBothParallelOrSequential(List<String> testSuitesParallel,
            List<String> testSuitesSequential) {
        final List<String> intersection = ListUtils.intersection(testSuitesParallel, testSuitesSequential);
        if (!intersection.isEmpty()) {
            final String message = String.format(MESSAGE_BOTH_PARALLEL_AND_SEQUENTIAL_EXECUTION,
                    getListForPrint(intersection));
            log.error(message);
            throw new IllegalStateException();
        }
    }

    /**
     * @param bulkRunMode the mode to configure the run of all tests
     * @return String friendly version for print to the log or console
     */
    private static String getBulkModeForPrint(final BulkRunMode bulkRunMode) {
        return bulkRunMode.toString().replace("_", " ").toLowerCase(ENGLISH);
    }

    /**
     * @param testRunInfoService          the service responsible for managing run information
     * @param parallelSuites              the suite names to be executed in parallel
     * @param sequentialSuites            the suite names to be executed in sequential manner
     * @param activeSuites                the suite names that are active
     * @param unspecifiedTestSuiteRunMode the default run mode for suites that don't explicitly mention a run mode.
     */
    private static void updateTestSuiteMappings(final TestRunInfoService testRunInfoService,
            final List<String> parallelSuites, final List<String> sequentialSuites, final List<String> activeSuites,
            final TestCaseRunMode unspecifiedTestSuiteRunMode) {
        testRunInfoService.setRunModeForTestSuites(parallelSuites, TestCaseRunMode.PARALLEL);
        testRunInfoService.setRunModeForTestSuites(sequentialSuites, TestCaseRunMode.SEQUENTIAL);
        testRunInfoService.setRunModeForTestSuites(
                getDefaultRunModeTestSuites(activeSuites, parallelSuites, sequentialSuites),
                unspecifiedTestSuiteRunMode);
    }

    /**
     * @param activeSuites     the suite names that are active
     * @param parallelSuites   the suite names to be executed in parallel
     * @param sequentialSuites the suite names to be executed in sequential manner
     * @return
     */
    private static List<String> getDefaultRunModeTestSuites(final List<String> activeSuites,
            final List<String> parallelSuites, final List<String> sequentialSuites) {
        return removeAll(new ArrayList<>(activeSuites), union(parallelSuites, sequentialSuites));
    }

    /**
     * @param unspecifiedTestSuiteRunMode the default run mode for suites that don't explicitly mention a run mode.
     * @param activeSuites                the suite names that are active
     * @param sequentialSuites            the suite names to be executed in sequential manner
     * @param parallelSuites              the suite names to be executed in parallel
     */
    private static void addWarningsForMisconfiguredTestSuites(final TestCaseRunMode unspecifiedTestSuiteRunMode,
            final List<String> activeSuites, final List<String> sequentialSuites,
            final List<String> parallelSuites) {
        addWarningForSubsetOfRules(activeSuites, sequentialSuites, TEST_SUITES_SEQUENTIAL);
        addWarningForSubsetOfRules(activeSuites, parallelSuites, TEST_SUITES_PARALLEL);
        addInformativeNoteForUnspecifiedRules(unspecifiedTestSuiteRunMode, activeSuites, sequentialSuites,
                parallelSuites);
    }

    /**
     * Displays an informative message in case there is at least one test suite left for default run mode.
     *
     * @param unspecifiedTestSuiteRunMode the default run mode for suites that don't explicitly mention a run mode.
     * @param activeSuites                the suite names that are active
     * @param sequentialSuites            the suite names to be executed in sequential manner
     * @param parallelSuites              the suite names to be executed in parallel
     */
    private static void addInformativeNoteForUnspecifiedRules(final TestCaseRunMode unspecifiedTestSuiteRunMode,
            final List<String> activeSuites, final List<String> sequentialSuites,
            final List<String> parallelSuites) {
        List<String> union = union(sequentialSuites, parallelSuites);
        if (!union.containsAll(activeSuites)) {
            List<String> copy = new ArrayList<>(activeSuites);
            copy.removeAll(union);

            log.info(format(MESSAGE_TEST_SUITES_WITH_UNSPECIFIED_MAPPING, getListForPrint(copy),
                    unspecifiedTestSuiteRunMode.name()));
        }
    }

    /**
     * Displays a warning message for test suites that have rules defined for sequential or parallel execution
     * but are not in active test suites.
     *
     * @param testSuites          suite names contained in 'container' suites
     * @param testSuitesContained suite names contained in 'contained' suites
     * @param key                 run configuration property key
     */
    private static void addWarningForSubsetOfRules(List<String> testSuites, List<String> testSuitesContained,
            String key) {
        List<String> intersectWithContained = ListUtils.intersection(testSuites, testSuitesContained);
        if (intersectWithContained.size() != testSuitesContained.size()) {
            List<String> notScheduledForRun = new ArrayList<>(testSuitesContained);
            notScheduledForRun.removeAll(intersectWithContained);
            log.warn(format(MESSAGE_NOT_SCHEDULED_FOR_RUN_RULES, getListForPrint(notScheduledForRun), key));
        }
    }

    /**
     * Returns the names of the suites from the run configuration java.util.Properties object at a certain key.
     *
     * @param runConfigurationProperties
     * @param key
     * @return
     */
    private static List<String> getTestSuitesForKey(Properties runConfigurationProperties, String key) {
        final String valueList = runConfigurationProperties.getProperty(key);
        return ArgumentProcessorUtils.parseTestSuitesToList(valueList);
    }

    private static void logErrors(List<RuntimeException> exceptions, String projectPath,
            final LoggingService loggingService) {
        logErrorsPrefix(loggingService);
        for (RuntimeException runtimeException : exceptions) {
            loggingService.logEvent(Level.ERROR, "Exception: " + runtimeException.getMessage());
        }
        logErrorsSuffix(projectPath, loggingService);
        loggingService.waitForAllLogTasksToFinish();
        System.exit(1);
    }

    private static void logErrorsSuffix(String projectPath, final LoggingService loggingService) {
        loggingService.logEvent(Level.ERROR,
                "FAILURE: Validation of slang files for project: \"" + projectPath + "\" failed.");
        loggingService.logEvent(Level.ERROR, "------------------------------------------------------------");
        loggingService.logEvent(Level.ERROR, "");
    }

    private static void logErrorsPrefix(final LoggingService loggingService) {
        loggingService.logEvent(Level.ERROR, "");
        loggingService.logEvent(Level.ERROR, "------------------------------------------------------------");
    }

    private static void generateTestCaseReport(SlangTestCaseRunReportGeneratorService reportGeneratorService,
            IRunTestResults runTestsResults, String testCaseReportLocation) throws IOException {
        if (StringUtils.isNotBlank(testCaseReportLocation)) {
            Path reportDirectoryPath = get(testCaseReportLocation);
            if (!exists(reportDirectoryPath)) {
                createDirectories(reportDirectoryPath);
            }
            reportGeneratorService.generateReport(runTestsResults, reportDirectoryPath.toString());
        }
    }

    @SuppressWarnings("Duplicates")
    private static void loadUserProperties() {
        try {
            UserConfigurationService userConfigurationService = new UserConfigurationServiceImpl();
            userConfigurationService.loadUserProperties();
        } catch (Exception ex) {
            System.out.println("Error occurred while loading user configuration: " + ex.getMessage());
            ex.printStackTrace();
        }
    }

    private static void parseArgs(String[] args, ApplicationArgs appArgs) {
        try {
            JCommander commander = new JCommander(appArgs, args);
            if (appArgs.isHelp()) {
                commander.usage();
                System.exit(0);
            }
        } catch (ParameterException e) {
            System.out.println(e.getMessage());
            System.out.println("You can use '--help' for usage");
            System.exit(1);
        }
    }

    private static List<String> parseTestSuites(ApplicationArgs appArgs) {
        final List<String> testSuitesArg = ListUtils.defaultIfNull(appArgs.getTestSuites(),
                new ArrayList<String>());
        return ArgumentProcessorUtils.parseTestSuitesToList(testSuitesArg);
    }

    private static String parseTestTimeout(ApplicationArgs appArgs) {
        Map<String, String> dynamicArgs = appArgs.getDynamicParams();
        return dynamicArgs.get(TEST_CASE_TIMEOUT_IN_MINUTES_KEY);
    }

    private static int parseThreadCountArg(ApplicationArgs appArgs, boolean isParallel) {
        if (!isParallel) {
            return 1;
        } else {
            int defaultThreadCount = Runtime.getRuntime().availableProcessors();
            String threadCountErrorMessage = format(
                    "Thread count is misconfigured. The thread count value must be a "
                            + "positive integer less than or equal to %d. Using %d threads.",
                    MAX_THREADS_TEST_RUNNER, defaultThreadCount);
            try {
                String stringThreadCount = appArgs.getThreadCount();
                if (stringThreadCount != null) {
                    int threadCount = parseInt(stringThreadCount);
                    if ((threadCount > 0) && (threadCount <= MAX_THREADS_TEST_RUNNER)) {
                        return threadCount;
                    } else {
                        log.warn(threadCountErrorMessage);
                    }
                }
            } catch (NumberFormatException nfEx) {
                log.warn(threadCountErrorMessage);
            }
            return defaultThreadCount;
        }
    }

    private static void printBuildSuccessSummary(String contentPath, SlangBuildResults buildResults,
            IRunTestResults runTestsResults, final LoggingService loggingService) {
        loggingService.logEvent(Level.INFO, "");
        loggingService.logEvent(Level.INFO, "------------------------------------------------------------");
        loggingService.logEvent(Level.INFO, "BUILD SUCCESS");
        loggingService.logEvent(Level.INFO, "------------------------------------------------------------");
        loggingService.logEvent(Level.INFO, "Found " + buildResults.getNumberOfCompiledSources()
                + " slang files under directory: \"" + contentPath + "\" and all are valid.");
        printNumberOfPassedAndSkippedTests(runTestsResults, loggingService);
        loggingService.logEvent(Level.INFO, "");
    }

    private static void printNumberOfPassedAndSkippedTests(IRunTestResults runTestsResults,
            final LoggingService loggingService) {
        loggingService.logEvent(Level.INFO, runTestsResults.getPassedTests().size() + " test cases passed");
        Map<String, TestRun> skippedTests = runTestsResults.getSkippedTests();
        if (skippedTests.size() > 0) {
            loggingService.logEvent(Level.INFO, skippedTests.size() + " test cases skipped");
        }
    }

    private static void printPassedTests(IRunTestResults runTestsResults, final LoggingService loggingService) {
        if (runTestsResults.getPassedTests().size() > 0) {
            loggingService.logEvent(Level.INFO, "------------------------------------------------------------");
            loggingService.logEvent(Level.INFO,
                    "Following " + runTestsResults.getPassedTests().size() + " test cases passed:");
            for (Map.Entry<String, TestRun> passedTest : runTestsResults.getPassedTests().entrySet()) {
                String testCaseReference = SlangTestCase
                        .generateTestCaseReference(passedTest.getValue().getTestCase());
                loggingService.logEvent(Level.INFO, "- " + testCaseReference.replaceAll("\n", "\n\t"));
            }
        }
    }

    private static void printBuildFailureSummary(String projectPath, IRunTestResults runTestsResults,
            final LoggingService loggingService) {
        printNumberOfPassedAndSkippedTests(runTestsResults, loggingService);
        final Map<String, TestRun> failedTests = runTestsResults.getFailedTests();
        logErrorsPrefix(loggingService);
        loggingService.logEvent(Level.ERROR, "BUILD FAILURE");
        loggingService.logEvent(Level.ERROR, "------------------------------------------------------------");
        loggingService.logEvent(Level.ERROR,
                "CloudSlang build for repository: \"" + projectPath + "\" failed due to failed tests.");
        loggingService.logEvent(Level.ERROR, "Following " + failedTests.size() + " tests failed:");
        for (Map.Entry<String, TestRun> failedTest : failedTests.entrySet()) {
            String failureMessage = failedTest.getValue().getMessage();
            loggingService.logEvent(Level.ERROR, "- " + failureMessage.replaceAll("\n", "\n\t"));
        }
        loggingService.logEvent(Level.ERROR, "");
    }

    private static void printSkippedTestsSummary(Map<String, TestRun> skippedTests,
            final LoggingService loggingService) {
        loggingService.logEvent(Level.INFO, "");
        loggingService.logEvent(Level.INFO, "------------------------------------------------------------");
        loggingService.logEvent(Level.INFO, "Following " + skippedTests.size() + " tests were skipped:");
        for (Map.Entry<String, TestRun> skippedTest : skippedTests.entrySet()) {
            String message = skippedTest.getValue().getMessage();
            loggingService.logEvent(Level.INFO, "- " + message.replaceAll("\n", "\n\t"));
        }
    }

    private static void printTestCoverageData(IRunTestResults runTestsResults,
            final LoggingService loggingService) {
        printCoveredExecutables(runTestsResults.getCoveredExecutables(), loggingService);
        printUncoveredExecutables(runTestsResults.getUncoveredExecutables(), loggingService);
        int coveredExecutablesSize = runTestsResults.getCoveredExecutables().size();
        int uncoveredExecutablesSize = runTestsResults.getUncoveredExecutables().size();
        int totalNumberOfExecutables = coveredExecutablesSize + uncoveredExecutablesSize;
        double coveragePercentage = (double) coveredExecutablesSize / (double) totalNumberOfExecutables * 100;
        loggingService.logEvent(Level.INFO, "");
        loggingService.logEvent(Level.INFO, "------------------------------------------------------------");
        loggingService.logEvent(Level.INFO, ((int) coveragePercentage) + "% of the content has tests");
        loggingService.logEvent(Level.INFO, "Out of " + totalNumberOfExecutables + " executables, "
                + coveredExecutablesSize + " executables have tests");
    }

    private static void printCoveredExecutables(Set<String> coveredExecutables,
            final LoggingService loggingService) {
        loggingService.logEvent(Level.INFO, "");
        loggingService.logEvent(Level.INFO, "------------------------------------------------------------");
        loggingService.logEvent(Level.INFO, "Following " + coveredExecutables.size() + " executables have tests:");
        for (String executable : coveredExecutables) {
            loggingService.logEvent(Level.INFO, "- " + executable);
        }
    }

    private static void printUncoveredExecutables(Set<String> uncoveredExecutables,
            final LoggingService loggingService) {
        loggingService.logEvent(Level.INFO, "");
        loggingService.logEvent(Level.INFO, "------------------------------------------------------------");
        loggingService.logEvent(Level.INFO,
                "Following " + uncoveredExecutables.size() + " executables do not have tests:");
        for (String executable : uncoveredExecutables) {
            loggingService.logEvent(Level.INFO, "- " + executable);
        }
    }

    private static String parseProjectPathArg(ApplicationArgs args) {
        String repositoryPath;

        if (args.getProjectRoot() != null) {
            repositoryPath = args.getProjectRoot();
            // if only one parameter was passed, we treat it as the project root
            // i.e. './cslang-builder some/path/to/project'
        } else if (args.getParameters().size() == 1) {
            repositoryPath = args.getParameters().get(0);
        } else {
            repositoryPath = System.getProperty("user.dir");
        }

        repositoryPath = FilenameUtils.separatorsToSystem(repositoryPath);

        Validate.isTrue(new File(repositoryPath).isDirectory(),
                "Directory path argument \'" + repositoryPath + "\' does not lead to a directory");

        return repositoryPath;
    }

    private static void registerEventHandlers(Slang slang) {
        slang.subscribeOnAllEvents(new ScoreEventListener() {
            @Override
            public synchronized void onEvent(ScoreEvent event) {
                logEvent(event);
            }
        });
    }

    private static void logEvent(ScoreEvent event) {
        log.debug(("Event received: " + event.getEventType() + " Data is: " + event.getData()));
    }

}