Skip to content

Commit b68b5b2

Browse files
Strengthen conformance value comparisons
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3655dcc commit b68b5b2

2 files changed

Lines changed: 190 additions & 43 deletions

File tree

tests/Conformance.Tests.ps1

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,18 @@ BeforeAll {
2626
throw "Expected one release archive root, but found $($suiteRoots.Count)."
2727
}
2828
$suiteDataPath = $suiteRoots[0].FullName
29+
$runnerPath = Join-Path $PSScriptRoot 'tools\Invoke-YamlTestSuite.ps1'
2930
$suiteResults = @(
30-
& (Join-Path $PSScriptRoot 'tools\Invoke-YamlTestSuite.ps1') `
31+
& $runnerPath `
3132
-Path $suiteDataPath `
3233
-CompareJson `
3334
-CompareEvents `
3435
-CompareOutYaml `
3536
-CompareEmitRoundTrip
3637
)
38+
$emptySuitePath = Join-Path $TestDrive 'empty-suite'
39+
$null = New-Item -Path $emptySuitePath -ItemType Directory
40+
. $runnerPath -Path $emptySuitePath -CompareJson
3741
}
3842

3943
Describe 'Released yaml-test-suite corpus accounting' {
@@ -145,6 +149,89 @@ Describe 'Released yaml-test-suite corpus accounting' {
145149
Should -Be @('ConstructedValueMismatch')
146150
}
147151

152+
It 'canonicalizes binary, complex-key, ordered, and unsupported values distinctly' {
153+
(ConvertTo-YamlSuiteCanonicalValue -Value ([byte[]] @(1, 2))) |
154+
Should -Not -Be (ConvertTo-YamlSuiteCanonicalValue -Value ([byte[]] @(3, 4)))
155+
156+
$firstComplex = [System.Collections.Specialized.OrderedDictionary]::new()
157+
$secondComplex = [System.Collections.Specialized.OrderedDictionary]::new()
158+
$firstComplex.Add([object[]] @('a'), 1)
159+
$secondComplex.Add([object[]] @('b'), 1)
160+
(ConvertTo-YamlSuiteCanonicalValue -Value $firstComplex) |
161+
Should -Not -Be (ConvertTo-YamlSuiteCanonicalValue -Value $secondComplex)
162+
163+
$firstOrder = [System.Collections.Specialized.OrderedDictionary]::new()
164+
$secondOrder = [System.Collections.Specialized.OrderedDictionary]::new()
165+
$firstOrder.Add('a', 1)
166+
$firstOrder.Add('b', 2)
167+
$secondOrder.Add('b', 2)
168+
$secondOrder.Add('a', 1)
169+
(ConvertTo-YamlSuiteCanonicalValue -Value $firstOrder) |
170+
Should -Not -Be (ConvertTo-YamlSuiteCanonicalValue -Value $secondOrder)
171+
172+
(ConvertTo-YamlSuiteCanonicalValue -Value ([uri] 'https://example.com/one')) |
173+
Should -Not -Be (
174+
ConvertTo-YamlSuiteCanonicalValue -Value ([uri] 'https://example.com/two')
175+
)
176+
}
177+
178+
It 'includes binary and complex mapping-key identity in reference signatures' {
179+
$sharedBytes = [byte[]] @(1, 2)
180+
$sharedBinaryGraph = [object[]] @($sharedBytes, $sharedBytes)
181+
$distinctBinaryGraph = [object[]] @([byte[]] @(1, 2), [byte[]] @(1, 2))
182+
(ConvertTo-YamlSuiteReferenceSignature -Value $sharedBinaryGraph) |
183+
Should -Not -Be (ConvertTo-YamlSuiteReferenceSignature -Value $distinctBinaryGraph)
184+
185+
$sharedKey = [object[]] @('key')
186+
$sharedKeyGraph = [System.Collections.Specialized.OrderedDictionary]::new()
187+
$sharedKeyGraph.Add($sharedKey, $sharedKey)
188+
$distinctKeyGraph = [System.Collections.Specialized.OrderedDictionary]::new()
189+
$distinctKeyGraph.Add([object[]] @('key'), [object[]] @('key'))
190+
(ConvertTo-YamlSuiteReferenceSignature -Value $sharedKeyGraph) |
191+
Should -Not -Be (ConvertTo-YamlSuiteReferenceSignature -Value $distinctKeyGraph)
192+
}
193+
194+
It 'detects altered ordered-map and alias semantics in out.yaml' {
195+
$mutatedSuitePath = Join-Path $TestDrive 'mutated-out-cases'
196+
$null = New-Item -Path $mutatedSuitePath -ItemType Directory -Force
197+
foreach ($case in @('J7PZ', 'UGM3')) {
198+
Copy-Item -LiteralPath (Join-Path $suiteDataPath $case) `
199+
-Destination $mutatedSuitePath -Recurse
200+
}
201+
202+
@'
203+
--- !!omap
204+
- Sammy Sosa: 63
205+
- Mark McGwire: 65
206+
- Ken Griffy: 58
207+
'@ | Set-Content -LiteralPath (Join-Path $mutatedSuitePath 'J7PZ\out.yaml') `
208+
-Encoding utf8NoBOM
209+
210+
$invoicePath = Join-Path $mutatedSuitePath 'UGM3\out.yaml'
211+
$invoice = Get-Content -LiteralPath $invoicePath -Raw
212+
$duplicateAddress = @'
213+
ship-to:
214+
given: Chris
215+
family: Dumars
216+
address:
217+
lines: |
218+
458 Walkman Dr.
219+
Suite #292
220+
city: Royal Oak
221+
state: MI
222+
postal: 48046
223+
'@
224+
$invoice.Replace('ship-to: *id001', $duplicateAddress.TrimEnd()) |
225+
Set-Content -LiteralPath $invoicePath -Encoding utf8NoBOM
226+
227+
$mutatedResults = @(
228+
& $runnerPath -Path $mutatedSuitePath -CompareOutYaml
229+
)
230+
@($mutatedResults | Where-Object OutYamlResult -EQ 'Fail').Count | Should -Be 2
231+
@($mutatedResults | Sort-Object Case | Select-Object -ExpandProperty OutYamlReason) |
232+
Should -Be @('OutYamlConstructionMismatch', 'OutYamlReferenceMismatch')
233+
}
234+
148235
It 'accounts for out.yaml representation comparisons' {
149236
@($suiteResults | Where-Object OutYamlResult -EQ 'Pass').Count | Should -Be 241
150237
@($suiteResults | Where-Object OutYamlResult -EQ 'PolicyDifference').Count |

tests/tools/Invoke-YamlTestSuite.ps1

Lines changed: 102 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ function Split-YamlSuiteJsonDocument {
115115
function ConvertTo-YamlSuiteCanonicalValue {
116116
param (
117117
[AllowNull()]
118-
[object] $Value
118+
[object] $Value,
119+
120+
[switch] $SortMappings
119121
)
120122

121123
if ($null -eq $Value -or $Value -is [System.DBNull]) {
@@ -127,6 +129,32 @@ function ConvertTo-YamlSuiteCanonicalValue {
127129
if ($Value -is [bool]) {
128130
return 'bool:{0}' -f $Value.ToString().ToLowerInvariant()
129131
}
132+
if ($Value -is [byte[]]) {
133+
return 'binary:{0}:{1}' -f $Value.Length, [System.Convert]::ToBase64String($Value)
134+
}
135+
if ($Value -is [char]) {
136+
return 'char:{0}' -f [int] $Value
137+
}
138+
if ($Value.GetType().IsEnum) {
139+
return 'enum:{0}:{1}' -f $Value.GetType().FullName, (
140+
[System.Convert]::ToUInt64($Value, [cultureinfo]::InvariantCulture)
141+
)
142+
}
143+
if ($Value -is [datetimeoffset]) {
144+
return 'timestamp:{0}:{1}' -f $Value.UtcTicks, $Value.Offset.Ticks
145+
}
146+
if ($Value -is [datetime]) {
147+
return 'datetime:{0}:{1}' -f $Value.Ticks, [int] $Value.Kind
148+
}
149+
if ($Value -is [timespan]) {
150+
return 'timespan:{0}' -f $Value.Ticks
151+
}
152+
if ($Value -is [guid]) {
153+
return 'guid:{0}' -f $Value.ToString('D')
154+
}
155+
if ($Value -is [uri]) {
156+
return 'uri:{0}:{1}' -f $Value.OriginalString.Length, $Value.OriginalString
157+
}
130158

131159
$typeCode = [System.Type]::GetTypeCode($Value.GetType())
132160
if ($Value -is [System.Numerics.BigInteger] -or $typeCode -in @(
@@ -149,27 +177,48 @@ function ConvertTo-YamlSuiteCanonicalValue {
149177
}
150178
if ($Value -is [System.Collections.IDictionary]) {
151179
$entries = [System.Collections.Generic.List[string]]::new()
152-
foreach ($key in $Value.Keys) {
153-
if ($key -isnot [string]) {
154-
return 'unsupported:non-string-mapping-key'
155-
}
156-
$canonicalValue = ConvertTo-YamlSuiteCanonicalValue -Value $Value[$key]
157-
$entries.Add(('{0}:{1}={2}' -f $key.Length, $key, $canonicalValue))
180+
foreach ($entry in $Value.GetEnumerator()) {
181+
$canonicalKey = ConvertTo-YamlSuiteCanonicalValue -Value $entry.Key `
182+
-SortMappings:$SortMappings
183+
$canonicalValue = ConvertTo-YamlSuiteCanonicalValue -Value $entry.Value `
184+
-SortMappings:$SortMappings
185+
$entries.Add(('{0}:{1}={2}:{3}' -f
186+
$canonicalKey.Length,
187+
$canonicalKey,
188+
$canonicalValue.Length,
189+
$canonicalValue
190+
))
191+
}
192+
$isOrdered = (
193+
$Value -is [System.Collections.Specialized.OrderedDictionary] -or
194+
$Value.GetType().FullName -ceq 'System.Management.Automation.OrderedHashtable'
195+
)
196+
if ($SortMappings -or -not $isOrdered) {
197+
$entries.Sort([System.StringComparer]::Ordinal)
158198
}
159-
$entries.Sort([System.StringComparer]::Ordinal)
160199
return 'map:{0}:{{{1}}}' -f $entries.Count, ($entries -join '|')
161200
}
162201
if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
163-
if ($Value -is [byte[]]) {
164-
return 'unsupported:System.Byte[]'
165-
}
166202
$items = [System.Collections.Generic.List[string]]::new()
167203
foreach ($item in $Value) {
168-
$items.Add((ConvertTo-YamlSuiteCanonicalValue -Value $item))
204+
$items.Add((
205+
ConvertTo-YamlSuiteCanonicalValue -Value $item -SortMappings:$SortMappings
206+
))
169207
}
170208
return 'sequence:{0}:[{1}]' -f $items.Count, ($items -join '|')
171209
}
172-
return 'unsupported:{0}' -f $Value.GetType().FullName
210+
211+
$serialized = [System.Management.Automation.PSSerializer]::Serialize($Value, 3)
212+
$payload = [System.Convert]::ToBase64String(
213+
[System.Text.Encoding]::UTF8.GetBytes($serialized)
214+
)
215+
if (-not $Value.GetType().IsValueType) {
216+
return 'unsupported-reference:{0}:{1}:{2}' -f
217+
$Value.GetType().FullName,
218+
[System.Runtime.CompilerServices.RuntimeHelpers]::GetHashCode($Value),
219+
$payload
220+
}
221+
return 'unsupported-value:{0}:{1}' -f $Value.GetType().FullName, $payload
173222
}
174223

175224
function ConvertTo-YamlSuiteReferenceSignature {
@@ -188,7 +237,7 @@ function ConvertTo-YamlSuiteReferenceSignature {
188237
while ($stack.Count -gt 0) {
189238
$frame = $stack.Pop()
190239
$current = $frame.Value
191-
if ($null -eq $current -or $current -is [string] -or $current -is [byte[]]) {
240+
if ($null -eq $current -or $current -is [string]) {
192241
continue
193242
}
194243
if ($current -isnot [System.Collections.IDictionary] -and
@@ -206,13 +255,22 @@ function ConvertTo-YamlSuiteReferenceSignature {
206255
if (-not $first) {
207256
continue
208257
}
258+
if ($current -is [byte[]]) {
259+
continue
260+
}
209261

210262
if ($current -is [System.Collections.IDictionary]) {
211-
foreach ($key in $current.Keys) {
212-
$childPath = '{0}{{{1}}}' -f $frame.Path, (ConvertTo-YamlSuiteCanonicalValue -Value $key)
263+
foreach ($entry in $current.GetEnumerator()) {
264+
$childPath = '{0}{{{1}}}' -f $frame.Path, (
265+
ConvertTo-YamlSuiteCanonicalValue -Value $entry.Key
266+
)
213267
$stack.Push([pscustomobject]@{
214-
Value = $current[$key]
215-
Path = $childPath
268+
Value = $entry.Value
269+
Path = "$childPath.value"
270+
})
271+
$stack.Push([pscustomobject]@{
272+
Value = $entry.Key
273+
Path = "$childPath.key"
216274
})
217275
}
218276
continue
@@ -333,32 +391,23 @@ function Test-YamlSuiteLegacyOrderedMapProjection {
333391
function Get-YamlSuiteJsonPolicyReason {
334392
[OutputType([string])]
335393
param (
336-
[Parameter(Mandatory)]
337-
[string] $Case,
338-
339394
[Parameter(Mandatory)]
340395
[object[]] $ExpectedValues,
341396

342397
[Parameter(Mandatory)]
343398
[object[]] $ActualValues
344399
)
345400

346-
switch -CaseSensitive ($Case) {
347-
'565N' {
348-
if (Test-YamlSuiteBinaryByteArrayProjection -ExpectedValues $ExpectedValues `
349-
-ActualValues $ActualValues) {
350-
return 'BinaryByteArrayProjection'
351-
}
352-
}
353-
'J7PZ' {
354-
if (Test-YamlSuiteLegacyOrderedMapProjection -ExpectedValues $ExpectedValues `
355-
-ActualValues $ActualValues) {
356-
return 'LegacyOrderedMapProjection'
357-
}
358-
}
359-
default { return '' }
401+
if (Test-YamlSuiteBinaryByteArrayProjection -ExpectedValues $ExpectedValues `
402+
-ActualValues $ActualValues) {
403+
return 'BinaryByteArrayProjection'
404+
}
405+
if (Test-YamlSuiteLegacyOrderedMapProjection -ExpectedValues $ExpectedValues `
406+
-ActualValues $ActualValues) {
407+
return 'LegacyOrderedMapProjection'
360408
}
361409
return ''
410+
return ''
362411
}
363412

364413
function ConvertFrom-YamlSuiteEventText {
@@ -749,13 +798,15 @@ foreach ($inputFile in $inputFiles) {
749798
$stream = $null
750799
$projectedValues = $null
751800
$projectedCanonical = $null
801+
$projectedJsonCanonical = $null
752802
$projectedReference = ''
753803
$projectionError = ''
754804
$eventExpected = $null
755805
$eventActual = $null
756806
$jsonExpected = $null
757807
$jsonActual = $null
758808
$outYamlCanonical = $null
809+
$outYamlReference = $null
759810
$emitCanonical = $null
760811
$emitReference = $null
761812

@@ -801,6 +852,8 @@ foreach ($inputFile in $inputFiles) {
801852
try {
802853
$projectedValues = (Invoke-InYamlModule -ScriptBlock $projectYamlSuiteStream -Arguments (, $stream.Value)).Value
803854
$projectedCanonical = ConvertTo-YamlSuiteCanonicalValue -Value ([object[]] $projectedValues)
855+
$projectedJsonCanonical = ConvertTo-YamlSuiteCanonicalValue `
856+
-Value ([object[]] $projectedValues) -SortMappings
804857
$projectedReference = ConvertTo-YamlSuiteReferenceSignature -Value ([object[]] $projectedValues)
805858
} catch {
806859
if ($_.Exception.Data.Contains('YamlErrorId')) {
@@ -845,13 +898,14 @@ foreach ($inputFile in $inputFiles) {
845898
foreach ($document in $expectedDocuments) {
846899
$expectedValues.Add((ConvertFrom-Json -InputObject $document -AsHashtable -NoEnumerate))
847900
}
848-
$expectedCanonical = ConvertTo-YamlSuiteCanonicalValue -Value ([object[]] $expectedValues.ToArray())
901+
$expectedCanonical = ConvertTo-YamlSuiteCanonicalValue `
902+
-Value ([object[]] $expectedValues.ToArray()) -SortMappings
849903
$jsonExpected = $expectedCanonical
850-
$jsonActual = $projectedCanonical
851-
if ($projectedCanonical -ceq $expectedCanonical) {
904+
$jsonActual = $projectedJsonCanonical
905+
if ($projectedJsonCanonical -ceq $expectedCanonical) {
852906
$jsonResult = 'Pass'
853907
} else {
854-
$reason = Get-YamlSuiteJsonPolicyReason -Case $casePath `
908+
$reason = Get-YamlSuiteJsonPolicyReason `
855909
-ExpectedValues ([object[]] $expectedValues.ToArray()) `
856910
-ActualValues ([object[]] $projectedValues)
857911
if ($reason) {
@@ -877,12 +931,17 @@ foreach ($inputFile in $inputFiles) {
877931
$outStream = Invoke-InYamlModule -ScriptBlock $readYamlSuiteStream -Arguments @($outYaml)
878932
$outValues = (Invoke-InYamlModule -ScriptBlock $projectYamlSuiteStream -Arguments (, $outStream.Value)).Value
879933
$outCanonical = ConvertTo-YamlSuiteCanonicalValue -Value ([object[]] $outValues)
934+
$outReference = ConvertTo-YamlSuiteReferenceSignature -Value ([object[]] $outValues)
880935
$outYamlCanonical = $outCanonical
881-
if ($outCanonical -ceq $projectedCanonical) {
882-
$outYamlResult = 'Pass'
883-
} else {
936+
$outYamlReference = $outReference
937+
if ($outCanonical -cne $projectedCanonical) {
884938
$outYamlResult = 'Fail'
885939
$outYamlReason = 'OutYamlConstructionMismatch'
940+
} elseif ($outReference -cne $projectedReference) {
941+
$outYamlResult = 'Fail'
942+
$outYamlReason = 'OutYamlReferenceMismatch'
943+
} else {
944+
$outYamlResult = 'Pass'
886945
}
887946
} catch {
888947
if ($_.Exception.Data.Contains('IsYamlException')) {
@@ -975,6 +1034,7 @@ foreach ($inputFile in $inputFiles) {
9751034
JsonExpected = $jsonExpected
9761035
JsonActual = $jsonActual
9771036
OutYamlActual = $outYamlCanonical
1037+
OutYamlRefs = $outYamlReference
9781038
EmitActual = $emitCanonical
9791039
EmitReferences = $emitReference
9801040
ProjectedActual = $projectedCanonical

0 commit comments

Comments
 (0)