changed README.md
 
@@ -1,20 1,9 @@
1
1
# Mneme /ni:mi:/
2
2
3
- Mneme helps you write tests.
4
-
5
- [Documentation on hexdocs.pm](https://hexdocs.pm/mneme)
6
-
7
- Inspired by [Jane Street's expect-test](https://github.com/janestreet/ppx_expect), which you can [read about here](https://blog.janestreet.com/the-joy-of-expect-tests/).
8
-
9
- Prior art: [assert_value](https://github.com/assert-value/assert_value_elixir)
3
Mneme helps you write tests. [Documentation on hexdocs.pm](https://hexdocs.pm/mneme).
10
4
11
5
## Installation
12
6
13
- **MNEME IS NOT READY FOR GENERAL USE!**
14
- If you're finding it published on Hex, it's only so that I can be my own guinea pig and use it while working on other libraries.
15
-
16
- If available in Hex, the package can be installed by adding `mneme` to your list of dependencies in `mix.exs`:
17
-
18
7
```elixir
19
8
def deps do
20
9
[
 
@@ -22,28 11,3 @@ def deps do
22
11
]
23
12
end
24
13
```
25
-
26
- ## VSCode Integration
27
-
28
- For happier times in VSCode, set up ElixirLS, copy the `.vscode/tasks.json` from this repository to your workspace, and then add the following keybindings:
29
-
30
- ```json
31
- [
32
- {
33
- "key": "ctrl ; a",
34
- "command": "workbench.action.tasks.runTask",
35
- "args": "mix: Run tests"
36
- },
37
- {
38
- "key": "ctrl ; f",
39
- "command": "workbench.action.tasks.runTask",
40
- "args": "mix: Run tests in current file"
41
- },
42
- {
43
- "key": "ctrl ; c",
44
- "command": "workbench.action.tasks.runTask",
45
- "args": "mix: Run test at cursor"
46
- },
47
- ]
48
- ```
49
-
changed hex_metadata.config
 
@@ -1,14 1,15 @@
1
1
{<<"app">>,<<"mneme">>}.
2
2
{<<"build_tools">>,[<<"mix">>]}.
3
- {<<"description">>,<<"Semi-automated snapshot testing with ExUnit">>}.
3
{<<"description">>,<<"Snapshot/approval testing integrated into ExUnit">>}.
4
4
{<<"elixir">>,<<"~> 1.14">>}.
5
5
{<<"files">>,
6
- [<<"lib">>,<<"lib/mneme">>,<<"lib/mneme/serializer.ex">>,
7
- <<"lib/mneme/ex_unit_formatter.ex">>,<<"lib/mneme/server.ex">>,
8
- <<"lib/mneme/utils.ex">>,<<"lib/mneme/options.ex">>,
9
- <<"lib/mneme/assertion.ex">>,<<"lib/mneme/prompter">>,
10
- <<"lib/mneme/prompter/terminal.ex">>,<<"lib/mneme/prompter.ex">>,
11
- <<"lib/mneme/patcher.ex">>,<<"lib/mneme.ex">>,<<"priv">>,<<"priv/plts">>,
6
[<<"lib">>,<<"lib/mneme">>,<<"lib/mneme/ex_unit_formatter.ex">>,
7
<<"lib/mneme/server.ex">>,<<"lib/mneme/utils.ex">>,
8
<<"lib/mneme/options.ex">>,<<"lib/mneme/assertion.ex">>,
9
<<"lib/mneme/prompter">>,<<"lib/mneme/prompter/terminal.ex">>,
10
<<"lib/mneme/prompter.ex">>,<<"lib/mneme/patcher.ex">>,
11
<<"lib/mneme/assertion">>,<<"lib/mneme/assertion/builder.ex">>,
12
<<"lib/mneme.ex">>,<<"priv">>,<<"priv/plts">>,
12
13
<<"priv/plts/dialyzer.plt.hash">>,<<"priv/plts/dialyzer.plt">>,
13
14
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>]}.
14
15
{<<"licenses">>,[<<"MIT">>]}.
 
@@ -35,4 36,4 @@
35
36
{<<"optional">>,false},
36
37
{<<"repository">>,<<"hexpm">>},
37
38
{<<"requirement">>,<<"~> 0.6.0">>}]]}.
38
- {<<"version">>,<<"0.0.4">>}.
39
{<<"version">>,<<"0.0.5">>}.
changed lib/mneme.ex
 
@@ -1,13 1,21 @@
1
1
defmodule Mneme do
2
@external_resource "mix.exs"
2
3
@moduledoc """
3
- /ni:mi:/ - Snapshot testing for regular ol' Elixir code.
4
/ni:mi:/ - Snapshot testing integrated into ExUnit.
4
5
5
- Mneme helps you write tests using `auto_assert/1`, a replacement of
6
- sorts for ExUnit's `assert`. With `auto_assert`, you write an
7
- expression and Mneme updates the assertion based on the runtime value.
6
Snapshot tests assert that some expression matches a reference value.
7
It's like an ExUnit `assert`, except that the reference value is
8
managed for you by Mneme.
8
9
9
- For example, let's say you've written a test for a function that
10
- removes even numbers from a list:
10
Mneme follows in the footsteps of existing snapshot testing libraries
11
like [Insta](https://insta.rs/) (Rust), [expect-test](https://github.com/janestreet/ppx_expect)
12
(OCaml), and [assert_value](https://github.com/assert-value/assert_value_elixir)
13
(Elixir).
14
15
## Example
16
17
Let's say you've written a test for a function that removes even
18
numbers from a list:
11
19
12
20
test "drop_evens/1 should remove all even numbers from an enum" do
13
21
auto_assert drop_evens(1..10)
 
@@ -17,9 25,10 @@ defmodule Mneme do
17
25
auto_assert drop_evens([:a, :b, 2, :c])
18
26
end
19
27
20
- The first time you run this test, you'll receive three prompts
21
- (complete with diffs) asking if you'd like to update each of these
22
- expressions. After accepting, your test is re-written:
28
The first time you run this test, you'll see interactive prompts for
29
each call to `auto_assert` showing a diff and asking if you'd like to
30
accept the generated pattern. After accepting them, your test is
31
updated:
23
32
24
33
test "drop_evens/1 should remove all even numbers from an enum" do
25
34
auto_assert [1, 3, 5, 7, 9] <- drop_evens(1..10)
 
@@ -30,41 39,62 @@ defmodule Mneme do
30
39
end
31
40
32
41
The next time you run this test, you won't receive a prompt and these
33
- will act (almost) like any other assertion. (See `auto_assert/1` for
34
- details on the differences from ExUnit's `assert`.)
42
will act (almost) like any other assertion. If the result of the call
43
ever changes, you'll be prompted again and can choose to update the
44
test or reject it and let it fail.
35
45
36
- ## Setup
46
With a few exceptions, `auto_assert/1` acts very similarly to a normal
47
`assert`. See the [macro docs](`auto_assert/1`) for a list of
48
differences.
37
49
38
- # 1) add :mneme to your :import_deps in .formatter.exs
39
- [
40
- import_deps: [:mneme],
41
- inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
42
- ]
50
## Quick start
43
51
44
- # 2) start Mneme right after you start ExUnit in test/test_helper.exs
45
- ExUnit.start()
46
- Mneme.start()
52
1. Add `:mneme` do your deps in `mix.exs`:
47
53
48
- # test/my_test.exs
49
- defmodule MyTest do
50
- use ExUnit.Case, async: true
54
```
55
defp deps do
56
[
57
{:mneme, "~> #{Mneme.MixProject.version()}", only: :test}
58
]
59
end
60
```
51
61
52
- # 3) use Mneme wherever you use ExUnit.Case
53
- use Mneme
62
2. Add `:mneme` to your `:import_deps` in `.formatter.exs`:
54
63
55
- test "arithmetic" do
56
- # 4) use auto_assert instead of ExUnit's assert - run this test
57
- # and delight in all the typing you don't have to do
58
- auto_assert 2 2
59
- end
60
- end
64
```
65
[
66
import_deps: [:mneme],
67
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
68
]
69
```
70
71
3. Start Mneme right after you start ExUnit in `test/test_helper.exs`:
72
73
```
74
ExUnit.start()
75
Mneme.start()
76
```
77
78
4. Add `use Mneme` wherever you `use ExUnit.Case`:
79
80
```
81
defmodule MyTest do
82
use ExUnit.Case, async: true
83
use Mneme
84
85
test "arithmetic" do
86
# use auto_assert instead of ExUnit's assert - run this test
87
# and delight in all the typing you don't have to do
88
auto_assert 2 2
89
end
90
end
91
```
61
92
62
93
## Match patterns
63
94
64
95
Mneme tries to generate match patterns that are equivalent to what a
65
96
human (or at least a nice LLM) would write. Basic data types like
66
- strings, numbers, lists, tuples, etc. will serialize as you would
67
- expect.
97
strings, numbers, lists, tuples, etc. will be as you would expect.
68
98
69
99
Some values, however, do not have a literal representation that can be
70
100
used in a pattern match. Pids are such an example. For those, guards
 
@@ -75,9 105,9 @@ defmodule Mneme do
75
105
# after running the test and accepting the change
76
106
auto_assert pid when is_pid(pid) <- self()
77
107
78
- Additionally, variables in scope of the assertion will be pinned if
79
- they match the value of the expression. This is especially helpful
80
- when testing Ecto structs with associations, for instance:
108
Additionally, local variables can be found and pinned as a part of the
109
pattern. This keeps the number of hard-coded values down, reducing the
110
likelihood that tests have to be updated in the future.
81
111
82
112
test "create_post/1 creates a new post with valid attrs", %{user: user} do
83
113
valid_attrs = %{title: "my_post", author: user}
 
@@ -101,7 131,7 @@ defmodule Mneme do
101
131
* Pinned variables are generated by default if a value is equal to a
102
132
variable in scope.
103
133
104
- * Date and time values serialize to their sigil representation.
134
* Date and time values are written using their sigil representation.
105
135
106
136
* Struct patterns only include fields that are different from the
107
137
struct defaults.
 
@@ -145,27 175,40 @@ defmodule Mneme do
145
175
end
146
176
end
147
177
148
- See `__using__/1` for a description of available options.
178
### Options
179
180
#{Mneme.Options.docs()}
149
181
150
182
## Formatting
151
183
152
- Mneme uses the [`Rewrite`](https://github.com/hrzndhrn/rewrite) to
153
- update source code, which formats code before it is saved to a file.
184
Mneme uses [`Rewrite`](https://github.com/hrzndhrn/rewrite) to update
185
source source code, formatting that code before saving the file.
154
186
Currently, the Elixir formatter and `FreedomFormatter` are supported.
155
- If you do not use a formatter, the first auto-assertion will reformat
156
- the entire file.
187
**If you do not use a formatter, the first auto-assertion will reformat
188
the entire file.**
189
190
## Continuous Integration
191
192
In a CI environment, Mneme will not attempt to prompt and update any
193
assertions. This behavior is enabled by the `CI` environment variable,
194
which is set by convention by many continuous integration providers.
195
196
```bash
197
export CI=true
198
```
199
200
## Editor support
201
202
Guides for optional editor integration can be found here:
203
204
* [`VS Code`](vscode_setup.md)
157
205
"""
158
206
159
207
@doc """
160
208
Sets up Mneme configuration for this module and imports `auto_assert/1`.
161
209
162
- ## Options
163
-
164
- Options passed to `use Mneme` can be overriden in `describe` blocks or
165
- for individual tests. See the "Configuration" section in the module
166
- documentation for more.
167
-
168
- #{Mneme.Options.docs()}
210
This macro accepts all options described in the "Configuration"
211
section above.
169
212
170
213
## Example
171
214
 
@@ -232,7 275,7 @@ defmodule Mneme do
232
275
Prompts are only issued if the pattern doesn't match the value, so
233
276
that pattern can also be changed manually.
234
277
235
- # this assertion succeeds
278
# this assertion succeeds, so no prompt is issued
236
279
auto_assert [1, 2, | _] <- [1, 2] [:a, :b]
237
280
238
281
## Differences from ExUnit `assert`
changed lib/mneme/assertion.ex
 
@@ -1,16 1,25 @@
1
defmodule Mneme.AssertionError do
2
defexception [:message]
3
end
4
1
5
defmodule Mneme.Assertion do
2
6
@moduledoc false
3
7
4
8
alias __MODULE__
5
- alias Mneme.Serializer
9
alias Mneme.Assertion.Builder
6
10
7
11
defstruct [
8
12
:type,
9
- :code,
10
13
:value,
11
- :context,
14
:code,
12
15
:eval,
16
:file,
17
:line,
18
:module,
19
:test,
13
20
:patterns,
21
aliases: [],
22
binding: [],
14
23
prev_patterns: []
15
24
]
16
25
 
@@ -18,105 27,102 @@ defmodule Mneme.Assertion do
18
27
Build an assertion.
19
28
"""
20
29
def build(code, caller) do
21
- {setup_assertion, eval_assertion} = code_for_setup_and_eval(code, caller)
30
quote do
31
value = unquote(value_expr(code))
22
32
23
- case get_type(code) do
24
- :new ->
25
- quote do
26
- unquote(setup_assertion)
33
assertion =
34
Mneme.Assertion.new(
35
unquote(Macro.escape(code)),
36
value,
37
unquote(assertion_context(caller)) |> Keyword.put(:binding, binding())
38
)
27
39
28
- case Mneme.Server.await_assertion(assertion) do
40
eval_binding = [{{:value, :mneme}, value} | binding()]
41
42
try do
43
case Mneme.Server.register_assertion(assertion) do
44
{:ok, assertion} ->
45
Code.eval_quoted(assertion.eval, eval_binding, __ENV__)
46
47
:error ->
48
raise Mneme.AssertionError, message: "No pattern present"
49
end
50
rescue
51
error in [ExUnit.AssertionError] ->
52
case Mneme.Server.patch_assertion(assertion) do
29
53
{:ok, assertion} ->
30
- unquote(eval_assertion)
54
Code.eval_quoted(assertion.eval, eval_binding, __ENV__)
31
55
32
56
:error ->
33
- raise ExUnit.AssertionError, message: "No match present"
34
- end
35
- end
36
-
37
- :update ->
38
- quote do
39
- unquote(setup_assertion)
40
-
41
- try do
42
- unquote(eval_assertion)
43
- rescue
44
- error in [ExUnit.AssertionError] ->
45
- case Mneme.Server.await_assertion(assertion) do
46
- {:ok, assertion} ->
47
- unquote(eval_assertion)
48
-
49
- :error ->
50
- case __STACKTRACE__ do
51
- [head | _] -> reraise error, [head]
52
- [] -> reraise error, []
53
- end
57
case __STACKTRACE__ do
58
[head | _] -> reraise error, [head]
59
[] -> reraise error, []
54
60
end
55
61
end
56
- end
62
end
63
64
value
57
65
end
58
66
end
59
67
60
- defp code_for_setup_and_eval(code, caller) do
61
- context = test_context(caller)
62
-
63
- setup =
64
- quote do
65
- var!(value, :mneme) = unquote(value_expr(code))
66
-
67
- assertion =
68
- Mneme.Assertion.new(
69
- unquote(Macro.escape(code)),
70
- var!(value, :mneme),
71
- unquote(Macro.escape(context)) |> Map.put(:binding, binding())
72
- )
73
-
74
- binding = binding() binding(:mneme)
75
- end
76
-
77
- eval =
78
- quote do
79
- {result, _} = Code.eval_quoted(assertion.eval, binding, __ENV__)
80
- result
81
- end
82
-
83
- {setup, eval}
84
- end
85
-
86
- defp test_context(caller) do
68
defp assertion_context(caller) do
87
69
{test, _arity} = caller.function
88
70
89
- %{
71
[
90
72
file: caller.file,
91
73
line: caller.line,
92
74
module: caller.module,
93
75
test: test,
94
- #
95
- # TODO: access aliases some other way.
96
- #
97
- # Env :aliases is considered private and should not be relied on,
98
- # but I'm not sure where else to access the alias information
99
- # needed. Macro.Env.fetch_alias/2 is a thing, but it goes from
100
- # alias to resolved module, and I need resolved module to alias.
101
- # E.g. Macro.Env.fetch_alias(env, Bar) might return {:ok, Foo.Bar},
102
- # but I have Foo.Bar and need to know that Bar is the alias in
103
- # the current environment.
76
# TODO: Macro.Env :aliases is technically private; so we should
77
# access some other way
104
78
aliases: caller.aliases
105
- }
79
]
106
80
end
107
81
108
82
@doc false
109
83
def new(code, value, context) do
110
- patterns = Serializer.to_patterns(value, context)
111
-
112
- %Assertion{
84
assertion = %Assertion{
113
85
type: get_type(code),
114
- code: code,
115
86
value: value,
116
- context: context,
117
- patterns: patterns,
118
- eval: code_for_eval(code, patterns)
87
code: code,
88
file: context[:file],
89
line: context[:line],
90
module: context[:module],
91
test: context[:test],
92
aliases: context[:aliases] || [],
93
binding: context[:binding] || []
119
94
}
95
96
{prev_patterns, patterns} = build_and_select_pattern(assertion)
97
98
Map.merge(assertion, %{
99
patterns: patterns,
100
prev_patterns: prev_patterns,
101
eval: code_for_eval(code, hd(patterns))
102
})
103
end
104
105
defp build_and_select_pattern(%{type: :new, value: value} = assertion) do
106
{[], Builder.to_patterns(value, assertion)}
107
end
108
109
defp build_and_select_pattern(%{type: :update, value: value, code: code} = assertion) do
110
{expr, guard} =
111
case code do
112
{_, _, [{:<-, _, [{:when, _, [expr, guard]}, _]}]} -> {expr, guard}
113
{_, _, [{:<-, _, [expr, _]}]} -> {expr, nil}
114
{_, _, [{:==, _, [_, expr]}]} -> {expr, nil}
115
end
116
117
Builder.to_patterns(value, assertion)
118
|> Enum.split_while(fn
119
{^expr, ^guard, _} -> false
120
_ -> true
121
end)
122
|> case do
123
{patterns, []} -> {[], patterns}
124
{prev_reverse, patterns} -> {Enum.reverse(prev_reverse), patterns}
125
end
120
126
end
121
127
122
128
defp get_type({_, _, [{op, _, [_, _]}]}) when op in [:<-, :==], do: :update
 
@@ -125,12 131,12 @@ defmodule Mneme.Assertion do
125
131
@doc """
126
132
Regenerate assertion code for the given target.
127
133
"""
128
- def regenerate_code(%Assertion{} = assertion, target) when target in [:auto_assert, :assert] do
134
def regenerate_code(%Assertion{} = assertion, target) do
129
135
new_code = to_code(assertion, target)
130
136
131
137
assertion
132
138
|> Map.put(:code, new_code)
133
- |> Map.put(:eval, code_for_eval(new_code, assertion.patterns))
139
|> Map.put(:eval, code_for_eval(new_code, hd(assertion.patterns)))
134
140
end
135
141
136
142
@doc """
 
@@ -176,25 182,17 @@ defmodule Mneme.Assertion do
176
182
@doc """
177
183
Check whether the assertion struct represents the given AST node.
178
184
"""
179
- def same?(%Assertion{context: context}, node) do
185
def same?(%Assertion{line: line}, node) do
180
186
case node do
181
- {:auto_assert, meta, [_]} -> meta[:line] == context[:line]
187
{:auto_assert, meta, [_]} -> meta[:line] == line
182
188
_ -> false
183
189
end
184
190
end
185
191
186
192
@doc """
187
193
Generates assertion code for the given target.
188
-
189
- Target is one of:
190
-
191
- * `:auto_assert` - Generate an `auto_assert` call that is appropriate
192
- for updating the source code.
193
-
194
- * `:assert` - Generate an `assert` call that is appropriate for
195
- updating the source code.
196
194
"""
197
- def to_code(assertion, target) when target in [:auto_assert, :assert] do
195
def to_code(assertion, target) do
198
196
case assertion.patterns do
199
197
[{falsy, nil, _} | _] when falsy in [nil, false] ->
200
198
build_call(
 
@@ -226,30 224,30 @@ defmodule Mneme.Assertion do
226
224
{:__block__, [line: parent_meta[:line]], [value]}
227
225
end
228
226
229
- defp build_call(:auto_assert, :compare, code, falsy_expr, nil) do
227
defp build_call(:mneme, :compare, code, falsy_expr, nil) do
230
228
{:auto_assert, meta(code), [{:==, meta(value_expr(code)), [value_expr(code), falsy_expr]}]}
231
229
end
232
230
233
- defp build_call(:auto_assert, :match, code, expr, nil) do
231
defp build_call(:mneme, :match, code, expr, nil) do
234
232
{:auto_assert, meta(code), [{:<-, meta(value_expr(code)), [expr, value_expr(code)]}]}
235
233
end
236
234
237
- defp build_call(:auto_assert, :match, code, expr, guard) do
235
defp build_call(:mneme, :match, code, expr, guard) do
238
236
{:auto_assert, meta(code),
239
237
[{:<-, meta(value_expr(code)), [{:when, [], [expr, guard]}, value_expr(code)]}]}
240
238
end
241
239
242
- defp build_call(:assert, :compare, code, falsy, nil) do
240
defp build_call(:ex_unit, :compare, code, falsy, nil) do
243
241
{:assert, meta(code), [{:==, meta(value_expr(code)), [value_expr(code), falsy]}]}
244
242
end
245
243
246
- defp build_call(:assert, :match, code, expr, nil) do
244
defp build_call(:ex_unit, :match, code, expr, nil) do
247
245
{:assert, meta(code),
248
246
[{:=, meta(value_expr(code)), [normalize_heredoc(expr), value_expr(code)]}]}
249
247
end
250
248
251
- defp build_call(:assert, :match, code, expr, guard) do
252
- check = build_call(:assert, :match, code, expr, nil)
249
defp build_call(:ex_unit, :match, code, expr, guard) do
250
check = build_call(:ex_unit, :match, code, expr, nil)
253
251
254
252
quote do
255
253
unquote(check)
 
@@ -260,14 258,12 @@ defmodule Mneme.Assertion do
260
258
defp meta({_, meta, _}), do: meta
261
259
defp meta(_), do: []
262
260
263
- defp code_for_eval(code, [pattern | _]) do
264
- case pattern do
265
- {falsy, nil, _} when falsy in [nil, false] ->
266
- build_eval(:compare, code, falsy, nil)
261
defp code_for_eval(code, {falsy, nil, _}) when falsy in [nil, false] do
262
build_eval(:compare, code, falsy, nil)
263
end
267
264
268
- {expr, guard, _} ->
269
- build_eval(:match, code, expr, guard)
270
- end
265
defp code_for_eval(code, {expr, guard, _}) do
266
build_eval(:match, code, expr, guard)
271
267
end
272
268
273
269
defp build_eval(_, {:__block__, _, _} = code, _, _) do
 
@@ -275,11 271,11 @@ defmodule Mneme.Assertion do
275
271
end
276
272
277
273
defp build_eval(:compare, code, _falsy, nil) do
278
- {:assert, [], [{:==, [], [normalized_expect_expr(code), {:value, [], nil}]}]}
274
{:assert, [], [{:==, [], [normalized_expect_expr(code), Macro.var(:value, :mneme)]}]}
279
275
end
280
276
281
277
defp build_eval(:match, code, _expr, nil) do
282
- {:assert, [], [{:=, [], [normalized_expect_expr(code), {:value, [], nil}]}]}
278
{:assert, [], [{:=, [], [normalized_expect_expr(code), Macro.var(:value, :mneme)]}]}
283
279
end
284
280
285
281
defp build_eval(:match, code, expr, guard) do
 
@@ -292,7 288,9 @@ defmodule Mneme.Assertion do
292
288
end
293
289
end
294
290
291
defp value_expr({:__block__, _, [first, _second]}), do: value_expr(first)
295
292
defp value_expr({_, _, [{:<-, _, [_, value_expr]}]}), do: value_expr
293
defp value_expr({_, _, [{:=, _, [_, value_expr]}]}), do: value_expr
296
294
defp value_expr({_, _, [{:==, _, [value_expr, _]}]}), do: value_expr
297
295
defp value_expr({_, _, [value_expr]}), do: value_expr
added lib/mneme/assertion/builder.ex
 
@@ -0,0 1,273 @@
1
defmodule Mneme.Assertion.Builder do
2
@moduledoc false
3
4
@typedoc """
5
Represents a possible pattern that would match a runtime value.
6
"""
7
@type pattern :: {match_expression, guard_expression, notes}
8
9
@type match_expression :: Macro.t()
10
@type guard_expression :: Macro.t() | nil
11
@type notes :: [binary()]
12
13
@doc """
14
Converts `value` into an AST that could be used to match that value.
15
16
The second `context` argument is a map containing information about
17
the context in which the expressions will be evaluated. It contains:
18
19
* `:binding` - a keyword list of variables/values present in the
20
calling environment
21
22
Returns a list of possible matching patterns.
23
"""
24
@callback to_patterns(value :: any(), context :: map()) :: [pattern, ...]
25
26
@doc """
27
Default implementation of `c:to_pattern`.
28
"""
29
def to_patterns(value, context) do
30
patterns = do_to_patterns(value, context)
31
32
case fetch_pinned(value, context) do
33
{:ok, pin} -> [{pin, nil, []} | patterns]
34
:error -> patterns
35
end
36
end
37
38
defp with_meta(meta \\ [], context) do
39
Keyword.merge([line: context.line], meta)
40
end
41
42
defp fetch_pinned(value, context) do
43
case List.keyfind(context.binding || [], value, 1) do
44
{name, ^value} -> {:ok, {:^, with_meta(context), [make_var(name, context)]}}
45
_ -> :error
46
end
47
end
48
49
defp do_to_patterns(value, _context)
50
when is_atom(value) or is_integer(value) or is_float(value) do
51
pattern = {value, nil, []}
52
[pattern]
53
end
54
55
defp do_to_patterns(string, context) when is_binary(string) do
56
pattern =
57
if String.contains?(string, "\n") do
58
{{:__block__, with_meta([delimiter: ~S(""")], context), [format_for_heredoc(string)]},
59
nil, []}
60
else
61
{string, nil, []}
62
end
63
64
[pattern]
65
end
66
67
defp do_to_patterns(list, context) when is_list(list) do
68
enum_to_patterns(list, context)
69
end
70
71
defp do_to_patterns(tuple, context) when is_tuple(tuple) do
72
tuple
73
|> Tuple.to_list()
74
|> enum_to_patterns(context)
75
|> transform_patterns(&tuple_pattern/2, context)
76
end
77
78
for {var_name, guard} <- [ref: :is_reference, pid: :is_pid, port: :is_port] do
79
defp do_to_patterns(value, context) when unquote(guard)(value) do
80
guard_non_serializable(unquote(var_name), unquote(guard), value, context)
81
end
82
end
83
84
for module <- [DateTime, NaiveDateTime, Date, Time] do
85
defp do_to_patterns(%unquote(module){} = value, _context) do
86
pattern = {value |> inspect() |> Code.string_to_quoted!(), nil, []}
87
[pattern]
88
end
89
end
90
91
defp do_to_patterns(%URI{} = uri, context) do
92
struct_to_patterns(URI, Map.delete(uri, :authority), context, [])
93
end
94
95
defp do_to_patterns(%struct{} = value, context) do
96
if ecto_schema?(struct) do
97
{value, notes} = prepare_ecto_struct(value)
98
struct_to_patterns(struct, value, context, notes)
99
else
100
struct_to_patterns(struct, value, context, [])
101
end
102
end
103
104
defp do_to_patterns(%{} = map, context) when map_size(map) == 0 do
105
[map_pattern(context)]
106
end
107
108
defp do_to_patterns(%{} = map, context) do
109
patterns =
110
map
111
|> enum_to_patterns(context)
112
|> transform_patterns(&map_pattern/2, context)
113
114
[map_pattern(context) | patterns]
115
end
116
117
defp struct_to_patterns(struct, map, context, extra_notes) do
118
empty = struct.__struct__()
119
120
map
121
|> Map.filter(fn {k, v} -> v != Map.get(empty, k) end)
122
|> to_patterns(context)
123
|> transform_patterns(&struct_pattern(struct, &1, &2, extra_notes), context)
124
end
125
126
defp format_for_heredoc(string) when is_binary(string) do
127
if String.ends_with?(string, "\n") do
128
string
129
else
130
string <> "\\\n"
131
end
132
end
133
134
defp enum_to_patterns(values, context) do
135
values
136
|> Enum.map(&to_patterns(&1, context))
137
|> unzip_combine(context)
138
end
139
140
defp unzip_combine(nested_patterns, context, acc \\ []) do
141
if last_pattern?(nested_patterns) do
142
{patterns, _} = combine_and_pop(nested_patterns, context)
143
Enum.reverse([patterns | acc])
144
else
145
{patterns, rest} = combine_and_pop(nested_patterns, context)
146
unzip_combine(rest, context, [patterns | acc])
147
end
148
end
149
150
defp last_pattern?(nested_patterns) do
151
Enum.all?(nested_patterns, fn
152
[_] -> true
153
_ -> false
154
end)
155
end
156
157
defp combine_and_pop(nested_patterns, context) do
158
{patterns, rest_patterns} =
159
nested_patterns
160
|> Enum.map(&pop_pattern/1)
161
|> Enum.unzip()
162
163
{combine_patterns(patterns, context), rest_patterns}
164
end
165
166
defp pop_pattern([current, next | rest]), do: {current, [next | rest]}
167
defp pop_pattern([current]), do: {current, [current]}
168
169
defp combine_patterns(patterns, context) do
170
{exprs, {guard, notes}} =
171
Enum.map_reduce(patterns, {nil, []}, fn {expr, g1, n1}, {g2, n2} ->
172
{expr, {combine_guards(g1, g2, context), n1 n2}}
173
end)
174
175
{exprs, guard, notes}
176
end
177
178
defp combine_guards(nil, guard, _context), do: guard
179
defp combine_guards(guard, nil, _context), do: guard
180
defp combine_guards(g1, g2, context), do: {:and, with_meta(context), [g2, g1]}
181
182
defp guard_non_serializable(name, guard, value, context) do
183
var = make_var(name, context)
184
185
pattern =
186
{var, {guard, with_meta(context), [var]},
187
["Using guard for non-serializable value `#{inspect(value)}`"]}
188
189
[pattern]
190
end
191
192
defp make_var(name, context) do
193
{name, with_meta(context), nil}
194
end
195
196
defp transform_patterns(patterns, transform, context) do
197
Enum.map(patterns, &transform.(&1, context))
198
end
199
200
defp tuple_pattern({[e1, e2], guard, notes}, _context) do
201
{{e1, e2}, guard, notes}
202
end
203
204
defp tuple_pattern({exprs, guard, notes}, context) do
205
{{:{}, with_meta(context), exprs}, guard, notes}
206
end
207
208
defp map_pattern({tuples, guard, notes} \\ {[], nil, []}, context) do
209
{{:%{}, with_meta(context), tuples}, guard, notes}
210
end
211
212
defp struct_pattern(struct, {map_expr, guard, notes}, context, extra_notes) do
213
{aliased, _} =
214
context
215
|> Map.get(:aliases, [])
216
|> List.keyfind(struct, 1, {struct, struct})
217
218
aliases = aliased |> Module.split() |> Enum.map(&String.to_atom/1)
219
220
{{:%, with_meta(context), [{:__aliases__, with_meta(context), aliases}, map_expr]}, guard,
221
extra_notes notes}
222
end
223
224
defp ecto_schema?(module) do
225
function_exported?(module, :__schema__, 1)
226
end
227
228
defp prepare_ecto_struct(%schema{} = struct) do
229
notes = ["Patterns for Ecto structs exclude primary keys, association keys, and meta fields"]
230
231
primary_keys = schema.__schema__(:primary_key)
232
autogenerated_fields = get_autogenerated_fields(schema)
233
234
association_keys =
235
for assoc <- schema.__schema__(:associations),
236
%{owner_key: key} = schema.__schema__(:association, assoc) do
237
key
238
end
239
240
drop_fields =
241
Enum.concat([
242
primary_keys,
243
autogenerated_fields,
244
association_keys,
245
[:__meta__]
246
])
247
248
{Map.drop(struct, drop_fields), notes}
249
end
250
251
# The Schema.__schema__(:autogenerate_fields) call was introduced after
252
# Ecto v3.9.4, so we rely on an undocumented call using :autogenerate
253
# for versions prior to that.
254
ecto_supports_autogenerate_fields? =
255
with {:ok, charlist} <- :application.get_key(:ecto, :vsn),
256
{:ok, version} <- Version.parse(List.to_string(charlist)) do
257
Version.compare(version, "3.9.4") == :gt
258
else
259
_ -> false
260
end
261
262
if ecto_supports_autogenerate_fields? do
263
def get_autogenerated_fields(schema) do
264
schema.__schema__(:autogenerate_fields)
265
end
266
else
267
def get_autogenerated_fields(schema) do
268
:autogenerate
269
|> schema.__schema__()
270
|> Enum.flat_map(&elem(&1, 0))
271
end
272
end
273
end
changed lib/mneme/options.ex
 
