# 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
from abc import abstractmethod
from typing import TYPE_CHECKING, Any
from pynguin.ga.checked_coverage import (
compute_assertion_checked_coverage,
compute_assertion_checked_lines,
)
from pynguin.ga.fitness_metrics import (
analyze_results,
compute_branch_coverage,
compute_branch_covered_goals,
compute_branch_distance_fitness,
compute_branch_distance_fitness_is_covered,
compute_checked_coverage_statement_fitness_is_covered,
compute_line_coverage,
compute_line_coverage_fitness_is_covered,
compute_line_covered_goals,
)
if TYPE_CHECKING:
from pynguin.ga.testcasechromosome import TestCaseChromosome
from pynguin.ga.testsuitechromosome import TestSuiteChromosome
from pynguin.testcase.execution import AbstractTestCaseExecutor, ExecutionResult
[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.
Raises:
RuntimeError: If there are no subject properties to compute coverage for.
"""
[docs]
@abstractmethod
def compute_covered_goals(self, individual) -> set[Any]:
"""Compute the set of covered goals of the given individual.
Args:
individual: the chromosome to compute covered goals for.
Returns:
The set of covered goals.
"""
[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]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
results = self._run_test_suite_chromosome(individual)
merged_trace = analyze_results(results)
return compute_branch_covered_goals(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]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
result = self._run_test_case_chromosome(individual)
merged_trace = analyze_results([result])
return compute_branch_covered_goals(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]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
results = self._run_test_suite_chromosome(individual)
merged_trace = analyze_results(results)
return compute_line_covered_goals(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]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
result = self._run_test_case_chromosome(individual)
merged_trace = analyze_results([result])
return compute_line_covered_goals(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:
raise RuntimeError("No subject properties found to compute coverage.")
covered = len(merged_trace.checked_lines)
coverage = covered / existing
assert 0.0 <= coverage <= 1.0, "Coverage must be in [0,1]"
return coverage
[docs]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
results = self._run_test_suite_chromosome(individual)
merged_trace = analyze_results(results)
return set(merged_trace.checked_lines)
[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:
raise RuntimeError("No subject properties found to compute coverage.")
covered = len(merged_trace.checked_lines)
coverage = covered / existing
assert 0.0 <= coverage <= 1.0, "Coverage must be in [0,1]"
return coverage
[docs]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
result = self._run_test_case_chromosome(individual)
merged_trace = analyze_results([result])
return set(merged_trace.checked_lines)
[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]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
results = self._run_test_suite_chromosome(individual)
merged_trace = analyze_results(results)
return compute_assertion_checked_lines(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]
def compute_covered_goals(self, individual) -> set[Any]: # noqa: D102
result = self._run_test_case_chromosome(individual)
merged_trace = analyze_results([result])
return compute_assertion_checked_lines(merged_trace, self._executor.subject_properties)