""" * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. * Copyright (C) 2021-2024 Ad5001 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . """ from typing import Callable, Self from tests.plugins.natural.interfaces.base import Assertion, repr_, AssertionInterface from tests.plugins.natural.interfaces.int import IntComparisonAssertionInterface PRINT_PREFIX = (" " * 24) class SpyAssertion(Assertion): def __init__(self, assertion: bool, message: str, calls: list): super().__init__(assertion, message + "\n") if len(calls) > 0: self.message += self.render_calls(calls) else: self.message += f"{PRINT_PREFIX}0 registered calls." def render_calls(self, calls): lines = [f"{PRINT_PREFIX}{len(calls)} registered call(s):"] for call in calls: repr_args = [repr_(arg) for arg in call[0]] repr_kwargs = [f"{key}={repr_(arg)}" for key, arg in call[1].items()] lines.append(f" - {', '.join([*repr_args, *repr_kwargs])}") return ("\n" + PRINT_PREFIX).join(lines) class Methods: AT_LEAST_ONCE = "AT_LEAST_ONCE" EXACTLY = "EXACTLY" AT_LEAST = "AT_LEAST" AT_MOST = "AT_MOST" MORE_THAN = "AT_LEAST" LESS_THAN = "AT_MOST" class CalledInterface(IntComparisonAssertionInterface): """ Internal class generated by Spy.was_called. """ def __init__(self, calls: list[tuple[list, dict]]): super().__init__(len(calls)) self.__calls = calls self.__method = Methods.AT_LEAST_ONCE def __apply_method(self, calls): required = self._compare_to calls_count = len(calls) match self.__method: case Methods.AT_LEAST_ONCE: compare = len(calls) >= 1 error = "Method was not called" case Methods.EXACTLY: compare = len(calls) == required error = f"Method was not called {required} times ({required} != {calls_count})" case Methods.AT_LEAST: compare = len(calls) >= required error = f"Method was not called at least {required} times ({required} > {calls_count})" case Methods.AT_MOST: compare = len(calls) <= required error = f"Method was not called at most {required} times ({required} < {calls_count})" case Methods.MORE_THAN: compare = len(calls) > required error = f"Method was not called more than {required} times ({required} >= {calls_count})" case Methods.LESS_THAN: compare = len(calls) < required error = f"Method was not called less than {required} times ({required} <= {calls_count})" case _: raise RuntimeError(f"Unknown method {self.__method}.") return compare, error def __bool__(self) -> bool: """ Converts to boolean on assertion. """ compare, error = self.__apply_method(self.__calls) return bool(SpyAssertion(compare, error + ".", self.__calls)) """ Chaining methods """ def __call__(self, *args, **kwargs) -> Self: if len(args) != 1: raise RuntimeError("Cannot call called interface with more than one argument.") self._compare_to = int(args[0]) if self.__method == Methods.AT_LEAST_ONCE: self.__method = Methods.EXACTLY return self @property def at_least(self) -> Self: if self.__method == Methods.AT_LEAST_ONCE: self.__method = Methods.AT_LEAST else: raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}") return self @property def at_most(self) -> Self: if self.__method == Methods.AT_LEAST_ONCE: self.__method = Methods.AT_MOST else: raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}") return self @property def more_than(self) -> Self: if self.__method == Methods.AT_LEAST_ONCE: self.__method = Methods.MORE_THAN else: raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.MORE_THAN}") return self @property def less_than(self) -> Self: if self.__method == Methods.AT_LEAST_ONCE: self.__method = Methods.LESS_THAN else: raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.LESS_THAN}") return self @property def time(self) -> Self: return self @property def times(self) -> Self: return self """ Class properties. """ def __match_calls_for_condition(self, condition: Callable[[list, dict], bool]) -> tuple[bool, str]: calls = [] for call in self.__calls: if condition(call[0], call[1]): calls.append(call) compare, error = self.__apply_method(calls) return compare, error def with_arguments(self, *args, **kwargs) -> SpyAssertion: """ Checks if the Spy has been called the given number of times with at least the given arguments. """ def some_args_matched(a, kw): args_matched = all(( arg in a for arg in args )) kwargs_matched = all(( key in kw and kw[key] == arg for key, arg in kwargs.items() )) return args_matched and kwargs_matched compare, error = self.__match_calls_for_condition(some_args_matched) repr_args = ', '.join([repr(arg) for arg in args]) repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()]) msg = f"{error} with arguments ({repr_args}) and keyword arguments ({repr_kwargs})." return SpyAssertion(compare, msg, self.__calls) def with_arguments_matching(self, test_condition: Callable[[list, dict], bool]) -> SpyAssertion: """ Checks if the Spy has been called the given number of times with arguments matching the given conditions. """ compare, error = self.__match_calls_for_condition(test_condition) msg = f"{error} with arguments matching given conditions." return SpyAssertion(compare, msg, self.__calls) def with_exact_arguments(self, *args, **kwargs) -> SpyAssertion: """ Checks if the Spy has been called the given number of times with all the given arguments. """ compare, error = self.__match_calls_for_condition(lambda a, kw: a == args and kw == kwargs) repr_args = ', '.join([repr(arg) for arg in args]) repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()]) msg = f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs})." return SpyAssertion(compare, msg, self.__calls) def with_no_argument(self) -> SpyAssertion: """ Checks if the Spy has been called the given number of times with all the given arguments. """ compare, error = self.__match_calls_for_condition(lambda a, kw: len(a) == 0 and len(kw) == 0) return SpyAssertion(compare, f"{error} with no arguments.", self.__calls) class Spy(AssertionInterface): """ Class to spy into method calls with natural language expressions. """ def __init__(self, function: Callable = None): super().__init__(function) self.function = function self.calls = [] def __call__(self, *args, **kwargs): self.calls.append((args, kwargs)) if self.function is not None: self.function(*args, **kwargs) @property def called(self) -> CalledInterface: """ Returns a boolean-able interface to check conditions for a given number of time the spy was called. """ return CalledInterface(self.calls) @property def not_called(self) -> CalledInterface: """ Returns a boolean-able interface to check that conditions were never fulfilled in the times the spy was called. """ ret = CalledInterface(self.calls) return ret(0)