From ef65fbefed450a9d83b91c1f015e5ad1792bd44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Prchl=C3=ADk?= Date: Fri, 28 Jul 2023 13:30:24 +0200 Subject: [PATCH] Add callback to adjust() to make it observable by callers Whenever a rule is inspected, the given callback is called. This gives `adjust()` callers a chance to observe which rules are applied to which fmf nodes. --- fmf/base.py | 39 +++++++++++++++++++++++++++++++++++++-- tests/unit/test_adjust.py | 17 +++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/fmf/base.py b/fmf/base.py index c9c7a454..fafd3458 100644 --- a/fmf/base.py +++ b/fmf/base.py @@ -6,6 +6,7 @@ import subprocess from io import open from pprint import pformat as pretty +from typing import Any, Dict, Optional, Protocol import jsonschema from ruamel.yaml import YAML @@ -29,6 +30,25 @@ # Metadata # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +class AdjustCallback(Protocol): + """ + A callback for per-rule notifications made by Tree.adjust() + + Function be be called for every rule inspected by adjust(). + It will be given three arguments: fmf tree being inspected, + current adjust rule, and whether the rule was skipped (``None``), + applied (``True``) or not applied (``False``). + """ + + def __call__( + self, + node: 'Tree', + rule: Dict[str, Any], + applied: Optional[bool]) -> None: + pass + + class Tree: """ Metadata Tree """ @@ -320,7 +340,7 @@ def update(self, data): log.debug("Data for '{0}' updated.".format(self)) log.data(pretty(self.data)) - def adjust(self, context, key='adjust', undecided='skip'): + def adjust(self, context, key='adjust', undecided='skip', decision_callback=None): """ Adjust tree data based on provided context and rules @@ -334,6 +354,10 @@ class describing the environment context. By default, the key context dimension is not defined. By default, such rules are skipped. In order to raise the fmf.context.CannotDecide exception in such cases use undecided='raise'. + + Optional 'decision_callback' callback would be called for every adjust + rule inspected, with three arguments: current fmf node, current + adjust rule, and whether it was applied or not. """ # Check context sanity @@ -363,6 +387,8 @@ class describing the environment context. By default, the key if not isinstance(rule, dict): raise utils.FormatError("Adjust rule should be a dictionary.") + original_rule = rule.copy() + # Missing 'when' means always enabled rule try: condition = rule.pop('when') @@ -382,13 +408,22 @@ class describing the environment context. By default, the key # Apply remaining rule attributes if context matches try: if context.matches(condition): + if decision_callback: + decision_callback(self, original_rule, True) + self._merge_special(self.data, rule) # First matching rule wins, skip the rest unless continue if not continue_: break + else: + if decision_callback: + decision_callback(self, original_rule, False) # Handle undecided rules as requested except fmf.context.CannotDecide: + if decision_callback: + decision_callback(self, original_rule, None) + if undecided == 'skip': continue elif undecided == 'raise': @@ -400,7 +435,7 @@ class describing the environment context. By default, the key # Adjust all child nodes as well for child in self.children.values(): - child.adjust(context, key, undecided) + child.adjust(context, key, undecided, decision_callback=decision_callback) def get(self, name=None, default=None): """ diff --git a/tests/unit/test_adjust.py b/tests/unit/test_adjust.py index e3f8ae8b..caf24e9a 100644 --- a/tests/unit/test_adjust.py +++ b/tests/unit/test_adjust.py @@ -1,4 +1,5 @@ import copy +from unittest.mock import MagicMock import pytest from ruamel.yaml import YAML @@ -204,3 +205,19 @@ def test_continue_default(self, full, fedora): full.adjust(fedora) extend = full.find('/extend') assert extend.get('require') == ['one', 'two', 'three'] + + def test_adjust_callback(self, mini, fedora, centos): + # From the mini tree + rule = mini.data['adjust'] + + mock_callback = MagicMock(name='callback') + mini.adjust(fmf.Context(), decision_callback=mock_callback) + mock_callback.assert_called_once_with(mini, rule, None) + + mock_callback = MagicMock(name='callback') + mini.adjust(fedora, decision_callback=mock_callback) + mock_callback.assert_called_once_with(mini, rule, False) + + mock_callback = MagicMock(name='callback') + mini.adjust(centos, decision_callback=mock_callback) + mock_callback.assert_called_once_with(mini, rule, True)