diff --git a/src/pynguin/ga/algorithms/dynamosaalgorithm.py b/src/pynguin/ga/algorithms/dynamosaalgorithm.py index 63c6f926d..caf7313d2 100644 --- a/src/pynguin/ga/algorithms/dynamosaalgorithm.py +++ b/src/pynguin/ga/algorithms/dynamosaalgorithm.py @@ -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 @@ -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) diff --git a/tests/fixtures/simple_line_target.py b/tests/fixtures/simple_line_target.py new file mode 100644 index 000000000..8f6050aaf --- /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 \ No newline at end of file diff --git a/tests/ga/algorithms/test_dynamosa_line_goal_behavior.py b/tests/ga/algorithms/test_dynamosa_line_goal_behavior.py new file mode 100644 index 000000000..23dceaab3 --- /dev/null +++ b/tests/ga/algorithms/test_dynamosa_line_goal_behavior.py @@ -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 \ No newline at end of file diff --git a/tests/ga/algorithms/test_dynamosa_line_integration.py b/tests/ga/algorithms/test_dynamosa_line_integration.py new file mode 100644 index 000000000..4046f20f8 --- /dev/null +++ b/tests/ga/algorithms/test_dynamosa_line_integration.py @@ -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() + + + + + + diff --git a/tests/ga/algorithms/test_dynamosa_non_branch.py b/tests/ga/algorithms/test_dynamosa_non_branch.py new file mode 100644 index 000000000..01d141cad --- /dev/null +++ b/tests/ga/algorithms/test_dynamosa_non_branch.py @@ -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