1# DExTer : Debugging Experience Tester
2# ~~~~~~   ~         ~~         ~   ~~
3#
4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5# See https://llvm.org/LICENSE.txt for license information.
6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7from collections import OrderedDict
8import os
9from typing import List
10
11from dex.dextIR.BuilderIR import BuilderIR
12from dex.dextIR.DebuggerIR import DebuggerIR
13from dex.dextIR.StepIR import StepIR, StepKind
14
15
16def _step_kind_func(context, step):
17    if (step.current_location.path is None or
18        not os.path.exists(step.current_location.path)):
19        return StepKind.FUNC_UNKNOWN
20
21    if any(os.path.samefile(step.current_location.path, f)
22           for f in context.options.source_files):
23        return StepKind.FUNC
24
25    return StepKind.FUNC_EXTERNAL
26
27
28class DextIR:
29    """A full Dexter test report.
30
31    This is composed of all the other *IR classes. They are used together to
32    record Dexter inputs and the resultant debugger steps, providing a single
33    high level access container.
34
35    The Heuristic class works with dexter commands and the generated DextIR to
36    determine the debugging score for a given test.
37
38    Args:
39        commands: { name (str), commands (list[CommandIR])
40    """
41
42    def __init__(self,
43                 dexter_version: str,
44                 executable_path: str,
45                 source_paths: List[str],
46                 builder: BuilderIR = None,
47                 debugger: DebuggerIR = None,
48                 commands: OrderedDict = None):
49        self.dexter_version = dexter_version
50        self.executable_path = executable_path
51        self.source_paths = source_paths
52        self.builder = builder
53        self.debugger = debugger
54        self.commands = commands
55        self.steps: List[StepIR] = []
56
57    def __str__(self):
58        colors = 'rgby'
59        st = '## BEGIN ##\n'
60        color_idx = 0
61        for step in self.steps:
62            if step.step_kind in (StepKind.FUNC, StepKind.FUNC_EXTERNAL,
63                                  StepKind.FUNC_UNKNOWN):
64                color_idx += 1
65
66            color = colors[color_idx % len(colors)]
67            st += '<{}>{}</>\n'.format(color, step)
68        st += '## END ({} step{}) ##\n'.format(
69            self.num_steps, '' if self.num_steps == 1 else 's')
70        return st
71
72    @property
73    def num_steps(self):
74        return len(self.steps)
75
76    def _get_prev_step_in_this_frame(self, step):
77        """Find the most recent step in the same frame as `step`.
78
79        Returns:
80            StepIR or None if there is no previous step in this frame.
81        """
82        return next((s for s in reversed(self.steps)
83            if s.current_function == step.current_function
84            and s.num_frames == step.num_frames), None)
85
86    def _get_new_step_kind(self, context, step):
87        if step.current_function is None:
88            return StepKind.UNKNOWN
89
90        if len(self.steps) == 0:
91            return _step_kind_func(context, step)
92
93        prev_step = self.steps[-1]
94
95        if prev_step.current_function is None:
96            return StepKind.UNKNOWN
97
98        if prev_step.num_frames < step.num_frames:
99            return _step_kind_func(context, step)
100
101        if prev_step.num_frames > step.num_frames:
102            frame_step = self._get_prev_step_in_this_frame(step)
103            prev_step = frame_step if frame_step is not None else prev_step
104
105        # If we're missing line numbers to compare then the step kind has to be UNKNOWN.
106        if prev_step.current_location.lineno is None or step.current_location.lineno is None:
107            return StepKind.UNKNOWN
108
109        # We're in the same func as prev step, check lineo.
110        if prev_step.current_location.lineno > step.current_location.lineno:
111            return StepKind.VERTICAL_BACKWARD
112
113        if prev_step.current_location.lineno < step.current_location.lineno:
114            return StepKind.VERTICAL_FORWARD
115
116        # We're on the same line as prev step, check column.
117        if prev_step.current_location.column > step.current_location.column:
118            return StepKind.HORIZONTAL_BACKWARD
119
120        if prev_step.current_location.column < step.current_location.column:
121            return StepKind.HORIZONTAL_FORWARD
122
123        # This step is in exactly the same location as the prev step.
124        return StepKind.SAME
125
126    def new_step(self, context, step):
127        assert isinstance(step, StepIR), type(step)
128        step.step_kind = self._get_new_step_kind(context, step)
129        self.steps.append(step)
130        return step
131
132    def clear_steps(self):
133        self.steps.clear()
134