#!/usr/bin/env bash

function print_usage {
    echo ''
    echo 'CoreCLR test runner script.'
    echo ''
    echo 'Typical command line:'
    echo ''
    echo 'src/tests/run.sh'
    echo '    --testRootDir="temp/windows.x64.Debug"'
    echo '    --testNativeBinDir="runtime/artifacts/obj/Linux.x64.Debug/tests"'
    echo '    --coreOverlayDir="runtime/artifacts/tests/Linux.x64.Debug/Tests/Core_Root"'
    echo '    --copyNativeTestBin'
    echo ''
    echo 'Required arguments:'
    echo '  --testRootDir=<path>             : Root directory of the test build (e.g. runtime/artifacts/tests/windows.x64.Debug).'
    echo '  --testNativeBinDir=<path>        : Directory of the native CoreCLR test build (e.g. runtime/artifacts/obj/Linux.x64.Debug/tests).'
    echo '  (Also required: Either --coreOverlayDir, or all of the switches --coreOverlayDir overrides)'
    echo ''
    echo 'Optional arguments:'
    echo '  --coreOverlayDir=<path>          : Directory containing core binaries and test dependencies. If not specified, the'
    echo '                                     default is testRootDir/Tests/coreoverlay. This switch overrides --coreClrBinDir,'
    echo '                                     --mscorlibDir, and --coreFxBinDir.'
    echo '  --coreClrBinDir=<path>           : Directory of the CoreCLR build (e.g. runtime/artifacts/bin/coreclr/Linux.x64.Debug).'
    echo '  --mscorlibDir=<path>             : Directory containing the built mscorlib.dll. If not specified, it is expected to be'
    echo '                                       in the directory specified by --coreClrBinDir.'
    echo '  --coreFxBinDir="<path>"          : Directory with CoreFX build outputs'
    echo '                                     (e.g. "corefx/artifacts/runtime/netcoreapp-Linux-Debug-x64")'
    echo '                                     If files with the same name are present in multiple directories, the first one wins.'
    echo '  --testDir=<path>                 : Run tests only in the specified directory. The path is relative to the directory'
    echo '                                     specified by --testRootDir. Multiple of this switch may be specified.'
    echo '  --testDirFile=<path>             : Run tests only in the directories specified by the file at <path>. Paths are listed'
    echo '                                     one line, relative to the directory specified by --testRootDir.'
    echo '  --build-overlay-only             : Build coreoverlay only, and skip running tests.'
    echo '  --runFailingTestsOnly            : Run only the tests that are disabled on this platform due to unexpected failures.'
    echo '                                     Failing tests are listed in coreclr/tests/failingTestsOutsideWindows.txt, one per'
    echo '                                     line, as paths to .sh files relative to the directory specified by --testRootDir.'
    echo '  --disableEventLogging            : Disable the events logged by both VM and Managed Code'
    echo '  --jobs=<n>                       : Set the maximum number of processes the scheduler can create.'
    echo '  --sequential                     : Run tests sequentially (default is to run in parallel).'
    echo '  --playlist=<path>                : Run only the tests that are specified in the file at <path>, in the same format as'
    echo '                                     runFailingTestsOnly'
    echo '  -v, --verbose                    : Show output from each test.'
    echo '  -h|--help                        : Show usage information.'
    echo '  --useServerGC                    : Enable server GC for this test run'
    echo '  --test-env                       : Script to set environment variables for tests'
    echo '  --copyNativeTestBin              : Explicitly copy native test components into the test dir'
    echo '  --crossgen                       : Precompiles the framework managed assemblies'
    echo '  --runcrossgentests               : Runs the ready to run tests'
    echo '  --jitstress=<n>                  : Runs the tests with DOTNET_JitStress=n'
    echo '  --jitstressregs=<n>              : Runs the tests with DOTNET_JitStressRegs=n'
    echo '  --jitminopts                     : Runs the tests with DOTNET_JITMinOpts=1'
    echo '  --jitforcerelocs                 : Runs the tests with DOTNET_ForceRelocs=1'
    echo '  --jitdisasm                      : Runs jit-dasm on the tests'
    echo '  --gcstresslevel=<n>              : Runs the tests with DOTNET_GCStress=n'
    echo '  --gcname=<n>                     : Runs the tests with DOTNET_GCName=n'
    echo '  --ilasmroundtrip                 : Runs ilasm round trip on the tests'
    echo '    0: None                                1: GC on all allocs and '"'easy'"' places'
    echo '    2: GC on transitions to preemptive GC  4: GC on every allowable JITed instr'
    echo '    8: GC on every allowable NGEN instr   16: GC only on a unique stack trace'
    echo '  --long-gc                        : Runs the long GC tests'
    echo '  --gcsimulator                    : Runs the GCSimulator tests'
    echo '  --tieredcompilation              : Runs the tests with DOTNET_TieredCompilation=1'
    echo '  --link <ILlink>                  : Runs the tests after linking via ILlink'
    echo '  --show-time                      : Print execution sequence and running time for each test'
    echo '  --no-lf-conversion               : Do not execute LF conversion before running test script'
    echo '  --limitedDumpGeneration          : Enables the generation of a limited number of core dumps if test(s) crash, even if ulimit'
    echo '                                     is zero when launching this script. This option is intended for use in CI.'
    echo '  --xunitOutputPath=<path>         : Create xUnit XML report at the specified path (default: <test root>/coreclrtests.xml)'
    echo ''
    echo 'Runtime Code Coverage options:'
    echo '  --coreclr-coverage               : Optional argument to get coreclr code coverage reports'
    echo '  --coreclr-objs=<path>            : Location of root of the object directory'
    echo '                                     containing the linux/mac coreclr build'
    echo '  --coreclr-src=<path>             : Location of root of the directory'
    echo '                                     containing the coreclr source files'
    echo '  --coverage-output-dir=<path>     : Directory where coverage output will be written to'
    echo ''
}

