From f31dc73d23cae6de7a4119ec62bb7ad32d7545de Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 14 Oct 2023 10:56:07 +0200 Subject: [PATCH 01/18] Bump Erlang and Elixir locally --- .tool-versions | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.tool-versions b/.tool-versions index 1928052..38d92fa 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -erlang 23.2 -elixir 1.11.3-otp-23 +erlang 26.0.2 +elixir 1.15.4-otp-26 From 27fe3acb72350ed6ea5ae0febf3234f6ccbb1040 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 14 Oct 2023 10:56:58 +0200 Subject: [PATCH 02/18] Bump :dialyxir requirement --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 0ab3fdb..f24a870 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,7 @@ defmodule ToxiproxyEx.MixProject do {:castore, "~> 1.0.3"}, {:mint, "~> 1.0"}, {:ex_doc, "~> 0.23", only: :dev, runtime: false}, - {:dialyxir, "~> 1.0", only: [:dev], runtime: false} + {:dialyxir, "~> 1.4", only: [:dev], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 038bed0..a57f737 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, - "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, + "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.30.4", "e8395c8e3c007321abb30a334f9f7c0858d80949af298302daf77553468c0c39", [:mix], [{:earmark_parser, "~> 1.4.31", [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", "9a19f0c50ffaa02435668f5242f2b2a61d46b541ebf326884505dfd3dd7af5e4"}, From 1679673b1aaad98cb65833d8fd53eb0a20ddbf23 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 14 Oct 2023 10:57:40 +0200 Subject: [PATCH 03/18] Remove :logger from :extra_applications --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index f24a870..bc568a9 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,7 @@ defmodule ToxiproxyEx.MixProject do def application do [ - extra_applications: [:logger] + extra_applications: [] ] end From 19f2fd4843b1bce1381f557676e594ab5032c541 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 14 Oct 2023 11:06:45 +0200 Subject: [PATCH 04/18] Add specs to a bunch more functions --- lib/toxiproxy_ex.ex | 18 +++++++----------- lib/toxiproxy_ex/proxy.ex | 23 ++++++++++++++++++----- lib/toxiproxy_ex/server_error.ex | 6 ++++++ lib/toxiproxy_ex/toxic.ex | 12 +++++++++--- lib/toxiproxy_ex/toxic_collection.ex | 13 ++++++++++++- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/lib/toxiproxy_ex.ex b/lib/toxiproxy_ex.ex index 0409fcc..29db79b 100644 --- a/lib/toxiproxy_ex.ex +++ b/lib/toxiproxy_ex.ex @@ -10,12 +10,12 @@ defmodule ToxiproxyEx do @typedoc """ A proxy that intercepts traffic to and from an upstream server. """ - @opaque proxy :: %Proxy{} + @opaque proxy :: Proxy.t @typedoc """ A collection of proxies. """ - @opaque toxic_collection :: %ToxicCollection{} + @opaque toxic_collection :: ToxicCollection.t() @typedoc """ A hostname or IP address including a port number, e.g. `localhost:4539`. @@ -93,8 +93,6 @@ defmodule ToxiproxyEx do :error -> raise ServerError, message: "Could not destroy proxy" end end) - - :ok end @doc """ @@ -110,11 +108,9 @@ defmodule ToxiproxyEx do iex> ToxiproxyEx.get!(:test_mysql_master) """ @spec get!(atom() | String.t()) :: proxy() - def get!(name) when is_atom(name) do - get!(Atom.to_string(name)) - end + def get!(name) when is_atom(name) or is_binary(name) do + name = to_string(name) - def get!(name) do case Enum.find(all!().proxies, &(&1.name == name)) do nil -> raise ArgumentError, message: "Unknown proxy with name '#{name}'" proxy -> proxy @@ -136,7 +132,7 @@ defmodule ToxiproxyEx do iex> ToxiproxyEx.grep!(~r/master/) """ @spec grep!(Regex.t()) :: toxic_collection() - def grep!(pattern) do + def grep!(%Regex{} = pattern) do case Enum.filter(all!().proxies, &String.match?(&1.name, pattern)) do proxies = [_h | _t] -> ToxicCollection.new(proxies) [] -> raise ArgumentError, message: "No proxies found for regex '#{pattern}'" @@ -441,10 +437,10 @@ defmodule ToxiproxyEx do iex> ToxiproxyEx.version!() "2.1.2" """ - @spec version!() :: :ok + @spec version!() :: String.t() def version!() do case Client.version() do - {:ok, %{body: res}} -> res + {:ok, %{body: res}} when is_binary(res) -> res _ -> raise ServerError, message: "Could not fetch version" end end diff --git a/lib/toxiproxy_ex/proxy.ex b/lib/toxiproxy_ex/proxy.ex index f27ac8c..2a1cd5f 100644 --- a/lib/toxiproxy_ex/proxy.ex +++ b/lib/toxiproxy_ex/proxy.ex @@ -3,23 +3,34 @@ defmodule ToxiproxyEx.Proxy do alias ToxiproxyEx.{Client, Toxic} + @typedoc since: "1.2.0" + @type t() :: %__MODULE__{ + upstream: String.t(), + listen: String.t(), + name: String.t(), + enabled: boolean() + } + defstruct upstream: nil, listen: nil, name: nil, enabled: nil - def disable(proxy) do + @spec disable(t()) :: :ok | :error + def disable(%__MODULE__{} = proxy) do case Client.disable_proxy(proxy.name) do {:ok, _res} -> :ok _ -> :error end end - def enable(proxy) do + @spec enable(t()) :: :ok | :error + def enable(%__MODULE__{} = proxy) do case Client.enable_proxy(proxy.name) do {:ok, _res} -> :ok _ -> :error end end - def create(options) do + @spec create(keyword()) :: {:ok, t()} | :error + def create(options) when is_list(options) do upstream = Keyword.get(options, :upstream) listen = Keyword.get(options, :listen, "localhost:0") name = Keyword.get(options, :name) @@ -39,14 +50,16 @@ defmodule ToxiproxyEx.Proxy do end end - def destroy(proxy) do + @spec destroy(t()) :: :ok | :error + def destroy(%__MODULE__{} = proxy) do case Client.destroy_proxy(proxy.name) do {:ok, _res} -> :ok _ -> :error end end - def toxics(proxy) do + @spec toxics(t()) :: {:ok, [Toxic.t()]} | :error + def toxics(%__MODULE__{} = proxy) do case Client.list_toxics(proxy.name) do {:ok, %{body: toxics}} -> {:ok, Enum.map(toxics, &parse_toxic(&1, proxy))} _ -> :error diff --git a/lib/toxiproxy_ex/server_error.ex b/lib/toxiproxy_ex/server_error.ex index 48c4675..3dd65fd 100644 --- a/lib/toxiproxy_ex/server_error.ex +++ b/lib/toxiproxy_ex/server_error.ex @@ -2,5 +2,11 @@ defmodule ToxiproxyEx.ServerError do @moduledoc """ Raised when communication with the toxiproxy server fails. """ + + @typedoc since: "1.2.0" + @type t() :: %__MODULE__{ + message: String.t() + } + defexception message: "Server Error" end diff --git a/lib/toxiproxy_ex/toxic.ex b/lib/toxiproxy_ex/toxic.ex index e0243f2..9e38d95 100644 --- a/lib/toxiproxy_ex/toxic.ex +++ b/lib/toxiproxy_ex/toxic.ex @@ -3,6 +3,9 @@ defmodule ToxiproxyEx.Toxic do alias ToxiproxyEx.Client + @typedoc since: "1.2.0" + @type t() :: %__MODULE__{} + defstruct [ :type, :name, @@ -12,7 +15,8 @@ defmodule ToxiproxyEx.Toxic do :toxicity ] - def new(fields) do + @spec new(keyword()) :: t() + def new(fields) when is_list(fields) do fields = fields |> maybe_put_default(:toxicity, 1.0) @@ -43,7 +47,8 @@ defmodule ToxiproxyEx.Toxic do Keyword.put(fields, field, default) end - def create(toxic) do + @spec create(t()) :: {:ok, t()} | :error + def create(%__MODULE__{} = toxic) do case Client.create_toxic(toxic.proxy_name, %{ name: toxic.name, type: toxic.type, @@ -69,7 +74,8 @@ defmodule ToxiproxyEx.Toxic do end end - def destroy(toxic) do + @spec destroy(t()) :: :ok | :error + def destroy(%__MODULE__{} = toxic) do case Client.destroy_toxic(toxic.proxy_name, toxic.name) do {:ok, _res} -> :ok _ -> :error diff --git a/lib/toxiproxy_ex/toxic_collection.ex b/lib/toxiproxy_ex/toxic_collection.ex index 270df0e..857a05e 100644 --- a/lib/toxiproxy_ex/toxic_collection.ex +++ b/lib/toxiproxy_ex/toxic_collection.ex @@ -1,14 +1,25 @@ defmodule ToxiproxyEx.ToxicCollection do @moduledoc false + alias ToxiproxyEx.{Proxy, Toxic} + + @typedoc since: "1.2.0" + @type t() :: %__MODULE__{ + proxies: [Proxy.t()], + toxics: [Toxic.t()] + } + defstruct proxies: [], toxics: [] + @spec new([Proxy.t()] | Proxy.t()) :: t() + def new(proxies_or_proxy) + def new(proxies) when is_list(proxies) do proxies = Enum.reject(proxies, &is_nil/1) %__MODULE__{proxies: proxies} end - def new(proxy) do + def new(%Proxy{} = proxy) do new([proxy]) end end From fb957b4920eb4c63043e7c71799edac3d0257638 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 14 Oct 2023 11:08:07 +0200 Subject: [PATCH 05/18] Move :host default to mix.exs --- lib/toxiproxy_ex/client.ex | 8 ++------ mix.exs | 3 ++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/toxiproxy_ex/client.ex b/lib/toxiproxy_ex/client.ex index 355ad15..dd8cac2 100644 --- a/lib/toxiproxy_ex/client.ex +++ b/lib/toxiproxy_ex/client.ex @@ -52,15 +52,11 @@ defmodule ToxiproxyEx.Client do end defp client() do - url = Application.get_env(:toxiproxy_ex, :host, "http://127.0.0.1:8474") - middleware = [ - {Tesla.Middleware.BaseUrl, url}, + {Tesla.Middleware.BaseUrl, Application.fetch_env!(:toxiproxy_ex, :host)}, Tesla.Middleware.JSON ] - adapter = {Tesla.Adapter.Mint, []} - - Tesla.client(middleware, adapter) + Tesla.client(middleware, {Tesla.Adapter.Mint, []}) end end diff --git a/mix.exs b/mix.exs index bc568a9..ee1abed 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,8 @@ defmodule ToxiproxyEx.MixProject do def application do [ - extra_applications: [] + extra_applications: [], + env: [host: "http://127.0.0.1:8474"] ] end From fa7d4bda3223355e847bbe82d42bdc03ce4b8c67 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 14 Oct 2023 11:21:40 +0200 Subject: [PATCH 06/18] Remove indirection with ToxiproxyEx.Client --- lib/toxiproxy_ex.ex | 29 +++++------- lib/toxiproxy_ex/client.ex | 71 ++++++++-------------------- lib/toxiproxy_ex/proxy.ex | 52 ++++++++++---------- lib/toxiproxy_ex/server_error.ex | 4 +- lib/toxiproxy_ex/toxic.ex | 26 +++++----- lib/toxiproxy_ex/toxic_collection.ex | 6 +-- 6 files changed, 79 insertions(+), 109 deletions(-) diff --git a/lib/toxiproxy_ex.ex b/lib/toxiproxy_ex.ex index 29db79b..3b6550a 100644 --- a/lib/toxiproxy_ex.ex +++ b/lib/toxiproxy_ex.ex @@ -10,7 +10,7 @@ defmodule ToxiproxyEx do @typedoc """ A proxy that intercepts traffic to and from an upstream server. """ - @opaque proxy :: Proxy.t + @opaque proxy :: Proxy.t() @typedoc """ A collection of proxies. @@ -153,11 +153,10 @@ defmodule ToxiproxyEx do """ @spec all!() :: toxic_collection() def all!() do - case Client.list_proxies() do - {:ok, %{body: proxies}} -> Enum.map(proxies, &parse_proxy/1) - _ -> raise ServerError, message: "Could not fetch proxies." + case Client.request(:get, "/proxies") do + {:ok, proxies} -> proxies |> Enum.map(&parse_proxy/1) |> ToxicCollection.new() + {:error, _reason} -> raise ServerError, message: "Could not fetch proxies." end - |> ToxicCollection.new() end defp parse_proxy( @@ -323,7 +322,7 @@ defmodule ToxiproxyEx do ...> nil ...> end) """ - @spec apply!(toxic_collection(), (() -> any())) :: :ok + @spec apply!(toxic_collection(), (-> any())) :: :ok def apply!(%ToxicCollection{toxics: toxics}, fun) do dups = Enum.group_by(toxics, fn t -> [t.name, t.proxy_name] end) @@ -353,9 +352,7 @@ defmodule ToxiproxyEx do else raise ArgumentError, message: - "There are multiple toxics with the name '#{hd(hd(dups)).name}' for proxy '#{ - hd(hd(dups)).proxy_name - }', please override the default name (_)" + "There are multiple toxics with the name '#{hd(hd(dups)).name}' for proxy '#{hd(hd(dups)).proxy_name}', please override the default name (_)" end end @@ -382,7 +379,7 @@ defmodule ToxiproxyEx do ...> nil ...> end) """ - @spec down!(toxic_collection(), (() -> any())) :: :ok + @spec down!(toxic_collection(), (-> any())) :: :ok def down!(proxy = %Proxy{}, fun) do down!(ToxicCollection.new(proxy), fun) end @@ -420,9 +417,9 @@ defmodule ToxiproxyEx do """ @spec reset!() :: :ok def reset!() do - case Client.reset() do - {:ok, _} -> :ok - _ -> raise ServerError, message: "Could not reset toxiproxy" + case Client.request(:post, "/reset", %{}) do + {:ok, _body} -> :ok + {:error, _reason} -> raise ServerError, message: "Could not reset toxiproxy" end end @@ -439,9 +436,9 @@ defmodule ToxiproxyEx do """ @spec version!() :: String.t() def version!() do - case Client.version() do - {:ok, %{body: res}} when is_binary(res) -> res - _ -> raise ServerError, message: "Could not fetch version" + case Client.request(:get, "/version") do + {:ok, vsn} when is_binary(vsn) -> vsn + {:error, _reason} -> raise ServerError, message: "Could not fetch version" end end diff --git a/lib/toxiproxy_ex/client.ex b/lib/toxiproxy_ex/client.ex index dd8cac2..b835e5f 100644 --- a/lib/toxiproxy_ex/client.ex +++ b/lib/toxiproxy_ex/client.ex @@ -1,62 +1,31 @@ defmodule ToxiproxyEx.Client do @moduledoc false - def reset() do - client() - |> Tesla.post("/reset", %{}) - end - - def version() do - client() - |> Tesla.get("/version") - end - - def list_proxies() do - client() - |> Tesla.get("/proxies") - end - - def create_proxy(params) do - client() - |> Tesla.post("/proxies", params) - end - - def destroy_proxy(name) do - client() - |> Tesla.delete("/proxies/#{name}") - end - - def enable_proxy(name) do - client() - |> Tesla.post("/proxies/#{name}", %{enabled: true}) - end + alias Tesla.Env - def disable_proxy(name) do - client() - |> Tesla.post("/proxies/#{name}", %{enabled: false}) - end + @spec request(:get | :post | :delete, String.t(), map() | nil) :: + {:ok, response_body :: term()} | {:error, reason :: term()} + def request(method, path, params \\ nil) + when method in [:get, :post, :delete] and is_binary(path) do + middlewares = [ + {Tesla.Middleware.BaseUrl, Application.fetch_env!(:toxiproxy_ex, :host)}, + Tesla.Middleware.JSON + ] - def list_toxics(proxy_name) do - client() - |> Tesla.get("/proxies/#{proxy_name}/toxics") - end + client = Tesla.client(middlewares, {Tesla.Adapter.Mint, []}) - def create_toxic(proxy_name, params) do - client() - |> Tesla.post("/proxies/#{proxy_name}/toxics", params) - end + request_opts = [method: method, url: path] + request_opts = if params, do: Keyword.put(request_opts, :body, params), else: request_opts - def destroy_toxic(proxy_name, toxic_name) do - client() - |> Tesla.delete("/proxies/#{proxy_name}/toxics/#{toxic_name}") - end + case Tesla.request(client, request_opts) do + {:ok, %Env{status: status, body: body}} when status in 200..299 -> + {:ok, body} - defp client() do - middleware = [ - {Tesla.Middleware.BaseUrl, Application.fetch_env!(:toxiproxy_ex, :host)}, - Tesla.Middleware.JSON - ] + {:ok, %Env{} = env} -> + {:error, {:status, env}} - Tesla.client(middleware, {Tesla.Adapter.Mint, []}) + {:error, reason} -> + {:error, reason} + end end end diff --git a/lib/toxiproxy_ex/proxy.ex b/lib/toxiproxy_ex/proxy.ex index 2a1cd5f..7bb2df0 100644 --- a/lib/toxiproxy_ex/proxy.ex +++ b/lib/toxiproxy_ex/proxy.ex @@ -5,27 +5,27 @@ defmodule ToxiproxyEx.Proxy do @typedoc since: "1.2.0" @type t() :: %__MODULE__{ - upstream: String.t(), - listen: String.t(), - name: String.t(), - enabled: boolean() - } + upstream: String.t(), + listen: String.t(), + name: String.t(), + enabled: boolean() + } defstruct upstream: nil, listen: nil, name: nil, enabled: nil @spec disable(t()) :: :ok | :error def disable(%__MODULE__{} = proxy) do - case Client.disable_proxy(proxy.name) do - {:ok, _res} -> :ok - _ -> :error + case Client.request(:post, "/proxies/#{proxy.name}", %{enabled: false}) do + {:ok, _body} -> :ok + {:error, _reason} -> :error end end @spec enable(t()) :: :ok | :error def enable(%__MODULE__{} = proxy) do - case Client.enable_proxy(proxy.name) do - {:ok, _res} -> :ok - _ -> :error + case Client.request(:post, "/proxies/#{proxy.name}", %{enabled: true}) do + {:ok, _body} -> :ok + {:error, _reason} -> :error end end @@ -36,33 +36,35 @@ defmodule ToxiproxyEx.Proxy do name = Keyword.get(options, :name) enabled = Keyword.get(options, :enabled) - case Client.create_proxy(%{ - upstream: upstream, - name: name, - listen: listen, - enabled: enabled - }) do - {:ok, %{body: %{"listen" => listen, "enabled" => enabled, "name" => name}}} -> + body = %{ + upstream: upstream, + name: name, + listen: listen, + enabled: enabled + } + + case Client.request(:post, "/proxies", body) do + {:ok, %{"listen" => listen, "enabled" => enabled, "name" => name}} -> {:ok, %__MODULE__{upstream: upstream, listen: listen, name: name, enabled: enabled}} - _ -> + {:error, _reason} -> :error end end @spec destroy(t()) :: :ok | :error def destroy(%__MODULE__{} = proxy) do - case Client.destroy_proxy(proxy.name) do - {:ok, _res} -> :ok - _ -> :error + case Client.request(:delete, "/proxies/#{proxy.name}") do + {:ok, _body} -> :ok + {:error, _reason} -> :error end end @spec toxics(t()) :: {:ok, [Toxic.t()]} | :error def toxics(%__MODULE__{} = proxy) do - case Client.list_toxics(proxy.name) do - {:ok, %{body: toxics}} -> {:ok, Enum.map(toxics, &parse_toxic(&1, proxy))} - _ -> :error + case Client.request(:get, "/proxies/#{proxy.name}/toxics") do + {:ok, _body = toxics} -> {:ok, Enum.map(toxics, &parse_toxic(&1, proxy))} + {:error, _reason} -> :error end end diff --git a/lib/toxiproxy_ex/server_error.ex b/lib/toxiproxy_ex/server_error.ex index 3dd65fd..b6e2211 100644 --- a/lib/toxiproxy_ex/server_error.ex +++ b/lib/toxiproxy_ex/server_error.ex @@ -5,8 +5,8 @@ defmodule ToxiproxyEx.ServerError do @typedoc since: "1.2.0" @type t() :: %__MODULE__{ - message: String.t() - } + message: String.t() + } defexception message: "Server Error" end diff --git a/lib/toxiproxy_ex/toxic.ex b/lib/toxiproxy_ex/toxic.ex index 9e38d95..aa1b9d0 100644 --- a/lib/toxiproxy_ex/toxic.ex +++ b/lib/toxiproxy_ex/toxic.ex @@ -49,14 +49,16 @@ defmodule ToxiproxyEx.Toxic do @spec create(t()) :: {:ok, t()} | :error def create(%__MODULE__{} = toxic) do - case Client.create_toxic(toxic.proxy_name, %{ - name: toxic.name, - type: toxic.type, - stream: toxic.stream, - toxicity: toxic.toxicity, - attributes: toxic.attributes - }) do - {:ok, %{body: %{"attributes" => attributes, "toxicity" => toxicity}}} -> + body = %{ + name: toxic.name, + type: toxic.type, + stream: toxic.stream, + toxicity: toxic.toxicity, + attributes: toxic.attributes + } + + case Client.request(:post, "/proxies/#{toxic.proxy_name}/toxics", body) do + {:ok, %{"attributes" => attributes, "toxicity" => toxicity}} -> # Note: We update attributes and toxicity to ensure that our local representation is as close as possible to the data on the server. # We do not make use of those fields after `create` has been called but we update them anyway. {:ok, @@ -69,16 +71,16 @@ defmodule ToxiproxyEx.Toxic do proxy_name: toxic.proxy_name )} - _res -> + {:error, _reason} -> :error end end @spec destroy(t()) :: :ok | :error def destroy(%__MODULE__{} = toxic) do - case Client.destroy_toxic(toxic.proxy_name, toxic.name) do - {:ok, _res} -> :ok - _ -> :error + case Client.request(:delete, "/proxies/#{toxic.proxy_name}/toxics/#{toxic.name}") do + {:ok, _body} -> :ok + {:error, _reason} -> :error end end end diff --git a/lib/toxiproxy_ex/toxic_collection.ex b/lib/toxiproxy_ex/toxic_collection.ex index 857a05e..14b39dc 100644 --- a/lib/toxiproxy_ex/toxic_collection.ex +++ b/lib/toxiproxy_ex/toxic_collection.ex @@ -5,9 +5,9 @@ defmodule ToxiproxyEx.ToxicCollection do @typedoc since: "1.2.0" @type t() :: %__MODULE__{ - proxies: [Proxy.t()], - toxics: [Toxic.t()] - } + proxies: [Proxy.t()], + toxics: [Toxic.t()] + } defstruct proxies: [], toxics: [] From f1adaaaeddb03ac30832764da18885026af84085 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sun, 15 Oct 2023 09:02:17 +0200 Subject: [PATCH 07/18] Improve message for ToxiproxyEx.ServerError --- lib/toxiproxy_ex.ex | 69 ++++++-------------------------- lib/toxiproxy_ex/client.ex | 18 +++++---- lib/toxiproxy_ex/proxy.ex | 43 ++++++++------------ lib/toxiproxy_ex/server_error.ex | 36 ++++++++++++++++- lib/toxiproxy_ex/toxic.ex | 38 ++++++++---------- test/toxiproxy_ex_test.exs | 4 +- 6 files changed, 92 insertions(+), 116 deletions(-) diff --git a/lib/toxiproxy_ex.ex b/lib/toxiproxy_ex.ex index 3b6550a..f60e71f 100644 --- a/lib/toxiproxy_ex.ex +++ b/lib/toxiproxy_ex.ex @@ -1,5 +1,5 @@ defmodule ToxiproxyEx do - alias ToxiproxyEx.{Proxy, Client, Toxic, ToxicCollection, ServerError} + alias ToxiproxyEx.{Proxy, Client, Toxic, ToxicCollection} @external_resource "README.md" @moduledoc "README.md" @@ -54,12 +54,7 @@ defmodule ToxiproxyEx do listen: host_with_port() | nil, enabled: true | false | nil ) :: proxy() - def create!(options) do - case Proxy.create(options) do - {:ok, proxy} -> proxy - :error -> raise ServerError, message: "Could not create proxy" - end - end + defdelegate create!(options), to: Proxy, as: :create @doc """ Deletes one or multiple proxies on the toxiproxy server. @@ -87,12 +82,7 @@ defmodule ToxiproxyEx do end def destroy!(%ToxicCollection{proxies: proxies}) do - Enum.each(proxies, fn proxy -> - case Proxy.destroy(proxy) do - :ok -> nil - :error -> raise ServerError, message: "Could not destroy proxy" - end - end) + Enum.each(proxies, &Proxy.destroy/1) end @doc """ @@ -153,10 +143,9 @@ defmodule ToxiproxyEx do """ @spec all!() :: toxic_collection() def all!() do - case Client.request(:get, "/proxies") do - {:ok, proxies} -> proxies |> Enum.map(&parse_proxy/1) |> ToxicCollection.new() - {:error, _reason} -> raise ServerError, message: "Could not fetch proxies." - end + Client.request!(:get, "/proxies") + |> Enum.map(&parse_proxy/1) + |> ToxicCollection.new() end defp parse_proxy( @@ -331,24 +320,11 @@ defmodule ToxiproxyEx do if Enum.empty?(dups) do # Note: We probably don't care about the updated toxies here but we still use them rather than the one passed into the function. - toxics = - Enum.map(toxics, fn toxic -> - case Toxic.create(toxic) do - {:ok, toxic} -> toxic - :error -> raise ServerError, message: "Could not create toxic '#{toxic.name}'" - end - end) + toxics = Enum.map(toxics, &Toxic.create/1) fun.() - Enum.each(toxics, fn toxic -> - case Toxic.destroy(toxic) do - :ok -> nil - :error -> raise ServerError, message: "Could not destroy toxic '#{toxic.name}'" - end - end) - - :ok + Enum.each(toxics, &Toxic.destroy/1) else raise ArgumentError, message: @@ -385,23 +361,9 @@ defmodule ToxiproxyEx do end def down!(%ToxicCollection{proxies: proxies}, fun) do - Enum.each(proxies, fn proxy -> - case Proxy.disable(proxy) do - :ok -> nil - :error -> raise ServerError, message: "Could not disable proxy '#{proxy.name}'" - end - end) - + Enum.each(proxies, &Proxy.disable/1) fun.() - - Enum.each(proxies, fn proxy -> - case Proxy.enable(proxy) do - :ok -> nil - :error -> raise ServerError, message: "Could not enable proxy '#{proxy.name}'" - end - end) - - :ok + Enum.each(proxies, &Proxy.enable/1) end @doc """ @@ -417,10 +379,8 @@ defmodule ToxiproxyEx do """ @spec reset!() :: :ok def reset!() do - case Client.request(:post, "/reset", %{}) do - {:ok, _body} -> :ok - {:error, _reason} -> raise ServerError, message: "Could not reset toxiproxy" - end + Client.request!(:post, "/reset", %{}) + :ok end @doc """ @@ -436,10 +396,7 @@ defmodule ToxiproxyEx do """ @spec version!() :: String.t() def version!() do - case Client.request(:get, "/version") do - {:ok, vsn} when is_binary(vsn) -> vsn - {:error, _reason} -> raise ServerError, message: "Could not fetch version" - end + Client.request!(:get, "/version") end @doc """ diff --git a/lib/toxiproxy_ex/client.ex b/lib/toxiproxy_ex/client.ex index b835e5f..22de2c5 100644 --- a/lib/toxiproxy_ex/client.ex +++ b/lib/toxiproxy_ex/client.ex @@ -2,11 +2,12 @@ defmodule ToxiproxyEx.Client do @moduledoc false alias Tesla.Env + alias ToxiproxyEx.ServerError - @spec request(:get | :post | :delete, String.t(), map() | nil) :: - {:ok, response_body :: term()} | {:error, reason :: term()} - def request(method, path, params \\ nil) - when method in [:get, :post, :delete] and is_binary(path) do + @spec request!(:get | :post | :delete, String.t(), map() | nil) :: response_body :: term() + def request!(method, path, params \\ nil) + when method in [:get, :post, :delete] and is_binary(path) and + (is_nil(params) or is_map(params)) do middlewares = [ {Tesla.Middleware.BaseUrl, Application.fetch_env!(:toxiproxy_ex, :host)}, Tesla.Middleware.JSON @@ -19,13 +20,16 @@ defmodule ToxiproxyEx.Client do case Tesla.request(client, request_opts) do {:ok, %Env{status: status, body: body}} when status in 200..299 -> - {:ok, body} + body {:ok, %Env{} = env} -> - {:error, {:status, env}} + raise ServerError, method: method, path: path, reason: {:status, env} + + {:error, {Tesla.Middleware.JSON, :decode, %Jason.DecodeError{} = error}} -> + raise ServerError, method: method, path: path, reason: error {:error, reason} -> - {:error, reason} + raise ServerError, method: method, path: path, reason: reason end end end diff --git a/lib/toxiproxy_ex/proxy.ex b/lib/toxiproxy_ex/proxy.ex index 7bb2df0..54eb53e 100644 --- a/lib/toxiproxy_ex/proxy.ex +++ b/lib/toxiproxy_ex/proxy.ex @@ -13,23 +13,19 @@ defmodule ToxiproxyEx.Proxy do defstruct upstream: nil, listen: nil, name: nil, enabled: nil - @spec disable(t()) :: :ok | :error + @spec disable(t()) :: :ok def disable(%__MODULE__{} = proxy) do - case Client.request(:post, "/proxies/#{proxy.name}", %{enabled: false}) do - {:ok, _body} -> :ok - {:error, _reason} -> :error - end + Client.request!(:post, "/proxies/#{proxy.name}", %{enabled: false}) + :ok end - @spec enable(t()) :: :ok | :error + @spec enable(t()) :: :ok def enable(%__MODULE__{} = proxy) do - case Client.request(:post, "/proxies/#{proxy.name}", %{enabled: true}) do - {:ok, _body} -> :ok - {:error, _reason} -> :error - end + Client.request!(:post, "/proxies/#{proxy.name}", %{enabled: true}) + :ok end - @spec create(keyword()) :: {:ok, t()} | :error + @spec create(keyword()) :: t() def create(options) when is_list(options) do upstream = Keyword.get(options, :upstream) listen = Keyword.get(options, :listen, "localhost:0") @@ -43,29 +39,22 @@ defmodule ToxiproxyEx.Proxy do enabled: enabled } - case Client.request(:post, "/proxies", body) do - {:ok, %{"listen" => listen, "enabled" => enabled, "name" => name}} -> - {:ok, %__MODULE__{upstream: upstream, listen: listen, name: name, enabled: enabled}} + %{"listen" => listen, "enabled" => enabled, "name" => name} = + Client.request!(:post, "/proxies", body) - {:error, _reason} -> - :error - end + %__MODULE__{upstream: upstream, listen: listen, name: name, enabled: enabled} end - @spec destroy(t()) :: :ok | :error + @spec destroy(t()) :: :ok def destroy(%__MODULE__{} = proxy) do - case Client.request(:delete, "/proxies/#{proxy.name}") do - {:ok, _body} -> :ok - {:error, _reason} -> :error - end + Client.request!(:delete, "/proxies/#{proxy.name}") + :ok end - @spec toxics(t()) :: {:ok, [Toxic.t()]} | :error + @spec toxics(t()) :: [Toxic.t()] def toxics(%__MODULE__{} = proxy) do - case Client.request(:get, "/proxies/#{proxy.name}/toxics") do - {:ok, _body = toxics} -> {:ok, Enum.map(toxics, &parse_toxic(&1, proxy))} - {:error, _reason} -> :error - end + toxics = Client.request!(:get, "/proxies/#{proxy.name}/toxics") + Enum.map(toxics, &parse_toxic(&1, proxy)) end defp parse_toxic( diff --git a/lib/toxiproxy_ex/server_error.ex b/lib/toxiproxy_ex/server_error.ex index b6e2211..50eef9c 100644 --- a/lib/toxiproxy_ex/server_error.ex +++ b/lib/toxiproxy_ex/server_error.ex @@ -1,6 +1,6 @@ defmodule ToxiproxyEx.ServerError do @moduledoc """ - Raised when communication with the toxiproxy server fails. + Raised when communication with the Toxiproxy server fails. """ @typedoc since: "1.2.0" @@ -9,4 +9,38 @@ defmodule ToxiproxyEx.ServerError do } defexception message: "Server Error" + + @impl true + def exception(options) when is_list(options) do + method = Keyword.fetch!(options, :method) + path = Keyword.fetch!(options, :path) + reason = Keyword.fetch!(options, :reason) + + string_reason = + case reason do + {:status, %Tesla.Env{status: status, headers: headers, body: body}} -> + """ + invalid status code #{status}. + + Headers: #{inspect(headers)} + Body: #{inspect(body)} + + """ + + %Jason.DecodeError{} = error -> + Exception.message(error) + + other -> + inspect(other) + end + + message = """ + Request to the Toxiproxy server failed. + + Request: #{method |> to_string() |> String.upcase()} #{path} + Failure reason: #{string_reason} + """ + + %__MODULE__{message: message} + end end diff --git a/lib/toxiproxy_ex/toxic.ex b/lib/toxiproxy_ex/toxic.ex index aa1b9d0..1fdca61 100644 --- a/lib/toxiproxy_ex/toxic.ex +++ b/lib/toxiproxy_ex/toxic.ex @@ -47,7 +47,7 @@ defmodule ToxiproxyEx.Toxic do Keyword.put(fields, field, default) end - @spec create(t()) :: {:ok, t()} | :error + @spec create(t()) :: t() def create(%__MODULE__{} = toxic) do body = %{ name: toxic.name, @@ -57,30 +57,24 @@ defmodule ToxiproxyEx.Toxic do attributes: toxic.attributes } - case Client.request(:post, "/proxies/#{toxic.proxy_name}/toxics", body) do - {:ok, %{"attributes" => attributes, "toxicity" => toxicity}} -> - # Note: We update attributes and toxicity to ensure that our local representation is as close as possible to the data on the server. - # We do not make use of those fields after `create` has been called but we update them anyway. - {:ok, - new( - name: toxic.name, - type: toxic.type, - stream: toxic.stream, - toxicity: toxicity, - attributes: attributes, - proxy_name: toxic.proxy_name - )} + %{"attributes" => attributes, "toxicity" => toxicity} = + Client.request!(:post, "/proxies/#{toxic.proxy_name}/toxics", body) - {:error, _reason} -> - :error - end + # Note: We update attributes and toxicity to ensure that our local representation is as close as possible to the data on the server. + # We do not make use of those fields after `create` has been called but we update them anyway. + new( + name: toxic.name, + type: toxic.type, + stream: toxic.stream, + toxicity: toxicity, + attributes: attributes, + proxy_name: toxic.proxy_name + ) end - @spec destroy(t()) :: :ok | :error + @spec destroy(t()) :: :ok def destroy(%__MODULE__{} = toxic) do - case Client.request(:delete, "/proxies/#{toxic.proxy_name}/toxics/#{toxic.name}") do - {:ok, _body} -> :ok - {:error, _reason} -> :error - end + Client.request!(:delete, "/proxies/#{toxic.proxy_name}/toxics/#{toxic.name}") + :ok end end diff --git a/test/toxiproxy_ex_test.exs b/test/toxiproxy_ex_test.exs index d581362..ce10311 100644 --- a/test/toxiproxy_ex_test.exs +++ b/test/toxiproxy_ex_test.exs @@ -208,9 +208,7 @@ defmodule ToxiproxyExTest do |> ToxiproxyEx.upstream(:latency, latency: 100, name: "my_upstream_toxic") |> ToxiproxyEx.apply!(fn -> # Use private API to retrieve toxics for the proxy. - {:ok, toxics} = Proxy.toxics(proxy) - - names = Enum.map(toxics, & &1.name) |> Enum.sort() + names = proxy |> Proxy.toxics() |> Enum.map(& &1.name) |> Enum.sort() assert names == Enum.sort(["latency_downstream", "latency_upstream", "my_upstream_toxic"]) end) end) From 943d1bfb0316206beb0e320d9d67395d53e1e423 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sun, 15 Oct 2023 09:30:48 +0200 Subject: [PATCH 08/18] Improve assertions --- lib/toxiproxy_ex/client.ex | 3 +- test/support/proxy_assertions.ex | 37 ++++++++++++++++-------- test/support/test_helpers.ex | 49 ++++++++++++++++---------------- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/toxiproxy_ex/client.ex b/lib/toxiproxy_ex/client.ex index 22de2c5..84e379f 100644 --- a/lib/toxiproxy_ex/client.ex +++ b/lib/toxiproxy_ex/client.ex @@ -10,7 +10,8 @@ defmodule ToxiproxyEx.Client do (is_nil(params) or is_map(params)) do middlewares = [ {Tesla.Middleware.BaseUrl, Application.fetch_env!(:toxiproxy_ex, :host)}, - Tesla.Middleware.JSON + Tesla.Middleware.JSON, + Tesla.Middleware.KeepRequest ] client = Tesla.client(middlewares, {Tesla.Adapter.Mint, []}) diff --git a/test/support/proxy_assertions.ex b/test/support/proxy_assertions.ex index 22b64c0..b45cef8 100644 --- a/test/support/proxy_assertions.ex +++ b/test/support/proxy_assertions.ex @@ -1,19 +1,32 @@ defmodule ToxiproxyEx.ProxyAssertions do - defmacro assert_proxy_available(proxy) do - quote do - case connect_to_proxy(unquote(proxy)) do - {:ok, _socket} -> assert true - _ -> flunk("Proxy #{unquote(proxy).name} is not available but should be") - end + import ExUnit.Assertions + + alias ToxiproxyEx.Proxy + alias ToxiproxyEx.TestHelpers + + def assert_proxy_available(%Proxy{} = proxy) do + case TestHelpers.connect_to_proxy(proxy) do + {:ok, socket} -> + :gen_tcp.close(socket) + + {:error, reason} -> + flunk(""" + Proxy #{proxy.name} (shown below) should be available, but is not: \ + #{:inet.format_error(reason)} + + #{inspect(proxy)} + """) end end - defmacro assert_proxy_unavailable(proxy) do - quote do - case connect_to_proxy(unquote(proxy)) do - {:error, :econnrefused} -> assert true - _ -> flunk("Proxy #{unquote(proxy).name} is available but should not be") - end + def assert_proxy_unavailable(%Proxy{} = proxy) do + case TestHelpers.connect_to_proxy(proxy) do + {:error, :econnrefused} -> + :ok + + {:ok, socket} -> + :gen_tcp.close(socket) + flunk("Proxy #{proxy.name} is available but should not be") end end end diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index 212d583..d56896e 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -1,11 +1,13 @@ defmodule ToxiproxyEx.TestHelpers do + import ExUnit.Assertions + alias ToxiproxyEx.EchoServer def connect_to_proxy(proxy) do - [hostname, port] = String.split(proxy.listen, ":") + assert [hostname, port] = String.split(proxy.listen, ":") hostname = String.to_charlist(hostname) - {port, _rem} = Integer.parse(port) + assert {port, ""} = Integer.parse(port) :gen_tcp.connect(hostname, port, [ :binary, @@ -26,35 +28,34 @@ defmodule ToxiproxyEx.TestHelpers do {duration / 1_000, res} end - def with_tcpservers(count, fun) do - servers = - Enum.map(1..count, fn _i -> - EchoServer.create() - end) + def with_tcpservers(count, fun) when is_integer(count) do + parent_pid = self() + ref = make_ref() - server_pids = - Enum.map(servers, fn {socket, _port} -> - spawn(fn -> - EchoServer.start(socket) - end) - end) + tasks = + Enum.map(1..count, fn _i -> + task = + Task.async(fn -> + {socket, port} = EchoServer.create() + send(parent_pid, {:port, ref, port}) + EchoServer.start(socket) + end) - ports = Enum.map(servers, fn {_socket, port} -> port end) + assert_receive {:port, ^ref, port} - fun.(ports) + {task, port} + end) - Enum.each(server_pids, fn pid -> - Process.exit(pid, :kill) - end) + ports = Enum.map(tasks, fn {_task, port} -> port end) - Enum.each(servers, fn {socket, _port} -> - EchoServer.stop(socket) - end) + try do + fun.(ports) + after + Enum.each(tasks, fn {task, _port} -> Task.shutdown(task) end) + end end def with_tcpserver(fun) do - with_tcpservers(1, fn ports -> - fun.(hd(ports)) - end) + with_tcpservers(1, fn [port] -> fun.(port) end) end end From 0b2cb5b763e3abcfb0f3058e21c63fba4fe51d9d Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 16 Oct 2023 09:15:33 +0200 Subject: [PATCH 09/18] Fix /version endpoint in Toxiproxy 2.6.0 --- lib/toxiproxy_ex.ex | 5 ++++- lib/toxiproxy_ex/client.ex | 25 ++++++++++++++++++++++++- test/toxiproxy_ex_test.exs | 1 + 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/toxiproxy_ex.ex b/lib/toxiproxy_ex.ex index f60e71f..2b5d064 100644 --- a/lib/toxiproxy_ex.ex +++ b/lib/toxiproxy_ex.ex @@ -396,7 +396,10 @@ defmodule ToxiproxyEx do """ @spec version!() :: String.t() def version!() do - Client.request!(:get, "/version") + case Client.request!(:get, "/version") do + %{"version" => version} -> version + version -> version + end end @doc """ diff --git a/lib/toxiproxy_ex/client.ex b/lib/toxiproxy_ex/client.ex index 84e379f..808dd08 100644 --- a/lib/toxiproxy_ex/client.ex +++ b/lib/toxiproxy_ex/client.ex @@ -4,6 +4,28 @@ defmodule ToxiproxyEx.Client do alias Tesla.Env alias ToxiproxyEx.ServerError + # We can remove this once Toxiproxy will fix their stuff. See: + # https://github.com/Shopify/toxiproxy/pull/538 + defmodule BuggyToxiproxyVersionParserMiddleware do + @moduledoc false + + @behaviour Tesla.Middleware + + @impl true + def call(env, next, _options) do + with {:ok, env} <- Tesla.run(env, next) do + env = + if String.ends_with?(env.url, "/version") do + update_in(env.body, &String.trim_trailing(&1, "\\n")) + else + env + end + + {:ok, env} + end + end + end + @spec request!(:get | :post | :delete, String.t(), map() | nil) :: response_body :: term() def request!(method, path, params \\ nil) when method in [:get, :post, :delete] and is_binary(path) and @@ -11,7 +33,8 @@ defmodule ToxiproxyEx.Client do middlewares = [ {Tesla.Middleware.BaseUrl, Application.fetch_env!(:toxiproxy_ex, :host)}, Tesla.Middleware.JSON, - Tesla.Middleware.KeepRequest + Tesla.Middleware.KeepRequest, + BuggyToxiproxyVersionParserMiddleware ] client = Tesla.client(middlewares, {Tesla.Adapter.Mint, []}) diff --git a/test/toxiproxy_ex_test.exs b/test/toxiproxy_ex_test.exs index ce10311..86057cc 100644 --- a/test/toxiproxy_ex_test.exs +++ b/test/toxiproxy_ex_test.exs @@ -287,6 +287,7 @@ defmodule ToxiproxyExTest do test "version" do version = ToxiproxyEx.version!() + assert is_binary(version) assert String.starts_with?(version, "2.") end From 1a114e4eabe229c9fbb70cb28a649231adc703d5 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 16 Oct 2023 11:13:47 +0200 Subject: [PATCH 10/18] Improve CI (#1) --- .github/workflows/main.yml | 60 ++++++++++++++++++++++++++++++------ test/support/test_helpers.ex | 2 +- test/toxiproxy_ex_test.exs | 2 +- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bee5c78..4a068a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,18 +3,60 @@ on: branches: - main pull_request: - + +env: + MIX_ENV: test + jobs: test: + name: Test (Erlang ${{ matrix.otp }}, Elixir ${{ matrix.elixir }}, Toxiproxy ${{ matrix.toxiproxy }}) + runs-on: ubuntu-20.04 + + strategy: + matrix: + toxiproxy: + - "2.6.0" + - "2.5.0" + - "2.1.2" + elixir: + - "1.15" + otp: + - "26.1" + include: + # Oldest supported version pair. + - otp: "23.3" + elixir: "1.10.4-otp-23" + toxiproxy: "2.1.2" + + env: + TOXIPROXY_VERSION: ${{ matrix.toxiproxy }} + steps: - - uses: actions/checkout@v2 - - uses: erlef/setup-beam@v1 + - name: Check out this repository + uses: actions/checkout@v4 + + - name: Install Erlang and Elixir + uses: erlef/setup-beam@v1 with: - version-file: .tool-versions + otp-version: ${{ matrix.otp }} + elixir-version: ${{ matrix.elixir }} version-type: strict - - run: curl --silent -L https://github.com/Shopify/toxiproxy/releases/download/v2.1.2/toxiproxy-server-linux-amd64 -o ./toxiproxy-server - - run: chmod +x ./toxiproxy-server - - run: nohup bash -c "./toxiproxy-server > ./toxiproxy.log 2>&1 &" - - run: mix deps.get - - run: mix test + + - name: Install Toxiproxy + run: | + curl -v -L --fail https://github.com/Shopify/toxiproxy/releases/download/v${TOXIPROXY_VERSION}/toxiproxy-server-linux-amd64 -o ./toxiproxy-server + chmod +x ./toxiproxy-server + + - name: Start Toxiproxy + run: nohup bash -c "./toxiproxy-server > ./toxiproxy.log 2>&1 &" + + - name: Install dependencies + run: mix deps.get + + - name: Run tests + run: mix test + + - name: Dump Toxiproxy logs on failure + if: failure() + run: cat ./toxiproxy.log diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index d56896e..a9e88d4 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -41,7 +41,7 @@ defmodule ToxiproxyEx.TestHelpers do EchoServer.start(socket) end) - assert_receive {:port, ^ref, port} + assert_receive {:port, ^ref, port}, 100 {task, port} end) diff --git a/test/toxiproxy_ex_test.exs b/test/toxiproxy_ex_test.exs index 86057cc..ef4b464 100644 --- a/test/toxiproxy_ex_test.exs +++ b/test/toxiproxy_ex_test.exs @@ -65,7 +65,7 @@ defmodule ToxiproxyExTest do assert_proxy_available(proxy) # Use private API to retrieve toxics for the proxy. - {:ok, proxies} = Proxy.toxics(proxy) + proxies = Proxy.toxics(proxy) assert Enum.empty?(proxies) end) end From 99ba7a51c93b068bb812210894eda2053c78b100 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 16 Oct 2023 11:25:20 +0200 Subject: [PATCH 11/18] Cache and lint in CI --- .github/workflows/main.yml | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4a068a1..1374103 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,46 @@ env: MIX_ENV: test jobs: + lint: + name: Lint + runs-on: ubuntu-20.04 + + env: + ELIXIR_VERSION: "1.15" + OTP_VERSION: "26.1" + + steps: + - name: Check out this repository + uses: actions/checkout@v4 + + - name: Install Erlang and Elixir + uses: erlef/setup-beam@v1 + with: + otp-version: ${{ env.OTP_VERSION }} + elixir-version: ${{ env.ELIXIR_VERSION}} + + - name: Cache /_build and /deps + uses: actions/cache@v3 + id: cache-mix + with: + path: | + _build + deps + key: | + mix-elixir${{ env.ELIXIR_VERSION }}-otp${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + mix-elixir${{ env.ELIXIR_VERSION }}-otp${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}-${{ github.run_id }}- + mix-elixir${{ env.ELIXIR_VERSION }}-otp${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}- + + - name: Install dependencies + run: mix deps.get + + - name: Check for unused dependencies + run: mix deps.unlock --check-unused + + - name: Check for formatted code + run: mix format --check-formatted + test: name: Test (Erlang ${{ matrix.otp }}, Elixir ${{ matrix.elixir }}, Toxiproxy ${{ matrix.toxiproxy }}) @@ -51,6 +91,19 @@ jobs: - name: Start Toxiproxy run: nohup bash -c "./toxiproxy-server > ./toxiproxy.log 2>&1 &" + - name: Cache Mix compiled stuff + uses: actions/cache@v3 + id: cache-mix + with: + path: | + _build + deps + key: | + mix-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-${{ hashFiles('mix.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + mix-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-${{ hashFiles('mix.lock') }}-${{ github.run_id }}- + mix-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-${{ hashFiles('mix.lock') }}- + - name: Install dependencies run: mix deps.get From 57317c98ec67fe0beca49c9c7df2b2074969dfcf Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 16 Oct 2023 11:42:40 +0200 Subject: [PATCH 12/18] Run Dialyzer in CI (#3) --- .github/workflows/main.yml | 58 +++++++++++++++++++++++++++++++++++++- mix.exs | 17 +++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1374103..9b3ee38 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION}} - - name: Cache /_build and /deps + - name: Cache Mix compiled stuff uses: actions/cache@v3 id: cache-mix with: @@ -48,6 +48,62 @@ jobs: - name: Check for formatted code run: mix format --check-formatted + dialyzer: + name: Dialyze + runs-on: ubuntu-20.04 + + env: + ELIXIR_VERSION: "1.15" + OTP_VERSION: "26.1" + + steps: + - name: Check out this repository + uses: actions/checkout@v4 + + - name: Install Erlang and Elixir + uses: erlef/setup-beam@v1 + with: + otp-version: ${{ env.OTP_VERSION }} + elixir-version: ${{ env.ELIXIR_VERSION}} + + - name: Cache Mix compiled stuff + uses: actions/cache@v3 + id: cache-mix + with: + path: | + _build + deps + key: | + mix-elixir${{ env.ELIXIR_VERSION }}-otp${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + mix-elixir${{ env.ELIXIR_VERSION }}-otp${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}-${{ github.run_id }}- + mix-elixir${{ env.ELIXIR_VERSION }}-otp${{ env.OTP_VERSION }}-${{ hashFiles('mix.lock') }}- + + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old + # ones. Cache key based on Elixir and Erlang version (also useful when running in matrix). + - name: Cache Dialyzer's PLT + uses: actions/cache@v3 + id: cache-plt + with: + path: priv/plts + key: | + plt-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + plt-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}- + + # Create PLTs if no cache was found + - name: Create PLTs + if: steps.cache-plt.outputs.cache-hit != 'true' + run: | + mkdir -p priv/plts + mix dialyzer --plt + + - name: Install dependencies + run: mix deps.get + + - name: Run Dialyzer + run: mix dialyzer --format github + test: name: Test (Erlang ${{ matrix.otp }}, Elixir ${{ matrix.elixir }}, Toxiproxy ${{ matrix.toxiproxy }}) diff --git a/mix.exs b/mix.exs index ee1abed..60fe287 100644 --- a/mix.exs +++ b/mix.exs @@ -12,6 +12,13 @@ defmodule ToxiproxyEx.MixProject do start_permanent: Mix.env() == :prod, deps: deps(), + # Dialyzer + dialyzer: [ + plt_local_path: "priv/plts", + plt_core_path: "priv/plts", + plt_add_apps: [:ssl, :crypto, :mix, :ex_unit, :erts, :kernel, :stdlib] + ], + # Hex description: "Elixir Client for Toxiproxy", package: package(), @@ -34,9 +41,13 @@ defmodule ToxiproxyEx.MixProject do {:jason, ">= 1.0.0"}, {:castore, "~> 1.0.3"}, {:mint, "~> 1.0"}, - {:ex_doc, "~> 0.23", only: :dev, runtime: false}, - {:dialyxir, "~> 1.4", only: [:dev], runtime: false} - ] + {:ex_doc, "~> 0.23", only: :dev, runtime: false} + ] ++ + if Version.match?(System.version(), "~> 1.12") do + [{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}] + else + [] + end end defp package do From 526d372df697fec8b75ff62c810c60893828f6d7 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 16 Oct 2023 18:11:29 +0200 Subject: [PATCH 13/18] Ignore /priv/plts in Git --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d3782ef..7bb3eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). toxiproxy_ex-*.tar +/priv/plts From 11dc70cbe223a8b57455d8eacbc80ec9ea7e89d7 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 16 Oct 2023 18:11:49 +0200 Subject: [PATCH 14/18] Clean proxies in apply!/2 if function crashes --- lib/toxiproxy_ex.ex | 33 ++++++++++-------- test/toxiproxy_ex_test.exs | 68 ++++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/lib/toxiproxy_ex.ex b/lib/toxiproxy_ex.ex index 2b5d064..8d2106e 100644 --- a/lib/toxiproxy_ex.ex +++ b/lib/toxiproxy_ex.ex @@ -311,24 +311,29 @@ defmodule ToxiproxyEx do ...> nil ...> end) """ - @spec apply!(toxic_collection(), (-> any())) :: :ok - def apply!(%ToxicCollection{toxics: toxics}, fun) do - dups = - Enum.group_by(toxics, fn t -> [t.name, t.proxy_name] end) - |> Enum.map(fn {_group, toxics} -> toxics end) - |> Enum.filter(fn toxics -> length(toxics) > 1 end) + @spec apply!(toxic_collection(), (-> result)) :: result when result: var + def apply!(%ToxicCollection{toxics: toxics}, fun) when is_function(fun, 0) do + toxics + |> Enum.group_by(fn %Toxic{} = toxic -> {toxic.name, toxic.proxy_name} end) + |> Enum.each(fn + {_name_and_proxy_name, [toxic, _other_toxic | _rest]} -> + raise ArgumentError, """ + there are multiple toxics with the name #{inspect(toxic.name)} for proxy \ + #{inspect(toxic.proxy_name)}, please override the default name (_)\ + """ + + {_name_and_proxy_name, [_toxic]} -> + :ok + end) - if Enum.empty?(dups) do - # Note: We probably don't care about the updated toxies here but we still use them rather than the one passed into the function. - toxics = Enum.map(toxics, &Toxic.create/1) + # We probably don't care about the updated toxics here but we still use + # rather than the one passed into the function. + toxics = Enum.map(toxics, &Toxic.create/1) + try do fun.() - + after Enum.each(toxics, &Toxic.destroy/1) - else - raise ArgumentError, - message: - "There are multiple toxics with the name '#{hd(hd(dups)).name}' for proxy '#{hd(hd(dups)).proxy_name}', please override the default name (_)" end end diff --git a/test/toxiproxy_ex_test.exs b/test/toxiproxy_ex_test.exs index ef4b464..7eeb9e4 100644 --- a/test/toxiproxy_ex_test.exs +++ b/test/toxiproxy_ex_test.exs @@ -320,13 +320,67 @@ defmodule ToxiproxyExTest do |> ToxiproxyEx.downstream(:latency, latency: 100) |> ToxiproxyEx.downstream(:latency, latency: 100) - assert_raise ArgumentError, - "There are multiple toxics with the name 'latency_downstream' for proxy 'test_echo_server', please override the default name (_)", - fn -> - ToxiproxyEx.apply!(collection, fn -> - nil - end) - end + message = """ + there are multiple toxics with the name "latency_downstream" for proxy \ + "test_echo_server", please override the default name (_)\ + """ + + assert_raise ArgumentError, message, fn -> + ToxiproxyEx.apply!(collection, fn -> + nil + end) + end end) end + + describe "apply!/2" do + @describetag :focus + + test "destroys applied toxics if the passed function raises" do + with_tcpserver(fn port -> + proxy = ToxiproxyEx.create!(upstream: "localhost:#{port}", name: "test_echo_server") + + toxic_collection = + proxy + |> ToxiproxyEx.downstream(:latency, name: "apply_raise1_downstream", latency: 100) + |> ToxiproxyEx.downstream(:latency, name: "apply_raise2_downstream", timeout: 1000) + + assert_raise RuntimeError, "boom", fn -> + ToxiproxyEx.apply!(toxic_collection, fn -> raise "boom" end) + end + + assert ToxiproxyEx.Client.request!(:get, "/proxies/#{proxy.name}/toxics") == [] + end) + end + + test "destroys applied toxics if the passed function exits" do + with_tcpserver(fn port -> + proxy = ToxiproxyEx.create!(upstream: "localhost:#{port}", name: "test_echo_server") + + toxic_collection = + proxy + |> ToxiproxyEx.downstream(:latency, name: "apply_exit1_downstream", latency: 100) + |> ToxiproxyEx.downstream(:latency, name: "apply_exit2_downstream", timeout: 1000) + + catch_exit(ToxiproxyEx.apply!(toxic_collection, fn -> exit(:boom) end)) + + assert ToxiproxyEx.Client.request!(:get, "/proxies/#{proxy.name}/toxics") == [] + end) + end + + test "destroys applied toxics if the passed function throws" do + with_tcpserver(fn port -> + proxy = ToxiproxyEx.create!(upstream: "localhost:#{port}", name: "test_echo_server") + + toxic_collection = + proxy + |> ToxiproxyEx.downstream(:latency, name: "apply_throw1_downstream", latency: 100) + |> ToxiproxyEx.downstream(:latency, name: "apply_throw2_downstream", timeout: 1000) + + catch_throw(ToxiproxyEx.apply!(toxic_collection, fn -> throw(:boom) end)) + + assert ToxiproxyEx.Client.request!(:get, "/proxies/#{proxy.name}/toxics") == [] + end) + end + end end From 27ef989635de8269803e3859e4feaf0d27fa8d98 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 17 Oct 2023 09:47:36 +0200 Subject: [PATCH 15/18] Fix crashing function in ToxiproxyEx.down!/2 --- lib/toxiproxy_ex.ex | 14 ++++++++++---- test/toxiproxy_ex_test.exs | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/lib/toxiproxy_ex.ex b/lib/toxiproxy_ex.ex index 8d2106e..dfec550 100644 --- a/lib/toxiproxy_ex.ex +++ b/lib/toxiproxy_ex.ex @@ -360,15 +360,21 @@ defmodule ToxiproxyEx do ...> nil ...> end) """ - @spec down!(toxic_collection(), (-> any())) :: :ok + @spec down!(toxic_collection() | proxy(), (-> result)) :: result when result: var + def down!(proxy_or_collection, fun) + def down!(proxy = %Proxy{}, fun) do down!(ToxicCollection.new(proxy), fun) end - def down!(%ToxicCollection{proxies: proxies}, fun) do + def down!(%ToxicCollection{proxies: proxies}, fun) when is_function(fun, 0) do Enum.each(proxies, &Proxy.disable/1) - fun.() - Enum.each(proxies, &Proxy.enable/1) + + try do + fun.() + after + Enum.each(proxies, &Proxy.enable/1) + end end @doc """ diff --git a/test/toxiproxy_ex_test.exs b/test/toxiproxy_ex_test.exs index 7eeb9e4..3f1a78f 100644 --- a/test/toxiproxy_ex_test.exs +++ b/test/toxiproxy_ex_test.exs @@ -334,8 +334,6 @@ defmodule ToxiproxyExTest do end describe "apply!/2" do - @describetag :focus - test "destroys applied toxics if the passed function raises" do with_tcpserver(fn port -> proxy = ToxiproxyEx.create!(upstream: "localhost:#{port}", name: "test_echo_server") @@ -383,4 +381,38 @@ defmodule ToxiproxyExTest do end) end end + + describe "down!/2" do + test "destroys applied toxics if the passed function raises" do + with_tcpserver(fn port -> + proxy = ToxiproxyEx.create!(upstream: "localhost:#{port}", name: "proxy_which_is_down") + + assert_raise RuntimeError, "boom", fn -> + ToxiproxyEx.down!(proxy, fn -> raise "boom" end) + end + + assert %{"enabled" => true} = ToxiproxyEx.Client.request!(:get, "/proxies/#{proxy.name}") + end) + end + + test "destroys applied toxics if the passed function exits" do + with_tcpserver(fn port -> + proxy = ToxiproxyEx.create!(upstream: "localhost:#{port}", name: "proxy_which_is_down") + + catch_exit(ToxiproxyEx.down!(proxy, fn -> exit(:boom) end)) + + assert %{"enabled" => true} = ToxiproxyEx.Client.request!(:get, "/proxies/#{proxy.name}") + end) + end + + test "destroys applied toxics if the passed function throws" do + with_tcpserver(fn port -> + proxy = ToxiproxyEx.create!(upstream: "localhost:#{port}", name: "proxy_which_is_down") + + catch_throw(ToxiproxyEx.down!(proxy, fn -> throw(:boom) end)) + + assert %{"enabled" => true} = ToxiproxyEx.Client.request!(:get, "/proxies/#{proxy.name}") + end) + end + end end From f77d7371bd0d58c3c6206589fba8c419e331113e Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 17 Oct 2023 19:11:05 +0200 Subject: [PATCH 16/18] Don't include PLTs in the Hex package --- .github/workflows/main.yml | 4 ++-- .gitignore | 3 ++- mix.exs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b3ee38..f6f82a0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -85,7 +85,7 @@ jobs: uses: actions/cache@v3 id: cache-plt with: - path: priv/plts + path: plts key: | plt-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-${{ hashFiles('**/mix.lock') }} restore-keys: | @@ -95,7 +95,7 @@ jobs: - name: Create PLTs if: steps.cache-plt.outputs.cache-hit != 'true' run: | - mkdir -p priv/plts + mkdir -p plts mix dialyzer --plt - name: Install dependencies diff --git a/.gitignore b/.gitignore index 7bb3eb1..097d261 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). toxiproxy_ex-*.tar -/priv/plts +# Dialyzer PLTs. +/plts diff --git a/mix.exs b/mix.exs index 60fe287..943d14a 100644 --- a/mix.exs +++ b/mix.exs @@ -14,8 +14,8 @@ defmodule ToxiproxyEx.MixProject do # Dialyzer dialyzer: [ - plt_local_path: "priv/plts", - plt_core_path: "priv/plts", + plt_local_path: "plts", + plt_core_path: "plts", plt_add_apps: [:ssl, :crypto, :mix, :ex_unit, :erts, :kernel, :stdlib] ], From 730456402b2e4c3e63c483d9fc72dd768eb12dfc Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 30 Oct 2023 09:32:09 +0100 Subject: [PATCH 17/18] Update .github/workflows/main.yml --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f6f82a0..2fca8df 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -91,6 +91,9 @@ jobs: restore-keys: | plt-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}- + - name: Install dependencies + run: mix deps.get + # Create PLTs if no cache was found - name: Create PLTs if: steps.cache-plt.outputs.cache-hit != 'true' @@ -98,9 +101,6 @@ jobs: mkdir -p plts mix dialyzer --plt - - name: Install dependencies - run: mix deps.get - - name: Run Dialyzer run: mix dialyzer --format github From b4036aabdd63793cf74cedf175de1594029e2f56 Mon Sep 17 00:00:00 2001 From: Joel Ambass Date: Thu, 16 Nov 2023 14:36:40 +0100 Subject: [PATCH 18/18] Update lib/toxiproxy_ex/client.ex --- lib/toxiproxy_ex/client.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/toxiproxy_ex/client.ex b/lib/toxiproxy_ex/client.ex index 808dd08..1ac856c 100644 --- a/lib/toxiproxy_ex/client.ex +++ b/lib/toxiproxy_ex/client.ex @@ -4,6 +4,7 @@ defmodule ToxiproxyEx.Client do alias Tesla.Env alias ToxiproxyEx.ServerError + # ToxiproxyEx 3.0 TODO: # We can remove this once Toxiproxy will fix their stuff. See: # https://github.com/Shopify/toxiproxy/pull/538 defmodule BuggyToxiproxyVersionParserMiddleware do