11364750dSJames Henderson# DExTer : Debugging Experience Tester
21364750dSJames Henderson# ~~~~~~   ~         ~~         ~   ~~
31364750dSJames Henderson#
41364750dSJames Henderson# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
51364750dSJames Henderson# See https://llvm.org/LICENSE.txt for license information.
61364750dSJames Henderson# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
71364750dSJames Henderson"""Conditional Controller Class for DExTer.-"""
81364750dSJames Henderson
91364750dSJames Henderson
101364750dSJames Hendersonimport os
111364750dSJames Hendersonimport time
121364750dSJames Hendersonfrom collections import defaultdict
131364750dSJames Hendersonfrom itertools import chain
141364750dSJames Henderson
151364750dSJames Hendersonfrom dex.debugger.DebuggerControllers.ControllerHelpers import in_source_file, update_step_watches
161364750dSJames Hendersonfrom dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase
171364750dSJames Hendersonfrom dex.debugger.DebuggerBase import DebuggerBase
181364750dSJames Hendersonfrom dex.utils.Exceptions import DebuggerException
191364750dSJames Henderson
201364750dSJames Henderson
211364750dSJames Hendersonclass BreakpointRange:
221364750dSJames Henderson    """A range of breakpoints and a set of conditions.
231364750dSJames Henderson
241364750dSJames Henderson    The leading breakpoint (on line `range_from`) is always active.
251364750dSJames Henderson
261364750dSJames Henderson    When the leading breakpoint is hit the trailing range should be activated
271364750dSJames Henderson    when `expression` evaluates to any value in `values`. If there are no
281364750dSJames Henderson    conditions (`expression` is None) then the trailing breakpoint range should
291364750dSJames Henderson    always be activated upon hitting the leading breakpoint.
301364750dSJames Henderson
311364750dSJames Henderson    Args:
321364750dSJames Henderson       expression: None for no conditions, or a str expression to compare
331364750dSJames Henderson       against `values`.
341364750dSJames Henderson
351364750dSJames Henderson       hit_count: None for no limit, or int to set the number of times the
361364750dSJames Henderson                  leading breakpoint is triggered before it is removed.
371364750dSJames Henderson    """
381364750dSJames Henderson
391364750dSJames Henderson    def __init__(self, expression: str, path: str, range_from: int, range_to: int,
406cf69179SStephen Tozer                 values: list, hit_count: int, finish_on_remove: bool):
411364750dSJames Henderson        self.expression = expression
421364750dSJames Henderson        self.path = path
431364750dSJames Henderson        self.range_from = range_from
441364750dSJames Henderson        self.range_to = range_to
451364750dSJames Henderson        self.conditional_values = values
461364750dSJames Henderson        self.max_hit_count = hit_count
471364750dSJames Henderson        self.current_hit_count = 0
486cf69179SStephen Tozer        self.finish_on_remove = finish_on_remove
491364750dSJames Henderson
501364750dSJames Henderson    def has_conditions(self):
511364750dSJames Henderson        return self.expression != None
521364750dSJames Henderson
531364750dSJames Henderson    def get_conditional_expression_list(self):
541364750dSJames Henderson        conditional_list = []
551364750dSJames Henderson        for value in self.conditional_values:
561364750dSJames Henderson            # (<expression>) == (<value>)
571364750dSJames Henderson            conditional_expression = '({}) == ({})'.format(self.expression, value)
581364750dSJames Henderson            conditional_list.append(conditional_expression)
591364750dSJames Henderson        return conditional_list
601364750dSJames Henderson
611364750dSJames Henderson    def add_hit(self):
621364750dSJames Henderson        self.current_hit_count += 1
631364750dSJames Henderson
641364750dSJames Henderson    def should_be_removed(self):
651364750dSJames Henderson        if self.max_hit_count == None:
661364750dSJames Henderson            return False
671364750dSJames Henderson        return self.current_hit_count >= self.max_hit_count
681364750dSJames Henderson
691364750dSJames Henderson
701364750dSJames Hendersonclass ConditionalController(DebuggerControllerBase):
711364750dSJames Henderson    def __init__(self, context, step_collection):
721364750dSJames Henderson      self._bp_ranges = None
731364750dSJames Henderson      self._watches = set()
741364750dSJames Henderson      self._step_index = 0
751364750dSJames Henderson      self._pause_between_steps = context.options.pause_between_steps
761364750dSJames Henderson      self._max_steps = context.options.max_steps
771364750dSJames Henderson      # Map {id: BreakpointRange}
781364750dSJames Henderson      self._leading_bp_handles = {}
793a094d8bSJeremy Morse      super(ConditionalController, self).__init__(context, step_collection)
803a094d8bSJeremy Morse      self._build_bp_ranges()
811364750dSJames Henderson
821364750dSJames Henderson    def _build_bp_ranges(self):
831364750dSJames Henderson        commands = self.step_collection.commands
841364750dSJames Henderson        self._bp_ranges = []
851364750dSJames Henderson        try:
861364750dSJames Henderson            limit_commands = commands['DexLimitSteps']
871364750dSJames Henderson            for lc in limit_commands:
881364750dSJames Henderson                bpr = BreakpointRange(
891364750dSJames Henderson                  lc.expression,
901364750dSJames Henderson                  lc.path,
911364750dSJames Henderson                  lc.from_line,
921364750dSJames Henderson                  lc.to_line,
931364750dSJames Henderson                  lc.values,
946cf69179SStephen Tozer                  lc.hit_count,
956cf69179SStephen Tozer                  False)
961364750dSJames Henderson                self._bp_ranges.append(bpr)
971364750dSJames Henderson        except KeyError:
981364750dSJames Henderson            raise DebuggerException('Missing DexLimitSteps commands, cannot conditionally step.')
996cf69179SStephen Tozer        if 'DexFinishTest' in commands:
1006cf69179SStephen Tozer            finish_commands = commands['DexFinishTest']
1016cf69179SStephen Tozer            for ic in finish_commands:
1026cf69179SStephen Tozer                bpr = BreakpointRange(
1036cf69179SStephen Tozer                  ic.expression,
1046cf69179SStephen Tozer                  ic.path,
1056cf69179SStephen Tozer                  ic.on_line,
1066cf69179SStephen Tozer                  ic.on_line,
1076cf69179SStephen Tozer                  ic.values,
1086cf69179SStephen Tozer                  ic.hit_count + 1,
1096cf69179SStephen Tozer                  True)
1106cf69179SStephen Tozer                self._bp_ranges.append(bpr)
1111364750dSJames Henderson
1121364750dSJames Henderson    def _set_leading_bps(self):
1131364750dSJames Henderson        # Set a leading breakpoint for each BreakpointRange, building a
1141364750dSJames Henderson        # map of {leading bp id: BreakpointRange}.
1151364750dSJames Henderson        for bpr in self._bp_ranges:
1161364750dSJames Henderson            if bpr.has_conditions():
1171364750dSJames Henderson                # Add a conditional breakpoint for each condition.
1181364750dSJames Henderson                for cond_expr in bpr.get_conditional_expression_list():
1191364750dSJames Henderson                    id = self.debugger.add_conditional_breakpoint(bpr.path,
1201364750dSJames Henderson                                                                  bpr.range_from,
1211364750dSJames Henderson                                                                  cond_expr)
1221364750dSJames Henderson                    self._leading_bp_handles[id] = bpr
1231364750dSJames Henderson            else:
1241364750dSJames Henderson                # Add an unconditional breakpoint.
1251364750dSJames Henderson                id = self.debugger.add_breakpoint(bpr.path, bpr.range_from)
1261364750dSJames Henderson                self._leading_bp_handles[id] = bpr
1271364750dSJames Henderson
1283a094d8bSJeremy Morse    def _run_debugger_custom(self, cmdline):
1291364750dSJames Henderson        # TODO: Add conditional and unconditional breakpoint support to dbgeng.
1301364750dSJames Henderson        if self.debugger.get_name() == 'dbgeng':
1311364750dSJames Henderson            raise DebuggerException('DexLimitSteps commands are not supported by dbgeng')
1321364750dSJames Henderson
1331364750dSJames Henderson        self.step_collection.clear_steps()
1341364750dSJames Henderson        self._set_leading_bps()
1351364750dSJames Henderson
1361364750dSJames Henderson        for command_obj in chain.from_iterable(self.step_collection.commands.values()):
1371364750dSJames Henderson            self._watches.update(command_obj.get_watches())
1381364750dSJames Henderson
1393a094d8bSJeremy Morse        self.debugger.launch(cmdline)
1401364750dSJames Henderson        time.sleep(self._pause_between_steps)
1416cf69179SStephen Tozer
1426cf69179SStephen Tozer        exit_desired = False
1436cf69179SStephen Tozer
1441364750dSJames Henderson        while not self.debugger.is_finished:
1451364750dSJames Henderson            while self.debugger.is_running:
1461364750dSJames Henderson                pass
1471364750dSJames Henderson
1481364750dSJames Henderson            step_info = self.debugger.get_step_info(self._watches, self._step_index)
1491364750dSJames Henderson            if step_info.current_frame:
1501364750dSJames Henderson                self._step_index += 1
1511364750dSJames Henderson                update_step_watches(step_info, self._watches, self.step_collection.commands)
1521364750dSJames Henderson                self.step_collection.new_step(self.context, step_info)
1531364750dSJames Henderson
1541364750dSJames Henderson            bp_to_delete = []
1551364750dSJames Henderson            for bp_id in self.debugger.get_triggered_breakpoint_ids():
1561364750dSJames Henderson                try:
1571364750dSJames Henderson                    # See if this is one of our leading breakpoints.
1581364750dSJames Henderson                    bpr = self._leading_bp_handles[bp_id]
1591364750dSJames Henderson                except KeyError:
1601364750dSJames Henderson                    # This is a trailing bp. Mark it for removal.
1611364750dSJames Henderson                    bp_to_delete.append(bp_id)
1621364750dSJames Henderson                    continue
1631364750dSJames Henderson
1641364750dSJames Henderson                bpr.add_hit()
1651364750dSJames Henderson                if bpr.should_be_removed():
1666cf69179SStephen Tozer                    if bpr.finish_on_remove:
1676cf69179SStephen Tozer                        exit_desired = True
1681364750dSJames Henderson                    bp_to_delete.append(bp_id)
1691364750dSJames Henderson                    del self._leading_bp_handles[bp_id]
1701364750dSJames Henderson                # Add a range of trailing breakpoints covering the lines
1711364750dSJames Henderson                # requested in the DexLimitSteps command. Ignore first line as
1721364750dSJames Henderson                # that's covered by the leading bp we just hit and include the
1731364750dSJames Henderson                # final line.
1741364750dSJames Henderson                for line in range(bpr.range_from + 1, bpr.range_to + 1):
1751364750dSJames Henderson                    self.debugger.add_breakpoint(bpr.path, line)
1761364750dSJames Henderson
1771364750dSJames Henderson            # Remove any trailing or expired leading breakpoints we just hit.
178*b3f14802Sgbtozers            self.debugger.delete_breakpoints(bp_to_delete)
1791364750dSJames Henderson
1806cf69179SStephen Tozer            if exit_desired:
1816cf69179SStephen Tozer                break
1821364750dSJames Henderson            self.debugger.go()
1831364750dSJames Henderson            time.sleep(self._pause_between_steps)
184