8000 Add meck:wait_for/6 by rlipscombe · Pull Request #253 · eproxus/meck · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add meck:wait_for/6 #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

rlipscombe
Copy link
Contributor
@rlipscombe rlipscombe commented Apr 11, 2025

When testing some non-deterministic code, I needed something that would
wait for a function to be called with [1, 2, 3], but not necessarily all
at once, and not necessarily in that order.

That is: m:f([1]), m:f([2]), m:f([3]) is just as valid as m:f([1, 2]),
m:f([3]), and as valid as m:f([2]), m:f([3, 1]), and so on.

So: invent a way for meck to update what it's waiting for as the history
is built. By accumulating state as the calls happen, we can check that
the function is called with the correct arguments, whatever the order.

This is called a 'condition'.

I also succeeded in generalising 'times_called' to demonstrate its
usefulness.

-record(tracker, {opt_func :: '_' | atom(),
args_matcher :: meck_args_matcher:args_matcher(),
opt_caller_pid :: '_' | pid(),
countdown :: non_neg_integer(),
awaiting :: {cond_fun(), cond_state()},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether 'awaiting' here should be 'condition'.

end,
Cond = fun
(_, T) when T =:= 0 ->
{halt, ok};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

halt and cont are taken from Elixir's Enum.reduce_while. The ability to return {halt, Reply} is maybe overkill, but it means that you can return something other than ok from meck:wait_for.

wait_for(Mod, {Cond, Times - 1}, OptFunc, ArgsMatcher, OptCallerPid, Timeout).

wait_for(Mod, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout) ->
EffectiveTimeout =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that this got reformatted, making the diff bigger. I can revert just this bit, if that's wanted.

case Filter(HistoryRec) of
true ->
{_Pid, {_M, _F, Args}, _Result} = HistoryRec,
Cond(Args, CondState);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure whether just 'Args' was enough here, or whether also passing 'Result' would be useful. I couldn't pass the whole HistoryRec, because we don't see the whole thing when updating the tracker (unless I'm misreading that code).

end
end;
update_tracker(_Func, _Args, _CallerPid, Tracker) ->
Tracker.

-spec timeout_to_timestamp(Timeout::non_neg_integer()) -> erlang:timestamp().
-spec timeout_to_timestamp(Timeout :: non_neg_integer()) -> erlang:timestamp().
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, more reformatting noise. Can revert.

%% When
Pid = erlang:spawn(fun() ->
test:foo(1, 1),
test:foo(1, 2)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs that these tests don't actually address the "any order / grouping" motivation. I can add more tests if the general approach looks sensible.

@rlipscombe
Copy link
Contributor Author

In the back of my head, I also wonder whether this can be generalised with some combinators, so you could write something like this:

meck:wait_for(meck_condition:all_of([
    meck_condition:match_args(['_', meck:is(IsA)]),
    meck_condition:match_args(['_', meck:is(IsB)])
)).

...where we expect a bunch of calls to eventually match all of the conditions.

When testing some non-deterministic code, I needed something that would
wait for a function to be called with [1, 2, 3], but not necessarily all
at once, and not necessarily in that order.

That is: m:f([1]), m:f([2]), m:f([3]) is just as valid as m:f([1, 2]),
m:f([3]), and as valid as m:f([2]), m:f([3, 1]), and so on.

So: invent a way for meck to update what it's waiting for as the history
is built. By accumulating state as the calls happen, we can check that
the function is called with the correct arguments, whatever the order.

This is called a 'condition'.

I also succeeded in generalising 'times_called' to demonstrate its
usefulness.
@eproxus
Copy link
Owner
eproxus commented May 9, 2025

I like that last idea, making it more composable. We can probably make the API even smoother:

meck:wait(my_mocked_module, meck:condition({calls, unordered}, [
    meck:condition(args, MatchSpecA),
    meck:condition(args, MatchSpecB)
])).

(Not sure what the purpose of meck:is(A|B) was)

Then you could have different conditions, like meck:condition({calls, any} (wait for only one of the calls), meck:condition({calls, sequence}) (wait for the calls in the exact sequence described) or even meck:condition({calls, exact}) (don't allow any other calls to the mock except these)...

@rlipscombe
Copy link
Contributor Author
rlipscombe commented May 9, 2025

Not sure what the purpose of meck:is(A|B) was

It was just off the top of my head. The intent was to say: wait for this to be called with its second arg matching IsA or IsB, but it has to match both at some point, in any order.

Then you could have different conditions, like meck:condition({calls, any} (wait for only one of the calls)...

Yeah; something like that. At this point, though, adding combinators and helpers is a bonus, and isn't required for this PR. We can add those later fairly easily, as long as we're happy with the shape of this now.

In fact, I think we should hold off on adding combinators until we've got a better idea of which ones people want.

@eproxus
Copy link
Owner
eproxus commented May 9, 2025

Adding new APIs is tricky and should be done with care. I would rather add a generic API right now that supports very few combinations, rather than the specific one in this PR because then it has to be supported for a long time.

@rlipscombe
Copy link
Contributor Author
rlipscombe commented May 9, 2025

Adding new APIs is tricky and should be done with care.

Agreed. That's why I'm suggesting not adding any new APIs for combinators at this point. We can add wait_for (or expand wait so it takes a condition). We don't add any combinators (they're not needed -- the interface is general enough that people can write their own).

To be clear:

  1. I'm proposing adding wait_for (or an overload of wait). I'm suggesting that this is simple enough, and general enough, that it's sufficient for all likely use cases.
  2. I'm NOT proposing adding any combinators or helpers at this point.

My suggestion about combinators -- #253 (comment) was me thinking out loud; ignore it. We can come back to it later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0