From f3b31243fbbefa9d5bc34db11d5eeb648f6833c5 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Sun, 1 Feb 2026 23:14:47 +0900 Subject: [PATCH 1/5] Add comprehensive scalar type tests for ConvertTo-Json --- .../ConvertTo-Json.Tests.ps1 | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 1f2abe05c68..fc1fa9b0382 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -156,4 +156,360 @@ Describe 'ConvertTo-Json' -tags "CI" { $actual = ConvertTo-Json -Compress -InputObject $obj $actual | Should -Be '{"Positive":18446744073709551615,"Negative":-18446744073709551615}' } + #region Comprehensive Scalar Type Tests (Phase 1) + # Test coverage for ConvertTo-Json scalar serialization + # Covers: Pipeline vs InputObject, ETS vs no ETS, all primitive and special types + + Context 'Primitive scalar types' { + It 'Should serialize value correctly' -TestCases @( + # Integer types + @{ TypeName = 'int'; Value = 42; Expected = '42' } + @{ TypeName = 'int'; Value = -42; Expected = '-42' } + @{ TypeName = 'int'; Value = 0; Expected = '0' } + @{ TypeName = 'int'; Value = [int]::MaxValue; Expected = '2147483647' } + @{ TypeName = 'int'; Value = [int]::MinValue; Expected = '-2147483648' } + @{ TypeName = 'long'; Value = 9223372036854775807L; Expected = '9223372036854775807' } + @{ TypeName = 'long'; Value = -9223372036854775808L; Expected = '-9223372036854775808' } + # Floating-point types + @{ TypeName = 'double'; Value = 3.14159; Expected = '3.14159' } + @{ TypeName = 'double'; Value = -3.14159; Expected = '-3.14159' } + @{ TypeName = 'double'; Value = 0.0; Expected = '0.0' } + @{ TypeName = 'float'; Value = [float]3.14; Expected = '3.14' } + @{ TypeName = 'decimal'; Value = 123.456d; Expected = '123.456' } + # Boolean + @{ TypeName = 'bool'; Value = $true; Expected = 'true' } + @{ TypeName = 'bool'; Value = $false; Expected = 'false' } + ) { + param($TypeName, $Value, $Expected) + $Value | ConvertTo-Json -Compress | Should -BeExactly $Expected + } + } + + Context 'String scalar types' { + It 'Should serialize string correctly' -TestCases @( + @{ Description = 'regular'; Value = 'hello'; Expected = '"hello"' } + @{ Description = 'empty'; Value = ''; Expected = '""' } + @{ Description = 'with spaces'; Value = 'hello world'; Expected = '"hello world"' } + @{ Description = 'with newline'; Value = "line1`nline2"; Expected = '"line1\nline2"' } + @{ Description = 'with tab'; Value = "col1`tcol2"; Expected = '"col1\tcol2"' } + @{ Description = 'with quotes'; Value = 'say "hello"'; Expected = '"say \"hello\""' } + @{ Description = 'with backslash'; Value = 'c:\path'; Expected = '"c:\\path"' } + @{ Description = 'unicode'; Value = '日本語'; Expected = '"日本語"' } + @{ Description = 'emoji'; Value = '😀'; Expected = '"😀"' } + ) { + param($Description, $Value, $Expected) + $Value | ConvertTo-Json -Compress | Should -BeExactly $Expected + } + } + + Context 'DateTime and related types' { + It 'Should serialize DateTime with UTC kind' { + $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) + $json = $dt | ConvertTo-Json -Compress + $json | Should -BeExactly '"2024-06-15T10:30:00Z"' + } + + It 'Should serialize DateTime with Local kind' { + $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Local) + $json = $dt | ConvertTo-Json -Compress + $json | Should -Match '^"2024-06-15T10:30:00' + } + + It 'Should serialize DateTime with Unspecified kind' { + $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Unspecified) + $json = $dt | ConvertTo-Json -Compress + $json | Should -BeExactly '"2024-06-15T10:30:00"' + } + + It 'Should serialize DateTimeOffset correctly' { + $dto = [DateTimeOffset]::new(2024, 6, 15, 10, 30, 0, [TimeSpan]::FromHours(9)) + $json = $dto | ConvertTo-Json -Compress + $json | Should -BeExactly '"2024-06-15T10:30:00+09:00"' + } + + It 'Should serialize TimeSpan as object with properties' { + $ts = [TimeSpan]::new(1, 2, 3, 4, 5) + $json = $ts | ConvertTo-Json -Compress + # TimeSpan is serialized as object with all properties + $json | Should -Match '"Ticks":' + $json | Should -Match '"Days":1' + $json | Should -Match '"Hours":2' + $json | Should -Match '"Minutes":3' + $json | Should -Match '"Seconds":4' + } + } + + Context 'Guid type' { + It 'Should serialize Guid as string via InputObject' { + $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') + $json = ConvertTo-Json -InputObject $guid -Compress + $json | Should -BeExactly '"12345678-1234-1234-1234-123456789abc"' + } + + It 'Should serialize Guid as object with Extended properties via Pipeline' { + $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') + $json = $guid | ConvertTo-Json -Compress + # Pipeline adds Extended property (Guid) + $json | Should -Match '"value":"12345678-1234-1234-1234-123456789abc"' + $json | Should -Match '"Guid":"12345678-1234-1234-1234-123456789abc"' + } + + It 'Should serialize empty Guid correctly via InputObject' { + $json = ConvertTo-Json -InputObject ([Guid]::Empty) -Compress + $json | Should -BeExactly '"00000000-0000-0000-0000-000000000000"' + } + } + + Context 'Uri type' { + It 'Should serialize Uri correctly' -TestCases @( + @{ Description = 'http'; UriString = 'http://example.com'; Expected = '"http://example.com"' } + @{ Description = 'https with path'; UriString = 'https://example.com/path'; Expected = '"https://example.com/path"' } + @{ Description = 'with query'; UriString = 'https://example.com/search?q=test'; Expected = '"https://example.com/search?q=test"' } + @{ Description = 'file'; UriString = 'file:///c:/temp/file.txt'; Expected = '"file:///c:/temp/file.txt"' } + ) { + param($Description, $UriString, $Expected) + $uri = [Uri]$UriString + $json = $uri | ConvertTo-Json -Compress + $json | Should -BeExactly $Expected + } + } + + Context 'Enum types' { + It 'Should serialize enum :: as ' -TestCases @( + @{ EnumType = 'System.DayOfWeek'; Value = 'Sunday'; Expected = '0' } + @{ EnumType = 'System.DayOfWeek'; Value = 'Monday'; Expected = '1' } + @{ EnumType = 'System.DayOfWeek'; Value = 'Saturday'; Expected = '6' } + @{ EnumType = 'System.ConsoleColor'; Value = 'Red'; Expected = '12' } + @{ EnumType = 'System.IO.FileAttributes'; Value = 'ReadOnly'; Expected = '1' } + @{ EnumType = 'System.IO.FileAttributes'; Value = 'Hidden'; Expected = '2' } + ) { + param($EnumType, $Value, $Expected) + $enumValue = [Enum]::Parse($EnumType, $Value) + $json = $enumValue | ConvertTo-Json -Compress + $json | Should -BeExactly $Expected + } + + It 'Should serialize enum :: as "" with -EnumsAsStrings' -TestCases @( + @{ EnumType = 'System.DayOfWeek'; Value = 'Sunday'; Expected = 'Sunday' } + @{ EnumType = 'System.DayOfWeek'; Value = 'Monday'; Expected = 'Monday' } + @{ EnumType = 'System.ConsoleColor'; Value = 'Red'; Expected = 'Red' } + ) { + param($EnumType, $Value, $Expected) + $enumValue = [Enum]::Parse($EnumType, $Value) + $json = $enumValue | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly "`"$Expected`"" + } + + It 'Should serialize flags enum correctly' { + $flags = [System.IO.FileAttributes]::ReadOnly -bor [System.IO.FileAttributes]::Hidden + $json = $flags | ConvertTo-Json -Compress + $json | Should -BeExactly '3' + } + + It 'Should serialize flags enum as string with -EnumsAsStrings' { + $flags = [System.IO.FileAttributes]::ReadOnly -bor [System.IO.FileAttributes]::Hidden + $json = $flags | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly '"ReadOnly, Hidden"' + } + } + + Context 'IPAddress type' { + It 'Should serialize IPAddress v4 correctly via InputObject' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $json = ConvertTo-Json -InputObject $ip -Compress + # Raw object serializes base properties + $json | Should -Not -Match 'IPAddressToString' + } + + It 'Should serialize IPAddress v6 correctly via InputObject' { + $ip = [System.Net.IPAddress]::Parse('::1') + $json = ConvertTo-Json -InputObject $ip -Compress + $json | Should -Not -Match 'IPAddressToString' + } + + It 'Should serialize IPAddress with Extended properties via Pipeline' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $json = $ip | ConvertTo-Json -Compress + # Pipeline wraps in PSObject, adding Extended property + $json | Should -Match 'IPAddressToString' + } + } + + Context 'Pipeline vs InputObject for scalars' { + It 'Should serialize identically via Pipeline and InputObject' -TestCases @( + @{ TypeName = 'int'; Value = 42 } + @{ TypeName = 'string'; Value = 'hello' } + @{ TypeName = 'bool'; Value = $true } + @{ TypeName = 'double'; Value = 3.14 } + # Note: Guid differs between Pipeline and InputObject (Guid adds Extended properties via Pipeline) + ) { + param($TypeName, $Value) + $jsonPipeline = $Value | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $Value -Compress + $jsonPipeline | Should -BeExactly $jsonInputObject + } + + It 'Should serialize DateTime identically via Pipeline and InputObject' { + $dt = [DateTime]::new(2024, 1, 15, 10, 30, 0, [DateTimeKind]::Utc) + $jsonPipeline = $dt | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dt -Compress + $jsonPipeline | Should -BeExactly $jsonInputObject + } + + It 'Should serialize DateTimeOffset identically via Pipeline and InputObject' { + $dto = [DateTimeOffset]::new(2024, 1, 15, 10, 30, 0, [TimeSpan]::Zero) + $jsonPipeline = $dto | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dto -Compress + $jsonPipeline | Should -BeExactly $jsonInputObject + } + + It 'Should serialize Uri identically via Pipeline and InputObject' { + $uri = [Uri]'https://example.com/path' + $jsonPipeline = $uri | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $uri -Compress + $jsonPipeline | Should -BeExactly $jsonInputObject + } + } + + Context 'Scalars as elements of arrays' { + It 'Should serialize array of correctly' -TestCases @( + @{ TypeName = 'int'; Values = @(1, 2, 3); Expected = '[1,2,3]' } + @{ TypeName = 'string'; Values = @('a', 'b', 'c'); Expected = '["a","b","c"]' } + @{ TypeName = 'bool'; Values = @($true, $false, $true); Expected = '[true,false,true]' } + @{ TypeName = 'double'; Values = @(1.1, 2.2, 3.3); Expected = '[1.1,2.2,3.3]' } + ) { + param($TypeName, $Values, $Expected) + $json = $Values | ConvertTo-Json -Compress + $json | Should -BeExactly $Expected + } + + It 'Should serialize array of Guid with Extended properties via Pipeline' { + $guids = @( + [Guid]'11111111-1111-1111-1111-111111111111', + [Guid]'22222222-2222-2222-2222-222222222222' + ) + $json = $guids | ConvertTo-Json -Compress + # Pipeline adds Extended properties to each Guid + $json | Should -Match '"value":"11111111-1111-1111-1111-111111111111"' + $json | Should -Match '"value":"22222222-2222-2222-2222-222222222222"' + } + + It 'Should serialize array of enum correctly' { + $enums = @([DayOfWeek]::Monday, [DayOfWeek]::Wednesday, [DayOfWeek]::Friday) + $json = $enums | ConvertTo-Json -Compress + $json | Should -BeExactly '[1,3,5]' + } + + It 'Should serialize array of enum as strings with -EnumsAsStrings' { + $enums = @([DayOfWeek]::Monday, [DayOfWeek]::Wednesday, [DayOfWeek]::Friday) + $json = $enums | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly '["Monday","Wednesday","Friday"]' + } + + It 'Should serialize mixed type array correctly' { + $mixed = @(1, 'two', $true, 3.14) + $json = $mixed | ConvertTo-Json -Compress + $json | Should -BeExactly '[1,"two",true,3.14]' + } + + It 'Should serialize array with null elements correctly' { + $arr = @(1, $null, 'three') + $json = $arr | ConvertTo-Json -Compress + $json | Should -BeExactly '[1,null,"three"]' + } + } + + Context 'Scalars as values in hashtables and PSCustomObject' { + It 'Should serialize hashtable with scalar values correctly' { + $hash = [ordered]@{ + intVal = 42 + strVal = 'hello' + boolVal = $true + doubleVal = 3.14 + nullVal = $null + } + $json = $hash | ConvertTo-Json -Compress + $json | Should -BeExactly '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14,"nullVal":null}' + } + + It 'Should serialize PSCustomObject with scalar values correctly' { + $obj = [PSCustomObject]@{ + intVal = 42 + strVal = 'hello' + boolVal = $true + doubleVal = 3.14 + } + $json = $obj | ConvertTo-Json -Compress + $json | Should -BeExactly '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14}' + } + + It 'Should serialize hashtable with DateTime value correctly' { + $hash = @{ dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) } + $json = $hash | ConvertTo-Json -Compress + $json | Should -BeExactly '{"dt":"2024-06-15T10:30:00Z"}' + } + + It 'Should serialize hashtable with Guid value correctly' { + $hash = @{ id = [Guid]'12345678-1234-1234-1234-123456789abc' } + $json = $hash | ConvertTo-Json -Compress + $json | Should -BeExactly '{"id":"12345678-1234-1234-1234-123456789abc"}' + } + + It 'Should serialize hashtable with enum value correctly' { + $hash = @{ day = [DayOfWeek]::Monday } + $json = $hash | ConvertTo-Json -Compress + $json | Should -BeExactly '{"day":1}' + } + + It 'Should serialize hashtable with enum as string correctly' { + $hash = @{ day = [DayOfWeek]::Monday } + $json = $hash | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly '{"day":"Monday"}' + } + + It 'Should serialize hashtable with Uri value correctly' { + $hash = @{ url = [Uri]'https://example.com' } + $json = $hash | ConvertTo-Json -Compress + $json | Should -BeExactly '{"url":"https://example.com"}' + } + + It 'Should serialize hashtable with BigInteger value correctly' { + $hash = @{ big = 18446744073709551615n } + $json = $hash | ConvertTo-Json -Compress + $json | Should -BeExactly '{"big":18446744073709551615}' + } + } + + Context 'ETS properties on scalar types' { + It 'Should ignore ETS properties on string' { + $str = 'hello' + $str = Add-Member -InputObject $str -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $str | ConvertTo-Json -Compress + $json | Should -BeExactly '"hello"' + $json | Should -Not -Match 'MyProp' + } + + It 'Should ignore ETS properties on DateTime' { + $dt = [DateTime]::new(2024, 6, 15, 0, 0, 0, [DateTimeKind]::Utc) + $dt = Add-Member -InputObject $dt -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $dt | ConvertTo-Json -Compress + $json | Should -Match '^"2024-06-15' + $json | Should -Not -Match 'MyProp' + } + + It 'Should include ETS properties on Uri' { + $uri = [Uri]'https://example.com' + $uri = Add-Member -InputObject $uri -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $uri | ConvertTo-Json -Compress + $json | Should -Match 'MyProp' + $json | Should -Match 'value.*https://example.com' + } + + It 'Should include ETS properties on Guid' { + $guid = [Guid]::NewGuid() + $guid = Add-Member -InputObject $guid -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $guid | ConvertTo-Json -Compress + $json | Should -Match 'MyProp' + } + } } From c1ea77d0beb54eced21cd3e10c528181084d80e0 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Mon, 2 Feb 2026 19:51:12 +0900 Subject: [PATCH 2/5] Address review comments: consolidate tests, add missing types and edge cases --- .../ConvertTo-Json.Tests.ps1 | 355 ++++++++++-------- 1 file changed, 205 insertions(+), 150 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index fc1fa9b0382..af19f22f87d 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -161,32 +161,67 @@ Describe 'ConvertTo-Json' -tags "CI" { # Covers: Pipeline vs InputObject, ETS vs no ETS, all primitive and special types Context 'Primitive scalar types' { - It 'Should serialize value correctly' -TestCases @( + It 'Should serialize value correctly via Pipeline and InputObject' -TestCases @( + # Byte types + @{ TypeName = 'byte'; Value = [byte]0; Expected = '0' } + @{ TypeName = 'byte'; Value = [byte]255; Expected = '255' } + @{ TypeName = 'sbyte'; Value = [sbyte]-128; Expected = '-128' } + @{ TypeName = 'sbyte'; Value = [sbyte]127; Expected = '127' } + # Short types + @{ TypeName = 'short'; Value = [short]-32768; Expected = '-32768' } + @{ TypeName = 'short'; Value = [short]32767; Expected = '32767' } + @{ TypeName = 'ushort'; Value = [ushort]0; Expected = '0' } + @{ TypeName = 'ushort'; Value = [ushort]65535; Expected = '65535' } # Integer types @{ TypeName = 'int'; Value = 42; Expected = '42' } @{ TypeName = 'int'; Value = -42; Expected = '-42' } @{ TypeName = 'int'; Value = 0; Expected = '0' } @{ TypeName = 'int'; Value = [int]::MaxValue; Expected = '2147483647' } @{ TypeName = 'int'; Value = [int]::MinValue; Expected = '-2147483648' } - @{ TypeName = 'long'; Value = 9223372036854775807L; Expected = '9223372036854775807' } - @{ TypeName = 'long'; Value = -9223372036854775808L; Expected = '-9223372036854775808' } + @{ TypeName = 'uint'; Value = [uint]0; Expected = '0' } + @{ TypeName = 'uint'; Value = [uint]::MaxValue; Expected = '4294967295' } + # Long types + @{ TypeName = 'long'; Value = [long]::MaxValue; Expected = '9223372036854775807' } + @{ TypeName = 'long'; Value = [long]::MinValue; Expected = '-9223372036854775808' } + @{ TypeName = 'ulong'; Value = [ulong]0; Expected = '0' } + @{ TypeName = 'ulong'; Value = [ulong]::MaxValue; Expected = '18446744073709551615' } # Floating-point types + @{ TypeName = 'float'; Value = [float]3.14; Expected = '3.14' } + @{ TypeName = 'float'; Value = [float]::NaN; Expected = '"NaN"' } + @{ TypeName = 'float'; Value = [float]::PositiveInfinity; Expected = '"Infinity"' } + @{ TypeName = 'float'; Value = [float]::NegativeInfinity; Expected = '"-Infinity"' } @{ TypeName = 'double'; Value = 3.14159; Expected = '3.14159' } @{ TypeName = 'double'; Value = -3.14159; Expected = '-3.14159' } @{ TypeName = 'double'; Value = 0.0; Expected = '0.0' } - @{ TypeName = 'float'; Value = [float]3.14; Expected = '3.14' } + @{ TypeName = 'double'; Value = [double]::NaN; Expected = '"NaN"' } + @{ TypeName = 'double'; Value = [double]::PositiveInfinity; Expected = '"Infinity"' } + @{ TypeName = 'double'; Value = [double]::NegativeInfinity; Expected = '"-Infinity"' } @{ TypeName = 'decimal'; Value = 123.456d; Expected = '123.456' } # Boolean @{ TypeName = 'bool'; Value = $true; Expected = 'true' } @{ TypeName = 'bool'; Value = $false; Expected = 'false' } ) { param($TypeName, $Value, $Expected) - $Value | ConvertTo-Json -Compress | Should -BeExactly $Expected + $jsonPipeline = $Value | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $Value -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should include ETS properties on ' -TestCases @( + @{ TypeName = 'int'; Value = 42 } + @{ TypeName = 'double'; Value = 3.14 } + ) { + param($TypeName, $Value) + $valueWithEts = Add-Member -InputObject $Value -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $valueWithEts | ConvertTo-Json -Compress + $json | Should -Match 'MyProp' + $json | Should -Match '"value":' } } Context 'String scalar types' { - It 'Should serialize string correctly' -TestCases @( + It 'Should serialize string correctly via Pipeline and InputObject' -TestCases @( @{ Description = 'regular'; Value = 'hello'; Expected = '"hello"' } @{ Description = 'empty'; Value = ''; Expected = '""' } @{ Description = 'with spaces'; Value = 'hello world'; Expected = '"hello world"' } @@ -194,19 +229,31 @@ Describe 'ConvertTo-Json' -tags "CI" { @{ Description = 'with tab'; Value = "col1`tcol2"; Expected = '"col1\tcol2"' } @{ Description = 'with quotes'; Value = 'say "hello"'; Expected = '"say \"hello\""' } @{ Description = 'with backslash'; Value = 'c:\path'; Expected = '"c:\\path"' } - @{ Description = 'unicode'; Value = '日本語'; Expected = '"日本語"' } - @{ Description = 'emoji'; Value = '😀'; Expected = '"😀"' } + @{ Description = 'unicode'; Value = '???'; Expected = '"???"' } + @{ Description = 'emoji'; Value = '??'; Expected = '"??"' } ) { param($Description, $Value, $Expected) - $Value | ConvertTo-Json -Compress | Should -BeExactly $Expected + $jsonPipeline = $Value | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $Value -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should ignore ETS properties on string' { + $str = Add-Member -InputObject 'hello' -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $str | ConvertTo-Json -Compress + $json | Should -BeExactly '"hello"' + $json | Should -Not -Match 'MyProp' } } Context 'DateTime and related types' { - It 'Should serialize DateTime with UTC kind' { + It 'Should serialize DateTime with UTC kind via Pipeline and InputObject' { $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) - $json = $dt | ConvertTo-Json -Compress - $json | Should -BeExactly '"2024-06-15T10:30:00Z"' + $jsonPipeline = $dt | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dt -Compress + $jsonPipeline | Should -BeExactly '"2024-06-15T10:30:00Z"' + $jsonInputObject | Should -BeExactly '"2024-06-15T10:30:00Z"' } It 'Should serialize DateTime with Local kind' { @@ -215,28 +262,55 @@ Describe 'ConvertTo-Json' -tags "CI" { $json | Should -Match '^"2024-06-15T10:30:00' } - It 'Should serialize DateTime with Unspecified kind' { + It 'Should serialize DateTime with Unspecified kind via Pipeline and InputObject' { $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Unspecified) - $json = $dt | ConvertTo-Json -Compress - $json | Should -BeExactly '"2024-06-15T10:30:00"' + $jsonPipeline = $dt | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dt -Compress + $jsonPipeline | Should -BeExactly '"2024-06-15T10:30:00"' + $jsonInputObject | Should -BeExactly '"2024-06-15T10:30:00"' } - It 'Should serialize DateTimeOffset correctly' { + It 'Should serialize DateTimeOffset correctly via Pipeline and InputObject' { $dto = [DateTimeOffset]::new(2024, 6, 15, 10, 30, 0, [TimeSpan]::FromHours(9)) - $json = $dto | ConvertTo-Json -Compress - $json | Should -BeExactly '"2024-06-15T10:30:00+09:00"' + $jsonPipeline = $dto | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dto -Compress + $jsonPipeline | Should -BeExactly '"2024-06-15T10:30:00+09:00"' + $jsonInputObject | Should -BeExactly '"2024-06-15T10:30:00+09:00"' + } + + It 'Should serialize DateOnly as object with properties' { + $d = [DateOnly]::new(2024, 6, 15) + $json = $d | ConvertTo-Json -Compress + $json | Should -Match '"Year":2024' + $json | Should -Match '"Month":6' + $json | Should -Match '"Day":15' + } + + It 'Should serialize TimeOnly as object with properties' { + $t = [TimeOnly]::new(10, 30, 45) + $json = $t | ConvertTo-Json -Compress + $json | Should -Match '"Hour":10' + $json | Should -Match '"Minute":30' + $json | Should -Match '"Second":45' } It 'Should serialize TimeSpan as object with properties' { $ts = [TimeSpan]::new(1, 2, 3, 4, 5) $json = $ts | ConvertTo-Json -Compress - # TimeSpan is serialized as object with all properties $json | Should -Match '"Ticks":' $json | Should -Match '"Days":1' $json | Should -Match '"Hours":2' $json | Should -Match '"Minutes":3' $json | Should -Match '"Seconds":4' } + + It 'Should ignore ETS properties on DateTime' { + $dt = [DateTime]::new(2024, 6, 15, 0, 0, 0, [DateTimeKind]::Utc) + $dt = Add-Member -InputObject $dt -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $dt | ConvertTo-Json -Compress + $json | Should -BeExactly '"2024-06-15T00:00:00Z"' + $json | Should -Not -Match 'MyProp' + } } Context 'Guid type' { @@ -246,22 +320,28 @@ Describe 'ConvertTo-Json' -tags "CI" { $json | Should -BeExactly '"12345678-1234-1234-1234-123456789abc"' } - It 'Should serialize Guid as object with Extended properties via Pipeline' { + It 'Should serialize Guid with Extended properties via Pipeline' { $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') $json = $guid | ConvertTo-Json -Compress - # Pipeline adds Extended property (Guid) - $json | Should -Match '"value":"12345678-1234-1234-1234-123456789abc"' - $json | Should -Match '"Guid":"12345678-1234-1234-1234-123456789abc"' + $json | Should -BeExactly '{"value":"12345678-1234-1234-1234-123456789abc","Guid":"12345678-1234-1234-1234-123456789abc"}' } It 'Should serialize empty Guid correctly via InputObject' { $json = ConvertTo-Json -InputObject ([Guid]::Empty) -Compress $json | Should -BeExactly '"00000000-0000-0000-0000-000000000000"' } + + It 'Should include ETS properties on Guid via Pipeline' { + $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') + $guid = Add-Member -InputObject $guid -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $guid | ConvertTo-Json -Compress + $json | Should -Match 'MyProp' + $json | Should -Match '"value":"12345678-1234-1234-1234-123456789abc"' + } } Context 'Uri type' { - It 'Should serialize Uri correctly' -TestCases @( + It 'Should serialize Uri correctly via Pipeline and InputObject' -TestCases @( @{ Description = 'http'; UriString = 'http://example.com'; Expected = '"http://example.com"' } @{ Description = 'https with path'; UriString = 'https://example.com/path'; Expected = '"https://example.com/path"' } @{ Description = 'with query'; UriString = 'https://example.com/search?q=test'; Expected = '"https://example.com/search?q=test"' } @@ -269,13 +349,23 @@ Describe 'ConvertTo-Json' -tags "CI" { ) { param($Description, $UriString, $Expected) $uri = [Uri]$UriString + $jsonPipeline = $uri | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $uri -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should include ETS properties on Uri' { + $uri = [Uri]'https://example.com' + $uri = Add-Member -InputObject $uri -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $uri | ConvertTo-Json -Compress - $json | Should -BeExactly $Expected + $json | Should -Match 'MyProp' + $json | Should -Match '"value":"https://example.com"' } } Context 'Enum types' { - It 'Should serialize enum :: as ' -TestCases @( + It 'Should serialize enum :: as via Pipeline and InputObject' -TestCases @( @{ EnumType = 'System.DayOfWeek'; Value = 'Sunday'; Expected = '0' } @{ EnumType = 'System.DayOfWeek'; Value = 'Monday'; Expected = '1' } @{ EnumType = 'System.DayOfWeek'; Value = 'Saturday'; Expected = '6' } @@ -285,11 +375,13 @@ Describe 'ConvertTo-Json' -tags "CI" { ) { param($EnumType, $Value, $Expected) $enumValue = [Enum]::Parse($EnumType, $Value) - $json = $enumValue | ConvertTo-Json -Compress - $json | Should -BeExactly $Expected + $jsonPipeline = $enumValue | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $enumValue -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected } - It 'Should serialize enum :: as "" with -EnumsAsStrings' -TestCases @( + It 'Should serialize enum as "" with -EnumsAsStrings' -TestCases @( @{ EnumType = 'System.DayOfWeek'; Value = 'Sunday'; Expected = 'Sunday' } @{ EnumType = 'System.DayOfWeek'; Value = 'Monday'; Expected = 'Monday' } @{ EnumType = 'System.ConsoleColor'; Value = 'Red'; Expected = 'Red' } @@ -300,10 +392,12 @@ Describe 'ConvertTo-Json' -tags "CI" { $json | Should -BeExactly "`"$Expected`"" } - It 'Should serialize flags enum correctly' { + It 'Should serialize flags enum correctly via Pipeline and InputObject' { $flags = [System.IO.FileAttributes]::ReadOnly -bor [System.IO.FileAttributes]::Hidden - $json = $flags | ConvertTo-Json -Compress - $json | Should -BeExactly '3' + $jsonPipeline = $flags | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $flags -Compress + $jsonPipeline | Should -BeExactly '3' + $jsonInputObject | Should -BeExactly '3' } It 'Should serialize flags enum as string with -EnumsAsStrings' { @@ -314,73 +408,57 @@ Describe 'ConvertTo-Json' -tags "CI" { } Context 'IPAddress type' { - It 'Should serialize IPAddress v4 correctly via InputObject' { + It 'Should serialize IPAddress v4 without IPAddressToString via InputObject' { $ip = [System.Net.IPAddress]::Parse('192.168.1.1') $json = ConvertTo-Json -InputObject $ip -Compress - # Raw object serializes base properties + $json | Should -Match '"Address":16885952' $json | Should -Not -Match 'IPAddressToString' } - It 'Should serialize IPAddress v6 correctly via InputObject' { + It 'Should serialize IPAddress v4 with IPAddressToString via Pipeline' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $json = $ip | ConvertTo-Json -Compress + $json | Should -Match '"Address":16885952' + $json | Should -Match '"IPAddressToString":"192.168.1.1"' + } + + It 'Should serialize IPAddress v6 correctly' { $ip = [System.Net.IPAddress]::Parse('::1') - $json = ConvertTo-Json -InputObject $ip -Compress - $json | Should -Not -Match 'IPAddressToString' + $jsonInputObject = ConvertTo-Json -InputObject $ip -Compress + $jsonPipeline = $ip | ConvertTo-Json -Compress + $jsonInputObject | Should -Not -Match 'IPAddressToString' + $jsonPipeline | Should -Match '"IPAddressToString":"::1"' } - It 'Should serialize IPAddress with Extended properties via Pipeline' { + It 'Should include ETS properties on IPAddress' { $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $ip = Add-Member -InputObject $ip -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $ip | ConvertTo-Json -Compress - # Pipeline wraps in PSObject, adding Extended property + $json | Should -Match 'MyProp' $json | Should -Match 'IPAddressToString' } } - Context 'Pipeline vs InputObject for scalars' { - It 'Should serialize identically via Pipeline and InputObject' -TestCases @( - @{ TypeName = 'int'; Value = 42 } - @{ TypeName = 'string'; Value = 'hello' } - @{ TypeName = 'bool'; Value = $true } - @{ TypeName = 'double'; Value = 3.14 } - # Note: Guid differs between Pipeline and InputObject (Guid adds Extended properties via Pipeline) - ) { - param($TypeName, $Value) - $jsonPipeline = $Value | ConvertTo-Json -Compress - $jsonInputObject = ConvertTo-Json -InputObject $Value -Compress - $jsonPipeline | Should -BeExactly $jsonInputObject - } - - It 'Should serialize DateTime identically via Pipeline and InputObject' { - $dt = [DateTime]::new(2024, 1, 15, 10, 30, 0, [DateTimeKind]::Utc) - $jsonPipeline = $dt | ConvertTo-Json -Compress - $jsonInputObject = ConvertTo-Json -InputObject $dt -Compress - $jsonPipeline | Should -BeExactly $jsonInputObject - } - - It 'Should serialize DateTimeOffset identically via Pipeline and InputObject' { - $dto = [DateTimeOffset]::new(2024, 1, 15, 10, 30, 0, [TimeSpan]::Zero) - $jsonPipeline = $dto | ConvertTo-Json -Compress - $jsonInputObject = ConvertTo-Json -InputObject $dto -Compress - $jsonPipeline | Should -BeExactly $jsonInputObject - } - - It 'Should serialize Uri identically via Pipeline and InputObject' { - $uri = [Uri]'https://example.com/path' - $jsonPipeline = $uri | ConvertTo-Json -Compress - $jsonInputObject = ConvertTo-Json -InputObject $uri -Compress - $jsonPipeline | Should -BeExactly $jsonInputObject - } - } Context 'Scalars as elements of arrays' { - It 'Should serialize array of correctly' -TestCases @( + It 'Should serialize array of correctly via Pipeline and InputObject' -TestCases @( @{ TypeName = 'int'; Values = @(1, 2, 3); Expected = '[1,2,3]' } @{ TypeName = 'string'; Values = @('a', 'b', 'c'); Expected = '["a","b","c"]' } - @{ TypeName = 'bool'; Values = @($true, $false, $true); Expected = '[true,false,true]' } @{ TypeName = 'double'; Values = @(1.1, 2.2, 3.3); Expected = '[1.1,2.2,3.3]' } ) { param($TypeName, $Values, $Expected) - $json = $Values | ConvertTo-Json -Compress - $json | Should -BeExactly $Expected + $jsonPipeline = $Values | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $Values -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + # Note: bool array test uses InputObject only because $true/$false are singletons + # and ETS properties added in other tests would affect Pipeline serialization + It 'Should serialize array of bool correctly via InputObject' { + $bools = @($true, $false, $true) + $json = ConvertTo-Json -InputObject $bools -Compress + $json | Should -BeExactly '[true,false,true]' } It 'Should serialize array of Guid with Extended properties via Pipeline' { @@ -389,15 +467,16 @@ Describe 'ConvertTo-Json' -tags "CI" { [Guid]'22222222-2222-2222-2222-222222222222' ) $json = $guids | ConvertTo-Json -Compress - # Pipeline adds Extended properties to each Guid $json | Should -Match '"value":"11111111-1111-1111-1111-111111111111"' $json | Should -Match '"value":"22222222-2222-2222-2222-222222222222"' } - It 'Should serialize array of enum correctly' { + It 'Should serialize array of enum correctly via Pipeline and InputObject' { $enums = @([DayOfWeek]::Monday, [DayOfWeek]::Wednesday, [DayOfWeek]::Friday) - $json = $enums | ConvertTo-Json -Compress - $json | Should -BeExactly '[1,3,5]' + $jsonPipeline = $enums | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $enums -Compress + $jsonPipeline | Should -BeExactly '[1,3,5]' + $jsonInputObject | Should -BeExactly '[1,3,5]' } It 'Should serialize array of enum as strings with -EnumsAsStrings' { @@ -406,9 +485,10 @@ Describe 'ConvertTo-Json' -tags "CI" { $json | Should -BeExactly '["Monday","Wednesday","Friday"]' } - It 'Should serialize mixed type array correctly' { + # Note: mixed array test uses InputObject only due to $true singleton issue + It 'Should serialize mixed type array correctly via InputObject' { $mixed = @(1, 'two', $true, 3.14) - $json = $mixed | ConvertTo-Json -Compress + $json = ConvertTo-Json -InputObject $mixed -Compress $json | Should -BeExactly '[1,"two",true,3.14]' } @@ -417,10 +497,18 @@ Describe 'ConvertTo-Json' -tags "CI" { $json = $arr | ConvertTo-Json -Compress $json | Should -BeExactly '[1,null,"three"]' } + + It 'Should include ETS properties on array via InputObject' { + $arr = @(1, 2, 3) + $arr = Add-Member -InputObject $arr -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = ConvertTo-Json -InputObject $arr -Compress + $json | Should -Match 'MyProp' + $json | Should -Match '"value":\[1,2,3\]' + } } Context 'Scalars as values in hashtables and PSCustomObject' { - It 'Should serialize hashtable with scalar values correctly' { + It 'Should serialize hashtable with scalar values correctly via Pipeline and InputObject' { $hash = [ordered]@{ intVal = 42 strVal = 'hello' @@ -428,37 +516,40 @@ Describe 'ConvertTo-Json' -tags "CI" { doubleVal = 3.14 nullVal = $null } - $json = $hash | ConvertTo-Json -Compress - $json | Should -BeExactly '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14,"nullVal":null}' + $expected = '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14,"nullVal":null}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected } - It 'Should serialize PSCustomObject with scalar values correctly' { + It 'Should serialize PSCustomObject with scalar values correctly via Pipeline and InputObject' { $obj = [PSCustomObject]@{ intVal = 42 strVal = 'hello' boolVal = $true doubleVal = 3.14 } - $json = $obj | ConvertTo-Json -Compress - $json | Should -BeExactly '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14}' - } - - It 'Should serialize hashtable with DateTime value correctly' { - $hash = @{ dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) } - $json = $hash | ConvertTo-Json -Compress - $json | Should -BeExactly '{"dt":"2024-06-15T10:30:00Z"}' - } - - It 'Should serialize hashtable with Guid value correctly' { - $hash = @{ id = [Guid]'12345678-1234-1234-1234-123456789abc' } - $json = $hash | ConvertTo-Json -Compress - $json | Should -BeExactly '{"id":"12345678-1234-1234-1234-123456789abc"}' - } - - It 'Should serialize hashtable with enum value correctly' { - $hash = @{ day = [DayOfWeek]::Monday } - $json = $hash | ConvertTo-Json -Compress - $json | Should -BeExactly '{"day":1}' + $expected = '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize hashtable with value correctly' -TestCases @( + @{ TypeName = 'DateTime'; Value = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc); Expected = '{"val":"2024-06-15T10:30:00Z"}' } + @{ TypeName = 'Guid'; Value = [Guid]'12345678-1234-1234-1234-123456789abc'; Expected = '{"val":"12345678-1234-1234-1234-123456789abc"}' } + @{ TypeName = 'Enum'; Value = [DayOfWeek]::Monday; Expected = '{"val":1}' } + @{ TypeName = 'Uri'; Value = [Uri]'https://example.com'; Expected = '{"val":"https://example.com"}' } + @{ TypeName = 'BigInteger'; Value = 18446744073709551615n; Expected = '{"val":18446744073709551615}' } + ) { + param($TypeName, $Value, $Expected) + $hash = @{ val = $Value } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected } It 'Should serialize hashtable with enum as string correctly' { @@ -467,49 +558,13 @@ Describe 'ConvertTo-Json' -tags "CI" { $json | Should -BeExactly '{"day":"Monday"}' } - It 'Should serialize hashtable with Uri value correctly' { - $hash = @{ url = [Uri]'https://example.com' } - $json = $hash | ConvertTo-Json -Compress - $json | Should -BeExactly '{"url":"https://example.com"}' - } - - It 'Should serialize hashtable with BigInteger value correctly' { - $hash = @{ big = 18446744073709551615n } - $json = $hash | ConvertTo-Json -Compress - $json | Should -BeExactly '{"big":18446744073709551615}' - } - } - - Context 'ETS properties on scalar types' { - It 'Should ignore ETS properties on string' { - $str = 'hello' - $str = Add-Member -InputObject $str -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru - $json = $str | ConvertTo-Json -Compress - $json | Should -BeExactly '"hello"' - $json | Should -Not -Match 'MyProp' - } - - It 'Should ignore ETS properties on DateTime' { - $dt = [DateTime]::new(2024, 6, 15, 0, 0, 0, [DateTimeKind]::Utc) - $dt = Add-Member -InputObject $dt -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru - $json = $dt | ConvertTo-Json -Compress - $json | Should -Match '^"2024-06-15' - $json | Should -Not -Match 'MyProp' - } - - It 'Should include ETS properties on Uri' { - $uri = [Uri]'https://example.com' - $uri = Add-Member -InputObject $uri -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru - $json = $uri | ConvertTo-Json -Compress - $json | Should -Match 'MyProp' - $json | Should -Match 'value.*https://example.com' - } - - It 'Should include ETS properties on Guid' { - $guid = [Guid]::NewGuid() - $guid = Add-Member -InputObject $guid -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru - $json = $guid | ConvertTo-Json -Compress + It 'Should include ETS properties on hashtable via InputObject' { + $hash = @{ a = 1 } + $hash = Add-Member -InputObject $hash -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = ConvertTo-Json -InputObject $hash -Compress $json | Should -Match 'MyProp' } } + + #endregion Comprehensive Scalar Type Tests (Phase 1) } From 15d38648e74d1671832e5c29cb740c1bc984e59d Mon Sep 17 00:00:00 2001 From: yotsuda Date: Mon, 2 Feb 2026 21:05:50 +0900 Subject: [PATCH 3/5] Address second review: use BeExactly, add BigInt and Enum ETS tests --- .../ConvertTo-Json.Tests.ps1 | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index af19f22f87d..8263b9382a0 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -197,6 +197,8 @@ Describe 'ConvertTo-Json' -tags "CI" { @{ TypeName = 'double'; Value = [double]::PositiveInfinity; Expected = '"Infinity"' } @{ TypeName = 'double'; Value = [double]::NegativeInfinity; Expected = '"-Infinity"' } @{ TypeName = 'decimal'; Value = 123.456d; Expected = '123.456' } + # BigInteger + @{ TypeName = 'BigInteger'; Value = 18446744073709551615n; Expected = '18446744073709551615' } # Boolean @{ TypeName = 'bool'; Value = $true; Expected = 'true' } @{ TypeName = 'bool'; Value = $false; Expected = 'false' } @@ -281,27 +283,19 @@ Describe 'ConvertTo-Json' -tags "CI" { It 'Should serialize DateOnly as object with properties' { $d = [DateOnly]::new(2024, 6, 15) $json = $d | ConvertTo-Json -Compress - $json | Should -Match '"Year":2024' - $json | Should -Match '"Month":6' - $json | Should -Match '"Day":15' + $json | Should -BeExactly '{"Year":2024,"Month":6,"Day":15,"DayOfWeek":6,"DayOfYear":167,"DayNumber":739051}' } It 'Should serialize TimeOnly as object with properties' { $t = [TimeOnly]::new(10, 30, 45) $json = $t | ConvertTo-Json -Compress - $json | Should -Match '"Hour":10' - $json | Should -Match '"Minute":30' - $json | Should -Match '"Second":45' + $json | Should -BeExactly '{"Hour":10,"Minute":30,"Second":45,"Millisecond":0,"Microsecond":0,"Nanosecond":0,"Ticks":378450000000}' } It 'Should serialize TimeSpan as object with properties' { $ts = [TimeSpan]::new(1, 2, 3, 4, 5) $json = $ts | ConvertTo-Json -Compress - $json | Should -Match '"Ticks":' - $json | Should -Match '"Days":1' - $json | Should -Match '"Hours":2' - $json | Should -Match '"Minutes":3' - $json | Should -Match '"Seconds":4' + $json | Should -BeExactly '{"Ticks":937840050000,"Days":1,"Hours":2,"Milliseconds":5,"Microseconds":0,"Nanoseconds":0,"Minutes":3,"Seconds":4,"TotalDays":1.0854630208333333,"TotalHours":26.0511125,"TotalMilliseconds":93784005.0,"TotalMicroseconds":93784005000.0,"TotalNanoseconds":93784005000000.0,"TotalMinutes":1563.06675,"TotalSeconds":93784.005}' } It 'Should ignore ETS properties on DateTime' { @@ -309,7 +303,27 @@ Describe 'ConvertTo-Json' -tags "CI" { $dt = Add-Member -InputObject $dt -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $dt | ConvertTo-Json -Compress $json | Should -BeExactly '"2024-06-15T00:00:00Z"' - $json | Should -Not -Match 'MyProp' + } + + It 'Should include ETS properties on DateTimeOffset' { + $dto = [DateTimeOffset]::new(2024, 6, 15, 10, 30, 0, [TimeSpan]::Zero) + $dto = Add-Member -InputObject $dto -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $dto | ConvertTo-Json -Compress + $json | Should -BeExactly '{"value":"2024-06-15T10:30:00+00:00","MyProp":"test"}' + } + + It 'Should include ETS properties on DateOnly' { + $d = [DateOnly]::new(2024, 6, 15) + $d = Add-Member -InputObject $d -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $d | ConvertTo-Json -Compress + $json | Should -BeExactly '{"Year":2024,"Month":6,"Day":15,"DayOfWeek":6,"DayOfYear":167,"DayNumber":739051,"MyProp":"test"}' + } + + It 'Should include ETS properties on TimeOnly' { + $t = [TimeOnly]::new(10, 30, 45) + $t = Add-Member -InputObject $t -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $t | ConvertTo-Json -Compress + $json | Should -BeExactly '{"Hour":10,"Minute":30,"Second":45,"Millisecond":0,"Microsecond":0,"Nanosecond":0,"Ticks":378450000000,"MyProp":"test"}' } } @@ -335,8 +349,7 @@ Describe 'ConvertTo-Json' -tags "CI" { $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') $guid = Add-Member -InputObject $guid -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $guid | ConvertTo-Json -Compress - $json | Should -Match 'MyProp' - $json | Should -Match '"value":"12345678-1234-1234-1234-123456789abc"' + $json | Should -BeExactly '{"value":"12345678-1234-1234-1234-123456789abc","MyProp":"test","Guid":"12345678-1234-1234-1234-123456789abc"}' } } @@ -359,8 +372,7 @@ Describe 'ConvertTo-Json' -tags "CI" { $uri = [Uri]'https://example.com' $uri = Add-Member -InputObject $uri -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $uri | ConvertTo-Json -Compress - $json | Should -Match 'MyProp' - $json | Should -Match '"value":"https://example.com"' + $json | Should -BeExactly '{"value":"https://example.com","MyProp":"test"}' } } @@ -405,37 +417,44 @@ Describe 'ConvertTo-Json' -tags "CI" { $json = $flags | ConvertTo-Json -Compress -EnumsAsStrings $json | Should -BeExactly '"ReadOnly, Hidden"' } + + It 'Should include ETS properties on Enum' { + $enum = Add-Member -InputObject ([DayOfWeek]::Monday) -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $enum | ConvertTo-Json -Compress + $json | Should -BeExactly '{"value":1,"MyProp":"test"}' + } } Context 'IPAddress type' { - It 'Should serialize IPAddress v4 without IPAddressToString via InputObject' { + It 'Should serialize IPAddress v4 correctly via InputObject' { $ip = [System.Net.IPAddress]::Parse('192.168.1.1') $json = ConvertTo-Json -InputObject $ip -Compress - $json | Should -Match '"Address":16885952' - $json | Should -Not -Match 'IPAddressToString' + $json | Should -BeExactly '{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952}' } - It 'Should serialize IPAddress v4 with IPAddressToString via Pipeline' { + It 'Should serialize IPAddress v4 correctly via Pipeline' { $ip = [System.Net.IPAddress]::Parse('192.168.1.1') $json = $ip | ConvertTo-Json -Compress - $json | Should -Match '"Address":16885952' - $json | Should -Match '"IPAddressToString":"192.168.1.1"' + $json | Should -BeExactly '{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952,"IPAddressToString":"192.168.1.1"}' + } + + It 'Should serialize IPAddress v6 correctly via InputObject' { + $ip = [System.Net.IPAddress]::Parse('::1') + $json = ConvertTo-Json -InputObject $ip -Compress + $json | Should -BeExactly '{"AddressFamily":23,"ScopeId":0,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":null}' } - It 'Should serialize IPAddress v6 correctly' { + It 'Should serialize IPAddress v6 correctly via Pipeline' { $ip = [System.Net.IPAddress]::Parse('::1') - $jsonInputObject = ConvertTo-Json -InputObject $ip -Compress - $jsonPipeline = $ip | ConvertTo-Json -Compress - $jsonInputObject | Should -Not -Match 'IPAddressToString' - $jsonPipeline | Should -Match '"IPAddressToString":"::1"' + $json = $ip | ConvertTo-Json -Compress + $json | Should -BeExactly '{"AddressFamily":23,"ScopeId":0,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":null,"IPAddressToString":"::1"}' } It 'Should include ETS properties on IPAddress' { $ip = [System.Net.IPAddress]::Parse('192.168.1.1') $ip = Add-Member -InputObject $ip -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $ip | ConvertTo-Json -Compress - $json | Should -Match 'MyProp' - $json | Should -Match 'IPAddressToString' + $json | Should -BeExactly '{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952,"MyProp":"test","IPAddressToString":"192.168.1.1"}' } } From ef02f772eef747685f63fe888bd63a65306f686a Mon Sep 17 00:00:00 2001 From: yotsuda Date: Tue, 3 Feb 2026 06:48:40 +0900 Subject: [PATCH 4/5] Replace -Match with -BeExactly and add $null test case Address review feedback by replacing all -Match assertions with -BeExactly for more precise test validation. Also add missing $null test case to Primitive scalar types. Changes: - Add $null test case to Primitive scalar types TestCases - Replace -Match with -BeExactly in ETS properties tests (9 instances) - Remove redundant -Not -Match assertion in String ETS test - Calculate timezone offset dynamically for DateTime Local kind test - Use full JSON string comparison for Guid array, Array ETS, and Hashtable ETS tests All tests pass successfully (114 tests). Co-Authored-By: Claude Sonnet 4.5 --- .../ConvertTo-Json.Tests.ps1 | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 8263b9382a0..d0eef5174c9 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -202,6 +202,8 @@ Describe 'ConvertTo-Json' -tags "CI" { # Boolean @{ TypeName = 'bool'; Value = $true; Expected = 'true' } @{ TypeName = 'bool'; Value = $false; Expected = 'false' } + # Null + @{ TypeName = 'null'; Value = $null; Expected = 'null' } ) { param($TypeName, $Value, $Expected) $jsonPipeline = $Value | ConvertTo-Json -Compress @@ -211,14 +213,13 @@ Describe 'ConvertTo-Json' -tags "CI" { } It 'Should include ETS properties on ' -TestCases @( - @{ TypeName = 'int'; Value = 42 } - @{ TypeName = 'double'; Value = 3.14 } + @{ TypeName = 'int'; Value = 42; Expected = '{"value":42,"MyProp":"test"}' } + @{ TypeName = 'double'; Value = 3.14; Expected = '{"value":3.14,"MyProp":"test"}' } ) { - param($TypeName, $Value) + param($TypeName, $Value, $Expected) $valueWithEts = Add-Member -InputObject $Value -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $valueWithEts | ConvertTo-Json -Compress - $json | Should -Match 'MyProp' - $json | Should -Match '"value":' + $json | Should -BeExactly $Expected } } @@ -245,7 +246,6 @@ Describe 'ConvertTo-Json' -tags "CI" { $str = Add-Member -InputObject 'hello' -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = $str | ConvertTo-Json -Compress $json | Should -BeExactly '"hello"' - $json | Should -Not -Match 'MyProp' } } @@ -261,7 +261,9 @@ Describe 'ConvertTo-Json' -tags "CI" { It 'Should serialize DateTime with Local kind' { $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Local) $json = $dt | ConvertTo-Json -Compress - $json | Should -Match '^"2024-06-15T10:30:00' + $offset = $dt.ToString('zzz') + $expected = '"2024-06-15T10:30:00' + $offset + '"' + $json | Should -BeExactly $expected } It 'Should serialize DateTime with Unspecified kind via Pipeline and InputObject' { @@ -486,8 +488,7 @@ Describe 'ConvertTo-Json' -tags "CI" { [Guid]'22222222-2222-2222-2222-222222222222' ) $json = $guids | ConvertTo-Json -Compress - $json | Should -Match '"value":"11111111-1111-1111-1111-111111111111"' - $json | Should -Match '"value":"22222222-2222-2222-2222-222222222222"' + $json | Should -BeExactly '[{"value":"11111111-1111-1111-1111-111111111111","Guid":"11111111-1111-1111-1111-111111111111"},{"value":"22222222-2222-2222-2222-222222222222","Guid":"22222222-2222-2222-2222-222222222222"}]' } It 'Should serialize array of enum correctly via Pipeline and InputObject' { @@ -521,8 +522,7 @@ Describe 'ConvertTo-Json' -tags "CI" { $arr = @(1, 2, 3) $arr = Add-Member -InputObject $arr -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = ConvertTo-Json -InputObject $arr -Compress - $json | Should -Match 'MyProp' - $json | Should -Match '"value":\[1,2,3\]' + $json | Should -BeExactly '{"value":[1,2,3],"MyProp":"test"}' } } @@ -581,7 +581,7 @@ Describe 'ConvertTo-Json' -tags "CI" { $hash = @{ a = 1 } $hash = Add-Member -InputObject $hash -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru $json = ConvertTo-Json -InputObject $hash -Compress - $json | Should -Match 'MyProp' + $json | Should -BeExactly '{"a":1,"MyProp":"test"}' } } From 5b8877ebb453de3657096e3ab88998d7bd6e546c Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Tue, 3 Feb 2026 18:13:39 +0900 Subject: [PATCH 5/5] Update test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 Co-authored-by: Ilya --- .../Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index d0eef5174c9..4ac0818faf4 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -460,7 +460,6 @@ Describe 'ConvertTo-Json' -tags "CI" { } } - Context 'Scalars as elements of arrays' { It 'Should serialize array of correctly via Pipeline and InputObject' -TestCases @( @{ TypeName = 'int'; Values = @(1, 2, 3); Expected = '[1,2,3]' }