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
15f98ee40fSTobias Hietafrom dex.debugger.DebuggerControllers.ControllerHelpers import (
16f98ee40fSTobias Hieta    in_source_file,
17f98ee40fSTobias Hieta    update_step_watches,
18f98ee40fSTobias Hieta)
19f98ee40fSTobias Hietafrom dex.debugger.DebuggerControllers.DebuggerControllerBase import (
20f98ee40fSTobias Hieta    DebuggerControllerBase,
21f98ee40fSTobias Hieta)
221364750dSJames Hendersonfrom dex.debugger.DebuggerBase import DebuggerBase
231364750dSJames Hendersonfrom dex.utils.Exceptions import DebuggerException
24ee5617dcSStephen Tozerfrom dex.utils.Timeout import Timeout
251364750dSJames Henderson
261364750dSJames Henderson
271364750dSJames Hendersonclass BreakpointRange:
281364750dSJames Henderson    """A range of breakpoints and a set of conditions.
291364750dSJames Henderson
301364750dSJames Henderson    The leading breakpoint (on line `range_from`) is always active.
311364750dSJames Henderson
321364750dSJames Henderson    When the leading breakpoint is hit the trailing range should be activated
331364750dSJames Henderson    when `expression` evaluates to any value in `values`. If there are no
341364750dSJames Henderson    conditions (`expression` is None) then the trailing breakpoint range should
351364750dSJames Henderson    always be activated upon hitting the leading breakpoint.
361364750dSJames Henderson
371364750dSJames Henderson    Args:
381364750dSJames Henderson       expression: None for no conditions, or a str expression to compare
391364750dSJames Henderson       against `values`.
401364750dSJames Henderson
411364750dSJames Henderson       hit_count: None for no limit, or int to set the number of times the
421364750dSJames Henderson                  leading breakpoint is triggered before it is removed.
431364750dSJames Henderson    """
441364750dSJames Henderson
45f98ee40fSTobias Hieta    def __init__(
46f98ee40fSTobias Hieta        self,
47f98ee40fSTobias Hieta        expression: str,
48f98ee40fSTobias Hieta        path: str,
49f98ee40fSTobias Hieta        range_from: int,
50f98ee40fSTobias Hieta        range_to: int,
51f98ee40fSTobias Hieta        values: list,
52f98ee40fSTobias Hieta        hit_count: int,
53f98ee40fSTobias Hieta        finish_on_remove: bool,
54f98ee40fSTobias Hieta    ):
551364750dSJames Henderson        self.expression = expression
561364750dSJames Henderson        self.path = path
571364750dSJames Henderson        self.range_from = range_from
581364750dSJames Henderson        self.range_to = range_to
591364750dSJames Henderson        self.conditional_values = values
601364750dSJames Henderson        self.max_hit_count = hit_count
611364750dSJames Henderson        self.current_hit_count = 0
626cf69179SStephen Tozer        self.finish_on_remove = finish_on_remove
631364750dSJames Henderson
641364750dSJames Henderson    def has_conditions(self):
65*ca92bdfaSEisuke Kawashima        return self.expression is not None
661364750dSJames Henderson
671364750dSJames Henderson    def get_conditional_expression_list(self):
681364750dSJames Henderson        conditional_list = []
691364750dSJames Henderson        for value in self.conditional_values:
701364750dSJames Henderson            # (<expression>) == (<value>)
71f98ee40fSTobias Hieta            conditional_expression = "({}) == ({})".format(self.expression, value)
721364750dSJames Henderson            conditional_list.append(conditional_expression)
731364750dSJames Henderson        return conditional_list
741364750dSJames Henderson
751364750dSJames Henderson    def add_hit(self):
761364750dSJames Henderson        self.current_hit_count += 1
771364750dSJames Henderson
781364750dSJames Henderson    def should_be_removed(self):
79*ca92bdfaSEisuke Kawashima        if self.max_hit_count is None:
801364750dSJames Henderson            return False
811364750dSJames Henderson        return self.current_hit_count >= self.max_hit_count
821364750dSJames Henderson
831364750dSJames Henderson
841364750dSJames Hendersonclass ConditionalController(DebuggerControllerBase):
851364750dSJames Henderson    def __init__(self, context, step_collection):
861364750dSJames Henderson        self._bp_ranges = None
871364750dSJames Henderson        self._watches = set()
881364750dSJames Henderson        self._step_index = 0
891364750dSJames Henderson        self._pause_between_steps = context.options.pause_between_steps
901364750dSJames Henderson        self._max_steps = context.options.max_steps
911364750dSJames Henderson        # Map {id: BreakpointRange}
921364750dSJames Henderson        self._leading_bp_handles = {}
933a094d8bSJeremy Morse        super(ConditionalController, self).__init__(context, step_collection)
943a094d8bSJeremy Morse        self._build_bp_ranges()
951364750dSJames Henderson
961364750dSJames Henderson    def _build_bp_ranges(self):
971364750dSJames Henderson        commands = self.step_collection.commands
981364750dSJames Henderson        self._bp_ranges = []
991364750dSJames Henderson        try:
100f98ee40fSTobias Hieta            limit_commands = commands["DexLimitSteps"]
1011364750dSJames Henderson            for lc in limit_commands:
1021364750dSJames Henderson                bpr = BreakpointRange(
1031364750dSJames Henderson                    lc.expression,
1041364750dSJames Henderson                    lc.path,
1051364750dSJames Henderson                    lc.from_line,
1061364750dSJames Henderson                    lc.to_line,
1071364750dSJames Henderson                    lc.values,
1086cf69179SStephen Tozer                    lc.hit_count,
109f98ee40fSTobias Hieta                    False,
110f98ee40fSTobias Hieta                )
1111364750dSJames Henderson                self._bp_ranges.append(bpr)
1121364750dSJames Henderson        except KeyError:
113f98ee40fSTobias Hieta            raise DebuggerException(
114f98ee40fSTobias Hieta                "Missing DexLimitSteps commands, cannot conditionally step."
115f98ee40fSTobias Hieta            )
116f98ee40fSTobias Hieta        if "DexFinishTest" in commands:
117f98ee40fSTobias Hieta            finish_commands = commands["DexFinishTest"]
1186cf69179SStephen Tozer            for ic in finish_commands:
1196cf69179SStephen Tozer                bpr = BreakpointRange(
1206cf69179SStephen Tozer                    ic.expression,
1216cf69179SStephen Tozer                    ic.path,
1226cf69179SStephen Tozer                    ic.on_line,
1236cf69179SStephen Tozer                    ic.on_line,
1246cf69179SStephen Tozer                    ic.values,
1256cf69179SStephen Tozer                    ic.hit_count + 1,
126f98ee40fSTobias Hieta                    True,
127f98ee40fSTobias Hieta                )
1286cf69179SStephen Tozer                self._bp_ranges.append(bpr)
1291364750dSJames Henderson
1301364750dSJames Henderson    def _set_leading_bps(self):
1311364750dSJames Henderson        # Set a leading breakpoint for each BreakpointRange, building a
1321364750dSJames Henderson        # map of {leading bp id: BreakpointRange}.
1331364750dSJames Henderson        for bpr in self._bp_ranges:
1341364750dSJames Henderson            if bpr.has_conditions():
1351364750dSJames Henderson                # Add a conditional breakpoint for each condition.
1361364750dSJames Henderson                for cond_expr in bpr.get_conditional_expression_list():
137f98ee40fSTobias Hieta                    id = self.debugger.add_conditional_breakpoint(
138f98ee40fSTobias Hieta                        bpr.path, bpr.range_from, cond_expr
139f98ee40fSTobias Hieta                    )
1401364750dSJames Henderson                    self._leading_bp_handles[id] = bpr
1411364750dSJames Henderson            else:
1421364750dSJames Henderson                # Add an unconditional breakpoint.
1431364750dSJames Henderson                id = self.debugger.add_breakpoint(bpr.path, bpr.range_from)
1441364750dSJames Henderson                self._leading_bp_handles[id] = bpr
1451364750dSJames Henderson
1463a094d8bSJeremy Morse    def _run_debugger_custom(self, cmdline):
1471364750dSJames Henderson        # TODO: Add conditional and unconditional breakpoint support to dbgeng.
148f98ee40fSTobias Hieta        if self.debugger.get_name() == "dbgeng":
149f98ee40fSTobias Hieta            raise DebuggerException(
150f98ee40fSTobias Hieta                "DexLimitSteps commands are not supported by dbgeng"
151f98ee40fSTobias Hieta            )
1521364750dSJames Henderson
1531364750dSJames Henderson        self.step_collection.clear_steps()
1541364750dSJames Henderson        self._set_leading_bps()
1551364750dSJames Henderson
1561364750dSJames Henderson        for command_obj in chain.from_iterable(self.step_collection.commands.values()):
1571364750dSJames Henderson            self._watches.update(command_obj.get_watches())
1581364750dSJames Henderson
1593a094d8bSJeremy Morse        self.debugger.launch(cmdline)
1601364750dSJames Henderson        time.sleep(self._pause_between_steps)
1616cf69179SStephen Tozer
1626cf69179SStephen Tozer        exit_desired = False
163ee5617dcSStephen Tozer        timed_out = False
164ee5617dcSStephen Tozer        total_timeout = Timeout(self.context.options.timeout_total)
1656cf69179SStephen Tozer
1661364750dSJames Henderson        while not self.debugger.is_finished:
167ee5617dcSStephen Tozer            breakpoint_timeout = Timeout(self.context.options.timeout_breakpoint)
168ee5617dcSStephen Tozer            while self.debugger.is_running and not timed_out:
169ee5617dcSStephen Tozer                # Check to see whether we've timed out while we're waiting.
170ee5617dcSStephen Tozer                if total_timeout.timed_out():
171f98ee40fSTobias Hieta                    self.context.logger.error(
172f98ee40fSTobias Hieta                        "Debugger session has been "
173f98ee40fSTobias Hieta                        f"running for {total_timeout.elapsed}s, timeout reached!"
174f98ee40fSTobias Hieta                    )
175ee5617dcSStephen Tozer                    timed_out = True
176ee5617dcSStephen Tozer                if breakpoint_timeout.timed_out():
177f98ee40fSTobias Hieta                    self.context.logger.error(
178f98ee40fSTobias Hieta                        f"Debugger session has not "
179f98ee40fSTobias Hieta                        f"hit a breakpoint for {breakpoint_timeout.elapsed}s, timeout "
180f98ee40fSTobias Hieta                        "reached!"
181f98ee40fSTobias Hieta                    )
182ee5617dcSStephen Tozer                    timed_out = True
183ee5617dcSStephen Tozer
184ee5617dcSStephen Tozer            if timed_out:
185ee5617dcSStephen Tozer                break
1861364750dSJames Henderson
1871364750dSJames Henderson            step_info = self.debugger.get_step_info(self._watches, self._step_index)
1881364750dSJames Henderson            if step_info.current_frame:
1891364750dSJames Henderson                self._step_index += 1
190f98ee40fSTobias Hieta                update_step_watches(
191f98ee40fSTobias Hieta                    step_info, self._watches, self.step_collection.commands
192f98ee40fSTobias Hieta                )
1931364750dSJames Henderson                self.step_collection.new_step(self.context, step_info)
1941364750dSJames Henderson
1951364750dSJames Henderson            bp_to_delete = []
1961364750dSJames Henderson            for bp_id in self.debugger.get_triggered_breakpoint_ids():
1971364750dSJames Henderson                try:
1981364750dSJames Henderson                    # See if this is one of our leading breakpoints.
1991364750dSJames Henderson                    bpr = self._leading_bp_handles[bp_id]
2001364750dSJames Henderson                except KeyError:
2011364750dSJames Henderson                    # This is a trailing bp. Mark it for removal.
2021364750dSJames Henderson                    bp_to_delete.append(bp_id)
2031364750dSJames Henderson                    continue
2041364750dSJames Henderson
2051364750dSJames Henderson                bpr.add_hit()
2061364750dSJames Henderson                if bpr.should_be_removed():
2076cf69179SStephen Tozer                    if bpr.finish_on_remove:
2086cf69179SStephen Tozer                        exit_desired = True
2091364750dSJames Henderson                    bp_to_delete.append(bp_id)
2101364750dSJames Henderson                    del self._leading_bp_handles[bp_id]
2111364750dSJames Henderson                # Add a range of trailing breakpoints covering the lines
2121364750dSJames Henderson                # requested in the DexLimitSteps command. Ignore first line as
2131364750dSJames Henderson                # that's covered by the leading bp we just hit and include the
2141364750dSJames Henderson                # final line.
2151364750dSJames Henderson                for line in range(bpr.range_from + 1, bpr.range_to + 1):
2161364750dSJames Henderson                    self.debugger.add_breakpoint(bpr.path, line)
2171364750dSJames Henderson
2181364750dSJames Henderson            # Remove any trailing or expired leading breakpoints we just hit.
219b3f14802Sgbtozers            self.debugger.delete_breakpoints(bp_to_delete)
2201364750dSJames Henderson
2216cf69179SStephen Tozer            if exit_desired:
2226cf69179SStephen Tozer                break
2231364750dSJames Henderson            self.debugger.go()
2241364750dSJames Henderson            time.sleep(self._pause_between_steps)
225