diff --git a/cross-project-tests/debuginfo-tests/dexter/Commands.md b/cross-project-tests/debuginfo-tests/dexter/Commands.md --- a/cross-project-tests/debuginfo-tests/dexter/Commands.md +++ b/cross-project-tests/debuginfo-tests/dexter/Commands.md @@ -12,6 +12,7 @@ * [DexDeclareAddress](Commands.md#DexDeclareAddress) * [DexDeclareFile](Commands.md#DexDeclareFile) * [DexFinishTest](Commands.md#DexFinishTest) +* [DexCommandLine](Commands.md#DexCommandLine) --- ## DexExpectProgramState @@ -336,6 +337,25 @@ ### Heuristic This command does not contribute to the heuristic score. +---- +## DexCommandLine + DexCommandLine(command_line) + + Args: + command_line (list): List of strings that form the command line. + +### Description +Specifies the command line with which to launch the test. The arguments will +be appended to the default command line, i.e. the path to the compiled binary, +and will be passed to the program under test. + +This command does not contribute to any part of the debug experience testing or +runtime instrumentation -- it's only for communicating arguments to the program +under test. + +### Heuristic +This command does not contribute to the heuristic score. + --- ## DexWatch DexWatch(*expressions) diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py b/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py @@ -18,6 +18,7 @@ from dex.utils.Exceptions import CommandParseError from dex.command.CommandBase import CommandBase +from dex.command.commands.DexCommandLine import DexCommandLine from dex.command.commands.DexDeclareFile import DexDeclareFile from dex.command.commands.DexDeclareAddress import DexDeclareAddress from dex.command.commands.DexExpectProgramState import DexExpectProgramState @@ -41,6 +42,7 @@ { name (str): command (class) } """ return { + DexCommandLine.get_name() : DexCommandLine, DexDeclareAddress.get_name() : DexDeclareAddress, DexDeclareFile.get_name() : DexDeclareFile, DexExpectProgramState.get_name() : DexExpectProgramState, @@ -322,6 +324,10 @@ # TODO: keep stored paths as PurePaths for 'longer'. cmd_path = str(PurePath(cmd_path)) declared_files.add(cmd_path) + elif type(command) is DexCommandLine and 'DexCommandLine' in commands: + msg = "More than one DexCommandLine in file" + raise format_parse_err(msg, path, file_lines, err_point) + assert (path, cmd_point) not in commands[command_name], ( command_name, commands[command_name]) commands[command_name][path, cmd_point] = command diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexCommandLine.py b/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexCommandLine.py new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexCommandLine.py @@ -0,0 +1,31 @@ +# 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 specifies the command line with which to run the test. +""" + +from dex.command.CommandBase import CommandBase + +class DexCommandLine(CommandBase): + def __init__(self, the_cmdline): + if type(the_cmdline) is not list: + raise TypeError('Expected list, got {}'.format(type(the_cmdline))) + for x in the_cmdline: + if type(x) is not str: + raise TypeError('Command line element "{}" has type {}'.format(x, type(x))) + self.the_cmdline = the_cmdline + super(DexCommandLine, self).__init__() + + def eval(self): + raise NotImplementedError('DexCommandLine commands cannot be evaled.') + + @staticmethod + def get_name(): + return __class__.__name__ + + @staticmethod + def get_subcommands() -> dict: + return None diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ConditionalController.py @@ -69,16 +69,15 @@ class ConditionalController(DebuggerControllerBase): def __init__(self, context, step_collection): - self.context = context - self.step_collection = step_collection self._bp_ranges = None - self._build_bp_ranges() self._watches = set() self._step_index = 0 self._pause_between_steps = context.options.pause_between_steps self._max_steps = context.options.max_steps # Map {id: BreakpointRange} self._leading_bp_handles = {} + super(ConditionalController, self).__init__(context, step_collection) + self._build_bp_ranges() def _build_bp_ranges(self): commands = self.step_collection.commands @@ -126,7 +125,7 @@ id = self.debugger.add_breakpoint(bpr.path, bpr.range_from) self._leading_bp_handles[id] = bpr - def _run_debugger_custom(self): + def _run_debugger_custom(self, cmdline): # TODO: Add conditional and unconditional breakpoint support to dbgeng. if self.debugger.get_name() == 'dbgeng': raise DebuggerException('DexLimitSteps commands are not supported by dbgeng') @@ -137,7 +136,7 @@ for command_obj in chain.from_iterable(self.step_collection.commands.values()): self._watches.update(command_obj.get_watches()) - self.debugger.launch() + self.debugger.launch(cmdline) time.sleep(self._pause_between_steps) exit_desired = False diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DebuggerControllerBase.py @@ -9,6 +9,10 @@ import abc class DebuggerControllerBase(object, metaclass=abc.ABCMeta): + def __init__(self, context, step_collection): + self.context = context + self.step_collection = step_collection + @abc.abstractclassmethod def _run_debugger_custom(self): """Specify your own implementation of run_debugger_custom in your own @@ -20,9 +24,19 @@ """Responsible for correctly launching and tearing down the debugger. """ self.debugger = debugger + + # Fetch command line options, if any. + the_cmdline = [] + commands = self.step_collection.commands + if 'DexCommandLine' in commands: + cmd_line_objs = commands['DexCommandLine'] + assert len(cmd_line_objs) == 1 + cmd_line_obj = cmd_line_objs[0] + the_cmdline = cmd_line_obj.the_cmdline + with self.debugger: if not self.debugger.loading_error: - self._run_debugger_custom() + self._run_debugger_custom(the_cmdline) # We may need to pickle this debugger controller after running the # debugger. Debuggers are not picklable objects, so set to None. diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/DefaultController.py @@ -23,11 +23,10 @@ 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.source_files = context.options.source_files self.watches = set() self.step_index = 0 + super(DefaultController, self).__init__(context, step_collection) def _break_point_all_lines(self): for s in self.context.options.source_files: @@ -73,10 +72,10 @@ return False - def _run_debugger_custom(self): + def _run_debugger_custom(self, cmdline): self.step_collection.debugger = self.debugger.debugger_info self._break_point_all_lines() - self.debugger.launch() + self.debugger.launch(cmdline) for command_obj in chain.from_iterable(self.step_collection.commands.values()): self.watches.update(command_obj.get_watches()) diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py @@ -95,7 +95,8 @@ # but is something that should be considered in the future. raise NotImplementedError('delete_conditional_breakpoint is not yet implemented by dbgeng') - def launch(self): + def launch(self, cmdline): + assert len(cmdline) == 0, "Command lines unimplemented for dbgeng right now" # We are, by this point, already launched. self.step_info = probe_process.probe_state(self.client) diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py @@ -169,8 +169,8 @@ pass self._target.BreakpointDelete(id) - def launch(self): - self._process = self._target.LaunchSimple(None, None, os.getcwd()) + def launch(self, cmdline): + self._process = self._target.LaunchSimple(cmdline, None, os.getcwd()) if not self._process or self._process.GetNumThreads() == 0: raise DebuggerException('could not launch process') if self._process.GetNumThreads() != 1: diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py @@ -229,7 +229,26 @@ bp.Delete() break - def launch(self): + def _fetch_property(self, props, name): + num_props = props.Count + result = None + for x in range(1, num_props+1): + item = props.Item(x) + if item.Name == name: + return item + assert False, "Couldn't find property {}".format(name) + + def launch(self, cmdline): + cmdline_str = ' '.join(cmdline) + + # In a slightly baroque manner, lookup the VS project that runs when + # you click "run", and set its command line options to the desired + # command line options. + startup_proj_name = str(self._fetch_property(self._interface.Solution.Properties, 'StartupProject')) + project = self._fetch_property(self._interface.Solution, startup_proj_name) + ActiveConfiguration = self._fetch_property(project.Properties, 'ActiveConfiguration').Object + ActiveConfiguration.DebugSettings.CommandArguments = cmdline_str + self._fn_go() def step(self): diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/command_line.c b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/command_line.c new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/command_line.c @@ -0,0 +1,16 @@ +// UNSUPPORTED: dbgeng +// +// RUN: %dexter_regression_test -- %s | FileCheck %s +// CHECK: command_line.c: + +int main(int argc, const char **argv) { + if (argc == 4) + return 0; // DexLabel('retline') + + return 1; // DexUnreachable() +} + +// DexExpectWatchValue('argc', '4', on_line=ref('retline')) + +// Three args will be appended to the 'default' argument. +// DexCommandLine(['a', 'b', 'c'])