/plenum/test/testable.py

https://github.com/hyperledger/indy-plenum · Python · 158 lines · 121 code · 28 blank · 9 comment · 46 complexity · c4cf3fad57ba9b5e71f7a5ca13256f63 MD5 · raw file

  1. import inspect
  2. import time
  3. from functools import wraps
  4. from typing import Any, List, NamedTuple, Tuple, Optional, Iterable, Union, \
  5. Callable
  6. from typing import Dict
  7. import traceback
  8. try:
  9. from plenum.test import NO_SPIES
  10. except ImportError:
  11. pass
  12. from plenum.common.util import objSearchReplace
  13. from stp_core.common.log import getlogger
  14. logger = getlogger()
  15. Entry = NamedTuple('Entry', [('starttime', float),
  16. ('endtime', float),
  17. ('method', str),
  18. ('params', Dict),
  19. ('result', Any)])
  20. SpyableMethod = Union[str, Callable]
  21. SpyableMethods = Iterable[SpyableMethod]
  22. class SpyLog(list):
  23. def getLast(self, method: SpyableMethod, required: bool = False) -> \
  24. Optional[Entry]:
  25. entry = None # type: Optional[Entry]
  26. if callable(method):
  27. method = method.__name__
  28. try:
  29. entry = next(x for x in reversed(self) if x.method == method)
  30. except StopIteration:
  31. if required:
  32. raise RuntimeError(
  33. "spylog entry for method {} not found".format(method))
  34. return entry
  35. def getAll(self, method: SpyableMethod) -> List[Entry]:
  36. if callable(method):
  37. method = method.__name__
  38. return list(reversed([x for x in self if x.method == method]))
  39. def getLastParam(self, method: str, paramIndex: int = 0) -> Any:
  40. return self.getLastParams(method)[paramIndex]
  41. def getLastParams(self, method: str, required: bool = True) -> Tuple:
  42. last = self.getLast(method, required)
  43. return last.params if last is not None else None
  44. def getLastResult(self, method: str, required: bool = True) -> Tuple:
  45. last = self.getLast(method, required)
  46. return last.result if last is not None else None
  47. def count(self, method: SpyableMethod) -> int:
  48. if callable(method):
  49. method = method.__name__
  50. return sum(1 for x in self if x.method == method)
  51. def spy(func, is_init, should_spy, spy_log=None):
  52. sig = inspect.signature(func)
  53. # sets up spylog, but doesn't spy on init
  54. def init_only(self, *args, **kwargs):
  55. self.spylog = SpyLog()
  56. return func(self, *args, **kwargs)
  57. init_only.__name__ = func.__name__
  58. # sets up spylog, and also spys on init
  59. def init_wrap(self, *args, **kwargs):
  60. self.spylog = SpyLog()
  61. return wrap(self, *args, **kwargs)
  62. init_wrap.__name__ = func.__name__
  63. # wraps a function call
  64. @wraps(func)
  65. def wrap(self, *args, **kwargs):
  66. start = time.perf_counter()
  67. r = None
  68. try:
  69. r = func(self, *args, **kwargs)
  70. except Exception as ex:
  71. r = ex
  72. # TODO: This should be error actually
  73. logger.warning("Caught exception {}".format(ex))
  74. logger.warning("{}".format(traceback.format_exc()))
  75. raise
  76. finally:
  77. bound = sig.bind(self, *args, **kwargs)
  78. params = dict(bound.arguments)
  79. params.pop('self', None)
  80. used_log = self.spylog if hasattr(self, 'spylog') else spy_log
  81. used_log.append(Entry(start,
  82. time.perf_counter(),
  83. func.__name__,
  84. params,
  85. r))
  86. return r
  87. return wrap if not is_init else init_wrap if should_spy else init_only
  88. def spyable(name: str = None, methods: SpyableMethods = None,
  89. deep_level: int = None):
  90. def decorator(clas):
  91. if 'NO_SPIES' in globals() and globals()['NO_SPIES']:
  92. # Since spylog consumes resources, benchmarking tests need to be
  93. # able to not have spyables, so they set a module global `NO_SPIES`,
  94. # it's their responsibility to unset it
  95. logger.info(
  96. 'NOT USING SPIES ON METHODS AS THEY ARE EXPLICITLY DISABLED')
  97. return clas
  98. nonlocal name
  99. name = name if name else "Spyable" + clas.__name__
  100. spyable_type = type(name, (clas,), {})
  101. morphed = {} # type: Dict[Callable, Callable]
  102. matches = []
  103. for nm, func in [(method, getattr(clas, method))
  104. for method in dir(clas)
  105. if callable(getattr(clas, method))]:
  106. isInit = nm == "__init__"
  107. matched = (nm if methods and nm in methods else
  108. func if methods and func in methods else
  109. None)
  110. # if method was specified to be spied on or is `__init__` method
  111. # or is does not have name starting with `__`
  112. shouldSpy = bool(matched) if methods else (
  113. not nm.startswith("__") or isInit)
  114. if shouldSpy or isInit:
  115. newFunc = spy(func, isInit, shouldSpy)
  116. morphed[func] = newFunc
  117. setattr(spyable_type, nm, newFunc)
  118. logger.debug("in {} added spy on {}".
  119. format(spyable_type.__name__, nm))
  120. matches.append(matched)
  121. if methods:
  122. for m in methods:
  123. if m not in matches:
  124. logger.warning(
  125. "method {} not found, so no spy added".format(m),
  126. extra={"cli": False})
  127. objSearchReplace(spyable_type, morphed,
  128. logMsg="Applying spy remapping", deepLevel=deep_level)
  129. return spyable_type
  130. return decorator