Source code for pynguin.ga.computations

#  This file is part of Pynguin.
#
#  SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors
#
#  SPDX-License-Identifier: MIT
#
"""Provides classes for computations on chromosomes, e.g., fitness and coverage."""

from __future__ import annotations

import abc
import dataclasses
import math
import statistics
from abc import abstractmethod
from typing import TYPE_CHECKING, Any, TypeVar

from pynguin.instrumentation import version
from pynguin.instrumentation.tracer import ExecutionTrace
from pynguin.slicer.dynamicslicer import AssertionSlicer, DynamicSlicer

if TYPE_CHECKING:
    from collections.abc import Callable

    from pynguin.ga.testcasechromosome import TestCaseChromosome
    from pynguin.ga.testsuitechromosome import TestSuiteChromosome
    from pynguin.instrumentation.tracer import SubjectProperties
    from pynguin.slicer.dynamicslicer import SlicingCriterion, UniqueInstruction
    from pynguin.testcase.execution import AbstractTestCaseExecutor, ExecutionResult
    from pynguin.testcase.statement import Statement


[docs] @dataclasses.dataclass(eq=False) class ChromosomeComputation(abc.ABC): """An abstract computation on chromosomes.""" _executor: AbstractTestCaseExecutor """Executor that will be used by the computation to execute chromosomes."""
[docs] class TestCaseChromosomeComputation(ChromosomeComputation, abc.ABC): """A function that computes something on a test case chromosome.""" def _run_test_case_chromosome(self, individual: TestCaseChromosome) -> ExecutionResult: """Runs a test suite and updates the execution results. Updates all test cases that were changed. Args: individual: The individual to run Returns: A list of execution results """ if individual.changed or individual.get_last_execution_result() is None: individual.set_last_execution_result(self._executor.execute(individual.test_case)) individual.changed = False result = individual.get_last_execution_result() assert result is not None return result
[docs] class TestSuiteChromosomeComputation(ChromosomeComputation, abc.ABC): """A function that computes something on a test suite chromosome.""" def _run_test_suite_chromosome(self, individual: TestSuiteChromosome) -> list[ExecutionResult]: """Runs a test suite and updates the execution results. Updates all test cases that were changed. Args: individual: The individual to run Returns: A list of execution results """ test_case_chromosomes = tuple( ( test_case_chromosome, test_case_chromosome.changed or test_case_chromosome.get_last_execution_result() is None, ) for test_case_chromosome in individual.test_case_chromosomes ) changed_results_iterator = iter( self._executor.execute_multiple( test_case_chromosome.test_case for test_case_chromosome, changed in test_case_chromosomes if changed ) ) results: list[ExecutionResult] = [] for test_case_chromosome, changed in test_case_chromosomes: result: ExecutionResult | None if changed: result = next(changed_results_iterator) test_case_chromosome.set_last_execution_result(result) test_case_chromosome.changed = False # If we execute a suite which in turn executes it's test cases, # then we have to invalidate the values of the test cases, because # the test case is no longer aware that it was changed. test_case_chromosome.invalidate_cache() else: result = test_case_chromosome.get_last_execution_result() assert result is not None results.append(result) return results
[docs] class FitnessFunction: """Interface for a fitness function."""
[docs] @abstractmethod def compute_fitness(self, individual) -> float: """Calculate the fitness value. Args: individual: the chromosome to compute the fitness for. Returns: the new fitness # noqa: DAR202 """
[docs] @abstractmethod def compute_is_covered(self, individual) -> bool: """Compute if the goal of this fitness function is covered. This computation is usually cheaper than computing the fitness, because we are not interested in the distance, but only a boolean result. Args: individual: the chromosome to check coverage on. Returns: True, if the goal of this fitness function is covered. """
[docs] @abstractmethod def is_maximisation_function(self) -> bool: """Do we need to maximise or minimise this function? Returns: Whether or not this is a maximisation function # noqa: DAR202 """
[docs] class TestCaseFitnessFunction(TestCaseChromosomeComputation, FitnessFunction, abc.ABC): """Base class for test case fitness functions.""" def __init__(self, executor, code_object_id: int): # noqa: D107 super().__init__(executor) self._code_object_id = code_object_id @property def code_object_id(self) -> int: """The code object id, where the target of the fitness function is located. Returns: The code object id where the target of the fitness function is located. """ return self._code_object_id
[docs] class BranchDistanceTestCaseFitnessFunction(TestCaseFitnessFunction): """A fitness function based on branch distances and entered code objects."""
[docs] def compute_fitness(self, individual) -> float: # noqa: D102 result = self._run_test_case_chromosome(individual) merged_trace = analyze_results([result]) return compute_branch_distance_fitness(merged_trace, self._executor.subject_properties)
[docs] def compute_is_covered(self, individual) -> bool: # noqa: D102 result = self._run_test_case_chromosome(individual) merged_trace = analyze_results([result]) return compute_branch_distance_fitness_is_covered( merged_trace, self._executor.subject_properties )
[docs] def is_maximisation_function(self) -> bool: # noqa: D102 return False
[docs] class TestSuiteFitnessFunction(TestSuiteChromosomeComputation, FitnessFunction, abc.ABC): """Base class for test suite fitness functions."""
[docs] class BranchDistanceTestSuiteFitnessFunction(TestSuiteFitnessFunction): """A fitness function based on branch distances and entered code objects.""" def __init__(self, executor): # noqa: D107 super().__init__(executor) self._excluded_code_objects: set[int] = set() self._excluded_true_predicates: set[int] = set() self._excluded_false_predicates: set[int] = set()
[docs] def restrict( self, exclude_code: set[int], exclude_true: set[int], exclude_false: set[int] ) -> None: """Restrict this fitness function. Restricts the fitness function with respect to the branches/code objects it considers. Args: exclude_code: Ids of the code objects that should not be considered. exclude_true: Ids of predicates whose True branch should not be considered. exclude_false: Ids of predicates whose False branch should not be considered. """ self._excluded_code_objects.update(exclude_code) self._excluded_true_predicates.update(exclude_true) self._excluded_false_predicates.update(exclude_false)
[docs] def compute_fitness(self, individual) -> float: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return compute_branch_distance_fitness( merged_trace, self._executor.subject_properties, self._excluded_code_objects, self._excluded_true_predicates, self._excluded_false_predicates, )
[docs] def compute_is_covered(self, individual) -> bool: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return compute_branch_distance_fitness_is_covered( merged_trace, self._executor.subject_properties, self._excluded_code_objects, self._excluded_true_predicates, self._excluded_false_predicates, )
[docs] def is_maximisation_function(self) -> bool: # noqa: D102 return False
[docs] class LineTestSuiteFitnessFunction(TestSuiteFitnessFunction): """A fitness function based on lines covered and entered code objects."""
[docs] def compute_fitness(self, individual) -> float: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) existing_lines = self._executor.subject_properties.existing_lines return len(existing_lines) - len(merged_trace.covered_line_ids)
[docs] def compute_is_covered(self, individual) -> bool: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return compute_line_coverage_fitness_is_covered( merged_trace, self._executor.subject_properties, )
[docs] def is_maximisation_function(self) -> bool: # noqa: D102 return False
[docs] class StatementCheckedTestSuiteFitnessFunction(TestSuiteFitnessFunction): """A fitness function for the checked statement coverage of test suites. A fitness function based on lines included in the backward slice of each statement of a test suite. """
[docs] def compute_fitness(self, individual) -> float: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return len(self._executor.subject_properties.existing_lines) - len( merged_trace.checked_lines )
[docs] def compute_is_covered(self, individual) -> bool: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return compute_checked_coverage_statement_fitness_is_covered( merged_trace, self._executor.subject_properties, )
[docs] def is_maximisation_function(self) -> bool: # noqa: D102 return False
[docs] class CoverageFunction: """Interface for a coverage function."""
[docs] @abstractmethod def compute_coverage(self, individual) -> float: """Compute the coverage of the given individual. Args: individual: the chromosome to compute the coverage for. Returns: The computed coverage. """
[docs] class TestSuiteCoverageFunction(TestSuiteChromosomeComputation, CoverageFunction, abc.ABC): """Base class for all coverage functions that act on test suite level."""
[docs] class TestCaseCoverageFunction(TestCaseChromosomeComputation, CoverageFunction, abc.ABC): """Base class for all coverage functions that act on test case level."""
[docs] class TestSuiteBranchCoverageFunction(TestSuiteCoverageFunction): """Computes branch coverage on test suites."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return compute_branch_coverage(merged_trace, self._executor.subject_properties)
[docs] class TestCaseBranchCoverageFunction(TestCaseCoverageFunction): """Computes branch coverage on test cases."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 result = self._run_test_case_chromosome(individual) merged_trace = analyze_results([result]) return compute_branch_coverage(merged_trace, self._executor.subject_properties)
[docs] class TestSuiteLineCoverageFunction(TestSuiteCoverageFunction): """Computes line coverage on test suites."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return compute_line_coverage(merged_trace, self._executor.subject_properties)
[docs] class TestCaseLineCoverageFunction(TestCaseCoverageFunction): """Computes line coverage on test cases."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 result = self._run_test_case_chromosome(individual) merged_trace = analyze_results([result]) return compute_line_coverage(merged_trace, self._executor.subject_properties)
[docs] class TestSuiteStatementCheckedCoverageFunction(TestSuiteCoverageFunction): """Computes checked coverage on the statements of test suites."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) existing = len(self._executor.subject_properties.existing_lines) if existing == 0: # Nothing to cover => everything is covered. coverage = 1.0 else: covered = len(merged_trace.checked_lines) coverage = covered / existing assert 0.0 <= coverage <= 1.0, "Coverage must be in [0,1]" return coverage
[docs] class TestCaseStatementCheckedCoverageFunction(TestCaseCoverageFunction): """Computes checked coverage on the statements of test cases."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 result = self._run_test_case_chromosome(individual) merged_trace = analyze_results([result]) existing = len(self._executor.subject_properties.existing_lines) if existing == 0: # Nothing to cover => everything is covered. coverage = 1.0 else: covered = len(merged_trace.checked_lines) coverage = covered / existing assert 0.0 <= coverage <= 1.0, "Coverage must be in [0,1]" return coverage
[docs] class TestSuiteAssertionCheckedCoverageFunction(TestSuiteCoverageFunction): """Computes checked coverage on test suites with assertions."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 results = self._run_test_suite_chromosome(individual) merged_trace = analyze_results(results) return compute_assertion_checked_coverage(merged_trace, self._executor.subject_properties)
[docs] class TestCaseAssertionCheckedCoverageFunction(TestCaseCoverageFunction): """Computes checked coverage on test cases with assertions."""
[docs] def compute_coverage(self, individual) -> float: # noqa: D102 result = self._run_test_case_chromosome(individual) merged_trace = analyze_results([result]) return compute_assertion_checked_coverage(merged_trace, self._executor.subject_properties)
[docs] class ComputationCache: """Caches computation results and computes values on demand.""" def __init__( # noqa: D107 self, chromosome, *, fitness_functions: list[FitnessFunction] | None = None, coverage_functions: list[CoverageFunction] | None = None, fitness_cache: dict[FitnessFunction, float] | None = None, is_covered_cache: dict[FitnessFunction, bool] | None = None, coverage_cache: dict[CoverageFunction, float] | None = None, ): self._chromosome = chromosome self._fitness_functions = fitness_functions or [] self._coverage_functions = coverage_functions or [] self._fitness_cache: dict[FitnessFunction, float] = fitness_cache or {} self._is_covered_cache: dict[FitnessFunction, bool] = is_covered_cache or {} self._coverage_cache: dict[CoverageFunction, float] = coverage_cache or {}
[docs] def clone(self, new_chromosome) -> ComputationCache: """Create a deep copy of this cache. Args: new_chromosome: The chromosome with which this cache is associated. Returns: A deep copy. """ return ComputationCache( new_chromosome, fitness_functions=list(self._fitness_functions), coverage_functions=list(self._coverage_functions), fitness_cache=dict(self._fitness_cache), is_covered_cache=dict(self._is_covered_cache), coverage_cache=dict(self._coverage_cache), )
[docs] def get_fitness_functions(self) -> list[FitnessFunction]: """Provide the currently configured fitness functions of this chromosome. Returns: The list of currently configured fitness functions """ return self._fitness_functions
[docs] def add_fitness_function( self, fitness_function: FitnessFunction, ) -> None: """Adds the given fitness function. Args: fitness_function: A fitness function """ assert not fitness_function.is_maximisation_function(), ( "Currently only minimization is supported" ) self._fitness_functions.append(fitness_function)
[docs] def get_coverage_functions(self) -> list[CoverageFunction]: """Provide the currently configured coverage functions of this chromosome. Returns: The list of currently configured coverage functions. """ return self._coverage_functions
[docs] def add_coverage_function( self, coverage_function: CoverageFunction, ) -> None: """Adds a coverage function. Args: coverage_function: A fitness function """ self._coverage_functions.append(coverage_function)
T = TypeVar("T", CoverageFunction, FitnessFunction) def _check_cache( self, comp: Callable[[T | None], None], cache: dict[T, Any], funcs: list[T], only: T | None = None, ) -> None: """Check if values need to be computed. Args: comp: The function to execute, if values need to be computed. cache: The cache that should be checked. funcs: The functions that are used to fill the respective cache. only: Only compute the values for this function, optional. """ if self._chromosome.changed: # If the chromosome has changed, we invalidate all values computed so far self.invalidate_cache() # Compute those values in which we are interested. comp(only) # Mark individual as no longer changed. self._chromosome.changed = False elif len(cache) != len(funcs): # The individual has not changed, but not all values are cached. # So we might have to compute the missing ones. comp(only) def _compute_fitness(self, only: FitnessFunction | None = None): for fitness_func in self._fitness_functions if only is None else (only,): if fitness_func not in self._fitness_cache: new_value = fitness_func.compute_fitness(self._chromosome) assert ( # noqa: PT018 not math.isnan(new_value) and not math.isinf(new_value) and new_value >= 0 ), f"Invalid fitness value {new_value}" self._fitness_cache[fitness_func] = new_value # When computing a minimising fitness value, we can also determine # whether the goal is covered without calling compute_is_covered, # simply by checking if the fitness value is close enough to zero. self._is_covered_cache[fitness_func] = math.isclose(new_value, 0.0) def _compute_is_covered(self, only: FitnessFunction | None = None): for fitness_func in self._fitness_functions if only is None else (only,): if fitness_func not in self._is_covered_cache: new_value = fitness_func.compute_is_covered(self._chromosome) self._is_covered_cache[fitness_func] = new_value def _compute_coverage(self, only: CoverageFunction | None = None): for coverage_func in self._coverage_functions if only is None else (only,): if coverage_func not in self._coverage_cache: new_value = coverage_func.compute_coverage(self._chromosome) assert ( # noqa: PT018 not math.isnan(new_value) and not math.isinf(new_value) and (0 <= new_value <= 1) ), f"Invalid coverage value {new_value}" self._coverage_cache[coverage_func] = new_value
[docs] def invalidate_cache(self) -> None: """Invalidate all cached computation values.""" self._fitness_cache.clear() self._is_covered_cache.clear() self._coverage_cache.clear()
[docs] def set_fitness_values(self, fitness_values: dict[FitnessFunction, float]) -> None: """Sets the fitness values for the specific functions. Args: fitness_values: A dictionary of fitness values, keyed by fitness function. """ for fitness_key, value in fitness_values.items(): self._fitness_cache[fitness_key] = value
[docs] def get_fitness(self) -> float: """Provide a sum of the current fitness values. Returns: The sum of the current fitness values """ self._check_cache( self._compute_fitness, self._fitness_cache, self._fitness_functions, ) return sum(self._fitness_cache.values())
[docs] def get_fitness_for(self, fitness_function: FitnessFunction) -> float: """Returns the fitness values of a specific fitness function. Args: fitness_function: The fitness function Returns: Its fitness value """ self._check_cache( self._compute_fitness, self._fitness_cache, self._fitness_functions, fitness_function, ) return self._fitness_cache[fitness_function]
[docs] def get_is_covered(self, fitness_function: FitnessFunction) -> bool: """Check if the individual covers this fitness function. Args: fitness_function: The fitness function to check Returns: True, iff the individual covers the fitness function. """ self._check_cache( self._compute_is_covered, self._is_covered_cache, self._fitness_functions, fitness_function, ) return self._is_covered_cache[fitness_function]
[docs] def set_coverage_values(self, coverage_values: dict[CoverageFunction, float]) -> None: """Sets the coverage values for the specific functions. Args: coverage_values: A dictionary of coverage values, keyed by coverage function. """ for coverage_key, value in coverage_values.items(): self._coverage_cache[coverage_key] = value
[docs] def get_coverage(self) -> float: """Provides the mean coverage value. Returns: The mean coverage value """ self._check_cache( self._compute_coverage, self._coverage_cache, self._coverage_functions, ) return statistics.mean(self._coverage_cache.values())
[docs] def get_coverage_for(self, coverage_function: CoverageFunction) -> float: """Provides the coverage value for a certain coverage function. Args: coverage_function: The fitness function whose coverage value shall be returned Returns: The coverage value for the fitness function """ self._check_cache( self._compute_coverage, self._coverage_cache, self._coverage_functions, coverage_function, ) return self._coverage_cache[coverage_function]
[docs] def normalise(value: float) -> float: """Normalise a value. Args: value: The value to normalise Returns: The normalised value Raises: RuntimeError: if the value is negative """ if value < 0: raise RuntimeError("Values to normalise cannot be negative") if math.isinf(value): return 1.0 return value / (1.0 + value)
[docs] def analyze_results(results: list[ExecutionResult]) -> ExecutionTrace: """Merge the trace of the given results. Args: results: The list of execution results to analyze Returns: the merged traces. """ merged = ExecutionTrace() for result in results: trace = result.execution_trace assert trace is not None merged.merge(trace) return merged
[docs] def compute_branch_distance_fitness( trace: ExecutionTrace, subject_properties: SubjectProperties, exclude_code: set[int] | None = None, exclude_true: set[int] | None = None, exclude_false: set[int] | None = None, ) -> float: """Computes fitness based on covered branches and branch distances. Args: trace: The execution trace subject_properties: All known data exclude_code: Ids of the code objects that should not be considered. exclude_true: Ids of predicates whose True branch should not be considered. exclude_false: Ids of predicates whose False branch should not be considered. Returns: The computed fitness value """ # Handle None. Cannot use empty set as default, because of mutable default args. exclude_code = set() if exclude_code is None else exclude_code # Check if all branch-less code objects were executed. code_objects_missing: float = sum( 1.0 for code_object_id in subject_properties.branch_less_code_objects if code_object_id not in trace.executed_code_objects and code_object_id not in exclude_code ) assert code_objects_missing >= 0.0, "Amount of non covered code objects cannot be negative" # Handle None for branches. exclude_true = set() if exclude_true is None else exclude_true exclude_false = set() if exclude_false is None else exclude_false # Check if all predicates are covered predicate_fitness: float = 0.0 for predicate in subject_properties.existing_predicates: if predicate not in exclude_true: predicate_fitness += _predicate_fitness(predicate, trace.true_distances, trace) if predicate not in exclude_false: predicate_fitness += _predicate_fitness(predicate, trace.false_distances, trace) assert predicate_fitness >= 0.0, "Predicate fitness cannot be negative." return code_objects_missing + predicate_fitness
def _predicate_fitness( predicate: int, branch_distances: dict[int, float], trace: ExecutionTrace ) -> float: if predicate in branch_distances and branch_distances[predicate] == 0.0: return 0.0 if predicate in trace.executed_predicates and trace.executed_predicates[predicate] >= 2: return normalise(branch_distances[predicate]) return 1.0
[docs] def compute_branch_distance_fitness_is_covered( trace: ExecutionTrace, subject_properties: SubjectProperties, exclude_code: set[int] | None = None, exclude_true: set[int] | None = None, exclude_false: set[int] | None = None, ) -> bool: """Computes if all branches and code objects have been executed. Args: trace: The execution trace subject_properties: All known data exclude_code: Ids of the code objects that should not be considered. exclude_true: Ids of predicates whose True branch should not be considered. exclude_false: Ids of predicates whose False branch should not be considered. Returns: True, if all branches were covered """ # Handle None. Cannot use empty set as default, because of mutable default args. exclude_code = set() if exclude_code is None else exclude_code # Check if all branch-less code objects were executed. if any( code_object_id not in trace.executed_code_objects and code_object_id not in exclude_code for code_object_id in subject_properties.branch_less_code_objects ): return False # Handle None for branches. exclude_true = set() if exclude_true is None else exclude_true exclude_false = set() if exclude_false is None else exclude_false # Check if all predicates are covered for predicate in subject_properties.existing_predicates: if predicate not in exclude_true and (predicate, 0.0) not in trace.true_distances: return False if predicate not in exclude_false and (predicate, 0.0) not in trace.false_distances: return False return True
[docs] def compute_line_coverage_fitness_is_covered( trace: ExecutionTrace, subject_properties: SubjectProperties ) -> bool: """Computes if all lines and code objects have been executed. Args: trace: The execution trace subject_properties: All known data Returns: True, if all lines were covered, false otherwise """ return len(trace.covered_line_ids) == len(subject_properties.existing_lines)
[docs] def compute_checked_coverage_statement_fitness_is_covered( trace: ExecutionTrace, subject_properties: SubjectProperties ) -> bool: """Computes if all lines and code objects are checked by a return statement. Args: trace: The execution trace subject_properties: All known data Returns: True, if all lines were checked by a return, false otherwise """ return len(trace.checked_lines) == len(subject_properties.existing_lines)
[docs] def compute_branch_coverage(trace: ExecutionTrace, subject_properties: SubjectProperties) -> float: """Computes branch coverage on bytecode instructions. The resulting coverage should be equal to decision coverage on source code. Args: trace: The execution trace subject_properties: All known data Returns: The computed coverage value """ covered = len( trace.executed_code_objects.intersection(subject_properties.branch_less_code_objects) ) existing = sum(1 for _ in subject_properties.branch_less_code_objects) # Every predicate creates two branches existing += len(subject_properties.existing_predicates) * 2 # A branch is covered if it has a distance of 0.0 # Must consider both branches created by a predicate, i.e. true and false. covered += len([v for v in trace.true_distances.values() if v == 0.0]) covered += len([v for v in trace.false_distances.values() if v == 0.0]) coverage = 1.0 if existing == 0 else covered / existing assert 0.0 <= coverage <= 1.0, "Coverage must be in [0,1]" return coverage
[docs] def compute_line_coverage(trace: ExecutionTrace, subject_properties: SubjectProperties) -> float: """Computes line coverage on bytecode instructions. Args: trace: The execution trace subject_properties: All known data Returns: The computed coverage value """ existing = len(subject_properties.existing_lines) if existing == 0: # Nothing to cover => everything is covered. coverage = 1.0 else: covered = len(trace.covered_line_ids) coverage = covered / existing assert 0.0 <= coverage <= 1.0, "Coverage must be in [0,1]" return coverage
def _cleanse_included_implicit_return_none( subject_properties: SubjectProperties, statement_checked_lines: set[int], statement_slice: list[UniqueInstruction], ): # check if the last included instructions before the store # are a explicit "return None" if version.end_with_explicit_return_none(statement_slice[:-1]): statement_checked_lines.remove( DynamicSlicer.get_line_id_by_instruction( statement_slice[-version.RETURN_NONE_SIZE - 1], subject_properties, ) )
[docs] def compute_statement_checked_lines( statements: list[Statement], trace: ExecutionTrace, subject_properties: SubjectProperties, statement_slicing_criteria: dict[int, SlicingCriterion], ) -> set[int]: """Computes checked coverage on bytecode instructions. Each statement can be sliced, returning a list of instructions that are checked by the return value of the statement. If we combine all lists of instructions returned by slicing all statements, we get the combined dynamic slice of the test execution's statements. We then can map all instructions inside the slice to lines that are checked covered of the module under test. Args: statements: The sliced instructions trace: The execution trace subject_properties: All known data statement_slicing_criteria: a dictionary of statement positions and its slicing criteria Returns: The checked line ids of lines checked by the statements """ known_code_objects = subject_properties.existing_code_objects dynamic_slicer = DynamicSlicer(known_code_objects) checked_lines_ids = set() for statement in statements: if statement.get_position() not in statement_slicing_criteria: # if there is no slicing criterion there was an exception during # the test case execution and the latter statements after the one # with an exception will never be executed, # thus having no slicing criterion break statement_slice = dynamic_slicer.slice( trace, statement_slicing_criteria[statement.get_position()], ) statement_checked_lines = DynamicSlicer.map_instructions_to_lines( statement_slice, subject_properties ) _cleanse_included_implicit_return_none( subject_properties, statement_checked_lines, statement_slice, ) checked_lines_ids.update(statement_checked_lines) return checked_lines_ids
[docs] def compute_assertion_checked_coverage( trace: ExecutionTrace, subject_properties: SubjectProperties ) -> float: """Computes checked coverage on bytecode instructions. Each assertion can be sliced, returning a list of instructions that are checked by an assertion. If we combine all lists of instructions returned by slicing all assertions, we get the combined dynamic slice of the test execution's assertions. We then can map all instructions inside the slice to lines that are checked covered of the module under test. To calculate the coverage we can then divide the amount of lines checked covered through the test execution by the lines overall available in the module under test. Args: trace: The execution trace subject_properties: All known data Returns: The computed coverage value """ existing = len(subject_properties.existing_lines) if existing == 0: # Nothing to cover => everything is covered. coverage = 1.0 else: assertion_slicer = AssertionSlicer(subject_properties.existing_code_objects) checked_instructions = [] for executed_assertion in trace.executed_assertions: assertion_checked_instructions = assertion_slicer.slice_assertion( executed_assertion, trace ) executed_assertion.assertion.checked_instructions.extend(assertion_checked_instructions) # checked at any point by the assertion of a statement checked_instructions.extend(assertion_checked_instructions) # reduce coverage to lines instead of instructions checked_lines = DynamicSlicer.map_instructions_to_lines( checked_instructions, subject_properties ) covered = len(checked_lines) coverage = covered / existing assert 0.0 <= coverage <= 1.0, "Coverage must be in [0,1]" return coverage
[docs] def compare(fitness_1: float, fitness_2: float) -> int: """Compare the two specified values. Args: fitness_1: The first value to compare fitness_2: The second value to compare Returns: the value 0 if fitness_1 is equal to fitness_2; a value less than 0 if fitness_1 is less than fitness_2; and a value greater than 0 if fitness_1 is greater than fitness_2 """ if fitness_1 < fitness_2: return -1 if fitness_1 > fitness_2: return 1 return 0