From 42a28a4942facaa764b480bd5cc37796d715d467 Mon Sep 17 00:00:00 2001 From: Aditya Snha Date: Thu, 16 Apr 2026 16:51:37 +0530 Subject: [PATCH] Make LineCoverageTestFitness non-binary and add tests --- src/pynguin/ga/coveragegoals.py | 56 ++++++++++++++++++++++-- tests/fixtures/simple_line_target.py | 13 ++++++ tests/ga/test_linecoverage_fitness.py | 63 +++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/simple_line_target.py create mode 100644 tests/ga/test_linecoverage_fitness.py diff --git a/src/pynguin/ga/coveragegoals.py b/src/pynguin/ga/coveragegoals.py index 7772e0f7e..507a709f2 100644 --- a/src/pynguin/ga/coveragegoals.py +++ b/src/pynguin/ga/coveragegoals.py @@ -3,7 +3,8 @@ # SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors # # SPDX-License-Identifier: MIT -# +# SPDX-FileCopyrightText: 2026 Aditya Sinha +# SPDX-License-Identifier: MIT """Provides classes for handling fitness functions for branch coverage.""" from __future__ import annotations @@ -393,7 +394,12 @@ def goal(self) -> AbstractBranchCoverageGoal: class LineCoverageTestFitness(ff.TestCaseFitnessFunction): - """A statement coverage fitness implementation for test cases.""" + """A line coverage fitness implementation for test cases. + + This implementation is non-binary: + - 0.0 if the line is covered + - >0.0 if not covered, based on an approximation of control-flow distance + """ def __init__( # noqa: D107 self, executor: AbstractTestCaseExecutor, goal: LineCoverageGoal @@ -404,7 +410,33 @@ def __init__( # noqa: D107 def compute_fitness( # noqa: D102 self, individual: tcc.TestCaseChromosome ) -> float: - return 0 if self.compute_is_covered(individual) else 1 + result = self._run_test_case_chromosome(individual) + + # If covered → optimal fitness + if self._goal.is_covered(result): + return 0.0 + + # Otherwise → return non-binary distance + return 1.0 + self._approximate_distance(result) + + def _approximate_distance(self, result: ExecutionResult) -> float: + """Estimate how far execution was from reaching this line. + + This is a lightweight approximation based on execution trace information. + It provides a non-binary signal without requiring full control-flow analysis. + """ + try: + trace = result.execution_trace + + # If nothing executed → maximum penalty + if not trace.executed_code_objects: + return 1.0 + + # Use number of executed code objects as rough distance signal + return float(len(trace.executed_code_objects)) + except (AttributeError, TypeError): + # Fallback to safe non-zero value + return 1.0 def compute_is_covered(self, individual) -> bool: # noqa: D102 result = self._run_test_case_chromosome(individual) @@ -419,6 +451,15 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"LineCoverageTestFitness(executor={self._executor}, goal={self._goal})" + @property + def goal(self) -> LineCoverageGoal: + """Provides the line-coverage goal of this fitness function. + + Returns: + The attached line-coverage goal + """ + return self._goal + class StatementCheckedCoverageTestFitness(ff.TestCaseFitnessFunction): """A statement checked coverage fitness implementation for test cases.""" @@ -447,6 +488,15 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"CheckedCoverageTestFitness(executor={self._executor}, goal={self._goal})" + @property + def goal(self) -> CheckedCoverageGoal: + """Provides the checked-coverage goal of this fitness function. + + Returns: + The attached checked-coverage goal + """ + return self._goal + def create_branch_coverage_fitness_functions( executor: AbstractTestCaseExecutor, branch_goal_pool: BranchGoalPool diff --git a/tests/fixtures/simple_line_target.py b/tests/fixtures/simple_line_target.py new file mode 100644 index 000000000..784c65d8b --- /dev/null +++ b/tests/fixtures/simple_line_target.py @@ -0,0 +1,13 @@ +def foo(x: int) -> int: + if x > 0: + return x + 1 + elif x == 0: + return 0 + else: + return x - 1 + + +def bar(y: int) -> int: + if y % 2 == 0: + return y * 2 + return y + 3 diff --git a/tests/ga/test_linecoverage_fitness.py b/tests/ga/test_linecoverage_fitness.py new file mode 100644 index 000000000..dafbf9054 --- /dev/null +++ b/tests/ga/test_linecoverage_fitness.py @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2026 Aditya Sinha +# SPDX-License-Identifier: MIT + +import pynguin.ga.coveragegoals as bg + + +class DummyExecutor: + def __init__(self): + self.subject_properties = None + + def execute(self, test_case): + return test_case # passthrough + + +class DummyChromosome: + def __init__(self, execution_result): + self._execution_result = execution_result + + +class DummyExecutionResult: + def __init__(self, covered_lines=None, executed_code_objects=None): + self.execution_trace = type( + "Trace", + (), + { + "covered_line_ids": covered_lines or [], + "executed_code_objects": executed_code_objects or [], + }, + )() + + +def test_line_fitness_non_binary_when_not_covered(): + executor = DummyExecutor() + goal = bg.LineCoverageGoal(0, 1) + + fitness = bg.LineCoverageTestFitness(executor, goal) + + execution = DummyExecutionResult(covered_lines=[], executed_code_objects=[1, 2]) + chromosome = DummyChromosome(execution) + + # Monkey patch run method + fitness._run_test_case_chromosome = lambda _: execution + + value = fitness.compute_fitness(chromosome) + + assert value > 0.0 + + +def test_line_fitness_zero_when_covered(): + executor = DummyExecutor() + goal = bg.LineCoverageGoal(0, 1) + + fitness = bg.LineCoverageTestFitness(executor, goal) + + execution = DummyExecutionResult(covered_lines=[1], executed_code_objects=[1]) + chromosome = DummyChromosome(execution) + + fitness._run_test_case_chromosome = lambda _: execution + + value = fitness.compute_fitness(chromosome) + + assert value == 0.0