# simple spectrum-based fault localizer for python # unittests using coverage information import argparse import collections import importlib import inspect import math import os import unittest import coverage # class to store all fault localization stats class FL: def __init__(self): # store number of passing and failing tests self.total_passed = 0 self.total_failed = 0 # store line numbers that were covered for # passing and failing tests # counter per line of code # defaultdict calls a function whenever there is a missing key # returns the "default value" provided by this function # lambda: 0 is an "inline function" that always returns 0 self.failed_lines = collections.defaultdict(lambda: 0) self.passed_lines = collections.defaultdict(lambda: 0) def passed(self, covered): self.total_passed += 1 self._add_to_dict(self.passed_lines, covered) def failed(self, covered): self.total_failed += 1 self._add_to_dict(self.failed_lines, covered) def tarantula(self, line): """ Calculate a tarantuala score for the provided line :param line: the line number for the score :return: the score for the given line """ num = self.failed_lines[line] / self.total_failed denom = num + self.passed_lines[line] / self.total_passed if denom == 0: return None return num / denom def ochiai(self, line): passed = self.passed_lines[line] failed = self.failed_lines[line] denom = math.sqrt(self.total_failed * (failed + passed)) if denom == 0: return None return failed / denom def _add_to_dict(self, counter_dict, covered): """ Increments the counters in `counter_dict` for each of the lines provided in `covered` :param counter_dict: dictionary of line counts :param covered: iterable list of lines that were covered """ for line in covered: # check if line is in counters if line not in counter_dict: counter_dict[line] = 0 # update the counter for line `line` by adding 1 counter_dict[line] += 1 parser = argparse.ArgumentParser() parser.add_argument("test_file", help="path to the test file to run") parser.add_argument("target_file", help="the file to localize") # parse the arguments args = parser.parse_args() print(f"The test file is: {args.test_file} and the target file is: {args.target_file}") module_name, _ = os.path.splitext(os.path.basename(args.test_file)) print(f"MODULE: {module_name}") # load the test file dynamically as a module spec = importlib.util.spec_from_file_location(module_name, args.test_file) i = importlib.util.module_from_spec(spec) spec.loader.exec_module(i) # make a coverage object for calculating coverage cov = coverage.Coverage() # make a fault localization object fl = FL() # search through module i and find test cases for name, obj in inspect.getmembers(i): # filter out any non-class objects if not inspect.isclass(obj): continue # load the class as a test suite suite = unittest.defaultTestLoader.loadTestsFromTestCase(obj) # loop through all tests that we found for test in suite: # start collecting coverage info cov.erase() cov.start() # run the test res = test.run() # stop collecting coverage cov.stop() # grab the coverage data data = cov.get_data() # grab the lines from the target file # convert file to an absolute path lines = data.lines(os.path.abspath(args.target_file)) # check if test passed if res.wasSuccessful(): fl.passed(lines) else: fl.failed(lines) # we have run all the tests print(f"Passed: {fl.total_passed}") print(f"Failed: {fl.total_failed}") # score all the lines in the target file # use coverage to get a list of the executable lines in a file (_, stmts, _, _) = cov.analysis(os.path.abspath(args.target_file)) tarantula_scores = [(i, fl.tarantula(i)) for i in stmts if fl.tarantula(i) is not None] tarantula_scores.sort(reverse=True, key= lambda x: x[1]) print(tarantula_scores[:30]) ochiai_scores = [(i, fl.ochiai(i)) for i in stmts if fl.ochiai(i) is not None] ochiai_scores.sort(reverse=True, key= lambda x: x[1]) print(ochiai_scores[:30])