[Dexter] Add !then node (#202546)
In order to exercise more control over stepping in Dexter tests, this
patch adds the `!then` node which can be used to step out of a function
or exit the current test. Unlike expect nodes, !then nodes appear as
direct singular children of a state node:
!where {lines: 10}: !then finish
The two currently available commands are "step_out" and "finish".
step_out performs a debugger "step out" command, skipping over all
!wheres in the current frame and not stepping into any lower !wheres.
The finish command ends the debugger session immediately after finishing
the current step.
This commit is contained in:
parent
97df9d1c25
commit
d08ca31f48
8 changed files with 196 additions and 10 deletions
|
|
@ -130,11 +130,24 @@ class ScriptDebuggerController(DebuggerControllerBase):
|
|||
]
|
||||
self.debugger.collect_watches(step_info, watches)
|
||||
|
||||
active_thens = [
|
||||
then
|
||||
for where_match in active_where_matches.values()
|
||||
for then in where_match.active_thens
|
||||
]
|
||||
should_step_out = any(then.command == "step_out" for then in active_thens)
|
||||
should_finish = any(then.command == "finish" for then in active_thens)
|
||||
|
||||
# Our stepping behaviour is as follows:
|
||||
# - If any !where matches the current stack frame, we step.
|
||||
# - If any !then is active, we follow its command.
|
||||
# - Otherwise, if any !where matches the current stack frame, we step.
|
||||
# - Otherwise, if any !where matches any non-current stack frame, we step out.
|
||||
# - Otherwise, we continue.
|
||||
if any(
|
||||
if should_finish:
|
||||
next_action = DebuggerAction.EXIT
|
||||
elif should_step_out:
|
||||
next_action = DebuggerAction.STEP_OUT
|
||||
elif any(
|
||||
where_match.frame_idx == 0
|
||||
for where_match in active_where_matches.values()
|
||||
):
|
||||
|
|
@ -155,14 +168,15 @@ class ScriptDebuggerController(DebuggerControllerBase):
|
|||
if (
|
||||
bp_ids
|
||||
and where not in script.root_wheres
|
||||
and where not in pending_wheres
|
||||
and (where not in pending_wheres or should_step_out)
|
||||
):
|
||||
bp_to_delete.extend(bp_ids)
|
||||
bp_ids.clear()
|
||||
self.debugger.delete_breakpoints(bp_to_delete)
|
||||
for where in pending_wheres:
|
||||
if not self._where_bps[where]:
|
||||
self.add_where_entry_bp(where)
|
||||
if not should_step_out:
|
||||
for where in pending_wheres:
|
||||
if not self._where_bps[where]:
|
||||
self.add_where_entry_bp(where)
|
||||
|
||||
if step_info.current_frame:
|
||||
self._step_index += 1
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from typing import Dict, List, Tuple
|
|||
|
||||
from dex.dextIR import FrameIR, StepIR
|
||||
from dex.test_script import DexterScript, Scope
|
||||
from dex.test_script.Nodes import Expect, FileLabels, Value, Where
|
||||
from dex.test_script.Nodes import Expect, FileLabels, Value, Where, Then
|
||||
|
||||
|
||||
def is_subpath(subpath: str, superpath: str) -> bool:
|
||||
|
|
@ -61,6 +61,7 @@ class WhereMatchResult:
|
|||
|
||||
frame_idx: int
|
||||
active_expects: List[Value] = field(default_factory=list)
|
||||
active_thens: List[Then] = field(default_factory=list)
|
||||
pending_wheres: List[Where] = field(default_factory=list)
|
||||
|
||||
|
||||
|
|
@ -118,6 +119,17 @@ def get_active_where_matches(
|
|||
):
|
||||
active_where_expects[scope.where].active_expects.append(expect)
|
||||
|
||||
script.visit_script(visit_where=get_active_wheres, visit_expect=get_active_expects)
|
||||
def get_active_thens(then: Then, scope: Scope):
|
||||
if (
|
||||
scope.where in active_where_expects
|
||||
and active_where_expects[scope.where].frame_idx == 0
|
||||
):
|
||||
active_where_expects[scope.where].active_thens.append(then)
|
||||
|
||||
script.visit_script(
|
||||
visit_where=get_active_wheres,
|
||||
visit_expect=get_active_expects,
|
||||
visit_then=get_active_thens,
|
||||
)
|
||||
|
||||
return active_where_expects
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ def setup_yaml_parser(loader):
|
|||
Value,
|
||||
DexRange,
|
||||
Label,
|
||||
Then,
|
||||
]
|
||||
for c in reg_classes:
|
||||
c.register_yaml(loader)
|
||||
|
|
@ -192,6 +193,45 @@ class Value(Expect):
|
|||
yaml.add_representer(Value, Value.representer)
|
||||
|
||||
|
||||
##############
|
||||
## Execution Nodes: Can appear as leaf nodes directly under a state node to perform debugger actions when they become
|
||||
## active, to advance the debugger state.
|
||||
|
||||
|
||||
class Then:
|
||||
"""Used to perform actions, such as finishing the test or continuing. Will trigger as soon as it becomes active, and
|
||||
intends to advance debugger state, so this must be used directly under a state node, e.g.:
|
||||
`!where {line: 4}: !then finish`
|
||||
"""
|
||||
|
||||
def __init__(self, command: str):
|
||||
self.command = command
|
||||
if not self.is_valid():
|
||||
raise DexterNodeError(self, f'Invalid !then command "{self.command}"')
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
valid_commands = ["finish", "step_out"]
|
||||
if self.command not in valid_commands:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return f"Then({self.command})"
|
||||
|
||||
@staticmethod
|
||||
def constructor(loader, node):
|
||||
return Then(loader.construct_scalar(node))
|
||||
|
||||
@staticmethod
|
||||
def representer(dumper, data):
|
||||
return dumper.represent_scalar("!then", data.command)
|
||||
|
||||
@staticmethod
|
||||
def register_yaml(loader):
|
||||
yaml.add_constructor("!then", Then.constructor, loader)
|
||||
yaml.add_representer(Then, Then.representer)
|
||||
|
||||
|
||||
##############
|
||||
## Utility Nodes: Can be used anywhere in a script as a form of syntactic sugar.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from dex.test_script.Nodes import (
|
|||
Expect,
|
||||
FileLabels,
|
||||
Where,
|
||||
Then,
|
||||
setup_yaml_parser,
|
||||
)
|
||||
|
||||
|
|
@ -176,7 +177,10 @@ class DexterScript:
|
|||
if result := do(visit_where, key, scope):
|
||||
return result
|
||||
new_scope = scope.add_where(key)
|
||||
if result := self._visit_script(
|
||||
if isinstance(value, Then):
|
||||
if result := do(visit_then, value, new_scope):
|
||||
return result
|
||||
elif result := self._visit_script(
|
||||
value, new_scope, visit_where, visit_expect, visit_then
|
||||
):
|
||||
return result
|
||||
|
|
@ -191,6 +195,7 @@ class DexterScript:
|
|||
self,
|
||||
visit_where: Optional[Callable[[Where, Scope], Any]] = None,
|
||||
visit_expect: Optional[Callable[[Expect, Any, Scope], Any]] = None,
|
||||
visit_then: Optional[Callable[[Then, Scope], Any]] = None,
|
||||
) -> Any:
|
||||
"""Visits all nodes in the script in pre-order traversal, calling any non-none provided visitor functions for
|
||||
each respective node type. Note that we do not visit expected values independently of their associated expect;
|
||||
|
|
@ -199,7 +204,7 @@ class DexterScript:
|
|||
If any visit function returns a truthy value, traversal will early-exit and this function returns that value;
|
||||
otherwise, this function returns None."""
|
||||
return self._visit_script(
|
||||
self.script_obj, self.root_scope, visit_where, visit_expect
|
||||
self.script_obj, self.root_scope, visit_where, visit_expect, visit_then
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
// RUN: %dexter_regression_test_cxx_build %s -o %t
|
||||
// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t \
|
||||
// RUN: -- %s | FileCheck %s
|
||||
|
||||
/// Test that when we use !then finish, we finish the entire test immediately,
|
||||
/// without observing any more steps afterwards.
|
||||
|
||||
void fizz() {}
|
||||
void buzz() {}
|
||||
void fizzbuzz() {}
|
||||
|
||||
void doFizzbuzz(int N) {
|
||||
// CHECK: then_finish.cpp([[# @LINE + 1 ]]:12)
|
||||
for (int I = 1; I < N; ++I) {
|
||||
// CHECK-COUNT-15: then_finish.cpp([[# @LINE + 1 ]]:9)
|
||||
if (I % 3 == 0) { // !dex_label loop_top
|
||||
if (I % 5 == 0)
|
||||
// CHECK: then_finish.cpp([[# @LINE + 1 ]]:9)
|
||||
fizzbuzz(); // !dex_label fizzbuzz
|
||||
else
|
||||
fizz();
|
||||
} else if (I % 5 == 0) {
|
||||
buzz();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// We'll see main in "Frame 1" at the same step that we exit from; we should
|
||||
/// not see it (or doFizzbuzz) afterwards.
|
||||
// CHECK: Frame 1:
|
||||
// CHECK-NEXT: main
|
||||
// CHECK-NOT: main
|
||||
// CHECK-NOT: doFizzbuzz
|
||||
|
||||
int main() {
|
||||
int V = 0;
|
||||
doFizzbuzz(30); // !dex_label main_start
|
||||
V = 1;
|
||||
return 0; // !dex_label main_end
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
!where {function: main}:
|
||||
!where {function: doFizzbuzz}:
|
||||
!and {lines: !label loop_top}:
|
||||
!value I: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||
!and {lines: !label fizzbuzz}: !then finish
|
||||
!and {lines: !range [!label main_start, !label main_end]}:
|
||||
!value V: [0, 1]
|
||||
!where {lines: !label main_end}:
|
||||
!value V: 1
|
||||
...
|
||||
*/
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// RUN: %dexter_regression_test_cxx_build %s -o %t
|
||||
// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t \
|
||||
// RUN: -- %s | FileCheck %s
|
||||
|
||||
/// Test that when we use !then step_out, we jump out of the current frame, but
|
||||
/// continue stepping through the frame above.
|
||||
/// The function `doFizzbuzz` loops N-1 times before exiting, but in the 15th
|
||||
/// iteration we hit `!then step_out`. This test looks at the debug output from
|
||||
/// Dexter's stepping, and checks that we step on the first `if` in the loop
|
||||
/// 15 times, then we step on the `fizzbuzz` line once, and then we see no more
|
||||
/// steps inside `doFizzbuzz` afterwards, and finally we step on the last line
|
||||
/// in `main`, verifying that Dexter has stepped out of the function.
|
||||
|
||||
void fizz() {}
|
||||
void buzz() {}
|
||||
void fizzbuzz() {}
|
||||
|
||||
void doFizzbuzz(int N) {
|
||||
// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:12)
|
||||
for (int I = 1; I < N; ++I) {
|
||||
// CHECK-COUNT-15: then_step_out.cpp([[# @LINE + 1 ]]:9)
|
||||
if (I % 3 == 0) { // !dex_label loop_top
|
||||
if (I % 5 == 0)
|
||||
// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:9)
|
||||
fizzbuzz(); // !dex_label fizzbuzz
|
||||
else
|
||||
fizz();
|
||||
} else if (I % 5 == 0) {
|
||||
buzz();
|
||||
}
|
||||
}
|
||||
}
|
||||
// CHECK-NOT: doFizzbuzz
|
||||
|
||||
int main() {
|
||||
int V = 0;
|
||||
doFizzbuzz(30); // !dex_label main_start
|
||||
V = 1;
|
||||
// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:3)
|
||||
return 0; // !dex_label main_end
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
!where {function: main}:
|
||||
!where {function: doFizzbuzz}:
|
||||
!and {lines: !label loop_top}:
|
||||
!value I: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||
!and {lines: !label fizzbuzz}: !then step_out
|
||||
!and {lines: !range [!label main_start, !label main_end]}:
|
||||
!value V: [0, 1]
|
||||
...
|
||||
*/
|
||||
|
|
@ -22,3 +22,9 @@ CHECK-NEXT: Script starting line [[# @LINE + 1 ]]
|
|||
!where {function: foo}:
|
||||
- !value x: 5
|
||||
...
|
||||
|
||||
CHECK-NEXT: Script starting line [[# @LINE + 1 ]]
|
||||
---
|
||||
# CHECK-NEXT: Invalid !then command "bar"
|
||||
!where {function: foo}: !then bar
|
||||
...
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ CHECK: ? !where {function: foo}
|
|||
CHECK-NEXT: : !value 'x': 5
|
||||
CHECK-NEXT: ? !where {file: lib.cpp, lines: !range [10, 20]}
|
||||
CHECK-NEXT: : !value 'y': 10
|
||||
CHECK-NEXT: ? !and {lines: 14}
|
||||
CHECK-NEXT: : !then 'finish'
|
||||
CHECK-NEXT: ? !where {lines: 5}
|
||||
CHECK-NEXT: : ? !and {lines: 5}
|
||||
CHECK-NEXT: : !value 'y': 7
|
||||
|
|
@ -17,6 +19,7 @@ CHECK-NEXT: !value 'z': bees
|
|||
!value x: 5
|
||||
!where {file: "lib.cpp", lines: !range [10, 20]}:
|
||||
!value y: 10
|
||||
!and {lines: 14}: !then finish
|
||||
!where {lines: 5}:
|
||||
!and {lines: 5}:
|
||||
!value y: 7
|
||||
|
|
|
|||
Loading…
Reference in a new issue