Fix sequence point update for switch/if/for/while/do-while/do-until statements (#7305)

- Make switch-statement report correct error position when it fails to evaluate the condition.
- Make for-statement report correct error position when it fails to evaluate the initializer.
- For the condition of `if/for/while/do-while/do-until` statements, the sequence point update is either duplicate in some cases (for `if/for/while`) which causes debugger to stop at the condition twice  before moving forward, or missing (for `do-while/do-until`) which causes debugger to skip the condition. They are fixed.
This commit is contained in:
Dongbo Wang
2018-07-25 09:56:32 -07:00
committed by GitHub
parent 439f97c533
commit 87d8fc969c
3 changed files with 461 additions and 44 deletions
@@ -743,14 +743,16 @@ namespace System.Management.Automation.Language
s_builtinAttributeGenerator.Add(typeof(ValidateNotNullOrEmptyAttribute), NewValidateNotNullOrEmptyAttribute);
}
private Compiler(List<IScriptExtent> sequencePoints)
private Compiler(List<IScriptExtent> sequencePoints, Dictionary<IScriptExtent, int> sequencePointIndexMap)
{
_sequencePoints = sequencePoints;
_sequencePointIndexMap = sequencePointIndexMap;
}
internal Compiler()
{
_sequencePoints = new List<IScriptExtent>();
_sequencePointIndexMap = new Dictionary<IScriptExtent, int>();
}
internal bool CompilingConstantExpression { get; set; }
@@ -766,6 +768,7 @@ namespace System.Management.Automation.Language
private int _switchTupleIndex = VariableAnalysis.Unanalyzed;
private int _foreachTupleIndex = VariableAnalysis.Unanalyzed;
private readonly List<IScriptExtent> _sequencePoints;
private readonly Dictionary<IScriptExtent, int> _sequencePointIndexMap;
private int _stmtCount;
internal bool CompilingMemberFunction { get; set; }
@@ -903,17 +906,31 @@ namespace System.Management.Automation.Language
ExpressionCache.Constant(version));
}
internal Expression UpdatePosition(Ast ast)
private int AddSequencePoint(IScriptExtent extent)
{
_sequencePoints.Add(ast.Extent);
// Make sure we don't add the same extent to the sequence point list twice.
if (!_sequencePointIndexMap.TryGetValue(extent, out int index))
{
_sequencePoints.Add(extent);
index = _sequencePoints.Count - 1;
_sequencePointIndexMap.Add(extent, index);
}
return index;
}
private Expression UpdatePosition(Ast ast)
{
IScriptExtent extent = ast.Extent;
int index = AddSequencePoint(extent);
// If we just added the first sequence point, then we don't want to check for breakpoints - we'll do that
// in EnterScriptFunction.
// Except for while/do loops, in this case we want to check breakpoints on the first sequence point since it
// will be executed multiple times.
return ((_sequencePoints.Count == 1) && !_generatingWhileOrDoLoop)
return (index == 0 && !_generatingWhileOrDoLoop)
? ExpressionCache.Empty
: new UpdatePositionExpr(ast.Extent, _sequencePoints.Count - 1, _debugSymbolDocument, !_compilingSingleExpression);
: new UpdatePositionExpr(extent, index, _debugSymbolDocument, !_compilingSingleExpression);
}
private int _tempCounter;
@@ -1683,7 +1700,7 @@ namespace System.Management.Automation.Language
// on this sequence point, but it makes it safe to access the CurrentPosition
// property in FunctionContext (which can happen if there are exceptions
// defining the functions.)
_sequencePoints.Add(ast.Extent);
AddSequencePoint(ast.Extent);
}
var compileInterpretChoice = (_stmtCount > 300) ? CompileInterpretChoice.NeverCompile : CompileInterpretChoice.CompileOnDemand;
@@ -1853,8 +1870,8 @@ namespace System.Management.Automation.Language
var exprs = new List<Expression>();
var temps = new List<ParameterExpression> { _executionContextParameter, LocalVariablesParameter };
GenerateFunctionProlog(exprs, temps, null);
_sequencePoints.Add(expressionAst.Extent);
exprs.Add(new UpdatePositionExpr(expressionAst.Extent, _sequencePoints.Count - 1, _debugSymbolDocument, checkBreakpoints: true));
int index = AddSequencePoint(expressionAst.Extent);
exprs.Add(new UpdatePositionExpr(expressionAst.Extent, index, _debugSymbolDocument, checkBreakpoints: true));
var result = Compile(expressionAst).Cast(typeof(object));
exprs.Add(Expression.Label(_returnTarget, result));
var body = Expression.Block(new[] { _executionContextParameter, LocalVariablesParameter }, exprs);
@@ -2143,7 +2160,7 @@ namespace System.Management.Automation.Language
private Tuple<Action<FunctionContext>, Type> CompileTrap(TrapStatementAst trap)
{
var compiler = new Compiler(_sequencePoints) { _compilingTrap = true };
var compiler = new Compiler(_sequencePoints, _sequencePointIndexMap) { _compilingTrap = true };
string funcName = _currentFunctionName + "<trap>";
if (trap.TrapType != null)
{
@@ -2517,8 +2534,8 @@ namespace System.Management.Automation.Language
if (entryExtent != null)
{
_sequencePoints.Add(entryExtent);
exprs.Add(new UpdatePositionExpr(entryExtent, _sequencePoints.Count - 1, _debugSymbolDocument, checkBreakpoints: false));
int index = AddSequencePoint(entryExtent);
exprs.Add(new UpdatePositionExpr(entryExtent, index, _debugSymbolDocument, checkBreakpoints: false));
}
exprs.Add(
@@ -2568,8 +2585,8 @@ namespace System.Management.Automation.Language
}
var extent = propertyMember.InitialValue.Extent;
_sequencePoints.Add(extent);
exprs.Add(new UpdatePositionExpr(extent, _sequencePoints.Count - 1, _debugSymbolDocument, checkBreakpoints: false));
int index = AddSequencePoint(extent);
exprs.Add(new UpdatePositionExpr(extent, index, _debugSymbolDocument, checkBreakpoints: false));
var property = _memberFunctionType.Type.GetProperty(propertyMember.Name, bindingFlags);
exprs.Add(
Expression.Assign(
@@ -2952,12 +2969,12 @@ namespace System.Management.Automation.Language
{
int clauseCount = ifStmtAst.Clauses.Count;
var exprs = new Tuple<BlockExpression, Expression>[clauseCount];
var exprs = new Tuple<Expression, Expression>[clauseCount];
for (int i = 0; i < clauseCount; ++i)
{
IfClause ifClause = ifStmtAst.Clauses[i];
var cond = Expression.Block(
UpdatePosition(ifClause.Item1),
var cond = UpdatePositionForInitializerOrCondition(
ifClause.Item1,
CaptureStatementResults(ifClause.Item1, CaptureAstContext.Condition).Convert(typeof(bool)));
var body = Compile(ifClause.Item2);
exprs[i] = Tuple.Create(cond, body);
@@ -3022,7 +3039,7 @@ namespace System.Management.Automation.Language
}
var exprs = new List<Expression>
{
{
// Set current position in case of errors.
UpdatePosition(assignmentStatementAst),
ReduceAssignment((ISupportsAssignment)assignmentStatementAst.Left,
@@ -4174,7 +4191,7 @@ namespace System.Management.Automation.Language
_loopTargets.Add(new LoopGotoTargets(loopLabel ?? string.Empty, breakLabel, continueLabel));
_generatingWhileOrDoLoop = true;
var loopBodyExprs = new List<Expression>
{
{
s_callCheckForInterrupts,
Compile(loopStatement.Body),
ExpressionCache.Empty
@@ -4190,6 +4207,7 @@ namespace System.Management.Automation.Language
{
test = Expression.Not(test);
}
test = UpdatePositionForInitializerOrCondition(loopStatement.Condition, test);
exprs.Add(Expression.IfThen(test, Expression.Goto(repeatLabel)));
exprs.Add(Expression.Label(breakLabel));
@@ -4240,10 +4258,26 @@ namespace System.Management.Automation.Language
// $foreach/$switch = GetEnumerator $enumerable
var enumerable = NewTemp(typeof(object), "enumerable");
temps.Add(enumerable);
// Update position to make it safe to access 'CurrentPosition' property in FunctionContext in case
// that the evaluation of 'stmt.Condition' throws exception.
if (generatingForeach)
{
// For foreach statement, we want the debugger to stop at 'stmt.Condition' before evaluating it.
// The debugger will stop at 'stmt.Condition' only once. The following enumeration will stop at
// the foreach variable.
exprs.Add(UpdatePosition(stmt.Condition));
}
else
{
// For switch statement, we don't want the debugger to stop at 'stmt.Condition' before evaluating it.
// The following enumeration will stop at 'stmt.Condition' again, and we don't want the debugger to
// stop at 'stmt.Condition' twice before getting into one of its case clauses.
var extent = stmt.Condition.Extent;
int index = AddSequencePoint(extent);
exprs.Add(new UpdatePositionExpr(extent, index, _debugSymbolDocument, checkBreakpoints: false));
}
exprs.Add(
Expression.Assign(enumerable,
GetRangeEnumerator(stmt.Condition.GetPureExpression())
@@ -4354,6 +4388,19 @@ namespace System.Management.Automation.Language
return null;
}
private Expression UpdatePositionForInitializerOrCondition(PipelineBaseAst pipelineBaseAst, Expression initializerOrCondition)
{
if (pipelineBaseAst is PipelineAst pipelineAst && !pipelineAst.Background && pipelineAst.GetPureExpression() != null)
{
// If the initializer or condition is a pure expression (CommandExpressionAst without redirection),
// then we need to add a sequence point. If it's an AssignmentStatementAst, we don't need to add
// sequence point here because one will be added when processing the AssignmentStatementAst.
initializerOrCondition = Expression.Block(UpdatePosition(pipelineBaseAst), initializerOrCondition);
}
return initializerOrCondition;
}
public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst)
{
return GenerateDoLoop(doWhileStatementAst);
@@ -4367,13 +4414,17 @@ namespace System.Management.Automation.Language
public object VisitForStatement(ForStatementAst forStatementAst)
{
// We should not preserve the partial output if exception is thrown when evaluating the initializer.
var init = (forStatementAst.Initializer != null)
? CaptureStatementResults(forStatementAst.Initializer, CaptureAstContext.AssignmentWithoutResultPreservation)
: null;
Expression init = null;
PipelineBaseAst initializer = forStatementAst.Initializer;
if (initializer != null)
{
init = CaptureStatementResults(initializer, CaptureAstContext.AssignmentWithoutResultPreservation);
init = UpdatePositionForInitializerOrCondition(initializer, init);
}
var generateCondition = forStatementAst.Condition != null
? () => Expression.Block(UpdatePosition(forStatementAst.Condition),
CaptureStatementResults(forStatementAst.Condition, CaptureAstContext.Condition))
PipelineBaseAst condition = forStatementAst.Condition;
var generateCondition = condition != null
? () => UpdatePositionForInitializerOrCondition(condition, CaptureStatementResults(condition, CaptureAstContext.Condition))
: (Func<Expression>)null;
var loop = GenerateWhileLoop(forStatementAst.Label, generateCondition,
@@ -4389,9 +4440,9 @@ namespace System.Management.Automation.Language
public object VisitWhileStatement(WhileStatementAst whileStatementAst)
{
PipelineBaseAst condition = whileStatementAst.Condition;
return GenerateWhileLoop(whileStatementAst.Label,
() => Expression.Block(UpdatePosition(whileStatementAst.Condition),
CaptureStatementResults(whileStatementAst.Condition, CaptureAstContext.Condition)),
() => UpdatePositionForInitializerOrCondition(condition, CaptureStatementResults(condition, CaptureAstContext.Condition)),
(loopBody, breakTarget, continueTarget) => loopBody.Add(Compile(whileStatementAst.Body)));
}
@@ -1,6 +1,10 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
$script1 = @'
Describe "Breakpoints when set should be hit" -tag "CI" {
Context "Basic tests" {
BeforeAll {
$script = @'
'aaa'.ToString() > $null
'aa' > $null
"a" 2> $null | ForEach-Object { $_ }
@@ -8,32 +12,285 @@ $script1 = @'
'bb'.ToSTring() > $null
'bbb'
'@
$script2 = @'
"line 1"
"line 2"
"line 3"
$path = Setup -PassThru -File BasicTest.ps1 -Content $script
$bps = 1..6 | ForEach-Object { set-psbreakpoint -script $path -line $_ -Action { continue } }
}
AfterAll {
$bps | Remove-PSBreakPoint
}
It "A redirected breakpoint is hit" {
& $path
foreach ( $bp in $bps ) {
$bp.HitCount | Should -Be 1
}
}
}
Context "Break point on switch condition should be hit only when enumerating it" {
BeforeAll {
$script = @'
$test = 1..2
switch ($test)
{
default {}
}
'@
$path = Setup -PassThru -File SwitchScript.ps1 -Content $script
$breakpoint = Set-PSBreakpoint -Script $path -Line 2 -Action { continue }
}
AfterAll {
Remove-PSBreakpoint -Breakpoint $breakpoint
}
It "switch condition should be hit 3 times" {
## MoveNext() will be called on the condition for 3 times
$null = & $path
$breakpoint.HitCount | Should -Be 3
}
}
Context "Break point on for-statement initializer should be hit" {
BeforeAll {
$for_script_1 = @'
$test = 2
for ("string".Length;
$test -gt 0; $test--) { }
'@
Describe "Breakpoints when set should be hit" -tag "CI" {
BeforeAll {
$path = setup -pass -f TestScript_1.ps1 -content $script1
$bps = 1..6 | ForEach-Object { set-psbreakpoint -script $path -line $_ -Action { continue } }
$for_script_2 = @'
$test = $PSCommandPath
for (Test-Path $test;
$test -eq "blah";) { }
'@
$for_script_3 = @'
for ($test = 2;
$test -gt 0; $test--) { }
'@
$for_script_4 = @'
$test = 2
for (;$test -gt 0;
$test--) { }
'@
$for_script_5 = @'
$test = $PSCommandPath
for (;Test-Path $test;)
{
$test = "blah"
}
'@
$ForScript_1 = Setup -PassThru -File ForScript_1.ps1 -Content $for_script_1
$bp_1 = Set-PSBreakpoint -Script $ForScript_1 -Line 2 -Action { continue }
$ForScript_2 = Setup -PassThru -File ForScript_2.ps1 -Content $for_script_2
$bp_2 = Set-PSBreakpoint -Script $ForScript_2 -Line 2 -Action { continue }
$ForScript_3 = Setup -PassThru -File ForScript_3.ps1 -Content $for_script_3
$bp_3 = Set-PSBreakpoint -Script $ForScript_3 -Line 1 -Action { continue }
$ForScript_4 = Setup -PassThru -File ForScript_4.ps1 -Content $for_script_4
$bp_4 = Set-PSBreakpoint -Script $ForScript_4 -Line 2 -Action { continue }
$ForScript_5 = Setup -PassThru -File ForScript_5.ps1 -Content $for_script_5
$bp_5 = Set-PSBreakpoint -Script $ForScript_5 -Line 2 -Action { continue }
$testCases = @(
@{ Name = "expression initializer should be hit once"; Path = $ForScript_1; Breakpoint = $bp_1; HitCount = 1 }
@{ Name = "pipeline initializer should be hit once"; Path = $ForScript_2; Breakpoint = $bp_2; HitCount = 1 }
@{ Name = "assignment initializer should be hit 3 times"; Path = $ForScript_3; Breakpoint = $bp_3; HitCount = 1 }
@{ Name = "pipeline condition should be hit 3 times"; Path = $ForScript_4; Breakpoint = $bp_4; HitCount = 3 }
@{ Name = "pipeline condition should be hit 2 times"; Path = $ForScript_5; Breakpoint = $bp_5; HitCount = 2 }
)
}
AfterAll {
Get-PSBreakpoint -Script $ForScript_1, $ForScript_2, $ForScript_3, $ForScript_4, $ForScript_5 | Remove-PSBreakpoint
}
It "for-statement <Name>" -TestCases $testCases {
param($Path, $Breakpoint, $HitCount)
$null = & $Path
$Breakpoint.HitCount | Should -Be $HitCount
}
}
AfterAll {
$bps | Remove-PSBreakPoint
Context "Break point on while loop condition should be hit" {
BeforeAll {
$while_script_1 = @'
$test = "string"
while ($test.Contains("str"))
{
$test = "blah"
}
'@
$while_script_2 = @'
$test = $PSCommandPath
while (Test-Path $test)
{
$test = "blah"
}
'@
$WhileScript_1 = Setup -PassThru -File WhileScript_1.ps1 -Content $while_script_1
$bp_1 = Set-PSBreakpoint -Script $WhileScript_1 -Line 2 -Action { continue }
$WhileScript_2 = Setup -PassThru -File WhileScript_2.ps1 -Content $while_script_2
$bp_2 = Set-PSBreakpoint -Script $WhileScript_2 -Line 2 -Action { continue }
$testCases = @(
@{ Name = "expression condition should be hit 2 times"; Path = $WhileScript_1; Breakpoint = $bp_1; HitCount = 2 }
@{ Name = "pipeline condition should be hit 2 times"; Path = $WhileScript_2; Breakpoint = $bp_2; HitCount = 2 }
)
}
AfterAll {
Get-PSBreakpoint -Script $WhileScript_1, $WhileScript_2 | Remove-PSBreakpoint
}
It "while loop <Name>" -TestCases $testCases {
param($Path, $Breakpoint, $HitCount)
$null = & $Path
$Breakpoint.HitCount | Should -Be $HitCount
}
}
It "A redirected breakpoint is hit" {
& $path
foreach ( $bp in $bps ) {
$bp.HitCount | Should -Be 1
Context "Break point on do-while loop condition should be hit" {
BeforeAll {
$do_while_script_1 = @'
$test = "blah"
do { echo $test }
while ($test.Contains("str"))
'@
$do_while_script_2 = @'
$test = "blah"
do { echo $test }
while (Test-Path $test)
'@
$DoWhileScript_1 = Setup -PassThru -File DoWhileScript_1.ps1 -Content $do_while_script_1
$bp_1 = Set-PSBreakpoint -Script $DoWhileScript_1 -Line 2 -Action { continue }
$DoWhileScript_2 = Setup -PassThru -File DoWhileScript_2.ps1 -Content $do_while_script_2
$bp_2 = Set-PSBreakpoint -Script $DoWhileScript_2 -Line 2 -Action { continue }
$testCases = @(
@{ Name = "expression condition should be hit 2 times"; Path = $DoWhileScript_1; Breakpoint = $bp_1; HitCount = 1 }
@{ Name = "pipeline condition should be hit 2 times"; Path = $DoWhileScript_2; Breakpoint = $bp_2; HitCount = 1 }
)
}
AfterAll {
Get-PSBreakpoint -Script $DoWhileScript_1, $DoWhileScript_2 | Remove-PSBreakpoint
}
It "Do-While loop <Name>" -TestCases $testCases {
param($Path, $Breakpoint, $HitCount)
$null = & $Path
$Breakpoint.HitCount | Should -Be $HitCount
}
}
Context "Break point on do-until loop condition should be hit" {
BeforeAll {
$do_until_script_1 = @'
$test = "blah"
do { echo $test }
until ($test.Contains("bl"))
'@
$do_until_script_2 = @'
$test = $PSCommandPath
do { echo $test }
until (Test-Path $test)
'@
$DoUntilScript_1 = Setup -PassThru -File DoUntilScript_1.ps1 -Content $do_until_script_1
$bp_1 = Set-PSBreakpoint -Script $DoUntilScript_1 -Line 2 -Action { continue }
$DoUntilScript_2 = Setup -PassThru -File DoUntilScript_2.ps1 -Content $do_until_script_2
$bp_2 = Set-PSBreakpoint -Script $DoUntilScript_2 -Line 2 -Action { continue }
$testCases = @(
@{ Name = "expression condition should be hit 2 times"; Path = $DoUntilScript_1; Breakpoint = $bp_1; HitCount = 1 }
@{ Name = "pipeline condition should be hit 2 times"; Path = $DoUntilScript_2; Breakpoint = $bp_2; HitCount = 1 }
)
}
AfterAll {
Get-PSBreakpoint -Script $DoUntilScript_1, $DoUntilScript_2 | Remove-PSBreakpoint
}
It "Do-Until loop <Name>" -TestCases $testCases {
param($Path, $Breakpoint, $HitCount)
$null = & $Path
$Breakpoint.HitCount | Should -Be $HitCount
}
}
Context "Break point on if condition should be hit" {
BeforeAll {
$if_script_1 = @'
if ("string".Contains('str'))
{ }
'@
$if_script_2 = @'
if (Test-Path $PSCommandPath)
{ }
'@
$if_script_3 = @'
if ($false) {}
elseif ("string".Contains('str'))
{ }
'@
$if_script_4 = @'
if ($false) {}
elseif (Test-Path $PSCommandPath)
{ }
'@
$IfScript_1 = Setup -PassThru -File IfScript_1.ps1 -Content $if_script_1
$bp_1 = Set-PSBreakpoint -Script $IfScript_1 -Line 1 -Action { continue }
$IfScript_2 = Setup -PassThru -File IfScript_2.ps1 -Content $if_script_2
$bp_2 = Set-PSBreakpoint -Script $IfScript_2 -Line 1 -Action { continue }
$IfScript_3 = Setup -PassThru -File IfScript_3.ps1 -Content $if_script_3
$bp_3 = Set-PSBreakpoint -Script $IfScript_3 -Line 2 -Action { continue }
$IfScript_4 = Setup -PassThru -File IfScript_4.ps1 -Content $if_script_4
$bp_4 = Set-PSBreakpoint -Script $IfScript_4 -Line 2 -Action { continue }
$testCases = @(
@{ Name = "expression if-condition should be hit once"; Path = $IfScript_1; Breakpoint = $bp_1; HitCount = 1 }
@{ Name = "pipeline if-condition should be hit once"; Path = $IfScript_2; Breakpoint = $bp_2; HitCount = 1 }
@{ Name = "expression elseif-condition should be hit once"; Path = $IfScript_3; Breakpoint = $bp_3; HitCount = 1 }
@{ Name = "pipeline elseif-condition should be hit once"; Path = $IfScript_4; Breakpoint = $bp_4; HitCount = 1 }
)
}
AfterAll {
Get-PSBreakpoint -Script $IfScript_1, $IfScript_2, $IfScript_3, $IfScript_4 | Remove-PSBreakpoint
}
It "If statement <Name>" -TestCases $testCases {
param($Path, $Breakpoint, $HitCount)
$null = & $Path
$Breakpoint.HitCount | Should -Be $HitCount
}
}
}
Describe "It should be possible to reset runspace debugging" -tag "Feature" {
BeforeAll {
$path = setup -pass -f TestScript_2.ps1 -content $script2
$scriptPath = "$testdrive/TestScript_2.ps1"
$script = @'
"line 1"
"line 2"
"line 3"
'@
$scriptPath = Setup -PassThru -File TestScript.ps1 -Content $script
$iss = [initialsessionstate]::CreateDefault2();
$rs = [runspacefactory]::CreateRunspace($iss)
$rs.Name = "TestRunspaceDebuggerReset"
@@ -87,6 +344,6 @@ Describe "It should be possible to reset runspace debugging" -tag "Feature" {
}
It "The script should run without a break" {
$ps.Commands.Clear()
$ps.addscript("$testdrive/TestScript_2.ps1").Invoke().Count | Should -Be 3
$ps.addscript($scriptPath).Invoke().Count | Should -Be 3
}
}
@@ -0,0 +1,109 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Describe "Error position Tests" -Tags "CI" {
BeforeAll {
$switch_condition_script = @'
$test = 1
switch ($null[0]) {
"a" {};
}
'@
$for_expression_initializer_script = @'
$test = 1
for ($null[0];
$test -gt 1;) { }
'@
$for_pipeline_initializer_script = @'
$test = 1
for (Test-Path $null;
$test -gt 1;) { }
'@
$for_expression_condition_script = @'
$test = 1
for (;$null[0];)
{ }
'@
$for_pipeline_condition_script = @'
$test = 1
for (;Test-Path $null;)
{ }
'@
$do_while_expression_condition_script = @'
$test = 1
do {}
while ($null[0])
'@
$do_while_pipeline_condition_script = @'
$test = 1
do {}
while (Test-Path $null)
'@
$do_until_expression_condition_script = @'
$test = 1
do {}
until ($null[0])
'@
$do_until_pipeline_condition_script = @'
$test = 1
do {}
until (Test-Path $null)
'@
$testCases = @(
@{ Name = "switch condition"; FileName = "SwitchError2.ps1"; Script = $switch_condition_script; MatchText = "SwitchError2.ps1: line 2" }
@{ Name = "for statement expression initializer"; FileName = "ForError1.ps1"; Script = $for_expression_initializer_script; MatchText = "ForError1.ps1: line 2" }
@{ Name = "for statement pipeline initializer"; FileName = "ForError2.ps1"; Script = $for_pipeline_initializer_script; MatchText = "ForError2.ps1: line 2" }
@{ Name = "for statement expression condition"; FileName = "ForError3.ps1"; Script = $for_expression_condition_script; MatchText = "ForError3.ps1: line 2" }
@{ Name = "for statement pipeline condition"; FileName = "ForError4.ps1"; Script = $for_pipeline_condition_script; MatchText = "ForError4.ps1: line 2" }
@{ Name = "do-while statement expression condition"; FileName = "DoWhileError1.ps1"; Script = $do_while_expression_condition_script; MatchText = "DoWhileError1.ps1: line 3" }
@{ Name = "do-while statement pipeline condition"; FileName = "DoWhileError2.ps1"; Script = $do_while_pipeline_condition_script; MatchText = "DoWhileError2.ps1: line 3" }
@{ Name = "do-until statement expression condition"; FileName = "DoUntilError1.ps1"; Script = $do_until_expression_condition_script; MatchText = "DoUntilError1.ps1: line 3" }
@{ Name = "do-until statement pipeline condition"; FileName = "DoUntilError2.ps1"; Script = $do_until_pipeline_condition_script; MatchText = "DoUntilError2.ps1: line 3" }
)
}
It "<Name> evaluation failure should report correct error position" -TestCases $testCases {
param($FileName, $Script, $MatchText)
$testFile = Join-Path $TestDrive $FileName
Set-Content -Path $testFile -Encoding Ascii -Value $Script
try { & $testFile } catch { $errorRecord = $_ }
$errorRecord | Should -Not -BeNullOrEmpty
$errorRecord.ScriptStackTrace | Should -Match $MatchText
}
It "switch condition MoveNext failure should report correct error position" {
$code = @'
using System;
using System.Collections.Generic;
namespace SwitchTest
{
public class Test
{
public static IEnumerable<string> GetName()
{
yield return "Hello world";
throw new ArgumentException();
}
}
}
'@
$testFile = Join-Path $TestDrive "SwitchError1.ps1"
Set-Content -Path $testFile -Encoding Ascii -Value @'
$test = 1
$enumerable = [SwitchTest.Test]::GetName()
switch ($enumerable) {
"hello world" { $test = 1; $_ }
"Yay" { $test = 2; $_ }
}
'@
if (-not ("SwitchTest.Test" -as [type])) {
Add-Type -TypeDefinition $code
}
try { & $testFile > $null } catch { $errorRecord = $_ }
$errorRecord | Should -Not -BeNullOrEmpty
$errorRecord.ScriptStackTrace | Should -Match "SwitchError1.ps1: line 3"
}
}