Skip to content

Commit 98e04aa

Browse files
authored
Add verbose option/config for printing the test command (#135)
The new option (off by default) allows you to see the command that will be run just before running the tests. Might be useful to understand what's happening under the hood when diagnosing strange behavior.
1 parent 6a14b8a commit 98e04aa

File tree

10 files changed

+157
-4
lines changed

10 files changed

+157
-4
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ specified in the configuration.
104104
- `--task <task name>`: Run a different mix task (default: `"test"`).
105105
- `--(no-)timestamp`: Display the current time before running the tests
106106
(default: `false`).
107+
- `--(no-)verbose`: Display the command to be run before running the tests
108+
(default: `false`).
107109
- `--(no-)watch`: Don't run tests when a file changes (default: `true`).
108110

109111
All of the `<mix test arguments>` are passed through to `mix test` on every test
@@ -343,6 +345,21 @@ if Mix.env == :dev do
343345
end
344346
```
345347

348+
### `verbose`: Display the command to be run before running the tests
349+
350+
When `verbose` is set to true, `mix test.interactive` will display the command
351+
line it is about to execute just before running the tests.
352+
353+
```elixir
354+
# config/config.exs
355+
import Config
356+
357+
if Mix.env == :dev do
358+
config :mix_test_interactive,
359+
verbose: true
360+
end
361+
```
362+
346363
## Compatibility Notes
347364

348365
On Linux you may need to install `inotify-tools`.

lib/mix/tasks/test/interactive.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ defmodule Mix.Tasks.Test.Interactive do
4848
- `--task <task name>`: Run a different mix task (default: `"test"`).
4949
- `--(no-)timestamp`: Display the current time before running the tests
5050
(default: `false`).
51+
- `--(no-)verbose`: Display the command to be run before running the tests
52+
(default: `false`).
5153
- `--(no-)watch`: Don't run tests when a file changes (default: `true`).
5254
5355
All of the `<mix test arguments>` are passed through to `mix test` on every
@@ -129,8 +131,10 @@ defmodule Mix.Tasks.Test.Interactive do
129131
`MixTestInteractive.PortRunner`).
130132
- `task: <task name>`: The mix task to use when running tests (default:
131133
`"test"`).
132-
- `timestamp: true`: Print current time (UTC) before running tests (default:
133-
false).
134+
- `timestamp: true`: Display the current time (UTC) before running the tests
135+
(default: false).
136+
- `verbose: true`: Display the command to be run before running the tests
137+
(default: `false`)
134138
"""
135139

136140
use Mix.Task
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
defmodule MixTestInteractive.CommandLineFormatter do
2+
@moduledoc false
3+
4+
@special_chars ~r/[\s&|;<>*?()\[\]{}$`'"]/
5+
@whitespace ~r/\s/
6+
7+
def call(command, args) do
8+
Enum.map_join([command | args], " ", &format_argument/1)
9+
end
10+
11+
defp format_argument(arg) do
12+
if arg =~ @special_chars do
13+
quote_argument(arg)
14+
else
15+
arg
16+
end
17+
end
18+
19+
defp quote_argument(arg) do
20+
cond do
21+
# Prefer double quotes for arguments with only spaces or special characters
22+
String.match?(arg, @whitespace) and not String.contains?(arg, ~s(")) ->
23+
~s("#{arg}")
24+
25+
# Use single quotes if the argument contains double quotes but no single quotes
26+
String.contains?(arg, ~s(")) and not String.contains?(arg, "'") ->
27+
~s('#{arg}')
28+
29+
# Escape single quotes using the '"'"' trick if both quotes are present
30+
String.contains?(arg, "'") ->
31+
~s('#{String.replace(arg, "'", "'\\''")}')
32+
33+
# Default to double quotes for other special characters
34+
true ->
35+
~s("#{arg}")
36+
end
37+
end
38+
end

lib/mix_test_interactive/command_line_parser.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ defmodule MixTestInteractive.CommandLineParser do
3333
runner: :string,
3434
task: :string,
3535
timestamp: :boolean,
36+
verbose: :boolean,
3637
version: :boolean,
3738
watch: :boolean
3839
]
@@ -70,6 +71,8 @@ defmodule MixTestInteractive.CommandLineParser do
7071
(default: `"test"`)
7172
--(no-)timestamp Display the current time before running
7273
the tests (default: `false`)
74+
--(no-)verbose Display the command to be run before
75+
running the tests (default: `false`)
7376
--(no-)watch Run tests when a watched file changes
7477
(default: `true`)
7578
@@ -152,6 +155,7 @@ defmodule MixTestInteractive.CommandLineParser do
152155
{:runner, runner}, config -> %{config | runner: runner}
153156
{:timestamp, show_timestamp?}, config -> %{config | show_timestamp?: show_timestamp?}
154157
{:task, task}, config -> %{config | task: task}
158+
{:verbose, verbose}, config -> %{config | verbose?: verbose}
155159
_pair, config -> config
156160
end)
157161
|> handle_custom_command(mti_opts)

lib/mix_test_interactive/config.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ defmodule MixTestInteractive.Config do
77
@application :mix_test_interactive
88

99
typedstruct do
10-
field :ansi_enabled?, boolean
11-
field :clear?, boolean, default: false
10+
field :ansi_enabled?, boolean()
11+
field :clear?, boolean(), default: false
1212
field :command, {String.t(), [String.t()]}, default: {"mix", []}
1313
field :exclude, [Regex.t()], default: [~r/\.#/, ~r{priv/repo/migrations}]
1414
field :extra_extensions, [String.t()], default: []
1515
field :runner, module(), default: MixTestInteractive.PortRunner
1616
field :show_timestamp?, boolean(), default: false
1717
field :task, String.t(), default: "test"
18+
field :verbose?, boolean(), default: false
1819
end
1920

2021
@doc """
@@ -31,6 +32,7 @@ defmodule MixTestInteractive.Config do
3132
|> load(:runner)
3233
|> load(:timestamp, rename: :show_timestamp?)
3334
|> load(:task)
35+
|> load(:verbose, rename: :verbose?)
3436
end
3537

3638
@doc false

lib/mix_test_interactive/port_runner.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule MixTestInteractive.PortRunner do
1111
"""
1212
@behaviour MixTestInteractive.TestRunner
1313

14+
alias MixTestInteractive.CommandLineFormatter
1415
alias MixTestInteractive.Config
1516
alias MixTestInteractive.TestRunner
1617

@@ -37,6 +38,8 @@ defmodule MixTestInteractive.PortRunner do
3738
{zombie_killer(), [command] ++ command_args ++ task ++ task_args}
3839
end
3940

41+
maybe_print_command(config, runner_program, runner_program_args)
42+
4043
runner.(runner_program, runner_program_args,
4144
env: [{"MIX_ENV", "test"}],
4245
into: IO.stream(:stdio, :line)
@@ -51,6 +54,14 @@ defmodule MixTestInteractive.PortRunner do
5154
["do", "eval", enable_command, ",", task]
5255
end
5356

57+
defp maybe_print_command(%Config{verbose?: false} = _config, _runner_program, _runner_program_args), do: :ok
58+
59+
defp maybe_print_command(%Config{} = _config, runner_program, runner_program_args) do
60+
runner_program
61+
|> CommandLineFormatter.call(runner_program_args)
62+
|> IO.puts()
63+
end
64+
5465
defp zombie_killer do
5566
@application
5667
|> :code.priv_dir()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
defmodule MixTestInteractive.CommandLineFormatterTest do
2+
use ExUnit.Case, async: true
3+
4+
alias MixTestInteractive.CommandLineFormatter
5+
6+
describe "formatting a command line with proper quoting" do
7+
test "formats simple commands without quoting" do
8+
assert CommandLineFormatter.call("mix", ["test", "--stale", "file.exs"]) ==
9+
~s(mix test --stale file.exs)
10+
end
11+
12+
test "double-quotes arguments with spaces" do
13+
assert CommandLineFormatter.call("mix", ["test", "file with spaces.txt"]) ==
14+
~s(mix test "file with spaces.txt")
15+
end
16+
17+
test "double-quotes arguments with special characters" do
18+
args = ["do", "eval", "Application.put_env(:elixir,:ansi_enabled,true)", ",", "test"]
19+
expected = ~s[mix do eval "Application.put_env(:elixir,:ansi_enabled,true)" , test]
20+
21+
assert CommandLineFormatter.call("mix", args) == expected
22+
end
23+
24+
test "single-quotes arguments containing only double quotes" do
25+
args = ["do", "eval", ~s[IO.puts("running tests!")], ",", "test"]
26+
expected = ~s[mix do eval 'IO.puts("running tests!")' , test]
27+
28+
assert CommandLineFormatter.call("mix", args) == expected
29+
end
30+
31+
test "double-quotes arguments with mixed single and double quotes" do
32+
args = ["do", "eval", ~s[IO.puts("I'm testing")], ",", "test"]
33+
expected = ~s[mix do eval 'IO.puts(\"I'\\''m testing\")' , test]
34+
35+
assert CommandLineFormatter.call("mix", args) == expected
36+
end
37+
end
38+
end

test/mix_test_interactive/command_line_parser_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ defmodule MixTestInteractive.CommandLineParserTest do
156156
refute config.show_timestamp?
157157
end
158158

159+
test "sets verbose? flag with --verbose" do
160+
{:ok, %{config: config}} = CommandLineParser.parse(["--verbose"])
161+
assert config.verbose?
162+
end
163+
164+
test "clears verbose? flag with --no-verbose" do
165+
{:ok, %{config: config}} = CommandLineParser.parse(["--no-verbose"])
166+
refute config.verbose?
167+
end
168+
159169
test "configures custom mix task with --task" do
160170
{:ok, %{config: config}} = CommandLineParser.parse(["--task", "custom_task"])
161171
assert config.task == "custom_task"

test/mix_test_interactive/config_test.exs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,16 @@ defmodule MixTestInteractive.ConfigTest do
9797
config = Config.load_from_environment()
9898
assert config.task == "test"
9999
end
100+
101+
test "takes :verbose from the env" do
102+
Process.put(:verbose, true)
103+
config = Config.load_from_environment()
104+
assert config.verbose?
105+
end
106+
107+
test "defaults verbose to false" do
108+
config = Config.load_from_environment()
109+
refute config.verbose?
110+
end
100111
end
101112
end

test/mix_test_interactive/port_runner_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule MixTestInteractive.PortRunnerTest do
22
use ExUnit.Case, async: true
33

4+
import ExUnit.CaptureIO
5+
46
alias MixTestInteractive.Config
57
alias MixTestInteractive.PortRunner
68

@@ -106,6 +108,22 @@ defmodule MixTestInteractive.PortRunnerTest do
106108
assert {"custom_command", ["--custom_arg", "test", "--cover"], _options} =
107109
run(args: ["--cover"], config: config)
108110
end
111+
112+
test "does not display command by default" do
113+
{result, output} = with_io(fn -> run() end)
114+
115+
assert {"mix", _args, _env} = result
116+
assert output == ""
117+
end
118+
119+
test "displays command in verbose mode" do
120+
config = config(%{verbose?: true})
121+
122+
{result, output} = with_io(fn -> run(config: config) end)
123+
124+
assert {"mix", _args, _env} = result
125+
assert output =~ "mix test"
126+
end
109127
end
110128
end
111129
end

0 commit comments

Comments
 (0)