From df1abad288c4fe7e16363b82cf8bec52a62a88f5 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:39:59 +0100 Subject: [PATCH 01/76] Include files from mutpy in the project --- src/pynguin/assertion/assertiongenerator.py | 4 +- .../assertion/mutation_analysis/controller.py | 207 +++++++++++++ .../mutation_analysis/mutationadapter.py | 19 +- .../mutation_analysis/operators/__init__.py | 57 ++++ .../mutation_analysis/operators/arithmetic.py | 90 ++++++ .../mutation_analysis/operators/base.py | 169 +++++++++++ .../mutation_analysis/operators/decorator.py | 65 ++++ .../mutation_analysis/operators/exception.py | 40 +++ .../operators/inheritance.py | 207 +++++++++++++ .../mutation_analysis/operators/logical.py | 102 +++++++ .../mutation_analysis/operators/loop.py | 56 ++++ .../mutation_analysis/operators/misc.py | 131 +++++++++ .../assertion/mutation_analysis/utils.py | 277 ++++++++++++++++++ 13 files changed, 1407 insertions(+), 17 deletions(-) create mode 100644 src/pynguin/assertion/mutation_analysis/controller.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/__init__.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/arithmetic.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/base.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/decorator.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/exception.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/inheritance.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/logical.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/loop.py create mode 100644 src/pynguin/assertion/mutation_analysis/operators/misc.py create mode 100644 src/pynguin/assertion/mutation_analysis/utils.py diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index 8d683835..9235cfbb 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -15,7 +15,7 @@ from typing import TYPE_CHECKING -import mutpy +import pynguin.assertion.mutation_analysis.utils as mu import pynguin.assertion.assertion as ass import pynguin.assertion.assertion_trace as at @@ -279,7 +279,7 @@ def __init__(self, plain_executor: ex.TestCaseExecutor, *, testing: bool = False adapter = ma.MutationAdapter() # Evil hack to change the way mutpy creates mutated modules. - mutpy.utils.create_module = self._create_module_with_instrumentation + mu.create_module = self._create_module_with_instrumentation self._mutated_modules = [x for x, _ in adapter.mutate_module()] def _add_assertions(self, test_cases: list[tc.TestCase]): diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py new file mode 100644 index 00000000..7ba1436c --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -0,0 +1,207 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py. +""" + +import random + +from pynguin.assertion.mutation_analysis import utils + + +class MutationController: + + def __init__(self, target_loader, mutant_generator): + super().__init__() + self.target_loader = target_loader + self.mutant_generator = mutant_generator + + def mutate_module(self, target_module, to_mutate, target_ast, coverage_injector=None): + for mutations, mutant_ast in self.mutant_generator.mutate(target_ast, to_mutate, coverage_injector, + module=target_module): + yield self.create_mutant_module(target_module, mutant_ast), mutations + + @utils.TimeRegister + def create_target_ast(self, target_module): + with open(target_module.__file__) as target_file: + return utils.create_ast(target_file.read()) + + @utils.TimeRegister + def create_mutant_module(self, target_module, mutant_ast): + try: + return utils.create_module( + ast_node=mutant_ast, + module_name=target_module.__name__ + ) + except BaseException: + return None + + +class HOMStrategy: + + def __init__(self, order=2): + self.order = order + + def remove_bad_mutations(self, mutations_to_apply, available_mutations, allow_same_operators=True): + for mutation_to_apply in mutations_to_apply: + for available_mutation in available_mutations[:]: + if mutation_to_apply.node == available_mutation.node or \ + mutation_to_apply.node in available_mutation.node.children or \ + available_mutation.node in mutation_to_apply.node.children or \ + (not allow_same_operators and mutation_to_apply.operator == available_mutation.operator): + available_mutations.remove(available_mutation) + + +class FirstToLastHOMStrategy(HOMStrategy): + name = 'FIRST_TO_LAST' + + def generate(self, mutations): + mutations = mutations[:] + while mutations: + mutations_to_apply = [] + index = 0 + available_mutations = mutations[:] + while len(mutations_to_apply) < self.order and available_mutations: + try: + mutation = available_mutations.pop(index) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + index = 0 if index == -1 else -1 + except IndexError: + break + self.remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply + + +class EachChoiceHOMStrategy(HOMStrategy): + name = 'EACH_CHOICE' + + def generate(self, mutations): + mutations = mutations[:] + while mutations: + mutations_to_apply = [] + available_mutations = mutations[:] + while len(mutations_to_apply) < self.order and available_mutations: + try: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + except IndexError: + break + self.remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply + + +class BetweenOperatorsHOMStrategy(HOMStrategy): + name = 'BETWEEN_OPERATORS' + + def generate(self, mutations): + usage = {mutation: 0 for mutation in mutations} + not_used = mutations[:] + while not_used: + mutations_to_apply = [] + available_mutations = mutations[:] + available_mutations.sort(key=lambda x: usage[x]) + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + if not usage[mutation]: + not_used.remove(mutation) + usage[mutation] += 1 + self.remove_bad_mutations(mutations_to_apply, available_mutations, allow_same_operators=False) + yield mutations_to_apply + + +class RandomHOMStrategy(HOMStrategy): + name = 'RANDOM' + + def __init__(self, *args, shuffler=random.shuffle, **kwargs): + super().__init__(*args, **kwargs) + self.shuffler = shuffler + + def generate(self, mutations): + mutations = mutations[:] + self.shuffler(mutations) + while mutations: + mutations_to_apply = [] + available_mutations = mutations[:] + while len(mutations_to_apply) < self.order and available_mutations: + try: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + except IndexError: + break + self.remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply + + +hom_strategies = [ + BetweenOperatorsHOMStrategy, + EachChoiceHOMStrategy, + FirstToLastHOMStrategy, + RandomHOMStrategy, +] + + +class FirstOrderMutator: + + def __init__(self, operators, percentage=100): + self.operators = operators + self.sampler = utils.RandomSampler(percentage) + + def mutate(self, target_ast, to_mutate=None, coverage_injector=None, module=None): + for op in utils.sort_operators(self.operators): + for mutation, mutant in op().mutate(target_ast, to_mutate, self.sampler, coverage_injector, module=module): + yield [mutation], mutant + + +class HighOrderMutator(FirstOrderMutator): + + def __init__(self, *args, hom_strategy=None, **kwargs): + super().__init__(*args, **kwargs) + self.hom_strategy = hom_strategy or FirstToLastHOMStrategy(order=2) + + def mutate(self, target_ast, to_mutate=None, coverage_injector=None, module=None): + mutations = self.generate_all_mutations(coverage_injector, module, target_ast, to_mutate) + for mutations_to_apply in self.hom_strategy.generate(mutations): + generators = [] + applied_mutations = [] + mutant = target_ast + for mutation in mutations_to_apply: + generator = mutation.operator().mutate( + mutant, + to_mutate=to_mutate, + sampler=self.sampler, + coverage_injector=coverage_injector, + module=module, + only_mutation=mutation, + ) + try: + new_mutation, mutant = generator.__next__() + except StopIteration: + assert False, 'no mutations!' + applied_mutations.append(new_mutation) + generators.append(generator) + yield applied_mutations, mutant + self.finish_generators(generators) + + def generate_all_mutations(self, coverage_injector, module, target_ast, to_mutate): + mutations = [] + for op in utils.sort_operators(self.operators): + for mutation, _ in op().mutate(target_ast, to_mutate, None, coverage_injector, module=module): + mutations.append(mutation) + return mutations + + def finish_generators(self, generators): + for generator in reversed(generators): + try: + generator.__next__() + except StopIteration: + continue + assert False, 'too many mutations!' diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 226dd1b6..179ee299 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -11,11 +11,10 @@ from typing import TYPE_CHECKING -import mutpy.controller as mc -import mutpy.operators as mo -import mutpy.operators.loop as mol -import mutpy.utils as mu -import mutpy.views as mv +import pynguin.assertion.mutation_analysis.controller as mc +import pynguin.assertion.mutation_analysis.operators as mo +import pynguin.assertion.mutation_analysis.operators.loop as mol +import pynguin.assertion.mutation_analysis.utils as mu import pynguin.configuration as config @@ -56,7 +55,6 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: part is a list of all the mutations operators applied. """ controller = self._build_mutation_controller() - controller.score = mc.MutationScore() mutants = [] @@ -77,17 +75,13 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: def _build_mutation_controller(self) -> mc.MutationController: _LOGGER.info("Setup mutation controller") - built_views = self._get_views() mutant_generator = self._get_mutant_generator() self.target_loader = mu.ModulesLoader( [config.configuration.module_name], config.configuration.project_path ) return mc.MutationController( - runner_cls=None, target_loader=self.target_loader, - test_loader=None, - views=built_views, mutant_generator=mutant_generator, ) @@ -118,8 +112,3 @@ def _get_mutant_generator(self) -> mc.FirstOrderMutator: hom_strategy = self._strategies[mutation_strategy](order) return mc.HighOrderMutator(operators_set, percentage, hom_strategy) raise ConfigurationException("No suitable mutation strategy found.") - - @staticmethod - def _get_views() -> list[mv.QuietTextView]: - # We do not want any output from MutPy here - return [mv.QuietTextView()] diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py new file mode 100644 index 00000000..7f3d7daa --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -0,0 +1,57 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/__init__.py. +""" + +from .arithmetic import * +from .base import * +from .decorator import * +from .exception import * +from .inheritance import * +from .logical import * +from .loop import * +from .misc import * + +SuperCallingInsert = utils.get_by_python_version([ + SuperCallingInsertPython27, + SuperCallingInsertPython35, +]) + +standard_operators = { + ArithmeticOperatorDeletion, + ArithmeticOperatorReplacement, + AssignmentOperatorReplacement, + BreakContinueReplacement, + ConditionalOperatorDeletion, + ConditionalOperatorInsertion, + ConstantReplacement, + DecoratorDeletion, + ExceptionHandlerDeletion, + ExceptionSwallowing, + HidingVariableDeletion, + LogicalConnectorReplacement, + LogicalOperatorDeletion, + LogicalOperatorReplacement, + OverriddenMethodCallingPositionChange, + OverridingMethodDeletion, + RelationalOperatorReplacement, + SliceIndexRemove, + SuperCallingDeletion, + SuperCallingInsert, +} + +experimental_operators = { + ClassmethodDecoratorInsertion, + OneIterationLoop, + ReverseIterationLoop, + SelfVariableDeletion, + StatementDeletion, + StaticmethodDecoratorInsertion, + ZeroIterationLoop, +} diff --git a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py new file mode 100644 index 00000000..e84244c1 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py @@ -0,0 +1,90 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/arithmetic.py. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators.base import MutationResign, MutationOperator, AbstractUnaryOperatorDeletion + + +class ArithmeticOperatorDeletion(AbstractUnaryOperatorDeletion): + def get_operator_type(self): + return ast.UAdd, ast.USub + + +class AbstractArithmeticOperatorReplacement(MutationOperator): + def should_mutate(self, node): + raise NotImplementedError() + + def mutate_Add(self, node): + if self.should_mutate(node): + return ast.Sub() + raise MutationResign() + + def mutate_Sub(self, node): + if self.should_mutate(node): + return ast.Add() + raise MutationResign() + + def mutate_Mult_to_Div(self, node): + if self.should_mutate(node): + return ast.Div() + raise MutationResign() + + def mutate_Mult_to_FloorDiv(self, node): + if self.should_mutate(node): + return ast.FloorDiv() + raise MutationResign() + + def mutate_Mult_to_Pow(self, node): + if self.should_mutate(node): + return ast.Pow() + raise MutationResign() + + def mutate_Div_to_Mult(self, node): + if self.should_mutate(node): + return ast.Mult() + raise MutationResign() + + def mutate_Div_to_FloorDiv(self, node): + if self.should_mutate(node): + return ast.FloorDiv() + raise MutationResign() + + def mutate_FloorDiv_to_Div(self, node): + if self.should_mutate(node): + return ast.Div() + raise MutationResign() + + def mutate_FloorDiv_to_Mult(self, node): + if self.should_mutate(node): + return ast.Mult() + raise MutationResign() + + def mutate_Mod(self, node): + if self.should_mutate(node): + return ast.Mult() + raise MutationResign() + + def mutate_Pow(self, node): + if self.should_mutate(node): + return ast.Mult() + raise MutationResign() + + +class ArithmeticOperatorReplacement(AbstractArithmeticOperatorReplacement): + def should_mutate(self, node): + return not isinstance(node.parent, ast.AugAssign) + + def mutate_USub(self, node): + return ast.UAdd() + + def mutate_UAdd(self, node): + return ast.USub() diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py new file mode 100644 index 00000000..5537b6ee --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -0,0 +1,169 @@ + +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/base.py. +""" + +import ast +import copy +import re + +from pynguin.assertion.mutation_analysis import utils + + +class MutationResign(Exception): + pass + + +class Mutation: + def __init__(self, operator, node, visitor=None): + self.operator = operator + self.node = node + self.visitor = visitor + + +def copy_node(mutate): + def f(self, node): + copied_node = copy.deepcopy(node, memo={ + id(node.parent): node.parent, + }) + return mutate(self, copied_node) + + return f + + +class MutationOperator: + def mutate(self, node, to_mutate=None, sampler=None, coverage_injector=None, module=None, only_mutation=None): + self.to_mutate = to_mutate + self.sampler = sampler + self.only_mutation = only_mutation + self.coverage_injector = coverage_injector + self.module = module + for new_node in self.visit(node): + yield Mutation(operator=self.__class__, node=self.current_node, visitor=self.visitor), new_node + + def visit(self, node): + if self.has_notmutate(node) or (self.coverage_injector and not self.coverage_injector.is_covered(node)): + return + if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: + return + self.fix_lineno(node) + visitors = self.find_visitors(node) + if visitors: + for visitor in visitors: + try: + if self.sampler and not self.sampler.is_mutation_time(): + raise MutationResign + if self.only_mutation and \ + (self.only_mutation.node != node or self.only_mutation.visitor != visitor.__name__): + raise MutationResign + new_node = visitor(node) + self.visitor = visitor.__name__ + self.current_node = node + self.fix_node_internals(node, new_node) + ast.fix_missing_locations(new_node) + yield new_node + except MutationResign: + pass + finally: + for new_node in self.generic_visit(node): + yield new_node + else: + for new_node in self.generic_visit(node): + yield new_node + + def generic_visit(self, node): + for field, old_value in ast.iter_fields(node): + if isinstance(old_value, list): + generator = self.generic_visit_list(old_value) + elif isinstance(old_value, ast.AST): + generator = self.generic_visit_real_node(node, field, old_value) + else: + generator = [] + + for _ in generator: + yield node + + def generic_visit_list(self, old_value): + old_values_copy = old_value[:] + for position, value in enumerate(old_values_copy): + if isinstance(value, ast.AST): + for new_value in self.visit(value): + if not isinstance(new_value, ast.AST): + old_value[position:position + 1] = new_value + elif value is None: + del old_value[position] + else: + old_value[position] = new_value + + yield + old_value[:] = old_values_copy + + def generic_visit_real_node(self, node, field, old_value): + for new_node in self.visit(old_value): + if new_node is None: + delattr(node, field) + else: + setattr(node, field, new_node) + yield + setattr(node, field, old_value) + + def has_notmutate(self, node): + try: + for decorator in node.decorator_list: + if decorator.id == utils.notmutate.__name__: + return True + return False + except AttributeError: + return False + + def fix_lineno(self, node): + if not hasattr(node, 'lineno') and getattr(node, 'parent', None) is not None and hasattr(node.parent, 'lineno'): + node.lineno = node.parent.lineno + + def fix_node_internals(self, old_node, new_node): + if not hasattr(new_node, 'parent'): + new_node.children = old_node.children + new_node.parent = old_node.parent + if not hasattr(new_node, 'lineno') and hasattr(old_node, 'lineno'): + new_node.lineno = old_node.lineno + if hasattr(old_node, 'marker'): + new_node.marker = old_node.marker + + def find_visitors(self, node): + method_prefix = 'mutate_' + node.__class__.__name__ + return self.getattrs_like(method_prefix) + + def getattrs_like(ob, attr_like): + pattern = re.compile(attr_like + "($|(_\w+)+$)") + return [getattr(ob, attr) for attr in dir(ob) if pattern.match(attr)] + + def set_lineno(self, node, lineno): + for n in ast.walk(node): + if hasattr(n, 'lineno'): + n.lineno = lineno + + def shift_lines(self, nodes, shift_by=1): + for node in nodes: + ast.increment_lineno(node, shift_by) + + @classmethod + def name(cls): + return ''.join([c for c in cls.__name__ if str.isupper(c)]) + + @classmethod + def long_name(cls): + return ' '.join(map(str.lower, (re.split('([A-Z][a-z]*)', cls.__name__)[1::2]))) + + +class AbstractUnaryOperatorDeletion(MutationOperator): + def mutate_UnaryOp(self, node): + if isinstance(node.op, self.get_operator_type()): + return node.operand + raise MutationResign() diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py new file mode 100644 index 00000000..62af67b7 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -0,0 +1,65 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/decorator.py. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node, MutationResign + + +class DecoratorDeletion(MutationOperator): + @copy_node + def mutate_FunctionDef(self, node): + if node.decorator_list: + node.decorator_list = [] + return node + else: + raise MutationResign() + + @classmethod + def name(cls): + return 'DDL' + + +class AbstractMethodDecoratorInsertionMutationOperator(MutationOperator): + @copy_node + def mutate_FunctionDef(self, node): + if not isinstance(node.parent, ast.ClassDef): + raise MutationResign() + for decorator in node.decorator_list: + if isinstance(decorator, ast.Call): + decorator_name = decorator.func.id + elif isinstance(decorator, ast.Attribute): + decorator_name = decorator.value.id + else: + decorator_name = decorator.id + if decorator_name == self.get_decorator_name(): + raise MutationResign() + if node.decorator_list: + lineno = node.decorator_list[-1].lineno + else: + lineno = node.lineno + decorator = ast.Name(id=self.get_decorator_name(), ctx=ast.Load(), lineno=lineno) + self.shift_lines(node.body, 1) + node.decorator_list.append(decorator) + return node + + def get_decorator_name(self): + raise NotImplementedError() + + +class ClassmethodDecoratorInsertion(AbstractMethodDecoratorInsertionMutationOperator): + def get_decorator_name(self): + return 'classmethod' + + +class StaticmethodDecoratorInsertion(AbstractMethodDecoratorInsertionMutationOperator): + def get_decorator_name(self): + return 'staticmethod' diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py new file mode 100644 index 00000000..ce1314b7 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -0,0 +1,40 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/exception.py. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, MutationResign + + +class BaseExceptionHandlerOperator(MutationOperator): + + @staticmethod + def _replace_exception_body(exception_node, body): + return ast.ExceptHandler(type=exception_node.type, name=exception_node.name, lineno=exception_node.lineno, + body=body) + + +class ExceptionHandlerDeletion(BaseExceptionHandlerOperator): + def mutate_ExceptHandler(self, node): + if node.body and isinstance(node.body[0], ast.Raise): + raise MutationResign() + return self._replace_exception_body(node, [ast.Raise(lineno=node.body[0].lineno)]) + + +class ExceptionSwallowing(BaseExceptionHandlerOperator): + def mutate_ExceptHandler(self, node): + if len(node.body) == 1 and isinstance(node.body[0], ast.Pass): + raise MutationResign() + return self._replace_exception_body(node, [ast.Pass(lineno=node.body[0].lineno)]) + + @classmethod + def name(cls): + return 'EXS' diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py new file mode 100644 index 00000000..fc9c4ac6 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -0,0 +1,207 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/inheritance.py. +""" + +import ast +import functools + +from pynguin.assertion.mutation_analysis import utils +from pynguin.assertion.mutation_analysis.operators.base import MutationResign, MutationOperator, copy_node + + +class AbstractOverriddenElementModification(MutationOperator): + def is_overridden(self, node, name=None): + if not isinstance(node.parent, ast.ClassDef): + raise MutationResign() + if not name: + name = node.name + parent = node.parent + parent_names = [] + while parent: + if not isinstance(parent, ast.Module): + parent_names.append(parent.name) + if not isinstance(parent, ast.ClassDef) and not isinstance(parent, ast.Module): + raise MutationResign() + parent = parent.parent + getattr_rec = lambda obj, attr: functools.reduce(getattr, attr, obj) + try: + klass = getattr_rec(self.module, reversed(parent_names)) + except AttributeError: + raise MutationResign() + for base_klass in type.mro(klass)[1:-1]: + if hasattr(base_klass, name): + return True + return False + + +class HidingVariableDeletion(AbstractOverriddenElementModification): + def mutate_Assign(self, node): + if len(node.targets) > 1: + raise MutationResign() + if isinstance(node.targets[0], ast.Name) and self.is_overridden(node, name=node.targets[0].id): + return ast.Pass() + elif isinstance(node.targets[0], ast.Tuple) and isinstance(node.value, ast.Tuple): + return self.mutate_unpack(node) + else: + raise MutationResign() + + def mutate_unpack(self, node): + target = node.targets[0] + value = node.value + new_targets = [] + new_values = [] + for target_element, value_element in zip(target.elts, value.elts): + if not self.is_overridden(node, getattr(target_element, 'id', None)): + new_targets.append(target_element) + new_values.append(value_element) + if len(new_targets) == len(target.elts): + raise MutationResign() + if not new_targets: + return ast.Pass() + elif len(new_targets) == 1: + node.targets = new_targets + node.value = new_values[0] + return node + else: + target.elts = new_targets + value.elts = new_values + return node + + @classmethod + def name(cls): + return 'IHD' + + +class AbstractSuperCallingModification(MutationOperator): + def is_super_call(self, node, stmt): + return isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call) and \ + isinstance(stmt.value.func, ast.Attribute) and isinstance(stmt.value.func.value, ast.Call) and \ + isinstance(stmt.value.func.value.func, ast.Name) and stmt.value.func.value.func.id == 'super' and \ + stmt.value.func.attr == node.name + + def should_mutate(self, node): + return isinstance(node.parent, ast.ClassDef) + + def get_super_call(self, node): + for index, stmt in enumerate(node.body): + if self.is_super_call(node, stmt): + break + else: + return None, None + return index, stmt + + +class OverriddenMethodCallingPositionChange(AbstractSuperCallingModification): + def should_mutate(self, node): + return super().should_mutate(node) and len(node.body) > 1 + + @copy_node + def mutate_FunctionDef(self, node): + if not self.should_mutate(node): + raise MutationResign() + index, stmt = self.get_super_call(node) + if index is None: + raise MutationResign() + super_call = node.body[index] + del node.body[index] + if index == 0: + self.set_lineno(super_call, node.body[-1].lineno) + self.shift_lines(node.body, -1) + node.body.append(super_call) + else: + self.set_lineno(super_call, node.body[0].lineno) + self.shift_lines(node.body, 1) + node.body.insert(0, super_call) + return node + + @classmethod + def name(cls): + return 'IOP' + + +class OverridingMethodDeletion(AbstractOverriddenElementModification): + def mutate_FunctionDef(self, node): + if self.is_overridden(node): + return ast.Pass() + raise MutationResign() + + @classmethod + def name(cls): + return 'IOD' + + +class SuperCallingDeletion(AbstractSuperCallingModification): + @copy_node + def mutate_FunctionDef(self, node): + if not self.should_mutate(node): + raise MutationResign() + index, _ = self.get_super_call(node) + if index is None: + raise MutationResign() + node.body[index] = ast.Pass(lineno=node.body[index].lineno) + return node + + +class SuperCallingInsertPython27(AbstractSuperCallingModification, AbstractOverriddenElementModification): + __python_version__ = (2, 7) + + def should_mutate(self, node): + return super().should_mutate(node) and self.is_overridden(node) + + @copy_node + def mutate_FunctionDef(self, node): + if not self.should_mutate(node): + raise MutationResign() + index, stmt = self.get_super_call(node) + if index is not None: + raise MutationResign() + node.body.insert(0, self.create_super_call(node)) + self.shift_lines(node.body[1:], 1) + return node + + @copy_node + def create_super_call(self, node): + super_call = utils.create_ast('super().{}()'.format(node.name)).body[0] + for arg in node.args.args[1:-len(node.args.defaults) or None]: + super_call.value.args.append(ast.Name(id=arg.arg, ctx=ast.Load())) + for arg, default in zip(node.args.args[-len(node.args.defaults):], node.args.defaults): + super_call.value.keywords.append(ast.keyword(arg=arg.arg, value=default)) + for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): + super_call.value.keywords.append(ast.keyword(arg=arg.arg, value=default)) + if node.args.vararg: + self.add_vararg_to_super_call(super_call, node.args.vararg) + if node.args.kwarg: + self.add_kwarg_to_super_call(super_call, node.args.kwarg) + self.set_lineno(super_call, node.body[0].lineno) + return super_call + + @staticmethod + def add_kwarg_to_super_call(super_call, kwarg): + super_call.value.kwargs = ast.Name(id=kwarg, ctx=ast.Load()) + + @staticmethod + def add_vararg_to_super_call(super_call, vararg): + super_call.value.starargs = ast.Name(id=vararg, ctx=ast.Load()) + + @classmethod + def name(cls): + return 'SCI' + + +class SuperCallingInsertPython35(SuperCallingInsertPython27): + __python_version__ = (3, 5) + + @staticmethod + def add_kwarg_to_super_call(super_call, kwarg): + super_call.value.keywords.append(ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load()))) + + @staticmethod + def add_vararg_to_super_call(super_call, vararg): + super_call.value.args.append(ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load()))) diff --git a/src/pynguin/assertion/mutation_analysis/operators/logical.py b/src/pynguin/assertion/mutation_analysis/operators/logical.py new file mode 100644 index 00000000..395ae697 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/logical.py @@ -0,0 +1,102 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/logical.py. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, AbstractUnaryOperatorDeletion, copy_node + + +class ConditionalOperatorDeletion(AbstractUnaryOperatorDeletion): + def get_operator_type(self): + return ast.Not + + def mutate_NotIn(self, node): + return ast.In() + + +class ConditionalOperatorInsertion(MutationOperator): + def negate_test(self, node): + not_node = ast.UnaryOp(op=ast.Not(), operand=node.test) + node.test = not_node + return node + + @copy_node + def mutate_While(self, node): + return self.negate_test(node) + + @copy_node + def mutate_If(self, node): + return self.negate_test(node) + + def mutate_In(self, node): + return ast.NotIn() + + +class LogicalConnectorReplacement(MutationOperator): + def mutate_And(self, node): + return ast.Or() + + def mutate_Or(self, node): + return ast.And() + + +class LogicalOperatorDeletion(AbstractUnaryOperatorDeletion): + def get_operator_type(self): + return ast.Invert + + +class LogicalOperatorReplacement(MutationOperator): + def mutate_BitAnd(self, node): + return ast.BitOr() + + def mutate_BitOr(self, node): + return ast.BitAnd() + + def mutate_BitXor(self, node): + return ast.BitAnd() + + def mutate_LShift(self, node): + return ast.RShift() + + def mutate_RShift(self, node): + return ast.LShift() + + +class RelationalOperatorReplacement(MutationOperator): + def mutate_Lt(self, node): + return ast.Gt() + + def mutate_Lt_to_LtE(self, node): + return ast.LtE() + + def mutate_Gt(self, node): + return ast.Lt() + + def mutate_Gt_to_GtE(self, node): + return ast.GtE() + + def mutate_LtE(self, node): + return ast.GtE() + + def mutate_LtE_to_Lt(self, node): + return ast.Lt() + + def mutate_GtE(self, node): + return ast.LtE() + + def mutate_GtE_to_Gt(self, node): + return ast.Gt() + + def mutate_Eq(self, node): + return ast.NotEq() + + def mutate_NotEq(self, node): + return ast.Eq() diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py new file mode 100644 index 00000000..f0dc6ca6 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -0,0 +1,56 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/loop.py. +""" + +import ast + +from pynguin.assertion.mutation_analysis.operators import copy_node, MutationOperator + + +class OneIterationLoop(MutationOperator): + def one_iteration(self, node): + node.body.append(ast.Break(lineno=node.body[-1].lineno + 1)) + return node + + @copy_node + def mutate_For(self, node): + return self.one_iteration(node) + + @copy_node + def mutate_While(self, node): + return self.one_iteration(node) + + +class ReverseIterationLoop(MutationOperator): + @copy_node + def mutate_For(self, node): + old_iter = node.iter + node.iter = ast.Call( + func=ast.Name(id=reversed.__name__, ctx=ast.Load()), + args=[old_iter], + keywords=[], + starargs=None, + kwargs=None, + ) + return node + + +class ZeroIterationLoop(MutationOperator): + def zero_iteration(self, node): + node.body = [ast.Break(lineno=node.body[0].lineno)] + return node + + @copy_node + def mutate_For(self, node): + return self.zero_iteration(node) + + @copy_node + def mutate_While(self, node): + return self.zero_iteration(node) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py new file mode 100644 index 00000000..50ca9b07 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -0,0 +1,131 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/misc.py. +""" + +import ast + +from pynguin.assertion.mutation_analysis import utils +from pynguin.assertion.mutation_analysis.operators.arithmetic import AbstractArithmeticOperatorReplacement +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, MutationResign + + +class AssignmentOperatorReplacement(AbstractArithmeticOperatorReplacement): + def should_mutate(self, node): + return isinstance(node.parent, ast.AugAssign) + + @classmethod + def name(cls): + return 'ASR' + + +class BreakContinueReplacement(MutationOperator): + def mutate_Break(self, node): + return ast.Continue() + + def mutate_Continue(self, node): + return ast.Break() + + +class ConstantReplacement(MutationOperator): + FIRST_CONST_STRING = 'mutpy' + SECOND_CONST_STRING = 'python' + + def help_str(self, node): + if utils.is_docstring(node): + raise MutationResign() + + if node.s != self.FIRST_CONST_STRING: + return self.FIRST_CONST_STRING + else: + return self.SECOND_CONST_STRING + + def help_str_empty(self, node): + if not node.s or utils.is_docstring(node): + raise MutationResign() + return '' + + def mutate_Constant_num(self, node): + if isinstance(node.value, (int, float)) and not isinstance(node.value, bool): + return ast.Constant(n=node.n + 1) + else: + raise MutationResign() + + def mutate_Constant_str(self, node): + if isinstance(node.value, str): + return ast.Constant(s=self.help_str(node)) + else: + raise MutationResign() + + def mutate_Constant_str_empty(self, node): + if isinstance(node.value, str): + return ast.Constant(s=self.help_str_empty(node)) + else: + raise MutationResign() + + def mutate_Num(self, node): + return ast.Num(n=node.n + 1) + + def mutate_Str(self, node): + return ast.Str(s=self.help_str(node)) + + def mutate_Str_empty(self, node): + return ast.Str(s=self.help_str_empty(node)) + + @classmethod + def name(cls): + return 'CRP' + + +class SliceIndexRemove(MutationOperator): + def mutate_Slice_remove_lower(self, node): + if not node.lower: + raise MutationResign() + + return ast.Slice(lower=None, upper=node.upper, step=node.step) + + def mutate_Slice_remove_upper(self, node): + if not node.upper: + raise MutationResign() + + return ast.Slice(lower=node.lower, upper=None, step=node.step) + + def mutate_Slice_remove_step(self, node): + if not node.step: + raise MutationResign() + + return ast.Slice(lower=node.lower, upper=node.upper, step=None) + + +class SelfVariableDeletion(MutationOperator): + def mutate_Attribute(self, node): + try: + if node.value.id == 'self': + return ast.Name(id=node.attr, ctx=ast.Load()) + else: + raise MutationResign() + except AttributeError: + raise MutationResign() + + +class StatementDeletion(MutationOperator): + def mutate_Assign(self, node): + return ast.Pass() + + def mutate_Return(self, node): + return ast.Pass() + + def mutate_Expr(self, node): + if utils.is_docstring(node.value): + raise MutationResign() + return ast.Pass() + + @classmethod + def name(cls): + return 'SDL' diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py new file mode 100644 index 00000000..10e6732d --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -0,0 +1,277 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py. +""" + +import ast +import copy +import importlib +import inspect +import os +import pkgutil +import random +import sys +import time +import types +from collections import defaultdict + +from importlib._bootstrap_external import EXTENSION_SUFFIXES, ExtensionFileLoader + + +def create_module(ast_node, module_name='mutant', module_dict=None): + code = compile(ast_node, module_name, 'exec') + module = types.ModuleType(module_name) + module.__dict__.update(module_dict or {}) + exec(code, module.__dict__) + return module + + +def notmutate(sth): + return sth + + +class ModulesLoaderException(Exception): + def __init__(self, name, exception): + self.name = name + self.exception = exception + + def __str__(self): + return "can't load {}".format(self.name) + + +class ModulesLoader: + def __init__(self, names, path): + self.names = names + self.path = path or '.' + self.ensure_in_path(self.path) + + def load(self, without_modules=None, exclude_c_extensions=True): + results = [] + without_modules = without_modules or [] + for name in self.names: + results += self.load_single(name) + for module, to_mutate in results: + # yield only if module is not explicitly excluded and only source modules (.py) if demanded + if module not in without_modules and not (exclude_c_extensions and self._is_c_extension(module)): + yield module, to_mutate + + def load_single(self, name): + full_path = self.get_full_path(name) + if os.path.exists(full_path): + if self.is_file(full_path): + return self.load_file(full_path) + elif self.is_directory(full_path): + return self.load_directory(full_path) + if self.is_package(name): + return self.load_package(name) + else: + return self.load_module(name) + + def get_full_path(self, name): + if os.path.isabs(name): + return name + return os.path.abspath(os.path.join(self.path, name)) + + @staticmethod + def is_file(name): + return os.path.isfile(name) + + @staticmethod + def is_directory(name): + return os.path.exists(name) and os.path.isdir(name) + + @staticmethod + def is_package(name): + try: + module = importlib.import_module(name) + return hasattr(module, '__file__') and module.__file__.endswith('__init__.py') + except ImportError: + return False + finally: + sys.path_importer_cache.clear() + + def load_file(self, name): + if name.endswith('.py'): + dirname = os.path.dirname(name) + self.ensure_in_path(dirname) + module_name = self.get_filename_without_extension(name) + return self.load_module(module_name) + + def ensure_in_path(self, directory): + if directory not in sys.path: + sys.path.insert(0, directory) + + @staticmethod + def get_filename_without_extension(path): + return os.path.basename(os.path.splitext(path)[0]) + + @staticmethod + def load_package(name): + result = [] + try: + package = importlib.import_module(name) + for _, module_name, ispkg in pkgutil.walk_packages(package.__path__, package.__name__ + '.'): + if not ispkg: + try: + module = importlib.import_module(module_name) + result.append((module, None)) + except ImportError as _: + pass + except ImportError as _: + pass + return result + + def load_directory(self, name): + if os.path.isfile(os.path.join(name, '__init__.py')): + parent_dir = self._get_parent_directory(name) + self.ensure_in_path(parent_dir) + return self.load_package(os.path.basename(name)) + else: + result = [] + for file in os.listdir(name): + modules = self.load_single(os.path.join(name, file)) + if modules: + result += modules + return result + + def load_module(self, name): + module, remainder_path, last_exception = self._split_by_module_and_remainder(name) + if not self._module_has_member(module, remainder_path): + raise ModulesLoaderException(name, last_exception) + return [(module, '.'.join(remainder_path) if remainder_path else None)] + + @staticmethod + def _get_parent_directory(name): + parent_dir = os.path.abspath(os.path.join(name, os.pardir)) + return parent_dir + + @staticmethod + def _split_by_module_and_remainder(name): + """Takes a path string and returns the contained module and the remaining path after it. + + Example: "mymodule.mysubmodule.MyClass.my_func" -> mysubmodule, "MyClass.my_func" + """ + module_path = name.split('.') + member_path = [] + last_exception = None + while True: + try: + module = importlib.import_module('.'.join(module_path)) + break + except ImportError as error: + member_path = [module_path.pop()] + member_path + last_exception = error + if not module_path: + raise ModulesLoaderException(name, last_exception) + return module, member_path, last_exception + + @staticmethod + def _module_has_member(module, member_path): + attr = module + for part in member_path: + if hasattr(attr, part): + attr = getattr(attr, part) + else: + return False + return True + + @staticmethod + def _is_c_extension(module): + if isinstance(getattr(module, '__loader__', None), ExtensionFileLoader): + return True + module_filename = inspect.getfile(module) + module_filetype = os.path.splitext(module_filename)[1] + return module_filetype in EXTENSION_SUFFIXES + + +class Timer: + time_provider = time.time + + def __init__(self): + self.duration = 0 + self.start = self.time_provider() + + def stop(self): + self.duration = self.time_provider() - self.start + return self.duration + + +class TimeRegister: + executions = defaultdict(float) + timer_class = Timer + stack = [] + + def __init__(self, method): + self.method = method + + def __get__(self, obj, ownerClass=None): + return types.MethodType(self, obj) + + def __call__(self, *args, **kwargs): + if self.stack and self.stack[-1] == self.method: + return self.method(*args, **kwargs) + + self.stack.append(self.method) + time_reg = self.timer_class() + result = self.method(*args, **kwargs) + + self.executions[self.method.__name__] += time_reg.stop() + self.stack.pop() + return result + + @classmethod + def clean(cls): + cls.executions.clear() + cls.stack = [] + + +class RandomSampler: + def __init__(self, percentage): + self.percentage = percentage if 0 < percentage < 100 else 100 + + def is_mutation_time(self): + return random.randrange(100) < self.percentage + + +class ParentNodeTransformer(ast.NodeTransformer): + def visit(self, node): + if getattr(node, 'parent', None): + node = copy.copy(node) + if hasattr(node, 'lineno'): + del node.lineno + node.parent = getattr(self, 'parent', None) + node.children = [] + self.parent = node + result_node = super().visit(node) + self.parent = node.parent + if self.parent: + self.parent.children += [node] + node.children + return result_node + + +def create_ast(code): + return ParentNodeTransformer().visit(ast.parse(code)) + + +def is_docstring(node): + def_node = node.parent.parent + return (isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) and def_node.body and + isinstance(def_node.body[0], ast.Expr) and isinstance(def_node.body[0].value, ast.Str) and + def_node.body[0].value == node) + + +def get_by_python_version(classes, python_version=sys.version_info): + candidates = [cls for cls in classes if cls.__python_version__ <= python_version] + if not candidates: + raise NotImplementedError('MutPy does not support Python {}.'.format(sys.version)) + return max([candidate for candidate in candidates], key=lambda cls: cls.__python_version__) + + +def sort_operators(operators): + return sorted(operators, key=lambda cls: cls.name()) From 4b19b33f077d8ecb4c242ee1fee2666fd5ced98b Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 21:49:24 +0100 Subject: [PATCH 02/76] Add type hints and remove useless parameters and classes --- .../assertion/mutation_analysis/controller.py | 103 ++++++++----- .../mutation_analysis/operators/__init__.py | 5 - .../mutation_analysis/operators/arithmetic.py | 30 ++-- .../mutation_analysis/operators/base.py | 60 ++++---- .../mutation_analysis/operators/decorator.py | 18 +-- .../mutation_analysis/operators/exception.py | 10 +- .../operators/inheritance.py | 80 +++++------ .../mutation_analysis/operators/logical.py | 48 +++---- .../mutation_analysis/operators/loop.py | 14 +- .../mutation_analysis/operators/misc.py | 48 +++---- .../assertion/mutation_analysis/utils.py | 136 +++++++----------- 11 files changed, 276 insertions(+), 276 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 7ba1436c..634bd40c 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -8,31 +8,42 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py. """ +from __future__ import annotations +import ast import random +import types + +from typing import Generator, Callable from pynguin.assertion.mutation_analysis import utils +from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator class MutationController: - def __init__(self, target_loader, mutant_generator): - super().__init__() + def __init__(self, target_loader: utils.ModulesLoader, mutant_generator: FirstOrderMutator) -> None: self.target_loader = target_loader self.mutant_generator = mutant_generator - def mutate_module(self, target_module, to_mutate, target_ast, coverage_injector=None): - for mutations, mutant_ast in self.mutant_generator.mutate(target_ast, to_mutate, coverage_injector, - module=target_module): + def mutate_module( + self, + target_module: types.ModuleType, + to_mutate: str | None, + target_ast: ast.AST, + ) -> Generator[tuple[list[Mutation], types.ModuleType | None], None, None]: + for mutations, mutant_ast in self.mutant_generator.mutate( + target_ast, + to_mutate, + module=target_module, + ): yield self.create_mutant_module(target_module, mutant_ast), mutations - @utils.TimeRegister - def create_target_ast(self, target_module): + def create_target_ast(self, target_module: types.ModuleType) -> ast.AST: with open(target_module.__file__) as target_file: return utils.create_ast(target_file.read()) - @utils.TimeRegister - def create_mutant_module(self, target_module, mutant_ast): + def create_mutant_module(self, target_module: types.ModuleType, mutant_ast: ast.Module) -> types.ModuleType | None: try: return utils.create_module( ast_node=mutant_ast, @@ -44,10 +55,15 @@ def create_mutant_module(self, target_module, mutant_ast): class HOMStrategy: - def __init__(self, order=2): + def __init__(self, order: int = 2) -> None: self.order = order - def remove_bad_mutations(self, mutations_to_apply, available_mutations, allow_same_operators=True): + def remove_bad_mutations( + self, + mutations_to_apply: list[Mutation], + available_mutations: list[Mutation], + allow_same_operators: bool = True + ) -> None: for mutation_to_apply in mutations_to_apply: for available_mutation in available_mutations[:]: if mutation_to_apply.node == available_mutation.node or \ @@ -60,10 +76,10 @@ def remove_bad_mutations(self, mutations_to_apply, available_mutations, allow_sa class FirstToLastHOMStrategy(HOMStrategy): name = 'FIRST_TO_LAST' - def generate(self, mutations): + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: mutations = mutations[:] while mutations: - mutations_to_apply = [] + mutations_to_apply: list[Mutation] = [] index = 0 available_mutations = mutations[:] while len(mutations_to_apply) < self.order and available_mutations: @@ -81,10 +97,10 @@ def generate(self, mutations): class EachChoiceHOMStrategy(HOMStrategy): name = 'EACH_CHOICE' - def generate(self, mutations): + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: mutations = mutations[:] while mutations: - mutations_to_apply = [] + mutations_to_apply: list[Mutation] = [] available_mutations = mutations[:] while len(mutations_to_apply) < self.order and available_mutations: try: @@ -100,11 +116,11 @@ def generate(self, mutations): class BetweenOperatorsHOMStrategy(HOMStrategy): name = 'BETWEEN_OPERATORS' - def generate(self, mutations): + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: usage = {mutation: 0 for mutation in mutations} not_used = mutations[:] while not_used: - mutations_to_apply = [] + mutations_to_apply: list[Mutation] = [] available_mutations = mutations[:] available_mutations.sort(key=lambda x: usage[x]) while len(mutations_to_apply) < self.order and available_mutations: @@ -120,11 +136,11 @@ def generate(self, mutations): class RandomHOMStrategy(HOMStrategy): name = 'RANDOM' - def __init__(self, *args, shuffler=random.shuffle, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, order: int = 2, shuffler: Callable = random.shuffle) -> None: + super().__init__(order) self.shuffler = shuffler - def generate(self, mutations): + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: mutations = mutations[:] self.shuffler(mutations) while mutations: @@ -151,24 +167,39 @@ def generate(self, mutations): class FirstOrderMutator: - def __init__(self, operators, percentage=100): + def __init__(self, operators: list[type[MutationOperator]], percentage: int = 100) -> None: self.operators = operators self.sampler = utils.RandomSampler(percentage) - def mutate(self, target_ast, to_mutate=None, coverage_injector=None, module=None): + def mutate( + self, + target_ast: ast.AST, + to_mutate: str | None = None, + module: types.ModuleType | None = None, + ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: for op in utils.sort_operators(self.operators): - for mutation, mutant in op().mutate(target_ast, to_mutate, self.sampler, coverage_injector, module=module): + for mutation, mutant in op().mutate(target_ast, to_mutate, self.sampler, module=module): yield [mutation], mutant class HighOrderMutator(FirstOrderMutator): - def __init__(self, *args, hom_strategy=None, **kwargs): - super().__init__(*args, **kwargs) - self.hom_strategy = hom_strategy or FirstToLastHOMStrategy(order=2) - - def mutate(self, target_ast, to_mutate=None, coverage_injector=None, module=None): - mutations = self.generate_all_mutations(coverage_injector, module, target_ast, to_mutate) + def __init__( + self, + operators: list[type[MutationOperator]], + percentage: int = 100, + hom_strategy: HOMStrategy | None = None, + ) -> None: + super().__init__(operators, percentage) + self.hom_strategy = hom_strategy or FirstToLastHOMStrategy() + + def mutate( + self, + target_ast: ast.AST, + to_mutate: str | None = None, + module: types.ModuleType | None = None, + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: + mutations = self.generate_all_mutations(module, target_ast, to_mutate) for mutations_to_apply in self.hom_strategy.generate(mutations): generators = [] applied_mutations = [] @@ -178,7 +209,6 @@ def mutate(self, target_ast, to_mutate=None, coverage_injector=None, module=None mutant, to_mutate=to_mutate, sampler=self.sampler, - coverage_injector=coverage_injector, module=module, only_mutation=mutation, ) @@ -191,14 +221,19 @@ def mutate(self, target_ast, to_mutate=None, coverage_injector=None, module=None yield applied_mutations, mutant self.finish_generators(generators) - def generate_all_mutations(self, coverage_injector, module, target_ast, to_mutate): - mutations = [] + def generate_all_mutations( + self, + module: types.ModuleType | None, + target_ast: ast.AST, + to_mutate: str | None, + ) -> list[Mutation]: + mutations: list[Mutation] = [] for op in utils.sort_operators(self.operators): - for mutation, _ in op().mutate(target_ast, to_mutate, None, coverage_injector, module=module): + for mutation, _ in op().mutate(target_ast, to_mutate, None, module=module): mutations.append(mutation) return mutations - def finish_generators(self, generators): + def finish_generators(self, generators: list[Generator]) -> None: for generator in reversed(generators): try: generator.__next__() diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py index 7f3d7daa..d373bf12 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -18,11 +18,6 @@ from .loop import * from .misc import * -SuperCallingInsert = utils.get_by_python_version([ - SuperCallingInsertPython27, - SuperCallingInsertPython35, -]) - standard_operators = { ArithmeticOperatorDeletion, ArithmeticOperatorReplacement, diff --git a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py index e84244c1..2db9db9a 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py +++ b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py @@ -20,71 +20,71 @@ def get_operator_type(self): class AbstractArithmeticOperatorReplacement(MutationOperator): - def should_mutate(self, node): + def should_mutate(self, node: ast.AST) -> bool: raise NotImplementedError() - def mutate_Add(self, node): + def mutate_Add(self, node: ast.Add) -> ast.Sub: if self.should_mutate(node): return ast.Sub() raise MutationResign() - def mutate_Sub(self, node): + def mutate_Sub(self, node: ast.Sub) -> ast.Add: if self.should_mutate(node): return ast.Add() raise MutationResign() - def mutate_Mult_to_Div(self, node): + def mutate_Mult_to_Div(self, node: ast.Mult) -> ast.Div: if self.should_mutate(node): return ast.Div() raise MutationResign() - def mutate_Mult_to_FloorDiv(self, node): + def mutate_Mult_to_FloorDiv(self, node: ast.Mult) -> ast.FloorDiv: if self.should_mutate(node): return ast.FloorDiv() raise MutationResign() - def mutate_Mult_to_Pow(self, node): + def mutate_Mult_to_Pow(self, node: ast.Mult) -> ast.Pow: if self.should_mutate(node): return ast.Pow() raise MutationResign() - def mutate_Div_to_Mult(self, node): + def mutate_Div_to_Mult(self, node: ast.Div) -> ast.Mult: if self.should_mutate(node): return ast.Mult() raise MutationResign() - def mutate_Div_to_FloorDiv(self, node): + def mutate_Div_to_FloorDiv(self, node: ast.Div) -> ast.FloorDiv: if self.should_mutate(node): return ast.FloorDiv() raise MutationResign() - def mutate_FloorDiv_to_Div(self, node): + def mutate_FloorDiv_to_Div(self, node: ast.FloorDiv) -> ast.Div: if self.should_mutate(node): return ast.Div() raise MutationResign() - def mutate_FloorDiv_to_Mult(self, node): + def mutate_FloorDiv_to_Mult(self, node: ast.FloorDiv) -> ast.Mult: if self.should_mutate(node): return ast.Mult() raise MutationResign() - def mutate_Mod(self, node): + def mutate_Mod(self, node: ast.Mod) -> ast.Mult: if self.should_mutate(node): return ast.Mult() raise MutationResign() - def mutate_Pow(self, node): + def mutate_Pow(self, node: ast.Pow) -> ast.Mult: if self.should_mutate(node): return ast.Mult() raise MutationResign() class ArithmeticOperatorReplacement(AbstractArithmeticOperatorReplacement): - def should_mutate(self, node): + def should_mutate(self, node: ast.AST) -> bool: return not isinstance(node.parent, ast.AugAssign) - def mutate_USub(self, node): + def mutate_USub(self, node: ast.USub) -> ast.UAdd: return ast.UAdd() - def mutate_UAdd(self, node): + def mutate_UAdd(self, node: ast.UAdd) -> ast.USub: return ast.USub() diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 5537b6ee..63ba4171 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -9,10 +9,14 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/base.py. """ +from __future__ import annotations import ast import copy import re +import types + +from typing import Generator, Callable from pynguin.assertion.mutation_analysis import utils @@ -22,7 +26,7 @@ class MutationResign(Exception): class Mutation: - def __init__(self, operator, node, visitor=None): + def __init__(self, operator: type[MutationOperator], node: ast.AST, visitor: Callable[[], None] | None = None): self.operator = operator self.node = node self.visitor = visitor @@ -39,17 +43,23 @@ def f(self, node): class MutationOperator: - def mutate(self, node, to_mutate=None, sampler=None, coverage_injector=None, module=None, only_mutation=None): + def mutate( + self, + node: ast.AST, + to_mutate: str | None = None, + sampler: utils.RandomSampler | None = None, + module: types.ModuleType | None = None, + only_mutation: Mutation | None = None + ): self.to_mutate = to_mutate self.sampler = sampler self.only_mutation = only_mutation - self.coverage_injector = coverage_injector self.module = module for new_node in self.visit(node): yield Mutation(operator=self.__class__, node=self.current_node, visitor=self.visitor), new_node - def visit(self, node): - if self.has_notmutate(node) or (self.coverage_injector and not self.coverage_injector.is_covered(node)): + def visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: + if self.has_notmutate(node): return if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: return @@ -78,7 +88,7 @@ def visit(self, node): for new_node in self.generic_visit(node): yield new_node - def generic_visit(self, node): + def generic_visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: for field, old_value in ast.iter_fields(node): if isinstance(old_value, list): generator = self.generic_visit_list(old_value) @@ -90,22 +100,20 @@ def generic_visit(self, node): for _ in generator: yield node - def generic_visit_list(self, old_value): + def generic_visit_list(self, old_value: list[ast.AST | None]) -> Generator[None, None, None]: old_values_copy = old_value[:] for position, value in enumerate(old_values_copy): if isinstance(value, ast.AST): for new_value in self.visit(value): if not isinstance(new_value, ast.AST): old_value[position:position + 1] = new_value - elif value is None: - del old_value[position] else: old_value[position] = new_value yield old_value[:] = old_values_copy - def generic_visit_real_node(self, node, field, old_value): + def generic_visit_real_node(self, node: ast.AST, field: str, old_value: ast.AST) -> Generator[None, None, None]: for new_node in self.visit(old_value): if new_node is None: delattr(node, field) @@ -114,7 +122,7 @@ def generic_visit_real_node(self, node, field, old_value): yield setattr(node, field, old_value) - def has_notmutate(self, node): + def has_notmutate(self, node: ast.AST) -> bool: try: for decorator in node.decorator_list: if decorator.id == utils.notmutate.__name__: @@ -123,11 +131,11 @@ def has_notmutate(self, node): except AttributeError: return False - def fix_lineno(self, node): + def fix_lineno(self, node: ast.AST) -> None: if not hasattr(node, 'lineno') and getattr(node, 'parent', None) is not None and hasattr(node.parent, 'lineno'): node.lineno = node.parent.lineno - def fix_node_internals(self, old_node, new_node): + def fix_node_internals(self, old_node: ast.AST, new_node: ast.AST) -> None: if not hasattr(new_node, 'parent'): new_node.children = old_node.children new_node.parent = old_node.parent @@ -136,34 +144,38 @@ def fix_node_internals(self, old_node, new_node): if hasattr(old_node, 'marker'): new_node.marker = old_node.marker - def find_visitors(self, node): + def find_visitors(self, node: ast.AST) -> list[Callable[[ast.AST], ast.AST]]: method_prefix = 'mutate_' + node.__class__.__name__ return self.getattrs_like(method_prefix) - def getattrs_like(ob, attr_like): - pattern = re.compile(attr_like + "($|(_\w+)+$)") - return [getattr(ob, attr) for attr in dir(ob) if pattern.match(attr)] + def getattrs_like(self, attr_like: str) -> list[Callable[[ast.AST], ast.AST]]: + pattern = re.compile(attr_like + r"($|(_\w+)+$)") + return [ + getattr(self, attr) + for attr in dir(self) + if pattern.match(attr) + ] - def set_lineno(self, node, lineno): + def set_lineno(self, node: ast.AST, lineno: int) -> None: for n in ast.walk(node): if hasattr(n, 'lineno'): n.lineno = lineno - def shift_lines(self, nodes, shift_by=1): + def shift_lines(self, nodes: list[ast.AST], shift_by: int = 1) -> None: for node in nodes: ast.increment_lineno(node, shift_by) @classmethod - def name(cls): - return ''.join([c for c in cls.__name__ if str.isupper(c)]) + def name(cls) -> str: + return "".join([c for c in cls.__name__ if str.isupper(c)]) @classmethod - def long_name(cls): - return ' '.join(map(str.lower, (re.split('([A-Z][a-z]*)', cls.__name__)[1::2]))) + def long_name(cls) -> str: + return " ".join(map(str.lower, (re.split('([A-Z][a-z]*)', cls.__name__)[1::2]))) class AbstractUnaryOperatorDeletion(MutationOperator): - def mutate_UnaryOp(self, node): + def mutate_UnaryOp(self, node: ast.UnaryOp) -> ast.expr: if isinstance(node.op, self.get_operator_type()): return node.operand raise MutationResign() diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 62af67b7..7dbffd7e 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -16,7 +16,7 @@ class DecoratorDeletion(MutationOperator): @copy_node - def mutate_FunctionDef(self, node): + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: if node.decorator_list: node.decorator_list = [] return node @@ -24,13 +24,13 @@ def mutate_FunctionDef(self, node): raise MutationResign() @classmethod - def name(cls): - return 'DDL' + def name(cls) -> str: + return "DDL" class AbstractMethodDecoratorInsertionMutationOperator(MutationOperator): @copy_node - def mutate_FunctionDef(self, node): + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: if not isinstance(node.parent, ast.ClassDef): raise MutationResign() for decorator in node.decorator_list: @@ -51,15 +51,15 @@ def mutate_FunctionDef(self, node): node.decorator_list.append(decorator) return node - def get_decorator_name(self): + def get_decorator_name(self) -> str: raise NotImplementedError() class ClassmethodDecoratorInsertion(AbstractMethodDecoratorInsertionMutationOperator): - def get_decorator_name(self): - return 'classmethod' + def get_decorator_name(self) -> str: + return "classmethod" class StaticmethodDecoratorInsertion(AbstractMethodDecoratorInsertionMutationOperator): - def get_decorator_name(self): - return 'staticmethod' + def get_decorator_name(self) -> str: + return "staticmethod" diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py index ce1314b7..338c8ef0 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/exception.py +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -17,24 +17,24 @@ class BaseExceptionHandlerOperator(MutationOperator): @staticmethod - def _replace_exception_body(exception_node, body): + def _replace_exception_body(exception_node: ast.ExceptHandler, body: list[ast.stmt]) -> ast.ExceptHandler: return ast.ExceptHandler(type=exception_node.type, name=exception_node.name, lineno=exception_node.lineno, body=body) class ExceptionHandlerDeletion(BaseExceptionHandlerOperator): - def mutate_ExceptHandler(self, node): + def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler: if node.body and isinstance(node.body[0], ast.Raise): raise MutationResign() return self._replace_exception_body(node, [ast.Raise(lineno=node.body[0].lineno)]) class ExceptionSwallowing(BaseExceptionHandlerOperator): - def mutate_ExceptHandler(self, node): + def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler: if len(node.body) == 1 and isinstance(node.body[0], ast.Pass): raise MutationResign() return self._replace_exception_body(node, [ast.Pass(lineno=node.body[0].lineno)]) @classmethod - def name(cls): - return 'EXS' + def name(cls) -> str: + return "EXS" diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index fc9c4ac6..6b1a5372 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -17,7 +17,7 @@ class AbstractOverriddenElementModification(MutationOperator): - def is_overridden(self, node, name=None): + def is_overridden(self, node: ast.AST, name: str | None = None) -> bool: if not isinstance(node.parent, ast.ClassDef): raise MutationResign() if not name: @@ -42,7 +42,7 @@ def is_overridden(self, node, name=None): class HidingVariableDeletion(AbstractOverriddenElementModification): - def mutate_Assign(self, node): + def mutate_Assign(self, node: ast.Assign) -> ast.stmt: if len(node.targets) > 1: raise MutationResign() if isinstance(node.targets[0], ast.Name) and self.is_overridden(node, name=node.targets[0].id): @@ -52,7 +52,7 @@ def mutate_Assign(self, node): else: raise MutationResign() - def mutate_unpack(self, node): + def mutate_unpack(self, node: ast.Assign) -> ast.stmt: target = node.targets[0] value = node.value new_targets = [] @@ -75,21 +75,26 @@ def mutate_unpack(self, node): return node @classmethod - def name(cls): - return 'IHD' + def name(cls) -> str: + return "IHD" class AbstractSuperCallingModification(MutationOperator): - def is_super_call(self, node, stmt): - return isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call) and \ - isinstance(stmt.value.func, ast.Attribute) and isinstance(stmt.value.func.value, ast.Call) and \ - isinstance(stmt.value.func.value.func, ast.Name) and stmt.value.func.value.func.id == 'super' and \ - stmt.value.func.attr == node.name - - def should_mutate(self, node): + def is_super_call(self, node: ast.AST, stmt: ast.stmt) -> bool: + return ( + isinstance(stmt, ast.Expr) + and isinstance(stmt.value, ast.Call) + and isinstance(stmt.value.func, ast.Attribute) + and isinstance(stmt.value.func.value, ast.Call) + and isinstance(stmt.value.func.value.func, ast.Name) + and stmt.value.func.value.func.id == 'super' + and stmt.value.func.attr == node.name + ) + + def should_mutate(self, node: ast.AST) -> bool: return isinstance(node.parent, ast.ClassDef) - def get_super_call(self, node): + def get_super_call(self, node: ast.AST) -> tuple[int, ast.stmt] | tuple[None, None]: for index, stmt in enumerate(node.body): if self.is_super_call(node, stmt): break @@ -99,11 +104,11 @@ def get_super_call(self, node): class OverriddenMethodCallingPositionChange(AbstractSuperCallingModification): - def should_mutate(self, node): + def should_mutate(self, node: ast.AST) -> bool: return super().should_mutate(node) and len(node.body) > 1 @copy_node - def mutate_FunctionDef(self, node): + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: if not self.should_mutate(node): raise MutationResign() index, stmt = self.get_super_call(node) @@ -122,24 +127,24 @@ def mutate_FunctionDef(self, node): return node @classmethod - def name(cls): - return 'IOP' + def name(cls) -> str: + return "IOP" class OverridingMethodDeletion(AbstractOverriddenElementModification): - def mutate_FunctionDef(self, node): + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass: if self.is_overridden(node): return ast.Pass() raise MutationResign() @classmethod - def name(cls): - return 'IOD' + def name(cls) -> str: + return "IOD" class SuperCallingDeletion(AbstractSuperCallingModification): @copy_node - def mutate_FunctionDef(self, node): + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: if not self.should_mutate(node): raise MutationResign() index, _ = self.get_super_call(node) @@ -149,14 +154,13 @@ def mutate_FunctionDef(self, node): return node -class SuperCallingInsertPython27(AbstractSuperCallingModification, AbstractOverriddenElementModification): - __python_version__ = (2, 7) +class SuperCallingInsert(AbstractSuperCallingModification, AbstractOverriddenElementModification): - def should_mutate(self, node): + def should_mutate(self, node: ast.AST) -> bool: return super().should_mutate(node) and self.is_overridden(node) @copy_node - def mutate_FunctionDef(self, node): + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: if not self.should_mutate(node): raise MutationResign() index, stmt = self.get_super_call(node) @@ -167,7 +171,7 @@ def mutate_FunctionDef(self, node): return node @copy_node - def create_super_call(self, node): + def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: super_call = utils.create_ast('super().{}()'.format(node.name)).body[0] for arg in node.args.args[1:-len(node.args.defaults) or None]: super_call.value.args.append(ast.Name(id=arg.arg, ctx=ast.Load())) @@ -183,25 +187,13 @@ def create_super_call(self, node): return super_call @staticmethod - def add_kwarg_to_super_call(super_call, kwarg): - super_call.value.kwargs = ast.Name(id=kwarg, ctx=ast.Load()) - - @staticmethod - def add_vararg_to_super_call(super_call, vararg): - super_call.value.starargs = ast.Name(id=vararg, ctx=ast.Load()) - - @classmethod - def name(cls): - return 'SCI' - - -class SuperCallingInsertPython35(SuperCallingInsertPython27): - __python_version__ = (3, 5) - - @staticmethod - def add_kwarg_to_super_call(super_call, kwarg): + def add_kwarg_to_super_call(super_call: ast.Expr, kwarg: ast.AST) -> None: super_call.value.keywords.append(ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load()))) @staticmethod - def add_vararg_to_super_call(super_call, vararg): + def add_vararg_to_super_call(super_call: ast.Expr, vararg: ast.AST) -> None: super_call.value.args.append(ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load()))) + + @classmethod + def name(cls) -> str: + return "SCI" diff --git a/src/pynguin/assertion/mutation_analysis/operators/logical.py b/src/pynguin/assertion/mutation_analysis/operators/logical.py index 395ae697..01b1635f 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/logical.py +++ b/src/pynguin/assertion/mutation_analysis/operators/logical.py @@ -15,88 +15,88 @@ class ConditionalOperatorDeletion(AbstractUnaryOperatorDeletion): - def get_operator_type(self): + def get_operator_type(self) -> type: return ast.Not - def mutate_NotIn(self, node): + def mutate_NotIn(self, node: ast.NotIn) -> ast.In: return ast.In() class ConditionalOperatorInsertion(MutationOperator): - def negate_test(self, node): + def negate_test(self, node: ast.If | ast.While) -> ast.If | ast.While: not_node = ast.UnaryOp(op=ast.Not(), operand=node.test) node.test = not_node return node @copy_node - def mutate_While(self, node): + def mutate_While(self, node: ast.While) -> ast.While: return self.negate_test(node) @copy_node - def mutate_If(self, node): + def mutate_If(self, node: ast.If) -> ast.If: return self.negate_test(node) - def mutate_In(self, node): + def mutate_In(self, node: ast.In) -> ast.NotIn: return ast.NotIn() class LogicalConnectorReplacement(MutationOperator): - def mutate_And(self, node): + def mutate_And(self, node: ast.And) -> ast.Or: return ast.Or() - def mutate_Or(self, node): + def mutate_Or(self, node: ast.Or) -> ast.And: return ast.And() class LogicalOperatorDeletion(AbstractUnaryOperatorDeletion): - def get_operator_type(self): + def get_operator_type(self) -> type: return ast.Invert class LogicalOperatorReplacement(MutationOperator): - def mutate_BitAnd(self, node): + def mutate_BitAnd(self, node: ast.BitAnd) -> ast.BitOr: return ast.BitOr() - def mutate_BitOr(self, node): + def mutate_BitOr(self, node: ast.BitOr) -> ast.BitAnd: return ast.BitAnd() - def mutate_BitXor(self, node): + def mutate_BitXor(self, node: ast.BitXor) -> ast.BitAnd: return ast.BitAnd() - def mutate_LShift(self, node): + def mutate_LShift(self, node: ast.LShift) -> ast.RShift: return ast.RShift() - def mutate_RShift(self, node): + def mutate_RShift(self, node: ast.RShift) -> ast.LShift: return ast.LShift() class RelationalOperatorReplacement(MutationOperator): - def mutate_Lt(self, node): + def mutate_Lt(self, node: ast.Lt) -> ast.Gt: return ast.Gt() - def mutate_Lt_to_LtE(self, node): + def mutate_Lt_to_LtE(self, node: ast.Lt) -> ast.LtE: return ast.LtE() - def mutate_Gt(self, node): + def mutate_Gt(self, node: ast.Gt) -> ast.Lt: return ast.Lt() - def mutate_Gt_to_GtE(self, node): + def mutate_Gt_to_GtE(self, node: ast.Gt) -> ast.GtE: return ast.GtE() - def mutate_LtE(self, node): + def mutate_LtE(self, node: ast.LtE) -> ast.GtE: return ast.GtE() - def mutate_LtE_to_Lt(self, node): + def mutate_LtE_to_Lt(self, node: ast.LtE) -> ast.Lt: return ast.Lt() - def mutate_GtE(self, node): + def mutate_GtE(self, node: ast.GtE) -> ast.LtE: return ast.LtE() - def mutate_GtE_to_Gt(self, node): + def mutate_GtE_to_Gt(self, node: ast.GtE) -> ast.Gt: return ast.Gt() - def mutate_Eq(self, node): + def mutate_Eq(self, node: ast.Eq) -> ast.NotEq: return ast.NotEq() - def mutate_NotEq(self, node): + def mutate_NotEq(self, node: ast.NotEq) -> ast.Eq: return ast.Eq() diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py index f0dc6ca6..528c1bba 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/loop.py +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -15,22 +15,22 @@ class OneIterationLoop(MutationOperator): - def one_iteration(self, node): + def one_iteration(self, node: ast.For | ast.While) -> ast.For | ast.While: node.body.append(ast.Break(lineno=node.body[-1].lineno + 1)) return node @copy_node - def mutate_For(self, node): + def mutate_For(self, node: ast.For) -> ast.For: return self.one_iteration(node) @copy_node - def mutate_While(self, node): + def mutate_While(self, node: ast.While) -> ast.While: return self.one_iteration(node) class ReverseIterationLoop(MutationOperator): @copy_node - def mutate_For(self, node): + def mutate_For(self, node: ast.For) -> ast.For: old_iter = node.iter node.iter = ast.Call( func=ast.Name(id=reversed.__name__, ctx=ast.Load()), @@ -43,14 +43,14 @@ def mutate_For(self, node): class ZeroIterationLoop(MutationOperator): - def zero_iteration(self, node): + def zero_iteration(self, node: ast.For | ast.While) -> ast.For | ast.While: node.body = [ast.Break(lineno=node.body[0].lineno)] return node @copy_node - def mutate_For(self, node): + def mutate_For(self, node: ast.For) -> ast.For: return self.zero_iteration(node) @copy_node - def mutate_While(self, node): + def mutate_While(self, node: ast.While) -> ast.While: return self.zero_iteration(node) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index 50ca9b07..092e5f10 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -17,19 +17,19 @@ class AssignmentOperatorReplacement(AbstractArithmeticOperatorReplacement): - def should_mutate(self, node): + def should_mutate(self, node: ast.AST) -> bool: return isinstance(node.parent, ast.AugAssign) @classmethod - def name(cls): - return 'ASR' + def name(cls) -> str: + return "ASR" class BreakContinueReplacement(MutationOperator): - def mutate_Break(self, node): + def mutate_Break(self, node: ast.Break) -> ast.Continue: return ast.Continue() - def mutate_Continue(self, node): + def mutate_Continue(self, node: ast.Continue) -> ast.Break: return ast.Break() @@ -37,7 +37,7 @@ class ConstantReplacement(MutationOperator): FIRST_CONST_STRING = 'mutpy' SECOND_CONST_STRING = 'python' - def help_str(self, node): + def help_str(self, node: ast.AST) -> str: if utils.is_docstring(node): raise MutationResign() @@ -46,57 +46,57 @@ def help_str(self, node): else: return self.SECOND_CONST_STRING - def help_str_empty(self, node): + def help_str_empty(self, node: ast.AST) -> str: if not node.s or utils.is_docstring(node): raise MutationResign() return '' - def mutate_Constant_num(self, node): + def mutate_Constant_num(self, node: ast.Constant) -> ast.Constant: if isinstance(node.value, (int, float)) and not isinstance(node.value, bool): return ast.Constant(n=node.n + 1) else: raise MutationResign() - def mutate_Constant_str(self, node): + def mutate_Constant_str(self, node: ast.Constant) -> ast.Constant: if isinstance(node.value, str): return ast.Constant(s=self.help_str(node)) else: raise MutationResign() - def mutate_Constant_str_empty(self, node): + def mutate_Constant_str_empty(self, node: ast.Constant) -> ast.Constant: if isinstance(node.value, str): return ast.Constant(s=self.help_str_empty(node)) else: raise MutationResign() - def mutate_Num(self, node): + def mutate_Num(self, node: ast.Num) -> ast.Num: return ast.Num(n=node.n + 1) - def mutate_Str(self, node): + def mutate_Str(self, node: ast.Str) -> ast.Str: return ast.Str(s=self.help_str(node)) - def mutate_Str_empty(self, node): + def mutate_Str_empty(self, node: ast.Str) -> ast.Str: return ast.Str(s=self.help_str_empty(node)) @classmethod - def name(cls): - return 'CRP' + def name(cls) -> str: + return "CRP" class SliceIndexRemove(MutationOperator): - def mutate_Slice_remove_lower(self, node): + def mutate_Slice_remove_lower(self, node: ast.Slice) -> ast.Slice: if not node.lower: raise MutationResign() return ast.Slice(lower=None, upper=node.upper, step=node.step) - def mutate_Slice_remove_upper(self, node): + def mutate_Slice_remove_upper(self, node: ast.Slice) -> ast.Slice: if not node.upper: raise MutationResign() return ast.Slice(lower=node.lower, upper=None, step=node.step) - def mutate_Slice_remove_step(self, node): + def mutate_Slice_remove_step(self, node: ast.Slice) -> ast.Slice: if not node.step: raise MutationResign() @@ -104,7 +104,7 @@ def mutate_Slice_remove_step(self, node): class SelfVariableDeletion(MutationOperator): - def mutate_Attribute(self, node): + def mutate_Attribute(self, node: ast.Attribute) -> ast.Name: try: if node.value.id == 'self': return ast.Name(id=node.attr, ctx=ast.Load()) @@ -115,17 +115,17 @@ def mutate_Attribute(self, node): class StatementDeletion(MutationOperator): - def mutate_Assign(self, node): + def mutate_Assign(self, node: ast.Assign) -> ast.Pass: return ast.Pass() - def mutate_Return(self, node): + def mutate_Return(self, node: ast.Return) -> ast.Pass: return ast.Pass() - def mutate_Expr(self, node): + def mutate_Expr(self, node: ast.Expr) -> ast.Pass: if utils.is_docstring(node.value): raise MutationResign() return ast.Pass() @classmethod - def name(cls): - return 'SDL' + def name(cls) -> str: + return "SDL" diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 10e6732d..2e3c5201 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -17,15 +17,17 @@ import pkgutil import random import sys -import time import types -from collections import defaultdict from importlib._bootstrap_external import EXTENSION_SUFFIXES, ExtensionFileLoader +from typing import Any +from typing import Generator +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator -def create_module(ast_node, module_name='mutant', module_dict=None): - code = compile(ast_node, module_name, 'exec') + +def create_module(ast_node: ast.Module, module_name: str = "mutant", module_dict: dict[str, Any] | None = None): + code = compile(ast_node, module_name, "exec") module = types.ModuleType(module_name) module.__dict__.update(module_dict or {}) exec(code, module.__dict__) @@ -37,7 +39,7 @@ def notmutate(sth): class ModulesLoaderException(Exception): - def __init__(self, name, exception): + def __init__(self, name: str, exception: Exception) -> None: self.name = name self.exception = exception @@ -46,13 +48,17 @@ def __str__(self): class ModulesLoader: - def __init__(self, names, path): + def __init__(self, names: list[str], path: str | None) -> None: self.names = names self.path = path or '.' self.ensure_in_path(self.path) - def load(self, without_modules=None, exclude_c_extensions=True): - results = [] + def load( + self, + without_modules: list[types.ModuleType] | None = None, + exclude_c_extensions: bool = True, + ) -> Generator[tuple[types.ModuleType, str | None], None, None]: + results: list[tuple[types.ModuleType, str | None]] = [] without_modules = without_modules or [] for name in self.names: results += self.load_single(name) @@ -61,7 +67,7 @@ def load(self, without_modules=None, exclude_c_extensions=True): if module not in without_modules and not (exclude_c_extensions and self._is_c_extension(module)): yield module, to_mutate - def load_single(self, name): + def load_single(self, name: str) -> list[tuple[types.ModuleType, str | None]]: full_path = self.get_full_path(name) if os.path.exists(full_path): if self.is_file(full_path): @@ -73,21 +79,21 @@ def load_single(self, name): else: return self.load_module(name) - def get_full_path(self, name): + def get_full_path(self, name: str) -> str: if os.path.isabs(name): return name return os.path.abspath(os.path.join(self.path, name)) @staticmethod - def is_file(name): + def is_file(name: str) -> bool: return os.path.isfile(name) @staticmethod - def is_directory(name): + def is_directory(name: str) -> bool: return os.path.exists(name) and os.path.isdir(name) @staticmethod - def is_package(name): + def is_package(name: str) -> bool: try: module = importlib.import_module(name) return hasattr(module, '__file__') and module.__file__.endswith('__init__.py') @@ -96,24 +102,25 @@ def is_package(name): finally: sys.path_importer_cache.clear() - def load_file(self, name): + def load_file(self, name: str) -> list[tuple[types.ModuleType, str | None]] | None: if name.endswith('.py'): dirname = os.path.dirname(name) self.ensure_in_path(dirname) module_name = self.get_filename_without_extension(name) return self.load_module(module_name) + return None - def ensure_in_path(self, directory): + def ensure_in_path(self, directory: str) -> None: if directory not in sys.path: sys.path.insert(0, directory) @staticmethod - def get_filename_without_extension(path): + def get_filename_without_extension(path: str) -> str: return os.path.basename(os.path.splitext(path)[0]) @staticmethod - def load_package(name): - result = [] + def load_package(name: str) -> list[tuple[types.ModuleType, str | None]]: + result: list[tuple[types.ModuleType, str | None]] = [] try: package = importlib.import_module(name) for _, module_name, ispkg in pkgutil.walk_packages(package.__path__, package.__name__ + '.'): @@ -127,7 +134,7 @@ def load_package(name): pass return result - def load_directory(self, name): + def load_directory(self, name: str) -> list[tuple[types.ModuleType, str | None]]: if os.path.isfile(os.path.join(name, '__init__.py')): parent_dir = self._get_parent_directory(name) self.ensure_in_path(parent_dir) @@ -140,26 +147,25 @@ def load_directory(self, name): result += modules return result - def load_module(self, name): + def load_module(self, name: str) -> list[tuple[types.ModuleType, str | None]]: module, remainder_path, last_exception = self._split_by_module_and_remainder(name) if not self._module_has_member(module, remainder_path): raise ModulesLoaderException(name, last_exception) return [(module, '.'.join(remainder_path) if remainder_path else None)] @staticmethod - def _get_parent_directory(name): - parent_dir = os.path.abspath(os.path.join(name, os.pardir)) - return parent_dir + def _get_parent_directory(name: str) -> str: + return os.path.abspath(os.path.join(name, os.pardir)) @staticmethod - def _split_by_module_and_remainder(name): + def _split_by_module_and_remainder(name: str) -> tuple[types.ModuleType, list[str], ImportError | None]: """Takes a path string and returns the contained module and the remaining path after it. Example: "mymodule.mysubmodule.MyClass.my_func" -> mysubmodule, "MyClass.my_func" """ module_path = name.split('.') - member_path = [] - last_exception = None + member_path: list[str] = [] + last_exception: ImportError | None = None while True: try: module = importlib.import_module('.'.join(module_path)) @@ -172,7 +178,7 @@ def _split_by_module_and_remainder(name): return module, member_path, last_exception @staticmethod - def _module_has_member(module, member_path): + def _module_has_member(module: types.ModuleType, member_path: str) -> bool: attr = module for part in member_path: if hasattr(attr, part): @@ -182,7 +188,7 @@ def _module_has_member(module, member_path): return True @staticmethod - def _is_c_extension(module): + def _is_c_extension(module: types.ModuleType) -> bool: if isinstance(getattr(module, '__loader__', None), ExtensionFileLoader): return True module_filename = inspect.getfile(module) @@ -190,57 +196,20 @@ def _is_c_extension(module): return module_filetype in EXTENSION_SUFFIXES -class Timer: - time_provider = time.time - - def __init__(self): - self.duration = 0 - self.start = self.time_provider() - - def stop(self): - self.duration = self.time_provider() - self.start - return self.duration - - -class TimeRegister: - executions = defaultdict(float) - timer_class = Timer - stack = [] - - def __init__(self, method): - self.method = method - - def __get__(self, obj, ownerClass=None): - return types.MethodType(self, obj) - - def __call__(self, *args, **kwargs): - if self.stack and self.stack[-1] == self.method: - return self.method(*args, **kwargs) - - self.stack.append(self.method) - time_reg = self.timer_class() - result = self.method(*args, **kwargs) - - self.executions[self.method.__name__] += time_reg.stop() - self.stack.pop() - return result - - @classmethod - def clean(cls): - cls.executions.clear() - cls.stack = [] - - class RandomSampler: - def __init__(self, percentage): + def __init__(self, percentage: int) -> None: self.percentage = percentage if 0 < percentage < 100 else 100 - def is_mutation_time(self): + def is_mutation_time(self) -> bool: return random.randrange(100) < self.percentage class ParentNodeTransformer(ast.NodeTransformer): - def visit(self, node): + def __init__(self) -> None: + super().__init__() + self.parent = None + + def visit(self, node: ast.AST) -> ast.AST: if getattr(node, 'parent', None): node = copy.copy(node) if hasattr(node, 'lineno'): @@ -255,23 +224,20 @@ def visit(self, node): return result_node -def create_ast(code): +def create_ast(code: str) -> ast.AST: return ParentNodeTransformer().visit(ast.parse(code)) -def is_docstring(node): +def is_docstring(node: ast.AST) -> bool: def_node = node.parent.parent - return (isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) and def_node.body and - isinstance(def_node.body[0], ast.Expr) and isinstance(def_node.body[0].value, ast.Str) and - def_node.body[0].value == node) - - -def get_by_python_version(classes, python_version=sys.version_info): - candidates = [cls for cls in classes if cls.__python_version__ <= python_version] - if not candidates: - raise NotImplementedError('MutPy does not support Python {}.'.format(sys.version)) - return max([candidate for candidate in candidates], key=lambda cls: cls.__python_version__) + return ( + isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) + and def_node.body + and isinstance(def_node.body[0], ast.Expr) + and isinstance(def_node.body[0].value, ast.Str) + and def_node.body[0].value == node + ) -def sort_operators(operators): +def sort_operators(operators: list[type[MutationOperator]]) -> list[type[MutationOperator]]: return sorted(operators, key=lambda cls: cls.name()) From 30e7b8759935d743f7b8d1deae12961c7dcdbe30 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 21:53:11 +0100 Subject: [PATCH 03/76] Fix type hint --- src/pynguin/assertion/mutation_analysis/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 634bd40c..69693011 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -31,7 +31,7 @@ def mutate_module( target_module: types.ModuleType, to_mutate: str | None, target_ast: ast.AST, - ) -> Generator[tuple[list[Mutation], types.ModuleType | None], None, None]: + ) -> Generator[tuple[types.ModuleType | None, list[Mutation]], None, None]: for mutations, mutant_ast in self.mutant_generator.mutate( target_ast, to_mutate, From 75a22bf0b69135b07b9d50b203bf1b8d49d9bacf Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 21:59:18 +0100 Subject: [PATCH 04/76] Remove notmutate decorator --- .../assertion/mutation_analysis/operators/base.py | 11 ----------- src/pynguin/assertion/mutation_analysis/utils.py | 4 ---- 2 files changed, 15 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 63ba4171..5712ef04 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -59,8 +59,6 @@ def mutate( yield Mutation(operator=self.__class__, node=self.current_node, visitor=self.visitor), new_node def visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: - if self.has_notmutate(node): - return if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: return self.fix_lineno(node) @@ -122,15 +120,6 @@ def generic_visit_real_node(self, node: ast.AST, field: str, old_value: ast.AST) yield setattr(node, field, old_value) - def has_notmutate(self, node: ast.AST) -> bool: - try: - for decorator in node.decorator_list: - if decorator.id == utils.notmutate.__name__: - return True - return False - except AttributeError: - return False - def fix_lineno(self, node: ast.AST) -> None: if not hasattr(node, 'lineno') and getattr(node, 'parent', None) is not None and hasattr(node.parent, 'lineno'): node.lineno = node.parent.lineno diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 2e3c5201..97c3c809 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -34,10 +34,6 @@ def create_module(ast_node: ast.Module, module_name: str = "mutant", module_dict return module -def notmutate(sth): - return sth - - class ModulesLoaderException(Exception): def __init__(self, name: str, exception: Exception) -> None: self.name = name From 3e7402b7313bf4613270ab74bca6765b3e5de4ad Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:22:24 +0100 Subject: [PATCH 05/76] Remove useless ModulesLoader by importlib.import_module --- .../assertion/mutation_analysis/controller.py | 3 +- .../mutation_analysis/mutationadapter.py | 41 ++--- .../assertion/mutation_analysis/utils.py | 156 ------------------ 3 files changed, 18 insertions(+), 182 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 69693011..573fc617 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -22,8 +22,7 @@ class MutationController: - def __init__(self, target_loader: utils.ModulesLoader, mutant_generator: FirstOrderMutator) -> None: - self.target_loader = target_loader + def __init__(self, mutant_generator: FirstOrderMutator) -> None: self.mutant_generator = mutant_generator def mutate_module( diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 179ee299..0e5d8e56 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -7,6 +7,7 @@ """Provides an adapter for the MutPy mutation testing framework.""" from __future__ import annotations +import importlib import logging from typing import TYPE_CHECKING @@ -14,7 +15,6 @@ import pynguin.assertion.mutation_analysis.controller as mc import pynguin.assertion.mutation_analysis.operators as mo import pynguin.assertion.mutation_analysis.operators.loop as mol -import pynguin.assertion.mutation_analysis.utils as mu import pynguin.configuration as config @@ -42,9 +42,6 @@ class MutationAdapter: config.MutationStrategy.EACH_CHOICE: mc.EachChoiceHOMStrategy, } - def __init__(self): # noqa: D107 - self.target_loader: mu.ModulesLoader | None = None - def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: """Mutates the modules specified in the configuration. @@ -58,32 +55,28 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: mutants = [] - if self.target_loader is not None: - for target_module, to_mutate in self.target_loader.load(): - _LOGGER.info("Build AST for %s", target_module.__name__) - target_ast = controller.create_target_ast(target_module) - _LOGGER.info("Mutate module %s", target_module.__name__) - mutant_modules = controller.mutate_module( - target_module=target_module, - to_mutate=to_mutate, - target_ast=target_ast, - ) - for mutant_module, mutations in mutant_modules: - mutants.append((mutant_module, mutations)) + target_module = importlib.import_module(config.configuration.module_name) + to_mutate = None + + _LOGGER.info("Build AST for %s", target_module.__name__) + target_ast = controller.create_target_ast(target_module) + _LOGGER.info("Mutate module %s", target_module.__name__) + mutant_modules = controller.mutate_module( + target_module=target_module, + to_mutate=to_mutate, + target_ast=target_ast, + ) + + for mutant_module, mutations in mutant_modules: + mutants.append((mutant_module, mutations)) + _LOGGER.info("Generated %d mutants", len(mutants)) return mutants def _build_mutation_controller(self) -> mc.MutationController: _LOGGER.info("Setup mutation controller") mutant_generator = self._get_mutant_generator() - self.target_loader = mu.ModulesLoader( - [config.configuration.module_name], config.configuration.project_path - ) - - return mc.MutationController( - target_loader=self.target_loader, - mutant_generator=mutant_generator, - ) + return mc.MutationController(mutant_generator) def _get_mutant_generator(self) -> mc.FirstOrderMutator: operators_set = set() diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 97c3c809..2bdd4d21 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -11,17 +11,10 @@ import ast import copy -import importlib -import inspect -import os -import pkgutil import random -import sys import types -from importlib._bootstrap_external import EXTENSION_SUFFIXES, ExtensionFileLoader from typing import Any -from typing import Generator from pynguin.assertion.mutation_analysis.operators.base import MutationOperator @@ -43,155 +36,6 @@ def __str__(self): return "can't load {}".format(self.name) -class ModulesLoader: - def __init__(self, names: list[str], path: str | None) -> None: - self.names = names - self.path = path or '.' - self.ensure_in_path(self.path) - - def load( - self, - without_modules: list[types.ModuleType] | None = None, - exclude_c_extensions: bool = True, - ) -> Generator[tuple[types.ModuleType, str | None], None, None]: - results: list[tuple[types.ModuleType, str | None]] = [] - without_modules = without_modules or [] - for name in self.names: - results += self.load_single(name) - for module, to_mutate in results: - # yield only if module is not explicitly excluded and only source modules (.py) if demanded - if module not in without_modules and not (exclude_c_extensions and self._is_c_extension(module)): - yield module, to_mutate - - def load_single(self, name: str) -> list[tuple[types.ModuleType, str | None]]: - full_path = self.get_full_path(name) - if os.path.exists(full_path): - if self.is_file(full_path): - return self.load_file(full_path) - elif self.is_directory(full_path): - return self.load_directory(full_path) - if self.is_package(name): - return self.load_package(name) - else: - return self.load_module(name) - - def get_full_path(self, name: str) -> str: - if os.path.isabs(name): - return name - return os.path.abspath(os.path.join(self.path, name)) - - @staticmethod - def is_file(name: str) -> bool: - return os.path.isfile(name) - - @staticmethod - def is_directory(name: str) -> bool: - return os.path.exists(name) and os.path.isdir(name) - - @staticmethod - def is_package(name: str) -> bool: - try: - module = importlib.import_module(name) - return hasattr(module, '__file__') and module.__file__.endswith('__init__.py') - except ImportError: - return False - finally: - sys.path_importer_cache.clear() - - def load_file(self, name: str) -> list[tuple[types.ModuleType, str | None]] | None: - if name.endswith('.py'): - dirname = os.path.dirname(name) - self.ensure_in_path(dirname) - module_name = self.get_filename_without_extension(name) - return self.load_module(module_name) - return None - - def ensure_in_path(self, directory: str) -> None: - if directory not in sys.path: - sys.path.insert(0, directory) - - @staticmethod - def get_filename_without_extension(path: str) -> str: - return os.path.basename(os.path.splitext(path)[0]) - - @staticmethod - def load_package(name: str) -> list[tuple[types.ModuleType, str | None]]: - result: list[tuple[types.ModuleType, str | None]] = [] - try: - package = importlib.import_module(name) - for _, module_name, ispkg in pkgutil.walk_packages(package.__path__, package.__name__ + '.'): - if not ispkg: - try: - module = importlib.import_module(module_name) - result.append((module, None)) - except ImportError as _: - pass - except ImportError as _: - pass - return result - - def load_directory(self, name: str) -> list[tuple[types.ModuleType, str | None]]: - if os.path.isfile(os.path.join(name, '__init__.py')): - parent_dir = self._get_parent_directory(name) - self.ensure_in_path(parent_dir) - return self.load_package(os.path.basename(name)) - else: - result = [] - for file in os.listdir(name): - modules = self.load_single(os.path.join(name, file)) - if modules: - result += modules - return result - - def load_module(self, name: str) -> list[tuple[types.ModuleType, str | None]]: - module, remainder_path, last_exception = self._split_by_module_and_remainder(name) - if not self._module_has_member(module, remainder_path): - raise ModulesLoaderException(name, last_exception) - return [(module, '.'.join(remainder_path) if remainder_path else None)] - - @staticmethod - def _get_parent_directory(name: str) -> str: - return os.path.abspath(os.path.join(name, os.pardir)) - - @staticmethod - def _split_by_module_and_remainder(name: str) -> tuple[types.ModuleType, list[str], ImportError | None]: - """Takes a path string and returns the contained module and the remaining path after it. - - Example: "mymodule.mysubmodule.MyClass.my_func" -> mysubmodule, "MyClass.my_func" - """ - module_path = name.split('.') - member_path: list[str] = [] - last_exception: ImportError | None = None - while True: - try: - module = importlib.import_module('.'.join(module_path)) - break - except ImportError as error: - member_path = [module_path.pop()] + member_path - last_exception = error - if not module_path: - raise ModulesLoaderException(name, last_exception) - return module, member_path, last_exception - - @staticmethod - def _module_has_member(module: types.ModuleType, member_path: str) -> bool: - attr = module - for part in member_path: - if hasattr(attr, part): - attr = getattr(attr, part) - else: - return False - return True - - @staticmethod - def _is_c_extension(module: types.ModuleType) -> bool: - if isinstance(getattr(module, '__loader__', None), ExtensionFileLoader): - return True - module_filename = inspect.getfile(module) - module_filetype = os.path.splitext(module_filename)[1] - return module_filetype in EXTENSION_SUFFIXES - - class RandomSampler: def __init__(self, percentage: int) -> None: self.percentage = percentage if 0 < percentage < 100 else 100 From d41bf9400a30553a91778647b5d6c6b59e5c7e67 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:27:00 +0100 Subject: [PATCH 06/76] Remove unused to_mutate parameter --- .../assertion/mutation_analysis/controller.py | 12 +++--------- .../assertion/mutation_analysis/mutationadapter.py | 2 -- .../assertion/mutation_analysis/operators/base.py | 2 -- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 573fc617..f095facb 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -28,12 +28,10 @@ def __init__(self, mutant_generator: FirstOrderMutator) -> None: def mutate_module( self, target_module: types.ModuleType, - to_mutate: str | None, target_ast: ast.AST, ) -> Generator[tuple[types.ModuleType | None, list[Mutation]], None, None]: for mutations, mutant_ast in self.mutant_generator.mutate( target_ast, - to_mutate, module=target_module, ): yield self.create_mutant_module(target_module, mutant_ast), mutations @@ -173,11 +171,10 @@ def __init__(self, operators: list[type[MutationOperator]], percentage: int = 10 def mutate( self, target_ast: ast.AST, - to_mutate: str | None = None, module: types.ModuleType | None = None, ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: for op in utils.sort_operators(self.operators): - for mutation, mutant in op().mutate(target_ast, to_mutate, self.sampler, module=module): + for mutation, mutant in op().mutate(target_ast, self.sampler, module=module): yield [mutation], mutant @@ -195,10 +192,9 @@ def __init__( def mutate( self, target_ast: ast.AST, - to_mutate: str | None = None, module: types.ModuleType | None = None, ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: - mutations = self.generate_all_mutations(module, target_ast, to_mutate) + mutations = self.generate_all_mutations(module, target_ast) for mutations_to_apply in self.hom_strategy.generate(mutations): generators = [] applied_mutations = [] @@ -206,7 +202,6 @@ def mutate( for mutation in mutations_to_apply: generator = mutation.operator().mutate( mutant, - to_mutate=to_mutate, sampler=self.sampler, module=module, only_mutation=mutation, @@ -224,11 +219,10 @@ def generate_all_mutations( self, module: types.ModuleType | None, target_ast: ast.AST, - to_mutate: str | None, ) -> list[Mutation]: mutations: list[Mutation] = [] for op in utils.sort_operators(self.operators): - for mutation, _ in op().mutate(target_ast, to_mutate, None, module=module): + for mutation, _ in op().mutate(target_ast, None, module=module): mutations.append(mutation) return mutations diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 0e5d8e56..31ca776b 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -56,14 +56,12 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: mutants = [] target_module = importlib.import_module(config.configuration.module_name) - to_mutate = None _LOGGER.info("Build AST for %s", target_module.__name__) target_ast = controller.create_target_ast(target_module) _LOGGER.info("Mutate module %s", target_module.__name__) mutant_modules = controller.mutate_module( target_module=target_module, - to_mutate=to_mutate, target_ast=target_ast, ) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 5712ef04..d8960fdf 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -46,12 +46,10 @@ class MutationOperator: def mutate( self, node: ast.AST, - to_mutate: str | None = None, sampler: utils.RandomSampler | None = None, module: types.ModuleType | None = None, only_mutation: Mutation | None = None ): - self.to_mutate = to_mutate self.sampler = sampler self.only_mutation = only_mutation self.module = module From 30f4425b54f0772550acda14d92f624d694fa0d3 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:27:56 +0100 Subject: [PATCH 07/76] Remove unused ModulesLoaderException class --- src/pynguin/assertion/mutation_analysis/utils.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 2bdd4d21..11efe029 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -27,15 +27,6 @@ def create_module(ast_node: ast.Module, module_name: str = "mutant", module_dict return module -class ModulesLoaderException(Exception): - def __init__(self, name: str, exception: Exception) -> None: - self.name = name - self.exception = exception - - def __str__(self): - return "can't load {}".format(self.name) - - class RandomSampler: def __init__(self, percentage: int) -> None: self.percentage = percentage if 0 < percentage < 100 else 100 From 4d3055f787edc53c0b7fc90732a3d28e4ef39e3d Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:37:00 +0100 Subject: [PATCH 08/76] Use inspect.getsource instead of open to get the source code of a module --- src/pynguin/assertion/mutation_analysis/controller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index f095facb..b4a3a529 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -11,6 +11,7 @@ from __future__ import annotations import ast +import inspect import random import types @@ -37,8 +38,8 @@ def mutate_module( yield self.create_mutant_module(target_module, mutant_ast), mutations def create_target_ast(self, target_module: types.ModuleType) -> ast.AST: - with open(target_module.__file__) as target_file: - return utils.create_ast(target_file.read()) + target_source_code = inspect.getsource(target_module) + return utils.create_ast(target_source_code) def create_mutant_module(self, target_module: types.ModuleType, mutant_ast: ast.Module) -> types.ModuleType | None: try: From 52e99decc600cabb207cb48489d604fd0596085e Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:08:37 +0100 Subject: [PATCH 09/76] Refactor ParentNodeTransformer to use setattr, getattr and hasattr --- .../assertion/mutation_analysis/utils.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 11efe029..aa8652dd 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -38,20 +38,28 @@ def is_mutation_time(self) -> bool: class ParentNodeTransformer(ast.NodeTransformer): def __init__(self) -> None: super().__init__() - self.parent = None + self.parent: ast.AST | None = None def visit(self, node: ast.AST) -> ast.AST: - if getattr(node, 'parent', None): + if getattr(node, "parent", None) is not None: node = copy.copy(node) - if hasattr(node, 'lineno'): - del node.lineno - node.parent = getattr(self, 'parent', None) - node.children = [] + if hasattr(node, "lineno"): + delattr(node, "lineno") + + setattr(node, "parent", self.parent) + setattr(node, "children", []) + self.parent = node + result_node = super().visit(node) - self.parent = node.parent - if self.parent: - self.parent.children += [node] + node.children + + self.parent = getattr(node, "parent", None) + + if self.parent is not None: + node_children = getattr(node, "children", []) + parent_children = getattr(self.parent, "children", []) + setattr(self.parent, "children", parent_children + [node] + node_children) + return result_node From 655c1a779c9ae133a49fc9743e587a86b5974797 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 00:07:30 +0100 Subject: [PATCH 10/76] Refactor ParentNodeTransformer to be more performant and explicit --- .../assertion/mutation_analysis/utils.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index aa8652dd..9f1f74c2 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -41,26 +41,39 @@ def __init__(self) -> None: self.parent: ast.AST | None = None def visit(self, node: ast.AST) -> ast.AST: - if getattr(node, "parent", None) is not None: + # Copy the node because an optimisation of the AST makes it + # reuse the same node at multiple places in the tree to + # improve memory usage. It would break our goal to create a + # tree with a single parent for each node if we don't copy. + if hasattr(node, "parent"): node = copy.copy(node) if hasattr(node, "lineno"): delattr(node, "lineno") setattr(node, "parent", self.parent) - setattr(node, "children", []) + setattr(node, "children", set()) + parent_save = self.parent self.parent = node - result_node = super().visit(node) + # Visit the children of the node and discard the result + # as it returns the same node with the children modified. + super().visit(node) - self.parent = getattr(node, "parent", None) + self.parent = parent_save + # Add all the ancestors of the node to the children list + # of the parent if it exists. This is done here so that + # the tree has been fully traversed before adding the children. if self.parent is not None: - node_children = getattr(node, "children", []) - parent_children = getattr(self.parent, "children", []) - setattr(self.parent, "children", parent_children + [node] + node_children) + parent_children: set[ast.AST] = getattr(self.parent, "children") - return result_node + parent_children.add(node) + + node_children: set[ast.AST] = getattr(node, "children") + parent_children.update(node_children) + + return node def create_ast(code: str) -> ast.AST: From 500640f096b1f9a53ca4183124472649e88cc608 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 00:34:41 +0100 Subject: [PATCH 11/76] Refactor controller module --- .../assertion/mutation_analysis/controller.py | 80 ++++++++----------- .../assertion/mutation_analysis/utils.py | 4 - 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index b4a3a529..b8f70819 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -63,68 +63,64 @@ def remove_bad_mutations( allow_same_operators: bool = True ) -> None: for mutation_to_apply in mutations_to_apply: - for available_mutation in available_mutations[:]: - if mutation_to_apply.node == available_mutation.node or \ - mutation_to_apply.node in available_mutation.node.children or \ - available_mutation.node in mutation_to_apply.node.children or \ - (not allow_same_operators and mutation_to_apply.operator == available_mutation.operator): + for available_mutation in available_mutations.copy(): + if ( + mutation_to_apply.node == available_mutation.node + or mutation_to_apply.node in getattr(available_mutation.node, "children") + or available_mutation.node in getattr(mutation_to_apply.node, "children") + or ( + not allow_same_operators + and mutation_to_apply.operator == available_mutation.operator + ) + ): available_mutations.remove(available_mutation) class FirstToLastHOMStrategy(HOMStrategy): - name = 'FIRST_TO_LAST' def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: - mutations = mutations[:] + mutations = mutations.copy() while mutations: mutations_to_apply: list[Mutation] = [] index = 0 - available_mutations = mutations[:] + available_mutations = mutations.copy() while len(mutations_to_apply) < self.order and available_mutations: - try: - mutation = available_mutations.pop(index) - mutations_to_apply.append(mutation) - mutations.remove(mutation) - index = 0 if index == -1 else -1 - except IndexError: - break + mutation = available_mutations.pop(index) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + index = 0 if index == -1 else -1 self.remove_bad_mutations(mutations_to_apply, available_mutations) yield mutations_to_apply class EachChoiceHOMStrategy(HOMStrategy): - name = 'EACH_CHOICE' def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: - mutations = mutations[:] + mutations = mutations.copy() while mutations: mutations_to_apply: list[Mutation] = [] - available_mutations = mutations[:] + available_mutations = mutations.copy() while len(mutations_to_apply) < self.order and available_mutations: - try: - mutation = available_mutations.pop(0) - mutations_to_apply.append(mutation) - mutations.remove(mutation) - except IndexError: - break + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) self.remove_bad_mutations(mutations_to_apply, available_mutations) yield mutations_to_apply class BetweenOperatorsHOMStrategy(HOMStrategy): - name = 'BETWEEN_OPERATORS' def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: usage = {mutation: 0 for mutation in mutations} - not_used = mutations[:] + not_used = mutations.copy() while not_used: mutations_to_apply: list[Mutation] = [] - available_mutations = mutations[:] + available_mutations = mutations.copy() available_mutations.sort(key=lambda x: usage[x]) while len(mutations_to_apply) < self.order and available_mutations: mutation = available_mutations.pop(0) mutations_to_apply.append(mutation) - if not usage[mutation]: + if usage[mutation] == 0: not_used.remove(mutation) usage[mutation] += 1 self.remove_bad_mutations(mutations_to_apply, available_mutations, allow_same_operators=False) @@ -132,37 +128,25 @@ def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, class RandomHOMStrategy(HOMStrategy): - name = 'RANDOM' def __init__(self, order: int = 2, shuffler: Callable = random.shuffle) -> None: super().__init__(order) self.shuffler = shuffler def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: - mutations = mutations[:] + mutations = mutations.copy() self.shuffler(mutations) while mutations: - mutations_to_apply = [] - available_mutations = mutations[:] + mutations_to_apply: list[Mutation] = [] + available_mutations = mutations.copy() while len(mutations_to_apply) < self.order and available_mutations: - try: - mutation = available_mutations.pop(0) - mutations_to_apply.append(mutation) - mutations.remove(mutation) - except IndexError: - break + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) self.remove_bad_mutations(mutations_to_apply, available_mutations) yield mutations_to_apply -hom_strategies = [ - BetweenOperatorsHOMStrategy, - EachChoiceHOMStrategy, - FirstToLastHOMStrategy, - RandomHOMStrategy, -] - - class FirstOrderMutator: def __init__(self, operators: list[type[MutationOperator]], percentage: int = 100) -> None: @@ -174,7 +158,7 @@ def mutate( target_ast: ast.AST, module: types.ModuleType | None = None, ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: - for op in utils.sort_operators(self.operators): + for op in self.operators: for mutation, mutant in op().mutate(target_ast, self.sampler, module=module): yield [mutation], mutant @@ -222,7 +206,7 @@ def generate_all_mutations( target_ast: ast.AST, ) -> list[Mutation]: mutations: list[Mutation] = [] - for op in utils.sort_operators(self.operators): + for op in self.operators: for mutation, _ in op().mutate(target_ast, None, module=module): mutations.append(mutation) return mutations diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 9f1f74c2..1da6493a 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -89,7 +89,3 @@ def is_docstring(node: ast.AST) -> bool: and isinstance(def_node.body[0].value, ast.Str) and def_node.body[0].value == node ) - - -def sort_operators(operators: list[type[MutationOperator]]) -> list[type[MutationOperator]]: - return sorted(operators, key=lambda cls: cls.name()) From c5b8d0a49c78f081c9305157abe67fc771cb96e0 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 00:35:25 +0100 Subject: [PATCH 12/76] Remove unused dependencies --- src/pynguin/assertion/mutation_analysis/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 1da6493a..0850f204 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -16,8 +16,6 @@ from typing import Any -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator - def create_module(ast_node: ast.Module, module_name: str = "mutant", module_dict: dict[str, Any] | None = None): code = compile(ast_node, module_name, "exec") From 709747f4e0e4a4559b1841e4dda72db20e462da5 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:54:39 +0100 Subject: [PATCH 13/76] Move HOM strategies to another file --- .../assertion/mutation_analysis/controller.py | 101 +--------------- .../mutation_analysis/mutationadapter.py | 11 +- .../assertion/mutation_analysis/stategies.py | 113 ++++++++++++++++++ 3 files changed, 121 insertions(+), 104 deletions(-) create mode 100644 src/pynguin/assertion/mutation_analysis/stategies.py diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index b8f70819..256d9d76 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -12,14 +12,13 @@ import ast import inspect -import random import types -from typing import Generator, Callable +from typing import Generator from pynguin.assertion.mutation_analysis import utils from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator - +from pynguin.assertion.mutation_analysis.stategies import HOMStrategy, FirstToLastHOMStrategy class MutationController: @@ -51,102 +50,6 @@ def create_mutant_module(self, target_module: types.ModuleType, mutant_ast: ast. return None -class HOMStrategy: - - def __init__(self, order: int = 2) -> None: - self.order = order - - def remove_bad_mutations( - self, - mutations_to_apply: list[Mutation], - available_mutations: list[Mutation], - allow_same_operators: bool = True - ) -> None: - for mutation_to_apply in mutations_to_apply: - for available_mutation in available_mutations.copy(): - if ( - mutation_to_apply.node == available_mutation.node - or mutation_to_apply.node in getattr(available_mutation.node, "children") - or available_mutation.node in getattr(mutation_to_apply.node, "children") - or ( - not allow_same_operators - and mutation_to_apply.operator == available_mutation.operator - ) - ): - available_mutations.remove(available_mutation) - - -class FirstToLastHOMStrategy(HOMStrategy): - - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: - mutations = mutations.copy() - while mutations: - mutations_to_apply: list[Mutation] = [] - index = 0 - available_mutations = mutations.copy() - while len(mutations_to_apply) < self.order and available_mutations: - mutation = available_mutations.pop(index) - mutations_to_apply.append(mutation) - mutations.remove(mutation) - index = 0 if index == -1 else -1 - self.remove_bad_mutations(mutations_to_apply, available_mutations) - yield mutations_to_apply - - -class EachChoiceHOMStrategy(HOMStrategy): - - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: - mutations = mutations.copy() - while mutations: - mutations_to_apply: list[Mutation] = [] - available_mutations = mutations.copy() - while len(mutations_to_apply) < self.order and available_mutations: - mutation = available_mutations.pop(0) - mutations_to_apply.append(mutation) - mutations.remove(mutation) - self.remove_bad_mutations(mutations_to_apply, available_mutations) - yield mutations_to_apply - - -class BetweenOperatorsHOMStrategy(HOMStrategy): - - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: - usage = {mutation: 0 for mutation in mutations} - not_used = mutations.copy() - while not_used: - mutations_to_apply: list[Mutation] = [] - available_mutations = mutations.copy() - available_mutations.sort(key=lambda x: usage[x]) - while len(mutations_to_apply) < self.order and available_mutations: - mutation = available_mutations.pop(0) - mutations_to_apply.append(mutation) - if usage[mutation] == 0: - not_used.remove(mutation) - usage[mutation] += 1 - self.remove_bad_mutations(mutations_to_apply, available_mutations, allow_same_operators=False) - yield mutations_to_apply - - -class RandomHOMStrategy(HOMStrategy): - - def __init__(self, order: int = 2, shuffler: Callable = random.shuffle) -> None: - super().__init__(order) - self.shuffler = shuffler - - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: - mutations = mutations.copy() - self.shuffler(mutations) - while mutations: - mutations_to_apply: list[Mutation] = [] - available_mutations = mutations.copy() - while len(mutations_to_apply) < self.order and available_mutations: - mutation = available_mutations.pop(0) - mutations_to_apply.append(mutation) - mutations.remove(mutation) - self.remove_bad_mutations(mutations_to_apply, available_mutations) - yield mutations_to_apply - - class FirstOrderMutator: def __init__(self, operators: list[type[MutationOperator]], percentage: int = 100) -> None: diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 31ca776b..4029f164 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -15,6 +15,7 @@ import pynguin.assertion.mutation_analysis.controller as mc import pynguin.assertion.mutation_analysis.operators as mo import pynguin.assertion.mutation_analysis.operators.loop as mol +import pynguin.assertion.mutation_analysis.stategies as ms import pynguin.configuration as config @@ -34,12 +35,12 @@ class MutationAdapter: """Adapter class for interactions with the MutPy mutation testing framework.""" _strategies: ClassVar[ - dict[config.MutationStrategy, Callable[[int], mc.HOMStrategy]] + dict[config.MutationStrategy, Callable[[int], ms.HOMStrategy]] ] = { - config.MutationStrategy.FIRST_TO_LAST: mc.FirstToLastHOMStrategy, - config.MutationStrategy.BETWEEN_OPERATORS: mc.BetweenOperatorsHOMStrategy, - config.MutationStrategy.RANDOM: mc.RandomHOMStrategy, - config.MutationStrategy.EACH_CHOICE: mc.EachChoiceHOMStrategy, + config.MutationStrategy.FIRST_TO_LAST: ms.FirstToLastHOMStrategy, + config.MutationStrategy.BETWEEN_OPERATORS: ms.BetweenOperatorsHOMStrategy, + config.MutationStrategy.RANDOM: ms.RandomHOMStrategy, + config.MutationStrategy.EACH_CHOICE: ms.EachChoiceHOMStrategy, } def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: diff --git a/src/pynguin/assertion/mutation_analysis/stategies.py b/src/pynguin/assertion/mutation_analysis/stategies.py new file mode 100644 index 00000000..6e9cbb58 --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/stategies.py @@ -0,0 +1,113 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py. +""" +from __future__ import annotations + +import random + +from typing import Generator, Callable + +from pynguin.assertion.mutation_analysis.operators.base import Mutation + + +def remove_bad_mutations( + mutations_to_apply: list[Mutation], + available_mutations: list[Mutation], + allow_same_operators: bool = True +) -> None: + for mutation_to_apply in mutations_to_apply: + for available_mutation in available_mutations.copy(): + if ( + mutation_to_apply.node == available_mutation.node + or mutation_to_apply.node in getattr(available_mutation.node, "children") + or available_mutation.node in getattr(mutation_to_apply.node, "children") + or ( + not allow_same_operators + and mutation_to_apply.operator == available_mutation.operator + ) + ): + available_mutations.remove(available_mutation) + + +class HOMStrategy: + + def __init__(self, order: int = 2) -> None: + self.order = order + + +class FirstToLastHOMStrategy(HOMStrategy): + + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + mutations = mutations.copy() + while mutations: + mutations_to_apply: list[Mutation] = [] + index = 0 + available_mutations = mutations.copy() + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(index) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + index = 0 if index == -1 else -1 + remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply + + +class EachChoiceHOMStrategy(HOMStrategy): + + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + mutations = mutations.copy() + while mutations: + mutations_to_apply: list[Mutation] = [] + available_mutations = mutations.copy() + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply + + +class BetweenOperatorsHOMStrategy(HOMStrategy): + + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + usage = {mutation: 0 for mutation in mutations} + not_used = mutations.copy() + while not_used: + mutations_to_apply: list[Mutation] = [] + available_mutations = mutations.copy() + available_mutations.sort(key=lambda x: usage[x]) + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + if usage[mutation] == 0: + not_used.remove(mutation) + usage[mutation] += 1 + remove_bad_mutations(mutations_to_apply, available_mutations, allow_same_operators=False) + yield mutations_to_apply + + +class RandomHOMStrategy(HOMStrategy): + + def __init__(self, order: int = 2, shuffler: Callable = random.shuffle) -> None: + super().__init__(order) + self.shuffler = shuffler + + def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + mutations = mutations.copy() + self.shuffler(mutations) + while mutations: + mutations_to_apply: list[Mutation] = [] + available_mutations = mutations.copy() + while len(mutations_to_apply) < self.order and available_mutations: + mutation = available_mutations.pop(0) + mutations_to_apply.append(mutation) + mutations.remove(mutation) + remove_bad_mutations(mutations_to_apply, available_mutations) + yield mutations_to_apply From c057ab4deafb3a13249c6e702cda3d6fbeb51ee9 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:14:13 +0100 Subject: [PATCH 14/76] Refactor is_docstring --- src/pynguin/assertion/mutation_analysis/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 0850f204..15a320bc 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -79,11 +79,18 @@ def create_ast(code: str) -> ast.AST: def is_docstring(node: ast.AST) -> bool: - def_node = node.parent.parent + if not isinstance(node, ast.Str): + return False + + expression_node = getattr(node, "parent") + + if not isinstance(expression_node, ast.Expr): + return False + + def_node = getattr(expression_node, "parent") + return ( isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) and def_node.body - and isinstance(def_node.body[0], ast.Expr) - and isinstance(def_node.body[0].value, ast.Str) - and def_node.body[0].value == node + and def_node.body[0] == expression_node ) From 74a933fc46f3a453617aaeff63dfe38db01d1038 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:46:14 +0100 Subject: [PATCH 15/76] Move mutators to another file --- .../assertion/mutation_analysis/controller.py | 80 +------------ .../mutation_analysis/mutationadapter.py | 7 +- .../assertion/mutation_analysis/mutators.py | 112 ++++++++++++++++++ 3 files changed, 120 insertions(+), 79 deletions(-) create mode 100644 src/pynguin/assertion/mutation_analysis/mutators.py diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 256d9d76..67d04eeb 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -17,12 +17,13 @@ from typing import Generator from pynguin.assertion.mutation_analysis import utils -from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator -from pynguin.assertion.mutation_analysis.stategies import HOMStrategy, FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.operators.base import Mutation +from pynguin.assertion.mutation_analysis.mutators import Mutator + class MutationController: - def __init__(self, mutant_generator: FirstOrderMutator) -> None: + def __init__(self, mutant_generator: Mutator) -> None: self.mutant_generator = mutant_generator def mutate_module( @@ -48,76 +49,3 @@ def create_mutant_module(self, target_module: types.ModuleType, mutant_ast: ast. ) except BaseException: return None - - -class FirstOrderMutator: - - def __init__(self, operators: list[type[MutationOperator]], percentage: int = 100) -> None: - self.operators = operators - self.sampler = utils.RandomSampler(percentage) - - def mutate( - self, - target_ast: ast.AST, - module: types.ModuleType | None = None, - ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: - for op in self.operators: - for mutation, mutant in op().mutate(target_ast, self.sampler, module=module): - yield [mutation], mutant - - -class HighOrderMutator(FirstOrderMutator): - - def __init__( - self, - operators: list[type[MutationOperator]], - percentage: int = 100, - hom_strategy: HOMStrategy | None = None, - ) -> None: - super().__init__(operators, percentage) - self.hom_strategy = hom_strategy or FirstToLastHOMStrategy() - - def mutate( - self, - target_ast: ast.AST, - module: types.ModuleType | None = None, - ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: - mutations = self.generate_all_mutations(module, target_ast) - for mutations_to_apply in self.hom_strategy.generate(mutations): - generators = [] - applied_mutations = [] - mutant = target_ast - for mutation in mutations_to_apply: - generator = mutation.operator().mutate( - mutant, - sampler=self.sampler, - module=module, - only_mutation=mutation, - ) - try: - new_mutation, mutant = generator.__next__() - except StopIteration: - assert False, 'no mutations!' - applied_mutations.append(new_mutation) - generators.append(generator) - yield applied_mutations, mutant - self.finish_generators(generators) - - def generate_all_mutations( - self, - module: types.ModuleType | None, - target_ast: ast.AST, - ) -> list[Mutation]: - mutations: list[Mutation] = [] - for op in self.operators: - for mutation, _ in op().mutate(target_ast, None, module=module): - mutations.append(mutation) - return mutations - - def finish_generators(self, generators: list[Generator]) -> None: - for generator in reversed(generators): - try: - generator.__next__() - except StopIteration: - continue - assert False, 'too many mutations!' diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 4029f164..e69cc60a 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -16,6 +16,7 @@ import pynguin.assertion.mutation_analysis.operators as mo import pynguin.assertion.mutation_analysis.operators.loop as mol import pynguin.assertion.mutation_analysis.stategies as ms +import pynguin.assertion.mutation_analysis.mutators as mu import pynguin.configuration as config @@ -77,7 +78,7 @@ def _build_mutation_controller(self) -> mc.MutationController: mutant_generator = self._get_mutant_generator() return mc.MutationController(mutant_generator) - def _get_mutant_generator(self) -> mc.FirstOrderMutator: + def _get_mutant_generator(self) -> mu.FirstOrderMutator: operators_set = set() operators_set |= mo.standard_operators @@ -94,7 +95,7 @@ def _get_mutant_generator(self) -> mc.FirstOrderMutator: mutation_strategy = config.configuration.test_case_output.mutation_strategy if mutation_strategy == config.MutationStrategy.FIRST_ORDER_MUTANTS: - return mc.FirstOrderMutator(operators_set, percentage) + return mu.FirstOrderMutator(operators_set, percentage) order = config.configuration.test_case_output.mutation_order if order <= 0: @@ -102,5 +103,5 @@ def _get_mutant_generator(self) -> mc.FirstOrderMutator: if mutation_strategy in self._strategies: hom_strategy = self._strategies[mutation_strategy](order) - return mc.HighOrderMutator(operators_set, percentage, hom_strategy) + return mu.HighOrderMutator(operators_set, percentage, hom_strategy) raise ConfigurationException("No suitable mutation strategy found.") diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py new file mode 100644 index 00000000..ac90b33b --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -0,0 +1,112 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py. +""" +from __future__ import annotations + +import ast +import abc +import types + +from typing import Generator + +from pynguin.assertion.mutation_analysis import utils +from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator +from pynguin.assertion.mutation_analysis.stategies import HOMStrategy, FirstToLastHOMStrategy + + +class Mutator(abc.ABC): + @abc.abstractmethod + def mutate( + self, + target_ast: ast.AST, + module: types.ModuleType | None = None, + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: + """Mutate the given AST. + + Args: + target_ast: The AST to mutate. + module: The module to mutate. + + Returns: + A generator of mutations and the mutated AST. + """ + + +class FirstOrderMutator(Mutator): + + def __init__(self, operators: list[type[MutationOperator]], percentage: int = 100) -> None: + self.operators = operators + self.sampler = utils.RandomSampler(percentage) + + def mutate( + self, + target_ast: ast.AST, + module: types.ModuleType | None = None, + ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: + for op in self.operators: + for mutation, mutant in op().mutate(target_ast, self.sampler, module=module): + yield [mutation], mutant + + +class HighOrderMutator(FirstOrderMutator): + + def __init__( + self, + operators: list[type[MutationOperator]], + percentage: int = 100, + hom_strategy: HOMStrategy | None = None, + ) -> None: + super().__init__(operators, percentage) + self.hom_strategy = hom_strategy or FirstToLastHOMStrategy() + + def mutate( + self, + target_ast: ast.AST, + module: types.ModuleType | None = None, + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: + mutations = self.generate_all_mutations(module, target_ast) + for mutations_to_apply in self.hom_strategy.generate(mutations): + generators = [] + applied_mutations = [] + mutant = target_ast + for mutation in mutations_to_apply: + generator = mutation.operator().mutate( + mutant, + sampler=self.sampler, + module=module, + only_mutation=mutation, + ) + try: + new_mutation, mutant = generator.__next__() + except StopIteration: + assert False, 'no mutations!' + applied_mutations.append(new_mutation) + generators.append(generator) + yield applied_mutations, mutant + self.finish_generators(generators) + + def generate_all_mutations( + self, + module: types.ModuleType | None, + target_ast: ast.AST, + ) -> list[Mutation]: + mutations: list[Mutation] = [] + for op in self.operators: + for mutation, _ in op().mutate(target_ast, None, module=module): + mutations.append(mutation) + return mutations + + def finish_generators(self, generators: list[Generator]) -> None: + for generator in reversed(generators): + try: + generator.__next__() + except StopIteration: + continue + assert False, 'too many mutations!' From 868c1be88fc19445bf9780fe7cc61cf1837b23b9 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:58:24 +0100 Subject: [PATCH 16/76] Remove operators names --- .../mutation_analysis/operators/base.py | 8 -------- .../mutation_analysis/operators/decorator.py | 4 ---- .../mutation_analysis/operators/exception.py | 4 ---- .../mutation_analysis/operators/inheritance.py | 16 ---------------- .../mutation_analysis/operators/misc.py | 12 ------------ 5 files changed, 44 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index d8960fdf..5d2953e2 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -152,14 +152,6 @@ def shift_lines(self, nodes: list[ast.AST], shift_by: int = 1) -> None: for node in nodes: ast.increment_lineno(node, shift_by) - @classmethod - def name(cls) -> str: - return "".join([c for c in cls.__name__ if str.isupper(c)]) - - @classmethod - def long_name(cls) -> str: - return " ".join(map(str.lower, (re.split('([A-Z][a-z]*)', cls.__name__)[1::2]))) - class AbstractUnaryOperatorDeletion(MutationOperator): def mutate_UnaryOp(self, node: ast.UnaryOp) -> ast.expr: diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 7dbffd7e..482abd9f 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -23,10 +23,6 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: else: raise MutationResign() - @classmethod - def name(cls) -> str: - return "DDL" - class AbstractMethodDecoratorInsertionMutationOperator(MutationOperator): @copy_node diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py index 338c8ef0..5d1dcac2 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/exception.py +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -34,7 +34,3 @@ def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler: if len(node.body) == 1 and isinstance(node.body[0], ast.Pass): raise MutationResign() return self._replace_exception_body(node, [ast.Pass(lineno=node.body[0].lineno)]) - - @classmethod - def name(cls) -> str: - return "EXS" diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 6b1a5372..60614965 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -74,10 +74,6 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt: value.elts = new_values return node - @classmethod - def name(cls) -> str: - return "IHD" - class AbstractSuperCallingModification(MutationOperator): def is_super_call(self, node: ast.AST, stmt: ast.stmt) -> bool: @@ -126,10 +122,6 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: node.body.insert(0, super_call) return node - @classmethod - def name(cls) -> str: - return "IOP" - class OverridingMethodDeletion(AbstractOverriddenElementModification): def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass: @@ -137,10 +129,6 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass: return ast.Pass() raise MutationResign() - @classmethod - def name(cls) -> str: - return "IOD" - class SuperCallingDeletion(AbstractSuperCallingModification): @copy_node @@ -193,7 +181,3 @@ def add_kwarg_to_super_call(super_call: ast.Expr, kwarg: ast.AST) -> None: @staticmethod def add_vararg_to_super_call(super_call: ast.Expr, vararg: ast.AST) -> None: super_call.value.args.append(ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load()))) - - @classmethod - def name(cls) -> str: - return "SCI" diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index 092e5f10..fc7b7f49 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -20,10 +20,6 @@ class AssignmentOperatorReplacement(AbstractArithmeticOperatorReplacement): def should_mutate(self, node: ast.AST) -> bool: return isinstance(node.parent, ast.AugAssign) - @classmethod - def name(cls) -> str: - return "ASR" - class BreakContinueReplacement(MutationOperator): def mutate_Break(self, node: ast.Break) -> ast.Continue: @@ -78,10 +74,6 @@ def mutate_Str(self, node: ast.Str) -> ast.Str: def mutate_Str_empty(self, node: ast.Str) -> ast.Str: return ast.Str(s=self.help_str_empty(node)) - @classmethod - def name(cls) -> str: - return "CRP" - class SliceIndexRemove(MutationOperator): def mutate_Slice_remove_lower(self, node: ast.Slice) -> ast.Slice: @@ -125,7 +117,3 @@ def mutate_Expr(self, node: ast.Expr) -> ast.Pass: if utils.is_docstring(node.value): raise MutationResign() return ast.Pass() - - @classmethod - def name(cls) -> str: - return "SDL" From 8473dcac738814dfe6710add3f7ca4d86a0726f4 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:06:17 +0100 Subject: [PATCH 17/76] Remove unused mutation operators --- .../mutation_analysis/mutationadapter.py | 8 +--- .../mutation_analysis/operators/__init__.py | 4 -- .../mutation_analysis/operators/decorator.py | 37 ------------------- .../mutation_analysis/operators/misc.py | 24 ------------ 4 files changed, 1 insertion(+), 72 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index e69cc60a..31ae2b8d 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -81,13 +81,7 @@ def _build_mutation_controller(self) -> mc.MutationController: def _get_mutant_generator(self) -> mu.FirstOrderMutator: operators_set = set() operators_set |= mo.standard_operators - - # Only use a selected set of the experimental operators. - operators_set |= { - mol.OneIterationLoop, - mol.ReverseIterationLoop, - mol.ZeroIterationLoop, - } + operators_set |= mo.experimental_operators # percentage of the generated mutants (mutation sampling) percentage = 100 diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py index d373bf12..5137d5df 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -42,11 +42,7 @@ } experimental_operators = { - ClassmethodDecoratorInsertion, OneIterationLoop, ReverseIterationLoop, - SelfVariableDeletion, - StatementDeletion, - StaticmethodDecoratorInsertion, ZeroIterationLoop, } diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 482abd9f..89c67ac5 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -22,40 +22,3 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: return node else: raise MutationResign() - - -class AbstractMethodDecoratorInsertionMutationOperator(MutationOperator): - @copy_node - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: - if not isinstance(node.parent, ast.ClassDef): - raise MutationResign() - for decorator in node.decorator_list: - if isinstance(decorator, ast.Call): - decorator_name = decorator.func.id - elif isinstance(decorator, ast.Attribute): - decorator_name = decorator.value.id - else: - decorator_name = decorator.id - if decorator_name == self.get_decorator_name(): - raise MutationResign() - if node.decorator_list: - lineno = node.decorator_list[-1].lineno - else: - lineno = node.lineno - decorator = ast.Name(id=self.get_decorator_name(), ctx=ast.Load(), lineno=lineno) - self.shift_lines(node.body, 1) - node.decorator_list.append(decorator) - return node - - def get_decorator_name(self) -> str: - raise NotImplementedError() - - -class ClassmethodDecoratorInsertion(AbstractMethodDecoratorInsertionMutationOperator): - def get_decorator_name(self) -> str: - return "classmethod" - - -class StaticmethodDecoratorInsertion(AbstractMethodDecoratorInsertionMutationOperator): - def get_decorator_name(self) -> str: - return "staticmethod" diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index fc7b7f49..5f1a80f1 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -93,27 +93,3 @@ def mutate_Slice_remove_step(self, node: ast.Slice) -> ast.Slice: raise MutationResign() return ast.Slice(lower=node.lower, upper=node.upper, step=None) - - -class SelfVariableDeletion(MutationOperator): - def mutate_Attribute(self, node: ast.Attribute) -> ast.Name: - try: - if node.value.id == 'self': - return ast.Name(id=node.attr, ctx=ast.Load()) - else: - raise MutationResign() - except AttributeError: - raise MutationResign() - - -class StatementDeletion(MutationOperator): - def mutate_Assign(self, node: ast.Assign) -> ast.Pass: - return ast.Pass() - - def mutate_Return(self, node: ast.Return) -> ast.Pass: - return ast.Pass() - - def mutate_Expr(self, node: ast.Expr) -> ast.Pass: - if utils.is_docstring(node.value): - raise MutationResign() - return ast.Pass() From e5b387a6a6230377c671cb629f073fe8da56eb55 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:45:10 +0100 Subject: [PATCH 18/76] Remove MutationResign exception --- .../mutation_analysis/operators/arithmetic.py | 124 ++++++----- .../mutation_analysis/operators/base.py | 80 ++++--- .../mutation_analysis/operators/decorator.py | 14 +- .../mutation_analysis/operators/exception.py | 48 +++-- .../operators/inheritance.py | 204 +++++++++++------- .../mutation_analysis/operators/loop.py | 42 ++-- .../mutation_analysis/operators/misc.py | 115 ++++++---- 7 files changed, 383 insertions(+), 244 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py index 2db9db9a..d81c7300 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py +++ b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py @@ -11,7 +11,7 @@ import ast -from pynguin.assertion.mutation_analysis.operators.base import MutationResign, MutationOperator, AbstractUnaryOperatorDeletion +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, AbstractUnaryOperatorDeletion class ArithmeticOperatorDeletion(AbstractUnaryOperatorDeletion): @@ -23,65 +23,77 @@ class AbstractArithmeticOperatorReplacement(MutationOperator): def should_mutate(self, node: ast.AST) -> bool: raise NotImplementedError() - def mutate_Add(self, node: ast.Add) -> ast.Sub: - if self.should_mutate(node): - return ast.Sub() - raise MutationResign() - - def mutate_Sub(self, node: ast.Sub) -> ast.Add: - if self.should_mutate(node): - return ast.Add() - raise MutationResign() - - def mutate_Mult_to_Div(self, node: ast.Mult) -> ast.Div: - if self.should_mutate(node): - return ast.Div() - raise MutationResign() - - def mutate_Mult_to_FloorDiv(self, node: ast.Mult) -> ast.FloorDiv: - if self.should_mutate(node): - return ast.FloorDiv() - raise MutationResign() - - def mutate_Mult_to_Pow(self, node: ast.Mult) -> ast.Pow: - if self.should_mutate(node): - return ast.Pow() - raise MutationResign() - - def mutate_Div_to_Mult(self, node: ast.Div) -> ast.Mult: - if self.should_mutate(node): - return ast.Mult() - raise MutationResign() - - def mutate_Div_to_FloorDiv(self, node: ast.Div) -> ast.FloorDiv: - if self.should_mutate(node): - return ast.FloorDiv() - raise MutationResign() - - def mutate_FloorDiv_to_Div(self, node: ast.FloorDiv) -> ast.Div: - if self.should_mutate(node): - return ast.Div() - raise MutationResign() - - def mutate_FloorDiv_to_Mult(self, node: ast.FloorDiv) -> ast.Mult: - if self.should_mutate(node): - return ast.Mult() - raise MutationResign() - - def mutate_Mod(self, node: ast.Mod) -> ast.Mult: - if self.should_mutate(node): - return ast.Mult() - raise MutationResign() - - def mutate_Pow(self, node: ast.Pow) -> ast.Mult: - if self.should_mutate(node): - return ast.Mult() - raise MutationResign() + def mutate_Add(self, node: ast.Add) -> ast.Sub | None: + if not self.should_mutate(node): + return None + + return ast.Sub() + + def mutate_Sub(self, node: ast.Sub) -> ast.Add | None: + if not self.should_mutate(node): + return None + + return ast.Add() + + def mutate_Mult_to_Div(self, node: ast.Mult) -> ast.Div | None: + if not self.should_mutate(node): + return None + + return ast.Div() + + def mutate_Mult_to_FloorDiv(self, node: ast.Mult) -> ast.FloorDiv | None: + if not self.should_mutate(node): + return None + + return ast.FloorDiv() + + def mutate_Mult_to_Pow(self, node: ast.Mult) -> ast.Pow | None: + if not self.should_mutate(node): + return None + + return ast.Pow() + + def mutate_Div_to_Mult(self, node: ast.Div) -> ast.Mult | None: + if not self.should_mutate(node): + return None + + return ast.Mult() + + def mutate_Div_to_FloorDiv(self, node: ast.Div) -> ast.FloorDiv | None: + if not self.should_mutate(node): + return None + + return ast.FloorDiv() + + def mutate_FloorDiv_to_Div(self, node: ast.FloorDiv) -> ast.Div | None: + if not self.should_mutate(node): + return None + + return ast.Div() + + def mutate_FloorDiv_to_Mult(self, node: ast.FloorDiv) -> ast.Mult | None: + if not self.should_mutate(node): + return None + + return ast.Mult() + + def mutate_Mod(self, node: ast.Mod) -> ast.Mult | None: + if not self.should_mutate(node): + return None + + return ast.Mult() + + def mutate_Pow(self, node: ast.Pow) -> ast.Mult | None: + if not self.should_mutate(node): + return None + + return ast.Mult() class ArithmeticOperatorReplacement(AbstractArithmeticOperatorReplacement): def should_mutate(self, node: ast.AST) -> bool: - return not isinstance(node.parent, ast.AugAssign) + parent = getattr(node, "parent") + return not isinstance(parent, ast.AugAssign) def mutate_USub(self, node: ast.USub) -> ast.UAdd: return ast.UAdd() diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 5d2953e2..50e85254 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -11,6 +11,7 @@ """ from __future__ import annotations +import abc import ast import copy import re @@ -21,10 +22,6 @@ from pynguin.assertion.mutation_analysis import utils -class MutationResign(Exception): - pass - - class Mutation: def __init__(self, operator: type[MutationOperator], node: ast.AST, visitor: Callable[[], None] | None = None): self.operator = operator @@ -60,29 +57,41 @@ def visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: return self.fix_lineno(node) + visitors = self.find_visitors(node) - if visitors: - for visitor in visitors: - try: - if self.sampler and not self.sampler.is_mutation_time(): - raise MutationResign - if self.only_mutation and \ - (self.only_mutation.node != node or self.only_mutation.visitor != visitor.__name__): - raise MutationResign - new_node = visitor(node) - self.visitor = visitor.__name__ - self.current_node = node - self.fix_node_internals(node, new_node) - ast.fix_missing_locations(new_node) - yield new_node - except MutationResign: - pass - finally: - for new_node in self.generic_visit(node): - yield new_node - else: - for new_node in self.generic_visit(node): - yield new_node + + if not visitors: + yield from self.generic_visit(node) + return + + for visitor in visitors: + if self.sampler and not self.sampler.is_mutation_time(): + yield from self.generic_visit(node) + continue + + if ( + self.only_mutation + and ( + self.only_mutation.node != node + or self.only_mutation.visitor != visitor.__name__ + ) + ): + yield from self.generic_visit(node) + continue + + new_node = visitor(node) + + if new_node is None: + yield from self.generic_visit(node) + continue + + self.visitor = visitor.__name__ + self.current_node = node + self.fix_node_internals(node, new_node) + ast.fix_missing_locations(new_node) + yield new_node + + yield from self.generic_visit(node) def generic_visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: for field, old_value in ast.iter_fields(node): @@ -131,11 +140,11 @@ def fix_node_internals(self, old_node: ast.AST, new_node: ast.AST) -> None: if hasattr(old_node, 'marker'): new_node.marker = old_node.marker - def find_visitors(self, node: ast.AST) -> list[Callable[[ast.AST], ast.AST]]: + def find_visitors(self, node: ast.AST) -> list[Callable[[ast.AST], ast.AST | None]]: method_prefix = 'mutate_' + node.__class__.__name__ return self.getattrs_like(method_prefix) - def getattrs_like(self, attr_like: str) -> list[Callable[[ast.AST], ast.AST]]: + def getattrs_like(self, attr_like: str) -> list[Callable[[ast.AST], ast.AST | None]]: pattern = re.compile(attr_like + r"($|(_\w+)+$)") return [ getattr(self, attr) @@ -153,8 +162,13 @@ def shift_lines(self, nodes: list[ast.AST], shift_by: int = 1) -> None: ast.increment_lineno(node, shift_by) -class AbstractUnaryOperatorDeletion(MutationOperator): - def mutate_UnaryOp(self, node: ast.UnaryOp) -> ast.expr: - if isinstance(node.op, self.get_operator_type()): - return node.operand - raise MutationResign() +class AbstractUnaryOperatorDeletion(abc.ABC, MutationOperator): + @abc.abstractmethod + def get_operator_type(self) -> type[ast.unaryop]: + pass + + def mutate_UnaryOp(self, node: ast.UnaryOp) -> ast.expr | None: + if not isinstance(node.op, self.get_operator_type()): + return None + + return node.operand diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 89c67ac5..378f3463 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -11,14 +11,14 @@ import ast -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node, MutationResign +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node class DecoratorDeletion(MutationOperator): @copy_node - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: - if node.decorator_list: - node.decorator_list = [] - return node - else: - raise MutationResign() + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST | None: + if not node.decorator_list: + return None + + node.decorator_list = [] + return node diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py index 5d1dcac2..a423be40 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/exception.py +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -11,26 +11,42 @@ import ast -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, MutationResign +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator -class BaseExceptionHandlerOperator(MutationOperator): +def replace_exception_handler( + exception_handler: ast.ExceptHandler, + body: list[ast.stmt], +) -> ast.ExceptHandler: + return ast.ExceptHandler( + type=exception_handler.type, + name=exception_handler.name, + lineno=exception_handler.lineno, + body=body, + ) - @staticmethod - def _replace_exception_body(exception_node: ast.ExceptHandler, body: list[ast.stmt]) -> ast.ExceptHandler: - return ast.ExceptHandler(type=exception_node.type, name=exception_node.name, lineno=exception_node.lineno, - body=body) +class ExceptionHandlerDeletion(MutationOperator): + def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler | None: + if not node.body: + return None -class ExceptionHandlerDeletion(BaseExceptionHandlerOperator): - def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler: - if node.body and isinstance(node.body[0], ast.Raise): - raise MutationResign() - return self._replace_exception_body(node, [ast.Raise(lineno=node.body[0].lineno)]) + first_statement = node.body[0] + if isinstance(first_statement, ast.Raise): + return None -class ExceptionSwallowing(BaseExceptionHandlerOperator): - def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler: - if len(node.body) == 1 and isinstance(node.body[0], ast.Pass): - raise MutationResign() - return self._replace_exception_body(node, [ast.Pass(lineno=node.body[0].lineno)]) + return replace_exception_handler(node, [ast.Raise(lineno=first_statement.lineno)]) + + +class ExceptionSwallowing(MutationOperator): + def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler | None: + if not node.body: + return None + + first_statement = node.body[0] + + if len(node.body) == 1 and isinstance(first_statement, ast.Pass): + return None + + return replace_exception_handler(node, [ast.Pass(lineno=first_statement.lineno)]) diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 60614965..668edd3e 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -13,59 +13,89 @@ import functools from pynguin.assertion.mutation_analysis import utils -from pynguin.assertion.mutation_analysis.operators.base import MutationResign, MutationOperator, copy_node +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node class AbstractOverriddenElementModification(MutationOperator): - def is_overridden(self, node: ast.AST, name: str | None = None) -> bool: - if not isinstance(node.parent, ast.ClassDef): - raise MutationResign() + def is_overridden(self, node: ast.AST, name: str | None = None) -> bool | None: + parent = getattr(node, "parent") + + if not isinstance(parent, ast.ClassDef) or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + return None + if not name: name = node.name - parent = node.parent - parent_names = [] - while parent: + + parent_names: list[str] = [] + + while parent is not None: if not isinstance(parent, ast.Module): parent_names.append(parent.name) if not isinstance(parent, ast.ClassDef) and not isinstance(parent, ast.Module): - raise MutationResign() - parent = parent.parent + return None + parent = getattr(parent, "parent") + getattr_rec = lambda obj, attr: functools.reduce(getattr, attr, obj) + try: klass = getattr_rec(self.module, reversed(parent_names)) except AttributeError: - raise MutationResign() + return None + for base_klass in type.mro(klass)[1:-1]: if hasattr(base_klass, name): return True + return False class HidingVariableDeletion(AbstractOverriddenElementModification): - def mutate_Assign(self, node: ast.Assign) -> ast.stmt: - if len(node.targets) > 1: - raise MutationResign() - if isinstance(node.targets[0], ast.Name) and self.is_overridden(node, name=node.targets[0].id): + def mutate_Assign(self, node: ast.Assign) -> ast.stmt | None: + if len(node.targets) != 1: + return None + + first_expression = node.targets[0] + + if isinstance(first_expression, ast.Name): + overridden = self.is_overridden(node, first_expression.id) + + if overridden is None or not overridden: + return None + return ast.Pass() - elif isinstance(node.targets[0], ast.Tuple) and isinstance(node.value, ast.Tuple): + elif isinstance(first_expression, ast.Tuple) and isinstance(node.value, ast.Tuple): return self.mutate_unpack(node) else: - raise MutationResign() + return None + + def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: + if not node.targets: + return None - def mutate_unpack(self, node: ast.Assign) -> ast.stmt: target = node.targets[0] value = node.value - new_targets = [] - new_values = [] + + new_targets: list[ast.Name] = [] + new_values: list[ast.expr] = [] for target_element, value_element in zip(target.elts, value.elts): - if not self.is_overridden(node, getattr(target_element, 'id', None)): + if not isinstance(target_element, ast.Name) or not isinstance(value_element, ast.expr): + continue + + overridden = self.is_overridden(node, target_element.id) + + if overridden is None: + return None + + if not overridden: new_targets.append(target_element) new_values.append(value_element) + if len(new_targets) == len(target.elts): - raise MutationResign() + return None + if not new_targets: return ast.Pass() - elif len(new_targets) == 1: + elif len(new_targets) == 1 and len(new_values) == 1: node.targets = new_targets node.value = new_values[0] return node @@ -76,7 +106,7 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt: class AbstractSuperCallingModification(MutationOperator): - def is_super_call(self, node: ast.AST, stmt: ast.stmt) -> bool: + def is_super_call(self, node: ast.FunctionDef, stmt: ast.stmt) -> bool: return ( isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call) @@ -87,97 +117,125 @@ def is_super_call(self, node: ast.AST, stmt: ast.stmt) -> bool: and stmt.value.func.attr == node.name ) - def should_mutate(self, node: ast.AST) -> bool: - return isinstance(node.parent, ast.ClassDef) + def should_mutate(self, node: ast.FunctionDef) -> bool: + parent = getattr(node, "parent") + return isinstance(parent, ast.ClassDef) - def get_super_call(self, node: ast.AST) -> tuple[int, ast.stmt] | tuple[None, None]: + def get_super_call(self, node: ast.FunctionDef) -> tuple[int, ast.stmt] | None: for index, stmt in enumerate(node.body): if self.is_super_call(node, stmt): - break - else: - return None, None - return index, stmt + return index, stmt + return None class OverriddenMethodCallingPositionChange(AbstractSuperCallingModification): - def should_mutate(self, node: ast.AST) -> bool: + def should_mutate(self, node: ast.FunctionDef) -> bool: return super().should_mutate(node) and len(node.body) > 1 @copy_node - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: - if not self.should_mutate(node): - raise MutationResign() - index, stmt = self.get_super_call(node) - if index is None: - raise MutationResign() - super_call = node.body[index] + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: + if not self.should_mutate(node) or not node.body: + return None + + super_call = self.get_super_call(node) + + if super_call is None: + return None + + index, statement = super_call + del node.body[index] + if index == 0: - self.set_lineno(super_call, node.body[-1].lineno) + self.set_lineno(statement, node.body[-1].lineno) self.shift_lines(node.body, -1) - node.body.append(super_call) + node.body.append(statement) else: - self.set_lineno(super_call, node.body[0].lineno) + self.set_lineno(statement, node.body[0].lineno) self.shift_lines(node.body, 1) - node.body.insert(0, super_call) + node.body.insert(0, statement) + return node class OverridingMethodDeletion(AbstractOverriddenElementModification): - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass: - if self.is_overridden(node): - return ast.Pass() - raise MutationResign() + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass | None: + overridden = self.is_overridden(node) + + if overridden is None or not overridden: + return None + + return ast.Pass() class SuperCallingDeletion(AbstractSuperCallingModification): @copy_node - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: - if not self.should_mutate(node): - raise MutationResign() - index, _ = self.get_super_call(node) - if index is None: - raise MutationResign() + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: + if not self.should_mutate(node) or not node.body: + return None + + super_call = self.get_super_call(node) + + if super_call is None: + return None + + index, _ = super_call + node.body[index] = ast.Pass(lineno=node.body[index].lineno) + return node class SuperCallingInsert(AbstractSuperCallingModification, AbstractOverriddenElementModification): - def should_mutate(self, node: ast.AST) -> bool: - return super().should_mutate(node) and self.is_overridden(node) - @copy_node - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: - if not self.should_mutate(node): - raise MutationResign() - index, stmt = self.get_super_call(node) - if index is not None: - raise MutationResign() + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: + overridden = self.is_overridden(node) + + if not self.should_mutate(node) or not node.body or overridden is None or not overridden: + return None + + super_call = self.get_super_call(node) + + if super_call is not None: + return None + node.body.insert(0, self.create_super_call(node)) self.shift_lines(node.body[1:], 1) + return node @copy_node def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: - super_call = utils.create_ast('super().{}()'.format(node.name)).body[0] + function_def: ast.FunctionDef = utils.create_ast(f"super().{node.name}()") + + super_call: ast.Expr = function_def.body[0] + + super_call_value: ast.Call = super_call.value + for arg in node.args.args[1:-len(node.args.defaults) or None]: - super_call.value.args.append(ast.Name(id=arg.arg, ctx=ast.Load())) + super_call_value.args.append(ast.Name(id=arg.arg, ctx=ast.Load())) + for arg, default in zip(node.args.args[-len(node.args.defaults):], node.args.defaults): - super_call.value.keywords.append(ast.keyword(arg=arg.arg, value=default)) + super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) + for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): - super_call.value.keywords.append(ast.keyword(arg=arg.arg, value=default)) - if node.args.vararg: - self.add_vararg_to_super_call(super_call, node.args.vararg) - if node.args.kwarg: - self.add_kwarg_to_super_call(super_call, node.args.kwarg) + super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) + + if node.args.vararg is not None: + self.add_vararg_to_super_call(super_call_value, node.args.vararg) + + if node.args.kwarg is not None: + self.add_kwarg_to_super_call(super_call_value, node.args.kwarg) + self.set_lineno(super_call, node.body[0].lineno) + return super_call @staticmethod - def add_kwarg_to_super_call(super_call: ast.Expr, kwarg: ast.AST) -> None: - super_call.value.keywords.append(ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load()))) + def add_kwarg_to_super_call(super_call_value: ast.Call, kwarg: ast.arg) -> None: + super_call_value.keywords.append(ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load()))) @staticmethod - def add_vararg_to_super_call(super_call: ast.Expr, vararg: ast.AST) -> None: - super_call.value.args.append(ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load()))) + def add_vararg_to_super_call(super_call_value: ast.Call, vararg: ast.arg) -> None: + super_call_value.args.append(ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load()))) diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py index 528c1bba..d05fbfe4 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/loop.py +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -10,22 +10,39 @@ """ import ast +import typing from pynguin.assertion.mutation_analysis.operators import copy_node, MutationOperator +T = typing.TypeVar("T", ast.For, ast.While) + + +def one_iteration(node: T) -> T | None: + if not node.body: + return None + + node.body.append(ast.Break(lineno=node.body[-1].lineno + 1)) + return node + + +def zero_iteration(node: T) -> T | None: + if not node.body: + return None + + node.body = [ast.Break(lineno=node.body[0].lineno)] + return node + + class OneIterationLoop(MutationOperator): - def one_iteration(self, node: ast.For | ast.While) -> ast.For | ast.While: - node.body.append(ast.Break(lineno=node.body[-1].lineno + 1)) - return node @copy_node - def mutate_For(self, node: ast.For) -> ast.For: - return self.one_iteration(node) + def mutate_For(self, node: ast.For) -> ast.For | None: + return one_iteration(node) @copy_node - def mutate_While(self, node: ast.While) -> ast.While: - return self.one_iteration(node) + def mutate_While(self, node: ast.While) -> ast.While | None: + return one_iteration(node) class ReverseIterationLoop(MutationOperator): @@ -43,14 +60,11 @@ def mutate_For(self, node: ast.For) -> ast.For: class ZeroIterationLoop(MutationOperator): - def zero_iteration(self, node: ast.For | ast.While) -> ast.For | ast.While: - node.body = [ast.Break(lineno=node.body[0].lineno)] - return node @copy_node - def mutate_For(self, node: ast.For) -> ast.For: - return self.zero_iteration(node) + def mutate_For(self, node: ast.For) -> ast.For | None: + return zero_iteration(node) @copy_node - def mutate_While(self, node: ast.While) -> ast.While: - return self.zero_iteration(node) + def mutate_While(self, node: ast.While) -> ast.While | None: + return zero_iteration(node) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index 5f1a80f1..9b397c85 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -13,12 +13,13 @@ from pynguin.assertion.mutation_analysis import utils from pynguin.assertion.mutation_analysis.operators.arithmetic import AbstractArithmeticOperatorReplacement -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, MutationResign +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator class AssignmentOperatorReplacement(AbstractArithmeticOperatorReplacement): def should_mutate(self, node: ast.AST) -> bool: - return isinstance(node.parent, ast.AugAssign) + parent = getattr(node, "parent") + return isinstance(parent, ast.AugAssign) class BreakContinueReplacement(MutationOperator): @@ -30,66 +31,90 @@ def mutate_Continue(self, node: ast.Continue) -> ast.Break: class ConstantReplacement(MutationOperator): - FIRST_CONST_STRING = 'mutpy' - SECOND_CONST_STRING = 'python' + FIRST_CONST_STRING = "mutpy" + SECOND_CONST_STRING = "python" - def help_str(self, node: ast.AST) -> str: + def help_str(self, node: ast.Constant) -> str | None: if utils.is_docstring(node): - raise MutationResign() + return None - if node.s != self.FIRST_CONST_STRING: - return self.FIRST_CONST_STRING - else: + if node.value == self.FIRST_CONST_STRING: return self.SECOND_CONST_STRING - def help_str_empty(self, node: ast.AST) -> str: - if not node.s or utils.is_docstring(node): - raise MutationResign() - return '' - - def mutate_Constant_num(self, node: ast.Constant) -> ast.Constant: - if isinstance(node.value, (int, float)) and not isinstance(node.value, bool): - return ast.Constant(n=node.n + 1) - else: - raise MutationResign() - - def mutate_Constant_str(self, node: ast.Constant) -> ast.Constant: - if isinstance(node.value, str): - return ast.Constant(s=self.help_str(node)) - else: - raise MutationResign() - - def mutate_Constant_str_empty(self, node: ast.Constant) -> ast.Constant: - if isinstance(node.value, str): - return ast.Constant(s=self.help_str_empty(node)) - else: - raise MutationResign() + return self.FIRST_CONST_STRING + + @staticmethod + def help_str_empty(node: ast.Constant) -> str | None: + if not node.value or utils.is_docstring(node): + return None + + return "" + + def mutate_Constant_num(self, node: ast.Constant) -> ast.Constant | None: + value = node.value + + if not isinstance(value, (int, float)) or isinstance(value, bool): + return None + + return ast.Constant(value + 1) + + def mutate_Constant_str(self, node: ast.Constant) -> ast.Constant | None: + if not isinstance(node.value, str): + return None + + new_value = self.help_str(node) + + if new_value is None: + return None + + return ast.Constant(new_value) + + def mutate_Constant_str_empty(self, node: ast.Constant) -> ast.Constant | None: + if not isinstance(node.value, str): + return None + + new_value = self.help_str_empty(node) + + if new_value is None: + return None + + return ast.Constant(new_value) def mutate_Num(self, node: ast.Num) -> ast.Num: - return ast.Num(n=node.n + 1) + return ast.Num(node.value + 1) + + def mutate_Str(self, node: ast.Str) -> ast.Str | None: + new_value = self.help_str(node) + + if new_value is None: + return None + + return ast.Str(new_value) + + def mutate_Str_empty(self, node: ast.Str) -> ast.Str | None: + new_value = self.help_str_empty(node) - def mutate_Str(self, node: ast.Str) -> ast.Str: - return ast.Str(s=self.help_str(node)) + if new_value is None: + return None - def mutate_Str_empty(self, node: ast.Str) -> ast.Str: - return ast.Str(s=self.help_str_empty(node)) + return ast.Str(new_value) class SliceIndexRemove(MutationOperator): - def mutate_Slice_remove_lower(self, node: ast.Slice) -> ast.Slice: - if not node.lower: - raise MutationResign() + def mutate_Slice_remove_lower(self, node: ast.Slice) -> ast.Slice | None: + if node.lower is None: + return None return ast.Slice(lower=None, upper=node.upper, step=node.step) - def mutate_Slice_remove_upper(self, node: ast.Slice) -> ast.Slice: - if not node.upper: - raise MutationResign() + def mutate_Slice_remove_upper(self, node: ast.Slice) -> ast.Slice | None: + if node.upper is None: + return None return ast.Slice(lower=node.lower, upper=None, step=node.step) - def mutate_Slice_remove_step(self, node: ast.Slice) -> ast.Slice: - if not node.step: - raise MutationResign() + def mutate_Slice_remove_step(self, node: ast.Slice) -> ast.Slice | None: + if node.step is None: + return None return ast.Slice(lower=node.lower, upper=node.upper, step=None) From 6a07687b38e88e355000356df1fa7945c80f048f Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:58:16 +0100 Subject: [PATCH 19/76] Improve visitor functions --- .../mutation_analysis/operators/base.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 50e85254..006ff4d7 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -14,10 +14,9 @@ import abc import ast import copy -import re import types -from typing import Generator, Callable +from typing import Generator, Callable, TypeVar from pynguin.assertion.mutation_analysis import utils @@ -39,10 +38,13 @@ def f(self, node): return f +T = TypeVar("T", bound=ast.AST) + + class MutationOperator: def mutate( self, - node: ast.AST, + node: T, sampler: utils.RandomSampler | None = None, module: types.ModuleType | None = None, only_mutation: Mutation | None = None @@ -53,9 +55,10 @@ def mutate( for new_node in self.visit(node): yield Mutation(operator=self.__class__, node=self.current_node, visitor=self.visitor), new_node - def visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: + def visit(self, node: T) -> Generator[ast.AST, None, None]: if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: return + self.fix_lineno(node) visitors = self.find_visitors(node) @@ -89,6 +92,7 @@ def visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: self.current_node = node self.fix_node_internals(node, new_node) ast.fix_missing_locations(new_node) + yield new_node yield from self.generic_visit(node) @@ -140,16 +144,13 @@ def fix_node_internals(self, old_node: ast.AST, new_node: ast.AST) -> None: if hasattr(old_node, 'marker'): new_node.marker = old_node.marker - def find_visitors(self, node: ast.AST) -> list[Callable[[ast.AST], ast.AST | None]]: - method_prefix = 'mutate_' + node.__class__.__name__ - return self.getattrs_like(method_prefix) - - def getattrs_like(self, attr_like: str) -> list[Callable[[ast.AST], ast.AST | None]]: - pattern = re.compile(attr_like + r"($|(_\w+)+$)") + def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: + node_name = node.__class__.__name__ + method_prefix = f"mutate_{node_name}" return [ - getattr(self, attr) + visitor for attr in dir(self) - if pattern.match(attr) + if attr.startswith(method_prefix) and callable(visitor := getattr(self, attr)) ] def set_lineno(self, node: ast.AST, lineno: int) -> None: From 429d9511d4e96078d339b878c99ae740244382eb Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:25:07 +0100 Subject: [PATCH 20/76] Refactor MutationOperator --- .../assertion/mutation_analysis/mutators.py | 6 +- .../mutation_analysis/operators/base.py | 112 ++++++++++-------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index ac90b33b..b2e0dc52 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -51,7 +51,7 @@ def mutate( module: types.ModuleType | None = None, ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: for op in self.operators: - for mutation, mutant in op().mutate(target_ast, self.sampler, module=module): + for mutation, mutant in op.mutate(target_ast, self.sampler, module=module): yield [mutation], mutant @@ -77,7 +77,7 @@ def mutate( applied_mutations = [] mutant = target_ast for mutation in mutations_to_apply: - generator = mutation.operator().mutate( + generator = mutation.operator.mutate( mutant, sampler=self.sampler, module=module, @@ -99,7 +99,7 @@ def generate_all_mutations( ) -> list[Mutation]: mutations: list[Mutation] = [] for op in self.operators: - for mutation, _ in op().mutate(target_ast, None, module=module): + for mutation, _ in op.mutate(target_ast, None, module=module): mutations.append(mutation) return mutations diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 006ff4d7..42c2dd0f 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -22,10 +22,10 @@ class Mutation: - def __init__(self, operator: type[MutationOperator], node: ast.AST, visitor: Callable[[], None] | None = None): - self.operator = operator + def __init__(self, node: ast.AST, operator: type[MutationOperator], visitor_name: str) -> None: self.node = node - self.visitor = visitor + self.operator = operator + self.visitor_name = visitor_name def copy_node(mutate): @@ -42,20 +42,30 @@ def f(self, node): class MutationOperator: + @classmethod def mutate( - self, + cls, node: T, sampler: utils.RandomSampler | None = None, module: types.ModuleType | None = None, only_mutation: Mutation | None = None ): + operator = cls(sampler, module, only_mutation) + + for current_node, mutated_node, visitor_name in operator.visit(node): + yield Mutation(current_node, cls, visitor_name), mutated_node + + def __init__( + self, + sampler: utils.RandomSampler | None, + module: types.ModuleType | None, + only_mutation: Mutation | None, + ) -> None: self.sampler = sampler - self.only_mutation = only_mutation self.module = module - for new_node in self.visit(node): - yield Mutation(operator=self.__class__, node=self.current_node, visitor=self.visitor), new_node + self.only_mutation = only_mutation - def visit(self, node: T) -> Generator[ast.AST, None, None]: + def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: return @@ -76,28 +86,26 @@ def visit(self, node: T) -> Generator[ast.AST, None, None]: self.only_mutation and ( self.only_mutation.node != node - or self.only_mutation.visitor != visitor.__name__ + or self.only_mutation.visitor_name != visitor.__name__ ) ): yield from self.generic_visit(node) continue - new_node = visitor(node) + mutated_node = visitor(node) - if new_node is None: + if mutated_node is None: yield from self.generic_visit(node) continue - self.visitor = visitor.__name__ - self.current_node = node - self.fix_node_internals(node, new_node) - ast.fix_missing_locations(new_node) + self.fix_node_internals(node, mutated_node) + ast.fix_missing_locations(mutated_node) - yield new_node + yield node, mutated_node, visitor.__name__ yield from self.generic_visit(node) - def generic_visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: + def generic_visit(self, node: ast.AST) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: for field, old_value in ast.iter_fields(node): if isinstance(old_value, list): generator = self.generic_visit_list(old_value) @@ -106,43 +114,45 @@ def generic_visit(self, node: ast.AST) -> Generator[ast.AST, None, None]: else: generator = [] - for _ in generator: - yield node + for current_node, visitor_name in generator: + yield current_node, node, visitor_name - def generic_visit_list(self, old_value: list[ast.AST | None]) -> Generator[None, None, None]: - old_values_copy = old_value[:] - for position, value in enumerate(old_values_copy): + def generic_visit_list(self, old_value: list) -> Generator[tuple[ast.AST, str], None, None]: + for position, value in enumerate(old_value.copy()): if isinstance(value, ast.AST): - for new_value in self.visit(value): - if not isinstance(new_value, ast.AST): - old_value[position:position + 1] = new_value - else: - old_value[position] = new_value - - yield - old_value[:] = old_values_copy - - def generic_visit_real_node(self, node: ast.AST, field: str, old_value: ast.AST) -> Generator[None, None, None]: - for new_node in self.visit(old_value): - if new_node is None: - delattr(node, field) - else: - setattr(node, field, new_node) - yield - setattr(node, field, old_value) + for current_node, mutated_node, visitor_name in self.visit(value): + old_value[position] = mutated_node + yield current_node, visitor_name + + old_value[position] = value + + def generic_visit_real_node(self, node: ast.AST, field: str, old_value: ast.AST) -> Generator[tuple[ast.AST, str], None, None]: + for current_node, mutated_node, visitor_name in self.visit(old_value): + setattr(node, field, mutated_node) + yield current_node, visitor_name + + setattr(node, field, old_value) def fix_lineno(self, node: ast.AST) -> None: - if not hasattr(node, 'lineno') and getattr(node, 'parent', None) is not None and hasattr(node.parent, 'lineno'): - node.lineno = node.parent.lineno + parent = getattr(node, "parent") + if not hasattr(node, "lineno") and parent is not None and hasattr(parent, "lineno"): + parent_lineno = getattr(parent, "lineno") + setattr(node, "lineno", parent_lineno) def fix_node_internals(self, old_node: ast.AST, new_node: ast.AST) -> None: - if not hasattr(new_node, 'parent'): - new_node.children = old_node.children - new_node.parent = old_node.parent - if not hasattr(new_node, 'lineno') and hasattr(old_node, 'lineno'): - new_node.lineno = old_node.lineno - if hasattr(old_node, 'marker'): - new_node.marker = old_node.marker + if not hasattr(new_node, "parent"): + old_node_children = getattr(old_node, "children") + old_node_parent = getattr(old_node, "parent") + setattr(new_node, "children", old_node_children) + setattr(new_node, "parent", old_node_parent) + + if not hasattr(new_node, "lineno") and hasattr(old_node, "lineno"): + old_node_lineno = getattr(old_node, "lineno") + setattr(new_node, "lineno", old_node_lineno) + + if hasattr(old_node, "marker"): + old_node_marker = getattr(old_node, "marker") + setattr(new_node, "marker", old_node_marker) def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: node_name = node.__class__.__name__ @@ -154,9 +164,9 @@ def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: ] def set_lineno(self, node: ast.AST, lineno: int) -> None: - for n in ast.walk(node): - if hasattr(n, 'lineno'): - n.lineno = lineno + for child_node in ast.walk(node): + if hasattr(child_node, "lineno"): + setattr(child_node, "lineno", lineno) def shift_lines(self, nodes: list[ast.AST], shift_by: int = 1) -> None: for node in nodes: From 613e1f74b8c2fd82f7a9a048db20928534d408b1 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:31:10 +0100 Subject: [PATCH 21/76] Extract some functions --- .../mutation_analysis/operators/base.py | 79 ++++++++++--------- .../operators/inheritance.py | 12 +-- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 42c2dd0f..f0fc9ec7 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -21,11 +21,38 @@ from pynguin.assertion.mutation_analysis import utils -class Mutation: - def __init__(self, node: ast.AST, operator: type[MutationOperator], visitor_name: str) -> None: - self.node = node - self.operator = operator - self.visitor_name = visitor_name +def fix_lineno(node: ast.AST) -> None: + parent = getattr(node, "parent") + if not hasattr(node, "lineno") and parent is not None and hasattr(parent, "lineno"): + parent_lineno = getattr(parent, "lineno") + setattr(node, "lineno", parent_lineno) + + +def fix_node_internals(old_node: ast.AST, new_node: ast.AST) -> None: + if not hasattr(new_node, "parent"): + old_node_children = getattr(old_node, "children") + old_node_parent = getattr(old_node, "parent") + setattr(new_node, "children", old_node_children) + setattr(new_node, "parent", old_node_parent) + + if not hasattr(new_node, "lineno") and hasattr(old_node, "lineno"): + old_node_lineno = getattr(old_node, "lineno") + setattr(new_node, "lineno", old_node_lineno) + + if hasattr(old_node, "marker"): + old_node_marker = getattr(old_node, "marker") + setattr(new_node, "marker", old_node_marker) + + +def set_lineno(node: ast.AST, lineno: int) -> None: + for child_node in ast.walk(node): + if hasattr(child_node, "lineno"): + setattr(child_node, "lineno", lineno) + + +def shift_lines(nodes: list[ast.AST], shift_by: int = 1) -> None: + for node in nodes: + ast.increment_lineno(node, shift_by) def copy_node(mutate): @@ -38,10 +65,18 @@ def f(self, node): return f +class Mutation: + def __init__(self, node: ast.AST, operator: type[MutationOperator], visitor_name: str) -> None: + self.node = node + self.operator = operator + self.visitor_name = visitor_name + + T = TypeVar("T", bound=ast.AST) class MutationOperator: + @classmethod def mutate( cls, @@ -69,7 +104,7 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: return - self.fix_lineno(node) + fix_lineno(node) visitors = self.find_visitors(node) @@ -98,7 +133,7 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: yield from self.generic_visit(node) continue - self.fix_node_internals(node, mutated_node) + fix_node_internals(node, mutated_node) ast.fix_missing_locations(mutated_node) yield node, mutated_node, visitor.__name__ @@ -133,27 +168,6 @@ def generic_visit_real_node(self, node: ast.AST, field: str, old_value: ast.AST) setattr(node, field, old_value) - def fix_lineno(self, node: ast.AST) -> None: - parent = getattr(node, "parent") - if not hasattr(node, "lineno") and parent is not None and hasattr(parent, "lineno"): - parent_lineno = getattr(parent, "lineno") - setattr(node, "lineno", parent_lineno) - - def fix_node_internals(self, old_node: ast.AST, new_node: ast.AST) -> None: - if not hasattr(new_node, "parent"): - old_node_children = getattr(old_node, "children") - old_node_parent = getattr(old_node, "parent") - setattr(new_node, "children", old_node_children) - setattr(new_node, "parent", old_node_parent) - - if not hasattr(new_node, "lineno") and hasattr(old_node, "lineno"): - old_node_lineno = getattr(old_node, "lineno") - setattr(new_node, "lineno", old_node_lineno) - - if hasattr(old_node, "marker"): - old_node_marker = getattr(old_node, "marker") - setattr(new_node, "marker", old_node_marker) - def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: node_name = node.__class__.__name__ method_prefix = f"mutate_{node_name}" @@ -163,15 +177,6 @@ def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: if attr.startswith(method_prefix) and callable(visitor := getattr(self, attr)) ] - def set_lineno(self, node: ast.AST, lineno: int) -> None: - for child_node in ast.walk(node): - if hasattr(child_node, "lineno"): - setattr(child_node, "lineno", lineno) - - def shift_lines(self, nodes: list[ast.AST], shift_by: int = 1) -> None: - for node in nodes: - ast.increment_lineno(node, shift_by) - class AbstractUnaryOperatorDeletion(abc.ABC, MutationOperator): @abc.abstractmethod diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 668edd3e..f86bc3c1 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -147,12 +147,12 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: del node.body[index] if index == 0: - self.set_lineno(statement, node.body[-1].lineno) - self.shift_lines(node.body, -1) + set_lineno(statement, node.body[-1].lineno) + shift_lines(node.body, -1) node.body.append(statement) else: - self.set_lineno(statement, node.body[0].lineno) - self.shift_lines(node.body, 1) + set_lineno(statement, node.body[0].lineno) + shift_lines(node.body, 1) node.body.insert(0, statement) return node @@ -201,7 +201,7 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: return None node.body.insert(0, self.create_super_call(node)) - self.shift_lines(node.body[1:], 1) + shift_lines(node.body[1:], 1) return node @@ -228,7 +228,7 @@ def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: if node.args.kwarg is not None: self.add_kwarg_to_super_call(super_call_value, node.args.kwarg) - self.set_lineno(super_call, node.body[0].lineno) + set_lineno(super_call, node.body[0].lineno) return super_call From c980a843e28f10e7358e5201b2ef0823bc4ca2bd Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:52:07 +0100 Subject: [PATCH 22/76] Convert copy_node from a decorator to a function --- .../mutation_analysis/operators/base.py | 18 +++----- .../mutation_analysis/operators/decorator.py | 6 +-- .../operators/inheritance.py | 44 ++++++++++--------- .../mutation_analysis/operators/logical.py | 9 ++-- .../mutation_analysis/operators/loop.py | 24 +++++----- 5 files changed, 47 insertions(+), 54 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index f0fc9ec7..ee3d9350 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -55,16 +55,6 @@ def shift_lines(nodes: list[ast.AST], shift_by: int = 1) -> None: ast.increment_lineno(node, shift_by) -def copy_node(mutate): - def f(self, node): - copied_node = copy.deepcopy(node, memo={ - id(node.parent): node.parent, - }) - return mutate(self, copied_node) - - return f - - class Mutation: def __init__(self, node: ast.AST, operator: type[MutationOperator], visitor_name: str) -> None: self.node = node @@ -75,8 +65,14 @@ def __init__(self, node: ast.AST, operator: type[MutationOperator], visitor_name T = TypeVar("T", bound=ast.AST) -class MutationOperator: +def copy_node(node: T) -> T: + parent = getattr(node, "parent") + return copy.deepcopy(node, memo={ + id(parent): parent, + }) + +class MutationOperator: @classmethod def mutate( cls, diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 378f3463..91f42fee 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -15,10 +15,10 @@ class DecoratorDeletion(MutationOperator): - @copy_node def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST | None: if not node.decorator_list: return None - node.decorator_list = [] - return node + mutated_node = copy_node(node) + mutated_node.decorator_list = [] + return mutated_node diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index f86bc3c1..4b23738c 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -13,7 +13,7 @@ import functools from pynguin.assertion.mutation_analysis import utils -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node, set_lineno, shift_lines class AbstractOverriddenElementModification(MutationOperator): @@ -132,30 +132,31 @@ class OverriddenMethodCallingPositionChange(AbstractSuperCallingModification): def should_mutate(self, node: ast.FunctionDef) -> bool: return super().should_mutate(node) and len(node.body) > 1 - @copy_node def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: if not self.should_mutate(node) or not node.body: return None - super_call = self.get_super_call(node) + mutated_node = copy_node(node) + + super_call = self.get_super_call(mutated_node) if super_call is None: return None index, statement = super_call - del node.body[index] + del mutated_node.body[index] if index == 0: - set_lineno(statement, node.body[-1].lineno) - shift_lines(node.body, -1) - node.body.append(statement) + set_lineno(statement, mutated_node.body[-1].lineno) + shift_lines(mutated_node.body, -1) + mutated_node.body.append(statement) else: - set_lineno(statement, node.body[0].lineno) - shift_lines(node.body, 1) - node.body.insert(0, statement) + set_lineno(statement, mutated_node.body[0].lineno) + shift_lines(mutated_node.body, 1) + mutated_node.body.insert(0, statement) - return node + return mutated_node class OverridingMethodDeletion(AbstractOverriddenElementModification): @@ -169,43 +170,44 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass | None: class SuperCallingDeletion(AbstractSuperCallingModification): - @copy_node def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: if not self.should_mutate(node) or not node.body: return None - super_call = self.get_super_call(node) + mutated_node = copy_node(node) + + super_call = self.get_super_call(mutated_node) if super_call is None: return None index, _ = super_call - node.body[index] = ast.Pass(lineno=node.body[index].lineno) + mutated_node.body[index] = ast.Pass(lineno=mutated_node.body[index].lineno) - return node + return mutated_node class SuperCallingInsert(AbstractSuperCallingModification, AbstractOverriddenElementModification): - @copy_node def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: overridden = self.is_overridden(node) if not self.should_mutate(node) or not node.body or overridden is None or not overridden: return None - super_call = self.get_super_call(node) + mutated_node = copy_node(node) + + super_call = self.get_super_call(mutated_node) if super_call is not None: return None - node.body.insert(0, self.create_super_call(node)) - shift_lines(node.body[1:], 1) + mutated_node.body.insert(0, self.create_super_call(mutated_node)) + shift_lines(mutated_node.body[1:], 1) - return node + return mutated_node - @copy_node def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: function_def: ast.FunctionDef = utils.create_ast(f"super().{node.name}()") diff --git a/src/pynguin/assertion/mutation_analysis/operators/logical.py b/src/pynguin/assertion/mutation_analysis/operators/logical.py index 01b1635f..28dd8672 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/logical.py +++ b/src/pynguin/assertion/mutation_analysis/operators/logical.py @@ -24,15 +24,14 @@ def mutate_NotIn(self, node: ast.NotIn) -> ast.In: class ConditionalOperatorInsertion(MutationOperator): def negate_test(self, node: ast.If | ast.While) -> ast.If | ast.While: - not_node = ast.UnaryOp(op=ast.Not(), operand=node.test) - node.test = not_node - return node + mutated_node = copy_node(node) + not_node = ast.UnaryOp(op=ast.Not(), operand=mutated_node.test) + mutated_node.test = not_node + return mutated_node - @copy_node def mutate_While(self, node: ast.While) -> ast.While: return self.negate_test(node) - @copy_node def mutate_If(self, node: ast.If) -> ast.If: return self.negate_test(node) diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py index d05fbfe4..4849734e 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/loop.py +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -22,49 +22,45 @@ def one_iteration(node: T) -> T | None: if not node.body: return None - node.body.append(ast.Break(lineno=node.body[-1].lineno + 1)) - return node + mutated_node = copy_node(node) + mutated_node.body.append(ast.Break(lineno=mutated_node.body[-1].lineno + 1)) + return mutated_node def zero_iteration(node: T) -> T | None: if not node.body: return None - node.body = [ast.Break(lineno=node.body[0].lineno)] - return node + mutated_node = copy_node(node) + mutated_node.body = [ast.Break(lineno=mutated_node.body[0].lineno)] + return mutated_node class OneIterationLoop(MutationOperator): - - @copy_node def mutate_For(self, node: ast.For) -> ast.For | None: return one_iteration(node) - @copy_node def mutate_While(self, node: ast.While) -> ast.While | None: return one_iteration(node) class ReverseIterationLoop(MutationOperator): - @copy_node def mutate_For(self, node: ast.For) -> ast.For: - old_iter = node.iter - node.iter = ast.Call( + mutated_node = copy_node(node) + old_iter = mutated_node.iter + mutated_node.iter = ast.Call( func=ast.Name(id=reversed.__name__, ctx=ast.Load()), args=[old_iter], keywords=[], starargs=None, kwargs=None, ) - return node + return mutated_node class ZeroIterationLoop(MutationOperator): - - @copy_node def mutate_For(self, node: ast.For) -> ast.For | None: return zero_iteration(node) - @copy_node def mutate_While(self, node: ast.While) -> ast.While | None: return zero_iteration(node) From 965f42526f11482c195037138d4b8087d55b32d7 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:02:46 +0100 Subject: [PATCH 23/76] Refactor MutationOperator.visit --- .../mutation_analysis/operators/base.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index ee3d9350..80da3820 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -109,30 +109,24 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: return for visitor in visitors: - if self.sampler and not self.sampler.is_mutation_time(): - yield from self.generic_visit(node) - continue - if ( - self.only_mutation + ( + self.sampler is None + or self.sampler.is_mutation_time() + ) and ( - self.only_mutation.node != node - or self.only_mutation.visitor_name != visitor.__name__ + self.only_mutation is None + or ( + self.only_mutation.node == node + and self.only_mutation.visitor_name == visitor.__name__ + ) ) + and (mutated_node := visitor(node)) is not None ): - yield from self.generic_visit(node) - continue - - mutated_node = visitor(node) - - if mutated_node is None: - yield from self.generic_visit(node) - continue - - fix_node_internals(node, mutated_node) - ast.fix_missing_locations(mutated_node) + fix_node_internals(node, mutated_node) + ast.fix_missing_locations(mutated_node) - yield node, mutated_node, visitor.__name__ + yield node, mutated_node, visitor.__name__ yield from self.generic_visit(node) From bd6820a5a1070e717403fe23b8601a62b93588ae Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:07:45 +0100 Subject: [PATCH 24/76] Refactor operators lists --- .../mutation_analysis/operators/__init__.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py index 5137d5df..f4bd4da0 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -9,16 +9,16 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/__init__.py. """ -from .arithmetic import * -from .base import * -from .decorator import * -from .exception import * -from .inheritance import * -from .logical import * -from .loop import * -from .misc import * +from pynguin.assertion.mutation_analysis.operators.arithmetic import * +from pynguin.assertion.mutation_analysis.operators.base import * +from pynguin.assertion.mutation_analysis.operators.decorator import * +from pynguin.assertion.mutation_analysis.operators.exception import * +from pynguin.assertion.mutation_analysis.operators.inheritance import * +from pynguin.assertion.mutation_analysis.operators.logical import * +from pynguin.assertion.mutation_analysis.operators.loop import * +from pynguin.assertion.mutation_analysis.operators.misc import * -standard_operators = { +standard_operators = [ ArithmeticOperatorDeletion, ArithmeticOperatorReplacement, AssignmentOperatorReplacement, @@ -39,10 +39,10 @@ SliceIndexRemove, SuperCallingDeletion, SuperCallingInsert, -} +] -experimental_operators = { +experimental_operators = [ OneIterationLoop, ReverseIterationLoop, ZeroIterationLoop, -} +] From 6c6850fb35c6a424ba95b9b0f09a9c8a63085e89 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:23:51 +0100 Subject: [PATCH 25/76] Remove MutationController --- .../assertion/mutation_analysis/controller.py | 51 ------------------- .../mutation_analysis/mutationadapter.py | 46 ++++++++--------- .../assertion/mutation_analysis/utils.py | 5 +- 3 files changed, 21 insertions(+), 81 deletions(-) delete mode 100644 src/pynguin/assertion/mutation_analysis/controller.py diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py deleted file mode 100644 index 67d04eeb..00000000 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ /dev/null @@ -1,51 +0,0 @@ -# This file is part of Pynguin. -# -# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors -# -# SPDX-License-Identifier: MIT -# -"""Provides classes for mutation testing. - -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py. -""" -from __future__ import annotations - -import ast -import inspect -import types - -from typing import Generator - -from pynguin.assertion.mutation_analysis import utils -from pynguin.assertion.mutation_analysis.operators.base import Mutation -from pynguin.assertion.mutation_analysis.mutators import Mutator - - -class MutationController: - - def __init__(self, mutant_generator: Mutator) -> None: - self.mutant_generator = mutant_generator - - def mutate_module( - self, - target_module: types.ModuleType, - target_ast: ast.AST, - ) -> Generator[tuple[types.ModuleType | None, list[Mutation]], None, None]: - for mutations, mutant_ast in self.mutant_generator.mutate( - target_ast, - module=target_module, - ): - yield self.create_mutant_module(target_module, mutant_ast), mutations - - def create_target_ast(self, target_module: types.ModuleType) -> ast.AST: - target_source_code = inspect.getsource(target_module) - return utils.create_ast(target_source_code) - - def create_mutant_module(self, target_module: types.ModuleType, mutant_ast: ast.Module) -> types.ModuleType | None: - try: - return utils.create_module( - ast_node=mutant_ast, - module_name=target_module.__name__ - ) - except BaseException: - return None diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 31ae2b8d..18d5f2d4 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -7,16 +7,16 @@ """Provides an adapter for the MutPy mutation testing framework.""" from __future__ import annotations +import inspect import importlib import logging from typing import TYPE_CHECKING -import pynguin.assertion.mutation_analysis.controller as mc import pynguin.assertion.mutation_analysis.operators as mo -import pynguin.assertion.mutation_analysis.operators.loop as mol import pynguin.assertion.mutation_analysis.stategies as ms import pynguin.assertion.mutation_analysis.mutators as mu +import pynguin.assertion.mutation_analysis.utils as utils import pynguin.configuration as config @@ -53,49 +53,43 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: A list of tuples where the first entry is the mutated module and the second part is a list of all the mutations operators applied. """ - controller = self._build_mutation_controller() - - mutants = [] + _LOGGER.info("Setup mutation generator") + mutant_generator = self._get_mutant_generator() + _LOGGER.info("Import module %s", config.configuration.module_name) target_module = importlib.import_module(config.configuration.module_name) _LOGGER.info("Build AST for %s", target_module.__name__) - target_ast = controller.create_target_ast(target_module) - _LOGGER.info("Mutate module %s", target_module.__name__) - mutant_modules = controller.mutate_module( - target_module=target_module, - target_ast=target_ast, - ) + target_source_code = inspect.getsource(target_module) + target_ast = utils.create_ast(target_source_code) - for mutant_module, mutations in mutant_modules: - mutants.append((mutant_module, mutations)) + _LOGGER.info("Mutate module %s", target_module.__name__) + mutants = [ + (utils.create_module(mutant_ast, target_module.__name__), mutations) + for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module) + ] _LOGGER.info("Generated %d mutants", len(mutants)) return mutants - def _build_mutation_controller(self) -> mc.MutationController: - _LOGGER.info("Setup mutation controller") - mutant_generator = self._get_mutant_generator() - return mc.MutationController(mutant_generator) - def _get_mutant_generator(self) -> mu.FirstOrderMutator: - operators_set = set() - operators_set |= mo.standard_operators - operators_set |= mo.experimental_operators - - # percentage of the generated mutants (mutation sampling) - percentage = 100 + operators = [ + *mo.standard_operators, + *mo.experimental_operators, + ] mutation_strategy = config.configuration.test_case_output.mutation_strategy if mutation_strategy == config.MutationStrategy.FIRST_ORDER_MUTANTS: - return mu.FirstOrderMutator(operators_set, percentage) + return mu.FirstOrderMutator(operators) order = config.configuration.test_case_output.mutation_order + if order <= 0: raise ConfigurationException("Mutation order should be > 0.") if mutation_strategy in self._strategies: hom_strategy = self._strategies[mutation_strategy](order) - return mu.HighOrderMutator(operators_set, percentage, hom_strategy) + return mu.HighOrderMutator(operators, hom_strategy=hom_strategy) + raise ConfigurationException("No suitable mutation strategy found.") diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 15a320bc..c6a8ec9a 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -14,13 +14,10 @@ import random import types -from typing import Any - -def create_module(ast_node: ast.Module, module_name: str = "mutant", module_dict: dict[str, Any] | None = None): +def create_module(ast_node: ast.Module, module_name: str) -> types.ModuleType: code = compile(ast_node, module_name, "exec") module = types.ModuleType(module_name) - module.__dict__.update(module_dict or {}) exec(code, module.__dict__) return module From dfcf1609bbe753f33fae9e32610d365175e12463 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:51:35 +0100 Subject: [PATCH 26/76] Move utils.create_module to MutationAdapter --- src/pynguin/assertion/assertiongenerator.py | 22 ++++++------------- .../mutation_analysis/mutationadapter.py | 10 ++++++++- .../assertion/mutation_analysis/utils.py | 8 ------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index 9235cfbb..bb0a020e 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -15,8 +15,6 @@ from typing import TYPE_CHECKING -import pynguin.assertion.mutation_analysis.utils as mu - import pynguin.assertion.assertion as ass import pynguin.assertion.assertion_trace as at import pynguin.assertion.assertiontraceobserver as ato @@ -232,22 +230,16 @@ def get_score(self) -> float: return self.num_killed_mutants / divisor -class MutationAnalysisAssertionGenerator(AssertionGenerator): +class MutationAnalysisAssertionGenerator(AssertionGenerator, ma.MutationAdapter): """Uses mutation analysis to filter out less relevant assertions.""" - def _create_module_with_instrumentation( - self, ast_node, module_name="mutant", module_dict=None - ): - # Mimics mutpy.utils.create_module but adds instrumentation to the resulting - # module + def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: code = compile(ast_node, module_name, "exec") if self._testing: self._testing_created_mutants.append(ast.unparse(ast_node)) code = self._transformer.instrument_module(code) module = types.ModuleType(module_name) - module.__dict__.update(module_dict or {}) - - exec(code, module.__dict__) # noqa: S102 + exec(code, module.__dict__) return module def __init__(self, plain_executor: ex.TestCaseExecutor, *, testing: bool = False): @@ -276,11 +268,11 @@ def __init__(self, plain_executor: ex.TestCaseExecutor, *, testing: bool = False self._testing = testing self._testing_created_mutants: list[str] = [] self._testing_mutation_summary: _MutationSummary = _MutationSummary() - adapter = ma.MutationAdapter() - # Evil hack to change the way mutpy creates mutated modules. - mu.create_module = self._create_module_with_instrumentation - self._mutated_modules = [x for x, _ in adapter.mutate_module()] + self._mutated_modules = [ + module + for module, _ in self.mutate_module() + ] def _add_assertions(self, test_cases: list[tc.TestCase]): super()._add_assertions(test_cases) diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 18d5f2d4..0be2a1fe 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -7,9 +7,11 @@ """Provides an adapter for the MutPy mutation testing framework.""" from __future__ import annotations +import ast import inspect import importlib import logging +import types from typing import TYPE_CHECKING @@ -65,13 +67,19 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: _LOGGER.info("Mutate module %s", target_module.__name__) mutants = [ - (utils.create_module(mutant_ast, target_module.__name__), mutations) + (self.create_module(mutant_ast, target_module.__name__), mutations) for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module) ] _LOGGER.info("Generated %d mutants", len(mutants)) return mutants + def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: + code = compile(ast_node, module_name, "exec") + module = types.ModuleType(module_name) + exec(code, module.__dict__) + return module + def _get_mutant_generator(self) -> mu.FirstOrderMutator: operators = [ *mo.standard_operators, diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index c6a8ec9a..99101c55 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -12,14 +12,6 @@ import ast import copy import random -import types - - -def create_module(ast_node: ast.Module, module_name: str) -> types.ModuleType: - code = compile(ast_node, module_name, "exec") - module = types.ModuleType(module_name) - exec(code, module.__dict__) - return module class RandomSampler: From 26455603455c49f5d3d3d4673021eddd26848c14 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:54:38 +0100 Subject: [PATCH 27/76] Move is_docstring to misc module --- .../mutation_analysis/operators/misc.py | 24 ++++++++++++++++--- .../assertion/mutation_analysis/utils.py | 18 -------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index 9b397c85..0140f43b 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -7,15 +7,33 @@ """Provides classes for mutation testing. Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/misc.py. +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py. """ import ast -from pynguin.assertion.mutation_analysis import utils from pynguin.assertion.mutation_analysis.operators.arithmetic import AbstractArithmeticOperatorReplacement from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +def is_docstring(node: ast.AST) -> bool: + if not isinstance(node, ast.Str): + return False + + expression_node = getattr(node, "parent") + + if not isinstance(expression_node, ast.Expr): + return False + + def_node = getattr(expression_node, "parent") + + return ( + isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) + and def_node.body + and def_node.body[0] == expression_node + ) + + class AssignmentOperatorReplacement(AbstractArithmeticOperatorReplacement): def should_mutate(self, node: ast.AST) -> bool: parent = getattr(node, "parent") @@ -35,7 +53,7 @@ class ConstantReplacement(MutationOperator): SECOND_CONST_STRING = "python" def help_str(self, node: ast.Constant) -> str | None: - if utils.is_docstring(node): + if is_docstring(node): return None if node.value == self.FIRST_CONST_STRING: @@ -45,7 +63,7 @@ def help_str(self, node: ast.Constant) -> str | None: @staticmethod def help_str_empty(node: ast.Constant) -> str | None: - if not node.value or utils.is_docstring(node): + if not node.value or is_docstring(node): return None return "" diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 99101c55..dce9f787 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -65,21 +65,3 @@ def visit(self, node: ast.AST) -> ast.AST: def create_ast(code: str) -> ast.AST: return ParentNodeTransformer().visit(ast.parse(code)) - - -def is_docstring(node: ast.AST) -> bool: - if not isinstance(node, ast.Str): - return False - - expression_node = getattr(node, "parent") - - if not isinstance(expression_node, ast.Expr): - return False - - def_node = getattr(expression_node, "parent") - - return ( - isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) - and def_node.body - and def_node.body[0] == expression_node - ) From ed598d708ec73735e390481afe61e08e6a70709b Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:06:58 +0100 Subject: [PATCH 28/76] Move RandomSampler to sampler module --- .../assertion/mutation_analysis/mutators.py | 4 ++-- .../mutation_analysis/operators/base.py | 6 +++--- .../assertion/mutation_analysis/sampler.py | 19 +++++++++++++++++++ .../assertion/mutation_analysis/utils.py | 9 --------- 4 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 src/pynguin/assertion/mutation_analysis/sampler.py diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index b2e0dc52..e357304d 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -16,9 +16,9 @@ from typing import Generator -from pynguin.assertion.mutation_analysis import utils from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator from pynguin.assertion.mutation_analysis.stategies import HOMStrategy, FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.sampler import RandomSampler class Mutator(abc.ABC): @@ -43,7 +43,7 @@ class FirstOrderMutator(Mutator): def __init__(self, operators: list[type[MutationOperator]], percentage: int = 100) -> None: self.operators = operators - self.sampler = utils.RandomSampler(percentage) + self.sampler = RandomSampler(percentage) def mutate( self, diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 80da3820..d7117b60 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -18,7 +18,7 @@ from typing import Generator, Callable, TypeVar -from pynguin.assertion.mutation_analysis import utils +from pynguin.assertion.mutation_analysis.sampler import RandomSampler def fix_lineno(node: ast.AST) -> None: @@ -77,7 +77,7 @@ class MutationOperator: def mutate( cls, node: T, - sampler: utils.RandomSampler | None = None, + sampler: RandomSampler | None = None, module: types.ModuleType | None = None, only_mutation: Mutation | None = None ): @@ -88,7 +88,7 @@ def mutate( def __init__( self, - sampler: utils.RandomSampler | None, + sampler: RandomSampler | None, module: types.ModuleType | None, only_mutation: Mutation | None, ) -> None: diff --git a/src/pynguin/assertion/mutation_analysis/sampler.py b/src/pynguin/assertion/mutation_analysis/sampler.py new file mode 100644 index 00000000..7db7e2ed --- /dev/null +++ b/src/pynguin/assertion/mutation_analysis/sampler.py @@ -0,0 +1,19 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +"""Provides classes for mutation testing. + +Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py. +""" +import random + + +class RandomSampler: + def __init__(self, percentage: int) -> None: + self.percentage = percentage if 0 < percentage < 100 else 100 + + def is_mutation_time(self) -> bool: + return random.randrange(100) < self.percentage diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index dce9f787..81a31253 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -11,15 +11,6 @@ import ast import copy -import random - - -class RandomSampler: - def __init__(self, percentage: int) -> None: - self.percentage = percentage if 0 < percentage < 100 else 100 - - def is_mutation_time(self) -> bool: - return random.randrange(100) < self.percentage class ParentNodeTransformer(ast.NodeTransformer): From 88a9b27309c0af3233b212fb17e94cd3cf111496 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:09:12 +0100 Subject: [PATCH 29/76] Switch from random module to Pynguin randomness module --- src/pynguin/assertion/mutation_analysis/sampler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/sampler.py b/src/pynguin/assertion/mutation_analysis/sampler.py index 7db7e2ed..8bd6294b 100644 --- a/src/pynguin/assertion/mutation_analysis/sampler.py +++ b/src/pynguin/assertion/mutation_analysis/sampler.py @@ -8,7 +8,7 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py. """ -import random +from pynguin.utils import randomness class RandomSampler: @@ -16,4 +16,4 @@ def __init__(self, percentage: int) -> None: self.percentage = percentage if 0 < percentage < 100 else 100 def is_mutation_time(self) -> bool: - return random.randrange(100) < self.percentage + return randomness.next_int(0, 100) < self.percentage From d7cfccf1341df35a79396e447fe8c0a539c4ebc1 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:12:00 +0100 Subject: [PATCH 30/76] Move create_ast to ParentNodeTransformer.create_ast --- .../assertion/mutation_analysis/mutationadapter.py | 4 ++-- src/pynguin/assertion/mutation_analysis/utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 0be2a1fe..402d62e5 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -18,10 +18,10 @@ import pynguin.assertion.mutation_analysis.operators as mo import pynguin.assertion.mutation_analysis.stategies as ms import pynguin.assertion.mutation_analysis.mutators as mu -import pynguin.assertion.mutation_analysis.utils as utils import pynguin.configuration as config +from pynguin.assertion.mutation_analysis.utils import ParentNodeTransformer from pynguin.utils.exceptions import ConfigurationException @@ -63,7 +63,7 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: _LOGGER.info("Build AST for %s", target_module.__name__) target_source_code = inspect.getsource(target_module) - target_ast = utils.create_ast(target_source_code) + target_ast = ParentNodeTransformer.create_ast(target_source_code) _LOGGER.info("Mutate module %s", target_module.__name__) mutants = [ diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/utils.py index 81a31253..15299c69 100644 --- a/src/pynguin/assertion/mutation_analysis/utils.py +++ b/src/pynguin/assertion/mutation_analysis/utils.py @@ -14,6 +14,10 @@ class ParentNodeTransformer(ast.NodeTransformer): + @classmethod + def create_ast(cls, code: str) -> ast.AST: + return cls().visit(ast.parse(code)) + def __init__(self) -> None: super().__init__() self.parent: ast.AST | None = None @@ -52,7 +56,3 @@ def visit(self, node: ast.AST) -> ast.AST: parent_children.update(node_children) return node - - -def create_ast(code: str) -> ast.AST: - return ParentNodeTransformer().visit(ast.parse(code)) From cdad45945ad7eab262f85b0dc800fc28ed830043 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:13:26 +0100 Subject: [PATCH 31/76] Rename utils module to transformer module --- src/pynguin/assertion/mutation_analysis/mutationadapter.py | 2 +- .../assertion/mutation_analysis/operators/inheritance.py | 1 - .../assertion/mutation_analysis/{utils.py => transformer.py} | 0 3 files changed, 1 insertion(+), 2 deletions(-) rename src/pynguin/assertion/mutation_analysis/{utils.py => transformer.py} (100%) diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/mutationadapter.py index 402d62e5..ce3de6eb 100644 --- a/src/pynguin/assertion/mutation_analysis/mutationadapter.py +++ b/src/pynguin/assertion/mutation_analysis/mutationadapter.py @@ -21,7 +21,7 @@ import pynguin.configuration as config -from pynguin.assertion.mutation_analysis.utils import ParentNodeTransformer +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.utils.exceptions import ConfigurationException diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 4b23738c..f91367f5 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -12,7 +12,6 @@ import ast import functools -from pynguin.assertion.mutation_analysis import utils from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node, set_lineno, shift_lines diff --git a/src/pynguin/assertion/mutation_analysis/utils.py b/src/pynguin/assertion/mutation_analysis/transformer.py similarity index 100% rename from src/pynguin/assertion/mutation_analysis/utils.py rename to src/pynguin/assertion/mutation_analysis/transformer.py From 8b9121071c3d85be3bfb346260cae260a5d06ae1 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:16:47 +0100 Subject: [PATCH 32/76] Rename mutationadapter module to controller module --- src/pynguin/assertion/assertiongenerator.py | 4 ++-- .../mutation_analysis/{mutationadapter.py => controller.py} | 0 .../{test_mutationadapter.py => test_controller.py} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/pynguin/assertion/mutation_analysis/{mutationadapter.py => controller.py} (100%) rename tests/assertion/mutation_analysis/{test_mutationadapter.py => test_controller.py} (88%) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index bb0a020e..04e0e4ab 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -18,7 +18,7 @@ import pynguin.assertion.assertion as ass import pynguin.assertion.assertion_trace as at import pynguin.assertion.assertiontraceobserver as ato -import pynguin.assertion.mutation_analysis.mutationadapter as ma +import pynguin.assertion.mutation_analysis.controller as c import pynguin.configuration as config import pynguin.ga.chromosomevisitor as cv import pynguin.testcase.execution as ex @@ -230,7 +230,7 @@ def get_score(self) -> float: return self.num_killed_mutants / divisor -class MutationAnalysisAssertionGenerator(AssertionGenerator, ma.MutationAdapter): +class MutationAnalysisAssertionGenerator(AssertionGenerator, c.MutationAdapter): """Uses mutation analysis to filter out less relevant assertions.""" def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: diff --git a/src/pynguin/assertion/mutation_analysis/mutationadapter.py b/src/pynguin/assertion/mutation_analysis/controller.py similarity index 100% rename from src/pynguin/assertion/mutation_analysis/mutationadapter.py rename to src/pynguin/assertion/mutation_analysis/controller.py diff --git a/tests/assertion/mutation_analysis/test_mutationadapter.py b/tests/assertion/mutation_analysis/test_controller.py similarity index 88% rename from tests/assertion/mutation_analysis/test_mutationadapter.py rename to tests/assertion/mutation_analysis/test_controller.py index 7f697160..52067708 100644 --- a/tests/assertion/mutation_analysis/test_mutationadapter.py +++ b/tests/assertion/mutation_analysis/test_controller.py @@ -9,10 +9,10 @@ import mutpy.controller -import pynguin.assertion.mutation_analysis.mutationadapter as ma +import pynguin.assertion.mutation_analysis.controller as c -class FooAdapter(ma.MutationAdapter): +class FooAdapter(c.MutationAdapter): pass From 336819cfb59ae1dc02dd274b53c8d3394aaa815f Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:18:24 +0100 Subject: [PATCH 33/76] Rename MutationAdapter to MutationController --- src/pynguin/assertion/assertiongenerator.py | 2 +- src/pynguin/assertion/mutation_analysis/controller.py | 2 +- tests/assertion/mutation_analysis/test_controller.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index 04e0e4ab..8cd33f2e 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -230,7 +230,7 @@ def get_score(self) -> float: return self.num_killed_mutants / divisor -class MutationAnalysisAssertionGenerator(AssertionGenerator, c.MutationAdapter): +class MutationAnalysisAssertionGenerator(AssertionGenerator, c.MutationController): """Uses mutation analysis to filter out less relevant assertions.""" def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index ce3de6eb..7d7ce3fe 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) -class MutationAdapter: +class MutationController: """Adapter class for interactions with the MutPy mutation testing framework.""" _strategies: ClassVar[ diff --git a/tests/assertion/mutation_analysis/test_controller.py b/tests/assertion/mutation_analysis/test_controller.py index 52067708..bce9e192 100644 --- a/tests/assertion/mutation_analysis/test_controller.py +++ b/tests/assertion/mutation_analysis/test_controller.py @@ -12,7 +12,7 @@ import pynguin.assertion.mutation_analysis.controller as c -class FooAdapter(c.MutationAdapter): +class FooController(c.MutationController): pass @@ -21,7 +21,7 @@ class FooMutController(mutpy.controller.MutationController): def test_mutate_module(): - adapter = FooAdapter() + adapter = FooController() controller = FooMutController( MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock() ) From c81b7d4bc6de48b3e1bc114d7a92a20adcd88742 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:26:31 +0100 Subject: [PATCH 34/76] Extract code to create_mutants method --- .../assertion/mutation_analysis/controller.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 7d7ce3fe..561d6a82 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -21,6 +21,7 @@ import pynguin.configuration as config +from pynguin.assertion.mutation_analysis.operators.base import Mutation from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.utils.exceptions import ConfigurationException @@ -66,14 +67,22 @@ def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: target_ast = ParentNodeTransformer.create_ast(target_source_code) _LOGGER.info("Mutate module %s", target_module.__name__) - mutants = [ - (self.create_module(mutant_ast, target_module.__name__), mutations) - for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module) - ] + mutants = self.create_mutants(mutant_generator, target_ast, target_module) _LOGGER.info("Generated %d mutants", len(mutants)) return mutants + def create_mutants( + self, + mutant_generator: mu.FirstOrderMutator, + target_ast: ast.Module, + target_module: types.ModuleType, + ) -> list[tuple[ModuleType, list[Mutation]]]: + return [ + (self.create_module(mutant_ast, target_module.__name__), mutations) + for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module) + ] + def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: code = compile(ast_node, module_name, "exec") module = types.ModuleType(module_name) From 45e41e86216f8f4f56f5032cfd7db9139d709f7d Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:27:11 +0100 Subject: [PATCH 35/76] Remove over identation --- .../assertion/mutation_analysis/mutators.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index e357304d..52426e06 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -22,21 +22,21 @@ class Mutator(abc.ABC): - @abc.abstractmethod - def mutate( - self, - target_ast: ast.AST, - module: types.ModuleType | None = None, - ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: - """Mutate the given AST. - - Args: - target_ast: The AST to mutate. - module: The module to mutate. - - Returns: - A generator of mutations and the mutated AST. - """ + @abc.abstractmethod + def mutate( + self, + target_ast: ast.AST, + module: types.ModuleType | None = None, + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: + """Mutate the given AST. + + Args: + target_ast: The AST to mutate. + module: The module to mutate. + + Returns: + A generator of mutations and the mutated AST. + """ class FirstOrderMutator(Mutator): From 4c0d0c4956359adc5ceaf6ce3b9722b931277333 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:29:05 +0100 Subject: [PATCH 36/76] Use a dataclass for Mutation class --- .../assertion/mutation_analysis/operators/base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index d7117b60..dd80c5af 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -16,6 +16,7 @@ import copy import types +from dataclasses import dataclass from typing import Generator, Callable, TypeVar from pynguin.assertion.mutation_analysis.sampler import RandomSampler @@ -55,11 +56,11 @@ def shift_lines(nodes: list[ast.AST], shift_by: int = 1) -> None: ast.increment_lineno(node, shift_by) +@dataclass class Mutation: - def __init__(self, node: ast.AST, operator: type[MutationOperator], visitor_name: str) -> None: - self.node = node - self.operator = operator - self.visitor_name = visitor_name + node: ast.AST + operator: type[MutationOperator] + visitor_name: str T = TypeVar("T", bound=ast.AST) From 99547a028e565c1b0e5a10498319613fdb1e3113 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:41:20 +0100 Subject: [PATCH 37/76] Catch exceptions while creating mutants --- .../assertion/mutation_analysis/controller.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 561d6a82..65573647 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -78,10 +78,18 @@ def create_mutants( target_ast: ast.Module, target_module: types.ModuleType, ) -> list[tuple[ModuleType, list[Mutation]]]: - return [ - (self.create_module(mutant_ast, target_module.__name__), mutations) - for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module) - ] + mutants: list[tuple[ModuleType, list[Mutation]]] = [] + + for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module): + try: + mutant_module = self.create_module(mutant_ast, target_module.__name__) + except Exception as exception: + _LOGGER.debug("Error creating mutant: %s", exception) + continue + + mutants.append((mutant_module, mutations)) + + return mutants def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: code = compile(ast_node, module_name, "exec") From d84fb2d3a14b38389c3d2687df2bb1d823bc20db Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:57:29 +0100 Subject: [PATCH 38/76] Fix forgotten utils --- .../assertion/mutation_analysis/operators/inheritance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index f91367f5..0a799ea1 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -13,6 +13,7 @@ import functools from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node, set_lineno, shift_lines +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer class AbstractOverriddenElementModification(MutationOperator): @@ -208,7 +209,7 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: return mutated_node def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: - function_def: ast.FunctionDef = utils.create_ast(f"super().{node.name}()") + function_def: ast.FunctionDef = ParentNodeTransformer.create_ast(f"super().{node.name}()") super_call: ast.Expr = function_def.body[0] From c82eeb38fedde51f186f3c648b28d1a035278844 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:25:36 +0100 Subject: [PATCH 39/76] Use getattr instead of node.children --- src/pynguin/assertion/mutation_analysis/operators/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index dd80c5af..d90b0de5 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -98,7 +98,9 @@ def __init__( self.only_mutation = only_mutation def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: - if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node.children: + node_children = getattr(node, "children") + + if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node_children: return fix_lineno(node) From fe9975fdfed6b8cf33f3243c326bddc5336ea7ac Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 21 Mar 2024 23:35:23 +0100 Subject: [PATCH 40/76] Remove redundant calls to generic_visit --- .../assertion/mutation_analysis/operators/base.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index d90b0de5..ba61cabd 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -105,13 +105,7 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: fix_lineno(node) - visitors = self.find_visitors(node) - - if not visitors: - yield from self.generic_visit(node) - return - - for visitor in visitors: + for visitor in self.find_visitors(node): if ( ( self.sampler is None @@ -131,7 +125,7 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: yield node, mutated_node, visitor.__name__ - yield from self.generic_visit(node) + yield from self.generic_visit(node) def generic_visit(self, node: ast.AST) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: for field, old_value in ast.iter_fields(node): From 913bb77cdf31152453875aceb70b5705dd13d895 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 00:03:37 +0100 Subject: [PATCH 41/76] Use an assert instead of hidding an error --- src/pynguin/assertion/mutation_analysis/sampler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pynguin/assertion/mutation_analysis/sampler.py b/src/pynguin/assertion/mutation_analysis/sampler.py index 8bd6294b..c9e32678 100644 --- a/src/pynguin/assertion/mutation_analysis/sampler.py +++ b/src/pynguin/assertion/mutation_analysis/sampler.py @@ -13,7 +13,8 @@ class RandomSampler: def __init__(self, percentage: int) -> None: - self.percentage = percentage if 0 < percentage < 100 else 100 + assert 0 <= percentage and percentage <= 100 + self.percentage = percentage def is_mutation_time(self) -> bool: return randomness.next_int(0, 100) < self.percentage From 1e65cff9beaa8963d134bcbd7d3c0b0326f27da9 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:53:22 +0100 Subject: [PATCH 42/76] Remove sampler as it was always set to 100% --- .../assertion/mutation_analysis/mutators.py | 18 +++++------------ .../mutation_analysis/operators/base.py | 11 +--------- .../assertion/mutation_analysis/sampler.py | 20 ------------------- 3 files changed, 6 insertions(+), 43 deletions(-) delete mode 100644 src/pynguin/assertion/mutation_analysis/sampler.py diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index 52426e06..d2c82848 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -18,7 +18,6 @@ from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator from pynguin.assertion.mutation_analysis.stategies import HOMStrategy, FirstToLastHOMStrategy -from pynguin.assertion.mutation_analysis.sampler import RandomSampler class Mutator(abc.ABC): @@ -41,9 +40,8 @@ def mutate( class FirstOrderMutator(Mutator): - def __init__(self, operators: list[type[MutationOperator]], percentage: int = 100) -> None: + def __init__(self, operators: list[type[MutationOperator]]) -> None: self.operators = operators - self.sampler = RandomSampler(percentage) def mutate( self, @@ -51,7 +49,7 @@ def mutate( module: types.ModuleType | None = None, ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: for op in self.operators: - for mutation, mutant in op.mutate(target_ast, self.sampler, module=module): + for mutation, mutant in op.mutate(target_ast, module): yield [mutation], mutant @@ -60,10 +58,9 @@ class HighOrderMutator(FirstOrderMutator): def __init__( self, operators: list[type[MutationOperator]], - percentage: int = 100, hom_strategy: HOMStrategy | None = None, ) -> None: - super().__init__(operators, percentage) + super().__init__(operators) self.hom_strategy = hom_strategy or FirstToLastHOMStrategy() def mutate( @@ -77,12 +74,7 @@ def mutate( applied_mutations = [] mutant = target_ast for mutation in mutations_to_apply: - generator = mutation.operator.mutate( - mutant, - sampler=self.sampler, - module=module, - only_mutation=mutation, - ) + generator = mutation.operator.mutate(mutant, module, mutation) try: new_mutation, mutant = generator.__next__() except StopIteration: @@ -99,7 +91,7 @@ def generate_all_mutations( ) -> list[Mutation]: mutations: list[Mutation] = [] for op in self.operators: - for mutation, _ in op.mutate(target_ast, None, module=module): + for mutation, _ in op.mutate(target_ast, module): mutations.append(mutation) return mutations diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index ba61cabd..6175c85b 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -19,8 +19,6 @@ from dataclasses import dataclass from typing import Generator, Callable, TypeVar -from pynguin.assertion.mutation_analysis.sampler import RandomSampler - def fix_lineno(node: ast.AST) -> None: parent = getattr(node, "parent") @@ -78,22 +76,19 @@ class MutationOperator: def mutate( cls, node: T, - sampler: RandomSampler | None = None, module: types.ModuleType | None = None, only_mutation: Mutation | None = None ): - operator = cls(sampler, module, only_mutation) + operator = cls(module, only_mutation) for current_node, mutated_node, visitor_name in operator.visit(node): yield Mutation(current_node, cls, visitor_name), mutated_node def __init__( self, - sampler: RandomSampler | None, module: types.ModuleType | None, only_mutation: Mutation | None, ) -> None: - self.sampler = sampler self.module = module self.only_mutation = only_mutation @@ -108,10 +103,6 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: for visitor in self.find_visitors(node): if ( ( - self.sampler is None - or self.sampler.is_mutation_time() - ) - and ( self.only_mutation is None or ( self.only_mutation.node == node diff --git a/src/pynguin/assertion/mutation_analysis/sampler.py b/src/pynguin/assertion/mutation_analysis/sampler.py deleted file mode 100644 index c9e32678..00000000 --- a/src/pynguin/assertion/mutation_analysis/sampler.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file is part of Pynguin. -# -# SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors -# -# SPDX-License-Identifier: MIT -# -"""Provides classes for mutation testing. - -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py. -""" -from pynguin.utils import randomness - - -class RandomSampler: - def __init__(self, percentage: int) -> None: - assert 0 <= percentage and percentage <= 100 - self.percentage = percentage - - def is_mutation_time(self) -> bool: - return randomness.next_int(0, 100) < self.percentage From 350290e642278c8b74fa82f6f13bf7b53391790f Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:19:04 +0100 Subject: [PATCH 43/76] Fix typing --- src/pynguin/assertion/assertiongenerator.py | 5 +- .../assertion/mutation_analysis/controller.py | 9 +-- .../assertion/mutation_analysis/mutators.py | 14 ++-- .../mutation_analysis/operators/__init__.py | 64 ++++++++++++++--- .../mutation_analysis/operators/arithmetic.py | 5 +- .../mutation_analysis/operators/base.py | 64 ++++++++++------- .../mutation_analysis/operators/decorator.py | 3 +- .../mutation_analysis/operators/exception.py | 8 ++- .../operators/inheritance.py | 70 ++++++++++++++----- .../mutation_analysis/operators/logical.py | 13 +++- .../mutation_analysis/operators/loop.py | 3 +- .../mutation_analysis/operators/misc.py | 10 +-- .../assertion/mutation_analysis/stategies.py | 40 ++++++++--- .../mutation_analysis/transformer.py | 9 ++- 14 files changed, 228 insertions(+), 89 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index 8cd33f2e..5ef9c52a 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -269,10 +269,7 @@ def __init__(self, plain_executor: ex.TestCaseExecutor, *, testing: bool = False self._testing_created_mutants: list[str] = [] self._testing_mutation_summary: _MutationSummary = _MutationSummary() - self._mutated_modules = [ - module - for module, _ in self.mutate_module() - ] + self._mutated_modules = [module for module, _ in self.mutate_module()] def _add_assertions(self, test_cases: list[tc.TestCase]): super()._add_assertions(test_cases) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 65573647..7eb024e6 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -8,17 +8,16 @@ from __future__ import annotations import ast -import inspect import importlib +import inspect import logging import types from typing import TYPE_CHECKING +import pynguin.assertion.mutation_analysis.mutators as mu import pynguin.assertion.mutation_analysis.operators as mo import pynguin.assertion.mutation_analysis.stategies as ms -import pynguin.assertion.mutation_analysis.mutators as mu - import pynguin.configuration as config from pynguin.assertion.mutation_analysis.operators.base import Mutation @@ -47,7 +46,7 @@ class MutationController: config.MutationStrategy.EACH_CHOICE: ms.EachChoiceHOMStrategy, } - def mutate_module(self) -> list[tuple[ModuleType, list[mo.Mutation]]]: + def mutate_module(self) -> list[tuple[ModuleType, list[Mutation]]]: """Mutates the modules specified in the configuration. Uses MutPy's mutation procedure. @@ -81,6 +80,8 @@ def create_mutants( mutants: list[tuple[ModuleType, list[Mutation]]] = [] for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module): + assert isinstance(mutant_ast, ast.Module) + try: mutant_module = self.create_module(mutant_ast, target_module.__name__) except Exception as exception: diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index d2c82848..e8b7b9fb 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -10,14 +10,16 @@ """ from __future__ import annotations -import ast import abc +import ast import types from typing import Generator -from pynguin.assertion.mutation_analysis.operators.base import Mutation, MutationOperator -from pynguin.assertion.mutation_analysis.stategies import HOMStrategy, FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.operators.base import Mutation +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.stategies import FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.stategies import HOMStrategy class Mutator(abc.ABC): @@ -47,7 +49,7 @@ def mutate( self, target_ast: ast.AST, module: types.ModuleType | None = None, - ) -> Generator[tuple[list[Mutation], ast.Module], None, None]: + ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: for op in self.operators: for mutation, mutant in op.mutate(target_ast, module): yield [mutation], mutant @@ -78,7 +80,7 @@ def mutate( try: new_mutation, mutant = generator.__next__() except StopIteration: - assert False, 'no mutations!' + assert False, "no mutations!" applied_mutations.append(new_mutation) generators.append(generator) yield applied_mutations, mutant @@ -101,4 +103,4 @@ def finish_generators(self, generators: list[Generator]) -> None: generator.__next__() except StopIteration: continue - assert False, 'too many mutations!' + assert False, "too many mutations!" diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py index f4bd4da0..833d844a 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -9,14 +9,62 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/__init__.py. """ -from pynguin.assertion.mutation_analysis.operators.arithmetic import * -from pynguin.assertion.mutation_analysis.operators.base import * -from pynguin.assertion.mutation_analysis.operators.decorator import * -from pynguin.assertion.mutation_analysis.operators.exception import * -from pynguin.assertion.mutation_analysis.operators.inheritance import * -from pynguin.assertion.mutation_analysis.operators.logical import * -from pynguin.assertion.mutation_analysis.operators.loop import * -from pynguin.assertion.mutation_analysis.operators.misc import * +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node +from pynguin.assertion.mutation_analysis.operators.base import set_lineno +from pynguin.assertion.mutation_analysis.operators.base import shift_lines +from pynguin.assertion.mutation_analysis.operators.decorator import DecoratorDeletion +from pynguin.assertion.mutation_analysis.operators.exception import ( + ExceptionHandlerDeletion, +) +from pynguin.assertion.mutation_analysis.operators.exception import ExceptionSwallowing +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + HidingVariableDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverriddenMethodCallingPositionChange, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverridingMethodDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + SuperCallingDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import SuperCallingInsert +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorInsertion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalConnectorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + RelationalOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.loop import OneIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ReverseIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ZeroIterationLoop +from pynguin.assertion.mutation_analysis.operators.misc import ( + AssignmentOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.misc import BreakContinueReplacement +from pynguin.assertion.mutation_analysis.operators.misc import ConstantReplacement +from pynguin.assertion.mutation_analysis.operators.misc import SliceIndexRemove + standard_operators = [ ArithmeticOperatorDeletion, diff --git a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py index d81c7300..81443259 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py +++ b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py @@ -11,7 +11,10 @@ import ast -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, AbstractUnaryOperatorDeletion +from pynguin.assertion.mutation_analysis.operators.base import ( + AbstractUnaryOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator class ArithmeticOperatorDeletion(AbstractUnaryOperatorDeletion): diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 6175c85b..428709ff 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -1,4 +1,3 @@ - # This file is part of Pynguin. # # SPDX-FileCopyrightText: 2019-2023 Pynguin Contributors @@ -17,7 +16,10 @@ import types from dataclasses import dataclass -from typing import Generator, Callable, TypeVar +from typing import Callable +from typing import Generator +from typing import Iterable +from typing import TypeVar def fix_lineno(node: ast.AST) -> None: @@ -49,7 +51,10 @@ def set_lineno(node: ast.AST, lineno: int) -> None: setattr(child_node, "lineno", lineno) -def shift_lines(nodes: list[ast.AST], shift_by: int = 1) -> None: +T = TypeVar("T", bound=ast.AST) + + +def shift_lines(nodes: list[T], shift_by: int = 1) -> None: for node in nodes: ast.increment_lineno(node, shift_by) @@ -61,14 +66,14 @@ class Mutation: visitor_name: str -T = TypeVar("T", bound=ast.AST) - - def copy_node(node: T) -> T: parent = getattr(node, "parent") - return copy.deepcopy(node, memo={ - id(parent): parent, - }) + return copy.deepcopy( + node, + memo={ + id(parent): parent, + }, + ) class MutationOperator: @@ -77,8 +82,8 @@ def mutate( cls, node: T, module: types.ModuleType | None = None, - only_mutation: Mutation | None = None - ): + only_mutation: Mutation | None = None, + ) -> Generator[tuple[Mutation, ast.AST], None, None]: operator = cls(module, only_mutation) for current_node, mutated_node, visitor_name in operator.visit(node): @@ -95,22 +100,23 @@ def __init__( def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: node_children = getattr(node, "children") - if self.only_mutation and self.only_mutation.node != node and self.only_mutation.node not in node_children: + if ( + self.only_mutation + and self.only_mutation.node != node + and self.only_mutation.node not in node_children + ): return fix_lineno(node) for visitor in self.find_visitors(node): if ( - ( - self.only_mutation is None - or ( - self.only_mutation.node == node - and self.only_mutation.visitor_name == visitor.__name__ - ) + self.only_mutation is None + or ( + self.only_mutation.node == node + and self.only_mutation.visitor_name == visitor.__name__ ) - and (mutated_node := visitor(node)) is not None - ): + ) and (mutated_node := visitor(node)) is not None: fix_node_internals(node, mutated_node) ast.fix_missing_locations(mutated_node) @@ -118,19 +124,24 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: yield from self.generic_visit(node) - def generic_visit(self, node: ast.AST) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: + def generic_visit( + self, node: ast.AST + ) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: for field, old_value in ast.iter_fields(node): + generator: Iterable[tuple[ast.AST, str]] if isinstance(old_value, list): generator = self.generic_visit_list(old_value) elif isinstance(old_value, ast.AST): generator = self.generic_visit_real_node(node, field, old_value) else: - generator = [] + generator = () for current_node, visitor_name in generator: yield current_node, node, visitor_name - def generic_visit_list(self, old_value: list) -> Generator[tuple[ast.AST, str], None, None]: + def generic_visit_list( + self, old_value: list + ) -> Generator[tuple[ast.AST, str], None, None]: for position, value in enumerate(old_value.copy()): if isinstance(value, ast.AST): for current_node, mutated_node, visitor_name in self.visit(value): @@ -139,7 +150,9 @@ def generic_visit_list(self, old_value: list) -> Generator[tuple[ast.AST, str], old_value[position] = value - def generic_visit_real_node(self, node: ast.AST, field: str, old_value: ast.AST) -> Generator[tuple[ast.AST, str], None, None]: + def generic_visit_real_node( + self, node: ast.AST, field: str, old_value: ast.AST + ) -> Generator[tuple[ast.AST, str], None, None]: for current_node, mutated_node, visitor_name in self.visit(old_value): setattr(node, field, mutated_node) yield current_node, visitor_name @@ -152,7 +165,8 @@ def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: return [ visitor for attr in dir(self) - if attr.startswith(method_prefix) and callable(visitor := getattr(self, attr)) + if attr.startswith(method_prefix) + and callable(visitor := getattr(self, attr)) ] diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 91f42fee..70557189 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -11,7 +11,8 @@ import ast -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node class DecoratorDeletion(MutationOperator): diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py index a423be40..ae43b1ec 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/exception.py +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -36,7 +36,9 @@ def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler | N if isinstance(first_statement, ast.Raise): return None - return replace_exception_handler(node, [ast.Raise(lineno=first_statement.lineno)]) + return replace_exception_handler( + node, [ast.Raise(lineno=first_statement.lineno)] + ) class ExceptionSwallowing(MutationOperator): @@ -49,4 +51,6 @@ def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler | N if len(node.body) == 1 and isinstance(first_statement, ast.Pass): return None - return replace_exception_handler(node, [ast.Pass(lineno=first_statement.lineno)]) + return replace_exception_handler( + node, [ast.Pass(lineno=first_statement.lineno)] + ) diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 0a799ea1..60e41455 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -12,7 +12,12 @@ import ast import functools -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, copy_node, set_lineno, shift_lines +from typing import cast + +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node +from pynguin.assertion.mutation_analysis.operators.base import set_lineno +from pynguin.assertion.mutation_analysis.operators.base import shift_lines from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer @@ -20,7 +25,9 @@ class AbstractOverriddenElementModification(MutationOperator): def is_overridden(self, node: ast.AST, name: str | None = None) -> bool | None: parent = getattr(node, "parent") - if not isinstance(parent, ast.ClassDef) or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + if not isinstance(parent, ast.ClassDef) or not isinstance( + node, (ast.FunctionDef, ast.AsyncFunctionDef) + ): return None if not name: @@ -31,7 +38,9 @@ def is_overridden(self, node: ast.AST, name: str | None = None) -> bool | None: while parent is not None: if not isinstance(parent, ast.Module): parent_names.append(parent.name) - if not isinstance(parent, ast.ClassDef) and not isinstance(parent, ast.Module): + if not isinstance(parent, ast.ClassDef) and not isinstance( + parent, ast.Module + ): return None parent = getattr(parent, "parent") @@ -63,7 +72,9 @@ def mutate_Assign(self, node: ast.Assign) -> ast.stmt | None: return None return ast.Pass() - elif isinstance(first_expression, ast.Tuple) and isinstance(node.value, ast.Tuple): + elif isinstance(first_expression, ast.Tuple) and isinstance( + node.value, ast.Tuple + ): return self.mutate_unpack(node) else: return None @@ -72,13 +83,15 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: if not node.targets: return None - target = node.targets[0] - value = node.value + target = cast(ast.List | ast.Tuple | ast.Set, node.targets[0]) + value = cast(ast.List | ast.Tuple | ast.Set, node.value) - new_targets: list[ast.Name] = [] + new_targets: list[ast.expr] = [] new_values: list[ast.expr] = [] for target_element, value_element in zip(target.elts, value.elts): - if not isinstance(target_element, ast.Name) or not isinstance(value_element, ast.expr): + if not isinstance(target_element, ast.Name) or not isinstance( + value_element, ast.expr + ): continue overridden = self.is_overridden(node, target_element.id) @@ -113,7 +126,7 @@ def is_super_call(self, node: ast.FunctionDef, stmt: ast.stmt) -> bool: and isinstance(stmt.value.func, ast.Attribute) and isinstance(stmt.value.func.value, ast.Call) and isinstance(stmt.value.func.value.func, ast.Name) - and stmt.value.func.value.func.id == 'super' + and stmt.value.func.value.func.id == "super" and stmt.value.func.attr == node.name ) @@ -188,12 +201,19 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: return mutated_node -class SuperCallingInsert(AbstractSuperCallingModification, AbstractOverriddenElementModification): +class SuperCallingInsert( + AbstractSuperCallingModification, AbstractOverriddenElementModification +): def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: overridden = self.is_overridden(node) - if not self.should_mutate(node) or not node.body or overridden is None or not overridden: + if ( + not self.should_mutate(node) + or not node.body + or overridden is None + or not overridden + ): return None mutated_node = copy_node(node) @@ -209,19 +229,27 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: return mutated_node def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: - function_def: ast.FunctionDef = ParentNodeTransformer.create_ast(f"super().{node.name}()") + module = ParentNodeTransformer.create_ast(f"super().{node.name}()") + + assert module.body + + super_call = module.body[0] - super_call: ast.Expr = function_def.body[0] + assert isinstance(super_call, ast.Expr) - super_call_value: ast.Call = super_call.value + super_call_value = super_call.value - for arg in node.args.args[1:-len(node.args.defaults) or None]: + assert isinstance(super_call_value, ast.Call) + + for arg in node.args.args[1 : -len(node.args.defaults) or None]: super_call_value.args.append(ast.Name(id=arg.arg, ctx=ast.Load())) - for arg, default in zip(node.args.args[-len(node.args.defaults):], node.args.defaults): + for arg, default in zip( + node.args.args[-len(node.args.defaults) :], node.args.defaults + ): super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) - for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): + for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): # type: ignore[assignment] super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) if node.args.vararg is not None: @@ -236,8 +264,12 @@ def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: @staticmethod def add_kwarg_to_super_call(super_call_value: ast.Call, kwarg: ast.arg) -> None: - super_call_value.keywords.append(ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load()))) + super_call_value.keywords.append( + ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load())) + ) @staticmethod def add_vararg_to_super_call(super_call_value: ast.Call, vararg: ast.arg) -> None: - super_call_value.args.append(ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load()))) + super_call_value.args.append( + ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load())) + ) diff --git a/src/pynguin/assertion/mutation_analysis/operators/logical.py b/src/pynguin/assertion/mutation_analysis/operators/logical.py index 28dd8672..b9373c19 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/logical.py +++ b/src/pynguin/assertion/mutation_analysis/operators/logical.py @@ -11,7 +11,13 @@ import ast -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator, AbstractUnaryOperatorDeletion, copy_node +from typing import TypeVar + +from pynguin.assertion.mutation_analysis.operators.base import ( + AbstractUnaryOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node class ConditionalOperatorDeletion(AbstractUnaryOperatorDeletion): @@ -22,8 +28,11 @@ def mutate_NotIn(self, node: ast.NotIn) -> ast.In: return ast.In() +T = TypeVar("T", ast.If, ast.While) + + class ConditionalOperatorInsertion(MutationOperator): - def negate_test(self, node: ast.If | ast.While) -> ast.If | ast.While: + def negate_test(self, node: T) -> T: mutated_node = copy_node(node) not_node = ast.UnaryOp(op=ast.Not(), operand=mutated_node.test) mutated_node.test = not_node diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py index 4849734e..c51c3d79 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/loop.py +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -12,7 +12,8 @@ import ast import typing -from pynguin.assertion.mutation_analysis.operators import copy_node, MutationOperator +from pynguin.assertion.mutation_analysis.operators import MutationOperator +from pynguin.assertion.mutation_analysis.operators import copy_node T = typing.TypeVar("T", ast.For, ast.While) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index 0140f43b..eac3bc48 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -12,7 +12,9 @@ import ast -from pynguin.assertion.mutation_analysis.operators.arithmetic import AbstractArithmeticOperatorReplacement +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + AbstractArithmeticOperatorReplacement, +) from pynguin.assertion.mutation_analysis.operators.base import MutationOperator @@ -20,16 +22,16 @@ def is_docstring(node: ast.AST) -> bool: if not isinstance(node, ast.Str): return False - expression_node = getattr(node, "parent") + expression_node: ast.AST = getattr(node, "parent") if not isinstance(expression_node, ast.Expr): return False - def_node = getattr(expression_node, "parent") + def_node: ast.AST = getattr(expression_node, "parent") return ( isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) - and def_node.body + and def_node.body # type: ignore[return-value] and def_node.body[0] == expression_node ) diff --git a/src/pynguin/assertion/mutation_analysis/stategies.py b/src/pynguin/assertion/mutation_analysis/stategies.py index 6e9cbb58..0bd0389a 100644 --- a/src/pynguin/assertion/mutation_analysis/stategies.py +++ b/src/pynguin/assertion/mutation_analysis/stategies.py @@ -10,9 +10,11 @@ """ from __future__ import annotations +import abc import random -from typing import Generator, Callable +from typing import Callable +from typing import Generator from pynguin.assertion.mutation_analysis.operators.base import Mutation @@ -20,14 +22,16 @@ def remove_bad_mutations( mutations_to_apply: list[Mutation], available_mutations: list[Mutation], - allow_same_operators: bool = True + allow_same_operators: bool = True, ) -> None: for mutation_to_apply in mutations_to_apply: for available_mutation in available_mutations.copy(): if ( mutation_to_apply.node == available_mutation.node - or mutation_to_apply.node in getattr(available_mutation.node, "children") - or available_mutation.node in getattr(mutation_to_apply.node, "children") + or mutation_to_apply.node + in getattr(available_mutation.node, "children") + or available_mutation.node + in getattr(mutation_to_apply.node, "children") or ( not allow_same_operators and mutation_to_apply.operator == available_mutation.operator @@ -36,15 +40,23 @@ def remove_bad_mutations( available_mutations.remove(available_mutation) -class HOMStrategy: +class HOMStrategy(abc.ABC): def __init__(self, order: int = 2) -> None: self.order = order + @abc.abstractmethod + def generate( + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: + raise NotImplementedError + class FirstToLastHOMStrategy(HOMStrategy): - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + def generate( + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: mutations = mutations.copy() while mutations: mutations_to_apply: list[Mutation] = [] @@ -61,7 +73,9 @@ def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, class EachChoiceHOMStrategy(HOMStrategy): - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + def generate( + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: mutations = mutations.copy() while mutations: mutations_to_apply: list[Mutation] = [] @@ -76,7 +90,9 @@ def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, class BetweenOperatorsHOMStrategy(HOMStrategy): - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + def generate( + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: usage = {mutation: 0 for mutation in mutations} not_used = mutations.copy() while not_used: @@ -89,7 +105,9 @@ def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, if usage[mutation] == 0: not_used.remove(mutation) usage[mutation] += 1 - remove_bad_mutations(mutations_to_apply, available_mutations, allow_same_operators=False) + remove_bad_mutations( + mutations_to_apply, available_mutations, allow_same_operators=False + ) yield mutations_to_apply @@ -99,7 +117,9 @@ def __init__(self, order: int = 2, shuffler: Callable = random.shuffle) -> None: super().__init__(order) self.shuffler = shuffler - def generate(self, mutations: list[Mutation]) -> Generator[list[Mutation], None, None]: + def generate( + self, mutations: list[Mutation] + ) -> Generator[list[Mutation], None, None]: mutations = mutations.copy() self.shuffler(mutations) while mutations: diff --git a/src/pynguin/assertion/mutation_analysis/transformer.py b/src/pynguin/assertion/mutation_analysis/transformer.py index 15299c69..e2fbebca 100644 --- a/src/pynguin/assertion/mutation_analysis/transformer.py +++ b/src/pynguin/assertion/mutation_analysis/transformer.py @@ -12,17 +12,22 @@ import ast import copy +from typing import TypeVar + + +T = TypeVar("T", bound=ast.AST) + class ParentNodeTransformer(ast.NodeTransformer): @classmethod - def create_ast(cls, code: str) -> ast.AST: + def create_ast(cls, code: str) -> ast.Module: return cls().visit(ast.parse(code)) def __init__(self) -> None: super().__init__() self.parent: ast.AST | None = None - def visit(self, node: ast.AST) -> ast.AST: + def visit(self, node: T) -> T: # Copy the node because an optimisation of the AST makes it # reuse the same node at multiple places in the tree to # improve memory usage. It would break our goal to create a From 63a893a1e08c6733e99696281650d38c7e3a8111 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:41:25 +0100 Subject: [PATCH 44/76] Fix ruff errors --- src/pynguin/assertion/assertiongenerator.py | 6 +- .../assertion/mutation_analysis/controller.py | 32 ++- .../assertion/mutation_analysis/mutators.py | 60 +++-- .../mutation_analysis/operators/__init__.py | 7 +- .../mutation_analysis/operators/arithmetic.py | 165 ++++++++++-- .../mutation_analysis/operators/base.py | 136 ++++++++-- .../mutation_analysis/operators/decorator.py | 12 +- .../mutation_analysis/operators/exception.py | 37 ++- .../operators/inheritance.py | 237 ++++++++++++----- .../mutation_analysis/operators/logical.py | 248 ++++++++++++++++-- .../mutation_analysis/operators/loop.py | 76 +++++- .../mutation_analysis/operators/misc.py | 166 ++++++++++-- .../assertion/mutation_analysis/stategies.py | 69 +++-- .../mutation_analysis/transformer.py | 27 +- src/pynguin/utils/randomness.py | 9 + 15 files changed, 1066 insertions(+), 221 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index 5ef9c52a..f158a640 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -233,13 +233,15 @@ def get_score(self) -> float: class MutationAnalysisAssertionGenerator(AssertionGenerator, c.MutationController): """Uses mutation analysis to filter out less relevant assertions.""" - def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: + def create_module( # noqa: D102 + self, ast_node: ast.Module, module_name: str + ) -> types.ModuleType: code = compile(ast_node, module_name, "exec") if self._testing: self._testing_created_mutants.append(ast.unparse(ast_node)) code = self._transformer.instrument_module(code) module = types.ModuleType(module_name) - exec(code, module.__dict__) + exec(code, module.__dict__) # noqa: S102 return module def __init__(self, plain_executor: ex.TestCaseExecutor, *, testing: bool = False): diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 7eb024e6..72b0485e 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -20,7 +20,6 @@ import pynguin.assertion.mutation_analysis.stategies as ms import pynguin.configuration as config -from pynguin.assertion.mutation_analysis.operators.base import Mutation from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.utils.exceptions import ConfigurationException @@ -30,6 +29,9 @@ from types import ModuleType from typing import ClassVar + from pynguin.assertion.mutation_analysis.operators.base import Mutation + from pynguin.assertion.mutation_analysis.operators.base import MutationOperator + _LOGGER = logging.getLogger(__name__) @@ -49,8 +51,6 @@ class MutationController: def mutate_module(self) -> list[tuple[ModuleType, list[Mutation]]]: """Mutates the modules specified in the configuration. - Uses MutPy's mutation procedure. - Returns: A list of tuples where the first entry is the mutated module and the second part is a list of all the mutations operators applied. @@ -77,6 +77,17 @@ def create_mutants( target_ast: ast.Module, target_module: types.ModuleType, ) -> list[tuple[ModuleType, list[Mutation]]]: + """Creates mutants for the given module. + + Args: + mutant_generator: The mutant generator. + target_ast: The AST of the target module. + target_module: The target module. + + Returns: + A list of tuples where the first entry is the mutated module and the second + part is a list of all the mutations operators applied. + """ mutants: list[tuple[ModuleType, list[Mutation]]] = [] for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module): @@ -84,7 +95,7 @@ def create_mutants( try: mutant_module = self.create_module(mutant_ast, target_module.__name__) - except Exception as exception: + except Exception as exception: # noqa: BLE001 _LOGGER.debug("Error creating mutant: %s", exception) continue @@ -93,13 +104,22 @@ def create_mutants( return mutants def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: + """Creates a module from an AST node. + + Args: + ast_node: The AST node. + module_name: The name of the module. + + Returns: + The created module. + """ code = compile(ast_node, module_name, "exec") module = types.ModuleType(module_name) - exec(code, module.__dict__) + exec(code, module.__dict__) # noqa: S102 return module def _get_mutant_generator(self) -> mu.FirstOrderMutator: - operators = [ + operators: list[type[MutationOperator]] = [ *mo.standard_operators, *mo.experimental_operators, ] diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index e8b7b9fb..28c88fdc 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -11,18 +11,26 @@ from __future__ import annotations import abc -import ast -import types -from typing import Generator +from typing import TYPE_CHECKING -from pynguin.assertion.mutation_analysis.operators.base import Mutation -from pynguin.assertion.mutation_analysis.operators.base import MutationOperator from pynguin.assertion.mutation_analysis.stategies import FirstToLastHOMStrategy from pynguin.assertion.mutation_analysis.stategies import HOMStrategy +if TYPE_CHECKING: + import ast + import types + + from collections.abc import Generator + + from pynguin.assertion.mutation_analysis.operators.base import Mutation + from pynguin.assertion.mutation_analysis.operators.base import MutationOperator + + class Mutator(abc.ABC): + """A mutator is responsible for mutating an AST.""" + @abc.abstractmethod def mutate( self, @@ -35,17 +43,23 @@ def mutate( target_ast: The AST to mutate. module: The module to mutate. - Returns: + Yields: A generator of mutations and the mutated AST. """ class FirstOrderMutator(Mutator): + """A mutator that applies first order mutations.""" def __init__(self, operators: list[type[MutationOperator]]) -> None: + """Initialize the mutator. + + Args: + operators: The operators to use for mutation. + """ self.operators = operators - def mutate( + def mutate( # noqa: D102 self, target_ast: ast.AST, module: types.ModuleType | None = None, @@ -56,37 +70,43 @@ def mutate( class HighOrderMutator(FirstOrderMutator): + """A mutator that applies high order mutations.""" def __init__( self, operators: list[type[MutationOperator]], hom_strategy: HOMStrategy | None = None, ) -> None: + """Initialize the mutator. + + Args: + operators: The operators to use for mutation. + hom_strategy: The strategy to use for higher order mutations. + """ super().__init__(operators) self.hom_strategy = hom_strategy or FirstToLastHOMStrategy() - def mutate( + def mutate( # noqa: D102 self, target_ast: ast.AST, module: types.ModuleType | None = None, ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: - mutations = self.generate_all_mutations(module, target_ast) + mutations = self._generate_all_mutations(module, target_ast) for mutations_to_apply in self.hom_strategy.generate(mutations): generators = [] applied_mutations = [] mutant = target_ast for mutation in mutations_to_apply: generator = mutation.operator.mutate(mutant, module, mutation) - try: - new_mutation, mutant = generator.__next__() - except StopIteration: - assert False, "no mutations!" + next_value = next(generator, None) + assert next_value is not None + new_mutation, mutant = next_value applied_mutations.append(new_mutation) generators.append(generator) yield applied_mutations, mutant - self.finish_generators(generators) + self._finish_generators(generators) - def generate_all_mutations( + def _generate_all_mutations( self, module: types.ModuleType | None, target_ast: ast.AST, @@ -97,10 +117,8 @@ def generate_all_mutations( mutations.append(mutation) return mutations - def finish_generators(self, generators: list[Generator]) -> None: + @staticmethod + def _finish_generators(generators: list[Generator]) -> None: for generator in reversed(generators): - try: - generator.__next__() - except StopIteration: - continue - assert False, "too many mutations!" + value = next(generator, None) + assert value is None, "too many mutations!" diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py index 833d844a..076f7571 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -16,9 +16,6 @@ ArithmeticOperatorReplacement, ) from pynguin.assertion.mutation_analysis.operators.base import MutationOperator -from pynguin.assertion.mutation_analysis.operators.base import copy_node -from pynguin.assertion.mutation_analysis.operators.base import set_lineno -from pynguin.assertion.mutation_analysis.operators.base import shift_lines from pynguin.assertion.mutation_analysis.operators.decorator import DecoratorDeletion from pynguin.assertion.mutation_analysis.operators.exception import ( ExceptionHandlerDeletion, @@ -66,7 +63,7 @@ from pynguin.assertion.mutation_analysis.operators.misc import SliceIndexRemove -standard_operators = [ +standard_operators: list[type[MutationOperator]] = [ ArithmeticOperatorDeletion, ArithmeticOperatorReplacement, AssignmentOperatorReplacement, @@ -89,7 +86,7 @@ SuperCallingInsert, ] -experimental_operators = [ +experimental_operators: list[type[MutationOperator]] = [ OneIterationLoop, ReverseIterationLoop, ZeroIterationLoop, diff --git a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py index 81443259..0cb7480c 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py +++ b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py @@ -9,6 +9,7 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/arithmetic.py. """ +import abc import ast from pynguin.assertion.mutation_analysis.operators.base import ( @@ -18,75 +19,183 @@ class ArithmeticOperatorDeletion(AbstractUnaryOperatorDeletion): - def get_operator_type(self): - return ast.UAdd, ast.USub + """A class that mutate arithmetic operators by deleting them.""" + def get_operator_type(self) -> type: # noqa: D102 + return ast.UAdd | ast.USub # type: ignore[return-value] -class AbstractArithmeticOperatorReplacement(MutationOperator): + +class AbstractArithmeticOperatorReplacement(abc.ABC, MutationOperator): + """An abstract class that mutates arithmetic operators by replacing them.""" + + @abc.abstractmethod def should_mutate(self, node: ast.AST) -> bool: - raise NotImplementedError() + """Check if the operator should be mutated. + + Args: + node: The node to check. + + Returns: + True if the operator should be mutated, False otherwise. + """ - def mutate_Add(self, node: ast.Add) -> ast.Sub | None: + def mutate_Add(self, node: ast.Add) -> ast.Sub | None: # noqa: N802 + """Mutate an Add operator to a Sub operator. + + Args: + node: The Add operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Sub() - def mutate_Sub(self, node: ast.Sub) -> ast.Add | None: + def mutate_Sub(self, node: ast.Sub) -> ast.Add | None: # noqa: N802 + """Mutate a Sub operator to an Add operator. + + Args: + node: The Sub operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Add() - def mutate_Mult_to_Div(self, node: ast.Mult) -> ast.Div | None: + def mutate_Mult_to_Div(self, node: ast.Mult) -> ast.Div | None: # noqa: N802 + """Mutate a Mult operator to a Div operator. + + Args: + node: The Mult operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Div() - def mutate_Mult_to_FloorDiv(self, node: ast.Mult) -> ast.FloorDiv | None: + def mutate_Mult_to_FloorDiv( # noqa: N802 + self, node: ast.Mult + ) -> ast.FloorDiv | None: + """Mutate a Mult operator to a FloorDiv operator. + + Args: + node: The Mult operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.FloorDiv() - def mutate_Mult_to_Pow(self, node: ast.Mult) -> ast.Pow | None: + def mutate_Mult_to_Pow(self, node: ast.Mult) -> ast.Pow | None: # noqa: N802 + """Mutate a Mult operator to a Pow operator. + + Args: + node: The Mult operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Pow() - def mutate_Div_to_Mult(self, node: ast.Div) -> ast.Mult | None: + def mutate_Div_to_Mult(self, node: ast.Div) -> ast.Mult | None: # noqa: N802 + """Mutate a Div operator to a Mult operator. + + Args: + node: The Div operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Mult() - def mutate_Div_to_FloorDiv(self, node: ast.Div) -> ast.FloorDiv | None: + def mutate_Div_to_FloorDiv( # noqa: N802 + self, node: ast.Div + ) -> ast.FloorDiv | None: + """Mutate a Div operator to a FloorDiv operator. + + Args: + node: The Div operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.FloorDiv() - def mutate_FloorDiv_to_Div(self, node: ast.FloorDiv) -> ast.Div | None: + def mutate_FloorDiv_to_Div( # noqa: N802 + self, node: ast.FloorDiv + ) -> ast.Div | None: + """Mutate a FloorDiv operator to a Div operator. + + Args: + node: The FloorDiv operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Div() - def mutate_FloorDiv_to_Mult(self, node: ast.FloorDiv) -> ast.Mult | None: + def mutate_FloorDiv_to_Mult( # noqa: N802 + self, node: ast.FloorDiv + ) -> ast.Mult | None: + """Mutate a FloorDiv operator to a Mult operator. + + Args: + node: The FloorDiv operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Mult() - def mutate_Mod(self, node: ast.Mod) -> ast.Mult | None: + def mutate_Mod(self, node: ast.Mod) -> ast.Mult | None: # noqa: N802 + """Mutate a Mod operator to a Mult operator. + + Args: + node: The Mod operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None return ast.Mult() - def mutate_Pow(self, node: ast.Pow) -> ast.Mult | None: + def mutate_Pow(self, node: ast.Pow) -> ast.Mult | None: # noqa: N802 + """Mutate a Pow operator to a Mult operator. + + Args: + node: The Pow operator to mutate. + + Returns: + The mutated operator, or None if the operator should not be mutated. + """ if not self.should_mutate(node): return None @@ -94,12 +203,30 @@ def mutate_Pow(self, node: ast.Pow) -> ast.Mult | None: class ArithmeticOperatorReplacement(AbstractArithmeticOperatorReplacement): - def should_mutate(self, node: ast.AST) -> bool: - parent = getattr(node, "parent") + """A class that mutates arithmetic operators by replacing them.""" + + def should_mutate(self, node: ast.AST) -> bool: # noqa: D102 + parent = node.parent # type: ignore[attr-defined] return not isinstance(parent, ast.AugAssign) - def mutate_USub(self, node: ast.USub) -> ast.UAdd: + def mutate_USub(self, node: ast.USub) -> ast.UAdd: # noqa: N802 + """Mutate a USub operator to a UAdd operator. + + Args: + node: The USub operator to mutate. + + Returns: + The mutated operator. + """ return ast.UAdd() - def mutate_UAdd(self, node: ast.UAdd) -> ast.USub: + def mutate_UAdd(self, node: ast.UAdd) -> ast.USub: # noqa: N802 + """Mutate a UAdd operator to a USub operator. + + Args: + node: The UAdd operator to mutate. + + Returns: + The mutated operator. + """ return ast.USub() diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 428709ff..c6ff74e8 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -13,61 +13,99 @@ import abc import ast import copy -import types from dataclasses import dataclass -from typing import Callable -from typing import Generator -from typing import Iterable +from typing import TYPE_CHECKING from typing import TypeVar +if TYPE_CHECKING: + import types + + from collections.abc import Callable + from collections.abc import Generator + from collections.abc import Iterable + + def fix_lineno(node: ast.AST) -> None: - parent = getattr(node, "parent") + """Fix the line number of a node if it is not set. + + Args: + node: The node to fix. + """ + parent = node.parent # type: ignore[attr-defined] if not hasattr(node, "lineno") and parent is not None and hasattr(parent, "lineno"): - parent_lineno = getattr(parent, "lineno") - setattr(node, "lineno", parent_lineno) + parent_lineno = parent.lineno + node.lineno = parent_lineno def fix_node_internals(old_node: ast.AST, new_node: ast.AST) -> None: + """Fix the internals of a node. + + Args: + old_node: The old node. + new_node: The new node. + """ if not hasattr(new_node, "parent"): - old_node_children = getattr(old_node, "children") - old_node_parent = getattr(old_node, "parent") - setattr(new_node, "children", old_node_children) - setattr(new_node, "parent", old_node_parent) + old_node_children = old_node.children # type: ignore[attr-defined] + old_node_parent = old_node.parent # type: ignore[attr-defined] + new_node.children = old_node_children # type: ignore[attr-defined] + new_node.parent = old_node_parent # type: ignore[attr-defined] if not hasattr(new_node, "lineno") and hasattr(old_node, "lineno"): - old_node_lineno = getattr(old_node, "lineno") - setattr(new_node, "lineno", old_node_lineno) + old_node_lineno = old_node.lineno + new_node.lineno = old_node_lineno if hasattr(old_node, "marker"): - old_node_marker = getattr(old_node, "marker") - setattr(new_node, "marker", old_node_marker) + old_node_marker = old_node.marker + new_node.marker = old_node_marker # type: ignore[attr-defined] def set_lineno(node: ast.AST, lineno: int) -> None: + """Set the line number of a node. + + Args: + node: The node to set the line number for. + lineno: The line number to set. + """ for child_node in ast.walk(node): if hasattr(child_node, "lineno"): - setattr(child_node, "lineno", lineno) + child_node.lineno = lineno T = TypeVar("T", bound=ast.AST) def shift_lines(nodes: list[T], shift_by: int = 1) -> None: + """Shift the line numbers of a list of nodes. + + Args: + nodes: The nodes to shift. + shift_by: The amount to shift by. + """ for node in nodes: ast.increment_lineno(node, shift_by) @dataclass class Mutation: + """Represents a mutation.""" + node: ast.AST operator: type[MutationOperator] visitor_name: str def copy_node(node: T) -> T: - parent = getattr(node, "parent") + """Copy a node. + + Args: + node: The node to copy. + + Returns: + The copied node. + """ + parent = node.parent # type: ignore[attr-defined] return copy.deepcopy( node, memo={ @@ -77,6 +115,8 @@ def copy_node(node: T) -> T: class MutationOperator: + """A class that represents a mutation operator.""" + @classmethod def mutate( cls, @@ -84,6 +124,16 @@ def mutate( module: types.ModuleType | None = None, only_mutation: Mutation | None = None, ) -> Generator[tuple[Mutation, ast.AST], None, None]: + """Mutate a node. + + Args: + node: The node to mutate. + module: The module to use. + only_mutation: The mutation to apply. + + Yields: + A tuple containing the mutation and the mutated node. + """ operator = cls(module, only_mutation) for current_node, mutated_node, visitor_name in operator.visit(node): @@ -94,11 +144,25 @@ def __init__( module: types.ModuleType | None, only_mutation: Mutation | None, ) -> None: + """Initializes the operator. + + Args: + module: The module to use. + only_mutation: The mutation to apply. + """ self.module = module self.only_mutation = only_mutation def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: - node_children = getattr(node, "children") + """Visit a node. + + Args: + node: The node to visit. + + Yields: + A tuple containing the current node, the mutated node, and the visitor name. + """ + node_children = node.children # type: ignore[attr-defined] if ( self.only_mutation @@ -109,7 +173,7 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: fix_lineno(node) - for visitor in self.find_visitors(node): + for visitor in self._find_visitors(node): if ( self.only_mutation is None or ( @@ -122,24 +186,24 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: yield node, mutated_node, visitor.__name__ - yield from self.generic_visit(node) + yield from self._generic_visit(node) - def generic_visit( + def _generic_visit( self, node: ast.AST ) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: for field, old_value in ast.iter_fields(node): generator: Iterable[tuple[ast.AST, str]] if isinstance(old_value, list): - generator = self.generic_visit_list(old_value) + generator = self._generic_visit_list(old_value) elif isinstance(old_value, ast.AST): - generator = self.generic_visit_real_node(node, field, old_value) + generator = self._generic_visit_real_node(node, field, old_value) else: generator = () for current_node, visitor_name in generator: yield current_node, node, visitor_name - def generic_visit_list( + def _generic_visit_list( self, old_value: list ) -> Generator[tuple[ast.AST, str], None, None]: for position, value in enumerate(old_value.copy()): @@ -150,7 +214,7 @@ def generic_visit_list( old_value[position] = value - def generic_visit_real_node( + def _generic_visit_real_node( self, node: ast.AST, field: str, old_value: ast.AST ) -> Generator[tuple[ast.AST, str], None, None]: for current_node, mutated_node, visitor_name in self.visit(old_value): @@ -159,7 +223,7 @@ def generic_visit_real_node( setattr(node, field, old_value) - def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: + def _find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: node_name = node.__class__.__name__ method_prefix = f"mutate_{node_name}" return [ @@ -171,11 +235,25 @@ def find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: class AbstractUnaryOperatorDeletion(abc.ABC, MutationOperator): + """An abstract class that mutates unary operators by deleting them.""" + @abc.abstractmethod - def get_operator_type(self) -> type[ast.unaryop]: - pass + def get_operator_type(self) -> type: + """Get the operator type. + + Returns: + The operator type. + """ + + def mutate_UnaryOp(self, node: ast.UnaryOp) -> ast.expr | None: # noqa: N802 + """Mutate a unary operator. + + Args: + node: The node to mutate. - def mutate_UnaryOp(self, node: ast.UnaryOp) -> ast.expr | None: + Returns: + The mutated node, or None if the node should not be mutated. + """ if not isinstance(node.op, self.get_operator_type()): return None diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 70557189..78cf1bd6 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -16,7 +16,17 @@ class DecoratorDeletion(MutationOperator): - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST | None: + """A class that mutates decorators by deleting them.""" + + def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.AST | None: # noqa: N802 + """Mutate a function definition by deleting its decorators. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the decorators should not be mutated. + """ if not node.decorator_list: return None diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py index ae43b1ec..129a360a 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/exception.py +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -18,6 +18,15 @@ def replace_exception_handler( exception_handler: ast.ExceptHandler, body: list[ast.stmt], ) -> ast.ExceptHandler: + """Replace an exception handler with a new body. + + Args: + exception_handler: The exception handler to replace. + body: The new body. + + Returns: + The new exception handler. + """ return ast.ExceptHandler( type=exception_handler.type, name=exception_handler.name, @@ -27,7 +36,19 @@ def replace_exception_handler( class ExceptionHandlerDeletion(MutationOperator): - def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler | None: + """A class that mutates exception handlers by deleting them.""" + + def mutate_ExceptHandler( # noqa: N802 + self, node: ast.ExceptHandler + ) -> ast.ExceptHandler | None: + """Mutate an exception handler by deleting it. + + Args: + node: The exception handler to mutate. + + Returns: + The mutated node, or None if the exception handler should not be mutated. + """ if not node.body: return None @@ -42,7 +63,19 @@ def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler | N class ExceptionSwallowing(MutationOperator): - def mutate_ExceptHandler(self, node: ast.ExceptHandler) -> ast.ExceptHandler | None: + """A class that mutates exception handlers by ignoring the caught exception.""" + + def mutate_ExceptHandler( # noqa: N802 + self, node: ast.ExceptHandler + ) -> ast.ExceptHandler | None: + """Mutate an exception handler by ignoring the caught exception. + + Args: + node: The exception handler to mutate. + + Returns: + The mutated node, or None if the exception handler should not be mutated. + """ if not node.body: return None diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 60e41455..9073d469 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -9,9 +9,12 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/inheritance.py. """ +import abc import ast import functools +from collections.abc import Iterable +from typing import Any from typing import cast from pynguin.assertion.mutation_analysis.operators.base import MutationOperator @@ -21,45 +24,70 @@ from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer +def getattr_rec(obj: object, attr: Iterable[str]) -> Any: + """Get an attribute recursively. + + Args: + obj: The object to get the attribute from. + attr: The attribute to get. + + Returns: + The attribute. + """ + return functools.reduce(getattr, attr, obj) + + class AbstractOverriddenElementModification(MutationOperator): - def is_overridden(self, node: ast.AST, name: str | None = None) -> bool | None: - parent = getattr(node, "parent") + """An abstract class that provides a method to check if an element is overridden.""" + + def is_overridden(self, node: ast.AST, name: str) -> bool | None: + """Check if a method is overridden. + + Args: + node: The node to check. + name: The name of the method to check. + + Returns: + True if the method is overridden, False if it is not, None on error. + """ + parent: ast.AST = node.parent # type: ignore[attr-defined] if not isinstance(parent, ast.ClassDef) or not isinstance( - node, (ast.FunctionDef, ast.AsyncFunctionDef) + node, ast.FunctionDef | ast.AsyncFunctionDef | ast.Assign ): return None - if not name: - name = node.name - parent_names: list[str] = [] while parent is not None: if not isinstance(parent, ast.Module): - parent_names.append(parent.name) + parent_names.append(parent.name) # type: ignore[attr-defined] if not isinstance(parent, ast.ClassDef) and not isinstance( parent, ast.Module ): return None - parent = getattr(parent, "parent") - - getattr_rec = lambda obj, attr: functools.reduce(getattr, attr, obj) + parent = parent.parent # type: ignore[attr-defined,union-attr] try: klass = getattr_rec(self.module, reversed(parent_names)) except AttributeError: return None - for base_klass in type.mro(klass)[1:-1]: - if hasattr(base_klass, name): - return True - - return False + return any(hasattr(base_klass, name) for base_klass in type.mro(klass)[1:-1]) class HidingVariableDeletion(AbstractOverriddenElementModification): - def mutate_Assign(self, node: ast.Assign) -> ast.stmt | None: + """A class that mutates hiding variables by deleting them.""" + + def mutate_Assign(self, node: ast.Assign) -> ast.stmt | None: # noqa: N802 + """Mutate an assignment by deleting a hiding variable. + + Args: + node: The assignment to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ if len(node.targets) != 1: return None @@ -72,14 +100,23 @@ def mutate_Assign(self, node: ast.Assign) -> ast.stmt | None: return None return ast.Pass() - elif isinstance(first_expression, ast.Tuple) and isinstance( + + if isinstance(first_expression, ast.Tuple) and isinstance( node.value, ast.Tuple ): return self.mutate_unpack(node) - else: - return None + + return None def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: + """Mutate an assignment by deleting a hiding variable in an unpacking. + + Args: + node: The assignment to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ if not node.targets: return None @@ -88,7 +125,7 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: new_targets: list[ast.expr] = [] new_values: list[ast.expr] = [] - for target_element, value_element in zip(target.elts, value.elts): + for target_element, value_element in zip(target.elts, value.elts, strict=False): if not isinstance(target_element, ast.Name) or not isinstance( value_element, ast.expr ): @@ -108,50 +145,91 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: if not new_targets: return ast.Pass() - elif len(new_targets) == 1 and len(new_values) == 1: + if len(new_targets) == 1 and len(new_values) == 1: node.targets = new_targets node.value = new_values[0] return node - else: - target.elts = new_targets - value.elts = new_values - return node + target.elts = new_targets + value.elts = new_values + return node -class AbstractSuperCallingModification(MutationOperator): - def is_super_call(self, node: ast.FunctionDef, stmt: ast.stmt) -> bool: - return ( - isinstance(stmt, ast.Expr) - and isinstance(stmt.value, ast.Call) - and isinstance(stmt.value.func, ast.Attribute) - and isinstance(stmt.value.func.value, ast.Call) - and isinstance(stmt.value.func.value.func, ast.Name) - and stmt.value.func.value.func.id == "super" - and stmt.value.func.attr == node.name - ) +def is_super_call(node: ast.FunctionDef, stmt: ast.stmt) -> bool: + """Check if a statement is a super call. + + Args: + node: The function definition to check. + stmt: The statement to check. + Returns: + True if the statement is a super call, False otherwise. + """ + return ( + isinstance(stmt, ast.Expr) + and isinstance(stmt.value, ast.Call) + and isinstance(stmt.value.func, ast.Attribute) + and isinstance(stmt.value.func.value, ast.Call) + and isinstance(stmt.value.func.value.func, ast.Name) + and stmt.value.func.value.func.id == "super" + and stmt.value.func.attr == node.name + ) + + +def get_super_call(node: ast.FunctionDef) -> tuple[int, ast.stmt] | None: + """Get the super call from a function definition. + + Args: + node: The function definition to get the super call from. + + Returns: + The index and the statement of the super call, or None if it does not exist. + """ + for index, stmt in enumerate(node.body): + if is_super_call(node, stmt): + return index, stmt + return None + + +class AbstractSuperCallingModification(abc.ABC, MutationOperator): + """An abstract class that provides methods to mutate super calls.""" + + @abc.abstractmethod def should_mutate(self, node: ast.FunctionDef) -> bool: - parent = getattr(node, "parent") - return isinstance(parent, ast.ClassDef) + """Check if the node should be mutated. - def get_super_call(self, node: ast.FunctionDef) -> tuple[int, ast.stmt] | None: - for index, stmt in enumerate(node.body): - if self.is_super_call(node, stmt): - return index, stmt - return None + Args: + node: The node to check. + + Returns: + True if the node should be mutated, False otherwise. + """ + parent = node.parent # type: ignore[attr-defined] + return isinstance(parent, ast.ClassDef) class OverriddenMethodCallingPositionChange(AbstractSuperCallingModification): - def should_mutate(self, node: ast.FunctionDef) -> bool: + """A class that mutates the position of the super call in an overridden method.""" + + def should_mutate(self, node: ast.FunctionDef) -> bool: # noqa: D102 return super().should_mutate(node) and len(node.body) > 1 - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.FunctionDef | None: + """Mutate the position of the super call in an overridden method. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ if not self.should_mutate(node) or not node.body: return None mutated_node = copy_node(node) - super_call = self.get_super_call(mutated_node) + super_call = get_super_call(mutated_node) if super_call is None: return None @@ -173,8 +251,20 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: class OverridingMethodDeletion(AbstractOverriddenElementModification): - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass | None: - overridden = self.is_overridden(node) + """A class that mutates overriding methods by deleting them.""" + + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.Pass | None: + """Mutate a function definition by deleting it. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + overridden = self.is_overridden(node, node.name) if overridden is None or not overridden: return None @@ -183,13 +273,25 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.Pass | None: class SuperCallingDeletion(AbstractSuperCallingModification): - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: + """A class that mutates super calls by deleting them.""" + + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.FunctionDef | None: + """Mutate a function definition by deleting the super call. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ if not self.should_mutate(node) or not node.body: return None mutated_node = copy_node(node) - super_call = self.get_super_call(mutated_node) + super_call = get_super_call(mutated_node) if super_call is None: return None @@ -204,9 +306,20 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: class SuperCallingInsert( AbstractSuperCallingModification, AbstractOverriddenElementModification ): + """A class that mutates super calls by inserting them.""" - def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: - overridden = self.is_overridden(node) + def mutate_FunctionDef( # noqa: N802 + self, node: ast.FunctionDef + ) -> ast.FunctionDef | None: + """Mutate a function definition by inserting the super call. + + Args: + node: The function definition to mutate. + + Returns: + The mutated node, or None if the node should not be mutated. + """ + overridden = self.is_overridden(node, node.name) if ( not self.should_mutate(node) @@ -218,17 +331,17 @@ def mutate_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef | None: mutated_node = copy_node(node) - super_call = self.get_super_call(mutated_node) + super_call = get_super_call(mutated_node) if super_call is not None: return None - mutated_node.body.insert(0, self.create_super_call(mutated_node)) + mutated_node.body.insert(0, self._create_super_call(mutated_node)) shift_lines(mutated_node.body[1:], 1) return mutated_node - def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: + def _create_super_call(self, node: ast.FunctionDef) -> ast.Expr: module = ParentNodeTransformer.create_ast(f"super().{node.name}()") assert module.body @@ -245,31 +358,33 @@ def create_super_call(self, node: ast.FunctionDef) -> ast.Expr: super_call_value.args.append(ast.Name(id=arg.arg, ctx=ast.Load())) for arg, default in zip( - node.args.args[-len(node.args.defaults) :], node.args.defaults + node.args.args[-len(node.args.defaults) :], node.args.defaults, strict=False ): super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) - for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): # type: ignore[assignment] + for arg, default in zip( # type: ignore[assignment] + node.args.kwonlyargs, node.args.kw_defaults, strict=False + ): super_call_value.keywords.append(ast.keyword(arg=arg.arg, value=default)) if node.args.vararg is not None: - self.add_vararg_to_super_call(super_call_value, node.args.vararg) + self._add_vararg_to_super_call(super_call_value, node.args.vararg) if node.args.kwarg is not None: - self.add_kwarg_to_super_call(super_call_value, node.args.kwarg) + self._add_kwarg_to_super_call(super_call_value, node.args.kwarg) set_lineno(super_call, node.body[0].lineno) return super_call @staticmethod - def add_kwarg_to_super_call(super_call_value: ast.Call, kwarg: ast.arg) -> None: + def _add_kwarg_to_super_call(super_call_value: ast.Call, kwarg: ast.arg) -> None: super_call_value.keywords.append( ast.keyword(arg=None, value=ast.Name(id=kwarg.arg, ctx=ast.Load())) ) @staticmethod - def add_vararg_to_super_call(super_call_value: ast.Call, vararg: ast.arg) -> None: + def _add_vararg_to_super_call(super_call_value: ast.Call, vararg: ast.arg) -> None: super_call_value.args.append( ast.Starred(ctx=ast.Load(), value=ast.Name(id=vararg.arg, ctx=ast.Load())) ) diff --git a/src/pynguin/assertion/mutation_analysis/operators/logical.py b/src/pynguin/assertion/mutation_analysis/operators/logical.py index b9373c19..945b05ec 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/logical.py +++ b/src/pynguin/assertion/mutation_analysis/operators/logical.py @@ -21,90 +21,280 @@ class ConditionalOperatorDeletion(AbstractUnaryOperatorDeletion): + """A class that mutates conditional operators by deleting them.""" + def get_operator_type(self) -> type: + """Get the operator type.""" return ast.Not - def mutate_NotIn(self, node: ast.NotIn) -> ast.In: + def mutate_NotIn(self, node: ast.NotIn) -> ast.In: # noqa: N802 + """Mutate a NotIn operator to an In operator. + + Args: + node: The NotIn operator to mutate. + + Returns: + The mutated operator. + """ return ast.In() T = TypeVar("T", ast.If, ast.While) +def negate_test(node: T) -> T: + """Negate the test of a node. + + Args: + node: The node to negate. + + Returns: + The mutated node. + """ + mutated_node = copy_node(node) + not_node = ast.UnaryOp(op=ast.Not(), operand=mutated_node.test) + mutated_node.test = not_node + return mutated_node + + class ConditionalOperatorInsertion(MutationOperator): - def negate_test(self, node: T) -> T: - mutated_node = copy_node(node) - not_node = ast.UnaryOp(op=ast.Not(), operand=mutated_node.test) - mutated_node.test = not_node - return mutated_node + """A class that mutates conditional operators by inserting them.""" + + def mutate_While(self, node: ast.While) -> ast.While: # noqa: N802 + """Mutate a While node by negating its test. + + Args: + node: The While node to mutate. + + Returns: + The mutated node. + """ + return negate_test(node) + + def mutate_If(self, node: ast.If) -> ast.If: # noqa: N802 + """Mutate an If node by negating its test. + + Args: + node: The If node to mutate. + + Returns: + The mutated node. + """ + return negate_test(node) - def mutate_While(self, node: ast.While) -> ast.While: - return self.negate_test(node) + def mutate_In(self, node: ast.In) -> ast.NotIn: # noqa: N802 + """Mutate an In operator to a NotIn operator. - def mutate_If(self, node: ast.If) -> ast.If: - return self.negate_test(node) + Args: + node: The In operator to mutate. - def mutate_In(self, node: ast.In) -> ast.NotIn: + Returns: + The mutated operator. + """ return ast.NotIn() class LogicalConnectorReplacement(MutationOperator): - def mutate_And(self, node: ast.And) -> ast.Or: + """A class that mutates logical connectors by replacing them.""" + + def mutate_And(self, node: ast.And) -> ast.Or: # noqa: N802 + """Mutate an And operator to an Or operator. + + Args: + node: The And operator to mutate. + + Returns: + The mutated operator. + """ return ast.Or() - def mutate_Or(self, node: ast.Or) -> ast.And: + def mutate_Or(self, node: ast.Or) -> ast.And: # noqa: N802 + """Mutate an Or operator to an And operator. + + Args: + node: The Or operator to mutate. + + Returns: + The mutated operator. + """ return ast.And() class LogicalOperatorDeletion(AbstractUnaryOperatorDeletion): - def get_operator_type(self) -> type: + """A class that mutates logical operators by deleting them.""" + + def get_operator_type(self) -> type: # noqa: D102 return ast.Invert class LogicalOperatorReplacement(MutationOperator): - def mutate_BitAnd(self, node: ast.BitAnd) -> ast.BitOr: + """A class that mutates logical operators by replacing them.""" + + def mutate_BitAnd(self, node: ast.BitAnd) -> ast.BitOr: # noqa: N802 + """Mutate a BitAnd operator to a BitOr operator. + + Args: + node: The BitAnd operator to mutate. + + Returns: + The mutated operator. + """ return ast.BitOr() - def mutate_BitOr(self, node: ast.BitOr) -> ast.BitAnd: + def mutate_BitOr(self, node: ast.BitOr) -> ast.BitAnd: # noqa: N802 + """Mutate a BitOr operator to a BitAnd operator. + + Args: + node: The BitOr operator to mutate. + + Returns: + The mutated operator. + """ return ast.BitAnd() - def mutate_BitXor(self, node: ast.BitXor) -> ast.BitAnd: + def mutate_BitXor(self, node: ast.BitXor) -> ast.BitAnd: # noqa: N802 + """Mutate a BitXor operator to a BitAnd operator. + + Args: + node: The BitXor operator to mutate. + + Returns: + The mutated operator. + """ return ast.BitAnd() - def mutate_LShift(self, node: ast.LShift) -> ast.RShift: + def mutate_LShift(self, node: ast.LShift) -> ast.RShift: # noqa: N802 + """Mutate a LShift operator to a RShift operator. + + Args: + node: The LShift operator to mutate. + + Returns: + The mutated operator. + """ return ast.RShift() - def mutate_RShift(self, node: ast.RShift) -> ast.LShift: + def mutate_RShift(self, node: ast.RShift) -> ast.LShift: # noqa: N802 + """Mutate a RShift operator to a LShift operator. + + Args: + node: The RShift operator to mutate. + + Returns: + The mutated operator. + """ return ast.LShift() class RelationalOperatorReplacement(MutationOperator): - def mutate_Lt(self, node: ast.Lt) -> ast.Gt: + """A class that mutates relational operators by replacing them.""" + + def mutate_Lt(self, node: ast.Lt) -> ast.Gt: # noqa: N802 + """Mutate a Lt operator to a Gt operator. + + Args: + node: The Lt operator to mutate. + + Returns: + The mutated operator. + """ return ast.Gt() - def mutate_Lt_to_LtE(self, node: ast.Lt) -> ast.LtE: + def mutate_Lt_to_LtE(self, node: ast.Lt) -> ast.LtE: # noqa: N802 + """Mutate a Lt operator to a LtE operator. + + Args: + node: The Lt operator to mutate. + + Returns: + The mutated operator. + """ return ast.LtE() - def mutate_Gt(self, node: ast.Gt) -> ast.Lt: + def mutate_Gt(self, node: ast.Gt) -> ast.Lt: # noqa: N802 + """Mutate a Gt operator to a Lt operator. + + Args: + node: The Gt operator to mutate. + + Returns: + The mutated operator. + """ return ast.Lt() - def mutate_Gt_to_GtE(self, node: ast.Gt) -> ast.GtE: + def mutate_Gt_to_GtE(self, node: ast.Gt) -> ast.GtE: # noqa: N802 + """Mutate a Gt operator to a GtE operator. + + Args: + node: The Gt operator to mutate. + + Returns: + The mutated operator. + """ return ast.GtE() - def mutate_LtE(self, node: ast.LtE) -> ast.GtE: + def mutate_LtE(self, node: ast.LtE) -> ast.GtE: # noqa: N802 + """Mutate a LtE operator to a GtE operator. + + Args: + node: The LtE operator to mutate. + + Returns: + The mutated operator. + """ return ast.GtE() - def mutate_LtE_to_Lt(self, node: ast.LtE) -> ast.Lt: + def mutate_LtE_to_Lt(self, node: ast.LtE) -> ast.Lt: # noqa: N802 + """Mutate a LtE operator to a Lt operator. + + Args: + node: The LtE operator to mutate. + + Returns: + The mutated operator. + """ return ast.Lt() - def mutate_GtE(self, node: ast.GtE) -> ast.LtE: + def mutate_GtE(self, node: ast.GtE) -> ast.LtE: # noqa: N802 + """Mutate a GtE operator to a LtE operator. + + Args: + node: The GtE operator to mutate. + + Returns: + The mutated operator. + """ return ast.LtE() - def mutate_GtE_to_Gt(self, node: ast.GtE) -> ast.Gt: + def mutate_GtE_to_Gt(self, node: ast.GtE) -> ast.Gt: # noqa: N802 + """Mutate a GtE operator to a Gt operator. + + Args: + node: The GtE operator to mutate. + + Returns: + The mutated operator. + """ return ast.Gt() - def mutate_Eq(self, node: ast.Eq) -> ast.NotEq: + def mutate_Eq(self, node: ast.Eq) -> ast.NotEq: # noqa: N802 + """Mutate an Eq operator to a NotEq operator. + + Args: + node: The Eq operator to mutate. + + Returns: + The mutated operator. + """ return ast.NotEq() - def mutate_NotEq(self, node: ast.NotEq) -> ast.Eq: + def mutate_NotEq(self, node: ast.NotEq) -> ast.Eq: # noqa: N802 + """Mutate a NotEq operator to an Eq operator. + + Args: + node: The NotEq operator to mutate. + + Returns: + The mutated operator. + """ return ast.Eq() diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py index c51c3d79..eb0871c7 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/loop.py +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -12,14 +12,22 @@ import ast import typing -from pynguin.assertion.mutation_analysis.operators import MutationOperator -from pynguin.assertion.mutation_analysis.operators import copy_node +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator +from pynguin.assertion.mutation_analysis.operators.base import copy_node T = typing.TypeVar("T", ast.For, ast.While) def one_iteration(node: T) -> T | None: + """Mutate a loop to have only one iteration. + + Args: + node: The loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ if not node.body: return None @@ -29,6 +37,14 @@ def one_iteration(node: T) -> T | None: def zero_iteration(node: T) -> T | None: + """Mutate a loop to have zero iterations. + + Args: + node: The loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ if not node.body: return None @@ -38,15 +54,43 @@ def zero_iteration(node: T) -> T | None: class OneIterationLoop(MutationOperator): - def mutate_For(self, node: ast.For) -> ast.For | None: + """A class that mutates loops to have only one iteration.""" + + def mutate_For(self, node: ast.For) -> ast.For | None: # noqa: N802 + """Mutate a For loop to have only one iteration. + + Args: + node: The For loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ return one_iteration(node) - def mutate_While(self, node: ast.While) -> ast.While | None: + def mutate_While(self, node: ast.While) -> ast.While | None: # noqa: N802 + """Mutate a While loop to have only one iteration. + + Args: + node: The While loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ return one_iteration(node) class ReverseIterationLoop(MutationOperator): - def mutate_For(self, node: ast.For) -> ast.For: + """A class that mutates loops by reversing their iteration.""" + + def mutate_For(self, node: ast.For) -> ast.For: # noqa: N802 + """Mutate a For loop by reversing its iteration. + + Args: + node: The For loop to mutate. + + Returns: + The mutated loop. + """ mutated_node = copy_node(node) old_iter = mutated_node.iter mutated_node.iter = ast.Call( @@ -60,8 +104,26 @@ def mutate_For(self, node: ast.For) -> ast.For: class ZeroIterationLoop(MutationOperator): - def mutate_For(self, node: ast.For) -> ast.For | None: + """A class that mutates loops to have zero iterations.""" + + def mutate_For(self, node: ast.For) -> ast.For | None: # noqa: N802 + """Mutate a For loop to have zero iterations. + + Args: + node: The For loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ return zero_iteration(node) - def mutate_While(self, node: ast.While) -> ast.While | None: + def mutate_While(self, node: ast.While) -> ast.While | None: # noqa: N802 + """Mutate a While loop to have zero iterations. + + Args: + node: The While loop to mutate. + + Returns: + The mutated loop, or None if the loop should not be mutated. + """ return zero_iteration(node) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index eac3bc48..1d03f9ee 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -19,42 +19,80 @@ def is_docstring(node: ast.AST) -> bool: + """Check if the given node is a docstring. + + Args: + node: The node to check. + + Returns: + True if the node is a docstring, False otherwise. + """ if not isinstance(node, ast.Str): return False - expression_node: ast.AST = getattr(node, "parent") + expression_node: ast.AST = node.parent # type: ignore[attr-defined] if not isinstance(expression_node, ast.Expr): return False - def_node: ast.AST = getattr(expression_node, "parent") + def_node: ast.AST = expression_node.parent # type: ignore[attr-defined] return ( - isinstance(def_node, (ast.FunctionDef, ast.ClassDef, ast.Module)) + isinstance(def_node, ast.FunctionDef | ast.ClassDef | ast.Module) and def_node.body # type: ignore[return-value] and def_node.body[0] == expression_node ) class AssignmentOperatorReplacement(AbstractArithmeticOperatorReplacement): - def should_mutate(self, node: ast.AST) -> bool: - parent = getattr(node, "parent") + """A class that mutates assignment operators by replacing them.""" + + def should_mutate(self, node: ast.AST) -> bool: # noqa: D102 + parent = node.parent # type: ignore[attr-defined] return isinstance(parent, ast.AugAssign) class BreakContinueReplacement(MutationOperator): - def mutate_Break(self, node: ast.Break) -> ast.Continue: + """A class that mutates break and continue statements by replacing them.""" + + def mutate_Break(self, node: ast.Break) -> ast.Continue: # noqa: N802 + """Mutate a Break statement to a Continue statement. + + Args: + node: The Break statement to mutate. + + Returns: + The mutated statement. + """ return ast.Continue() - def mutate_Continue(self, node: ast.Continue) -> ast.Break: + def mutate_Continue(self, node: ast.Continue) -> ast.Break: # noqa: N802 + """Mutate a Continue statement to a Break statement. + + Args: + node: The Continue statement to mutate. + + Returns: + The mutated statement. + """ return ast.Break() class ConstantReplacement(MutationOperator): + """A class that mutates constants by replacing them.""" + FIRST_CONST_STRING = "mutpy" SECOND_CONST_STRING = "python" def help_str(self, node: ast.Constant) -> str | None: + """Help function for mutating strings. + + Args: + node: The string to mutate. + + Returns: + The mutated string, or None if the string should not be mutated. + """ if is_docstring(node): return None @@ -65,20 +103,48 @@ def help_str(self, node: ast.Constant) -> str | None: @staticmethod def help_str_empty(node: ast.Constant) -> str | None: + """Help function for mutating empty strings. + + Args: + node: The string to mutate. + + Returns: + The mutated string, or None if the string should not be mutated. + """ if not node.value or is_docstring(node): return None return "" - def mutate_Constant_num(self, node: ast.Constant) -> ast.Constant | None: + def mutate_Constant_num( # noqa: N802 + self, node: ast.Constant + ) -> ast.Constant | None: + """Mutate a numeric constant by adding 1. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ value = node.value - if not isinstance(value, (int, float)) or isinstance(value, bool): + if not isinstance(value, int | float) or isinstance(value, bool): return None return ast.Constant(value + 1) - def mutate_Constant_str(self, node: ast.Constant) -> ast.Constant | None: + def mutate_Constant_str( # noqa: N802 + self, node: ast.Constant + ) -> ast.Constant | None: + """Mutate a string constant by replacing it. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ if not isinstance(node.value, str): return None @@ -89,7 +155,17 @@ def mutate_Constant_str(self, node: ast.Constant) -> ast.Constant | None: return ast.Constant(new_value) - def mutate_Constant_str_empty(self, node: ast.Constant) -> ast.Constant | None: + def mutate_Constant_str_empty( # noqa: N802 + self, node: ast.Constant + ) -> ast.Constant | None: + """Mutate an empty string constant by replacing it. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ if not isinstance(node.value, str): return None @@ -100,10 +176,26 @@ def mutate_Constant_str_empty(self, node: ast.Constant) -> ast.Constant | None: return ast.Constant(new_value) - def mutate_Num(self, node: ast.Num) -> ast.Num: + def mutate_Num(self, node: ast.Num) -> ast.Num: # noqa: N802 + """Mutate a numeric constant by adding 1. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant. + """ return ast.Num(node.value + 1) - def mutate_Str(self, node: ast.Str) -> ast.Str | None: + def mutate_Str(self, node: ast.Str) -> ast.Str | None: # noqa: N802 + """Mutate a string constant by replacing it. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ new_value = self.help_str(node) if new_value is None: @@ -111,7 +203,15 @@ def mutate_Str(self, node: ast.Str) -> ast.Str | None: return ast.Str(new_value) - def mutate_Str_empty(self, node: ast.Str) -> ast.Str | None: + def mutate_Str_empty(self, node: ast.Str) -> ast.Str | None: # noqa: N802 + """Mutate an empty string constant by replacing it. + + Args: + node: The constant to mutate. + + Returns: + The mutated constant, or None if the constant should not be mutated. + """ new_value = self.help_str_empty(node) if new_value is None: @@ -121,19 +221,51 @@ def mutate_Str_empty(self, node: ast.Str) -> ast.Str | None: class SliceIndexRemove(MutationOperator): - def mutate_Slice_remove_lower(self, node: ast.Slice) -> ast.Slice | None: + """A class that mutates slice indices by removing them.""" + + def mutate_Slice_remove_lower( # noqa: N802 + self, node: ast.Slice + ) -> ast.Slice | None: + """Mutate a Slice index by removing the lower bound. + + Args: + node: The Slice index to mutate. + + Returns: + The mutated index, or None if the index should not be mutated. + """ if node.lower is None: return None return ast.Slice(lower=None, upper=node.upper, step=node.step) - def mutate_Slice_remove_upper(self, node: ast.Slice) -> ast.Slice | None: + def mutate_Slice_remove_upper( # noqa: N802 + self, node: ast.Slice + ) -> ast.Slice | None: + """Mutate a Slice index by removing the upper bound. + + Args: + node: The Slice index to mutate. + + Returns: + The mutated index, or None if the index should not be mutated. + """ if node.upper is None: return None return ast.Slice(lower=node.lower, upper=None, step=node.step) - def mutate_Slice_remove_step(self, node: ast.Slice) -> ast.Slice | None: + def mutate_Slice_remove_step( # noqa: N802 + self, node: ast.Slice + ) -> ast.Slice | None: + """Mutate a Slice index by removing the step. + + Args: + node: The Slice index to mutate. + + Returns: + The mutated index, or None if the index should not be mutated. + """ if node.step is None: return None diff --git a/src/pynguin/assertion/mutation_analysis/stategies.py b/src/pynguin/assertion/mutation_analysis/stategies.py index 0bd0389a..f6501fbc 100644 --- a/src/pynguin/assertion/mutation_analysis/stategies.py +++ b/src/pynguin/assertion/mutation_analysis/stategies.py @@ -11,27 +11,39 @@ from __future__ import annotations import abc -import random -from typing import Callable -from typing import Generator +from typing import TYPE_CHECKING -from pynguin.assertion.mutation_analysis.operators.base import Mutation +from pynguin.utils import randomness + + +if TYPE_CHECKING: + from collections.abc import Generator + + from pynguin.assertion.mutation_analysis.operators.base import Mutation def remove_bad_mutations( mutations_to_apply: list[Mutation], available_mutations: list[Mutation], - allow_same_operators: bool = True, + allow_same_operators: bool = True, # noqa: FBT001, FBT002 ) -> None: + """Remove bad mutations from the available mutations. + + Args: + mutations_to_apply: The mutations that are already selected. + available_mutations: The mutations that are available. + allow_same_operators: Whether the same operator should be allowed. + + Returns: + The list of available mutations without the bad mutations. + """ for mutation_to_apply in mutations_to_apply: for available_mutation in available_mutations.copy(): if ( mutation_to_apply.node == available_mutation.node - or mutation_to_apply.node - in getattr(available_mutation.node, "children") - or available_mutation.node - in getattr(mutation_to_apply.node, "children") + or mutation_to_apply.node in available_mutation.node.children # type: ignore[attr-defined] + or available_mutation.node in mutation_to_apply.node.children # type: ignore[attr-defined] or ( not allow_same_operators and mutation_to_apply.operator == available_mutation.operator @@ -41,20 +53,34 @@ def remove_bad_mutations( class HOMStrategy(abc.ABC): + """A strategy for higher order mutations.""" def __init__(self, order: int = 2) -> None: + """Initialize the strategy. + + Args: + order: The order of the mutations. + """ self.order = order @abc.abstractmethod def generate( self, mutations: list[Mutation] ) -> Generator[list[Mutation], None, None]: - raise NotImplementedError + """Generate the mutations. + + Args: + mutations: The mutations to generate from. + + Returns: + A generator for the mutations. + """ class FirstToLastHOMStrategy(HOMStrategy): + """A strategy that selects the first mutation and then the last one.""" - def generate( + def generate( # noqa: D102 self, mutations: list[Mutation] ) -> Generator[list[Mutation], None, None]: mutations = mutations.copy() @@ -72,8 +98,9 @@ def generate( class EachChoiceHOMStrategy(HOMStrategy): + """A strategy that selects the mutations in order.""" - def generate( + def generate( # noqa: D102 self, mutations: list[Mutation] ) -> Generator[list[Mutation], None, None]: mutations = mutations.copy() @@ -89,11 +116,12 @@ def generate( class BetweenOperatorsHOMStrategy(HOMStrategy): + """A strategy that selects mutations between different operators.""" - def generate( + def generate( # noqa: D102 self, mutations: list[Mutation] ) -> Generator[list[Mutation], None, None]: - usage = {mutation: 0 for mutation in mutations} + usage = dict.fromkeys(mutations, 0) not_used = mutations.copy() while not_used: mutations_to_apply: list[Mutation] = [] @@ -112,16 +140,21 @@ def generate( class RandomHOMStrategy(HOMStrategy): + """A strategy that selects mutations randomly.""" - def __init__(self, order: int = 2, shuffler: Callable = random.shuffle) -> None: + def __init__(self, order: int = 2) -> None: + """Initialize the strategy. + + Args: + order: The order of the mutations. + """ super().__init__(order) - self.shuffler = shuffler - def generate( + def generate( # noqa: D102 self, mutations: list[Mutation] ) -> Generator[list[Mutation], None, None]: mutations = mutations.copy() - self.shuffler(mutations) + randomness.shuffle(mutations) while mutations: mutations_to_apply: list[Mutation] = [] available_mutations = mutations.copy() diff --git a/src/pynguin/assertion/mutation_analysis/transformer.py b/src/pynguin/assertion/mutation_analysis/transformer.py index e2fbebca..e14090ab 100644 --- a/src/pynguin/assertion/mutation_analysis/transformer.py +++ b/src/pynguin/assertion/mutation_analysis/transformer.py @@ -19,15 +19,34 @@ class ParentNodeTransformer(ast.NodeTransformer): + """A transformer that adds a parent attribute to each node of the AST.""" + @classmethod def create_ast(cls, code: str) -> ast.Module: + """Create an AST from a string. + + Args: + code: The code to parse. + + Returns: + The module node of the AST with the parent and children attributes set. + """ return cls().visit(ast.parse(code)) def __init__(self) -> None: + """Initialize the transformer.""" super().__init__() self.parent: ast.AST | None = None def visit(self, node: T) -> T: + """Transform a node of the AST. + + Args: + node: The node to transform. + + Returns: + The transformed node. + """ # Copy the node because an optimisation of the AST makes it # reuse the same node at multiple places in the tree to # improve memory usage. It would break our goal to create a @@ -37,8 +56,8 @@ def visit(self, node: T) -> T: if hasattr(node, "lineno"): delattr(node, "lineno") - setattr(node, "parent", self.parent) - setattr(node, "children", set()) + node.parent = self.parent # type: ignore[attr-defined] + node.children = set() # type: ignore[attr-defined] parent_save = self.parent self.parent = node @@ -53,11 +72,11 @@ def visit(self, node: T) -> T: # of the parent if it exists. This is done here so that # the tree has been fully traversed before adding the children. if self.parent is not None: - parent_children: set[ast.AST] = getattr(self.parent, "children") + parent_children: set[ast.AST] = self.parent.children # type: ignore[attr-defined] parent_children.add(node) - node_children: set[ast.AST] = getattr(node, "children") + node_children: set[ast.AST] = node.children # type: ignore[attr-defined] parent_children.update(node_children) return node diff --git a/src/pynguin/utils/randomness.py b/src/pynguin/utils/randomness.py index a186edd5..30a358d8 100644 --- a/src/pynguin/utils/randomness.py +++ b/src/pynguin/utils/randomness.py @@ -189,3 +189,12 @@ def next_bytes(length: int) -> bytes: Random bytes of given length. """ return bytes(next_byte() for _ in range(length)) + + +def shuffle(ls: list) -> None: + """Shuffle the given list. + + Args: + ls: The list to shuffle. + """ + RNG.shuffle(ls) From 2fc8aa514c62efa0091eed8f63a110d690bc23e3 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:51:01 +0100 Subject: [PATCH 45/76] Fix AbstractSuperCallingModification --- .../assertion/mutation_analysis/operators/inheritance.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 9073d469..928abaef 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -9,7 +9,6 @@ Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/inheritance.py. """ -import abc import ast import functools @@ -190,10 +189,9 @@ def get_super_call(node: ast.FunctionDef) -> tuple[int, ast.stmt] | None: return None -class AbstractSuperCallingModification(abc.ABC, MutationOperator): - """An abstract class that provides methods to mutate super calls.""" +class AbstractSuperCallingModification(MutationOperator): + """A class that provides methods to mutate super calls.""" - @abc.abstractmethod def should_mutate(self, node: ast.FunctionDef) -> bool: """Check if the node should be mutated. From 4c674c67c8a365b50b12f358fe20d9b2db1ecb39 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:53:46 +0100 Subject: [PATCH 46/76] Fix test_mutate_module --- .../mutation_analysis/test_controller.py | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/tests/assertion/mutation_analysis/test_controller.py b/tests/assertion/mutation_analysis/test_controller.py index bce9e192..260dc458 100644 --- a/tests/assertion/mutation_analysis/test_controller.py +++ b/tests/assertion/mutation_analysis/test_controller.py @@ -4,32 +4,13 @@ # # SPDX-License-Identifier: MIT # -from unittest import mock -from unittest.mock import MagicMock - -import mutpy.controller - import pynguin.assertion.mutation_analysis.controller as c - - -class FooController(c.MutationController): - pass - - -class FooMutController(mutpy.controller.MutationController): - pass +import pynguin.configuration as config def test_mutate_module(): - adapter = FooController() - controller = FooMutController( - MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock() - ) - with mock.patch.object(controller, "mutate_module", MagicMock()) as mutated: - with mock.patch.object( - adapter, "_build_mutation_controller", mutated - ) as mock_obj: - adapter.target_loader = MagicMock() - adapter.mutate_module() - mock_obj.assert_called_once() - mutated.assert_called_once() + controller = c.MutationController() + config.configuration.module_name = "tests.fixtures.examples.triangle" + config.configuration.seeding.seed = 42 + mutations = controller.mutate_module() + assert len(mutations) == 14 From 3640fc59bf7f385185a0c4a1845a34208d3fbf1b Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:55:38 +0100 Subject: [PATCH 47/76] Remove Mutpy dependency --- poetry.lock | 78 +++++++------------------------------------------- pyproject.toml | 1 - 2 files changed, 11 insertions(+), 68 deletions(-) diff --git a/poetry.lock b/poetry.lock index d7d32231..35626921 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,19 +21,6 @@ files = [ {file = "asciitree-0.3.3.tar.gz", hash = "sha256:4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e"}, ] -[[package]] -name = "astmonkey" -version = "0.3.6" -description = "astmonkey is a set of tools to play with Python AST." -optional = false -python-versions = "*" -files = [ - {file = "astmonkey-0.3.6.tar.gz", hash = "sha256:f82dbdd18a2d1810ef43782d3a29743bacd2b09422b8193a72a572b118c8cfb5"}, -] - -[package.dependencies] -pydot = "*" - [[package]] name = "astroid" version = "3.0.3" @@ -744,26 +731,6 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[[package]] -name = "mutpy-pynguin" -version = "0.7.1" -description = "Mutation testing tool for Python 3.x source code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "MutPy-Pynguin-0.7.1.tar.gz", hash = "sha256:31fe168eff221ece0129768b68375ca0d03c6514567caf19878ae7a618b9ab89"}, - {file = "MutPy_Pynguin-0.7.1-py3-none-any.whl", hash = "sha256:9c6ce1afe1f818e97a16f498d0f3ecf65127d93ac74f70eea5ee7b0833122a4e"}, -] - -[package.dependencies] -astmonkey = ">=0.3.6" -Jinja2 = ">=2.7.1" -PyYAML = ">=5.3.1" -termcolor = ">=1.0.0" - -[package.extras] -pytest = ["pytest (>=3.0)"] - [[package]] name = "mypy" version = "1.8.0" @@ -924,25 +891,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "pydot" -version = "2.0.0" -description = "Python interface to Graphviz's Dot" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydot-2.0.0-py3-none-any.whl", hash = "sha256:408a47913ea7bd5d2d34b274144880c1310c4aee901f353cf21fe2e526a4ea28"}, - {file = "pydot-2.0.0.tar.gz", hash = "sha256:60246af215123fa062f21cd791be67dda23a6f280df09f68919e637a1e4f3235"}, -] - -[package.dependencies] -pyparsing = ">=3" - -[package.extras] -dev = ["black", "chardet"] -release = ["zest.releaser[recommended]"] -tests = ["black", "chardet", "tox"] - [[package]] name = "pygments" version = "2.17.2" @@ -958,20 +906,6 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pyparsing" -version = "3.1.1" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pytest" version = "8.0.1" @@ -1088,6 +1022,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1095,8 +1030,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1113,6 +1055,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1120,6 +1063,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1604,4 +1548,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.10, <3.11" -content-hash = "0b5a2dcd794800dd9bf6e4c76d7df7fe39187dbc62869c07810f43ef263aabea" +content-hash = "79689b6f81f0ff9dd742d51f931fec59a7d30d49f255e6a5582d830201c163e8" diff --git a/pyproject.toml b/pyproject.toml index 5f028a67..c24ad4bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ black = "^24.2.0" bytecode = "^0.15.1" jellyfish = "^1.0.3" Jinja2 = "^3.1.3" -MutPy-Pynguin = "^0.7.1" networkx = "^3.2" rich = "^13.7.0" Pygments = "^2.17.2" From acbbc8a381ecbbe0c68812999af54f96c81bdb5b Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:05:10 +0100 Subject: [PATCH 48/76] Improve modules docstrings --- src/pynguin/assertion/mutation_analysis/__init__.py | 2 +- src/pynguin/assertion/mutation_analysis/controller.py | 2 +- src/pynguin/assertion/mutation_analysis/mutators.py | 5 +++-- .../assertion/mutation_analysis/operators/__init__.py | 5 +++-- .../assertion/mutation_analysis/operators/arithmetic.py | 5 +++-- src/pynguin/assertion/mutation_analysis/operators/base.py | 5 +++-- .../assertion/mutation_analysis/operators/decorator.py | 5 +++-- .../assertion/mutation_analysis/operators/exception.py | 5 +++-- .../assertion/mutation_analysis/operators/inheritance.py | 5 +++-- .../assertion/mutation_analysis/operators/logical.py | 5 +++-- src/pynguin/assertion/mutation_analysis/operators/loop.py | 5 +++-- src/pynguin/assertion/mutation_analysis/operators/misc.py | 7 ++++--- src/pynguin/assertion/mutation_analysis/stategies.py | 5 +++-- src/pynguin/assertion/mutation_analysis/transformer.py | 5 +++-- 14 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/__init__.py b/src/pynguin/assertion/mutation_analysis/__init__.py index 3869afd8..3cfe5f53 100644 --- a/src/pynguin/assertion/mutation_analysis/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/__init__.py @@ -4,4 +4,4 @@ # # SPDX-License-Identifier: MIT # -"""Provides an adapter for MutPy's mutation analysis.""" +"""Provides the classes required for mutation analysis.""" diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 72b0485e..53bac5d8 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -4,7 +4,7 @@ # # SPDX-License-Identifier: MIT # -"""Provides an adapter for the MutPy mutation testing framework.""" +"""Provides a controller for generating mutants.""" from __future__ import annotations import ast diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index 28c88fdc..0794acd7 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides classes for mutating ASTs. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py +and integrated in Pynguin. """ from __future__ import annotations diff --git a/src/pynguin/assertion/mutation_analysis/operators/__init__.py b/src/pynguin/assertion/mutation_analysis/operators/__init__.py index 076f7571..0e2523e6 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/__init__.py +++ b/src/pynguin/assertion/mutation_analysis/operators/__init__.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides mutation operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/__init__.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/__init__.py +and integrated in Pynguin. """ from pynguin.assertion.mutation_analysis.operators.arithmetic import ( diff --git a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py index 0cb7480c..a9494908 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py +++ b/src/pynguin/assertion/mutation_analysis/operators/arithmetic.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides arithmetic operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/arithmetic.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/arithmetic.py +and integrated in Pynguin. """ import abc diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index c6ff74e8..2ea5bab9 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides base classes for mutation operators. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/base.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/base.py +and integrated in Pynguin. """ from __future__ import annotations diff --git a/src/pynguin/assertion/mutation_analysis/operators/decorator.py b/src/pynguin/assertion/mutation_analysis/operators/decorator.py index 78cf1bd6..62785940 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/decorator.py +++ b/src/pynguin/assertion/mutation_analysis/operators/decorator.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides decorators operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/decorator.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/decorator.py +and integrated in Pynguin. """ import ast diff --git a/src/pynguin/assertion/mutation_analysis/operators/exception.py b/src/pynguin/assertion/mutation_analysis/operators/exception.py index 129a360a..14da8a2a 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/exception.py +++ b/src/pynguin/assertion/mutation_analysis/operators/exception.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides exception operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/exception.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/exception.py +and integrated in Pynguin. """ import ast diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 928abaef..2ae77503 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides inheritance operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/inheritance.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/inheritance.py +and integrated in Pynguin. """ import ast diff --git a/src/pynguin/assertion/mutation_analysis/operators/logical.py b/src/pynguin/assertion/mutation_analysis/operators/logical.py index 945b05ec..877cea09 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/logical.py +++ b/src/pynguin/assertion/mutation_analysis/operators/logical.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides logical operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/logical.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/logical.py +and integrated in Pynguin. """ import ast diff --git a/src/pynguin/assertion/mutation_analysis/operators/loop.py b/src/pynguin/assertion/mutation_analysis/operators/loop.py index eb0871c7..55a3ad19 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/loop.py +++ b/src/pynguin/assertion/mutation_analysis/operators/loop.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides loop operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/loop.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/loop.py +and integrated in Pynguin. """ import ast diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index 1d03f9ee..dc84089f 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -4,10 +4,11 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides miscellaneous operators for mutation analysis. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/misc.py. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/operators/misc.py +and https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py +and integrated in Pynguin. """ import ast diff --git a/src/pynguin/assertion/mutation_analysis/stategies.py b/src/pynguin/assertion/mutation_analysis/stategies.py index f6501fbc..6d32ec4e 100644 --- a/src/pynguin/assertion/mutation_analysis/stategies.py +++ b/src/pynguin/assertion/mutation_analysis/stategies.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides strategies for higher order mutations. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/controller.py +and integrated in Pynguin. """ from __future__ import annotations diff --git a/src/pynguin/assertion/mutation_analysis/transformer.py b/src/pynguin/assertion/mutation_analysis/transformer.py index e14090ab..f08b922e 100644 --- a/src/pynguin/assertion/mutation_analysis/transformer.py +++ b/src/pynguin/assertion/mutation_analysis/transformer.py @@ -4,9 +4,10 @@ # # SPDX-License-Identifier: MIT # -"""Provides classes for mutation testing. +"""Provides a transformer for modules ASTs. -Comes from https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py. +Based on https://github.com/se2p/mutpy-pynguin/blob/main/mutpy/utils.py +and integrated in Pynguin. """ import ast From d620b991576305072fb86cbf1c670e4d1268b9b5 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:29:50 +0100 Subject: [PATCH 49/76] Fix typo in module name --- src/pynguin/assertion/mutation_analysis/controller.py | 2 +- src/pynguin/assertion/mutation_analysis/mutators.py | 4 ++-- .../mutation_analysis/{stategies.py => strategies.py} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename src/pynguin/assertion/mutation_analysis/{stategies.py => strategies.py} (100%) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 53bac5d8..eab212ea 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -17,7 +17,7 @@ import pynguin.assertion.mutation_analysis.mutators as mu import pynguin.assertion.mutation_analysis.operators as mo -import pynguin.assertion.mutation_analysis.stategies as ms +import pynguin.assertion.mutation_analysis.strategies as ms import pynguin.configuration as config from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index 0794acd7..7725f7f9 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -15,8 +15,8 @@ from typing import TYPE_CHECKING -from pynguin.assertion.mutation_analysis.stategies import FirstToLastHOMStrategy -from pynguin.assertion.mutation_analysis.stategies import HOMStrategy +from pynguin.assertion.mutation_analysis.strategies import FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import HOMStrategy if TYPE_CHECKING: diff --git a/src/pynguin/assertion/mutation_analysis/stategies.py b/src/pynguin/assertion/mutation_analysis/strategies.py similarity index 100% rename from src/pynguin/assertion/mutation_analysis/stategies.py rename to src/pynguin/assertion/mutation_analysis/strategies.py From f697a7dc3e128a8b4a05c334a94607a2991aeb08 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:10:19 +0100 Subject: [PATCH 50/76] Add replacement_node field in Mutation --- .../mutation_analysis/operators/base.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 2ea5bab9..cdc47cc4 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -93,6 +93,7 @@ class Mutation: """Represents a mutation.""" node: ast.AST + replacement_node: ast.AST operator: type[MutationOperator] visitor_name: str @@ -137,8 +138,15 @@ def mutate( """ operator = cls(module, only_mutation) - for current_node, mutated_node, visitor_name in operator.visit(node): - yield Mutation(current_node, cls, visitor_name), mutated_node + for ( + current_node, + replacement_node, + mutated_node, + visitor_name, + ) in operator.visit(node): + yield Mutation( + current_node, replacement_node, cls, visitor_name + ), mutated_node def __init__( self, @@ -154,7 +162,9 @@ def __init__( self.module = module self.only_mutation = only_mutation - def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: + def visit( + self, node: T + ) -> Generator[tuple[ast.AST, ast.AST, ast.AST, str], None, None]: """Visit a node. Args: @@ -185,15 +195,15 @@ def visit(self, node: T) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: fix_node_internals(node, mutated_node) ast.fix_missing_locations(mutated_node) - yield node, mutated_node, visitor.__name__ + yield node, mutated_node, mutated_node, visitor.__name__ yield from self._generic_visit(node) def _generic_visit( self, node: ast.AST - ) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: + ) -> Generator[tuple[ast.AST, ast.AST, ast.AST, str], None, None]: for field, old_value in ast.iter_fields(node): - generator: Iterable[tuple[ast.AST, str]] + generator: Iterable[tuple[ast.AST, ast.AST, str]] if isinstance(old_value, list): generator = self._generic_visit_list(old_value) elif isinstance(old_value, ast.AST): @@ -201,26 +211,33 @@ def _generic_visit( else: generator = () - for current_node, visitor_name in generator: - yield current_node, node, visitor_name + for current_node, replacement_node, visitor_name in generator: + yield current_node, replacement_node, node, visitor_name def _generic_visit_list( self, old_value: list - ) -> Generator[tuple[ast.AST, str], None, None]: + ) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: for position, value in enumerate(old_value.copy()): if isinstance(value, ast.AST): - for current_node, mutated_node, visitor_name in self.visit(value): + for ( + current_node, + replacement_node, + mutated_node, + visitor_name, + ) in self.visit(value): old_value[position] = mutated_node - yield current_node, visitor_name + yield current_node, replacement_node, visitor_name old_value[position] = value def _generic_visit_real_node( self, node: ast.AST, field: str, old_value: ast.AST - ) -> Generator[tuple[ast.AST, str], None, None]: - for current_node, mutated_node, visitor_name in self.visit(old_value): + ) -> Generator[tuple[ast.AST, ast.AST, str], None, None]: + for current_node, replacement_node, mutated_node, visitor_name in self.visit( + old_value + ): setattr(node, field, mutated_node) - yield current_node, visitor_name + yield current_node, replacement_node, visitor_name setattr(node, field, old_value) From db8527ba8e31b534a6060f4016e5b6f0602334d5 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:24:18 +0100 Subject: [PATCH 51/76] Remove visitor for deprecated ast.Num and ast.Str --- .../mutation_analysis/operators/misc.py | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/misc.py b/src/pynguin/assertion/mutation_analysis/operators/misc.py index dc84089f..b4f28e76 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/misc.py +++ b/src/pynguin/assertion/mutation_analysis/operators/misc.py @@ -177,49 +177,6 @@ def mutate_Constant_str_empty( # noqa: N802 return ast.Constant(new_value) - def mutate_Num(self, node: ast.Num) -> ast.Num: # noqa: N802 - """Mutate a numeric constant by adding 1. - - Args: - node: The constant to mutate. - - Returns: - The mutated constant. - """ - return ast.Num(node.value + 1) - - def mutate_Str(self, node: ast.Str) -> ast.Str | None: # noqa: N802 - """Mutate a string constant by replacing it. - - Args: - node: The constant to mutate. - - Returns: - The mutated constant, or None if the constant should not be mutated. - """ - new_value = self.help_str(node) - - if new_value is None: - return None - - return ast.Str(new_value) - - def mutate_Str_empty(self, node: ast.Str) -> ast.Str | None: # noqa: N802 - """Mutate an empty string constant by replacing it. - - Args: - node: The constant to mutate. - - Returns: - The mutated constant, or None if the constant should not be mutated. - """ - new_value = self.help_str_empty(node) - - if new_value is None: - return None - - return ast.Str(new_value) - class SliceIndexRemove(MutationOperator): """A class that mutates slice indices by removing them.""" From d30146401625c1ed7b3b5bf4d817012b04de71f6 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:13:25 +0100 Subject: [PATCH 52/76] Force module to be not None --- src/pynguin/assertion/mutation_analysis/mutators.py | 8 ++++---- src/pynguin/assertion/mutation_analysis/operators/base.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/mutators.py b/src/pynguin/assertion/mutation_analysis/mutators.py index 7725f7f9..6b4ad2fa 100644 --- a/src/pynguin/assertion/mutation_analysis/mutators.py +++ b/src/pynguin/assertion/mutation_analysis/mutators.py @@ -36,7 +36,7 @@ class Mutator(abc.ABC): def mutate( self, target_ast: ast.AST, - module: types.ModuleType | None = None, + module: types.ModuleType, ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: """Mutate the given AST. @@ -63,7 +63,7 @@ def __init__(self, operators: list[type[MutationOperator]]) -> None: def mutate( # noqa: D102 self, target_ast: ast.AST, - module: types.ModuleType | None = None, + module: types.ModuleType, ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: for op in self.operators: for mutation, mutant in op.mutate(target_ast, module): @@ -90,7 +90,7 @@ def __init__( def mutate( # noqa: D102 self, target_ast: ast.AST, - module: types.ModuleType | None = None, + module: types.ModuleType, ) -> Generator[tuple[list[Mutation], ast.AST], None, None]: mutations = self._generate_all_mutations(module, target_ast) for mutations_to_apply in self.hom_strategy.generate(mutations): @@ -109,7 +109,7 @@ def mutate( # noqa: D102 def _generate_all_mutations( self, - module: types.ModuleType | None, + module: types.ModuleType, target_ast: ast.AST, ) -> list[Mutation]: mutations: list[Mutation] = [] diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index cdc47cc4..508cd17b 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -123,7 +123,7 @@ class MutationOperator: def mutate( cls, node: T, - module: types.ModuleType | None = None, + module: types.ModuleType, only_mutation: Mutation | None = None, ) -> Generator[tuple[Mutation, ast.AST], None, None]: """Mutate a node. @@ -150,7 +150,7 @@ def mutate( def __init__( self, - module: types.ModuleType | None, + module: types.ModuleType, only_mutation: Mutation | None, ) -> None: """Initializes the operator. From 213cb9fe5b1cf8cdf2b8832890385fa4984e3cdb Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:13:46 +0100 Subject: [PATCH 53/76] Fix doc requirements --- docs/requirements.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index ca0b8c86..d041fedd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,5 @@ alabaster==0.7.16 ; python_version >= "3.10" and python_version < "3.11" asciitree==0.3.3 ; python_version >= "3.10" and python_version < "3.11" -astmonkey==0.3.6 ; python_version >= "3.10" and python_version < "3.11" astroid==3.0.3 ; python_version >= "3.10" and python_version < "3.11" babel==2.14.0 ; python_version >= "3.10" and python_version < "3.11" black==24.2.0 ; python_version >= "3.10" and python_version < "3.11" @@ -21,16 +20,13 @@ libcst==1.2.0 ; python_version >= "3.10" and python_version < "3.11" markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "3.11" markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "3.11" mdurl==0.1.2 ; python_version >= "3.10" and python_version < "3.11" -mutpy-pynguin==0.7.1 ; python_version >= "3.10" and python_version < "3.11" mypy-extensions==1.0.0 ; python_version >= "3.10" and python_version < "3.11" networkx==3.2.1 ; python_version >= "3.10" and python_version < "3.11" packaging==23.2 ; python_version >= "3.10" and python_version < "3.11" pathspec==0.12.1 ; python_version >= "3.10" and python_version < "3.11" platformdirs==4.2.0 ; python_version >= "3.10" and python_version < "3.11" pluggy==1.4.0 ; python_version >= "3.10" and python_version < "3.11" -pydot==2.0.0 ; python_version >= "3.10" and python_version < "3.11" pygments==2.17.2 ; python_version >= "3.10" and python_version < "3.11" -pyparsing==3.1.1 ; python_version >= "3.10" and python_version < "3.11" pytest==8.0.1 ; python_version >= "3.10" and python_version < "3.11" pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "3.11" requests==2.31.0 ; python_version >= "3.10" and python_version < "3.11" @@ -49,7 +45,6 @@ sphinxcontrib-jquery==4.1 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-qthelp==1.0.7 ; python_version >= "3.10" and python_version < "3.11" sphinxcontrib-serializinghtml==1.1.10 ; python_version >= "3.10" and python_version < "3.11" -termcolor==2.4.0 ; python_version >= "3.10" and python_version < "3.11" tomli==2.0.1 ; python_version >= "3.10" and python_version < "3.11" typing-extensions==4.9.0 ; python_version >= "3.10" and python_version < "3.11" typing-inspect==0.9.0 ; python_version >= "3.10" and python_version < "3.11" From 8b5a1f90e2924265d09ac41a375cb1b4b3f808c0 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:22:23 +0100 Subject: [PATCH 54/76] Move create_module to transformer module --- .../assertion/mutation_analysis/controller.py | 11 ++++------- .../assertion/mutation_analysis/transformer.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index eab212ea..8c8b173e 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -11,7 +11,6 @@ import importlib import inspect import logging -import types from typing import TYPE_CHECKING @@ -21,6 +20,7 @@ import pynguin.configuration as config from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer +from pynguin.assertion.mutation_analysis.transformer import create_module from pynguin.utils.exceptions import ConfigurationException @@ -75,7 +75,7 @@ def create_mutants( self, mutant_generator: mu.FirstOrderMutator, target_ast: ast.Module, - target_module: types.ModuleType, + target_module: ModuleType, ) -> list[tuple[ModuleType, list[Mutation]]]: """Creates mutants for the given module. @@ -103,7 +103,7 @@ def create_mutants( return mutants - def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleType: + def create_module(self, ast_node: ast.Module, module_name: str) -> ModuleType: """Creates a module from an AST node. Args: @@ -113,10 +113,7 @@ def create_module(self, ast_node: ast.Module, module_name: str) -> types.ModuleT Returns: The created module. """ - code = compile(ast_node, module_name, "exec") - module = types.ModuleType(module_name) - exec(code, module.__dict__) # noqa: S102 - return module + return create_module(ast_node, module_name) def _get_mutant_generator(self) -> mu.FirstOrderMutator: operators: list[type[MutationOperator]] = [ diff --git a/src/pynguin/assertion/mutation_analysis/transformer.py b/src/pynguin/assertion/mutation_analysis/transformer.py index f08b922e..97d4f7dd 100644 --- a/src/pynguin/assertion/mutation_analysis/transformer.py +++ b/src/pynguin/assertion/mutation_analysis/transformer.py @@ -12,10 +12,27 @@ import ast import copy +import types from typing import TypeVar +def create_module(ast_node: ast.Module, module_name: str) -> types.ModuleType: + """Creates a module from an AST node. + + Args: + ast_node: The AST node. + module_name: The name of the module. + + Returns: + The created module. + """ + code = compile(ast_node, module_name, "exec") + module = types.ModuleType(module_name) + exec(code, module.__dict__) # noqa: S102 + return module + + T = TypeVar("T", bound=ast.AST) From 8a5df8bd078e41fc2bef950ca00284d34ecee1cb Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:24:30 +0100 Subject: [PATCH 55/76] Fix MutationOperator._find_visitors --- src/pynguin/assertion/mutation_analysis/operators/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 508cd17b..f3dfc486 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -14,6 +14,7 @@ import abc import ast import copy +import re from dataclasses import dataclass from typing import TYPE_CHECKING @@ -243,11 +244,11 @@ def _generic_visit_real_node( def _find_visitors(self, node: T) -> list[Callable[[T], ast.AST | None]]: node_name = node.__class__.__name__ - method_prefix = f"mutate_{node_name}" + method_prefix_pattern = re.compile(f"^mutate_{node_name}(_\\w+)?$") return [ visitor for attr in dir(self) - if attr.startswith(method_prefix) + if method_prefix_pattern.match(attr) is not None and callable(visitor := getattr(self, attr)) ] From e7aa44d3d0ae5706583112237e3cb76fba0cea85 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:25:11 +0100 Subject: [PATCH 56/76] Add tests for operators --- .../mutation_analysis/mutators/__init__.py | 0 .../mutators/operators/__init__.py | 0 .../mutators/operators/test_arithmetic.py | 291 ++++++++++ .../mutators/operators/test_decorator.py | 89 ++++ .../mutators/operators/test_exception.py | 130 +++++ .../mutators/operators/test_inheritance.py | 334 ++++++++++++ .../mutators/operators/test_logical.py | 497 ++++++++++++++++++ .../mutators/operators/test_loop.py | 177 +++++++ .../mutators/operators/test_misc.py | 258 +++++++++ tests/testutils.py | 49 ++ 10 files changed, 1825 insertions(+) create mode 100644 tests/assertion/mutation_analysis/mutators/__init__.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/__init__.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/test_arithmetic.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/test_decorator.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/test_exception.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/test_inheritance.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/test_logical.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/test_loop.py create mode 100644 tests/assertion/mutation_analysis/mutators/operators/test_misc.py diff --git a/tests/assertion/mutation_analysis/mutators/__init__.py b/tests/assertion/mutation_analysis/mutators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/assertion/mutation_analysis/mutators/operators/__init__.py b/tests/assertion/mutation_analysis/mutators/operators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_arithmetic.py b/tests/assertion/mutation_analysis/mutators/operators/test_arithmetic.py new file mode 100644 index 00000000..c98a8426 --- /dev/null +++ b/tests/assertion/mutation_analysis/mutators/operators/test_arithmetic.py @@ -0,0 +1,291 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorReplacement, +) +from tests.testutils import assert_mutation + + +def test_usub_deletion(): + assert_mutation( + ArithmeticOperatorDeletion, + inspect.cleandoc( + """ + x = 0 + y = -x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_uadd_deletion(): + assert_mutation( + ArithmeticOperatorDeletion, + inspect.cleandoc( + """ + x = 0 + y = +x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_add_to_sub_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x + 1 + z = x + 2 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x - 1 + z = x + 2 + """ + ): ("mutate_Add", ast.Add, ast.Sub), + inspect.cleandoc( + """ + x = 0 + y = x + 1 + z = x - 2 + """ + ): ("mutate_Add", ast.Add, ast.Sub), + }, + ) + + +def test_sub_to_add_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x - 1 + z = x - 2 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x + 1 + z = x - 2 + """ + ): ("mutate_Sub", ast.Sub, ast.Add), + inspect.cleandoc( + """ + x = 0 + y = x - 1 + z = x + 2 + """ + ): ("mutate_Sub", ast.Sub, ast.Add), + }, + ) + + +def test_mult_to_div_and_pow_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x / 1 + """ + ): ("mutate_Mult_to_Div", ast.Mult, ast.Div), + inspect.cleandoc( + """ + x = 0 + y = x // 1 + """ + ): ("mutate_Mult_to_FloorDiv", ast.Mult, ast.FloorDiv), + inspect.cleandoc( + """ + x = 0 + y = x ** 1 + """ + ): ("mutate_Mult_to_Pow", ast.Mult, ast.Pow), + }, + ) + + +def test_div_to_mult_and_floordiv_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x / 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_Div_to_Mult", ast.Div, ast.Mult), + inspect.cleandoc( + """ + x = 0 + y = x // 1 + """ + ): ("mutate_Div_to_FloorDiv", ast.Div, ast.FloorDiv), + }, + ) + + +def test_floor_div_to_mult_and_div_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x // 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_FloorDiv_to_Mult", ast.FloorDiv, ast.Mult), + inspect.cleandoc( + """ + x = 0 + y = x / 1 + """ + ): ("mutate_FloorDiv_to_Div", ast.FloorDiv, ast.Div), + }, + ) + + +def test_mod_to_mult_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x % 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_Mod", ast.Mod, ast.Mult), + }, + ) + + +def test_pow_to_mult_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = x ** 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = x * 1 + """ + ): ("mutate_Pow", ast.Pow, ast.Mult), + }, + ) + + +def test_augmented_assign_ignore(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x += 1 + """ + ), + {}, + ) + + +def test_usub_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = -x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = +x + """ + ): ("mutate_USub", ast.USub, ast.UAdd), + }, + ) + + +def test_uadd_replacement(): + assert_mutation( + ArithmeticOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + y = +x + """ + ), + { + inspect.cleandoc( + """ + x = 0 + y = -x + """ + ): ("mutate_UAdd", ast.UAdd, ast.USub), + }, + ) diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_decorator.py b/tests/assertion/mutation_analysis/mutators/operators/test_decorator.py new file mode 100644 index 00000000..9c8cac7f --- /dev/null +++ b/tests/assertion/mutation_analysis/mutators/operators/test_decorator.py @@ -0,0 +1,89 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.decorator import DecoratorDeletion +from tests.testutils import assert_mutation + + +def test_single_decorator_deletion(): + assert_mutation( + DecoratorDeletion, + inspect.cleandoc( + """ + import atexit + + @atexit.register + def foo(): + pass + """ + ), + { + inspect.cleandoc( + """ + import atexit + + def foo(): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_multiple_decorators_deletion(): + assert_mutation( + DecoratorDeletion, + inspect.cleandoc( + """ + from abc import ABC, abstractmethod + + class Foo(ABC): + @classmethod + @abstractmethod + def bar(): + pass + """ + ), + { + inspect.cleandoc( + """ + from abc import ABC, abstractmethod + + class Foo(ABC): + def bar(): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_decorator_with_arguments_deletion(): + assert_mutation( + DecoratorDeletion, + inspect.cleandoc( + """ + from functools import lru_cache + + @lru_cache(maxsize=128) + def foo(x: int) -> int: + return x + """ + ), + { + inspect.cleandoc( + """ + from functools import lru_cache + + def foo(x: int) -> int: + return x + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_exception.py b/tests/assertion/mutation_analysis/mutators/operators/test_exception.py new file mode 100644 index 00000000..a8a0c2a3 --- /dev/null +++ b/tests/assertion/mutation_analysis/mutators/operators/test_exception.py @@ -0,0 +1,130 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.exception import ( + ExceptionHandlerDeletion, +) +from pynguin.assertion.mutation_analysis.operators.exception import ExceptionSwallowing +from tests.testutils import assert_mutation + + +def test_exception_handler_deletion(): + assert_mutation( + ExceptionHandlerDeletion, + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + """ + ), + { + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + }, + ) + + +def test_two_exception_handler_deletion(): + assert_mutation( + ExceptionHandlerDeletion, + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + except ZeroDivisionError: + pass + """ + ), + { + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + except ZeroDivisionError: + pass + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + except ZeroDivisionError: + raise + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + }, + ) + + +def test_raise_no_deletion(): + assert_mutation( + ExceptionHandlerDeletion, + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + """ + ), + {}, + ) + + +def test_exception_swallowing(): + assert_mutation( + ExceptionSwallowing, + inspect.cleandoc( + """ + try: + pass + except ValueError: + raise + """ + ), + { + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + """ + ): ("mutate_ExceptHandler", ast.ExceptHandler, ast.ExceptHandler), + }, + ) + + +def test_exception_no_swallowing_when_pass(): + assert_mutation( + ExceptionSwallowing, + inspect.cleandoc( + """ + try: + pass + except ValueError: + pass + """ + ), + {}, + ) diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_inheritance.py b/tests/assertion/mutation_analysis/mutators/operators/test_inheritance.py new file mode 100644 index 00000000..ee99d3e6 --- /dev/null +++ b/tests/assertion/mutation_analysis/mutators/operators/test_inheritance.py @@ -0,0 +1,334 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + HidingVariableDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverriddenMethodCallingPositionChange, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + OverridingMethodDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import ( + SuperCallingDeletion, +) +from pynguin.assertion.mutation_analysis.operators.inheritance import SuperCallingInsert +from tests.testutils import assert_mutation + + +def test_hiding_variable_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x = 1 + class Bar(Foo): + x = 2 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x = 1 + class Bar(Foo): + pass + """ + ): ("mutate_Assign", ast.Assign, ast.Pass), + }, + ) + + +def test_hiding_variable_in_2_elements_tuple_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + x, y = 2, 3 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + y = 3 + """ + ): ("mutate_Assign", ast.Assign, ast.Assign), + }, + ) + + +def test_hiding_variable_in_3_elements_tuple_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + x, y, z = 2, 3, 4 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x = 1 + class Baz(Foo): + y, z = 3, 4 + """ + ): ("mutate_Assign", ast.Assign, ast.Assign), + }, + ) + + +def test_hiding_tuple_deletion(): + assert_mutation( + HidingVariableDeletion, + inspect.cleandoc( + """ + class Foo: + x, y = 1, 2 + class Baz(Foo): + x, y = 3, 4 + """ + ), + { + inspect.cleandoc( + """ + class Foo: + x, y = 1, 2 + class Baz(Foo): + pass + """ + ): ("mutate_Assign", ast.Assign, ast.Pass), + }, + ) + + +def test_super_call_position_change_from_first_to_last(): + assert_mutation( + OverriddenMethodCallingPositionChange, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + pass + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + super().baz(x) + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_super_call_position_change_from_last_to_first(): + assert_mutation( + OverriddenMethodCallingPositionChange, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + super().baz(x) + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_super_call_position_ignore_when_only_one_statement(): + assert_mutation( + OverriddenMethodCallingPositionChange, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + """ + ), + {}, + ) + + +def test_overriding_method_deletion(): + assert_mutation( + OverridingMethodDeletion, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.Pass), + }, + ) + + +def test_overriding_method_deletion_in_inner_class(): + assert_mutation( + OverridingMethodDeletion, + inspect.cleandoc( + """ + class Outer: + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ), + { + inspect.cleandoc( + """ + class Outer: + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.Pass), + }, + ) + + +def test_overriding_method_deletion_when_base_class_in_another_module(): + assert_mutation( + OverridingMethodDeletion, + inspect.cleandoc( + """ + from ast import NodeTransformer + + class Foo(NodeTransformer): + def visit(self, node): + pass + """ + ), + { + inspect.cleandoc( + """ + from ast import NodeTransformer + + class Foo(NodeTransformer): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.Pass), + }, + ) + + +def test_super_call_deletion(): + assert_mutation( + SuperCallingDeletion, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) + + +def test_super_call_insertion(): + assert_mutation( + SuperCallingInsert, + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + pass + """ + ), + { + inspect.cleandoc( + """ + class Foo: + def baz(self, x: int): + pass + class Bar(Foo): + def baz(self, x: int): + super().baz(x) + pass + """ + ): ("mutate_FunctionDef", ast.FunctionDef, ast.FunctionDef), + }, + ) diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_logical.py b/tests/assertion/mutation_analysis/mutators/operators/test_logical.py new file mode 100644 index 00000000..5eab3dea --- /dev/null +++ b/tests/assertion/mutation_analysis/mutators/operators/test_logical.py @@ -0,0 +1,497 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + ConditionalOperatorInsertion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalConnectorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorDeletion, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + LogicalOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.logical import ( + RelationalOperatorReplacement, +) +from tests.testutils import assert_mutation + + +def test_not_operator_negation(): + assert_mutation( + ConditionalOperatorDeletion, + inspect.cleandoc( + """ + x = True + y = not x + """ + ), + { + inspect.cleandoc( + """ + x = True + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_not_in_operator_negation(): + assert_mutation( + ConditionalOperatorDeletion, + inspect.cleandoc( + """ + x = 1 not in [1, 2, 3] + """ + ), + { + inspect.cleandoc( + """ + x = 1 in [1, 2, 3] + """ + ): ("mutate_NotIn", ast.NotIn, ast.In), + }, + ) + + +def test_while_condition_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + x = 1 + while x < 10: + x += 1 + """ + ), + { + inspect.cleandoc( + """ + x = 1 + while not (x < 10): + x += 1 + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_if_condition_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + x = 1 + if x < 10: + x += 1 + """ + ), + { + inspect.cleandoc( + """ + x = 1 + if not (x < 10): + x += 1 + """ + ): ("mutate_If", ast.If, ast.If), + }, + ) + + +def test_if_elif_condition_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + x = 1 + if x < 10: + x += 1 + elif x < 20: + x += 2 + """ + ), + { + inspect.cleandoc( + """ + x = 1 + if not (x < 10): + x += 1 + elif x < 20: + x += 2 + """ + ): ("mutate_If", ast.If, ast.If), + inspect.cleandoc( + """ + x = 1 + if x < 10: + x += 1 + elif not (x < 20): + x += 2 + """ + ): ("mutate_If", ast.If, ast.If), + }, + ) + + +def test_in_negation(): + assert_mutation( + ConditionalOperatorInsertion, + inspect.cleandoc( + """ + y = 1 in [1, 2, 3] + """ + ), + { + inspect.cleandoc( + """ + y = 1 not in [1, 2, 3] + """ + ): ("mutate_In", ast.In, ast.NotIn), + }, + ) + + +def test_and_to_or_replacement(): + assert_mutation( + LogicalConnectorReplacement, + inspect.cleandoc( + """ + x = True + y = False + z = x and y + """ + ), + { + inspect.cleandoc( + """ + x = True + y = False + z = x or y + """ + ): ("mutate_And", ast.And, ast.Or), + }, + ) + + +def test_or_to_and_replacement(): + assert_mutation( + LogicalConnectorReplacement, + inspect.cleandoc( + """ + x = True + y = False + z = x or y + """ + ), + { + inspect.cleandoc( + """ + x = True + y = False + z = x and y + """ + ): ("mutate_Or", ast.Or, ast.And), + }, + ) + + +def test_logical_operator_deletion(): + assert_mutation( + LogicalOperatorDeletion, + inspect.cleandoc( + """ + x = True + y = ~x + """ + ), + { + inspect.cleandoc( + """ + x = True + y = x + """ + ): ("mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + ) + + +def test_bin_and_to_bin_or_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x & y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x | y + """ + ): ("mutate_BitAnd", ast.BitAnd, ast.BitOr), + }, + ) + + +def test_bin_or_to_bin_and_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x | y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x & y + """ + ): ("mutate_BitOr", ast.BitOr, ast.BitAnd), + }, + ) + + +def test_bin_xor_to_bin_and_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x ^ y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x & y + """ + ): ("mutate_BitXor", ast.BitXor, ast.BitAnd), + }, + ) + + +def test_bin_lshift_to_bin_rshift_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x << y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >> y + """ + ): ("mutate_LShift", ast.LShift, ast.RShift), + }, + ) + + +def test_bin_rshift_to_bin_lshift_replacement(): + assert_mutation( + LogicalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >> y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x << y + """ + ): ("mutate_RShift", ast.RShift, ast.LShift), + }, + ) + + +def test_lt_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x < y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x > y + """ + ): ("mutate_Lt", ast.Lt, ast.Gt), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x <= y + """ + ): ("mutate_Lt_to_LtE", ast.Lt, ast.LtE), + }, + ) + + +def test_gt_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x > y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x < y + """ + ): ("mutate_Gt", ast.Gt, ast.Lt), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >= y + """ + ): ("mutate_Gt_to_GtE", ast.Gt, ast.GtE), + }, + ) + + +def test_lte_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x <= y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >= y + """ + ): ("mutate_LtE", ast.LtE, ast.GtE), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x < y + """ + ): ("mutate_LtE_to_Lt", ast.LtE, ast.Lt), + }, + ) + + +def test_gte_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x >= y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x <= y + """ + ): ("mutate_GtE", ast.GtE, ast.LtE), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x > y + """ + ): ("mutate_GtE_to_Gt", ast.GtE, ast.Gt), + }, + ) + + +def test_eq_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x == y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x != y + """ + ): ("mutate_Eq", ast.Eq, ast.NotEq), + }, + ) + + +def test_not_eq_replacement(): + assert_mutation( + RelationalOperatorReplacement, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x != y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = x == y + """ + ): ("mutate_NotEq", ast.NotEq, ast.Eq), + }, + ) diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_loop.py b/tests/assertion/mutation_analysis/mutators/operators/test_loop.py new file mode 100644 index 00000000..2195c5aa --- /dev/null +++ b/tests/assertion/mutation_analysis/mutators/operators/test_loop.py @@ -0,0 +1,177 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.loop import OneIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ReverseIterationLoop +from pynguin.assertion.mutation_analysis.operators.loop import ZeroIterationLoop +from tests.testutils import assert_mutation + + +def test_one_iteration_for_loop_break(): + assert_mutation( + OneIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + pass + break + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) + + +def test_one_iteration_while_loop_break(): + assert_mutation( + OneIterationLoop, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + break + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_one_iteration_multiple_loops_break(): + assert_mutation( + OneIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + i = 0 + while i < 10: + i += 1 + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + pass + break + i = 0 + while i < 10: + i += 1 + """ + ): ("mutate_For", ast.For, ast.For), + inspect.cleandoc( + """ + for x in range(10): + pass + i = 0 + while i < 10: + i += 1 + break + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_zero_iteration_for_loop_break(): + assert_mutation( + ZeroIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + break + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) + + +def test_zero_iteration_while_loop_break(): + assert_mutation( + ZeroIterationLoop, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + break + """ + ): ("mutate_While", ast.While, ast.While), + }, + ) + + +def test_zero_iteration_for_loop_multiple_statements_break(): + assert_mutation( + ZeroIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + pass + """ + ), + { + inspect.cleandoc( + """ + for x in range(10): + break + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) + + +def test_reverse_iteration_for_loop(): + assert_mutation( + ReverseIterationLoop, + inspect.cleandoc( + """ + for x in range(10): + pass + """ + ), + { + inspect.cleandoc( + """ + for x in reversed(range(10)): + pass + """ + ): ("mutate_For", ast.For, ast.For), + }, + ) diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_misc.py b/tests/assertion/mutation_analysis/mutators/operators/test_misc.py new file mode 100644 index 00000000..ce882283 --- /dev/null +++ b/tests/assertion/mutation_analysis/mutators/operators/test_misc.py @@ -0,0 +1,258 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.operators.misc import ( + AssignmentOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.misc import BreakContinueReplacement +from pynguin.assertion.mutation_analysis.operators.misc import ConstantReplacement +from pynguin.assertion.mutation_analysis.operators.misc import SliceIndexRemove +from tests.testutils import assert_mutation + + +def test_add_to_sub_replacement(): + assert_mutation( + AssignmentOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x += 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + x -= 1 + """ + ): ("mutate_Add", ast.Add, ast.Sub), + }, + ) + + +def test_sub_to_add_replacement(): + assert_mutation( + AssignmentOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x -= 1 + """ + ), + { + inspect.cleandoc( + """ + x = 0 + x += 1 + """ + ): ("mutate_Sub", ast.Sub, ast.Add), + }, + ) + + +def test_normal_use_ignore(): + assert_mutation( + AssignmentOperatorReplacement, + inspect.cleandoc( + """ + x = 0 + x = x + 1 + """ + ), + {}, + ) + + +def test_break_to_continue_replacement(): + assert_mutation( + BreakContinueReplacement, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + break + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + continue + """ + ): ("mutate_Break", ast.Break, ast.Continue), + }, + ) + + +def test_continue_to_break_replacement(): + assert_mutation( + BreakContinueReplacement, + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + continue + """ + ), + { + inspect.cleandoc( + """ + i = 0 + while i < 10: + i += 1 + break + """ + ): ("mutate_Continue", ast.Continue, ast.Break), + }, + ) + + +def test_numbers_increase(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + """ + x = 1 - 2 + 99 + """ + ), + { + inspect.cleandoc( + """ + x = 2 - 2 + 99 + """ + ): ("mutate_Constant_num", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 1 - 3 + 99 + """ + ): ("mutate_Constant_num", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 1 - 2 + 100 + """ + ): ("mutate_Constant_num", ast.Constant, ast.Constant), + }, + ) + + +def test_string_replacement(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + """ + x = 'x' + y = '' + 'y' + """ + ), + { + inspect.cleandoc( + f""" + x = '{ConstantReplacement.FIRST_CONST_STRING}' + y = '' + 'y' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + f""" + x = 'x' + y = '{ConstantReplacement.FIRST_CONST_STRING}' + 'y' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + f""" + x = 'x' + y = '' + '{ConstantReplacement.FIRST_CONST_STRING}' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = '' + y = '' + 'y' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 'x' + y = '' + '' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + inspect.cleandoc( + """ + x = 'x' + y = '' + '' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + }, + ) + + +def test_first_constant_string_replacement(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + f""" + x = '{ConstantReplacement.FIRST_CONST_STRING}' + """ + ), + { + inspect.cleandoc( + f""" + x = '{ConstantReplacement.SECOND_CONST_STRING}' + """ + ): ("mutate_Constant_str", ast.Constant, ast.Constant), + inspect.cleandoc( + f""" + x = '' + """ + ): ("mutate_Constant_str_empty", ast.Constant, ast.Constant), + }, + ) + + +def test_docstring_ignore(): + assert_mutation( + ConstantReplacement, + inspect.cleandoc( + """ + def foo(): + '''Docstring''' + pass + """ + ), + {}, + ) + + +def test_slice_index_replacement(): + assert_mutation( + SliceIndexRemove, + inspect.cleandoc( + """ + x = [1, 2, 3] + y = x[1:2] + """ + ), + { + inspect.cleandoc( + """ + x = [1, 2, 3] + y = x[1:] + """ + ): ("mutate_Slice_remove_upper", ast.Slice, ast.Slice), + inspect.cleandoc( + """ + x = [1, 2, 3] + y = x[:2] + """ + ): ("mutate_Slice_remove_lower", ast.Slice, ast.Slice), + }, + ) diff --git a/tests/testutils.py b/tests/testutils.py index 0d947b75..a762ce7e 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -5,11 +5,16 @@ # SPDX-License-Identifier: MIT # """Some utilites to make testing easier.""" +import ast + import pynguin.utils.generic.genericaccessibleobject as gao from pynguin.analyses.typesystem import Instance from pynguin.analyses.typesystem import ProperType from pynguin.analyses.typesystem import TypeSystem +from pynguin.assertion.mutation_analysis.operators import MutationOperator +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer +from pynguin.assertion.mutation_analysis.transformer import create_module def feed_typesystem(system: TypeSystem, generic: gao.GenericAccessibleObject): @@ -32,3 +37,47 @@ def feed(typ: ProperType): if isinstance(generic, gao.GenericField): system.to_type_info(generic.owner.raw_type) + + +def assert_mutation( + operator: type[MutationOperator], + source_code: str, + expected_mutants_source_code: dict[str, tuple[str, type[ast.AST], type[ast.AST]]], +): + module_ast = ParentNodeTransformer.create_ast(source_code) + module = create_module(module_ast, "mutant") + + expected_mutants_processed_source_code = { + ast.unparse(ParentNodeTransformer.create_ast(mutant_source_code)): mutant_info + for mutant_source_code, mutant_info in expected_mutants_source_code.items() + } + + for mutation, mutant_ast in operator.mutate(module_ast, module): + assert mutation.operator is operator, f"{mutation.operator} is not {operator}" + assert mutation.visitor_name in dir( + operator + ), f"{mutation.visitor_name} not in {dir(operator)}" + + mutant_source_code = ast.unparse(mutant_ast) + + assert ( + mutant_source_code in expected_mutants_processed_source_code + ), f"{repr(mutant_source_code)} not in {expected_mutants_processed_source_code}" + + visitor_name, node_type, replacement_node_type = ( + expected_mutants_processed_source_code.pop(mutant_source_code) + ) + + assert ( + mutation.visitor_name == visitor_name + ), f"{mutation.visitor_name} is not {visitor_name}" + assert isinstance( + mutation.node, node_type + ), f"{mutation.node} is not {node_type}" + assert isinstance( + mutation.replacement_node, replacement_node_type + ), f"{mutation.replacement_node} is not {replacement_node_type}" + + assert ( + not expected_mutants_processed_source_code + ), f"Remaining mutants: {expected_mutants_processed_source_code}" From 19308188f616abd8f1d9b2e58bb23ccbd4006a88 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:37:09 +0100 Subject: [PATCH 57/76] Move operators location --- tests/assertion/mutation_analysis/mutators/operators/__init__.py | 0 .../mutation_analysis/{mutators => operators}/__init__.py | 0 .../mutation_analysis/{mutators => }/operators/test_arithmetic.py | 0 .../mutation_analysis/{mutators => }/operators/test_decorator.py | 0 .../mutation_analysis/{mutators => }/operators/test_exception.py | 0 .../{mutators => }/operators/test_inheritance.py | 0 .../mutation_analysis/{mutators => }/operators/test_logical.py | 0 .../mutation_analysis/{mutators => }/operators/test_loop.py | 0 .../mutation_analysis/{mutators => }/operators/test_misc.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/assertion/mutation_analysis/mutators/operators/__init__.py rename tests/assertion/mutation_analysis/{mutators => operators}/__init__.py (100%) rename tests/assertion/mutation_analysis/{mutators => }/operators/test_arithmetic.py (100%) rename tests/assertion/mutation_analysis/{mutators => }/operators/test_decorator.py (100%) rename tests/assertion/mutation_analysis/{mutators => }/operators/test_exception.py (100%) rename tests/assertion/mutation_analysis/{mutators => }/operators/test_inheritance.py (100%) rename tests/assertion/mutation_analysis/{mutators => }/operators/test_logical.py (100%) rename tests/assertion/mutation_analysis/{mutators => }/operators/test_loop.py (100%) rename tests/assertion/mutation_analysis/{mutators => }/operators/test_misc.py (100%) diff --git a/tests/assertion/mutation_analysis/mutators/operators/__init__.py b/tests/assertion/mutation_analysis/mutators/operators/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/assertion/mutation_analysis/mutators/__init__.py b/tests/assertion/mutation_analysis/operators/__init__.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/__init__.py rename to tests/assertion/mutation_analysis/operators/__init__.py diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_arithmetic.py b/tests/assertion/mutation_analysis/operators/test_arithmetic.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/operators/test_arithmetic.py rename to tests/assertion/mutation_analysis/operators/test_arithmetic.py diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_decorator.py b/tests/assertion/mutation_analysis/operators/test_decorator.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/operators/test_decorator.py rename to tests/assertion/mutation_analysis/operators/test_decorator.py diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_exception.py b/tests/assertion/mutation_analysis/operators/test_exception.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/operators/test_exception.py rename to tests/assertion/mutation_analysis/operators/test_exception.py diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_inheritance.py b/tests/assertion/mutation_analysis/operators/test_inheritance.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/operators/test_inheritance.py rename to tests/assertion/mutation_analysis/operators/test_inheritance.py diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_logical.py b/tests/assertion/mutation_analysis/operators/test_logical.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/operators/test_logical.py rename to tests/assertion/mutation_analysis/operators/test_logical.py diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_loop.py b/tests/assertion/mutation_analysis/operators/test_loop.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/operators/test_loop.py rename to tests/assertion/mutation_analysis/operators/test_loop.py diff --git a/tests/assertion/mutation_analysis/mutators/operators/test_misc.py b/tests/assertion/mutation_analysis/operators/test_misc.py similarity index 100% rename from tests/assertion/mutation_analysis/mutators/operators/test_misc.py rename to tests/assertion/mutation_analysis/operators/test_misc.py From f7193295e335cdd483ea182bc4df257fde7a0202 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:30:15 +0100 Subject: [PATCH 58/76] Freeze Mutation class --- src/pynguin/assertion/mutation_analysis/operators/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index f3dfc486..9fe40218 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -89,7 +89,7 @@ def shift_lines(nodes: list[T], shift_by: int = 1) -> None: ast.increment_lineno(node, shift_by) -@dataclass +@dataclass(frozen=True) class Mutation: """Represents a mutation.""" From bdf80b6a02b9d90a2c49fcc93b69983cb0132121 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 15:47:25 +0100 Subject: [PATCH 59/76] Add __post_init__ in Mutation --- .../assertion/mutation_analysis/operators/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 9fe40218..62d54bb7 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -98,6 +98,17 @@ class Mutation: operator: type[MutationOperator] visitor_name: str + def __post_init__(self): + """Initialize the mutation. + + Raises: + ValueError: If the visitor is not found in the operator. + """ + if self.visitor_name not in dir(self.operator): + raise ValueError( + f"Visitor {self.visitor_name} not found in operator {self.operator}" + ) + def copy_node(node: T) -> T: """Copy a node. From 9026cfb9465d20a0a0072846dc405902ad813850 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:29:08 +0100 Subject: [PATCH 60/76] Add tests for strategies --- .../mutation_analysis/test_strategies.py | 195 ++++++++++++++++++ tests/testutils.py | 18 +- 2 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 tests/assertion/mutation_analysis/test_strategies.py diff --git a/tests/assertion/mutation_analysis/test_strategies.py b/tests/assertion/mutation_analysis/test_strategies.py new file mode 100644 index 00000000..f7ca2b31 --- /dev/null +++ b/tests/assertion/mutation_analysis/test_strategies.py @@ -0,0 +1,195 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast + +from pynguin.assertion.mutation_analysis.operators import ArithmeticOperatorDeletion +from pynguin.assertion.mutation_analysis.operators import AssignmentOperatorReplacement +from pynguin.assertion.mutation_analysis.operators import ConstantReplacement +from pynguin.assertion.mutation_analysis.operators.base import Mutation +from pynguin.assertion.mutation_analysis.strategies import BetweenOperatorsHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import EachChoiceHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import FirstToLastHOMStrategy +from pynguin.assertion.mutation_analysis.strategies import RandomHOMStrategy +from pynguin.utils.randomness import RNG +from tests.testutils import create_aor_mutation_on_substraction + + +def test_first_to_last_hom_strategy_generation(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = FirstToLastHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[2]], + [mutations[1]], + ] + + +def test_first_to_last_hom_strategy_same_node(): + node = ast.Sub(children=[]) + + mutations = [ + create_aor_mutation_on_substraction(node), + create_aor_mutation_on_substraction(node), + ] + + order = 2 + + strategy = FirstToLastHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0]], + [mutations[1]], + ] + + +def test_first_to_last_hom_strategy_child_node_generation(): + child_node = ast.Sub(children=[]) + node = ast.UnaryOp(children=[child_node]) + + mutations = [ + create_aor_mutation_on_substraction(child_node), + Mutation( + node=node, + replacement_node=ast.Name(id="foo", children=[]), + operator=ArithmeticOperatorDeletion, + visitor_name="mutate_UnaryOp", + ), + ] + + order = 2 + + strategy = FirstToLastHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0]], + [mutations[1]], + ] + + +def test_each_choice_hom_strategy_generation(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = EachChoiceHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[1]], + [mutations[2]], + ] + + +def test_between_operators_hom_strategy_generation_if_one_operator(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = BetweenOperatorsHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0]], + [mutations[1]], + ] + + +def test_between_operators_hom_strategy_generation_if_two_operators(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + Mutation( + node=ast.Sub(children=[]), + replacement_node=ast.Add(children=[]), + operator=AssignmentOperatorReplacement, + visitor_name="mutate_Sub", + ), + ] + + order = 2 + + strategy = BetweenOperatorsHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[2]], + [mutations[1], mutations[2]], + ] + + +def test_between_operators_hom_strategy_generation_if_three_operators(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + Mutation( + node=ast.Sub(children=[]), + replacement_node=ast.Add(children=[]), + operator=AssignmentOperatorReplacement, + visitor_name="mutate_Sub", + ), + Mutation( + node=ast.Constant(value=1, children=[]), + replacement_node=ast.Constant(value=2, children=[]), + operator=ConstantReplacement, + visitor_name="mutate_Constant_num", + ), + ] + + order = 2 + + strategy = BetweenOperatorsHOMStrategy(order) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[0], mutations[2]], + [mutations[1], mutations[3]], + ] + + +def test_random_hom_strategy_generation(): + mutations = [ + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + create_aor_mutation_on_substraction(), + ] + + order = 2 + + strategy = RandomHOMStrategy(order) + + RNG.seed(42) + + mutations_to_apply = list(strategy.generate(mutations)) + + assert mutations_to_apply == [ + [mutations[1], mutations[0]], + [mutations[2]], + ] diff --git a/tests/testutils.py b/tests/testutils.py index a762ce7e..345a2c00 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -12,7 +12,11 @@ from pynguin.analyses.typesystem import Instance from pynguin.analyses.typesystem import ProperType from pynguin.analyses.typesystem import TypeSystem -from pynguin.assertion.mutation_analysis.operators import MutationOperator +from pynguin.assertion.mutation_analysis.operators.arithmetic import ( + ArithmeticOperatorReplacement, +) +from pynguin.assertion.mutation_analysis.operators.base import Mutation +from pynguin.assertion.mutation_analysis.operators.base import MutationOperator from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.assertion.mutation_analysis.transformer import create_module @@ -81,3 +85,15 @@ def assert_mutation( assert ( not expected_mutants_processed_source_code ), f"Remaining mutants: {expected_mutants_processed_source_code}" + + +def create_aor_mutation_on_substraction(node: ast.Sub | None = None) -> Mutation: + if node is None: + node = ast.Sub(children=[]) + + return Mutation( + node=node, + replacement_node=ast.Add(children=[]), + operator=ArithmeticOperatorReplacement, + visitor_name="mutate_Sub", + ) From c02a7d98663d441e84e7217a191ebff4c3a85193 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 17:26:38 +0100 Subject: [PATCH 61/76] Add a test for first order mutator --- .../mutation_analysis/test_mutators.py | 50 +++++++++++++ tests/testutils.py | 75 +++++++++++++++---- 2 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 tests/assertion/mutation_analysis/test_mutators.py diff --git a/tests/assertion/mutation_analysis/test_mutators.py b/tests/assertion/mutation_analysis/test_mutators.py new file mode 100644 index 00000000..a503d505 --- /dev/null +++ b/tests/assertion/mutation_analysis/test_mutators.py @@ -0,0 +1,50 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2023 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +import ast +import inspect + +from pynguin.assertion.mutation_analysis.mutators import FirstOrderMutator +from pynguin.assertion.mutation_analysis.operators import ArithmeticOperatorReplacement +from pynguin.assertion.mutation_analysis.operators import AssignmentOperatorReplacement +from tests.testutils import assert_mutator_mutation + + +def test_first_order_mutator_generation(): + assert_mutator_mutation( + FirstOrderMutator( + [ + ArithmeticOperatorReplacement, + AssignmentOperatorReplacement, + ] + ), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z += x + y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z -= x + y + """ + ): {("mutate_Add", ast.Add, ast.Sub)}, + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z += x - y + """ + ): {("mutate_Add", ast.Add, ast.Sub)}, + }, + ) diff --git a/tests/testutils.py b/tests/testutils.py index 345a2c00..0e738a3f 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -12,6 +12,7 @@ from pynguin.analyses.typesystem import Instance from pynguin.analyses.typesystem import ProperType from pynguin.analyses.typesystem import TypeSystem +from pynguin.assertion.mutation_analysis.mutators import FirstOrderMutator from pynguin.assertion.mutation_analysis.operators.arithmetic import ( ArithmeticOperatorReplacement, ) @@ -47,13 +48,15 @@ def assert_mutation( operator: type[MutationOperator], source_code: str, expected_mutants_source_code: dict[str, tuple[str, type[ast.AST], type[ast.AST]]], -): +) -> None: module_ast = ParentNodeTransformer.create_ast(source_code) module = create_module(module_ast, "mutant") expected_mutants_processed_source_code = { - ast.unparse(ParentNodeTransformer.create_ast(mutant_source_code)): mutant_info - for mutant_source_code, mutant_info in expected_mutants_source_code.items() + ast.unparse( + ParentNodeTransformer.create_ast(expected_mutant_source_code) + ): expected_mutant_info + for expected_mutant_source_code, expected_mutant_info in expected_mutants_source_code.items() } for mutation, mutant_ast in operator.mutate(module_ast, module): @@ -68,19 +71,65 @@ def assert_mutation( mutant_source_code in expected_mutants_processed_source_code ), f"{repr(mutant_source_code)} not in {expected_mutants_processed_source_code}" - visitor_name, node_type, replacement_node_type = ( - expected_mutants_processed_source_code.pop(mutant_source_code) + expected_mutant_info = expected_mutants_processed_source_code.pop( + mutant_source_code ) + mutant_info = ( + mutation.visitor_name, + type(mutation.node), + type(mutation.replacement_node), + ) + + assert ( + expected_mutant_info == mutant_info + ), f"{expected_mutant_info} != {mutant_info}" + + assert ( + not expected_mutants_processed_source_code + ), f"Remaining mutants: {expected_mutants_processed_source_code}" + + +def assert_mutator_mutation( + mutator: FirstOrderMutator, + source_code: str, + expected_mutants_source_code: dict[ + str, set[tuple[str, type[ast.AST], type[ast.AST]]] + ], +) -> None: + module_ast = ParentNodeTransformer.create_ast(source_code) + module = create_module(module_ast, "mutant") + + expected_mutants_processed_source_code = { + ast.unparse( + ParentNodeTransformer.create_ast(expected_mutant_source_code) + ): expected_mutant_info + for expected_mutant_source_code, expected_mutant_info in expected_mutants_source_code.items() + } + + for mutations, mutant_ast in mutator.mutate(module_ast, module): + mutant_source_code = ast.unparse(mutant_ast) + + assert ( + mutant_source_code in expected_mutants_processed_source_code + ), f"{repr(mutant_source_code)} not in {expected_mutants_processed_source_code}" + + expected_mutant_info = expected_mutants_processed_source_code.pop( + mutant_source_code + ) + + mutant_info = { + ( + mutation.visitor_name, + type(mutation.node), + type(mutation.replacement_node), + ) + for mutation in mutations + } + assert ( - mutation.visitor_name == visitor_name - ), f"{mutation.visitor_name} is not {visitor_name}" - assert isinstance( - mutation.node, node_type - ), f"{mutation.node} is not {node_type}" - assert isinstance( - mutation.replacement_node, replacement_node_type - ), f"{mutation.replacement_node} is not {replacement_node_type}" + expected_mutant_info == mutant_info + ), f"{expected_mutant_info} != {mutant_info}" assert ( not expected_mutants_processed_source_code From 7760f1487f6d12317b63c8f19fc1e6193cc1b3b7 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 17:30:43 +0100 Subject: [PATCH 62/76] Improve the test of first order mutator --- tests/assertion/mutation_analysis/test_mutators.py | 4 ++-- tests/testutils.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/assertion/mutation_analysis/test_mutators.py b/tests/assertion/mutation_analysis/test_mutators.py index a503d505..aab2c497 100644 --- a/tests/assertion/mutation_analysis/test_mutators.py +++ b/tests/assertion/mutation_analysis/test_mutators.py @@ -37,7 +37,7 @@ def test_first_order_mutator_generation(): z = 0 z -= x + y """ - ): {("mutate_Add", ast.Add, ast.Sub)}, + ): {(AssignmentOperatorReplacement, "mutate_Add", ast.Add, ast.Sub)}, inspect.cleandoc( """ x = 1 @@ -45,6 +45,6 @@ def test_first_order_mutator_generation(): z = 0 z += x - y """ - ): {("mutate_Add", ast.Add, ast.Sub)}, + ): {(ArithmeticOperatorReplacement, "mutate_Add", ast.Add, ast.Sub)}, }, ) diff --git a/tests/testutils.py b/tests/testutils.py index 0e738a3f..9f1541cd 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -94,7 +94,7 @@ def assert_mutator_mutation( mutator: FirstOrderMutator, source_code: str, expected_mutants_source_code: dict[ - str, set[tuple[str, type[ast.AST], type[ast.AST]]] + str, set[tuple[type[MutationOperator], str, type[ast.AST], type[ast.AST]]] ], ) -> None: module_ast = ParentNodeTransformer.create_ast(source_code) @@ -120,6 +120,7 @@ def assert_mutator_mutation( mutant_info = { ( + mutation.operator, mutation.visitor_name, type(mutation.node), type(mutation.replacement_node), From c48e8f68e09a4ba6d7d2e1aa96e2e8eb8d421592 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 26 Mar 2024 23:25:03 +0100 Subject: [PATCH 63/76] Fix a bug with hiding variables --- .../mutation_analysis/operators/inheritance.py | 16 +++++++++------- tests/testutils.py | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py index 2ae77503..3795a01a 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/inheritance.py +++ b/src/pynguin/assertion/mutation_analysis/operators/inheritance.py @@ -120,8 +120,10 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: if not node.targets: return None - target = cast(ast.List | ast.Tuple | ast.Set, node.targets[0]) - value = cast(ast.List | ast.Tuple | ast.Set, node.value) + mutated_node = copy_node(node) + + target = cast(ast.List | ast.Tuple | ast.Set, mutated_node.targets[0]) + value = cast(ast.List | ast.Tuple | ast.Set, mutated_node.value) new_targets: list[ast.expr] = [] new_values: list[ast.expr] = [] @@ -131,7 +133,7 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: ): continue - overridden = self.is_overridden(node, target_element.id) + overridden = self.is_overridden(mutated_node, target_element.id) if overridden is None: return None @@ -146,12 +148,12 @@ def mutate_unpack(self, node: ast.Assign) -> ast.stmt | None: if not new_targets: return ast.Pass() if len(new_targets) == 1 and len(new_values) == 1: - node.targets = new_targets - node.value = new_values[0] - return node + mutated_node.targets = new_targets + mutated_node.value = new_values[0] + return mutated_node target.elts = new_targets value.elts = new_values - return node + return mutated_node def is_super_call(node: ast.FunctionDef, stmt: ast.stmt) -> bool: diff --git a/tests/testutils.py b/tests/testutils.py index 9f1541cd..7860444a 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -89,6 +89,13 @@ def assert_mutation( not expected_mutants_processed_source_code ), f"Remaining mutants: {expected_mutants_processed_source_code}" + processed_source_code = ast.unparse(module_ast) + expected_source_code = ast.unparse(ast.parse(source_code)) + + assert ( + expected_source_code == processed_source_code + ), f"Source code changed: {processed_source_code} != {expected_source_code}" + def assert_mutator_mutation( mutator: FirstOrderMutator, @@ -136,6 +143,13 @@ def assert_mutator_mutation( not expected_mutants_processed_source_code ), f"Remaining mutants: {expected_mutants_processed_source_code}" + processed_source_code = ast.unparse(module_ast) + expected_source_code = ast.unparse(ast.parse(source_code)) + + assert ( + expected_source_code == processed_source_code + ), f"Source code changed: {processed_source_code} != {expected_source_code}" + def create_aor_mutation_on_substraction(node: ast.Sub | None = None) -> Mutation: if node is None: From 3df00130cb37b6fec3b2de5eaa57f77092e4bbf8 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:04:57 +0100 Subject: [PATCH 64/76] Add tests for high order mutator --- .../mutation_analysis/test_mutators.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/assertion/mutation_analysis/test_mutators.py b/tests/assertion/mutation_analysis/test_mutators.py index aab2c497..3002cddd 100644 --- a/tests/assertion/mutation_analysis/test_mutators.py +++ b/tests/assertion/mutation_analysis/test_mutators.py @@ -8,8 +8,11 @@ import inspect from pynguin.assertion.mutation_analysis.mutators import FirstOrderMutator +from pynguin.assertion.mutation_analysis.mutators import HighOrderMutator +from pynguin.assertion.mutation_analysis.operators import ArithmeticOperatorDeletion from pynguin.assertion.mutation_analysis.operators import ArithmeticOperatorReplacement from pynguin.assertion.mutation_analysis.operators import AssignmentOperatorReplacement +from pynguin.assertion.mutation_analysis.operators import ConstantReplacement from tests.testutils import assert_mutator_mutation @@ -48,3 +51,107 @@ def test_first_order_mutator_generation(): ): {(ArithmeticOperatorReplacement, "mutate_Add", ast.Add, ast.Sub)}, }, ) + + +def test_high_order_mutator_generation(): + assert_mutator_mutation( + HighOrderMutator( + [ + ArithmeticOperatorReplacement, + AssignmentOperatorReplacement, + ] + ), + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z += x + y + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = 2 + z = 0 + z -= x - y + """ + ): { + (AssignmentOperatorReplacement, "mutate_Add", ast.Add, ast.Sub), + (ArithmeticOperatorReplacement, "mutate_Add", ast.Add, ast.Sub), + }, + }, + ) + + +def test_high_order_mutator_generation_with_same_node(): + assert_mutator_mutation( + HighOrderMutator( + [ + ArithmeticOperatorDeletion, + ArithmeticOperatorReplacement, + ] + ), + inspect.cleandoc( + """ + x = 1 + y = -x + """ + ), + { + inspect.cleandoc( + """ + x = 1 + y = x + """ + ): { + (ArithmeticOperatorDeletion, "mutate_UnaryOp", ast.UnaryOp, ast.Name), + }, + inspect.cleandoc( + """ + x = 1 + y = +x + """ + ): { + (ArithmeticOperatorReplacement, "mutate_USub", ast.USub, ast.UAdd), + }, + }, + ) + + +def test_high_order_mutator_generation_with_multiple_visitors(): + assert_mutator_mutation( + HighOrderMutator([ConstantReplacement]), + inspect.cleandoc( + """ + x = 'test' + """ + ), + { + inspect.cleandoc( + """ + x = 'mutpy' + """ + ): { + ( + ConstantReplacement, + "mutate_Constant_str", + ast.Constant, + ast.Constant, + ), + }, + inspect.cleandoc( + """ + x = '' + """ + ): { + ( + ConstantReplacement, + "mutate_Constant_str_empty", + ast.Constant, + ast.Constant, + ), + }, + }, + ) From 9ed875ad71699ef142d2ed0dacc92a2b5fa002d3 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 27 Mar 2024 00:08:15 +0100 Subject: [PATCH 65/76] Improve constant in tests --- tests/assertion/mutation_analysis/test_mutators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/assertion/mutation_analysis/test_mutators.py b/tests/assertion/mutation_analysis/test_mutators.py index 3002cddd..c10ce6dc 100644 --- a/tests/assertion/mutation_analysis/test_mutators.py +++ b/tests/assertion/mutation_analysis/test_mutators.py @@ -130,8 +130,8 @@ def test_high_order_mutator_generation_with_multiple_visitors(): ), { inspect.cleandoc( - """ - x = 'mutpy' + f""" + x = '{ConstantReplacement.FIRST_CONST_STRING}' """ ): { ( From 53467656ce9fc885140e4177ac4ceae95fcb6a2e Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:49:33 +0100 Subject: [PATCH 66/76] Remove shuffle function --- src/pynguin/assertion/mutation_analysis/strategies.py | 2 +- src/pynguin/utils/randomness.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/strategies.py b/src/pynguin/assertion/mutation_analysis/strategies.py index 6d32ec4e..4c2d4aa7 100644 --- a/src/pynguin/assertion/mutation_analysis/strategies.py +++ b/src/pynguin/assertion/mutation_analysis/strategies.py @@ -155,7 +155,7 @@ def generate( # noqa: D102 self, mutations: list[Mutation] ) -> Generator[list[Mutation], None, None]: mutations = mutations.copy() - randomness.shuffle(mutations) + randomness.RNG.shuffle(mutations) while mutations: mutations_to_apply: list[Mutation] = [] available_mutations = mutations.copy() diff --git a/src/pynguin/utils/randomness.py b/src/pynguin/utils/randomness.py index 30a358d8..a186edd5 100644 --- a/src/pynguin/utils/randomness.py +++ b/src/pynguin/utils/randomness.py @@ -189,12 +189,3 @@ def next_bytes(length: int) -> bytes: Random bytes of given length. """ return bytes(next_byte() for _ in range(length)) - - -def shuffle(ls: list) -> None: - """Shuffle the given list. - - Args: - ls: The list to shuffle. - """ - RNG.shuffle(ls) From 2c202c6c26d0afdbd397830d92c724f4422891a9 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:31:27 +0100 Subject: [PATCH 67/76] Refactor MutationController and MutationAnalysisAssertionGenerator --- src/pynguin/assertion/assertiongenerator.py | 92 +++++++++++--- .../assertion/mutation_analysis/controller.py | 120 +++++------------- src/pynguin/generator.py | 71 ++++++++++- .../mutation_analysis/test_controller.py | 33 ++++- .../test_assertion_generation_integration.py | 37 +++++- 5 files changed, 235 insertions(+), 118 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index f158a640..abd7288c 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -18,7 +18,8 @@ import pynguin.assertion.assertion as ass import pynguin.assertion.assertion_trace as at import pynguin.assertion.assertiontraceobserver as ato -import pynguin.assertion.mutation_analysis.controller as c +import pynguin.assertion.mutation_analysis.controller as ct +import pynguin.assertion.mutation_analysis.mutators as mu import pynguin.configuration as config import pynguin.ga.chromosomevisitor as cv import pynguin.testcase.execution as ex @@ -230,12 +231,52 @@ def get_score(self) -> float: return self.num_killed_mutants / divisor -class MutationAnalysisAssertionGenerator(AssertionGenerator, c.MutationController): - """Uses mutation analysis to filter out less relevant assertions.""" +class InstrumentedMutationController(ct.MutationController): + """A controller that creates instrumented mutants.""" + + def __init__( + self, + mutant_generator: mu.Mutator, + module_ast: ast.Module, + module: types.ModuleType, + tracer: ex.ExecutionTracer, + *, + testing: bool = False, + ) -> None: + """Create new controller. + + Args: + mutant_generator: The mutant generator. + module_ast: The module AST. + module: The module. + tracer: The execution tracer. + testing: Enable test mode, currently required for integration testing. + """ + super().__init__(mutant_generator, module_ast, module) + + self._tracer = tracer + + self._transformer = build_transformer( + tracer, + {config.CoverageMetric.BRANCH}, + DynamicConstantProvider(ConstantPool(), EmptyConstantProvider(), 0, 1), + ) + + # Some debug information + self._testing = testing + self._testing_created_mutants: list[str] = [] - def create_module( # noqa: D102 - self, ast_node: ast.Module, module_name: str - ) -> types.ModuleType: + @property + def tracer(self) -> ex.ExecutionTracer: + """Provides the execution tracer. + + Returns: + The execution tracer. + """ + return self._tracer + + def create_mutant(self, ast_node: ast.Module) -> types.ModuleType: # noqa: D102 + module_name = self._module.__name__ code = compile(ast_node, module_name, "exec") if self._testing: self._testing_created_mutants.append(ast.unparse(ast_node)) @@ -244,34 +285,49 @@ def create_module( # noqa: D102 exec(code, module.__dict__) # noqa: S102 return module - def __init__(self, plain_executor: ex.TestCaseExecutor, *, testing: bool = False): + +class MutationAnalysisAssertionGenerator(AssertionGenerator): + """Uses mutation analysis to filter out less relevant assertions.""" + + def __init__( + self, + plain_executor: ex.TestCaseExecutor, + mutation_controller: InstrumentedMutationController, + *, + testing: bool = False, + ): """Initializes the generator. Args: plain_executor: Executor used for plain execution + mutation_controller: Controller for mutation analysis testing: Enable test mode, currently required for integration testing. """ super().__init__(plain_executor) # We use a separate tracer and executor to execute tests on the mutants. - self._mutation_tracer = ex.ExecutionTracer() - self._mutation_tracer.current_thread_identifier = ( - threading.current_thread().ident - ) - self._mutation_executor = ex.TestCaseExecutor(self._mutation_tracer) + self._mutation_executor = ex.TestCaseExecutor(mutation_controller.tracer) self._mutation_executor.add_observer(ato.AssertionVerificationObserver()) - self._transformer = build_transformer( - self._mutation_tracer, - {config.CoverageMetric.BRANCH}, - DynamicConstantProvider(ConstantPool(), EmptyConstantProvider(), 0, 1), + mutation_controller.tracer.current_thread_identifier = ( + threading.current_thread().ident ) + self._mutated_modules = [ + module for module, _ in mutation_controller.create_mutants() + ] + # Some debug information self._testing = testing - self._testing_created_mutants: list[str] = [] self._testing_mutation_summary: _MutationSummary = _MutationSummary() - self._mutated_modules = [module for module, _ in self.mutate_module()] + @property + def mutated_modules(self) -> list[types.ModuleType]: + """Provides the mutated modules. + + Returns: + The mutated modules. + """ + return self._mutated_modules def _add_assertions(self, test_cases: list[tc.TestCase]): super()._add_assertions(test_cases) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 8c8b173e..099829c2 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -8,81 +8,59 @@ from __future__ import annotations import ast -import importlib -import inspect import logging from typing import TYPE_CHECKING -import pynguin.assertion.mutation_analysis.mutators as mu -import pynguin.assertion.mutation_analysis.operators as mo -import pynguin.assertion.mutation_analysis.strategies as ms -import pynguin.configuration as config - -from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.assertion.mutation_analysis.transformer import create_module -from pynguin.utils.exceptions import ConfigurationException if TYPE_CHECKING: - from collections.abc import Callable + import types + from types import ModuleType - from typing import ClassVar + + import pynguin.assertion.mutation_analysis.mutators as mu from pynguin.assertion.mutation_analysis.operators.base import Mutation - from pynguin.assertion.mutation_analysis.operators.base import MutationOperator _LOGGER = logging.getLogger(__name__) class MutationController: - """Adapter class for interactions with the MutPy mutation testing framework.""" - - _strategies: ClassVar[ - dict[config.MutationStrategy, Callable[[int], ms.HOMStrategy]] - ] = { - config.MutationStrategy.FIRST_TO_LAST: ms.FirstToLastHOMStrategy, - config.MutationStrategy.BETWEEN_OPERATORS: ms.BetweenOperatorsHOMStrategy, - config.MutationStrategy.RANDOM: ms.RandomHOMStrategy, - config.MutationStrategy.EACH_CHOICE: ms.EachChoiceHOMStrategy, - } + """A controller that creates mutants.""" - def mutate_module(self) -> list[tuple[ModuleType, list[Mutation]]]: - """Mutates the modules specified in the configuration. + def __init__( + self, + mutant_generator: mu.Mutator, + module_ast: ast.Module, + module: types.ModuleType, + ) -> None: + """Initialize the controller. - Returns: - A list of tuples where the first entry is the mutated module and the second - part is a list of all the mutations operators applied. + Args: + mutant_generator: The mutant generator to use. + module_ast: The AST of the module to mutate. + module: The module to mutate. """ - _LOGGER.info("Setup mutation generator") - mutant_generator = self._get_mutant_generator() - - _LOGGER.info("Import module %s", config.configuration.module_name) - target_module = importlib.import_module(config.configuration.module_name) + self._mutant_generator = mutant_generator + self._module_ast = module_ast + self._module = module - _LOGGER.info("Build AST for %s", target_module.__name__) - target_source_code = inspect.getsource(target_module) - target_ast = ParentNodeTransformer.create_ast(target_source_code) + def create_mutant(self, mutant_ast: ast.Module) -> ModuleType: + """Creates a mutant of the module. - _LOGGER.info("Mutate module %s", target_module.__name__) - mutants = self.create_mutants(mutant_generator, target_ast, target_module) - - _LOGGER.info("Generated %d mutants", len(mutants)) - return mutants + Args: + mutant_ast: The mutant AST. - def create_mutants( - self, - mutant_generator: mu.FirstOrderMutator, - target_ast: ast.Module, - target_module: ModuleType, - ) -> list[tuple[ModuleType, list[Mutation]]]: - """Creates mutants for the given module. + Returns: + The created mutant module. + """ + return create_module(mutant_ast, self._module.__name__) - Args: - mutant_generator: The mutant generator. - target_ast: The AST of the target module. - target_module: The target module. + def create_mutants(self) -> list[tuple[ModuleType, list[Mutation]]]: + """Creates mutants for the module. Returns: A list of tuples where the first entry is the mutated module and the second @@ -90,11 +68,13 @@ def create_mutants( """ mutants: list[tuple[ModuleType, list[Mutation]]] = [] - for mutations, mutant_ast in mutant_generator.mutate(target_ast, target_module): + for mutations, mutant_ast in self._mutant_generator.mutate( + self._module_ast, self._module + ): assert isinstance(mutant_ast, ast.Module) try: - mutant_module = self.create_module(mutant_ast, target_module.__name__) + mutant_module = self.create_mutant(mutant_ast) except Exception as exception: # noqa: BLE001 _LOGGER.debug("Error creating mutant: %s", exception) continue @@ -102,37 +82,3 @@ def create_mutants( mutants.append((mutant_module, mutations)) return mutants - - def create_module(self, ast_node: ast.Module, module_name: str) -> ModuleType: - """Creates a module from an AST node. - - Args: - ast_node: The AST node. - module_name: The name of the module. - - Returns: - The created module. - """ - return create_module(ast_node, module_name) - - def _get_mutant_generator(self) -> mu.FirstOrderMutator: - operators: list[type[MutationOperator]] = [ - *mo.standard_operators, - *mo.experimental_operators, - ] - - mutation_strategy = config.configuration.test_case_output.mutation_strategy - - if mutation_strategy == config.MutationStrategy.FIRST_ORDER_MUTANTS: - return mu.FirstOrderMutator(operators) - - order = config.configuration.test_case_output.mutation_order - - if order <= 0: - raise ConfigurationException("Mutation order should be > 0.") - - if mutation_strategy in self._strategies: - hom_strategy = self._strategies[mutation_strategy](order) - return mu.HighOrderMutator(operators, hom_strategy=hom_strategy) - - raise ConfigurationException("No suitable mutation strategy found.") diff --git a/src/pynguin/generator.py b/src/pynguin/generator.py index 30c65b82..50d4e44c 100644 --- a/src/pynguin/generator.py +++ b/src/pynguin/generator.py @@ -21,6 +21,7 @@ import datetime import enum import importlib +import inspect import json import logging import sys @@ -31,6 +32,9 @@ from typing import cast import pynguin.assertion.assertiongenerator as ag +import pynguin.assertion.mutation_analysis.mutators as mu +import pynguin.assertion.mutation_analysis.operators as mo +import pynguin.assertion.mutation_analysis.strategies as ms import pynguin.configuration as config import pynguin.ga.chromosome as chrom import pynguin.ga.chromosomevisitor as cv @@ -47,6 +51,7 @@ from pynguin.analyses.constants import RestrictedConstantPool from pynguin.analyses.constants import collect_static_constants from pynguin.analyses.module import generate_test_cluster +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.instrumentation.machinery import InstrumentationFinder from pynguin.instrumentation.machinery import install_import_hook from pynguin.slicer.statementslicingobserver import StatementSlicingObserver @@ -55,6 +60,7 @@ from pynguin.testcase.execution import ExecutionTracer from pynguin.testcase.execution import TestCaseExecutor from pynguin.utils import randomness +from pynguin.utils.exceptions import ConfigurationException from pynguin.utils.report import get_coverage_report from pynguin.utils.report import render_coverage_report from pynguin.utils.report import render_xml_coverage_report @@ -62,7 +68,10 @@ if TYPE_CHECKING: + from collections.abc import Callable + from pynguin.analyses.module import ModuleTestCluster + from pynguin.assertion.mutation_analysis.operators.base import MutationOperator from pynguin.ga.algorithms.generationalgorithm import GenerationAlgorithm @@ -592,14 +601,70 @@ def _minimize_assertions(generation_result: tsc.TestSuiteChromosome): ) +_strategies: dict[config.MutationStrategy, Callable[[int], ms.HOMStrategy]] = { + config.MutationStrategy.FIRST_TO_LAST: ms.FirstToLastHOMStrategy, + config.MutationStrategy.BETWEEN_OPERATORS: ms.BetweenOperatorsHOMStrategy, + config.MutationStrategy.RANDOM: ms.RandomHOMStrategy, + config.MutationStrategy.EACH_CHOICE: ms.EachChoiceHOMStrategy, +} + + +def _setup_mutant_generator() -> mu.Mutator: + operators: list[type[MutationOperator]] = [ + *mo.standard_operators, + *mo.experimental_operators, + ] + + mutation_strategy = config.configuration.test_case_output.mutation_strategy + + if mutation_strategy == config.MutationStrategy.FIRST_ORDER_MUTANTS: + return mu.FirstOrderMutator(operators) + + order = config.configuration.test_case_output.mutation_order + + if order <= 0: + raise ConfigurationException("Mutation order should be > 0.") + + if mutation_strategy in _strategies: + hom_strategy = _strategies[mutation_strategy](order) + return mu.HighOrderMutator(operators, hom_strategy=hom_strategy) + + raise ConfigurationException("No suitable mutation strategy found.") + + +def _setup_mutation_analysis_assertion_generator( + executor: TestCaseExecutor, +) -> ag.MutationAnalysisAssertionGenerator: + _LOGGER.info("Setup mutation generator") + mutant_generator = _setup_mutant_generator() + + _LOGGER.info("Import module %s", config.configuration.module_name) + module = importlib.import_module(config.configuration.module_name) + + _LOGGER.info("Build AST for %s", module.__name__) + module_source_code = inspect.getsource(module) + module_ast = ParentNodeTransformer.create_ast(module_source_code) + + _LOGGER.info("Mutate module %s", module.__name__) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module, mutation_tracer + ) + assertion_generator = ag.MutationAnalysisAssertionGenerator( + executor, mutation_controller + ) + + _LOGGER.info("Generated %d mutants", len(assertion_generator.mutated_modules)) + return assertion_generator + + def _generate_assertions(executor, generation_result): ass_gen = config.configuration.test_case_output.assertion_generation if ass_gen != config.AssertionGenerator.NONE: _LOGGER.info("Start generating assertions") + generator: cv.ChromosomeVisitor if ass_gen == config.AssertionGenerator.MUTATION_ANALYSIS: - generator: cv.ChromosomeVisitor = ag.MutationAnalysisAssertionGenerator( - executor - ) + generator = _setup_mutation_analysis_assertion_generator(executor) else: generator = ag.AssertionGenerator(executor) generation_result.accept(generator) diff --git a/tests/assertion/mutation_analysis/test_controller.py b/tests/assertion/mutation_analysis/test_controller.py index 260dc458..f18f2f58 100644 --- a/tests/assertion/mutation_analysis/test_controller.py +++ b/tests/assertion/mutation_analysis/test_controller.py @@ -4,13 +4,36 @@ # # SPDX-License-Identifier: MIT # -import pynguin.assertion.mutation_analysis.controller as c +import importlib +import inspect +import threading + +import pynguin.assertion.assertiongenerator as ag +import pynguin.assertion.mutation_analysis.mutators as mu +import pynguin.assertion.mutation_analysis.operators as mo import pynguin.configuration as config +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer +from pynguin.testcase.execution import ExecutionTracer + + +def test_create_mutants(): + mutant_generator = mu.FirstOrderMutator( + [*mo.standard_operators, *mo.experimental_operators] + ) -def test_mutate_module(): - controller = c.MutationController() - config.configuration.module_name = "tests.fixtures.examples.triangle" + module = importlib.import_module("tests.fixtures.examples.triangle") + module_source_code = inspect.getsource(module) + + module_ast = ParentNodeTransformer.create_ast(module_source_code) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module, mutation_tracer + ) config.configuration.seeding.seed = 42 - mutations = controller.mutate_module() + + mutation_controller.tracer.current_thread_identifier = ( + threading.current_thread().ident + ) + mutations = mutation_controller.create_mutants() assert len(mutations) == 14 diff --git a/tests/assertion/test_assertion_generation_integration.py b/tests/assertion/test_assertion_generation_integration.py index 20ee16d7..4516a59e 100644 --- a/tests/assertion/test_assertion_generation_integration.py +++ b/tests/assertion/test_assertion_generation_integration.py @@ -6,11 +6,14 @@ # import ast import importlib +import inspect import threading import pytest import pynguin.assertion.assertiongenerator as ag +import pynguin.assertion.mutation_analysis.mutators as mu +import pynguin.assertion.mutation_analysis.operators as mo import pynguin.configuration as config import pynguin.ga.testcasechromosome as tcc import pynguin.ga.testsuitechromosome as tsc @@ -20,6 +23,7 @@ from pynguin.analyses.constants import EmptyConstantProvider from pynguin.analyses.module import generate_test_cluster from pynguin.analyses.seeding import AstToTestCaseTransformer +from pynguin.assertion.mutation_analysis.transformer import ParentNodeTransformer from pynguin.instrumentation.machinery import install_import_hook from pynguin.testcase.execution import ExecutionTracer from pynguin.testcase.execution import TestCaseExecutor @@ -54,7 +58,8 @@ def test_generate_mutation_assertions(generator, expected_result): tracer = ExecutionTracer() tracer.current_thread_identifier = threading.current_thread().ident with install_import_hook(module_name, tracer): - importlib.reload(importlib.import_module(module_name)) + module = importlib.import_module(module_name) + importlib.reload(module) cluster = generate_test_cluster(module_name) transformer = AstToTestCaseTransformer(cluster, False, EmptyConstantProvider()) transformer.visit( @@ -73,7 +78,19 @@ def test_generate_mutation_assertions(generator, expected_result): suite = tsc.TestSuiteChromosome() suite.add_test_case_chromosome(chromosome) - gen = generator(TestCaseExecutor(tracer)) + if generator is ag.MutationAnalysisAssertionGenerator: + mutant_generator = mu.FirstOrderMutator( + [*mo.standard_operators, *mo.experimental_operators] + ) + module_source_code = inspect.getsource(module) + module_ast = ParentNodeTransformer.create_ast(module_source_code) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module, mutation_tracer + ) + gen = generator(TestCaseExecutor(tracer), mutation_controller) + else: + gen = generator(TestCaseExecutor(tracer)) suite.accept(gen) visitor = tc_to_ast.TestCaseToAstVisitor(ns.NamingScope(prefix="module"), set()) @@ -265,7 +282,8 @@ def test_mutation_analysis_integration_full( tracer = ExecutionTracer() tracer.current_thread_identifier = threading.current_thread().ident with install_import_hook(module_name, tracer): - importlib.reload(importlib.import_module(module_name)) + module = importlib.import_module(module_name) + importlib.reload(module) cluster = generate_test_cluster(module_name) transformer = AstToTestCaseTransformer(cluster, False, EmptyConstantProvider()) transformer.visit(ast.parse(test_case_str)) @@ -275,8 +293,17 @@ def test_mutation_analysis_integration_full( suite = tsc.TestSuiteChromosome() suite.add_test_case_chromosome(chromosome) + mutant_generator = mu.FirstOrderMutator( + [*mo.standard_operators, *mo.experimental_operators] + ) + module_source_code = inspect.getsource(module) + module_ast = ParentNodeTransformer.create_ast(module_source_code) + mutation_tracer = ExecutionTracer() + mutation_controller = ag.InstrumentedMutationController( + mutant_generator, module_ast, module, mutation_tracer, testing=True + ) gen = ag.MutationAnalysisAssertionGenerator( - TestCaseExecutor(tracer), testing=True + TestCaseExecutor(tracer), mutation_controller, testing=True ) suite.accept(gen) @@ -292,7 +319,7 @@ def test_mutation_analysis_integration_full( ) assert summary.get_metrics() == metrics - assert gen._testing_created_mutants == mutants + assert mutation_controller._testing_created_mutants == mutants visitor = tc_to_ast.TestCaseToAstVisitor(ns.NamingScope(prefix="module"), set()) test_case.accept(visitor) source = ast.unparse( From cb204373d684dc85b10b596bca7587cefab93bd7 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:39:52 +0100 Subject: [PATCH 68/76] Improve MutationOperator specifications --- .../assertion/mutation_analysis/operators/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pynguin/assertion/mutation_analysis/operators/base.py b/src/pynguin/assertion/mutation_analysis/operators/base.py index 62d54bb7..53609eec 100644 --- a/src/pynguin/assertion/mutation_analysis/operators/base.py +++ b/src/pynguin/assertion/mutation_analysis/operators/base.py @@ -140,6 +140,10 @@ def mutate( ) -> Generator[tuple[Mutation, ast.AST], None, None]: """Mutate a node. + This method will temporarily modify the node provided and yield itself modified + with the mutations. If you want to keep the original node while using the + generator, you should copy it before passing it to this method. + Args: node: The node to mutate. module: The module to use. @@ -179,11 +183,15 @@ def visit( ) -> Generator[tuple[ast.AST, ast.AST, ast.AST, str], None, None]: """Visit a node. + This method will temporarily modify the node provided and yield itself modified + with other information. If you want to keep the original node while using the + generator, you should copy it before passing it to this method. + Args: node: The node to visit. Yields: - A tuple containing the current node, the mutated node, and the visitor name. + A tuple (current node, replacement node, mutated node, visitor name). """ node_children = node.children # type: ignore[attr-defined] From c40987d10a29d5715f55596876b051e7d0ee0721 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:42:59 +0100 Subject: [PATCH 69/76] Rename a variable --- tests/assertion/test_assertion_generation_integration.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/assertion/test_assertion_generation_integration.py b/tests/assertion/test_assertion_generation_integration.py index 4516a59e..8b3ec055 100644 --- a/tests/assertion/test_assertion_generation_integration.py +++ b/tests/assertion/test_assertion_generation_integration.py @@ -282,8 +282,8 @@ def test_mutation_analysis_integration_full( tracer = ExecutionTracer() tracer.current_thread_identifier = threading.current_thread().ident with install_import_hook(module_name, tracer): - module = importlib.import_module(module_name) - importlib.reload(module) + module_type = importlib.import_module(module_name) + importlib.reload(module_type) cluster = generate_test_cluster(module_name) transformer = AstToTestCaseTransformer(cluster, False, EmptyConstantProvider()) transformer.visit(ast.parse(test_case_str)) @@ -296,11 +296,11 @@ def test_mutation_analysis_integration_full( mutant_generator = mu.FirstOrderMutator( [*mo.standard_operators, *mo.experimental_operators] ) - module_source_code = inspect.getsource(module) + module_source_code = inspect.getsource(module_type) module_ast = ParentNodeTransformer.create_ast(module_source_code) mutation_tracer = ExecutionTracer() mutation_controller = ag.InstrumentedMutationController( - mutant_generator, module_ast, module, mutation_tracer, testing=True + mutant_generator, module_ast, module_type, mutation_tracer, testing=True ) gen = ag.MutationAnalysisAssertionGenerator( TestCaseExecutor(tracer), mutation_controller, testing=True From a8a0d38f094a315a8005a8e44bde477d8103637e Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 23 Apr 2024 02:01:52 +0200 Subject: [PATCH 70/76] Fix current_thread_identifier --- src/pynguin/generator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pynguin/generator.py b/src/pynguin/generator.py index 50d4e44c..c99125ac 100644 --- a/src/pynguin/generator.py +++ b/src/pynguin/generator.py @@ -642,6 +642,7 @@ def _setup_mutation_analysis_assertion_generator( module = importlib.import_module(config.configuration.module_name) _LOGGER.info("Build AST for %s", module.__name__) + executor.tracer.current_thread_identifier = threading.current_thread().ident module_source_code = inspect.getsource(module) module_ast = ParentNodeTransformer.create_ast(module_source_code) From d4cf739a5f9fc0679123842dcc060af61fe8180c Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 23 Apr 2024 03:33:28 +0200 Subject: [PATCH 71/76] Replace the list by a generator in create_mutants return type --- .../assertion/mutation_analysis/controller.py | 15 +++++++-------- .../mutation_analysis/test_controller.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 099829c2..238c29a7 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -11,6 +11,7 @@ import logging from typing import TYPE_CHECKING +from typing import Generator from pynguin.assertion.mutation_analysis.transformer import create_module @@ -59,15 +60,15 @@ def create_mutant(self, mutant_ast: ast.Module) -> ModuleType: """ return create_module(mutant_ast, self._module.__name__) - def create_mutants(self) -> list[tuple[ModuleType, list[Mutation]]]: + def create_mutants( + self, + ) -> Generator[tuple[ModuleType, list[Mutation]], None, None]: """Creates mutants for the module. Returns: - A list of tuples where the first entry is the mutated module and the second - part is a list of all the mutations operators applied. + A generator of tuples where the first entry is the mutated module and the + second part is a list of all the mutations operators applied. """ - mutants: list[tuple[ModuleType, list[Mutation]]] = [] - for mutations, mutant_ast in self._mutant_generator.mutate( self._module_ast, self._module ): @@ -79,6 +80,4 @@ def create_mutants(self) -> list[tuple[ModuleType, list[Mutation]]]: _LOGGER.debug("Error creating mutant: %s", exception) continue - mutants.append((mutant_module, mutations)) - - return mutants + yield mutant_module, mutations diff --git a/tests/assertion/mutation_analysis/test_controller.py b/tests/assertion/mutation_analysis/test_controller.py index f18f2f58..861d2317 100644 --- a/tests/assertion/mutation_analysis/test_controller.py +++ b/tests/assertion/mutation_analysis/test_controller.py @@ -35,5 +35,5 @@ def test_create_mutants(): mutation_controller.tracer.current_thread_identifier = ( threading.current_thread().ident ) - mutations = mutation_controller.create_mutants() + mutations = list(mutation_controller.create_mutants()) assert len(mutations) == 14 From a8cbe96f962b36510ce0a46e793cdce7c967866e Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 23 Apr 2024 03:39:24 +0200 Subject: [PATCH 72/76] Add mutant_count method --- src/pynguin/assertion/mutation_analysis/controller.py | 10 ++++++++++ tests/assertion/mutation_analysis/test_controller.py | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 238c29a7..269f9d9a 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -81,3 +81,13 @@ def create_mutants( continue yield mutant_module, mutations + + def mutant_count(self) -> int: + """Calculates the number of mutants that can be created. + + Returns: + The number of mutants that can be created. + """ + return sum( + 1 for _ in self._mutant_generator.mutate(self._module_ast, self._module) + ) diff --git a/tests/assertion/mutation_analysis/test_controller.py b/tests/assertion/mutation_analysis/test_controller.py index 861d2317..eb838d96 100644 --- a/tests/assertion/mutation_analysis/test_controller.py +++ b/tests/assertion/mutation_analysis/test_controller.py @@ -35,5 +35,7 @@ def test_create_mutants(): mutation_controller.tracer.current_thread_identifier = ( threading.current_thread().ident ) - mutations = list(mutation_controller.create_mutants()) + mutations = tuple(mutation_controller.create_mutants()) + mutant_count = mutation_controller.mutant_count() assert len(mutations) == 14 + assert mutant_count == 14 From f8ecd146fb5e87598c94fafef8170ae76efd781c Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 23 Apr 2024 04:08:37 +0200 Subject: [PATCH 73/76] Remove mutated_modules property --- src/pynguin/assertion/assertiongenerator.py | 37 +++++++++------------ src/pynguin/generator.py | 2 +- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index abd7288c..3d76ab1b 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -309,40 +309,33 @@ def __init__( self._mutation_executor = ex.TestCaseExecutor(mutation_controller.tracer) self._mutation_executor.add_observer(ato.AssertionVerificationObserver()) - mutation_controller.tracer.current_thread_identifier = ( - threading.current_thread().ident - ) - self._mutated_modules = [ - module for module, _ in mutation_controller.create_mutants() - ] + self._mutation_controller = mutation_controller # Some debug information self._testing = testing self._testing_mutation_summary: _MutationSummary = _MutationSummary() - @property - def mutated_modules(self) -> list[types.ModuleType]: - """Provides the mutated modules. - - Returns: - The mutated modules. - """ - return self._mutated_modules - def _add_assertions(self, test_cases: list[tc.TestCase]): super()._add_assertions(test_cases) tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult]]] = [ (test, []) for test in test_cases ] + mutant_count = self._mutation_controller.mutant_count() + with self._mutation_executor.temporarily_add_observer( ato.AssertionVerificationObserver() ): - for idx, mutated_module in enumerate(self._mutated_modules): + self._mutation_controller.tracer.current_thread_identifier = ( + threading.current_thread().ident + ) + for idx, (mutated_module, _) in enumerate( + self._mutation_controller.create_mutants(), start=1 + ): self._logger.info( "Running tests on mutant %3i/%i", - idx + 1, - len(self._mutated_modules), + idx, + mutant_count, ) self._mutation_executor.module_provider.add_mutated_version( module_name=config.configuration.module_name, @@ -351,9 +344,11 @@ def _add_assertions(self, test_cases: list[tc.TestCase]): for test, results in tests_and_results: results.append(self._mutation_executor.execute(test)) - summary = self.__compute_mutation_summary( - len(self._mutated_modules), tests_and_results - ) + self._mutation_controller.tracer.current_thread_identifier = ( + threading.current_thread().ident + ) + + summary = self.__compute_mutation_summary(mutant_count, tests_and_results) self.__report_mutation_summary(summary) self.__remove_non_relevant_assertions(tests_and_results, summary) diff --git a/src/pynguin/generator.py b/src/pynguin/generator.py index c99125ac..ad56dd44 100644 --- a/src/pynguin/generator.py +++ b/src/pynguin/generator.py @@ -655,7 +655,7 @@ def _setup_mutation_analysis_assertion_generator( executor, mutation_controller ) - _LOGGER.info("Generated %d mutants", len(assertion_generator.mutated_modules)) + _LOGGER.info("Generated %d mutants", mutation_controller.mutant_count()) return assertion_generator From 8a6705d1a9a9f9e5dee3da2d95930210ed179ce1 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 23 Apr 2024 04:51:34 +0200 Subject: [PATCH 74/76] Return None when a mutant module cannot be created --- src/pynguin/assertion/assertiongenerator.py | 9 +++++++++ src/pynguin/assertion/mutation_analysis/controller.py | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index 3d76ab1b..f59a1779 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -332,6 +332,15 @@ def _add_assertions(self, test_cases: list[tc.TestCase]): for idx, (mutated_module, _) in enumerate( self._mutation_controller.create_mutants(), start=1 ): + if mutated_module is None: + self._logger.info( + "Skipping mutant %3i/%i because " + "it created an invalid module", + idx, + mutant_count, + ) + continue + self._logger.info( "Running tests on mutant %3i/%i", idx, diff --git a/src/pynguin/assertion/mutation_analysis/controller.py b/src/pynguin/assertion/mutation_analysis/controller.py index 269f9d9a..a25e8702 100644 --- a/src/pynguin/assertion/mutation_analysis/controller.py +++ b/src/pynguin/assertion/mutation_analysis/controller.py @@ -62,12 +62,13 @@ def create_mutant(self, mutant_ast: ast.Module) -> ModuleType: def create_mutants( self, - ) -> Generator[tuple[ModuleType, list[Mutation]], None, None]: + ) -> Generator[tuple[ModuleType | None, list[Mutation]], None, None]: """Creates mutants for the module. Returns: - A generator of tuples where the first entry is the mutated module and the - second part is a list of all the mutations operators applied. + A generator of tuples where the first entry is the mutated module or None + if the mutated module cannot be created and the second part is a list of + all the mutations operators applied. """ for mutations, mutant_ast in self._mutant_generator.mutate( self._module_ast, self._module @@ -78,7 +79,7 @@ def create_mutants( mutant_module = self.create_mutant(mutant_ast) except Exception as exception: # noqa: BLE001 _LOGGER.debug("Error creating mutant: %s", exception) - continue + mutant_module = None yield mutant_module, mutations From 810278519b3e480a1a3e062343c6288ec889f255 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Tue, 23 Apr 2024 05:02:09 +0200 Subject: [PATCH 75/76] Return None instead of an ExecutionResult when a mutant cannot be created --- src/pynguin/assertion/assertiongenerator.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index f59a1779..57b82ff5 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -317,7 +317,7 @@ def __init__( def _add_assertions(self, test_cases: list[tc.TestCase]): super()._add_assertions(test_cases) - tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult]]] = [ + tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult | None]]] = [ (test, []) for test in test_cases ] @@ -339,6 +339,8 @@ def _add_assertions(self, test_cases: list[tc.TestCase]): idx, mutant_count, ) + for _, results in tests_and_results: + results.append(None) continue self._logger.info( @@ -363,7 +365,7 @@ def _add_assertions(self, test_cases: list[tc.TestCase]): @staticmethod def __remove_non_relevant_assertions( - tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult]]], + tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult | None]]], mutation_summary: _MutationSummary, ) -> None: for test, results in tests_and_results: @@ -372,7 +374,7 @@ def __remove_non_relevant_assertions( results, mutation_summary.mutant_information, strict=True ): # Ignore timed out executions - if len(mut.timed_out_by) == 0: + if result is not None and len(mut.timed_out_by) == 0: merged.merge(result.assertion_verification_trace) for stmt_idx, statement in enumerate(test.statements): for assertion_idx, assertion in reversed( @@ -384,13 +386,13 @@ def __remove_non_relevant_assertions( @staticmethod def __compute_mutation_summary( number_of_mutants: int, - tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult]]], + tests_and_results: list[tuple[tc.TestCase, list[ex.ExecutionResult | None]]], ) -> _MutationSummary: mutation_info = [_MutantInfo(i) for i in range(number_of_mutants)] for test_num, (_, results) in enumerate(tests_and_results): # For each mutation, check if we had a violated assertion for info, result in zip(mutation_info, results, strict=True): - if info.timed_out_by: + if result is None or info.timed_out_by: continue if result.timeout: # Mutant caused timeout From 79319a389fbe34fcea7d47496e38b619a8c846f1 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:11:22 +0200 Subject: [PATCH 76/76] Reset tracer before creating mutant --- src/pynguin/assertion/assertiongenerator.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pynguin/assertion/assertiongenerator.py b/src/pynguin/assertion/assertiongenerator.py index e883b503..241b7362 100644 --- a/src/pynguin/assertion/assertiongenerator.py +++ b/src/pynguin/assertion/assertiongenerator.py @@ -276,6 +276,8 @@ def tracer(self) -> ex.ExecutionTracer: return self._tracer def create_mutant(self, ast_node: ast.Module) -> types.ModuleType: # noqa: D102 + self._tracer.current_thread_identifier = threading.current_thread().ident + self._tracer.reset() module_name = self._module.__name__ code = compile(ast_node, module_name, "exec") if self._testing: @@ -283,6 +285,7 @@ def create_mutant(self, ast_node: ast.Module) -> types.ModuleType: # noqa: D102 code = self._transformer.instrument_module(code) module = types.ModuleType(module_name) exec(code, module.__dict__) # noqa: S102 + self._tracer.store_import_trace() return module @@ -326,9 +329,6 @@ def _add_assertions(self, test_cases: list[tc.TestCase]): with self._mutation_executor.temporarily_add_observer( ato.AssertionVerificationObserver() ): - self._mutation_controller.tracer.current_thread_identifier = ( - threading.current_thread().ident - ) for idx, (mutated_module, _) in enumerate( self._mutation_controller.create_mutants(), start=1 ): @@ -355,10 +355,6 @@ def _add_assertions(self, test_cases: list[tc.TestCase]): for test, results in tests_and_results: results.append(self._mutation_executor.execute(test)) - self._mutation_controller.tracer.current_thread_identifier = ( - threading.current_thread().ident - ) - summary = self.__compute_mutation_summary(mutant_count, tests_and_results) self.__report_mutation_summary(summary) self.__remove_non_relevant_assertions(tests_and_results, summary)