AzDOTestResults.psm1

using namespace System.IO
using namespace System.Text

Set-StrictMode -Version 'Latest'
#Requires -Version 5.0

$PSDefaultParameterValues.Clear()

function Get-BuildEnvironment {
<#
  .SYNOPSIS
  Gathers the appropriate build environment variables in Azure DevOps
  required for the download process and returns a custom object containing
  the values
#>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param()
    process {
        [PSCustomObject](@{
                OrganizationUri = $env:SYSTEM_TEAMFOUNDATIONSERVERURI
                Project         = $env:SYSTEM_TEAMPROJECT
                AccessToken     = $env:SYSTEM_ACCESSTOKEN
                BuildUri        = $env:BUILD_BUILDURI
                CommonTestResultsFolder = $env:COMMON_TESTRESULTSDIRECTORY
                TempTestResultsFolder = "$env:AGENT_TEMPDIRECTORY/TestResults"
                ProjectUri      = "$($env:SYSTEM_TEAMFOUNDATIONSERVERURI)/$($env:SYSTEM_TEAMPROJECT)"
            })
    }
}

function Get-AuthorizationHeader {
<#
  .SYNOPSIS
  Creates the HTTP Authorization Header.
#>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Token
    )

    process {
        "Basic " + [Convert]::ToBase64String([Encoding]::ASCII.GetBytes(("{0}:{1}" -f '', $Token)))
    }
}

function Get-TrxAttachmentList {
<#
  .SYNOPSIS
  Gets the list of coverage attachments from a TRX file.
#>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $FilePath
    )

    process {
        $xml = [xml] (Get-Content $FilePath -ErrorAction Stop)
        $ns = New-Object Xml.XmlNamespaceManager $xml.NameTable
        $ns.AddNamespace( "ns", 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010')
        $nodes = $xml.SelectNodes('//ns:UriAttachments/ns:UriAttachment/ns:A/@href', $ns) | Select-Object -ExpandProperty '#text'
        $nodes
    }
}

