From 17640326166d94f9b83f5a2a180b69c04cffcd4c Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Mon, 15 Jul 2024 11:57:28 -0400 Subject: [PATCH] wip: non-interrupting watcher working --- lib/mix/tasks/mneme.watch.ex | 11 ++++ lib/mneme.ex | 8 +++ lib/mneme/watch.ex | 117 +++++++++++++++++++++++++++++++++++ mix.exs | 4 +- mix.lock | 1 + test/mneme/example_test.exs | 2 +- 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 lib/mix/tasks/mneme.watch.ex create mode 100644 lib/mneme/watch.ex diff --git a/lib/mix/tasks/mneme.watch.ex b/lib/mix/tasks/mneme.watch.ex new file mode 100644 index 0000000..c5889e5 --- /dev/null +++ b/lib/mix/tasks/mneme.watch.ex @@ -0,0 +1,11 @@ +defmodule Mix.Tasks.Mneme.Watch do + @shortdoc "Re-runs tests on save, interrupting Mneme prompts" + @moduledoc """ + TODO + """ + + use Mix.Task + + @impl Mix.Task + defdelegate run(args), to: Mneme.Watch +end diff --git a/lib/mneme.ex b/lib/mneme.ex index 7a5e80c..c8d06d0 100644 --- a/lib/mneme.ex +++ b/lib/mneme.ex @@ -364,6 +364,8 @@ defmodule Mneme do """ @doc section: :setup def start(opts \\ []) do + maybe_reenable_ansi() + opts = if Keyword.has_key?(opts, :restart) do [ @@ -389,6 +391,12 @@ defmodule Mneme do :ok end + defp maybe_reenable_ansi do + if System.get_env("MIX_MNEME_WATCH") == "true" do + Application.put_env(:elixir, :ansi_enabled, true) + end + end + defp start_server! do children = [ Mneme.Server diff --git a/lib/mneme/watch.ex b/lib/mneme/watch.ex new file mode 100644 index 0000000..b273cfd --- /dev/null +++ b/lib/mneme/watch.ex @@ -0,0 +1,117 @@ +defmodule Mneme.Watch do + @moduledoc false + + use GenServer + + @doc """ + Runs `mix.test` with the given CLI arguments, restarting when files change. + """ + @spec run([String.t()]) :: no_return() + def run(args \\ []) do + Mix.env(:test) + ensure_os!() + + :ok = Application.ensure_started(:file_system) + + children = [ + {__MODULE__, cli_args: args} + ] + + Supervisor.start_link(children, strategy: :one_for_one) + + :timer.sleep(:infinity) + end + + @doc false + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @impl GenServer + def init(opts) do + Code.compiler_options(ignore_module_conflict: true) + + state = opts |> Keyword.validate!([:cli_args]) |> Map.new() + file_system_opts = [dirs: [File.cwd!()], name: :mneme_file_system_watcher] + + case FileSystem.start_link(file_system_opts) do + {:ok, _} -> + FileSystem.subscribe(:mneme_file_system_watcher) + {:ok, state, {:continue, :first_run}} + + other -> + other + end + end + + @impl GenServer + def handle_continue(:first_run, state) do + Mix.Task.run(:test, state.cli_args) + flush() + + {:noreply, state} + end + + @impl GenServer + def handle_info({:file_event, _pid, {path, _events}}, state) do + path = Path.relative_to_cwd(path) + + if watching?(path) do + IO.puts("detected change: #{path}") + run_tests(state.cli_args) + flush() + end + + {:noreply, state} + end + + defp watching?(path) do + watching_directory?(path) and watching_extension?(path) + end + + defp watching_directory?(path) do + ignored = ~w(deps/ _build/ .lexical/ .elixir_ls/ .elixir-tools/) + not String.starts_with?(path, ignored) + end + + defp watching_extension?(path) do + watching = ~w(.erl .ex .exs .eex .leex .heex .xrl .yrl .hrl) + Path.extname(path) in watching + end + + defp run_tests(cli_args) do + Code.unrequire_files(Code.required_files()) + IEx.Helpers.recompile() + Mix.Task.reenable(:test) + Mix.Task.run(:test, cli_args) + end + + defp flush do + receive do + _ -> flush() + after + 0 -> :ok + end + end + + defp ensure_os! do + case os_type() do + :unix -> + :ok + + unsupported -> + error = "file watcher is unsupported on OS: #{inspect(unsupported)}" + + [:red, "error: ", :default_color, error] + |> IO.ANSI.format() + |> then(&IO.puts(:stderr, &1)) + + System.halt(1) + end + end + + defp os_type do + {os_type, _} = :os.type() + os_type + end +end diff --git a/mix.exs b/mix.exs index 8fe181d..0e20efa 100644 --- a/mix.exs +++ b/mix.exs @@ -41,6 +41,7 @@ defmodule Mneme.MixProject do {:nimble_options, "~> 1.0"}, {:sourceror, "~> 1.0"}, {:rewrite, "~> 0.10.1"}, + {:file_system, "~> 1.0"}, # Development / Test {:benchee, "~> 1.0", only: :dev}, @@ -83,7 +84,8 @@ defmodule Mneme.MixProject do dialyzer: :test, coveralls: :test, "coveralls.html": :test, - "test.mneme_not_started": :test + "test.mneme_not_started": :test, + "mneme.watch": :test ] end diff --git a/mix.lock b/mix.lock index ca0db26..1391930 100644 --- a/mix.lock +++ b/mix.lock @@ -8,6 +8,7 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [: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", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "glob_ex": {:hex, :glob_ex, "0.1.7", "eae6b6377147fb712ac45b360e6dbba00346689a87f996672fe07e97d70597b1", [:mix], [], "hexpm", "decc1c21c0c73df3c9c994412716345c1692477b9470e337f628a7e08da0da6a"}, "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, diff --git a/test/mneme/example_test.exs b/test/mneme/example_test.exs index ec3dc37..8c7f01f 100644 --- a/test/mneme/example_test.exs +++ b/test/mneme/example_test.exs @@ -24,7 +24,7 @@ defmodule Mneme.ExampleTest do end test "2" do - s2 = %MyStruct{field: 5} + s2 = %MyStruct{field: 1} auto_assert %MyStruct{field: 5, list: [:foo, :buzz]} <- %{