tests/manual.pester.coverage.ps1

<#
    .SYNOPSIS
        Runs dbatools tests.
 
    .DESCRIPTION
        This file will either run all tests for dbatools or merely run the specified tests.
 
    .PARAMETER Path
        The Path to the test files to run
#>

[CmdletBinding()]
Param (
    [string[]]
    $Path,
    
    [ValidateSet('None', 'Default', 'Passed', 'Failed', 'Pending', 'Skipped', 'Inconclusive', 'Describe', 'Context', 'Summary', 'Header', 'All', 'Fails')]
    [string]
    $Show = "All",
    
    [switch]
    $TestIntegration,
    
    [switch]
    $SkipHelpTest,
    
    [switch]
    $IncludeCoverage
)
# required to calculate coverage
$global:dbatools_dotsourcemodule = $true

Write-Host -Object "Running $PSCommandpath" -ForegroundColor Cyan
$ModuleBase = Split-Path -Path $PSScriptRoot -Parent
if (Get-Module dbatools) { Remove-Module dbatools }

Write-Host "Importing: $ModuleBase\dbatools.psm1"
Import-Module "$ModuleBase\dbatools.psm1" -DisableNameChecking
$ScriptAnalyzerRules = Get-ScriptAnalyzerRule

. $PSScriptRoot\..\internal\Write-Message.ps1
. $PSScriptRoot\..\internal\Stop-Function.ps1

$testInt = $false
if ($config_TestIntegration) { $testInt = $true }
if ($TestIntegration) { $testInt = $true }

