From b16f44f91da2ba6c605b661961fc14468c1e6492 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Fri, 3 Apr 2026 09:23:53 +0200 Subject: [PATCH] Fix #2657 and #2649: Clear-TestDrive performance and NUnit3 ToString crash #2657: Clear-TestDrive was extremely slow with many files because it called Remove-Item on every individual file. Replaced with HashSet-based approach that only calls Remove-Item -Recurse on root new items. #2649: NUnit3 report writing crashed when a test output objects ToString() throws. Wrapped in try/catch with a descriptive fallback string. Copilot-generated fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/functions/TestDrive.ps1 | 28 ++++++++++++++++++++++++---- src/functions/TestResults.NUnit3.ps1 | 7 ++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/functions/TestDrive.ps1 b/src/functions/TestDrive.ps1 index e1374f33f..f175ddf38 100644 --- a/src/functions/TestDrive.ps1 +++ b/src/functions/TestDrive.ps1 @@ -112,12 +112,32 @@ function Clear-TestDrive { Remove-TestDriveSymbolicLinks -Path $TestDrivePath - foreach ($i in [IO.Directory]::GetFileSystemEntries($TestDrivePath, '*.*', [System.IO.SearchOption]::AllDirectories)) { - if ($Exclude -contains $i) { - continue + $allCurrent = [IO.Directory]::GetFileSystemEntries($TestDrivePath, '*.*', [System.IO.SearchOption]::AllDirectories) + + # Collect new items (those not in the snapshot taken before the test) + $newItems = foreach ($i in $allCurrent) { + if ($Exclude -notcontains $i) { + $i } + } - & $SafeCommands['Remove-Item'] -Force -Recurse $i -ErrorAction Ignore + if (-not $newItems) { + return + } + + # Build a set of new item paths for O(1) parent lookups + $newItemSet = [System.Collections.Generic.HashSet[string]]::new( + [string[]]@($newItems), + [System.StringComparer]::OrdinalIgnoreCase) + + # Only delete "root" new items (those whose parent directory is not also a new item). + # Deleting with -Recurse removes all descendants in one call, avoiding redundant + # Remove-Item calls on already-deleted children. + foreach ($item in $newItemSet) { + $parent = [IO.Path]::GetDirectoryName($item) + if (-not $newItemSet.Contains($parent)) { + & $SafeCommands['Remove-Item'] -Path $item -Force -Recurse -ErrorAction Ignore + } } } } diff --git a/src/functions/TestResults.NUnit3.ps1 b/src/functions/TestResults.NUnit3.ps1 index 2066c562a..85ecfa6cf 100644 --- a/src/functions/TestResults.NUnit3.ps1 +++ b/src/functions/TestResults.NUnit3.ps1 @@ -554,7 +554,12 @@ function Format-CDataString ($Output) { $linesCount = $out.Length $o = for ($i = 0; $i -lt $linesCount; $i++) { # The input is array of objects, convert them to strings. - $line = if ($null -eq $out[$i]) { [String]::Empty } else { $out[$i].ToString() } + $line = if ($null -eq $out[$i]) { + [String]::Empty + } + else { + try { $out[$i].ToString() } catch { "" } + } if (0 -gt $line.IndexOfAny($script:invalidCDataChars)) { # No special chars that need replacing.