tasks/JaCoCo.coverage.build.ps1

param
(
    # Project path
    [Parameter()]
    [System.String]
    $ProjectPath = (property ProjectPath $BuildRoot),

    [Parameter()]
    # Base directory of all output (default to 'output')
    [System.String]
    $OutputDirectory = (property OutputDirectory (Join-Path $BuildRoot 'output')),

    [Parameter()]
    [System.String]
    $BuiltModuleSubdirectory = (property BuiltModuleSubdirectory ''),

    [Parameter()]
    [System.Management.Automation.SwitchParameter]
    $VersionedOutputDirectory = (property VersionedOutputDirectory $true),

    [Parameter()]
    [System.String]
    $ProjectName = (property ProjectName ''),

    [Parameter()]
    [System.String]
    $PesterOutputFolder = (property PesterOutputFolder 'testResults'),

    [Parameter()]
    [System.String]
    $PesterOutputFormat = (property PesterOutputFormat ''),

    [Parameter()]
    [System.Object[]]
    $PesterScript = (property PesterScript ''),

    [Parameter()]
    [System.String[]]
    $PesterTag = (property PesterTag @()),

    [Parameter()]
    [System.String[]]
    $PesterExcludeTag = (property PesterExcludeTag @()),

    [Parameter()]
    [System.String]
    $CodeCoverageThreshold = (property CodeCoverageThreshold ''),

    # Build Configuration object
    [Parameter()]
    [System.Collections.Hashtable]
    $BuildInfo = (property BuildInfo @{ })
)


# Synopsis: Merging several code coverage files together.
task Merge_CodeCoverage_Files {
    if ([System.String]::IsNullOrEmpty($ProjectName))
    {
        $ProjectName = Get-SamplerProjectName -BuildRoot $BuildRoot
    }

    if ([System.String]::IsNullOrEmpty($SourcePath))
    {
        $SourcePath = Get-SamplerSourcePath -BuildRoot $BuildRoot
    }

    $OutputDirectory = Get-SamplerAbsolutePath -Path $OutputDirectory -RelativeTo $BuildRoot

    "`tProject Name = '$ProjectName'"
    "`tSource Path = '$SourcePath'"
    "`tOutput Directory = '$OutputDirectory'"

    if ($VersionedOutputDirectory)
    {
        # VersionedOutputDirectory is not [bool]'' nor $false nor [bool]$null
        # Assume true, wherever it was set
        $VersionedOutputDirectory = $true
    }
    else
    {
        # VersionedOutputDirectory may be [bool]'' but we can't tell where it's
        # coming from, so assume the build info (Build.yaml) is right
        $VersionedOutputDirectory = $BuildInfo['VersionedOutputDirectory']
    }

    $GetBuiltModuleManifestParams = @{
        OutputDirectory          = $OutputDirectory
        BuiltModuleSubdirectory  = $BuiltModuleSubDirectory
        ModuleName               = $ProjectName
        VersionedOutputDirectory = $VersionedOutputDirectory
        ErrorAction              = 'Stop'
    }

    $builtModuleManifest = Get-SamplerBuiltModuleManifest @GetBuiltModuleManifestParams

    "`tBuilt Module Manifest = '$builtModuleManifest'"

    $ModuleVersion = Get-BuiltModuleVersion @GetBuiltModuleManifestParams
    $ModuleVersionObject = Split-ModuleVersion -ModuleVersion $ModuleVersion
    $ModuleVersionFolder = $ModuleVersionObject.Version
    $preReleaseTag = $ModuleVersionObject.PreReleaseString

    "`tModule Version = '$ModuleVersion'"
    "`tModule Version Folder = '$ModuleVersionFolder'"
    "`tPre-release Tag = '$preReleaseTag'"

    $osShortName = Get-OperatingSystemShortName

    $powerShellVersion = 'PSv.{0}' -f $PSVersionTable.PSVersion

    $moduleFileName = '{0}.psm1' -f $ProjectName

    $PesterOutputFolder = Get-SamplerAbsolutePath -Path $PesterOutputFolder -RelativeTo $OutputDirectory

    "`tPester Output Folder = '$PesterOutputFolder'"

    $GetCodeCoverageThresholdParameters = @{
        RuntimeCodeCoverageThreshold = $CodeCoverageThreshold
        BuildInfo                    = $BuildInfo
    }

    $CodeCoverageThreshold = Get-CodeCoverageThreshold @GetCodeCoverageThresholdParameters

    if (-not $CodeCoverageThreshold)
    {
        $CodeCoverageThreshold = 0
    }

    "`tCode Coverage Threshold = '$CodeCoverageThreshold'"

    if ($CodeCoverageThreshold -gt 0)
    {
        $getPesterOutputFileFileNameParameters = @{
            ProjectName       = $ProjectName
            ModuleVersion     = $ModuleVersion
            OsShortName       = $osShortName
            PowerShellVersion = $powerShellVersion
        }

        $pesterOutputFileFileName = Get-PesterOutputFileFileName @getPesterOutputFileFileNameParameters

        $getCodeCoverageOutputFile = @{
            BuildInfo          = $BuildInfo
            PesterOutputFolder = $PesterOutputFolder
        }

        $CodeCoverageOutputFile = Get-SamplerCodeCoverageOutputFile @getCodeCoverageOutputFile

        if (-not $CodeCoverageOutputFile)
        {
            $CodeCoverageOutputFile = (Join-Path -Path $PesterOutputFolder -ChildPath "CodeCov_$pesterOutputFileFileName")
        }

        "`tCode Coverage Output File = $CodeCoverageOutputFile"

        $CodeCoverageMergedOutputFile = 'CodeCov_Merged.xml'

        if ($BuildInfo.CodeCoverage.CodeCoverageMergedOutputFile)
        {
            $CodeCoverageMergedOutputFile = $BuildInfo.CodeCoverage.CodeCoverageMergedOutputFile
        }

        $CodeCoverageMergedOutputFile = Get-SamplerAbsolutePath -Path $CodeCoverageMergedOutputFile -RelativeTo $PesterOutputFolder

        "`tCode Coverage Merge Output File = $CodeCoverageMergedOutputFile"

        $CodeCoverageFilePattern = 'Codecov*.xml'

        if ($BuildInfo.ContainsKey('CodeCoverage') -and $BuildInfo.CodeCoverage.ContainsKey('CodeCoverageFilePattern'))
        {
            $CodeCoverageFilePattern = $BuildInfo.CodeCoverage.CodeCoverageFilePattern
        }

        "`tCode Coverage File Pattern = $CodeCoverageFilePattern"

        if (-not [System.String]::IsNullOrEmpty($CodeCoverageFilePattern))
        {
            $codecovFiles = Get-ChildItem -Path $PesterOutputFolder -Include $CodeCoverageFilePattern -Recurse
        }

        "`tMerging Code Coverage Files = '{0}'" -f ($codecovFiles.FullName -join ', ')
        ""

        if (Test-Path -Path $CodeCoverageMergedOutputFile)
        {
            Write-Build Yellow "File $CodeCoverageMergedOutputFile found, deleting file."

            Remove-Item -Path $CodeCoverageMergedOutputFile -Force
        }

        Write-Build White "Processing folder: $OutputDirectory"

        if ($codecovFiles.Count -gt 1)
        {
            Write-Build DarkGray "Started merging $($codecovFiles.Count) code coverage files!"

            Start-CodeCoverageMerge -Files $codecovFiles -TargetFile $CodeCoverageMergedOutputFile

            Write-Build Green "Merge completed. Saved merge result to: $CodeCoverageMergedOutputFile"
        }
        else
        {
            throw "Found $($codecovFiles.Count) code coverage file. Need at least two files to merge."
        }
    }
    else
    {
        Write-Build White 'Code coverage is not enabled, skipping.'
    }
}

