changed .formatter.exs
 
@@ -14,5 14,6 @@ locals_without_parens = [
14
14
"examples/*.{ex,exs}"
15
15
],
16
16
locals_without_parens: locals_without_parens,
17
- export: [locals_without_parens: locals_without_parens]
17
export: [locals_without_parens: locals_without_parens],
18
import_deps: [:stream_data]
18
19
]
changed CHANGELOG.md
 
@@ -2,6 2,27 @@
2
2
3
3
This format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
5
## v0.3.4 (2023-05-22)
6
7
### Added
8
9
* Tested to support OTP 26.0 when using Elixir 1.14.4.
10
* Support expressions that return functions, serializing them with an `is_function(fun, arity)` guard.
11
* When generating a pattern for a MapSet, add a note suggesting using `MapSet.to_list/1` for better serialization.
12
13
### Changed
14
15
* Format pattern notes to be more obvious when they're present.
16
* Generate charlist patterns using `sigil_c` instead of single quotes, e.g. `~c"foo"` instead of `'foo'`. See [this discussion](https://elixirforum.com/t/convert-charlists-into-c-charlists/49455) for more context.
17
18
### Fixed
19
20
* Numerous fixes related to vars used in guards:
21
* Generated vars will no longer shadow variables in scope (e.g. if `pid` is in scope, a different pid will use the var `pid1`).
22
* The same var will no longer be used for different values of the same type.
23
* Multiple, redundant guards will no longer be emitted for the same var (e.g. `[self(), self()]` would result in `[pid, pid] when is_pid(pid) and is_pid(pid)`).
24
* Numerous fixes related to pattern generation, especially in regards to map keys.
25
5
26
## v0.3.3 (2023-05-01)
6
27
7
28
### Changed
changed README.md
 
@@ -1,6 1,9 @@
1
1
# /ni:mi:/ - Snapshot testing for Elixir ExUnit
2
2
3
- https://user-images.githubusercontent.com/503938/227819477-c7097fbc-b9a4-44a1-b3ea-f1b420c18799.mp4
3
<details>
4
<summary>🎥 Video Demo</summary>
5
<p>https://user-images.githubusercontent.com/503938/227819477-c7097fbc-b9a4-44a1-b3ea-f1b420c18799.mp4</p>
6
</details>
4
7
5
8
---
6
9
 
@@ -15,19 18,14 @@ https://user-images.githubusercontent.com/503938/227819477-c7097fbc-b9a4-44a1-b3
15
18
[![CI](https://github.com/zachallaun/mneme/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/zachallaun/mneme/actions/workflows/ci.yml)
16
19
17
20
Snapshot tests assert that some expression matches a reference value.
18
- It's like an ExUnit `assert`, except that the reference value is
19
- managed for you by Mneme.
21
It's like a regular `assert`, except that the reference value is generated for you by Mneme.
20
22
21
- Mneme follows in the footsteps of existing snapshot testing libraries
22
- like [Insta](https://insta.rs/) (Rust), [expect-test](https://github.com/janestreet/ppx_expect)
23
- (OCaml), and [assert_value](https://github.com/assert-value/assert_value_elixir)
24
- (Elixir). Instead of simple value or string comparison, however, Mneme
25
- leans heavily into pattern matching.
23
Mneme follows in the footsteps of existing snapshot testing libraries like [Insta](https://insta.rs/) (Rust), [expect-test](https://github.com/janestreet/ppx_expect) (OCaml), and [assert_value](https://github.com/assert-value/assert_value_elixir) (Elixir).
24
Instead of simple value or string comparison, however, Mneme focuses on pattern matching.
26
25
27
- ## Example
26
## A brief example
28
27
29
- Let's say you've written a test for a function that removes even
30
- numbers from a list:
28
Let's say you're working on a function that removes even numbers from a list:
31
29
32
30
```elixir
33
31
test "drop_evens/1 should remove all even numbers from an enum" do
 
@@ -39,10 37,9 @@ test "drop_evens/1 should remove all even numbers from an enum" do
39
37
end
40
38
```
41
39
42
- The first time you run this test, you'll see interactive prompts for
43
- each call to `auto_assert` showing a diff and asking if you'd like to
44
- accept the generated pattern. After accepting them, your test is
45
- updated:
40
Notice that these assertions don't really _assert_ anything yet.
41
That's okay, because the first time you run `mix test`, Mneme will generate the patterns and prompt you with diffs.
42
When you accept them, your test is updated for you:
46
43
47
44
```elixir
48
45
test "drop_evens/1 should remove all even numbers from an enum" do
 
@@ -54,14 51,17 @@ test "drop_evens/1 should remove all even numbers from an enum" do
54
51
end
55
52
```
56
53
57
- The next time you run this test, you won't receive a prompt and these
58
- will act (almost) like any other assertion. If the result of the call
59
- ever changes, you'll be prompted again and can choose to update the
60
- test or reject it and let it fail.
54
The next time you run your tests, you won't receive prompts (unless something changes!), and these auto-assertions will act like a normal `assert`.
55
If things _do_ change, you're prompted again and can choose to accept and update the test or reject the change and let it fail.
61
56
62
- With a few exceptions, `auto_assert/1` acts very similarly to a normal
63
- `assert`. See the [macro docs](https://hexdocs.pm/mneme/Mneme.html#auto_assert/1)
64
- for a list of differences.
57
## A brief tour
58
59
To see Mneme in action without adding it to a project, you can download and run the standalone tour:
60
61
```shell
62
curl -o tour_mneme.exs https://raw.githubusercontent.com/zachallaun/mneme/main/examples/tour_mneme.exs
63
elixir tour_mneme.exs
64
```
65
65
66
66
## Quick start
67
67
 
@@ -106,20 106,34 @@ for a list of differences.
106
106
end
107
107
```
108
108
109
- ## Match patterns
109
## Pattern matching
110
110
111
- Mneme tries to generate match patterns that are equivalent to what a
112
- human (or at least a nice LLM) would write. Basic data types like
113
- strings, numbers, lists, tuples, etc. will be as you would expect.
111
Snapshot testing is a powerful tool, allowing you to ensure that your code behaves as expected, both now and in the future.
112
However, traditional snapshot testing can be brittle, breaking whenever there is any change, even ones that are inconsequential to what is being tested.
114
113
115
- Some values, however, do not have a literal representation that can be
116
- used in a pattern match. Pids are such an example. For those, guards
117
- are used:
114
Mneme addresses this by introducing Elixir's pattern matching to snapshot testing.
115
With pattern matching, tests become more flexible, only failing when a change affects the expected structure.
116
This allows you to focus on changes that matter, saving time and reducing noise in tests.
117
118
To facilitate this, Mneme generates the patterns that you were likely to have written yourself.
119
If a value contains some input variable in its structure, Mneme will try to use a pinned variable (e.g. `^date`).
120
If the value is an Ecto struct, Mneme will omit autogenerated fields like timestamps that are likely to change with every run.
121
And if Mneme doesn't get it quite right, you can update the pattern yourself -- you won't be prompted unless the pattern no longer matches.
122
123
## Generated patterns
124
125
Mneme tries to generate match patterns that are equivalent to what a human (or at least a nice LLM) would write.
126
Basic data types like strings, numbers, lists, tuples, etc. will be as you would expect.
127
128
Some values, however, do not have a literal representation that can be used in a pattern match.
129
Pids are such an example.
130
For those, guards are used:
118
131
119
132
```elixir
120
133
auto_assert self()
121
134
122
- # after running the test and accepting the change
135
# generates:
136
123
137
auto_assert pid when is_pid(pid) <- self()
124
138
```
125
139
 
@@ -134,7 148,8 @@ test "create_post/1 creates a new post with valid attrs", %{user: user} do
134
148
auto_assert create_post(valid_attrs)
135
149
end
136
150
137
- # after running the test
151
# generates:
152
138
153
test "create_post/1 creates a new post with valid attrs", %{user: user} do
139
154
valid_attrs = %{title: "my_post", author: user}
changed hex_metadata.config
 
@@ -14,8 14,7 @@
14
14
<<"lib/mneme/prompter/terminal.ex">>,<<"lib/mneme/prompter.ex">>,
15
15
<<"lib/mneme/patcher.ex">>,<<"lib/mneme/errors.ex">>,
16
16
<<"lib/mneme/assertion">>,<<"lib/mneme/assertion/pattern_builder.ex">>,
17
- <<"lib/mneme/assertion/pattern.ex">>,<<"lib/.elixir_ls">>,
18
- <<"lib/.elixir_ls/.gitignore">>,<<"lib/mneme.ex">>,<<"priv">>,
17
<<"lib/mneme/assertion/pattern.ex">>,<<"lib/mneme.ex">>,<<"priv">>,
19
18
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}.
20
19
{<<"licenses">>,[<<"MIT">>]}.
21
20
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/zachallaun/mneme">>}]}.
 
@@ -41,4 40,4 @@
41
40
{<<"optional">>,false},
42
41
{<<"repository">>,<<"hexpm">>},
43
42
{<<"requirement">>,<<"~> 0.6.0">>}]]}.
44
- {<<"version">>,<<"0.3.3">>}.
43
{<<"version">>,<<"0.3.4">>}.
removed lib/.elixir_ls/.gitignore
 
@@ -1 0,0 @@
1
- *
\ No newline at end of file
changed lib/mneme/assertion.ex
 
@@ -532,7 532,7 @@ defmodule Mneme.Assertion do
532
532
def code_for_eval(:auto_assert_raise, {_, _, [exception, message, _]}, e) do
533
533
quote do
534
534
assert_raise unquote(exception), unquote(unescape_strings(message)), fn ->
535
- raise unquote(Macro.escape(e))
535
raise unquote(quote_exception(e))
536
536
end
537
537
end
538
538
end
 
@@ -540,7 540,7 @@ defmodule Mneme.Assertion do
540
540
def code_for_eval(:auto_assert_raise, {_, _, [exception, _]}, e) do
541
541
quote do
542
542
assert_raise unquote(exception), fn ->
543
- raise unquote(Macro.escape(e))
543
raise unquote(quote_exception(e))
544
544
end
545
545
end
546
546
end
 
@@ -661,4 661,14 @@ defmodule Mneme.Assertion do
661
661
662
662
defp meta({_, meta, _}), do: meta
663
663
defp meta(_), do: []
664
665
defp quote_exception(%exception{} = err) do
666
kvs =
667
err
668
|> Map.from_struct()
669
|> Map.delete(:__exception__)
670
|> Enum.map(&Macro.escape/1)
671
672
{:%, [], [exception, {:%{}, [], kvs}]}
673
end
664
674
end
changed lib/mneme/assertion/pattern.ex
 
@@ -18,4 18,24 @@ defmodule Mneme.Assertion.Pattern do
18
18
def new(expr, other_attrs \\ []) do
19
19
struct(Pattern, Keyword.put(other_attrs, :expr, expr))
20
20
end
21
22
@doc """
23
Combines a list of patterns into a single list pattern.
24
"""
25
def combine(patterns) when is_list(patterns) do
26
{exprs, {guard, notes}} =
27
Enum.map_reduce(
28
patterns,
29
{nil, []},
30
fn %Pattern{expr: expr, guard: g1, notes: n1}, {g2, n2} ->
31
{expr, {combine_guards(g1, g2), n1 n2}}
32
end
33
)
34
35
Pattern.new(exprs, guard: guard, notes: notes)
36
end
37
38
defp combine_guards(nil, guard), do: guard
39
defp combine_guards(guard, nil), do: guard
40
defp combine_guards(g1, {_, meta, _} = g2), do: {:and, meta, [g2, g1]}
21
41
end
changed lib/mneme/assertion/pattern_builder.ex
 
@@ -3,17 3,31 @@ defmodule Mneme.Assertion.PatternBuilder do
3
3
4
4
alias Mneme.Assertion.Pattern
5
5
6
@map_key_pattern_error :__map_key_pattern_error__
7
6
8
@doc """
7
9
Builds pattern expressions from a runtime value.
8
10
"""
9
- @spec to_patterns(term(), context :: map()) :: [Pattern.t(), ...]
11
@spec to_patterns(term(), Mneme.Assertion.context()) :: [Pattern.t(), ...]
10
12
def to_patterns(value, context) do
11
- context = Map.put(context, :keysets, get_keysets(context.original_pattern))
12
- patterns = do_to_patterns(value, context)
13
context =
14
context
15
|> Map.take([:line, :aliases, :binding, :original_pattern])
16
|> Map.put_new(:aliases, [])
17
|> Map.put_new(:binding, [])
18
|> Map.put(:keysets, get_keysets(context.original_pattern))
19
|> Map.put(:map_key_pattern?, false)
20
21
{patterns, _vars} = to_patterns(value, context, [])
22
patterns
23
end
24
25
defp to_patterns(value, context, vars) do
26
{patterns, vars} = do_to_patterns(value, context, vars)
13
27
14
28
case fetch_pinned(value, context) do
15
- {:ok, pin} -> [Pattern.new(pin) | patterns]
16
- :error -> patterns
29
{:ok, pin} -> {[Pattern.new(pin) | patterns], vars}
30
:error -> {patterns, vars}
17
31
end
18
32
end
19
33
 
@@ -46,79 60,101 @@ defmodule Mneme.Assertion.PatternBuilder do
46
60
47
61
defp fetch_pinned(_, _), do: :error
48
62
49
- defp do_to_patterns(int, context) when is_integer(int) do
50
- {:__block__, with_meta([token: inspect(int)], context), [int]}
51
- |> Pattern.new()
52
- |> List.wrap()
63
@spec do_to_patterns(term(), map(), [atom()]) :: {[Pattern.t(), ...], [atom()]}
64
defp do_to_patterns(value, context, vars)
65
66
defp do_to_patterns(int, context, vars) when is_integer(int) do
67
pattern = Pattern.new({:__block__, with_meta([token: inspect(int)], context), [int]})
68
{[pattern], vars}
53
69
end
54
70
55
- defp do_to_patterns(value, _context) when is_atom(value) or is_float(value) do
56
- [Pattern.new(value)]
71
defp do_to_patterns(value, _context, vars) when is_atom(value) or is_float(value) do
72
{[Pattern.new(value)], vars}
57
73
end
58
74
59
- defp do_to_patterns(string, context) when is_binary(string) do
60
- cond do
61
- !String.printable?(string) ->
62
- [Pattern.new({:<<>>, [], String.to_charlist(string)})]
75
defp do_to_patterns(string, context, vars) when is_binary(string) do
76
patterns =
77
cond do
78
!String.printable?(string) ->
79
[Pattern.new({:<<>>, [], :erlang.binary_to_list(string)})]
63
80
64
- String.contains?(string, "\n") ->
65
- [string_pattern(string, context), heredoc_pattern(string, context)]
81
String.contains?(string, "\n") ->
82
[string_pattern(string, context), heredoc_pattern(string, context)]
66
83
67
- true ->
68
- [string_pattern(string, context)]
69
- end
84
true ->
85
[string_pattern(string, context)]
86
end
87
88
{patterns, vars}
70
89
end
71
90
72
- defp do_to_patterns([], _), do: [Pattern.new([])]
91
defp do_to_patterns([], _, vars), do: {[Pattern.new([])], vars}
73
92
74
- defp do_to_patterns(list, context) when is_list(list) do
75
- patterns = enum_to_patterns(list, context)
93
defp do_to_patterns(list, context, vars) when is_list(list) do
94
{patterns, vars} = enum_to_patterns(list, context, vars)
76
95
77
96
if List.ascii_printable?(list) do
78
- patterns [Pattern.new(list)]
97
{patterns [charlist_pattern(list, context)], vars}
79
98
else
80
- patterns
99
{patterns, vars}
81
100
end
82
101
end
83
102
84
- defp do_to_patterns(tuple, context) when is_tuple(tuple) do
85
- tuple
86
- |> Tuple.to_list()
87
- |> enum_to_patterns(context)
88
- |> Enum.map(&to_tuple_pattern(&1, context))
103
defp do_to_patterns(tuple, context, vars) when is_tuple(tuple) do
104
{patterns, vars} =
105
tuple
106
|> Tuple.to_list()
107
|> enum_to_patterns(context, vars)
108
109
{Enum.map(patterns, &list_pattern_to_tuple_pattern(&1, context)), vars}
89
110
end
90
111
91
- for {var_name, guard} <- [ref: :is_reference, pid: :is_pid, port: :is_port] do
92
- defp do_to_patterns(value, context) when unquote(guard)(value) do
93
- [guard_pattern(unquote(var_name), unquote(guard), value, context)]
112
for {var_name, guard} <- [ref: :is_reference, pid: :is_pid, port: :is_port, fun: :is_function] do
113
defp do_to_patterns(value, context, vars) when unquote(guard)(value) do
114
if context.map_key_pattern?, do: throw({@map_key_pattern_error, value})
115
116
{pattern, vars} = guard_pattern(unquote(var_name), unquote(guard), value, context, vars)
117
{[pattern], vars}
94
118
end
95
119
end
96
120
97
121
for module <- [Range, Regex, DateTime, NaiveDateTime, Date, Time] do
98
- defp do_to_patterns(%unquote(module){} = value, context) do
122
defp do_to_patterns(%unquote(module){} = value, context, vars) do
99
123
{call, meta, args} = value |> inspect() |> Code.string_to_quoted!()
100
- [Pattern.new({call, with_meta(meta, context), args})]
124
pattern = Pattern.new({call, with_meta(meta, context), args})
125
{[pattern], vars}
101
126
end
102
127
end
103
128
104
- defp do_to_patterns(%URI{} = uri, context) do
105
- struct_to_patterns(URI, Map.delete(uri, :authority), context, [])
129
defp do_to_patterns(%URI{} = uri, context, vars) do
130
struct_to_patterns(URI, Map.delete(uri, :authority), context, vars, [])
106
131
end
107
132
108
- defp do_to_patterns(%struct{} = value, context) do
133
defp do_to_patterns(%MapSet{} = set, context, vars) do
134
struct_to_patterns(MapSet, set, context, vars, [
135
"MapSets do not serialize well, consider transforming to a list using `MapSet.to_list/1`"
136
])
137
end
138
139
defp do_to_patterns(%struct{} = value, context, vars) do
109
140
if ecto_schema?(struct) do
110
141
{value, notes} = prepare_ecto_struct(value)
111
- struct_to_patterns(struct, value, context, notes)
142
struct_to_patterns(struct, value, context, vars, notes)
112
143
else
113
- struct_to_patterns(struct, value, context, [])
144
struct_to_patterns(struct, value, context, vars, [])
114
145
end
115
146
end
116
147
117
- defp do_to_patterns(%{} = map, context) when map_size(map) == 0 do
118
- [map_pattern([], context)]
148
defp do_to_patterns(map, context, vars) when map_size(map) == 0 do
149
{[Pattern.new({:%{}, with_meta(context), []})], vars}
119
150
end
120
151
121
- defp do_to_patterns(%{} = map, context) do
152
defp do_to_patterns(map, %{map_key_pattern?: true} = context, vars) when is_map(map) do
153
{patterns, vars} = enumerate_map_patterns(map, context, vars)
154
{[List.last(patterns)], vars}
155
end
156
157
defp do_to_patterns(map, context, vars) when is_map(map) do
122
158
sub_maps =
123
159
for keyset <- context.keysets,
124
160
sub_map = Map.take(map, keyset),
 
@@ -126,37 162,93 @@ defmodule Mneme.Assertion.PatternBuilder do
126
162
sub_map
127
163
end
128
164
129
- patterns =
130
- for map <- sub_maps [map],
131
- enum_pattern <- enum_to_patterns(map, context) do
132
- to_map_pattern(enum_pattern, context)
133
- end
165
{patterns, vars} =
166
(sub_maps [map])
167
|> Enum.flat_map_reduce(vars, fn map, vars ->
168
enumerate_map_patterns(map, context, vars)
169
end)
134
170
135
- [map_pattern([], context) | patterns]
171
if contains_empty_map_pattern?(patterns) do
172
{patterns, vars}
173
else
174
{[Pattern.new({:%{}, with_meta(context), []}) | patterns], vars}
175
end
136
176
end
137
177
138
- defp struct_to_patterns(struct, map, context, extra_notes) do
178
defp contains_empty_map_pattern?(patterns) do
179
Enum.any?(patterns, fn
180
%Pattern{expr: {:%{}, _, []}} -> true
181
_ -> false
182
end)
183
end
184
185
defp enumerate_map_patterns(map, context, vars) do
186
{nested_patterns, {vars, bad_map_keys}} =
187
map
188
|> Enum.sort_by(&elem(&1, 0))
189
|> Enum.flat_map_reduce({vars, []}, fn {k, v}, {vars, bad_map_keys} ->
190
try do
191
{k_patterns, vars} = to_patterns(k, %{context | map_key_pattern?: true}, vars)
192
{v_patterns, vars} = to_patterns(v, context, vars)
193
194
tuples =
195
[k_patterns, v_patterns]
196
|> combine_nested()
197
|> Enum.map(&list_pattern_to_tuple_pattern(&1, context))
198
199
{[tuples], {vars, bad_map_keys}}
200
catch
201
{@map_key_pattern_error, bad_key} ->
202
if context.map_key_pattern?, do: throw({@map_key_pattern_error, bad_key})
203
{[], {vars, [bad_key | bad_map_keys]}}
204
end
205
end)
206
207
map_patterns =
208
nested_patterns
209
|> combine_nested()
210
|> Enum.map(&keyword_pattern_to_map_pattern(&1, context))
211
212
{maybe_bad_map_key_notes(map_patterns, bad_map_keys), vars}
213
end
214
215
defp maybe_bad_map_key_notes(patterns, []), do: patterns
216
217
defp maybe_bad_map_key_notes(patterns, keys) do
218
note = "Cannot match on following values in map keys: #{inspect(keys)}"
219
220
Enum.map(patterns, fn pattern ->
221
%{pattern | notes: [note | pattern.notes]}
222
end)
223
end
224
225
defp struct_to_patterns(struct, map, context, vars, extra_notes) do
139
226
defaults = struct.__struct__()
140
227
141
- map
142
- |> Map.filter(fn {k, v} -> v != Map.get(defaults, k) end)
143
- |> to_patterns(context)
144
- |> Enum.map(&to_struct_pattern(struct, &1, context, extra_notes))
228
{patterns, vars} =
229
map
230
|> Map.filter(fn {k, v} -> v != Map.get(defaults, k) end)
231
|> to_patterns(context, vars)
232
233
{Enum.map(patterns, &map_to_struct_pattern(&1, struct, context, extra_notes)), vars}
145
234
end
146
235
147
- defp enum_to_patterns(values, context) do
148
- values
149
- |> Enum.map(&to_patterns(&1, context))
150
- |> unzip_combine(context)
236
defp enum_to_patterns(values, context, vars) do
237
{nested_patterns, vars} = Enum.map_reduce(values, vars, &to_patterns(&1, context, &2))
238
{combine_nested(nested_patterns), vars}
151
239
end
152
240
153
- defp unzip_combine(nested_patterns, context, acc \\ []) do
241
# Combines element-wise patterns into patterns of the same length such
242
# that all patterns are present in the result:
243
# [[a1, a2], [b1], [c1, c2, c3]]
244
# => [[a1, b1, c1], [a2, b1, c2], [a2, b1, c3]]
245
defp combine_nested(nested_patterns, acc \\ []) do
154
246
if last_pattern?(nested_patterns) do
155
- {patterns, _} = combine_and_pop(nested_patterns, context)
247
{patterns, _} = combine_and_pop(nested_patterns)
156
248
Enum.reverse([patterns | acc])
157
249
else
158
- {patterns, rest} = combine_and_pop(nested_patterns, context)
159
- unzip_combine(rest, context, [patterns | acc])
250
{patterns, rest} = combine_and_pop(nested_patterns)
251
combine_nested(rest, [patterns | acc])
160
252
end
161
253
end
162
254
 
@@ -167,61 259,56 @@ defmodule Mneme.Assertion.PatternBuilder do
167
259
end)
168
260
end
169
261
170
- defp combine_and_pop(nested_patterns, context) do
262
defp combine_and_pop(nested_patterns) do
171
263
{patterns, rest_patterns} =
172
264
nested_patterns
173
265
|> Enum.map(&pop_pattern/1)
174
266
|> Enum.unzip()
175
267
176
- {combine_patterns(patterns, context), rest_patterns}
268
{Pattern.combine(patterns), rest_patterns}
177
269
end
178
270
179
- defp pop_pattern([current, next | rest]), do: {current, [next | rest]}
180
- defp pop_pattern([current]), do: {current, [current]}
271
defp pop_pattern([x | [_ | _] = xs]), do: {x, xs}
272
defp pop_pattern([x]), do: {x, [x]}
181
273
182
- defp combine_patterns(patterns, context) do
183
- {exprs, {guard, notes}} =
184
- Enum.map_reduce(
185
- patterns,
186
- {nil, []},
187
- fn %Pattern{expr: expr, guard: g1, notes: n1}, {g2, n2} ->
188
- {expr, {combine_guards(g1, g2, context), n1 n2}}
274
defp guard_pattern(name, guard, value, context, vars) do
275
if existing = List.keyfind(vars, value, 1) do
276
{var, _} = existing
277
{Pattern.new(make_var(var, context)), vars}
278
else
279
{var_name, _, _} = var = make_unique_var(name, context, vars)
280
281
guard =
282
if is_function(value) do
283
{:arity, arity} = Function.info(value, :arity)
284
{guard, with_meta(context), [var, arity]}
285
else
286
{guard, with_meta(context), [var]}
189
287
end
190
- )
191
288
192
- Pattern.new(exprs, guard: guard, notes: notes)
289
pattern =
290
Pattern.new(var,
291
guard: guard,
292
notes: ["Using guard for non-serializable value `#{inspect(value)}`"]
293
)
294
295
{pattern, [{var_name, value} | vars]}
296
end
193
297
end
194
298
195
- defp combine_guards(nil, guard, _context), do: guard
196
- defp combine_guards(guard, nil, _context), do: guard
197
- defp combine_guards(g1, g2, context), do: {:and, with_meta(context), [g2, g1]}
198
-
199
- defp guard_pattern(name, guard, value, context) do
200
- var = make_var(name, context)
201
-
202
- Pattern.new(var,
203
- guard: {guard, with_meta(context), [var]},
204
- notes: ["Using guard for non-serializable value `#{inspect(value)}`"]
205
- )
206
- end
207
-
208
- defp make_var(name, context) do
209
- {name, with_meta(context), nil}
210
- end
211
-
212
- defp to_tuple_pattern(%Pattern{expr: [e1, e2]} = pattern, _context) do
299
defp list_pattern_to_tuple_pattern(%Pattern{expr: [e1, e2]} = pattern, _context) do
213
300
%{pattern | expr: {e1, e2}}
214
301
end
215
302
216
- defp to_tuple_pattern(%Pattern{expr: exprs} = pattern, context) do
303
defp list_pattern_to_tuple_pattern(%Pattern{expr: exprs} = pattern, context) do
217
304
%{pattern | expr: {:{}, with_meta(context), exprs}}
218
305
end
219
306
220
- defp to_map_pattern(%Pattern{expr: tuples} = pattern, context) do
221
- %{pattern | expr: map_pattern(tuples, context).expr}
307
defp keyword_pattern_to_map_pattern(%Pattern{expr: tuples} = pattern, context) do
308
%{pattern | expr: {:%{}, with_meta(context), tuples}}
222
309
end
223
310
224
- defp to_struct_pattern(struct, map_pattern, context, extra_notes) do
311
defp map_to_struct_pattern(map_pattern, struct, context, extra_notes) do
225
312
{aliased, _} =
226
313
context
227
314
|> Map.get(:aliases, [])
 
@@ -283,14 370,17 @@ defmodule Mneme.Assertion.PatternBuilder do
283
370
end
284
371
end
285
372
286
- defp map_pattern(tuples, context) do
287
- Pattern.new({:%{}, with_meta(context), tuples})
288
- end
289
-
290
373
defp string_pattern(string, context) do
291
374
Pattern.new({:__block__, with_meta([delimiter: ~S(")], context), [escape(string)]})
292
375
end
293
376
377
defp charlist_pattern(charlist, context) do
378
Pattern.new(
379
{:sigil_c, with_meta([delimiter: ~S(")], context),
380
[{:<<>>, [], [List.to_string(charlist)]}, []]}
381
)
382
end
383
294
384
defp heredoc_pattern(string, context) do
295
385
Pattern.new(
296
386
{:__block__, with_meta([delimiter: ~S(""")], context),
 
@@ -309,4 399,23 @@ defmodule Mneme.Assertion.PatternBuilder do
309
399
defp escape(string) when is_binary(string) do
310
400
String.replace(string, "\\", "\\\\")
311
401
end
402
403
defp make_var(name, context) do
404
{name, with_meta(context), nil}
405
end
406
407
defp make_unique_var(name, context, vars) do
408
vars = Keyword.keys(context[:binding] vars)
409
name = get_unique_name(name, vars)
410
make_var(name, context)
411
end
412
413
defp get_unique_name(name, var_names) do
414
if(name in var_names, do: get_unique_name(name, var_names, 1), else: name)
415
end
416
417
defp get_unique_name(name, var_names, i) do
418
name_i = :"#{name}#{i}"
419
if(name_i in var_names, do: get_unique_name(name, var_names, i 1), else: name_i)
420
end
312
421
end
changed lib/mneme/options.ex
 
@@ -64,6 64,11 @@ defmodule Mneme.Options do
64
64
]
65
65
66
66
@private_options [
67
dry_run: [
68
type: :boolean,
69
default: false,
70
doc: "Prevents changes from being written to the file when `true`."
71
],
67
72
prompter: [
68
73
type: :atom,
69
74
default: Mneme.Prompter.Terminal,
changed lib/mneme/patcher.ex
 
@@ -41,30 41,26 @@ defmodule Mneme.Patcher do
41
41
"""
42
42
@spec finalize!(state) :: :ok | {:error, term()}
43
43
def finalize!(project) do
44
- if Application.get_env(:mneme, :dry_run) do
44
unsaved_files =
45
project
46
|> Project.sources()
47
|> Enum.flat_map(fn source ->
48
file = Source.path(source)
49
50
if source.private[:hash] == content_hash(file) do
51
case Source.save(source) do
52
:ok -> []
53
_ -> [file]
54
end
55
else
56
[file]
57
end
58
end)
59
60
if unsaved_files == [] do
45
61
:ok
46
62
else
47
- unsaved_files =
48
- project
49
- |> Project.sources()
50
- |> Enum.flat_map(fn source ->
51
- file = Source.path(source)
52
-
53
- if source.private[:hash] == content_hash(file) do
54
- case Source.save(source) do
55
- :ok -> []
56
- _ -> [file]
57
- end
58
- else
59
- [file]
60
- end
61
- end)
62
-
63
- if unsaved_files == [] do
64
- :ok
65
- else
66
- {:error, {:not_saved, unsaved_files}}
67
- end
63
{:error, {:not_saved, unsaved_files}}
68
64
end
69
65
end
70
66
 
@@ -101,7 97,11 @@ defmodule Mneme.Patcher do
101
97
ast = replace_assertion_node(node, assertion.code)
102
98
source = Source.update(source, :mneme, ast: ast)
103
99
104
- {{:ok, assertion}, Project.update(project, source)}
100
if opts.dry_run do
101
{{:ok, assertion}, project}
102
else
103
{{:ok, assertion}, Project.update(project, source)}
104
end
105
105
106
106
:reject ->
107
107
{{:error, :rejected}, project}
changed lib/mneme/prompter/terminal.ex
 
@@ -10,7 10,6 @@ defmodule Mneme.Prompter.Terminal do
10
10
@middle_dot_char "·"
11
11
@bullet_char "●"
12
12
@empty_bullet_char "○"
13
- @info_char "🛈"
14
13
@arrow_left_char "❮"
15
14
@arrow_right_char "❯"
16
15
 
@@ -243,11 242,10 @@ defmodule Mneme.Prompter.Terminal do
243
242
notes = Enum.uniq(notes)
244
243
245
244
[
246
- "\n#{@info_char} Notes about this assertion:\n",
247
- notes |> Owl.Data.unlines() |> Owl.Data.add_prefix(" * "),
245
"\n",
246
notes |> Owl.Data.unlines() |> Owl.Data.add_prefix(tag("Note: ", :magenta)),
248
247
"\n"
249
248
]
250
- |> tag(:faint)
251
249
end
252
250
253
251
defp format_input(%{stage: stage} = assertion, opts) do
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.3"
7
def version, do: "0.3.4"
8
8
9
9
def project do
10
10
[
 
@@ -49,6 49,7 @@ defmodule Mneme.MixProject do
49
49
# is merged:
50
50
# {:excoveralls, "~> 0.15", only: :test},
51
51
{:ecto, "~> 3.9", only: :test},
52
{:stream_data, "~> 0.5.0", only: [:dev, :test]},
52
53
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
53
54
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
54
55
{:makeup_json, ">= 0.0.0", only: :dev, runtime: false},