#1319 backend: refactor action handlers to their separate classes
Merged 4 years ago by praiskup. Opened 4 years ago by frostyx.
copr/ frostyx/copr refactor-action-handlers  into  master

file modified
+130 -124
@@ -48,6 +48,39 @@ 

          # TODO: describe actions

  

      """

+ 

+     @classmethod

+     def create_from(cls, opts, action, log=None):

+         action_class = cls.get_action_class(action)

+         return action_class(opts, action, log)

+ 

+     @classmethod

+     def get_action_class(cls, action):

+         action_type = action["action_type"]

+         action_class = {

+             ActionType.LEGAL_FLAG: LegalFlag,

+             ActionType.CREATEREPO: Createrepo,

+             ActionType.UPDATE_COMPS: CompsUpdate,

+             ActionType.GEN_GPG_KEY: GenerateGpgKey,

+             ActionType.RAWHIDE_TO_RELEASE: RawhideToRelease,

+             ActionType.FORK: Fork,

+             ActionType.BUILD_MODULE: BuildModule,

+             ActionType.CANCEL_BUILD: CancelBuild,

+         }.get(action_type, None)

+ 

+         if action_type == ActionType.DELETE:

+             object_type = action["object_type"]

+             action_class = {

+                 "copr": DeleteProject,

+                 "build": DeleteBuild,

+                 "builds": DeleteMultipleBuilds,

+                 "chroot": DeleteChroot,

+             }.get(object_type, action_class)

+ 

+         if not action_class:

+             raise ValueError("Unexpected action type")

+         return action_class

+ 

      # TODO: get more form opts, decrease number of parameters

      def __init__(self, opts, action, log=None):

  
@@ -61,15 +94,23 @@ 

          self.log = log if log else get_redis_logger(self.opts, "backend.actions", "actions")

  

      def __str__(self):

-         return "<Action: {}>".format(self.data)

+         return "<{}(Action): {}>".format(self.__class__.__name__, self.data)

  

-     def get_chroot_result_dir(self, chroot, project_dirname, ownername):

-         return os.path.join(self.destdir, ownername, project_dirname, chroot)

+     def run(self):

+         """

+         This is an abstract class, implement this function for specific actions

+         in their own classes

