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
7"""This is the main entry point.
8It implements some functionality common to all subtools such as command line
9parsing and running the unit-testing harnesses, before calling the reequested
10subtool.
11"""
12
13import imp
14import os
15import sys
16
17from dex.utils import PrettyOutput, Timer
18from dex.utils import ExtArgParse as argparse
19from dex.utils import get_root_directory
20from dex.utils.Exceptions import Error, ToolArgumentError
21from dex.utils.UnitTests import unit_tests_ok
22from dex.utils.Version import version
23from dex.utils import WorkingDirectory
24from dex.utils.ReturnCode import ReturnCode
25
26
27def _output_bug_report_message(context):
28    """ In the event of a catastrophic failure, print bug report request to the
29        user.
30    """
31    context.o.red(
32        '\n\n'
33        '<g>****************************************</>\n'
34        '<b>****************************************</>\n'
35        '****************************************\n'
36        '**                                    **\n'
37        '** <y>This is a bug in <a>DExTer</>.</>           **\n'
38        '**                                    **\n'
39        '**                  <y>Please report it.</> **\n'
40        '**                                    **\n'
41        '****************************************\n'
42        '<b>****************************************</>\n'
43        '<g>****************************************</>\n'
44        '\n'
45        '<b>system:</>\n'
46        '<d>{}</>\n\n'
47        '<b>version:</>\n'
48        '<d>{}</>\n\n'
49        '<b>args:</>\n'
50        '<d>{}</>\n'
51        '\n'.format(sys.platform, version('DExTer'),
52                    [sys.executable] + sys.argv),
53        stream=PrettyOutput.stderr)
54
55
56def get_tools_directory():
57    """ Returns directory path where DExTer tool imports can be
58        found.
59    """
60    tools_directory = os.path.join(get_root_directory(), 'tools')
61    assert os.path.isdir(tools_directory), tools_directory
62    return tools_directory
63
64
65def get_tool_names():
66    """ Returns a list of expected DExTer Tools
67    """
68    return [
69        'clang-opt-bisect', 'help', 'list-debuggers', 'no-tool-',
70        'run-debugger-internal-', 'test', 'view'
71    ]
72
73
74def _set_auto_highlights(context):
75    """Flag some strings for auto-highlighting.
76    """
77    context.o.auto_reds.extend([
78        r'[Ee]rror\:',
79        r'[Ee]xception\:',
80        r'un(expected|recognized) argument',
81    ])
82    context.o.auto_yellows.extend([
83        r'[Ww]arning\:',
84        r'\(did you mean ',
85        r'During handling of the above exception, another exception',
86    ])
87
88
89def _get_options_and_args(context):
90    """ get the options and arguments from the commandline
91    """
92    parser = argparse.ExtArgumentParser(context, add_help=False)
93    parser.add_argument('tool', default=None, nargs='?')
94    options, args = parser.parse_known_args(sys.argv[1:])
95
96    return options, args
97
98
99def _get_tool_name(options):
100    """ get the name of the dexter tool (if passed) specified on the command
101        line, otherwise return 'no_tool_'.
102    """
103    tool_name = options.tool
104    if tool_name is None:
105        tool_name = 'no_tool_'
106    else:
107        _is_valid_tool_name(tool_name)
108    return tool_name
109
110
111def _is_valid_tool_name(tool_name):
112    """ check tool name matches a tool directory within the dexter tools
113        directory.
114    """
115    valid_tools = get_tool_names()
116    if tool_name not in valid_tools:
117        raise Error('invalid tool "{}" (choose from {})'.format(
118            tool_name,
119            ', '.join([t for t in valid_tools if not t.endswith('-')])))
120
121
122def _import_tool_module(tool_name):
123    """ Imports the python module at the tool directory specificed by
124        tool_name.
125    """
126    # format tool argument to reflect tool directory form.
127    tool_name = tool_name.replace('-', '_')
128
129    tools_directory = get_tools_directory()
130    module_info = imp.find_module(tool_name, [tools_directory])
131
132    return imp.load_module(tool_name, *module_info)
133
134
135def tool_main(context, tool, args):
136    with Timer(tool.name):
137        options, defaults = tool.parse_command_line(args)
138        Timer.display = options.time_report
139        Timer.indent = options.indent_timer_level
140        Timer.fn = context.o.blue
141        context.options = options
142        context.version = version(tool.name)
143
144        if options.version:
145            context.o.green('{}\n'.format(context.version))
146            return ReturnCode.OK
147
148        if (options.unittest != 'off' and not unit_tests_ok(context)):
149            raise Error('<d>unit test failures</>')
150
151        if options.colortest:
152            context.o.colortest()
153            return ReturnCode.OK
154
155        try:
156            tool.handle_base_options(defaults)
157        except ToolArgumentError as e:
158            raise Error(e)
159
160        dir_ = context.options.working_directory
161        with WorkingDirectory(context, dir=dir_) as context.working_directory:
162            return_code = tool.go()
163
164        return return_code
165
166
167class Context(object):
168    """Context encapsulates globally useful objects and data; passed to many
169    Dexter functions.
170    """
171
172    def __init__(self):
173        self.o: PrettyOutput = None
174        self.working_directory: str = None
175        self.options: dict = None
176        self.version: str = None
177        self.root_directory: str = None
178
179
180def main() -> ReturnCode:
181
182    context = Context()
183
184    with PrettyOutput() as context.o:
185        try:
186            context.root_directory = get_root_directory()
187            # Flag some strings for auto-highlighting.
188            _set_auto_highlights(context)
189            options, args = _get_options_and_args(context)
190            # raises 'Error' if command line tool is invalid.
191            tool_name = _get_tool_name(options)
192            module = _import_tool_module(tool_name)
193            return tool_main(context, module.Tool(context), args)
194        except Error as e:
195            context.o.auto(
196                '\nerror: {}\n'.format(str(e)), stream=PrettyOutput.stderr)
197            try:
198                if context.options.error_debug:
199                    raise
200            except AttributeError:
201                pass
202            return ReturnCode._ERROR
203        except (KeyboardInterrupt, SystemExit):
204            raise
205        except:  # noqa
206            _output_bug_report_message(context)
207            raise
208