def generator_send(self, func, arg): """Given a generator, send it the given value and return a response.""" if self._do_raise_keyboardinterrupt: raise KeyboardInterrupt("ctrl-c interrupted execution of a ffi method!") try: res = func.send(arg) if Get.isinstance(res): # Get. return self.lib.PyGeneratorResponseGet( res.product_type, res.subject_declared_type, res.subject, ) elif type(res) in (tuple, list): # GetMulti. return self.lib.PyGeneratorResponseGetMulti( tuple( self.lib.PyGeneratorResponseGet( get.product_type, get.subject_declared_type, get.subject, ) for get in res ) ) else: raise ValueError(f"internal engine error: unrecognized coroutine result {res}") except StopIteration as e: if not e.args: raise # This was a `return` from a coroutine, as opposed to a `StopIteration` raised # by calling `next()` on an empty iterator. return self.lib.PyGeneratorResponseBreak(e.value)
def run_rule( rule, *, rule_args: Optional[Sequence[Any]] = None, mock_gets: Optional[Sequence[MockGet]] = None, union_membership: Optional[UnionMembership] = None, ): """A test helper function that runs an @rule with a set of arguments and mocked Get providers. An @rule named `my_rule` that takes one argument and makes no `Get` requests can be invoked like so (although you could also just invoke it directly): ``` return_value = run_rule(my_rule, rule_args=[arg1]) ``` In the case of an @rule that makes Get requests, things get more interesting: the `mock_gets` argument must be provided as a sequence of `MockGet`s. Each MockGet takes the Product and Subject type, along with a one-argument function that takes a subject value and returns a product value. So in the case of an @rule named `my_co_rule` that takes one argument and makes Get requests for a product type `Listing` with subject type `Dir`, the invoke might look like: ``` return_value = run_rule( my_co_rule, rule_args=[arg1], mock_gets=[ MockGet( product_type=Listing, subject_type=Dir, mock=lambda dir_subject: Listing(..), ), ], ) ``` If any of the @rule's Get requests involve union members, you should pass a `UnionMembership` mapping the union base to any union members you'd like to test. For example, if your rule has `await Get[TestResult](TargetAdaptor, target_adaptor)`, you may pass `UnionMembership({TargetAdaptor: PythonTestsTargetAdaptor})` to this function. :returns: The return value of the completed @rule. """ task_rule = getattr(rule, "rule", None) if task_rule is None: raise TypeError( f"Expected to receive a decorated `@rule`; got: {rule}") if rule_args is not None and len(rule_args) != len( task_rule.input_selectors): raise ValueError( "Rule expected to receive arguments of the form: {}; got: {}". format(task_rule.input_selectors, rule_args)) if mock_gets is not None and len(mock_gets) != len(task_rule.input_gets): raise ValueError( "Rule expected to receive Get providers for {}; got: {}".format( task_rule.input_gets, mock_gets)) res = rule(*(rule_args or ())) if not isinstance(res, (CoroutineType, GeneratorType)): return res def get(product, subject): provider = next( (mock_get.mock for mock_get in mock_gets if mock_get.product_type == product and (mock_get.subject_type == type(subject) or (union_membership and union_membership.is_member( mock_get.subject_type, subject)))), None, ) if provider is None: raise AssertionError( "Rule requested: Get{}, which cannot be satisfied.".format( (product, type(subject), subject))) return provider(subject) rule_coroutine = res rule_input = None while True: try: res = rule_coroutine.send(rule_input) if Get.isinstance(res): rule_input = get(res.product_type, res.subject) elif type(res) in (tuple, list): rule_input = [get(g.product_type, g.subject) for g in res] else: return res except StopIteration as e: if e.args: return e.value