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