runner.py
  1  """
  2  Find tests, identify the ones that the user wants, and invoke them.
  3  """
  4  
  5  from __future__ import annotations
  6  
  7  import importlib
  8  import traceback
  9  from types import ModuleType
 10  from typing import Iterator
 11  
 12  from arti_rpc_tests import FatalException
 13  from arti_rpc_tests.context import TestContext
 14  
 15  # Is this the right way to do this?
 16  #
 17  # Should we be instead listing all the files in $(basename __file__)/tests ?
 18  _TEST_MODS = [
 19      "basic",
 20      "connpt",
 21      "connect",
 22      "meta_features",
 23      "release_obj",
 24  ]
 25  
 26  
 27  # Return a list of all the python modules that we should search for tests.
 28  def all_modules() -> list[ModuleType]:
 29      return [
 30          importlib.import_module(f"arti_rpc_tests.tests.{name}") for name in _TEST_MODS
 31      ]
 32  
 33  
 34  def run_tests(
 35      testfilter: TestFilter, modules: list[ModuleType], context: TestContext
 36  ) -> bool:
 37      """
 38      Run every test listed by `testfilter` in the provided `modules`,
 39      using the facilities in `context`.
 40  
 41      Return True if every test passed, and False otherwise.
 42  
 43      May raise FatalException if a test failed completely.
 44      """
 45      to_run: list[TestCase] = []
 46  
 47      for m in modules:
 48          to_run.extend(testfilter.list_tests(m))
 49  
 50      print(f"Found {len(to_run)} tests")
 51  
 52      successes = failures = 0
 53      for test in to_run:
 54          if test.run(context):
 55              successes += 1
 56          else:
 57              failures += 1
 58  
 59      if failures:
 60          print(f"{failures}/{len(to_run)} tests failed!")
 61      else:
 62          print(f"All {successes} tests succeeded")
 63      assert successes + failures == len(to_run)
 64  
 65      return failures == 0
 66  
 67  
 68  class TestFilter:
 69      """
 70      Selects one or more tests that we should run.
 71      """
 72  
 73      def __init__(self):
 74          # No features supported yet
 75          pass
 76  
 77      def list_tests(self, module: ModuleType) -> Iterator[TestCase]:
 78          """
 79          Yield every test in `module` that this filter permits.
 80          """
 81          for name in sorted(dir(module)):
 82              obj = getattr(module, name)
 83  
 84              if callable(obj) and getattr(obj, "arti_rpc_test", False):
 85                  sname = name.removeprefix("test_")
 86                  yield TestCase(f"{module.__name__}.{sname}", obj)
 87  
 88  
 89  class TestCase:
 90      """
 91      A single test case.
 92      """
 93  
 94      def __init__(self, name, function):
 95          self.name = name
 96          self.function = function
 97  
 98      def run(self, context: TestContext) -> bool:
 99          """
100          Try to run this test within `context`.
101  
102          Returns True on success and False on failure.
103  
104          May raise FatalEception if test execution should stop entirely.
105          """
106          try:
107              print(self.name, "...", flush=True, end="")
108              self.run_inner(context)
109              print("OK")
110              return True
111          except FatalException:
112              print("FATAL EXCEPTION")
113              raise
114          except Exception:
115              print("FAILED")
116              traceback.print_exc()
117              return False
118  
119      def run_inner(self, context: TestContext) -> None:
120          """
121          Run this test; raise an exception on failure.
122  
123          Raise a FatalException if all test execution should stop entirely.
124          """
125          if not context.arti_process_is_running():
126              raise FatalException("Arti process not running at start of test!")
127  
128          self.function(context)
129  
130          if not context.arti_process_is_running():
131              raise FatalException("Arti process not running at end of test!")