function Confirm-CodeCoverageFileFormat
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Xml.XmlDocument]
        $CodeCovFile
    )

    $report = ($CodeCovFile.GetEnumerator() | Where-Object -FilterScript { $_.Name -eq "Report"})
    if ($null -ne $report -and $report.OuterXml -like "*JACOCO*")
    {
        return $true
    }

    return $false
}

function Start-CodeCoverageMerge
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Object[]]
        $Files,

        [Parameter(Mandatory = $true)]
        [System.String]
        $TargetFile
    )

    $firstFile = $Files | Select-Object -First 1
    $otherFiles = $Files | Select-Object -Skip 1

    [xml]$targetDocument = Get-Content -Path $firstFile.FullName -Raw

    if (Confirm-CodeCoverageFileFormat -CodeCovFile $targetDocument)
    {
        Write-Verbose "Successfully imported $($firstFile.Name) as a baseline"

        $merged = 0
        foreach ($file in $otherFiles)
        {
            [xml]$mergeDocument = Get-Content -Path $file.FullName -Raw
            Write-Verbose "Merging $($file.Name) into baseline"
            if (Confirm-CodeCoverageFileFormat -CodeCovFile $mergeDocument)
            {
                $targetDocument = Merge-JaCoCoReport -OriginalDocument $targetDocument -MergeDocument $mergeDocument
                $merged++
            }
            else
            {
                Write-Verbose "The following code coverage file is not using the JaCoCo format: $($file.Name)"
            }
        }

        Write-Verbose "Merge completed: Successfully merged $merged files into the baseline"

        $targetDocument = Update-JaCoCoStatistic -Document $targetDocument

        $xmlSettings = New-Object -TypeName 'System.Xml.XmlWriterSettings'
        $xmlSettings.Indent = $true
        $xmlSettings.Encoding = [System.Text.Encoding]::ASCII

        $TargetFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TargetFile)

        $xmlWriter = [System.Xml.XmlWriter]::Create($TargetFile, $xmlSettings)

        $targetDocument.Save($xmlWriter)

        $xmlWriter.Close()
    }
    else
    {
        throw "The following code coverage file is not using the JaCoCo format: $($firstFile.Name)"
    }
}

