From 4e5290eda4bd817a3c947484c30bca2d3a029a70 Mon Sep 17 00:00:00 2001 From: Roger Lipscombe Date: Fri, 11 Apr 2025 14:54:43 +0100 Subject: [PATCH 1/3] Add meck:wait_for/6 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. --- src/meck.erl | 5 +++ src/meck_proc.erl | 89 +++++++++++++++++++++++++++------------------ test/meck_tests.erl | 57 +++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 36 deletions(-) diff --git a/src/meck.erl b/src/meck.erl index 9a3f835..aec3fbf 100644 --- a/src/meck.erl +++ b/src/meck.erl @@ -54,6 +54,7 @@ -export([wait/4]). -export([wait/5]). -export([wait/6]). +-export([wait_for/6]). -export([mocked/0]). %% Syntactic sugar @@ -601,6 +602,10 @@ wait(Times, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout) ArgsMatcher = meck_args_matcher:new(OptArgsSpec), meck_proc:wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout). +wait_for({Cond, CondState}, Mod, OptFunc, OptArgsSpec, OptCallerPid, Timeout) when is_function(Cond, 2) -> + ArgsMatcher = meck_args_matcher:new(OptArgsSpec), + meck_proc:wait_for(Mod, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout). + %% @doc Erases the call history for a mocked module or a list of mocked modules. %% %% This function will erase all calls made heretofore from the history of the diff --git a/src/meck_proc.erl b/src/meck_proc.erl index 80bbf81..f7d2398 100644 --- a/src/meck_proc.erl +++ b/src/meck_proc.erl @@ -26,6 +26,7 @@ -export([list_expects/2]). -export([get_history/1]). -export([wait/6]). +-export([wait_for/6]). -export([reset/1]). -export([validate/1]). -export([stop/1]). @@ -64,10 +65,13 @@ trackers = [] :: [tracker()], restore = false :: boolean()}). +-type cond_state() :: term(). +-type cond_fun() :: fun((Args :: [term()], cond_state()) -> cond_state()). + -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()}, reply_to :: {Caller::pid(), Tag::any()}, expire_at :: erlang:timestamp()}). @@ -158,14 +162,24 @@ get_history(Mod) -> Timeout::non_neg_integer()) -> ok. wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout) -> - EffectiveTimeout = case Timeout of - 0 -> - infinity; - _Else -> - Timeout - end, + Cond = fun + (_, T) when T =:= 0 -> + {halt, ok}; + (_, T) -> + {cont, T - 1} + end, + wait_for(Mod, {Cond, Times - 1}, OptFunc, ArgsMatcher, OptCallerPid, Timeout). + +wait_for(Mod, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout) -> + EffectiveTimeout = + case Timeout of + 0 -> + infinity; + _Else -> + Timeout + end, Name = meck_util:proc_name(Mod), - try gen_server:call(Name, {wait, Times, OptFunc, ArgsMatcher, OptCallerPid, + try gen_server:call(Name, {wait_for, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout}, EffectiveTimeout) of @@ -304,18 +318,34 @@ handle_call(get_history, _From, S = #state{history = undefined}) -> {reply, [], S}; handle_call(get_history, _From, S) -> {reply, lists:reverse(S#state.history), S}; -handle_call({wait, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout}, From, +handle_call({wait_for, {Cond, CondState1}, OptFunc, ArgsMatcher, OptCallerPid, Timeout}, From, S = #state{history = History, trackers = Trackers}) -> - case times_called(OptFunc, ArgsMatcher, OptCallerPid, History) of - CalledSoFar when CalledSoFar >= Times -> + Filter = meck_history:new_filter(OptCallerPid, OptFunc, ArgsMatcher), + Result = lists:foldl( + fun(HistoryRec, {cont, CondState} = Acc) -> + case Filter(HistoryRec) of + true -> + {_Pid, {_M, _F, Args}, _Result} = HistoryRec, + Cond(Args, CondState); + false -> + Acc + end; + (_HistoryRec, {halt, _Reply} = Acc) -> + Acc + end, + {cont, CondState1}, + History + ), + case Result of + {halt, _Reply} -> {reply, ok, S}; - _CalledSoFar when Timeout =:= 0 -> + {cont, _} when Timeout =:= 0 -> {reply, {error, timeout}, S}; - CalledSoFar -> + {cont, CondState2} -> Tracker = #tracker{opt_func = OptFunc, args_matcher = ArgsMatcher, opt_caller_pid = OptCallerPid, - countdown = Times - CalledSoFar, + awaiting = {Cond, CondState2}, reply_to = From, expire_at = timeout_to_timestamp(Timeout)}, {noreply, S#state{trackers = [Tracker | Trackers]}} @@ -693,22 +723,6 @@ cleanup(Mod) -> Res. --spec times_called(OptFunc::'_' | atom(), - meck_args_matcher:args_matcher(), - OptCallerPid::'_' | pid(), - meck_history:history()) -> - non_neg_integer(). -times_called(OptFunc, ArgsMatcher, OptCallerPid, History) -> - Filter = meck_history:new_filter(OptCallerPid, OptFunc, ArgsMatcher), - lists:foldl(fun(HistoryRec, Acc) -> - case Filter(HistoryRec) of - true -> - Acc + 1; - _Else -> - Acc - end - end, 0, History). - -spec update_trackers(meck_history:history_record(), [tracker()]) -> UpdTracker::[tracker()]. update_trackers(HistoryRecord, Trackers) -> @@ -738,7 +752,7 @@ update_tracker(Func, Args, CallerPid, #tracker{opt_func = OptFunc, args_matcher = ArgsMatcher, opt_caller_pid = OptCallerPid, - countdown = Countdown, + awaiting = {Cond, CondState}, reply_to = ReplyTo, expire_at = ExpireAt} = Tracker) when (OptFunc =:= '_' orelse Func =:= OptFunc) andalso @@ -750,17 +764,20 @@ update_tracker(Func, Args, CallerPid, case is_expired(ExpireAt) of true -> expired; - false when Countdown == 1 -> - gen_server:reply(ReplyTo, ok), - expired; false -> - Tracker#tracker{countdown = Countdown - 1} + case Cond(Args, CondState) of + {halt, Result} -> + gen_server:reply(ReplyTo, Result), + expired; + {cont, CondState2} -> + Tracker#tracker{awaiting = {Cond, CondState2}} + end 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(). timeout_to_timestamp(Timeout) -> {MacroSecs, Secs, MicroSecs} = os:timestamp(), MicroSecs2 = MicroSecs + Timeout * 1000, diff --git a/test/meck_tests.erl b/test/meck_tests.erl index 0c33988..e9e6e68 100644 --- a/test/meck_tests.erl +++ b/test/meck_tests.erl @@ -1634,6 +1634,63 @@ wait_purge_expired_tracker_test() -> %% Clean meck:unload(). +wait_for_test() -> + %% Given + meck:new(test, [non_strict]), + meck:expect(test, foo, 2, ok), + %% When + Pid = erlang:spawn(fun() -> + test:foo(1, 1), + test:foo(1, 2) + end), + %% Then + Cond = fun([_A, B], Expected) -> + case Expected -- [B] of + [] -> {halt, ok}; + Remaining -> {cont, Remaining} + end + end, + meck:wait_for( + {Cond, [1, 2]}, + test, + foo, + ['_', '_'], + Pid, + 1 + ), + %% Clean + meck:unload(). + +wait_for_fails_test() -> + %% Given + meck:new(test, [non_strict]), + meck:expect(test, foo, 2, ok), + %% When + Pid = erlang:spawn(fun() -> + test:foo(1, 1), + test:foo(1, 2) + end), + %% Then + Cond = fun([_A, B], Expected) -> + case Expected -- [B] of + [] -> {halt, ok}; + Remaining -> {cont, Remaining} + end + end, + ?assertError( + timeout, + meck:wait_for( + {Cond, [1, 2, 3]}, + test, + foo, + ['_', '_'], + Pid, + 1 + ) + ), + %% Clean + meck:unload(). + mocked_test() -> %% At start, no modules should be mocked: [] = meck:mocked(), From 367bfe4b0ddea246527a608fc1770b01717fe7b4 Mon Sep 17 00:00:00 2001 From: Roger Lipscombe Date: Thu, 18 Dec 2025 16:15:37 +0000 Subject: [PATCH 2/3] Replace wait_for/6 with a variant of wait/6 --- src/meck_proc.erl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/meck_proc.erl b/src/meck_proc.erl index f7d2398..3412535 100644 --- a/src/meck_proc.erl +++ b/src/meck_proc.erl @@ -26,7 +26,6 @@ -export([list_expects/2]). -export([get_history/1]). -export([wait/6]). --export([wait_for/6]). -export([reset/1]). -export([validate/1]). -export([stop/1]). @@ -65,6 +64,7 @@ trackers = [] :: [tracker()], restore = false :: boolean()}). +-type condition() :: {cond_fun(), cond_state()}. -type cond_state() :: term(). -type cond_fun() :: fun((Args :: [term()], cond_state()) -> cond_state()). @@ -155,7 +155,7 @@ get_history(Mod) -> gen_server(call, Mod, get_history). -spec wait(Mod::atom(), - Times::non_neg_integer(), + Times::non_neg_integer() | Condition::condition(), OptFunc::'_' | atom(), meck_args_matcher:args_matcher(), OptCallerPid::'_' | pid(), @@ -168,9 +168,8 @@ wait(Mod, Times, OptFunc, ArgsMatcher, OptCallerPid, Timeout) -> (_, T) -> {cont, T - 1} end, - wait_for(Mod, {Cond, Times - 1}, OptFunc, ArgsMatcher, OptCallerPid, Timeout). - -wait_for(Mod, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout) -> + wait(Mod, {Cond, Times - 1}, OptFunc, ArgsMatcher, OptCallerPid, Timeout); +wait(Mod, {Cond, CondState}, OptFunc, ArgsMatcher, OptCallerPid, Timeout) -> EffectiveTimeout = case Timeout of 0 -> From f2324e0999bf669fc9f5a11aa453bbdafb29000e Mon Sep 17 00:00:00 2001 From: Roger Lipscombe Date: Thu, 18 Dec 2025 16:15:46 +0000 Subject: [PATCH 3/3] Rename 'waiting' to 'condition' --- src/meck_proc.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/meck_proc.erl b/src/meck_proc.erl index 3412535..a47e7bf 100644 --- a/src/meck_proc.erl +++ b/src/meck_proc.erl @@ -71,7 +71,7 @@ -record(tracker, {opt_func :: '_' | atom(), args_matcher :: meck_args_matcher:args_matcher(), opt_caller_pid :: '_' | pid(), - awaiting :: {cond_fun(), cond_state()}, + condition :: condition(), reply_to :: {Caller::pid(), Tag::any()}, expire_at :: erlang:timestamp()}). @@ -344,7 +344,7 @@ handle_call({wait_for, {Cond, CondState1}, OptFunc, ArgsMatcher, OptCallerPid, T Tracker = #tracker{opt_func = OptFunc, args_matcher = ArgsMatcher, opt_caller_pid = OptCallerPid, - awaiting = {Cond, CondState2}, + condition = {Cond, CondState2}, reply_to = From, expire_at = timeout_to_timestamp(Timeout)}, {noreply, S#state{trackers = [Tracker | Trackers]}} @@ -751,7 +751,7 @@ update_tracker(Func, Args, CallerPid, #tracker{opt_func = OptFunc, args_matcher = ArgsMatcher, opt_caller_pid = OptCallerPid, - awaiting = {Cond, CondState}, + condition = {Cond, CondState}, reply_to = ReplyTo, expire_at = ExpireAt} = Tracker) when (OptFunc =:= '_' orelse Func =:= OptFunc) andalso @@ -769,7 +769,7 @@ update_tracker(Func, Args, CallerPid, gen_server:reply(ReplyTo, Result), expired; {cont, CondState2} -> - Tracker#tracker{awaiting = {Cond, CondState2}} + Tracker#tracker{condition = {Cond, CondState2}} end end end;