Skip to content

Commit

Permalink
Merge pull request #13 from tsmanner/shortcircuit_redundant_step
Browse files Browse the repository at this point in the history
Implementation of BEP2 Proposal 2
  • Loading branch information
tsmanner authored Jun 19, 2019
2 parents 0f6b852 + 8c288b5 commit 5c8adfb
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 13 deletions.
9 changes: 1 addition & 8 deletions bazaarci/runner/node.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Set, Union
from typing import Any, Optional, Set, Union


class Node:
Expand Down Expand Up @@ -28,10 +28,3 @@ def start(self) -> None:
raise NotImplementedError(
"Class `{}` has not implemented a `start` method!".format(self.__class__.__name__)
)

def run(self) -> None:
""" Code that will be executed in the background thread.
"""
raise NotImplementedError(
"Class `{}` has not implemented a `run` method!".format(self.__class__.__name__)
)
48 changes: 45 additions & 3 deletions bazaarci/runner/step.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
""" Step
Definition of the Step class and run behavior decorator functions
"""
from functools import reduce, wraps
from threading import Event, Thread
from typing import Callable, Optional

Expand Down Expand Up @@ -34,9 +38,8 @@ def start(self):
self.thread = Thread(target=self.run)
self.thread.start()

def run(self):
[product.wait() for product in self.consumes()]
if self.target is not None:
def _run(self):
if self.target and callable(self.target):
self.output = self.target()
[product.set() for product in self.produces()]

Expand All @@ -49,3 +52,42 @@ def __str__(self):

def __repr__(self):
return "{}({})".format(self.__class__.__name__, self.name)


def set_run_behavior(class_or_instance, *args):
""" Build the run function from _run and the
incoming list of behaviorals
"""
run_function = class_or_instance._run
for wrapper in reversed(args):
run_function = wrapper(run_function)
setattr(class_or_instance, "run", run_function)


def wait_for_producers(func):
""" Waits on all `Product`s in `self.consumes` before
calling the function.
"""
@wraps(func)
def wrapped(self):
[product.wait() for product in self.consumes()]
func(self)
return wrapped


def skip_if_redundant(func):
""" Calls the function only if any output `Product`s
have not been set yet.
"""
@wraps(func)
def wrapped(self):
# If there are output products and they have all already been set,
# then this step is not required to run.
all_set = reduce(lambda x, y: x and y.wait(0), self.produces(), True)
if len(self.produces()) == 0 or not all_set:
func(self)
return wrapped


# By default, Step should wait for producers
set_run_behavior(Step, wait_for_producers)
4 changes: 2 additions & 2 deletions tests/test_Step.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def test_start(self, mock_Thread):
self.assertIsNotNone(s.thread)
mock_Thread.assert_called_once_with(target=s.run)

def test_run(self):
def test__run(self):
mock_target = MagicMock()
s = Step("test", target=mock_target)
s.run()
s._run()
mock_target.assert_called_once_with()
51 changes: 51 additions & 0 deletions tests/test_step_run_behavior.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from bazaarci.runner.step import set_run_behavior, wait_for_producers, skip_if_redundant


class TestStepRunBehavior(TestCase):
def test_set_run_behavior(self):
mock_Step = MagicMock()
mock_Decorator1 = MagicMock()
mock_Decorator2 = MagicMock()
set_run_behavior(mock_Step, mock_Decorator1, mock_Decorator2)
mock_Decorator2.assert_called_once_with(mock_Step._run)
mock_Decorator1.assert_called_once_with(mock_Decorator2.return_value)

def test_wait_for_producers(self):
mock_Step = MagicMock()
mock_run = MagicMock()
mock_Step.consumes.return_value = [MagicMock()]
wrapped_run = wait_for_producers(mock_run)
wrapped_run(mock_Step)
mock_Step.consumes.assert_called_once_with()
mock_Step.consumes.return_value[0].wait.assert_called_once_with()
mock_run.assert_called_once_with(mock_Step)

def test_skip_if_redundant(self):
with self.subTest("No output Products"):
# There are no outputs, always run
mock_Step = MagicMock()
mock_run = MagicMock()
mock_Step.produces.return_value = []
wrapped_run = skip_if_redundant(mock_run)
wrapped_run(mock_Step)
mock_run.assert_called_once_with(mock_Step)
with self.subTest("Unset output Product"):
# The output Product is not set, run
mock_Step = MagicMock()
mock_run = MagicMock()
mock_Step.produces.return_value = [MagicMock()]
mock_Step.produces.return_value[0].wait.return_value = False
wrapped_run = skip_if_redundant(mock_run)
wrapped_run(mock_Step)
mock_run.assert_called_once_with(mock_Step)
with self.subTest("Set output Product"):
# The output Product is set, skip run
mock_Step = MagicMock()
mock_run = MagicMock()
mock_Step.produces.return_value = [MagicMock()]
mock_Step.produces.return_value[0].wait.return_value = True
wrapped_run = skip_if_redundant(mock_run)
wrapped_run(mock_Step)
mock_run.assert_not_called()

0 comments on commit 5c8adfb

Please sign in to comment.