Modules/businessdev.ALbuild.Apps/Private/Get-BcAlTestProcedure.ps1
|
function Get-BcAlTestProcedure { <# .SYNOPSIS Enumerates the [Test] procedures in AL sources and returns their statement lines. .DESCRIPTION Shared AL scan behind Get-BcTestQuality and Test-BcTestAssertion, so both see the same set of tests and the same bodies and can only differ in the verdict they draw. For every procedure that carries a [Test] attribute it returns the declaring codeunit, the procedure name, its location and the executable statement lines of its body (comments stripped, block keywords and the surrounding begin/end removed). The body runs from the first 'begin' after the procedure header to its matching 'end', so nested begin/end blocks stay inside the test and the next procedure is not swallowed. .PARAMETER WorkspaceRoot AL source root to scan; tooling folders (.alpackages, output, .git, node_modules, ...) are skipped. .PARAMETER Path A single .al file to scan instead of a whole workspace. .OUTPUTS PSCustomObject per test: Codeunit, TestName, FilePath, Line, Statements[]. #> [CmdletBinding(DefaultParameterSetName = 'Workspace')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, ParameterSetName = 'Workspace')] [ValidateNotNullOrEmpty()] [string] $WorkspaceRoot, [Parameter(Mandatory, ParameterSetName = 'File')] [ValidateNotNullOrEmpty()] [string] $Path ) $files = if ($PSCmdlet.ParameterSetName -eq 'File') { , (Get-Item -LiteralPath $Path) } else { $root = (Resolve-Path -LiteralPath $WorkspaceRoot).Path Get-ChildItem -LiteralPath $root -Filter '*.al' -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName.Substring($root.Length) -notmatch '[\\/](\.alpackages|\.altemplates|\.snapshots|\.output|output|\.git|node_modules)[\\/]' } } $result = [System.Collections.Generic.List[object]]::new() foreach ($file in $files) { $lines = @(Get-Content -LiteralPath $file.FullName -ErrorAction SilentlyContinue) if ($lines.Count -eq 0) { continue } $cuName = '' $m = [regex]::Match(($lines -join "`n"), '(?im)^\s*codeunit\s+\d+\s+("(?<q>[^"]+)"|(?<b>[A-Za-z0-9_]+))') if ($m.Success) { $cuName = if ($m.Groups['q'].Success) { $m.Groups['q'].Value } else { $m.Groups['b'].Value } } $isTestAttr = $false for ($i = 0; $i -lt $lines.Count; $i++) { $t = $lines[$i].Trim() if ($t -match '^\[Test\b') { $isTestAttr = $true; continue } $pm = [regex]::Match($t, '(?i)^(local\s+|internal\s+)*procedure\s+(?<n>[A-Za-z0-9_]+)') # Any other attribute ([HandlerFunctions], [TransactionModel], ...) keeps the pending [Test]. if (-not $pm.Success) { if ($t -ne '' -and $t -notmatch '^\[') { $isTestAttr = $false }; continue } if (-not $isTestAttr) { continue } $isTestAttr = $false $statements = [System.Collections.Generic.List[string]]::new() $depth = 0; $started = $false for ($j = $i + 1; $j -lt $lines.Count; $j++) { $code = ($lines[$j] -replace '//.*$', '').Trim() if ($code -eq '') { continue } $lower = $code.ToLowerInvariant() $opens = ([regex]::Matches($lower, '(?<![A-Za-z0-9_])begin(?![A-Za-z0-9_])')).Count $closes = ([regex]::Matches($lower, '(?<![A-Za-z0-9_])end(?![A-Za-z0-9_])')).Count if (-not $started) { if ($opens -gt 0) { $started = $true; $depth = 0 } else { continue } } $isKeyword = $lower -match '^(begin|end|end;|else|do|then|var)$' if (-not $isKeyword -and $lower -notmatch '^[{}();]+$' -and $lower -notmatch '^(begin|end)\b') { [void]$statements.Add($code) } $depth += $opens - $closes if ($depth -le 0) { break } } $result.Add([PSCustomObject]@{ Codeunit = $cuName TestName = $pm.Groups['n'].Value FilePath = $file.FullName Line = $i + 1 Statements = @($statements) }) } } return @($result) } |