function print_results {
    echo ""
    echo "======================="
    echo "     Test Results"
    echo "======================="
    echo "# CoreCLR Bin Dir  : $coreClrBinDir"
    echo "# Tests Discovered : $countTotalTests"
    echo "# Passed           : $countPassedTests"
    echo "# Failed           : $countFailedTests"
    echo "# Skipped          : $countSkippedTests"
    echo "======================="
}

# Initialize counters for bookkeeping.
countTotalTests=0
countPassedTests=0
countFailedTests=0
countSkippedTests=0

# Variables for xUnit-style XML output. XML format: https://xunit.github.io/docs/format-xml-v2.html
xunitOutputPath=
xunitTestOutputPath=

# Variables for text file output. These can be passed back to run.sh using the "--playlist" argument
# to rerun specific tests.
testsPassOutputPath=
testsFailOutputPath=
testsSkipOutputPath=

# libExtension determines extension for dynamic library files
# runtimeName determines where CoreFX Runtime files will be located
OSName=$(uname -s)
libExtension=
case $OSName in
    Darwin)
        libExtension="dylib"
        ;;

    Linux)
        libExtension="so"
        ;;

    NetBSD)
        libExtension="so"
        ;;

    *)
        echo "Unsupported OS $OSName detected, configuring as if for Linux"
        libExtension="so"
        ;;
esac

function xunit_output_begin {
    if [ -z "$xunitOutputPath" ]; then
        xunitOutputPath=$testRootDir/coreclrtests.xml
    fi
    if ! [ -e $(basename "$xunitOutputPath") ]; then
        xunitOutputPath=$testRootDir/coreclrtests.xml
    fi
    xunitTestOutputPath=${xunitOutputPath}.test
    if [ -e "$xunitOutputPath" ]; then
        rm -f -r "$xunitOutputPath"
    fi
    if [ -e "$xunitTestOutputPath" ]; then
        rm -f -r "$xunitTestOutputPath"
    fi
}

function xunit_output_add_test {
    # <assemblies>
    #   <assembly>
    #     <collection>
    #       <test .../> <!-- Write this element here -->

    local scriptFilePath=$1
    local outputFilePath=$2
    local testResult=$3 # Pass, Fail, or Skip
    local testScriptExitCode=$4
    local testRunningTime=$5

    local testPath=${scriptFilePath%.sh} # Remove trailing ".sh"
    local testDir=$(dirname "$testPath")
    local testName=$(basename "$testPath")

    # Replace '/' with '.'
    testPath=$(echo "$testPath" | tr / .)
    testDir=$(echo "$testDir" | tr / .)

    local line

    line="      "
    line="${line}<test"
    line="${line} name=\"${testPath}\""
    line="${line} type=\"${testDir}\""
    line="${line} method=\"${testName}\""
    line="${line} result=\"${testResult}\""
    if [[ -n "$testRunningTime" && "$testResult" != "Skip" ]]; then
        line="${line} time=\"${testRunningTime}\""
    fi

    if [[ "$testResult" == "Pass" ]]; then
        line="${line}/>"
        echo "$line" >>"$xunitTestOutputPath"
        return
    fi

    line="${line}>"
    echo "$line" >>"$xunitTestOutputPath"

    line="        "
    if [[ "$testResult" == "Skip" ]]; then
        line="${line}<reason><![CDATA[$(cat "$outputFilePath")]]></reason>"
        echo "$line" >>"$xunitTestOutputPath"
    else
        line="${line}<failure exception-type=\"Exit code: ${testScriptExitCode}\">"
        echo "$line" >>"$xunitTestOutputPath"

        line="          "
        line="${line}<message>"
        echo "$line" >>"$xunitTestOutputPath"
        line="            "
        line="${line}<![CDATA["
        echo "$line" >>"$xunitTestOutputPath"
        cat "$outputFilePath" >>"$xunitTestOutputPath"
        line="            "
        line="${line}]]>"
        echo "$line" >>"$xunitTestOutputPath"
        line="          "
        line="${line}</message>"
        echo "$line" >>"$xunitTestOutputPath"

        line="        "
        line="${line}</failure>"
        echo "$line" >>"$xunitTestOutputPath"
    fi

    line="      "
    line="${line}</test>"
    echo "$line" >>"$xunitTestOutputPath"
}

function xunit_output_end {
    local errorSource=$1
    local errorMessage=$2

    local errorCount
    if [ -z "$errorSource" ]; then
        ((errorCount = 0))
    else
        ((errorCount = 1))
    fi

    echo '<?xml version="1.0" encoding="utf-8"?>' >>"$xunitOutputPath"
    echo '<assemblies>' >>"$xunitOutputPath"

    local line

    # <assembly ...>
    line="  "
    line="${line}<assembly"
    line="${line} name=\"CoreClrTestAssembly\""
    line="${line} total=\"${countTotalTests}\""
    line="${line} passed=\"${countPassedTests}\""
    line="${line} failed=\"${countFailedTests}\""
    line="${line} skipped=\"${countSkippedTests}\""
    line="${line} errors=\"${errorCount}\""
    line="${line}>"
    echo "$line" >>"$xunitOutputPath"

    # <collection ...>
    line="    "
    line="${line}<collection"
    line="${line} name=\"CoreClrTestCollection\""
    line="${line} total=\"${countTotalTests}\""
    line="${line} passed=\"${countPassedTests}\""
    line="${line} failed=\"${countFailedTests}\""
    line="${line} skipped=\"${countSkippedTests}\""
    line="${line}>"
    echo "$line" >>"$xunitOutputPath"

    # <test .../> <test .../> ...
    if [ -f "$xunitTestOutputPath" ]; then
        cat "$xunitTestOutputPath" >>"$xunitOutputPath"
        rm -f "$xunitTestOutputPath"
    fi

    # </collection>
    line="    "
    line="${line}</collection>"
    echo "$line" >>"$xunitOutputPath"

    if [ -n "$errorSource" ]; then
        # <errors>
        line="    "
        line="${line}<errors>"
        echo "$line" >>"$xunitOutputPath"

        # <error ...>
        line="      "
        line="${line}<error"
        line="${line} type=\"TestHarnessError\""
        line="${line} name=\"${errorSource}\""
        line="${line}>"
        echo "$line" >>"$xunitOutputPath"

        # <failure .../>
        line="        "
        line="${line}<failure>${errorMessage}</failure>"
        echo "$line" >>"$xunitOutputPath"

        # </error>
        line="      "
        line="${line}</error>"
        echo "$line" >>"$xunitOutputPath"

        # </errors>
        line="    "
        line="${line}</errors>"
        echo "$line" >>"$xunitOutputPath"
    fi

    # </assembly>
    line="  "
    line="${line}</assembly>"
    echo "$line" >>"$xunitOutputPath"

    # </assemblies>
    echo '</assemblies>' >>"$xunitOutputPath"
}

