xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/tools/test/Tool.py (revision ca92bdfa3ef8f9a1cc97167fc96601f8bd7b436b)
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"""Test tool."""
81364750dSJames Henderson
91364750dSJames Hendersonimport math
101364750dSJames Hendersonimport os
111364750dSJames Hendersonimport csv
121364750dSJames Hendersonimport pickle
131364750dSJames Hendersonimport shutil
14e6cc7b72SShubham Sandeep Rastogiimport platform
151364750dSJames Henderson
161364750dSJames Hendersonfrom dex.command.ParseCommand import get_command_infos
171364750dSJames Hendersonfrom dex.debugger.Debuggers import run_debugger_subprocess
181364750dSJames Hendersonfrom dex.debugger.DebuggerControllers.DefaultController import DefaultController
191364750dSJames Hendersonfrom dex.debugger.DebuggerControllers.ConditionalController import ConditionalController
201364750dSJames Hendersonfrom dex.dextIR.DextIR import DextIR
211364750dSJames Hendersonfrom dex.heuristic import Heuristic
221364750dSJames Hendersonfrom dex.tools import TestToolBase
231364750dSJames Hendersonfrom dex.utils.Exceptions import DebuggerException
241364750dSJames Hendersonfrom dex.utils.Exceptions import BuildScriptException, HeuristicException
251364750dSJames Hendersonfrom dex.utils.PrettyOutputBase import Stream
261364750dSJames Hendersonfrom dex.utils.ReturnCode import ReturnCode
271364750dSJames Henderson
281364750dSJames Henderson
291364750dSJames Hendersonclass TestCase(object):
301364750dSJames Henderson    def __init__(self, context, name, heuristic, error):
311364750dSJames Henderson        self.context = context
321364750dSJames Henderson        self.name = name
331364750dSJames Henderson        self.heuristic = heuristic
341364750dSJames Henderson        self.error = error
351364750dSJames Henderson
361364750dSJames Henderson    @property
371364750dSJames Henderson    def penalty(self):
381364750dSJames Henderson        try:
391364750dSJames Henderson            return self.heuristic.penalty
401364750dSJames Henderson        except AttributeError:
41f98ee40fSTobias Hieta            return float("nan")
421364750dSJames Henderson
431364750dSJames Henderson    @property
441364750dSJames Henderson    def max_penalty(self):
451364750dSJames Henderson        try:
461364750dSJames Henderson            return self.heuristic.max_penalty
471364750dSJames Henderson        except AttributeError:
48f98ee40fSTobias Hieta            return float("nan")
491364750dSJames Henderson
501364750dSJames Henderson    @property
511364750dSJames Henderson    def score(self):
521364750dSJames Henderson        try:
531364750dSJames Henderson            return self.heuristic.score
541364750dSJames Henderson        except AttributeError:
55f98ee40fSTobias Hieta            return float("nan")
561364750dSJames Henderson
571364750dSJames Henderson    def __str__(self):
581364750dSJames Henderson        if self.error and self.context.options.verbose:
591364750dSJames Henderson            verbose_error = str(self.error)
601364750dSJames Henderson        else:
61f98ee40fSTobias Hieta            verbose_error = ""
621364750dSJames Henderson
631364750dSJames Henderson        if self.error:
64f98ee40fSTobias Hieta            script_error = (
65f98ee40fSTobias Hieta                " : {}".format(self.error.script_error.splitlines()[0])
66f98ee40fSTobias Hieta                if getattr(self.error, "script_error", None)
67f98ee40fSTobias Hieta                else ""
68f98ee40fSTobias Hieta            )
691364750dSJames Henderson
70f98ee40fSTobias Hieta            error = " [{}{}]".format(str(self.error).splitlines()[0], script_error)
711364750dSJames Henderson        else:
72f98ee40fSTobias Hieta            error = ""
731364750dSJames Henderson
741364750dSJames Henderson        try:
751364750dSJames Henderson            summary = self.heuristic.summary_string
761364750dSJames Henderson        except AttributeError:
77f98ee40fSTobias Hieta            summary = "<r>nan/nan (nan)</>"
78f98ee40fSTobias Hieta        return "{}: {}{}\n{}".format(self.name, summary, error, verbose_error)
791364750dSJames Henderson
801364750dSJames Henderson
811364750dSJames Hendersonclass Tool(TestToolBase):
821364750dSJames Henderson    """Run the specified DExTer test(s) with the specified compiler and linker
831364750dSJames Henderson    options and produce a dextIR file as well as printing out the debugging
841364750dSJames Henderson    experience score calculated by the DExTer heuristic.
851364750dSJames Henderson    """
861364750dSJames Henderson
871364750dSJames Henderson    def __init__(self, *args, **kwargs):
881364750dSJames Henderson        super(Tool, self).__init__(*args, **kwargs)
891364750dSJames Henderson        self._test_cases = []
901364750dSJames Henderson
911364750dSJames Henderson    @property
921364750dSJames Henderson    def name(self):
93f98ee40fSTobias Hieta        return "DExTer test"
941364750dSJames Henderson
951364750dSJames Henderson    def add_tool_arguments(self, parser, defaults):
96f98ee40fSTobias Hieta        parser.add_argument(
97f98ee40fSTobias Hieta            "--fail-lt",
981364750dSJames Henderson            type=float,
991364750dSJames Henderson            default=0.0,  # By default TEST always succeeds.
100f98ee40fSTobias Hieta            help="exit with status FAIL(2) if the test result"
101f98ee40fSTobias Hieta            " is less than this value.",
102f98ee40fSTobias Hieta            metavar="<float>",
103f98ee40fSTobias Hieta        )
104f98ee40fSTobias Hieta        parser.add_argument(
105f98ee40fSTobias Hieta            "--calculate-average",
1061364750dSJames Henderson            action="store_true",
107f98ee40fSTobias Hieta            help="calculate the average score of every test run",
108f98ee40fSTobias Hieta        )
1091364750dSJames Henderson        super(Tool, self).add_tool_arguments(parser, defaults)
1101364750dSJames Henderson
1111364750dSJames Henderson    def _init_debugger_controller(self):
1121364750dSJames Henderson        step_collection = DextIR(
1131364750dSJames Henderson            executable_path=self.context.options.executable,
1141364750dSJames Henderson            source_paths=self.context.options.source_files,
115f98ee40fSTobias Hieta            dexter_version=self.context.version,
116f98ee40fSTobias Hieta        )
1171364750dSJames Henderson
1181364750dSJames Henderson        step_collection.commands, new_source_files = get_command_infos(
119f98ee40fSTobias Hieta            self.context.options.test_files, self.context.options.source_root_dir
120f98ee40fSTobias Hieta        )
1211364750dSJames Henderson
1221364750dSJames Henderson        self.context.options.source_files.extend(list(new_source_files))
1231364750dSJames Henderson
124f98ee40fSTobias Hieta        if "DexLimitSteps" in step_collection.commands:
1251364750dSJames Henderson            debugger_controller = ConditionalController(self.context, step_collection)
1261364750dSJames Henderson        else:
1271364750dSJames Henderson            debugger_controller = DefaultController(self.context, step_collection)
1281364750dSJames Henderson
1291364750dSJames Henderson        return debugger_controller
1301364750dSJames Henderson
13145a40c16SStephen Tozer    def _get_steps(self):
132f98ee40fSTobias Hieta        """Generate a list of debugger steps from a test case."""
1331364750dSJames Henderson        debugger_controller = self._init_debugger_controller()
1341364750dSJames Henderson        debugger_controller = run_debugger_subprocess(
135f98ee40fSTobias Hieta            debugger_controller, self.context.working_directory.path
136f98ee40fSTobias Hieta        )
1371364750dSJames Henderson        steps = debugger_controller.step_collection
1381364750dSJames Henderson        return steps
1391364750dSJames Henderson
1401364750dSJames Henderson    def _get_results_basename(self, test_name):
1411364750dSJames Henderson        def splitall(x):
1421364750dSJames Henderson            while len(x) > 0:
1431364750dSJames Henderson                x, y = os.path.split(x)
1441364750dSJames Henderson                yield y
145f98ee40fSTobias Hieta
1461364750dSJames Henderson        all_components = reversed([x for x in splitall(test_name)])
147f98ee40fSTobias Hieta        return "_".join(all_components)
1481364750dSJames Henderson
1491364750dSJames Henderson    def _get_results_path(self, test_name):
1501364750dSJames Henderson        """Returns the path to the test results directory for the test denoted
1511364750dSJames Henderson        by test_name.
1521364750dSJames Henderson        """
153*ca92bdfaSEisuke Kawashima        assert self.context.options.results_directory is not None
154f98ee40fSTobias Hieta        return os.path.join(
155f98ee40fSTobias Hieta            self.context.options.results_directory,
156f98ee40fSTobias Hieta            self._get_results_basename(test_name),
157f98ee40fSTobias Hieta        )
1581364750dSJames Henderson
1591364750dSJames Henderson    def _get_results_text_path(self, test_name):
160f98ee40fSTobias Hieta        """Returns path results .txt file for test denoted by test_name."""
1611364750dSJames Henderson        test_results_path = self._get_results_path(test_name)
162f98ee40fSTobias Hieta        return "{}.txt".format(test_results_path)
1631364750dSJames Henderson
1641364750dSJames Henderson    def _get_results_pickle_path(self, test_name):
165f98ee40fSTobias Hieta        """Returns path results .dextIR file for test denoted by test_name."""
1661364750dSJames Henderson        test_results_path = self._get_results_path(test_name)
167f98ee40fSTobias Hieta        return "{}.dextIR".format(test_results_path)
1681364750dSJames Henderson
1691364750dSJames Henderson    def _record_steps(self, test_name, steps):
1701364750dSJames Henderson        """Write out the set of steps out to the test's .txt and .json
1712bd62e0bSOCHyams        results file if a results directory has been specified.
1721364750dSJames Henderson        """
1732bd62e0bSOCHyams        if self.context.options.results_directory:
1741364750dSJames Henderson            output_text_path = self._get_results_text_path(test_name)
175f98ee40fSTobias Hieta            with open(output_text_path, "w") as fp:
1761364750dSJames Henderson                self.context.o.auto(str(steps), stream=Stream(fp))
1771364750dSJames Henderson
1781364750dSJames Henderson            output_dextIR_path = self._get_results_pickle_path(test_name)
179f98ee40fSTobias Hieta            with open(output_dextIR_path, "wb") as fp:
1801364750dSJames Henderson                pickle.dump(steps, fp, protocol=pickle.HIGHEST_PROTOCOL)
1811364750dSJames Henderson
1821364750dSJames Henderson    def _record_score(self, test_name, heuristic):
1832bd62e0bSOCHyams        """Write out the test's heuristic score to the results .txt file
1842bd62e0bSOCHyams        if a results directory has been specified.
1851364750dSJames Henderson        """
1862bd62e0bSOCHyams        if self.context.options.results_directory:
1871364750dSJames Henderson            output_text_path = self._get_results_text_path(test_name)
188f98ee40fSTobias Hieta            with open(output_text_path, "a") as fp:
1891364750dSJames Henderson                self.context.o.auto(heuristic.verbose_output, stream=Stream(fp))
1901364750dSJames Henderson
1911364750dSJames Henderson    def _record_test_and_display(self, test_case):
1921364750dSJames Henderson        """Output test case to o stream and record test case internally for
1931364750dSJames Henderson        handling later.
1941364750dSJames Henderson        """
1951364750dSJames Henderson        self.context.o.auto(test_case)
1961364750dSJames Henderson        self._test_cases.append(test_case)
1971364750dSJames Henderson
1981364750dSJames Henderson    def _record_failed_test(self, test_name, exception):
1991364750dSJames Henderson        """Instantiate a failed test case with failure exception and
2001364750dSJames Henderson        store internally.
2011364750dSJames Henderson        """
2021364750dSJames Henderson        test_case = TestCase(self.context, test_name, None, exception)
2031364750dSJames Henderson        self._record_test_and_display(test_case)
2041364750dSJames Henderson
2051364750dSJames Henderson    def _record_successful_test(self, test_name, steps, heuristic):
2061364750dSJames Henderson        """Instantiate a successful test run, store test for handling later.
2071364750dSJames Henderson        Display verbose output for test case if required.
2081364750dSJames Henderson        """
2091364750dSJames Henderson        test_case = TestCase(self.context, test_name, heuristic, None)
2101364750dSJames Henderson        self._record_test_and_display(test_case)
2111364750dSJames Henderson        if self.context.options.verbose:
212f98ee40fSTobias Hieta            self.context.o.auto("\n{}\n".format(steps))
2131364750dSJames Henderson            self.context.o.auto(heuristic.verbose_output)
2141364750dSJames Henderson
2151364750dSJames Henderson    def _run_test(self, test_name):
2161364750dSJames Henderson        """Attempt to run test files specified in options.source_files. Store
2171364750dSJames Henderson        result internally in self._test_cases.
2181364750dSJames Henderson        """
2191364750dSJames Henderson        try:
22045a40c16SStephen Tozer            if self.context.options.binary:
221e6cc7b72SShubham Sandeep Rastogi                if platform.system() == 'Darwin' and os.path.exists(self.context.options.binary + '.dSYM'):
222e6cc7b72SShubham Sandeep Rastogi                    # On Darwin, the debug info is in the .dSYM which might not be found by lldb, copy it into the tmp working directory
223e6cc7b72SShubham Sandeep Rastogi                    shutil.copytree(self.context.options.binary + '.dSYM', self.context.options.executable + '.dSYM')
22445a40c16SStephen Tozer                # Copy user's binary into the tmp working directory.
22545a40c16SStephen Tozer                shutil.copy(
22645a40c16SStephen Tozer                    self.context.options.binary, self.context.options.executable
22745a40c16SStephen Tozer                )
22845a40c16SStephen Tozer            steps = self._get_steps()
2291364750dSJames Henderson            self._record_steps(test_name, steps)
2301364750dSJames Henderson            heuristic_score = Heuristic(self.context, steps)
2311364750dSJames Henderson            self._record_score(test_name, heuristic_score)
232f98ee40fSTobias Hieta        except (BuildScriptException, DebuggerException, HeuristicException) as e:
2331364750dSJames Henderson            self._record_failed_test(test_name, e)
2341364750dSJames Henderson            return
2351364750dSJames Henderson
2361364750dSJames Henderson        self._record_successful_test(test_name, steps, heuristic_score)
2371364750dSJames Henderson        return
2381364750dSJames Henderson
2391364750dSJames Henderson    def _handle_results(self) -> ReturnCode:
2401364750dSJames Henderson        return_code = ReturnCode.OK
2411364750dSJames Henderson        options = self.context.options
2421364750dSJames Henderson
2431364750dSJames Henderson        if not options.verbose:
244f98ee40fSTobias Hieta            self.context.o.auto("\n")
2451364750dSJames Henderson
2461364750dSJames Henderson        if options.calculate_average:
2471364750dSJames Henderson            # Calculate and print the average score
2481364750dSJames Henderson            score_sum = 0.0
2491364750dSJames Henderson            num_tests = 0
2501364750dSJames Henderson            for test_case in self._test_cases:
2511364750dSJames Henderson                score = test_case.score
2521364750dSJames Henderson                if not test_case.error and not math.isnan(score):
2531364750dSJames Henderson                    score_sum += test_case.score
2541364750dSJames Henderson                    num_tests += 1
2551364750dSJames Henderson
2561364750dSJames Henderson            if num_tests != 0:
2571364750dSJames Henderson                print("@avg: ({:.4f})".format(score_sum / num_tests))
2581364750dSJames Henderson
2592bd62e0bSOCHyams        has_failed = lambda test: test.score < options.fail_lt or test.error
2602bd62e0bSOCHyams        if any(map(has_failed, self._test_cases)):
2612bd62e0bSOCHyams            return_code = ReturnCode.FAIL
2622bd62e0bSOCHyams
2632bd62e0bSOCHyams        if options.results_directory:
264f98ee40fSTobias Hieta            summary_path = os.path.join(options.results_directory, "summary.csv")
265f98ee40fSTobias Hieta            with open(summary_path, mode="w", newline="") as fp:
266f98ee40fSTobias Hieta                writer = csv.writer(fp, delimiter=",")
267f98ee40fSTobias Hieta                writer.writerow(["Test Case", "Score", "Error"])
2681364750dSJames Henderson
2691364750dSJames Henderson                for test_case in self._test_cases:
270f98ee40fSTobias Hieta                    writer.writerow(
271f98ee40fSTobias Hieta                        [
272f98ee40fSTobias Hieta                            test_case.name,
273f98ee40fSTobias Hieta                            "{:.4f}".format(test_case.score),
274f98ee40fSTobias Hieta                            test_case.error,
275f98ee40fSTobias Hieta                        ]
276f98ee40fSTobias Hieta                    )
2771364750dSJames Henderson
2781364750dSJames Henderson        return return_code
279