function Get-TestRunList {
<#
  .SYNOPSIS
  Invokes an Azure DevOps REST call to retrieve the list of test runs associated with the build
 
  .PARAMETER BuildUri
  The URI of the build associated with the tests
 
  .PARAMETER BaseUri
  The base URI containing the organization name and project name, such
  as https://dev.azure.com/myOrg/myProject.
 
  .PARAMETER AccessToken
  A PAT token or build access token providing authorization for the request
#>

    [CmdletBinding()]
    [OutputType([psobject[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $BuildUri,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $BaseUri,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $AccessToken
    )

    process {
        $AuthHeader = Get-AuthorizationHeader $AccessToken

        $params = @{
            Uri     = "$BaseUri/_apis/test/runs?api-version=5.0&buildUri=$BuildUri"
            Headers = @{
                Authorization = $AuthHeader
                Accept        = 'application/json'
            }
            Method  = 'Get'
        }

        $content = (Invoke-WebRequestWithRetry -Parameters $params).Content
        Write-Verbose "Received $content"
        ($content | ConvertFrom-Json).value
    }
}

function Get-TestAttachmentList {
<#
  .SYNOPSIS
  Invokes an Azure DevOps REST call to retrieve the test run details for a specific test
 
  .PARAMETER TestUri
  The URI for retrieving the test details
 
  .PARAMETER AccessToken
  A PAT token or build access token providing authorization for the request
#>

    [CmdletBinding()]
    [OutputType([psobject[]])]
    param(

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $TestUri,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $AccessToken
    )

    process {
        $AuthHeader = Get-AuthorizationHeader $AccessToken

        $params = @{
            Uri     = "$TestUri/attachments?api-version=5.0-preview.1"
            Headers = @{
                Authorization = $AuthHeader
                Accept        = 'application/json'
            }
            Method  = 'Get'
        }

        $content = (Invoke-WebRequestWithRetry -Parameters $params).Content
        Write-Verbose "Received $content"
        [PsCustomObject[]]($content | ConvertFrom-Json).value
    }
}

function Get-TestAttachment {
<#
  .SYNOPSIS
  Invokes an Azure DevOps REST call to retrieve a specific test attachment
 
  .PARAMETER AttachmentUri
  The URI for retrieving the test attachment
 
  .PARAMETER OutputPath
  The absolute path to a folder or file where the content should be saved.
 
  .PARAMETER AccessToken
  A PAT token or build access token providing authorization for the request
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $AttachmentUri,


        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $OutputPath,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $AccessToken
    )

    process {
        $AuthHeader = Get-AuthorizationHeader $AccessToken

        $params = @{
            Uri     = $AttachmentUri
            Headers = @{
                Authorization = $AuthHeader
                Accept        = 'application/octet-stream'
            }
            Method  = 'Get'
            OutFile = $OutputPath
        }

        Write-Verbose "Downloading '$AttachmentUri' to '$OutputPath'"
        Invoke-WebRequestWithRetry $params
    }
}

function Join-FilePath {
<#
  .SYNOPSIS
  Combines two filesystem paths and returns the full path, with proper OS-specific
  directory separators
#>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $Path,

        [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $ChildPath
    )
    process {
        $combinedPath = [Path]::Combine($Path, $ChildPath)
        [Path]::GetFullPath($combinedPath)
    }
}

function Invoke-WebRequestWithRetry {
<#
  .SYNOPSIS
  A variant of the Invoke-WebRequest method which supports automatic retries
#>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [hashtable] $Parameters,

        [ValidateRange(1, 100)]
        [int] $MaxRetries = 3,

        [ValidateRange(0, 16000)]
        [int] $SleepTime = 1000
    )

    process {
        $isComplete = $true
        $retryCount = 0
        do {
            try {
                $result = Invoke-WebRequest @Parameters -ErrorAction Stop -UseBasicParsing
                $isComplete = $true
                $result
            }
            catch {
                Write-Verbose $_.Exception
                if ($retryCount -ge $MaxRetries) {
                    $isComplete = $true
                    Throw (New-Object InvalidOperationException("Failed after $MaxRetries retries, $_.Exception"))
                }
                else {
                    $retryCount ++
                    $isComplete = $false
                    Write-Verbose "Failed: Retry $retryCount of $MaxRetries"
                    Start-Sleep -Milliseconds $SleepTime
                }
            }
        } while (-not $isComplete)
    }
}

function Group-TestAttachmentList {
<#
  .SYNOPSIS
  Gathers the list of attachments into files which represent Test Run Summaries (TRX files)
  and all other content
#>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNull()]
        [PSCustomObject[]] $Attachments
    )

    process {
        $trxFiles = New-Object System.Collections.ArrayList
        $otherFiles = New-Object System.Collections.ArrayList
        foreach ($attachment in $Attachments) {
            if ('attachmentType' -in $attachment.PSobject.Properties.name) {
                $attachmentType = ($attachment | Select-Object -ExpandProperty 'attachmentType' -First 1)
                if ('tmiTestRunSummary' -eq $attachmentType) {
                    [void]$trxFiles.Add($attachment)
                }
                else {
                    [void]$otherFiles.Add($attachment)
                }
            }
            else {
                $extension = [Path]::GetExtension($attachment.fileName)
                if ('.trx' -eq $extension) {
                    [void]$trxFiles.Add($attachment)
                }
                else {
                    [void]$otherFiles.Add($attachment)
                }
            }
        }
        [PsCustomObject]@{
            TrxContent   = $trxFiles.ToArray()
            OtherContent = $otherFiles.ToArray()
        }
    }
}

function Get-GroupedAttachmentList {
<#
  .SYNOPSIS
  Downloads the list of test attachments and groups the results of files which represent
  Test Run Summaries (TRX files) and all other content
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Uri] $TestUri,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $AccessToken
    )
    process {
        $attachments = Get-TestAttachmentList -TestUri $TestUri -AccessToken $AccessToken
        Group-TestAttachmentList -Attachments $attachments
    }
}

