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},
|