changed
CHANGELOG.md
|
@@ -2,30 2,39 @@
|
2
2
|
|
3
3
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
4
4
|
|
5
|
- ## v0.3.0-rc.0
|
6
|
-
|
7
|
- ### Breaking changes
|
8
|
-
|
9
|
- * `auto_assert` now uses `<-` when comparing against falsy values instead of `==`. Support for comparisons using `==` has been removed.
|
5
|
## v0.3.0-rc.1
|
10
6
|
|
11
7
|
### Enhancements
|
12
8
|
|
13
|
- * Add [`auto_assert_receive`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_receive/2) and [`auto_assert_received`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_received/1).
|
14
|
- * Add [`auto_assert_raise`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_raise/3).
|
15
|
- * Existing auto-assertions will now run even when `Mneme.start()` is not called, but new auto-assertions will fail ([#32](https://github.com/zachallaun/mneme/issues/32)).
|
16
|
- * Generate patterns for ranges will now use `..` and `..//` syntax instead of `%Range{}`.
|
9
|
* [Core] Add a `:force_update` option that forces re-generating assertion patterns even when they succeed. See the [Options documentation](https://hexdocs.pm/mneme/Mneme.html#module-options) for more info.
|
10
|
* [CLI] Prompts will now show options that were overridden from the defaults.
|
11
|
* [CLI] Mneme now prints a one-line summary of new, updated, rejected, and skipped assertions at the end of the test run.
|
12
|
|
13
|
## v0.3.0-rc.0
|
14
|
|
15
|
It is now recommended to use Elixir v1.14.4 or later.
|
16
|
|
17
|
### Breaking changes
|
18
|
|
19
|
* [Core] `auto_assert` now uses `<-` when comparing against falsy values instead of `==`. Support for comparisons using `==` has been removed.
|
20
|
|
21
|
### Enhancements
|
22
|
|
23
|
* [Core] Add three new auto-assertions:
|
24
|
* [`auto_assert_raise`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_raise/3)
|
25
|
* [`auto_assert_receive`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_receive/2)
|
26
|
* [`auto_assert_received`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_received/1)
|
27
|
* [Core] Existing auto-assertions will now run when `Mneme.start()` is not called, but new or failing auto-assertions will fail without prompting ([#32](https://github.com/zachallaun/mneme/issues/32)).
|
28
|
* [Core] Pattern generation improvements:
|
29
|
* Ranges now use range syntax like `1..10` and `1..10//2` instead of generating a `%Range{}` struct.
|
17
30
|
|
18
31
|
### Fixes
|
19
32
|
|
20
|
- * Fix a configuration precedence bug that caused options set in application config to always override options local to the test module, describe block, or test.
|
21
|
- * Fix a confusing diff result that could occur with some binary operations ([#11](https://github.com/zachallaun/mneme/issues/11)).
|
22
|
- * Don't display preceding comments in diffs ([#26](https://github.com/zachallaun/mneme/issues/26)).
|
23
|
- * Fix a number of diffing errors related to structs.
|
24
|
- * Fix a compatibility issue with Ecto ~> 3.9.4 ([#34](https://github.com/zachallaun/mneme/issues/34)).
|
25
|
-
|
26
|
- ### Notes
|
27
|
-
|
28
|
- * Upgrade to Elixir v1.14.4 or later to fix a diff syntax highlighting bug related to escaped string interpolations ([#30](https://github.com/zachallaun/mneme/issues/30)).
|
33
|
* [Core] Fix a configuration precedence bug that caused options set in application config to always override module, describe, or test options.
|
34
|
* [Core] Fix a compatibility issue with Ecto ~> 3.9.4 ([#34](https://github.com/zachallaun/mneme/issues/34)).
|
35
|
* [CLI] Fix a confusing diff result that could occur with some binary operations ([#11](https://github.com/zachallaun/mneme/issues/11)).
|
36
|
* [CLI] Preceding comments are no longer shown in diffs ([#26](https://github.com/zachallaun/mneme/issues/26)).
|
37
|
* [CLI] Fix a number of diffing errors related to structs.
|
29
38
|
|
30
39
|
## v0.2.7 (2023-03-29)
|
changed
hex_metadata.config
|
@@ -44,4 44,4 @@
|
44
44
|
{<<"optional">>,false},
|
45
45
|
{<<"repository">>,<<"hexpm">>},
|
46
46
|
{<<"requirement">>,<<"~> 0.16.0">>}]]}.
|
47
|
- {<<"version">>,<<"0.3.0-rc.0">>}.
|
47
|
{<<"version">>,<<"0.3.0-rc.1">>}.
|
changed
lib/mneme/assertion.ex
|
@@ -100,10 100,10 @@ defmodule Mneme.Assertion do
|
100
100
|
patch(assertion, env)
|
101
101
|
|
102
102
|
:update ->
|
103
|
- {:ok, assertion} = Mneme.Server.register_assertion(assertion)
|
104
|
-
|
105
103
|
try do
|
106
|
- eval(assertion, env)
|
104
|
assertion
|
105
|
|> Mneme.Server.register_assertion()
|
106
|
|> handle_assertion(assertion, env)
|
107
107
|
rescue
|
108
108
|
error in [ExUnit.AssertionError] ->
|
109
109
|
patch(assertion, env, error)
|
|
@@ -124,32 124,27 @@ defmodule Mneme.Assertion do
|
124
124
|
reraise error, __STACKTRACE__
|
125
125
|
end
|
126
126
|
|
127
|
- defp patch(assertion, env, error \\ nil) do
|
128
|
- result =
|
129
|
- case assertion do
|
130
|
- %{kind: :auto_assert_raise, value: nil} -> {:ok, assertion}
|
131
|
- %{kind: :auto_assert_receive, value: []} -> {:ok, assertion}
|
132
|
- %{kind: :auto_assert_received, value: []} -> {:ok, assertion}
|
133
|
- _ -> Mneme.Server.patch_assertion(assertion)
|
134
|
- end
|
135
|
-
|
136
|
- case result do
|
137
|
- {:ok, assertion} ->
|
138
|
- eval(assertion, env)
|
139
|
-
|
140
|
- {:error, :skip} ->
|
141
|
- :ok
|
142
|
-
|
143
|
- {:error, :no_pattern} ->
|
144
|
- if error do
|
145
|
- reraise error, [stacktrace_entry(assertion)]
|
146
|
- else
|
147
|
- assertion_error!()
|
148
|
- end
|
149
|
-
|
150
|
- {:error, {:internal, error, stacktrace}} ->
|
151
|
- raise Mneme.InternalError, original_error: error, original_stacktrace: stacktrace
|
127
|
defp patch(assertion, env, existing_error \\ nil) do
|
128
|
case assertion do
|
129
|
%{kind: :auto_assert_raise, value: nil} -> {:ok, assertion}
|
130
|
%{kind: :auto_assert_receive, value: []} -> {:ok, assertion}
|
131
|
%{kind: :auto_assert_received, value: []} -> {:ok, assertion}
|
132
|
_ -> Mneme.Server.patch_assertion(assertion)
|
152
133
|
end
|
134
|
|> handle_assertion(assertion, env, existing_error)
|
135
|
end
|
136
|
|
137
|
defp handle_assertion(result, assertion, env, existing_error \\ nil)
|
138
|
defp handle_assertion({:ok, assertion}, _, env, _), do: eval(assertion, env)
|
139
|
defp handle_assertion({:error, :skipped}, _, _, _), do: :ok
|
140
|
defp handle_assertion({:error, :rejected}, _, _, nil), do: assertion_error!()
|
141
|
|
142
|
defp handle_assertion({:error, :rejected}, assertion, _, error) do
|
143
|
reraise error, [stacktrace_entry(assertion)]
|
144
|
end
|
145
|
|
146
|
defp handle_assertion({:error, {:internal, error, stacktrace}}, _, _, _) do
|
147
|
raise Mneme.InternalError, original_error: error, original_stacktrace: stacktrace
|
153
148
|
end
|
154
149
|
|
155
150
|
defp eval(%{value: value, context: ctx} = assertion, env) do
|
changed
lib/mneme/options.ex
|
@@ -9,46 9,56 @@ defmodule Mneme.Options do
|
9
9
|
type: {:in, [:prompt, :accept, :reject]},
|
10
10
|
default: :prompt,
|
11
11
|
doc: """
|
12
|
- The action to be taken when an auto-assertion updates. Actions are one of
|
13
|
- `:prompt`, `:accept`, or `:reject`. If `CI=true` is set in environment
|
14
|
- variables, the action will _always_ be `:reject`.
|
12
|
The action to be taken when an auto-assertion updates. Actions are
|
13
|
one of `:prompt`, `:accept`, or `:reject`. If `CI=true` is set in
|
14
|
environment variables, the action will _always_ be `:reject`.
|
15
15
|
"""
|
16
16
|
],
|
17
17
|
default_pattern: [
|
18
18
|
type: {:in, [:infer, :first, :last]},
|
19
19
|
default: :infer,
|
20
20
|
doc: """
|
21
|
- The default pattern to be selected if prompted to update an assertion.
|
22
|
- Can be one of `:infer`, `:first`, or `:last`.
|
21
|
The default pattern to be selected if prompted to update an
|
22
|
assertion. Can be one of `:infer`, `:first`, or `:last`.
|
23
23
|
"""
|
24
24
|
],
|
25
25
|
diff: [
|
26
26
|
type: {:in, [:text, :semantic]},
|
27
27
|
default: :semantic,
|
28
28
|
doc: """
|
29
|
- Controls the diff engine used to display changes when an auto-assertion
|
30
|
- updates. If `:semantic`, uses a custom diff engine to highlight only
|
31
|
- meaningful changes in the value. If `:text`, uses the Myers Difference
|
32
|
- algorithm to highlight all changes in text.
|
29
|
Controls the diff engine used to display changes when an auto-
|
30
|
assertion updates. If `:semantic`, uses a custom diff engine to
|
31
|
highlight only meaningful changes in the value. If `:text`, uses
|
32
|
the Myers Difference algorithm to highlight all changes in text.
|
33
33
|
"""
|
34
34
|
],
|
35
35
|
diff_style: [
|
36
36
|
type: {:in, [:side_by_side, :stacked]},
|
37
37
|
default: :side_by_side,
|
38
38
|
doc: """
|
39
|
- Controls how diffs are rendered when the `:diff` option is set to `:semantic`.
|
40
|
- If `:side_by_side`, old and new code will be rendered side-by-side if the
|
41
|
- terminal has sufficient space. If `:stacked`, old and new code will be
|
42
|
- rendered one on top of the other.
|
39
|
Controls how diffs are rendered when the `:diff` option is set to
|
40
|
`:semantic`. If `:side_by_side`, old and new code will be rendered
|
41
|
side-by-side if the terminal has sufficient space. If `:stacked`,
|
42
|
old and new code will be rendered one on top of the other.
|
43
|
"""
|
44
|
],
|
45
|
force_update: [
|
46
|
type: :boolean,
|
47
|
default: false,
|
48
|
doc: """
|
49
|
Setting to `true` will force auto-assertions to update even when
|
50
|
they would otherwise succeed. This can be especially helpful when
|
51
|
adding new keys to maps or structs since a pattern like `%{}`
|
52
|
would not normally prompt as the match still succeeds.
|
43
53
|
"""
|
44
54
|
],
|
45
55
|
target: [
|
46
56
|
type: {:in, [:mneme, :ex_unit]},
|
47
57
|
default: :mneme,
|
48
58
|
doc: """
|
49
|
- The target output for auto-assertions. If `:mneme`, the expression will
|
50
|
- remain an auto-assertion. If `:ex_unit`, the expression will be rewritten
|
51
|
- as an ExUnit assertion.
|
59
|
The target output for auto-assertions. If `:mneme`, the expression
|
60
|
will remain an auto-assertion. If `:ex_unit`, the expression will
|
61
|
be rewritten as an ExUnit assertion.
|
52
62
|
"""
|
53
63
|
]
|
54
64
|
]
|
|
@@ -117,6 127,28 @@ defmodule Mneme.Options do
|
117
127
|
:ok
|
118
128
|
end
|
119
129
|
|
130
|
@doc """
|
131
|
Returns a filtered list of options that differ from their defaults.
|
132
|
|
133
|
## Examples
|
134
|
|
135
|
iex> Mneme.Options.overrides(%{default_pattern: :infer, force_update: true})
|
136
|
[force_update: true]
|
137
|
|
138
|
"""
|
139
|
def overrides(opts) do
|
140
|
sorted_opts =
|
141
|
opts
|
142
|
|> Keyword.new()
|
143
|
|> Enum.sort_by(&elem(&1, 0))
|
144
|
|
145
|
for {key, value} <- sorted_opts,
|
146
|
attrs = @options_schema.schema[key],
|
147
|
value != attrs[:default] do
|
148
|
{key, value}
|
149
|
end
|
150
|
end
|
151
|
|
120
152
|
@doc """
|
121
153
|
Fetch all valid Mneme options from the current test tags and environment.
|
122
154
|
"""
|
|
@@ -169,10 201,7 @@ defmodule Mneme.Options do
|
169
201
|
|
170
202
|
defp drop_opts(opts, key_or_keys), do: Keyword.drop(opts, List.wrap(key_or_keys))
|
171
203
|
|
172
|
- @doc """
|
173
|
- Collect all registered Mneme attributes from the given tags, in order of precedence.
|
174
|
- """
|
175
|
- def collect_attributes(%{registered: %{} = attrs}) do
|
204
|
defp collect_attributes(%{registered: %{} = attrs}) do
|
176
205
|
%{}
|
177
206
|
|> collect_attributes(Map.get(attrs, @test_attr, []))
|
178
207
|
|> collect_attributes(Map.get(attrs, @describe_attr, []))
|
|
@@ -180,7 209,7 @@ defmodule Mneme.Options do
|
180
209
|
|> collect_attributes([:persistent_term.get(@config_cache)])
|
181
210
|
end
|
182
211
|
|
183
|
- def collect_attributes(_), do: %{}
|
212
|
defp collect_attributes(_), do: %{}
|
184
213
|
|
185
214
|
defp collect_attributes(acc, lower_priority) do
|
186
215
|
new =
|
changed
lib/mneme/patcher.ex
|
@@ -78,25 78,25 @@ defmodule Mneme.Patcher do
|
78
78
|
|
79
79
|
Returns `{result, patch_state}`.
|
80
80
|
"""
|
81
|
- @spec patch!(state, Assertion.t(), map()) ::
|
81
|
@spec patch!(state, Assertion.t(), non_neg_integer(), map()) ::
|
82
82
|
{{:ok, Assertion.t()} | {:error, term()}, state}
|
83
|
- def patch!(%Project{} = project, %Assertion{} = assertion, %{} = opts) do
|
83
|
def patch!(%Project{} = project, %Assertion{} = assertion, counter, %{} = opts) do
|
84
84
|
{project, source} = load_source!(project, assertion.context.file)
|
85
85
|
{assertion, node} = prepare_assertion(assertion, source)
|
86
|
- patch!(project, source, assertion, node, opts)
|
86
|
patch!(project, source, assertion, counter, node, opts)
|
87
87
|
rescue
|
88
88
|
error ->
|
89
89
|
{{:error, {:internal, error, __STACKTRACE__}}, project}
|
90
90
|
end
|
91
91
|
|
92
|
- defp patch!(_, _, %{value: :__mneme__super_secret_test_value_goes_boom__}, _, _) do
|
92
|
defp patch!(_, _, %{value: :__mneme__super_secret_test_value_goes_boom__}, _, _, _) do
|
93
93
|
raise ArgumentError, "I told you!"
|
94
94
|
end
|
95
95
|
|
96
|
- defp patch!(project, source, assertion, node, opts) do
|
96
|
defp patch!(project, source, assertion, counter, node, opts) do
|
97
97
|
assertion = Assertion.generate_code(assertion, opts.target, opts.default_pattern)
|
98
98
|
|
99
|
- case prompt_change(assertion, opts) do
|
99
|
case prompt_change(assertion, counter, opts) do
|
100
100
|
:accept ->
|
101
101
|
ast = replace_assertion_node(node, assertion.code)
|
102
102
|
source = Source.update(source, :mneme, ast: ast)
|
|
@@ -104,25 104,25 @@ defmodule Mneme.Patcher do
|
104
104
|
{{:ok, assertion}, Project.update(project, source)}
|
105
105
|
|
106
106
|
:reject ->
|
107
|
- {{:error, :no_pattern}, project}
|
107
|
{{:error, :rejected}, project}
|
108
108
|
|
109
109
|
:skip ->
|
110
|
- {{:error, :skip}, project}
|
110
|
{{:error, :skipped}, project}
|
111
111
|
|
112
112
|
:prev ->
|
113
|
- patch!(project, source, Assertion.prev(assertion), node, opts)
|
113
|
patch!(project, source, Assertion.prev(assertion), counter, node, opts)
|
114
114
|
|
115
115
|
:next ->
|
116
|
- patch!(project, source, Assertion.next(assertion), node, opts)
|
116
|
patch!(project, source, Assertion.next(assertion), counter, node, opts)
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
120
|
- defp prompt_change(assertion, %{action: :prompt, prompter: prompter} = opts) do
|
120
|
defp prompt_change(assertion, counter, %{action: :prompt, prompter: prompter} = opts) do
|
121
121
|
diff = %{left: format_node(assertion.rich_ast), right: format_node(assertion.code)}
|
122
|
- prompter.prompt!(assertion, diff, opts)
|
122
|
prompter.prompt!(assertion, counter, diff, opts)
|
123
123
|
end
|
124
124
|
|
125
|
- defp prompt_change(_, %{action: action}), do: action
|
125
|
defp prompt_change(_, _, %{action: action}), do: action
|
126
126
|
|
127
127
|
defp prepare_assertion(assertion, source) do
|
128
128
|
zipper =
|
changed
lib/mneme/prompter.ex
|
@@ -7,5 7,5 @@ defmodule Mneme.Prompter do
|
7
7
|
@type diff :: %{left: String.t(), right: String.t()}
|
8
8
|
@type options :: map()
|
9
9
|
|
10
|
- @callback prompt!(Mneme.Assertion.t(), diff, options) :: response
|
10
|
@callback prompt!(Mneme.Assertion.t(), counter :: non_neg_integer(), diff, options) :: response
|
11
11
|
end
|
changed
lib/mneme/prompter/terminal.ex
|
@@ -12,7 12,7 @@ defmodule Mneme.Prompter.Terminal do
|
12
12
|
@empty_bullet_char "○"
|
13
13
|
@info_char "🛈"
|
14
14
|
@arrow_left_char "❮"
|
15
|
- @arrow_right_car "❯"
|
15
|
@arrow_right_char "❯"
|
16
16
|
|
17
17
|
@box_horizontal "─"
|
18
18
|
@box_vertical "│"
|
|
@@ -20,21 20,21 @@ defmodule Mneme.Prompter.Terminal do
|
20
20
|
@box_cross_up "┴"
|
21
21
|
|
22
22
|
@impl true
|
23
|
- def prompt!(%Assertion{} = assertion, diff, opts) do
|
24
|
- Owl.IO.puts(["\n", message(assertion, diff, opts)])
|
23
|
def prompt!(%Assertion{} = assertion, counter, diff, opts) do
|
24
|
Owl.IO.puts(["\n", message(assertion, counter, diff, opts)])
|
25
25
|
input()
|
26
26
|
end
|
27
27
|
|
28
28
|
@doc false
|
29
|
- def message(%Assertion{} = assertion, diff, opts) do
|
29
|
def message(%Assertion{} = assertion, counter, diff, opts) do
|
30
30
|
notes = Assertion.notes(assertion)
|
31
31
|
|
32
32
|
[
|
33
|
- format_header(assertion),
|
33
|
format_header(assertion, counter, opts),
|
34
34
|
"\n",
|
35
35
|
format_diff(diff, opts),
|
36
36
|
format_notes(notes),
|
37
|
- format_input(assertion),
|
37
|
format_input(assertion, opts),
|
38
38
|
"\n"
|
39
39
|
]
|
40
40
|
end
|
|
@@ -98,7 98,7 @@ defmodule Mneme.Prompter.Terminal do
|
98
98
|
task = Task.async(Mneme.Diff, :format, [left, right])
|
99
99
|
|
100
100
|
case Task.yield(task, 1500) || Task.shutdown(task, :brutal_kill) do
|
101
|
- {:ok, {:ok, {nil, nil}}} -> nil
|
101
|
{:ok, {:ok, {nil, nil}}} -> {Owl.Data.lines(left), Owl.Data.lines(right)}
|
102
102
|
{:ok, {:ok, {nil, ins}}} -> {Owl.Data.lines(left), ins}
|
103
103
|
{:ok, {:ok, {del, nil}}} -> {del, Owl.Data.lines(right)}
|
104
104
|
{:ok, {:ok, {del, ins}}} -> {del, ins}
|
|
@@ -196,27 196,37 @@ defmodule Mneme.Prompter.Terminal do
|
196
196
|
width = terminal_width(opts)
|
197
197
|
|
198
198
|
if largest_side * 2 <= width do
|
199
|
- div(width, 2) - 1
|
199
|
div(width, 2)
|
200
200
|
end
|
201
201
|
end
|
202
202
|
|
203
203
|
defp diff_side_by_side(%{diff_style: :stacked}, _), do: nil
|
204
204
|
|
205
205
|
defp terminal_width(%{terminal_width: width}) when is_integer(width), do: width
|
206
|
- defp terminal_width(_opts), do: Owl.IO.columns() || 98
|
206
|
defp terminal_width(_opts), do: (Owl.IO.columns() || 99) - 1
|
207
207
|
|
208
208
|
defp eof_newline(code), do: String.trim_trailing(code) <> "\n"
|
209
209
|
|
210
|
- defp format_header(assertion) do
|
211
|
- %{stage: stage, context: %{module: module, test: test}} = assertion
|
210
|
defp format_header(assertion, counter, opts) do
|
211
|
%{context: %{module: module, test: test}} = assertion
|
212
|
overrides = Mneme.Options.overrides(opts)
|
213
|
|
214
|
stage =
|
215
|
case assertion.stage do
|
216
|
:new -> tag("[#{counter}] New", :cyan)
|
217
|
:update -> tag("[#{counter}] Update", :yellow)
|
218
|
end
|
212
219
|
|
213
220
|
[
|
214
|
- format_stage(stage),
|
215
|
- tag([" ", @bullet_char, " "], :faint),
|
221
|
stage,
|
222
|
tag([" ", @middle_dot_char, " "], :faint),
|
216
223
|
to_string(test),
|
217
|
- " (",
|
218
|
- inspect(module),
|
219
|
- ")\n",
|
224
|
[" (", inspect(module), ")"],
|
225
|
if(overrides == [],
|
226
|
do: [],
|
227
|
else: tag([" ", @middle_dot_char, " ", inspect(overrides)], :faint)
|
228
|
),
|
229
|
"\n",
|
220
230
|
format_file(assertion),
|
221
231
|
"\n"
|
222
232
|
]
|
|
@@ -227,9 237,6 @@ defmodule Mneme.Prompter.Terminal do
|
227
237
|
tag([path, ":", to_string(line)], :faint)
|
228
238
|
end
|
229
239
|
|
230
|
- defp format_stage(:new), do: tag("[Mneme] New", :cyan)
|
231
|
- defp format_stage(:update), do: tag("[Mneme] Changed", :yellow)
|
232
|
-
|
233
240
|
defp format_notes([]), do: []
|
234
241
|
|
235
242
|
defp format_notes(notes) do
|
|
@@ -243,12 250,12 @@ defmodule Mneme.Prompter.Terminal do
|
243
250
|
|> tag(:faint)
|
244
251
|
end
|
245
252
|
|
246
|
- defp format_input(%{stage: stage} = assertion) do
|
253
|
defp format_input(%{stage: stage} = assertion, opts) do
|
247
254
|
nav = Assertion.pattern_index(assertion)
|
248
255
|
|
249
256
|
[
|
250
257
|
"\n",
|
251
|
- format_explanation(stage),
|
258
|
format_explanation(stage, opts),
|
252
259
|
"\n",
|
253
260
|
tag("> ", :faint),
|
254
261
|
"\n",
|
|
@@ -270,17 277,31 @@ defmodule Mneme.Prompter.Terminal do
|
270
277
|
|
271
278
|
defp format_nav_options({index, count}) do
|
272
279
|
dots = Enum.map(0..(count - 1), &if(&1 == index, do: @bullet_char, else: @empty_bullet_char))
|
273
|
- tag(["#{@arrow_left_char} j ", dots, " k #{@arrow_right_car}"], :faint)
|
280
|
|
281
|
[
|
282
|
tag(@arrow_left_char, :faint),
|
283
|
" j ",
|
284
|
tag(dots, :faint),
|
285
|
" k ",
|
286
|
tag(@arrow_right_char, :faint)
|
287
|
]
|
274
288
|
end
|
275
289
|
|
276
|
- defp format_explanation(:new) do
|
290
|
defp format_explanation(:new, _opts) do
|
277
291
|
"Accept new assertion?"
|
278
292
|
end
|
279
293
|
|
280
|
- defp format_explanation(:update) do
|
294
|
defp format_explanation(:update, %{force_update: true}) do
|
281
295
|
[
|
282
|
- tag("Value has changed! ", :yellow),
|
283
|
- "Update pattern?"
|
296
|
tag("Value may have changed.", :yellow),
|
297
|
" Update pattern?"
|
298
|
]
|
299
|
end
|
300
|
|
301
|
defp format_explanation(:update, _opts) do
|
302
|
[
|
303
|
tag("Value has changed!", :yellow),
|
304
|
" Update pattern?"
|
284
305
|
]
|
285
306
|
end
|
changed
lib/mneme/server.ex
|
@@ -31,7 31,13 @@ defmodule Mneme.Server do
|
31
31
|
opts: %{},
|
32
32
|
to_register: [],
|
33
33
|
to_patch: [],
|
34
|
- skipped: 0
|
34
|
stats: %{
|
35
|
counter: 0,
|
36
|
new: 0,
|
37
|
updated: 0,
|
38
|
skipped: 0,
|
39
|
rejected: 0
|
40
|
}
|
35
41
|
]
|
36
42
|
|
37
43
|
@type t :: %__MODULE__{
|
|
@@ -41,7 47,13 @@ defmodule Mneme.Server do
|
41
47
|
opts: %{{mod :: module(), test :: atom()} => map()},
|
42
48
|
to_register: [{any(), from :: pid()}],
|
43
49
|
to_patch: [{any(), from :: pid()}],
|
44
|
- skipped: pos_integer()
|
50
|
stats: %{
|
51
|
counter: non_neg_integer(),
|
52
|
new: non_neg_integer(),
|
53
|
updated: non_neg_integer(),
|
54
|
skipped: non_neg_integer(),
|
55
|
rejected: non_neg_integer()
|
56
|
}
|
45
57
|
}
|
46
58
|
|
47
59
|
@doc """
|
|
@@ -116,26 128,21 @@ defmodule Mneme.Server do
|
116
128
|
{:reply, :ok, state, {:continue, :process_next}}
|
117
129
|
end
|
118
130
|
|
119
|
- def handle_call(
|
120
|
- {:formatter, {:module_finished, %{name: mod}}},
|
121
|
- _from,
|
122
|
- %{current_module: mod} = state
|
123
|
- ) do
|
124
|
- {:reply, :ok, state |> flush_io() |> Map.put(:current_module, nil),
|
125
|
- {:continue, :process_next}}
|
131
|
def handle_call({:formatter, {:module_finished, %{name: mod}}}, _from, state) do
|
132
|
if state.current_module == mod do
|
133
|
state =
|
134
|
state
|
135
|
|> Map.put(:current_module, nil)
|
136
|
|> flush_io()
|
137
|
|
138
|
{:reply, :ok, state, {:continue, :process_next}}
|
139
|
else
|
140
|
{:reply, :ok, state}
|
141
|
end
|
126
142
|
end
|
127
143
|
|
128
|
- def handle_call({:formatter, {:suite_finished, _}}, _from, %{skipped: skipped} = state) do
|
129
|
- case Patcher.finalize!(state.patch_state) do
|
130
|
- :ok -> :ok
|
131
|
- {:error, {:not_saved, files}} -> ensure_exit_with_error!(:not_saved, files)
|
132
|
- end
|
133
|
-
|
134
|
- if skipped > 0 do
|
135
|
- ensure_exit_with_error!(:skipped, skipped)
|
136
|
- end
|
137
|
-
|
138
|
- {:reply, :ok, flush_io(state)}
|
144
|
def handle_call({:formatter, {:suite_finished, _}}, _from, state) do
|
145
|
{:reply, :ok, state |> finalize() |> flush_io()}
|
139
146
|
end
|
140
147
|
|
141
148
|
def handle_call({:formatter, _msg}, _from, %{current_module: nil} = state) do
|
|
@@ -161,14 168,19 @@ defmodule Mneme.Server do
|
161
168
|
end
|
162
169
|
|
163
170
|
defp do_patch_assertion(state, {assertion, from}) do
|
171
|
{state, counter} = inc_and_return_stat(state, :counter)
|
172
|
|
164
173
|
%Assertion{context: %{module: module, test: test}} = assertion
|
165
174
|
opts = state.opts[{module, test}]
|
166
175
|
|
167
|
- {reply, patch_state} = Patcher.patch!(state.patch_state, assertion, opts)
|
176
|
{reply, patch_state} = Patcher.patch!(state.patch_state, assertion, counter, opts)
|
168
177
|
GenServer.reply(from, reply)
|
169
178
|
|
170
|
- case reply do
|
171
|
- {:error, :skip} -> Map.update!(state, :skipped, &(&1 1))
|
179
|
case {reply, assertion.stage} do
|
180
|
{{:ok, _}, :new} -> inc_stat(state, :new)
|
181
|
{{:ok, _}, :update} -> inc_stat(state, :updated)
|
182
|
{{:error, :skipped}, _} -> inc_stat(state, :skipped)
|
183
|
{{:error, :rejected}, _} -> inc_stat(state, :rejected)
|
172
184
|
_ -> state
|
173
185
|
end
|
174
186
|
|> Map.put(:patch_state, patch_state)
|
|
@@ -179,13 191,11 @@ defmodule Mneme.Server do
|
179
191
|
%Assertion{context: %{module: module, test: test}} = assertion
|
180
192
|
opts = state.opts[{module, test}]
|
181
193
|
|
182
|
- case opts.target do
|
183
|
- :mneme ->
|
184
|
- GenServer.reply(from, {:ok, assertion})
|
185
|
- state
|
186
|
-
|
187
|
- :ex_unit ->
|
188
|
- %{state | to_patch: [{assertion, from} | state.to_patch]}
|
194
|
if opts.force_update || opts.target == :ex_unit do
|
195
|
%{state | to_patch: [{assertion, from} | state.to_patch]}
|
196
|
else
|
197
|
GenServer.reply(from, {:ok, assertion})
|
198
|
state
|
189
199
|
end
|
190
200
|
end
|
191
201
|
|
|
@@ -227,14 237,19 @@ defmodule Mneme.Server do
|
227
237
|
defp current_module?(%{current_module: mod}, mod), do: true
|
228
238
|
defp current_module?(_state, _mod), do: false
|
229
239
|
|
230
|
- defp ensure_exit_with_error!(reason, arg)
|
240
|
defp finalize(%{stats: stats} = state) do
|
241
|
case Patcher.finalize!(state.patch_state) do
|
242
|
:ok -> :ok
|
243
|
{:error, {:not_saved, files}} -> ensure_exit_with_error!(:not_saved, files)
|
244
|
end
|
231
245
|
|
232
|
- defp ensure_exit_with_error!(:skipped, skipped) do
|
233
|
- ensure_exit_with_error!(fn ->
|
234
|
- message = if skipped == 1, do: "1 assertion skipped", else: "#{skipped} assertions skipped"
|
246
|
if stats.skipped > 0 do
|
247
|
ensure_exit_with_error!()
|
248
|
end
|
235
249
|
|
236
|
- IO.puts(["\n", IO.ANSI.format([:red, "[Mneme] ", message])])
|
237
|
- end)
|
250
|
print_summary(stats)
|
251
|
|
252
|
state
|
238
253
|
end
|
239
254
|
|
240
255
|
defp ensure_exit_with_error!(:not_saved, files) do
|
|
@@ -244,13 259,14 @@ defmodule Mneme.Server do
|
244
259
|
Enum.map(files, &[" * ", &1, "\n"])
|
245
260
|
]
|
246
261
|
|
247
|
- IO.puts(["\n", IO.ANSI.format([:red, "[Mneme] ", message])])
|
262
|
["\n", Owl.Data.tag(["[Mneme] ", message], :red)]
|
263
|
|> Owl.IO.puts()
|
248
264
|
end)
|
249
265
|
end
|
250
266
|
|
251
|
- defp ensure_exit_with_error!(fun) when is_function(fun, 0) do
|
267
|
defp ensure_exit_with_error!(fun \\ nil) do
|
252
268
|
System.at_exit(fn _ ->
|
253
|
- fun.()
|
269
|
fun && fun.()
|
254
270
|
|
255
271
|
exit_status =
|
256
272
|
ExUnit.configuration()
|
|
@@ -259,4 275,33 @@ defmodule Mneme.Server do
|
259
275
|
exit({:shutdown, exit_status})
|
260
276
|
end)
|
261
277
|
end
|
278
|
|
279
|
defp inc_stat(state, stat) do
|
280
|
Map.update!(state, :stats, fn stats ->
|
281
|
Map.update!(stats, stat, &(&1 1))
|
282
|
end)
|
283
|
end
|
284
|
|
285
|
defp inc_and_return_stat(state, stat) do
|
286
|
state = inc_stat(state, stat)
|
287
|
{state, state.stats[stat]}
|
288
|
end
|
289
|
|
290
|
defp print_summary(stats) do
|
291
|
formatted =
|
292
|
for stat <- [:new, :updated, :rejected, :skipped],
|
293
|
stats[stat] != 0 do
|
294
|
format_stat(stat, stats[stat])
|
295
|
end
|
296
|
|
297
|
case formatted do
|
298
|
[] -> :ok
|
299
|
_ -> Owl.IO.puts(["\n\n[Mneme Assertions] ", Enum.intersperse(formatted, ", ")])
|
300
|
end
|
301
|
end
|
302
|
|
303
|
defp format_stat(:new, n), do: Owl.Data.tag(["#{n} new"], :green)
|
304
|
defp format_stat(:updated, n), do: Owl.Data.tag(["#{n} updated"], :green)
|
305
|
defp format_stat(:rejected, n), do: Owl.Data.tag(["#{n} rejected"], :red)
|
306
|
defp format_stat(:skipped, n), do: Owl.Data.tag(["#{n} skipped"], :yellow)
|
262
307
|
end
|
changed
mix.exs
|
@@ -4,7 4,7 @@ defmodule Mneme.MixProject do
|
4
4
|
@app :mneme
|
5
5
|
@source_url "https://github.com/zachallaun/mneme"
|
6
6
|
|
7
|
- def version, do: "0.3.0-rc.0"
|
7
|
def version, do: "0.3.0-rc.1"
|
8
8
|
|
9
9
|
def project do
|
10
10
|
[
|