tests/lib/common.ps1
|
# 测试框架公共库:仓库定位、编译器输入指纹、JSON 读写、GitHub 报告。 $ErrorActionPreference = 'Stop' function Get-RepoRoot { param([string]$StartDir) if ($env:REPO_ROOT -and (Test-Path -LiteralPath (Join-Path $env:REPO_ROOT 'ps12exe.psd1'))) { return (Resolve-Path -LiteralPath $env:REPO_ROOT).Path } $dir = if ($StartDir) { $StartDir } elseif ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } while ($dir) { if (Test-Path -LiteralPath (Join-Path $dir 'ps12exe.psd1')) { return (Resolve-Path -LiteralPath $dir).Path } $parent = Split-Path -Path $dir -Parent if (-not $parent -or $parent -eq $dir) { break } $dir = $parent } throw "找不到仓库根目录(缺少 ps12exe.psd1):$StartDir" } function Get-NormalizedRelPath { param([string]$Path, [string]$Base) $full = [System.IO.Path]::GetFullPath($Path) if ($Base) { $base = [System.IO.Path]::GetFullPath($Base).TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar if ($full.StartsWith($base, [System.StringComparison]::OrdinalIgnoreCase)) { $full = $full.Substring($base.Length) } } return ($full -replace '\\', '/').TrimStart('/') } # 编译输入按「组件」划分:构建缓存键只依赖该构建真正相关的组件,改 CoreCompiler 不会 # 让走 CodeDom/TinySharp 的构建一起失效。路径模式:以 '/' 结尾为目录前缀,支持 '*' 通配。 # 只收录会影响编译产物字节的文件;locale/GUI/WebServer/Interact/exe21sp 等只在编译期或 # 测试运行期使用、不进入产物的文件刻意排除,避免无关改动把构建缓存全打掉。 $script:BuildComponentPatterns = [ordered]@{ common = @( 'ps12exe.ps1', 'ps12exe.psm1', 'ps12exe.psd1', 'src/AstAnalyze.ps1', 'src/BuildFrame.ps1', 'src/ConstProgramCheck.ps1', 'src/GolfModeHeader.ps1', 'src/InitCompileThings.ps1', 'src/PSObjectToString.ps1', 'src/ReadScriptFile.ps1', 'src/predicate.ps1', 'src/programFrames/constexpr.cs', 'src/programFrames/CoreHost.cs', 'src/programFrames/default.cs', 'src/programFrames/DllExport.cs', 'src/programFrames/pack.cs', 'src/programFrames/TinySharp.cs', 'src/RuntimePwsh2.0/' ) codeDom = @('src/CodeDomCompiler.ps1', 'src/ExeSinker.ps1') tinySharp = @('src/TinySharpCompiler.ps1') core = @('src/CoreCompiler.ps1') ps2exe = @('src/.subrepo/PS2EXE2ps12exe/') } function Test-ComponentPathMatch { param([string]$RelPath, [string]$Pattern) $Pattern = $Pattern -replace '\\', '/' if ($Pattern.EndsWith('/')) { return $RelPath.StartsWith($Pattern, [System.StringComparison]::OrdinalIgnoreCase) } if ($Pattern.Contains('*')) { return $RelPath -like $Pattern } return $RelPath -eq $Pattern } # 解析单个组件的文件清单。目录模式递归收集(跳过 node_modules 与 >2mb 文件)。 function Get-ComponentInputFiles { param([string]$RepoRoot, [string]$Component) if (-not $script:BuildComponentPatterns.Contains($Component)) { throw "未知的编译输入组件:$Component" } $files = [System.Collections.Generic.List[string]]::new() foreach ($pat in @($script:BuildComponentPatterns[$Component])) { if ($pat.EndsWith('/')) { $dir = Join-Path $RepoRoot (($pat.TrimEnd('/')) -replace '/', [System.IO.Path]::DirectorySeparatorChar) if (-not (Test-Path -LiteralPath $dir)) { continue } Get-ChildItem -LiteralPath $dir -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $rel = Get-NormalizedRelPath -Path $_.FullName -Base $RepoRoot if ($rel -match '/(node_modules|\.git)/') { return $false } if ($_.Length -gt 2mb) { return $false } return $true } | ForEach-Object { $files.Add($_.FullName) } } else { $p = Join-Path $RepoRoot ($pat -replace '/', [System.IO.Path]::DirectorySeparatorChar) if (Test-Path -LiteralPath $p -PathType Leaf) { $files.Add((Resolve-Path -LiteralPath $p).Path) } } } return @($files) } # 全部编译输入(所有组件的并集,按路径去重排序)。 function Get-CompilerInputFiles { param([string]$RepoRoot) $all = [System.Collections.Generic.List[string]]::new() foreach ($c in $script:BuildComponentPatterns.Keys) { foreach ($f in (Get-ComponentInputFiles -RepoRoot $RepoRoot -Component $c)) { $all.Add($f) } } return @($all | Sort-Object -Unique) } # 扫描仓库文件,默认排除 vcs/build/第三方子模块;跳过 reparse point,避免 junction 造成无限递归。 function Get-RepoFiles { param( [string]$RepoRoot, [string[]]$Extensions, [string[]]$ExcludeRelPatterns = @('^(\.git|build|img|docs)/', '^tests/\.cache/', '^src/\.subrepo/(vscode-plug|ps12exeOnline)/') ) $result = [System.Collections.Generic.List[object]]::new() function Test-Excluded([string]$rel) { if ($rel -match '/node_modules/') { return $true } foreach ($pat in $ExcludeRelPatterns) { if ($rel -match $pat) { return $true } } return $false } function Walk([string]$dir) { foreach ($item in (Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)) { $rel = $item.FullName.Substring($RepoRoot.Length + 1).Replace('\', '/') if ($item.PSIsContainer) { if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { continue } if (Test-Excluded $rel) { continue } Walk $item.FullName } elseif (-not (Test-Excluded $rel) -and ($Extensions -contains $item.Extension.ToLowerInvariant())) { [void]$result.Add($item) } } } Walk $RepoRoot return $result } # 对指定组件(默认全部)的编译输入做内容哈希。构建缓存键用 -Components 只取相关组件, # 使某个组件的源变化只让其自身及依赖它的构建失效。 function Get-SourceFingerprint { param([string]$RepoRoot, [string[]]$Components) $names = if ($Components -and $Components.Count) { @($Components) } else { @($script:BuildComponentPatterns.Keys) } $names = @($names | Sort-Object -Unique) $sha = [System.Security.Cryptography.SHA256]::Create() try { $sb = [System.Text.StringBuilder]::new() foreach ($c in $names) { foreach ($f in (Get-ComponentInputFiles -RepoRoot $RepoRoot -Component $c | Sort-Object)) { $rel = Get-NormalizedRelPath -Path $f -Base $RepoRoot $hash = [System.BitConverter]::ToString($sha.ComputeHash([System.IO.File]::ReadAllBytes($f))).Replace('-', '') [void]$sb.Append($rel).Append(':').Append($hash).Append("`n") } } $final = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($sb.ToString())) return [System.BitConverter]::ToString($final).Replace('-', '').Substring(0, 32) } finally { $sha.Dispose() } } function Get-StringHash { param([string]$Text) $sha = [System.Security.Cryptography.SHA256]::Create() try { return [System.BitConverter]::ToString($sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Text))).Replace('-', '').Substring(0, 32) } finally { $sha.Dispose() } } function Write-TextFileNoBom { param([string]$Path, [string]$Text) [System.IO.File]::WriteAllText($Path, $Text, [System.Text.UTF8Encoding]::new($false)) } function Read-JsonFile { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return $null } return (Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json) } function Write-JsonFile { param([string]$Path, $Object) $text = $Object | ConvertTo-Json -Depth 16 -Compress Write-TextFileNoBom -Path $Path -Text $text } # ConvertFrom-Json 产出 PSCustomObject;测试用例里统一用哈希表索引。 function ConvertTo-HashtableDeep { param($Value) if ($null -eq $Value) { return $null } if ($Value -is [System.Management.Automation.PSCustomObject]) { $ht = @{} foreach ($p in $Value.PSObject.Properties) { $ht[$p.Name] = ConvertTo-HashtableDeep $p.Value } return $ht } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { $arr = @() foreach ($item in $Value) { $arr += , (ConvertTo-HashtableDeep $item) } return $arr } return $Value } function Stop-ProcessTree { param([int]$ProcessId) if ($ProcessId -le 0) { return } try { & "$env:SystemRoot\System32\taskkill.exe" /PID $ProcessId /T /F 2>&1 | Out-Null } catch {} } # 与旧 CI 一致的 GitHub 友好错误格式。$Failures: @( @{ Name; Error; Stack; Log } ) function Write-CIGitHubReport { param([array]$Failures, [string]$ResultFile) if (-not $Failures -or $Failures.Count -eq 0) { return } Write-Output '::group::PSVersion' Write-Output $PSVersionTable Write-Output '::endgroup::' foreach ($f in $Failures) { Write-Output "::error title=TEST $($f.Name)::$($f.Error)" Write-Output '::group::test error details' Write-Output "test: $($f.Name)" Write-Output "message: $($f.Error)" if ($f.Stack) { Write-Output '--- stack trace ---'; Write-Output $f.Stack } if ($f.Log) { Write-Output '--- worker log ---'; Write-Output (($f.Log -split "`r?`n" | Select-Object -Last 120) -join "`n") } Write-Output '::endgroup::' } } |