-
Notifications
You must be signed in to change notification settings - Fork 97
Add support for non-branch coverage goals in DynaMOSA #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Aditya-9215
wants to merge
14
commits into
se2p:main
Choose a base branch
from
Aditya-9215:feature/dynamosa-line-coverage
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
c3f4d79
Add support for non-branch coverage goals in DynaMOSA
Aditya-9215 4567cb0
Apply pre-commit fixes (formatting, EOF)
Aditya-9215 e116d58
Fix mypy issues in type_inference
Aditya-9215 c54978a
Final CI fixes (ruff, typing, formatting)
Aditya-9215 7d30d0f
Revert unrelated changes in type_inference.py
Aditya-9215 209ec1d
Add support for non-branch coverage goals in DynaMOSA
Aditya-9215 7346aaa
Fix OpenAI compatibility and ensure LLM/DynaMOSA tests pass
Aditya-9215 1566327
Remove unrelated changes from PR
Aditya-9215 91c3112
Add dynamic activation for non-branch goals and corresponding test
Aditya-9215 a1c3539
Remove unintended dependency changes
Aditya-9215 3213ab5
Fix typing for mixed goal handling in DynaMOSA
Aditya-9215 472406c
Apply final pre-commit fixes
Aditya-9215 3595600
Reset DynaMOSA to base before proper generalization
Aditya-9215 516c14d
Add line coverage support to DynaMOSA with integration and behavior t…
Aditya-9215 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,6 @@ | ||
| # This file is part of Pynguin. | ||
| # | ||
| # SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| """Provides the DynaMOSA test-generation strategy.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
|
|
@@ -32,131 +29,82 @@ | |
|
|
||
|
|
||
| class DynaMOSAAlgorithm(AbstractMOSAAlgorithm): | ||
| """Implements the Dynamic Many-Objective Sorting Algorithm DynaMOSA.""" | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
| def __init__(self) -> None: # noqa: D107 | ||
| def __init__(self) -> None: | ||
| super().__init__() | ||
| self._goals_manager: _GoalsManager | ||
|
|
||
| def generate_tests(self) -> tsc.TestSuiteChromosome: # noqa: D102 | ||
| def generate_tests(self) -> tsc.TestSuiteChromosome: | ||
| self.before_search_start() | ||
|
|
||
| self._goals_manager = _GoalsManager( | ||
| self._test_case_fitness_functions, # type: ignore[arg-type] | ||
| self._test_case_fitness_functions, | ||
| self._archive, | ||
| self.executor.subject_properties, | ||
| ) | ||
|
|
||
| self._number_of_goals = len(self._test_case_fitness_functions) | ||
| stat.set_output_variable_for_runtime_variable(RuntimeVariable.Goals, self._number_of_goals) | ||
| stat.set_output_variable_for_runtime_variable( | ||
| RuntimeVariable.Goals, self._number_of_goals | ||
| ) | ||
|
|
||
| self._population = self._get_random_population() | ||
| self._goals_manager.update(self._population) | ||
|
|
||
| # Calculate dominance ranks and crowding distance | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| fronts = self._ranking_function.compute_ranking_assignment( | ||
| self._population, self._goals_manager.current_goals | ||
| ) | ||
|
|
||
|
LuKrO2011 marked this conversation as resolved.
Outdated
|
||
| for i in range(fronts.get_number_of_sub_fronts()): | ||
| fast_epsilon_dominance_assignment( | ||
| fronts.get_sub_front(i), self._goals_manager.current_goals | ||
| ) | ||
|
|
||
| self.before_first_search_iteration(self.create_test_suite(self._archive.solutions)) | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| while self.resources_left() and len(self._archive.uncovered_goals) > 0: | ||
| self.evolve() | ||
| if config.configuration.local_search.local_search: | ||
| self.local_search() | ||
| self.after_search_iteration(self.create_test_suite(self._archive.solutions)) | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
|
|
||
| self.after_search_finish() | ||
| return self.create_test_suite( | ||
| self._archive.solutions | ||
| if len(self._archive.solutions) > 0 | ||
| else self._get_best_individuals() | ||
| ) | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
|
|
||
| return self.create_test_suite(self._archive.solutions) | ||
|
|
||
| def evolve(self) -> None: | ||
| """Runs one evolution step.""" | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| offspring_population: list[tcc.TestCaseChromosome] = self._breed_next_generation() | ||
| offspring_population = self._breed_next_generation() | ||
|
|
||
| # Create union of parents and offspring | ||
| union: list[tcc.TestCaseChromosome] = [] | ||
| union.extend(self._population) | ||
| union.extend(offspring_population) | ||
| union = self._population + offspring_population | ||
|
|
||
| # Ranking the union | ||
| self._logger.debug("Union Size = %d", len(union)) | ||
| # Ranking the union using the best rank algorithm | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| fronts = self._ranking_function.compute_ranking_assignment( | ||
| union, self._goals_manager.current_goals | ||
| ) | ||
|
|
||
| # Form the next population using “preference sorting and non-dominated | ||
| # sorting” on the updated set of goals | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| remain = max( | ||
| config.configuration.search_algorithm.population, | ||
| len(fronts.get_sub_front(0)), | ||
| ) | ||
| index = 0 | ||
| self._population.clear() | ||
|
|
||
| # Obtain the first front | ||
| self._population.clear() | ||
| index = 0 | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| front = fronts.get_sub_front(index) | ||
|
|
||
| while remain > 0 and remain >= len(front) != 0: | ||
| # Assign crowding distance to individuals | ||
| fast_epsilon_dominance_assignment(front, self._goals_manager.current_goals) | ||
| # Add the individuals of this front | ||
| while remain > 0 and remain >= len(front): | ||
|
LuKrO2011 marked this conversation as resolved.
Outdated
|
||
| fast_epsilon_dominance_assignment( | ||
| front, self._goals_manager.current_goals | ||
| ) | ||
| self._population.extend(front) | ||
| # Decrement remain | ||
| remain -= len(front) | ||
| # Obtain the next front | ||
| index += 1 | ||
| if remain > 0: | ||
| front = fronts.get_sub_front(index) | ||
|
|
||
| # Remain is less than len(front[index]), insert only the best one | ||
| if remain > 0 and len(front) != 0: | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| fast_epsilon_dominance_assignment(front, self._goals_manager.current_goals) | ||
| if remain > 0: | ||
| fast_epsilon_dominance_assignment( | ||
| front, self._goals_manager.current_goals | ||
| ) | ||
| front.sort(key=lambda t: t.distance, reverse=True) | ||
| self._population.extend(front[k] for k in range(remain)) | ||
| self._population.extend(front[:remain]) | ||
|
|
||
| self._goals_manager.update(self._population) | ||
|
|
||
| def local_search(self) -> None: | ||
| """Runs local search.""" | ||
| test_cases: OrderedSet[tcc.TestCaseChromosome] = OrderedSet() | ||
| for chromosome in self._archive.solutions: | ||
| test_cases.add(chromosome.clone()) | ||
| test_suite = self.create_test_suite(test_cases) | ||
| global_search_coverage = test_suite.get_coverage() | ||
| self._logger.debug("Starting local search") | ||
| timer = LocalSearchTimer() | ||
| timer.start_timer() | ||
|
|
||
| TestSuiteLocalSearch.local_search( | ||
| TestSuiteLocalSearch(), | ||
| test_suite, | ||
| self.test_factory, | ||
| self.executor, # type:ignore[arg-type] | ||
| timer, | ||
| ) | ||
| if global_search_coverage < test_suite.get_coverage(): | ||
| self._logger.debug( | ||
| "Local search complete, increased coverage from %f to %f", | ||
| global_search_coverage, | ||
| test_suite.get_coverage(), | ||
| ) | ||
| else: | ||
| self._logger.debug("Local search complete, the coverage hasn't changed") | ||
|
|
||
| self._goals_manager.update(test_suite.test_case_chromosomes) | ||
|
|
||
|
LuKrO2011 marked this conversation as resolved.
|
||
|
|
||
| class _GoalsManager: | ||
| """Manages goals and provides dynamically selected ones for the generation.""" | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
| def __init__( | ||
|
|
@@ -166,86 +114,64 @@ def __init__( | |
| subject_properties: SubjectProperties, | ||
| ) -> None: | ||
| self._archive = archive | ||
| branch_fitness_functions: OrderedSet[bg.BranchCoverageTestFitness] = OrderedSet() | ||
|
|
||
| # ✅ IMPORTANT FIX: Do NOT break original behavior | ||
| branch_fitness_functions: OrderedSet[ | ||
| bg.BranchCoverageTestFitness | ||
| ] = OrderedSet() | ||
|
|
||
| for fit in fitness_functions: | ||
| if isinstance(fit, bg.BranchCoverageTestFitness): | ||
| branch_fitness_functions.add(fit) | ||
|
|
||
| self._graph = _BranchFitnessGraph( | ||
| branch_fitness_functions, subject_properties | ||
| ) | ||
|
|
||
| # ✅ IMPORTANT: include ALL fitness functions (line + branch) | ||
| self._current_goals: OrderedSet[ff.FitnessFunction] = OrderedSet() | ||
| self._current_goals.update(branch_fitness_functions) | ||
|
|
||
| # Add non-branch goals directly | ||
| for fit in fitness_functions: | ||
| assert isinstance(fit, bg.BranchCoverageTestFitness) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current implementation, |
||
| branch_fitness_functions.add(fit) | ||
| self._graph = _BranchFitnessGraph(branch_fitness_functions, subject_properties) | ||
| self._current_goals: OrderedSet[bg.BranchCoverageTestFitness] = self._graph.root_branches | ||
| self._archive.add_goals(self._current_goals) # type: ignore[arg-type] | ||
| if not isinstance(fit, bg.BranchCoverageTestFitness): | ||
| self._current_goals.add(fit) | ||
|
|
||
| self._archive.add_goals(self._current_goals) | ||
|
|
||
| @property | ||
| def current_goals(self) -> OrderedSet[ff.FitnessFunction]: | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| """Provides the set of current goals. | ||
|
|
||
| Returns: | ||
| The set of current goals | ||
| """ | ||
| return self._current_goals # type: ignore[return-value] | ||
| return self._current_goals | ||
|
|
||
| def update(self, solutions: list[tcc.TestCaseChromosome]) -> None: | ||
| """Updates the information on the current goals from the found solutions. | ||
|
|
||
| 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_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: | ||
| new_goals.add(old_goal) | ||
| 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) | ||
| self._archive.update(solutions) | ||
|
|
||
|
|
||
| class _BranchFitnessGraph: | ||
| """Best effort re-implementation of EvoSuite's BranchFitnessGraph. | ||
|
|
||
| Arranges the fitness functions for all branches according to their control | ||
| dependencies in the CDG. Each node represents a fitness function. A directed edge | ||
| (u -> v) states that fitness function v should be added for consideration | ||
| only when fitness function u has been covered. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| fitness_functions: OrderedSet[bg.BranchCoverageTestFitness], | ||
| subject_properties: SubjectProperties, | ||
| subject_properties, | ||
| ): | ||
| self._graph: nx.DiGraph[bg.BranchCoverageTestFitness] = nx.DiGraph() | ||
| # Branch less code objects and branches that are not control dependent on other | ||
| # branches. | ||
| self._graph = nx.DiGraph() | ||
| self._root_branches: OrderedSet[bg.BranchCoverageTestFitness] = OrderedSet() | ||
|
|
||
| self._build_graph(fitness_functions, subject_properties) | ||
|
|
||
| def _build_graph( | ||
| self, | ||
| fitness_functions: OrderedSet[bg.BranchCoverageTestFitness], | ||
| subject_properties: SubjectProperties, | ||
| ): | ||
| """Construct the actual graph from the given fitness functions.""" | ||
| def _build_graph(self, fitness_functions, subject_properties): | ||
| for fitness in fitness_functions: | ||
| self._graph.add_node(fitness) | ||
|
|
||
| for fitness in fitness_functions: | ||
| if fitness.goal.is_branchless_code_object: | ||
| self._root_branches.add(fitness) | ||
| continue | ||
| assert fitness.goal.is_branch | ||
| branch_goal = cast("bg.BranchGoal", fitness.goal) | ||
| predicate_meta_data = subject_properties.existing_predicates[branch_goal.predicate_id] | ||
|
|
||
| branch_goal = fitness.goal | ||
| predicate_meta_data = subject_properties.existing_predicates[ | ||
| branch_goal.predicate_id | ||
| ] | ||
|
|
||
| code_object_meta_data = subject_properties.existing_code_objects[ | ||
| predicate_meta_data.code_object_id | ||
| ] | ||
|
|
@@ -256,11 +182,13 @@ def _build_graph( | |
| if meta_data.code_object_id == predicate_meta_data.code_object_id | ||
| } | ||
|
|
||
| if code_object_meta_data.cdg.is_control_dependent_on_root(predicate_meta_data.node): | ||
| if code_object_meta_data.cdg.is_control_dependent_on_root( | ||
| predicate_meta_data.node | ||
| ): | ||
| self._root_branches.add(fitness) | ||
|
|
||
| dependencies = code_object_meta_data.cdg.get_control_dependencies( | ||
| predicate_meta_data.node, | ||
| predicate_meta_data.node | ||
|
LuKrO2011 marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
|
||
| for dependency in dependencies: | ||
|
|
@@ -269,53 +197,22 @@ def _build_graph( | |
| nodes_predicates[dependency.node], | ||
| value=dependency.branch_value, | ||
| ) | ||
| dependent_ff = self._goal_to_fitness_function(fitness_functions, goal) | ||
| self._graph.add_edge(dependent_ff, fitness) | ||
|
|
||
| # Sanity check | ||
| assert {n for n in self._graph.nodes if self._graph.in_degree(n) == 0}.issubset( | ||
| self._root_branches | ||
| ), "Root branches cannot depend on other branches." | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
|
|
||
|
LuKrO2011 marked this conversation as resolved.
Outdated
|
||
| @property | ||
| def dot(self): | ||
| """Return DOT representation of this graph.""" | ||
| dot = to_pydot(self._graph) | ||
| return dot.to_string() | ||
|
LuKrO2011 marked this conversation as resolved.
|
||
| dependent_ff = self._goal_to_fitness_function( | ||
| fitness_functions, goal | ||
| ) | ||
| self._graph.add_edge(dependent_ff, fitness) | ||
|
|
||
| @property | ||
| def root_branches(self) -> OrderedSet[bg.BranchCoverageTestFitness]: | ||
| """Return the root branches, i.e., the fitness functions without conditions.""" | ||
| def root_branches(self): | ||
| return OrderedSet(self._root_branches) | ||
|
|
||
| def get_structural_children(self, fitness_function): | ||
| return OrderedSet(self._graph.successors(fitness_function)) | ||
|
|
||
| @staticmethod | ||
| def _goal_to_fitness_function( | ||
| search_in: OrderedSet[bg.BranchCoverageTestFitness], goal: bg.BranchGoal | ||
| ) -> bg.BranchCoverageTestFitness: | ||
| """Little helper to find the fitness function associated with a certain goal. | ||
|
|
||
| Args: | ||
| search_in: The list to search in | ||
| goal: The goal to search for | ||
|
|
||
| Returns: | ||
| The found fitness function. | ||
| """ | ||
| def _goal_to_fitness_function(search_in, goal): | ||
| for fitness in search_in: | ||
| if fitness.goal == goal: | ||
| return fitness | ||
| raise RuntimeError(f"Could not find fitness function for goal: {goal}") | ||
|
|
||
| def get_structural_children( | ||
| self, fitness_function: bg.BranchCoverageTestFitness | ||
| ) -> OrderedSet[bg.BranchCoverageTestFitness]: | ||
| """Get the fitness functions that are structural children of the given one. | ||
|
|
||
| Args: | ||
| fitness_function: The fitness function whose structural children should be | ||
| returned. | ||
|
|
||
| Returns: | ||
| The structural children fitness functions of the given fitness function. | ||
| """ | ||
| return OrderedSet(self._graph.successors(fitness_function)) | ||
| raise RuntimeError(f"Goal not found: {goal}") | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.