tests/Unit/Public/Get-sqmWaitStatistics.Tests.ps1

#Requires -Modules Pester
<#
.SYNOPSIS
    Unit Tests fuer Get-sqmWaitStatistics
 
.DESCRIPTION
    Die Fixture ist ein echter Kundenlauf (SQL 2022, SUEB011IBP, Top 25). Genau
    dieser Datensatz hat beide Schwaechen gezeigt: SOS_WORK_DISPATCHER mit 88 %
    der ausgewiesenen Wartezeit und Fehlalarme auf PAGEIOLATCH_SH (2,05 ms) und
    SOS_SCHEDULER_YIELD (0,12 ms).
#>


BeforeAll {
    . "$PSScriptRoot\..\..\..\tests\TestHelpers.ps1"
    Import-sqmTestModule
    $script:TestDir = New-TempTestDirectory

    function New-WaitRow {
        param($Type, [long]$Count, [long]$WaitMs, [long]$MaxMs, [long]$SignalMs, [double]$AvgMs)
        [PSCustomObject]@{
            wait_type             = $Type
            waiting_tasks_count   = $Count
            wait_time_ms          = $WaitMs
            max_wait_time_ms      = $MaxMs
            signal_wait_time_ms   = $SignalMs
            resource_wait_time_ms = $WaitMs - $SignalMs
            avg_wait_ms           = $AvgMs
        }
    }

    $script:CustomerWaits = @(
        New-WaitRow 'SOS_WORK_DISPATCHER' 8481864 59046197000 82628850 3954784 6961.46
        New-WaitRow 'CXCONSUMER' 177252141 2587228100 3953651 37350540 14.6
        New-WaitRow 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' 19101 1146148900 60160 11790 60004.65
        New-WaitRow 'QDS_ASYNC_QUEUE' 7404 1145894500 109166243 3154 154766.95
        New-WaitRow 'CXPACKET' 395601356 915066700 941341 49989992 2.31
        New-WaitRow 'CXSYNC_PORT' 400081 882142800 26664599 150022 2204.91
        New-WaitRow 'PREEMPTIVE_XE_DISPATCHER' 3 508874500 268086608 0 169624832.33
        New-WaitRow 'BACKUPBUFFER' 97202564 123778700 714 3260932 1.27
        New-WaitRow 'ASYNC_IO_COMPLETION' 327 91442400 4596846 47 279640.5
        New-WaitRow 'BACKUPIO' 67566859 84269500 372 3132415 1.25
        New-WaitRow 'PAGEIOLATCH_SH' 36858777 75560600 1988 3547287 2.05
        New-WaitRow 'OLEDB' 1468903916 47558200 43282 0 0.03
        New-WaitRow 'LATCH_EX' 23827142 36934800 3224 5263450 1.55
        New-WaitRow 'SOS_SCHEDULER_YIELD' 270528765 31433700 3844 31012820 0.12
        New-WaitRow 'BACKUPTHREAD' 8088 24494200 1195866 1286 3028.46
        New-WaitRow 'BROKER_TASK_STOP' 2440 24420700 10061 1400 10008.5
        New-WaitRow 'PAGEIOLATCH_EX' 63742912 16873600 413 156640 0.26
        New-WaitRow 'MEMORY_ALLOCATION_EXT' 9034020323 11463300 221 0 0
        New-WaitRow 'ASYNC_NETWORK_IO' 30531253 7683100 2016 933681 0.25
        New-WaitRow 'MSQL_XP' 107679 6905200 3894 0 64.13
        New-WaitRow 'BUFFERPOOL_SCAN' 3045 4932900 17288 1692 1619.99
        New-WaitRow 'IO_COMPLETION' 6256567 3765500 719 156454 0.6
        New-WaitRow 'SLEEP_BPOOL_FLUSH' 335245 3666700 216 38944 10.94
        New-WaitRow 'WAIT_ON_SYNC_STATISTICS_REFRESH' 10763 3121200 20119 0 289.99
        New-WaitRow 'WRITELOG' 1288894 2546100 839 97178 1.98
    )
}

AfterAll {
    if (Test-Path $script:TestDir) { Remove-Item $script:TestDir -Recurse -Force }
    if (Get-Module sqmSQLTool) { Remove-Module sqmSQLTool -Force }
    $env:MSSQLTOOLS_SKIP_AUTO_UPDATE = $null
}