@@ -12,11 12,12 @@ defmodule Mneme.Options do
12
12
"""
13
13
],
14
14
target: [
15
- type: {:in, [:auto_assert, :assert]},
16
- default: :auto_assert,
15
type: {:in, [:mneme, :ex_unit]},
16
default: :mneme,
17
17
doc: """
18
- The call output when an auto-assertion updates. If `:assert`, auto-assertions
19
- will be rewritten with ExUnit's `assert` when they update.
18
The target output for auto-assertions. If `:mneme`, the expression will
19
remain an auto-assertion. If `:ex_unit`, the expression will be rewritten
20
as an ExUnit assertion.
20
21
"""
21
22
]
22
23
]
changed lib/mneme/patcher.ex
 
@@ -16,7 16,7 @@ defmodule Mneme.Patcher do
16
16
@doc """
17
17
Load and cache and source and AST required by the context.
18
18
"""
19
- def load_file!(%Project{} = project, %{file: file}) do
19
def load_file!(%Project{} = project, file) do
20
20
case Project.source(project, file) do
21
21
{:ok, _source} ->
22
22
project
 
@@ -29,7 29,10 @@ defmodule Mneme.Patcher do
29
29
@doc """
30
30
Finalize all patches, writing all results to disk.
31
31
"""
32
- def finalize!(project), do: Project.save(project)
32
def finalize!(project) do
33
:ok = Project.save(project)
34
project
35
end
33
36
34
37
@doc """
35
38
Run an assertion patch.
 
