mirror of
https://github.com/microsoft/mcp.git
synced 2026-11-04 12:01:31 +00:00
223 lines
8.8 KiB
PowerShell
223 lines
8.8 KiB
PowerShell
# Install-Module -Name Pester -Force -SkipPublisherCheck
|
|
# Invoke-Pester -Path eng/scripts/StartupPerformance.Common.Tests.ps1
|
|
#
|
|
# Focused tests for the pure helpers used by Test-StartupPerformance.ps1.
|
|
|
|
BeforeAll {
|
|
. $PSScriptRoot/StartupPerformance.Common.ps1
|
|
|
|
$script:RunFolder = Join-Path $PSScriptRoot '.perf-common-testruns'
|
|
if (Test-Path $script:RunFolder) {
|
|
Remove-Item -Recurse -Force $script:RunFolder
|
|
}
|
|
New-Item -ItemType Directory -Path $script:RunFolder | Out-Null
|
|
}
|
|
|
|
AfterAll {
|
|
if (Test-Path $script:RunFolder) {
|
|
Remove-Item -Recurse -Force $script:RunFolder
|
|
}
|
|
}
|
|
|
|
Describe "Get-TimingStats" {
|
|
It "returns the single middle value as the median for an odd number of samples" {
|
|
# sorted: 3,5,9 -> middle = 5
|
|
$stats = Get-TimingStats -Samples @(9, 3, 5)
|
|
$stats.median | Should -Be 5
|
|
}
|
|
|
|
It "averages the two middle values as the median for an even number of samples" {
|
|
# sorted: 10,20,30,40 -> (20+30)/2 = 25
|
|
$stats = Get-TimingStats -Samples @(40, 10, 30, 20)
|
|
$stats.median | Should -Be 25
|
|
}
|
|
|
|
It "produces a fractional median when the two middle values differ by an odd amount" {
|
|
# sorted: 10,11 -> (10+11)/2 = 10.5
|
|
$stats = Get-TimingStats -Samples @(11, 10)
|
|
$stats.median | Should -Be 10.5
|
|
}
|
|
|
|
It "reports the first sample in original (unsorted) order" {
|
|
$stats = Get-TimingStats -Samples @(40, 10, 30, 20)
|
|
$stats.first | Should -Be 40
|
|
}
|
|
|
|
It "computes the average rounded to one decimal place" {
|
|
# (1+2+2)/3 = 1.666... -> 1.7
|
|
$stats = Get-TimingStats -Samples @(1, 2, 2)
|
|
$stats.average | Should -Be 1.7
|
|
}
|
|
|
|
It "handles a single sample" {
|
|
$stats = Get-TimingStats -Samples @(42)
|
|
$stats.median | Should -Be 42
|
|
$stats.first | Should -Be 42
|
|
$stats.average | Should -Be 42
|
|
}
|
|
|
|
It "throws when no samples are provided" {
|
|
{ Get-TimingStats -Samples @() } | Should -Throw
|
|
}
|
|
}
|
|
|
|
Describe "Get-TimingStats percentiles" {
|
|
It "reports p50 equal to the median for an odd number of samples" {
|
|
$stats = Get-TimingStats -Samples @(9, 3, 5)
|
|
$stats.p50 | Should -Be 5
|
|
}
|
|
|
|
It "orders p50 <= p95 <= p99 and keeps them within the sample range" {
|
|
$stats = Get-TimingStats -Samples @(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
|
$stats.p50 | Should -BeLessOrEqual $stats.p95
|
|
$stats.p95 | Should -BeLessOrEqual $stats.p99
|
|
$stats.p99 | Should -BeLessOrEqual 10
|
|
$stats.p95 | Should -BeGreaterThan 9 # rank 8.55 lands between 9 and 10
|
|
}
|
|
|
|
It "returns the single value for all percentiles when one sample" {
|
|
$stats = Get-TimingStats -Samples @(42)
|
|
$stats.p50 | Should -Be 42
|
|
$stats.p95 | Should -Be 42
|
|
$stats.p99 | Should -Be 42
|
|
}
|
|
}
|
|
|
|
Describe "Get-Percentile" {
|
|
It "returns the min at p0 and max at p100" {
|
|
(Get-Percentile -Samples @(10, 20, 30, 40) -Percentile 0.0) | Should -Be 10
|
|
(Get-Percentile -Samples @(10, 20, 30, 40) -Percentile 1.0) | Should -Be 40
|
|
}
|
|
|
|
It "interpolates between ranks" {
|
|
# sorted 10,20,30,40; rank = 0.5*3 = 1.5 -> 20 + 0.5*(30-20) = 25
|
|
(Get-Percentile -Samples @(40, 10, 30, 20) -Percentile 0.5) | Should -Be 25
|
|
}
|
|
|
|
It "returns an exact sample when the rank is an integer" {
|
|
# sorted 1..11, rank = 0.9*(11-1) = 9 -> sorted[9] = 10
|
|
(Get-Percentile -Samples @(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) -Percentile 0.9) | Should -Be 10
|
|
}
|
|
|
|
It "throws for out-of-range percentiles" {
|
|
{ Get-Percentile -Samples @(1) -Percentile 1.5 } | Should -Throw
|
|
{ Get-Percentile -Samples @(1) -Percentile -0.1 } | Should -Throw
|
|
}
|
|
|
|
It "throws when no samples are provided" {
|
|
{ Get-Percentile -Samples @() -Percentile 0.5 } | Should -Throw
|
|
}
|
|
}
|
|
|
|
Describe "Split-ColdWarm" {
|
|
It "treats the first sample as cold and the rest as warm" {
|
|
$split = Split-ColdWarm -Samples @(100, 40, 50, 60)
|
|
$split.cold_ms | Should -Be 100
|
|
# warm samples 40,50,60 -> median 50
|
|
$split.warm.median | Should -Be 50
|
|
}
|
|
|
|
It "falls back to the single sample for warm when only one sample exists" {
|
|
$split = Split-ColdWarm -Samples @(77)
|
|
$split.cold_ms | Should -Be 77
|
|
$split.warm.median | Should -Be 77
|
|
}
|
|
|
|
It "throws when no samples are provided" {
|
|
{ Split-ColdWarm -Samples @() } | Should -Throw
|
|
}
|
|
}
|
|
|
|
Describe "Resolve-PerfOutputPath" {
|
|
It "resolves a bare filename against the base directory and returns an absolute path" {
|
|
$result = Resolve-PerfOutputPath -Path 'startup-e2e.json' -BaseDirectory $script:RunFolder
|
|
[System.IO.Path]::IsPathRooted($result) | Should -Be $true
|
|
Split-Path -Parent $result | Should -Be ([System.IO.Path]::GetFullPath($script:RunFolder))
|
|
}
|
|
|
|
It "creates a missing parent directory for a relative nested path" {
|
|
$nested = Join-Path 'nested-a' 'nested-b/out.json'
|
|
$result = Resolve-PerfOutputPath -Path $nested -BaseDirectory $script:RunFolder
|
|
Test-Path (Split-Path -Parent $result) | Should -Be $true
|
|
}
|
|
|
|
It "creates a missing parent directory for an absolute path" {
|
|
$abs = Join-Path $script:RunFolder 'abs-dir/out.json'
|
|
$result = Resolve-PerfOutputPath -Path $abs
|
|
Test-Path (Split-Path -Parent $result) | Should -Be $true
|
|
$result | Should -Be ([System.IO.Path]::GetFullPath($abs))
|
|
}
|
|
|
|
It "does not throw for a bare filename (regression for empty Split-Path parent)" {
|
|
{ Resolve-PerfOutputPath -Path 'only-a-filename.json' -BaseDirectory $script:RunFolder } | Should -Not -Throw
|
|
}
|
|
}
|
|
|
|
Describe "Get-PerfRunMetadata" {
|
|
It "records all controlled-environment dimensions required by issue #3120" {
|
|
$m = Get-PerfRunMetadata
|
|
foreach ($key in @('commit', 'timestamp', 'runtime', 'os', 'os_architecture', 'cpu_cores', 'machine_memory_gb', 'workload_catalog_version')) {
|
|
$m.Contains($key) | Should -BeTrue -Because "metadata must include '$key'"
|
|
}
|
|
}
|
|
|
|
It "reports a positive processor count" {
|
|
(Get-PerfRunMetadata).cpu_cores | Should -BeGreaterThan 0
|
|
}
|
|
}
|
|
|
|
Describe "Get-WorkloadCatalogVersion" {
|
|
It "returns the catalog version from the committed workload catalog" {
|
|
Get-WorkloadCatalogVersion | Should -Not -BeNullOrEmpty
|
|
}
|
|
|
|
It "returns 'unknown' when the catalog file is absent" {
|
|
Get-WorkloadCatalogVersion -CatalogPath (Join-Path $script:RunFolder 'no-such-catalog.json') | Should -Be 'unknown'
|
|
}
|
|
}
|
|
|
|
Describe "Get-TimingStats sub-millisecond precision" {
|
|
It "preserves fractional-millisecond percentiles instead of rounding to whole ms (F-004)" {
|
|
# Regression: double samples previously bound to [long[]] and were rounded to 0,0,1,1.
|
|
$stats = Get-TimingStats -Samples @(0.41, 0.49, 0.51, 0.59)
|
|
$stats.median | Should -Be 0.5
|
|
$stats.p95 | Should -BeGreaterThan 0.5
|
|
$stats.p95 | Should -BeLessThan 0.6
|
|
}
|
|
}
|
|
|
|
Describe "Invoke-StartupRegressionGate" {
|
|
It "reports no failures for an identical baseline" {
|
|
$s = [ordered]@{ mcp_stdio_to_tools_list_ms = [ordered]@{ p50 = 100; p95 = 120; p99 = 140 } }
|
|
$b = [ordered]@{ mcp_stdio_to_tools_list_ms = [ordered]@{ p50 = 100; p95 = 120; p99 = 140 } }
|
|
$gate = Invoke-StartupRegressionGate -ResultScenarios $s -BaselineScenarios $b
|
|
@($gate.Failures).Count | Should -Be 0
|
|
}
|
|
|
|
It "fails when nothing overlaps results and baseline (F-007)" {
|
|
$s = [ordered]@{ mcp_stdio_to_tools_list_ms = [ordered]@{ p50 = 100; p95 = 120; p99 = 140 } }
|
|
$b = [ordered]@{ }
|
|
$gate = Invoke-StartupRegressionGate -ResultScenarios $s -BaselineScenarios $b
|
|
$gate.Failures | Should -Contain 'no_comparable_metrics'
|
|
}
|
|
|
|
It "fails when a metric the baseline expects is missing from results (F-007)" {
|
|
$s = [ordered]@{ mcp_stdio_to_tools_list_ms = [ordered]@{ p50 = 100; p95 = 120 } }
|
|
$b = [ordered]@{ mcp_stdio_to_tools_list_ms = [ordered]@{ p50 = 100; p95 = 120; p99 = 140 } }
|
|
$gate = Invoke-StartupRegressionGate -ResultScenarios $s -BaselineScenarios $b
|
|
$gate.Failures | Should -Contain 'mcp_stdio_default (p99, missing in results)'
|
|
}
|
|
|
|
It "treats a positive current against a zero baseline as a failure (F-010)" {
|
|
$s = [ordered]@{ mcp_stdio_to_tools_list_ms = [ordered]@{ p50 = 100; p95 = 120; p99 = 140 } }
|
|
$b = [ordered]@{ mcp_stdio_to_tools_list_ms = [ordered]@{ p50 = 0; p95 = 0; p99 = 0 } }
|
|
$gate = Invoke-StartupRegressionGate -ResultScenarios $s -BaselineScenarios $b
|
|
@($gate.Failures).Count | Should -BeGreaterThan 0
|
|
}
|
|
|
|
It "accepts a -RepoRoot argument when stamping run metadata (F-009)" {
|
|
$m = Get-PerfRunMetadata -RepoRoot $PSScriptRoot
|
|
$m.Contains('commit') | Should -BeTrue
|
|
}
|
|
}
|