checking.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. '''
  2. A simplified version of the unittest module,
  3. adapted to run unit tests on data sets instead of code.
  4. @author: olivier.massot, 2018
  5. '''
  6. import inspect
  7. import re
  8. import sys
  9. import traceback
  10. UNKNOWN = 0
  11. SUCCESS = 1
  12. FAILURE = 2
  13. ERROR = 3
  14. def _linenumber(obj_):
  15. """ uses the inspect module to find the line number of an object in the module """
  16. try:
  17. _, line_no = inspect.findsource(obj_)
  18. return line_no
  19. except AttributeError:
  20. return -1
  21. class CheckingException(Exception):
  22. """ Base class for checking exceptions """
  23. pass
  24. class TestError():
  25. """ Error logged during a test """
  26. def __init__(self, message, info={}, critical=False):
  27. self.message = message
  28. self.info = info
  29. self.critical = critical
  30. def __repr__(self):
  31. return f"TestError[message='{self.message}'; info={self.info}; critical={self.critical}]"
  32. class TestResult():
  33. """ Result of a test """
  34. _result_to_str = {UNKNOWN: 'Inconnu',
  35. SUCCESS: 'Succès',
  36. FAILURE: 'Echec',
  37. ERROR: 'Erreur'}
  38. def __init__(self, test):
  39. self._test = test
  40. self._name = ""
  41. self._status = SUCCESS
  42. self.errors = []
  43. self._exc_info = None
  44. @property
  45. def name(self):
  46. return self._name or self._test.__name__[5:]
  47. @property
  48. def title(self):
  49. try:
  50. return self._test.__doc__.split("\n")[0].strip()
  51. except AttributeError:
  52. return self.name
  53. @property
  54. def description(self):
  55. try:
  56. return re.sub(" +", " ",
  57. self._test.__doc__.strip(),
  58. flags=re.MULTILINE) # @UndefinedVariable
  59. except AttributeError:
  60. return ""
  61. @property
  62. def status(self):
  63. return self._status
  64. @property
  65. def status_str(self):
  66. return TestResult._result_to_str[self._status]
  67. def __repr__(self):
  68. return f"TestResult[title='{self.title}'; " \
  69. "status={self.status}; " \
  70. "name={self.name}; " \
  71. "method={self._test.__name__}; " \
  72. "errors_count={len(self.errors)}]"
  73. def log_error(self, message, info={}, critical=False):
  74. self._status = FAILURE
  75. error = TestError(message, info, critical)
  76. self.errors.append(error)
  77. def log_exception(self, message, info={}):
  78. self._status = ERROR
  79. error = TestError(message, info)
  80. self.errors.append(error)
  81. def handle_exception(self, exc_info):
  82. typ, value, trace = exc_info
  83. self.log_exception("Une erreur s'est produite: {}".format(typ.__name__),
  84. {"exc_info": "{}\n{}\n{}".format(typ.__name__, value,
  85. ''.join(traceback.format_tb(trace)))})
  86. class Comlink():
  87. """ Class used to provide pointers to the checker events """
  88. def _started_test(self, test):
  89. pass
  90. def _ended_test(self, test):
  91. pass
  92. class BaseChecker():
  93. """ Base class for checkers """
  94. def __init__(self):
  95. self._test_running = None
  96. self.comlink = Comlink()
  97. self.tests = sorted([m for _, m \
  98. in inspect.getmembers(self, predicate=inspect.ismethod) \
  99. if m.__name__[:5] == 'test_'], key=_linenumber)
  100. def setUp(self):
  101. pass
  102. def tearDown(self):
  103. pass
  104. def log_error(self, message, **info):
  105. self._test_running.log_error(message, info)
  106. def log_critical(self, message, **info):
  107. self._test_running.log_error(f"[CRITIQUE] {message}", info, critical=True)
  108. def run(self, rxfilter="", dry=False):
  109. # 'rxfilter' allow to filter the tests to run with a regex
  110. # if 'dry' is set to True, setUp and tearDown are skipped
  111. # both are used for testing purpose
  112. tests_results = []
  113. for test in self.tests:
  114. if rxfilter and not re.fullmatch(rxfilter,
  115. test.__name__,
  116. re.IGNORECASE): # @UndefinedVariable
  117. continue
  118. result = TestResult(test)
  119. self._test_running = result
  120. self.comlink._started_test(result)
  121. try:
  122. if not dry:
  123. self.setUp()
  124. test()
  125. if not dry:
  126. self.tearDown()
  127. except CheckingException as e:
  128. result.log_exception(str(e))
  129. except:
  130. result.handle_exception(sys.exc_info())
  131. tests_results.append(result)
  132. self.comlink._ended_test(result)
  133. if any(err.critical for err in result.errors):
  134. break
  135. return tests_results
  136. if __name__ == '__main__':
  137. class ExampleChecker(BaseChecker):
  138. def test_c(self):
  139. """ Test 1 """
  140. for i in range(10):
  141. self.log_error(f"error-{i}", i=i)
  142. def test_b(self):
  143. """ Test 2
  144. some longer description """
  145. raise Exception("bla bla")
  146. ch = ExampleChecker()
  147. results = ch.run()
  148. for r in results:
  149. print(r)
  150. for e in r.errors:
  151. print(e)