Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/pynguin/ga/coveragegoals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
# SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors
#
# SPDX-License-Identifier: MIT
#
# SPDX-FileCopyrightText: 2026 Aditya Sinha
# SPDX-License-Identifier: MIT
Comment on lines +6 to +7
"""Provides classes for handling fitness functions for branch coverage."""

from __future__ import annotations
Expand Down Expand Up @@ -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
Expand All @@ -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))
Comment on lines +431 to +436
Comment on lines +419 to +436
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)
Expand All @@ -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."""
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions tests/fixtures/simple_line_target.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +1 to +13
63 changes: 63 additions & 0 deletions tests/ga/test_linecoverage_fitness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2026 Aditya Sinha
# SPDX-License-Identifier: MIT
Comment on lines +1 to +3

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
Comment on lines +39 to +47


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
Loading