# # Run the new parser, return either errors or the ast # function Get-ParseResults { [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True,Mandatory=$True)] [string]$src, [switch]$Ast ) $errors = $null $result = [System.Management.Automation.Language.Parser]::ParseInput($src, [ref]$null, [ref]$errors) if ($Ast) { $result } else { ,$errors } } # # Run script and return errors # function Get-RuntimeError { [CmdletBinding()] param( [Parameter(ValueFromPipeline=$True,Mandatory=$True)] [string]$src, [Parameter()] [int]$Timeout = 0 ) $errors = $null try { # some tests cannot be run in a isolated runspace # easily because they require more than just a single # execution if ( $Timeout -eq 0 ) { [scriptblock]::Create($src).Invoke() > $null } else { $ps = [powershell]::Create() $ps.AddScript($src) > $null $ar = $ps.BeginInvoke() # give it 250 milliseconds to complete start-sleep -mill 250 if ( $ar.IsCompleted ) { $ps.EndInvoke($ar) } # wait another ${Timeout} seconds, then give up else { Start-Sleep -sec $Timeout if ( $ar.IsCompleted ) { # this can throw, which will be picked up below $ps.EndInvoke($ar) } else { # if it didn't throw, then return a constructed error $ER = Write-Error "Operation Timed Out ('$src')" 2>&1 return $ER } } } } catch { return $_.Exception.InnerException.ErrorRecord } finally { if ( $ps -ne $null ) { $ps.dispose() } } } function position_message { param($position) if ($position.Line.Length -lt $position.ColumnNumber) { $position.Line + " <<<<" } else { $position.Line.Insert($position.ColumnNumber, " <<<<") } } # # Pester friendly version of Test-Error # function ShouldBeParseError { [CmdletBinding()] param( [string]$src, [string[]]$expectedErrors, [int[]]$expectedOffsets, # This is a temporary solution after moving type creation from parse time to runtime [switch]$SkipAndCheckRuntimeError, # for test coverarage purpose, tests validate columnNumber or offset [switch]$CheckColumnNumber ) Context "Parse error expected: <<$src>>" { # Test case error if this fails $expectedErrors.Count | Should Be $expectedOffsets.Count if ($SkipAndCheckRuntimeError) { It "error should happen at parse time, not at runtime" -Skip {} $errors = Get-RuntimeError -Src $src -Timeout 10 # for runtime errors we will only get the first one $expectedErrors = ,$expectedErrors[0] $expectedOffsets = ,$expectedOffsets[0] } else { $errors = Get-ParseResults -Src $src } It "Error count" { $errors.Count | Should Be $expectedErrors.Count } for ($i = 0; $i -lt $errors.Count; ++$i) { $err = $errors[$i] if ($SkipAndCheckRuntimeError) { $errorId = $err.FullyQualifiedErrorId } else { $errorId = $err.ErrorId } It "Error Id" { $errorId | Should Be $expectedErrors[$i] } $acutalPostion = $err.Extent.StartScriptPosition.Offset if ( $CheckColumnNumber ) { $acutalPostion = $err.Extent.StartScriptPosition.ColumnNumber } It "Error position" -Pending:$SkipAndCheckRuntimeError { $acutalPostion | Should Be $expectedOffsets[$i] } } } } function Flatten-Ast { [CmdletBinding()] param([System.Management.Automation.Language.Ast] $ast) $ast $ast | gm -type property | ? { ($prop = $_.Name) -ne 'Parent' } | % { $ast.$prop | ? { $_ -is [System.Management.Automation.Language.Ast] } | % { Flatten-Ast $_ } } } function Test-ErrorStmt { param([string]$src, [string]$errorStmtExtent) $a = $args Context "Error Statement expected: <<$src>>" { $ast = Get-ParseResults $src -Ast $asts = @(Flatten-Ast $ast.EndBlock.Statements[0]) It 'Type is ErrorStatementAst' { $asts[0].GetType() | Should Be System.Management.Automation.Language.ErrorStatementAst } It "`$asts.count" { $asts.Count | Should Be ($a.Count + 1) } It "`$asts[0].Extent.Text" { $asts[0].Extent.Text | Should Be $errorStmtExtent } for ($i = 0; $i -lt $a.Count; ++$i) { It "`$asts[$($i + 1)].Extent.Text" { $asts[$i + 1].Extent.Text | Should Be $a[$i] } } } } function Test-Ast { param([string]$src) $a = $args $ast = Get-ParseResults $src -Ast $asts = @(Flatten-Ast $ast) Context "Ast Validation: <<$src>>" { It "`$asts.count" { $asts.Count | Should Be $a.Count } for ($i = 0; $i -lt $a.Count; ++$i) { It "`$asts[$i].Extent.Text" { $asts[$i].Extent.Text | Should Be $a[$i] } } } } ## ErrorStatement is special for SwitchStatement function Test-ErrorStmtForSwitchFlag { param([string]$src, [string]$flagName) $a = $args $ast = Get-ParseResults $src -Ast $ast = $ast.EndBlock.Statements[0] Context "Ast Validation: <<$src>>" { $ast.GetType() | Should Be System.Management.Automation.Language.ErrorStatementAst $ast.Flags.ContainsKey($flagName) | Should be $true $asts = @(Flatten-Ast $ast.Flags[$flagName].Item2) $asts.Count | Should Be $a.Count for ($i = 0; $i -lt $a.Count; ++$i) { $asts[$i].Extent.Text | Should Be $a[$i] } } } Export-ModuleMember -Function Test-ErrorStmt, Test-Ast, ShouldBeParseError, Get-ParseResults, Get-RuntimeError, Test-ErrorStmtForSwitchFlag