Describe 'Get-sqmWaitStatistics' {

    Context 'Parameter-Validierung' {
        It 'Funktion existiert' {
            Get-Command Get-sqmWaitStatistics | Should -Not -BeNullOrEmpty
        }

        It '<_> Parameter existiert' -ForEach @('TopN', 'IncludeIdle', 'SnapshotBefore', 'SaveSnapshot', 'OutputPath') {
            (Get-Command Get-sqmWaitStatistics).Parameters.ContainsKey($_) | Should -Be $true
        }
    }

    Context 'Kundenlauf SUEB011IBP (SQL 2022)' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { $script:CustomerWaits }
            InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true }

            $script:Result = Get-sqmWaitStatistics -SqlInstance 'SUEB011IBP' -TopN 25
            $script:ByType = @{}
            foreach ($r in $script:Result) { $script:ByType[$r.WaitType] = $r }
        }

        It 'Idle-Wait <_> erscheint nicht im Report' -ForEach @(
            'SOS_WORK_DISPATCHER', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'QDS_ASYNC_QUEUE',
            'PREEMPTIVE_XE_DISPATCHER', 'BROKER_TASK_STOP', 'SLEEP_BPOOL_FLUSH',
            'MEMORY_ALLOCATION_EXT'
        ) {
            $script:ByType.ContainsKey($_) | Should -Be $false
        }

        It 'Parallelismus ist der dominierende Befund (~88 % der echten Waits)' {
            $cx = ($script:Result | Where-Object Category -eq 'Parallelism' |
                Measure-Object WaitTimePct -Sum).Sum
            $cx | Should -BeGreaterThan 85
        }

        It 'CXSYNC_PORT wird als Parallelism erkannt, nicht als Other' {
            $script:ByType['CXSYNC_PORT'].Category | Should -Be 'Parallelism'
        }

        It 'CXCONSUMER meldet weiterhin eine Empfehlung' {
            $script:ByType['CXCONSUMER'].Recommendation | Should -Match 'MAXDOP'
        }

        It 'PAGEIOLATCH_SH mit 2,05 ms meldet keinen I/O-Engpass' {
            $script:ByType['PAGEIOLATCH_SH'].Recommendation | Should -Not -Match 'bottleneck|Engpass'
        }

        It 'PAGEIOLATCH_SH weist den Durchschnitt als unauffaellig aus' {
            $script:ByType['PAGEIOLATCH_SH'].Recommendation | Should -Match '2[.,]05'
        }

        It 'SOS_SCHEDULER_YIELD meldet keinen CPU-Druck (Signal-Wait-Anteil 2,7 %)' {
            $rec = $script:ByType['SOS_SCHEDULER_YIELD'].Recommendation
            $rec | Should -Not -Match 'optimize long-running|langlaufende Abfragen'
            $rec | Should -Match 'Inconspicuous|Unauffaellig'
        }

        It 'ASYNC_NETWORK_IO mit 0,25 ms meldet keinen langsamen Client' {
            $script:ByType['ASYNC_NETWORK_IO'].Recommendation | Should -Not -Match 'too slowly|zu langsam'
        }

        It 'SignalWaitPct wird je Wait ausgewiesen' {
            $script:ByType['SOS_SCHEDULER_YIELD'].SignalWaitPct | Should -BeGreaterThan 90
        }

        It 'WaitTimePct summiert sich ueber die bereinigte Basis auf ~100 %' {
            ($script:Result | Measure-Object WaitTimePct -Sum).Sum | Should -BeGreaterThan 99
        }
    }

    Context 'Schwellwerte feuern bei echten Problemen' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true }
        }

        It 'PAGEIOLATCH_SH mit 45 ms meldet den I/O-Engpass' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(New-WaitRow 'PAGEIOLATCH_SH' 1000 45000 500 100 45.0)
            }
            $r = Get-sqmWaitStatistics -SqlInstance 'TESTSERVER'
            $r.Recommendation | Should -Match 'bottleneck|Engpass'
        }

        It 'SOS_SCHEDULER_YIELD meldet CPU-Druck bei hohem Signal-Wait-Anteil' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(New-WaitRow 'SOS_SCHEDULER_YIELD' 100000 50000 10 45000 0.5)
            }
            $r = Get-sqmWaitStatistics -SqlInstance 'TESTSERVER'
            $r.Recommendation | Should -Match 'CPU pressure|CPU-Druck'
        }

        It 'THREADPOOL meldet immer, unabhaengig vom Durchschnitt' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(New-WaitRow 'THREADPOOL' 10 20 5 2 2.0)
            }
            $r = Get-sqmWaitStatistics -SqlInstance 'TESTSERVER'
            $r.Recommendation | Should -Match 'worker|Worker'
        }
    }

    Context 'IncludeIdle' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { $script:CustomerWaits }
            InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true }
        }

        It 'Mit -IncludeIdle erscheinen die Idle-Waits wieder' {
            $r = Get-sqmWaitStatistics -SqlInstance 'SUEB011IBP' -IncludeIdle -TopN 25
            $r.WaitType | Should -Contain 'SOS_WORK_DISPATCHER'
        }
    }

    Context 'Snapshot-Vergleich' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true }
        }

        It 'SaveSnapshot liefert die Rohdaten zurueck' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(New-WaitRow 'CXPACKET' 100 5000 50 20 50.0)
            }
            $snap = Get-sqmWaitStatistics -SqlInstance 'TESTSERVER' -SaveSnapshot
            $snap.wait_type    | Should -Be 'CXPACKET'
            $snap.wait_time_ms | Should -Be 5000
        }

        # Die Schwellwerte muessen im Delta-Modus auf den Delta-Durchschnitt greifen,
        # nicht auf den kumulierten - sonst haengt die Bewertung wieder an der Uptime.
        It 'Delta-Modus bewertet den Delta-Durchschnitt (60 ms meldet den Engpass)' {
            $before = @(New-WaitRow 'PAGEIOLATCH_SH' 100 1000 50 100 10.0)
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(New-WaitRow 'PAGEIOLATCH_SH' 200 7000 50 700 35.0)
            }
            $r = Get-sqmWaitStatistics -SqlInstance 'TESTSERVER' -SnapshotBefore $before -EnableException
            $r.IsDelta        | Should -Be $true
            $r.WaitTimeSec    | Should -Be 6.0
            $r.AvgWaitMs      | Should -Be 60
            $r.SignalWaitPct  | Should -Be 10
            $r.Recommendation | Should -Match 'bottleneck|Engpass'
        }

        It 'Delta-Modus: niedriger Delta-Durchschnitt meldet keinen Engpass' {
            $before = @(New-WaitRow 'PAGEIOLATCH_SH' 100 1000 50 100 10.0)
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(New-WaitRow 'PAGEIOLATCH_SH' 1100 3000 50 300 2.7)
            }
            $r = Get-sqmWaitStatistics -SqlInstance 'TESTSERVER' -SnapshotBefore $before -EnableException
            $r.AvgWaitMs      | Should -Be 2
            $r.Recommendation | Should -Match 'Inconspicuous|Unauffaellig'
        }
    }

    Context 'Randfaelle' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true }
        }

        It 'Nur Idle-Waits: leeres Ergebnis statt Division durch Null' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(
                    New-WaitRow 'SOS_WORK_DISPATCHER' 100 5000000 900 10 50000.0
                    New-WaitRow 'QDS_ASYNC_QUEUE' 50 4000000 800 5 80000.0
                )
            }
            $script:r = $null
            { $script:r = Get-sqmWaitStatistics -SqlInstance 'TESTSERVER' -EnableException } | Should -Not -Throw
            $script:r | Should -BeNullOrEmpty
        }

        It 'Wait mit wait_time_ms = 0 wirft nicht' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
                @(New-WaitRow 'PAGEIOLATCH_SH' 5 0 0 0 0.0)
            }
            { Get-sqmWaitStatistics -SqlInstance 'TESTSERVER' -EnableException } | Should -Not -Throw
        }
    }

    Context 'Ausgabe' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { $script:CustomerWaits }
            Mock -ModuleName sqmSQLTool Invoke-sqmOpenReport { }
            InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true }
        }

        It 'Laeuft mit OutputPath ohne Fehler durch' {
            { Get-sqmWaitStatistics -SqlInstance 'SUEB011IBP' -OutputPath $script:TestDir -NoOpen } |
                Should -Not -Throw
        }
    }
}