+         """

+         raise NotImplementedError()

  

-     def handle_legal_flag(self):

+ 

+ class LegalFlag(Action):

+     def run(self):

          self.log.debug("Action legal-flag: ignoring")

  

-     def handle_createrepo(self):

+ 

+ class Createrepo(Action):

+     def run(self):

          self.log.info("Action createrepo")

          data = json.loads(self.data["data"])

          ownername = data["ownername"]
@@ -97,7 +138,22 @@ 

  

          return result

  

-     def handle_fork(self, result):

+ 

+ class GPGMixin(object):

+     def generate_gpg_key(self, ownername, projectname):

+         if self.opts.do_sign is False:

+             # skip key creation, most probably sign component is unused

+             return True

+         try:

+             create_user_keys(ownername, projectname, self.opts)

+             return True

+         except CoprKeygenRequestError as e:

+             self.log.exception(e)

+             return False

+ 

+ 

+ class Fork(Action, GPGMixin):

+     def run(self):

          sign = self.opts.do_sign

          self.log.info("Action fork %s", self.data["object_type"])

          data = json.loads(self.data["data"])
@@ -107,7 +163,7 @@ 

  

          if not os.path.exists(old_path):

              self.log.info("Source copr directory doesn't exist: %s", old_path)

-             result.result = ActionResult.FAILURE

+             result = ActionResult.FAILURE

              return

  

          try:
@@ -156,20 +212,23 @@ 

  

                      self.log.info("Forked build %s as %s", src_path, dst_path)

  

-             result.result = ActionResult.SUCCESS

+             result = ActionResult.SUCCESS

              for chroot_path in chroot_paths:

                  if not call_copr_repo(chroot_path):

-                     result.result = ActionResult.FAILURE

+                     result = ActionResult.FAILURE

  

          except (CoprSignError, CreateRepoError, CoprRequestException, IOError) as ex:

              self.log.error("Failure during project forking")

              self.log.error(str(ex))

              self.log.error(traceback.format_exc())

-             result.result = ActionResult.FAILURE

+             result = ActionResult.FAILURE

+         return result

  

-     def handle_delete_project(self, result):

+ 

+ class DeleteProject(Action):

+     def run(self):

          self.log.debug("Action delete copr")

-         result.result = ActionResult.SUCCESS

+         result = ActionResult.SUCCESS

  

          ext_data = json.loads(self.data["data"])

          ownername = ext_data["ownername"]
@@ -177,7 +236,7 @@ 

  

          if not ownername:

              self.log.error("Received empty ownername!")

-             result.result = ActionResult.FAILURE

+             result = ActionResult.FAILURE

              return

  

          for dirname in project_dirnames:
@@ -188,8 +247,11 @@ 

              if os.path.exists(path):

                  self.log.info("Removing copr dir {}".format(path))

                  shutil.rmtree(path)

+         return result

+ 

  

-     def handle_comps_update(self, result):

+ class CompsUpdate(Action):

+     def run(self):

          self.log.debug("Action comps update")

  

          ext_data = json.loads(self.data["data"])
@@ -204,7 +266,7 @@ 

          path = self.get_chroot_result_dir(chroot, projectname, ownername)

          ensure_dir_exists(path, self.log)

          local_comps_path = os.path.join(path, "comps.xml")

-         result.result = ActionResult.SUCCESS

+         result = ActionResult.SUCCESS

          if not ext_data.get("comps_present", True):

              silent_remove(local_comps_path)

              self.log.info("deleted comps.xml for %s/%s/%s from %s ",
@@ -217,7 +279,37 @@ 

              except Exception:

                  self.log.exception("Failed to update comps from %s at location %s",

                                     remote_comps_url, local_comps_path)

-                 result.result = ActionResult.FAILURE

+                 result = ActionResult.FAILURE

+         return result

+ 

+ 

+ class DeleteMultipleBuilds(Action):

+     def run(self):

+         self.log.debug("Action delete multiple builds.")

+ 

+         # == EXAMPLE DATA ==

+         # ownername: @copr

+         # projectname: testproject

+         # project_dirnames:

+         #   testproject:pr:10:

+         #     srpm-builds: [00849545, 00849546]

+         #     fedora-30-x86_64: [00849545-example, 00849545-foo]

+         #   [...]

+         ext_data = json.loads(self.data["data"])

+ 

+         ownername = ext_data["ownername"]

+         projectname = ext_data["projectname"]

+         project_dirnames = ext_data["project_dirnames"]

+         build_ids = ext_data["build_ids"]

+ 

+         result = ActionResult.SUCCESS

+         for project_dirname, chroot_builddirs in project_dirnames.items():

+             if ActionResult.FAILURE == \

+                self._handle_delete_builds(ownername, projectname,

+                                           project_dirname, chroot_builddirs,

+                                           build_ids):

+                 result = ActionResult.FAILURE

+         return result

  

      def _handle_delete_builds(self, ownername, projectname, project_dirname,

                                chroot_builddirs, build_ids):
@@ -249,7 +341,9 @@ 

  

          return result

  

-     def handle_delete_build(self):

+ 

+ class DeleteBuild(DeleteMultipleBuilds):

+     def run(self):

          self.log.info("Action delete build.")

  

          # == EXAMPLE DATA ==
@@ -275,34 +369,9 @@ 

                                            project_dirname, chroot_builddirs,

                                            build_ids)

  

-     def handle_delete_multiple_builds(self):

-         self.log.debug("Action delete multiple builds.")

  

-         # == EXAMPLE DATA ==

-         # ownername: @copr

-         # projectname: testproject

-         # project_dirnames:

-         #   testproject:pr:10:

-         #     srpm-builds: [00849545, 00849546]

-         #     fedora-30-x86_64: [00849545-example, 00849545-foo]

-         #   [...]

-         ext_data = json.loads(self.data["data"])

- 

-         ownername = ext_data["ownername"]

-         projectname = ext_data["projectname"]

-         project_dirnames = ext_data["project_dirnames"]

-         build_ids = ext_data["build_ids"]

- 

-         result = ActionResult.SUCCESS

-         for project_dirname, chroot_builddirs in project_dirnames.items():

-             if ActionResult.FAILURE == \

-                self._handle_delete_builds(ownername, projectname,

-                                           project_dirname, chroot_builddirs,

-                                           build_ids):

-                 result = ActionResult.FAILURE

-         return result

- 

-     def handle_delete_chroot(self):

+ class DeleteChroot(Action):

+     def run(self):

          self.log.info("Action delete project chroot.")

  

          ext_data = json.loads(self.data["data"])
@@ -317,8 +386,11 @@ 

              self.log.error("Directory %s not found", chroot_path)

              return

          shutil.rmtree(chroot_path)

+         return ActionResult.SUCCESS

  

-     def handle_generate_gpg_key(self, result):

+ 

+ class GenerateGpgKey(Action, GPGMixin):

+     def run(self):

          ext_data = json.loads(self.data["data"])

          self.log.info("Action generate gpg key: %s", ext_data)

  
@@ -326,24 +398,13 @@ 

          projectname = ext_data["projectname"]

  

          success = self.generate_gpg_key(ownername, projectname)

-         result.result = ActionResult.SUCCESS if success else ActionResult.FAILURE

+         return ActionResult.SUCCESS if success else ActionResult.FAILURE

  

-     def generate_gpg_key(self, ownername, projectname):

-         if self.opts.do_sign is False:

-             # skip key creation, most probably sign component is unused

-             return True

-         try:

-             create_user_keys(ownername, projectname, self.opts)

-             return True

-         except CoprKeygenRequestError as e:

-             self.log.exception(e)

-             return False

  

-     def handle_rawhide_to_release(self):

+ class RawhideToRelease(Action):

+     def run(self):

          data = json.loads(self.data["data"])

- 

          result = ActionResult.SUCCESS

- 

          try:

              chrootdir = os.path.join(self.opts.destdir, data["ownername"], data["projectname"], data["dest_chroot"])

              if not os.path.exists(chrootdir):
@@ -370,8 +431,8 @@ 

          return result

  

  

-     def handle_cancel_build(self, result):

-         result.result = ActionResult.SUCCESS

+ class CancelBuild(Action):

+     def run(self):

          data = json.loads(self.data["data"])

          task_id = data["task_id"]

  
@@ -381,8 +442,7 @@ 

              self.log.info("Found VM %s for task %s", vmd.vm_ip, task_id)

          else:

              self.log.error("No VM found for task %s", task_id)

-             result.result = ActionResult.FAILURE

-             return

+             return ActionResult.FAILURE

  

          conn = SSHConnection(

              user=self.opts.build_user,
@@ -395,35 +455,32 @@ 

              rc, out, err = conn.run_expensive(cmd)

          except SSHConnectionError:

              self.log.exception("Error running cmd: %s", cmd)

-             result.result = ActionResult.FAILURE

-             return

+             return ActionResult.FAILURE

  

          cmd_debug(cmd, rc, out, err, self.log)

  

          if rc != 0:

-             result.result = ActionResult.FAILURE

-             return

+             return ActionResult.FAILURE

  

          try:

              pid = int(out.strip())

          except ValueError:

              self.log.exception("Invalid pid %s received", out)

-             result.result = ActionResult.FAILURE

-             return

+             return ActionResult.FAILURE

  

          cmd = "kill -9 -{}".format(pid)

          try:

              rc, out, err = conn.run_expensive(cmd)

          except SSHConnectionError:

              self.log.exception("Error running cmd: %s", cmd)

-             result.result = ActionResult.FAILURE

-             return

+             return ActionResult.FAILURE

  

          cmd_debug(cmd, rc, out, err, self.log)

-         result.result = ActionResult.SUCCESS

+         return ActionResult.SUCCESS

  

  

-     def handle_build_module(self):

+ class BuildModule(Action):

+     def run(self):

          result = ActionResult.SUCCESS

          try:

              data = json.loads(self.data["data"])
@@ -478,57 +535,6 @@ 

  

          return result

  

-     def run(self):

-         """ Handle action (other then builds) - like rename or delete of project """

-         self.log.info("Executing: %s", str(self))

- 

-         # TODO: we don't need Munch() here, drop it

-         result = Munch()

-         result.id = self.data["id"]

- 

-         action_type = self.data["action_type"]

- 

-         if action_type == ActionType.DELETE:

-             if self.data["object_type"] == "copr":

-                 self.handle_delete_project(result)

-                 # TODO: we shouldn't ignore errors

-                 result.result = ActionResult.SUCCESS

-             elif self.data["object_type"] == "build":

-                 result.result = self.handle_delete_build()

-             elif self.data["object_type"] == "builds":

-                 result.result = self.handle_delete_multiple_builds()

-             elif self.data["object_type"] == "chroot":

-                 self.handle_delete_chroot()

-                 # TODO: we shouldn't ignore errors

-                 result.result = ActionResult.SUCCESS

- 

-         elif action_type == ActionType.LEGAL_FLAG:

-             self.handle_legal_flag()

- 

-         elif action_type == ActionType.FORK:

-             self.handle_fork(result)

- 

-         elif action_type == ActionType.CREATEREPO:

-             result.result = self.handle_createrepo()

- 

-         elif action_type == ActionType.UPDATE_COMPS:

-             self.handle_comps_update(result)

- 

-         elif action_type == ActionType.GEN_GPG_KEY:

-             self.handle_generate_gpg_key(result)

- 

-         elif action_type == ActionType.RAWHIDE_TO_RELEASE:

-             result.result = self.handle_rawhide_to_release()

- 

-         elif action_type == ActionType.BUILD_MODULE:

-             result.result = self.handle_build_module()

- 

-         elif action_type == ActionType.CANCEL_BUILD:

-             self.handle_cancel_build(result)

- 

-         self.log.info("Action result: %s", result)

-         return result

- 

  

  # TODO: sync with ActionTypeEnum from common

  class ActionType(object):

@@ -76,11 +76,12 @@ 

          sys.exit(1)

  

      action_task = resp.json()

-     action = Action(opts, action_task, log=log)

+     action = Action.create_from(opts, action_task, log=log)

      result = ActionResult.FAILURE

      try:

-         action_result = action.run()

-         result = action_result.result

+         log.info("Executing: %s", str(action))

+         result = action.run()

+         log.info("Action result: %s", result)

      except Exception:

          log.exception("action failed for unknown error")

  

file modified
+29 -29
@@ -120,7 +120,7 @@ 

  

      def test_action_run_legal_flag(self, mc_time):

          mc_time.time.return_value = self.test_time

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.LEGAL_FLAG,
@@ -138,7 +138,7 @@ 

      def test_action_handle_forks(self, mc_call, mc_unsign_rpms_in_dir, mc_exists, mc_copy_tree, mc_time):

          mc_time.time.return_value = self.test_time

          mc_exists = True

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.FORK,
@@ -208,7 +208,7 @@ 

              handle.write(self.test_content)

  

          self.opts.destdir = tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.RENAME,
@@ -239,7 +239,7 @@ 

          tmp_dir = self.make_temp_dir()

  

          self.opts.destdir = os.path.join(tmp_dir, "dir-not-exists")

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.RENAME,
@@ -269,7 +269,7 @@ 

          os.mkdir(os.path.join(tmp_dir, "new_dir"))

  

          self.opts.destdir = tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.RENAME,
@@ -295,7 +295,7 @@ 

          tmp_dir = self.make_temp_dir()

          self.opts.destdir = tmp_dir

  

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -311,7 +311,7 @@ 

  

          assert os.path.exists(os.path.join(tmp_dir, "foo", "bar"))

          assert not os.path.exists(os.path.join(tmp_dir, "foo", "baz"))

-         assert test_action.run().result == ActionResult.SUCCESS

+         assert test_action.run() == ActionResult.SUCCESS

          assert os.path.exists(os.path.join(tmp_dir, "old_dir"))

          assert not os.path.exists(os.path.join(tmp_dir, "foo", "bar"))

  
@@ -322,7 +322,7 @@ 

  

          tmp_dir = self.make_temp_dir()

          self.opts.destdir=tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -350,7 +350,7 @@ 

  

          tmp_dir = self.make_temp_dir()

          self.opts.destdir = tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "id": 1,
@@ -363,7 +363,7 @@ 

          )

          result = test_action.run()

          assert len(mc_call.call_args_list) == 0

-         assert result.result == ActionResult.FAILURE

+         assert result == ActionResult.FAILURE

  

      @mock.patch("backend.actions.uses_devel_repo")

      def test_delete_build_succeeded(self, mc_devel, mc_time):
@@ -389,7 +389,7 @@ 

              fh.write(self.test_content)

  

          self.opts.destdir = tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -402,7 +402,7 @@ 

          )

  

          assert os.path.exists(foo_pkg_dir)

-         assert test_action.run().result == ActionResult.SUCCESS

+         assert test_action.run() == ActionResult.SUCCESS

          assert not os.path.exists(foo_pkg_dir)

          assert not os.path.exists(log_path)

          assert os.path.exists(chroot_1_dir)
@@ -444,7 +444,7 @@ 

  

          self.opts.destdir = self.tmp_dir_name

  

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -463,7 +463,7 @@ 

              },

          )

  

-         assert test_action.run().result == ActionResult.SUCCESS

+         assert test_action.run() == ActionResult.SUCCESS

  

          new_primary = load_primary_xml(repodata)

          new_primary_devel = load_primary_xml(repodata_devel)
@@ -495,7 +495,7 @@ 

              fh.write("foo\n")

  

          self.opts.destdir = tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -507,7 +507,7 @@ 

              },

          )

          # just fail

-         assert test_action.run().result == ActionResult.FAILURE

+         assert test_action.run() == ActionResult.FAILURE

  

      @mock.patch("backend.actions.uses_devel_repo")

      def test_delete_two_chroots(self, mc_devel, mc_time):
@@ -540,7 +540,7 @@ 

          mc_time.time.return_value = self.test_time

  

          self.opts.destdir = self.tmp_dir_name

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -559,7 +559,7 @@ 

                  }),

              },

          )

-         assert test_action.run().result == ActionResult.SUCCESS

+         assert test_action.run() == ActionResult.SUCCESS

  

          assert not os.path.exists(os.path.join(chroot_20_path, "build-00000015.log"))

          assert not os.path.exists(os.path.join(chroot_21_path, "build-00000015.log"))
@@ -609,7 +609,7 @@ 

          mc_time.time.return_value = self.test_time

  

          self.opts.destdir = self.tmp_dir_name

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -630,7 +630,7 @@ 

              },

          )

  

-         assert test_action.run().result == ActionResult.SUCCESS

+         assert test_action.run() == ActionResult.SUCCESS

  

          assert not os.path.exists(os.path.join(chroot_20_path, "build-15.log"))

          assert not os.path.exists(os.path.join(chroot_21_path, "build-15.log"))
@@ -661,7 +661,7 @@ 

          chroot_21_path = os.path.join(self.tmp_dir_name, "foo", "bar", "fedora-21-x86_64")

  

          self.opts.destdir = self.tmp_dir_name

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -682,7 +682,7 @@ 

          assert os.path.exists(chroot_20_path)

          assert os.path.exists(chroot_21_path)

          result = test_action.run()

-         assert result.result == ActionResult.FAILURE

+         assert result == ActionResult.FAILURE

  

          # shouldn't touch chroot dirs

          assert os.path.exists(chroot_20_path)
@@ -716,7 +716,7 @@ 

          })

  

          self.opts.destdir = tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,
@@ -753,7 +753,7 @@ 

          })

          self.opts.destdir = tmp_dir

  

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.CREATEREPO,
@@ -761,7 +761,7 @@ 

                  "id": 8

              },

          )

-         assert test_action.run().result == ActionResult.SUCCESS

+         assert test_action.run() == ActionResult.SUCCESS

  

          for chroot in ['fedora-20-x86_64', 'epel-6-i386']:

              cmd = ['copr-repo',
@@ -784,7 +784,7 @@ 

              "project_dirnames": ["bar"]

          })

          self.opts.destdir = tmp_dir

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.CREATEREPO,
@@ -792,7 +792,7 @@ 

                  "id": 9

              },

          )

-         assert test_action.run().result == ActionResult.FAILURE

+         assert test_action.run() == ActionResult.FAILURE

  

      @unittest.skip("Fixme, test doesn't work.")

      @mock.patch("backend.actions.create_user_keys")
@@ -807,7 +807,7 @@ 

          expected_call = mock.call(uname, pname, self.opts)

  

          mc_front_cb = MagicMock()

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.GEN_GPG_KEY,
@@ -855,7 +855,7 @@ 

          tmp_dir = self.make_temp_dir()

          self.opts.destdir = os.path.join(tmp_dir, "dir-not-exists")

  

-         test_action = Action(

+         test_action = Action.create_from(

              opts=self.opts,

              action={

                  "action_type": ActionType.DELETE,

The current implementation is too clumsy and IMHO stops being good
enough. We have one gigantic class Action which does everything.
It implements methods to handle every possible type of an action.
Then run() method parses action data, determine what type
of action it is and call an appropriate handler method.

This doesn't make much sense in the first place. We should have
specific classes for each and every type of an action. This way
we will have those classes responsible for only one thing and
the chance that they will affect each other is lower.

Moreover, the current solution doesn't allow to easily define
action type scpecific properties, such as its priority. Therefore,
I decided to refactor our current implementation.

While I was at it, I refactored the result behavior. There was

TODO: we don't need Munch() here, drop it
result = Munch()

which is true. We created result munch only to store basically
just one value - the integer value of the actual result (and id,
which was useless). Then we would pass it to each hander method
just so it can update its value. That's so C++.
I threw away the whole "result" concept. Actions don't take result
as their parameter anymore and simply return the result value. Not
a munch, but the actual integer result value that we are interested
in.

This PR was dissected from PR#1318
I am continuing the discussion here

The get_actoin_class introduction should be in first commit, to make it easier to review.

Done

Mixin has concrete meaning in python ... IMO it is confusing.

I can't find any official definition what Mixin is but I always thought it is exactly this. A small class providing some functionality, that is then used via multiple inheritance. But thinking about it ... the mixin should provide extra functionality for the original class, but the class shouldn't be dependent on it, right?

delete and delete multiple builds are not compatible (different task json), I don't think this inherit will work.

This isn't obvious from the diff, it is much more clear when viewing the changed file - Both DeleteBuild and DeleteMultipleBuilds define their own run() method, so it should work fine. The only reason for the inheritance is so they can share _handle_delete_builds method. But I will do it differently, it isn't obvious enough.

rebased onto 2cf5f03

4 years ago

Id suggest you to run beaker tests... with the rest of the orig. pr it could be more opaque

Pull-Request has been merged by praiskup

4 years ago