function text_file_output_begin {
    if [ -z "$testsPassOutputPath" ]; then
        testsPassOutputPath=$testRootDir/coreclrtests.pass.txt
    fi
    if ! [ -e $(basename "$testsPassOutputPath") ]; then
        testsPassOutputPath=$testRootDir/coreclrtests.pass.txt
    fi
    if [ -e "$testsPassOutputPath" ]; then
        rm -f "$testsPassOutputPath"
    fi
    if [ -z "$testsFailOutputPath" ]; then
        testsFailOutputPath=$testRootDir/coreclrtests.fail.txt
    fi
    if ! [ -e $(basename "$testsFailOutputPath") ]; then
        testsFailOutputPath=$testRootDir/coreclrtests.fail.txt
    fi
    if [ -e "$testsFailOutputPath" ]; then
        rm -f "$testsFailOutputPath"
    fi
    if [ -z "$testsSkipOutputPath" ]; then
        testsSkipOutputPath=$testRootDir/coreclrtests.skip.txt
    fi
    if ! [ -e $(basename "$testsSkipOutputPath") ]; then
        testsSkipOutputPath=$testRootDir/coreclrtests.skip.txt
    fi
    if [ -e "$testsSkipOutputPath" ]; then
        rm -f "$testsSkipOutputPath"
    fi
}

function text_file_output_add_test {
    local scriptFilePath=$1
    local testResult=$2 # Pass, Fail, or Skip

    if [[ "$testResult" == "Pass" ]]; then
        echo "$scriptFilePath" >>"$testsPassOutputPath"
    elif [[ "$testResult" == "Skip" ]]; then
        echo "$scriptFilePath" >>"$testsSkipOutputPath"
    else
        echo "$scriptFilePath" >>"$testsFailOutputPath"
    fi
}

function exit_with_error {
    local errorSource=$1
    local errorMessage=$2
    local printUsage=$3

    if [ -z "$printUsage" ]; then
        ((printUsage = 0))
    fi

    echo "$errorMessage"
    xunit_output_end "$errorSource" "$errorMessage"
    if ((printUsage != 0)); then
        print_usage
    fi
    exit $EXIT_CODE_EXCEPTION
}

# Handle Ctrl-C. We will stop execution and print the results that
# we gathered so far.
function handle_ctrl_c {
    local errorSource='handle_ctrl_c'

    echo ""
    echo "*** Stopping... ***"
    print_results
    exit_with_error "$errorSource" "Test run aborted by Ctrl+C."
}

# Register the Ctrl-C handler
trap handle_ctrl_c INT

function create_core_overlay {
    local errorSource='create_core_overlay'
    local printUsage=1

    if [ -n "$coreOverlayDir" ]; then
        export CORE_ROOT="$coreOverlayDir"

        if [ -n "$copyNativeTestBin" ]; then
            copy_test_native_bin_to_test_root $coreOverlayDir
        fi

        return
    fi

    # Check inputs to make sure we have enough information to create the core layout. $testRootDir/Tests/Core_Root should
    # already exist and contain test dependencies that are not built.
    local testDependenciesDir=$testRootDir/Tests/Core_Root
    if [ ! -d "$testDependenciesDir" ]; then
        exit_with_error "$errorSource" "Did not find the test dependencies directory: $testDependenciesDir"
    fi
    if [ -z "$coreClrBinDir" ]; then
        exit_with_error "$errorSource" "One of --coreOverlayDir or --coreClrBinDir must be specified." "$printUsage"
    fi
    if [ ! -d "$coreClrBinDir" ]; then
        exit_with_error "$errorSource" "Directory specified by --coreClrBinDir does not exist: $coreClrBinDir"
    fi

    # Create the overlay
    coreOverlayDir=$testRootDir/Tests/coreoverlay
    export CORE_ROOT="$coreOverlayDir"
    if [ -e "$coreOverlayDir" ]; then
        rm -f -r "$coreOverlayDir"
    fi
    mkdir "$coreOverlayDir"

    cp -f -v "$coreFxBinDir/"* "$coreOverlayDir/" 2>/dev/null
    cp -f -p -v "$coreClrBinDir/"* "$coreOverlayDir/" 2>/dev/null
    if [ -d "$mscorlibDir/bin" ]; then
        cp -f -v "$mscorlibDir/artifacts/"* "$coreOverlayDir/" 2>/dev/null
    fi
    cp -f -v "$testDependenciesDir/"xunit* "$coreOverlayDir/" 2>/dev/null
    cp -n -v "$testDependenciesDir/"* "$coreOverlayDir/" 2>/dev/null
    if [ -f "$coreOverlayDir/mscorlib.ni.dll" ]; then
        # Test dependencies come from a Windows build, and mscorlib.ni.dll would be the one from Windows
        rm -f "$coreOverlayDir/mscorlib.ni.dll"
    fi
    if [ -f "$coreOverlayDir/System.Private.CoreLib.ni.dll" ]; then
        # Test dependencies come from a Windows build, and System.Private.CoreLib.ni.dll would be the one from Windows
        rm -f "$coreOverlayDir/System.Private.CoreLib.ni.dll"
    fi
    copy_test_native_bin_to_test_root $coreOverlayDir
}

