CloudSlang/cloud-slang

View on GitHub
cloudslang-content-verifier/src/main/java/io/cloudslang/lang/tools/build/tester/SlangTestRunner.java

Summary

Maintainability
F
3 days
Test Coverage
/*******************************************************************************
 * (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.tester;

import io.cloudslang.lang.api.Slang;
import io.cloudslang.lang.compiler.SlangSource;
import io.cloudslang.lang.compiler.modeller.DependenciesHelper;
import io.cloudslang.lang.compiler.modeller.model.Executable;
import io.cloudslang.lang.entities.CompilationArtifact;
import io.cloudslang.lang.entities.ScoreLangConstants;
import io.cloudslang.lang.entities.SystemProperty;
import io.cloudslang.lang.entities.bindings.values.Value;
import io.cloudslang.lang.tools.build.SlangBuildMain;
import io.cloudslang.lang.tools.build.SlangBuildMain.BulkRunMode;
import io.cloudslang.lang.tools.build.SlangBuildMain.TestCaseRunMode;
import io.cloudslang.lang.logging.LoggingService;
import io.cloudslang.lang.tools.build.tester.parallel.MultiTriggerTestCaseEventListener;
import io.cloudslang.lang.tools.build.tester.parallel.report.LoggingSlangTestCaseEventListener;
import io.cloudslang.lang.tools.build.tester.parallel.report.ThreadSafeRunTestResults;
import io.cloudslang.lang.tools.build.tester.parallel.services.ParallelTestCaseExecutorService;
import io.cloudslang.lang.tools.build.tester.parallel.services.TestCaseEventDispatchService;
import io.cloudslang.lang.tools.build.tester.parallel.testcaseevents.FailedSlangTestCaseEvent;
import io.cloudslang.lang.tools.build.tester.parse.SlangTestCase;
import io.cloudslang.lang.tools.build.tester.parse.TestCasesYamlParser;
import io.cloudslang.lang.tools.build.tester.runconfiguration.BuildModeConfig;
import io.cloudslang.lang.tools.build.tester.runconfiguration.TestRunInfoService;
import io.cloudslang.lang.tools.build.tester.runconfiguration.strategy.RunMultipleTestSuiteConflictResolutionStrategy;
import io.cloudslang.lang.tools.build.tester.runconfiguration.strategy.SequentialRunTestSuiteResolutionStrategy;
import io.cloudslang.score.events.EventConstants;
import java.io.File;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.logging.log4j.Level;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import static java.lang.Long.parseLong;
import static java.lang.String.valueOf;
import static java.lang.System.getProperty;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.apache.commons.collections4.CollectionUtils.containsAny;
import static org.apache.commons.collections4.CollectionUtils.isEmpty;


@Component
public class SlangTestRunner {

    private static final String PROJECT_PATH_TOKEN = "${project_path}";
    public static final long MAX_TIME_PER_TESTCASE_IN_MINUTES = 10;
    public static final String TEST_CASE_TIMEOUT_IN_MINUTES_KEY = "test.case.timeout.in.minutes";
    public static final String PREFIX_DASH = "    - ";

    @Autowired
    private TestCasesYamlParser parser;

    @Autowired
    private Slang slang;

    @Autowired
    private ParallelTestCaseExecutorService parallelTestCaseExecutorService;

    @Autowired
    private TestCaseEventDispatchService testCaseEventDispatchService;

    @Autowired
    private TestRunInfoService testRunInfoService;

    @Autowired
    private DependenciesHelper dependenciesHelper;

    @Autowired
    private LoggingService loggingService;

    @Autowired
    private LoggingSlangTestCaseEventListener loggingSlangTestCaseEventListener;

    private String[] testCaseFileExtensions = {"yaml", "yml"};
    private static final String TEST_CASE_PASSED = "Test case passed: ";
    private static final String TEST_CASE_FAILED = "Test case failed: ";

    private static final String UNAVAILABLE_NAME = "N/A";

    public enum TestCaseRunState {
        PARALLEL,
        SEQUENTIAL,
        INACTIVE
    }

    public Map<String, SlangTestCase> createTestCases(String testPath, Set<String> allAvailableExecutables) {
        Validate.notEmpty(testPath, "You must specify a path for tests");
        File testPathDir = new File(testPath);
        Validate.isTrue(testPathDir.isDirectory(),
                "Directory path argument \'" + testPath + "\' does not lead to a directory");
        Collection<File> testCasesFiles = FileUtils.listFiles(testPathDir, testCaseFileExtensions, true);

        loggingService.logEvent(Level.INFO, "");
        loggingService.logEvent(Level.INFO, "--- parsing test cases ---");
        loggingService.logEvent(Level.INFO, "Start parsing all test cases files under: " + testPath);
        loggingService.logEvent(Level.INFO, testCasesFiles.size() + " test cases files were found");

        Map<String, SlangTestCase> testCases = new HashMap<>();
        Set<SlangTestCase> testCasesWithMissingReference = new HashSet<>();
        for (File testCaseFile : testCasesFiles) {
            Validate.isTrue(testCaseFile.isFile(),
                    "file path \'" + testCaseFile.getAbsolutePath() + "\' must lead to a file");

            Map<String, SlangTestCase> testCasesFromCurrentFile =
                    parser.parseTestCases(SlangSource.fromFile(testCaseFile));
            for (Map.Entry<String, SlangTestCase> currentTestCaseEntry : testCasesFromCurrentFile.entrySet()) {
                SlangTestCase currentTestCase = currentTestCaseEntry.getValue();
                String currentTestCaseName = currentTestCaseEntry.getKey();
                String testFlowPath = currentTestCase.getTestFlowPath();
                currentTestCase.setName(currentTestCaseName);
                if (StringUtils.isBlank(currentTestCase.getResult())) {
                    currentTestCase.setResult(getResultFromFileName(testFlowPath));
                }
                if (currentTestCase.getThrowsException() == null) {
                    currentTestCase.setThrowsException(false);
                }
                // Make sure the new test cases names are unique
                if (testCases.containsKey(currentTestCaseName)) {
                    throw new RuntimeException("Test case with the name: " + currentTestCaseName +
                            " already exists. Test case name should be unique across the project"
                    );
                }
                if (!allAvailableExecutables.contains(testFlowPath)) {
                    testCasesWithMissingReference.add(currentTestCase);
                }
                testCases.put(currentTestCaseName, currentTestCase);
            }
        }
        printTestCasesWithMissingReference(testCasesWithMissingReference);
        return testCases;
    }

    /**
     *
     * @param projectPath the project path
     * @param testCases the test cases
     * @param compiledFlows the compiled flows
     * @param runTestsResults is updated to reflect skipped, failed passes test cases.
     */
    public void runTestsSequential(String projectPath, Map<String, SlangTestCase> testCases,
                                   Map<String, CompilationArtifact> compiledFlows, IRunTestResults runTestsResults) {

        if (MapUtils.isEmpty(testCases)) {
            return;
        }
        printTestForActualRunSummary(TestCaseRunMode.SEQUENTIAL, testCases);

        for (Map.Entry<String, SlangTestCase> testCaseEntry : testCases.entrySet()) {
            SlangTestCase testCase = testCaseEntry.getValue();

            loggingService.logEvent(Level.INFO, "Running test: " +
                    SlangTestCase.generateTestCaseReference(testCase) + " - " + testCase.getDescription());
            try {
                CompilationArtifact compiledTestFlow = getCompiledTestFlow(compiledFlows, testCase);
                runTest(testCase, compiledTestFlow, projectPath);
                runTestsResults.addPassedTest(testCase.getName(), new TestRun(testCase, null));
            } catch (RuntimeException e) {
                runTestsResults.addFailedTest(testCase.getName(), new TestRun(testCase, e.getMessage()));
            }
        }
    }

    public void runTestsParallel(String projectPath, Map<String, SlangTestCase> testCases,
                                 Map<String, CompilationArtifact> compiledFlows,
                                 ThreadSafeRunTestResults runTestsResults) {
        if (MapUtils.isEmpty(testCases)) {
            return;
        }
        printTestForActualRunSummary(TestCaseRunMode.PARALLEL, testCases);

        testCaseEventDispatchService.unregisterAllListeners();
        testCaseEventDispatchService.registerListener(runTestsResults); // for gathering of report data
        testCaseEventDispatchService.registerListener(loggingSlangTestCaseEventListener); // for logging purpose

        MultiTriggerTestCaseEventListener multiTriggerTestCaseEventListener = new MultiTriggerTestCaseEventListener();
        slang.subscribeOnEvents(multiTriggerTestCaseEventListener, createListenerEventTypesSet());
        try {
            Map<SlangTestCase, Future<?>> testCaseFutures = new LinkedHashMap<>();
            for (Map.Entry<String, SlangTestCase> testCaseEntry : testCases.entrySet()) {
                SlangTestCase testCase = testCaseEntry.getValue();
                SlangTestCaseRunnable slangTestCaseRunnable =
                        new SlangTestCaseRunnable(testCase, compiledFlows, projectPath, this,
                                testCaseEventDispatchService, multiTriggerTestCaseEventListener);
                testCaseFutures.put(testCase, parallelTestCaseExecutorService.submitTestCase(slangTestCaseRunnable));
            }

            final long testCaseTimeoutMinutes = getTestCaseTimeoutInMinutes();
            for (Map.Entry<SlangTestCase, Future<?>> slangTestCaseFutureEntry : testCaseFutures.entrySet()) {
                SlangTestCase testCase = slangTestCaseFutureEntry.getKey();
                Future<?> testCaseFuture = slangTestCaseFutureEntry.getValue();
                try {
                    testCaseFuture.get(testCaseTimeoutMinutes, MINUTES);
                } catch (InterruptedException e) {
                    loggingService.logEvent(Level.ERROR, "Interrupted while waiting for result: ", e);
                } catch (TimeoutException e) {
                    testCaseEventDispatchService.notifyListeners(new FailedSlangTestCaseEvent(testCase,
                            "Timeout reached for test case " + testCase.getName(), e));
                } catch (Exception e) {
                    testCaseEventDispatchService.notifyListeners(
                            new FailedSlangTestCaseEvent(testCase, e.getMessage(), e));
                }
            }
        } finally {
            testCaseEventDispatchService.unregisterAllListeners();
            slang.unSubscribeOnEvents(multiTriggerTestCaseEventListener);
        }
    }

    private void printTestForActualRunSummary(TestCaseRunMode runMode, Map<String, SlangTestCase> testCases) {
        if (!MapUtils.isEmpty(testCases)) {
            loggingService.logEvent(Level.INFO, "Running " + testCases.size() + " test(s) in " +
                    runMode.toString().toLowerCase(Locale.ENGLISH) + ": ");
            for (Map.Entry<String, SlangTestCase> stringSlangTestCaseEntry : testCases.entrySet()) {
                final SlangTestCase slangTestCase = stringSlangTestCaseEntry.getValue();
                loggingService.logEvent(Level.INFO, PREFIX_DASH +
                        SlangTestCase.generateTestCaseReference(slangTestCase));
            }
        }
    }

    /**
     * Processes skipped tests and also handles null testcase failures
     * @param bulkRunMode the bulk run mode
     * @param testCases the test cases
     * @param testSuites active test suites
     * @param runTestsResults is updated to reflect skipped and fail fast scenarios
     * @param buildModeConfig the build mode config
     * @return a map with split test cases
     */
    public Map<TestCaseRunState, Map<String, SlangTestCase>> splitTestCasesByRunState(
            final BulkRunMode bulkRunMode,
            final Map<String, SlangTestCase> testCases,
            final List<String> testSuites,
            final IRunTestResults runTestsResults,
            final BuildModeConfig buildModeConfig) {
        Map<TestCaseRunState, Map<String, SlangTestCase>> resultMap = new HashMap<>();

        // Prepare the 3 categories inactive, parallel, sequential
        for (TestCaseRunState testCaseRunState : TestCaseRunState.values()) {
            resultMap.put(testCaseRunState, new LinkedHashMap<String, SlangTestCase>());
        }

        for (Map.Entry<String, SlangTestCase> testCaseEntry : testCases.entrySet()) {
            final SlangTestCase testCase = testCaseEntry.getValue();
            if (testCase == null) {
                processQuickFailTest(runTestsResults);
                continue;
            }

            if (isTestCaseInActiveSuite(testCase, testSuites) &&
                    isEnabledByBuildMode(buildModeConfig.getBuildMode(), testCase,
                            buildModeConfig.getChangedFiles(), buildModeConfig.getAllTestedFlowModels())) {
                processActiveTest(bulkRunMode, resultMap, testCaseEntry, testCase);
            } else {
                processSkippedTest(runTestsResults, testCaseEntry, testCase, resultMap);
            }
        }

        return resultMap;
    }

    private boolean isEnabledByBuildMode(
            SlangBuildMain.BuildMode buildMode,
            SlangTestCase slangTestCase,
            Set<String> changedExecutables,
            Map<String, Executable> allTestedFlowModels) {
        return (buildMode == SlangBuildMain.BuildMode.BASIC) ||
                (buildMode == SlangBuildMain.BuildMode.CHANGED &&
                        isAffectedTestCase(slangTestCase, changedExecutables, allTestedFlowModels));
    }

    private boolean isAffectedTestCase(SlangTestCase slangTestCase, Set<String> changedExecutables,
                                       Map<String, Executable> allTestedFlowModels) {
        String testFlowPath = slangTestCase.getTestFlowPath();
        Executable testCaseReference = allTestedFlowModels.get(testFlowPath);
        if (testCaseReference == null) {
            throw new RuntimeException("Test case reference[" + testFlowPath + "] not found in compiled models.");
        }
        Set<String> testCaseDependencies = dependenciesHelper.fetchDependencies(testCaseReference, allTestedFlowModels);
        testCaseDependencies.add(testFlowPath);
        return containsAny(testCaseDependencies, changedExecutables);
    }

    private void processQuickFailTest(final IRunTestResults runTestsResults) {
        runTestsResults.addFailedTest(UNAVAILABLE_NAME, new TestRun(null, "Test case cannot be null"));
    }

    private void processActiveTest(final BulkRunMode bulkRunMode, final Map<TestCaseRunState,
            Map<String, SlangTestCase>> resultMap, Map.Entry<String, SlangTestCase> testCaseEntry,
                                   SlangTestCase testCase) {
        if (bulkRunMode == BulkRunMode.POSSIBLY_MIXED) {
            TestCaseRunMode runModeForTestCase = testRunInfoService
                    .getRunModeForTestCase(testCase, new RunMultipleTestSuiteConflictResolutionStrategy(),
                    new SequentialRunTestSuiteResolutionStrategy());
            if (runModeForTestCase == TestCaseRunMode.SEQUENTIAL) {
                resultMap.get(TestCaseRunState.SEQUENTIAL).put(testCaseEntry.getKey(), testCase);
            } else if (runModeForTestCase == TestCaseRunMode.PARALLEL) {
                resultMap.get(TestCaseRunState.PARALLEL).put(testCaseEntry.getKey(), testCase);
            }
        } else if (bulkRunMode == BulkRunMode.ALL_SEQUENTIAL) {
            resultMap.get(TestCaseRunState.SEQUENTIAL).put(testCaseEntry.getKey(), testCase);
        } else if (bulkRunMode == BulkRunMode.ALL_PARALLEL) {
            resultMap.get(TestCaseRunState.PARALLEL).put(testCaseEntry.getKey(), testCase);
        }
    }

    private void processSkippedTest(final IRunTestResults runTestsResults,
                                    Map.Entry<String, SlangTestCase> testCaseEntry, SlangTestCase testCase,
                                    final Map<TestCaseRunState, Map<String, SlangTestCase>> resultMap) {
        String message = "Skipping test: " + SlangTestCase.generateTestCaseReference(testCase) +
                " because it is not in active test suites";
        loggingService.logEvent(Level.INFO, message);

        runTestsResults.addSkippedTest(testCase.getName(), new TestRun(testCase, message));
        resultMap.get(TestCaseRunState.INACTIVE).put(testCaseEntry.getKey(), testCaseEntry.getValue());
    }

    private long getTestCaseTimeoutInMinutes() {
        try {
            return parseLong(getProperty(TEST_CASE_TIMEOUT_IN_MINUTES_KEY, valueOf(MAX_TIME_PER_TESTCASE_IN_MINUTES)));
        } catch (NumberFormatException nfEx) {
            loggingService.logEvent(Level.WARN,
                    String.format("Misconfigured test case timeout '%s'. Using default timeout %d.",
                            getProperty(TEST_CASE_TIMEOUT_IN_MINUTES_KEY), MAX_TIME_PER_TESTCASE_IN_MINUTES));
            return MAX_TIME_PER_TESTCASE_IN_MINUTES;
        }
    }

    public boolean isTestCaseInActiveSuite(SlangTestCase testCase, List<String> testSuites) {
        return (isEmpty(testCase.getTestSuites()) && testSuites.contains(SlangBuildMain.DEFAULT_TESTS)) ||
                containsAny(testSuites, testCase.getTestSuites());
    }

    private void printTestCasesWithMissingReference(Set<SlangTestCase> testCasesWithMissingReference) {
        int testCasesWithMissingReferenceSize = testCasesWithMissingReference.size();
        if (testCasesWithMissingReferenceSize > 0) {
            loggingService.logEvent(Level.INFO, "");
            loggingService.logEvent(Level.INFO, testCasesWithMissingReferenceSize +
                    " test cases have missing test flow references:");
            for (SlangTestCase slangTestCase : testCasesWithMissingReference) {
                loggingService.logEvent(Level.INFO,
                        "For test case: " + SlangTestCase.generateTestCaseReference(slangTestCase) +
                                " testFlowPath reference not found: " +
                                slangTestCase.getTestFlowPath()
                );
            }
        }
    }

    public CompilationArtifact getCompiledTestFlow(Map<String, CompilationArtifact> compiledFlows,
                                                   SlangTestCase testCase) {
        String testFlowPath = testCase.getTestFlowPath();
        if (StringUtils.isEmpty(testFlowPath)) {
            throw new RuntimeException("For test case: " + SlangTestCase.generateTestCaseReference(testCase) +
                    " testFlowPath property is mandatory");
        }
        String testFlowPathTransformed = testFlowPath.replace(File.separatorChar, '.');
        CompilationArtifact compiledTestFlow = compiledFlows.get(testFlowPathTransformed);
        if (compiledTestFlow == null) {
            throw new RuntimeException("Test flow: " + testFlowPath + " is missing. Referenced in test case: " +
                    SlangTestCase.generateTestCaseReference(testCase));
        }
        return compiledTestFlow;
    }

    public void runTest(SlangTestCase testCase, CompilationArtifact compiledTestFlow, String projectPath) {

        Map<String, Value> convertedInputs = getTestCaseInputsMap(testCase);
        Set<SystemProperty> systemProperties = getTestSystemProperties(testCase, projectPath);

        runTestCaseSequentiallyToCompletion(testCase, compiledTestFlow, convertedInputs, systemProperties);
    }

    public void runTestCaseParallel(SlangTestCase testCase, CompilationArtifact compiledTestFlow,
                                    String projectPath,
                                    MultiTriggerTestCaseEventListener multiTriggerTestCaseEventListener) {

        Map<String, Value> convertedInputs = getTestCaseInputsMap(testCase);
        Set<SystemProperty> systemProperties = getTestSystemProperties(testCase, projectPath);

        runTestCaseInParallelToCompletion(testCase, compiledTestFlow, convertedInputs,
                systemProperties, multiTriggerTestCaseEventListener);
    }

    private Set<SystemProperty> getTestSystemProperties(SlangTestCase testCase, String projectPath) {
        String systemPropertiesFile = testCase.getSystemPropertiesFile();
        if (StringUtils.isEmpty(systemPropertiesFile)) {
            return new HashSet<>();
        }
        systemPropertiesFile = StringUtils.replace(systemPropertiesFile, PROJECT_PATH_TOKEN, projectPath);
        return parser.parseProperties(systemPropertiesFile);
    }

    private Map<String, Value> getTestCaseInputsMap(SlangTestCase testCase) {
        List<Map> inputs = testCase.getInputs();
        Map<String, Serializable> convertedInputs = new HashMap<>();
        convertedInputs = convertMapParams(inputs, convertedInputs);
        return io.cloudslang.lang.entities.utils.MapUtils.convertMapNonSensitiveValues(convertedInputs);
    }

    private Map<String, Serializable> convertMapParams(List<Map> params, Map<String, Serializable> convertedInputs) {
        if (CollectionUtils.isNotEmpty(params)) {
            for (Map param : params) {
                convertedInputs.put(
                        (String) param.keySet().iterator().next(),
                        (Serializable) param.values().iterator().next());
            }
        }
        return convertedInputs;
    }

    private Map<String, Serializable> getTestCaseOutputsMap(SlangTestCase testCase) {
        List<Map> outputs = testCase.getOutputs();
        Map<String, Serializable> convertedOutputs = new HashMap<>();
        return convertMapParams(outputs, convertedOutputs);
    }

    /**
     * This method will trigger the flow in a synchronize matter, meaning only one flow can run at a time.
     *
     * @param testCase the test Case
     * @param compilationArtifact the artifact to trigger
     * @param inputs flow inputs
     * @param systemProperties the system properties
     * @return executionId
     */
    public Long runTestCaseSequentiallyToCompletion(SlangTestCase testCase, CompilationArtifact compilationArtifact,
                                                    Map<String, Value> inputs,
                                                    Set<SystemProperty> systemProperties) {

        final String result = testCase.getResult();
        final Map<String, Serializable> outputs = getTestCaseOutputsMap(testCase);
        String flowName = testCase.getTestFlowPath();

        TriggerTestCaseEventListener testsEventListener = new TriggerTestCaseEventListener();
        slang.subscribeOnEvents(testsEventListener, createListenerEventTypesSet());

        Long executionId = slang.run(compilationArtifact, inputs, systemProperties);

        while (!testsEventListener.isFlowFinished()) {
            poll();
        }
        slang.unSubscribeOnEvents(testsEventListener);

        String errorMessageFlowExecution = testsEventListener.getErrorMessage();

        String message;
        String testCaseReference = SlangTestCase.generateTestCaseReference(testCase);

        if (BooleanUtils.isTrue(testCase.getThrowsException())) {
            return handleExpectedExceptionCase(testCase, compilationArtifact, flowName, executionId,
                    errorMessageFlowExecution, testCaseReference);
        }

        if (StringUtils.isNotBlank(errorMessageFlowExecution)) {
            // unexpected exception occurred during flow execution
            message = "Error occurred while running test: " + testCaseReference + " - " +
                    testCase.getDescription() + "\n\t" + errorMessageFlowExecution;
            loggingService.logEvent(Level.INFO, message);
            throw new RuntimeException(message);
        }

        String executionResult = testsEventListener.getResult();
        if (result != null && !result.equals(executionResult)) {
            message = TEST_CASE_FAILED + testCaseReference + " - " + testCase.getDescription() +
                    "\n\tExpected result: " + result + "\n\tActual result: " + executionResult;
            loggingService.logEvent(Level.ERROR, message);
            throw new RuntimeException(message);
        }

        Map<String, Serializable> executionOutputs = testsEventListener.getOutputs();
        handleTestCaseFailuresFromOutputs(testCase, testCaseReference, outputs, executionOutputs);

        loggingService.logEvent(Level.INFO, TEST_CASE_PASSED + testCaseReference +
                ". Finished running: " + flowName + " with result: " + executionResult);
        return executionId;
    }

    public Long runTestCaseInParallelToCompletion(SlangTestCase testCase, CompilationArtifact compilationArtifact,
                                                  Map<String, Value> inputs,
                                                  Set<SystemProperty> systemProperties,
                                                  MultiTriggerTestCaseEventListener globalListener) {

        final String result = testCase.getResult();
        final Map<String, Serializable> outputs = getTestCaseOutputsMap(testCase);
        String flowName = testCase.getTestFlowPath();

        Long executionId = slang.run(compilationArtifact, inputs, systemProperties);

        while (!globalListener.isFlowFinishedByExecutionId(executionId)) {
            poll();
        }

        String errorMessageFlowExecution = globalListener.getErrorMessageByExecutionId(executionId);

        String message;
        String testCaseReference = SlangTestCase.generateTestCaseReference(testCase);

        if (BooleanUtils.isTrue(testCase.getThrowsException())) {
            return handleExpectedExceptionCase(testCase, compilationArtifact, flowName, executionId,
                    errorMessageFlowExecution, testCaseReference);
        }

        if (StringUtils.isNotBlank(errorMessageFlowExecution)) {
            // unexpected exception occurred during flow execution
            message = "Error occurred while running test: " + testCaseReference + " - " +
                    testCase.getDescription() + "\n\t" + errorMessageFlowExecution;
            loggingService.logEvent(Level.INFO, message);
            throw new RuntimeException(message);
        }

        String executionResult = globalListener.getResultByExecutionId(executionId);
        if (result != null && !result.equals(executionResult)) {
            message = TEST_CASE_FAILED + testCaseReference + " - " + testCase.getDescription() +
                    "\n\tExpected result: " + result + "\n\tActual result: " + executionResult;
            loggingService.logEvent(Level.ERROR, message);
            throw new RuntimeException(message);
        }

        Map<String, Serializable> executionOutputs = globalListener.getOutputsByExecutionId(executionId);
        handleTestCaseFailuresFromOutputs(testCase, testCaseReference, outputs, executionOutputs);

        loggingService.logEvent(Level.INFO, TEST_CASE_PASSED + testCaseReference +
                ". Finished running: " + flowName + " with result: " + executionResult);
        return executionId;
    }

    private Long handleExpectedExceptionCase(SlangTestCase testCase, CompilationArtifact compilationArtifact,
                                             String flowName, Long executionId,
                                             String errorMessageFlowExecution, String testCaseReference) {
        String message;
        if (StringUtils.isBlank(errorMessageFlowExecution)) {

            message = TEST_CASE_FAILED + testCaseReference + " - " + testCase.getDescription() + "\n\tFlow " +
                    compilationArtifact.getExecutionPlan().getName() + " did not throw an exception as expected";
            loggingService.logEvent(Level.INFO, message);
            throw new RuntimeException(message);
        }
        loggingService.logEvent(Level.INFO, TEST_CASE_PASSED + testCaseReference +
                ". Finished running: " + flowName + " with exception as expected");
        return executionId;
    }

    private void poll() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException ignore) {
        }
    }

    private void handleTestCaseFailuresFromOutputs(SlangTestCase testCase, String testCaseReference,
                                                   Map<String, Serializable> outputs,
                                                   Map<String, Serializable> executionOutputs) {
        String message;
        if (MapUtils.isNotEmpty(outputs)) {
            for (Map.Entry<String, Serializable> output : outputs.entrySet()) {
                String outputName = output.getKey();
                Serializable outputValue = output.getValue();
                Serializable executionOutputValue = executionOutputs.get(outputName);
                if (!executionOutputs.containsKey(outputName) ||
                        !outputsAreEqual(outputValue, executionOutputValue)) {
                    message = TEST_CASE_FAILED + testCaseReference + " - " + testCase.getDescription() +
                            "\n\tFor output: " + outputName + "\n\tExpected value: " + outputValue +
                            "\n\tActual value: " + executionOutputValue;

                    loggingService.logEvent(Level.ERROR, message);
                    throw new RuntimeException(message);
                }
            }
        }
    }

    private boolean outputsAreEqual(Serializable outputValue, Serializable executionOutputValue) {
        return executionOutputValue == outputValue ||
                StringUtils.equals(executionOutputValue.toString(), outputValue.toString());
    }

    private Set<String> createListenerEventTypesSet() {
        Set<String> handlerTypes = new HashSet<>();
        handlerTypes.add(ScoreLangConstants.EVENT_EXECUTION_FINISHED);
        handlerTypes.add(ScoreLangConstants.SLANG_EXECUTION_EXCEPTION);
        handlerTypes.add(ScoreLangConstants.EVENT_OUTPUT_END);
        handlerTypes.add(EventConstants.SCORE_ERROR_EVENT);
        handlerTypes.add(EventConstants.SCORE_FAILURE_EVENT);
        handlerTypes.add(EventConstants.SCORE_FINISHED_EVENT);
        return handlerTypes;
    }

    private String getResultFromFileName(String fileName) {

        int dashPosition = fileName.lastIndexOf('-');
        if (dashPosition > 0 && dashPosition < fileName.length()) {
            return fileName.substring(dashPosition + 1);
        } else {
            return null;
        }
    }
}