Skip to content
Draft
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
4 changes: 4 additions & 0 deletions src/pynguin/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ class TestCaseOutputConfiguration:
Only works when running in a subprocess. If set to "", the are stored in the
output_path."""

store_best_population: bool = True
"""Whether to store and update the current best population as pytest in the
output_path during the search."""

export_strategy: ExportStrategy = ExportStrategy.PY_TEST
"""The export strategy determines for which test-runner system the
generated tests should fit."""
Expand Down
6 changes: 6 additions & 0 deletions src/pynguin/ga/generationalgorithmfactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,12 @@ def get_search_algorithm(self) -> GenerationAlgorithm:
if stop.observes_execution:
self._executor.add_observer(stop)
strategy.add_search_observer(so.LogSearchObserver())

if config.configuration.test_case_output.store_best_population:
strategy.add_search_observer(
so.BestPopulationObserver(config.configuration.test_case_output.output_path)
)

strategy.add_search_observer(sso.SequenceStartTimeObserver())
strategy.add_search_observer(sso.IterationObserver())
strategy.add_search_observer(sso.BestIndividualObserver())
Expand Down
67 changes: 67 additions & 0 deletions src/pynguin/ga/searchobserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@

from abc import ABC
from abc import abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING

from pynguin.testcase import export


if TYPE_CHECKING:
import pynguin.ga.testsuitechromosome as tsc

BEST_POPULATION_FILE_NAME = "best_population.py"


class SearchObserver(ABC):
"""Observes the execution of a search algorithm."""
Expand Down Expand Up @@ -85,3 +90,65 @@ def after_search_iteration( # noqa: D102

def after_search_finish(self) -> None:
"""Not used."""


class BestPopulationObserver(SearchObserver):
"""Observes the search and stores the best individuals in a file."""

_logger = logging.getLogger(__name__)

def __init__(self, store_path: str):
"""Initialize the observer with the path to store the best population.

Args:
store_path: Path to the file where the best individuals will be stored.
"""
self.store_path = Path(store_path + "/" + BEST_POPULATION_FILE_NAME)
self.best_coverage = -1.0
self.iteration = 0
self.store_path.parent.mkdir(parents=True, exist_ok=True)

def before_search_start(self, start_time_ns: int) -> None: # noqa: D102
self.iteration = 0
self.best_coverage = -1.0
self._logger.debug("Best population observer started, storing to: %s", self.store_path)

def before_first_search_iteration(self, initial: tsc.TestSuiteChromosome) -> None: # noqa: D102
current_coverage = initial.get_coverage()
if current_coverage > self.best_coverage:
self.best_coverage = current_coverage
self._store_best_population(initial)

def after_search_iteration(self, best: tsc.TestSuiteChromosome) -> None: # noqa: D102
self.iteration += 1
current_coverage = best.get_coverage()

# Only store if this is better than our current best
if current_coverage > self.best_coverage:
self.best_coverage = current_coverage
self._store_best_population(best)
self._logger.debug(
"New best individual found at iteration %d with coverage %5f, stored to %s",
self.iteration,
current_coverage,
self.store_path,
)

def after_search_finish(self) -> None: # noqa: D102
self._logger.debug("Best population observer finished")

def _store_best_population(self, chromosome: tsc.TestSuiteChromosome) -> None:
"""Store the best population to the file.

Args:
chromosome: The chromosome containing the best individuals.
"""
try:
export_visitor = export.PyTestChromosomeToAstVisitor(store_call_return=False)
chromosome.accept(export_visitor)
export.save_module_to_file(
export_visitor.to_module(), self.store_path, format_with_black=False
)

except Exception as e: # noqa: BLE001
self._logger.warning("Failed to store best population: %s", e)
1 change: 1 addition & 0 deletions tests/ga/algorithms/test_randomalgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def _combine_current_individual(*_):

config.configuration.stopping.maximum_search_time = 1
config.configuration.algorithm = config.Algorithm.RANDOM
config.configuration.test_case_output.store_best_population = False
logger = MagicMock(Logger)
algorithm = gaf.TestSuiteGenerationAlgorithmFactory(
executor, MagicMock(ModuleTestCluster)
Expand Down
7 changes: 5 additions & 2 deletions tests/utils/test_configuration_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def expected_toml(tmp_path):
[test_case_output]
output_path = ""
crash_path = ""
store_best_population = true
export_strategy = "PY_TEST"
max_length_test_case = 2500
assertion_generation = "MUTATION_ANALYSIS"
Expand Down Expand Up @@ -220,8 +221,8 @@ def expected_txt(tmp_path):
expected_txt = Path(tmp_path) / f"expected-{PYNGUIN_CONFIG_TXT}"
expected = """("Configuration(project_path='', module_name='', "
"test_case_output=TestCaseOutputConfiguration(output_path='', crash_path='', "
"export_strategy=<ExportStrategy.PY_TEST: 'PY_TEST'>, "
'max_length_test_case=2500, '
'store_best_population=True, export_strategy=<ExportStrategy.PY_TEST: '
"'PY_TEST'>, max_length_test_case=2500, "
'assertion_generation=<AssertionGenerator.MUTATION_ANALYSIS: '
"'MUTATION_ANALYSIS'>, allow_stale_assertions=False, "
'mutation_strategy=<MutationStrategy.FIRST_ORDER_MUTANTS: '
Expand Down Expand Up @@ -532,6 +533,8 @@ def expected_parameters() -> str:
FIRST_ORDER_MUTANTS
--test_case_output.post_process
True
--test_case_output.store_best_population
True
--test_creation.any_weight
0
--test_creation.bytes_length
Expand Down