@@ -61,7 64,7 @@ defmodule Mneme.Patcher do
61
64
defp prompt_change(_, _, %{action: action}, _), do: {action, nil}
62
65
63
66
defp patch_assertion(project, assertion, opts) do
64
- source = Project.source!(project, assertion.context.file)
67
source = Project.source!(project, assertion.file)
65
68
66
69
zipper =
67
70
source
changed lib/mneme/prompter/terminal.ex
 
@@ -8,9 8,6 @@ defmodule Mneme.Prompter.Terminal do
8
8
alias Mneme.Assertion
9
9
alias Rewrite.Source
10
10
11
- @cursor_save "\e7"
12
- @cursor_restore "\e8"
13
-
14
11
@bullet_char "●"
15
12
@empty_bullet_char "○"
16
13
@info_char "🛈"
 
@@ -19,30 16,22 @@ defmodule Mneme.Prompter.Terminal do
19
16
20
17
@impl true
21
18
def prompt!(%Source{} = source, %Assertion{} = assertion, _prompt_state) do
22
- %{type: type, context: context} = assertion
23
-
24
- message =
25
- message(
26
- source,
27
- type,
28
- context,
29
- Assertion.pattern_index(assertion),
30
- Assertion.notes(assertion)
31
- )
19
message = message(source, assertion)
32
20
33
21
Owl.IO.puts(["\n\n", message])
34
22
result = input()
35
- IO.write([IO.ANSI.cursor_down(2), "\r"])
36
23
37
24
{result, nil}
38
25
end
39
26
40
27
@doc false
41
- def message(source, type, context, pattern_nav, notes) do
28
def message(source, %Assertion{type: type} = assertion) do
29
notes = Assertion.notes(assertion)
30
pattern_nav = Assertion.pattern_index(assertion)
42
31
prefix = tag("│ ", :light_black)
43
32
44
33
[
45
- header_tag(type, context),
34
header_tag(assertion),
46
35
"\n",
47
36
diff(source),
48
37
notes_tag(notes),
 
@@ -50,16 39,13 @@ defmodule Mneme.Prompter.Terminal do
50
39
explanation_tag(type),
51
40
"\n",
52
41
tag("> ", :light_black),
53
- @cursor_save,
54
42
"\n",
55
43
input_options_tag(pattern_nav)
56
44
]
57
45
|> Owl.Data.add_prefix(prefix)
58
46
end
59
47
60
- defp input() do
61
- IO.write(@cursor_restore)
62
-
48
defp input do
63
49
case gets() do
64
50
"y" -> :accept
65
51
"n" -> :reject
 