function Get-TrxContent {
<#
  .SYNOPSIS
  Downloads the TRX file and returns an array of expected child content paths.
#>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNull()]
        [PsCustomObject[]] $Files,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)]
        [ValidateNotNull()]
        [string] $OutputFolder,

        [Parameter(ValueFromPipeline = $true)]
        [string] $TrxDependencyPath = '$trxFolder/In/$folder'
    )
    process {
        $trxChildPaths = New-Object System.Collections.ArrayList
        foreach ($trxFile in $Files) {
            Write-Verbose "Downloading TRX: $($trxFile.fileName)"
            $trxFolder = [Path]::GetFileNameWithoutExtension($trxFile.fileName).Replace(' ', '_')
            $trxDirectoryName = Join-FilePath -Path $OutputFolder -ChildPath $trxFolder
            Write-Verbose "Configuring TRX folder: $trxDirectoryName"
            $trxAttachments = Get-TrxAttachmentList -FilePath (Join-FilePath -Path $OutputFolder -ChildPath $trxFile.fileName)
            Write-Verbose "Processing attachments"
            foreach ($node in $trxAttachments) {
                $normalizedNode = (Join-Path -Path '.' -ChildPath $node).Substring(2)
                $folder = [Path]::GetDirectoryName($normalizedNode)
                Write-Verbose "$node => $folder"
                if ($TrxDependencyPath.StartsWith('/') -or $TrxDependencyPath.StartsWith('\')) {
                    $TrxDependencyPath = $TrxDependencyPath.Substring(1)
                }

                $expandedPath = $ExecutionContext.InvokeCommand.ExpandString($TrxDependencyPath)
                if ($expandedPath) {
                    $nodePath = Join-FilePath -Path $OutputFolder -ChildPath $expandedPath
                }
                else {
                    $nodePath = $OutputFolder
                }

                $nodeFileName = [Path]::GetFileName($node)
                Write-Verbose "The file '$nodeFileName' will be stored at '$nodePath'"
                $path = Join-FilePath -Path $nodePath -ChildPath $nodeFileName
                [void]$trxChildPaths.Add($path)
            }
        }

        $trxChildPaths.ToArray()
    }
}

function Group-ChildContent {
<#
  .SYNOPSIS
  Determines the proper file locations for a set of files given the list of TRX child paths,
  the content files being downloaded, and the Output Folder and returns a hash list of file
  names and their destinations.
 
  .PARAMETER TrxContentList
  The list of paths for expected TRX child content
 
  .PARAMETER FileList
  The list of files to be examined
 
  .PARAMETER OutputFolder
  The output destination for non-TRX children
#>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNull()]
        [string[]] $TrxContentList,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)]
        [ValidateNotNull()]
        [string[]] $FileList,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [string] $OutputFolder
    )
    process {
        $fileHash = @{ }
        $files = $FileList | Get-Unique
        if ($null -ne $TrxContentList) {
            $childContent = $files | Where-Object { [Path]::GetExtension($_) -eq '.coverage' }
            # foreach child get the matching TRX reference
            foreach ($child in $childContent) {
                $outArr = [array]($TrxContentList | Where-Object { [Path]::GetFileName($_) -eq  $child })
                [void]$fileHash.Add($child, $outArr)
            }

            $simpleContent = $files | Where-Object { $childContent -notcontains $_ }
            $simpleContent | Foreach-Object { [void]$fileHash.Add($_,  "$OutputFolder/$_" ) }
        }
        else {
            $files | Foreach-Object { [void]$fileHash.Add($_, "$OutputFolder/$_" ) }
        }
        $fileHash
    }
}

