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
[