<#

.SYNOPSIS
This script runs a google test executable and collects logs or dumps
as necessary.

.PARAMETER Path
    The path to the test executable.

.PARAMETER Kernel
    Runs for Windows kernel mode, given the path for binaries.

.PARAMETER Filter
    A filter to include test cases from the list to execute. Multiple filters
    are separated by :. Negative filters are prefixed with -.

.PARAMETER ListTestCases
    Lists all the test cases.

.PARAMETER IsolationMode
    Controls the isolation mode when running each test case.

.PARAMETER KeepOutputOnSuccess
    Don't discard console output or logs on success.

.PARAMETER GenerateXmlResults
    Generates an xml Test report for the run.

.PARAMETER Debugger
    Attaches the debugger to the process.

.PARAMETER InitialBreak
    Debugger starts broken into the process to allow setting breakpoints, etc.

.PARAMETER BreakOnFailure
    Triggers a break point on a test failure.

.PARAMETER LogProfile
    The name of the profile to use for log collection.

.PARAMETER CompressOutput
    Compresses the output files generated for failed test cases.

.PARAMETER NoProgress
    Disables the progress bar.

.Parameter EnableAppVerifier
    Enables all basic Application Verifier checks on the test binary.

.Parameter EnableSystemVerifier
    Enables TCPIP verifier in user mode tests.

.Parameter CodeCoverage
    Collects code coverage for this test run. Incompatible with -Debugger.

.Parameter AZP
    Runs in Azure Pipelines mode.

.Parameter ErrorsAsWarnings
    Treats all errors as warnings.

.PARAMETER DuoNic
    Uses DuoNic instead of loopback.

#>

param (
    [Parameter(Mandatory = $true)]
    [string]$Path,

    [Parameter(Mandatory = $false)]
    [string]$Kernel = "",

    [Parameter(Mandatory = $false)]
    [string]$Filter = "",

    [Parameter(Mandatory = $false)]
    [switch]$ListTestCases = $false,

    [Parameter(Mandatory = $false)]
    [ValidateSet("Batch", "Isolated")]
    [string]$IsolationMode = "Isolated",

    [Parameter(Mandatory = $false)]
    [switch]$KeepOutputOnSuccess = $false,

    [Parameter(Mandatory = $false)]
    [switch]$GenerateXmlResults = $false,

    [Parameter(Mandatory = $false)]
    [switch]$Debugger = $false,

    [Parameter(Mandatory = $false)]
    [switch]$InitialBreak = $false,

    [Parameter(Mandatory = $false)]
    [switch]$BreakOnFailure = $false,

    [Parameter(Mandatory = $false)]
    [ValidateSet("None", "Basic.Light", "Datapath.Light", "Datapath.Verbose", "Stacks.Light", "Stacks.Verbose", "RPS.Light", "RPS.Verbose", "Performance.Light", "Basic.Verbose", "Performance.Light", "Performance.Verbose", "Full.Light", "Full.Verbose", "SpinQuic.Light", "SpinQuicWarnings.Light")]
    [string]$LogProfile = "None",

    [Parameter(Mandatory = $false)]
    [switch]$CompressOutput = $false,

    [Parameter(Mandatory = $false)]
    [switch]$NoProgress = $false,

    [Parameter(Mandatory = $false)]
    [switch]$EnableAppVerifier = $false,

    [Parameter(Mandatory = $false)]
    [switch]$EnableSystemVerifier = $false,

    [Parameter(Mandatory = $false)]
    [switch]$CodeCoverage = $false,

    [Parameter(Mandatory = $false)]
    [String]$PfxPath = "",

    [Parameter(Mandatory = $false)]
    [switch]$AZP = $false,

    [Parameter(Mandatory = $false)]
    [switch]$GHA = $false,

    [Parameter(Mandatory = $false)]
    [switch]$ErrorsAsWarnings = $false,

    [Parameter(Mandatory = $false)]
    [string]$ExtraArtifactDir = "",

    [Parameter(Mandatory = $false)]
    [switch]$DuoNic = $false,

    [Parameter(Mandatory = $false)]
    [string]$OsRunner = "",

    [Parameter(Mandatory = $false)]
    [switch]$UseQtip = $false
)

Set-StrictMode -Version 'Latest'
$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'