declare -a skipCrossGenFiles

function is_skip_crossgen_test {
    for skip in "${skipCrossGenFiles[@]}"; do
        if [[ "$1" == "$skip" ]]; then
            return 0
        fi
    done
    return 1
}

function precompile_overlay_assemblies {
    skipCrossGenFiles=($(read_array "$(dirname "$0")/skipCrossGenFiles.$ARCH.txt"))

    if [[ "$doCrossgen" == 1 ]]; then
        local overlayDir=$CORE_ROOT

        filesToPrecompile=$(find -L $overlayDir -iname \*.dll -not -iname \*.ni.dll -not -iname \*-ms-win-\* -type f )
        for fileToPrecompile in ${filesToPrecompile}
        do
            local filename=${fileToPrecompile}
            if [[ "$jitdisasm" == 1 ]]; then
                $overlayDir/corerun $overlayDir/jit-dasm.dll --crossgen $overlayDir/crossgen --platform $overlayDir --output $testRootDir/dasm $filename
                local exitCode=$?
                if [[ "$exitCode" != 0 ]]; then
                    echo Unable to generate dasm for $filename
                fi
            else
                if is_skip_crossgen_test "$(basename $filename)"; then
                    continue
                fi
                echo Precompiling $filename
                $overlayDir/crossgen /Platform_Assemblies_Paths $overlayDir $filename 1> $filename.stdout 2>$filename.stderr
                local exitCode=$?
                if [[ $exitCode != 0 ]]; then
                    if grep -q -e '0x80131018' $filename.stderr; then
                        printf "\n\t$filename is not a managed assembly.\n\n"
                    else
                        echo Unable to precompile $filename.
                        cat $filename.stdout
                        cat $filename.stderr
                        exit $exitCode
                    fi
                else
                    rm $filename.{stdout,stderr}
                fi
            fi
        done
    else
        echo Skipping crossgen of FX assemblies.
    fi
}

function copy_test_native_bin_to_test_root {
    local errorSource='copy_test_native_bin_to_test_root'
    local coreRootDir=$1

    if [ -z "$testNativeBinDir" ]; then
        exit_with_error "$errorSource" "--testNativeBinDir is required."
    fi

    # Copy native test components from the native test build into the respective test directory in the test root directory
    find "$testNativeBinDir" -type f -iname "*.$libExtension" |
        while IFS='' read -r filePath || [ -n "$filePath" ]; do
            local dirPath=$(dirname "$filePath")
            cp -f "$filePath" "$coreRootDir"
        done
}

# Variables for unsupported and failing tests
declare -a unsupportedTests
declare -a failingTests
declare -a excludedTests
declare -a playlistTests
((runFailingTestsOnly = 0))