function Copy-TestResultToCommon {
<#
 .SYNOPSIS
 Retrieves test results for SonarQube from the current Azure DevOps build and places them in the
 Common Test Results folder ($Common.TestResultsDirectory)
 
 .DESCRIPTION
 Retrieves the test attachments from a build and places them in appropriate locations
 for SonarQube. This method expects the Azure DevOps environment variables to be set
 in order to automatically determine the location and build identity. This method calls
 Copy-TestResult using the appropriate Azure DevOps environment variables.
 
 .EXAMPLE
 Copy-TestResultToCommon
#>

    [CmdletBinding()]
    param()
    process {
        $buildEnv = Get-BuildEnvironment
        Copy-TestResult -ProjectUri $buildEnv.ProjectUri -AccessToken $buildEnv.AccessToken -BuildUri $buildEnv.BuildUri -OutputFolder $buildEnv.CommonTestResultsFolder
    }
}

function Copy-TestResultToTemp {
    <#
     .SYNOPSIS
     Retrieves test results for SonarQube from the current Azure DevOps build and places them in the
     Common Test Results folder ($Agent.TempDirectory)/TestResults
 
     .DESCRIPTION
     Retrieves the test attachments from a build and places them in appropriate locations
     for SonarQube. This method expects the Azure DevOps environment variables to be set
     in order to automatically determine the location and build identity. This method calls
     Copy-TestResult using the appropriate Azure DevOps environment variables.
 
     .EXAMPLE
     Copy-TestResultToTemp
    #>

        [CmdletBinding()]
        param()
        process {
            $buildEnv = Get-BuildEnvironment
            Copy-TestResult -ProjectUri $buildEnv.ProjectUri -AccessToken $buildEnv.AccessToken -BuildUri $buildEnv.BuildUri -OutputFolder $buildEnv.TempTestResultsFolder
        }
}

function Copy-TestResult {
<#
 .SYNOPSIS
 Retrieves test results from a specific Azure DevOps build. If no build details are provided, the current
 build environment is used based on the Azure DevOps environment variables.
 
 .DESCRIPTION
 Retrieves the test attachments from a build and places them in a specified location.
 
 .PARAMETER OutputFolder
 The location for storing the test results. Tests will be organized based on the expected
 folder conventions for SonarQube and the contents of any downloaded TRX files.
 
 .PARAMETER ProjectUri
 The URI to the project root in Azure DevOps.
 
 .PARAMETER AccessToken
 The PAT token or authorization token to use for requesting the build details.
 
 .PARAMETER BuildUri
 The VSTFS URI for the build whose test results should be downloaded.
 
 .PARAMETER TrxDependencyPath
 The format string to use for creating child folders for the TRX file dependencies. The string can utilize a replacement variable, $folder,
 which indicates the folder path for a given dependency (as specified in the TRX file). A second variable, $trxFolder, is the safe folder
 based on the name of the TRX file. The default path is '$trxFolder/In/$folder'. Note that the path string should not be double-quoted
 when the replacement variables are used. All folder paths will be relative to OutputFolder.
 
 .EXAMPLE
 Copy-TestResult -ProjectUri https://dev.azure.com/myorg/project -AccessToken <PAT> -BuildUri vstfs:///Build/Build/1234 -OutputFolder c:\test-results -TrxDependencyPath 'In/$folder'
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $OutputFolder,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Uri] $ProjectUri = $script:AzDOBuild.ProjectUri,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $AccessToken = $script:AzDOBuild.AccessToken,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $BuildUri = $script:AzDOBuild.BuildUri,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $TrxDependencyPath = '$trxFolder/In/$folder'
    )

    process {
        $ErrorActionPreference = "Stop"

        # Ensure none of the parameters are $null or empty
        $parameters = @('OutputFolder', 'ProjectUri', 'AccessToken', 'BuildUri')
        foreach ($parameter in $parameters) {
             $value = Get-Variable -Name $parameter -ErrorAction SilentlyContinue;
             if ([string]::IsNullOrWhiteSpace($value.Value)){
                throw [System.ArgumentNullException] $parameter
            }
        }

        $tests = Get-TestRunList -BuildUri $BuildUri -BaseUri $ProjectUri -AccessToken $AccessToken
        if (-Not (Test-Path $OutputFolder)) {
            Write-Verbose "Creating output folder: '$OutputFolder'"
            [void](New-Item -ItemType Directory -Path $OutputFolder -Force)
        }

        foreach ($test in $tests) {
            $content = Get-GroupedAttachmentList -TestUri $test.url -AccessToken $AccessToken

            $trxFiles = $content.TrxContent
            $otherFiles = $content.OtherContent

            # Download TRX to get details about any related content locations
            foreach($item in $trxFiles) {
                Get-TestAttachment -AttachmentUri $item.url -OutputPath "$OutputFolder/$($item.fileName)" -AccessToken $AccessToken
            }

            $trxNodes = Get-TrxContent -Files $trxFiles -OutputFolder $OutputFolder -TrxDependencyPath $TrxDependencyPath
            # Create the required folders for child content
            foreach($node in $trxNodes) {
                if ($node) {
                    $path = [Path]::GetDirectoryName($node)
                    if ($path) {
                        Write-Verbose "Creating output location: '$path'"
                        [void](New-Item -ItemType Directory -Path $path -Force)
                    }
                }
            }

            # Download the reamining content
            $simpleFileList = $otherFiles | Select-Object -ExpandProperty 'fileName'
            $childLocations = Group-ChildContent -TrxContentList $trxNodes -FileList $simpleFileList -OutputFolder $OutputFolder
            foreach ($attachment in $otherFiles) {
                Write-Verbose "Downloading $($attachment.fileName)"
                $targetLocations = $childLocations[$attachment.FileName]
                $target = $targetLocations[0]
                Write-Verbose "Writing $($attachment.fileName) to $target"
                Get-TestAttachment -AttachmentUri $attachment.url -OutputPath $target -AccessToken $AccessToken

                if ($targetLocations.Length -gt 1){
                    foreach($dest in $targetLocations | Select-Object -Skip 1 ){
                        Write-Verbose "Writing $($attachment.fileName) to $dest"
                        [void](Copy-Item $target -Destination $dest -Force)
                    }
                }
            }
        }
    }
}

