diff --git a/d.diff b/d.diff new file mode 100644 index 00000000000000..ffa4caadfb881e --- /dev/null +++ b/d.diff @@ -0,0 +1,712 @@ +diff --git a/debuginfo-tests/dexter/Commands.md b/debuginfo-tests/dexter/Commands.md +index c30a0d7214c..2e2fecfed92 100644 +--- a/debuginfo-tests/dexter/Commands.md ++++ b/debuginfo-tests/dexter/Commands.md +@@ -173,6 +173,34 @@ Expect the source line this is found on will never be stepped on to. + [TODO] + + ++---- ++## DexLimitSteps ++ DexLimitSteps(expr, *values [, **from_line=1],[,**to_line=Max] ++ [,**on_line]) ++ ++ Args: ++ expr (str): variable or value to compare. ++ ++ Arg list: ++ values (str): At least one potential value the expr may evaluate to. ++ ++ Keyword args: ++ from_line (int): Define the start of the limited step range. ++ to_line (int): Define the end of the limited step range. ++ on_line (int): Define a range with length 1 starting and ending on the ++ same line. ++ ++### Description ++Define a limited stepping range that is predicated on a condition. When ++'(expr) == (values[n])', set a range of temporary, unconditional break points within ++the test file defined by the range from_line and to_line or on_line. ++ ++The condition is only evaluated on the line 'from_line' or 'on_line'. If the ++condition is not true at the start of the range, the whole range is ignored. ++ ++DexLimitSteps commands are useful for reducing the amount of steps gathered in ++large test cases that would normally take much longer to complete. ++ + ---- + ## DexLabel + DexLabel(name) +diff --git a/debuginfo-tests/dexter/dex/command/ParseCommand.py b/debuginfo-tests/dexter/dex/command/ParseCommand.py +index 4cc9ae12592..8246ea9e3cf 100644 +--- a/debuginfo-tests/dexter/dex/command/ParseCommand.py ++++ b/debuginfo-tests/dexter/dex/command/ParseCommand.py +@@ -24,6 +24,7 @@ from dex.command.commands.DexExpectStepOrder import DexExpectStepOrder + from dex.command.commands.DexExpectWatchType import DexExpectWatchType + from dex.command.commands.DexExpectWatchValue import DexExpectWatchValue + from dex.command.commands.DexLabel import DexLabel ++from dex.command.commands.DexLimitSteps import DexLimitSteps + from dex.command.commands.DexUnreachable import DexUnreachable + from dex.command.commands.DexWatch import DexWatch + from dex.utils import Timer +@@ -42,6 +43,7 @@ def _get_valid_commands(): + DexExpectWatchType.get_name() : DexExpectWatchType, + DexExpectWatchValue.get_name() : DexExpectWatchValue, + DexLabel.get_name() : DexLabel, ++ DexLimitSteps.get_name() : DexLimitSteps, + DexUnreachable.get_name() : DexUnreachable, + DexWatch.get_name() : DexWatch + } +diff --git a/debuginfo-tests/dexter/dex/command/commands/DexLimitSteps.py b/debuginfo-tests/dexter/dex/command/commands/DexLimitSteps.py +new file mode 100644 +index 00000000000..d66401b5599 +--- /dev/null ++++ b/debuginfo-tests/dexter/dex/command/commands/DexLimitSteps.py +@@ -0,0 +1,54 @@ ++# DExTer : Debugging Experience Tester ++# ~~~~~~ ~ ~~ ~ ~~ ++# ++# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. ++# See https://llvm.org/LICENSE.txt for license information. ++# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ++"""A Command that enables test writers to specify a limited number of break ++points using an start condition and range. ++""" ++ ++from dex.command.CommandBase import CommandBase ++ ++class DexLimitSteps(CommandBase): ++ def __init__(self, *args, **kwargs): ++ self.expression = args[0] ++ self.values = [str(arg) for arg in args[1:]] ++ try: ++ on_line = kwargs.pop('on_line') ++ self.from_line = on_line ++ self.to_line = on_line ++ except KeyError: ++ self.from_line = kwargs.pop('from_line', 1) ++ self.to_line = kwargs.pop('to_line', 999999) ++ if kwargs: ++ raise TypeError('unexpected named args: {}'.format( ++ ', '.join(kwargs))) ++ super(DexLimitSteps, self).__init__() ++ ++ def resolve_label(self, label_line_pair): ++ label, lineno = label_line_pair ++ if isinstance(self.from_line, str): ++ if self.from_line == label: ++ self.from_line = lineno ++ if isinstance(self.to_line, str): ++ if self.to_line == label: ++ self.to_line = lineno ++ ++ def has_labels(self): ++ return len(self.get_label_args()) > 0 ++ ++ def get_label_args(self): ++ return [label for label in (self.from_line, self.to_line) ++ if isinstance(label, str)] ++ ++ def eval(self): ++ raise NotImplementedError('DexLimitSteps commands cannot be evaled.') ++ ++ @staticmethod ++ def get_name(): ++ return __class__.__name__ ++ ++ @staticmethod ++ def get_subcommands() -> dict: ++ return None +diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py +index 2261396b94b..12f4f4ab7a0 100644 +--- a/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py ++++ b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py +@@ -120,6 +120,14 @@ class DebuggerBase(object, metaclass=abc.ABCMeta): + def add_breakpoint(self, file_, line): + pass + ++ @abc.abstractmethod ++ def add_conditional_breakpoint(self, file_, line, condition): ++ pass ++ ++ @abc.abstractmethod ++ def delete_conditional_breakpoint(self, file_, line, condition): ++ pass ++ + @abc.abstractmethod + def launch(self): + pass +diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py +new file mode 100644 +index 00000000000..4e4327b53f8 +--- /dev/null ++++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py +@@ -0,0 +1,127 @@ ++# DExTer : Debugging Experience Tester ++# ~~~~~~ ~ ~~ ~ ~~ ++# ++# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. ++# See https://llvm.org/LICENSE.txt for license information. ++# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ++"""Conditional Controller Class for DExTer.-""" ++ ++ ++import os ++import time ++from collections import defaultdict ++from itertools import chain ++ ++from dex.debugger.DebuggerControllers.ControllerHelpers import in_source_file, update_step_watches ++from dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase ++from dex.debugger.DebuggerBase import DebuggerBase ++from dex.utils.Exceptions import DebuggerException ++ ++ ++class ConditionalBpRange: ++ """Represents a conditional range of breakpoints within a source file descending from ++ one line to another.""" ++ ++ def __init__(self, expression: str, path: str, range_from: int, range_to: int, values: list): ++ self.expression = expression ++ self.path = path ++ self.range_from = range_from ++ self.range_to = range_to ++ self.conditional_values = values ++ ++ def get_conditional_expression_list(self): ++ conditional_list = [] ++ for value in self.conditional_values: ++ # () == () ++ conditional_expression = '({}) == ({})'.format(self.expression, value) ++ conditional_list.append(conditional_expression) ++ return conditional_list ++ ++ ++class ConditionalController(DebuggerControllerBase): ++ def __init__(self, context, step_collection): ++ self.context = context ++ self.step_collection = step_collection ++ self._conditional_bps = None ++ self._watches = set() ++ self._step_index = 0 ++ self._build_conditional_bps() ++ self._path_and_line_to_conditional_bp = defaultdict(list) ++ self._pause_between_steps = context.options.pause_between_steps ++ self._max_steps = context.options.max_steps ++ ++ def _build_conditional_bps(self): ++ commands = self.step_collection.commands ++ self._conditional_bps = [] ++ try: ++ limit_commands = commands['DexLimitSteps'] ++ for lc in limit_commands: ++ conditional_bp = ConditionalBpRange( ++ lc.expression, ++ lc.path, ++ lc.from_line, ++ lc.to_line, ++ lc.values) ++ self._conditional_bps.append(conditional_bp) ++ except KeyError: ++ raise DebuggerException('Missing DexLimitSteps commands, cannot conditionally step.') ++ ++ def _set_conditional_bps(self): ++ # When we break in the debugger we need a quick and easy way to look up ++ # which conditional bp we've breaked on. ++ for cbp in self._conditional_bps: ++ conditional_bp_list = self._path_and_line_to_conditional_bp[(cbp.path, cbp.range_from)] ++ conditional_bp_list.append(cbp) ++ ++ # Set break points only on the first line of any conditional range, we'll set ++ # more break points for a range when the condition is satisfied. ++ for cbp in self._conditional_bps: ++ for cond_expr in cbp.get_conditional_expression_list(): ++ self.debugger.add_conditional_breakpoint(cbp.path, cbp.range_from, cond_expr) ++ ++ def _conditional_met(self, cbp): ++ for cond_expr in cbp.get_conditional_expression_list(): ++ valueIR = self.debugger.evaluate_expression(cond_expr) ++ if valueIR.type_name == 'bool' and valueIR.value == 'true': ++ return True ++ return False ++ ++ def _run_debugger_custom(self): ++ # TODO: Add conditional and unconditional breakpoint support to dbgeng. ++ if self.debugger.get_name() == 'dbgeng': ++ raise DebuggerException('DexLimitSteps commands are not supported by dbgeng') ++ ++ self.step_collection.clear_steps() ++ self._set_conditional_bps() ++ ++ for command_obj in chain.from_iterable(self.step_collection.commands.values()): ++ self._watches.update(command_obj.get_watches()) ++ ++ self.debugger.launch() ++ time.sleep(self._pause_between_steps) ++ while not self.debugger.is_finished: ++ while self.debugger.is_running: ++ pass ++ ++ step_info = self.debugger.get_step_info(self._watches, self._step_index) ++ if step_info.current_frame: ++ self._step_index += 1 ++ update_step_watches(step_info, self._watches, self.step_collection.commands) ++ self.step_collection.new_step(self.context, step_info) ++ ++ loc = step_info.current_location ++ conditional_bp_key = (loc.path, loc.lineno) ++ if conditional_bp_key in self._path_and_line_to_conditional_bp: ++ ++ conditional_bps = self._path_and_line_to_conditional_bp[conditional_bp_key] ++ for cbp in conditional_bps: ++ if self._conditional_met(cbp): ++ # Unconditional range should ignore first line as that's the ++ # conditional bp we just hit and should be inclusive of final line ++ for line in range(cbp.range_from + 1, cbp.range_to + 1): ++ self.debugger.add_conditional_breakpoint(cbp.path, line, condition='') ++ ++ # Clear any uncondtional break points at this loc. ++ self.debugger.delete_conditional_breakpoint(file_=loc.path, line=loc.lineno, condition='') ++ self.debugger.go() ++ time.sleep(self._pause_between_steps) +diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ControllerHelpers.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ControllerHelpers.py +new file mode 100644 +index 00000000000..adac7674aff +--- /dev/null ++++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ControllerHelpers.py +@@ -0,0 +1,37 @@ ++# DExTer : Debugging Experience Tester ++# ~~~~~~ ~ ~~ ~ ~~ ++# ++# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. ++# See https://llvm.org/LICENSE.txt for license information. ++# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ++ ++import os ++from itertools import chain ++ ++def in_source_file(source_files, step_info): ++ if not step_info.current_frame: ++ return False ++ if not step_info.current_location.path: ++ return False ++ if not os.path.exists(step_info.current_location.path): ++ return False ++ return any(os.path.samefile(step_info.current_location.path, f) \ ++ for f in source_files) ++ ++def update_step_watches(step_info, watches, commands): ++ watch_cmds = ['DexUnreachable', 'DexExpectStepOrder'] ++ towatch = chain.from_iterable(commands[x] ++ for x in watch_cmds ++ if x in commands) ++ try: ++ # Iterate over all watches of the types named in watch_cmds ++ for watch in towatch: ++ loc = step_info.current_location ++ if (os.path.exists(loc.path) ++ and os.path.samefile(watch.path, loc.path) ++ and watch.lineno == loc.lineno): ++ result = watch.eval(step_info) ++ step_info.watches.update(result) ++ break ++ except KeyError: ++ pass +diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py +index ff98baa2d0e..87b13fc7f3a 100644 +--- a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py ++++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py +@@ -4,7 +4,7 @@ + # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. + # See https://llvm.org/LICENSE.txt for license information. + # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +-"""Default class for controlling debuggers.""" ++"""Abstract Base class for controlling debuggers.""" + + import abc + +diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py +index 0077a19e601..c41a3eff0d3 100644 +--- a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py ++++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py +@@ -4,61 +4,37 @@ + # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. + # See https://llvm.org/LICENSE.txt for license information. + # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +-"""Base class for controlling debuggers.""" ++"""Default class for controlling debuggers.""" + + from itertools import chain + import os + import time + + from dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase +-from dex.utils.Exceptions import DebuggerException ++from dex.debugger.DebuggerControllers.ControllerHelpers import in_source_file, update_step_watches ++from dex.utils.Exceptions import DebuggerException, LoadDebuggerException + + class DefaultController(DebuggerControllerBase): + def __init__(self, context, step_collection): + self.context = context + self.step_collection = step_collection ++ self.source_files = self.context.options.source_files + self.watches = set() + self.step_index = 0 + +- def _update_step_watches(self, step_info): +- watch_cmds = ['DexUnreachable', 'DexExpectStepOrder'] +- towatch = chain.from_iterable(self.step_collection.commands[x] +- for x in watch_cmds +- if x in self.step_collection.commands) +- try: +- # Iterate over all watches of the types named in watch_cmds +- for watch in towatch: +- loc = step_info.current_location +- if (os.path.exists(loc.path) +- and os.path.samefile(watch.path, loc.path) +- and watch.lineno == loc.lineno): +- result = watch.eval(step_info) +- step_info.watches.update(result) +- break +- except KeyError: +- pass +- + def _break_point_all_lines(self): + for s in self.context.options.source_files: + with open(s, 'r') as fp: + num_lines = len(fp.readlines()) + for line in range(1, num_lines + 1): +- self.debugger.add_breakpoint(s, line) +- +- def _in_source_file(self, step_info): +- if not step_info.current_frame: +- return False +- if not step_info.current_location.path: +- return False +- if not os.path.exists(step_info.current_location.path): +- return False +- return any(os.path.samefile(step_info.current_location.path, f) \ +- for f in self.context.options.source_files) ++ try: ++ self.debugger.add_breakpoint(s, line) ++ except DebuggerException: ++ raise LoadDebuggerException(DebuggerException.msg) + + def _run_debugger_custom(self): + self.step_collection.debugger = self.debugger.debugger_info + self._break_point_all_lines() +- + self.debugger.launch() + + for command_obj in chain.from_iterable(self.step_collection.commands.values()): +@@ -76,10 +52,10 @@ class DefaultController(DebuggerControllerBase): + step_info = self.debugger.get_step_info(self.watches, self.step_index) + + if step_info.current_frame: +- self._update_step_watches(step_info) ++ update_step_watches(step_info, self.watches, self.step_collection.commands) + self.step_collection.new_step(self.context, step_info) + +- if self._in_source_file(step_info): ++ if in_source_file(self.source_files, step_info): + self.debugger.step() + else: + self.debugger.go() +diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py +index 0afc748aecb..d812fd974f7 100644 +--- a/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py ++++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py +@@ -77,11 +77,21 @@ class DbgEng(DebuggerBase): + self.client.Control.RemoveBreakpoint(x) + + def add_breakpoint(self, file_, line): +- # This is something to implement in the future -- as it stands, Dexter +- # doesn't test for such things as "I can set a breakpoint on this line". +- # This is only called AFAICT right now to ensure we break on every step. ++ # Breakpoint setting/deleting is not supported by dbgeng at this moment ++ # but is something that should be considered in the future. ++ # TODO: this method is called in the DefaultController but has no effect. + pass + ++ def add_conditional_breakpoint(self, file_, line, condition): ++ # breakpoint setting/deleting is not supported by dbgeng at this moment ++ # but is something that should be considered in the future. ++ raise NotImplementedError('add_conditional_breakpoint is not yet implemented by dbgeng') ++ ++ def delete_conditional_breakpoint(self, file_, line, condition): ++ # breakpoint setting/deleting is not supported by dbgeng at this moment ++ # but is something that should be considered in the future. ++ raise NotImplementedError('delete_conditional_breakpoint is not yet implemented by dbgeng') ++ + def launch(self): + # We are, by this point, already launched. + self.step_info = probe_process.probe_state(self.client) +diff --git a/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py +index a943431c888..c7bb74681d9 100644 +--- a/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py ++++ b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py +@@ -105,9 +105,48 @@ class LLDB(DebuggerBase): + + def add_breakpoint(self, file_, line): + if not self._target.BreakpointCreateByLocation(file_, line): +- raise LoadDebuggerException( ++ raise DebuggerException( + 'could not add breakpoint [{}:{}]'.format(file_, line)) + ++ def add_conditional_breakpoint(self, file_, line, condition): ++ bp = self._target.BreakpointCreateByLocation(file_, line) ++ if bp: ++ bp.SetCondition(condition) ++ else: ++ raise DebuggerException( ++ 'could not add breakpoint [{}:{}]'.format(file_, line)) ++ ++ def delete_conditional_breakpoint(self, file_, line, condition): ++ bp_count = self._target.GetNumBreakpoints() ++ bps = [self._target.GetBreakpointAtIndex(ix) for ix in range(0, bp_count)] ++ ++ for bp in bps: ++ bp_cond = bp.GetCondition() ++ bp_cond = bp_cond if bp_cond is not None else '' ++ ++ if bp_cond != condition: ++ continue ++ ++ # If one of the bound bp locations for this bp is bound to the same ++ # line in file_ above, then delete the entire parent bp and all ++ # bp locs. ++ # https://lldb.llvm.org/python_reference/lldb.SBBreakpoint-class.html ++ for breakpoint_location in bp: ++ sb_address = breakpoint_location.GetAddress() ++ ++ sb_line_entry = sb_address.GetLineEntry() ++ bl_line = sb_line_entry.GetLine() ++ ++ sb_file_entry = sb_line_entry.GetFileSpec() ++ bl_dir = sb_file_entry.GetDirectory() ++ bl_file_name = sb_file_entry.GetFilename() ++ ++ bl_file_path = os.path.join(bl_dir, bl_file_name) ++ ++ if bl_file_path == file_ and bl_line == line: ++ self._target.BreakpointDelete(bp.GetID()) ++ break ++ + def launch(self): + self._process = self._target.LaunchSimple(None, None, os.getcwd()) + if not self._process or self._process.GetNumThreads() == 0: +diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py +index b9816f84f72..40a902bd205 100644 +--- a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py ++++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py +@@ -82,6 +82,9 @@ class VisualStudio(DebuggerBase, metaclass=abc.ABCMeta): # pylint: disable=abst + + @property + def _location(self): ++ #TODO: Find a better way of determining path, line and column info ++ # that doesn't require reading break points. This method requires ++ # all lines to have a break point on them. + bp = self._debugger.BreakpointLastHit + return { + 'path': getattr(bp, 'File', None), +@@ -111,8 +114,20 @@ class VisualStudio(DebuggerBase, metaclass=abc.ABCMeta): # pylint: disable=abst + def add_breakpoint(self, file_, line): + self._debugger.Breakpoints.Add('', file_, line) + ++ def add_conditional_breakpoint(self, file_, line, condition): ++ column = 1 ++ self._debugger.Breakpoints.Add('', file_, line, column, condition) ++ ++ def delete_conditional_breakpoint(self, file_, line, condition): ++ for bp in self._debugger.Breakpoints: ++ for bound_bp in bp.Children: ++ if (bound_bp.File == file_ and bound_bp.FileLine == line and ++ bound_bp.Condition == condition): ++ bp.Delete() ++ break ++ + def launch(self): +- self.step() ++ self._fn_go() + + def step(self): + self._fn_step() +diff --git a/debuginfo-tests/dexter/dex/tools/test/Tool.py b/debuginfo-tests/dexter/dex/tools/test/Tool.py +index a615c8cad90..43191fd44bd 100644 +--- a/debuginfo-tests/dexter/dex/tools/test/Tool.py ++++ b/debuginfo-tests/dexter/dex/tools/test/Tool.py +@@ -16,6 +16,7 @@ from dex.builder import run_external_build_script + from dex.command.ParseCommand import get_command_infos + from dex.debugger.Debuggers import run_debugger_subprocess + from dex.debugger.DebuggerControllers.DefaultController import DefaultController ++from dex.debugger.DebuggerControllers.ConditionalController import ConditionalController + from dex.dextIR.DextIR import DextIR + from dex.heuristic import Heuristic + from dex.tools import TestToolBase +@@ -136,9 +137,15 @@ class Tool(TestToolBase): + executable_path=self.context.options.executable, + source_paths=self.context.options.source_files, + dexter_version=self.context.version) ++ + step_collection.commands = get_command_infos( + self.context.options.source_files) +- debugger_controller = DefaultController(self.context, step_collection) ++ ++ if 'DexLimitSteps' in step_collection.commands: ++ debugger_controller = ConditionalController(self.context, step_collection) ++ else: ++ debugger_controller = DefaultController(self.context, step_collection) ++ + return debugger_controller + + def _get_steps(self, builderIR): +diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_check_json_step_count.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_check_json_step_count.cpp +new file mode 100644 +index 00000000000..45683fced2d +--- /dev/null ++++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_check_json_step_count.cpp +@@ -0,0 +1,20 @@ ++// Purpose: ++// Check number of step lines are correctly reported in json output. ++// ++// REQUIRES: system-linux ++// ++// RUN: %dexter_regression_test --verbose -- %s | FileCheck %s ++// CHECK: limit_steps_check_json_step_count.cpp ++// CHECK: ## BEGIN ## ++// CHECK-COUNT-3: json_step_count.cpp", ++ ++int main() { ++ int result = 0; ++ for(int ix = 0; ix != 10; ++ix) { ++ int index = ix; ++ result += index; // DexLabel('check') ++ } ++} ++ ++// DexExpectWatchValue('index', 2, 7, 9, on_line='check') ++// DexLimitSteps('ix', 2, 7, 9, on_line='check') +diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_loop.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_loop.cpp +new file mode 100644 +index 00000000000..5946fa6ba46 +--- /dev/null ++++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_loop.cpp +@@ -0,0 +1,20 @@ ++// Purpose: ++// Check the DexLimit steps only gathers step info for 2 iterations of a ++// for loop. ++// ++// REQUIRES: system-linux ++// ++// RUN: %dexter_regression_test -- %s | FileCheck %s ++// CHECK: limit_steps_expect_loop.cpp: ++ ++int main(const int argc, const char * argv[]) { ++ unsigned int sum = 1; ++ for(unsigned int ix = 0; ix != 5; ++ix) { ++ unsigned thing_to_add = ix + ix - ix; // DexLabel('start') ++ sum += ix; // DexLabel('end') ++ } ++ return sum; ++} ++ ++// DexLimitSteps('ix', 0, 3, from_line='start', to_line='end') ++// DexExpectWatchValue('ix', 0, 3, from_line='start', to_line='end') +diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_value.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_value.cpp +new file mode 100644 +index 00000000000..2715e28d66b +--- /dev/null ++++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_value.cpp +@@ -0,0 +1,18 @@ ++// Purpose: ++// Ensure that limited stepping breaks for all expected values. ++// ++// REQUIRES: system-linux ++// ++// RUN: %dexter_regression_test -- %s | FileCheck %s ++// CHECK: limit_steps_expect_value.cpp ++ ++int main() { ++ int i = 0; ++ i = 1; // DexLabel('from') ++ i = 2; ++ i = 3; ++ return 0; // DexLabel('long_range') ++} ++ ++// DexLimitSteps('i', '0', from_line='from', to_line='long_range') ++// DexExpectWatchValue('i', 0, 1, 2, 3, from_line='from', to_line='long_range') +diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_overlapping_ranges.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_overlapping_ranges.cpp +new file mode 100644 +index 00000000000..3200fe0979b +--- /dev/null ++++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_overlapping_ranges.cpp +@@ -0,0 +1,36 @@ ++// Purpose: ++// Ensure that multiple overlapping \DexLimitSteps ranges do not interfere. ++// ++// REQUIRES: system-linux ++// ++// RUN: %dexter_regression_test -- %s | FileCheck %s ++// CHECK: limit_steps_overlapping_ranges.cpp ++ ++int main() { ++ int val1; ++ int val2; ++ int placeholder; ++ for (int ix = 0; ix != 10; ++ix) { ++ placeholder=val1+val2; // DexLabel('from') ++ if (ix == 0) { ++ val1 = ix; ++ val2 = ix; // DexLabel('val1_check') ++ placeholder=val1+val2; // DexLabel('val1_check_to') ++ } ++ else if (ix == 2) { ++ val2 = ix; ++ val1 = ix; // DexLabel('val2_check') ++ placeholder=val1+val2; // DexLabel('val2_check_to') ++ } ++ placeholder=val1+val2; // DexLabel('to') ++ } ++ return val1 + val2; ++} ++ ++// DexExpectWatchValue('ix', 0, 2, 5, from_line='from', to_line='to') ++// DexExpectWatchValue('val1', 0, from_line='val1_check', to_line='val1_check_to') ++// DexExpectWatchValue('val2', 2, from_line='val2_check', to_line='val2_check_to') ++ ++// DexLimitSteps('ix', 5, from_line='from', to_line='to') ++// DexLimitSteps('val1', 0, from_line='val1_check', to_line='val1_check_to') ++// DexLimitSteps('val2', 2, from_line='val2_check', to_line='val2_check_to') +diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_same_line_conditional.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_same_line_conditional.cpp +new file mode 100644 +index 00000000000..060ff0d5fe7 +--- /dev/null ++++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_same_line_conditional.cpp +@@ -0,0 +1,26 @@ ++// Purpose: ++// Test that LimitStep commands can exist on the same from line. ++// ++// REQUIRES: system-linux ++// ++// RUN: %dexter_regression_test -- %s | FileCheck %s ++// CHECK: limit_steps_same_line_conditional.cpp ++ ++int main() { ++ int val1 = 0; ++ ++ int placeholder; ++ for(int ix = 0; ix != 4; ++ix) { ++ val1 = ix; ++ placeholder = ix; // DexLabel('from') ++ placeholder = ix; ++ val1 += 2; // DexLabel('to') ++ placeholder = ix; // DexLabel('extended_to') ++ } ++ return val1 + placeholder; ++} ++ ++// DexExpectWatchValue('val1', 0, 1, 3, from_line='from', to_line='extended_to') ++ ++// DexLimitSteps('ix', 0, from_line='from', to_line='to') ++// DexLimitSteps('ix', 1, from_line='from', to_line='extended_to') diff --git a/debuginfo-tests/dexter/Commands.md b/debuginfo-tests/dexter/Commands.md index c30a0d7214c916..2e2fecfed929a7 100644 --- a/debuginfo-tests/dexter/Commands.md +++ b/debuginfo-tests/dexter/Commands.md @@ -173,6 +173,34 @@ Expect the source line this is found on will never be stepped on to. [TODO] +---- +## DexLimitSteps + DexLimitSteps(expr, *values [, **from_line=1],[,**to_line=Max] + [,**on_line]) + + Args: + expr (str): variable or value to compare. + + Arg list: + values (str): At least one potential value the expr may evaluate to. + + Keyword args: + from_line (int): Define the start of the limited step range. + to_line (int): Define the end of the limited step range. + on_line (int): Define a range with length 1 starting and ending on the + same line. + +### Description +Define a limited stepping range that is predicated on a condition. When +'(expr) == (values[n])', set a range of temporary, unconditional break points within +the test file defined by the range from_line and to_line or on_line. + +The condition is only evaluated on the line 'from_line' or 'on_line'. If the +condition is not true at the start of the range, the whole range is ignored. + +DexLimitSteps commands are useful for reducing the amount of steps gathered in +large test cases that would normally take much longer to complete. + ---- ## DexLabel DexLabel(name) diff --git a/debuginfo-tests/dexter/dex/command/ParseCommand.py b/debuginfo-tests/dexter/dex/command/ParseCommand.py index 4cc9ae125920de..8246ea9e3cf975 100644 --- a/debuginfo-tests/dexter/dex/command/ParseCommand.py +++ b/debuginfo-tests/dexter/dex/command/ParseCommand.py @@ -24,6 +24,7 @@ from dex.command.commands.DexExpectWatchType import DexExpectWatchType from dex.command.commands.DexExpectWatchValue import DexExpectWatchValue from dex.command.commands.DexLabel import DexLabel +from dex.command.commands.DexLimitSteps import DexLimitSteps from dex.command.commands.DexUnreachable import DexUnreachable from dex.command.commands.DexWatch import DexWatch from dex.utils import Timer @@ -42,6 +43,7 @@ def _get_valid_commands(): DexExpectWatchType.get_name() : DexExpectWatchType, DexExpectWatchValue.get_name() : DexExpectWatchValue, DexLabel.get_name() : DexLabel, + DexLimitSteps.get_name() : DexLimitSteps, DexUnreachable.get_name() : DexUnreachable, DexWatch.get_name() : DexWatch } diff --git a/debuginfo-tests/dexter/dex/command/commands/DexLimitSteps.py b/debuginfo-tests/dexter/dex/command/commands/DexLimitSteps.py new file mode 100644 index 00000000000000..d66401b55995ba --- /dev/null +++ b/debuginfo-tests/dexter/dex/command/commands/DexLimitSteps.py @@ -0,0 +1,54 @@ +# DExTer : Debugging Experience Tester +# ~~~~~~ ~ ~~ ~ ~~ +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""A Command that enables test writers to specify a limited number of break +points using an start condition and range. +""" + +from dex.command.CommandBase import CommandBase + +class DexLimitSteps(CommandBase): + def __init__(self, *args, **kwargs): + self.expression = args[0] + self.values = [str(arg) for arg in args[1:]] + try: + on_line = kwargs.pop('on_line') + self.from_line = on_line + self.to_line = on_line + except KeyError: + self.from_line = kwargs.pop('from_line', 1) + self.to_line = kwargs.pop('to_line', 999999) + if kwargs: + raise TypeError('unexpected named args: {}'.format( + ', '.join(kwargs))) + super(DexLimitSteps, self).__init__() + + def resolve_label(self, label_line_pair): + label, lineno = label_line_pair + if isinstance(self.from_line, str): + if self.from_line == label: + self.from_line = lineno + if isinstance(self.to_line, str): + if self.to_line == label: + self.to_line = lineno + + def has_labels(self): + return len(self.get_label_args()) > 0 + + def get_label_args(self): + return [label for label in (self.from_line, self.to_line) + if isinstance(label, str)] + + def eval(self): + raise NotImplementedError('DexLimitSteps commands cannot be evaled.') + + @staticmethod + def get_name(): + return __class__.__name__ + + @staticmethod + def get_subcommands() -> dict: + return None diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py index 2261396b94b4ae..12f4f4ab7a0259 100644 --- a/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py @@ -120,6 +120,14 @@ def clear_breakpoints(self): def add_breakpoint(self, file_, line): pass + @abc.abstractmethod + def add_conditional_breakpoint(self, file_, line, condition): + pass + + @abc.abstractmethod + def delete_conditional_breakpoint(self, file_, line, condition): + pass + @abc.abstractmethod def launch(self): pass diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py new file mode 100644 index 00000000000000..4e4327b53f826b --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py @@ -0,0 +1,127 @@ +# DExTer : Debugging Experience Tester +# ~~~~~~ ~ ~~ ~ ~~ +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Conditional Controller Class for DExTer.-""" + + +import os +import time +from collections import defaultdict +from itertools import chain + +from dex.debugger.DebuggerControllers.ControllerHelpers import in_source_file, update_step_watches +from dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase +from dex.debugger.DebuggerBase import DebuggerBase +from dex.utils.Exceptions import DebuggerException + + +class ConditionalBpRange: + """Represents a conditional range of breakpoints within a source file descending from + one line to another.""" + + def __init__(self, expression: str, path: str, range_from: int, range_to: int, values: list): + self.expression = expression + self.path = path + self.range_from = range_from + self.range_to = range_to + self.conditional_values = values + + def get_conditional_expression_list(self): + conditional_list = [] + for value in self.conditional_values: + # () == () + conditional_expression = '({}) == ({})'.format(self.expression, value) + conditional_list.append(conditional_expression) + return conditional_list + + +class ConditionalController(DebuggerControllerBase): + def __init__(self, context, step_collection): + self.context = context + self.step_collection = step_collection + self._conditional_bps = None + self._watches = set() + self._step_index = 0 + self._build_conditional_bps() + self._path_and_line_to_conditional_bp = defaultdict(list) + self._pause_between_steps = context.options.pause_between_steps + self._max_steps = context.options.max_steps + + def _build_conditional_bps(self): + commands = self.step_collection.commands + self._conditional_bps = [] + try: + limit_commands = commands['DexLimitSteps'] + for lc in limit_commands: + conditional_bp = ConditionalBpRange( + lc.expression, + lc.path, + lc.from_line, + lc.to_line, + lc.values) + self._conditional_bps.append(conditional_bp) + except KeyError: + raise DebuggerException('Missing DexLimitSteps commands, cannot conditionally step.') + + def _set_conditional_bps(self): + # When we break in the debugger we need a quick and easy way to look up + # which conditional bp we've breaked on. + for cbp in self._conditional_bps: + conditional_bp_list = self._path_and_line_to_conditional_bp[(cbp.path, cbp.range_from)] + conditional_bp_list.append(cbp) + + # Set break points only on the first line of any conditional range, we'll set + # more break points for a range when the condition is satisfied. + for cbp in self._conditional_bps: + for cond_expr in cbp.get_conditional_expression_list(): + self.debugger.add_conditional_breakpoint(cbp.path, cbp.range_from, cond_expr) + + def _conditional_met(self, cbp): + for cond_expr in cbp.get_conditional_expression_list(): + valueIR = self.debugger.evaluate_expression(cond_expr) + if valueIR.type_name == 'bool' and valueIR.value == 'true': + return True + return False + + def _run_debugger_custom(self): + # TODO: Add conditional and unconditional breakpoint support to dbgeng. + if self.debugger.get_name() == 'dbgeng': + raise DebuggerException('DexLimitSteps commands are not supported by dbgeng') + + self.step_collection.clear_steps() + self._set_conditional_bps() + + for command_obj in chain.from_iterable(self.step_collection.commands.values()): + self._watches.update(command_obj.get_watches()) + + self.debugger.launch() + time.sleep(self._pause_between_steps) + while not self.debugger.is_finished: + while self.debugger.is_running: + pass + + step_info = self.debugger.get_step_info(self._watches, self._step_index) + if step_info.current_frame: + self._step_index += 1 + update_step_watches(step_info, self._watches, self.step_collection.commands) + self.step_collection.new_step(self.context, step_info) + + loc = step_info.current_location + conditional_bp_key = (loc.path, loc.lineno) + if conditional_bp_key in self._path_and_line_to_conditional_bp: + + conditional_bps = self._path_and_line_to_conditional_bp[conditional_bp_key] + for cbp in conditional_bps: + if self._conditional_met(cbp): + # Unconditional range should ignore first line as that's the + # conditional bp we just hit and should be inclusive of final line + for line in range(cbp.range_from + 1, cbp.range_to + 1): + self.debugger.add_conditional_breakpoint(cbp.path, line, condition='') + + # Clear any uncondtional break points at this loc. + self.debugger.delete_conditional_breakpoint(file_=loc.path, line=loc.lineno, condition='') + self.debugger.go() + time.sleep(self._pause_between_steps) diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ControllerHelpers.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ControllerHelpers.py new file mode 100644 index 00000000000000..adac7674aff6ef --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ControllerHelpers.py @@ -0,0 +1,37 @@ +# DExTer : Debugging Experience Tester +# ~~~~~~ ~ ~~ ~ ~~ +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import os +from itertools import chain + +def in_source_file(source_files, step_info): + if not step_info.current_frame: + return False + if not step_info.current_location.path: + return False + if not os.path.exists(step_info.current_location.path): + return False + return any(os.path.samefile(step_info.current_location.path, f) \ + for f in source_files) + +def update_step_watches(step_info, watches, commands): + watch_cmds = ['DexUnreachable', 'DexExpectStepOrder'] + towatch = chain.from_iterable(commands[x] + for x in watch_cmds + if x in commands) + try: + # Iterate over all watches of the types named in watch_cmds + for watch in towatch: + loc = step_info.current_location + if (os.path.exists(loc.path) + and os.path.samefile(watch.path, loc.path) + and watch.lineno == loc.lineno): + result = watch.eval(step_info) + step_info.watches.update(result) + break + except KeyError: + pass diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py index ff98baa2d0e21f..87b13fc7f3a877 100644 --- a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py @@ -4,7 +4,7 @@ # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -"""Default class for controlling debuggers.""" +"""Abstract Base class for controlling debuggers.""" import abc diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py index 0077a19e601a67..c41a3eff0d3ab4 100644 --- a/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py @@ -4,61 +4,37 @@ # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -"""Base class for controlling debuggers.""" +"""Default class for controlling debuggers.""" from itertools import chain import os import time from dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase -from dex.utils.Exceptions import DebuggerException +from dex.debugger.DebuggerControllers.ControllerHelpers import in_source_file, update_step_watches +from dex.utils.Exceptions import DebuggerException, LoadDebuggerException class DefaultController(DebuggerControllerBase): def __init__(self, context, step_collection): self.context = context self.step_collection = step_collection + self.source_files = self.context.options.source_files self.watches = set() self.step_index = 0 - def _update_step_watches(self, step_info): - watch_cmds = ['DexUnreachable', 'DexExpectStepOrder'] - towatch = chain.from_iterable(self.step_collection.commands[x] - for x in watch_cmds - if x in self.step_collection.commands) - try: - # Iterate over all watches of the types named in watch_cmds - for watch in towatch: - loc = step_info.current_location - if (os.path.exists(loc.path) - and os.path.samefile(watch.path, loc.path) - and watch.lineno == loc.lineno): - result = watch.eval(step_info) - step_info.watches.update(result) - break - except KeyError: - pass - def _break_point_all_lines(self): for s in self.context.options.source_files: with open(s, 'r') as fp: num_lines = len(fp.readlines()) for line in range(1, num_lines + 1): - self.debugger.add_breakpoint(s, line) - - def _in_source_file(self, step_info): - if not step_info.current_frame: - return False - if not step_info.current_location.path: - return False - if not os.path.exists(step_info.current_location.path): - return False - return any(os.path.samefile(step_info.current_location.path, f) \ - for f in self.context.options.source_files) + try: + self.debugger.add_breakpoint(s, line) + except DebuggerException: + raise LoadDebuggerException(DebuggerException.msg) def _run_debugger_custom(self): self.step_collection.debugger = self.debugger.debugger_info self._break_point_all_lines() - self.debugger.launch() for command_obj in chain.from_iterable(self.step_collection.commands.values()): @@ -76,10 +52,10 @@ def _run_debugger_custom(self): step_info = self.debugger.get_step_info(self.watches, self.step_index) if step_info.current_frame: - self._update_step_watches(step_info) + update_step_watches(step_info, self.watches, self.step_collection.commands) self.step_collection.new_step(self.context, step_info) - if self._in_source_file(step_info): + if in_source_file(self.source_files, step_info): self.debugger.step() else: self.debugger.go() diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py index 0afc748aecb1cf..d812fd974f79d8 100644 --- a/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py @@ -77,11 +77,21 @@ def clear_breakpoints(self): self.client.Control.RemoveBreakpoint(x) def add_breakpoint(self, file_, line): - # This is something to implement in the future -- as it stands, Dexter - # doesn't test for such things as "I can set a breakpoint on this line". - # This is only called AFAICT right now to ensure we break on every step. + # Breakpoint setting/deleting is not supported by dbgeng at this moment + # but is something that should be considered in the future. + # TODO: this method is called in the DefaultController but has no effect. pass + def add_conditional_breakpoint(self, file_, line, condition): + # breakpoint setting/deleting is not supported by dbgeng at this moment + # but is something that should be considered in the future. + raise NotImplementedError('add_conditional_breakpoint is not yet implemented by dbgeng') + + def delete_conditional_breakpoint(self, file_, line, condition): + # breakpoint setting/deleting is not supported by dbgeng at this moment + # but is something that should be considered in the future. + raise NotImplementedError('delete_conditional_breakpoint is not yet implemented by dbgeng') + def launch(self): # We are, by this point, already launched. self.step_info = probe_process.probe_state(self.client) diff --git a/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py index a943431c888770..c7bb74681d9fd5 100644 --- a/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py +++ b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py @@ -105,9 +105,48 @@ def clear_breakpoints(self): def add_breakpoint(self, file_, line): if not self._target.BreakpointCreateByLocation(file_, line): - raise LoadDebuggerException( + raise DebuggerException( 'could not add breakpoint [{}:{}]'.format(file_, line)) + def add_conditional_breakpoint(self, file_, line, condition): + bp = self._target.BreakpointCreateByLocation(file_, line) + if bp: + bp.SetCondition(condition) + else: + raise DebuggerException( + 'could not add breakpoint [{}:{}]'.format(file_, line)) + + def delete_conditional_breakpoint(self, file_, line, condition): + bp_count = self._target.GetNumBreakpoints() + bps = [self._target.GetBreakpointAtIndex(ix) for ix in range(0, bp_count)] + + for bp in bps: + bp_cond = bp.GetCondition() + bp_cond = bp_cond if bp_cond is not None else '' + + if bp_cond != condition: + continue + + # If one of the bound bp locations for this bp is bound to the same + # line in file_ above, then delete the entire parent bp and all + # bp locs. + # https://lldb.llvm.org/python_reference/lldb.SBBreakpoint-class.html + for breakpoint_location in bp: + sb_address = breakpoint_location.GetAddress() + + sb_line_entry = sb_address.GetLineEntry() + bl_line = sb_line_entry.GetLine() + + sb_file_entry = sb_line_entry.GetFileSpec() + bl_dir = sb_file_entry.GetDirectory() + bl_file_name = sb_file_entry.GetFilename() + + bl_file_path = os.path.join(bl_dir, bl_file_name) + + if bl_file_path == file_ and bl_line == line: + self._target.BreakpointDelete(bp.GetID()) + break + def launch(self): self._process = self._target.LaunchSimple(None, None, os.getcwd()) if not self._process or self._process.GetNumThreads() == 0: diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py index b9816f84f72386..40a902bd205991 100644 --- a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py @@ -82,6 +82,9 @@ def _dte_version(self): @property def _location(self): + #TODO: Find a better way of determining path, line and column info + # that doesn't require reading break points. This method requires + # all lines to have a break point on them. bp = self._debugger.BreakpointLastHit return { 'path': getattr(bp, 'File', None), @@ -111,8 +114,20 @@ def clear_breakpoints(self): def add_breakpoint(self, file_, line): self._debugger.Breakpoints.Add('', file_, line) + def add_conditional_breakpoint(self, file_, line, condition): + column = 1 + self._debugger.Breakpoints.Add('', file_, line, column, condition) + + def delete_conditional_breakpoint(self, file_, line, condition): + for bp in self._debugger.Breakpoints: + for bound_bp in bp.Children: + if (bound_bp.File == file_ and bound_bp.FileLine == line and + bound_bp.Condition == condition): + bp.Delete() + break + def launch(self): - self.step() + self._fn_go() def step(self): self._fn_step() diff --git a/debuginfo-tests/dexter/dex/tools/test/Tool.py b/debuginfo-tests/dexter/dex/tools/test/Tool.py index a615c8cad90c38..43191fd44bd5ef 100644 --- a/debuginfo-tests/dexter/dex/tools/test/Tool.py +++ b/debuginfo-tests/dexter/dex/tools/test/Tool.py @@ -16,6 +16,7 @@ from dex.command.ParseCommand import get_command_infos from dex.debugger.Debuggers import run_debugger_subprocess from dex.debugger.DebuggerControllers.DefaultController import DefaultController +from dex.debugger.DebuggerControllers.ConditionalController import ConditionalController from dex.dextIR.DextIR import DextIR from dex.heuristic import Heuristic from dex.tools import TestToolBase @@ -136,9 +137,15 @@ def _init_debugger_controller(self): executable_path=self.context.options.executable, source_paths=self.context.options.source_files, dexter_version=self.context.version) + step_collection.commands = get_command_infos( self.context.options.source_files) - debugger_controller = DefaultController(self.context, step_collection) + + if 'DexLimitSteps' in step_collection.commands: + debugger_controller = ConditionalController(self.context, step_collection) + else: + debugger_controller = DefaultController(self.context, step_collection) + return debugger_controller def _get_steps(self, builderIR): diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_check_json_step_count.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_check_json_step_count.cpp new file mode 100644 index 00000000000000..45683fced2d0b4 --- /dev/null +++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_check_json_step_count.cpp @@ -0,0 +1,20 @@ +// Purpose: +// Check number of step lines are correctly reported in json output. +// +// REQUIRES: system-linux +// +// RUN: %dexter_regression_test --verbose -- %s | FileCheck %s +// CHECK: limit_steps_check_json_step_count.cpp +// CHECK: ## BEGIN ## +// CHECK-COUNT-3: json_step_count.cpp", + +int main() { + int result = 0; + for(int ix = 0; ix != 10; ++ix) { + int index = ix; + result += index; // DexLabel('check') + } +} + +// DexExpectWatchValue('index', 2, 7, 9, on_line='check') +// DexLimitSteps('ix', 2, 7, 9, on_line='check') diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_loop.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_loop.cpp new file mode 100644 index 00000000000000..5946fa6ba46eb4 --- /dev/null +++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_loop.cpp @@ -0,0 +1,20 @@ +// Purpose: +// Check the DexLimit steps only gathers step info for 2 iterations of a +// for loop. +// +// REQUIRES: system-linux +// +// RUN: %dexter_regression_test -- %s | FileCheck %s +// CHECK: limit_steps_expect_loop.cpp: + +int main(const int argc, const char * argv[]) { + unsigned int sum = 1; + for(unsigned int ix = 0; ix != 5; ++ix) { + unsigned thing_to_add = ix + ix - ix; // DexLabel('start') + sum += ix; // DexLabel('end') + } + return sum; +} + +// DexLimitSteps('ix', 0, 3, from_line='start', to_line='end') +// DexExpectWatchValue('ix', 0, 3, from_line='start', to_line='end') diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_value.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_value.cpp new file mode 100644 index 00000000000000..2715e28d66bf4d --- /dev/null +++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_value.cpp @@ -0,0 +1,18 @@ +// Purpose: +// Ensure that limited stepping breaks for all expected values. +// +// REQUIRES: system-linux +// +// RUN: %dexter_regression_test -- %s | FileCheck %s +// CHECK: limit_steps_expect_value.cpp + +int main() { + int i = 0; + i = 1; // DexLabel('from') + i = 2; + i = 3; + return 0; // DexLabel('long_range') +} + +// DexLimitSteps('i', '0', from_line='from', to_line='long_range') +// DexExpectWatchValue('i', 0, 1, 2, 3, from_line='from', to_line='long_range') diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_overlapping_ranges.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_overlapping_ranges.cpp new file mode 100644 index 00000000000000..3200fe0979b3a1 --- /dev/null +++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_overlapping_ranges.cpp @@ -0,0 +1,36 @@ +// Purpose: +// Ensure that multiple overlapping \DexLimitSteps ranges do not interfere. +// +// REQUIRES: system-linux +// +// RUN: %dexter_regression_test -- %s | FileCheck %s +// CHECK: limit_steps_overlapping_ranges.cpp + +int main() { + int val1; + int val2; + int placeholder; + for (int ix = 0; ix != 10; ++ix) { + placeholder=val1+val2; // DexLabel('from') + if (ix == 0) { + val1 = ix; + val2 = ix; // DexLabel('val1_check') + placeholder=val1+val2; // DexLabel('val1_check_to') + } + else if (ix == 2) { + val2 = ix; + val1 = ix; // DexLabel('val2_check') + placeholder=val1+val2; // DexLabel('val2_check_to') + } + placeholder=val1+val2; // DexLabel('to') + } + return val1 + val2; +} + +// DexExpectWatchValue('ix', 0, 2, 5, from_line='from', to_line='to') +// DexExpectWatchValue('val1', 0, from_line='val1_check', to_line='val1_check_to') +// DexExpectWatchValue('val2', 2, from_line='val2_check', to_line='val2_check_to') + +// DexLimitSteps('ix', 5, from_line='from', to_line='to') +// DexLimitSteps('val1', 0, from_line='val1_check', to_line='val1_check_to') +// DexLimitSteps('val2', 2, from_line='val2_check', to_line='val2_check_to') diff --git a/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_same_line_conditional.cpp b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_same_line_conditional.cpp new file mode 100644 index 00000000000000..060ff0d5fe7063 --- /dev/null +++ b/debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_same_line_conditional.cpp @@ -0,0 +1,26 @@ +// Purpose: +// Test that LimitStep commands can exist on the same from line. +// +// REQUIRES: system-linux +// +// RUN: %dexter_regression_test -- %s | FileCheck %s +// CHECK: limit_steps_same_line_conditional.cpp + +int main() { + int val1 = 0; + + int placeholder; + for(int ix = 0; ix != 4; ++ix) { + val1 = ix; + placeholder = ix; // DexLabel('from') + placeholder = ix; + val1 += 2; // DexLabel('to') + placeholder = ix; // DexLabel('extended_to') + } + return val1 + placeholder; +} + +// DexExpectWatchValue('val1', 0, 1, 3, from_line='from', to_line='extended_to') + +// DexLimitSteps('ix', 0, from_line='from', to_line='to') +// DexLimitSteps('ix', 1, from_line='from', to_line='extended_to')