# Get an array of items by reading the specified file line by line.
function read_array {
    local theArray=()

    if [ ! -f "$1" ]; then
        return
    fi

    # bash in Mac OS X doesn't support 'readarray', so using alternate way instead.
    # readarray -t theArray < "$1"
    # Any line that starts with '#' is ignored.
    while IFS='' read -r line || [ -n "$line" ]; do
        if [[ $line != "#"* ]]; then
            theArray[${#theArray[@]}]=$line
        fi
    done < "$1"
    echo ${theArray[@]}
}

function load_unsupported_tests {
    # Load the list of tests that are not supported on this platform. These tests are disabled (skipped) permanently.
    unsupportedTests=($(read_array "$(dirname "${BASH_SOURCE[0]}")/testsUnsupportedOutsideWindows.txt"))
    unsupportedTests+=($(read_array "$(dirname "${BASH_SOURCE[0]}")/testsUnsupported.$ARCH.txt"))
}

function load_failing_tests {
    # Load the list of tests that fail on this platform. These tests are disabled (skipped) temporarily, pending investigation.
    failingTests=($(read_array "$(dirname "${BASH_SOURCE[0]}")/testsFailingOutsideWindows.txt"))
    failingTests+=($(read_array "$(dirname "${BASH_SOURCE[0]}")/testsFailing.$ARCH.txt"))
}

function load_excluded_tests {
    # Read the exclusion file and populate the excludedTests array
    while IFS=, read -r dllPath reasonMessage; do
        # Extract the directory path from the dllPath and add it to the excludedTests array
        dirPath=$(dirname "$dllPath")
        excludedTests+=("$dirPath")
    done < "${CORE_ROOT}/TestExclusionList.txt"
}

function load_playlist_tests {
    # Load the list of tests that are enabled as a part of this test playlist.
    playlistTests=($(read_array "${playlistFile}"))
}

function is_unsupported_test {
    for unsupportedTest in "${unsupportedTests[@]}"; do
        if [[ "$1" == "$unsupportedTest" ]]; then
            return 0
        fi
    done
    return 1
}

function is_failing_test {
    for failingTest in "${failingTests[@]}"; do
        if [[ "$1" == "$failingTest" ]]; then
            return 0
        fi
    done
    return 1
}

function is_excluded_test {
    for excludedTest in "${excludedTests[@]}"; do
        if [[ "$1" == "$excludedTest"* ]]; then
            return 0
        fi
    done

    return 1
}

function is_playlist_test {
    for playlistTest in "${playlistTests[@]}"; do
        if [[ "$1" == "$playlistTest" ]]; then
            return 0
        fi
    done
    return 1
}

function skip_unsupported_test {
    # This function runs in a background process. It should not echo anything, and should not use global variables. This
    # function is analogous to run_test, and causes the test to be skipped with the message below.

    local scriptFilePath=$1
    local outputFilePath=$2

    echo "Not supported on this platform." >"$outputFilePath"
    return 2 # skip the test
}

function skip_failing_test {
    # This function runs in a background process. It should not echo anything, and should not use global variables. This
    # function is analogous to run_test, and causes the test to be skipped with the message below.

    local scriptFilePath=$1
    local outputFilePath=$2

    echo "Temporarily disabled on this platform due to unexpected failures." >"$outputFilePath"
    return 2 # skip the test
}

function skip_non_playlist_test {
    # This function runs in a background process. It should not echo anything, and should not use global variables. This
    # function is analogous to run_test, and causes the test to be skipped with the message below.

    local scriptFilePath=$1
    local outputFilePath=$2

    echo "Test is not included in the running playlist." >"$outputFilePath"
    return 2 # skip the test
}

function set_up_core_dump_generation {
    # We will only enable dump generation here if we're on Mac or Linux
    if [[ ! ( "$(uname -s)" == "Darwin" || "$(uname -s)" == "Linux" ) ]]; then
        return
    fi

    # We won't enable dump generation on OS X/macOS if the machine hasn't been
    # configured with the kern.corefile pattern we expect.
    if [[ ( "$(uname -s)" == "Darwin" && "$(sysctl -n kern.corefile)" != "core.%P" ) ]]; then
        echo "WARNING: Core dump generation not being enabled due to unexpected kern.corefile value."
        return
    fi

    # Allow dump generation
    ulimit -c unlimited

    if [[ "$(uname -s)" == "Linux" ]]; then
        if [ -e /proc/self/coredump_filter ]; then
            # Include memory in private and shared file-backed mappings in the dump.
            # This ensures that we can see disassembly from our shared libraries when
            # inspecting the contents of the dump. See 'man core' for details.
            echo 0x3F > /proc/self/coredump_filter
        fi
    fi
}

function print_info_from_core_file {

    #### temporary
    if [[ "$ARCH" == "arm64" ]]; then
        echo "Not inspecting core dumps on arm64 at the moment."
        return
    fi
    ####

    local core_file_name=$1
    local executable_name=$2

    if ! [ -e $executable_name ]; then
        echo "Unable to find executable $executable_name"
        return
    elif ! [ -e $core_file_name ]; then
        echo "Unable to find core file $core_file_name"
        return
    fi

    # Use LLDB to inspect the core dump on Mac, and GDB everywhere else.
    if [[ "$OSName" == "Darwin" ]]; then
        hash lldb 2>/dev/null || { echo >&2 "LLDB was not found. Unable to print core file."; return; }

        echo "Printing info from core file $core_file_name"
        lldb -c $core_file_name -b -o 'bt'
    else
        # Use GDB to print the backtrace from the core file.
        hash gdb 2>/dev/null || { echo >&2 "GDB was not found. Unable to print core file."; return; }

        echo "Printing info from core file $core_file_name"
        gdb --batch -ex "thread apply all bt full" -ex "quit" $executable_name $core_file_name
    fi
}

function inspect_and_delete_core_files {
    # This function prints some basic information from core files in the current
    # directory and deletes them immediately.

    # Depending on distro/configuration, the core files may either be named "core"
    # or "core.<PID>" by default. We will read /proc/sys/kernel/core_uses_pid to
    # determine which one it is.
    # On OS X/macOS, we checked the kern.corefile value before enabling core dump
    # generation, so we know it always includes the PID.
    local core_name_uses_pid=0
    if [[ (( -e /proc/sys/kernel/core_uses_pid ) && ( "1" == $(cat /proc/sys/kernel/core_uses_pid) ))
          || ( "$(uname -s)" == "Darwin" ) ]]; then
        core_name_uses_pid=1
    fi

    if [[ "$core_name_uses_pid" == "1" ]]; then
        # We don't know what the PID of the process was, so let's look at all core
        # files whose name matches core.NUMBER
        for f in core.*; do
            [[ $f =~ core.[0-9]+ ]] && print_info_from_core_file "$f" $CORE_ROOT/"corerun" && rm "$f"
        done
    elif [ -f core ]; then
        print_info_from_core_file "core" $CORE_ROOT/"corerun"
        rm "core"
    fi
}

function run_test {
    # This function runs in a background process. It should not echo anything, and should not use global variables.

    local scriptFilePath=$1
    local outputFilePath=$2

    # Switch to directory where the script is
    cd "$(dirname "$scriptFilePath")"

    local scriptFileName=$(basename "$scriptFilePath")
    local outputFileName=$(basename "$outputFilePath")

    if [[ "$limitedCoreDumps" == "ON" ]]; then
        set_up_core_dump_generation
    fi

    "./$scriptFileName" >"$outputFileName" 2>&1
    local testScriptExitCode=$?

    # We will try to print some information from generated core dumps if a debugger
    # is available, and possibly store a dump in a non-transient location.
    if [[ "$limitedCoreDumps" == "ON" ]]; then
        inspect_and_delete_core_files
    fi

    return $testScriptExitCode
}

# Get the number of processors available to the scheduler
platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
if [[ "$platform" == "freebsd" ]]; then
  NumProc="$(($(sysctl -n hw.ncpu)+1))"
elif [[ "$platform" == "netbsd" || "$platform" == "sunos" ]]; then
  NumProc="$(($(getconf NPROCESSORS_ONLN)+1))"
elif [[ "$platform" == "darwin" ]]; then
  NumProc="$(($(getconf _NPROCESSORS_ONLN)+1))"
elif command -v nproc > /dev/null 2>&1; then
  NumProc="$(nproc)"
elif (NAME=""; . /etc/os-release; test "$NAME" = "Tizen"); then
  NumProc="$(getconf _NPROCESSORS_ONLN)"
else
  NumProc=1
fi

((maxProcesses = $NumProc * 3 / 2)) # long tests delay process creation, use a few more processors

((processCount = 0))
declare -a scriptFilePaths
declare -a outputFilePaths
declare -a processIds
declare -a testStartTimes
waitProcessIndex=
pidNone=0

function waitany {
    local pid
    local exitcode
    while true; do
        for (( i=0; i<$maxProcesses; i++ )); do
            pid=${processIds[$i]}
            if [[ -z "$pid" || "$pid" == "$pidNone" ]]; then
                continue
            fi
            if ! kill -0 $pid 2>/dev/null; then
                wait $pid
                exitcode=$?
                waitProcessIndex=$i
                processIds[$i]=$pidNone
                return $exitcode
            fi
        done
        sleep 0.1
    done
}

function get_available_process_index {
    local pid
    local i=0
    for (( i=0; i<$maxProcesses; i++ )); do
        pid=${processIds[$i]}
        if [[ -z "$pid" || "$pid" == "$pidNone" ]]; then
            break
        fi
    done
    echo $i
}

function finish_test {
    waitany
    local testScriptExitCode=$?
    local finishedProcessIndex=$waitProcessIndex
    ((--processCount))

    local scriptFilePath=${scriptFilePaths[$finishedProcessIndex]}
    local outputFilePath=${outputFilePaths[$finishedProcessIndex]}
    local scriptFileName=$(basename "$scriptFilePath")

    local testEndTime=
    local testRunningTime=
    local header=

    if ((verbose == 1)); then
        header=$(printf "[%4d]" $countTotalTests)
    fi

    if [[ "$showTime" == "ON" ]]; then
        testEndTime=$(date +%s)
        testRunningTime=$(( $testEndTime - ${testStartTimes[$finishedProcessIndex]} ))
        header=$header$(printf "[%4ds]" $testRunningTime)
    fi

    local testResult
    case $testScriptExitCode in
        0)
            let countPassedTests++
            testResult='Pass'
            if ((verbose == 1 || runFailingTestsOnly == 1)); then
                echo "PASSED   - ${header}${scriptFilePath}"
            else
                echo "         - ${header}${scriptFilePath}"
            fi
            ;;
        2)
            let countSkippedTests++
            testResult='Skip'
            echo "SKIPPED  - ${header}${scriptFilePath}"
            ;;
        *)
            let countFailedTests++
            testResult='Fail'
            echo "FAILED   - ${header}${scriptFilePath}"
            ;;
    esac
    let countTotalTests++

    if ((verbose == 1 || testScriptExitCode != 0)); then
        while IFS='' read -r line || [ -n "$line" ]; do
            echo "               $line"
        done <"$outputFilePath"
    fi

    xunit_output_add_test "$scriptFilePath" "$outputFilePath" "$testResult" "$testScriptExitCode" "$testRunningTime"
    text_file_output_add_test "$scriptFilePath" "$testResult"
}