function get-coverageindications($path) {
    $CBHRex = [regex]'(?smi)<#(.*)#>'
    $everything = (Get-Module dbatools).ExportedCommands.Values
    $everyfunction = $everything.Name
    $funcs = @()
    # assuming Get-DbaFoo.Tests.ps1 wants to initiate coverage for "Get-DbaFoo"
    $leaf = split-path $path -Leaf
    $func_name += $leaf.Replace('.Tests.ps1', '')
    if ($func_name -in $everyfunction) {
        $funcs += $func_name
        $f = $everything | where-object Name -eq $func_name
        $source = $f.Definition
        $CBH = $CBHRex.match($source).Value
        $cmdonly = $source.Replace($CBH, '')
        foreach($e in $everyfunction) {
            # hacky, I know, but every occurrence of any function plus a space kinda denotes usage !?
            $searchme = "$e "
            if ($cmdonly.contains($searchme)) {
                $funcs += $e
            }
        }
    }
    $testpaths = @()
    $allfiles = Get-ChildItem -File -Path "$ModuleBase\internal", "$ModuleBase\functions" -Filter '*.ps1'
    foreach($f in $funcs) {
        if ($f -in ('Connect-SqlInstance', 'Select-DefaultView', 'Stop-Function', 'Write-Message')) { continue }
        # can I find a correspondence to a physical file (again, on the convenience of having Get-DbaFoo.ps1 actually defining Get-DbaFoo)?
        $res = $allfiles | where-object { $_.Name.Replace('.ps1', '') -eq $f }
        if ($res.count -gt 0) {
            $testpaths += $res.FullName
        }
    }
    return $testpaths
}
$counter = 0
if ($Path) {
    foreach ($item in $Path) {
        $counter += 1
        if ($IncludeCoverage) {
            $allpaths = get-coverageindications $Item
        }
        if ($testInt) {
            if ($IncludeCoverage -and $allpaths) {
                Invoke-Pester -Script $item -CodeCoverage $allpaths -Show $Show -PassThru -CodeCoverageOutputFile "$ModuleBase\PesterCoverage$Counter.xml" | Export-CliXml -Path "$ModuleBase\PesterResults$Counter.xml"
            } else {
                Invoke-Pester $item -Show $Show
            }
        } else {
            if ($IncludeCoverage -and $allpaths) {
                Invoke-Pester $item -ExcludeTag "IntegrationTests" -CodeCoverage $allpaths -Show $Show
            } else {
                Invoke-Pester $item -ExcludeTag "IntegrationTests" -Show $Show
            }
        }
    }
    $results = @(Get-ChildItem -Path "$ModuleBase\PesterResults*.xml" | Import-Clixml)
    $totalcount = $results | Select-Object -ExpandProperty TotalCount | Measure-Object -Sum | Select-Object -ExpandProperty Sum
    $failedcount = $results | Select-Object -ExpandProperty FailedCount | Measure-Object -Sum | Select-Object -ExpandProperty Sum
    if ($failedcount -gt 0) {
        $faileditems = $results | Select-Object -ExpandProperty TestResult | Where-Object { $_.Passed -notlike $True }
        
        if ($faileditems) {
            Write-Warning "Failed tests summary:"
            $faileditems | ForEach-Object {
                $name = $_.Name
                [pscustomobject]@{
                    Describe = $_.Describe
                    Context = $_.Context
                    Name = "It $name"
                    Result = $_.Result
                    Message = $_.FailureMessage
                }
            } | Sort-Object Describe, Context, Name, Result, Message | Format-List
            
            throw "$failedcount tests failed."
        }
    }
    
    #handle coverage https://docs.codecov.io/reference#upload
    $report = @{'coverage'=@{}}
    $missed = $results.CodeCoverage | Select-Object -ExpandProperty MissedCommands | Sort-Object -Property File,Line -Unique
    $hits = $results.CodeCoverage | Select-Object -ExpandProperty HitCommands | Sort-Object -Property File,Line -Unique
    $LineCount = @{}
    $hits | ForEach-Object {
        $filename = $_.File.Replace("$ModuleBase\", '').Replace('\','/')
        if ($filename -notin $report['coverage'].Keys) {
            $report['coverage'][$filename] = @{}
            $LineCount[$filename] = (Get-Content $_.File | Measure-Object -Line).Lines
        }
        $report['coverage'][$filename][$_.Line] = 1
    }
    
    $missed | ForEach-Object {
        $filename = $_.File.Replace("$ModuleBase\", '').Replace('\','/')
        if ($filename -notin $report['coverage'].Keys) {
            $report['coverage'][$filename] = @{}
            $LineCount[$filename] = (Get-Content $_.File | Measure-Object -Line).Lines
        }
        if ($_.Line -notin $report['coverage'][$filename].Keys) {
            #miss only if not already covered
            $report['coverage'][$filename][$_.Line] = 0
        }
    }
    
    
    $newreport = @{'coverage'=[ordered]@{}}
    foreach($fname in $report['coverage'].Keys) {
        $Linecoverage = [ordered]@{}
        for($i=1; $i -le $LineCount[$fname]; $i++){
            if ($i -in $report['coverage'][$fname].Keys) {
                $Linecoverage["$i"] = $report['coverage'][$fname][$i]
            }
        }
        $newreport['coverage'][$fname] = $Linecoverage
    }
    $newreport | ConvertTo-Json -Depth 4 -Compress | Out-File -FilePath "$ModuleBase\PesterResultsCoverage.json" -Encoding utf8
    $params = @{}
    $params['branch'] = $env:APPVEYOR_REPO_BRANCH
    $params['service'] = "appveyor"
    $params['job'] = $env:APPVEYOR_ACCOUNT_NAME
    if ($params['job']) { $params['job'] += '/' + $env:APPVEYOR_PROJECT_SLUG }
    if ($params['job']) { $params['job'] += '/' + $env:APPVEYOR_BUILD_VERSION }
    $params['build'] = $env:APPVEYOR_JOB_ID
    $params['pr'] = $env:APPVEYOR_PULL_REQUEST_NUMBER
    $params['slug'] = $env:APPVEYOR_REPO_NAME
    $params['commit'] = $env:APPVEYOR_REPO_COMMIT
    Add-Type -AssemblyName System.Web
    $CodeCovParams = [System.Web.HttpUtility]::ParseQueryString([String]::Empty)
    $params.GetEnumerator() | Where-Object Value | ForEach-Object { $CodeCovParams.Add($_.Name, $_.Value) }
    $Request  = [System.UriBuilder]('https://codecov.io/upload/v2')
    $Request.Query = $CodeCovParams.ToString()
    write-host "sending all to $($Request.Uri)"
    Invoke-RestMethod -Uri $Request.Uri -Method Post -InFile "$ModuleBase\PesterResultsCoverage.json" -ContentType 'multipart/form-data' -verbose
}

else {
    if ($testInt) { Invoke-Pester -Show $Show }
    else { Invoke-Pester -ExcludeTag "IntegrationTests" -Show $Show }
}