├── test ├── test_helper.exs ├── support │ └── mocks.ex ├── intercepted_modules │ ├── do_block │ │ ├── intercepted_edge_cases.ex │ │ ├── intercepted_on_error.ex │ │ ├── intercepted_on_before.ex │ │ ├── intercepted_private_on_success_on_error_own_configuration.ex │ │ ├── intercepted_by_wrapper.ex │ │ ├── intercepted_wildcarded_mfa_callbacks.ex │ │ ├── intercepted_on_after.ex │ │ ├── intercepted_on_after_own_configuration.ex │ │ └── intercepted_on_success.ex │ ├── annotated │ │ ├── annotated_intercepted_on_error.ex │ │ ├── annotated_intercepted_edge_cases.ex │ │ ├── annotated_intercepted_on_before.ex │ │ ├── annotated_intercepted_by_wrapper.ex │ │ ├── annotated_intercepted_wildcarded_mfa_callbacks.ex │ │ ├── annotated_intercepted_on_after_own_configuration.ex │ │ ├── annotated_intercepted_on_after.ex │ │ └── annotated_intercepted_on_success.ex │ ├── wrong_configs_for_config_validator.ex │ ├── streamlined_intercept_config.ex │ └── intercept_config.ex ├── configuration │ ├── validator_test.exs │ └── configurator_test.exs ├── do_block │ ├── interceptor_edge_cases_test.exs │ ├── interceptor_private_own_configuration_on_success_error_test.exs │ ├── interceptor_on_after_own_configuration_test.exs │ ├── interceptor_on_before_test.exs │ ├── interceptor_wrapper_test.exs │ ├── interceptor_on_error_test.exs │ ├── interceptor_wildcarded_mfa_test.exs │ ├── interceptor_on_after_test.exs │ └── interceptor_on_success_test.exs ├── utils_test.exs ├── annotated │ ├── annotated_interceptor_on_after_own_configuration_test.exs │ ├── annotated_interceptor_on_before_test.exs │ ├── annotated_interceptor_edge_cases_test.exs │ ├── annotated_interceptor_wrapper_test.exs │ ├── annotated_interceptor_on_error_test.exs │ ├── annotated_interceptor_wildcarded_mfa_test.exs │ ├── annotated_interceptor_on_after_test.exs │ └── annotated_interceptor_on_success_test.exs └── function_arguments_test.exs ├── .formatter.exs ├── assets └── images │ ├── interceptor_logo_small.png │ ├── interceptor_logo_with_title.png │ └── interceptor_logo.svg ├── config ├── config.exs ├── test.exs └── dev.exs ├── lib ├── debug.ex ├── configuration │ ├── searcher.ex │ ├── validator.ex │ ├── configurator.ex │ └── configuration.ex ├── utils.ex ├── function_arguments.ex └── annotated_interceptor.ex ├── .gitignore ├── .github └── workflows │ ├── every_branch.yml │ └── main.yml ├── LICENSE ├── mix.exs ├── CHANGELOG.md ├── mix.lock └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/support/mocks.ex: -------------------------------------------------------------------------------- 1 | Mox.defmock(Interceptor.Configuration.SearcherMock, for: Interceptor.Configuration.Searcher) 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /assets/images/interceptor_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalbuquerque/interceptor/HEAD/assets/images/interceptor_logo_small.png -------------------------------------------------------------------------------- /assets/images/interceptor_logo_with_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalbuquerque/interceptor/HEAD/assets/images/interceptor_logo_with_title.png -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :interceptor, 4 | config_searcher: Interceptor.Configuration.Searcher 5 | 6 | import_config "#{Mix.env()}.exs" 7 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :interceptor, 4 | config_searcher: Interceptor.Configuration.SearcherMock, 5 | configuration: InterceptConfig, 6 | debug: true 7 | -------------------------------------------------------------------------------- /lib/debug.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Debug do 2 | alias Interceptor.Configuration 3 | 4 | def debug_message(message) do 5 | case Configuration.debug_mode? do 6 | true -> 7 | IO.puts(message) 8 | _ -> :nop 9 | end 10 | end 11 | 12 | def debug_ast(ast) do 13 | debug_message("############################## Will return the following:") 14 | 15 | ast 16 | |> Macro.to_string() 17 | |> debug_message() 18 | 19 | debug_message("############################## Will return the following AST") 20 | IO.inspect(ast) 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :interceptor, configuration: %{ 4 | {Pig, :hi_there, 0} => [ 5 | on_success: {Outsider, :on_success, 3} 6 | ], 7 | {Pig, :hi_there_big, 0} => [ 8 | on_success: {Outsider, :on_success, 3} 9 | ], 10 | {Pig, :hi, 1} => [ 11 | on_success: {Outsider, :on_success, 3} 12 | ], 13 | {Foo, :abc, 1} => [ 14 | before: {Outsider, :before, 1}, 15 | after: {Outsider, :right_after, 2}, 16 | on_success: {Outsider, :on_success, 3}, 17 | on_error: {Outsider, :on_error, 3}, 18 | ], 19 | {Foo, :yyy, 0} => [ 20 | wrapper: {Outsider, :wrapper, 2} 21 | ] 22 | }, 23 | debug: false 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | interceptor-*.tar 24 | 25 | .elixir_ls/ 26 | -------------------------------------------------------------------------------- /.github/workflows/every_branch.yml: -------------------------------------------------------------------------------- 1 | name: every branch 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | container: 11 | image: elixir:1.9.1-slim 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: Install Dependencies 17 | run: | 18 | mix local.rebar --force 19 | mix local.hex --force 20 | mix deps.get 21 | 22 | - name: Run Tests and Coveralls 23 | env: 24 | COVERALLS_REPO_TOKEN: ${{ secrets.coveralls_repo_token }} 25 | MIX_ENV: test 26 | run: mix coveralls.post --branch "${GITHUB_REF}" --name "GH ${GITHUB_WORKFLOW}" --committer "${GITHUB_ACTOR}" --sha "${GITHUB_SHA}" 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | container: 14 | image: elixir:1.9.1-slim 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | 19 | - name: Install Dependencies 20 | run: | 21 | mix local.rebar --force 22 | mix local.hex --force 23 | mix deps.get 24 | 25 | - name: Run Tests and Coveralls 26 | env: 27 | COVERALLS_REPO_TOKEN: ${{ secrets.coveralls_repo_token }} 28 | MIX_ENV: test 29 | run: mix coveralls.post --branch "${GITHUB_REF}" --name "GH ${GITHUB_WORKFLOW}" --committer "${GITHUB_ACTOR}" --sha "${GITHUB_SHA}" 30 | -------------------------------------------------------------------------------- /lib/configuration/searcher.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Configuration.Searcher do 2 | 3 | @callback search_intercept_config_modules() :: list() 4 | 5 | @standard_apps [:mix, :compiler, :logger, :ssl, :hex, :kernel, :public_key, :stdlib, :crypto, :elixir, :inets, :asn1, :iex, :makeup_elixir, :earmark, :nimble_parsec, :ex_doc, :makeup, :interceptor] 6 | 7 | @get_intercept_config_function :get_intercept_config 8 | 9 | def search_intercept_config_modules do 10 | Application.loaded_applications() 11 | |> Enum.map(fn {app, _desc, _version} -> app end) 12 | |> Enum.reject(fn app -> app in @standard_apps end) 13 | |> Enum.flat_map(&Application.spec(&1, :modules)) 14 | |> Enum.filter(&function_exported?(&1, @get_intercept_config_function, 0)) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_edge_cases.ex: -------------------------------------------------------------------------------- 1 | defmodule EdgeCases.Callbacks do 2 | def success_cb({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:edge_cases_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | end 8 | 9 | def error_cb({_module, _function, _args} = mfa, result, started_at) do 10 | Agent.update(:edge_cases_test_process, 11 | fn messages -> 12 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 13 | end) 14 | end 15 | end 16 | 17 | defmodule InterceptedEdgeCases1 do 18 | require Interceptor, as: I 19 | 20 | I.intercept do 21 | def to_intercept(a, b, _to_ignore), do: "#{a} #{b}" 22 | 23 | def intercept_with_prefix("some_prefix:" <> suffix), do: suffix 24 | 25 | def intercept_pattern_match_atom_argument(a, :normal) do 26 | "Normal #{a}" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 André Albuquerque 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_on_error.ex: -------------------------------------------------------------------------------- 1 | defmodule OnError.Callback do 2 | def on_error({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:on_error_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule InterceptedOnError1 do 12 | require Interceptor, as: I 13 | 14 | I.intercept do 15 | def to_intercept(), do: 1/0 16 | end 17 | end 18 | 19 | defmodule InterceptedOnError2 do 20 | require Interceptor, as: I 21 | 22 | I.intercept do 23 | def to_intercept(), do: Process.sleep(200) && 2/0 24 | def other_to_intercept(), do: 3/0 25 | 26 | IO.puts("This statement doesn't interfere in any way") 27 | end 28 | end 29 | 30 | defmodule InterceptedOnError3 do 31 | require Interceptor, as: I 32 | 33 | def definitely_not_to_intercept(), do: length("No macros plz") 34 | 35 | I.intercept do 36 | def not_to_intercept(), do: length("Not intercepted") 37 | def other_to_intercept(w), do: (w + private_function(1, 2, 3))/0 38 | 39 | defp private_function(x, y, z), do: x+y+z 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_on_error.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedOnError.Callback do 2 | def on_error({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:annotated_on_error_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule AnnotatedInterceptedOnError1 do 12 | use Interceptor.Annotated 13 | 14 | @intercept true 15 | def to_intercept(), do: 1/0 16 | end 17 | 18 | defmodule AnnotatedInterceptedOnError2 do 19 | use Interceptor.Annotated 20 | 21 | @intercept true 22 | def to_intercept(), do: Process.sleep(200) && 2/0 23 | 24 | @intercept true 25 | def other_to_intercept(), do: 3/0 26 | 27 | IO.puts("This statement doesn't interfere in any way") 28 | end 29 | 30 | defmodule AnnotatedInterceptedOnError3 do 31 | use Interceptor.Annotated 32 | 33 | def definitely_not_to_intercept(), do: length("No macros plz") 34 | 35 | @intercept true 36 | def not_to_intercept(), do: length("Not intercepted") 37 | 38 | @intercept true 39 | def other_to_intercept(w), do: (w + private_function(1, 2, 3))/0 40 | 41 | @intercept true 42 | defp private_function(x, y, z), do: x+y+z 43 | end 44 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_on_before.ex: -------------------------------------------------------------------------------- 1 | defmodule Before.Callback do 2 | def before({_module, _function, _args} = mfa) do 3 | Agent.update(:before_test_process, 4 | fn messages -> 5 | [{Interceptor.Utils.timestamp(), mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule InterceptedOnBefore1 do 12 | require Interceptor, as: I 13 | 14 | I.intercept do 15 | def to_intercept(), do: Interceptor.Utils.timestamp() 16 | end 17 | end 18 | 19 | defmodule InterceptedOnBefore2 do 20 | require Interceptor, as: I 21 | 22 | I.intercept do 23 | def to_intercept(), do: Interceptor.Utils.timestamp() 24 | def other_to_intercept(), do: "HELLO" 25 | 26 | IO.puts("This statement doesn't interfere in any way") 27 | end 28 | end 29 | 30 | defmodule InterceptedOnBefore3 do 31 | require Interceptor, as: I 32 | 33 | I.intercept do 34 | def not_to_intercept(), do: Interceptor.Utils.timestamp() 35 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 36 | 37 | defp private_function(x, y, z), do: x+y+z 38 | end 39 | end 40 | 41 | defmodule InterceptedOnBefore4 do 42 | require Interceptor, as: I 43 | 44 | I.intercept do 45 | def to_intercept, do: "Hello, even without args" 46 | 47 | def bla, do: 123 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_edge_cases.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedEdgeCases.Callbacks do 2 | def success_cb({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:annotated_edge_cases_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | end 8 | 9 | def error_cb({_module, _function, _args} = mfa, result, started_at) do 10 | Agent.update(:annotated_edge_cases_test_process, 11 | fn messages -> 12 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 13 | end) 14 | end 15 | end 16 | 17 | defmodule AnnotatedInterceptedEdgeCases1 do 18 | use Interceptor.Annotated 19 | 20 | @intercept true 21 | def to_intercept(a, b, _to_ignore), do: "#{a} #{b}" 22 | 23 | @intercept true 24 | def intercept_with_prefix("some_prefix:" <> abc), do: abc 25 | 26 | @intercept true 27 | def intercept_pattern_match_atom_argument(a, :normal) do 28 | "Normal #{a}" 29 | end 30 | end 31 | 32 | defmodule AnnotatedInterceptedEdgeCases2 do 33 | use Interceptor.Annotated, attribute_name: :xpto_intercept 34 | 35 | @xpto_intercept true 36 | def to_intercept(a, b, _to_ignore), do: "#{a} #{b}" 37 | 38 | @xpto_intercept true 39 | def intercept_with_prefix("some_prefix:" <> abc), do: abc 40 | end 41 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_on_before.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedBefore.Callback do 2 | def before({_module, _function, _args} = mfa) do 3 | Agent.update(:annotated_before_test_process, 4 | fn messages -> 5 | [{Interceptor.Utils.timestamp(), mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule AnnotatedInterceptedOnBefore1 do 12 | use Interceptor.Annotated 13 | 14 | @intercept true 15 | def to_intercept(), do: Interceptor.Utils.timestamp() 16 | end 17 | 18 | defmodule AnnotatedInterceptedOnBefore2 do 19 | use Interceptor.Annotated 20 | 21 | @intercept true 22 | def to_intercept(), do: Interceptor.Utils.timestamp() 23 | 24 | @intercept true 25 | def other_to_intercept(), do: "HELLO" 26 | 27 | IO.puts("This statement doesn't interfere in any way") 28 | end 29 | 30 | defmodule AnnotatedInterceptedOnBefore3 do 31 | use Interceptor.Annotated 32 | 33 | @intercept true 34 | def not_to_intercept(), do: Interceptor.Utils.timestamp() 35 | 36 | @intercept true 37 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 38 | 39 | @intercept true 40 | defp private_function(x, y, z), do: x+y+z 41 | end 42 | 43 | defmodule AnnotatedInterceptedOnBefore4 do 44 | use Interceptor.Annotated 45 | 46 | @intercept true 47 | def to_intercept, do: "Hello, even without args" 48 | 49 | @intercept true 50 | def bla, do: 123 51 | end 52 | -------------------------------------------------------------------------------- /lib/configuration/validator.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Configuration.Validator do 2 | alias Interceptor.Utils 3 | alias Interceptor.Configuration 4 | 5 | @config_searcher Application.get_env(:interceptor, :config_searcher) || Interceptor.Configuration.Searcher 6 | 7 | def check_if_intercepted_functions_exist(), 8 | do: _check_if_intercepted_functions_exist(Configuration.debug_mode?()) 9 | 10 | def _check_if_intercepted_functions_exist(_debug = false), do: :skip_intercept_config_validation 11 | def _check_if_intercepted_functions_exist(_debug = true) do 12 | modules_to_check = @config_searcher.search_intercept_config_modules() 13 | 14 | all_exist? = modules_to_check 15 | |> Enum.map(&get_intercept_config_from_module/1) 16 | |> Enum.flat_map(&Enum.map(&1, fn 17 | # we don't check wildcarded MFAs 18 | {{_m, f, a}, _callbacks} when f == :* or a == :* -> true 19 | {{m, f, a}, _callbacks} -> Utils.check_if_mfa_exists(m, f, a) 20 | end)) 21 | |> Enum.reduce(true, fn exists?, acc -> acc and exists? end) 22 | 23 | IO.puts("Checking interceptor configuration defined by the following modules: #{inspect(modules_to_check)}\nAll functions to intercept are exported: #{all_exist?}") 24 | 25 | all_exist? 26 | end 27 | 28 | defp get_intercept_config_from_module(module), 29 | do: apply(module, :get_intercept_config, []) 30 | end 31 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_private_on_success_on_error_own_configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule OwnCallbacks do 2 | def on_success({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:private_on_success_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | "Yo, I don't influence anything" 8 | end 9 | 10 | def on_error({_module, _function, _args} = mfa, error, started_at) do 11 | Agent.update(:private_on_error_test_process, 12 | fn messages -> 13 | [{started_at, Interceptor.Utils.timestamp(), error, mfa} | messages] 14 | end) 15 | "Yo, me neither" 16 | end 17 | end 18 | 19 | defmodule InterceptedPrivateOnSuccessOnErrorOwnConfiguration do 20 | use Interceptor, config: %{ 21 | "InterceptedPrivateOnSuccessOnErrorOwnConfiguration.square_plus_10/1" => 22 | [ 23 | on_success: "OwnCallbacks.on_success/3", 24 | on_error: "OwnCallbacks.on_error/3" 25 | ], 26 | "InterceptedPrivateOnSuccessOnErrorOwnConfiguration.divide_by_0/1" => 27 | [ 28 | on_success: "OwnCallbacks.on_success/3", 29 | on_error: "OwnCallbacks.on_error/3" 30 | ] 31 | } 32 | 33 | Interceptor.intercept do 34 | def public_square_plus_10(x), do: square_plus_10(x) 35 | 36 | defp square_plus_10(x) do 37 | Process.sleep(500) 38 | x*x + 10 39 | end 40 | 41 | def public_divide_by_0(y), do: divide_by_0(y) 42 | 43 | defp divide_by_0(y) do 44 | Process.sleep(600) 45 | y/0 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_by_wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule Wrapper.Callback do 2 | def wrap_returns_result({_module, _function, _args} = mfa, lambda) do 3 | result = lambda.() 4 | 5 | Agent.update(:wrapper_test_process, 6 | fn messages -> 7 | [{Interceptor.Utils.timestamp(), result, mfa} | messages] 8 | end) 9 | 10 | result 11 | end 12 | 13 | def wrap_returns_hello({_module, _function, _args} = mfa, lambda) do 14 | result = lambda.() 15 | 16 | Agent.update(:wrapper_test_process, 17 | fn messages -> 18 | [{Interceptor.Utils.timestamp(), result, mfa} | messages] 19 | end) 20 | 21 | "Hello" 22 | end 23 | end 24 | 25 | defmodule InterceptedByWrapper1 do 26 | require Interceptor, as: I 27 | 28 | I.intercept do 29 | def to_intercept(), do: Interceptor.Utils.timestamp() 30 | end 31 | end 32 | 33 | defmodule InterceptedByWrapper2 do 34 | require Interceptor, as: I 35 | 36 | I.intercept do 37 | def to_intercept(), do: Interceptor.Utils.timestamp() 38 | def other_to_intercept(), do: "HELLO" 39 | 40 | IO.puts("This statement doesn't interfere in any way") 41 | end 42 | end 43 | 44 | defmodule InterceptedByWrapper3 do 45 | require Interceptor, as: I 46 | 47 | I.intercept do 48 | def not_to_intercept(), do: Interceptor.Utils.timestamp() 49 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 50 | 51 | defp private_function(x, y, z), do: x+y+z 52 | end 53 | end 54 | 55 | defmodule InterceptedByWrapper4 do 56 | require Interceptor, as: I 57 | 58 | I.intercept do 59 | def to_intercept(), do: Interceptor.Utils.timestamp() 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_wildcarded_mfa_callbacks.ex: -------------------------------------------------------------------------------- 1 | defmodule WildcardedMfa.Callbacks do 2 | def success_cb({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:wildcarded_mfa_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | end 8 | 9 | def error_cb({_module, _function, _args} = mfa, result, started_at) do 10 | Agent.update(:wildcarded_mfa_test_process, 11 | fn messages -> 12 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 13 | end) 14 | end 15 | end 16 | 17 | defmodule InterceptedWildcardedMfa1 do 18 | require Interceptor, as: I 19 | 20 | I.intercept do 21 | def foo(abc), do: "x #{abc} x" 22 | 23 | def foo(abc, xyz), do: "y #{abc} #{xyz} y" 24 | 25 | def foo(abc, xyz, qqq, www, eee), do: "z #{abc} #{xyz} #{qqq} #{www} #{eee} z" 26 | 27 | def foo_nop(abc, zzz) do 28 | abc <> "###" <> zzz 29 | end 30 | end 31 | end 32 | 33 | defmodule InterceptedWildcardedMfa2 do 34 | require Interceptor, as: I 35 | 36 | I.intercept do 37 | def xyz(123), do: "It's a 123" 38 | 39 | def xyz(wut), do: "It's a #{inspect(wut)}" 40 | 41 | def foo(abc), do: "x #{abc} x" 42 | 43 | def foo(abc, xyz), do: "y #{abc} #{xyz} y" 44 | 45 | def foo(abc, xyz, qqq, www, eee), do: "z #{abc} #{xyz} #{qqq} #{www} #{eee} z" 46 | 47 | def foo_yes(abc, zzz) do 48 | abc <> "###" <> zzz 49 | end 50 | 51 | def simple_foo() do 52 | "simple foo" 53 | end 54 | 55 | def weird_function_weird_name_big_name("blade:" <> runner_name), do: "I'm a Blade runner named '#{runner_name}'" 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_by_wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedWrapper.Callback do 2 | def wrap_returns_result({_module, _function, _args} = mfa, lambda) do 3 | result = lambda.() 4 | 5 | Agent.update(:annotated_wrapper_test_process, 6 | fn messages -> 7 | [{Interceptor.Utils.timestamp(), result, mfa} | messages] 8 | end) 9 | 10 | result 11 | end 12 | 13 | def wrap_returns_hello({_module, _function, _args} = mfa, lambda) do 14 | result = lambda.() 15 | 16 | Agent.update(:annotated_wrapper_test_process, 17 | fn messages -> 18 | [{Interceptor.Utils.timestamp(), result, mfa} | messages] 19 | end) 20 | 21 | "Hello" 22 | end 23 | end 24 | 25 | defmodule AnnotatedInterceptedByWrapper1 do 26 | use Interceptor.Annotated 27 | 28 | @intercept true 29 | def to_intercept(), do: Interceptor.Utils.timestamp() 30 | end 31 | 32 | defmodule AnnotatedInterceptedByWrapper2 do 33 | use Interceptor.Annotated 34 | 35 | @intercept true 36 | def to_intercept(), do: Interceptor.Utils.timestamp() 37 | 38 | @intercept true 39 | def other_to_intercept(), do: "HELLO" 40 | 41 | IO.puts("This statement doesn't interfere in any way") 42 | end 43 | 44 | defmodule AnnotatedInterceptedByWrapper3 do 45 | use Interceptor.Annotated 46 | 47 | @intercept true 48 | def not_to_intercept(), do: Interceptor.Utils.timestamp() 49 | 50 | @intercept true 51 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 52 | 53 | defp private_function(x, y, z), do: x+y+z 54 | end 55 | 56 | defmodule AnnotatedInterceptedByWrapper4 do 57 | use Interceptor.Annotated 58 | 59 | @intercept true 60 | def to_intercept(), do: Interceptor.Utils.timestamp() 61 | end 62 | 63 | -------------------------------------------------------------------------------- /test/intercepted_modules/wrong_configs_for_config_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule WrongConfigNonExistingModule do 2 | @config %{ 3 | # the module of the intercepted function *does not exist*, the goal of this 4 | # is to test the Configuration.Validator.check_if_intercepted_functions_exist/0 5 | {NonExistingModule, :non_existing_function, 3} => [after: {After.Callback, :right_after, 2}], 6 | } 7 | 8 | def get_intercept_config(), do: @config 9 | end 10 | 11 | defmodule WrongConfigNonExistingFunction do 12 | @config %{ 13 | # the following intercepted function *do not exist* (the module exists), the goal of this 14 | # is to test the Configuration.Searcher.check_if_intercepted_functions_exist/0 15 | {InterceptedByWrapper4, :non_existing_function, 3} => [after: {After.Callback, :right_after, 2}], 16 | } 17 | 18 | def get_intercept_config(), do: @config 19 | end 20 | 21 | defmodule StreamlinedWrongConfigNonExistingModule do 22 | # TODO 23 | @config %{ 24 | # the module of the intercepted function *does not exist*, the goal of this 25 | # is to test the Configuration.Validator.check_if_intercepted_functions_exist/0 26 | {NonExistingModule, :non_existing_function, 3} => [after: {After.Callback, :right_after, 2}], 27 | } 28 | 29 | def get_intercept_config(), do: @config 30 | end 31 | 32 | defmodule StreamlinedWrongConfigNonExistingFunction do 33 | # TODO 34 | @config %{ 35 | # the following intercepted function *do not exist* (the module exists), the goal of this 36 | # is to test the Configuration.Searcher.check_if_intercepted_functions_exist/0 37 | {InterceptedByWrapper4, :non_existing_function, 3} => [after: {After.Callback, :right_after, 2}], 38 | } 39 | 40 | def get_intercept_config(), do: @config 41 | end 42 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :interceptor, 7 | package: package(), 8 | source_url: "https://github.com/amalbuquerque/interceptor", 9 | version: "0.5.4", 10 | elixir: "~> 1.7", 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | docs: docs(), 15 | test_coverage: [tool: ExCoveralls], 16 | preferred_cli_env: [ 17 | coveralls: :test, 18 | "coveralls.detail": :test, 19 | "coveralls.post": :test, 20 | "coveralls.html": :test 21 | ] 22 | ] 23 | end 24 | 25 | def application do 26 | [ 27 | extra_applications: [:logger] 28 | ] 29 | end 30 | 31 | defp deps do 32 | [ 33 | {:ex_doc, "~> 0.24", only: :dev}, 34 | {:mox, "1.0.0", only: :test}, 35 | {:excoveralls, "~> 0.13.4", only: :test} 36 | ] 37 | end 38 | 39 | defp elixirc_paths(:test), do: ["test/support", "lib", "test/intercepted_modules"] 40 | defp elixirc_paths(:dev), do: ["lib", "test/intercepted_modules/minefield.ex"] 41 | defp elixirc_paths(_), do: ["lib"] 42 | 43 | defp docs() do 44 | [ 45 | main: "Interceptor", 46 | canonical: "https://hexdocs.pm/interceptor", 47 | source_url: "https://github.com/amalbuquerque/interceptor", 48 | logo: "assets/images/interceptor_logo_small.png", 49 | assets: "assets" 50 | ] 51 | end 52 | 53 | defp package() do 54 | [ 55 | description: "Library to easily intercept function calls", 56 | licenses: ["MIT"], 57 | maintainers: ["André Albuquerque"], 58 | links: %{ 59 | Github: "https://github.com/amalbuquerque/interceptor" 60 | } 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_on_after.ex: -------------------------------------------------------------------------------- 1 | defmodule After.Callback do 2 | def right_after({_module, _function, _args} = mfa, result) do 3 | Agent.update(:after_test_process, 4 | fn messages -> 5 | [{Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule InterceptedOnAfter1 do 12 | require Interceptor, as: I 13 | 14 | I.intercept do 15 | def to_intercept(), do: Interceptor.Utils.timestamp() 16 | end 17 | end 18 | 19 | defmodule InterceptedOnAfter2 do 20 | require Interceptor, as: I 21 | 22 | I.intercept do 23 | def to_intercept(), do: Interceptor.Utils.timestamp() 24 | def other_to_intercept(), do: "HELLO" 25 | 26 | IO.puts("This statement doesn't interfere in any way") 27 | end 28 | end 29 | 30 | defmodule InterceptedOnAfter3 do 31 | require Interceptor, as: I 32 | 33 | I.intercept do 34 | def not_to_intercept(), do: Interceptor.Utils.timestamp() 35 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 36 | 37 | defp private_function(x, y, z), do: x+y+z 38 | end 39 | end 40 | 41 | defmodule InterceptedOnAfter4 do 42 | require Interceptor, as: I 43 | 44 | I.intercept do 45 | def to_intercept_guarded(arg) when is_atom(arg), do: "ATOM #{arg}" 46 | def to_intercept_guarded(arg), do: "SOMETHING ELSE #{arg}" 47 | end 48 | end 49 | 50 | defmodule InterceptedOnAfter5 do 51 | require Interceptor, as: I 52 | 53 | I.intercept do 54 | def it_has_threes(3) do 55 | "Has one three" 56 | end 57 | 58 | def it_has_threes(33), do: "Has two threes" 59 | 60 | def its_abc("abc"), do: true 61 | 62 | def its_abc(_else), do: false 63 | 64 | def something(%{abc: xyz}) do 65 | "something #{xyz}" 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_on_after_own_configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule After.OwnCallback do 2 | def right_after({_module, _function, _args} = mfa, result) do 3 | Agent.update(:after_test_process, 4 | fn messages -> 5 | [{:callback_overridden, result, mfa} | messages] 6 | end) 7 | end 8 | end 9 | 10 | defmodule MyOwn.InterceptConfig do 11 | def get_intercept_config do 12 | %{ 13 | {InterceptedOnAfterOwnConfiguration1, :to_intercept, 0} => [ 14 | after: {After.OwnCallback, :right_after, 2} 15 | ] 16 | } 17 | end 18 | end 19 | 20 | defmodule InterceptedOnAfterOwnConfiguration1 do 21 | use Interceptor, config: MyOwn.InterceptConfig 22 | 23 | Interceptor.intercept do 24 | def to_intercept(), do: Interceptor.Utils.timestamp() 25 | end 26 | end 27 | 28 | defmodule InterceptedOnAfterOwnConfiguration2 do 29 | use Interceptor, config: %{ 30 | {InterceptedOnAfterOwnConfiguration2, :to_intercept, 0} => [ 31 | after: {After.OwnCallback, :right_after, 2} 32 | ] 33 | } 34 | 35 | Interceptor.intercept do 36 | def to_intercept(), do: Interceptor.Utils.timestamp() 37 | end 38 | end 39 | 40 | defmodule InterceptedOnAfterOwnConfiguration3 do 41 | use Interceptor, config: %{ 42 | "InterceptedOnAfterOwnConfiguration3.to_intercept/0" => [ 43 | after: "After.OwnCallback.right_after/2" 44 | ] 45 | } 46 | 47 | Interceptor.intercept do 48 | def to_intercept(), do: Interceptor.Utils.timestamp() 49 | end 50 | end 51 | 52 | defmodule InterceptedOnAfterOwnConfiguration4 do 53 | use Interceptor, config: %{ 54 | "InterceptedOnAfterOwnConfiguration4.to_intercept/0" => [ 55 | after: {After.OwnCallback, :right_after, 2} 56 | ] 57 | } 58 | 59 | Interceptor.intercept do 60 | def to_intercept(), do: Interceptor.Utils.timestamp() 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/configuration/validator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Configuration.ValidatorTest do 2 | use ExUnit.Case, async: true 3 | alias Interceptor.Configuration.SearcherMock 4 | import Mox 5 | 6 | @subject Interceptor.Configuration.Validator 7 | 8 | setup :verify_on_exit! 9 | 10 | describe "when every intercepted function exists" do 11 | setup :searcher_returns_module_with_valid_config 12 | 13 | test "it returns 'true'" do 14 | assert @subject.check_if_intercepted_functions_exist() == true 15 | end 16 | end 17 | 18 | describe "when the module of one of the intercepted functions doesn't exist" do 19 | setup :searcher_returns_module_with_invalid_config_non_existing_module 20 | 21 | test "it returns 'false'" do 22 | refute @subject.check_if_intercepted_functions_exist() 23 | end 24 | end 25 | 26 | describe "when one of the intercepted functions doesn't exist" do 27 | setup :searcher_returns_module_with_invalid_config_non_existing_function 28 | 29 | test "it returns 'false'" do 30 | refute @subject.check_if_intercepted_functions_exist() 31 | end 32 | end 33 | 34 | defp searcher_returns_module_with_valid_config(_context) do 35 | stub(SearcherMock, :search_intercept_config_modules, fn -> [InterceptConfig] end) 36 | 37 | :ok 38 | end 39 | 40 | defp searcher_returns_module_with_invalid_config_non_existing_module(_context) do 41 | stub( 42 | SearcherMock, 43 | :search_intercept_config_modules, 44 | fn -> [InterceptConfig, WrongConfigNonExistingModule] end 45 | ) 46 | 47 | :ok 48 | end 49 | 50 | defp searcher_returns_module_with_invalid_config_non_existing_function(_context) do 51 | stub( 52 | SearcherMock, 53 | :search_intercept_config_modules, 54 | fn -> [InterceptConfig, WrongConfigNonExistingFunction] end 55 | ) 56 | 57 | :ok 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_wildcarded_mfa_callbacks.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedWildcardedMfa.Callbacks do 2 | def success_cb({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:annotated_wildcarded_mfa_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | end 8 | 9 | def error_cb({_module, _function, _args} = mfa, result, started_at) do 10 | Agent.update(:annotated_wildcarded_mfa_test_process, 11 | fn messages -> 12 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 13 | end) 14 | end 15 | end 16 | 17 | defmodule AnnotatedInterceptedWildcardedMfa1 do 18 | use Interceptor.Annotated 19 | 20 | @intercept true 21 | def foo(abc), do: "x #{abc} x" 22 | 23 | @intercept true 24 | def foo(abc, xyz), do: "y #{abc} #{xyz} y" 25 | 26 | @intercept true 27 | def foo(abc, xyz, qqq, www, eee), do: "z #{abc} #{xyz} #{qqq} #{www} #{eee} z" 28 | 29 | def foo_nop(abc, zzz) do 30 | abc <> "###" <> zzz 31 | end 32 | end 33 | 34 | defmodule AnnotatedInterceptedWildcardedMfa2 do 35 | use Interceptor.Annotated 36 | 37 | @intercept true 38 | def xyz(123), do: "It's a 123" 39 | 40 | @intercept true 41 | def xyz(wut), do: "It's a #{inspect(wut)}" 42 | 43 | @intercept true 44 | def foo(abc), do: "x #{abc} x" 45 | 46 | @intercept true 47 | def foo(abc, xyz), do: "y #{abc} #{xyz} y" 48 | 49 | @intercept true 50 | def foo(abc, xyz, qqq, www, eee), do: "z #{abc} #{xyz} #{qqq} #{www} #{eee} z" 51 | 52 | @intercept true 53 | def foo_yes(abc, zzz) do 54 | abc <> "###" <> zzz 55 | end 56 | 57 | @intercept true 58 | def simple_foo() do 59 | "simple foo" 60 | end 61 | 62 | @intercept true 63 | def weird_function_weird_name_big_name("blade:" <> runner_name), do: "I'm a Blade runner named '#{runner_name}'" 64 | end 65 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_on_after_own_configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedAfter.OwnCallback do 2 | def right_after({_module, _function, _args} = mfa, result) do 3 | Agent.update(:annotated_after_test_process, 4 | fn messages -> 5 | [{:callback_overridden, result, mfa} | messages] 6 | end) 7 | end 8 | end 9 | 10 | defmodule AnnotatedMyOwn.InterceptConfig do 11 | def get_intercept_config do 12 | %{ 13 | {AnnotatedInterceptedOnAfterOwnConfiguration1, :to_intercept, 0} => [ 14 | after: {AnnotatedAfter.OwnCallback, :right_after, 2} 15 | ] 16 | } 17 | end 18 | end 19 | 20 | defmodule AnnotatedInterceptedOnAfterOwnConfiguration1 do 21 | use Interceptor.Annotated, config: AnnotatedMyOwn.InterceptConfig 22 | 23 | @intercept true 24 | def to_intercept(), do: Interceptor.Utils.timestamp() 25 | end 26 | 27 | defmodule AnnotatedInterceptedOnAfterOwnConfiguration2 do 28 | use Interceptor.Annotated, config: %{ 29 | {AnnotatedInterceptedOnAfterOwnConfiguration2, :to_intercept, 0} => [ 30 | after: {AnnotatedAfter.OwnCallback, :right_after, 2} 31 | ] 32 | } 33 | 34 | @intercept true 35 | def to_intercept(), do: Interceptor.Utils.timestamp() 36 | end 37 | 38 | defmodule AnnotatedInterceptedOnAfterOwnConfiguration3 do 39 | use Interceptor.Annotated, config: %{ 40 | "AnnotatedInterceptedOnAfterOwnConfiguration3.to_intercept/0" => [ 41 | after: "AnnotatedAfter.OwnCallback.right_after/2" 42 | ] 43 | } 44 | 45 | @intercept true 46 | def to_intercept(), do: Interceptor.Utils.timestamp() 47 | end 48 | 49 | defmodule AnnotatedInterceptedOnAfterOwnConfiguration4 do 50 | use Interceptor.Annotated, config: %{ 51 | "AnnotatedInterceptedOnAfterOwnConfiguration4.to_intercept/0" => [ 52 | after: {AnnotatedAfter.OwnCallback, :right_after, 2} 53 | ] 54 | } 55 | 56 | @intercept true 57 | def to_intercept(), do: Interceptor.Utils.timestamp() 58 | end 59 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_on_after.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedAfter.Callback do 2 | def right_after({_module, _function, _args} = mfa, result) do 3 | Agent.update(:annotated_after_test_process, 4 | fn messages -> 5 | [{Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule AnnotatedInterceptedOnAfter1 do 12 | use Interceptor.Annotated 13 | 14 | @intercept true 15 | def to_intercept(), do: Interceptor.Utils.timestamp() 16 | end 17 | 18 | defmodule AnnotatedInterceptedOnAfter2 do 19 | use Interceptor.Annotated 20 | 21 | @intercept true 22 | def to_intercept(), do: Interceptor.Utils.timestamp() 23 | 24 | @intercept true 25 | def other_to_intercept(), do: "HELLO" 26 | 27 | IO.puts("This statement doesn't interfere in any way") 28 | end 29 | 30 | defmodule AnnotatedInterceptedOnAfter3 do 31 | use Interceptor.Annotated 32 | 33 | @intercept true 34 | def not_to_intercept(), do: Interceptor.Utils.timestamp() 35 | 36 | @intercept true 37 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 38 | 39 | @intercept true 40 | defp private_function(x, y, z), do: x+y+z 41 | end 42 | 43 | defmodule AnnotatedInterceptedOnAfter4 do 44 | use Interceptor.Annotated 45 | 46 | @intercept true 47 | def to_intercept_guarded(arg) when is_atom(arg), do: "ATOM #{arg}" 48 | 49 | @intercept true 50 | def to_intercept_guarded(arg), do: "SOMETHING ELSE #{arg}" 51 | end 52 | 53 | defmodule AnnotatedInterceptedOnAfter5 do 54 | use Interceptor.Annotated 55 | 56 | @intercept true 57 | def it_has_threes(3) do 58 | "Has one three" 59 | end 60 | 61 | @intercept true 62 | def it_has_threes(33), do: "Has two threes" 63 | 64 | @intercept true 65 | def its_abc("abc"), do: true 66 | 67 | @intercept true 68 | def its_abc(_else), do: false 69 | 70 | @intercept true 71 | def something(%{abc: xyz}) do 72 | "something #{xyz}" 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/intercepted_modules/annotated/annotated_intercepted_on_success.ex: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedOnSuccess.Callback do 2 | def on_success({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:annotated_on_success_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule AnnotatedInterceptedOnSuccess1 do 12 | use Interceptor.Annotated 13 | 14 | @intercept true 15 | def to_intercept(), do: Interceptor.Utils.timestamp() 16 | end 17 | 18 | defmodule AnnotatedInterceptedOnSuccess2 do 19 | use Interceptor.Annotated 20 | 21 | @intercept true 22 | def to_intercept(), do: Process.sleep(200) 23 | 24 | @intercept true 25 | def other_to_intercept(), do: "HELLO" 26 | 27 | IO.puts("This statement doesn't interfere in any way") 28 | end 29 | 30 | defmodule AnnotatedInterceptedOnSuccess3 do 31 | use Interceptor.Annotated 32 | 33 | def definitely_not_to_intercept(), do: "No macros plz" 34 | 35 | @intercept true 36 | def not_to_intercept(), do: "Not intercepted" 37 | 38 | @intercept true 39 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 40 | 41 | @intercept true 42 | defp private_function(x, y, z), do: x+y+z 43 | 44 | @intercept true 45 | def trickier_args_function(first_arg, [one, two, three], {abc, xyz}, %{baz: woz}, <>, foo \\ "bar") do 46 | [ 47 | first_arg, 48 | one, 49 | two, 50 | three, 51 | abc, 52 | xyz, 53 | woz, 54 | g, 55 | h, 56 | i, 57 | foo 58 | ] 59 | end 60 | end 61 | 62 | defmodule AnnotatedInterceptedOnSuccess4 do 63 | use Interceptor.Annotated 64 | 65 | alias La.Lu.Li.Weird.MyStruct 66 | 67 | @intercept true 68 | def with_struct(%MyStruct{name: n, age: a}) do 69 | [n, a] 70 | end 71 | 72 | @intercept true 73 | def with_structs( 74 | %MyStruct{name: n1, age: a1}, 75 | %La.Lu.Li.Weird.MyStruct{name: n2, age: a2}) do 76 | [n1, a1, n2, a2] 77 | end 78 | 79 | @intercept true 80 | def with_struct_already_assigned(%MyStruct{name: _n, age: _a} = xpto) do 81 | [xpto.name, xpto.age] 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/intercepted_modules/do_block/intercepted_on_success.ex: -------------------------------------------------------------------------------- 1 | defmodule OnSuccess.Callback do 2 | def on_success({_module, _function, _args} = mfa, result, started_at) do 3 | Agent.update(:on_success_test_process, 4 | fn messages -> 5 | [{started_at, Interceptor.Utils.timestamp(), result, mfa} | messages] 6 | end) 7 | "Doesn't influence the function at all" 8 | end 9 | end 10 | 11 | defmodule InterceptedOnSuccess1 do 12 | require Interceptor, as: I 13 | 14 | I.intercept do 15 | def to_intercept(), do: Interceptor.Utils.timestamp() 16 | end 17 | end 18 | 19 | defmodule InterceptedOnSuccess2 do 20 | require Interceptor, as: I 21 | 22 | I.intercept do 23 | def to_intercept(), do: Process.sleep(200) 24 | def other_to_intercept(), do: "HELLO" 25 | 26 | IO.puts("This statement doesn't interfere in any way") 27 | end 28 | end 29 | 30 | defmodule InterceptedOnSuccess3 do 31 | require Interceptor, as: I 32 | 33 | def definitely_not_to_intercept(), do: "No macros plz" 34 | 35 | I.intercept do 36 | def not_to_intercept(), do: "Not intercepted" 37 | def other_to_intercept(w), do: w + private_function(1, 2, 3) 38 | 39 | defp private_function(x, y, z), do: x+y+z 40 | 41 | def trickier_args_function(first_arg, [one, two, three], {abc, xyz}, %{baz: woz}, <>, foo \\ "bar") do 42 | [ 43 | first_arg, 44 | one, 45 | two, 46 | three, 47 | abc, 48 | xyz, 49 | woz, 50 | g, 51 | h, 52 | i, 53 | foo 54 | ] 55 | end 56 | end 57 | end 58 | 59 | defmodule La.Lu.Li.Weird.MyStruct do 60 | defstruct name: "ryuichi sakamoto", age: 67 61 | end 62 | 63 | 64 | defmodule InterceptedOnSuccess4 do 65 | require Interceptor, as: I 66 | 67 | alias La.Lu.Li.Weird.MyStruct 68 | 69 | I.intercept do 70 | def with_struct(%MyStruct{name: n, age: a}) do 71 | [n, a] 72 | end 73 | 74 | def with_structs( 75 | %MyStruct{name: n1, age: a1}, 76 | %La.Lu.Li.Weird.MyStruct{name: n2, age: a2}) do 77 | [n1, a1, n2, a2] 78 | end 79 | 80 | def with_struct_already_assigned(%MyStruct{name: _n, age: _a} = xpto) do 81 | [xpto.name, xpto.age] 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/do_block/interceptor_edge_cases_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorEdgeCasesTest do 2 | use ExUnit.Case 3 | 4 | @process_name :edge_cases_test_process 5 | 6 | describe "functions with argument using `<>`" do 7 | test "it passes the argument with the prefix before `<>`" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = InterceptedEdgeCases1.intercept_with_prefix("some_prefix:blabla") 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {InterceptedEdgeCases1, :intercept_with_prefix, ["some_prefix:blabla"]} 19 | end 20 | end 21 | 22 | describe "module with functions with ignored arguments" do 23 | test "it passes the ignored arguments as `:arg_cant_be_intercepted`" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = InterceptedEdgeCases1.to_intercept(1, 2, 3) 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert intercepted_mfa == {InterceptedEdgeCases1, :to_intercept, [1, 2, :arg_cant_be_intercepted]} 35 | end 36 | 37 | test "it intercepts the function with an argument pattern matching on atom value" do 38 | {:ok, _pid} = spawn_agent() 39 | 40 | result = InterceptedEdgeCases1.intercept_pattern_match_atom_argument(1, :normal) 41 | 42 | callback_calls = get_agent_messages() 43 | 44 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 45 | 46 | assert length(callback_calls) == 1 47 | assert result == callback_result 48 | assert intercepted_mfa == {InterceptedEdgeCases1, :intercept_pattern_match_atom_argument, [1, :normal]} 49 | end 50 | end 51 | 52 | defp spawn_agent() do 53 | @process_name 54 | |> Process.whereis() 55 | |> kill_agent() 56 | 57 | {:ok, pid} = Agent.start_link(fn -> [] end) 58 | true = Process.register(pid, @process_name) 59 | 60 | {:ok, pid} 61 | end 62 | 63 | defp kill_agent(nil), do: false 64 | defp kill_agent(pid) do 65 | case Process.alive?(pid) do 66 | true -> Process.exit(pid, :kill) 67 | _ -> false 68 | end 69 | end 70 | 71 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 72 | end 73 | -------------------------------------------------------------------------------- /test/do_block/interceptor_private_own_configuration_on_success_error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorPrivateOwnConfigurationOnSuccessErrorTest do 2 | use ExUnit.Case 3 | 4 | describe "module with two functions, own streamlined configuration" do 5 | test "it intercepts the private function after it is successfully called" do 6 | agent_name = :private_on_success_test_process 7 | {:ok, _pid} = spawn_agent(agent_name) 8 | 9 | result = InterceptedPrivateOnSuccessOnErrorOwnConfiguration.public_square_plus_10(3) 10 | 11 | callback_calls = get_agent_messages(agent_name) 12 | 13 | [{ 14 | started_at_timestamp, 15 | intercepted_timestamp, 16 | intercepted_result, 17 | intercepted_mfa 18 | }] = callback_calls 19 | 20 | assert length(callback_calls) == 1 21 | assert result == intercepted_result 22 | time_it_took_microseconds = intercepted_timestamp - started_at_timestamp 23 | assert time_it_took_microseconds > 500_000 24 | 25 | assert intercepted_mfa == {InterceptedPrivateOnSuccessOnErrorOwnConfiguration, :square_plus_10, [3]} 26 | end 27 | 28 | test "it intercepts the private function after it raises an error" do 29 | agent_name = :private_on_error_test_process 30 | {:ok, _pid} = spawn_agent(agent_name) 31 | 32 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 33 | InterceptedPrivateOnSuccessOnErrorOwnConfiguration.public_divide_by_0(42) 34 | end 35 | 36 | callback_calls = get_agent_messages(agent_name) 37 | 38 | [{ 39 | started_at_timestamp, 40 | intercepted_timestamp, 41 | intercepted_error, 42 | intercepted_mfa 43 | }] = callback_calls 44 | 45 | assert length(callback_calls) == 1 46 | assert %ArithmeticError{} == intercepted_error 47 | time_it_took_microseconds = intercepted_timestamp - started_at_timestamp 48 | assert time_it_took_microseconds > 600_000 49 | 50 | assert intercepted_mfa == {InterceptedPrivateOnSuccessOnErrorOwnConfiguration, :divide_by_0, [42]} 51 | end 52 | end 53 | 54 | defp spawn_agent(process_name) do 55 | process_name 56 | |> Process.whereis() 57 | |> kill_agent() 58 | 59 | {:ok, pid} = Agent.start_link(fn -> [] end) 60 | true = Process.register(pid, process_name) 61 | 62 | {:ok, pid} 63 | end 64 | 65 | defp kill_agent(nil), do: false 66 | defp kill_agent(pid) do 67 | case Process.alive?(pid) do 68 | true -> Process.exit(pid, :kill) 69 | _ -> false 70 | end 71 | end 72 | 73 | defp get_agent_messages(process_name), do: Agent.get(process_name, &(&1)) 74 | end 75 | -------------------------------------------------------------------------------- /lib/configuration/configurator.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Configurator do 2 | 3 | alias Interceptor.Utils 4 | 5 | defmacro __using__(_opts) do 6 | quote do 7 | import unquote(__MODULE__) 8 | 9 | Module.register_attribute(__MODULE__, :interceptions, accumulate: true) 10 | 11 | @before_compile unquote(__MODULE__) 12 | 13 | def debug_intercept_config(), do: get_intercept_config() 14 | end 15 | end 16 | 17 | defmacro __before_compile__(_env) do 18 | quote do 19 | def get_intercept_config() do 20 | @interceptions 21 | |> Enum.reverse() 22 | |> transform_streamlined_config_to_tuple_config() 23 | end 24 | end 25 | end 26 | 27 | @doc """ 28 | This function converts a map or a list of 2-element tuples (i.e. any structure 29 | that can be iterated with `Enum.map/2` as a list of 2-element tuples) into a 30 | "proper" (i.e. tuple-based) intercept configuration map. Each 31 | element is a `{mfa_to_intercept, callbacks}` tuple, where `mfa_to_intercept` 32 | is the MFA of the function to intercept as a `"Module.function/arity"` string, 33 | and `callbacks` is a keyword list whose keys may be one of `:before, :after, 34 | :on_success, :on_error or :wrapper`, and the values the callback functions to 35 | call also as a `"Module.function/arity"` string. 36 | 37 | If instead of a `"Module.function/arity"` string, a function is already in 38 | the MFA tuple format, i.e., it is already written as {Module, :function, 2}, 39 | instead of `"Module.function/2"`, the transformation won't do nothing. 40 | 41 | The intercepted function can now have a `*` for its `arity` or 42 | `function_name` and `arity` on both tuple-based and 43 | string-based format (e.g. `{Module, :*, :*}` or `Module.*/*`). 44 | """ 45 | def transform_streamlined_config_to_tuple_config(intercept_config) do 46 | intercept_config 47 | |> Enum.map(fn {mfa_to_intercept, callbacks} -> 48 | to_intercept = Utils.get_intercepted_mfa_from_string(mfa_to_intercept) 49 | 50 | {to_intercept, convert_callbacks_to_mfa(callbacks)} 51 | end) 52 | |> Enum.into(%{}) 53 | end 54 | 55 | defp convert_callbacks_to_mfa([callback | rest]), 56 | do: [convert_callback_to_mfa(callback) | convert_callbacks_to_mfa(rest)] 57 | 58 | defp convert_callbacks_to_mfa([]), do: [] 59 | 60 | defp convert_callback_to_mfa({type, mfa_string}) when type in [:before, :after, :on_success, :on_error, :wrapper] do 61 | mfa = Utils.get_mfa_from_string(mfa_string) 62 | 63 | {type, mfa} 64 | end 65 | 66 | defmacro intercept(mfa_to_intercept, callbacks) do 67 | quote bind_quoted: [mfa_to_intercept: mfa_to_intercept, callbacks: callbacks] do 68 | @interceptions {mfa_to_intercept, callbacks} 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.UtilsTest do 2 | use ExUnit.Case 3 | 4 | @subject Interceptor.Utils 5 | 6 | describe "random_atom/1" do 7 | test "it returns an atom of the passed size" do 8 | result = @subject.random_atom(42) 9 | 10 | assert result |> to_string() |> String.length() == 42 11 | assert is_atom(result) 12 | end 13 | end 14 | 15 | describe "random_string/1" do 16 | test "it returns a string of the passed size" do 17 | result = @subject.random_string(42) 18 | 19 | assert String.length(result) == 42 20 | assert is_binary(result) 21 | end 22 | end 23 | 24 | describe "get_mfa_from_string/1" do 25 | test "it returns the MFA of a simple module" do 26 | assert {TheCoolModule, :func, 0} == @subject.get_mfa_from_string("TheCoolModule.func/0") 27 | end 28 | 29 | test "it returns the MFA of a module that belongs to a big namespace" do 30 | assert {Zi.Zo.Mananananana.Foo.Bar.Qaz.Zac.Bla, :func, 0} == @subject.get_mfa_from_string("Zi.Zo.Mananananana.Foo.Bar.Qaz.Zac.Bla.func/0") 31 | end 32 | 33 | test "it raises an error when the string isn't valid" do 34 | assert_raise RuntimeError, fn -> 35 | @subject.get_mfa_from_string("oh no bad string") 36 | end 37 | end 38 | 39 | test "it raises an error when the MFA isn't valid" do 40 | assert_raise RuntimeError, fn -> 41 | @subject.get_mfa_from_string({:bla, 123, :blu, 456}) 42 | end 43 | end 44 | 45 | test "it returns the MFA of an existing function" do 46 | {module, func, arity} = @subject.get_mfa_from_string("Enum.member?/2") 47 | 48 | assert {Enum, :member?, 2} == {module, func, arity} 49 | assert function_exported?(module, func, arity) 50 | end 51 | end 52 | 53 | describe "get_intercepted_mfa_from_string/1" do 54 | test "it returns the MFA of a string with wildcarded arity" do 55 | assert {Bla.Blu, :func, :*} == @subject.get_intercepted_mfa_from_string("Bla.Blu.func/*") 56 | end 57 | 58 | test "it returns the MFA of a string with wildcarded function and arity" do 59 | assert {Bla.Blu, :*, :*} == @subject.get_intercepted_mfa_from_string("Bla.Blu.*/*") 60 | end 61 | 62 | test "it returns the exact same MFA of a tuple with wildcarded arity" do 63 | assert {Bla.Blu, :func, :*} == @subject.get_intercepted_mfa_from_string({Bla.Blu, :func, :*}) 64 | end 65 | 66 | test "it returns the exact same MFA of a tuple with wildcarded function and arity" do 67 | assert {Bla.Blu, :*, :*} == @subject.get_intercepted_mfa_from_string({Bla.Blu, :*, :*}) 68 | end 69 | 70 | test "it raises an error when the string isn't valid" do 71 | assert_raise RuntimeError, fn -> 72 | @subject.get_intercepted_mfa_from_string("oh no bad string") 73 | end 74 | end 75 | 76 | test "it raises an error when the MFA isn't valid" do 77 | assert_raise RuntimeError, fn -> 78 | @subject.get_intercepted_mfa_from_string({:bla, 123, :blu, 456}) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/intercepted_modules/streamlined_intercept_config.ex: -------------------------------------------------------------------------------- 1 | defmodule StreamlinedInterceptConfig do 2 | use Interceptor.Configurator 3 | 4 | intercept "InterceptedOnBefore1.to_intercept/0", 5 | before: "Before.Callback.before/1" 6 | intercept "Bla.Ble.Bli.Module.Name.Big.TooBig.to_intercept/0", 7 | after: "After.Callback.right_after/2" 8 | end 9 | 10 | 11 | defmodule StreamlinedInterceptConfig2 do 12 | use Interceptor.Configurator 13 | 14 | intercept "InterceptedOnBefore1.to_intercept/0", 15 | before: "Before.Callback.before/1", 16 | after: "After.Callback.right_after/2", 17 | on_success: "Success.Callback.if_everything_ok/3", 18 | on_error: "Error.Callback.if_borked/3" 19 | end 20 | 21 | 22 | defmodule StreamlinedInterceptConfigMixedFormat do 23 | use Interceptor.Configurator 24 | 25 | intercept "InterceptedOnBefore1.to_intercept/0", 26 | before: "Before.Callback.before/1", 27 | after: "After.Callback.right_after/2", 28 | on_success: "Success.Callback.if_everything_ok/3", 29 | on_error: "Error.Callback.if_borked/3" 30 | end 31 | 32 | 33 | defmodule StreamlinedInterceptConfigMixedFormat2 do 34 | use Interceptor.Configurator 35 | 36 | intercept {InterceptedOnBefore1, :to_intercept, 0}, 37 | before: "Before.Callback.before/1", 38 | after: {After.Callback, :right_after, 2}, 39 | on_success: "Success.Callback.if_everything_ok/3", 40 | on_error: {Error.Callback, :if_borked, 3} 41 | end 42 | 43 | defmodule StreamlinedInterceptConfigWildcarded do 44 | use Interceptor.Configurator 45 | 46 | intercept "InterceptedOnBefore1.*/*", 47 | before: "Before.Callback.before/1", 48 | after: {After.Callback, :right_after, 2}, 49 | on_success: "Success.Callback.if_everything_ok/3", 50 | on_error: {Error.Callback, :if_borked, 3} 51 | 52 | intercept "InterceptedOnBefore1.some_function/*", 53 | before: "Before.Callback.before/1", 54 | after: {After.Callback, :right_after, 2}, 55 | on_success: "Success.Callback.if_everything_ok/3", 56 | on_error: {Error.Callback, :if_borked, 3} 57 | end 58 | 59 | defmodule StreamlinedInterceptConfigBad do 60 | use Interceptor.Configurator 61 | 62 | intercept "InterceptedOnBefore1.to_intercept/0", 63 | before: "A B C D E F G H I J K L M N O P Q R S T U V" 64 | end 65 | 66 | defmodule StreamlinedInterceptConfigWildcardedCallbacks1 do 67 | use Interceptor.Configurator 68 | 69 | intercept "InterceptedOnBefore1.to_intercept/0", 70 | before: "Kaboom.function/*" 71 | end 72 | 73 | defmodule StreamlinedInterceptConfigWildcardedCallbacks2 do 74 | use Interceptor.Configurator 75 | 76 | intercept "InterceptedOnBefore1.to_intercept/0", 77 | after: "Kaboom.*/*" 78 | end 79 | 80 | defmodule StreamlinedInterceptConfigWildcardedCallbacks3 do 81 | use Interceptor.Configurator 82 | 83 | intercept "InterceptedOnBefore1.to_intercept/0", 84 | after: {Kaboom, :function, :*} 85 | end 86 | 87 | defmodule StreamlinedInterceptConfigWildcardedCallbacks4 do 88 | use Interceptor.Configurator 89 | 90 | intercept "InterceptedOnBefore1.to_intercept/0", 91 | after: {Kaboom, :*, :*} 92 | end 93 | -------------------------------------------------------------------------------- /test/do_block/interceptor_on_after_own_configuration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorOnAfterOwnConfigurationTest do 2 | use ExUnit.Case 3 | 4 | @process_name :after_test_process 5 | 6 | describe "module whose own intercept config points to other module" do 7 | test "it intercepts the function after it is called, overriding the global intercept config" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = InterceptedOnAfterOwnConfiguration1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {InterceptedOnAfterOwnConfiguration1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module whose own intercept config has the intercept config" do 23 | test "it intercepts the function after it is called" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = InterceptedOnAfterOwnConfiguration2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert intercepted_mfa == {InterceptedOnAfterOwnConfiguration2, :to_intercept, []} 35 | end 36 | end 37 | 38 | describe "module whose own intercept config has the streamlined intercept config" do 39 | test "it intercepts the function after it is called" do 40 | {:ok, _pid} = spawn_agent() 41 | 42 | result = InterceptedOnAfterOwnConfiguration3.to_intercept() 43 | 44 | callback_calls = get_agent_messages() 45 | 46 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 47 | 48 | assert length(callback_calls) == 1 49 | assert result == callback_result 50 | assert intercepted_mfa == {InterceptedOnAfterOwnConfiguration3, :to_intercept, []} 51 | end 52 | end 53 | 54 | describe "module whose own intercept config has a mixed streamlined intercept config" do 55 | test "it intercepts the function after it is called" do 56 | {:ok, _pid} = spawn_agent() 57 | 58 | result = InterceptedOnAfterOwnConfiguration4.to_intercept() 59 | 60 | callback_calls = get_agent_messages() 61 | 62 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 63 | 64 | assert length(callback_calls) == 1 65 | assert result == callback_result 66 | assert intercepted_mfa == {InterceptedOnAfterOwnConfiguration4, :to_intercept, []} 67 | end 68 | end 69 | 70 | defp spawn_agent() do 71 | @process_name 72 | |> Process.whereis() 73 | |> kill_agent() 74 | 75 | {:ok, pid} = Agent.start_link(fn -> [] end) 76 | true = Process.register(pid, @process_name) 77 | 78 | {:ok, pid} 79 | end 80 | 81 | defp kill_agent(nil), do: false 82 | defp kill_agent(pid) do 83 | case Process.alive?(pid) do 84 | true -> Process.exit(pid, :kill) 85 | _ -> false 86 | end 87 | end 88 | 89 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 90 | end 91 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_on_after_own_configuration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorOnAfterOwnConfigurationTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_after_test_process 5 | 6 | describe "module whose own intercept config points to other module" do 7 | test "it intercepts the function after it is called, overriding the global intercept config" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = AnnotatedInterceptedOnAfterOwnConfiguration1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {AnnotatedInterceptedOnAfterOwnConfiguration1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module whose own intercept config has the intercept config" do 23 | test "it intercepts the function after it is called" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = AnnotatedInterceptedOnAfterOwnConfiguration2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert intercepted_mfa == {AnnotatedInterceptedOnAfterOwnConfiguration2, :to_intercept, []} 35 | end 36 | end 37 | 38 | describe "module whose own intercept config has the streamlined intercept config" do 39 | test "it intercepts the function after it is called" do 40 | {:ok, _pid} = spawn_agent() 41 | 42 | result = AnnotatedInterceptedOnAfterOwnConfiguration3.to_intercept() 43 | 44 | callback_calls = get_agent_messages() 45 | 46 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 47 | 48 | assert length(callback_calls) == 1 49 | assert result == callback_result 50 | assert intercepted_mfa == {AnnotatedInterceptedOnAfterOwnConfiguration3, :to_intercept, []} 51 | end 52 | end 53 | 54 | describe "module whose own intercept config has a mixed streamlined intercept config" do 55 | test "it intercepts the function after it is called" do 56 | {:ok, _pid} = spawn_agent() 57 | 58 | result = AnnotatedInterceptedOnAfterOwnConfiguration4.to_intercept() 59 | 60 | callback_calls = get_agent_messages() 61 | 62 | [{:callback_overridden, callback_result, intercepted_mfa}] = callback_calls 63 | 64 | assert length(callback_calls) == 1 65 | assert result == callback_result 66 | assert intercepted_mfa == {AnnotatedInterceptedOnAfterOwnConfiguration4, :to_intercept, []} 67 | end 68 | end 69 | 70 | defp spawn_agent() do 71 | @process_name 72 | |> Process.whereis() 73 | |> kill_agent() 74 | 75 | {:ok, pid} = Agent.start_link(fn -> [] end) 76 | true = Process.register(pid, @process_name) 77 | 78 | {:ok, pid} 79 | end 80 | 81 | defp kill_agent(nil), do: false 82 | defp kill_agent(pid) do 83 | case Process.alive?(pid) do 84 | true -> Process.exit(pid, :kill) 85 | _ -> false 86 | end 87 | end 88 | 89 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 90 | end 91 | -------------------------------------------------------------------------------- /lib/configuration/configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Configuration do 2 | 3 | alias Interceptor.Configurator 4 | 5 | def debug_mode? do 6 | debug_mode? = Application.get_env(:interceptor, :debug, false) 7 | 8 | case is_boolean(debug_mode?) do 9 | true -> debug_mode? 10 | _ -> false 11 | end 12 | end 13 | 14 | def get_interceptor_module_function_for({module, function, args} = _to_intercept, interception_type) when is_list(args) do 15 | mfa_to_intercept = {module, function, length(args)} 16 | 17 | get_interceptor_module_function_for(mfa_to_intercept, interception_type) 18 | end 19 | 20 | def get_interceptor_module_function_for({module, function, nil = _arity}, interception_type) do 21 | # functions with arity 0 without parens have their arity as nil 22 | get_interceptor_module_function_for({module, function, 0}, interception_type) 23 | end 24 | 25 | def get_interceptor_module_function_for({module, function, _arity} = to_intercept, interception_type) do 26 | interception_configuration = get_configuration(module) 27 | configuration = interception_configuration[to_intercept] 28 | || interception_configuration[{module, function, :*}] 29 | || interception_configuration[{module, :*, :*}] 30 | 31 | configuration && Keyword.get(configuration, interception_type) 32 | end 33 | 34 | def mfa_is_intercepted?({_module, _function, _args} = mfa) do 35 | [ 36 | get_interceptor_module_function_for(mfa, :before), 37 | get_interceptor_module_function_for(mfa, :after), 38 | get_interceptor_module_function_for(mfa, :on_success), 39 | get_interceptor_module_function_for(mfa, :on_error), 40 | get_interceptor_module_function_for(mfa, :wrapper) 41 | ] 42 | |> Enum.reduce(false, 43 | fn intercept_config, acc -> acc || intercept_config != nil end) 44 | end 45 | 46 | def get_global_configuration() do 47 | Application.get_env(:interceptor, :configuration) 48 | |> case do 49 | config when is_map(config) -> config 50 | config_module -> 51 | config_module 52 | |> config_module_exists?() 53 | |> get_configuration_from_module() 54 | end 55 | end 56 | 57 | def get_configuration(module) do 58 | global_config = get_global_configuration() 59 | 60 | own_config = get_own_configuration(module) 61 | Map.merge(global_config, own_config) 62 | end 63 | 64 | defp get_own_configuration(module) do 65 | case Module.get_attribute(module, :own_config) do 66 | config when is_map(config) -> 67 | Configurator.transform_streamlined_config_to_tuple_config(config) 68 | module -> 69 | module 70 | |> config_module_exists?() 71 | |> get_configuration_from_module() 72 | end 73 | end 74 | 75 | defp config_module_exists?(module) do 76 | {ensure_result, _compiled_module} = Code.ensure_compiled(module) 77 | compiled? = ensure_result == :module 78 | 79 | defines_function? = [__info__: 1, get_intercept_config: 0] 80 | |> Enum.map(fn {name, arity} -> function_exported?(module, name, arity) end) 81 | |> Enum.all?(&(&1)) 82 | 83 | {compiled? && defines_function?, module} 84 | end 85 | 86 | defp get_configuration_from_module({false, nil}), do: %{} 87 | 88 | defp get_configuration_from_module({false, module}), 89 | do: raise "Your interceptor configuration is pointing to #{inspect(module)}, an invalid (non-existent?) module. Please check your configuration and try again. The module needs to exist and expose the get_intercept_config/0 function." 90 | 91 | defp get_configuration_from_module({true, module}), do: module.get_intercept_config() 92 | end 93 | -------------------------------------------------------------------------------- /test/do_block/interceptor_on_before_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorOnBeforeTest do 2 | use ExUnit.Case 3 | 4 | @process_name :before_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function before it is called" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = InterceptedOnBefore1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{intercepted_timestamp, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result > intercepted_timestamp 18 | assert intercepted_mfa == {InterceptedOnBefore1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module with two functions and other statement" do 23 | test "it intercepts the function before it is called" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = InterceptedOnBefore2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{intercepted_timestamp, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result > intercepted_timestamp 34 | assert intercepted_mfa == {InterceptedOnBefore2, :to_intercept, []} 35 | end 36 | 37 | test "it also intercepts the other function" do 38 | {:ok, _pid} = spawn_agent() 39 | 40 | result = InterceptedOnBefore2.other_to_intercept() 41 | 42 | callback_calls = get_agent_messages() 43 | 44 | [{_intercepted_timestamp, intercepted_mfa}] = callback_calls 45 | 46 | assert length(callback_calls) == 1 47 | assert result == "HELLO" 48 | assert intercepted_mfa == {InterceptedOnBefore2, :other_to_intercept, []} 49 | end 50 | end 51 | 52 | describe "module with two functions and a private one" do 53 | test "it intercepts the function" do 54 | {:ok, _pid} = spawn_agent() 55 | 56 | result = InterceptedOnBefore3.other_to_intercept(4) 57 | 58 | callback_calls = get_agent_messages() 59 | 60 | [{_intercepted_timestamp, intercepted_mfa}] = callback_calls 61 | 62 | assert length(callback_calls) == 1 63 | assert result == 10 64 | assert intercepted_mfa == {InterceptedOnBefore3, :other_to_intercept, [4]} 65 | end 66 | 67 | test "it doesn't intercept the function that isn't configured" do 68 | {:ok, _pid} = spawn_agent() 69 | 70 | now = Interceptor.Utils.timestamp() 71 | Process.sleep(50) 72 | result = InterceptedOnBefore3.not_to_intercept() 73 | 74 | callback_calls = get_agent_messages() 75 | 76 | assert result > now 77 | assert length(callback_calls) == 0 78 | end 79 | end 80 | 81 | describe "module with a single zero args function" do 82 | test "it intercepts the function" do 83 | {:ok, _pid} = spawn_agent() 84 | 85 | result = InterceptedOnBefore4.to_intercept() 86 | 87 | callback_calls = get_agent_messages() 88 | 89 | [{_intercepted_timestamp, intercepted_mfa}] = callback_calls 90 | 91 | assert length(callback_calls) == 1 92 | assert result == "Hello, even without args" 93 | assert intercepted_mfa == {InterceptedOnBefore4, :to_intercept, []} 94 | end 95 | end 96 | 97 | defp spawn_agent() do 98 | @process_name 99 | |> Process.whereis() 100 | |> kill_agent() 101 | 102 | {:ok, pid} = Agent.start_link(fn -> [] end) 103 | true = Process.register(pid, @process_name) 104 | 105 | {:ok, pid} 106 | end 107 | 108 | defp kill_agent(nil), do: false 109 | defp kill_agent(pid) do 110 | case Process.alive?(pid) do 111 | true -> Process.exit(pid, :kill) 112 | _ -> false 113 | end 114 | end 115 | 116 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 117 | end 118 | -------------------------------------------------------------------------------- /lib/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Utils do 2 | alias Interceptor.Configuration 3 | 4 | @mfa_regex ~r/(.*)\.(.*)\/(\d)$/ 5 | @mfa_arity_wildcarded_regex ~r/(.*)\.(.*)\/(\*)$/ 6 | @mfa_function_and_arity_wildcarded_regex ~r/(.*)\.(\*)\/(\*)$/ 7 | 8 | @allowed_intercepted_string_format "'./', './*' or '.*/*'" 9 | @allowed_intercepted_tuple_format "{Module, :function, }, {Module, :function, :*} or {Module, :*, :*}" 10 | 11 | def timestamp(), do: :os.system_time(:microsecond) 12 | 13 | def random_string(length \\ 20) do 14 | length 15 | |> :crypto.strong_rand_bytes() 16 | |> Base.encode32() 17 | |> String.slice(0..(length-1)) 18 | |> String.downcase() 19 | end 20 | 21 | def random_atom(length \\ 20), 22 | do: 23 | length 24 | |> random_string() 25 | |> String.to_atom() 26 | 27 | def get_intercepted_mfa_from_string(mfa_string) when is_binary(mfa_string) do 28 | parse_mfa_with_regexes(mfa_string) 29 | |> case do 30 | nil -> raise("Invalid intercepted MFA (#{mfa_string}), it needs to have the format #{@allowed_intercepted_string_format}.") 31 | [_all, string_module, string_function, string_arity] -> mfa_from_string(string_module, string_function, string_arity) 32 | end 33 | end 34 | 35 | def get_intercepted_mfa_from_string({module, function, arity} = mfa) when is_atom(module) and is_atom(function) and is_integer(arity) do 36 | mfa 37 | end 38 | 39 | def get_intercepted_mfa_from_string({module, :*, :*} = mfa) when is_atom(module) do 40 | mfa 41 | end 42 | 43 | def get_intercepted_mfa_from_string({module, function, :*} = mfa) when is_atom(module) and is_atom(function) do 44 | mfa 45 | end 46 | 47 | def get_intercepted_mfa_from_string(not_an_mfa) do 48 | raise("Invalid intercepted MFA (#{inspect(not_an_mfa)}), it needs to have the format #{@allowed_intercepted_string_format} or #{@allowed_intercepted_tuple_format}.") 49 | end 50 | 51 | def get_mfa_from_string(mfa_string) when is_binary(mfa_string) do 52 | Regex.run(@mfa_regex, mfa_string) 53 | |> case do 54 | nil -> raise("Invalid MFA (#{mfa_string}), it needs to have the format './'.") 55 | [_all, string_module, string_function, string_arity] -> mfa_from_string(string_module, string_function, string_arity) 56 | end 57 | end 58 | 59 | def get_mfa_from_string({module, function, arity} = mfa) when is_atom(module) and is_atom(function) and is_integer(arity) do 60 | mfa 61 | end 62 | 63 | def get_mfa_from_string(not_an_mfa) do 64 | raise("Invalid MFA (#{inspect(not_an_mfa)}), it needs to have the format './' or {Module, :function, }.") 65 | end 66 | 67 | def check_if_mfa_exists(module, function, arity) do 68 | {ensure_result, _loaded_module} = Code.ensure_loaded(module) 69 | loaded? = ensure_result == :module 70 | 71 | exported? = function_exported?(module, function, arity) 72 | function_exists? = loaded? && exported? 73 | 74 | if Configuration.debug_mode?() && !function_exists? do 75 | IO.puts("Warning! Invalid MFA (#{module}.#{function}/#{arity}), the given function doesn't exist. Module loaded? #{loaded?}, Function exported? #{exported?}") 76 | end 77 | 78 | function_exists? 79 | end 80 | 81 | defp mfa_from_string(string_module, string_function, string_arity) do 82 | module = String.to_atom("Elixir.#{string_module}") 83 | 84 | function = String.to_atom(string_function) 85 | arity = case string_arity do 86 | "*" -> :* 87 | _arity -> String.to_integer(string_arity) 88 | end 89 | 90 | {module, function, arity} 91 | end 92 | 93 | defp parse_mfa_with_regexes(mfa_string) do 94 | Regex.run(@mfa_regex, mfa_string) || 95 | Regex.run(@mfa_arity_wildcarded_regex, mfa_string) || 96 | Regex.run(@mfa_function_and_arity_wildcarded_regex, mfa_string) 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_on_before_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorOnBeforeTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_before_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function before it is called" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = AnnotatedInterceptedOnBefore1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{intercepted_timestamp, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result > intercepted_timestamp 18 | assert intercepted_mfa == {AnnotatedInterceptedOnBefore1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module with two functions and other statement" do 23 | test "it intercepts the function before it is called" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = AnnotatedInterceptedOnBefore2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{intercepted_timestamp, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result > intercepted_timestamp 34 | assert intercepted_mfa == {AnnotatedInterceptedOnBefore2, :to_intercept, []} 35 | end 36 | 37 | test "it also intercepts the other function" do 38 | {:ok, _pid} = spawn_agent() 39 | 40 | result = AnnotatedInterceptedOnBefore2.other_to_intercept() 41 | 42 | callback_calls = get_agent_messages() 43 | 44 | [{_intercepted_timestamp, intercepted_mfa}] = callback_calls 45 | 46 | assert length(callback_calls) == 1 47 | assert result == "HELLO" 48 | assert intercepted_mfa == {AnnotatedInterceptedOnBefore2, :other_to_intercept, []} 49 | end 50 | end 51 | 52 | describe "module with two functions and a private one" do 53 | test "it intercepts the function" do 54 | {:ok, _pid} = spawn_agent() 55 | 56 | result = AnnotatedInterceptedOnBefore3.other_to_intercept(4) 57 | 58 | callback_calls = get_agent_messages() 59 | 60 | [{_intercepted_timestamp, intercepted_mfa}] = callback_calls 61 | 62 | assert length(callback_calls) == 1 63 | assert result == 10 64 | assert intercepted_mfa == {AnnotatedInterceptedOnBefore3, :other_to_intercept, [4]} 65 | end 66 | 67 | test "it doesn't intercept the function that isn't configured" do 68 | {:ok, _pid} = spawn_agent() 69 | 70 | now = Interceptor.Utils.timestamp() 71 | Process.sleep(50) 72 | result = AnnotatedInterceptedOnBefore3.not_to_intercept() 73 | 74 | callback_calls = get_agent_messages() 75 | 76 | assert result > now 77 | assert length(callback_calls) == 0 78 | end 79 | end 80 | 81 | describe "module with a single zero args function" do 82 | test "it intercepts the function" do 83 | {:ok, _pid} = spawn_agent() 84 | 85 | result = AnnotatedInterceptedOnBefore4.to_intercept() 86 | 87 | callback_calls = get_agent_messages() 88 | 89 | [{_intercepted_timestamp, intercepted_mfa}] = callback_calls 90 | 91 | assert length(callback_calls) == 1 92 | assert result == "Hello, even without args" 93 | assert intercepted_mfa == {AnnotatedInterceptedOnBefore4, :to_intercept, []} 94 | end 95 | end 96 | 97 | defp spawn_agent() do 98 | @process_name 99 | |> Process.whereis() 100 | |> kill_agent() 101 | 102 | {:ok, pid} = Agent.start_link(fn -> [] end) 103 | true = Process.register(pid, @process_name) 104 | 105 | {:ok, pid} 106 | end 107 | 108 | defp kill_agent(nil), do: false 109 | defp kill_agent(pid) do 110 | case Process.alive?(pid) do 111 | true -> Process.exit(pid, :kill) 112 | _ -> false 113 | end 114 | end 115 | 116 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 117 | end 118 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_edge_cases_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorEdgeCasesTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_edge_cases_test_process 5 | 6 | describe "functions with argument using `<>`" do 7 | test "it passes the argument with the prefix before `<>`" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = AnnotatedInterceptedEdgeCases1.intercept_with_prefix("some_prefix:blabla") 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {AnnotatedInterceptedEdgeCases1, :intercept_with_prefix, ["some_prefix:blabla"]} 19 | end 20 | end 21 | 22 | describe "module with functions with ignored arguments" do 23 | test "it passes the ignored arguments as `:arg_cant_be_intercepted`" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = AnnotatedInterceptedEdgeCases1.to_intercept(1, 2, 3) 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert intercepted_mfa == {AnnotatedInterceptedEdgeCases1, :to_intercept, [1, 2, :arg_cant_be_intercepted]} 35 | end 36 | end 37 | 38 | describe "module with functions whose arguments are pattern-matching on atoms" do 39 | test "it intercepts the function as expected" do 40 | {:ok, _pid} = spawn_agent() 41 | 42 | result = AnnotatedInterceptedEdgeCases1.intercept_pattern_match_atom_argument(1, :normal) 43 | 44 | callback_calls = get_agent_messages() 45 | 46 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 47 | 48 | assert length(callback_calls) == 1 49 | assert result == callback_result 50 | assert intercepted_mfa == {AnnotatedInterceptedEdgeCases1, :intercept_pattern_match_atom_argument, [1, :normal]} 51 | end 52 | end 53 | 54 | describe "custom intercept attribute and functions with argument using `<>`" do 55 | test "it passes the argument with the prefix before `<>`" do 56 | {:ok, _pid} = spawn_agent() 57 | 58 | result = AnnotatedInterceptedEdgeCases2.intercept_with_prefix("some_prefix:blabla") 59 | 60 | callback_calls = get_agent_messages() 61 | 62 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 63 | 64 | assert length(callback_calls) == 1 65 | assert result == callback_result 66 | assert intercepted_mfa == {AnnotatedInterceptedEdgeCases2, :intercept_with_prefix, ["some_prefix:blabla"]} 67 | end 68 | end 69 | 70 | describe "custom intercept attribute and module with functions with ignored arguments" do 71 | test "it passes the ignored arguments as `:arg_cant_be_intercepted`" do 72 | {:ok, _pid} = spawn_agent() 73 | 74 | result = AnnotatedInterceptedEdgeCases2.to_intercept(1, 2, 3) 75 | 76 | callback_calls = get_agent_messages() 77 | 78 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 79 | 80 | assert length(callback_calls) == 1 81 | assert result == callback_result 82 | assert intercepted_mfa == {AnnotatedInterceptedEdgeCases2, :to_intercept, [1, 2, :arg_cant_be_intercepted]} 83 | end 84 | end 85 | 86 | defp spawn_agent() do 87 | @process_name 88 | |> Process.whereis() 89 | |> kill_agent() 90 | 91 | {:ok, pid} = Agent.start_link(fn -> [] end) 92 | true = Process.register(pid, @process_name) 93 | 94 | {:ok, pid} 95 | end 96 | 97 | defp kill_agent(nil), do: false 98 | defp kill_agent(pid) do 99 | case Process.alive?(pid) do 100 | true -> Process.exit(pid, :kill) 101 | _ -> false 102 | end 103 | end 104 | 105 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 106 | end 107 | -------------------------------------------------------------------------------- /test/do_block/interceptor_wrapper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorWrapperTest do 2 | use ExUnit.Case 3 | 4 | @process_name :wrapper_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function passing it as a lambda" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = InterceptedByWrapper1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {InterceptedByWrapper1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module with two functions and other statement" do 23 | test "it intercepts the function passing it as a lambda" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = InterceptedByWrapper2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert intercepted_timestamp > result 35 | assert intercepted_mfa == {InterceptedByWrapper2, :to_intercept, []} 36 | end 37 | 38 | test "it also intercepts the other function" do 39 | {:ok, _pid} = spawn_agent() 40 | 41 | result = InterceptedByWrapper2.other_to_intercept() 42 | 43 | callback_calls = get_agent_messages() 44 | 45 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 46 | 47 | assert length(callback_calls) == 1 48 | assert result == callback_result 49 | assert result == "HELLO" 50 | assert intercepted_mfa == {InterceptedByWrapper2, :other_to_intercept, []} 51 | end 52 | end 53 | 54 | describe "module with two functions and a private one" do 55 | test "it intercepts the function" do 56 | {:ok, _pid} = spawn_agent() 57 | 58 | result = InterceptedByWrapper3.other_to_intercept(4) 59 | 60 | callback_calls = get_agent_messages() 61 | 62 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 63 | 64 | assert length(callback_calls) == 1 65 | assert result == result_callback 66 | assert result == 10 67 | assert intercepted_mfa == {InterceptedByWrapper3, :other_to_intercept, [4]} 68 | end 69 | 70 | test "it doesn't intercept the function that isn't configured" do 71 | {:ok, _pid} = spawn_agent() 72 | 73 | now = Interceptor.Utils.timestamp() 74 | Process.sleep(50) 75 | result = InterceptedByWrapper3.not_to_intercept() 76 | 77 | callback_calls = get_agent_messages() 78 | 79 | assert result > now 80 | assert length(callback_calls) == 0 81 | end 82 | end 83 | 84 | describe "other module with a single function" do 85 | test "it intercepts the function passing it as a lambda and changes the return value" do 86 | {:ok, _pid} = spawn_agent() 87 | 88 | result = InterceptedByWrapper4.to_intercept() 89 | 90 | callback_calls = get_agent_messages() 91 | 92 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 93 | 94 | assert length(callback_calls) == 1 95 | assert result != callback_result 96 | assert result == "Hello" 97 | assert intercepted_mfa == {InterceptedByWrapper4, :to_intercept, []} 98 | end 99 | end 100 | 101 | defp spawn_agent() do 102 | @process_name 103 | |> Process.whereis() 104 | |> kill_agent() 105 | 106 | {:ok, pid} = Agent.start_link(fn -> [] end) 107 | true = Process.register(pid, @process_name) 108 | 109 | {:ok, pid} 110 | end 111 | 112 | defp kill_agent(nil), do: false 113 | defp kill_agent(pid) do 114 | case Process.alive?(pid) do 115 | true -> Process.exit(pid, :kill) 116 | _ -> false 117 | end 118 | end 119 | 120 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 121 | end 122 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_wrapper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorWrapperTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_wrapper_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function passing it as a lambda" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = AnnotatedInterceptedByWrapper1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {AnnotatedInterceptedByWrapper1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module with two functions and other statement" do 23 | test "it intercepts the function passing it as a lambda" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = AnnotatedInterceptedByWrapper2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert intercepted_timestamp > result 35 | assert intercepted_mfa == {AnnotatedInterceptedByWrapper2, :to_intercept, []} 36 | end 37 | 38 | test "it also intercepts the other function" do 39 | {:ok, _pid} = spawn_agent() 40 | 41 | result = AnnotatedInterceptedByWrapper2.other_to_intercept() 42 | 43 | callback_calls = get_agent_messages() 44 | 45 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 46 | 47 | assert length(callback_calls) == 1 48 | assert result == callback_result 49 | assert result == "HELLO" 50 | assert intercepted_mfa == {AnnotatedInterceptedByWrapper2, :other_to_intercept, []} 51 | end 52 | end 53 | 54 | describe "module with two functions and a private one" do 55 | test "it intercepts the function" do 56 | {:ok, _pid} = spawn_agent() 57 | 58 | result = AnnotatedInterceptedByWrapper3.other_to_intercept(4) 59 | 60 | callback_calls = get_agent_messages() 61 | 62 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 63 | 64 | assert length(callback_calls) == 1 65 | assert result == result_callback 66 | assert result == 10 67 | assert intercepted_mfa == {AnnotatedInterceptedByWrapper3, :other_to_intercept, [4]} 68 | end 69 | 70 | test "it doesn't intercept the function that isn't configured" do 71 | {:ok, _pid} = spawn_agent() 72 | 73 | now = Interceptor.Utils.timestamp() 74 | Process.sleep(50) 75 | result = AnnotatedInterceptedByWrapper3.not_to_intercept() 76 | 77 | callback_calls = get_agent_messages() 78 | 79 | assert result > now 80 | assert length(callback_calls) == 0 81 | end 82 | end 83 | 84 | describe "other module with a single function" do 85 | test "it intercepts the function passing it as a lambda and changes the return value" do 86 | {:ok, _pid} = spawn_agent() 87 | 88 | result = AnnotatedInterceptedByWrapper4.to_intercept() 89 | 90 | callback_calls = get_agent_messages() 91 | 92 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 93 | 94 | assert length(callback_calls) == 1 95 | assert result != callback_result 96 | assert result == "Hello" 97 | assert intercepted_mfa == {AnnotatedInterceptedByWrapper4, :to_intercept, []} 98 | end 99 | end 100 | 101 | defp spawn_agent() do 102 | @process_name 103 | |> Process.whereis() 104 | |> kill_agent() 105 | 106 | {:ok, pid} = Agent.start_link(fn -> [] end) 107 | true = Process.register(pid, @process_name) 108 | 109 | {:ok, pid} 110 | end 111 | 112 | defp kill_agent(nil), do: false 113 | defp kill_agent(pid) do 114 | case Process.alive?(pid) do 115 | true -> Process.exit(pid, :kill) 116 | _ -> false 117 | end 118 | end 119 | 120 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 121 | end 122 | -------------------------------------------------------------------------------- /test/do_block/interceptor_on_error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorOnErrorTest do 2 | use ExUnit.Case 3 | 4 | @process_name :on_error_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function after it errors" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 11 | InterceptedOnError1.to_intercept() 12 | end 13 | 14 | callback_calls = get_agent_messages() 15 | 16 | [{ 17 | _started_at_timestamp, 18 | _intercepted_timestamp, 19 | intercepted_error, 20 | intercepted_mfa 21 | }] = callback_calls 22 | 23 | assert length(callback_calls) == 1 24 | assert %ArithmeticError{} == intercepted_error 25 | assert intercepted_mfa == {InterceptedOnError1, :to_intercept, []} 26 | end 27 | end 28 | 29 | describe "module with two functions and other statement" do 30 | test "it intercepts the function on error" do 31 | {:ok, _pid} = spawn_agent() 32 | 33 | before_timestamp = Interceptor.Utils.timestamp() 34 | Process.sleep(10) 35 | 36 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 37 | InterceptedOnError2.to_intercept() 38 | end 39 | 40 | callback_calls = get_agent_messages() 41 | 42 | [{ 43 | started_at_timestamp, 44 | intercepted_timestamp, 45 | intercepted_error, 46 | intercepted_mfa 47 | }] = callback_calls 48 | 49 | assert length(callback_calls) == 1 50 | assert before_timestamp < started_at_timestamp 51 | time_it_took_microseconds = intercepted_timestamp - started_at_timestamp 52 | assert 200_000 < time_it_took_microseconds 53 | assert %ArithmeticError{} == intercepted_error 54 | assert intercepted_mfa == {InterceptedOnError2, :to_intercept, []} 55 | end 56 | 57 | test "it also intercepts the other function" do 58 | {:ok, _pid} = spawn_agent() 59 | 60 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 61 | InterceptedOnError2.other_to_intercept() 62 | end 63 | 64 | callback_calls = get_agent_messages() 65 | 66 | [{ 67 | _started_at_timestamp, 68 | _intercepted_timestamp, 69 | _intercepted_error, 70 | intercepted_mfa 71 | }] = callback_calls 72 | 73 | assert length(callback_calls) == 1 74 | assert intercepted_mfa == {InterceptedOnError2, :other_to_intercept, []} 75 | end 76 | end 77 | 78 | describe "module with two functions and a private one" do 79 | test "it intercepts the function on error" do 80 | {:ok, _pid} = spawn_agent() 81 | 82 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 83 | InterceptedOnError3.other_to_intercept(4) 84 | end 85 | 86 | callback_calls = get_agent_messages() 87 | 88 | [{ 89 | _started_at_timestamp, 90 | _intercepted_timestamp, 91 | _intercepted_error, 92 | intercepted_mfa 93 | }] = callback_calls 94 | 95 | assert length(callback_calls) == 1 96 | assert intercepted_mfa == {InterceptedOnError3, :other_to_intercept, [4]} 97 | end 98 | 99 | test "it doesn't intercept the function that isn't configured" do 100 | {:ok, _pid} = spawn_agent() 101 | 102 | assert_raise ArgumentError, ~r/argument error/, fn -> 103 | InterceptedOnError3.not_to_intercept() 104 | end 105 | 106 | callback_calls = get_agent_messages() 107 | 108 | assert length(callback_calls) == 0 109 | end 110 | 111 | test "it doesn't intercept the function that is outside of the intercept block" do 112 | {:ok, _pid} = spawn_agent() 113 | 114 | assert_raise ArgumentError, ~r/argument error/, fn -> 115 | InterceptedOnError3.definitely_not_to_intercept() 116 | end 117 | 118 | callback_calls = get_agent_messages() 119 | 120 | assert length(callback_calls) == 0 121 | end 122 | end 123 | 124 | defp spawn_agent() do 125 | @process_name 126 | |> Process.whereis() 127 | |> kill_agent() 128 | 129 | {:ok, pid} = Agent.start_link(fn -> [] end) 130 | true = Process.register(pid, @process_name) 131 | 132 | {:ok, pid} 133 | end 134 | 135 | defp kill_agent(nil), do: false 136 | defp kill_agent(pid) do 137 | case Process.alive?(pid) do 138 | true -> Process.exit(pid, :kill) 139 | _ -> false 140 | end 141 | end 142 | 143 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 144 | end 145 | -------------------------------------------------------------------------------- /test/do_block/interceptor_wildcarded_mfa_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorWildcardedMfaTest do 2 | use ExUnit.Case 3 | 4 | @process_name :wildcarded_mfa_test_process 5 | 6 | describe "intercepted function MFA with arity wildcards" do 7 | test "it intercepts the same function with arity 1" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = InterceptedWildcardedMfa1.foo(123) 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {InterceptedWildcardedMfa1, :foo, [123]} 19 | end 20 | 21 | test "it intercepts the same function with arity 2" do 22 | {:ok, _pid} = spawn_agent() 23 | 24 | result = InterceptedWildcardedMfa1.foo(123, 456) 25 | 26 | callback_calls = get_agent_messages() 27 | 28 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 29 | 30 | assert length(callback_calls) == 1 31 | assert result == callback_result 32 | assert intercepted_mfa == {InterceptedWildcardedMfa1, :foo, [123, 456]} 33 | end 34 | 35 | test "it intercepts the same function with arity 5" do 36 | {:ok, _pid} = spawn_agent() 37 | 38 | result = InterceptedWildcardedMfa1.foo(123, 456, 789, 333, 222) 39 | 40 | callback_calls = get_agent_messages() 41 | 42 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 43 | 44 | assert length(callback_calls) == 1 45 | assert result == callback_result 46 | assert intercepted_mfa == {InterceptedWildcardedMfa1, :foo, [123, 456, 789, 333, 222]} 47 | end 48 | 49 | test "it doesn't intercept a function without intercept configuration" do 50 | {:ok, _pid} = spawn_agent() 51 | 52 | InterceptedWildcardedMfa1.foo_nop("plz", "no intercept") 53 | 54 | assert [] == get_agent_messages() 55 | end 56 | end 57 | 58 | describe "intercepted function MFA with function and arity wildcards for a given module" do 59 | test "it intercepts the same function with arity 1" do 60 | {:ok, _pid} = spawn_agent() 61 | 62 | result = InterceptedWildcardedMfa2.foo(123) 63 | 64 | callback_calls = get_agent_messages() 65 | 66 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 67 | 68 | assert length(callback_calls) == 1 69 | assert result == callback_result 70 | assert intercepted_mfa == {InterceptedWildcardedMfa2, :foo, [123]} 71 | end 72 | 73 | test "it intercepts the same function with arity 2" do 74 | {:ok, _pid} = spawn_agent() 75 | 76 | result = InterceptedWildcardedMfa2.foo(123, 456) 77 | 78 | callback_calls = get_agent_messages() 79 | 80 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 81 | 82 | assert length(callback_calls) == 1 83 | assert result == callback_result 84 | assert intercepted_mfa == {InterceptedWildcardedMfa2, :foo, [123, 456]} 85 | end 86 | 87 | test "it intercepts the same function with arity 5" do 88 | {:ok, _pid} = spawn_agent() 89 | 90 | result = InterceptedWildcardedMfa2.foo(123, 456, 789, 333, 222) 91 | 92 | callback_calls = get_agent_messages() 93 | 94 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 95 | 96 | assert length(callback_calls) == 1 97 | assert result == callback_result 98 | assert intercepted_mfa == {InterceptedWildcardedMfa2, :foo, [123, 456, 789, 333, 222]} 99 | end 100 | 101 | test "it intercepts other function" do 102 | {:ok, _pid} = spawn_agent() 103 | 104 | result = InterceptedWildcardedMfa2.xyz(123) 105 | 106 | callback_calls = get_agent_messages() 107 | 108 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 109 | 110 | assert length(callback_calls) == 1 111 | assert result == callback_result 112 | assert intercepted_mfa == {InterceptedWildcardedMfa2, :xyz, [123]} 113 | end 114 | end 115 | 116 | defp spawn_agent() do 117 | @process_name 118 | |> Process.whereis() 119 | |> kill_agent() 120 | 121 | {:ok, pid} = Agent.start_link(fn -> [] end) 122 | true = Process.register(pid, @process_name) 123 | 124 | {:ok, pid} 125 | end 126 | 127 | defp kill_agent(nil), do: false 128 | defp kill_agent(pid) do 129 | case Process.alive?(pid) do 130 | true -> Process.exit(pid, :kill) 131 | _ -> false 132 | end 133 | end 134 | 135 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 136 | end 137 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_on_error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorOnErrorTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_on_error_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function after it errors" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 11 | AnnotatedInterceptedOnError1.to_intercept() 12 | end 13 | 14 | callback_calls = get_agent_messages() 15 | 16 | [{ 17 | _started_at_timestamp, 18 | _intercepted_timestamp, 19 | intercepted_error, 20 | intercepted_mfa 21 | }] = callback_calls 22 | 23 | assert length(callback_calls) == 1 24 | assert %ArithmeticError{} == intercepted_error 25 | assert intercepted_mfa == {AnnotatedInterceptedOnError1, :to_intercept, []} 26 | end 27 | end 28 | 29 | describe "module with two functions and other statement" do 30 | test "it intercepts the function on error" do 31 | {:ok, _pid} = spawn_agent() 32 | 33 | before_timestamp = Interceptor.Utils.timestamp() 34 | Process.sleep(10) 35 | 36 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 37 | AnnotatedInterceptedOnError2.to_intercept() 38 | end 39 | 40 | callback_calls = get_agent_messages() 41 | 42 | [{ 43 | started_at_timestamp, 44 | intercepted_timestamp, 45 | intercepted_error, 46 | intercepted_mfa 47 | }] = callback_calls 48 | 49 | assert length(callback_calls) == 1 50 | assert before_timestamp < started_at_timestamp 51 | time_it_took_microseconds = intercepted_timestamp - started_at_timestamp 52 | assert 200_000 < time_it_took_microseconds 53 | assert %ArithmeticError{} == intercepted_error 54 | assert intercepted_mfa == {AnnotatedInterceptedOnError2, :to_intercept, []} 55 | end 56 | 57 | test "it also intercepts the other function" do 58 | {:ok, _pid} = spawn_agent() 59 | 60 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 61 | AnnotatedInterceptedOnError2.other_to_intercept() 62 | end 63 | 64 | callback_calls = get_agent_messages() 65 | 66 | [{ 67 | _started_at_timestamp, 68 | _intercepted_timestamp, 69 | _intercepted_error, 70 | intercepted_mfa 71 | }] = callback_calls 72 | 73 | assert length(callback_calls) == 1 74 | assert intercepted_mfa == {AnnotatedInterceptedOnError2, :other_to_intercept, []} 75 | end 76 | end 77 | 78 | describe "module with two functions and a private one" do 79 | test "it intercepts the function on error" do 80 | {:ok, _pid} = spawn_agent() 81 | 82 | assert_raise ArithmeticError, ~r/bad argument in arithmetic expression/, fn -> 83 | AnnotatedInterceptedOnError3.other_to_intercept(4) 84 | end 85 | 86 | callback_calls = get_agent_messages() 87 | 88 | [{ 89 | _started_at_timestamp, 90 | _intercepted_timestamp, 91 | _intercepted_error, 92 | intercepted_mfa 93 | }] = callback_calls 94 | 95 | assert length(callback_calls) == 1 96 | assert intercepted_mfa == {AnnotatedInterceptedOnError3, :other_to_intercept, [4]} 97 | end 98 | 99 | test "it doesn't intercept the function that isn't configured" do 100 | {:ok, _pid} = spawn_agent() 101 | 102 | assert_raise ArgumentError, ~r/argument error/, fn -> 103 | AnnotatedInterceptedOnError3.not_to_intercept() 104 | end 105 | 106 | callback_calls = get_agent_messages() 107 | 108 | assert length(callback_calls) == 0 109 | end 110 | 111 | test "it doesn't intercept the function that is outside of the intercept block" do 112 | {:ok, _pid} = spawn_agent() 113 | 114 | assert_raise ArgumentError, ~r/argument error/, fn -> 115 | AnnotatedInterceptedOnError3.definitely_not_to_intercept() 116 | end 117 | 118 | callback_calls = get_agent_messages() 119 | 120 | assert length(callback_calls) == 0 121 | end 122 | end 123 | 124 | defp spawn_agent() do 125 | @process_name 126 | |> Process.whereis() 127 | |> kill_agent() 128 | 129 | {:ok, pid} = Agent.start_link(fn -> [] end) 130 | true = Process.register(pid, @process_name) 131 | 132 | {:ok, pid} 133 | end 134 | 135 | defp kill_agent(nil), do: false 136 | defp kill_agent(pid) do 137 | case Process.alive?(pid) do 138 | true -> Process.exit(pid, :kill) 139 | _ -> false 140 | end 141 | end 142 | 143 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 144 | end 145 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_wildcarded_mfa_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorWildcardedMfaTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_wildcarded_mfa_test_process 5 | 6 | describe "intercepted function MFA with arity wildcards" do 7 | test "it intercepts the same function with arity 1" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = AnnotatedInterceptedWildcardedMfa1.foo(123) 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {AnnotatedInterceptedWildcardedMfa1, :foo, [123]} 19 | end 20 | 21 | test "it intercepts the same function with arity 2" do 22 | {:ok, _pid} = spawn_agent() 23 | 24 | result = AnnotatedInterceptedWildcardedMfa1.foo(123, 456) 25 | 26 | callback_calls = get_agent_messages() 27 | 28 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 29 | 30 | assert length(callback_calls) == 1 31 | assert result == callback_result 32 | assert intercepted_mfa == {AnnotatedInterceptedWildcardedMfa1, :foo, [123, 456]} 33 | end 34 | 35 | test "it intercepts the same function with arity 5" do 36 | {:ok, _pid} = spawn_agent() 37 | 38 | result = AnnotatedInterceptedWildcardedMfa1.foo(123, 456, 789, 333, 222) 39 | 40 | callback_calls = get_agent_messages() 41 | 42 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 43 | 44 | assert length(callback_calls) == 1 45 | assert result == callback_result 46 | assert intercepted_mfa == {AnnotatedInterceptedWildcardedMfa1, :foo, [123, 456, 789, 333, 222]} 47 | end 48 | 49 | test "it doesn't intercept a function without intercept configuration" do 50 | {:ok, _pid} = spawn_agent() 51 | 52 | AnnotatedInterceptedWildcardedMfa1.foo_nop("plz", "no intercept") 53 | 54 | assert [] == get_agent_messages() 55 | end 56 | end 57 | 58 | describe "intercepted function MFAs with function and arity wildcards for a given module" do 59 | test "it intercepts the same function with arity 1" do 60 | {:ok, _pid} = spawn_agent() 61 | 62 | result = AnnotatedInterceptedWildcardedMfa2.foo(123) 63 | 64 | callback_calls = get_agent_messages() 65 | 66 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 67 | 68 | assert length(callback_calls) == 1 69 | assert result == callback_result 70 | assert intercepted_mfa == {AnnotatedInterceptedWildcardedMfa2, :foo, [123]} 71 | end 72 | 73 | test "it intercepts the same function with arity 2" do 74 | {:ok, _pid} = spawn_agent() 75 | 76 | result = AnnotatedInterceptedWildcardedMfa2.foo(123, 456) 77 | 78 | callback_calls = get_agent_messages() 79 | 80 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 81 | 82 | assert length(callback_calls) == 1 83 | assert result == callback_result 84 | assert intercepted_mfa == {AnnotatedInterceptedWildcardedMfa2, :foo, [123, 456]} 85 | end 86 | 87 | test "it intercepts the same function with arity 5" do 88 | {:ok, _pid} = spawn_agent() 89 | 90 | result = AnnotatedInterceptedWildcardedMfa2.foo(123, 456, 789, 333, 222) 91 | 92 | callback_calls = get_agent_messages() 93 | 94 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 95 | 96 | assert length(callback_calls) == 1 97 | assert result == callback_result 98 | assert intercepted_mfa == {AnnotatedInterceptedWildcardedMfa2, :foo, [123, 456, 789, 333, 222]} 99 | end 100 | 101 | test "it intercepts other function" do 102 | {:ok, _pid} = spawn_agent() 103 | 104 | result = AnnotatedInterceptedWildcardedMfa2.xyz(123) 105 | 106 | callback_calls = get_agent_messages() 107 | 108 | [{_started_at, _ended_at, callback_result, intercepted_mfa}] = callback_calls 109 | 110 | assert length(callback_calls) == 1 111 | assert result == callback_result 112 | assert intercepted_mfa == {AnnotatedInterceptedWildcardedMfa2, :xyz, [123]} 113 | end 114 | end 115 | 116 | defp spawn_agent() do 117 | @process_name 118 | |> Process.whereis() 119 | |> kill_agent() 120 | 121 | {:ok, pid} = Agent.start_link(fn -> [] end) 122 | true = Process.register(pid, @process_name) 123 | 124 | {:ok, pid} 125 | end 126 | 127 | defp kill_agent(nil), do: false 128 | defp kill_agent(pid) do 129 | case Process.alive?(pid) do 130 | true -> Process.exit(pid, :kill) 131 | _ -> false 132 | end 133 | end 134 | 135 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 136 | end 137 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Known issues 2 | 3 | - None at this time :) 4 | 5 | # Changelog for v0.5.4 6 | 7 | - Bump `mox`, `ex_doc` and `excoveralls` library versions; 8 | - Allow interception of functions pattern-matching on atom literals, e.g. `def foo(:bar), do: 123` 9 | 10 | # Changelog for v0.5.3 11 | 12 | * Allow the usage of wildcards when configuring the interception callbacks for a given function, i.e., several intercepted functions can now be configured with a single interception configuration entry by declaring the intercepted function as `{Intercepted, :*, :*}` or `"Intercepted.*/*"`; 13 | * Fix a bug (since the very first version 😅) that forced 0-arity functions to have parens or they wouldn't be intercepted. 14 | 15 | # Changelog for v0.5.2 16 | 17 | ## Changes 18 | 19 | * Allow to customize the attribute name when using the `Interceptor.Annotated`, hence we can use an attribute name other than `@intercept`. This will be really useful for a new library I'm thinking about named `cachemere`. 20 | 21 | # Changelog for v0.5.1 22 | 23 | ## Changes 24 | 25 | * Fix a bug where, if a function argument wasn't used (e.g. `_arg`) by the original function, interceptor was passing the actual `_arg` value to the callback function. Now, the `:arg_cant_be_intercepted` value it's passed to the callback function instead. This allowed us to fix the compiler warning about "using a `_bla` variable"; 26 | 27 | * You can intercept your functions using the `Interceptor.Annotated` module and annotating your functions with `@intercept true`, instead of relying on the previous strategy with the `Interceptor.intercept/1` macro. 28 | 29 | # Changelog for v0.4.3 30 | 31 | ## Changes 32 | 33 | * Address warning. 34 | 35 | # Changelog for v0.4.2 36 | 37 | ## Changes 38 | 39 | * Fix a bug where we were changing the headers of functions for which we didn't have any intercept configuration, leading to unused variables warnings. 40 | Example: original function `def abc(%{x: a}, [b]) do ...` without any intercept configuration, was still being changed to `def abc(%{x: a} = random_var1, [b] = random_var2) do ...`, because this is how we pass the argument values to the callbacks (we resort to those `random_varX` assignments). We now only change the function headers if the given function is in fact configured to be intercepted. 41 | 42 | * Fix a bug where intercepted functions pattern-matching on integers or strings were resulting in a compile error. Forgot about literals. 43 | 44 | # Changelog for v0.4.1 45 | 46 | ## Changes 47 | 48 | * Fix a bug where we weren't allowing arguments of an intercepted function to destructure existing structures (e.g `def foo(%Media{id: id}, x, y, z)`); 49 | 50 | # Changelog for v0.4.0 51 | 52 | ## Changes 53 | 54 | * Fix a bug where we weren't intercepting function definition with guard clauses; 55 | * Interceptor.Configuration.Validator allows one to check if the intercepted functions actually exist; 56 | * Organizing the existing test suite to cater for the "new" annotated tests; 57 | * New "annotated" way of intercepting functions (using `@intercept true`), instead of only supporting the `intercept/1` do-block. Still without the full test-suite, hence not recommended for now. 58 | 59 | # Changelog for v0.3.0 60 | 61 | ## Changes 62 | 63 | * Instead of passing the intercepted function arity to the callback functions, we now pass the actual argument values. 64 | This change allows to have the same interceptor function behaving differently with different arguments values. 65 | 66 | # Changelog for v0.2.0 67 | 68 | ## Changes 69 | 70 | * `Interceptor.Configurator` provides a DSL to define the intercept configuration, allowing `"Module.function/arity"` MFAs instead of tuple-based ones 71 | * Intercept configuration can now live directly on the intercepted module, instead of being exposed by a module set on the application configuration 72 | * Ability to intercept private functions as well 73 | * Refactor of the configuration code to its own `Interceptor.Configuration` module 74 | 75 | ## TODO 76 | 77 | * Allow multiple callbacks for a given moment, i.e., allow more than one callback to be invoked `after` the intercepted function is called, for example. 78 | 79 | # Changelog for v0.1.3 80 | 81 | ## Changes 82 | 83 | * Small documentation fixes 84 | * Bug fix: we had an error when trying to intercept a function without arguments 85 | - The AST of a `def foo, do: "hi"` function declaration is different from this `def foo(), do: "hi"` 86 | 87 | # Changelog for v0.1.2 88 | 89 | ## Changes 90 | 91 | Small documentation fixes. 92 | 93 | # Changelog for v0.1.1 94 | 95 | ## Highlights 96 | 97 | First version of the library, with tests and documentation. Implements the 98 | `before`, `after`, `on_success`, `on_error` and `wrapper` strategies of 99 | intercepting any function. As of now, it only intercepts public functions (but 100 | it shouldn't be difficult to intercept private ones as well). 101 | -------------------------------------------------------------------------------- /test/configuration/configurator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ConfiguratorTest do 2 | use ExUnit.Case 3 | 4 | describe "simple streamlined intercept configs" do 5 | test "it converts those streamlined configs to the expected tuple-based config" do 6 | expected = %{ 7 | {InterceptedOnBefore1, :to_intercept, 0} => 8 | [before: {Before.Callback, :before, 1}], 9 | {Bla.Ble.Bli.Module.Name.Big.TooBig, :to_intercept, 0} => 10 | [after: {After.Callback, :right_after, 2}], 11 | } 12 | assert expected == StreamlinedInterceptConfig.get_intercept_config() 13 | end 14 | end 15 | 16 | describe "one streamlined intercept config with all callbacks" do 17 | test "it converts those streamlined configs to the expected tuple-based config" do 18 | expected = %{ 19 | { 20 | InterceptedOnBefore1, :to_intercept, 0} => [ 21 | before: {Before.Callback, :before, 1}, 22 | after: {After.Callback, :right_after, 2}, 23 | on_success: {Success.Callback, :if_everything_ok, 3}, 24 | on_error: {Error.Callback, :if_borked, 3}, 25 | ], 26 | } 27 | assert expected == StreamlinedInterceptConfig2.get_intercept_config() 28 | end 29 | end 30 | 31 | describe "one streamlined intercept config with MFAs in tuple and string formats" do 32 | test "with the MFA of the intercepted function already in a tuple format, it converts those streamlined configs to the expected tuple-based config" do 33 | expected = %{ 34 | { 35 | InterceptedOnBefore1, :to_intercept, 0} => [ 36 | before: {Before.Callback, :before, 1}, 37 | after: {After.Callback, :right_after, 2}, 38 | on_success: {Success.Callback, :if_everything_ok, 3}, 39 | on_error: {Error.Callback, :if_borked, 3}, 40 | ], 41 | } 42 | assert expected == StreamlinedInterceptConfigMixedFormat.get_intercept_config() 43 | end 44 | 45 | test "with the MFA of some of the callback functions already in a tuple format, it converts those streamlined configs to the expected tuple-based config" do 46 | expected = %{ 47 | { 48 | InterceptedOnBefore1, :to_intercept, 0} => [ 49 | before: {Before.Callback, :before, 1}, 50 | after: {After.Callback, :right_after, 2}, 51 | on_success: {Success.Callback, :if_everything_ok, 3}, 52 | on_error: {Error.Callback, :if_borked, 3}, 53 | ], 54 | } 55 | assert expected == StreamlinedInterceptConfigMixedFormat2.get_intercept_config() 56 | end 57 | 58 | test "Mixed callback functions (tuple and streamlined config entries) with wildcards for the function and arity" do 59 | expected = %{ 60 | {InterceptedOnBefore1, :*, :*} => [ 61 | before: {Before.Callback, :before, 1}, 62 | after: {After.Callback, :right_after, 2}, 63 | on_success: {Success.Callback, :if_everything_ok, 3}, 64 | on_error: {Error.Callback, :if_borked, 3}, 65 | ], 66 | {InterceptedOnBefore1, :some_function, :*} => [ 67 | before: {Before.Callback, :before, 1}, 68 | after: {After.Callback, :right_after, 2}, 69 | on_success: {Success.Callback, :if_everything_ok, 3}, 70 | on_error: {Error.Callback, :if_borked, 3}, 71 | ], 72 | } 73 | assert expected == StreamlinedInterceptConfigWildcarded.get_intercept_config() 74 | end 75 | end 76 | 77 | describe "one bad intercept config" do 78 | test "it raises the expected error" do 79 | assert_raise RuntimeError, ~r/Invalid MFA/, fn -> 80 | StreamlinedInterceptConfigBad.get_intercept_config() 81 | end 82 | end 83 | end 84 | 85 | describe "wildcarded callback MFAs" do 86 | test "it raises the expected error (string-based MFA with wildcarded arity)" do 87 | assert_raise RuntimeError, ~r/Invalid MFA/, fn -> 88 | StreamlinedInterceptConfigWildcardedCallbacks1.get_intercept_config() 89 | end 90 | end 91 | 92 | test "it raises the expected error (string-based MFA with wildcarded function and arity)" do 93 | assert_raise RuntimeError, ~r/Invalid MFA/, fn -> 94 | StreamlinedInterceptConfigWildcardedCallbacks2.get_intercept_config() 95 | end 96 | end 97 | 98 | test "it raises the expected error (tuple-based MFA with wildcarded arity)" do 99 | assert_raise RuntimeError, ~r/Invalid MFA/, fn -> 100 | StreamlinedInterceptConfigWildcardedCallbacks3.get_intercept_config() 101 | end 102 | end 103 | 104 | test "it raises the expected error (tuple-based MFA with wildcarded function and arity)" do 105 | assert_raise RuntimeError, ~r/Invalid MFA/, fn -> 106 | StreamlinedInterceptConfigWildcardedCallbacks4.get_intercept_config() 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, 3 | "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm", "5e8806285d8a3a8999bd38e4a73c58d28534c856bc38c44818e5ba85bbda16fb"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, 5 | "ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"}, 6 | "excoveralls": {:hex, :excoveralls, "0.13.4", "7b0baee01fe150ef81153e6ffc0fc68214737f54570dc257b3ca4da8e419b812", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "faae00b3eee35cdf0342c10b669a7c91f942728217d2a7c7f644b24d391e6190"}, 7 | "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, 8 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 9 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 10 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 14 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 15 | "mox": {:hex, :mox, "1.0.0", "4b3c7005173f47ff30641ba044eb0fe67287743eec9bd9545e37f3002b0a9f8b", [:mix], [], "hexpm", "201b0a20b7abdaaab083e9cf97884950f8a30a1350a1da403b3145e213c6f4df"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 17 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 18 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 19 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Interceptor](https://github.com/amalbuquerque/interceptor/raw/master/assets/images/interceptor_logo_with_title.png) 2 | 3 | [![Actions Status](https://github.com/amalbuquerque/interceptor/workflows/Tests/badge.svg)](https://github.com/amalbuquerque/interceptor/actions) [![Coverage Status](https://coveralls.io/repos/github/amalbuquerque/interceptor/badge.svg?branch=refs/heads/master)](https://coveralls.io/github/amalbuquerque/interceptor?branch=refs/heads/master) [![hex.pm version](https://img.shields.io/hexpm/v/interceptor.svg)](https://hex.pm/packages/interceptor) [![hex.pm downloads](https://img.shields.io/hexpm/dt/interceptor.svg)](https://hex.pm/packages/interceptor) 4 | ========= 5 | 6 | The Interceptor library allows you to intercept function calls, by configuring 7 | the interception functions and using the `Interceptor.intercept/1` macro or the 8 | `@intercept true` annotation. 9 | 10 | ## Installation 11 | 12 | The package can be installed by adding `interceptor` to your list of 13 | dependencies in `mix.exs`: 14 | 15 | ```elixir 16 | def deps do 17 | [ 18 | {:interceptor, "~> 0.5.4"} 19 | ] 20 | end 21 | ``` 22 | 23 | ## Getting started 24 | 25 | Create a module using the `Interceptor.Configurator` module: 26 | 27 | ```elixir 28 | defmodule Interception.Config do 29 | use Interceptor.Configurator 30 | 31 | intercept "Intercepted.abc/1", 32 | before: "MyInterceptor.intercept_before/1", 33 | after: "MyInterceptor.intercept_after/2" 34 | # there's also `on_success`, `on_error` 35 | # and `wrapper` callbacks available! 36 | 37 | intercept "Intercepted.private_hello/1", 38 | on_success: "MyInterceptor.intercept_on_success/3" 39 | end 40 | ``` 41 | 42 | Point to the previous configuration module in your configuration: 43 | 44 | ```elixir 45 | # [...] 46 | config :interceptor, 47 | configuration: Interception.Config 48 | ``` 49 | 50 | Define your interceptor module, which contains the callback functions: 51 | 52 | ```elixir 53 | defmodule MyInterceptor do 54 | def intercept_before(mfa), 55 | do: IO.puts "Intercepted #{inspect(mfa)} before it started." 56 | 57 | def intercept_after(mfa, result), 58 | do: IO.puts "Intercepted #{inspect(mfa)} after it completed. Its result: #{inspect(result)}" 59 | 60 | def intercept_on_success(mfa, result, _start_timestamp), 61 | do: IO.puts "Intercepted #{inspect(mfa)} after it completed successfully. Its result: #{inspect(result)}" 62 | end 63 | ``` 64 | 65 | In the module that you want to intercept (in our case, `Intercepted`), place 66 | the functions that you want to intercept inside a `Interceptor.intercept/1` 67 | block. If your functions are placed out of this block or if they don't have 68 | a corresponding interceptor configuration, they won't be intercepted. 69 | 70 | In the next snippet, the `Intercepted.foo/0` function won't be intercepted 71 | because it's out of the `Interceptor.intercept/1` do-block. Notice that you 72 | can also intercept private functions. 73 | 74 | ```elixir 75 | defmodule Intercepted do 76 | require Interceptor, as: I 77 | 78 | I.intercept do 79 | def abc(x), do: "Got #{inspect(x)}" 80 | 81 | defp private_hello(y), do: "Hello #{inspect(y)}" 82 | end 83 | 84 | def foo, do: "Hi there" 85 | end 86 | ``` 87 | 88 | Alternatively, you can use the `Interceptor.Annotated` module and rely on 89 | the `@intercept true` "annotation": 90 | 91 | ```elixir 92 | defmodule Intercepted do 93 | use Interceptor.Annotated 94 | 95 | @intercept true 96 | def abc(x), do: "Got #{inspect(x)}" 97 | 98 | @intercept true 99 | defp private_hello(y), do: "Hello #{inspect(y)}" 100 | 101 | def foo, do: "Hi there" 102 | end 103 | ``` 104 | 105 | Now when you run your code, whenever the `Intercepted.abc/1` function is 106 | called, it will be intercepted *before* it starts and *after* it completes. 107 | Whenever the `Intercepted.private_hello/1` executes successfully, the 108 | corresponding callback will also be called. 109 | 110 | You also have `on_error` and `wrapper` callbacks. Check the full documentation 111 | for further examples and other alternative configuration approaches. 112 | 113 | ### Wildcarded interception configuration 114 | 115 | If you want to intercept all the `Intercepted` module functions without 116 | having to specify an `intercept Intercepted./, ...` entry for 117 | each function on the `Interception.Config` module, you can now use wildcards 😎. 118 | 119 | The following configuration lets us intercept every `Intercepted` function 120 | (inside the `Interceptor.intercept/1` block or annotated with the 121 | `@intercept true` attribute). 122 | 123 | ```elixir 124 | defmodule Interception.Config do 125 | use Interceptor.Configurator 126 | 127 | intercept "Intercepted.*/*", 128 | before: "MyInterceptor.intercept_before/1", 129 | after: "MyInterceptor.intercept_after/2" 130 | end 131 | ``` 132 | 133 | ## More info 134 | 135 | You can find the library documentation at 136 | [https://hexdocs.pm/interceptor](https://hexdocs.pm/interceptor). 137 | 138 | You can also find the changelog [here](https://github.com/amalbuquerque/interceptor/blob/master/CHANGELOG.md). 139 | 140 | ## TODO 141 | 142 | - Update docs to mention how to understand if we're trying to intercept non-existing functions with the `Interceptor.Configuration.Validator` module; 143 | -------------------------------------------------------------------------------- /lib/function_arguments.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.FunctionArguments do 2 | alias Interceptor.Utils 3 | 4 | @ignored_value :arg_cant_be_intercepted 5 | 6 | @doc """ 7 | Use this function to get a tuple containing a list with the names 8 | of the function arguments and the list of the arguments in AST form. 9 | 10 | For the function `def abcd(a, b, c), do: 123` you would get: 11 | 12 | ``` 13 | { 14 | [:a, :b, :c], 15 | [{:a, [], Elixir}, {:b, [], Elixir}, {:c, [], Elixir}] 16 | } 17 | ``` 18 | 19 | For a function with one or more "anonymous" arguments, this function 20 | will assign each argument like this to a random variable. 21 | 22 | For the function `def foo(x, y, {bar}), do: 42` it would return: 23 | 24 | ``` 25 | { 26 | [:x, :y, :a1b2c3d], 27 | [ 28 | {:x, [], Elixir}, 29 | {:y, [], Elixir}, 30 | {:=, [], [{:{}, [], [{:bar, [], Elixir}]}, {:a1b2c3d, [], Elixir}]} 31 | ] 32 | } 33 | ``` 34 | 35 | Notice the last assignment to the `a1b2c3d` random variable, returning 36 | the argument list in AST form as if the 37 | the function was defined like `def foo(x, y, {bar} = a1b2c3d), do: 42`. 38 | """ 39 | # functions with no arguments have nil as their `args_list` 40 | def get_args_names_and_new_args_list({_function_name, _metadata, nil} = _function_hdr), do: {[], nil} 41 | 42 | def get_args_names_and_new_args_list( 43 | {_function_name, _metadata, args_list} = _function_hdr) do 44 | args_list 45 | |> Enum.map(&get_arg_name_and_its_ast(&1)) 46 | |> Enum.unzip() 47 | end 48 | 49 | def get_actual_function_header( 50 | {:when, _guard_metadata, [ 51 | {_function_name, _metadata, _args_list} = function_hdr | _guard_clauses 52 | ]}), do: function_hdr 53 | 54 | def get_actual_function_header( 55 | {_function_name, _metadata, _args_list} = function_hdr), do: function_hdr 56 | 57 | def get_function_header_with_new_args_names( 58 | {:when, guard_metadata, [ 59 | {function_name, metadata, _args_list} = function_hdr | guard_clauses 60 | ]}) do 61 | 62 | {args_names, new_args_list} = get_args_names_and_new_args_list(function_hdr) 63 | 64 | new_function_header = {:when, guard_metadata, [ 65 | {function_name, metadata, new_args_list} | guard_clauses 66 | ]} 67 | 68 | {new_function_header, args_names} 69 | end 70 | 71 | def get_function_header_with_new_args_names( 72 | {function_name, metadata, _args_list} = function_hdr) do 73 | {args_names, new_args_list} = get_args_names_and_new_args_list(function_hdr) 74 | 75 | new_function_header = {function_name, metadata, new_args_list} 76 | {new_function_header, args_names} 77 | end 78 | 79 | @doc """ 80 | Returns the AST that gets us the value of each argument, so we can pass 81 | the intercepted function argument values to the callback. 82 | 83 | If the `arg_name` starts with `_`, it means it isn't used in the intercepted 84 | function body, hence we shouldn't access its value to pass it to the callback 85 | function, passing instead the @ignored_value. 86 | 87 | TODO: receive the current module and pass it as context 88 | """ 89 | def get_not_hygienic_args_values_ast(nil), do: [] 90 | 91 | def get_not_hygienic_args_values_ast(args_names) do 92 | args_names 93 | |> Enum.map(&to_string/1) 94 | |> Enum.map(fn 95 | "_" <> _arg_name -> @ignored_value 96 | arg_name -> 97 | arg_name = String.to_atom(arg_name) 98 | 99 | quote do: var!(unquote(Macro.var(arg_name, nil))) 100 | end) 101 | end 102 | 103 | # previously we were using Macro.escape for the mfa (arity), 104 | # but we now want the args values not to be quoted, 105 | # because they are already quoted 106 | def escape_module_function_but_not_args({module, function, args}) 107 | when is_atom(module) and is_atom(function) and is_list(args) do 108 | { 109 | :{}, 110 | [], 111 | [ 112 | module, 113 | function, 114 | args 115 | ] 116 | } 117 | end 118 | 119 | defp get_arg_name_and_its_ast({:=, _, [_operand_a, _operand_b] = assignment_operands} = arg_full_ast) do 120 | arg_variable = assignment_operands 121 | |> Enum.filter(&is_variable(&1)) 122 | |> hd() 123 | 124 | {arg_name, _, _} = arg_variable 125 | 126 | {arg_name, arg_full_ast} 127 | end 128 | 129 | defp get_arg_name_and_its_ast({:\\, _metadata, [arg_ast, _default_value_ast]} = arg_full_ast) do 130 | {arg_name, _, _} = arg_ast 131 | 132 | {arg_name, arg_full_ast} 133 | end 134 | 135 | # in this case, `arg_ast` doesn't contain an assignment, so we are "manually" 136 | # placing it inside an assignment statement 137 | defp get_arg_name_and_its_ast({arg_name, _metadata, _context} = arg_ast) 138 | when arg_name in [:<<>>, :{}, :%{}, :%, :<>] do 139 | random_name = Utils.random_atom() 140 | 141 | # arg variables always have their context as nil 142 | random_variable = Macro.var(random_name, nil) 143 | 144 | # if value_ast represents `{a,b,c}`, the 145 | # returned assignment (in AST form) will be like 146 | # `{a,b,c} = random_variable` 147 | assignment_ast = {:=, [], [arg_ast, random_variable]} 148 | 149 | {random_name, assignment_ast} 150 | end 151 | 152 | defp get_arg_name_and_its_ast({arg_name, _metadata, _context} = arg_ast) 153 | when is_atom(arg_name) do 154 | {arg_name, arg_ast} 155 | end 156 | 157 | defp get_arg_name_and_its_ast(arg_ast) when is_list(arg_ast) or is_tuple(arg_ast) or is_integer(arg_ast) or is_binary(arg_ast) or is_atom(arg_ast) do 158 | random_name = Utils.random_atom() 159 | 160 | # arg variables always have their context as nil 161 | random_variable = Macro.var(random_name, nil) 162 | 163 | # if value_ast represents `[a,b,c]`, the 164 | # returned assignment (in AST form) will be like 165 | # `[a,b,c] = random_variable` 166 | assignment_ast = {:=, [], [arg_ast, random_variable]} 167 | 168 | {random_name, assignment_ast} 169 | end 170 | 171 | defp is_variable({name, metadata, context}) 172 | when is_atom(name) and is_list(metadata) and is_atom(context), do: true 173 | 174 | defp is_variable(_other_ast), do: false 175 | end 176 | -------------------------------------------------------------------------------- /test/do_block/interceptor_on_after_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorOnAfterTest do 2 | use ExUnit.Case 3 | 4 | @process_name :after_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function after it is called" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = InterceptedOnAfter1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {InterceptedOnAfter1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module with two functions and other statement" do 23 | test "it intercepts the function after it is called" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = InterceptedOnAfter2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert result < intercepted_timestamp 35 | assert intercepted_mfa == {InterceptedOnAfter2, :to_intercept, []} 36 | end 37 | 38 | test "it also intercepts the other function" do 39 | {:ok, _pid} = spawn_agent() 40 | 41 | result = InterceptedOnAfter2.other_to_intercept() 42 | 43 | callback_calls = get_agent_messages() 44 | 45 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 46 | 47 | assert length(callback_calls) == 1 48 | assert result == callback_result 49 | assert result == "HELLO" 50 | assert intercepted_mfa == {InterceptedOnAfter2, :other_to_intercept, []} 51 | end 52 | end 53 | 54 | describe "module with two functions and a private one" do 55 | test "it intercepts the function" do 56 | {:ok, _pid} = spawn_agent() 57 | 58 | result = InterceptedOnAfter3.other_to_intercept(4) 59 | 60 | callback_calls = get_agent_messages() 61 | 62 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 63 | 64 | assert length(callback_calls) == 1 65 | assert result == result_callback 66 | assert result == 10 67 | assert intercepted_mfa == {InterceptedOnAfter3, :other_to_intercept, [4]} 68 | end 69 | 70 | test "it doesn't intercept the function that isn't configured" do 71 | {:ok, _pid} = spawn_agent() 72 | 73 | now = Interceptor.Utils.timestamp() 74 | Process.sleep(50) 75 | result = InterceptedOnAfter3.not_to_intercept() 76 | 77 | callback_calls = get_agent_messages() 78 | 79 | assert result > now 80 | assert length(callback_calls) == 0 81 | end 82 | end 83 | 84 | describe "module with two definitions of the same function, the first has a guard clause" do 85 | test "it intercepts the guarded function" do 86 | {:ok, _pid} = spawn_agent() 87 | 88 | result = InterceptedOnAfter4.to_intercept_guarded(:should_return_atom) 89 | 90 | callback_calls = get_agent_messages() 91 | 92 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 93 | 94 | assert length(callback_calls) == 1 95 | assert result == result_callback 96 | assert result == "ATOM should_return_atom" 97 | assert intercepted_mfa == {InterceptedOnAfter4, :to_intercept_guarded, [:should_return_atom]} 98 | end 99 | 100 | test "it intercepts the function without guard" do 101 | {:ok, _pid} = spawn_agent() 102 | 103 | result = InterceptedOnAfter4.to_intercept_guarded("boomerang") 104 | 105 | callback_calls = get_agent_messages() 106 | 107 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 108 | 109 | assert length(callback_calls) == 1 110 | assert result == result_callback 111 | assert result == "SOMETHING ELSE boomerang" 112 | assert intercepted_mfa == {InterceptedOnAfter4, :to_intercept_guarded, ["boomerang"]} 113 | end 114 | end 115 | 116 | describe "module with two definitions of the same function, both match on integers" do 117 | test "it intercepts the first function definition" do 118 | {:ok, _pid} = spawn_agent() 119 | 120 | result = InterceptedOnAfter5.it_has_threes(3) 121 | 122 | callback_calls = get_agent_messages() 123 | 124 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 125 | 126 | assert length(callback_calls) == 1 127 | assert result == result_callback 128 | assert result == "Has one three" 129 | assert intercepted_mfa == {InterceptedOnAfter5, :it_has_threes, [3]} 130 | end 131 | 132 | test "it intercepts the second function definition" do 133 | {:ok, _pid} = spawn_agent() 134 | 135 | result = InterceptedOnAfter5.it_has_threes(33) 136 | 137 | callback_calls = get_agent_messages() 138 | 139 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 140 | 141 | assert length(callback_calls) == 1 142 | assert result == result_callback 143 | assert result == "Has two threes" 144 | assert intercepted_mfa == {InterceptedOnAfter5, :it_has_threes, [33]} 145 | end 146 | end 147 | 148 | describe "module with two definitions of the same function, the first one match on `abc`" do 149 | test "it intercepts the first function definition" do 150 | {:ok, _pid} = spawn_agent() 151 | 152 | result = InterceptedOnAfter5.its_abc("abc") 153 | 154 | callback_calls = get_agent_messages() 155 | 156 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 157 | 158 | assert length(callback_calls) == 1 159 | assert result == result_callback 160 | assert result == true 161 | assert intercepted_mfa == {InterceptedOnAfter5, :its_abc, ["abc"]} 162 | end 163 | 164 | test "it intercepts the second function definition" do 165 | {:ok, _pid} = spawn_agent() 166 | 167 | result = InterceptedOnAfter5.its_abc(%{a: "map"}) 168 | 169 | callback_calls = get_agent_messages() 170 | 171 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 172 | 173 | assert length(callback_calls) == 1 174 | assert result == result_callback 175 | assert result == false 176 | assert intercepted_mfa == {InterceptedOnAfter5, :its_abc, [:arg_cant_be_intercepted]} 177 | end 178 | end 179 | 180 | defp spawn_agent() do 181 | @process_name 182 | |> Process.whereis() 183 | |> kill_agent() 184 | 185 | {:ok, pid} = Agent.start_link(fn -> [] end) 186 | true = Process.register(pid, @process_name) 187 | 188 | {:ok, pid} 189 | end 190 | 191 | defp kill_agent(nil), do: false 192 | defp kill_agent(pid) do 193 | case Process.alive?(pid) do 194 | true -> Process.exit(pid, :kill) 195 | _ -> false 196 | end 197 | end 198 | 199 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 200 | end 201 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_on_after_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorOnAfterTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_after_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function after it is called" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = AnnotatedInterceptedOnAfter1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 15 | 16 | assert length(callback_calls) == 1 17 | assert result == callback_result 18 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter1, :to_intercept, []} 19 | end 20 | end 21 | 22 | describe "module with two functions and other statement" do 23 | test "it intercepts the function after it is called" do 24 | {:ok, _pid} = spawn_agent() 25 | 26 | result = AnnotatedInterceptedOnAfter2.to_intercept() 27 | 28 | callback_calls = get_agent_messages() 29 | 30 | [{intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 31 | 32 | assert length(callback_calls) == 1 33 | assert result == callback_result 34 | assert result < intercepted_timestamp 35 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter2, :to_intercept, []} 36 | end 37 | 38 | test "it also intercepts the other function" do 39 | {:ok, _pid} = spawn_agent() 40 | 41 | result = AnnotatedInterceptedOnAfter2.other_to_intercept() 42 | 43 | callback_calls = get_agent_messages() 44 | 45 | [{_intercepted_timestamp, callback_result, intercepted_mfa}] = callback_calls 46 | 47 | assert length(callback_calls) == 1 48 | assert result == callback_result 49 | assert result == "HELLO" 50 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter2, :other_to_intercept, []} 51 | end 52 | end 53 | 54 | describe "module with two functions and a private one" do 55 | test "it intercepts the function" do 56 | {:ok, _pid} = spawn_agent() 57 | 58 | result = AnnotatedInterceptedOnAfter3.other_to_intercept(4) 59 | 60 | callback_calls = get_agent_messages() 61 | 62 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 63 | 64 | assert length(callback_calls) == 1 65 | assert result == result_callback 66 | assert result == 10 67 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter3, :other_to_intercept, [4]} 68 | end 69 | 70 | test "it doesn't intercept the function that isn't configured" do 71 | {:ok, _pid} = spawn_agent() 72 | 73 | now = Interceptor.Utils.timestamp() 74 | Process.sleep(50) 75 | result = AnnotatedInterceptedOnAfter3.not_to_intercept() 76 | 77 | callback_calls = get_agent_messages() 78 | 79 | assert result > now 80 | assert length(callback_calls) == 0 81 | end 82 | end 83 | 84 | describe "module with two definitions of the same function, the first has a guard clause" do 85 | test "it intercepts the guarded function" do 86 | {:ok, _pid} = spawn_agent() 87 | 88 | result = AnnotatedInterceptedOnAfter4.to_intercept_guarded(:should_return_atom) 89 | 90 | callback_calls = get_agent_messages() 91 | 92 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 93 | 94 | assert length(callback_calls) == 1 95 | assert result == result_callback 96 | assert result == "ATOM should_return_atom" 97 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter4, :to_intercept_guarded, [:should_return_atom]} 98 | end 99 | 100 | test "it intercepts the function without guard" do 101 | {:ok, _pid} = spawn_agent() 102 | 103 | result = AnnotatedInterceptedOnAfter4.to_intercept_guarded("boomerang") 104 | 105 | callback_calls = get_agent_messages() 106 | 107 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 108 | 109 | assert length(callback_calls) == 1 110 | assert result == result_callback 111 | assert result == "SOMETHING ELSE boomerang" 112 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter4, :to_intercept_guarded, ["boomerang"]} 113 | end 114 | end 115 | 116 | describe "module with two definitions of the same function, both match on integers" do 117 | test "it intercepts the first function definition" do 118 | {:ok, _pid} = spawn_agent() 119 | 120 | result = AnnotatedInterceptedOnAfter5.it_has_threes(3) 121 | 122 | callback_calls = get_agent_messages() 123 | 124 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 125 | 126 | assert length(callback_calls) == 1 127 | assert result == result_callback 128 | assert result == "Has one three" 129 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter5, :it_has_threes, [3]} 130 | end 131 | 132 | test "it intercepts the second function definition" do 133 | {:ok, _pid} = spawn_agent() 134 | 135 | result = AnnotatedInterceptedOnAfter5.it_has_threes(33) 136 | 137 | callback_calls = get_agent_messages() 138 | 139 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 140 | 141 | assert length(callback_calls) == 1 142 | assert result == result_callback 143 | assert result == "Has two threes" 144 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter5, :it_has_threes, [33]} 145 | end 146 | end 147 | 148 | describe "module with two definitions of the same function, the first one match on `abc`" do 149 | test "it intercepts the first function definition" do 150 | {:ok, _pid} = spawn_agent() 151 | 152 | result = AnnotatedInterceptedOnAfter5.its_abc("abc") 153 | 154 | callback_calls = get_agent_messages() 155 | 156 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 157 | 158 | assert length(callback_calls) == 1 159 | assert result == result_callback 160 | assert result == true 161 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter5, :its_abc, ["abc"]} 162 | end 163 | 164 | test "it intercepts the second function definition" do 165 | {:ok, _pid} = spawn_agent() 166 | 167 | result = AnnotatedInterceptedOnAfter5.its_abc(%{a: "map"}) 168 | 169 | callback_calls = get_agent_messages() 170 | 171 | [{_intercepted_timestamp, result_callback, intercepted_mfa}] = callback_calls 172 | 173 | assert length(callback_calls) == 1 174 | assert result == result_callback 175 | assert result == false 176 | assert intercepted_mfa == {AnnotatedInterceptedOnAfter5, :its_abc, [:arg_cant_be_intercepted]} 177 | end 178 | end 179 | 180 | defp spawn_agent() do 181 | @process_name 182 | |> Process.whereis() 183 | |> kill_agent() 184 | 185 | {:ok, pid} = Agent.start_link(fn -> [] end) 186 | true = Process.register(pid, @process_name) 187 | 188 | {:ok, pid} 189 | end 190 | 191 | defp kill_agent(nil), do: false 192 | defp kill_agent(pid) do 193 | case Process.alive?(pid) do 194 | true -> Process.exit(pid, :kill) 195 | _ -> false 196 | end 197 | end 198 | 199 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 200 | end 201 | -------------------------------------------------------------------------------- /assets/images/interceptor_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | Interceptor 103 | 104 | 105 | -------------------------------------------------------------------------------- /lib/annotated_interceptor.ex: -------------------------------------------------------------------------------- 1 | defmodule Interceptor.Annotated do 2 | @moduledoc """ 3 | The Interceptor library allows you to intercept function calls, as you can see 4 | in the `Interceptor` module documentation. 5 | 6 | This module allows you to intercept your functions using `@intercept true` 7 | "annotations", instead of having to use the `Interceptor.intercept/1` macro. 8 | 9 | This is how you can use the `Interceptor.Annotated` module on the example 10 | `Intercepted` module (defined on the `Interceptor` module documentation): 11 | 12 | ``` 13 | defmodule Intercepted do 14 | use Interceptor.Annotated 15 | 16 | @intercept true 17 | def abc(x), do: "Got \#\{inspect(x)\}" 18 | 19 | # the following function can't be intercepted 20 | # because it doesn't have the `@intercept true` annotation 21 | def not_intercepted(f, g, h), do: f+g+h 22 | end 23 | ``` 24 | 25 | This way of intercepting the `Intercepted.abc/1` function is equivalent to 26 | the one using the `Interceptor.intercept/1` macro described on the 27 | `Interceptor` module documentation. Please check it for more information 28 | on how to configure this library. 29 | 30 | _Note:_ If you want to override the interception configuration coming from the 31 | application configuration file (i.e. `config/config.exs`) or you simply just 32 | want to set the interception configuration on each intercepted module, you 33 | can also pass the interception `config` when using the `Interceptor.Annotated` 34 | module: `use Interceptor.Annotated, config: My.Interception.Config`. 35 | 36 | Check the section [Intercept configuration on the intercepted module](Interceptor.html#module-intercept-configuration-on-the-intercepted-module) on the 37 | `Interceptor` module docs for more information. 38 | """ 39 | 40 | alias Interceptor.Debug 41 | 42 | @empty_metadata [] 43 | 44 | defmacro __using__(opts) do 45 | own_config = Keyword.get(opts, :config) 46 | {own_config_module, _bindings} = Code.eval_quoted(own_config) 47 | Module.put_attribute(__CALLER__.module, :own_config, own_config_module) 48 | 49 | intercept_attribute = Keyword.get(opts, :attribute_name, :intercept) 50 | 51 | Module.put_attribute(__CALLER__.module, :attribute_intercept, intercept_attribute) 52 | 53 | Module.register_attribute(__CALLER__.module, intercept_attribute, accumulate: true) 54 | Module.register_attribute(__CALLER__.module, :intercepted, accumulate: true) 55 | 56 | quote do 57 | require Interceptor 58 | 59 | @on_definition {unquote(__MODULE__), :on_definition} 60 | @before_compile {unquote(__MODULE__), :before_compile} 61 | end 62 | end 63 | 64 | def on_definition(env, kind, fun, args, guards, body) do 65 | Debug.debug_message( 66 | "\n\n[on_definition] current module=#{env.module}, got Env=(OMITTED), kind=#{inspect(kind)}, fun=#{ 67 | inspect(fun) 68 | }, args=#{inspect(args)}, guards=#{inspect(guards)}, body=#{inspect(body)}" 69 | ) 70 | 71 | intercept_annotations = get_intercept_annotations(env.module) 72 | 73 | Debug.debug_message("Intercept annotations #{inspect(intercept_annotations)}") 74 | 75 | attrs = extract_attributes(env.module, body) 76 | intercepted = {kind, fun, args, guards, body, intercept_annotations, attrs} 77 | 78 | Module.put_attribute(env.module, :intercepted, intercepted) 79 | delete_intercept_annotation(env.module) 80 | end 81 | 82 | defmacro before_compile(env) do 83 | all_collected = Module.get_attribute(env.module, :intercepted) 84 | |> Enum.reverse() 85 | 86 | delete_attributes_used(env.module) 87 | 88 | to_print = 89 | all_collected 90 | |> Enum.reduce("", fn intercepted, acc -> acc <> "#{inspect(intercepted)}\n" end) 91 | 92 | Debug.debug_message("[before_compile] Everything we collected on the @intercepted attribute: #{to_print}") 93 | 94 | intercepted_functions = intercepted_functions(all_collected) 95 | 96 | to_intercept = filter_not_intercepted(all_collected, intercepted_functions) 97 | 98 | to_print = 99 | to_intercept 100 | |> Enum.reduce("", fn intercepted, acc -> acc <> "#{inspect(intercepted)}\n" end) 101 | 102 | Debug.debug_message( 103 | "[before_compile] I should now inject the intercepted functions on the #{env.module}. Here are the intercepted functions to inject: #{ 104 | to_print 105 | }" 106 | ) 107 | 108 | to_intercept 109 | |> reject_empty_clauses() 110 | |> Enum.reduce({nil, []}, fn d, acc -> 111 | decorate(env, d, acc) 112 | end) 113 | |> elem(1) 114 | |> Enum.reverse() 115 | end 116 | 117 | # Remove all defs which are not intercepted, 118 | # these don't need to be overrided. 119 | defp filter_not_intercepted(all, intercepted_functions) do 120 | Enum.filter( 121 | all, 122 | fn {_kind, fun, args, _guards, _body, _intercepts, _attrs} -> 123 | Map.has_key?(intercepted_functions, {fun, Enum.count(args)}) 124 | end 125 | ) 126 | end 127 | 128 | defp intercepted_functions(all) do 129 | key_fun = fn {_kind, fun, args, _guards, _body, _intercepts, _attrs} -> 130 | {fun, Enum.count(args)} 131 | end 132 | 133 | value_fun = fn {_kind, _fun, _args, _guards, _body, intercepts, _attrs} -> 134 | intercepts 135 | end 136 | 137 | all 138 | |> Enum.group_by(key_fun, value_fun) 139 | |> Enum.filter(fn {_key, intercepts} -> 140 | List.flatten(intercepts) != [] 141 | end) 142 | |> Enum.into(%{}) 143 | end 144 | 145 | defp reject_empty_clauses(all) do 146 | Enum.reject(all, fn {_kind, _fun, _args, _guards, body, _intercepts, _attrs} -> 147 | body == nil 148 | end) 149 | end 150 | 151 | defp implied_arities(args) do 152 | arity = Enum.count(args) 153 | 154 | default_count = 155 | args 156 | |> Enum.filter(fn 157 | {:\\, _, _} -> true 158 | _ -> false 159 | end) 160 | |> Enum.count() 161 | 162 | :lists.seq(arity, arity - default_count, -1) 163 | end 164 | 165 | defp decorate( 166 | env, 167 | # TODO: We currently ignore the intercepts value, it should be used to override the intercept configuration if passed 168 | {kind, fun, args, guard, body, _intercepts, attrs}, 169 | {prev_fun, all} 170 | ) do 171 | 172 | override_clause = 173 | implied_arities(args) 174 | |> Enum.map( 175 | "e do 176 | defoverridable [{unquote(fun), unquote(&1)}] 177 | end 178 | ) 179 | 180 | attrs = 181 | attrs 182 | |> Enum.map(fn {attr, value} -> 183 | {:@, [], [{attr, [], [Macro.escape(value)]}]} 184 | end) 185 | 186 | function_hdr_and_body = case guard do 187 | [] -> [ 188 | {fun, @empty_metadata, args}, 189 | body 190 | ] 191 | [guard] -> [ 192 | {:when, @empty_metadata, [ 193 | {fun, @empty_metadata, args}, 194 | guard 195 | ]}, 196 | body 197 | ] 198 | end 199 | 200 | def_clause = Interceptor.add_calls({kind, @empty_metadata, function_hdr_and_body}, env.module) 201 | 202 | arity = Enum.count(args) 203 | 204 | if {fun, arity} != prev_fun do 205 | {{fun, arity}, [def_clause] ++ override_clause ++ attrs ++ all} 206 | else 207 | {{fun, arity}, [def_clause] ++ attrs ++ all} 208 | end 209 | end 210 | 211 | # Extracts the attributes used in the body of the function, 212 | # so we can later keep them near the overrided functions. 213 | defp extract_attributes(module, body) do 214 | Macro.postwalk(body, %{}, fn 215 | {:@, _, [{attr, _, nil}]} = node, acc_attrs -> 216 | acc_attrs = Map.put(acc_attrs, attr, Module.get_attribute(module, attr)) 217 | {node, acc_attrs} 218 | 219 | not_attribute, acc -> 220 | {not_attribute, acc} 221 | end) 222 | # return the accumulated attrs 223 | |> elem(1) 224 | end 225 | 226 | defp get_intercept_annotations(module) do 227 | attribute = Module.get_attribute(module, :attribute_intercept) 228 | 229 | Module.get_attribute(module, attribute) 230 | end 231 | 232 | defp delete_intercept_annotation(module) do 233 | attribute = Module.get_attribute(module, :attribute_intercept) 234 | 235 | Module.delete_attribute(module, attribute) 236 | end 237 | 238 | defp delete_attributes_used(module) do 239 | Module.delete_attribute(module, :intercepted) 240 | Module.delete_attribute(module, :attribute_intercept) 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /test/do_block/interceptor_on_success_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InterceptorOnSuccessTest do 2 | use ExUnit.Case 3 | 4 | @process_name :on_success_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function after it is successfully called" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = InterceptedOnSuccess1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{ 15 | _started_at_timestamp, 16 | _intercepted_timestamp, 17 | intercepted_result, 18 | intercepted_mfa 19 | }] = callback_calls 20 | 21 | assert length(callback_calls) == 1 22 | assert result == intercepted_result 23 | 24 | assert intercepted_mfa == {InterceptedOnSuccess1, :to_intercept, []} 25 | end 26 | end 27 | 28 | describe "module with two functions and other statement" do 29 | test "it intercepts the function on success" do 30 | {:ok, _pid} = spawn_agent() 31 | 32 | before_timestamp = Interceptor.Utils.timestamp() 33 | Process.sleep(10) 34 | 35 | result = InterceptedOnSuccess2.to_intercept() 36 | 37 | callback_calls = get_agent_messages() 38 | 39 | [{ 40 | started_at_timestamp, 41 | intercepted_timestamp, 42 | intercepted_result, 43 | intercepted_mfa 44 | }] = callback_calls 45 | 46 | assert length(callback_calls) == 1 47 | assert before_timestamp < started_at_timestamp 48 | time_it_took_microseconds = intercepted_timestamp - started_at_timestamp 49 | assert 200_000 < time_it_took_microseconds 50 | assert result == intercepted_result 51 | assert intercepted_mfa == {InterceptedOnSuccess2, :to_intercept, []} 52 | end 53 | 54 | test "it also intercepts the other function" do 55 | {:ok, _pid} = spawn_agent() 56 | 57 | result = InterceptedOnSuccess2.other_to_intercept() 58 | 59 | callback_calls = get_agent_messages() 60 | 61 | [{ 62 | _started_at_timestamp, 63 | _intercepted_timestamp, 64 | _intercepted_result, 65 | intercepted_mfa 66 | }] = callback_calls 67 | 68 | assert length(callback_calls) == 1 69 | assert result == "HELLO" 70 | assert intercepted_mfa == {InterceptedOnSuccess2, :other_to_intercept, []} 71 | end 72 | end 73 | 74 | describe "module with two functions and a private one" do 75 | test "it intercepts the function on success" do 76 | {:ok, _pid} = spawn_agent() 77 | 78 | result = InterceptedOnSuccess3.other_to_intercept(4) 79 | 80 | callback_calls = get_agent_messages() 81 | 82 | [{ 83 | _started_at_timestamp, 84 | _intercepted_timestamp, 85 | _intercepted_result, 86 | intercepted_mfa 87 | }] = callback_calls 88 | 89 | assert length(callback_calls) == 1 90 | assert result == 10 91 | assert intercepted_mfa == {InterceptedOnSuccess3, :other_to_intercept, [4]} 92 | end 93 | 94 | test "it doesn't intercept the function that isn't configured" do 95 | {:ok, _pid} = spawn_agent() 96 | 97 | result = InterceptedOnSuccess3.not_to_intercept() 98 | 99 | callback_calls = get_agent_messages() 100 | 101 | assert result == "Not intercepted" 102 | assert length(callback_calls) == 0 103 | end 104 | 105 | test "it doesn't intercept the function that is outside of the intercept block" do 106 | {:ok, _pid} = spawn_agent() 107 | 108 | _result = InterceptedOnSuccess3.definitely_not_to_intercept() 109 | 110 | callback_calls = get_agent_messages() 111 | 112 | assert length(callback_calls) == 0 113 | end 114 | 115 | test "it intercepts the function with 'anonymous' arguments, using the default argument" do 116 | {:ok, _pid} = spawn_agent() 117 | 118 | result = InterceptedOnSuccess3.trickier_args_function( 119 | :a, 120 | [:b, :c, :d], 121 | {:e, :f}, 122 | %{baz: :g}, 123 | "xyz") 124 | 125 | callback_calls = get_agent_messages() 126 | 127 | [{ 128 | _started_at_timestamp, 129 | _intercepted_timestamp, 130 | _intercepted_result, 131 | intercepted_mfa 132 | }] = callback_calls 133 | 134 | assert length(callback_calls) == 1 135 | assert result == [ 136 | :a, 137 | :b, 138 | :c, 139 | :d, 140 | :e, 141 | :f, 142 | :g, 143 | 120, # 'x' 144 | 121, # 'y' 145 | 122, # 'z' 146 | "bar" 147 | ] 148 | 149 | expected_arguments = [ 150 | :a, 151 | [:b, :c, :d], 152 | {:e, :f}, 153 | %{baz: :g}, 154 | "xyz", 155 | "bar" 156 | ] 157 | 158 | assert intercepted_mfa == {InterceptedOnSuccess3, :trickier_args_function, expected_arguments} 159 | end 160 | 161 | test "it intercepts the function with 'anonymous' arguments, and doesn't use a default argument" do 162 | {:ok, _pid} = spawn_agent() 163 | 164 | result = InterceptedOnSuccess3.trickier_args_function( 165 | :a, 166 | [:b, :c, :d], 167 | {:e, :f}, 168 | %{baz: :g}, 169 | "xyz", 170 | "not_default") 171 | 172 | callback_calls = get_agent_messages() 173 | 174 | [{ 175 | _started_at_timestamp, 176 | _intercepted_timestamp, 177 | _intercepted_result, 178 | intercepted_mfa 179 | }] = callback_calls 180 | 181 | assert length(callback_calls) == 1 182 | assert result == [ 183 | :a, 184 | :b, 185 | :c, 186 | :d, 187 | :e, 188 | :f, 189 | :g, 190 | 120, # 'x' 191 | 121, # 'y' 192 | 122, # 'z' 193 | "not_default" 194 | ] 195 | 196 | expected_arguments = [ 197 | :a, 198 | [:b, :c, :d], 199 | {:e, :f}, 200 | %{baz: :g}, 201 | "xyz", 202 | "not_default" 203 | ] 204 | 205 | assert intercepted_mfa == {InterceptedOnSuccess3, :trickier_args_function, expected_arguments} 206 | end 207 | end 208 | 209 | describe "module with functions that use structures" do 210 | test "it intercepts the function with struct argument" do 211 | {:ok, _pid} = spawn_agent() 212 | 213 | result = InterceptedOnSuccess4.with_struct(%La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}) 214 | 215 | callback_calls = get_agent_messages() 216 | 217 | [{ 218 | _started_at_timestamp, 219 | _intercepted_timestamp, 220 | _intercepted_result, 221 | intercepted_mfa 222 | }] = callback_calls 223 | 224 | assert length(callback_calls) == 1 225 | assert result == ["andre", 32] 226 | 227 | expected_arguments = [ 228 | %La.Lu.Li.Weird.MyStruct{name: "andre", age: 32} 229 | ] 230 | 231 | assert intercepted_mfa == {InterceptedOnSuccess4, :with_struct, expected_arguments} 232 | end 233 | 234 | test "it intercepts the function with structs arguments" do 235 | {:ok, _pid} = spawn_agent() 236 | 237 | result = InterceptedOnSuccess4.with_structs(%La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}, %La.Lu.Li.Weird.MyStruct{name: "brua", age: 23}) 238 | 239 | callback_calls = get_agent_messages() 240 | 241 | [{ 242 | _started_at_timestamp, 243 | _intercepted_timestamp, 244 | _intercepted_result, 245 | intercepted_mfa 246 | }] = callback_calls 247 | 248 | assert length(callback_calls) == 1 249 | assert result == ["andre", 32, "brua", 23] 250 | 251 | expected_arguments = [ 252 | %La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}, 253 | %La.Lu.Li.Weird.MyStruct{name: "brua", age: 23} 254 | ] 255 | 256 | assert intercepted_mfa == {InterceptedOnSuccess4, :with_structs, expected_arguments} 257 | end 258 | 259 | test "it intercepts the function with struct argument already assigned" do 260 | {:ok, _pid} = spawn_agent() 261 | 262 | result = InterceptedOnSuccess4.with_struct_already_assigned(%La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}) 263 | 264 | callback_calls = get_agent_messages() 265 | 266 | [{ 267 | _started_at_timestamp, 268 | _intercepted_timestamp, 269 | _intercepted_result, 270 | intercepted_mfa 271 | }] = callback_calls 272 | 273 | assert length(callback_calls) == 1 274 | assert result == ["andre", 32] 275 | 276 | expected_arguments = [ 277 | %La.Lu.Li.Weird.MyStruct{name: "andre", age: 32} 278 | ] 279 | 280 | assert intercepted_mfa == {InterceptedOnSuccess4, :with_struct_already_assigned, expected_arguments} 281 | end 282 | end 283 | 284 | 285 | defp spawn_agent() do 286 | @process_name 287 | |> Process.whereis() 288 | |> kill_agent() 289 | 290 | {:ok, pid} = Agent.start_link(fn -> [] end) 291 | true = Process.register(pid, @process_name) 292 | 293 | {:ok, pid} 294 | end 295 | 296 | defp kill_agent(nil), do: false 297 | defp kill_agent(pid) do 298 | case Process.alive?(pid) do 299 | true -> Process.exit(pid, :kill) 300 | _ -> false 301 | end 302 | end 303 | 304 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 305 | end 306 | -------------------------------------------------------------------------------- /test/annotated/annotated_interceptor_on_success_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnnotatedInterceptorOnSuccessTest do 2 | use ExUnit.Case 3 | 4 | @process_name :annotated_on_success_test_process 5 | 6 | describe "module with a single function" do 7 | test "it intercepts the function after it is successfully called" do 8 | {:ok, _pid} = spawn_agent() 9 | 10 | result = AnnotatedInterceptedOnSuccess1.to_intercept() 11 | 12 | callback_calls = get_agent_messages() 13 | 14 | [{ 15 | _started_at_timestamp, 16 | _intercepted_timestamp, 17 | intercepted_result, 18 | intercepted_mfa 19 | }] = callback_calls 20 | 21 | assert length(callback_calls) == 1 22 | assert result == intercepted_result 23 | 24 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess1, :to_intercept, []} 25 | end 26 | end 27 | 28 | describe "module with two functions and other statement" do 29 | test "it intercepts the function on success" do 30 | {:ok, _pid} = spawn_agent() 31 | 32 | before_timestamp = Interceptor.Utils.timestamp() 33 | Process.sleep(10) 34 | 35 | result = AnnotatedInterceptedOnSuccess2.to_intercept() 36 | 37 | callback_calls = get_agent_messages() 38 | 39 | [{ 40 | started_at_timestamp, 41 | intercepted_timestamp, 42 | intercepted_result, 43 | intercepted_mfa 44 | }] = callback_calls 45 | 46 | assert length(callback_calls) == 1 47 | assert before_timestamp < started_at_timestamp 48 | time_it_took_microseconds = intercepted_timestamp - started_at_timestamp 49 | assert 200_000 < time_it_took_microseconds 50 | assert result == intercepted_result 51 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess2, :to_intercept, []} 52 | end 53 | 54 | test "it also intercepts the other function" do 55 | {:ok, _pid} = spawn_agent() 56 | 57 | result = AnnotatedInterceptedOnSuccess2.other_to_intercept() 58 | 59 | callback_calls = get_agent_messages() 60 | 61 | [{ 62 | _started_at_timestamp, 63 | _intercepted_timestamp, 64 | _intercepted_result, 65 | intercepted_mfa 66 | }] = callback_calls 67 | 68 | assert length(callback_calls) == 1 69 | assert result == "HELLO" 70 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess2, :other_to_intercept, []} 71 | end 72 | end 73 | 74 | describe "module with two functions and a private one" do 75 | test "it intercepts the function on success" do 76 | {:ok, _pid} = spawn_agent() 77 | 78 | result = AnnotatedInterceptedOnSuccess3.other_to_intercept(4) 79 | 80 | callback_calls = get_agent_messages() 81 | 82 | [{ 83 | _started_at_timestamp, 84 | _intercepted_timestamp, 85 | _intercepted_result, 86 | intercepted_mfa 87 | }] = callback_calls 88 | 89 | assert length(callback_calls) == 1 90 | assert result == 10 91 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess3, :other_to_intercept, [4]} 92 | end 93 | 94 | test "it doesn't intercept the function that isn't configured" do 95 | {:ok, _pid} = spawn_agent() 96 | 97 | result = AnnotatedInterceptedOnSuccess3.not_to_intercept() 98 | 99 | callback_calls = get_agent_messages() 100 | 101 | assert result == "Not intercepted" 102 | assert length(callback_calls) == 0 103 | end 104 | 105 | test "it doesn't intercept the function that is outside of the intercept block" do 106 | {:ok, _pid} = spawn_agent() 107 | 108 | _result = AnnotatedInterceptedOnSuccess3.definitely_not_to_intercept() 109 | 110 | callback_calls = get_agent_messages() 111 | 112 | assert length(callback_calls) == 0 113 | end 114 | 115 | test "it intercepts the function with 'anonymous' arguments, using the default argument" do 116 | {:ok, _pid} = spawn_agent() 117 | 118 | result = AnnotatedInterceptedOnSuccess3.trickier_args_function( 119 | :a, 120 | [:b, :c, :d], 121 | {:e, :f}, 122 | %{baz: :g}, 123 | "xyz") 124 | 125 | callback_calls = get_agent_messages() 126 | 127 | [{ 128 | _started_at_timestamp, 129 | _intercepted_timestamp, 130 | _intercepted_result, 131 | intercepted_mfa 132 | }] = callback_calls 133 | 134 | assert length(callback_calls) == 1 135 | assert result == [ 136 | :a, 137 | :b, 138 | :c, 139 | :d, 140 | :e, 141 | :f, 142 | :g, 143 | 120, # 'x' 144 | 121, # 'y' 145 | 122, # 'z' 146 | "bar" 147 | ] 148 | 149 | expected_arguments = [ 150 | :a, 151 | [:b, :c, :d], 152 | {:e, :f}, 153 | %{baz: :g}, 154 | "xyz", 155 | "bar" 156 | ] 157 | 158 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess3, :trickier_args_function, expected_arguments} 159 | end 160 | 161 | test "it intercepts the function with 'anonymous' arguments, and doesn't use a default argument" do 162 | {:ok, _pid} = spawn_agent() 163 | 164 | result = AnnotatedInterceptedOnSuccess3.trickier_args_function( 165 | :a, 166 | [:b, :c, :d], 167 | {:e, :f}, 168 | %{baz: :g}, 169 | "xyz", 170 | "not_default") 171 | 172 | callback_calls = get_agent_messages() 173 | 174 | [{ 175 | _started_at_timestamp, 176 | _intercepted_timestamp, 177 | _intercepted_result, 178 | intercepted_mfa 179 | }] = callback_calls 180 | 181 | assert length(callback_calls) == 1 182 | assert result == [ 183 | :a, 184 | :b, 185 | :c, 186 | :d, 187 | :e, 188 | :f, 189 | :g, 190 | 120, # 'x' 191 | 121, # 'y' 192 | 122, # 'z' 193 | "not_default" 194 | ] 195 | 196 | expected_arguments = [ 197 | :a, 198 | [:b, :c, :d], 199 | {:e, :f}, 200 | %{baz: :g}, 201 | "xyz", 202 | "not_default" 203 | ] 204 | 205 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess3, :trickier_args_function, expected_arguments} 206 | end 207 | end 208 | 209 | describe "module with functions that use structures" do 210 | test "it intercepts the function with struct argument" do 211 | {:ok, _pid} = spawn_agent() 212 | 213 | result = AnnotatedInterceptedOnSuccess4.with_struct(%La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}) 214 | 215 | callback_calls = get_agent_messages() 216 | 217 | [{ 218 | _started_at_timestamp, 219 | _intercepted_timestamp, 220 | _intercepted_result, 221 | intercepted_mfa 222 | }] = callback_calls 223 | 224 | assert length(callback_calls) == 1 225 | assert result == ["andre", 32] 226 | 227 | expected_arguments = [ 228 | %La.Lu.Li.Weird.MyStruct{name: "andre", age: 32} 229 | ] 230 | 231 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess4, :with_struct, expected_arguments} 232 | end 233 | 234 | test "it intercepts the function with structs arguments" do 235 | {:ok, _pid} = spawn_agent() 236 | 237 | result = AnnotatedInterceptedOnSuccess4.with_structs(%La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}, %La.Lu.Li.Weird.MyStruct{name: "brua", age: 23}) 238 | 239 | callback_calls = get_agent_messages() 240 | 241 | [{ 242 | _started_at_timestamp, 243 | _intercepted_timestamp, 244 | _intercepted_result, 245 | intercepted_mfa 246 | }] = callback_calls 247 | 248 | assert length(callback_calls) == 1 249 | assert result == ["andre", 32, "brua", 23] 250 | 251 | expected_arguments = [ 252 | %La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}, 253 | %La.Lu.Li.Weird.MyStruct{name: "brua", age: 23} 254 | ] 255 | 256 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess4, :with_structs, expected_arguments} 257 | end 258 | 259 | test "it intercepts the function with struct argument already assigned" do 260 | {:ok, _pid} = spawn_agent() 261 | 262 | result = AnnotatedInterceptedOnSuccess4.with_struct_already_assigned(%La.Lu.Li.Weird.MyStruct{name: "andre", age: 32}) 263 | 264 | callback_calls = get_agent_messages() 265 | 266 | [{ 267 | _started_at_timestamp, 268 | _intercepted_timestamp, 269 | _intercepted_result, 270 | intercepted_mfa 271 | }] = callback_calls 272 | 273 | assert length(callback_calls) == 1 274 | assert result == ["andre", 32] 275 | 276 | expected_arguments = [ 277 | %La.Lu.Li.Weird.MyStruct{name: "andre", age: 32} 278 | ] 279 | 280 | assert intercepted_mfa == {AnnotatedInterceptedOnSuccess4, :with_struct_already_assigned, expected_arguments} 281 | end 282 | end 283 | 284 | 285 | defp spawn_agent() do 286 | @process_name 287 | |> Process.whereis() 288 | |> kill_agent() 289 | 290 | {:ok, pid} = Agent.start_link(fn -> [] end) 291 | true = Process.register(pid, @process_name) 292 | 293 | {:ok, pid} 294 | end 295 | 296 | defp kill_agent(nil), do: false 297 | defp kill_agent(pid) do 298 | case Process.alive?(pid) do 299 | true -> Process.exit(pid, :kill) 300 | _ -> false 301 | end 302 | end 303 | 304 | defp get_agent_messages(), do: Agent.get(@process_name, &(&1)) 305 | end 306 | -------------------------------------------------------------------------------- /test/intercepted_modules/intercept_config.ex: -------------------------------------------------------------------------------- 1 | defmodule InterceptConfig do 2 | @config %{ 3 | ################# `Interceptor.intercept do ... end` tests 4 | 5 | # on before tests 6 | {InterceptedOnBefore1, :to_intercept, 0} => [before: {Before.Callback, :before, 1}], 7 | {InterceptedOnBefore2, :to_intercept, 0} => [before: {Before.Callback, :before, 1}], 8 | {InterceptedOnBefore2, :other_to_intercept, 0} => [before: {Before.Callback, :before, 1}], 9 | {InterceptedOnBefore3, :other_to_intercept, 1} => [before: {Before.Callback, :before, 1}], 10 | {InterceptedOnBefore4, :to_intercept, 0} => [before: {Before.Callback, :before, 1}], 11 | 12 | # on after tests 13 | {InterceptedOnAfter1, :to_intercept, 0} => [after: {After.Callback, :right_after, 2}], 14 | {InterceptedOnAfter2, :to_intercept, 0} => [after: {After.Callback, :right_after, 2}], 15 | {InterceptedOnAfter2, :other_to_intercept, 0} => [after: {After.Callback, :right_after, 2}], 16 | {InterceptedOnAfter3, :other_to_intercept, 1} => [after: {After.Callback, :right_after, 2}], 17 | {InterceptedOnAfter4, :to_intercept_guarded, 1} => [after: {After.Callback, :right_after, 2}], 18 | {InterceptedOnAfter5, :it_has_threes, 1} => [after: {After.Callback, :right_after, 2}], 19 | {InterceptedOnAfter5, :its_abc, 1} => [after: {After.Callback, :right_after, 2}], 20 | 21 | # on success tests 22 | {InterceptedOnSuccess1, :to_intercept, 0} => [on_success: {OnSuccess.Callback, :on_success, 3}], 23 | {InterceptedOnSuccess2, :to_intercept, 0} => [on_success: {OnSuccess.Callback, :on_success, 3}], 24 | {InterceptedOnSuccess2, :other_to_intercept, 0} => [on_success: {OnSuccess.Callback, :on_success, 3}], 25 | {InterceptedOnSuccess3, :other_to_intercept, 1} => [on_success: {OnSuccess.Callback, :on_success, 3}], 26 | {InterceptedOnSuccess3, :trickier_args_function, 6} => [on_success: {OnSuccess.Callback, :on_success, 3}], 27 | {InterceptedOnSuccess4, :with_struct, 1} => [on_success: {OnSuccess.Callback, :on_success, 3}], 28 | {InterceptedOnSuccess4, :with_structs, 2} => [on_success: {OnSuccess.Callback, :on_success, 3}], 29 | {InterceptedOnSuccess4, :with_struct_already_assigned, 1} => [on_success: {OnSuccess.Callback, :on_success, 3}], 30 | 31 | # on error tests 32 | {InterceptedOnError1, :to_intercept, 0} => [on_error: {OnError.Callback, :on_error, 3}], 33 | {InterceptedOnError2, :to_intercept, 0} => [on_error: {OnError.Callback, :on_error, 3}], 34 | {InterceptedOnError2, :other_to_intercept, 0} => [on_error: {OnError.Callback, :on_error, 3}], 35 | {InterceptedOnError3, :other_to_intercept, 1} => [on_error: {OnError.Callback, :on_error, 3}], 36 | 37 | # wrapper tests 38 | {InterceptedByWrapper1, :to_intercept, 0} => [wrapper: {Wrapper.Callback, :wrap_returns_result, 2}], 39 | {InterceptedByWrapper2, :to_intercept, 0} => [wrapper: {Wrapper.Callback, :wrap_returns_result, 2}], 40 | {InterceptedByWrapper2, :other_to_intercept, 0} => [wrapper: {Wrapper.Callback, :wrap_returns_result, 2}], 41 | {InterceptedByWrapper3, :other_to_intercept, 1} => [wrapper: {Wrapper.Callback, :wrap_returns_result, 2}], 42 | {InterceptedByWrapper4, :to_intercept, 0} => [wrapper: {Wrapper.Callback, :wrap_returns_hello, 2}], 43 | 44 | # edge cases 45 | {InterceptedEdgeCases1, :to_intercept, 3} => [on_success: {EdgeCases.Callbacks, :success_cb, 3}, on_error: {EdgeCases.Callbacks, :error_cb, 3}], 46 | {InterceptedEdgeCases1, :intercept_with_prefix, 1} => [on_success: {EdgeCases.Callbacks, :success_cb, 3}, on_error: {EdgeCases.Callbacks, :error_cb, 3}], 47 | {InterceptedEdgeCases1, :intercept_pattern_match_atom_argument, 2} => [on_success: {EdgeCases.Callbacks, :success_cb, 3}, on_error: {EdgeCases.Callbacks, :error_cb, 3}], 48 | 49 | # wildcarded callbacks 50 | {InterceptedWildcardedMfa1, :foo, :*} => [on_success: {WildcardedMfa.Callbacks, :success_cb, 3}, on_error: {WildcardedMfa.Callbacks, :error_cb, 3}], 51 | {InterceptedWildcardedMfa2, :*, :*} => [on_success: {WildcardedMfa.Callbacks, :success_cb, 3}, on_error: {WildcardedMfa.Callbacks, :error_cb, 3}], 52 | 53 | # these configs will be overridden by the module own configuration 54 | {InterceptedOnAfterOwnConfiguration1, :to_intercept, 0} => [after: {After.Callback, :right_after, 2}], 55 | 56 | ################# `@intercept :true` tests 57 | 58 | # on before tests 59 | {AnnotatedInterceptedOnBefore1, :to_intercept, 0} => [before: {AnnotatedBefore.Callback, :before, 1}], 60 | {AnnotatedInterceptedOnBefore2, :to_intercept, 0} => [before: {AnnotatedBefore.Callback, :before, 1}], 61 | {AnnotatedInterceptedOnBefore2, :other_to_intercept, 0} => [before: {AnnotatedBefore.Callback, :before, 1}], 62 | {AnnotatedInterceptedOnBefore3, :other_to_intercept, 1} => [before: {AnnotatedBefore.Callback, :before, 1}], 63 | {AnnotatedInterceptedOnBefore4, :to_intercept, 0} => [before: {AnnotatedBefore.Callback, :before, 1}], 64 | 65 | # on after tests 66 | {AnnotatedInterceptedOnAfter1, :to_intercept, 0} => [after: {AnnotatedAfter.Callback, :right_after, 2}], 67 | {AnnotatedInterceptedOnAfter2, :to_intercept, 0} => [after: {AnnotatedAfter.Callback, :right_after, 2}], 68 | {AnnotatedInterceptedOnAfter2, :other_to_intercept, 0} => [after: {AnnotatedAfter.Callback, :right_after, 2}], 69 | {AnnotatedInterceptedOnAfter3, :other_to_intercept, 1} => [after: {AnnotatedAfter.Callback, :right_after, 2}], 70 | {AnnotatedInterceptedOnAfter4, :to_intercept_guarded, 1} => [after: {AnnotatedAfter.Callback, :right_after, 2}], 71 | {AnnotatedInterceptedOnAfter5, :it_has_threes, 1} => [after: {AnnotatedAfter.Callback, :right_after, 2}], 72 | {AnnotatedInterceptedOnAfter5, :its_abc, 1} => [after: {AnnotatedAfter.Callback, :right_after, 2}], 73 | 74 | # on success tests 75 | {AnnotatedInterceptedOnSuccess1, :to_intercept, 0} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 76 | {AnnotatedInterceptedOnSuccess2, :to_intercept, 0} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 77 | {AnnotatedInterceptedOnSuccess2, :other_to_intercept, 0} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 78 | {AnnotatedInterceptedOnSuccess3, :other_to_intercept, 1} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 79 | {AnnotatedInterceptedOnSuccess3, :trickier_args_function, 6} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 80 | {AnnotatedInterceptedOnSuccess4, :with_struct, 1} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 81 | {AnnotatedInterceptedOnSuccess4, :with_structs, 2} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 82 | {AnnotatedInterceptedOnSuccess4, :with_struct_already_assigned, 1} => [on_success: {AnnotatedOnSuccess.Callback, :on_success, 3}], 83 | 84 | # on error tests 85 | {AnnotatedInterceptedOnError1, :to_intercept, 0} => [on_error: {AnnotatedOnError.Callback, :on_error, 3}], 86 | {AnnotatedInterceptedOnError2, :to_intercept, 0} => [on_error: {AnnotatedOnError.Callback, :on_error, 3}], 87 | {AnnotatedInterceptedOnError2, :other_to_intercept, 0} => [on_error: {AnnotatedOnError.Callback, :on_error, 3}], 88 | {AnnotatedInterceptedOnError3, :other_to_intercept, 1} => [on_error: {AnnotatedOnError.Callback, :on_error, 3}], 89 | 90 | # wrapper tests 91 | {AnnotatedInterceptedByWrapper1, :to_intercept, 0} => [wrapper: {AnnotatedWrapper.Callback, :wrap_returns_result, 2}], 92 | {AnnotatedInterceptedByWrapper2, :to_intercept, 0} => [wrapper: {AnnotatedWrapper.Callback, :wrap_returns_result, 2}], 93 | {AnnotatedInterceptedByWrapper2, :other_to_intercept, 0} => [wrapper: {AnnotatedWrapper.Callback, :wrap_returns_result, 2}], 94 | {AnnotatedInterceptedByWrapper3, :other_to_intercept, 1} => [wrapper: {AnnotatedWrapper.Callback, :wrap_returns_result, 2}], 95 | {AnnotatedInterceptedByWrapper4, :to_intercept, 0} => [wrapper: {AnnotatedWrapper.Callback, :wrap_returns_hello, 2}], 96 | 97 | # edge cases 98 | {AnnotatedInterceptedEdgeCases1, :to_intercept, 3} => [on_success: {AnnotatedEdgeCases.Callbacks, :success_cb, 3}, on_error: {AnnotatedEdgeCases.Callbacks, :error_cb, 3}], 99 | {AnnotatedInterceptedEdgeCases1, :intercept_with_prefix, 1} => [on_success: {AnnotatedEdgeCases.Callbacks, :success_cb, 3}, on_error: {AnnotatedEdgeCases.Callbacks, :error_cb, 3}], 100 | {AnnotatedInterceptedEdgeCases1, :intercept_pattern_match_atom_argument, 2} => [on_success: {AnnotatedEdgeCases.Callbacks, :success_cb, 3}, on_error: {AnnotatedEdgeCases.Callbacks, :error_cb, 3}], 101 | {AnnotatedInterceptedEdgeCases2, :to_intercept, 3} => [on_success: {AnnotatedEdgeCases.Callbacks, :success_cb, 3}, on_error: {AnnotatedEdgeCases.Callbacks, :error_cb, 3}], 102 | {AnnotatedInterceptedEdgeCases2, :intercept_with_prefix, 1} => [on_success: {AnnotatedEdgeCases.Callbacks, :success_cb, 3}, on_error: {AnnotatedEdgeCases.Callbacks, :error_cb, 3}], 103 | 104 | # wildcarded callbacks 105 | {AnnotatedInterceptedWildcardedMfa1, :foo, :*} => [on_success: {AnnotatedWildcardedMfa.Callbacks, :success_cb, 3}, on_error: {AnnotatedWildcardedMfa.Callbacks, :error_cb, 3}], 106 | {AnnotatedInterceptedWildcardedMfa2, :*, :*} => [on_success: {AnnotatedWildcardedMfa.Callbacks, :success_cb, 3}, on_error: {AnnotatedWildcardedMfa.Callbacks, :error_cb, 3}], 107 | 108 | 109 | # these configs will be overridden by the module own configuration 110 | {AnnotatedInterceptedOnAfterOwnConfiguration1, :to_intercept, 0} => [after: {After.Callback, :right_after, 2}], 111 | } 112 | 113 | def get_intercept_config(), do: @config 114 | end 115 | -------------------------------------------------------------------------------- /test/function_arguments_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FunctionArgumentsTest do 2 | use ExUnit.Case 3 | alias Interceptor.FunctionArguments 4 | 5 | describe "gets each argument name and the corresponding value (in AST form)" do 6 | test "it handles no arguments" do 7 | function_header = get_function_header("def abc(), do: 123") 8 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 9 | 10 | assert {[], []} == result 11 | end 12 | 13 | test "it handles simple arguments" do 14 | function_header = get_function_header("def abc(x, y, z), do: x+y+z") 15 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 16 | 17 | {args_names, args_ast} = result 18 | 19 | assert args_names == [:x, :y, :z] 20 | assert length(args_ast) == 3 21 | 22 | expected_args_ast = args_names 23 | |> Enum.map(&Macro.var(&1, nil)) 24 | 25 | args_ast 26 | |> Enum.zip(expected_args_ast) 27 | |> Enum.each(&assert_ast_match(&1)) 28 | end 29 | 30 | test "it handles arguments pattern matching on atoms" do 31 | function_header = get_function_header("def abc(:an_atom), do: 123") 32 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 33 | 34 | {args_names, args_ast} = result 35 | 36 | assert length(args_ast) == 1 37 | 38 | assert {:=, [], [:an_atom, {hd(args_names), [], nil}]} == hd(args_ast) 39 | end 40 | 41 | test "it handles arguments pattern matching on integers" do 42 | function_header = get_function_header("def abc(123), do: 456") 43 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 44 | 45 | {args_names, args_ast} = result 46 | 47 | assert length(args_ast) == 1 48 | 49 | assert {:=, [], [123, {hd(args_names), [], nil}]} == hd(args_ast) 50 | end 51 | 52 | test "it handles arguments pattern matching on binaries" do 53 | function_header = get_function_header(~s[def abc("zyx"), do: 456]) 54 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 55 | 56 | {args_names, args_ast} = result 57 | 58 | assert length(args_ast) == 1 59 | 60 | assert {:=, [], ["zyx", {hd(args_names), [], nil}]} == hd(args_ast) 61 | end 62 | 63 | test "it handles arguments pattern matching on tuples" do 64 | function_header = get_function_header(~s[def abc({1, 2}), do: 456]) 65 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 66 | 67 | {args_names, args_ast} = result 68 | 69 | assert length(args_ast) == 1 70 | 71 | assert {:=, [], [{1, 2}, {hd(args_names), [], nil}]} == hd(args_ast) 72 | end 73 | 74 | test "it handles arguments pattern matching on lists" do 75 | function_header = get_function_header("def abc([1, 2, 3, 4]), do: 456") 76 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 77 | 78 | {args_names, args_ast} = result 79 | 80 | assert length(args_ast) == 1 81 | 82 | assert {:=, [], [[1, 2, 3, 4], {hd(args_names), [], nil}]} == hd(args_ast) 83 | end 84 | 85 | test "it handles tuple destructure" do 86 | function_header = get_function_header("def abc({x, y, z}), do: x+y+z") 87 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 88 | 89 | {args_names, args_ast} = result 90 | 91 | assert length(args_names) == length(args_ast) 92 | assert length(args_ast) == 1 93 | 94 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 95 | 96 | expected_arg_ast = quote do: {x, y, z} 97 | expected_random_var_ast = Macro.var(hd(args_names), nil) 98 | 99 | assert_ast_match(arg_ast, expected_arg_ast) 100 | assert_ast_match(random_var_ast, expected_random_var_ast) 101 | end 102 | 103 | test "it handles binary destructure" do 104 | function_header = get_function_header("def abc(<>), do: [x,y,z]") 105 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 106 | 107 | {args_names, args_ast} = result 108 | 109 | assert length(args_names) == length(args_ast) 110 | assert length(args_ast) == 1 111 | 112 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 113 | 114 | expected_arg_ast = quote do: <> 115 | expected_random_var_ast = Macro.var(hd(args_names), nil) 116 | 117 | assert_ast_match(arg_ast, expected_arg_ast) 118 | assert_ast_match(random_var_ast, expected_random_var_ast) 119 | end 120 | 121 | test "it handles binary destructure with `<>`" do 122 | function_header = get_function_header(~s/def abc("a prefix" <> hallo), do: hallo/) 123 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 124 | 125 | {args_names, args_ast} = result 126 | 127 | assert length(args_names) == length(args_ast) 128 | assert length(args_ast) == 1 129 | 130 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 131 | 132 | expected_arg_ast = quote do: "a prefix" <> hallo 133 | expected_random_var_ast = Macro.var(hd(args_names), nil) 134 | 135 | assert_ast_match(arg_ast, expected_arg_ast) 136 | assert_ast_match(random_var_ast, expected_random_var_ast) 137 | end 138 | 139 | test "it handles map destructure" do 140 | function_header = get_function_header("def abc(%{a: x, b: y, c: z}), do: [x,y,z]") 141 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 142 | 143 | {args_names, args_ast} = result 144 | 145 | assert length(args_names) == length(args_ast) 146 | assert length(args_ast) == 1 147 | 148 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 149 | 150 | expected_arg_ast = quote do: %{a: x, b: y, c: z} 151 | expected_random_var_ast = Macro.var(hd(args_names), nil) 152 | 153 | assert_ast_match(arg_ast, expected_arg_ast) 154 | assert_ast_match(random_var_ast, expected_random_var_ast) 155 | end 156 | 157 | test "it handles structure destructure" do 158 | function_header = get_function_header("def abc(%Media{a: x, b: y, c: z}), do: [x,y,z]") 159 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 160 | 161 | {args_names, args_ast} = result 162 | 163 | assert length(args_names) == length(args_ast) 164 | assert length(args_ast) == 1 165 | 166 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 167 | 168 | expected_arg_ast = quote do: %Media{a: x, b: y, c: z} 169 | expected_random_var_ast = Macro.var(hd(args_names), nil) 170 | 171 | assert_ast_match(arg_ast, expected_arg_ast) 172 | assert_ast_match(random_var_ast, expected_random_var_ast) 173 | end 174 | 175 | test "it handles keyword list destructure" do 176 | function_header = get_function_header("def abc([a: x, b: y, c: z]), do: [x,y,z]") 177 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 178 | 179 | {args_names, args_ast} = result 180 | 181 | assert length(args_names) == length(args_ast) 182 | assert length(args_ast) == 1 183 | 184 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 185 | 186 | expected_arg_ast = quote do: [a: x, b: y, c: z] 187 | expected_random_var_ast = Macro.var(hd(args_names), nil) 188 | 189 | assert_ast_match(arg_ast, expected_arg_ast) 190 | assert_ast_match(random_var_ast, expected_random_var_ast) 191 | end 192 | 193 | test "it handles list destructure (one element)" do 194 | function_header = get_function_header("def abc([x]), do: [x]") 195 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 196 | 197 | {args_names, args_ast} = result 198 | 199 | assert length(args_names) == length(args_ast) 200 | assert length(args_ast) == 1 201 | 202 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 203 | 204 | expected_arg_ast = quote do: [x] 205 | expected_random_var_ast = Macro.var(hd(args_names), nil) 206 | 207 | assert_ast_match(arg_ast, expected_arg_ast) 208 | assert_ast_match(random_var_ast, expected_random_var_ast) 209 | end 210 | 211 | test "it handles list destructure (more than one element)" do 212 | function_header = get_function_header("def abc([x,y,z]), do: [x,y,z]") 213 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 214 | 215 | {args_names, args_ast} = result 216 | 217 | assert length(args_names) == length(args_ast) 218 | assert length(args_ast) == 1 219 | 220 | [{:=, _metadata, [arg_ast, random_var_ast]}] = args_ast 221 | 222 | expected_arg_ast = quote do: [x,y,z] 223 | expected_random_var_ast = Macro.var(hd(args_names), nil) 224 | 225 | assert_ast_match(arg_ast, expected_arg_ast) 226 | assert_ast_match(random_var_ast, expected_random_var_ast) 227 | end 228 | 229 | test "it handles existing assignments (variable first)" do 230 | function_header = get_function_header("def abc(a = {bar}), do: [a, bar]") 231 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 232 | 233 | {args_names, args_ast} = result 234 | 235 | assert args_names == [:a] 236 | assert length(args_ast) == 1 237 | 238 | expected_arg_ast = quote do: a = {bar} 239 | 240 | assert_ast_match(hd(args_ast), expected_arg_ast) 241 | end 242 | 243 | test "it handles existing assignments (variable after)" do 244 | function_header = get_function_header("def abc({bar} = foo), do: [foo, bar]") 245 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 246 | 247 | {args_names, args_ast} = result 248 | 249 | assert args_names == [:foo] 250 | assert length(args_ast) == 1 251 | 252 | expected_arg_ast = quote do: {bar} = foo 253 | 254 | assert_ast_match(hd(args_ast), expected_arg_ast) 255 | end 256 | 257 | test "it handles default values (nil)" do 258 | function_header = get_function_header("def abc(foo \\\\ nil), do: [foo]") 259 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 260 | 261 | {args_names, args_ast} = result 262 | 263 | assert args_names == [:foo] 264 | assert length(args_ast) == 1 265 | 266 | expected_arg_ast = quote do: foo \\ nil 267 | 268 | assert_ast_match(hd(args_ast), expected_arg_ast) 269 | end 270 | 271 | test "it handles default values (123)" do 272 | function_header = get_function_header("def abc(bar \\\\ 123), do: [bar]") 273 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 274 | 275 | {args_names, args_ast} = result 276 | 277 | assert args_names == [:bar] 278 | assert length(args_ast) == 1 279 | 280 | expected_arg_ast = quote do: bar \\ 123 281 | 282 | assert_ast_match(hd(args_ast), expected_arg_ast) 283 | end 284 | 285 | test "it handles default values (strings)" do 286 | function_header = get_function_header("def abc(baz \\\\ \"blabla\"), do: [baz]") 287 | result = FunctionArguments.get_args_names_and_new_args_list(function_header) 288 | 289 | {args_names, args_ast} = result 290 | 291 | assert args_names == [:baz] 292 | assert length(args_ast) == 1 293 | 294 | expected_arg_ast = quote do: baz \\ "blabla" 295 | 296 | assert_ast_match(hd(args_ast), expected_arg_ast) 297 | end 298 | end 299 | 300 | describe "the function that returns the expression to pass the argument values to the callback function, `get_not_hygienic_args_values_ast/1`" do 301 | test "returns the expected AST for each function arguments" do 302 | arg_names = [:a, :b, :c] 303 | result = FunctionArguments.get_not_hygienic_args_values_ast(arg_names) 304 | 305 | arg_names 306 | |> Enum.zip(result) 307 | |> Enum.each(&assert_not_hygienic_arg_value_ast(&1)) 308 | end 309 | 310 | test "returns a default value for each function argument that is ignored (starts with `_`)" do 311 | arg_names = [:a, :b, :c, :_x, :_y, :d, :e, :_z, :f] 312 | result = FunctionArguments.get_not_hygienic_args_values_ast(arg_names) 313 | 314 | normal_arguments_and_result = [0, 1, 2, 5, 6, 8] 315 | |> Enum.map(fn indx -> 316 | {Enum.at(arg_names, indx), Enum.at(result, indx)} 317 | end) 318 | 319 | normal_arguments_and_result 320 | |> Enum.each(&assert_not_hygienic_arg_value_ast(&1)) 321 | 322 | ignored_arguments_and_result = [3, 4, 7] 323 | |> Enum.map(fn indx -> 324 | {Enum.at(arg_names, indx), Enum.at(result, indx)} 325 | end) 326 | 327 | ignored_arguments_and_result 328 | |> Enum.each(fn {name, result} -> 329 | name = to_string(name) 330 | 331 | assert String.starts_with?(name, "_") 332 | assert result == :arg_cant_be_intercepted 333 | end) 334 | end 335 | end 336 | 337 | def assert_ast_match({ 338 | {_arg_name, _arg_metadata, _arg_context} = ast, 339 | {_expected_arg_name, _expected_arg_metadata, _expected_arg_context} = expected 340 | }), do: assert_ast_match(ast, expected) 341 | 342 | def assert_ast_match( 343 | {arg_name, _arg_metadata, _arg_context} = ast, 344 | {expected_arg_name, _expected_arg_metadata, _expected_arg_context} = expected) do 345 | assert arg_name == expected_arg_name 346 | 347 | assert Macro.to_string(ast) == Macro.to_string(expected) 348 | end 349 | 350 | def assert_ast_match(ast, expected) when is_list(ast) and is_list(expected) do 351 | assert Macro.to_string(ast) == Macro.to_string(expected) 352 | end 353 | 354 | def assert_not_hygienic_arg_value_ast({variable, ast}) when is_atom(variable) and is_tuple(ast) do 355 | expected = quote context: Interceptor.FunctionArguments, do: var!(unquote(Macro.var(variable, nil))) 356 | 357 | assert ast == expected 358 | end 359 | 360 | defp get_function_header(def_function_statement) do 361 | {:def, _metadata, [function_header | [[do: _function_body]]]} = Code.string_to_quoted!(def_function_statement) 362 | 363 | function_header 364 | end 365 | end 366 | --------------------------------------------------------------------------------