From 72c70a19466220ea805bb57af796a3e664ad7911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikit=D0=B0=20Tsvetk=D0=BEv?= Date: Thu, 16 Jan 2025 22:48:24 +0400 Subject: [PATCH] Add TipsPlugin to provide helpful tips (#105) --- tests/plugins/tip_adviser/__init__.py | 0 tests/plugins/tip_adviser/_utils.py | 27 +++++ tests/plugins/tip_adviser/test_tip_adviser.py | 88 ++++++++++++++ vedro/_config.py | 4 + vedro/plugins/tip_adviser/__init__.py | 3 + vedro/plugins/tip_adviser/_tip_adviser.py | 107 ++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 tests/plugins/tip_adviser/__init__.py create mode 100644 tests/plugins/tip_adviser/_utils.py create mode 100644 tests/plugins/tip_adviser/test_tip_adviser.py create mode 100644 vedro/plugins/tip_adviser/__init__.py create mode 100644 vedro/plugins/tip_adviser/_tip_adviser.py diff --git a/tests/plugins/tip_adviser/__init__.py b/tests/plugins/tip_adviser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins/tip_adviser/_utils.py b/tests/plugins/tip_adviser/_utils.py new file mode 100644 index 00000000..4e5bbfcd --- /dev/null +++ b/tests/plugins/tip_adviser/_utils.py @@ -0,0 +1,27 @@ +from argparse import Namespace +from typing import Any + +import pytest + +from vedro.core import Dispatcher +from vedro.events import ArgParsedEvent +from vedro.plugins.tip_adviser import TipAdviser, TipAdviserPlugin + +__all__ = ("dispatcher", "tip_adviser", "fire_arg_parsed_event",) + + +@pytest.fixture() +def dispatcher() -> Dispatcher: + return Dispatcher() + + +@pytest.fixture() +def tip_adviser(dispatcher: Dispatcher) -> TipAdviserPlugin: + tip_adviser = TipAdviserPlugin(TipAdviser) + tip_adviser.subscribe(dispatcher) + return tip_adviser + + +async def fire_arg_parsed_event(dispatcher: Dispatcher, **kwargs: Any) -> None: + args = Namespace(**kwargs) + await dispatcher.fire(ArgParsedEvent(args)) diff --git a/tests/plugins/tip_adviser/test_tip_adviser.py b/tests/plugins/tip_adviser/test_tip_adviser.py new file mode 100644 index 00000000..eb25e3da --- /dev/null +++ b/tests/plugins/tip_adviser/test_tip_adviser.py @@ -0,0 +1,88 @@ +from unittest.mock import patch + +import pytest +from baby_steps import given, then, when + +from vedro.core import Dispatcher, Report +from vedro.events import CleanupEvent +from vedro.plugins.tip_adviser import TipAdviser, TipAdviserPlugin + +from ._utils import dispatcher, fire_arg_parsed_event, tip_adviser + +__all__ = ("dispatcher", "tip_adviser",) # fixtures + + +@pytest.mark.usefixtures(tip_adviser.__name__) +async def test_no_tips_with_default_args(*, dispatcher: Dispatcher): + with given: + await fire_arg_parsed_event(dispatcher) + + report = Report() + cleanup_event = CleanupEvent(report) + + with when: + await dispatcher.fire(cleanup_event) + + with then: + assert report.summary == [] + + +async def test_no_tips_when_plugin_disabled(*, dispatcher: Dispatcher): + with given: + class CustomTipAdviser(TipAdviser): + show_tips = False + + tip_adviser = TipAdviserPlugin(CustomTipAdviser) + tip_adviser.subscribe(dispatcher) + + await fire_arg_parsed_event(dispatcher, repeats=2) + + report = Report() + cleanup_event = CleanupEvent(report) + + with when: + await dispatcher.fire(cleanup_event) + + with then: + assert report.summary == [] + + +@pytest.mark.parametrize(("seq_idx", "tip"), [ + (0, "Tip: Consider using `--fixed-seed` for consistent results across repeated runs"), + (1, "Tip: To disable these tips, run `vedro plugin disable vedro.plugins.tip_adviser`"), +]) +@pytest.mark.usefixtures(tip_adviser.__name__) +async def test_random_tip_selection_with_repeats(seq_idx: int, tip: str, *, + dispatcher: Dispatcher): + with given: + await fire_arg_parsed_event(dispatcher, repeats=2) + + report = Report() + cleanup_event = CleanupEvent(report) + + with when, patch("random.choice", lambda seq: seq[seq_idx]): + await dispatcher.fire(cleanup_event) + + with then: + assert report.summary == [tip] + + +@pytest.mark.parametrize(("seq_idx", "tip"), [ + (0, "Tip: Consider using `--fixed-seed` for consistent results across repeated runs"), + (1, "Tip: Consider using `--fail-fast-on-repeat` to to stop after the first failing repeat"), + (2, "Tip: To disable these tips, run `vedro plugin disable vedro.plugins.tip_adviser`"), +]) +@pytest.mark.usefixtures(tip_adviser.__name__) +async def test_random_tip_selection_with_repeats_and_fail_fast(seq_idx: int, tip: str, *, + dispatcher: Dispatcher): + with given: + await fire_arg_parsed_event(dispatcher, repeats=2, fail_fast=True) + + report = Report() + cleanup_event = CleanupEvent(report) + + with when, patch("random.choice", lambda seq: seq[seq_idx]): + await dispatcher.fire(cleanup_event) + + with then: + assert report.summary == [tip] diff --git a/vedro/_config.py b/vedro/_config.py index 5ed5a58b..bdc42a9f 100644 --- a/vedro/_config.py +++ b/vedro/_config.py @@ -20,6 +20,7 @@ import vedro.plugins.tagger as tagger import vedro.plugins.temp_keeper as temp_keeper import vedro.plugins.terminator as terminator +import vedro.plugins.tip_adviser as tip_adviser from vedro.core import ( Dispatcher, Factory, @@ -192,5 +193,8 @@ class Interrupter(interrupter.Interrupter): class SystemUpgrade(system_upgrade.SystemUpgrade): enabled = True + class TipAdviser(tip_adviser.TipAdviser): + enabled = True + class Terminator(terminator.Terminator): enabled = True diff --git a/vedro/plugins/tip_adviser/__init__.py b/vedro/plugins/tip_adviser/__init__.py new file mode 100644 index 00000000..7a8aa683 --- /dev/null +++ b/vedro/plugins/tip_adviser/__init__.py @@ -0,0 +1,3 @@ +from ._tip_adviser import TipAdviser, TipAdviserPlugin + +__all__ = ("TipAdviser", "TipAdviserPlugin",) diff --git a/vedro/plugins/tip_adviser/_tip_adviser.py b/vedro/plugins/tip_adviser/_tip_adviser.py new file mode 100644 index 00000000..fa982e0d --- /dev/null +++ b/vedro/plugins/tip_adviser/_tip_adviser.py @@ -0,0 +1,107 @@ +import random +from typing import Type, final + +from vedro.core import Dispatcher, Plugin, PluginConfig +from vedro.events import ArgParsedEvent, CleanupEvent + +__all__ = ("TipAdviser", "TipAdviserPlugin",) + + +@final +class TipAdviserPlugin(Plugin): + """ + Provides tips and suggestions based on command-line arguments after test execution. + + The `TipAdviserPlugin` analyzes specific command-line arguments and offers relevant tips + to enhance the test execution experience. + + Tips are displayed in the summary report after the test run. + """ + + def __init__(self, config: Type["TipAdviser"]) -> None: + """ + Initialize the TipAdviserPlugin with the provided configuration. + + :param config: The TipAdviser configuration class. + """ + super().__init__(config) + self._show_tips: bool = config.show_tips + self._repeats: int = 1 + self._fail_fast: bool = False + self._fixed_seed: bool = False + + def subscribe(self, dispatcher: Dispatcher) -> None: + """ + Subscribe to Vedro events for parsing arguments and cleaning up after execution. + + :param dispatcher: The dispatcher to register event listeners on. + """ + dispatcher.listen(ArgParsedEvent, self.on_arg_parsed) \ + .listen(CleanupEvent, self.on_cleanup) + + def on_arg_parsed(self, event: ArgParsedEvent) -> None: + """ + Handle the event after command-line arguments are parsed. + + Extract relevant arguments and store their values for use in generating tips. + + :param event: The ArgParsedEvent instance containing parsed arguments. + """ + self._repeats = event.args.repeats if hasattr(event.args, 'repeats') else 1 + self._fail_fast = event.args.fail_fast if hasattr(event.args, 'fail_fast') else False + self._fixed_seed = event.args.fixed_seed if hasattr(event.args, 'fixed_seed') else False + + # Note: In Vedro, plugins are generally designed to operate independently and + # do not have knowledge of each other's existence. This modular design ensures + # that plugins can be used in isolation without unintended dependencies or + # interference. + # However, the `TipAdviserPlugin` is an experimental plugin that is aware of + # certain other plugins and their associated command-line arguments. + # This awareness allows it to provide helpful tips based on the presence and + # configuration of those plugins. While this behavior breaks the typical + # isolation of Vedro plugins, it is intentional to enhance the user experience + # by offering actionable suggestions tailored to the test execution context. + + def on_cleanup(self, event: CleanupEvent) -> None: + """ + Handle the cleanup event by displaying a random tip, if applicable. + + Based on the parsed arguments, generates tips for enhancing test runs. Tips are + added to the test run's summary report if the `show_tips` option is enabled. + + :param event: The CleanupEvent instance containing the test report. + """ + if not self._show_tips: + return + + tips = [] + if self._repeats > 1 and not self._fixed_seed: + tips.append( + "Consider using `--fixed-seed` for consistent results across repeated runs" + ) + if self._repeats > 1 and self._fail_fast: + tips.append( + "Consider using `--fail-fast-on-repeat` to to stop after the first failing repeat" + ) + + if len(tips) > 0: + tips.append( + "To disable these tips, run `vedro plugin disable vedro.plugins.tip_adviser`" + ) + random_tip = random.choice(tips) + event.report.add_summary(f"Tip: {random_tip}") + + +class TipAdviser(PluginConfig): + """ + Configuration class for the TipAdviserPlugin. + + Defines settings for the TipAdviserPlugin, including whether tips should be displayed + during test execution. + """ + + plugin = TipAdviserPlugin + description = "Provides random tips based on Vedro command-line arguments" + + # If True, the plugin will display tips at the end of the test run + show_tips: bool = True