@@ -70,7 56,14 @@ defmodule Mneme.Prompter.Terminal do
70
56
end
71
57
72
58
defp gets do
73
- IO.gets("") |> normalize_gets()
59
resp =
60
[IO.ANSI.cursor_up(2), IO.ANSI.cursor_right(4)]
61
|> IO.gets()
62
|> normalize_gets()
63
64
IO.write([IO.ANSI.cursor_down(1), "\r"])
65
66
resp
74
67
end
75
68
76
69
defp normalize_gets(value) when is_binary(value) do
 
@@ -103,20 96,20 @@ defmodule Mneme.Prompter.Terminal do
103
96
104
97
defp eof_newline(code), do: String.trim_trailing(code) <> "\n"
105
98
106
- defp header_tag(type, context) do
99
defp header_tag(%Assertion{type: type, test: test, module: module} = assertion) do
107
100
[
108
101
type_tag(type),
109
102
tag([" ", @bullet_char, " "], [:faint, :light_black]),
110
- to_string(context.test),
103
to_string(test),
111
104
" (",
112
- to_string(context.module),
105
to_string(module),
113
106
")\n",
114
- file_tag(context),
107
file_tag(assertion),
115
108
"\n"
116
109
]
117
110
end
118
111
119
- defp file_tag(%{file: file, line: line} = _context) do
112
defp file_tag(%Assertion{file: file, line: line}) do
120
113
path = Path.relative_to_cwd(file)
121
114
tag([path, ":", to_string(line)], :light_black)
122
115
end
removed lib/mneme/serializer.ex
 
