Skip to content

Instantly share code, notes, and snippets.

@marciol
Created September 14, 2021 23:01
Show Gist options
  • Save marciol/8e173fb81517cc2b59c50d8a414386b7 to your computer and use it in GitHub Desktop.
Save marciol/8e173fb81517cc2b59c50d8a414386b7 to your computer and use it in GitHub Desktop.
IExWatchTests Utility
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
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