Created
September 14, 2021 23:01
-
-
Save marciol/8e173fb81517cc2b59c50d8a414386b7 to your computer and use it in GitHub Desktop.
IExWatchTests Utility
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Code.compiler_options(ignore_module_conflict: true) | |
Code.compile_file("~/.iex/iex_watch_tests.exs", File.cwd!()) | |
unless GenServer.whereis(IExWatchTests) do | |
{:ok, pid} = IExWatchTests.start_link() | |
# Process will not exit when the iex goes out | |
Process.unlink(pid) | |
end | |
import IExWatchTests.Helpers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule IExWatchTests do | |
use GenServer | |
@moduledoc """ | |
This utility allows to get the same effect of using the | |
`fcwatch | mix test --stale --listen-on-stdin`, but with | |
more controll and without the starting time penalty. | |
The best way to use it is to place it on a directory under | |
the `~/.iex/` directory and in the `~/.iex` file itself add: | |
``` | |
Code.compiler_options(ignore_module_conflict: true) | |
Code.compile_file("~/.iex/iex_watch_tests.exs", File.cwd!()) | |
unless GenServer.whereis(IExWatchTests) do | |
{:ok, pid} = IExWatchTests.start_link() | |
# Process will not exit when the iex goes out | |
Process.unlink(pid) | |
end | |
import IExWatchTests.Helpers | |
``` | |
To call `iex` just do: | |
``` | |
ELIXIR_ERL_OPTIONS="-pa $HOME/.iex" MIX_ENV=test iex -S mix | |
``` | |
The `IExWatchTests.Helpers` allows to call `f` and `s` and `a` | |
to run failed, stale and all tests respectively. | |
You can call `w` to watch tests and `uw` to unwatch. | |
Theres is a really simple throttle mecanism that disallow run the suite concurrently. | |
""" | |
defmodule Helpers do | |
defdelegate a, to: IExWatchTests, as: :run_all_tests | |
defdelegate f, to: IExWatchTests, as: :run_failed_tests | |
defdelegate s, to: IExWatchTests, as: :run_stale_tests | |
defdelegate w, to: IExWatchTests, as: :watch_tests | |
defdelegate uw, to: IExWatchTests, as: :unwatch_tests | |
end | |
defmodule Observer do | |
use GenServer | |
@impl true | |
def init(opts) do | |
{:ok, opts} | |
end | |
@impl true | |
def handle_cast({:suite_finished, _times_us}, config) do | |
IExWatchTests.unlock() | |
{:noreply, config} | |
end | |
@impl true | |
def handle_cast(_, config) do | |
{:noreply, config} | |
end | |
end | |
def start_link do | |
GenServer.start_link(__MODULE__, %{watcher: nil, lock: false}, name: __MODULE__) | |
end | |
def watch_tests do | |
GenServer.cast(__MODULE__, :watch_tests) | |
end | |
def unwatch_tests do | |
GenServer.cast(__MODULE__, :unwatch_tests) | |
end | |
def run_all_tests do | |
GenServer.call(__MODULE__, {:run, :all}, :infinity) | |
end | |
def run_failed_tests do | |
GenServer.call(__MODULE__, {:run, :failed}, :infinity) | |
end | |
def run_stale_tests do | |
GenServer.call(__MODULE__, {:run, :stale}, :infinity) | |
end | |
def unlock do | |
GenServer.cast(__MODULE__, :unlock) | |
end | |
@impl true | |
def init(state) do | |
Process.flag(:trap_exit, true) | |
ExUnit.start(autorun: false, formatters: [ExUnit.CLIFormatter, IExWatchTests.Observer]) | |
{:ok, state} | |
end | |
@impl true | |
def handle_cast(:watch_tests, state) do | |
{:ok, pid} = | |
Task.start(fn -> | |
cmd = "fswatch lib test" | |
port = Port.open({:spawn, cmd}, [:binary, :exit_status]) | |
watch_loop(port) | |
end) | |
{:noreply, %{state | watcher: pid}} | |
end | |
@impl true | |
def handle_cast(:unwatch_tests, %{watcher: pid} = state) do | |
if is_nil(pid) or not Process.alive?(pid) do | |
IO.puts("Watcher not running!") | |
else | |
Process.exit(pid, :kill) | |
end | |
{:noreply, %{state | watcher: nil}} | |
end | |
@impl true | |
def handle_cast({:run, mode}, %{lock: false} = state) do | |
do_run_tests(mode) | |
{:noreply, %{state | lock: true}} | |
end | |
@impl true | |
def handle_cast({:run, _mode}, %{lock: true} = state) do | |
{:noreply, state} | |
end | |
@impl true | |
def handle_cast(:unlock, state) do | |
{:noreply, %{state | lock: false}} | |
end | |
@impl true | |
def handle_call({:run, _mode}, _from, %{lock: true} = state) do | |
{:reply, :locked, state} | |
end | |
@impl true | |
def handle_call({:run, mode}, _from, %{lock: false} = state) do | |
do_run_tests(mode) | |
{:reply, :ok, %{state | lock: true}} | |
end | |
@impl true | |
def handle_info(_msg, state) do | |
{:noreply, state} | |
end | |
defp watch_loop(port) do | |
receive do | |
{^port, {:data, _msg}} -> | |
GenServer.cast(__MODULE__, {:run, :stale}) | |
watch_loop(port) | |
end | |
end | |
defp do_run_tests(mode) do | |
IEx.Helpers.recompile() | |
# Reset config | |
ExUnit.configure( | |
exclude: [], | |
include: [], | |
only_test_ids: nil | |
) | |
Code.required_files() | |
|> Enum.filter(&String.ends_with?(&1, "_test.exs")) | |
|> Code.unrequire_files() | |
args = | |
case mode do | |
:all -> | |
[] | |
:failed -> | |
["--failed"] | |
:stale -> | |
["--stale"] | |
end | |
result = | |
ExUnit.CaptureIO.capture_io(fn -> | |
Mix.Tasks.Test.run(args) | |
end) | |
if result =~ ~r/No stale tests/ or | |
result =~ ~r/There are no tests to run/ do | |
IExWatchTests.unlock() | |
end | |
IO.puts(result) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment