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