From 5e8ac6282a22dc0408b000227cf1e7d461fbeaa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarl=20Andr=C3=A9=20H=C3=BCbenthal?= Date: Mon, 30 Sep 2024 10:56:28 +0200 Subject: [PATCH] fix reuse not working and improve mix task with watch feature (#128) --- .../protocols/container_builder_helper.ex | 7 +- lib/mix/tasks/testcontainers/test.ex | 110 +++++++++++++++--- lib/testcontainers.ex | 1 - lib/util/constants.ex | 2 +- mix.exs | 8 +- mix.lock | 2 + .../container_builder_helper_test.exs | 2 +- 7 files changed, 103 insertions(+), 29 deletions(-) diff --git a/lib/container/protocols/container_builder_helper.ex b/lib/container/protocols/container_builder_helper.ex index c206239..e032937 100644 --- a/lib/container/protocols/container_builder_helper.ex +++ b/lib/container/protocols/container_builder_helper.ex @@ -7,23 +7,22 @@ defmodule Testcontainers.ContainerBuilderHelper do def build(builder, state) when is_map(state) and is_struct(builder) do config = ContainerBuilder.build(builder) - |> Container.with_label(container_version_label(), library_version()) |> Container.with_label(container_lang_label(), container_lang_value()) |> Container.with_label(container_label(), "#{true}") - reuse = config.reuse && true == Map.get(state.properties, "testcontainers.reuse.enable", false) - - if reuse do + if config.reuse && "true" == Map.get(state.properties, "testcontainers.reuse.enable", "false") do hash = Hash.struct_to_hash(config) config |> Container.with_label(container_reuse(), "true") |> Container.with_label(container_reuse_hash_label(), hash) |> Container.with_label(container_sessionId_label(), state.session_id) + |> Container.with_label(container_version_label(), library_version()) |> Kernel.then(&{:reuse, &1, hash}) else config |> Container.with_label(container_reuse(), "false") |> Container.with_label(container_sessionId_label(), state.session_id) + |> Container.with_label(container_version_label(), library_version()) |> Kernel.then(&{:noreuse, &1, nil}) end end diff --git a/lib/mix/tasks/testcontainers/test.ex b/lib/mix/tasks/testcontainers/test.ex index 1f83428..e946094 100644 --- a/lib/mix/tasks/testcontainers/test.ex +++ b/lib/mix/tasks/testcontainers/test.ex @@ -5,17 +5,61 @@ defmodule Mix.Tasks.Testcontainers.Test do @shortdoc "Runs tests with a Postgres container" def run(args) do - Application.ensure_all_started(:tesla) - Application.ensure_all_started(:hackney) + Enum.each([:tesla, :hackney, :fs, :logger], fn app -> + {:ok, _} = Application.ensure_all_started(app) + end) + {:ok, _} = Testcontainers.start_link() - {opts, _, _} = OptionParser.parse(args, switches: [ - database: :string - ]) + {opts, _, _} = OptionParser.parse(args, + switches: [ + database: :string, + watch: [:string, :keep] + ] + ) database = opts[:database] || "postgres" + folder_to_watch = Keyword.get_values(opts, :watch) + + if Enum.empty?(folder_to_watch) do + IO.puts("No folders specified. Only running tests.") + run_tests_and_exit(database) + else + check_folders_exist(folder_to_watch) + run_tests_and_watch(database, folder_to_watch) + end + end + + defp check_folders_exist(folders) do + Enum.each(folders, fn folder -> + unless File.dir?(folder) do + Mix.raise("Folder does not exist: #{folder}") + end + end) + end + + defp run_tests_and_exit(database) do + {container, env} = setup_container(database) + run_tests(env) + Testcontainers.stop_container(container.container_id) + end + + defp run_tests_and_watch(database, folders) do + {container, env} = setup_container(database) + + Enum.each(folders, fn folder -> + :fs.start_link(String.to_atom("watcher_" <> folder), Path.absname(folder)) + :fs.subscribe(String.to_atom("watcher_" <> folder)) + end) + + Process.flag(:trap_exit, true) + + run_tests(env) + loop(env, container) + end - {container, port} = case database do + defp setup_container(database) do + case database do "postgres" -> {:ok, container} = Testcontainers.start_container( PostgresContainer.new() @@ -24,7 +68,7 @@ defmodule Mix.Tasks.Testcontainers.Test do |> PostgresContainer.with_reuse(true) ) port = PostgresContainer.port(container) - {container, port} + {container, create_env(port)} "mysql" -> {:ok, container} = Testcontainers.start_container( MySqlContainer.new() @@ -33,26 +77,54 @@ defmodule Mix.Tasks.Testcontainers.Test do |> MySqlContainer.with_reuse(true) ) port = MySqlContainer.port(container) - {container, port} + {container, create_env(port)} _ -> Mix.raise("Unsupported database: #{database}") end + end - env = [ + defp create_env(port) do + [ {"DB_USER", "test"}, {"DB_PASSWORD", "test"}, {"DB_HOST", Testcontainers.get_host()}, - {"DB_PORT", port |> Integer.to_string()} + {"DB_PORT", Integer.to_string(port)} ] + end - try do - {output, exit_code} = System.cmd("mix", ["test"], env: env) - if exit_code != 0 do - IO.puts(output) - raise "\u274c Tests failed" - end - IO.puts("\u2705 Tests passed") - after - Testcontainers.stop_container(container.container_id) + defp run_tests(env) do + test_pid = spawn(fn -> + System.cmd("mix", ["test"], env: env, into: IO.stream(:stdio, :line)) + end) + + Process.monitor(test_pid) + + receive do + {:DOWN, _ref, :process, ^test_pid, _reason} -> + IO.puts("Test process completed.") + end + end + + defp loop(env, container) do + receive do + {_watcher_process, {:fs, :file_event}, {changed_file, _type}} -> + IO.puts("#{changed_file} was updated, waiting for more changes...") + wait_for_changes(env, container) + + after 5000 -> + loop(env, container) + end + end + + defp wait_for_changes(env, container) do + receive do + {_watcher_process, {:fs, :file_event}, {changed_file, _type}} -> + IO.puts("#{changed_file} was updated, waiting for more changes...") + wait_for_changes(env, container) + + after 1000 -> + IO.ANSI.clear() + run_tests(env) + loop(env, container) end end end diff --git a/lib/testcontainers.ex b/lib/testcontainers.ex index 6bb625d..203ce01 100644 --- a/lib/testcontainers.ex +++ b/lib/testcontainers.ex @@ -13,7 +13,6 @@ defmodule Testcontainers do alias Testcontainers.Connection alias Testcontainers.Container alias Testcontainers.ContainerBuilder - alias Testcontainers.Util.Hash alias Testcontainers.Util.PropertiesParser import Testcontainers.Constants diff --git a/lib/util/constants.ex b/lib/util/constants.ex index 43fcc36..5112ff9 100644 --- a/lib/util/constants.ex +++ b/lib/util/constants.ex @@ -2,7 +2,7 @@ defmodule Testcontainers.Constants do @moduledoc false def library_name, do: :testcontainers - def library_version, do: "1.10.3" + def library_version, do: "1.10.4" def container_label, do: "org.testcontainers" def container_lang_label, do: "org.testcontainers.lang" def container_reuse_hash_label, do: "org.testcontainers.reuse-hash" diff --git a/mix.exs b/mix.exs index 9fdd383..dc13ea5 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule TestcontainersElixir.MixProject do use Mix.Project @app :testcontainers - @version "1.10.3" + @version "1.10.4" @source_url "https://github.com/testcontainers/testcontainers-elixir" def project do @@ -22,7 +22,7 @@ defmodule TestcontainersElixir.MixProject do licenses: ["MIT"] ], test_coverage: [ - summary: [threshold: 68], + summary: [threshold: 60], ignore_modules: [ TestHelper, Inspect.Testcontainers.TestUser, @@ -86,7 +86,9 @@ defmodule TestcontainersElixir.MixProject do # RabbitMQ {:amqp, "~> 3.3", only: [:dev, :test]}, # EMQX - {:tortoise311, "~> 0.12.0", only: [:dev, :test]} + {:tortoise311, "~> 0.12.0", only: [:dev, :test]}, + # For watching directories for file changes in mix task + {:fs, "~> 8.6"} ] end diff --git a/mix.lock b/mix.lock index 4a3b3a5..891f47c 100644 --- a/mix.lock +++ b/mix.lock @@ -20,6 +20,7 @@ "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "ex_docker_engine_api": {:hex, :ex_docker_engine_api, "1.43.1", "1161e34b6bea5cef84d8fdc1d5d510fcb0c463941ce84c36f4a0f44a9096eb96", [:mix], [{:hackney, "~> 1.20", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.7", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "ec8fc499389aeef56ddca67e89e9e98098cff50587b56e8b4613279f382793b1"}, "exmqtt": {:git, "https://github.com/ryanwinchester/exmqtt.git", "b6da7d412b1e7fe8c78f5e8de1895c990e34ff67", [branch: "master"]}, + "fs": {:hex, :fs, "8.6.1", "7c9c0d0211e8c520e4e9eda63b960605c2711839f47285e6166c332d973be8ea", [:rebar3], [], "hexpm", "61ea2bdaedae4e2024d0d25c63e44dccf65622d4402db4a2df12868d1546503f"}, "gen_state_machine": {:hex, :gen_state_machine, "3.0.0", "1e57f86a494e5c6b14137ebef26a7eb342b3b0070c7135f2d6768ed3f6b6cdff", [:mix], [], "hexpm", "0a59652574bebceb7309f6b749d2a41b45fdeda8dbb4da0791e355dd19f0ed15"}, "getopt": {:hex, :getopt, "1.0.2", "33d9b44289fe7ad08627ddfe1d798e30b2da0033b51da1b3a2d64e72cd581d02", [:rebar3], [], "hexpm", "a0029aea4322fb82a61f6876a6d9c66dc9878b6cb61faa13df3187384fd4ea26"}, "gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"}, @@ -34,6 +35,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "mix_rebar3": {:hex, :mix_rebar3, "0.2.0", "b33656ef3047f21a19fac3254cb30a1d2c75ea419a3ad28c4b88f42c62a4202d", [:mix], [], "hexpm", "11eabb70c0a7ead9aa3631f048c3d7d5e868172b87b6493d0dc6f6d591c1afae"}, "myxql": {:hex, :myxql, "0.6.4", "1502ea37ee23c31b79725b95d4cc3553693c2bda7421b1febc50722fd988c918", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a3307f4671f3009d3708283649adf205bfe280f7e036fc8ef7f16dbf821ab8e9"}, "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, diff --git a/test/container/container_builder_helper_test.exs b/test/container/container_builder_helper_test.exs index fe01749..8b36661 100644 --- a/test/container/container_builder_helper_test.exs +++ b/test/container/container_builder_helper_test.exs @@ -18,7 +18,7 @@ defmodule Testcontainers.ContainerBuilderHelperTest do test "build/2 returns a tuple with true, built config with correct labels and a non nil hash" do builder = Testcontainers.PostgresContainer.new() |> Testcontainers.PostgresContainer.with_reuse(true) - state = %{ properties: %{ "testcontainers.reuse.enable" => true }, session_id: "123" } + state = %{ properties: %{ "testcontainers.reuse.enable" => "true" }, session_id: "123" } {:reuse, built, hash} = ContainerBuilderHelper.build(builder, state) assert hash != nil assert Map.get(built.labels, container_reuse()) == "true"