private/tests/Write-ZtTestProgress.ps1
|
function Write-ZtTestProgress { <# .SYNOPSIS Appends a progress entry to the overall test execution progress log. .DESCRIPTION Appends a single line to _progress.log in the logs folder, recording when each test starts, completes, or fails. This append-only log provides an at-a-glance timeline of all test executions and makes it easy to identify hanging tests (STARTED without a matching COMPLETED/FAILED line). Uses a per-file named mutex plus [System.IO.File]::AppendAllText to serialize writes from parallel runspaces/processes and keep each entry on a single line. .PARAMETER TestID The test ID to log progress for. .PARAMETER LogsPath Path to the logs folder. If empty or null, the function is a no-op. .PARAMETER Action The progress action: Started, Completed, or Failed. .PARAMETER Duration The test duration (for Completed/Failed actions). .PARAMETER ErrorMessage The error message (for Failed actions). .EXAMPLE PS C:\> Write-ZtTestProgress -TestID 25384 -LogsPath $logsPath -Action Started Appends a STARTED line for test 25384 to the progress log. .EXAMPLE PS C:\> Write-ZtTestProgress -TestID 25384 -LogsPath $logsPath -Action Completed -Duration $result.Duration Appends a COMPLETED line for test 25384 to the progress log. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $TestID, [string] $LogsPath, [Parameter(Mandatory = $true)] [ValidateSet('Started', 'Completed', 'Failed')] [string] $Action, [timespan] $Duration, $ErrorMessage ) process { if (-not $LogsPath) { return } try { [void][System.IO.Directory]::CreateDirectory($LogsPath) $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff') $actionPadded = $Action.ToUpper().PadRight(10) $line = "$timestamp $actionPadded $TestID" if ($null -ne $Duration) { $line += " $($Duration.ToString('hh\:mm\:ss\.fff'))" } if ($Action -eq 'Completed') { $line += ' Pass' } if ($Action -eq 'Failed' -and $ErrorMessage) { $errorText = "$ErrorMessage" $errorText = $errorText -replace '[\r\n\t]+', ' ' if ($errorText.Length -gt 1000) { $errorText = $errorText.Substring(0, 1000) + '...' } $line += " $errorText" } $line += [System.Environment]::NewLine $progressFilePath = Join-Path $LogsPath '_progress.log' $fullPath = [System.IO.Path]::GetFullPath($progressFilePath) $normalizedPath = if ($IsWindows) { $fullPath.ToLowerInvariant() } else { $fullPath } # Cache the mutex name per resolved path to avoid repeated SHA256 hashing if (-not $script:ZtProgressMutexCache) { $script:ZtProgressMutexCache = @{} } if ($script:ZtProgressMutexCache.ContainsKey($normalizedPath)) { $mutexName = $script:ZtProgressMutexCache[$normalizedPath] } else { $pathBytes = [System.Text.Encoding]::UTF8.GetBytes($normalizedPath) $pathHashBytes = [System.Security.Cryptography.SHA256]::HashData($pathBytes) $pathHash = [System.BitConverter]::ToString($pathHashBytes).Replace('-', '') $mutexName = "Local\ZtProgress_$pathHash" $script:ZtProgressMutexCache[$normalizedPath] = $mutexName } $mutex = $null $lockAcquired = $false try { $mutex = [System.Threading.Mutex]::new($false, $mutexName) $lockAcquired = $mutex.WaitOne([TimeSpan]::FromSeconds(5)) if (-not $lockAcquired) { throw "Timed out waiting for progress log mutex '$mutexName'." } [System.IO.File]::AppendAllText($fullPath, $line) } finally { if ($lockAcquired -and $null -ne $mutex) { $null = $mutex.ReleaseMutex() } if ($null -ne $mutex) { $mutex.Dispose() } } } catch { Write-PSFMessage -Level Warning -Message "Failed to write progress log for test '{0}': {1}" -StringValues $TestID, $_.Exception.Message -Tag log } } } |