mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
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:
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user