Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
29 changes: 25 additions & 4 deletions src/pynguin/ga/algorithms/dynamosaalgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,22 @@ def __init__(
) -> None:
self._archive = archive
branch_fitness_functions: OrderedSet[bg.BranchCoverageTestFitness] = OrderedSet()
non_branch_fitness_functions: OrderedSet[ff.FitnessFunction] = OrderedSet()

for fit in fitness_functions:
assert isinstance(fit, bg.BranchCoverageTestFitness)
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.

In the current implementation, DynaMOSA will not work if one fitness_functions is not BranchCoverageTestFitness. If this stays like this, we should still ensure that at least one of the fitness_functions is a BranchCoverageTestFitness.

branch_fitness_functions.add(fit)
if isinstance(fit, bg.BranchCoverageTestFitness):
branch_fitness_functions.add(fit)
else:
non_branch_fitness_functions.add(fit)

self._graph = _BranchFitnessGraph(branch_fitness_functions, subject_properties)
self._current_goals: OrderedSet[bg.BranchCoverageTestFitness] = self._graph.root_branches

# Start with branch root goals
self._current_goals: OrderedSet[ff.FitnessFunction] = OrderedSet(self._graph.root_branches)

# Store non-branch goals separately (DO NOT activate yet)
self._non_branch_goals: OrderedSet[ff.FitnessFunction] = non_branch_fitness_functions
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.

The name might cause confusion with CodeObjectGoals which are also "non-branch" goals but might belong to the self._current_goals.
If the logic stays like this, I am fine with this naming, but add a respective comment on what's the difference.


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

@property
Expand All @@ -194,7 +205,7 @@ def update(self, solutions: list[tcc.TestCaseChromosome]) -> None:
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:
Expand All @@ -208,6 +219,16 @@ def update(self, solutions: list[tcc.TestCaseChromosome]) -> None:
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)
# Add non-branch goals ONLY after all branch goals are covered
if len(self._archive.uncovered_goals) == 0:
added = False
for goal in self._non_branch_goals:
if goal not in self._current_goals:
self._current_goals.add(goal)
added = True

if added:
self._archive.add_goals(self._current_goals) # type: ignore[arg-type]
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.

For simplicity, let's assume the second FitnessFunction is a LineCoverageTestFitness function.
First, such goals should probably be encoded as LineCoverageGoals.
Second, we might already be able to cover such goals before all branches are covered.



class _BranchFitnessGraph:
Expand Down
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