Set-Variable -Name "AzDOBuild" -Value (Get-BuildEnvironment) -Scope script -Option Constant
Export-ModuleMember -Function Copy-TestResult, Copy-TestResultToCommon, Copy-TestResultToTemp
# SIG # Begin signature block
# MIIccwYJKoZIhvcNAQcCoIIcZDCCHGACAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUw2ia8CUCpdCupe+DPmh4ntLC
# giKggheiMIIFKzCCBBOgAwIBAgIQA92NH6Ao4oJnDDTTKeZ3HzANBgkqhkiG9w0B
# AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz
# c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTE5MDkwOTAwMDAwMFoXDTIyMDkx
# MjEyMDAwMFowaDELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0dlb3JnaWExGTAXBgNV
# BAcTEEF2b25kYWxlIEVzdGF0ZXMxFTATBgNVBAoTDEtlbm5ldGggTXVzZTEVMBMG
# A1UEAxMMS2VubmV0aCBNdXNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
# AQEAsr5Lse8T47fphRV8/FhwMUgYR6h1FrGlf3YLe8nhl6ade5WHAtFZqlguZU+f
# WyP/R5NHf9bKMbHg6IiIuedyBE2fEzCZH5CJwgC3J0Df5qrNf4qI5fBobsryV/mg
# zSOzLBbunPfak1XNTNL0w4ak6jCz8OMAZ9Q+ThHgv5XIO5IhyTqkoMijkeLfg32x
# 5hfYeSQsXDma0yzyd7tGDrhmN+ayhh0nuTYmXS3fjxq8oLztMZhzfRP8Qfvs3SrW
# nZn+tCkGMPawOBc7M3GA5Zvc7yc9OiMprLOFN1BM8aNdGU+xRAQQ6lpO2vQOt8T4
# YaQApMxeI4UYF4iFT62OSDDfdQIDAQABo4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5
# eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYEFPy4/m56aqGve2X/EydtiYe2woZp
# MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzB3BgNVHR8EcDBu
# MDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVkLWNz
# LWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItYXNz
# dXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAwEwKjAoBggrBgEF
# BQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBBAEwgYQG
# CCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tME4GCCsGAQUFBzAChkJodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25pbmdDQS5jcnQwDAYDVR0TAQH/BAIw
# ADANBgkqhkiG9w0BAQsFAAOCAQEA+MVLwwa9SEsrYO063xHRLZWuLYsKQ0nXySzt
# kGoTnFwqHDBTTMkOtX2jLTZEnO0fpopEZzPoQJoO84njwxxqF6BQnYdu+x15ndXn
# oUWQsFSreaWUJZK4M2mL0kOZH14gDZNz3Ok+1BqQQozpXVazd5YJMjdJFvEZN+xz
# F0rwczzTnvhR1w+bKkb23YcCkF1S+VOhGZIpkUhftNdIx6pMfjg+pKxb8rOKwjAa
# MEpTukSZXD7BhGCHAouurTYcIdeLfPz3w+e2amvi/MCo+YcjcDowFtfqAbL7HEea
# k8nVTy9MPntNuxpOZQT8HalvGjH5KVjilkL+HXLc4nn1JMu/6DCCBTAwggQYoAMC
# AQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMC
# VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0
# LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTEz
# MTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8G
# A1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQTCC
# ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjTsxx/DhGvZ3cH0wsxSRnP
# 0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3Gde2qvCchqXYJawOeSg6
# funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEoJrskacLCUvIUZ4qJRdQt
# oaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCiq85/6XzLkqHlOzEcz+ry
# CuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xnTrXE94zRICUj6whkPlKW
# wfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAPZHM8np+mM6n9Gd8lk9EC
# AwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGG
# MBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEBBG0wazAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3J0
# MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGln
# aUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsME8GA1UdIARIMEYw
# OAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2Vy
# dC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRaxLl7KgqjpepxA8Bg+S32
# ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkqhkiG9w0B
# AQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE7zBh134LYP3DPQ/Er4v9
# 7yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4TS63XX0R58zYUBor3nEZO
# XP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwukaPAJRHinBRHoXpoaK+b
# p1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2iiQC/i9yfhzXSUWW6Fkd
# 6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/GkxAG/AeB+ova+YJJ92Ju
# oVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBmowggVSoAMCAQICEAMBmgI6/1ix
# a9bV6uYX8GYwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoT
# DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UE
# AxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0xMB4XDTE0MTAyMjAwMDAwMFoXDTI0
# MTAyMjAwMDAwMFowRzELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSUw
# IwYDVQQDExxEaWdpQ2VydCBUaW1lc3RhbXAgUmVzcG9uZGVyMIIBIjANBgkqhkiG
# 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo2Rd/Hyz4II14OD2xirmSXU7zG7gU6mfH2RZ
# 5nxrf2uMnVX4kuOe1VpjWwJJUNmDzm9m7t3LhelfpfnUh3SIRDsZyeX1kZ/GFDms
# JOqoSyyRicxeKPRktlC39RKzc5YKZ6O+YZ+u8/0SeHUOplsU/UUjjoZEVX0YhgWM
# VYd5SEb3yg6Np95OX+Koti1ZAmGIYXIYaLm4fO7m5zQvMXeBMB+7NgGN7yfj95rw
# TDFkjePr+hmHqH7P7IwMNlt6wXq4eMfJBi5GEMiN6ARg27xzdPpO2P6qQPGyznBG
# g+naQKFZOtkVCVeZVjCT88lhzNAIzGvsYkKRrALA76TwiRGPdwIDAQABo4IDNTCC
# AzEwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYI
# KwYBBQUHAwgwggG/BgNVHSAEggG2MIIBsjCCAaEGCWCGSAGG/WwHATCCAZIwKAYI
# KwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwggFkBggrBgEF
# BQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBl
# AHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBj
# AGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0
# ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAg
# AFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABp
# AG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBu
# AGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBm
# AGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMB8GA1UdIwQYMBaAFBUAEisTmLKZ
# B+0e36K+Vw0rZwLNMB0GA1UdDgQWBBRhWk0ktkkynUoqeRqDS/QeicHKfTB9BgNV
# HR8EdjB0MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRB
# c3N1cmVkSURDQS0xLmNybDA4oDagNIYyaHR0cDovL2NybDQuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwdwYIKwYBBQUHAQEEazBpMCQGCCsG
# AQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0
# dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEu
# Y3J0MA0GCSqGSIb3DQEBBQUAA4IBAQCdJX4bM02yJoFcm4bOIyAPgIfliP//sdRq
# LDHtOhcZcRfNqRu8WhY5AJ3jbITkWkD73gYBjDf6m7GdJH7+IKRXrVu3mrBgJupp
# VyFdNC8fcbCDlBkFazWQEKB7l8f2P+fiEUGmvWLZ8Cc9OB0obzpSCfDscGLTYkuw
# 4HOmksDTjjHYL+NtFxMG7uQDthSr849Dp3GdId0UyhVdkkHa+Q+B0Zl0DSbEDn8b
# tfWg8cZ3BigV6diT5VUW8LsKqxzbXEgnZsijiwoc5ZXarsQuWaBh3drzbaJh6YoL
# bewSGL33VVRAA5Ira8JRwgpIr7DUbuD0FAo6G+OPPcqvao173NhEMIIGzTCCBbWg
# AwIBAgIQBv35A5YDreoACus/J7u6GzANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG
# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
# cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcN
# MDYxMTEwMDAwMDAwWhcNMjExMTEwMDAwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMG
# A1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw
# HwYDVQQDExhEaWdpQ2VydCBBc3N1cmVkIElEIENBLTEwggEiMA0GCSqGSIb3DQEB
# AQUAA4IBDwAwggEKAoIBAQDogi2Z+crCQpWlgHNAcNKeVlRcqcTSQQaPyTP8TUWR
# XIGf7Syc+BZZ3561JBXCmLm0d0ncicQK2q/LXmvtrbBxMevPOkAMRk2T7It6NggD
# qww0/hhJgv7HxzFIgHweog+SDlDJxofrNj/YMMP/pvf7os1vcyP+rFYFkPAyIRaJ
# xnCI+QWXfaPHQ90C6Ds97bFBo+0/vtuVSMTuHrPyvAwrmdDGXRJCgeGDboJzPyZL
# FJCuWWYKxI2+0s4Grq2Eb0iEm09AufFM8q+Y+/bOQF1c9qjxL6/siSLyaxhlscFz
# rdfx2M8eCnRcQrhofrfVdwonVnwPYqQ/MhRglf0HBKIJAgMBAAGjggN6MIIDdjAO
# BgNVHQ8BAf8EBAMCAYYwOwYDVR0lBDQwMgYIKwYBBQUHAwEGCCsGAQUFBwMCBggr
# BgEFBQcDAwYIKwYBBQUHAwQGCCsGAQUFBwMIMIIB0gYDVR0gBIIByTCCAcUwggG0
# BgpghkgBhv1sAAEEMIIBpDA6BggrBgEFBQcCARYuaHR0cDovL3d3dy5kaWdpY2Vy
# dC5jb20vc3NsLWNwcy1yZXBvc2l0b3J5Lmh0bTCCAWQGCCsGAQUFBwICMIIBVh6C
# AVIAQQBuAHkAIAB1AHMAZQAgAG8AZgAgAHQAaABpAHMAIABDAGUAcgB0AGkAZgBp
# AGMAYQB0AGUAIABjAG8AbgBzAHQAaQB0AHUAdABlAHMAIABhAGMAYwBlAHAAdABh
# AG4AYwBlACAAbwBmACAAdABoAGUAIABEAGkAZwBpAEMAZQByAHQAIABDAFAALwBD
# AFAAUwAgAGEAbgBkACAAdABoAGUAIABSAGUAbAB5AGkAbgBnACAAUABhAHIAdAB5
# ACAAQQBnAHIAZQBlAG0AZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkAbQBpAHQAIABs
# AGkAYQBiAGkAbABpAHQAeQAgAGEAbgBkACAAYQByAGUAIABpAG4AYwBvAHIAcABv
# AHIAYQB0AGUAZAAgAGgAZQByAGUAaQBuACAAYgB5ACAAcgBlAGYAZQByAGUAbgBj
# AGUALjALBglghkgBhv1sAxUwEgYDVR0TAQH/BAgwBgEB/wIBADB5BggrBgEFBQcB
# AQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggr
# BgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNz
# dXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0
# aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDAdBgNVHQ4EFgQUFQASKxOYspkH7R7for5XDStnAs0wHwYDVR0jBBgwFoAU
# Reuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEFBQADggEBAEZQPsm3KCSn
# OB22WymvUs9S6TFHq1Zce9UNC0Gz7+x1H3Q48rJcYaKclcNQ5IK5I9G6OoZyrTh4
# rHVdFxc0ckeFlFbR67s2hHfMJKXzBBlVqefj56tizfuLLZDCwNK1lL1eT7EF0g49
# GqkUW6aGMWKoqDPkmzmnxPXOHXh2lCVz5Cqrz5x2S+1fwksW5EtwTACJHvzFebxM
# Elf+X+EevAJdqP77BzhPDcZdkbkPZ0XN1oPt55INjbFpjE/7WeAjD9KqrgB87pxC
# Ds+R1ye3Fu4Pw718CqDuLAhVhSK46xgaTfwqIa1JMYNHlXdx3LEbS0scEJx3FMGd
# Ty9alQgpECYxggQ7MIIENwIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM
# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQD
# EyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBAhAD3Y0f
# oCjigmcMNNMp5ncfMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQowCKACgACh
# AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM
# BgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBS6VXtcfxTkQeMAMGlK+b8ICga7
# 2zANBgkqhkiG9w0BAQEFAASCAQAKTwmEresOvmiBuRM/ITwHYxcycb0TrnmWXqzU
# 0I0llxlReZBOplxyEEwES+EnH8A7Zljta+zw+QBZlChiqCA7y+C/fB3fkXcezJpr
# t2mbX2AShfKNLTRniIEP9BAGpQ+55o0k8n0KhTDXCgm6RwmH5tT69L144cYylk+u
# T1U6az4ZwztfYz7mLm7AazAc/UhCQiJqkZNiigFxLdjAfAApyuxIM6jHmpCN1nXB
# az63QiDD7GrEUy9V9HJCKMMX9uDXrRBaOhY9D0gO25RywKo8rqd/GWeC7VYSS+KG
# 6OPm9jr0udwm0mvqL41WmaEYLWH08BulT53C0vWxC5AGvgQ4oYICDzCCAgsGCSqG
# SIb3DQEJBjGCAfwwggH4AgEBMHYwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp
# Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMY
# RGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0xAhADAZoCOv9YsWvW1ermF/BmMAkGBSsO
# AwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEP
# Fw0xOTA5MjUwMTQ3MzhaMCMGCSqGSIb3DQEJBDEWBBRvxKGis7mgk3EDdGUp0mBj
# LIG7MjANBgkqhkiG9w0BAQEFAASCAQA3LnIDohGPnZ1VuY7SODcWHTIJHaZDVD6f
# nSLNMTrMhK9NevQduHxNuOvUViHWAI6xzJM4pjKT0xe0TiYyDOek9ikQLuDzIaI1
# gnZoPzwxuwnFNP5RiHlbWw8YLwYz7ySheIZGbwIRo8TRU1XXspIZny9i46uQMAcP
# 8yyqIK/neo6dosWRIU4wNhPk2HK5j4gOEiEdWSUFJzBcCo7fFzxDHcU0cQ0UNrDJ
# z4ZBqCbKuCOL3+BdSu4JLAED7WHtAwMab0cEkMyJkpq5D8F0RPEX+94vqokPzaBA
# +s1Ybgq2d9Yke50aSCNmjcMZXWA4pI7MYp9puE0bu8oCfV3i72Y6
# SIG # End signature block