Engines/Terraform/Invoke-AvmTerraformTest.ps1

function Invoke-AvmTerraformTest {
    <#
    .SYNOPSIS
        Validate Terraform examples and warn about uncovered local modules.

    .DESCRIPTION
        Initializes and validates each immediate examples/ configuration,
        including examples marked .e2eignore. Libraries remain child modules,
        allowing deprecated outputs. Diagnostics are relative to Context.Root.

        Each initialization uses a fresh, process-local TF_DATA_DIR so stale
        installed-module records cannot inflate coverage. Direct and transitive
        module directories are compared with the root and immediate modules/
        configurations. Uncovered modules produce warnings, not failures.

        FilesProcessed counts direct example configuration files. No examples
        returns Status='skipped'; real validation errors return Status='fail'.

    .PARAMETER Context
        Module context produced by Get-AvmModuleContext. Must have
        Ecosystem='terraform'.

    .PARAMETER AllowPathFallback
        Pass through to Resolve-AvmTool.

    .PARAMETER NoInit
        Validate examples using their existing initialization. Coverage is not
        assessed because existing module manifests can contain stale records;
        a warning reports that limitation.

    .PARAMETER UseExistingInit
        Reuse initialization prepared by a composite command and assess module
        coverage from each example's existing module manifest.

    .OUTPUTS
        pscustomobject with Engine, Tool, ToolPath, ToolSource, Status,
        FilesProcessed, Issues.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        $Context,

        [switch] $AllowPathFallback,

        [switch] $NoInit,

        [switch] $UseExistingInit
    )

    Set-StrictMode -Version 3.0
    $ErrorActionPreference = 'Stop'

    if ($Context.Ecosystem -ne 'terraform') {
        throw [System.ArgumentException]::new(
            "Invoke-AvmTerraformTest requires a terraform context (got Ecosystem='$($Context.Ecosystem)').")
    }
    if ($NoInit -and $UseExistingInit) {
        throw [System.ArgumentException]::new('NoInit and UseExistingInit cannot be combined.')
    }

    $tool = Resolve-AvmTool -Name 'terraform' -ModuleRoot $Context.Root -AllowPathFallback:$AllowPathFallback

    $scope = Get-AvmTerraformValidationScope -Root $Context.Root
    $issues = [System.Collections.Generic.List[object]]::new()
    $coverageIssues = [System.Collections.Generic.List[object]]::new()
    $coveredModules = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
    $filesProcessed = 0
    $anyFail = $false
    $exampleIndex = 0

    foreach ($example in $scope.Examples) {
        $exampleIndex++
        $dataDirectory = $null
        $ownsDataDirectory = $false
        $environment = @{}
        try {
            if (-not $NoInit -and -not $UseExistingInit) {
                Write-AvmLog (
                    'validate: initializing example {0}/{1}: {2}' -f
                    $exampleIndex,
                    $scope.Examples.Count,
                    $example.RelativePath) -Level Info | Out-Null
                $dataDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('avm-validate-' + [guid]::NewGuid().ToString('N').Substring(0, 12))
                $null = New-Item -ItemType Directory -Path $dataDirectory -ErrorAction Stop
                $ownsDataDirectory = $true
                $environment.TF_DATA_DIR = $dataDirectory
                $initResult = Invoke-AvmTerraformInit `
                    -TerraformPath $tool.Path `
                    -WorkingDirectory $example.Path `
                    -EnvVars $environment `
                    -StreamOutput:(Test-AvmVerboseEnabled) `
                    -Label ('terraform init {0}' -f $example.RelativePath) `
                    -BackendFalse `
                    -NoColor `
                    -IgnoreExitCode

                if ($initResult.ExitCode -ne 0) {
                    $message = Add-AvmProcessFailureDetail `
                        -Message ("terraform init for '{0}' failed with exit code {1}." -f $example.RelativePath, $initResult.ExitCode) `
                        -StdOut $initResult.StdOut `
                        -StdErr $initResult.StdErr
                    throw [AvmProcessException]::new($message)
                }
            }
            elseif ($UseExistingInit) {
                $dataDirectory = Join-Path $example.Path '.terraform'
                $environment.TF_PLUGIN_CACHE_DIR = Get-AvmTerraformPluginCachePath
            }

            $result = Invoke-AvmProcess `
                -FilePath $tool.Path `
                -ArgumentList @('validate', '-no-color', '-json') `
                -WorkingDirectory $example.Path `
                -EnvVars $environment `
                -IgnoreExitCode
            $filesProcessed += $example.Files.Count

            if ($result.ExitCode -notin @(0, 1)) {
                $message = Add-AvmProcessFailureDetail `
                    -Message ("terraform validate for '{0}' exited with code {1}." -f $example.RelativePath, $result.ExitCode) `
                    -StdOut $result.StdOut `
                    -StdErr $result.StdErr
                throw [AvmProcessException]::new($message)
            }

            try {
                $parsed = ConvertFrom-Json -InputObject ([string]$result.StdOut) -AsHashtable -ErrorAction Stop
            }
            catch [System.ArgumentException] {
                throw [AvmProcessException]::new(
                    "Could not parse terraform validate -json output for '$($example.RelativePath)': $($_.Exception.Message)")
            }
            if ($parsed -isnot [System.Collections.IDictionary] -or
                -not $parsed.Contains('valid') -or $parsed.valid -isnot [bool] -or
                -not $parsed.Contains('diagnostics') -or $parsed.diagnostics -isnot [array]) {
                throw [AvmProcessException]::new(
                    "Invalid terraform validate -json output for '$($example.RelativePath)': expected valid and diagnostics.")
            }

            $hasErrors = $false
            foreach ($diag in $parsed.diagnostics) {
                if ($diag -isnot [System.Collections.IDictionary] -or
                    -not $diag.Contains('severity') -or $diag.severity -notin @('error', 'warning') -or
                    -not $diag.Contains('summary') -or $diag.summary -isnot [string]) {
                    throw [AvmProcessException]::new(
                        "Invalid terraform validate diagnostic for '$($example.RelativePath)'.")
                }
                $severity = [string]$diag.severity
                if ($severity -eq 'error') { $hasErrors = $true }
                $detail = if ($diag.Contains('detail')) { [string]$diag.detail } else { '' }
                $message = if ($detail) { "$($diag.summary) - $detail" } else { [string]$diag.summary }
                $file = $example.RelativePath
                $line = 0
                $column = 0
                if ($diag.Contains('range') -and $diag.range) {
                    if ($diag.range.filename) {
                        $absoluteFile = [System.IO.Path]::GetFullPath([string]$diag.range.filename, $example.Path)
                        $file = [System.IO.Path]::GetRelativePath($Context.Root, $absoluteFile).Replace('\', '/')
                    }
                    if ($diag.range.start) {
                        $line = [int]$diag.range.start.line
                        $column = [int]$diag.range.start.column
                    }
                }
                $issues.Add([pscustomobject][ordered]@{
                        File     = $file
                        Line     = $line
                        Column   = $column
                        Severity = $severity
                        Code     = ''
                        Message  = ('[{0}] {1}' -f $example.RelativePath, $message)
                    })
            }

            if ($result.ExitCode -ne 0 -or -not $parsed.valid -or $hasErrors) {
                $anyFail = $true
                if (-not $hasErrors) {
                    $message = Add-AvmProcessFailureDetail `
                        -Message ("terraform validate for '{0}' reported failure without error diagnostics (exit {1})." -f $example.RelativePath, $result.ExitCode) `
                        -StdOut '' `
                        -StdErr $result.StdErr
                    $issues.Add([pscustomobject][ordered]@{
                            File     = $example.RelativePath
                            Line     = 0
                            Column   = 0
                            Severity = 'error'
                            Code     = ''
                            Message  = $message
                        })
                }
                continue
            }
            if ($NoInit) { continue }

            try {
                $manifestPath = Join-Path -Path (Join-Path -Path $dataDirectory -ChildPath 'modules') -ChildPath 'modules.json'
                $manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop
                foreach ($directory in (ConvertFrom-AvmTerraformModuleManifest -Payload $manifest -WorkingDirectory $example.Path -TestFiles $example.TestFiles)) {
                    $null = $coveredModules.Add($directory)
                }
            }
            catch [System.Management.Automation.ItemNotFoundException], [System.IO.IOException], [System.UnauthorizedAccessException], [AvmConfigurationException] {
                $coverageIssues.Add([pscustomobject][ordered]@{
                        File     = $example.RelativePath
                        Line     = 0
                        Column   = 0
                        Severity = 'warning'
                        Code     = 'terraform.module-coverage-unavailable'
                        Message  = "Could not determine module coverage for '$($example.RelativePath)': $($_.Exception.Message)"
                    })
            }
        }
        finally {
            if ($ownsDataDirectory -and (Test-Path -LiteralPath $dataDirectory -PathType Container)) {
                Remove-Item -LiteralPath $dataDirectory -Recurse -Force -ErrorAction Stop -ProgressAction SilentlyContinue
            }
        }
    }

    if ($scope.Examples.Count -eq 0) {
        $coverageIssues.Add([pscustomobject][ordered]@{
                File     = ''
                Line     = 0
                Column   = 0
                Severity = 'warning'
                Code     = 'terraform.module-coverage'
                Message  = 'No Terraform examples found under examples/; validation was skipped.'
            })
    }
    if ($NoInit -and $scope.Examples.Count -gt 0) {
        $coverageIssues.Add([pscustomobject][ordered]@{
                File     = ''
                Line     = 0
                Column   = 0
                Severity = 'warning'
                Code     = 'terraform.module-coverage-unavailable'
                Message  = 'Module coverage is not assessed with -NoInit because existing module manifests may be stale. Rerun without -NoInit to check coverage.'
            })
    }
    else {
        foreach ($module in $scope.Modules) {
            if ($coveredModules.Contains($module.Path)) { continue }
            $coverageIssues.Add([pscustomobject][ordered]@{
                    File     = [System.IO.Path]::GetRelativePath($Context.Root, $module.Files[0]).Replace('\', '/')
                    Line     = 0
                    Column   = 0
                    Severity = 'warning'
                    Code     = 'terraform.module-coverage'
                    Message  = "No example coverage was confirmed for module '$($module.RelativePath)'. Add an example that calls this checkout's module directly or transitively."
                })
        }
    }
    foreach ($issue in $coverageIssues) {
        $issues.Add($issue)
        Write-AvmLog -Message $issue.Message -Level Warning -File $issue.File
        Register-AvmPresentedIssue -Issue $issue
    }

    return [pscustomobject][ordered]@{
        Engine         = 'terraform'
        Tool           = ('{0}/{1}' -f $tool.Name, $tool.Version)
        ToolPath       = $tool.Path
        ToolSource     = $tool.Source
        Status         = if ($anyFail) { 'fail' } elseif ($scope.Examples.Count -eq 0) { 'skipped' } else { 'pass' }
        FilesProcessed = $filesProcessed
        Issues         = $issues.ToArray()
    }
}

# SIG # Begin signature block
# MIInUgYJKoZIhvcNAQcCoIInQzCCJz8CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC4s/63/dBkYyrl
# BogIJMjuft3t1amRLvDxyaJqaxs26KCCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1
# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP
# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC
# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf
# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j
# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT
# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT
# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw
# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl
# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN
# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK
# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK
# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY
# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu
# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE
# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz
# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6
# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO
# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD
# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC
# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX
# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ
# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq
# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo
# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv
# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a
# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1
# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO
# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7
# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ
# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS
# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm
# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3
# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E
# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX
# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP
# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw
# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC
# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D
# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY
# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI
# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6
# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w
# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7
# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK
# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK
# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw
# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT
# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghnfMIIZ2wIBATBu
# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc
# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIEIGCkVQVv19d1bGVbcDLAwHvz0AK/
# xpRaRcidy80L9374MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBv
# AGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAE
# ggEAujyqZSxwKXgzz43aMp4+zobStRAu1ajlwk2dYHC/2CX56Ohx+LGnElLtU5NT
# iRYMqI+M76LKv2yJ+4OKKy3Yf+wxulNH9r5DNCRiOLpzTySwYWsuAx1tsCQd/OMU
# LjQcVF/4mKSVhblm8RD6OhzNjK2aGCdQYL6nDU92fti8ZaIPhnRaUeJkghwVBBxB
# RKh5rZmj32sFkhRsh9LCHYkXxneO8yctTIM2/VsTzLMwrr/scTI7N8RwS2VwOhWs
# uAmBcLLxwb3aTMxicC5PE9OoSi9+p+xSoJ1uLrFWFpjhX852A1hnFRgGqY+hZXLC
# 7c0zMwUEsu8JbJK36lNfvICROqGCF68wgherBgorBgEEAYI3AwMBMYIXmzCCF5cG
# CSqGSIb3DQEHAqCCF4gwgheEAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsqhkiG
# 9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQC
# AQUABCAwCTyYz7aQV5Z1LaWli8Z5dYNAWOq272bWAkdPbYhD8QIGasTl3Lc0GBIy
# MDI2MTAxNTE5MDU1MS45MVowBIACAfSggdmkgdYwgdMxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5k
# IE9wZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjMy
# MUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2
# aWNloIIR/jCCBygwggUQoAMCAQICEzMAAAIaqaAdBqAPQ6oAAQAAAhowDQYJKoZI
# hvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO
# BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEm
# MCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjUwODE0
# MTg0ODI4WhcNMjYxMTEzMTg0ODI4WjCB0zELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0
# aW9ucyBMaW1pdGVkMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzIxQS0wNUUw
# LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCZgQDBJPPv2rZXdlbNDkS/tEqB
# p1C0wHLv5XddDxHQ0vxH2a6nFyolD8o95kYlMRH71Cr+3sc5B8FsPLp7RN6m8EVX
# 9FjfD4s48wqfRSiHb/wi91JsnyoBZFWjPZL1WsnNmkHz9/mtBxEROBf+3w3roPYm
# URe/h9lAHtfNwkxevWm6G5ds631FgTI3VDdntiNGSF8GxFz5IP8L0XiLBmp9CCjz
# YYbjCC4iGMlTv5cx+u/i/EAU1WDeafU+gxYZlaKj57Xj48Zg9UsqVp37QiF0crkC
# A/JcqSoCERmliFhhUQi0c46+qvC6TFUAlcy9YDcZq1aRFmffdYMlW2CEJbpc8uLV
# wMqIYTlRxdlJXg6NAhQHy+nYtQxFe53kjj0UgFwT2dPTTPwD4R6Ss8z44CTTtoN/
# Blt2ZnnqPu5vl80Mt/zIhvxDFnwyvhHBbL9zMG5XmuRZBD6ayMnkAq1hnEl2dpl6
# FSBQ0CtT+7fpIfV5coxAZFev/F4oUYjy++/kmXWSdnxSoRCv0/ENuKzs5enZZIwr
# mUsZ1hUfxWjCdgXexs6JGTHlDkZoTJN6E5CnZJ91uwlmWDRJeYemEaehbX+BD/k/
# oGBKrg8BYhloMmPoC8ssJ1tRGBHlqk1BB53bNhSBRuMAID9OiYDwuXsCuu/ahkaJ
# Q7lV2LjHG0DcFFNBNQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFPCrIgndAyg9qwNw
# Z0ai9tpjwiU2MB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1Ud
# HwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggr
# BgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw
# MTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgw
# DgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDVrvZYWrsHpslPdU4n
# bWedOg8n07+rnVvDVuE99DLru7L5/zHxqSKnM0vaTlvqa3G49tkakGqkEqC4PBCb
# FWlfxwaZp96jfAavhrxiTpLLT20SH83DCWzKrsFGsk2fpsY4HyIbg5PL6mYxSHsV
# 6M09GC+B6j84/K2bg02swyD3xRWWtnEY05iyJ+lEkWDmMT9i7qWoVrWVOb1we49j
# FZragTALSwQCxMVvr2Iqk3Sw7X3EFkKvSHkKVT0+Cjp6SIlvtAmgPOsOg9AfBs0D
# zsK2jtMu6mGPSb2X8jvSAuMSrndIeO5RHPCmY3F2bXxCD6uWRowLpjYq6Q58nugJ
# K729w0ZAz6KeX2Cw2CKtnrImT1WxcSyhO2hHt8w1To/Lq58lAYxOarpkKrZ4gY5d
# YwFvv1kXq2IpNripqaLdRLSZNjjUnXb1eYCCVXL66NJmQe7aUckNEezsWOchdlVQ
# TmmXrJQiXbeMbnR9FMtBxK13Bj8u8lSAQcIjOO+UtOou3olVHltyzlo3gOHRg8b3
# kH2IMxmuriuWLlKcY1Z6/ksuwNjV9usrq5WkP6my9Iuw2mG3btBwdGxh0AwAtcz4
# c2zPYtnzGI5/C3qs6xVZeiIdXzr9N4zLlNkVSXuoHn0g2gxImANGVp1Vd5P1/A66
# KsUiiqCMoaTe87ZsQutgw3RBXDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkA
# AAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVow
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX
# 9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1q
# UoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8d
# q6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byN
# pOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2k
# rnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4d
# Pf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgS
# Uei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8
# QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6Cm
# gyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzF
# ER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQID
# AQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQU
# KqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1
# GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0
# bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA
# QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL
# j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p
# Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwU
# tj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN
# 3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU
# 5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5
# KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGy
# qVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB6
# 2FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltE
# AY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFp
# AUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcd
# FYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRb
# atGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQd
# VTNYs6FwZvKhggNZMIICQQIBATCCAQGhgdmkgdYwgdMxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5k
# IE9wZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjMy
# MUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2
# aWNloiMKAQEwBwYFKw4DAhoDFQDxiu62YqlKu5sJoBixTim3UW3wNqCBgzCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA
# 7ntAsTAiGA8yMDI2MTAxNTEyMDgxN1oYDzIwMjYxMDE2MTIwODE3WjB3MD0GCisG
# AQQBhFkKBAExLzAtMAoCBQDue0CxAgEAMAoCAQACAibmAgH/MAcCAQACAhSCMAoC
# BQDufJIxAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEA
# AgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBALaxNMXdcaXL6W/l
# TNLRxS64GIdN34mCzrrbqMJzClskxGxLIJ+jZxy2h/igcps1Msjr+r3vTsbdk1zu
# JAoUV11MAHFTCU28kv/e2SV+6BVnpNvXHbINHmyYj+CrjNNzgOy9kxE6OsrdwAB9
# 3rpiz/oDjfUZ1atE3kqvMmR/tEy7iRJnpPuJdE4aRTf45/QnGoKLbdVvUHYN/rSZ
# x0dE2nBvmZHXAkoyKAAwsSlA+9RL0VOP49w45K1zAB8IBHod07QirN2m+kSb0Ugv
# ILleFvA0cQ34aFpylEpSrUnzy1L+amUIJefdNtOlMcgad7V6m5pGelISEqrMdhBV
# TEB/BoUxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx
# MAITMwAAAhqpoB0GoA9DqgABAAACGjANBglghkgBZQMEAgEFAKCCAUowGgYJKoZI
# hvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCCO9hOz7H5D2Ie4
# RmAY6Gt/47xSrA0SfCaFvvmiiYQDqzCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQw
# gb0EIJ16Icetu2kpzAHbR2hVTz1Ycg4fxLvDfu5odwOZ6Yr7MIGYMIGApH4wfDEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj
# cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIaqaAdBqAPQ6oAAQAAAhow
# IgQggpPwkBdkcAFPaS7OyRoLQNTH8ozyF6n84V8YwnD/7V0wDQYJKoZIhvcNAQEL
# BQAEggIAUolvQ2oMKJXB9Jvrgk7oJgDrdrwKt+Rt02SRv99e6TJ7a/7jML/u86LS
# VgO30CMeSe15wg751EtHA0VhW4eJbOzt53z8qa8wevuOiR5D9IlXIYiq6+a3vOzG
# ShUFLymKJMfcsBYjzw/QVoCVRH+rtnoKwbAvxmF8Zq49UoBs+5JH1oLzTIwYOC5N
# NMD+MIdt9HPRZxDeji3+i/pLVkOqv88QiHGQEdfbuIoTNSsWh9jhgZW0Z4Hmk+Dk
# omdR4db/yhZFdpHCkySWkOG0J/vWh3b5ReSwPyF9Yrb4gEHoO/FCU3RjAiY63TO6
# LWYsGV6swwxQ8JY3R1SJF/FJxiuoldalyNIG26XJ2mF2enSSE8m6aq/AiJ61GPJm
# oGmOymeBH1fR0aaDOXPhqdj0A722Alh6GJ+vUJfJgGkMInhNC1sULcX2PCNDMRhS
# 9/8/nqRK5sWidU7lLCsXuGiZdwqOdQECATuo05+48DyUIDjQbZmr9gQtlscwRTGm
# PLxnd5mvy30UtVMxNmFAfsnq/C644GKm1amCFeit7TwLtzjSnVfD4aMCDH2Z91WE
# yqyxB37GSM/hThaG3TELp4m30umIPJQTvo3eQy0s3M/SMiygQ/w3mfojXPLMugP4
# TUq6YUZoVBRJdLhSDBo5hsB6BQ5/cpvUJZrVkCio2991u0URqUM=
# SIG # End signature block