@@ -1,271 0,0 @@
1
- defmodule Mneme.Serializer do
2
- @moduledoc false
3
-
4
- @typedoc """
5
- Represents a possible pattern that would match a runtime value.
6
- """
7
- @type pattern :: {match_expression, guard_expression, notes}
8
-
9
- @type match_expression :: Macro.t()
10
- @type guard_expression :: Macro.t() | nil
11
- @type notes :: [binary()]
12
-
13
- @doc """
14
- Converts `value` into an AST that could be used to match that value.
15
-
16
- The second `context` argument is a map containing information about
17
- the context in which the expressions will be evaluated. It contains:
18
-
19
- * `:binding` - a keyword list of variables/values present in the
20
- calling environment
21
-
22
- Returns a list of possible matching patterns.
23
- """
24
- @callback to_patterns(value :: any(), context :: map()) :: [pattern, ...]
25
-
26
- @doc """
27
- Default implementation of `c:to_pattern`.
28
- """
29
- def to_patterns(value, context) do
30
- patterns = do_to_patterns(value, context)
31
-
32
- case fetch_pinned(value, context) do
33
- {:ok, pin} -> [{pin, nil, []} | patterns]
34
- :error -> patterns
35
- end
36
- end
37
-
38
- defp with_meta(meta \\ [], context) do
39
- Keyword.merge([line: context[:line]], meta)
40
- end
41
-
42
- defp fetch_pinned(value, context) do
43
- case List.keyfind(context[:binding] || [], value, 1) do
44
- {name, ^value} -> {:ok, {:^, with_meta(context), [make_var(name, context)]}}
45
- _ -> :error
46
- end
47
- end
48
-
49
- defp do_to_patterns(value, _context)
50
- when is_atom(value) or is_integer(value) or is_float(value) do
51
- pattern = {value, nil, []}
52
- [pattern]
53
- end
54
-
55
- defp do_to_patterns(string, context) when is_binary(string) do
56
- pattern =
57
- if String.contains?(string, "\n") do
58
- {{:__block__, with_meta([delimiter: ~S(""")], context), [format_for_heredoc(string)]},
59
- nil, []}
60
- else
61
- {string, nil, []}
62
- end
63
-
64
- [pattern]
65
- end
66
-
67
- defp do_to_patterns(list, context) when is_list(list) do
68
- enum_to_patterns(list, context)
69
- end
70
-
71
- defp do_to_patterns(tuple, context) when is_tuple(tuple) do
72
- tuple
73
- |> Tuple.to_list()
74
- |> enum_to_patterns(context)
75
- |> transform_patterns(&tuple_pattern/2, context)
76
- end
77
-
78
- for {var_name, guard} <- [ref: :is_reference, pid: :is_pid, port: :is_port] do
79
- defp do_to_patterns(value, context) when unquote(guard)(value) do
80
- guard_non_serializable(unquote(var_name), unquote(guard), value, context)
81
- end
82
- end
83
-
84
- for module <- [DateTime, NaiveDateTime, Date, Time] do
85
- defp do_to_patterns(%unquote(module){} = value, _context) do
86
- pattern = {value |> inspect() |> Code.string_to_quoted!(), nil, []}
87
- [pattern]
88
- end
89
- end
90
-
91
- defp do_to_patterns(%URI{} = uri, context) do
92
- struct_to_patterns(URI, Map.delete(uri, :authority), context, [])
93
- end
94
-
95
- defp do_to_patterns(%struct{} = value, context) do
96
- if ecto_schema?(struct) do
97
- {value, notes} = prepare_ecto_struct(value)
98
- struct_to_patterns(struct, value, context, notes)
99
- else
100
- struct_to_patterns(struct, value, context, [])
101
- end
102
- end
103
-
104
- defp do_to_patterns(map, context) when is_map(map) do
105
- empty_map_pattern = map_pattern({[], nil, []}, context)
106
-
107
- patterns =
108
- map
109
- |> enum_to_patterns(context)
110
- |> transform_patterns(&map_pattern/2, context)
111
-
112
- [empty_map_pattern | patterns]
113
- end
114
-
115
- defp struct_to_patterns(struct, map, context, extra_notes) do
116
- empty = struct.__struct__()
117
-
118
- map
119
- |> Map.filter(fn {k, v} -> v != Map.get(empty, k) end)
120
- |> Mneme.Serializer.to_patterns(context)
121
- |> transform_patterns(&struct_pattern(struct, &1, &2, extra_notes), context)
122
- end
123
-
124
- defp format_for_heredoc(string) when is_binary(string) do
125
- if String.ends_with?(string, "\n") do
126
- string
127
- else
128
- string <> "\\\n"
129
- end
130
- end
131
-
132
- defp enum_to_patterns(values, context) do
133
- values
134
- |> Enum.map(&Mneme.Serializer.to_patterns(&1, context))
135
- |> unzip_combine(context)
136
- end
137
-
138
- defp unzip_combine(nested_patterns, context, acc \\ []) do
139
- if last_pattern?(nested_patterns) do
140
- {patterns, _} = combine_and_pop(nested_patterns, context)
141
- Enum.reverse([patterns | acc])
142
- else
143
- {patterns, rest} = combine_and_pop(nested_patterns, context)
144
- unzip_combine(rest, context, [patterns | acc])
145
- end
146
- end
147
-
148
- defp last_pattern?(nested_patterns) do
149
- Enum.all?(nested_patterns, fn
150
- [_] -> true
151
- _ -> false
152
- end)
153
- end
154
-
155
- defp combine_and_pop(nested_patterns, context) do
156
- {patterns, rest_patterns} =
157
- nested_patterns
158
- |> Enum.map(&pop_pattern/1)
159
- |> Enum.unzip()
160
-
161
- {combine_patterns(patterns, context), rest_patterns}
162
- end
163
-
164
- defp pop_pattern([current, next | rest]), do: {current, [next | rest]}
165
- defp pop_pattern([current]), do: {current, [current]}
166
-
167
- defp combine_patterns(patterns, context) do
168
- {exprs, {guard, notes}} =
169
- Enum.map_reduce(patterns, {nil, []}, fn {expr, g1, n1}, {g2, n2} ->
170
- {expr, {combine_guards(g1, g2, context), n1 n2}}
171
- end)
172
-
173
- {exprs, guard, notes}
174
- end
175
-
176
- defp combine_guards(nil, guard, _context), do: guard
177
- defp combine_guards(guard, nil, _context), do: guard
178
- defp combine_guards(g1, g2, context), do: {:and, with_meta(context), [g2, g1]}
179
-
180
- defp guard_non_serializable(name, guard, value, context) do
181
- var = make_var(name, context)
182
-
183
- pattern =
184
- {var, {guard, with_meta(context), [var]},
185
- ["Using guard for non-serializable value `#{inspect(value)}`"]}
186
-
187
- [pattern]
188
- end
189
-
190
- defp make_var(name, context) do
191
- {name, with_meta(context), nil}
192
- end
193
-
194
- defp transform_patterns(patterns, transform, context) do
195
- Enum.map(patterns, &transform.(&1, context))
196
- end
197
-
198
- defp tuple_pattern({[e1, e2], guard, notes}, _context) do
199
- {{e1, e2}, guard, notes}
200
- end
201
-
202
- defp tuple_pattern({exprs, guard, notes}, context) do
203
- {{:{}, with_meta(context), exprs}, guard, notes}
204
- end
205
-
206
- defp map_pattern({tuples, guard, notes}, context) do
207
- {{:%{}, with_meta(context), tuples}, guard, notes}
208
- end
209
-
210
- defp struct_pattern(struct, {map_expr, guard, notes}, context, extra_notes) do
211
- {aliased, _} =
212
- context
213
- |> Map.get(:aliases, [])
214
- |> List.keyfind(struct, 1, {struct, struct})
215
-
216
- aliases = aliased |> Module.split() |> Enum.map(&String.to_atom/1)
217
-
218
- {{:%, with_meta(context), [{:__aliases__, with_meta(context), aliases}, map_expr]}, guard,
219
- extra_notes notes}
220
- end
221
-
222
- defp ecto_schema?(module) do
223
- function_exported?(module, :__schema__, 1)
224
- end
225
-
226
- defp prepare_ecto_struct(%schema{} = struct) do
227
- notes = ["Patterns for Ecto structs exclude primary keys, association keys, and meta fields"]
228
-
229
- primary_keys = schema.__schema__(:primary_key)
230
- autogenerated_fields = get_autogenerated_fields(schema)
231
-
232
- association_keys =
233
- for assoc <- schema.__schema__(:associations),
234
- %{owner_key: key} = schema.__schema__(:association, assoc) do
235
- key
236
- end
237
-
238
- drop_fields =
239
- Enum.concat([
240
- primary_keys,
241
- autogenerated_fields,
242
- association_keys,
243
- [:__meta__]
244
- ])
245
-
246
- {Map.drop(struct, drop_fields), notes}
247
- end
248
-
249
- # The Schema.__schema__(:autogenerate_fields) call was introduced after
250
- # Ecto v3.9.4, so we rely on an undocumented call using :autogenerate
251
- # for versions prior to that.
252
- ecto_supports_autogenerate_fields? =
253
- with {:ok, charlist} <- :application.get_key(:ecto, :vsn),
254
- {:ok, version} <- Version.parse(List.to_string(charlist)) do
255
- Version.compare(version, "3.9.4") == :gt
256
- else
257
- _ -> false
258
- end
259
-
260
- if ecto_supports_autogenerate_fields? do
261
- def get_autogenerated_fields(schema) do
262
- schema.__schema__(:autogenerate_fields)
263
- end
264
- else
265
- def get_autogenerated_fields(schema) do
266
- :autogenerate
267
- |> schema.__schema__()
268
- |> Enum.flat_map(&elem(&1, 0))
269
- end
270
- end
271
- end
changed lib/mneme/server.ex
 
@@ -27,7 27,8 @@ defmodule Mneme.Server do
27
27
:io_pid,
28
28
:current_module,
29
29
opts: %{},
30
- assertions: []
30
to_register: [],
31
to_patch: []
31
32
]
32
33
33
34
@type t :: %__MODULE__{
 
@@ -35,7 36,8 @@ defmodule Mneme.Server do
35
36
io_pid: pid(),
36
37
current_module: module(),
37
38
opts: %{{mod :: module(), test :: atom()} => map()},
38
- assertions: [{any(), from :: pid()}]
39
to_register: [{any(), from :: pid()}],
40
to_patch: [{any(), from :: pid()}]
39
41
}
40
42
41
43
@doc """
 
@@ -45,10 47,17 @@ defmodule Mneme.Server do
45
47
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
46
48
end
47
49
50
@doc """
51
Register a new assertion.
52
"""
53
def register_assertion(assertion) do
54
GenServer.call(__MODULE__, {:register_assertion, assertion}, :infinity)
55
end
56
48
57
@doc """
49
58
Await the result of an assertion patch.
50
59
"""
51
- def await_assertion(assertion) do
60
def patch_assertion(assertion) do
52
61
GenServer.call(__MODULE__, {:patch_assertion, assertion}, :infinity)
53
62
end
54
63
 
@@ -68,9 77,19 @@ defmodule Mneme.Server do
68
77
end
69
78
70
79
@impl true
80
def handle_call({:register_assertion, assertion}, from, state) do
81
state =
82
case assertion.type do
83
:new -> Map.update!(state, :to_patch, &[{assertion, from} | &1])
84
:update -> Map.update!(state, :to_register, &[{assertion, from} | &1])
85
end
86
87
{:noreply, state, {:continue, :process_next}}
88
end
89
71
90
def handle_call({:patch_assertion, assertion}, from, state) do
72
- state = Map.update!(state, :assertions, &[{assertion, from} | &1])
73
- {:noreply, state, {:continue, :process_assertions}}
91
state = Map.update!(state, :to_patch, &[{assertion, from} | &1])
92
{:noreply, state, {:continue, :process_next}}
74
93
end
75
94
76
95
def handle_call({:capture_formatter, io_pid}, _from, state) do
 
@@ -81,7 100,7 @@ defmodule Mneme.Server do
81
100
%{module: module, name: test_name, tags: tags} = test
82
101
state = put_in(state.opts[{module, test_name}], Options.options(tags))
83
102
84
- {:reply, :ok, state, {:continue, :process_assertions}}
103
{:reply, :ok, state, {:continue, :process_next}}
85
104
end
86
105
87
106
def handle_call(
 
@@ -90,7 109,7 @@ defmodule Mneme.Server do
90
109
%{current_module: mod} = state
91
110
) do
92
111
{:reply, :ok, state |> flush_io() |> Map.put(:current_module, nil),
93
- {:continue, :process_assertions}}
112
{:continue, :process_next}}
94
113
end
95
114
96
115
def handle_call({:formatter, {:suite_finished, _}}, _from, state) do
 
@@ -109,21 128,27 @@ defmodule Mneme.Server do
109
128
end
110
129
111
130
@impl true
112
- def handle_continue(:process_assertions, state) do
113
- case pop_assertion(state) do
114
- {next, state} -> {:noreply, patch_assertion(state, next)}
115
- nil -> {:noreply, state}
131
def handle_continue(:process_next, state) do
132
case pop_to_register(state) do
133
{next, state} ->
134
{:noreply, do_register_assertion(state, next), {:continue, :process_next}}
135
136
nil ->
137
case pop_to_patch(state) do
138
{next, state} -> {:noreply, do_patch_assertion(state, next)}
139
nil -> {:noreply, state}
140
end
116
141
end
117
142
end
118
143
119
- defp patch_assertion(state, {assertion, from}) do
120
- %{module: module, test: test} = assertion.context
144
defp do_patch_assertion(state, {assertion, from}) do
145
%{module: module, test: test} = assertion
121
146
opts = state.opts[{module, test}]
122
147
123
148
state =
124
149
state
125
150
|> Map.put(:current_module, module)
126
- |> Map.put(:patch_state, Patcher.load_file!(state.patch_state, assertion.context))
151
|> Map.put(:patch_state, Patcher.load_file!(state.patch_state, assertion.file))
127
152
128
153
{reply, patch_state} = Patcher.patch!(state.patch_state, assertion, opts)
129
154
 
@@ -132,23 157,51 @@ defmodule Mneme.Server do
132
157
%{state | patch_state: patch_state}
133
158
end
134
159
160
defp do_register_assertion(state, {assertion, from}) do
161
%{module: module, test: test} = assertion
162
opts = state.opts[{module, test}]
163
164
case opts.target do
165
:mneme ->
166
GenServer.reply(from, {:ok, assertion})
167
state
168
169
:ex_unit ->
170
%{state | to_patch: [{assertion, from} | state.to_patch]}
171
end
172
end
173
135
174
defp flush_io(%{io_pid: io_pid} = state) do
136
175
output = StringIO.flush(io_pid)
137
176
if output != "", do: IO.write(output)
138
177
state
139
178
end
140
179
141
- defp pop_assertion(state), do: pop_assertion(state, [])
180
defp pop_to_register(state), do: pop_to_register(state, [])
142
181
143
- defp pop_assertion(%{assertions: []}, _acc), do: nil
182
defp pop_to_register(%{to_register: []}, _acc), do: nil
144
183
145
- defp pop_assertion(%{assertions: [next | rest]} = state, acc) do
146
- {%{context: %{module: module, test: test}}, _from} = next
184
defp pop_to_register(%{to_register: [next | rest]} = state, acc) do
185
{%{module: module, test: test}, _from} = next
186
187
if state.opts[{module, test}] do
188
{next, %{state | to_register: acc rest}}
189
else
190
pop_to_register(%{state | to_register: rest}, [next | acc])
191
end
192
end
193
194
defp pop_to_patch(state), do: pop_to_patch(state, [])
195
196
defp pop_to_patch(%{to_patch: []}, _acc), do: nil
197
198
defp pop_to_patch(%{to_patch: [next | rest]} = state, acc) do
199
{%{module: module, test: test}, _from} = next
147
200
148
201
if current_module?(state, module) && state.opts[{module, test}] do
149
- {next, %{state | assertions: acc rest}}
202
{next, %{state | to_patch: acc rest}}
150
203
else
151
- pop_assertion(%{state | assertions: rest}, [next | acc])
204
pop_to_patch(%{state | to_patch: rest}, [next | acc])
152
205
end
153
206
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.0.4"
7
def version, do: "0.0.5"
8
8
9
9
def project do
10
10
[
 
@@ -20,7 20,7 @@ defmodule Mneme.MixProject do
20
20
preferred_cli_env: preferred_cli_env(),
21
21
22
22
# Hex
23
- description: "Semi-automated snapshot testing with ExUnit",
23
description: "Snapshot/approval testing integrated into ExUnit",
24
24
package: package(),
25
25
26
26
# Docs
 
@@ -43,10 43,11 @@ defmodule Mneme.MixProject do
43
43
{:rewrite, "~> 0.6.0"},
44
44
45
45
# Development
46
- {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
47
46
{:excoveralls, "~> 0.15", only: :test},
48
47
{:ecto, "~> 3.9.4", only: :test},
49
- {:ex_doc, ">= 0.0.0", only: :dev}
48
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
49
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
50
{:makeup_json, ">= 0.0.0", only: :dev, runtime: false}
50
51
]
51
52
end
52
53
 
@@ -77,7 78,17 @@ defmodule Mneme.MixProject do
77
78
defp docs do
78
79
[
79
80
main: "Mneme",
80
- source_url: @source_url
81
api_reference: false,
82
source_url: @source_url,
83
extra_section: "GUIDES",
84
extras: [
85
"guides/vscode_setup.md": [title: "VS Code"]
86
],
87
groups_for_extras: [
88
"Editor Setup": [
89
"guides/vscode_setup.md"
90
]
91
]
81
92
]
82
93
end
unknown priv/plts/dialyzer.plt
CANNOT RENDER FILES LARGER THAN 1MB