if ($PSVersionTable.PSVersion.Major -le 2) { function Exit-CoverageAnalysis { } function Get-CoverageReport { } function Show-CoverageReport { } function Enter-CoverageAnalysis { param ( $CodeCoverage ) if ($CodeCoverage) { & $SafeCommands['Write-Error'] 'Code coverage analysis requires PowerShell 3.0 or later.' } } return } function Enter-CoverageAnalysis { [CmdletBinding()] param ( [object[]] $CodeCoverage, [object] $PesterState ) $coverageInfo = foreach ($object in $CodeCoverage) { Get-CoverageInfoFromUserInput -InputObject $object } $PesterState.CommandCoverage = @(Get-CoverageBreakpoints -CoverageInfo $coverageInfo) } function Exit-CoverageAnalysis { param ([object] $PesterState) & $SafeCommands['Set-StrictMode'] -Off $breakpoints = @($PesterState.CommandCoverage.Breakpoint) -ne $null if ($breakpoints.Count -gt 0) { & $SafeCommands['Remove-PSBreakpoint'] -Breakpoint $breakpoints } } function Get-CoverageInfoFromUserInput { param ( [Parameter(Mandatory = $true)] [object] $InputObject ) if ($InputObject -is [System.Collections.IDictionary]) { $unresolvedCoverageInfo = Get-CoverageInfoFromDictionary -Dictionary $InputObject } else { $unresolvedCoverageInfo = New-CoverageInfo -Path ([string]$InputObject) } Resolve-CoverageInfo -UnresolvedCoverageInfo $unresolvedCoverageInfo } function New-CoverageInfo { param ([string] $Path, [string] $Function = $null, [int] $StartLine = 0, [int] $EndLine = 0) return [pscustomobject]@{ Path = $Path Function = $Function StartLine = $StartLine EndLine = $EndLine } } function Get-CoverageInfoFromDictionary { param ([System.Collections.IDictionary] $Dictionary) [string] $path = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'Path', 'p' if ([string]::IsNullOrEmpty($path)) { throw "Coverage value '$Dictionary' is missing required Path key." } $startLine = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'StartLine', 'Start', 's' $endLine = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'EndLine', 'End', 'e' [string] $function = Get-DictionaryValueFromFirstKeyFound -Dictionary $Dictionary -Key 'Function', 'f' $startLine = Convert-UnknownValueToInt -Value $startLine -DefaultValue 0 $endLine = Convert-UnknownValueToInt -Value $endLine -DefaultValue 0 return New-CoverageInfo -Path $path -StartLine $startLine -EndLine $endLine -Function $function } function Convert-UnknownValueToInt { param ([object] $Value, [int] $DefaultValue = 0) try { return [int] $Value } catch { return $DefaultValue } } function Resolve-CoverageInfo { param ([psobject] $UnresolvedCoverageInfo) $path = $UnresolvedCoverageInfo.Path try { $resolvedPaths = & $SafeCommands['Resolve-Path'] -Path $path -ErrorAction Stop } catch { & $SafeCommands['Write-Error'] "Could not resolve coverage path '$path': $($_.Exception.Message)" return } $filePaths = foreach ($resolvedPath in $resolvedPaths) { $item = & $SafeCommands['Get-Item'] -LiteralPath $resolvedPath if ($item -is [System.IO.FileInfo] -and ('.ps1','.psm1') -contains $item.Extension) { $item.FullName } elseif (-not $item.PsIsContainer) { & $SafeCommands['Write-Warning'] "CodeCoverage path '$path' resolved to a non-PowerShell file '$($item.FullName)'; this path will not be part of the coverage report." } } $params = @{ StartLine = $UnresolvedCoverageInfo.StartLine EndLine = $UnresolvedCoverageInfo.EndLine Function = $UnresolvedCoverageInfo.Function } foreach ($filePath in $filePaths) { $params['Path'] = $filePath New-CoverageInfo @params } } function Get-CoverageBreakpoints { [CmdletBinding()] param ( [object[]] $CoverageInfo ) $fileGroups = @($CoverageInfo | & $SafeCommands['Group-Object'] -Property Path) foreach ($fileGroup in $fileGroups) { & $SafeCommands['Write-Verbose'] "Initializing code coverage analysis for file '$($fileGroup.Name)'" $totalCommands = 0 $analyzedCommands = 0 :commandLoop foreach ($command in Get-CommandsInFile -Path $fileGroup.Name) { $totalCommands++ foreach ($coverageInfoObject in $fileGroup.Group) { if (Test-CoverageOverlapsCommand -CoverageInfo $coverageInfoObject -Command $command) { $analyzedCommands++ New-CoverageBreakpoint -Command $command continue commandLoop } } } & $SafeCommands['Write-Verbose'] "Analyzing $analyzedCommands of $totalCommands commands in file '$($fileGroup.Name)' for code coverage" } } function Get-CommandsInFile { param ([string] $Path) $errors = $null $tokens = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors) if ($PSVersionTable.PSVersion.Major -ge 5) { # In PowerShell 5.0, dynamic keywords for DSC configurations are represented by the DynamicKeywordStatementAst # class. They still trigger breakpoints, but are not a child class of CommandBaseAst anymore. $predicate = { $args[0] -is [System.Management.Automation.Language.DynamicKeywordStatementAst] -or $args[0] -is [System.Management.Automation.Language.CommandBaseAst] } } else { $predicate = { $args[0] -is [System.Management.Automation.Language.CommandBaseAst] } } $searchNestedScriptBlocks = $true $ast.FindAll($predicate, $searchNestedScriptBlocks) } function Test-CoverageOverlapsCommand { param ([object] $CoverageInfo, [System.Management.Automation.Language.Ast] $Command) if ($CoverageInfo.Function) { Test-CommandInsideFunction -Command $Command -Function $CoverageInfo.Function } else { Test-CoverageOverlapsCommandByLineNumber @PSBoundParameters } } function Test-CommandInsideFunction { param ([System.Management.Automation.Language.Ast] $Command, [string] $Function) for ($ast = $Command; $null -ne $ast; $ast = $ast.Parent) { $functionAst = $ast -as [System.Management.Automation.Language.FunctionDefinitionAst] if ($null -ne $functionAst -and $functionAst.Name -like $Function) { return $true } } return $false } function Test-CoverageOverlapsCommandByLineNumber { param ([object] $CoverageInfo, [System.Management.Automation.Language.Ast] $Command) $commandStart = $Command.Extent.StartLineNumber $commandEnd = $Command.Extent.EndLineNumber $coverStart = $CoverageInfo.StartLine $coverEnd = $CoverageInfo.EndLine # An EndLine value of 0 means to cover the entire rest of the file from StartLine # (which may also be 0) if ($coverEnd -le 0) { $coverEnd = [int]::MaxValue } return (Test-RangeContainsValue -Value $commandStart -Min $coverStart -Max $coverEnd) -or (Test-RangeContainsValue -Value $commandEnd -Min $coverStart -Max $coverEnd) } function Test-RangeContainsValue { param ([int] $Value, [int] $Min, [int] $Max) return $Value -ge $Min -and $Value -le $Max } function New-CoverageBreakpoint { param ([System.Management.Automation.Language.Ast] $Command) if (IsIgnoredCommand -Command $Command) { return } $params = @{ Script = $Command.Extent.File Line = $Command.Extent.StartLineNumber Column = $Command.Extent.StartColumnNumber Action = { } } $breakpoint = & $SafeCommands['Set-PSBreakpoint'] @params [pscustomobject] @{ File = $Command.Extent.File Function = Get-ParentFunctionName -Ast $Command Line = $Command.Extent.StartLineNumber Command = Get-CoverageCommandText -Ast $Command Breakpoint = $breakpoint } } function IsIgnoredCommand { param ([System.Management.Automation.Language.Ast] $Command) if (-not $Command.Extent.File) { # This can happen if the script contains "configuration" or any similarly implemented # dynamic keyword. PowerShell modifies the script code and reparses it in memory, leading # to AST elements with no File in their Extent. return $true } if ($PSVersionTable.PSVersion.Major -ge 4) { if ($Command.Extent.Text -eq 'Configuration') { # More DSC voodoo. Calls to "configuration" generate breakpoints, but their HitCount # stays zero (even though they are executed.) For now, ignore them, unless we can come # up with a better solution. return $true } if (IsChildOfHashtableDynamicKeyword -Command $Command) { # The lines inside DSC resource declarations don't trigger their breakpoints when executed, # just like the "configuration" keyword itself. I don't know why, at this point, but just like # configuration, we'll ignore it so it doesn't clutter up the coverage analysis with useless junk. return $true } } if (IsClosingLoopCondition -Command $Command) { # For some reason, the closing expressions of do/while and do/until loops don't trigger their breakpoints. # To avoid useless clutter, we'll ignore those lines as well. return $true } return $false } function IsChildOfHashtableDynamicKeyword { param ([System.Management.Automation.Language.Ast] $Command) for ($ast = $Command.Parent; $null -ne $ast; $ast = $ast.Parent) { if ($PSVersionTable.PSVersion.Major -ge 5) { # The ast behaves differently for DSC resources with version 5+. There's a new DynamicKeywordStatementAst class, # and they no longer are represented by CommandAst objects. if ($ast -is [System.Management.Automation.Language.DynamicKeywordStatementAst] -and $ast.CommandElements[-1] -is [System.Management.Automation.Language.HashtableAst]) { return $true } } else { if ($ast -is [System.Management.Automation.Language.CommandAst] -and $null -ne $ast.DefiningKeyword -and $ast.DefiningKeyword.BodyMode -eq [System.Management.Automation.Language.DynamicKeywordBodyMode]::Hashtable) { return $true } } } return $false } function IsClosingLoopCondition { param ([System.Management.Automation.Language.Ast] $Command) $ast = $Command while ($null -ne $ast.Parent) { if (($ast.Parent -is [System.Management.Automation.Language.DoWhileStatementAst] -or $ast.Parent -is [System.Management.Automation.Language.DoUntilStatementAst]) -and $ast.Parent.Condition -eq $ast) { return $true } $ast = $ast.Parent } return $false } function Get-ParentFunctionName { param ([System.Management.Automation.Language.Ast] $Ast) $parent = $Ast.Parent while ($null -ne $parent -and $parent -isnot [System.Management.Automation.Language.FunctionDefinitionAst]) { $parent = $parent.Parent } if ($null -eq $parent) { return '' } else { return $parent.Name } } function Get-CoverageCommandText { param ([System.Management.Automation.Language.Ast] $Ast) $reportParentExtentTypes = @( [System.Management.Automation.Language.ReturnStatementAst] [System.Management.Automation.Language.ThrowStatementAst] [System.Management.Automation.Language.AssignmentStatementAst] [System.Management.Automation.Language.IfStatementAst] ) $parent = Get-ParentNonPipelineAst -Ast $Ast if ($null -ne $parent) { if ($parent -is [System.Management.Automation.Language.HashtableAst]) { return Get-KeyValuePairText -HashtableAst $parent -ChildAst $Ast } elseif ($reportParentExtentTypes -contains $parent.GetType()) { return $parent.Extent.Text } } return $Ast.Extent.Text } function Get-ParentNonPipelineAst { param ([System.Management.Automation.Language.Ast] $Ast) $parent = $null if ($null -ne $Ast) { $parent = $Ast.Parent } while ($parent -is [System.Management.Automation.Language.PipelineAst]) { $parent = $parent.Parent } return $parent } function Get-KeyValuePairText { param ( [System.Management.Automation.Language.HashtableAst] $HashtableAst, [System.Management.Automation.Language.Ast] $ChildAst ) & $SafeCommands['Set-StrictMode'] -Off foreach ($keyValuePair in $HashtableAst.KeyValuePairs) { if ($keyValuePair.Item2.PipelineElements -contains $ChildAst) { return '{0} = {1}' -f $keyValuePair.Item1.Extent.Text, $keyValuePair.Item2.Extent.Text } } # This shouldn't happen, but just in case, default to the old output of just the expression. return $ChildAst.Extent.Text } function Get-CoverageMissedCommands { param ([object[]] $CommandCoverage) $CommandCoverage | & $SafeCommands['Where-Object'] { $_.Breakpoint.HitCount -eq 0 } } function Get-CoverageHitCommands { param ([object[]] $CommandCoverage) $CommandCoverage | & $SafeCommands['Where-Object'] { $_.Breakpoint.HitCount -gt 0 } } function Get-CoverageReport { param ([object] $PesterState) $totalCommandCount = $PesterState.CommandCoverage.Count $missedCommands = @(Get-CoverageMissedCommands -CommandCoverage $PesterState.CommandCoverage | & $SafeCommands['Select-Object'] File, Line, Function, Command) $hitCommands = @(Get-CoverageHitCommands -CommandCoverage $PesterState.CommandCoverage | & $SafeCommands['Select-Object'] File, Line, Function, Command) $analyzedFiles = @($PesterState.CommandCoverage | & $SafeCommands['Select-Object'] -ExpandProperty File -Unique) $fileCount = $analyzedFiles.Count $executedCommandCount = $totalCommandCount - $missedCommands.Count [pscustomobject] @{ NumberOfCommandsAnalyzed = $totalCommandCount NumberOfFilesAnalyzed = $fileCount NumberOfCommandsExecuted = $executedCommandCount NumberOfCommandsMissed = $missedCommands.Count MissedCommands = $missedCommands HitCommands = $hitCommands AnalyzedFiles = $analyzedFiles } } function Show-CoverageReport { param ([object] $CoverageReport) if ($null -eq $CoverageReport -or $CoverageReport.NumberOfCommandsAnalyzed -eq 0) { return } $totalCommandCount = $CoverageReport.NumberOfCommandsAnalyzed $fileCount = $CoverageReport.NumberOfFilesAnalyzed $executedPercent = ($CoverageReport.NumberOfCommandsExecuted / $CoverageReport.NumberOfCommandsAnalyzed).ToString("P2") $commandPlural = $filePlural = '' if ($totalCommandCount -gt 1) { $commandPlural = 's' } if ($fileCount -gt 1) { $filePlural = 's' } $commonParent = Get-CommonParentPath -Path $CoverageReport.AnalyzedFiles $report = $CoverageReport.MissedCommands | & $SafeCommands['Select-Object'] -Property @( @{ Name = 'File'; Expression = { Get-RelativePath -Path $_.File -RelativeTo $commonParent } } 'Function' 'Line' 'Command' ) Write-Screen '' Write-Screen 'Code coverage report:' Write-Screen "Covered $executedPercent of $totalCommandCount analyzed command$commandPlural in $fileCount file$filePlural." if ($CoverageReport.MissedCommands.Count -gt 0) { Write-Screen '' Write-Screen 'Missed commands:' $report | & $SafeCommands['Format-Table'] -AutoSize | & $SafeCommands['Out-String'] | Write-Screen } } function Get-CommonParentPath { param ([string[]] $Path) $pathsToTest = @( $Path | & $SafeCommands['Select-Object'] -Unique ) if ($pathsToTest.Count -gt 0) { $parentPath = & $SafeCommands['Split-Path'] -Path $pathsToTest[0] -Parent while ($parentPath.Length -gt 0) { $nonMatches = $pathsToTest -notmatch "^$([regex]::Escape($parentPath))" if ($nonMatches.Count -eq 0) { return $parentPath } else { $parentPath = & $SafeCommands['Split-Path'] -Path $parentPath -Parent } } } return [string]::Empty } function Get-RelativePath { param ( [string] $Path, [string] $RelativeTo ) return $Path -replace "^$([regex]::Escape($RelativeTo))\\?" } # SIG # Begin signature block # MIInZwYJKoZIhvcNAQcCoIInWDCCJ1QCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCqG+zazlxP++cv # 0EUGZRX6zP2XM6BmoNh/fiywwj2BLqCCC7QwggXLMIIEs6ADAgECAhMzAAABQGf2 # mGsxCLhyAAAAAAFAMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMTgwNgYDVQQDEy9NaWNyb3NvZnQgV2luZG93cyBU # aGlyZCBQYXJ0eSBDb21wb25lbnQgQ0EgMjAxMjAeFw0yNTExMTMxOTU5NDNaFw0y # NjExMTAxOTU5NDNaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMS4wLAYDVQQDEyVNaWNyb3NvZnQgV2luZG93cyAzcmQgcGFydHkgQ29tcG9u # ZW50MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEArGt4Hrjs3D7+IIab # GkPGtg4XeWQFi8nKsdeF+N6nkYF9wsmw0qPk4OovEiJnCSRiEtLfYy3Icn4iOp7L # 24oCa0zlgYCND3OnNfYOuVpMsq3CSkXYg3H+VMiB11hOLcctTu4RWy4ZRKsL5NVI # AduJFgd/1Qs2mQpUuOyFfLKhu/+Z+T2c9YALxsfnPvqn8Tn8Tt8wdZuFwz7kK3jl # e2T3YBofybAR2aH678TnOxhLe2/hEpTdKaVfcqyrIX0Mw/LwqEJnj4G8amBbQ7OG # RnKm1VNd4nMpRt1SflNlVo1Q3Qqnj9K6mSPw99YGMCTaVr6jKPylsWyW4nsvu8m0 # MbBJJiLOi0sOTHtwQEVcBH6WM7xbODqwPaklS1wDbED1wfzImIUR8WsXJVjDm7Fg # x4JDo8oLIjKFrEvzBfrYWsvlDg8p9XgR7EB/IJwXGuJbFzo/S+r3D5DkUZXX/72k # XA4FeRJAgLePAuGChPvyQiLtKJeJm9ftJeukrIcs7JpJVHObAgMBAAGjggGoMIIB # pDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUB9FDBUZ0W11vsv6qqb9t # vKspCIowRQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEWMBQGA1UEBRMNMjMwODA5KzUwNjIyOTAfBgNVHSMEGDAWgBRhcaeHr/9p # 1SF2T1KTKAC+eRKrhDB0BgNVHR8EbTBrMGmgZ6BlhmNodHRwOi8vd3d3Lm1pY3Jv # c29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBXaW5kb3dzJTIwVGhpcmQl # MjBQYXJ0eSUyMENvbXBvbmVudCUyMENBJTIwMjAxMi5jcmwwgYEGCCsGAQUFBwEB # BHUwczBxBggrBgEFBQcwAoZlaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9jZXJ0cy9NaWNyb3NvZnQlMjBXaW5kb3dzJTIwVGhpcmQlMjBQYXJ0eSUyMENv # bXBvbmVudCUyMENBJTIwMjAxMi5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0B # AQsFAAOCAQEAmFzJPKhkAXPOcfy9S4q1nP3XCvmABVGv/VRzFZ1e+oVtVzOFkbZ6 # osY1OpZqa9CEvtgcrtciv1y9mV1SpxAMwfEqq+Nhw2GdZ3IWqFzWFsEe2S9U196s # XRIRIxglZMBWyzdy8rQ+P7L2+LciW4qcaw2wtv6YoI0q1isgk0t1OF5kEL0t9hhx # UrBavTdoqZ3b5JtwDqzmIsxuLtGbE9Pqbz2gx/me+7yduYpIcYlmEmPQkg3gmWkZ # lnSRePu7/5RXS2TGW1O5JP8aqVcMn0ZkHEhtT368pE/CxQVAkAYHNz6+enP9n+HC # 2udu395c0fVlRHy/d53f32RukfbjakH8uzCCBeEwggPJoAMCAQICCmELqsEAAAAA # AAkwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1 # dGhvcml0eSAyMDEwMB4XDTEyMDQxODIzNDgzOFoXDTI3MDQxODIzNTgzOFowgY4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xODA2BgNVBAMTL01p # Y3Jvc29mdCBXaW5kb3dzIFRoaXJkIFBhcnR5IENvbXBvbmVudCBDQSAyMDEyMIIB # IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo5wwhAmnYy7PCkfw6iT5ozAg # D15XMSaBmjEHslDUzmcJCGUKWqVLrtXtEC7npZm1n2gvmItYAqwgtCnEcb0oHKX9 # PJtk5MXr32ElvPDuaL/Rp8t+KgKBTmRcDFOGeVcZN2G3mPkMoE4iWZv5Gy1nPCc8 # VpBm4/1/ZX0Phr01R+iKzPTajulqTqunVeyiiR7VM0VTy/med73NLPkFuH90AR3o # +xjhQ9EN6arcN2+9/rgP7R1NAUZOCqz8gujsVoMTjjoB7RRkdOpksmYQtmhtyHAA # fVBILj1D7uAklcbNjsf9uOSVz91++5VeoQHNQ7EH16Qw7puGGipuwQtZonRviwID # AQABo4IBQzCCAT8wEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGFxp4ev/2nV # IXZPUpMoAL55EquEMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQE # AwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQ # W9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNv # bS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBa # BggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqG # SIb3DQEBCwUAA4ICAQBaimfazNX9DSZBd78KRni0s94SaSt3I8JlLwFf0gP0YbpQ # nS6MOXLzbD5qsR52bey384LczLvFaXAoc2YXP1Tr7gEWSMRG2RuAroE6jQ95bWiw # nuotPznTyjh+vV58CG4Z3MbC9DgzaGHiUkeD4QABVtK6y4eCBTEKQYtO539fX+1f # 0zktReuiE7/9HsKYQXFhFl/ICnAlfFlpMSTkcecKuwQX959yHsnSuxq+PQL+CQyy # Q7RZGplTk5YhX+DWtyYBQpU2rCf9vvSFd2g9GL30vpiIIhGGUhbzRewDlxBwh6Nw # Q3E828mGAxcM9XNbxn3hXGTt18VI1+0y4tGq08+n9ldOYfl362fyiLPeANoDj9CK # NDc+HdhiuNKx8+Evi3I7gZZ8b/zsZnZyYBsk8qCJbVttAC7vKN2GhwXCtLnlvmTC # KvJKFVyY4sQnhf9S42J+D7ICC9dmxwqy0z0gBBRQMlmDCn2b7Vo4EgFSui9eIHKO # SvH953ECjDvhB77Jc/TdR9i077SkszC5iT52yrkAmFZ+q+qKuKXQOKtpdxMLFC/p # qkEf97q9Ois0iu4Kq2PmY/eIJI4gDSs7nePCSVKsnx8OOTtd1G5QauZ9UjqqfDMV # KQ0mXgFYp06pPXqEb3Q/YJ/kMk82AK9tcdM+pkZlX4F08f7BcdpMoEFagt3xHzGC # GwkwghsFAgEBMIGmMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMTgwNgYDVQQDEy9NaWNyb3NvZnQgV2luZG93cyBUaGlyZCBQYXJ0eSBDb21w # b25lbnQgQ0EgMjAxMgITMwAAAUBn9phrMQi4cgAAAAABQDANBglghkgBZQMEAgEF # AKCCARgwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEICONwWKXH7dsutIwpVdzGbC2 # Jltl5fqVrNgeaNFAr39gMFAGCisGAQQBgjcKAxwxQgxAQjVGNDdGRDM1MjdFQTI3 # MUEyQUNCRjRGMjYxQUNGMDZBRkEwQjE0OUNBOTZCMjZCMDBENTY0NDY5MzlGMTFC # MTBaBgorBgEEAYI3AgEMMUwwSqAkgCIATQBpAGMAcgBvAHMAbwBmAHQAIABXAGkA # bgBkAG8AdwBzoSKAIGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS93aW5kb3dzMA0G # CSqGSIb3DQEBAQUABIIBgCmimQVjfcAzBPkoKvQ2pYY229q4oEyJSvrImizlVCNG # AIvrFa1iOiGbgWnIokN+TnlH7QAe0VMGGdM5ciV3jheXQfSnGpdOf5bHMBJmLFJF # puFB9pbXzkFuOak78Lmd/vW4WXDZXid0B+wHIKpmGWTbOwFejDHX6Vykg8heqcCq # 1TQXQhUnZ1goDXmezPb1x+M/YOwi3u1tlMmfLqS3k16HMWsuahlmb1TX67AVuRBw # 386Zm0JFlAS+Su4UN9k9WzJ44Ln5oqly0FDjhUbIk1Uhrez6X9WdJxKHjgXeBIP+ # IzWZO04D5EyuZfIXz2KEEWotNX7trEq86tH0/MZvONIwOaggxNVTS1iFLNlJxzP5 # m9OgXiZ0JC5n9eTKQFqS795OacWFeVOjHGShUwz0yA9RbeZ1gdo7LHeUMWCa3+fo # iOaWD28WcW88sVxG+TzADOZCHU/zJWYALynNREYX1i2i7ChOOY8XEJvLSVrvYUMz # D0azXmn3d+fZ9EoLinGXY6GCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCCF38GCSqG # SIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsqhkiG9w0B # CRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUA # BCD/+dOqzvYliLvG6Nlnz2HPt1kHJk/NfgniIXIQcnvEpAIGaedvz+3EGBMyMDI2 # MDUwNjA1MjEwOC43NzhaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBP # cGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTkzNS0wM0UwLUQ5 # NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghHtMIIH # IDCCBQigAwIBAgITMwAAAifVwIPDsS5XLQABAAACJzANBgkqhkiG9w0BAQsFADB8 # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N # aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNjAyMTkxOTQwMDRaFw0y # NzA1MTcxOTQwMDRaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYD # VQQLEx5uU2hpZWxkIFRTUyBFU046QTkzNS0wM0UwLUQ5NDcxJTAjBgNVBAMTHE1p # Y3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUAA4IC # DwAwggIKAoICAQDixWy1fDOSL4qj3A1pady+elIDLwnF3UuLzJIOWwGHcEgrxxwt # nyviUIDmmxylTUl1u+2rBPp2zT4BwwQhvGaJpExqvPLlDFlbfmSflKI86eFqofiZ # 7j8NTRO4l7wGg9Njm+muNauTcFW2qdfIjKE950Okrm9MnMOGYy+fibNYdxTPRPq1 # T4MLZK3s3vdMyMEOldcOQkSKpxD6/1Gk6gOmCu2KgI8f0ex6vYxnKDl9W0OLSEa/ # 6y82oIbsm+1QBifOQ47xWKTG1CmvtGr85LzA75/MAcUmRw5/of/qET0UFV1WulMc # JrI6DASAsNCNB+6WLrotuBZAj+VMlqbn5RMZ6Q4IY7JwaAiIXh7VjxrnwUOYZG8W # EGhfrA98di+7LEn9AqvvEOyG+UQcjVhCCbMGXigJXSApeyeWupCsD0jgQMNCxfB5 # BLBDWxgdY3dJBEPgxfkgTDQLBggtVv2d5CYxHKgIItB4bI5eSb5jkIG2WotnFetT # 0legpw/Eozwf39ao6tENY21eVWIzRw/GsmvwjYQF6vVrxOD0pGVsfqGF8s3VPeY7 # hI2TxHFMqNA0IB/a2NLY7JTxYAKAP/11EJZt7xbqDLMgD1YDdGEzGpQijm3nAPCL # 2CebP/jmu90abJ2W425yglGHTI/nCBrwSpfRCgwzrfFelJaCKM6+35aFfwIDAQAB # o4IBSTCCAUUwHQYDVR0OBBYEFNLW58N4MGSG6ud7jWqgT92orfReMB8GA1UdIwQY # MBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6 # Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRpbWUt # U3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYIKwYB # BQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWlj # cm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1UdEwEB # /wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeAMA0G # CSqGSIb3DQEBCwUAA4ICAQAqncud4PSC1teb2H6nRuy7sDiKK13FXJirVB4Tfwjd # o2Mb+QL4j7wZ/k4G9P0CANHZFrDQcK0VFDTysrYu8Z0Aha14acDZPsyIoPvAGRRh # aHEuf7NckRjkfa/ylo1KyII8jbL9N9sJAqBPL8V4FNBjljv+1GHDOw127rZz5ZST # PoAPb2SA0v5yDgcpUMfxglPyp6cnPPoQpTtD9OGx8Dwm2P+o1TPxBIy6I0T9Rauu # logVCvKwflfeLTcKAvnSG1rCjerSXmU1DNXOsAD/bsrSjgbX5mAbD7XTRMF/vawA # WESFcn/BjjizxeWZb00aYSlkJA2rVtFlMM481aVWXdAbXPP5RzUiWTlgyHf/G7lC # xHYWGIZuB13T3aI6Y8mEgn/ou40aiFJo8r0+i0P5GdNneWtxiR0CMKUfko+5s/73 # cwe1Wfp8BKXa270cicVQasFf5sRV7pFm+V7fNRXwCu7anTOmga76zO7/2t+zOlib # vphT+Q6Zd+B2qYsSn4xBaY+YzHpnycLW5cvJyhPxBCcb1oRYfhRzCADb2utI2EtG # Cjc2P2ii4LyR4QMb/n8cOweL9IqVTKKzzVk+zZJxV3vrp4LyuQXw0O30la6BcHdN # AAAB9UC83zs3G9d+AlIfZLM97tMUNKWjbBpIirFx6LTDFXVtZQd7hqzLYByjbjH0 # ujCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQEL # BQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNV # BAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4X # DTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh # bXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM # 57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm # 95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzB # RMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBb # fowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCO # Mcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYw # XE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW # /aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/w # EPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPK # Z6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2 # BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfH # CBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYB # BAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8v # BO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYM # KwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEF # BQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBW # BgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUH # AQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp # L2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsF # AAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518Jx # Nj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+ # iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2 # pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefw # C2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7 # T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFO # Ry3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhL # mm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3L # wUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5 # m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE # 0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQMIICOAIB # ATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUGA1UE # CxMeblNoaWVsZCBUU1MgRVNOOkE5MzUtMDNFMC1EOTQ3MSUwIwYDVQQDExxNaWNy # b3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQAjHzqthPwO # 0GDckDMA6x54lIiMKqCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy # MDEwMA0GCSqGSIb3DQEBCwUAAgUA7aRhEjAiGA8yMDI2MDUwNTEyMjkwNloYDzIw # MjYwNTA2MTIyOTA2WjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDtpGESAgEAMAoC # AQACAhcoAgH/MAcCAQACAhPiMAoCBQDtpbKSAgEAMDYGCisGAQQBhFkKBAIxKDAm # MAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcN # AQELBQADggEBAEiRJt+XtqD33KcZwFgkSjyM7FS9OgtnYjvxNyI0ng+kVOWFX/n6 # aDaK6rvo19tYiPeaj6PQfEYE4xm2x+oSyXh3/VoKreo4uJicnnHJE76BpO2duWjY # MSXGEVs7DUqfP4zTHt3WyVn7SFV+OUe13JyHMGePFZipk7gWCqIk9Ylbl67Ml/cE # UrJbRae2V4vDqvl4SRk2nvofCNPvZmzv0UId48XF9zIEKtMr+w590DchYmIzTffe # STiGYkEJZvBy5EtF5vVOTX39i5LJFMs71OMSB6FBHus+QoE37VBlxwrGTRx3gfSA # IdzucPkRr4iq/aixXCQ9mJIcufb0blsFHH0xggQNMIIECQIBATCBkzB8MQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3Nv # ZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAifVwIPDsS5XLQABAAACJzANBglg # hkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqG # SIb3DQEJBDEiBCAifwU+08qPxam5FDDtcON18aXLsgrLFXnRafxtPFvwuzCB+gYL # KoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIOXnARo1oVIcOLJKDqlE0adq/jZ9TXdl # nXWRcXGThBFyMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAC # EzMAAAIn1cCDw7EuVy0AAQAAAicwIgQgOqiMddXLIn1AIlJeYqkxykgHwYVut9+R # yReORFwEDskwDQYJKoZIhvcNAQELBQAEggIA4Wiy6EsHcixfvnJSyfLziHtLOjhX # kdVJW6Q5hthh8KlymkgONFHiI83g73Pr7hKTyLSHrmxbbgDox9W8nIdiUQYkBvvt # OlhHquEefFbc4yMtaVtMonqDVPhOPwM23RHDS72RRq5hpIKMFzGpuO9IjhsMEBcJ # Nrka8ZBVkEHRVi135Qm0qrhsYegZGnq12bDgsHlKp5EQTowbNXG9b8xdX6cNJwwo # rM8wIIOkHkaaDNrh7Z62UJIIa+c9p5tz4G2gYbbvpZx153zgnylac+aXGQ11DVIQ # erK4Atp+hSVhdncDNoCQNWqTAA01efkFiWRXlOHQXWhw00o5vCsIuA48v2Ait8au # riCxBvmSJakwHvdVKAwFVveA+HLYzjckcKlmHmK/L2SwYt+GlzwquD1cF5URTooC # Lg40HME7ImedauDhllWVw+sDPhk3E6fIOrklfjqzE3UV7f160XuHkiSPPYLpPouy # b8ni+R2CRcUh/aRCRcMxmsqnz6mQyv0/n1TsOrCxnl3waU+7Ci3YadmNJTqtT1mj # oAcUVhRMTDY5c38AueHwg2MRmg6cj7Tmi21JIT5T3i70JnDtt7altdleCYp+yCOk # 8qsPC70h6lCJF4DiYliRygYcRc6VcnvgFG47xgPgGoWMqiB6+E5aj6VptdgXVeI/ # XwUREmxoeBT8zdg= # SIG # End signature block