Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
name: Checks/Tests on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- elixir: '1.14'
Expand All @@ -29,6 +30,11 @@ jobs:
otp: '27'
- elixir: '1.18'
otp: '27'
- elixir: '1.19'
otp: '28'
- elixir: main
experimental: true
otp: '28'
steps:
- name: Set up Elixir
id: setup
Expand Down Expand Up @@ -80,7 +86,7 @@ jobs:
run: mix compile --warnings-as-errors

- name: Check formatting
if: ${{matrix.elixir == '1.18'}}
if: ${{matrix.elixir == '1.19'}}
run: mix format --check-formatted

- name: Run tests
Expand Down
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
erlang 27.2
elixir 1.18.1-otp-27
erlang 28.0
elixir 1.19-otp-28
19 changes: 14 additions & 5 deletions lib/mix_test_interactive/command_line_formatter.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
defmodule MixTestInteractive.CommandLineFormatter do
@moduledoc false

@special_chars ~r/[\s&|;<>*?()\[\]{}$`'"]/
@whitespace ~r/\s/

def call(command, args) do
Enum.map_join([command | args], " ", &format_argument/1)
end

defmacrop special_chars do
quote do
~r/[\s&|;<>*?()\[\]{}$`'"]/
end
end

defmacrop whitespace do
quote do
~r/\s/
end
end

defp format_argument(arg) do
if arg =~ @special_chars do
if arg =~ special_chars() do
quote_argument(arg)
else
arg
Expand All @@ -19,7 +28,7 @@ defmodule MixTestInteractive.CommandLineFormatter do
defp quote_argument(arg) do
cond do
# Prefer double quotes for arguments with only spaces or special characters
String.match?(arg, @whitespace) and not String.contains?(arg, ~s(")) ->
String.match?(arg, whitespace()) and not String.contains?(arg, ~s(")) ->
~s("#{arg}")

# Use single quotes if the argument contains double quotes but no single quotes
Expand Down
34 changes: 23 additions & 11 deletions lib/mix_test_interactive/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule MixTestInteractive.Config do
field :ansi_enabled?, boolean()
field :clear?, boolean(), default: false
field :command, {String.t(), [String.t()]}, default: {"mix", []}
field :exclude, [Regex.t()], default: [~r/\.#/, ~r{priv/repo/migrations}]
field :exclude, [Regex.t()]
field :extra_extensions, [String.t()], default: []
field :runner, module(), default: MixTestInteractive.PortRunner
field :show_timestamp?, boolean(), default: false
Expand All @@ -36,12 +36,32 @@ defmodule MixTestInteractive.Config do
end

@doc false
def new do
def new(overrides \\ []) do
os_type = ProcessTree.get(:os_type, default: :os.type())

default_ansi_enabled(%__MODULE__{}, os_type)
defaults = [ansi_enabled?: not match?({:win32, _os_name}, os_type), exclude: [~r/\.#/, ~r{priv/repo/migrations}]]
attrs = Keyword.merge(defaults, overrides)

struct!(%__MODULE__{}, attrs)
end

@doc false
def equal?(%__MODULE__{} = left, %__MODULE__{} = right) do
%{left | exclude: nil} == %{right | exclude: nil} and exclude_matches?(left, right.exclude)
end

@doc false
def exclude_contains?(%__MODULE__{} = config, %Regex{} = regex) do
regex.source in sources(config.exclude)
end

@doc false
def exclude_matches?(%__MODULE__{} = config, exclude) do
sources(config.exclude) == sources(exclude)
end

defp sources(exclude), do: Enum.map(exclude, &Regex.source/1)

defp load(%__MODULE__{} = config, app_key, opts \\ []) do
config_key = Keyword.get(opts, :rename, app_key)
transform = Keyword.get(opts, :transform, & &1)
Expand All @@ -59,14 +79,6 @@ defmodule MixTestInteractive.Config do
end
end

defp default_ansi_enabled(%__MODULE__{} = config, {:win32, _os_name} = _os_type) do
%{config | ansi_enabled?: false}
end

defp default_ansi_enabled(%__MODULE__{} = config, _os_type) do
%{config | ansi_enabled?: true}
end

defp parse_command({cmd, args} = command) when is_binary(cmd) and is_list(args), do: command
defp parse_command(command) when is_binary(command), do: {command, []}

Expand Down
17 changes: 13 additions & 4 deletions lib/mix_test_interactive/pattern_filter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@ defmodule MixTestInteractive.PatternFilter do
|> Kernel.++(with_line_number)
end

defp is_line_number_pattern?(pattern) do
case ExUnit.Filters.parse_path(pattern) do
{_path, []} -> false
_ -> true
if Version.compare(System.version(), "1.20.0-dev") == :lt do
defp is_line_number_pattern?(pattern) do
case ExUnit.Filters.parse_path(pattern) do
{_path, []} -> false
_ -> true
end
end
else
defp is_line_number_pattern?(pattern) do
case ExUnit.Filters.parse_paths([pattern]) do
{_path, []} -> false
_ -> true
end
end
end
end
2 changes: 1 addition & 1 deletion lib/mix_test_interactive/port_runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ defmodule MixTestInteractive.PortRunner do
defp enable_ansi(task) do
enable_command = "Application.put_env(:elixir, :ansi_enabled, true)"

["do", "eval", enable_command, ",", task]
["do", "eval", enable_command, "+", task]
end

defp maybe_print_command(%Config{verbose?: false} = _config, _runner_program, _runner_program_args), do: :ok
Expand Down
6 changes: 3 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule MixTestInteractive.MixProject do
deps: deps(),
description: description(),
docs: docs(),
elixir: "~> 1.13",
elixir: "~> 1.14",
name: "mix test.interactive",
package: package(),
source_url: @source_url,
Expand All @@ -32,10 +32,10 @@ defmodule MixTestInteractive.MixProject do

defp deps do
[
{:ex_doc, "~> 0.35.1", only: :dev, runtime: false},
{:ex_doc, "~> 0.38.2", only: :dev, runtime: false},
{:file_system, "~> 0.2 or ~> 1.0"},
{:process_tree, "~> 0.1.3 or ~> 0.2.0"},
{:styler, "~> 1.2", only: [:dev, :test], runtime: false},
{:styler, "~> 1.4", only: [:dev, :test], runtime: false},
{:typed_struct, "~> 0.3.0"}
]
end
Expand Down
10 changes: 5 additions & 5 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [: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", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"process_tree": {:hex, :process_tree, "0.2.0", "a934111ff00c1b696731edf35ecc6a4724105e045efa95a1977cac21ebad62cd", [:mix], [], "hexpm", "92901d3e9d2f40f4e09c7774a97ed0d1fac5d3541c62552df8346dfa0f9ee19b"},
"styler": {:hex, :styler, "1.2.1", "28f9e3d4b065c22575c56b8ae03d05188add1b21bec5ae664fc1551e2dfcc41b", [:mix], [], "hexpm", "71dc33980e530d21ca54db9c2075e646faa6e7b744a9d4a3dfb0ff01f56595f0"},
"styler": {:hex, :styler, "1.4.2", "420da8a9d10324625b75690ca9f2468bc00ee6eb78dead827e562368f9feabbb", [:mix], [], "hexpm", "ca22538b203b2424eef99a227e081143b9a9a4b26da75f26d920537fcd778832"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
}
10 changes: 5 additions & 5 deletions test/mix_test_interactive/command_line_parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defmodule MixTestInteractive.CommandLineParserTest do
describe "mix test.interactive options" do
test "retains original defaults when no options" do
{:ok, %{config: config}} = CommandLineParser.parse([])
assert config == Config.new()
assert Config.equal?(config, Config.new())
end

test "sets ansi_enabled? flag with --ansi-enabled" do
Expand Down Expand Up @@ -106,17 +106,17 @@ defmodule MixTestInteractive.CommandLineParserTest do

test "ignores custom command arguments if command is not specified" do
{:ok, %{config: config}} = CommandLineParser.parse(["--arg", "arg_with_missing_command"])
assert config.command == %Config{}.command
assert config.command == Config.new().command
end

test "configures watch exclusions with --exclude" do
{:ok, %{config: config}} = CommandLineParser.parse(["--exclude", "~$"])
assert config.exclude == [~r/~$/]
assert Config.exclude_matches?(config, [~r/~$/])
end

test "configures multiple watch exclusions with repeated --exclude options" do
{:ok, %{config: config}} = CommandLineParser.parse(["--exclude", "~$", "--exclude", "\.secret\.exs"])
assert config.exclude == [~r/~$/, ~r/.secret.exs/]
assert Config.exclude_matches?(config, [~r/~$/, ~r/.secret.exs/])
end

test "fails if watch exclusion is an invalid Regex" do
Expand Down Expand Up @@ -342,7 +342,7 @@ defmodule MixTestInteractive.CommandLineParserTest do
{:ok, %{config: config, settings: settings}} =
CommandLineParser.parse(["--exclude", "~$", "--", "--exclude", "integration"])

assert config.exclude == [~r/~$/]
assert Config.exclude_matches?(config, [~r/~$/])
assert settings.excludes == ["integration"]
end

Expand Down
6 changes: 3 additions & 3 deletions test/mix_test_interactive/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,18 @@ defmodule MixTestInteractive.ConfigTest do
test "takes :exclude from the env" do
Process.put(:exclude, [~r/migration_.*/])
config = Config.load_from_environment()
assert config.exclude == [~r/migration_.*/]
assert Config.exclude_matches?(config, [~r/migration_.*/])
end

test ":exclude contains common editor temp/swap files by default" do
config = Config.load_from_environment()
# Emacs lock symlink
assert ~r/\.#/ in config.exclude
assert Config.exclude_contains?(config, ~r/\.#/)
end

test "excludes default Phoenix migrations directory by default" do
config = Config.load_from_environment()
assert ~r{priv/repo/migrations} in config.exclude
assert Config.exclude_contains?(config, ~r{priv/repo/migrations})
end

test "takes :extra_extensions from the env" do
Expand Down
10 changes: 5 additions & 5 deletions test/mix_test_interactive/end_to_end_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ defmodule MixTestInteractive.EndToEndTest do
end
end

@config %Config{runner: DummyRunner}
@settings %Settings{}

setup do
{:ok, io} = StringIO.open("")
Process.group_leader(self(), io)

config = Config.new(runner: DummyRunner)
settings = %Settings{}

_pid = start_supervised!({DummyRunner, self()})
pid = start_supervised!({InteractiveMode, config: @config, name: :end_to_end, settings: @settings})
pid = start_supervised!({InteractiveMode, config: config, name: :end_to_end, settings: settings})

%{pid: pid}
end
Expand Down Expand Up @@ -160,7 +160,7 @@ defmodule MixTestInteractive.EndToEndTest do
end

defp assert_ran_tests(args \\ []) do
assert_receive {@config, ^args}, 100
assert_receive {%Config{}, ^args}, 100
end

defp refute_ran_tests do
Expand Down
9 changes: 5 additions & 4 deletions test/mix_test_interactive/paths_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule MixTestInteractive.PathsTest do
end

test "extra extensions are watched" do
config = %Config{extra_extensions: [".ex", ".haml", ".foo", ".txt"]}
config = Config.new(extra_extensions: [".ex", ".haml", ".foo", ".txt"])
assert watching?("foo.ex", config)
assert watching?("index.html.haml", config)
assert watching?("my.foo", config)
Expand Down Expand Up @@ -71,14 +71,15 @@ defmodule MixTestInteractive.PathsTest do
end

test "migrations_.* files should be excluded watched" do
refute watching?("migrations_files/foo.exs", %Config{exclude: [~r/migrations_.*/]})
refute watching?("migrations_files/foo.exs", Config.new(exclude: [~r/migrations_.*/]))
end

test "app.ex is not excluded by migrations_.* pattern" do
assert watching?("app.ex", %Config{exclude: [~r/migrations_.*/]})
assert watching?("app.ex", Config.new(exclude: [~r/migrations_.*/]))
end

defp watching?(path, config \\ %Config{}) do
defp watching?(path, config \\ nil) do
config = config || Config.new()
Paths.watching?(path, config)
end
end
20 changes: 11 additions & 9 deletions test/mix_test_interactive/port_runner_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ defmodule MixTestInteractive.PortRunnerTest do
@unix {:unix, :darwin}
@windows {:win32, :nt}

defp config(overrides \\ %{}) do
Map.merge(%Config{ansi_enabled?: false}, overrides)
defp config(overrides \\ []) do
[ansi_enabled?: false]
|> Keyword.merge(overrides)
|> Config.new()
end

defp run(options \\ []) do
Expand Down Expand Up @@ -73,8 +75,8 @@ defmodule MixTestInteractive.PortRunnerTest do
end

test "enables ansi output when turned on" do
config = config(%{ansi_enabled?: true})
{"mix", ["do", "eval", ansi, ",", "test"], _options} = run(config: config)
config = config(ansi_enabled?: true)
{"mix", ["do", "eval", ansi, "+", "test"], _options} = run(config: config)

assert ansi =~ ~r/:ansi_enabled/
end
Expand All @@ -88,22 +90,22 @@ defmodule MixTestInteractive.PortRunnerTest do
end

test "uses custom task" do
config = config(%{task: "custom_task"})
config = config(task: "custom_task")
assert {_command, ["custom_task"], _options} = run(config: config)
end

test "uses custom command with no args" do
config = config(%{command: {"custom_command", []}})
config = config(command: {"custom_command", []})
assert {"custom_command", _args, _options} = run(config: config)
end

test "uses custom command with args" do
config = config(%{command: {"custom_command", ["--custom_arg"]}})
config = config(command: {"custom_command", ["--custom_arg"]})
assert {"custom_command", ["--custom_arg", "test"], _options} = run(config: config)
end

test "prepends command args to test args" do
config = config(%{command: {"custom_command", ["--custom_arg"]}})
config = config(command: {"custom_command", ["--custom_arg"]})

assert {"custom_command", ["--custom_arg", "test", "--cover"], _options} =
run(args: ["--cover"], config: config)
Expand All @@ -117,7 +119,7 @@ defmodule MixTestInteractive.PortRunnerTest do
end

test "displays command in verbose mode" do
config = config(%{verbose?: true})
config = config(verbose?: true)

{result, output} = with_io(fn -> run(config: config) end)

Expand Down
4 changes: 2 additions & 2 deletions test/mix_test_interactive/runner_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ defmodule MixTestInteractive.RunnerTest do

describe "run/1" do
test "It delegates to the runner specified by the config" do
config = %Config{runner: DummyRunner}
config = Config.new(runner: DummyRunner)
args = ["--cover", "--raise"]

output =
Expand All @@ -45,7 +45,7 @@ defmodule MixTestInteractive.RunnerTest do
end

test "It outputs timestamp when specified by the config" do
config = %Config{runner: DummyRunner, show_timestamp?: true}
config = Config.new(runner: DummyRunner, show_timestamp?: true)

output =
capture_io(fn ->
Expand Down