Skip to content

Commit

Permalink
Add TipsPlugin to provide helpful tips (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsv1 authored Jan 16, 2025
1 parent 8a0479e commit 72c70a1
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 0 deletions.
Empty file.
27 changes: 27 additions & 0 deletions tests/plugins/tip_adviser/_utils.py
Original file line number Diff line number Diff line change
@@ -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))
88 changes: 88 additions & 0 deletions tests/plugins/tip_adviser/test_tip_adviser.py
Original file line number Diff line number Diff line change
@@ -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]
4 changes: 4 additions & 0 deletions vedro/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions vedro/plugins/tip_adviser/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._tip_adviser import TipAdviser, TipAdviserPlugin

__all__ = ("TipAdviser", "TipAdviserPlugin",)
107 changes: 107 additions & 0 deletions vedro/plugins/tip_adviser/_tip_adviser.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 72c70a1

Please sign in to comment.