From d92b33415cf1d90dd27b1f156d13e44e50ade7dc Mon Sep 17 00:00:00 2001 From: Philipp Hossner Date: Fri, 23 Nov 2018 15:14:12 +0100 Subject: [PATCH] -m flag now prints mutation diff instead of just the mutation (fixes #38,#40) --- mutpy/controller.py | 2 +- mutpy/test/test_views.py | 41 +++++++++++++++++++++++++++++++++++++++- mutpy/views.py | 34 +++++++++++++++++++++++---------- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/mutpy/controller.py b/mutpy/controller.py index 960c688b..cdce18d2 100644 --- a/mutpy/controller.py +++ b/mutpy/controller.py @@ -128,7 +128,7 @@ def mutate_module(self, target_module, to_mutate, total_duration): if self.mutation_number and self.mutation_number != mutation_number: self.score.inc_incompetent() continue - self.notify_mutation(mutation_number, mutations, target_module.__name__, mutant_ast) + self.notify_mutation(mutation_number, mutations, target_module, mutant_ast) mutant_module = self.create_mutant_module(target_module, mutant_ast) if mutant_module: self.run_tests_with_mutant(total_duration, mutant_module, mutations, coverage_result) diff --git a/mutpy/test/test_views.py b/mutpy/test/test_views.py index 09e11c92..0a407776 100644 --- a/mutpy/test/test_views.py +++ b/mutpy/test/test_views.py @@ -1,10 +1,25 @@ +import sys import unittest +from contextlib import contextmanager +from io import StringIO -from mutpy.views import QuietTextView +from mutpy import utils +from mutpy.views import QuietTextView, TextView COLOR_RED = 'red' +@contextmanager +def captured_output(): + new_out, new_err = StringIO(), StringIO() + old_out, old_err = sys.stdout, sys.stderr + try: + sys.stdout, sys.stderr = new_out, new_err + yield sys.stdout, sys.stderr + finally: + sys.stdout, sys.stderr = old_out, old_err + + class QuietTextViewTest(unittest.TestCase): @staticmethod def get_quiet_text_view(colored_output=False): @@ -19,3 +34,27 @@ def test_decorate_with_color(self): colored_text = text_view.decorate(text, color=COLOR_RED) # then self.assertEqual(expected_colored_text, colored_text) + + +class TextViewTest(unittest.TestCase): + SEPARATOR = '--------------------------------------------------------------------------------' + EOL = "\n" + + @staticmethod + def get_text_view(colored_output=False, show_mutants=False): + return TextView(colored_output=colored_output, show_mutants=show_mutants) + + def test_print_code(self): + # given + text_view = self.get_text_view(show_mutants=True) + original = utils.create_ast('x = x + 1') + mutant = utils.create_ast('x = x - 1') + # when + with captured_output() as (out, err): + text_view.print_code(mutant, original) + # then + output = out.getvalue().strip() + self.assertEqual( + self.SEPARATOR + self.EOL + '- 1: x = x + 1' + self.EOL + '+ 1: x = x - 1' + self.EOL + self.SEPARATOR, + output + ) diff --git a/mutpy/views.py b/mutpy/views.py index 99980e6a..41a6ecd8 100644 --- a/mutpy/views.py +++ b/mutpy/views.py @@ -1,8 +1,13 @@ +import ast +import datetime +import inspect import os import traceback -import datetime -import yaml +from difflib import unified_diff + import jinja2 +import yaml + from mutpy import codegen, termcolor, utils @@ -121,27 +126,36 @@ def original_tests_fail(self, result): def mutation(self, number, mutations, module, mutant): for mutation in mutations: self.level_print( - '[#{:>4}] {:<3} {}:{:<3}: '.format(number, mutation.operator.name(), module, mutation.node.lineno), + '[#{:>4}] {:<3} {}: '.format(number, mutation.operator.name(), module.__name__), ended=False, level=2, ) if mutation != mutations[-1]: print() if self.show_mutants: - self.print_code(mutant, mutation.node.lineno) + self.print_code(mutant, ast.parse(inspect.getsource(module))) def cant_load(self, name, exception): self.level_print(self.decorate('Can\'t load module: ', 'red', attrs=['bold']) + '{} ({}: {})'.format(name, exception.__class__.__name__, exception)) - def print_code(self, mutant, lineno): + def print_code(self, mutant, original): mutant_src = codegen.to_source(mutant) mutant_src = codegen.add_line_numbers(mutant_src) - src_lines = mutant_src.split("\n") - lineno = min(lineno, len(src_lines)) - src_lines[lineno - 1] = self.decorate('~' + src_lines[lineno - 1][1:], 'yellow') - snippet = src_lines[max(0, lineno - 5):min(len(src_lines), lineno + 5)] - print("\n{}\n".format('-'*80) + "\n".join(snippet) + "\n{}".format('-'*80)) + original_src = codegen.to_source(original) + original_src = codegen.add_line_numbers(original_src) + self._print_diff(mutant_src, original_src) + + def _print_diff(self, mutant_src, original_src): + diff = self._create_diff(mutant_src, original_src) + diff = [line for line in diff if not line.startswith(('---', '+++', '@@'))] + diff = [self.decorate(line, 'blue') if line.startswith('- ') else line for line in diff] + diff = [self.decorate(line, 'green') if line.startswith('+ ') else line for line in diff] + print("\n{}\n".format('-' * 80) + "\n".join(diff) + "\n{}".format('-' * 80)) + + @staticmethod + def _create_diff(mutant_src, original_src): + return list(unified_diff(original_src.split('\n'), mutant_src.split('\n'), n=4, lineterm='')) def killed(self, time, killer, *args, **kwargs): self.level_print(self.time_format(time) + ' ' + self.decorate('killed', 'green') + ' by ' + str(killer),