[Dexter] Add DexLimitSteps command and ConditionalController
authorTom Weaver <Tom.Weaver@Sony.com>
Tue, 2 Jun 2020 15:19:43 +0000 (16:19 +0100)
committerTom Weaver <Tom.Weaver@Sony.com>
Tue, 2 Jun 2020 15:19:43 +0000 (16:19 +0100)
  * Adds DexLimitSteps Command.
  * Add ConditionalController, a new DebuggerController type.
  * 5 regression tests
  * documentation

  Reviewers: jmorse

  Differential Revision: https://reviews.llvm.org/D79786

18 files changed:
d.diff [new file with mode: 0644]
debuginfo-tests/dexter/Commands.md
debuginfo-tests/dexter/dex/command/ParseCommand.py
debuginfo-tests/dexter/dex/command/commands/DexLimitSteps.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/DebuggerBase.py
debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ControllerHelpers.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py
debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py
debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py
debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py
debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
debuginfo-tests/dexter/dex/tools/test/Tool.py
debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_check_json_step_count.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_loop.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_expect_value.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_overlapping_ranges.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/limit_steps/limit_steps_same_line_conditional.cpp [new file with mode: 0644]

diff --git a/d.diff b/d.diff
new file mode 100644 (file)
index 0000000..ffa4caa
--- /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:
++            # (<expression>) == (<value>)
++            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')
index c30a0d7..2e2fecf 100644 (file)
@@ -174,6 +174,34 @@ Expect the source line this is found on will never be stepped on to.
 
 
 ----
+## 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)
 
index 4cc9ae1..8246ea9 100644 (file)
@@ -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 (file)
index 0000000..d66401b
--- /dev/null
@@ -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
index 2261396..12f4f4a 100644 (file)
@@ -121,6 +121,14 @@ class DebuggerBase(object, metaclass=abc.ABCMeta):
         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 (file)
index 0000000..4e4327b
--- /dev/null
@@ -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:
+            # (<expression>) == (<value>)
+            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 (file)
index 0000000..adac767
--- /dev/null
@@ -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
index ff98baa..87b13fc 100644 (file)
@@ -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
 
index 0077a19..c41a3ef 100644 (file)
@@ -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()
index 0afc748..d812fd9 100644 (file)
@@ -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)
index a943431..c7bb746 100644 (file)
@@ -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:
index b9816f8..40a902b 100644 (file)
@@ -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()
index a615c8c..43191fd 100644 (file)
@@ -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 (file)
index 0000000..45683fc
--- /dev/null
@@ -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 (file)
index 0000000..5946fa6
--- /dev/null
@@ -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 (file)
index 0000000..2715e28
--- /dev/null
@@ -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 (file)
index 0000000..3200fe0
--- /dev/null
@@ -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 (file)
index 0000000..060ff0d
--- /dev/null
@@ -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')