diff --git a/src/powershell/Tests/Unit/Update-InstanceSizeFlexibility.Tests.ps1 b/src/powershell/Tests/Unit/Update-InstanceSizeFlexibility.Tests.ps1 index 0c23bda67..e44367463 100644 --- a/src/powershell/Tests/Unit/Update-InstanceSizeFlexibility.Tests.ps1 +++ b/src/powershell/Tests/Unit/Update-InstanceSizeFlexibility.Tests.ps1 @@ -365,6 +365,62 @@ Describe 'Update-InstanceSizeFlexibility' { } } + Context 'Sort order' { + # Culture-aware collation interleaves case and orders "Dasv4 Series" before + # "DCadsv5-series DedicatedHost"; an ordinal comparison puts the uppercase letter first. + # Sorting with the current culture makes the same records reorder between hosts, turning + # a no-op regeneration into a few hundred lines of diff that bury any real change. + It 'Orders flexibility groups ordinally rather than by the current culture' { + Mock Invoke-AzRestMethod { + New-CatalogResponse -Items @( + (New-CatalogItem -Sku 'Standard_D2as_v4' -Group 'Dasv4 Series' -Ratio '1'), + (New-CatalogItem -Sku 'dcadsv5 type 1' -Group 'DCadsv5-series DedicatedHost' -Ratio '1') + ) + } + + Invoke-Generator $baseParams + + @(Import-Csv $outFile).InstanceSizeFlexibilityGroup | + Should -Be @('DCadsv5-series DedicatedHost', 'Dasv4 Series') + } + + It 'Orders SKUs within a group ordinally' { + Mock Invoke-AzRestMethod { + New-CatalogResponse -Items @( + (New-CatalogItem -Sku 'Standard_Dadsv5' -Group 'Dadsv5 Series' -Ratio '1'), + (New-CatalogItem -Sku 'Standard_DC2ads_v5' -Group 'Dadsv5 Series' -Ratio '1') + ) + } + + Invoke-Generator $baseParams + + @(Import-Csv $outFile).ArmSkuName | Should -Be @('Standard_DC2ads_v5', 'Standard_Dadsv5') + } + + It 'Orders identically under a culture with different collation rules' { + $originalCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture + try + { + [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::new('de-DE') + Mock Invoke-AzRestMethod { + New-CatalogResponse -Items @( + (New-CatalogItem -Sku 'Standard_D2as_v4' -Group 'Dasv4 Series' -Ratio '1'), + (New-CatalogItem -Sku 'dcadsv5 type 1' -Group 'DCadsv5-series DedicatedHost' -Ratio '1') + ) + } + + Invoke-Generator $baseParams + + @(Import-Csv $outFile).InstanceSizeFlexibilityGroup | + Should -Be @('DCadsv5-series DedicatedHost', 'Dasv4 Series') + } + finally + { + [System.Threading.Thread]::CurrentThread.CurrentCulture = $originalCulture + } + } + } + Context 'Published dataset' { It 'Has globally unique ArmSkuName values' { $csv = Import-Csv "$PSScriptRoot/../../../open-data/InstanceSizeFlexibility.csv" @@ -388,6 +444,24 @@ Describe 'Update-InstanceSizeFlexibility' { @($csv | Where-Object { [double]$_.Ratio -le 0 }).ArmSkuName | Should -BeNullOrEmpty } + It 'Is written in ordinal sort order' { + # Catches a file regenerated on a host whose culture collates differently, which + # reorders hundreds of rows without changing a single value. The comparer mirrors the + # one in src/scripts/Update-InstanceSizeFlexibility.ps1 and must stay in sync with it: + # if the script gains a sort key, this test keeps asserting the old order until it does + # too. + $csv = @(Import-Csv "$PSScriptRoot/../../../open-data/InstanceSizeFlexibility.csv") + $sorted = [Collections.Generic.List[object]]::new([object[]]$csv) + $sorted.Sort([Comparison[object]] { + param($x, $y) + $byGroup = [string]::CompareOrdinal($x.InstanceSizeFlexibilityGroup, $y.InstanceSizeFlexibilityGroup) + if ($byGroup -ne 0) { return $byGroup } + [string]::CompareOrdinal($x.ArmSkuName, $y.ArmSkuName) + }) + + ($sorted.ArmSkuName -join "`n") | Should -BeExactly ($csv.ArmSkuName -join "`n") + } + It 'Has the documented three-column schema' { $csv = Import-Csv "$PSScriptRoot/../../../open-data/InstanceSizeFlexibility.csv" @($csv[0].PSObject.Properties.Name) | Should -Be @('InstanceSizeFlexibilityGroup', 'ArmSkuName', 'Ratio') diff --git a/src/scripts/Update-InstanceSizeFlexibility.ps1 b/src/scripts/Update-InstanceSizeFlexibility.ps1 index c6406267a..d1123c316 100644 --- a/src/scripts/Update-InstanceSizeFlexibility.ps1 +++ b/src/scripts/Update-InstanceSizeFlexibility.ps1 @@ -368,10 +368,27 @@ if (-not $Raw) $allRecords = Get-NormalizedRecords -Records $allRecords } +# Sorted ordinally rather than with Sort-Object, whose comparer follows the current culture. +# Culture-aware collation orders "DCadsv5-series DedicatedHost" after "Dasv4 Series" while an +# ordinal comparison puts it before, so the same records regenerated on a differently configured +# host produce a few hundred lines of pure reordering that bury any real change in review. +# +# List.Sort is unstable where Sort-Object is stable, but no tie can reach it: $seen is keyed by +# group + ArmSkuName, and a duplicate ArmSkuName across groups throws above, so the two keys order +# every record uniquely in the normalized and the -Raw path alike. Keep the comparer in sync with +# the published-file order test in src/powershell/Tests/Unit/Update-InstanceSizeFlexibility.Tests.ps1. +$sortedRecords = [Collections.Generic.List[object]]::new() +if ($allRecords) { $sortedRecords.AddRange([object[]]@($allRecords)) } # an empty sweep leaves $allRecords null +$sortedRecords.Sort([Comparison[object]] { + param($x, $y) + $byGroup = [string]::CompareOrdinal($x.InstanceSizeFlexibilityGroup, $y.InstanceSizeFlexibilityGroup) + if ($byGroup -ne 0) { return $byGroup } + [string]::CompareOrdinal($x.ArmSkuName, $y.ArmSkuName) + }) + # Ratio is formatted invariantly rather than left to Export-Csv, which uses the current culture: # on a comma-decimal machine it would publish "2,1" and break every consumer of the file. -$rows = $allRecords | - Sort-Object InstanceSizeFlexibilityGroup, ArmSkuName | +$rows = $sortedRecords | Select-Object InstanceSizeFlexibilityGroup, ArmSkuName, @{ Name = 'Ratio'; Expression = { $_.Ratio.ToString([Globalization.CultureInfo]::InvariantCulture) } }