From 9d29be4adb56f5b00b4d768391b1dcbd8c4fd9f3 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Wed, 17 Jun 2026 16:15:27 +0200 Subject: [PATCH 1/3] Improve Should-BeString to show difference index and arrow marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When strings differ, show the difference index, expanded special characters (e.g. \n, \r, \t), and an arrow pointing to the first difference — matching the style of Should -Be. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../assert/String/Should-BeString.ps1 | 68 ++++++++++++++++++- .../assert/String/Should-BeString.Tests.ps1 | 17 ++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/functions/assert/String/Should-BeString.ps1 b/src/functions/assert/String/Should-BeString.ps1 index 3059fa12c..1a62f2417 100644 --- a/src/functions/assert/String/Should-BeString.ps1 +++ b/src/functions/assert/String/Should-BeString.ps1 @@ -96,8 +96,74 @@ function Should-BeString { $stringsAreEqual = Test-StringEqual -Expected $Expected -Actual $Actual -CaseSensitive:$CaseSensitive -IgnoreWhitespace:$IgnoreWhiteSpace -TrimWhitespace:$TrimWhitespace if (-not ($stringsAreEqual)) { - $Message = Get-AssertionMessage -Expected $Expected -Actual $Actual -Because $Because -DefaultMessage "Expected , but got ." + if ($Actual -is [string]) { + $Message = Get-StringDifferenceMessage -Expected $Expected -Actual $Actual -CaseSensitive:$CaseSensitive -Because $Because + } + else { + $Message = Get-AssertionMessage -Expected $Expected -Actual $Actual -Because $Because -DefaultMessage "Expected , but got ." + } Invoke-AssertionFailed -Message $Message -CallerCmdlet $PSCmdlet } Set-AssertionPassResult } + +function Get-StringDifferenceMessage { + param ( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Expected, + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Actual, + [switch] $CaseSensitive, + [string] $Because + ) + + $maxLength = [Math]::Max($Expected.Length, $Actual.Length) + + $differenceIndex = $null + for ($i = 0; $i -lt $maxLength -and ($null -eq $differenceIndex); ++$i) { + if ($CaseSensitive) { + if ($Expected[$i] -cne $Actual[$i]) { $differenceIndex = $i } + } + else { + if ($Expected[$i] -ne $Actual[$i]) { $differenceIndex = $i } + } + } + + $because = if ($Because) { " because $Because," } else { "" } + + $sb = [System.Text.StringBuilder]::new() + $null = $sb.AppendLine("Expected strings to be the same,$because but they were different.") + + if ($Expected.Length -ne $Actual.Length) { + $null = $sb.AppendLine("Expected length: $($Expected.Length)") + $null = $sb.AppendLine("Actual length: $($Actual.Length)") + } + else { + $null = $sb.AppendLine("String lengths are both $($Expected.Length).") + } + $null = $sb.AppendLine("Strings differ at index $differenceIndex.") + + $expectedExpanded = Expand-SpecialCharacters -InputObject $Expected + $actualExpanded = Expand-SpecialCharacters -InputObject $Actual + + # Recompute difference index on expanded strings + $maxLength = [Math]::Max($expectedExpanded.Length, $actualExpanded.Length) + $expandedDiffIndex = $null + for ($i = 0; $i -lt $maxLength -and ($null -eq $expandedDiffIndex); ++$i) { + if ($CaseSensitive) { + if ($expectedExpanded[$i] -cne $actualExpanded[$i]) { $expandedDiffIndex = $i } + } + else { + if ($expectedExpanded[$i] -ne $actualExpanded[$i]) { $expandedDiffIndex = $i } + } + } + + $prefix = "Expected: '" + $null = $sb.AppendLine("$prefix$expectedExpanded'") + $null = $sb.AppendLine("But was: '$actualExpanded'") + $null = $sb.Append((' ' * ($prefix.Length - 1)) + ('-' * $expandedDiffIndex) + '^') + + $sb.ToString() +} diff --git a/tst/functions/assert/String/Should-BeString.Tests.ps1 b/tst/functions/assert/String/Should-BeString.Tests.ps1 index db9e702fe..640e4dfab 100644 --- a/tst/functions/assert/String/Should-BeString.Tests.ps1 +++ b/tst/functions/assert/String/Should-BeString.Tests.ps1 @@ -118,6 +118,21 @@ Describe "Should-BeString" { It "Throws with default message when test fails" { $err = { Should-BeString -Expected "abc" -Actual "bde" } | Verify-AssertionFailed - $err.Exception.Message | Verify-Equal "Expected [string] 'abc', but got [string] 'bde'." + $err.Exception.Message | Verify-Like "*Expected strings to be the same*but they were different*" + $err.Exception.Message | Verify-Like "*Strings differ at index 0*" + $err.Exception.Message | Verify-Like "*Expected: 'abc'*" + $err.Exception.Message | Verify-Like "*But was: 'bde'*" + } + + It "Shows difference index and arrow marker" { + $err = { "hello world" | Should-BeString "Hello World" -CaseSensitive } | Verify-AssertionFailed + $err.Exception.Message | Verify-Like "*Strings differ at index 0*" + $err.Exception.Message | Verify-Like "*^*" + } + + It "Shows expanded whitespace characters in diff" { + $err = { "abc`ndef" | Should-BeString "abc`r`ndef" } | Verify-AssertionFailed + $err.Exception.Message | Verify-Like "*abc\r\ndef*" + $err.Exception.Message | Verify-Like "*abc\ndef*" } } From 3edb8cf8e1a7b48f9555e094b890c73503d2676a Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Wed, 17 Jun 2026 17:02:31 +0200 Subject: [PATCH 2/3] Replace wildcard test assertions with exact here-string validation Verify arrow position precisely using Verify-Equal with here-strings instead of Verify-Like wildcards. Add test for mid-string difference showing dashes before the arrow marker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../assert/String/Should-BeString.Tests.ps1 | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/tst/functions/assert/String/Should-BeString.Tests.ps1 b/tst/functions/assert/String/Should-BeString.Tests.ps1 index 640e4dfab..545487840 100644 --- a/tst/functions/assert/String/Should-BeString.Tests.ps1 +++ b/tst/functions/assert/String/Should-BeString.Tests.ps1 @@ -118,21 +118,51 @@ Describe "Should-BeString" { It "Throws with default message when test fails" { $err = { Should-BeString -Expected "abc" -Actual "bde" } | Verify-AssertionFailed - $err.Exception.Message | Verify-Like "*Expected strings to be the same*but they were different*" - $err.Exception.Message | Verify-Like "*Strings differ at index 0*" - $err.Exception.Message | Verify-Like "*Expected: 'abc'*" - $err.Exception.Message | Verify-Like "*But was: 'bde'*" + $err.Exception.Message | Verify-Equal @' +Expected strings to be the same, but they were different. +String lengths are both 3. +Strings differ at index 0. +Expected: 'abc' +But was: 'bde' + ^ +'@ } - It "Shows difference index and arrow marker" { + It "Shows arrow at correct position for case-sensitive difference" { $err = { "hello world" | Should-BeString "Hello World" -CaseSensitive } | Verify-AssertionFailed - $err.Exception.Message | Verify-Like "*Strings differ at index 0*" - $err.Exception.Message | Verify-Like "*^*" + $err.Exception.Message | Verify-Equal @' +Expected strings to be the same, but they were different. +String lengths are both 11. +Strings differ at index 0. +Expected: 'Hello World' +But was: 'hello world' + ^ +'@ + } + + It "Shows arrow with dashes for mid-string difference" { + $err = { "abc" | Should-BeString "abcdef" } | Verify-AssertionFailed + $err.Exception.Message | Verify-Equal @' +Expected strings to be the same, but they were different. +Expected length: 6 +Actual length: 3 +Strings differ at index 3. +Expected: 'abcdef' +But was: 'abc' + ---^ +'@ } It "Shows expanded whitespace characters in diff" { $err = { "abc`ndef" | Should-BeString "abc`r`ndef" } | Verify-AssertionFailed - $err.Exception.Message | Verify-Like "*abc\r\ndef*" - $err.Exception.Message | Verify-Like "*abc\ndef*" + $err.Exception.Message | Verify-Equal @' +Expected strings to be the same, but they were different. +Expected length: 8 +Actual length: 7 +Strings differ at index 3. +Expected: 'abc\r\ndef' +But was: 'abc\ndef' + ----^ +'@ } } From da7c708650c57605b522173305733ca84a314c41 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Wed, 17 Jun 2026 17:25:10 +0200 Subject: [PATCH 3/3] Fix cross-platform newline in diff message Use explicit LF in Get-StringDifferenceMessage and normalize expected values in tests with -replace to match on all platforms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../assert/String/Should-BeString.ps1 | 21 ++++++++++--------- .../assert/String/Should-BeString.Tests.ps1 | 16 +++++++------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/functions/assert/String/Should-BeString.ps1 b/src/functions/assert/String/Should-BeString.ps1 index 1a62f2417..fedc3e097 100644 --- a/src/functions/assert/String/Should-BeString.ps1 +++ b/src/functions/assert/String/Should-BeString.ps1 @@ -133,17 +133,18 @@ function Get-StringDifferenceMessage { $because = if ($Because) { " because $Because," } else { "" } - $sb = [System.Text.StringBuilder]::new() - $null = $sb.AppendLine("Expected strings to be the same,$because but they were different.") + $lines = @( + "Expected strings to be the same,$because but they were different." + ) if ($Expected.Length -ne $Actual.Length) { - $null = $sb.AppendLine("Expected length: $($Expected.Length)") - $null = $sb.AppendLine("Actual length: $($Actual.Length)") + $lines += "Expected length: $($Expected.Length)" + $lines += "Actual length: $($Actual.Length)" } else { - $null = $sb.AppendLine("String lengths are both $($Expected.Length).") + $lines += "String lengths are both $($Expected.Length)." } - $null = $sb.AppendLine("Strings differ at index $differenceIndex.") + $lines += "Strings differ at index $differenceIndex." $expectedExpanded = Expand-SpecialCharacters -InputObject $Expected $actualExpanded = Expand-SpecialCharacters -InputObject $Actual @@ -161,9 +162,9 @@ function Get-StringDifferenceMessage { } $prefix = "Expected: '" - $null = $sb.AppendLine("$prefix$expectedExpanded'") - $null = $sb.AppendLine("But was: '$actualExpanded'") - $null = $sb.Append((' ' * ($prefix.Length - 1)) + ('-' * $expandedDiffIndex) + '^') + $lines += "$prefix$expectedExpanded'" + $lines += "But was: '$actualExpanded'" + $lines += (' ' * ($prefix.Length - 1)) + ('-' * $expandedDiffIndex) + '^' - $sb.ToString() + $lines -join "`n" } diff --git a/tst/functions/assert/String/Should-BeString.Tests.ps1 b/tst/functions/assert/String/Should-BeString.Tests.ps1 index 545487840..4786c5b78 100644 --- a/tst/functions/assert/String/Should-BeString.Tests.ps1 +++ b/tst/functions/assert/String/Should-BeString.Tests.ps1 @@ -118,31 +118,31 @@ Describe "Should-BeString" { It "Throws with default message when test fails" { $err = { Should-BeString -Expected "abc" -Actual "bde" } | Verify-AssertionFailed - $err.Exception.Message | Verify-Equal @' + $err.Exception.Message | Verify-Equal (@' Expected strings to be the same, but they were different. String lengths are both 3. Strings differ at index 0. Expected: 'abc' But was: 'bde' ^ -'@ +'@ -replace "`r`n", "`n") } It "Shows arrow at correct position for case-sensitive difference" { $err = { "hello world" | Should-BeString "Hello World" -CaseSensitive } | Verify-AssertionFailed - $err.Exception.Message | Verify-Equal @' + $err.Exception.Message | Verify-Equal (@' Expected strings to be the same, but they were different. String lengths are both 11. Strings differ at index 0. Expected: 'Hello World' But was: 'hello world' ^ -'@ +'@ -replace "`r`n", "`n") } It "Shows arrow with dashes for mid-string difference" { $err = { "abc" | Should-BeString "abcdef" } | Verify-AssertionFailed - $err.Exception.Message | Verify-Equal @' + $err.Exception.Message | Verify-Equal (@' Expected strings to be the same, but they were different. Expected length: 6 Actual length: 3 @@ -150,12 +150,12 @@ Strings differ at index 3. Expected: 'abcdef' But was: 'abc' ---^ -'@ +'@ -replace "`r`n", "`n") } It "Shows expanded whitespace characters in diff" { $err = { "abc`ndef" | Should-BeString "abc`r`ndef" } | Verify-AssertionFailed - $err.Exception.Message | Verify-Equal @' + $err.Exception.Message | Verify-Equal (@' Expected strings to be the same, but they were different. Expected length: 8 Actual length: 7 @@ -163,6 +163,6 @@ Strings differ at index 3. Expected: 'abc\r\ndef' But was: 'abc\ndef' ----^ -'@ +'@ -replace "`r`n", "`n") } }