1: -module(gen_hook_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("eunit/include/eunit.hrl").
    5: 
    6: -define(HOOK_TAG1, global).
    7: -define(HOOK_TAG2, <<"some tag">>).
    8: 
    9: -define(assertEqualLists(L1, L2), ?assertEqual(lists:sort(L1), lists:sort(L2))).
   10: 
   11: all() ->
   12:     [single_handler_can_be_added_and_removed,
   13:      multiple_handlers_can_be_added_and_removed,
   14: 
   15:      local_fun_references_causes_error,
   16:      anonymous_fun_references_causes_error,
   17:      not_exported_external_fun_references_causes_error,
   18:      invalid_hook_handler_parameters_causes_error,
   19: 
   20:      run_fold_executes_handlers_in_the_right_order,
   21:      run_fold_stops_when_handler_returns_stop,
   22: 
   23:      errors_in_handlers_are_reported_but_ignored].
   24: 
   25: init_per_suite(Config) ->
   26:     application:ensure_all_started(exometer_core),
   27:     mongoose_config:set_opts(#{all_metrics_are_global => false}),
   28:     Config.
   29: 
   30: end_per_suite(Config) ->
   31:     mongoose_config:erase_opts(),
   32:     application:stop(exometer_core),
   33:     Config.
   34: 
   35: init_per_testcase(_, Config) ->
   36:     mongooseim_helper:start_link_loaded_hooks(),
   37:     Config.
   38: 
   39: end_per_testcase(_, Config) ->
   40:     meck:unload(),
   41:     Config.
   42: 
   43: 
   44: %%----------------------------------------------------------------
   45: %% test cases
   46: %%----------------------------------------------------------------
   47: single_handler_can_be_added_and_removed(_) ->
   48:     meck:new([mod1, mod2], [non_strict]),
   49:     PlusHandlerFn = get_hook_handler(mod1, plus, fun hook_handler_plus/3),
   50:     MultiplyHandlerFn = get_hook_handler(mod2, multiply, fun hook_handler_multiply/3),
   51:     %% check that there are no hook handlers added yet
   52:     ?assertEqual([], get_handlers_for_all_hooks()),
   53:     %% add various hook handlers
   54:     ?assertEqual(ok, gen_hook:add_handler(calculate, ?HOOK_TAG1, MultiplyHandlerFn,
   55:                                           #{id => 2}, 2)),
   56:     ?assertEqual(ok, gen_hook:add_handler(calculate, ?HOOK_TAG1, PlusHandlerFn,
   57:                                           #{id => 1}, 1)),
   58:     ?assertEqual(ok, gen_hook:add_handler(calculate, ?HOOK_TAG2, PlusHandlerFn,
   59:                                           #{id => 1}, 1)),
   60:     %% check that hook handlers are added
   61:     Tag1Handlers = [%% this list must be sorted by priority
   62:                     {hook_handler, 1, PlusHandlerFn,
   63:                      #{hook_name => calculate, hook_tag => ?HOOK_TAG1, id => 1}},
   64:                     {hook_handler, 2, MultiplyHandlerFn,
   65:                      #{hook_name => calculate, hook_tag => ?HOOK_TAG1, id => 2}}],
   66:     AllHandlers = [{{calculate, ?HOOK_TAG1}, Tag1Handlers},
   67:                    {{calculate, ?HOOK_TAG2},
   68:                     [{hook_handler, 1, PlusHandlerFn,
   69:                       #{hook_name => calculate, hook_tag => ?HOOK_TAG2,
   70:                         host_type =>?HOOK_TAG2, id => 1}}]}],
   71:     ?assertEqualLists(AllHandlers, get_handlers_for_all_hooks()),
   72:     %% try to add some hook handler second time and check that nothing has changed
   73:     ?assertEqual(ok, gen_hook:add_handler(calculate, ?HOOK_TAG1, MultiplyHandlerFn,
   74:                                           #{id => 2}, 2)),
   75:     ?assertEqualLists(AllHandlers, get_handlers_for_all_hooks()),
   76:     %% try to remove hook handler for ?HOOK_TAG2 and check that it's removed
   77:     ?assertEqual(ok, gen_hook:delete_handler(calculate, ?HOOK_TAG2, PlusHandlerFn,
   78:                                              #{id => 1}, 1)),
   79:     ?assertEqualLists([{{calculate, ?HOOK_TAG1}, Tag1Handlers},
   80:                        {{calculate, ?HOOK_TAG2}, []}],
   81:                       get_handlers_for_all_hooks()),
   82:     %% try to remove hook handler for ?HOOK_TAG2 second time
   83:     %% and check that nothing has changed
   84:     ?assertEqual(ok, gen_hook:delete_handler(calculate, ?HOOK_TAG2, PlusHandlerFn,
   85:                                              #{id => 1}, 1)),
   86:     ?assertEqualLists([{{calculate, ?HOOK_TAG1}, Tag1Handlers},
   87:                        {{calculate, ?HOOK_TAG2}, []}],
   88:                       get_handlers_for_all_hooks()),
   89:     %% try to remove hook handlers for ?HOOK_TAG1 and check that they are removed
   90:     ?assertEqual(ok, gen_hook:delete_handler(calculate, ?HOOK_TAG1, MultiplyHandlerFn,
   91:                                              #{id => 2}, 2)),
   92:     ?assertEqual(ok, gen_hook:delete_handler(calculate, ?HOOK_TAG1, PlusHandlerFn,
   93:                                              #{id => 1}, 1)),
   94:     ?assertEqualLists([{{calculate, ?HOOK_TAG1}, []}, {{calculate, ?HOOK_TAG2}, []}],
   95:                       get_handlers_for_all_hooks()).
   96: 
   97: multiple_handlers_can_be_added_and_removed(_) ->
   98:     meck:new([mod1, mod2], [non_strict]),
   99:     PlusHandlerFn = get_hook_handler(mod1, plus, fun hook_handler_plus/3),
  100:     MultiplyHandlerFn = get_hook_handler(mod2, multiply, fun hook_handler_multiply/3),
  101:     %% check that there are no hook handlers added yet
  102:     ?assertEqual([], get_handlers_for_all_hooks()),
  103:     %% add various hook handlers
  104:     HookHandlers = [{calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{id => 2}, 2},
  105:                     {calculate, ?HOOK_TAG2, PlusHandlerFn, #{id => 1}, 1},
  106:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{id => 1}, 1}],
  107:     ?assertEqual(ok, gen_hook:add_handlers(HookHandlers)),
  108:     %% check that hook handlers are added
  109:     Tag1Handlers = [%% this list must be sorted by priority
  110:                     {hook_handler, 1, PlusHandlerFn,
  111:                      #{hook_name => calculate, hook_tag => ?HOOK_TAG1, id => 1}},
  112:                     {hook_handler, 2, MultiplyHandlerFn,
  113:                      #{hook_name => calculate, hook_tag => ?HOOK_TAG1, id => 2}}],
  114:     AllHandlers = [{{calculate, ?HOOK_TAG1}, Tag1Handlers},
  115:                    {{calculate, ?HOOK_TAG2},
  116:                     [{hook_handler, 1, PlusHandlerFn,
  117:                       #{hook_name => calculate, hook_tag => ?HOOK_TAG2,
  118:                         host_type =>?HOOK_TAG2, id => 1}}]}],
  119:     ?assertEqualLists(AllHandlers, get_handlers_for_all_hooks()),
  120:     %% try to add hook handlers second time and check that nothing has changed
  121:     ?assertEqual(ok, gen_hook:add_handlers(HookHandlers)),
  122:     ?assertEqualLists(AllHandlers, get_handlers_for_all_hooks()),
  123:     %% try to remove hook handlers and check that they are removed
  124:     ?assertEqual(ok, gen_hook:delete_handlers(HookHandlers)),
  125:     ?assertEqualLists([{{calculate, ?HOOK_TAG1}, []}, {{calculate, ?HOOK_TAG2}, []}],
  126:                       get_handlers_for_all_hooks()),
  127:     %% try to remove hook handlers second time and check that nothing has changed
  128:     ?assertEqual(ok, gen_hook:delete_handlers(HookHandlers)),
  129:     ?assertEqualLists([{{calculate, ?HOOK_TAG1}, []}, {{calculate, ?HOOK_TAG2}, []}],
  130:                       get_handlers_for_all_hooks()).
  131: 
  132: local_fun_references_causes_error(_) ->
  133:     meck:new([mod1, mod2], [non_strict]),
  134:     PlusHandlerFn = get_hook_handler(mod1, plus, fun hook_handler_plus/3),
  135:     MultiplyHandlerFn = get_hook_handler(mod2, multiply, fun hook_handler_multiply/3),
  136:     %% check that there are no hook handlers added yet
  137:     ?assertEqual([], get_handlers_for_all_hooks()),
  138:     %% try to add multiple hook handlers, when one of them uses local function reference
  139:     LocalFunctionReference = fun hook_handler_plus/3,
  140:     HookHandlers = [{calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{id => 2}, 2},
  141:                     {calculate, ?HOOK_TAG2, LocalFunctionReference, #{id => 1}, 1},
  142:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{id => 1}, 1}],
  143:     ?assertError(#{what := only_external_function_references_allowed,
  144:                    function := LocalFunctionReference},
  145:                  gen_hook:add_handlers(HookHandlers)),
  146:     %% check that handlers in the list are partially added (till error occurs)
  147:     ?assertEqual([{{calculate, ?HOOK_TAG1},
  148:                    [{hook_handler, 2, MultiplyHandlerFn,
  149:                      #{hook_name => calculate, hook_tag => ?HOOK_TAG1, id => 2}}]}],
  150:                  get_handlers_for_all_hooks()),
  151:     %% try to remove the same list of handlers
  152:     ?assertError(#{what := only_external_function_references_allowed,
  153:                    function := LocalFunctionReference},
  154:                  gen_hook:delete_handlers(HookHandlers)),
  155:     %% check that partially added handlers are removed
  156:     ?assertEqual([{{calculate, ?HOOK_TAG1}, []}], get_handlers_for_all_hooks()).
  157: 
  158: anonymous_fun_references_causes_error(_) ->
  159:     %% check that there are no hook handlers added yet
  160:     ?assertEqual([], get_handlers_for_all_hooks()),
  161:     %% try to add hook handler using anonymous function reference
  162:     AnonymousFunctionReference = fun(Acc, _, _) -> {ok, Acc} end,
  163:     ?assertError(#{what := only_external_function_references_allowed,
  164:                    function := AnonymousFunctionReference},
  165:                  gen_hook:add_handler(calculate, ?HOOK_TAG1, AnonymousFunctionReference,
  166:                                       #{id => 2}, 2)),
  167:     %% check that nothing is added
  168:     ?assertEqual([], get_handlers_for_all_hooks()).
  169: 
  170: not_exported_external_fun_references_causes_error(_) ->
  171:     %% check that there are no hook handlers added yet
  172:     ?assertEqual([], get_handlers_for_all_hooks()),
  173:     %% try to add hook handler using function reference for a missing module
  174:     NotExportedExternalFunctionReference1 = fun missing_module:missing_function/3,
  175:     ?assertError(#{what := module_is_not_loaded, module := missing_module},
  176:                  gen_hook:add_handler(calculate, ?HOOK_TAG1,
  177:                                       NotExportedExternalFunctionReference1,
  178:                                       #{id => 2}, 2)),
  179:     %% try to add hook handler using function reference for a missing module
  180:     NotExportedExternalFunctionReference2 = fun ?MODULE:missing_function/3,
  181:     ?assertError(#{what := function_is_not_exported,
  182:                    function := NotExportedExternalFunctionReference2},
  183:                  gen_hook:add_handler(calculate, ?HOOK_TAG1,
  184:                                       NotExportedExternalFunctionReference2,
  185:                                       #{id => 2}, 2)),
  186:     %% check that nothing is added
  187:     ?assertEqual([], get_handlers_for_all_hooks()).
  188: 
  189: invalid_hook_handler_parameters_causes_error(_) ->
  190:     %% check that there are no hook handlers added yet
  191:     ?assertEqual([], get_handlers_for_all_hooks()),
  192:     HandlerFn = fun ?MODULE:hook_handler_stop/3,
  193:     InvalidHookHandlers = [{calculate, ?HOOK_TAG1, HandlerFn, invalid_extra_param, 2},
  194:                            {<<"invalid hook name">>, ?HOOK_TAG1, HandlerFn, #{}, 2},
  195:                            {calculate, ?HOOK_TAG1, HandlerFn, #{}, invalid_priority},
  196:                            {calculate, invalid_hook_tag, HandlerFn, #{}, 2}],
  197:     [?assertError(function_clause, gen_hook:add_handlers([HookHandler]))
  198:      || HookHandler <- InvalidHookHandlers],
  199:     ?assertEqual([], get_handlers_for_all_hooks()).
  200: 
  201: run_fold_executes_handlers_in_the_right_order(_) ->
  202:     meck:new(mod1, [non_strict]),
  203:     PlusHandlerFn = get_hook_handler(mod1, plus, fun hook_handler_plus/3),
  204:     MultiplyHandlerFn = get_hook_handler(mod1, multiply, fun hook_handler_multiply/3),
  205:     %% check that there are no hook handlers added yet
  206:     ?assertEqual([], get_handlers_for_all_hooks()),
  207:     %% add various hook handlers
  208:     HookHandlers = [{calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{n => 5}, 5},
  209:                     {calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{}, 2},
  210:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{n => 3}, 1},
  211:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{}, 4}],
  212:     ?assertEqual(ok, gen_hook:add_handlers(HookHandlers)),
  213:     %% run the hook
  214:     N = (((0 + 3) * 2) + 2) * 5, %% 40
  215:     ?assertEqual({ok, N}, gen_hook:run_fold(calculate, ?HOOK_TAG1, 0, #{n => 2})),
  216:     %% check hook handlers execution sequence
  217:     Self = self(),
  218:     ?assertEqual([{Self,
  219:                    {mod1, plus, [0, #{n => 2}, #{hook_name => calculate, n => 3,
  220:                                                  hook_tag => ?HOOK_TAG1}]},
  221:                    {ok, 3}},
  222:                   {Self,
  223:                    {mod1, multiply, [3, #{n => 2}, #{hook_name => calculate,
  224:                                                      hook_tag => ?HOOK_TAG1}]},
  225:                    {ok, 6}},
  226:                   {Self,
  227:                    {mod1, plus, [6, #{n => 2}, #{hook_name => calculate,
  228:                                                  hook_tag => ?HOOK_TAG1}]},
  229:                    {ok, 8}},
  230:                   {Self,
  231:                    {mod1, multiply, [8, #{n => 2}, #{hook_name => calculate, n => 5,
  232:                                                      hook_tag => ?HOOK_TAG1}]},
  233:                    {ok, 40}}],
  234:                  meck:history(mod1)).
  235: 
  236: run_fold_stops_when_handler_returns_stop(_) ->
  237:     meck:new(mod1, [non_strict]),
  238:     PlusHandlerFn = get_hook_handler(mod1, plus, fun hook_handler_plus/3),
  239:     StopHandlerFn = get_hook_handler(mod1, stop, fun hook_handler_stop/3),
  240:     MultiplyHandlerFn = get_hook_handler(mod1, multiply, fun hook_handler_multiply/3),
  241:     %% check that there are no hook handlers added yet
  242:     ?assertEqual([], get_handlers_for_all_hooks()),
  243:     %% add various hook handlers
  244:     HookHandlers = [{calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{n => 5}, 5},
  245:                     {calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{}, 2},
  246:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{n => 3}, 1},
  247:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{}, 4},
  248:                     {calculate, ?HOOK_TAG1, StopHandlerFn, #{}, 3}],
  249:     ?assertEqual(ok, gen_hook:add_handlers(HookHandlers)),
  250:     %% run the hook
  251:     N = ((0 + 3) * 2), %% 6
  252:     ?assertEqual({stop, N}, gen_hook:run_fold(calculate, ?HOOK_TAG1, 0, #{n => 2})),
  253:     %% check hook handlers execution sequence
  254:     Self = self(),
  255:     ?assertEqual([{Self,
  256:                    {mod1, plus, [0, #{n => 2}, #{hook_name => calculate, n => 3,
  257:                                                  hook_tag => ?HOOK_TAG1}]},
  258:                    {ok, 3}},
  259:                   {Self,
  260:                    {mod1, multiply, [3, #{n => 2}, #{hook_name => calculate,
  261:                                                      hook_tag => ?HOOK_TAG1}]},
  262:                    {ok, 6}},
  263:                   {Self,
  264:                    {mod1, stop, [6, #{n => 2}, #{hook_name => calculate,
  265:                                                  hook_tag => ?HOOK_TAG1}]},
  266:                    {stop, 6}}],
  267:                  meck:history(mod1)).
  268: 
  269: errors_in_handlers_are_reported_but_ignored(_) ->
  270:     meck:new(mod1, [non_strict]),
  271:     meck:new(gen_hook, [passthrough]),
  272:     PlusHandlerFn = get_hook_handler(mod1, plus, fun hook_handler_plus/3),
  273:     ErrorHandlerFn = get_hook_handler(mod1, error, fun hook_handler_error/3),
  274:     MultiplyHandlerFn = get_hook_handler(mod1, multiply, fun hook_handler_multiply/3),
  275:     %% check that there are no hook handlers added yet
  276:     ?assertEqual([], get_handlers_for_all_hooks()),
  277:     %% add various hook handlers
  278:     HookHandlers = [{calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{n => 5}, 5},
  279:                     {calculate, ?HOOK_TAG1, MultiplyHandlerFn, #{}, 2},
  280:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{n => 3}, 1},
  281:                     {calculate, ?HOOK_TAG1, PlusHandlerFn, #{}, 4},
  282:                     {calculate, ?HOOK_TAG1, ErrorHandlerFn, #{}, 3}],
  283:     ?assertEqual(ok, gen_hook:add_handlers(HookHandlers)),
  284:     %% run the hook
  285:     N = (((0 + 3) * 2) + 2) * 5, %% 40
  286:     ?assertEqual({ok, N}, gen_hook:run_fold(calculate, ?HOOK_TAG1, 0, #{n => 2})),
  287:     %% check that error is reported
  288:     ?assertEqual(true, meck:called(gen_hook, error_running_hook,
  289:                                    [#{class => error, reason => some_error, stacktrace => '_'},
  290:                                     {hook_handler, 3, ErrorHandlerFn,
  291:                                      #{hook_name => calculate, hook_tag => ?HOOK_TAG1}},
  292:                                     6, #{n => 2}, {calculate, ?HOOK_TAG1}])),
  293:     %% check hook handlers execution sequence
  294:     Self = self(),
  295:     ?assertMatch([{Self,
  296:                    {mod1, plus, [0, #{n := 2}, #{hook_name := calculate, n := 3,
  297:                                                  hook_tag := ?HOOK_TAG1}]},
  298:                    {ok, 3}},
  299:                   {Self,
  300:                    {mod1, multiply, [3, #{n := 2}, #{hook_name := calculate,
  301:                                                      hook_tag := ?HOOK_TAG1}]},
  302:                    {ok, 6}},
  303:                   {Self,
  304:                    {mod1, error, [6, #{n := 2}, #{hook_name := calculate,
  305:                                                   hook_tag := ?HOOK_TAG1}]},
  306:                    error, some_error, _},
  307:                   {Self,
  308:                    {mod1, plus, [6, #{n := 2}, #{hook_name := calculate,
  309:                                                  hook_tag := ?HOOK_TAG1}]},
  310:                    {ok, 8}},
  311:                   {Self,
  312:                    {mod1, multiply, [8, #{n := 2}, #{hook_name := calculate, n := 5,
  313:                                                      hook_tag := ?HOOK_TAG1}]},
  314:                    {ok, 40}}],
  315:                  meck:history(mod1)).
  316: 
  317: %%----------------------------------------------------------------
  318: %% helper functions
  319: %%----------------------------------------------------------------
  320: hook_handler_plus(Acc, _, #{n := N}) ->
  321:     {ok, Acc + N};
  322: hook_handler_plus(Acc, #{n := N}, _) ->
  323:     {ok, Acc + N}.
  324: 
  325: hook_handler_multiply(Acc, _, #{n := N}) ->
  326:     {ok, Acc * N};
  327: hook_handler_multiply(Acc, #{n := N}, _) ->
  328:     {ok, Acc * N}.
  329: 
  330: hook_handler_stop(Acc, _, _) ->
  331:     {stop, Acc}.
  332: 
  333: hook_handler_error(_, _, _) ->
  334:     error(some_error).
  335: 
  336: get_hook_handler(ModName, FunName, Fun) when is_function(Fun, 3) ->
  337:     meck:expect(ModName, FunName, Fun),
  338:     fun ModName:FunName/3.
  339: 
  340: get_handlers_for_all_hooks() ->
  341:     maps:to_list(persistent_term:get(gen_hook, #{})).