function finish_remaining_tests {
    # Finish the remaining tests in the order in which they were started
    while ((processCount > 0)); do
        finish_test
    done
}

function prep_test {
    local scriptFilePath=$1
    local scriptFileDir=$(dirname "$scriptFilePath")

    test "$verbose" = 1 && echo "Preparing $scriptFilePath"

    if [[ "$noLFConversion" != "ON" ]]; then
        # Convert DOS line endings to Unix if needed
        perl -pi -e 's/\r\n|\n|\r/\n/g' "$scriptFilePath"
    fi

    # Add executable file mode bit if needed
    chmod +x "$scriptFilePath"

    #remove any NI and Locks
    rm -f $scriptFileDir/*.ni.*
    rm -rf $scriptFileDir/lock
}

function start_test {
    local nextProcessIndex=$(get_available_process_index)
    local scriptFilePath=$1
    if ((runFailingTestsOnly == 1)) && ! is_failing_test "$scriptFilePath"; then
        return
    fi

    # Skip any test that's not in the current playlist, if a playlist was
    # given to us.
    if [ -n "$playlistFile" ] && ! is_playlist_test "$scriptFilePath"; then
        return
    fi

    if ((nextProcessIndex == maxProcesses)); then
        finish_test
        nextProcessIndex=$(get_available_process_index)
    fi

    scriptFilePaths[$nextProcessIndex]=$scriptFilePath
    local scriptFileName=$(basename "$scriptFilePath")
    local outputFilePath=$(dirname "$scriptFilePath")/${scriptFileName}.out
    outputFilePaths[$nextProcessIndex]=$outputFilePath

    if [[ "$showTime" == "ON" ]]; then
        testStartTimes[$nextProcessIndex]=$(date +%s)
    fi

    test "$verbose" = 1 && echo "Starting $scriptFilePath"
    if is_unsupported_test "$scriptFilePath"; then
        skip_unsupported_test "$scriptFilePath" "$outputFilePath" &
    elif ((runFailingTestsOnly == 0)) && is_failing_test "$scriptFilePath"; then
        skip_failing_test "$scriptFilePath" "$outputFilePath" &
    elif is_excluded_test "$scriptFilePath"; then
        skip_unsupported_test "$scriptFilePath" "$outputFilePath" &
    else
        run_test "$scriptFilePath" "$outputFilePath" &
    fi
    processIds[$nextProcessIndex]=$!

    ((++processCount))
}

# Get a list of directories in which to scan for tests by reading the
# specified file line by line.
function set_test_directories {
    local errorSource='set_test_directories'

    local listFileName=$1

    if [ ! -f "$listFileName" ]
    then
        exit_with_error "$errorSource" "Test directories file not found at $listFileName"
    fi
    testDirectories=($(read_array "$listFileName"))
}

function run_tests_in_directory {
    local testDir=$1

    # Recursively search through directories for .sh files to prepare them.
    # Note: This needs to occur before any test runs as some of the .sh files
    # depend on other .sh files
    for scriptFilePath in $(find "$testDir" -type f -iname '*.sh' | sort)
    do
        prep_test "${scriptFilePath:2}"
    done
    echo "The tests have been prepared"
    # Recursively search through directories for .sh files to run.
    for scriptFilePath in $(find "$testDir" -type f -iname '*.sh' | sort)
    do
        start_test "${scriptFilePath:2}"
    done
}

function coreclr_code_coverage {
    local coverageDir="$coverageOutputDir/Coverage"
    local toolsDir="$coverageOutputDir/Coverage/tools"
    local reportsDir="$coverageOutputDir/Coverage/reports"
    local packageName="unix-code-coverage-tools.1.0.0.nupkg"

    rm -rf $coverageDir
    mkdir -p $coverageDir
    mkdir -p $toolsDir
    mkdir -p $reportsDir
    pushd $toolsDir > /dev/null

    echo "Pulling down code coverage tools"
    wget -q https://www.myget.org/F/dotnet-buildtools/api/v2/package/unix-code-coverage-tools/1.0.0 -O $packageName
    echo "Unzipping to $toolsDir"
    unzip -q -o $packageName

    # Invoke gcovr
    chmod a+rwx ./gcovr
    chmod a+rwx ./$OSName/llvm-cov

    echo
    echo "Generating coreclr code coverage reports at $reportsDir/coreclr.html"
    echo "./gcovr $coreClrObjs --gcov-executable=$toolsDir/$OS/llvm-cov -r $coreClrSrc --html --html-details -o $reportsDir/coreclr.html"
    echo
    ./gcovr $coreClrObjs --gcov-executable=$toolsDir/$OSName/llvm-cov -r $coreClrSrc --html --html-details -o $reportsDir/coreclr.html
    exitCode=$?
    popd > /dev/null
    exit $exitCode
}

function check_cpu_architecture {
    local CPUName=$(uname -m)
    local __arch=

    case $CPUName in
        i686)
            __arch=x86
            ;;
        x86_64)
            __arch=x64
            ;;
        armv7l)
            __arch=arm
            ;;
        aarch64|arm64)
            __arch=arm64
            ;;
        loongarch64)
            __arch=loongarch64
            ;;
        riscv64)
            __arch=riscv64
            ;;
        *)
            echo "Unknown CPU $CPUName detected, configuring as if for x64"
            __arch=x64
            ;;
    esac

    echo "$__arch"
}

ARCH=$(check_cpu_architecture)
echo "Running on  CPU- $ARCH"

# Exit code constants
readonly EXIT_CODE_SUCCESS=0       # Script ran normally.
readonly EXIT_CODE_EXCEPTION=1     # Script exited because something exceptional happened (e.g. bad arguments, Ctrl-C interrupt).
readonly EXIT_CODE_TEST_FAILURE=2  # Script completed successfully, but one or more tests failed.

# Argument variables
testRootDir=
testNativeBinDir=
coreOverlayDir=
coreClrBinDir=
mscorlibDir=
coreFxBinDir=
coreClrObjs=
coreClrSrc=
coverageOutputDir=
testEnv=
playlistFile=
showTime=
noLFConversion=
buildOverlayOnly=
gcsimulator=
longgc=
limitedCoreDumps=
illinker=
((disableEventLogging = 0))
((serverGC = 0))

# Handle arguments
verbose=0
doCrossgen=0
jitdisasm=0
ilasmroundtrip=

for i in "$@"
do
    case $i in
        -h|--help)
            print_usage
            exit $EXIT_CODE_SUCCESS
            ;;
        -v|--verbose)
            verbose=1
            ;;
        --crossgen)
            doCrossgen=1
            ;;
        --jitstress=*)
            export DOTNET_JitStress=${i#*=}
            ;;
        --jitstressregs=*)
            export DOTNET_JitStressRegs=${i#*=}
            ;;
        --jitminopts)
            export DOTNET_JITMinOpts=1
            ;;
        --copyNativeTestBin)
            export copyNativeTestBin=1
            ;;
        --jitforcerelocs)
            export DOTNET_ForceRelocs=1
            ;;
        --link=*)
            export ILLINK=${i#*=}
            export DoLink=true
            ;;
        --tieredcompilation)
            export DOTNET_TieredCompilation=1
            ;;
        --jitdisasm)
            jitdisasm=1
            ;;
        --ilasmroundtrip)
            ((ilasmroundtrip = 1))
            ;;
        --testRootDir=*)
            testRootDir=${i#*=}
            ;;
        --testNativeBinDir=*)
            testNativeBinDir=${i#*=}
            ;;
        --coreOverlayDir=*)
            coreOverlayDir=${i#*=}
            ;;
        --coreClrBinDir=*)
            coreClrBinDir=${i#*=}
            ;;
        --mscorlibDir=*)
            mscorlibDir=${i#*=}
            ;;
        --coreFxBinDir=*)
            coreFxBinDir=${i#*=}
            ;;
        --testDir=*)
            testDirectories[${#testDirectories[@]}]=${i#*=}
            ;;
        --testDirFile=*)
            set_test_directories "${i#*=}"
            ;;
        --runFailingTestsOnly)
            ((runFailingTestsOnly = 1))
            ;;
        --disableEventLogging)
            ((disableEventLogging = 1))
            ;;
        --runcrossgentests)
            export RunCrossGen2=1
            ;;
        --jobs=*)
            maxProcesses=${i#*=}
            ;;
        --sequential)
            ((maxProcesses = 1))
            ;;
        --useServerGC)
            ((serverGC = 1))
            ;;
        --long-gc)
            ((longgc = 1))
            ;;
        --gcsimulator)
            ((gcsimulator = 1))
            ;;
        --playlist=*)
            playlistFile=${i#*=}
            ;;
        --coreclr-coverage)
            CoreClrCoverage=ON
            ;;
        --coreclr-objs=*)
            coreClrObjs=${i#*=}
            ;;
        --coreclr-src=*)
            coreClrSrc=${i#*=}
            ;;
        --coverage-output-dir=*)
            coverageOutputDir=${i#*=}
            ;;
        --test-env=*)
            testEnv=${i#*=}
            ;;
        --gcstresslevel=*)
            export DOTNET_GCStress=${i#*=}
            ;;
        --gcname=*)
            export DOTNET_GCName=${i#*=}
            ;;
        --show-time)
            showTime=ON
            ;;
        --no-lf-conversion)
            noLFConversion=ON
            ;;
        --build-overlay-only)
            buildOverlayOnly=ON
            ;;
        --limitedDumpGeneration)
            limitedCoreDumps=ON
            ;;
        --xunitOutputPath=*)
            xunitOutputPath=${i#*=}
            ;;
        *)
            echo "Unknown switch: $i"
            print_usage
            exit $EXIT_CODE_SUCCESS
            ;;
    esac
done

if [[ -n "$coreOverlayDir" && "$buildOverlayOnly" == "ON" ]]; then
    echo "Can not use \'--coreOverlayDir=<path>\' and \'--build-overlay-only\' at the same time."
    exit $EXIT_CODE_EXCEPTION
fi

if ((disableEventLogging == 0)); then
    export DOTNET_EnableEventLog=1
fi

export DOTNET_gcServer="$serverGC"

if [ -z "$testRootDir" ]; then
    echo "--testRootDir is required."
    print_usage
    exit $EXIT_CODE_EXCEPTION
fi
if [ ! -d "$testRootDir" ]; then
    echo "Directory specified by --testRootDir does not exist: $testRootDir"
    exit $EXIT_CODE_EXCEPTION
fi

# Copy native interop test libraries over to the mscorlib path in
# order for interop tests to run on linux.
if [[ -z "$mscorlibDir" ]]; then
    mscorlibDir=$coreClrBinDir
fi

if [[ -n "$longgc" ]]; then
    echo "Running Long GC tests"
    export RunningLongGCTests=1
fi

if [[ -n "$gcsimulator" ]]; then
    echo "Running GC simulator tests"
    export RunningGCSimulatorTests=1
fi

if [[ "$jitdisasm" -ne 0 ]]; then
    echo "Running jit disasm"
    export RunningJitDisasm=1
fi

if [[ -n "$ilasmroundtrip" ]]; then
    echo "Running Ilasm round trip"
    export RunningIlasmRoundTrip=1
fi

# If this is a coverage run, make sure the appropriate args have been passed
if [[ "$CoreClrCoverage" == "ON" ]]
then
    echo "Code coverage is enabled for this run"
    echo ""
    if [[ "$OSName" != "Darwin" && "$OSName" != "Linux" ]]
    then
        echo "Code Coverage not supported on $OS"
        exit 1
    fi

    if [ -z "$coreClrObjs" ]
    then
        echo "Coreclr obj files are required to generate code coverage reports"
        echo "Coreclr obj files root path can be passed using '--coreclr-obj' argument"
        exit 1
    fi

    if [ -z "$coreClrSrc" ]
    then
        echo "Coreclr src files are required to generate code coverage reports"
        echo "Coreclr src files root path can be passed using '--coreclr-src' argument"
        exit 1
    fi

    if [ -z "$coverageOutputDir" ]
    then
        echo "Output directory for coverage results must be specified"
        echo "Output path can be specified '--coverage-output-dir' argument"
        exit 1
    fi
fi

xunit_output_begin
text_file_output_begin
create_core_overlay
precompile_overlay_assemblies

if [[ "$buildOverlayOnly" == "ON" ]];
then
    echo "Build overlay directory '$coreOverlayDir' complete."
    exit 0
fi

if [ -n "$playlistFile" ]
then
    # Use a playlist file exclusively, if it was provided
    echo "Executing playlist $playlistFile"
    load_playlist_tests
else
    load_unsupported_tests
    load_failing_tests
    load_excluded_tests
fi

scriptPath=$(dirname $0)

export __TestEnv=$testEnv

cd "$testRootDir"


time_start=$(date +"%s")
if [ -z "$testDirectories" ]
then
    # No test directories were specified, so run everything in the current
    # directory and its subdirectories.
    run_tests_in_directory "."
else
    # Otherwise, run all the tests in each specified test directory.
    for testDir in "${testDirectories[@]}"
    do
        if [ ! -d "$testDir" ]; then
            echo "Test directory does not exist: $testDir"
        else
            run_tests_in_directory "./$testDir"
        fi
    done
fi
finish_remaining_tests

print_results

time_end=$(date +"%s")
time_diff=$(($time_end-$time_start))
echo "$(($time_diff / 60)) minutes and $(($time_diff % 60)) seconds taken to run CoreCLR tests."

xunit_output_end

if [[ "$CoreClrCoverage" == "ON" ]]
then
    coreclr_code_coverage
fi

if ((countFailedTests > 0)); then
    exit $EXIT_CODE_TEST_FAILURE
fi

exit $EXIT_CODE_SUCCESS