function Test-Administrator
{
    $user = [Security.Principal.WindowsIdentity]::GetCurrent();
    (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}

function Log($msg) {
    Write-Host "[$(Get-Date)] $msg"
}

function LogWrn($msg) {
    if ($AZP -and !$ErrorsAsWarnings) {
        Write-Host "##vso[task.LogIssue type=warning;][$(Get-Date)] $msg"
    } elseif ($GHA -and !$ErrorsAsWarnings) {
        Write-Host "::warning::[$(Get-Date)] $msg"
    } else {
        Write-Warning "[$(Get-Date)] $msg"
    }
}

function LogErr($msg) {
    if ($AZP -and !$ErrorsAsWarnings) {
        Write-Host "##vso[task.LogIssue type=error;][$(Get-Date)] $msg"
    } elseif ($GHA -and !$ErrorsAsWarnings) {
        Write-Host "::error::[$(Get-Date)] $msg"
    } else {
        Write-Warning "[$(Get-Date)] $msg"
    }
}

function LogFatal($msg) {
    if ($AZP -and !$ErrorsAsWarnings) {
        Write-Error "##vso[task.LogIssue type=error;][$(Get-Date)] $msg"
    } elseif ($GHA -and !$ErrorsAsWarnings) {
        Write-Error "::error::[$(Get-Date)] $msg"
    } else {
        Write-Error "[$(Get-Date)] $msg"
    }
}

# Make sure the test executable is present.
if (!(Test-Path $Path)) {
    Write-Error "$($Path) does not exist!"
}

# Validate the the kernel switch.
if ($Kernel -ne "" -and !$IsWindows) {
    Write-Error "-Kernel switch only supported on Windows";
}

# Validate the code coverage switch
if ($CodeCoverage) {
    if (!$IsWindows) {
        Write-Error "-CodeCoverage switch only supported on Windows";
    }
    if ($Debugger) {
        Write-Error "-CodeCoverage switch is not supported with debugging";
    }
    if ($Kernel -ne "") {
        Write-Error "-CodeCoverage is not supported for kernel mode tests";
    }
    if (!(Test-Path "C:\Program Files\OpenCppCoverage\OpenCppCoverage.exe")) {
        Write-Error "Code coverage tools are not installed";
    }
}

# Root directory of the project.
$RootDir = Split-Path $PSScriptRoot -Parent

# Script for controlling loggings.
$LogScript = Join-Path $RootDir "scripts" "log.ps1"

# Executable name.
$TestExeName = Split-Path $Path -Leaf
$CoverageName = "$(Split-Path $Path -LeafBase).cov"

$ExeLogFolder = $TestExeName
if (![string]::IsNullOrWhiteSpace($ExtraArtifactDir)) {
    $ExeLogFolder += "_$ExtraArtifactDir"
}

# Folder for log files.
$LogDir = Join-Path $RootDir "artifacts" "logs" $ExeLogFolder (Get-Date -UFormat "%m.%d.%Y.%T").Replace(':','.')
New-Item -Path $LogDir -ItemType Directory -Force | Out-Null

# Folder for coverage files
$CoverageDir = $null
if ($CodeCoverage) {
    $CoverageDir = Join-Path $RootDir "artifacts" "coverage"
    New-Item -Path $CoverageDir -ItemType Directory -Force | Out-Null
}

# The file path of the final XML results.
$FinalResultsPath = "$($LogDir)-results.xml"

# Base XML results data.
$XmlResults = [xml]@"
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="0" failures="0" retried="0" disabled="0" errors="0" time="0" timestamp="date" name="AllTests">
</testsuites>
"@
$XmlResults.testsuites.timestamp = Get-Date -UFormat "%Y-%m-%dT%T"

# XML for creating new (failure) result data.
$FailXmlText = @"
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="1" failures="1" retried="0" disabled="0" errors="0" time="0" name="AllTests">
  <testsuite name="TestSuiteName" tests="1" failures="1" disabled="0" errors="0" timestamp="date" time="0" >
    <testcase name="TestCaseName" status="run" result="completed" time="0" timestamp="date" classname="TestSuiteName">
      <failure message="Application Crashed" type=""><![CDATA[Application Crashed]]></failure>
    </testcase>
  </testsuite>
</testsuites>
"@

# Global state for tracking if any crashes occurred.
$global:CrashedProcessCount = 0

# Path to the WER registry key used for collecting dumps.
$WerDumpRegPath = "HKLM:\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\$TestExeName"

# Helper script to build up a combined XML file for all test cases. This
# function just appends the given test case's xml output to the existing xml
# file. If not present (because of a crash) it generates one instead and appends
# it.
function Add-XmlResults($TestCase) {
    $TestHasResults = Test-Path $TestCase.ResultsPath
    $TestSuiteName = $TestCase.Name.Split(".")[0]
    $TestCaseName = $TestCase.Name.Split(".")[1]

    $NewXmlResults = $null
    if ($TestHasResults) {
        # Get the results from the test output.
        $NewXmlResults = [xml](Get-Content $TestCase.ResultsPath)
        Remove-Item $TestCase.ResultsPath -Force | Out-Null
    } else {
        # Generate our own results xml.
        $NewXmlText = $FailXmlText.Replace("TestSuiteName", $TestSuiteName)
        $NewXmlText = $NewXmlText.Replace("TestCaseName", $TestCaseName)
        $NewXmlText = $NewXmlText.Replace("date", $TestCase.Timestamp)
        $NewXmlResults = [xml]($NewXmlText)
    }

    $IsFailure = $NewXmlResults.testsuites.failures -eq 1
    $IsRetry = $false
    $Time = $NewXmlResults.testsuites.testsuite.testcase.time -as [Decimal]

    $Node = $null
    if ($XmlResults.testsuites.tests -ne 0) {
        # Look for a matching test suite that might already exist.
        $Node = $XmlResults.testsuites.testsuite | Where-Object { $_.Name -eq $TestSuiteName }
    }
    if ($null -ne $Node) {
        $IsRetry = $Node.testcase | Where-Object { $_.Name -eq $TestCaseName }
        # Already has a matching test suite. Add the test case to it.
        $Node.tests = ($Node.tests -as [Int]) + 1
        if ($IsFailure) {
            $Node.failures = ($Node.failures -as [Int]) + 1
        }
        $Node.time = ($Node.time -as [Decimal]) + $Time
        $NewNode = $XmlResults.ImportNode($NewXmlResults.testsuites.testsuite.testcase, $true)
        $Node.AppendChild($NewNode) | Out-Null
    } else {
        # First instance of this test suite. Add the test suite.
        $NewNode = $XmlResults.ImportNode($NewXmlResults.testsuites.testsuite, $true)
        $XmlResults.testsuites.AppendChild($NewNode) | Out-Null
    }

    # Update the top level test and failure counts.
    $XmlResults.testsuites.tests = ($XmlResults.testsuites.tests -as [Int]) + 1
    if ($IsRetry) {
        $XmlResults.testsuites.retried = ($XmlResults.testsuites.retried -as [Int]) + 1
    }
    if ($IsFailure) {
        $XmlResults.testsuites.failures = ($XmlResults.testsuites.failures -as [Int]) + 1
    }
    $XmlResults.testsuites.time = ($XmlResults.testsuites.time -as [Decimal]) + $Time
}

# Asynchronously starts the test executable with the given arguments.
function Start-TestExecutable([String]$Arguments, [String]$OutputDir) {
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    if ($IsWindows) {
        if ($Debugger) {
            if (Get-Command "windbgx.exe" -ErrorAction SilentlyContinue) {
                $pinfo.FileName = "windbgx.exe"
            } else {
                $pinfo.FileName = "windbg.exe"
            }
            if ($InitialBreak) {
                $pinfo.Arguments = "-G $($Path) $($Arguments)"
            } else {
                $pinfo.Arguments = "-g -G $($Path) $($Arguments)"
            }
        } elseif ($CodeCoverage) {
            $CoverageOutput = Join-Path $OutputDir $CoverageName
            $pinfo.FileName = "C:\Program Files\OpenCppCoverage\OpenCppCoverage.exe"
            $pinfo.Arguments = "--modules=$(Split-Path $Path -Parent) --cover_children --sources src\core --excluded_sources unittest --working_dir $($OutputDir) --export_type binary:$($CoverageOutput) -- $($Path) $($Arguments)"
            $pinfo.WorkingDirectory = $OutputDir
        } else {
            $pinfo.FileName = $Path
            $pinfo.Arguments = $Arguments
            $pinfo.WorkingDirectory = $OutputDir
            if (Test-Administrator) {
                # Enable WER dump collection.
                New-ItemProperty -Path $WerDumpRegPath -Name DumpType -PropertyType DWord -Value 2 -Force | Out-Null
                New-ItemProperty -Path $WerDumpRegPath -Name DumpFolder -PropertyType ExpandString -Value $OutputDir -Force | Out-Null
            }
        }
    } else {
        if ($Debugger) {
            $pinfo.FileName = "gdb"
            if ($InitialBreak) {
                $pinfo.Arguments = "--args $($Path) $($Arguments)"
            } else {
                $pinfo.Arguments = "-ex=r --args $($Path) $($Arguments)"
            }
        } else {
            $pinfo.FileName = "bash"
            $pinfo.Arguments = "-c `"ulimit -c unlimited && LSAN_OPTIONS=report_objects=1 ASAN_OPTIONS=disable_coredump=0:abort_on_error=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 $($Path) $($Arguments) && echo Done`""
            $pinfo.WorkingDirectory = $OutputDir
        }
    }
    if (!$Debugger) {
        $pinfo.RedirectStandardOutput = $true
        $pinfo.RedirectStandardError = $true
    }
    $pinfo.UseShellExecute = $false
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p
}

# Asynchronously starts a single msquictest test case running.
function Start-TestCase([String]$Name, [int]$Trial = 1) {

    # Get a string of invalid chars for filenames
    $InvalidChars = [System.IO.Path]::GetInvalidFileNameChars() -join ''
    if (!$IsWindows) {
        # Add characters that Azure disallows in filenames, but aren't invalid on POSIX
        $InvalidChars = $InvalidChars,'"', ":", "<", ">", "|", "*", "?", "`r", "`n" -join ''
    }
    # Escape those chars for use in a regex and put them inside a regex set (the square brackets)
    $InvalidCharsToReplace = "[{0}]" -f [RegEx]::Escape($InvalidChars)
    $InstanceName = $Name -replace $InvalidCharsToReplace, "_"
    $LocalLogDir = Join-Path $LogDir ($InstanceName + "_$Trial")
    mkdir $LocalLogDir | Out-Null

    if ($LogProfile -ne "None") {
        # Start the logs
        & $LogScript -Start -Profile $LogProfile | Out-Null
    }

    # Build up the argument list.
    $ResultsPath = Join-Path $LocalLogDir "results.xml"
    $Arguments = "--gtest_catch_exceptions=0 --gtest_filter=$($Name) --gtest_output=xml:$($ResultsPath) --timeout 60000"
    if ($BreakOnFailure) {
        $Arguments += " --gtest_break_on_failure"
    }
    if ($Kernel -ne "") {
        $Arguments += " --kernelPriv"
    }
    if ($DuoNic) {
        $Arguments += " --duoNic"
    }
    if ($UseQtip) {
        $Arguments += " --useQTIP"
    }
    if ("" -ne $OsRunner) {
        $Arguments += " --osRunner=$OsRunner"
    }
    if ($PfxPath -ne "") {
        $Arguments += " -PfxPath:$PfxPath"
    }

    # Start the test process and return some information about the test case.
    [pscustomobject]@{
        Name = $Name
        InstanceName = $InstanceName
        LogDir = $LocalLogDir
        StartTime = Get-Date
        Timestamp = (Get-Date -UFormat "%Y-%m-%dT%T")
        ResultsPath = $ResultsPath
        Process = (Start-TestExecutable $Arguments $LocalLogDir)
    }
}

# Asynchronously start all the msquictest test cases running.
function Start-AllTestCases {

    $Name = "all"
    $InstanceName = $Name

    if ($LogProfile -ne "None") {
        # Start the logs
        & $LogScript -Start -Profile $LogProfile | Out-Null
    }

    # Build up the argument list.
    $Arguments = "--gtest_catch_exceptions=0 --gtest_output=xml:$($FinalResultsPath)"
    if ($Filter -ne "") {
        $Arguments += " --gtest_filter=$($Filter)"
    }
    if ($BreakOnFailure) {
        $Arguments += " --gtest_break_on_failure"
    }
    if ($Kernel -ne "") {
        $Arguments += " --kernelPriv"
    }
    if ($DuoNic) {
        $Arguments += " --duoNic"
    }
    if ($UseQtip) {
        $Arguments += " --useQTIP"
    }
    if ("" -ne $OsRunner) {
        $Arguments += " --osRunner=$OsRunner"
    }
    if ($PfxPath -ne "") {
        $Arguments += " -PfxPath:$PfxPath"
    }
    # Start the test process and return some information about the test case.
    [pscustomobject]@{
        Name = $Name
        InstanceName = $InstanceName
        LogDir = $LogDir
        StartTime = Get-Date
        Timestamp = (Get-Date -UFormat "%Y-%m-%dT%T")
        ResultsPath = $FinalResultsPath
        Process = (Start-TestExecutable $Arguments $LogDir)
    }
}

# Uses CDB.exe to print the crashing callstack in the dump file.
function PrintDumpCallStack($DumpFile) {
    $env:_NT_SYMBOL_PATH = Split-Path $Path
    try {
        if ($env:BUILD_BUILDNUMBER -ne $null) {
            $env:PATH += ";c:\Program Files (x86)\Windows Kits\10\Debuggers\x64"
        }
        $Output = cdb.exe -z $File -c "kn;q" | Join-String -Separator "`n"
        $Output = ($Output | Select-String -Pattern " # Child-SP(?s).*quit:").Matches[0].Groups[0].Value
        Write-Host "=================================================================================="
        Write-Host " $(Split-Path $DumpFile -Leaf)"
        Write-Host "=================================================================================="
        $Output -replace "quit:", "=================================================================================="
        $Output | Out-File "$DumpFile.txt"
    } catch {
        # Silently fail
    }
}

function PrintLldbCoreCallStack($CoreFile) {
    try {
        $Output = lldb $Path -c $CoreFile -b -o "`"bt all`""
        Write-Host "=================================================================================="
        Write-Host " $(Split-Path $CoreFile -Leaf)"
        Write-Host "=================================================================================="
        # Find line containing Current thread
        $Found = $false
        $LastThreadStart = 0
        for ($i = 0; $i -lt $Output.Length; $i++) {
            if ($Output[$i] -like "*stop reason =*") {
                if ($Found) {
                    break
                }
                $LastThreadStart = $i
            }
            if ($Output[$i] -like "*quic_bugcheck*") {
                $Found = $true
                for ($j = $LastThreadStart; $j -lt $i; $j++) {
                    $Output[$j]
                }
            }
            if ($Found) {
                $Output[$i]
            }
        }
        if (!$Found) {
            $Output | Join-String -Separator "`n"
        }
        $Output | Join-String -Separator "`n" | Out-File "$CoreFile.txt"
    } catch {
        # Silently Fail
    }
}

function PrintGdbCoreCallStack($CoreFile) {
    try {
        $Output = gdb $Path $CoreFile -batch -ex "`"bt`"" -ex "`"quit`""
        Write-Host "=================================================================================="
        Write-Host " $(Split-Path $CoreFile -Leaf)"
        Write-Host "=================================================================================="
        # Find line containing Current thread
        $Found = $false
        for ($i = 0; $i -lt $Output.Length; $i++) {
            if ($Output[$i] -like "*Current thread*") {
                $Found = $true
            }
            if ($Found) {
                $Output[$i]
            }
        }
        if (!$Found) {
            $Output | Join-String -Separator "`n"
        }
        $Output | Join-String -Separator "`n" | Out-File "$CoreFile.txt"
    } catch {
        # Silently Fail
    }
}

# Waits for the executable to finish and processes the results.
function Wait-TestCase($TestCase) {
    $ProcessCrashed = $false
    $AnyTestFailed = $false
    $StdOut = $null
    $StdOutTxt = $null
    $StdError = $null
    $StdErrorTxt = $null
    $IsReadingStreams = $false

    try {
        if (!$Debugger) {
            $IsReadingStreams = $true
            $StdOut = $TestCase.Process.StandardOutput.ReadToEndAsync()
            $StdError = $TestCase.Process.StandardError.ReadToEndAsync()
        }
        $TestCase.Process.WaitForExit()
        if ($TestCase.Process.ExitCode -ne 0) {
            Log "Process had nonzero exit code: $($TestCase.Process.ExitCode)"
            $ProcessCrashed = $true
        }
        if ($IsReadingStreams) {
            [System.Threading.Tasks.Task]::WaitAll(@($StdOut, $StdError))
            $StdOutTxt = $StdOut.Result
            $StdErrorTxt = $StdError.Result

            if (!$IsWindows -and !$ProcessCrashed) {
                $ProcessCrashed = $StdErrorTxt.Contains("Aborted")
            }
            $AnyTestFailed = $StdOutTxt.Contains("[  FAILED  ]")
            if (!(Test-Path $TestCase.ResultsPath) -and !$ProcessCrashed) {
                LogWrn "No test results generated! Treating as crash!"
                $ProcessCrashed = $true
            }
        }
        $DumpFiles = (Get-ChildItem $TestCase.LogDir) | Where-Object { $_.Extension -eq ".dmp" }
        if ($DumpFiles) {
            LogWrn "Dump file(s) generated"
            foreach ($File in $DumpFiles) {
                PrintDumpCallStack($File)
            }
            $ProcessCrashed = $true
        }
        $CoreFiles = (Get-ChildItem $TestCase.LogDir) | Where-Object { $_.Extension -eq ".core" }
        if ($CoreFiles) {
            LogWrn "Core file(s) generated"
            foreach ($File in $CoreFiles) {
                if ($IsMacOS) {
                    PrintLldbCoreCallStack $File
                } else {
                    PrintGdbCoreCallStack $File
                }
            }
            $ProcessCrashed = $true
        }
    } catch {
        LogWrn "Treating exception as crash!"
        $ProcessCrashed = $true
        throw
    } finally {
        # Add the current test case results.
        if ($IsolationMode -ne "Batch") {
            try { Add-XmlResults $TestCase } catch { }
        }

        if ($CodeCoverage) {
            $NewCoverage = Join-Path $TestCase.LogDir $Coveragename
            if ($IsolationMode -eq "Isolated") {
                # Merge coverage with previous runs
                $PreviousCoverage = Join-Path $CoverageDir $CoverageName
                if (!(Test-Path $PreviousCoverage)) {
                    # No previous coverage data, just copy
                    Copy-Item $NewCoverage $CoverageDir
                } else {
                    # Merge new coverage data with existing coverage data
                    # On a developer machine, this will always merge coverage until the dev deletes old coverage.
                    $TempMergedCoverage = Join-Path $CoverageDir "mergetemp.cov"
                    $CoverageExe = 'C:\"Program Files"\OpenCppCoverage\OpenCppCoverage.exe'
                    $CoverageMergeParams = " --input_coverage $($PreviousCoverage) --input_coverage $($NewCoverage) --export_type binary:$($TempMergedCoverage)"
                    Invoke-Expression ($CoverageExe + $CoverageMergeParams) | Out-Null
                    Move-Item $TempMergedCoverage $PreviousCoverage -Force
                }
            } else {
                # Copy the coverage to destination
                Copy-Item $NewCoverage $CoverageDir -Force
                # Copy coverage log
                $LogName = "LastCoverageResults-$(Split-Path $Path -LeafBase).log"
                Copy-Item (Join-Path $TestCase.LogDir "LastCoverageResults.log") (Join-Path $CoverageDir $LogName) -Force
            }
        }

        if ($ProcessCrashed) {
            $global:CrashedProcessCount++
        }

        if ($IsolationMode -eq "Batch") {
            if ($StdOutTxt) { Write-Host $StdOutTxt }
            if ($StdErrorTxt) { Write-Host $StdErrorTxt }
        } else {
            $Delta = (Get-Date) - $TestCase.StartTime
            if ($AnyTestFailed -or $ProcessCrashed) {
                LogErr "$($TestCase.Name) failed (in $($Delta.TotalSeconds) sec):"
                if ($StdOutTxt) { Write-Host $StdOutTxt }
                if ($StdErrorTxt) { Write-Host $StdErrorTxt }
            } else {
                Log "$($TestCase.Name) succeeded (in $($Delta.TotalSeconds) sec)"
            }
        }

        if ($KeepOutputOnSuccess -or $ProcessCrashed -or $AnyTestFailed) {

            if ($LogProfile -ne "None") {
                & $LogScript -Stop -OutputPath (Join-Path $TestCase.LogDir "quic")
            }

            if ($StdOutTxt) {
                $StdOutTxt > (Join-Path $TestCase.LogDir "stdout.txt")
            }

            if ($StdErrorTxt) {
                $StdErrorTxt > (Join-Path $TestCase.LogDir "stderr.txt")
            }

            if ($CompressOutput) {
                # Zip the output.
                CompressOutput-Archive -Path "$($TestCase.LogDir)\*" -DestinationPath "$($TestCase.LogDir).zip" | Out-Null
                Remove-Item $TestCase.LogDir -Recurse -Force | Out-Null
            }

        } else {
            if ($LogProfile -ne "None") {
                & $LogScript -Cancel | Out-Null
            }
            Remove-Item $TestCase.LogDir -Recurse -Force | Out-Null
        }
    }

    return $AnyTestFailed
}

# Runs the test executable to query all available test cases, parses the console
# output and returns a list of test case names.
function GetTestCases {
    $Arguments = " --gtest_list_tests"
    if ($Filter -ne "") {
        $Arguments = " --gtest_filter=$Filter --gtest_list_tests"
    }
    $stdout = Invoke-Expression ($Path + $Arguments)

    $Tests = New-Object System.Collections.ArrayList
    if ($null -ne $stdout) {
        $Lines = ($stdout.Split([Environment]::NewLine)) | Where-Object { $_.Length -ne 0 }
        $CurTestGroup = $null
        for ($i = 0; $i -lt $Lines.Length; $i++) {
            if (!($Lines[$i].StartsWith(" "))) {
                $CurTestGroup = $Lines[$i]
            } else {
                $Tests.Add($CurTestGroup + $Lines[$i].Split("#")[0].Trim()) | Out-Null
            }
        }
    }
    $Tests.ToArray()
}

function Get-WindowsKitTool {
    param (
        [string]$Arch = "x86",
        [Parameter(Mandatory = $true)]
        [string]$Tool
    )

    $KitBinRoot = "C:\Program Files (x86)\Windows Kits\10\bin"
    if (!(Test-Path $KitBinRoot)) {
        Write-Error "Windows Kit Binary Folder not Found"
        return ""
    }

    $FoundToolPath = $null
    $FoundToolVersion = "0"

    $Subfolders = Get-ChildItem -Path $KitBinRoot -Directory
    foreach ($Subfolder in $Subfolders) {
        $ToolPath = Join-Path $Subfolder "$Arch\$Tool"
        if (Test-Path $ToolPath) {
            $KitVersion = $Subfolder.Name

            if ($KitVersion -gt $FoundToolVersion) {
                $FoundToolVersion = $KitVersion
                $FoundToolPath = $ToolPath
            }
        }
    }

    if ($null -ne $FoundToolPath) {
        return $FoundToolPath
    }
    Write-Error "Failed to find tool"
    return $null
}

##############################################################
#                     Main Execution                         #
##############################################################

# Query all the test cases.
$TestCases = GetTestCases
if ($null -eq $TestCases) {
    Log "$Path (Skipped)"
    exit
}

$TestCount = ($TestCases -as [String[]]).Length

Log "$Path ($TestCount test case(s))"

if ($ListTestCases) {
    # List the tst cases.
    $TestCases
    exit
}

# Cancel any outstanding logs that might be leftover.
& $LogScript -Cancel | Out-Null

# Initialize WER dump registry key if necessary.
if ($IsWindows -and !(Test-Path $WerDumpRegPath) -and (Test-Administrator)) {
    New-Item -Path $WerDumpRegPath -Force | Out-Null
}

# Initialize application verifier (Windows only).
if ($IsWindows -and $EnableAppVerifier) {
    where.exe appverif.exe
    if ($LastExitCode -eq 0) {
        appverif.exe /verify $Path
    } else {
        Write-Warning "Application Verifier not installed!"
        $EnableAppVerifier = $false;
    }
}

$DriverPath = (Split-Path $Path -Parent)

# Install the kernel mode drivers.
if ($Kernel -ne "") {
    if ($null -ne (Get-Service -Name "msquicpriv" -ErrorAction Ignore)) {
        try { net.exe stop msquicpriv /y | Out-Null } catch {}
        sc.exe delete msquicpriv /y | Out-Null
    }
    if ($null -ne (Get-Service -Name "msquictestpriv" -ErrorAction Ignore)) {
        try { net.exe stop msquictestpriv /y | Out-Null } catch {}
        sc.exe delete msquictestpriv /y | Out-Null
    }
    Copy-Item (Join-Path $Kernel "msquictestpriv.sys") $DriverPath -Force
    Copy-Item (Join-Path $Kernel "msquicpriv.sys") $DriverPath -Force

    Log "Creating msquicpriv service"
    sc.exe create "msquicpriv" type= kernel binpath= (Join-Path $DriverPath msquicpriv.sys) start= demand | Out-Null
    if ($LastExitCode) {
        Log ("sc.exe " + $LastExitCode)
    }
    if ($EnableSystemVerifier) {
        verifier.exe /volatile /adddriver msquicpriv.sys msquictestpriv.sys /flags 0x9BB
        if ($LastExitCode) {
            Log ("verifier.exe " + $LastExitCode)
        }
    }

    Log "Starting msquicpriv service"
    sc.exe start msquicpriv
    if ($LastExitCode) {
        Log ("net.exe " + $LastExitCode)
    }

    try {
        if ("Running" -ne (Get-Service -Name msquicpriv).Status) {
            LogFatal "msquicpriv isn't running"
        }
    } catch {
        LogFatal "msquicpriv query failed"
    }
}

if ($IsWindows -and $EnableSystemVerifier) {
    verifier.exe /volatile /adddriver afd.sys netio.sys tcpip.sys /flags 0x9BB
    if ($LastExitCode) {
        Log ("verifier.exe " + $LastExitCode)
    }
}

try {
    if ($IsolationMode -eq "Batch") {
        # Run the the test process once for all tests.
        Wait-TestCase (Start-AllTestCases)
    } else {
        # Run the test cases individually.
        for ($i = 0; $i -lt $TestCount; $i++) {
            $AnyTestFailed = Wait-TestCase (Start-TestCase ($TestCases -as [String[]])[$i] 1)
            if ($AnyTestFailed) {
                Wait-TestCase (Start-TestCase ($TestCases -as [String[]])[$i] 2)
            }
            if (!$NoProgress) {
                Write-Progress -Activity "Running tests" -Status "Progress:" -PercentComplete ($i/$TestCount*100)
            }
        }
    }
} catch {
    Log "Exception Thrown"
    Log $_
    Get-Error
    $_ | Format-List *
} finally {
    if ($LogProfile -ne "None") {
        & $LogScript -Cancel | Out-Null
    }

    if ($IsWindows) {
        # Cleanup the WER registry.
        if (Test-Administrator) {
            Remove-Item -Path $WerDumpRegPath -Force | Out-Null
        }
        # Turn off App Verifier
        if ($EnableAppVerifier) {
            appverif.exe -disable * -for $Path
        }
    }

    if ($IsolationMode -eq "Batch") {
        if (Test-Path $FinalResultsPath) {
            $XmlResults = [xml](Get-Content $FinalResultsPath)
            if (!$GenerateXmlResults) {
                # Delete the XML results file since it's not needed.
                Remove-Item $FinalResultsPath -Force | Out-Null
            }
        } else {
            # No results file means the tests crashed most likely.
            $NewXmlText = $FailXmlText.Replace("TestSuiteName", "all")
            $NewXmlText = $NewXmlText.Replace("TestCaseName", "all")
            $NewXmlText = $NewXmlText.Replace("date", $XmlResults.testsuites.timestamp)
            $XmlResults = [xml]($NewXmlText)
            if ($GenerateXmlResults) {
                # Save the xml results.
                $XmlResults.Save($FinalResultsPath) | Out-Null
            }
        }
    } else {
        if ($GenerateXmlResults) {
            # Save the xml results.
            $XmlResults.Save($FinalResultsPath) | Out-Null
        }
    }

    $TestCount = $XmlResults.testsuites.tests -as [Int]
    $TestsFailed = $XmlResults.testsuites.failures -as [Int]
    $TestsRetried = $XmlResults.testsuites.retried -as [Int]

    # Uninstall the kernel mode test driver and revert the msquic driver.
    if ($Kernel -ne "") {
        net.exe stop msquicpriv /y | Out-Null
        sc.exe delete msquictestpriv | Out-Null
        sc.exe delete msquicpriv | Out-Null
        Remove-Item (Join-Path $DriverPath msquicpriv.sys) -Force
        Remove-Item (Join-Path $DriverPath msquictestpriv.sys) -Force
    }

    if ($IsWindows -and $EnableSystemVerifier) {
        if ($Kernel -ne "") {
            verifier.exe /volatile /removedriver msquicpriv.sys msquictestpriv.sys
            verifier.exe /volatile /flags 0x0
        }
        verifier.exe /volatile /removedriver afd.sys netio.sys tcpip.sys
        verifier.exe /volatile /flags 0x0
    }

    # Print out the results.
    Log "$($TestCount) test(s) run."
    if ($KeepOutputOnSuccess -or ($TestsFailed -ne 0) -or ($global:CrashedProcessCount -ne 0)) {
        Log "Output can be found in $($LogDir)"
        $ConsistentFailure = $TestsFailed - $TestsRetried
        $PassedWithRetry = $TestsRetried - $ConsistentFailure
        if ($ErrorsAsWarnings -or
            (($IsolationMode -eq "Isolated") -and ($TestsFailed -ne 0) -and ($TestsFailed -eq $TestsRetried))) {
            if ($TestsFailed -eq $TestsRetried) {
                Write-Warning "$($TestsRetried) test(s) passed with retry, $($global:CrashedProcessCount) test(s) crashed."
            } else { # for $ErrorsAsWarning
                Write-Warning "$($ConsistentFailure) test(s) failed with retry, $($PassedWithRetry) test(s) passed with retry, $($global:CrashedProcessCount) test(s) crashed."
            }
        } else {
            Write-Error "$($ConsistentFailure) test(s) failed with retry, $($PassedWithRetry) test(s) passed with retry, $($global:CrashedProcessCount) test(s) crashed."
            $LastExitCode = 1
        }
    } elseif ($AZP -and $TestCount -eq 0) {
        Write-Error "Failed to run any tests."
    } else {
        if (Test-Path $LogDir) {
            Remove-Item $LogDir -Recurse -Force | Out-Null
        }
    }
}
