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 |