# Synopsis: Convert JaCoCo coverage so it supports a built module by way of ModuleBuilder.
task Convert_Pester_Coverage {
    if ([System.String]::IsNullOrEmpty($ProjectName))
    {
        $ProjectName = Get-SamplerProjectName -BuildRoot $BuildRoot
    }

    if ([System.String]::IsNullOrEmpty($SourcePath))
    {
        $SourcePath = Get-SamplerSourcePath -BuildRoot $BuildRoot
    }

    $OutputDirectory = Get-SamplerAbsolutePath -Path $OutputDirectory -RelativeTo $BuildRoot

    "`tProject Name = '$ProjectName'"
    "`tSource Path = '$SourcePath'"
    "`tOutput Directory = '$OutputDirectory'"

    if ($VersionedOutputDirectory)
    {
        # VersionedOutputDirectory is not [bool]'' nor $false nor [bool]$null
        # Assume true, wherever it was set
        $VersionedOutputDirectory = $true
    }
    else
    {
        # VersionedOutputDirectory may be [bool]'' but we can't tell where it's
        # coming from, so assume the build info (Build.yaml) is right
        $VersionedOutputDirectory = $BuildInfo['VersionedOutputDirectory']
    }

    $GetBuiltModuleManifestParams = @{
        OutputDirectory          = $OutputDirectory
        BuiltModuleSubDirectory  = $BuiltModuleSubDirectory
        ModuleName               = $ProjectName
        VersionedOutputDirectory = $VersionedOutputDirectory
        ErrorAction              = 'Stop'
    }

    $builtModuleBase = Get-SamplerBuiltModuleBase @GetBuiltModuleManifestParams
    "`tBuilt Module Base = '$builtModuleBase'"

    $builtModuleManifest = Get-SamplerBuiltModuleManifest @GetBuiltModuleManifestParams
    "`tBuilt Module Manifest = '$builtModuleManifest'"

    if ($builtModuleRootScriptPath = Get-SamplerModuleRootPath -ModuleManifestPath $builtModuleManifest)
    {
        $builtModuleRootScriptPath = (Get-Item -Path $builtModuleRootScriptPath -ErrorAction SilentlyContinue).FullName
    }

    "`tBuilt ModuleRoot script = '$builtModuleRootScriptPath'"

    $builtDscResourcesFolder = Get-SamplerAbsolutePath -Path 'DSCResources' -RelativeTo $builtModuleBase
    "`tBuilt DSC Resource Path = '$builtDscResourcesFolder'"

    $ModuleVersion = Get-BuiltModuleVersion @GetBuiltModuleManifestParams
    $ModuleVersionObject = Split-ModuleVersion -ModuleVersion $ModuleVersion
    $ModuleVersionFolder = $ModuleVersionObject.Version
    $preReleaseTag = $ModuleVersionObject.PreReleaseString

    "`tModule Version = '$ModuleVersion'"
    "`tModule Version Folder = '$ModuleVersionFolder'"
    "`tPre-release Tag = '$preReleaseTag'"

    $GetCodeCoverageThresholdParameters = @{
        RuntimeCodeCoverageThreshold = $CodeCoverageThreshold
        BuildInfo                    = $BuildInfo
    }

    $CodeCoverageThreshold = Get-CodeCoverageThreshold @GetCodeCoverageThresholdParameters
    "`tCode Coverage Threshold = '$CodeCoverageThreshold'"

    if (-not $CodeCoverageThreshold)
    {
        $CodeCoverageThreshold = 0
    }

    $PesterOutputFolder = Get-SamplerAbsolutePath -Path $PesterOutputFolder -RelativeTo $OutputDirectory
    "`tPester Output Folder = '$PesterOutputFolder'"

    $osShortName = Get-OperatingSystemShortName

    $powerShellVersion = 'PSv.{0}' -f $PSVersionTable.PSVersion

    $moduleFileName = '{0}.psm1' -f $ProjectName

    "`tModule File Name = '$moduleFileName'"

    #### TODO: Split Script Task Variables here

    $getPesterOutputFileFileNameParameters = @{
        ProjectName       = $ProjectName
        ModuleVersion     = $ModuleVersion
        OsShortName       = $osShortName
        PowerShellVersion = $powerShellVersion
    }

    $pesterOutputFileFileName = Get-PesterOutputFileFileName @getPesterOutputFileFileNameParameters

    $getCodeCoverageOutputFile = @{
        BuildInfo          = $BuildInfo
        PesterOutputFolder = $PesterOutputFolder
    }

    $CodeCoverageOutputFile = Get-SamplerCodeCoverageOutputFile @getCodeCoverageOutputFile

    if (-not $CodeCoverageOutputFile)
    {
        $CodeCoverageOutputFile = (Join-Path -Path $PesterOutputFolder -ChildPath "CodeCov_$pesterOutputFileFileName")
    }

    "`t"
    "`tCodeCoverageOutputFile = $CodeCoverageOutputFile"

    $CodeCoverageOutputFileEncoding = $BuildInfo.Pester.CodeCoverageOutputFileEncoding

    if (-not $CodeCoverageOutputFileEncoding)
    {
        $CodeCoverageOutputFileEncoding = 'ascii'
    }

    "`tCodeCoverageOutputFileEncoding = $CodeCoverageOutputFileEncoding"
    ""

    if ($CodeCoverageThreshold -eq 0)
    {
        Write-Build -Color 'Green' -Text 'Coverage bypassed. Nothing to convert.'

        return
    }

    $PesterResultObjectClixml = Join-Path $PesterOutputFolder "PesterObject_$pesterOutputFileFileName"

    Write-Build -Color 'White' -Text "`tPester Output Object = $PesterResultObjectClixml"

    if (-not (Test-Path -Path $PesterResultObjectClixml))
    {
        throw "No command were tested, nothing to convert."
    }
    else
    {
        $pesterObject = Import-Clixml -Path $PesterResultObjectClixml
    }

    # Get all missed commands that are in the main module file.
    $missedCommands = $pesterObject.CodeCoverage.MissedCommands |
        Where-Object -FilterScript { $_.File -match [RegEx]::Escape($moduleFileName) }

    # Get all hit commands that are in the main module file.
    $hitCommands = $pesterObject.CodeCoverage.HitCommands |
        Where-Object -FilterScript { $_.File -match [RegEx]::Escape($moduleFileName) }

    <#
        The command Convert-LineNumber uses 'PassThru' very strange. It is needed
        to update the content of passed in object correctly (from the pipeline in
        this case). When using PassThru the command adds the properties SourceFile
        and SourceLineNumber.
 
        The command Convert-LineNumber is part of ModuleBuilder.
    #>

    $missedCommands | Convert-LineNumber -ErrorAction 'Stop' -PassThru | Out-Null
    $hitCommands | Convert-LineNumber -ErrorAction 'Stop' -PassThru | Out-Null

    # Blank line in output.
    ""

    Write-Build -Color 'White' -Text "Missed commands in source files:"

    # Output missed commands to visualize it in the pipeline output.
    $allMissedCommandsInSourceFiles = $missedCommands + (
        $pesterObject.CodeCoverage.MissedCommands |
            Where-Object -FilterScript { $_.File -notmatch [RegEx]::Escape($moduleFileName) }
    )

    $allMissedCommandsInSourceFiles |
        Select-Object @{
            Name = 'File'
            Expr = {
                if ($_.SourceFile)
                {
                    $_.SourceFile
                }
                else
                {
                    $_.File
                }
            }
        },
        @{
            Name = 'Line'
            Expr = {
                if ($_.SourceLineNumber)
                {
                    $_.SourceLineNumber
                }
                else
                {
                    $_.Line
                }
            }
        }, Function, Command |
            Out-String

    # Blank line in output.
    ""

    Write-Build -Color 'White' -Text "Converting coverage file."

    <#
        Cannot find a good example how package and class relate to PowerShell.
        This implementation tries to mimic what Pester outputs in its coverage
        file.
    #>


    Write-Build -Color 'DarkGray' -Text "`tBuilding new code coverage file against source."

    [System.Xml.XmlDocument] $coverageXml = ''

    <#
        This need to be set on Windows PowerShell even if it is already $null
        otherwise 'CreateDocumentType()' below will try to load the DTD. This
        does not happen on PowerShell and this line is not needed it Windows
        PowerShell is not used at all. Seems that setting this property changes
        something internal in [System.Xml.XmlDocument].
        See https://stackoverflow.com/questions/11135343/xml-documenttype-method-createdocumenttype-crashes-if-dtd-is-absent-net-c-sharp.
    #>

    $coverageXml.XmlResolver = $null

    # XML header.
    $xmlDeclaration = $coverageXml.CreateXmlDeclaration('1.0', 'UTF-8', 'no')

    # DTD: https://www.jacoco.org/jacoco/trunk/coverage/report.dtd
    $xmlDocumentType = $coverageXml.CreateDocumentType('report', '-//JACOCO//DTD Report 1.1//EN', 'report.dtd', $null)

    $coverageXml.AppendChild($xmlDeclaration) | Out-Null
    $coverageXml.AppendChild($xmlDocumentType) | Out-Null

    # Root element 'report'.
    $xmlElementReport = $coverageXml.CreateNode('element', 'report', $null)
    $xmlElementReport.SetAttribute('name', 'Sampler ({0})' -f (Get-Date).ToString('yyyy-mm-dd HH:mm:ss'))

    <#
        Child element 'sessioninfo'.
 
        The attributes 'start' and 'dump' is the time it took to run the tests in
        milliseconds, but it is not used in the end, we just add a plausible number
        here so it passes the referenced DTD, or any other parsing that might be done
        in the future.
    #>

    $testRunLengthInMilliseconds = 1785237 # ~30 minutes

    [System.Int64] $sessionInfoEndTime = [System.Math]::Floor((New-TimeSpan -Start (Get-Date -Date '01/01/1970') -End (Get-Date)).TotalMilliseconds)
    [System.Int64] $sessionInfoStartTime = [System.Math]::Floor($sessionInfoEndTime - $testRunLengthInMilliseconds)

    $xmlElementSessionInfo = $coverageXml.CreateNode('element', 'sessioninfo', $null)
    $xmlElementSessionInfo.SetAttribute('id', 'this')
    $xmlElementSessionInfo.SetAttribute('start', $sessionInfoStartTime)
    $xmlElementSessionInfo.SetAttribute('dump', $sessionInfoEndTime)
    $xmlElementReport.AppendChild($xmlElementSessionInfo) | Out-Null

    <#
        This is how each object in $allCommands looks like:
 
        # A method in a PowerShell class located in the Classes folder.
        File : C:\source\DnsServerDsc\output\MyModule\1.0.0\MyModule.psm1
        Line : 168
        StartLine : 168
        EndLine : 168
        StartColumn : 25
        EndColumn : 36
        Class : ResourceBase
        Function : Compare
        Command : $currentState = $this.Get() | ConvertTo-HashTableFromObject
        HitCount : 86
        SourceFile : .\Classes\001.ResourceBase.ps1
        SourceLineNumber : 153
 
        # A function located in private or public folder.
        File : C:\source\DnsServerDsc\output\MyModule\1.0.0\MyModule.psm1
        Line : 2658
        StartLine : 2658
        EndLine : 2658
        StartColumn : 26
        EndColumn : 29
        Class :
        Function : Get-LocalizedDataRecursive
        Command : $localizedData = @{}
        HitCount : 225
        SourceFile : .\Private\Get-LocalizedDataRecursive.ps1
        SourceLineNumber : 35
    #>

    $allCommands = $hitCommands + $missedCommands

    $sourcePathFolderName = (Split-Path -Path $SourcePath -Leaf) -replace '\\','/'

    $reportCounterInstruction = @{
        Missed  = 0
        Covered = 0
    }

    $reportCounterLine = @{
        Missed  = 0
        Covered = 0
    }

    $reportCounterMethod = @{
        Missed  = 0
        Covered = 0
    }

    $reportCounterClass = @{
        Missed  = 0
        Covered = 0
    }

    $packageCounterInstruction = @{
        Missed  = 0
        Covered = 0
    }

    $packageCounterLine = @{
        Missed  = 0
        Covered = 0
    }

    $packageCounterMethod = @{
        Missed  = 0
        Covered = 0
    }

    $packageCounterClass = @{
        Missed  = 0
        Covered = 0
    }

    $allSourceFileElements = @()

    # This is what the user expects to see.
    $packageDisplayName = $sourcePathFolderName

    # The module version is what is expected to be in the XML.
    $xmlPackageName = $ModuleVersionFolder

    Write-Debug -Message ('Creating XML output for JaCoCo package ''{0}''.' -f $packageDisplayName)

    <#
        Child element 'package'.
 
        This implementation assumes the attribute 'name' of the element 'package'
        should be the path to the folder that contains the PowerShell script files
        (relative from GitHub repository root).
    #>

    $xmlElementPackage = $coverageXml.CreateElement('package')
    $xmlElementPackage.SetAttribute('name', $xmlPackageName)

    $commandsGroupedOnSourceFile = $allCommands | Group-Object -Property 'SourceFile'

    foreach ($jaCocoClass in $commandsGroupedOnSourceFile)
    {
        $classCounterInstruction = @{
            Missed  = 0
            Covered = 0
        }

        $classCounterLine = @{
            Missed  = 0
            Covered = 0
        }

        $classCounterMethod = @{
            Missed  = 0
            Covered = 0
        }

        $classDisplayName = ($jaCocoClass.Name -replace '^\.', $sourcePathFolderName) -replace '\\','/'

        # The module version is what is expected to be in the XML.
        $sourceFilePath = ($jaCocoClass.Name -replace '^\.', $ModuleVersionFolder) -replace '\\','/'

        <#
            Get class name if it exist, otherwise use function name. The first
            object should in the array should give us the right information.
        #>

        $xmlClassName = if ([System.String]::IsNullOrEmpty($jaCocoClass.Group[0].Class))
        {
            if ([System.String]::IsNullOrEmpty($jaCocoClass.Group[0].Function))
            {
                '<script>'
            }
            else
            {
                $jaCocoClass.Group[0].Function
            }
        }
        else
        {
            $jaCocoClass.Group[0].Class
        }

        $sourceFileName = $sourceFilePath -replace [regex]::Escape('{0}/' -f $ModuleVersionFolder)

        Write-Debug -Message ("`tCreating XML output for JaCoCo class '{0}'." -f $classDisplayName)

        # Child element 'class'.
        $xmlElementClass = $coverageXml.CreateElement('class')
        $xmlElementClass.SetAttribute('name', $xmlClassName)
        $xmlElementClass.SetAttribute('sourcefilename', $sourceFileName)

        <#
            This assumes that a value in property Function is never $null. Test
            showed that commands at script level is assigned empty string in the
            Function property, so it should work for missed and hit commands at
            script level too.
 
            Sorting the objects after StartLine so they come in the order
            they appear in the code file. Also, it is necessary for the
            command Update-JoCaCoStatistic to work.
        #>

        $commandsGroupedOnFunction = $jaCocoClass.Group |
                Group-Object -Property 'Function' |
                Sort-Object -Property {
                    # Find the first line for each method.
                    ($_.Group.SourceLineNumber | Measure-Object -Minimum).Minimum
                }

        foreach ($jaCoCoMethod in $commandsGroupedOnFunction)
        {
            $functionName = if ([System.String]::IsNullOrEmpty($jaCoCoMethod.Name))
            {
                '<script>'
            }
            else
            {
                $jaCoCoMethod.Name
            }

            Write-Debug -Message ("`t`tCreating XML output for JaCoCo method '{0}'." -f $functionName)

            <#
                Sorting all commands in ascending order and using the first
                'SourceLineNumber' as the first line of the method. Assuming
                every code line for the method was in either $missedCommands
                or $hitCommands which the sorting is based on.
            #>

            $methodFirstLine = $jaCoCoMethod.Group |
                Sort-Object -Property 'SourceLineNumber' |
                    Select-Object -First 1 -ExpandProperty 'SourceLineNumber'

            # Child element 'method'.
            $xmlElementMethod = $coverageXml.CreateElement('method')
            $xmlElementMethod.SetAttribute('name', $functionName)
            $xmlElementMethod.SetAttribute('desc', '()')
            $xmlElementMethod.SetAttribute('line', $methodFirstLine)

            <#
                Documentation for counters:
                https://www.jacoco.org/jacoco/trunk/doc/counters.html
            #>


            <#
                Child element 'counter' and type INSTRUCTION.
 
                Each command can be hit multiple times, the INSTRUCTION counts
                how many times the command was hit or missed.
            #>

            $numberOfInstructionsCovered = (
                $jaCoCoMethod.Group |
                    Where-Object -FilterScript {
                        $_.HitCount -ge 1
                    }
            ).Count

            $numberOfInstructionsMissed = (
                $jaCoCoMethod.Group |
                    Where-Object -FilterScript {
                        $_.HitCount -eq 0
                    }
            ).Count

            $xmlElementCounterMethodInstruction = $coverageXml.CreateElement('counter')
            $xmlElementCounterMethodInstruction.SetAttribute('type', 'INSTRUCTION')
            $xmlElementCounterMethodInstruction.SetAttribute('missed', $numberOfInstructionsMissed)
            $xmlElementCounterMethodInstruction.SetAttribute('covered', $numberOfInstructionsCovered)
            $xmlElementMethod.AppendChild($xmlElementCounterMethodInstruction) | Out-Null

            $classCounterInstruction.Covered += $numberOfInstructionsCovered
            $classCounterInstruction.Missed += $numberOfInstructionsMissed

            $packageCounterInstruction.Covered += $numberOfInstructionsCovered
            $packageCounterInstruction.Missed += $numberOfInstructionsMissed

            $reportCounterInstruction.Covered += $numberOfInstructionsCovered
            $reportCounterInstruction.Missed += $numberOfInstructionsMissed

            <#
                Child element 'counter' and type LINE.
 
                The LINE counts how many unique lines that was hit or missed.
            #>

            $numberOfLinesCovered = (
                $jaCoCoMethod.Group |
                    Where-Object -FilterScript {
                        $_.HitCount -ge 1
                    } |
                        Sort-Object -Property 'SourceLineNumber' -Unique
            ).Count

            $numberOfLinesMissed = (
                $jaCoCoMethod.Group |
                    Where-Object -FilterScript {
                        $_.HitCount -eq 0
                    } |
                        Sort-Object -Property 'SourceLineNumber' -Unique
            ).Count

            $xmlElementCounterMethodLine = $coverageXml.CreateElement('counter')
            $xmlElementCounterMethodLine.SetAttribute('type', 'LINE')
            $xmlElementCounterMethodLine.SetAttribute('missed', $numberOfLinesMissed)
            $xmlElementCounterMethodLine.SetAttribute('covered', $numberOfLinesCovered)
            $xmlElementMethod.AppendChild($xmlElementCounterMethodLine) | Out-Null

            $classCounterLine.Covered += $numberOfLinesCovered
            $classCounterLine.Missed += $numberOfLinesMissed

            $packageCounterLine.Covered += $numberOfLinesCovered
            $packageCounterLine.Missed += $numberOfLinesMissed

            $reportCounterLine.Covered += $numberOfLinesCovered
            $reportCounterLine.Missed += $numberOfLinesMissed

            <#
                Child element 'counter' and type METHOD.
 
                The METHOD counts as covered if at least one line was hit in
                the method. This value seem not to be higher than 1, assuming
                that is true.
            #>

            $isLineInMethodCovered = (
                $jaCoCoMethod.Group |
                    Where-Object -FilterScript {
                        $_.HitCount -ge 1
                    }
            ).Count

            <#
                If at least one instructions was covered in the method, then
                method was covered.
            #>

            if ($isLineInMethodCovered)
            {
                $methodCovered = 1
                $methodMissed = 0

                $classCounterMethod.Covered += 1

                $packageCounterMethod.Covered += 1

                $reportCounterMethod.Covered += 1
            }
            else
            {
                $methodCovered = 0
                $methodMissed = 1

                $classCounterMethod.Missed += 1

                $packageCounterMethod.Missed += 1

                $reportCounterMethod.Missed += 1
            }

            $xmlElementCounterMethod = $coverageXml.CreateElement('counter')
            $xmlElementCounterMethod.SetAttribute('type', 'METHOD')
            $xmlElementCounterMethod.SetAttribute('missed', $methodMissed)
            $xmlElementCounterMethod.SetAttribute('covered', $methodCovered)
            $xmlElementMethod.AppendChild($xmlElementCounterMethod) | Out-Null

            $xmlElementClass.AppendChild($xmlElementMethod) | Out-Null
        }

        $xmlElementCounter_ClassInstruction = $coverageXml.CreateElement('counter')
        $xmlElementCounter_ClassInstruction.SetAttribute('type', 'INSTRUCTION')
        $xmlElementCounter_ClassInstruction.SetAttribute('missed', $classCounterInstruction.Missed)
        $xmlElementCounter_ClassInstruction.SetAttribute('covered', $classCounterInstruction.Covered)
        $xmlElementClass.AppendChild($xmlElementCounter_ClassInstruction) | Out-Null

        $xmlElementCounter_ClassLine = $coverageXml.CreateElement('counter')
        $xmlElementCounter_ClassLine.SetAttribute('type', 'LINE')
        $xmlElementCounter_ClassLine.SetAttribute('missed', $classCounterLine.Missed)
        $xmlElementCounter_ClassLine.SetAttribute('covered', $classCounterLine.Covered)
        $xmlElementClass.AppendChild($xmlElementCounter_ClassLine) | Out-Null

        if ($classCounterLine.Covered -gt 1)
        {
            $classCovered = 1
            $classMissed = 0

            $packageCounterClass.Covered += 1

            $reportCounterClass.Covered += 1
        }
        else
        {
            $classCovered = 0
            $classMissed = 1

            $packageCounterClass.Missed += 1

            $reportCounterClass.Missed += 1
        }

        $xmlElementCounter_ClassMethod = $coverageXml.CreateElement('counter')
        $xmlElementCounter_ClassMethod.SetAttribute('type', 'METHOD')
        $xmlElementCounter_ClassMethod.SetAttribute('missed', $classCounterMethod.Missed)
        $xmlElementCounter_ClassMethod.SetAttribute('covered', $classCounterMethod.Covered)
        $xmlElementClass.AppendChild($xmlElementCounter_ClassMethod) | Out-Null

        $xmlElementCounter_Class = $coverageXml.CreateElement('counter')
        $xmlElementCounter_Class.SetAttribute('type', 'CLASS')
        $xmlElementCounter_Class.SetAttribute('missed', $classMissed)
        $xmlElementCounter_Class.SetAttribute('covered', $classCovered)
        $xmlElementClass.AppendChild($xmlElementCounter_Class) | Out-Null

        $xmlElementPackage.AppendChild($xmlElementClass) | Out-Null

        <#
            Child element 'sourcefile'.
 
            Add sourcefile element to an array for each class. The array
            will be added to the XML document at the end of the package
            loop.
        #>

        $xmlElementSourceFile = $coverageXml.CreateElement('sourcefile')
        $xmlElementSourceFile.SetAttribute('name', $sourceFileName)

        $linesToReport = @()

        # Get all instructions that was covered by grouping on 'SourceLineNumber'.
        $linesCovered = $jaCocoClass.Group |
            Sort-Object -Property 'SourceLineNumber' |
                Where-Object {
                    $_.HitCount -ge 1
                } |
                    Group-Object -Property 'SourceLineNumber' -NoElement

        # Add each covered line with its count of instructions covered.
        $linesCovered |
            ForEach-Object {
                $linesToReport += @{
                    Line    = [System.UInt32] $_.Name
                    Covered = $_.Count
                    Missed  = 0
                }
            }

        # Get all instructions that was missed by grouping on 'SourceLineNumber'.
        $linesMissed = $jaCocoClass.Group |
            Sort-Object -Property 'SourceLineNumber' |
                Where-Object {
                    $_.HitCount -eq 0
                } |
                    Group-Object -Property 'SourceLineNumber' -NoElement

        # Add each missed line with its count of instructions missed.
        $linesMissed |
            ForEach-Object {
                # Test if there are an existing line that is covered.
                if ($linesToReport.Line -contains $_.Name)
                {
                    $lineNumberToLookup = $_.Name

                    $coveredLineItem = $linesToReport |
                        Where-Object -FilterScript {
                            $_.Line -eq $lineNumberToLookup
                        }

                    $coveredLineItem.Missed += $_.Count
                }
                else
                {
                    $linesToReport += @{
                        Line    = [System.UInt32] $_.Name
                        Covered = 0
                        Missed  = $_.Count
                    }
                }
            }

        $linesToReport |
            Sort-Object -Property 'Line' |
                ForEach-Object -Process {
                    $xmlElementLine = $coverageXml.CreateElement('line')
                    $xmlElementLine.SetAttribute('nr', $_.Line)

                    <#
                        Child element 'line'.
 
                        These attributes are best explained here:
                        https://stackoverflow.com/questions/33868761/how-to-interpret-the-jacoco-xml-file
                    #>


                    $xmlElementLine.SetAttribute('mi', $_.Missed)
                    $xmlElementLine.SetAttribute('ci', $_.Covered)
                    $xmlElementLine.SetAttribute('mb', 0)
                    $xmlElementLine.SetAttribute('cb', 0)

                    $xmlElementSourceFile.AppendChild($xmlElementLine) |
                        Out-Null
                    }

        <#
            Add counters to sourcefile element. Reuses those element that was
            created for the class element, as they will be the same.
        #>

        $xmlElementSourceFile.AppendChild($xmlElementCounter_ClassInstruction.CloneNode($false)) | Out-Null
        $xmlElementSourceFile.AppendChild($xmlElementCounter_ClassLine.CloneNode($false)) | Out-Null
        $xmlElementSourceFile.AppendChild($xmlElementCounter_ClassMethod.CloneNode($false)) | Out-Null
        $xmlElementSourceFile.AppendChild($xmlElementCounter_Class.CloneNode($false)) | Out-Null

        $allSourceFileElements += $xmlElementSourceFile
    } # end class loop

    # Add all sourcefile elements that was generated in the class-element-loop.
    $allSourceFileElements |
        ForEach-Object -Process {
            $xmlElementPackage.AppendChild($_) | Out-Null
        }

    # Add counters at the package level.
    $xmlElementCounter_PackageInstruction = $coverageXml.CreateElement('counter')
    $xmlElementCounter_PackageInstruction.SetAttribute('type', 'INSTRUCTION')
    $xmlElementCounter_PackageInstruction.SetAttribute('missed', $packageCounterInstruction.Missed)
    $xmlElementCounter_PackageInstruction.SetAttribute('covered', $packageCounterInstruction.Covered)
    $xmlElementPackage.AppendChild($xmlElementCounter_PackageInstruction) | Out-Null

    $xmlElementCounter_PackageLine = $coverageXml.CreateElement('counter')
    $xmlElementCounter_PackageLine.SetAttribute('type', 'LINE')
    $xmlElementCounter_PackageLine.SetAttribute('missed', $packageCounterLine.Missed)
    $xmlElementCounter_PackageLine.SetAttribute('covered', $packageCounterLine.Covered)
    $xmlElementPackage.AppendChild($xmlElementCounter_PackageLine) | Out-Null

    $xmlElementCounter_PackageMethod = $coverageXml.CreateElement('counter')
    $xmlElementCounter_PackageMethod.SetAttribute('type', 'METHOD')
    $xmlElementCounter_PackageMethod.SetAttribute('missed', $packageCounterMethod.Missed)
    $xmlElementCounter_PackageMethod.SetAttribute('covered', $packageCounterMethod.Covered)
    $xmlElementPackage.AppendChild($xmlElementCounter_PackageMethod) | Out-Null

    $xmlElementCounter_PackageClass = $coverageXml.CreateElement('counter')
    $xmlElementCounter_PackageClass.SetAttribute('type', 'CLASS')
    $xmlElementCounter_PackageClass.SetAttribute('missed', $packageCounterClass.Missed)
    $xmlElementCounter_PackageClass.SetAttribute('covered', $packageCounterClass.Covered)
    $xmlElementPackage.AppendChild($xmlElementCounter_PackageClass) | Out-Null

    $xmlElementReport.AppendChild($xmlElementPackage) | Out-Null

    # Add counters at the report level.
    $xmlElementCounter_ReportInstruction = $coverageXml.CreateElement('counter')
    $xmlElementCounter_ReportInstruction.SetAttribute('type', 'INSTRUCTION')
    $xmlElementCounter_ReportInstruction.SetAttribute('missed', $reportCounterInstruction.Missed)
    $xmlElementCounter_ReportInstruction.SetAttribute('covered', $reportCounterInstruction.Covered)
    $xmlElementReport.AppendChild($xmlElementCounter_ReportInstruction) | Out-Null

    $xmlElementCounter_ReportLine = $coverageXml.CreateElement('counter')
    $xmlElementCounter_ReportLine.SetAttribute('type', 'LINE')
    $xmlElementCounter_ReportLine.SetAttribute('missed', $reportCounterLine.Missed)
    $xmlElementCounter_ReportLine.SetAttribute('covered', $reportCounterLine.Covered)
    $xmlElementReport.AppendChild($xmlElementCounter_ReportLine) | Out-Null

    $xmlElementCounter_ReportMethod = $coverageXml.CreateElement('counter')
    $xmlElementCounter_ReportMethod.SetAttribute('type', 'METHOD')
    $xmlElementCounter_ReportMethod.SetAttribute('missed', $reportCounterMethod.Missed)
    $xmlElementCounter_ReportMethod.SetAttribute('covered', $reportCounterMethod.Covered)
    $xmlElementReport.AppendChild($xmlElementCounter_ReportMethod) | Out-Null

    $xmlElementCounter_ReportClass = $coverageXml.CreateElement('counter')
    $xmlElementCounter_ReportClass.SetAttribute('type', 'CLASS')
    $xmlElementCounter_ReportClass.SetAttribute('missed', $reportCounterClass.Missed)
    $xmlElementCounter_ReportClass.SetAttribute('covered', $reportCounterClass.Covered)
    $xmlElementReport.AppendChild($xmlElementCounter_ReportClass) | Out-Null

    $coverageXml.AppendChild($xmlElementReport) | Out-Null

    if ($DebugPreference -ne 'SilentlyContinue')
    {
        $StringWriter = New-Object -TypeName 'System.IO.StringWriter'
        $XmlWriter = New-Object -TypeName 'System.XMl.XmlTextWriter' -ArgumentList $StringWriter

        $xmlWriter.Formatting = 'indented'
        $xmlWriter.Indentation = 2

        $coverageXml.WriteContentTo($XmlWriter)

        $XmlWriter.Flush()

        $StringWriter.Flush()

        # Blank row in output
        ""

        Write-Debug -Message ($StringWriter.ToString() | Out-String)
    }

    $newCoverageFilePath = Join-Path -Path $PesterOutputFolder -ChildPath 'source_coverage.xml'

    Write-Build -Color 'DarkGray' -Text "`tWriting converted code coverage file to '$newCoverageFilePath'."

    $xmlSettings = New-Object -TypeName 'System.Xml.XmlWriterSettings'
    $xmlSettings.Indent = $true
    $xmlSettings.Encoding = [System.Text.Encoding]::$CodeCoverageOutputFileEncoding

    $xmlWriter = [System.Xml.XmlWriter]::Create($newCoverageFilePath, $xmlSettings)

    $coverageXml.Save($xmlWriter)

    $xmlWriter.Close()

    Write-Build -Color 'DarkGray' -Text "`tImporting original code coverage file '$CodeCoverageOutputFile'."

    $originalXml = New-Object -TypeName 'System.Xml.XmlDocument'

    <#
        This need to be set on Windows PowerShell even if it is already $null
        otherwise 'Load()' below will try to load the DTD. This
        does not happen on PowerShell and this line is not needed it Windows
        PowerShell is not used at all. Seems that setting this property changes
        something internal in [System.Xml.XmlDocument].
        See https://stackoverflow.com/questions/11135343/xml-documenttype-method-createdocumenttype-crashes-if-dtd-is-absent-net-c-sharp.
    #>

    $originalXml.XmlResolver = $null

    $originalXml.Load($CodeCoverageOutputFile)

    $codeCoverageOutputBackupFile = $CodeCoverageOutputFile -replace '\.xml', '.xml.bak'
    $newCoverageFilePath = Join-Path -Path $PesterOutputFolder -ChildPath $codeCoverageOutputBackupFile

    Write-Build -Color 'DarkGray' -Text "`tWriting a backup of original code coverage file to '$codeCoverageOutputBackupFile'."

    $xmlSettings = New-Object -TypeName 'System.Xml.XmlWriterSettings'
    $xmlSettings.Indent = $true
    $xmlSettings.Encoding = [System.Text.Encoding]::$CodeCoverageOutputFileEncoding

    $xmlWriter = [System.Xml.XmlWriter]::Create($codeCoverageOutputBackupFile, $xmlSettings)

    $originalXml.Save($xmlWriter)

    $xmlWriter.Close()

    Write-Build -Color 'DarkGray' -Text "`tRemoving XML node from original code coverage."

    $xPath = '//package[@name="{0}"]' -f $ModuleVersionFolder

    Write-Build -Color 'DarkGray' -Text "`t`tUsing XPath: '$xPath'."

    $elementToRemove = Select-XML -Xml $originalXml -XPath $xPath

    if ($elementToRemove)
    {
        $elementToRemove.Node.ParentNode.RemoveChild($elementToRemove.Node) | Out-Null
    }

    Write-Build -Color 'DarkGray' -Text "`tMerging temporary code coverage file with the original code coverage file."

    $targetXmlDocument = Merge-JaCoCoReport -OriginalDocument $originalXml -MergeDocument $coverageXml

    Write-Build -Color 'DarkGray' -Text "`tUpdating statistics in the new code coverage file."

    $targetXmlDocument = Update-JaCoCoStatistic -Document $targetXmlDocument

    Write-Build -Color 'DarkGray' -Text ("`tUpdating path to include source folder '{0}' in the package element in the coverage file." -f $sourcePathFolderName)

    Select-Xml -Xml $targetXmlDocument -XPath '//package' |
        ForEach-Object -Process {
            $_.Node.name = $_.Node.name -replace '^\d+\.\d+\.\d+', $sourcePathFolderName
        }

    Write-Build -Color 'DarkGray' -Text "`tWriting back updated code coverage file to '$CodeCoverageOutputFile'."

    $xmlSettings = New-Object -TypeName 'System.Xml.XmlWriterSettings'
    $xmlSettings.Indent = $true
    $xmlSettings.Encoding = [System.Text.Encoding]::$CodeCoverageOutputFileEncoding

    $xmlWriter = [System.Xml.XmlWriter]::Create($CodeCoverageOutputFile, $xmlSettings)

    $targetXmlDocument.Save($xmlWriter)

    $xmlWriter.Close()

    Write-Build -Color Green -Text 'Code Coverage successfully converted.'
}