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
43 changes: 31 additions & 12 deletions src/pynguin/ga/algorithms/dynamosaalgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,18 @@ def __init__(
) -> None:
self._archive = archive
branch_fitness_functions: OrderedSet[bg.BranchCoverageTestFitness] = OrderedSet()
other_fitness_functions: OrderedSet[ff.FitnessFunction] = OrderedSet()

for fit in fitness_functions:
assert isinstance(fit, bg.BranchCoverageTestFitness)
branch_fitness_functions.add(fit)
if isinstance(fit, bg.BranchCoverageTestFitness):
branch_fitness_functions.add(fit)
else:
other_fitness_functions.add(fit)
self._graph = _BranchFitnessGraph(branch_fitness_functions, subject_properties)
self._current_goals: OrderedSet[bg.BranchCoverageTestFitness] = self._graph.root_branches
self._current_goals: OrderedSet[ff.FitnessFunction] = OrderedSet(self._graph.root_branches)

# Add non-branch goals immediately (no delayed activation!)
self._current_goals.update(other_fitness_functions)
self._archive.add_goals(self._current_goals) # type: ignore[arg-type]

@property
Expand All @@ -189,24 +196,36 @@ def update(self, solutions: list[tcc.TestCaseChromosome]) -> None:
Args:
solutions: The previously found solutions
"""
# We must keep iterating, as long as new goals are added.
new_goals_added = True

while new_goals_added:
self._archive.update(solutions)
covered = self._archive.covered_goals
new_goals: OrderedSet[bg.BranchCoverageTestFitness] = OrderedSet()

new_goals: OrderedSet[ff.FitnessFunction] = OrderedSet()
new_goals_added = False

for old_goal in self._current_goals:
if old_goal in covered:
children = self._graph.get_structural_children(old_goal)
for child in children:
if child not in self._current_goals and child not in covered:
new_goals.add(child)
new_goals_added = True
else:
# Non-branch goals → always active
if not isinstance(old_goal, bg.BranchCoverageTestFitness):
new_goals.add(old_goal)
continue

# Branch not covered → keep it
if old_goal not in covered:
new_goals.add(old_goal)
continue

# Branch covered → activate children
for child in self._graph.get_structural_children(old_goal):
if child in self._current_goals or child in covered:
continue
new_goals.add(child)
new_goals_added = True

self._current_goals = new_goals
self._archive.add_goals(self._current_goals) # type: ignore[arg-type]

self._logger.debug("current goals after update: %s", self._current_goals)


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
53 changes: 53 additions & 0 deletions tests/ga/algorithms/test_dynamosa_line_goal_behavior.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# This file is part of Pynguin.
#
# SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors
#
# SPDX-License-Identifier: MIT

"""Integration-like test for DynaMOSA handling of non-branch fitness functions."""

from pynguin.ga.algorithms.dynamosaalgorithm import _GoalsManager
from pynguin.utils.orderedset import OrderedSet


class DummyArchive:
"""Minimal archive."""

def __init__(self):
self.covered_goals = set()
self.uncovered_goals = set()

def update(self, solutions):
pass

def add_goals(self, goals):
self.uncovered_goals = set(goals)


class DummySubject:
"""Minimal subject properties."""

existing_predicates = {}
existing_code_objects = {}


class DummyLineGoal:
"""Represents a non-branch goal."""

pass


def test_dynamosa_handles_only_non_branch_goals():
"""Ensure DynaMOSA works when only non-branch goals are present."""

archive = DummyArchive()
subject = DummySubject()

line_goal = DummyLineGoal()

goals = OrderedSet([line_goal])

manager = _GoalsManager(goals, archive, subject)

# Should include the non-branch goal
assert line_goal in manager.current_goals
37 changes: 37 additions & 0 deletions tests/ga/algorithms/test_dynamosa_line_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This file is part of Pynguin.
#
# SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors
#
# SPDX-License-Identifier: MIT

"""Integration test: DynaMOSA with line coverage."""

import pynguin.configuration as config
from pynguin.generator import run_pynguin


def test_dynamosa_with_line_coverage(tmp_path):
"""Ensure DynaMOSA works with line coverage on real code."""

config.configuration.module_name = "tests.fixtures.simple_line_target"
config.configuration.algorithm = config.Algorithm.DYNAMOSA

# ✅ ADD THIS
config.configuration.test_case_output.coverage_metrics = ["LINE", "BRANCH"]

config.configuration.statistics_output.statistics_backend = (
config.StatisticsBackend.NONE
)

config.configuration.test_case_output.output_path = tmp_path

# Run Pynguin
run_pynguin()

assert tmp_path.exists()






52 changes: 52 additions & 0 deletions tests/ga/algorithms/test_dynamosa_non_branch.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests with heavy mocking are great to test some behaviour of your code in isolation. In this case it is tested, that initially "non-branch" goals are not active, which makes sense if it is intended as it is in this case.
However, this does not test other properties of the algorithm, such as what happens once all branch goals are covered.

Even if that is also covered with a unit test with heavy mocking, it is still not tested that the behaviour is the same for non-mocked stuff. In general, using a simple non-mocked example with a real archive, real goals and a real subject is preferrable here.

Even if all of that is added, I would still not be convinced that now DynaMOSA + LineCoverage works. This must be tested with an integration test.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# This file is part of Pynguin.
#
# SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors
#
# SPDX-License-Identifier: MIT
#
"""Tests for non-branch goal handling in DynaMOSA."""

from typing import ClassVar

from pynguin.ga.algorithms.dynamosaalgorithm import _GoalsManager # noqa: PLC2701
from pynguin.utils.orderedset import OrderedSet


def test_non_branch_goals_added_after_branch_completion():
"""Ensure non-branch goals activate only after branch goals are covered."""

class DummyGoal:
"""Simple dummy goal."""

class DummyArchive:
"""Minimal archive mock."""

def __init__(self) -> None:
"""Initialize archive state."""
self.covered_goals = set()
self.uncovered_goals = set()

def update(self, solutions) -> None:
"""Mock update."""
return

def add_goals(self, goals) -> None:
"""Track uncovered goals."""
self.uncovered_goals = set(goals)

class DummySubject:
"""Minimal subject properties mock."""

existing_predicates: ClassVar[dict] = {}
existing_code_objects: ClassVar[dict] = {}

non_branch_goal = DummyGoal()

manager = _GoalsManager(
OrderedSet([non_branch_goal]),
DummyArchive(),
DummySubject(),
)

# Initially, non-branch goals should NOT be active
assert non_branch_goal not in manager.current_goals
Loading