<?xml version="1.0" encoding="UTF-8"?>
<meta-runner name="xUnit.net + dotCover 💕">
  <description>Run xUnit.net tests with dotCover coverage: http://www.wwwlicious.com/2015/09/25/teamcity-dotcover-xunit-at-last/</description>
  <settings>
    <parameters>
      <param name="xUnitNet.nugetSource" value="http://www.nuget.org/api/v2/" spec="text description='The nuget source url for the xunit runner' display='normal' label='Xunit Runner Nuget Source'" />
      <param name="xUnitNet.nugetExe" value="%teamcity.tool.NuGet.CommandLine.DEFAULT%" spec="text description='The path to the nuget package on the agent' display='normal' label='Nuget Package Path'" />
      <param name="xUnitNet.executable" value="xunit.console.exe" spec="text description='The xunit runner executable to use in the nuget package' display='normal' label='Xunit Runner Executable'" />
      <param name="xUnitNet.executable.args" value="" spec="text description='Custom xunit runner arguments' display='normal' label='Xunit Runner Executable additional arguments'" />
      <param name="xUnitNet.executable.legacymode" value="false" spec="checkbox checkedValue='true' description='For mixed mode assemblies, enables xunit runner to use legacy activiation' uncheckedValue='false' label='Enable .Net V2 Legacy Activation' display='normal'" />
      <param name="xUnitNet.assembliesPath" value="*/bin/*tests.dll" spec="text description='The assemblies to test; relative to the working directory. Supports multiple paths separated by comma(,) or newline. Asterix wildcard (*) is supported here' display='normal' label='Assemblies to test:'" />
      <param name="xUnitNet.assembliesExcludePath" value="" spec="text description='The assemblies to exclude; Supports multiple filenames separated by comma(,) or newline. Asterix wildcard (*) is supported here' display='normal' label='Assemblies to exclude:'" />
      <param name="xUnitNet.trait" value="" spec="text description='only run tests with matching name/value traits, one name=value per line (e.x. name=value)' validationMode='any' label='Include Traits' display='normal'" />
      <param name="xUnitNet.notrait" value="Category=Integration" spec="text description='do not run tests with matching name/value traits, one name=value per line (e.x. name=value)' validationMode='any' label='Exclude Traits' display='normal'" />
      <param name="xUnitNet.dotCover.enable" value="true" spec="checkbox checkedValue='true' description='Enables or disables running test coverage with dotCover' uncheckedValue='false' label='Enable dotCover coverage' display='normal'" />
      <param name="xUnitNet.dotCover.Filters" spec="text description='Specifies coverage filters using the config format. Documentation http://www.jetbrains.com/dotcover/help/dotCover__Console_Runner_Commands.html' label='dotCover Filters' validationMode='any' display='normal'"><![CDATA[<!-- Coverage filters. It's possible to use asterisks as wildcard symbols. -->
<Filters>
  <IncludeFilters>
    <FilterEntry>
      <ModuleMask>*</ModuleMask>
      <ClassMask>*</ClassMask>
      <FunctionMask>*</FunctionMask>
    </FilterEntry>
  </IncludeFilters>
  <ExcludeFilters>
    <FilterEntry>
      <ModuleMask>*Tests</ModuleMask>
    </FilterEntry>
  </ExcludeFilters>
</Filters>]]></param>
      <param name="xUnitNet.dotCover.AttributeFilters" spec="text description='Specifies attribute filters using the config format. Documentation http://www.jetbrains.com/dotcover/help/dotCover__Console_Runner_Commands.html' validationMode='any' label='dotCover AttributeFilters' display='normal'"><![CDATA[<!-- Attribute filters. It's possible to use asterisks as wildcard symbols. -->
<AttributeFilters>
  <AttributeFilterEntry>System.CodeDom.Compiler.GeneratedCodeAttribute</AttributeFilterEntry>
  <AttributeFilterEntry>System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute</AttributeFilterEntry>
  <AttributeFilterEntry>System.ObsoleteAttribute</AttributeFilterEntry>
</AttributeFilters>]]></param>
      <param name="xUnitNet.dotCover.exportReport" value="false" spec="checkbox checkedValue='true' description='If enabled, it will add a report to the build artifacts' uncheckedValue='false' label='Export dotCover report' display='normal'" />
      <param name="xUnitNet.dotCover.skipProcesses" value="" spec="text description='Comma or semicolon delimited list of processes to skip coverage on. Example: sqlservr.exe;pwsh.exe' validationMode='any' label='Skip Processes' display='normal'" />
      <param name="xUnitNet.dotCover.reportType" value="html" spec="select display='normal' description='The dotCover report type' label='ReportType' data_2='JSON' data_1='HTML' data_4='NDependXML' data_3='XML'" />
      <param name="xUnitNet.dotCover.reportOutputFile" value="dotCoverReport.html" spec="text description='the name of the report file to output' validationMode='any' label='Report FileName' display='normal'" />
    </parameters>
    <build-runners>
      <runner name="Install Test runner, Create config, Run tests, Update coverage" type="jetbrains_powershell">
        <parameters>
          <param name="jetbrains_powershell_execution" value="PS1" />
          <param name="jetbrains_powershell_noprofile" value="true" />
          <param name="jetbrains_powershell_errorToError" value="error" />
          <param name="jetbrains_powershell_script_mode" value="CODE" />
          <param name="jetbrains_powershell_bitness" value="x86" />
          <param name="teamcity.step.mode" value="default" />
          <param name="jetbrains_powershell_script_code"><![CDATA[[CmdletBinding()]
param (
    [string] $workingDir = "%teamcity.build.workingDir%",
    [string] $nugetExe = "%xUnitNet.nugetExe%\tools\nuget.exe",
    [string] $xUnitNuget = "%xUnitNet.nugetSource%",
    [string] $xUnitExe = "%xUnitNet.executable%",
    [string] $dotCoverExecutable = "%teamcity.tool.JetBrains.dotCover.CommandLineTools.bundled%",
    [boolean] $xunitLegacy = [System.Convert]::ToBoolean("%xUnitNet.executable.legacymode%"),
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string] $assemblyFilter = "%xUnitNet.assembliesPath%",
    [string] $assemblyExcludeFilter = "%xUnitNet.assembliesExcludePath%",
    [string] $traits = "%xUnitNet.trait%",
    [string] $notraits = "%xUnitNet.notrait%",
    [boolean] $dotCoverEnabled = [System.Convert]::ToBoolean("%xUnitNet.dotCover.enable%"),
    [boolean] $dotCoverReportEnabled = [System.Convert]::ToBoolean("%xUnitNet.dotCover.exportReport%"),
    [string] $dotCoverReportType = "%xUnitNet.dotCover.reportType%",
    [string] $dotCoverReportFile = "%xUnitNet.dotCover.reportOutputFile%",
    [string] $dotCoverFilter = "%xUnitNet.dotCover.Filters%",
    [string] $dotCoverSkipProcesses = "%xUnitNet.dotCover.skipProcesses%",
    [string] $dotCoverAttributeFilter = "%xUnitNet.dotCover.AttributeFilters%",
    [string] $xunitArgs = "%xUnitNet.executable.args%"
)

$ErrorActionPreference = "Stop"

try {
    ## get xunit runner from nuget package feed
    Invoke-Expression "$nugetExe install xunit.runner.console -source $xUnitNuget"
    $xunit = Join-Path $workingDir "xunit.runner.console.*\tools\$xUnitExe" | Resolve-Path
    
    if (-not $xunit) {
        # Try finding xunit under framework specific folder
        $xunit = Join-Path $workingDir "xunit.runner.console.*\tools\net4*\$xUnitExe" | Resolve-Path
    }

    if (-not $xunit) {
        throw New-Object System.Exception "Cannot find $xUnitExe after installing via nuget"
    }

    ## modify runner config if legacy v2 runtime activation mode required
    if ($xunitLegacy) {
        Write-Output "Setting xunit to run with legacy activation policy"
        $config = [string]$xunit + ".config"
        $doc = (Get-Content $config) -as [Xml]
        $doc.configuration.startup.SetAttribute("useLegacyV2RuntimeActivationPolicy", "true")
        $doc.Save($config)
    }

    ## Process Assemblies to be Excluded
    ## split string into array if multiple wildcard path separated by comma(,) or newline
    $assemblyExcludeFilterPaths = @()
    $excludedAssemblies = @("")

    if ($assemblyExcludeFilter) {
        $assemblyExcludeFilterPaths = $assemblyExcludeFilter.Trim() -split {$_ -eq "," -or $_ -eq "`n" }
    }

    foreach ($filterExcludePath in $assemblyExcludeFilterPaths) {
        ## Concatenate each Assembly into the array
        $excludedAssemblies += , $filterExcludePath.Trim()
    }

    ##split string into array if multiple wildcard path separated by comma(,) or newline
    $assemblyFilterPaths = $assemblyFilter.Trim() -split {$_ -eq "," -or $_ -eq "`n" }

    $assemblies = @()

    foreach ($filterPath in $assemblyFilterPaths) {
        ## Search for the assemblies using wildcard pattern (Also filter out excluded assemblies)
        $assembliesFromPath = (Get-ChildItem $filterPath.Trim() -Recurse -Exclude $excludedAssemblies.Trim() | Select-Object -ExpandProperty FullName)
        $assemblies += $assembliesFromPath
    }

    if (-not $assemblies) {
        $cwd = Get-Location
        $message = "No test assemblies found; cwd = $cwd ; assembly filter = $assemblyFilter"
        Write-Error $message
        throw New-Object System.Exception $message
    }
    else {
        Write-Output "Found test assemblies: " $assemblies
    }

    ## create single-line correctly formatted xunit argument

    ## the type will be a string for a single-line path without a wildcard.
    if ($assemblies.GetType() -eq [String]) {
        $assemblies = [String[]] $assemblies
    }

    # split multi-line xunit args into single line and correctly format
    $assemblyArg = "`"" + [String]::Join("`" `"", $assemblies) + "`""
    if ($traits.Length -gt 0) {
        $traitArg = ($traits.Trim().Split("`r`n") | Where-Object {$_ -ne ""} | ForEach-Object { "-trait """ + $_ + """"}) -join " "
    }
    if ($notraits.Length -gt 0) {
        $notraitArg = ($notraits.Trim().Split("`r`n") | Where-Object {$_ -ne ""} | ForEach-Object { "-notrait """ + $_ + """"}) -join " "
    }
    $xunitArg = $assemblyArg, $traitArg, $notraitArg, $xunitArgs -join " "

    ## filenames
    $outputFile = "xunitcoverage.dcvr"
    $settingsFileName = "coverage_settings.xml"
    $coverageLogFileName = "dotCoverXunitLog.txt"

    Write-Output "##teamcity[message text='Run test coverage:  $dotCoverEnabled']"

    ## do we run coverage?
    if ($dotCoverEnabled) {
        ## generate coverage settings file
        $settingsXml = @"
<?xml version="1.0" encoding="utf-8"?>
<CoverageParams>
  <TargetExecutable>$xunit</TargetExecutable>
  <TargetArguments>$xunitArg</TargetArguments>
  <TargetWorkingDir>$workingDir</TargetWorkingDir>
  <Output>$outputFile</Output>
  $dotCoverFilter
  $dotCoverAttributeFilter
</CoverageParams>
"@
        # Avoid BOM http://stackoverflow.com/questions/5596982/using-powershell-to-write-a-file-in-utf-8-without-the-bom
        $encoding = New-Object System.Text.UTF8Encoding -ArgumentList $false # no byte order mark
        [System.IO.File]::WriteAllText($settingsFileName, $settingsXml, $encoding)
        Write-Output "Wrote coverage parameters file to ${settingsFileName}: $settingsXml"

        ## Add process filters if specified
        if ($dotCoverSkipProcesses) {
            $processFilters = "/ProcessFilters=-:$([System.String]::Join(';-:', $dotCoverSkipProcesses.Split(@(',',';'), [System.StringSplitOptions]::RemoveEmptyEntries)))"
        }

        ## Run coverage
        $dotCover = Join-Path $dotCoverExecutable "dotCover.exe"
        $dotCoverArgs = " cover $settingsFileName /LogFile=$coverageLogFileName /ReturnTargetExitCode $processFilters"
        Write-Output "##teamcity[message text='dotCoverExecutable: $dotCoverExecutable']"
        Write-Output "##teamcity[message text='dotCoverArgs: $dotCoverArgs']"
        $command = "$dotCover $dotCoverArgs"
        Write-Output "Executing dotCover via command: $command"
        Invoke-Expression $command

        if ($LASTEXITCODE -ne 0) {
            throw New-Object System.Exception "dotCover run failed with error code $LASTEXITCODE"
        }

        ## Report coverage results and output build artifacts
        Write-Host "##teamcity[dotNetCoverage dotcover_home='$dotCoverExecutable']"
        Write-Output "##teamcity[importData type='dotNetCoverage' tool='dotcover' path='$outputFile']"
        Write-Output "##teamcity[publishArtifacts '$settingsFileName']"
        Write-Output "##teamcity[publishArtifacts '$coverageLogFileName']"
        Write-Output "##teamcity[publishArtifacts '$outputFile']"

        ## Optional generate report
        if ($dotCoverReportEnabled) {
            $reportCommand = "$dotCover report /source=$outputFile /output=$dotCoverReportFile /reportType=$dotCoverReportType"
            Write-Output "Creating dotCover report via command: $reportCommand"
            Invoke-Expression $reportCommand
            Write-Output "##teamcity[publishArtifacts '$dotCoverReportFile']"
        }
    }
    else {
        ## run xunit without coverage
        $xunitOnlyCommand = "$xunit $xunitArg"
        Write-Output "Running unit tests without coverage via command: $xunitOnlyCommand"
        Invoke-Expression $xunitOnlyCommand
    }
}
catch {
    Write-Output "##teamcity[buildStatus text='$_' status='FAILURE']"
    Write-Output "##teamcity[message text='$_' status='ERROR']"
    exit 1
}]]></param>
        </parameters>
      </runner>
    </build-runners>
    <requirements />
  </settings>
</meta-runner>

