changed .formatter.exs
 
@@ -10,7 10,8 @@ locals_without_parens = [
10
10
"{mix,.formatter}.exs",
11
11
"{config,lib}/**/*.{ex,exs}",
12
12
"test/**/*.{ex,exs}",
13
- "test_integration/**/*.{ex,exs}"
13
"test_integration/**/*.{ex,exs}",
14
"examples/*.{ex,exs}"
14
15
],
15
16
locals_without_parens: locals_without_parens,
16
17
export: [locals_without_parens: locals_without_parens]
changed CHANGELOG.md
 
@@ -1,52 1,69 @@
1
1
# Changelog
2
2
3
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3
This format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
5
## Unreleased
6
7
### Changed
8
9
* Pattern generation improvements:
10
* Multi-line strings will now generate both a heredoc and a single-line option.
11
12
### Removed
13
14
* No longer depend on `libgraph`.
4
15
5
16
## v0.3.0 (2023-04-10)
6
17
7
- It is now recommended to use Elixir v1.14.4 or later.
18
It is recommended to now use Elixir v1.14.4 or later.
8
19
9
- ### Breaking changes
20
### Added
10
21
11
- * [Core] `auto_assert` now uses `<-` when comparing against falsy values instead of `==`. Support for comparisons using `==` has been removed.
12
-
13
- ### Enhancements
14
-
15
- * [Core] Add three new auto-assertions:
22
* Add three new auto-assertions:
16
23
* [`auto_assert_raise`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_raise/3)
17
24
* [`auto_assert_receive`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_receive/2)
18
25
* [`auto_assert_received`](https://hexdocs.pm/mneme/Mneme.html#auto_assert_received/1)
19
- * [Core] Add a `:force_update` option that forces re-generating assertion patterns even when they succeed. See the [Options documentation](https://hexdocs.pm/mneme/Mneme.html#module-options) for more info.
20
- * [Core] Existing auto-assertions will now run when `Mneme.start()` is not called, but new or failing auto-assertions will fail without prompting ([#32](https://github.com/zachallaun/mneme/issues/32)).
21
- * [Core] Pattern generation improvements:
26
* Add a `:force_update` option that forces re-generating assertion patterns even when they succeed. See the [Options documentation](https://hexdocs.pm/mneme/Mneme.html#module-options) for more info.
27
* Prompts will now show options that were overridden from the defaults.
28
* Mneme now prints a one-line summary at the end of the test run.
29
30
### Changed
31
32
* For falsy values, `auto_assert` now generates `<-` pattern matches instead of `==` value comparisons, which have been removed.
33
* Existing auto-assertions will now run when `Mneme.start()` is not called, but new or failing auto-assertions will fail without prompting ([#32](https://github.com/zachallaun/mneme/issues/32)).
34
* Pattern generation improvements:
22
35
* Ranges now use range syntax like `1..10` and `1..10//2` instead of generating a `%Range{}` struct.
23
- * [CLI] Prompts will now show options that were overridden from the defaults.
24
- * [CLI] Mneme now prints a one-line summary at the end of the test run.
25
36
26
- ### Fixes
37
### Removed
27
38
28
- * [Core] Fix a configuration precedence bug that caused options set in application config to always override module, describe, or test options.
29
- * [Core] Fix a compatibility issue with Ecto ~> 3.9.4 ([#34](https://github.com/zachallaun/mneme/issues/34)).
30
- * [CLI] Fix a confusing diff result that could occur with some binary operations ([#11](https://github.com/zachallaun/mneme/issues/11)).
31
- * [CLI] Preceding comments are no longer shown in diffs ([#26](https://github.com/zachallaun/mneme/issues/26)).
32
- * [CLI] Fix a number of diffing errors related to structs.
39
* **Breaking:** `auto_assert` no longer supports value comparisons using `==`.
40
41
### Fixed
42
43
* Fix a configuration precedence bug that caused options set in application config to always override module, describe, or test options.
44
* Fix a compatibility issue with Ecto ~> 3.9.4 ([#34](https://github.com/zachallaun/mneme/issues/34)).
45
* Fix a confusing diff result that could occur with some binary operations ([#11](https://github.com/zachallaun/mneme/issues/11)).
46
* Preceding comments are no longer shown in diffs ([#26](https://github.com/zachallaun/mneme/issues/26)).
47
* Fix a number of diffing errors related to structs.
33
48
34
49
## v0.2.7 (2023-03-29)
35
50
36
- ### Fixes
51
### Fixed
37
52
38
53
* Fix a crash related to escaped string interpolation characters ([#29](https://github.com/zachallaun/mneme/issues/29)).
39
54
40
55
## v0.2.6 (2023-03-27)
41
56
42
- ### Enhancements
57
### Added
43
58
44
59
* Auto-assertion prompts can now be skipped (`s`) in addition to accepted (`y`) or rejected (`n`). This allows the test clause to continue so that later assertions might be run, but fails the test run once the suite finishes.
45
- * Updated formatting for semantic diffs:
46
- * Diffs will be displayed side-by-side if terminal width allows. To always display diffs stacked, use the `diff_style: :stacked` option; see the "Configuration" section of the `Mneme` module doc for more.
47
- * Both `:stacked` and `:side_by_side` diffs have updated, more consistent formatting.
60
* Semantic diffs will now be displayed side-by-side if terminal width allows. To always display diffs stacked, use the `diff_style: :stacked` option; see the "Configuration" section of the `Mneme` module doc for more.
48
61
49
- ### Fixes
62
### Changed
63
64
* Semantic diff formatting has been improved for clarity.
65
66
### Fixed
50
67
51
68
* Don't overwrite test files if their content changes after starting the test run ([#23](https://github.com/zachallaun/mneme/issues/23)).
52
69
* Fix a crash that occurred when a value contained nested strings with newlines, e.g. `{:ok, "hello\nworld"}` ([#25](https://github.com/zachallaun/mneme/issues/25)).
 
@@ -54,13 71,13 @@ It is now recommended to use Elixir v1.14.4 or later.
54
71
55
72
## v0.2.4, v0.2.5 (2023-03-25)
56
73
57
- ### Fixes
74
### Fixed
58
75
59
76
* Remove unnecessary files from Hex package. This cuts the package size down drastically.
60
77
61
78
## v0.2.3 (2023-03-25)
62
79
63
- ### Fixes
80
### Fixed
64
81
65
82
* Fix diffing for certain sigil variations.
66
83
* Fix `dbg`-related error when running `MIX_ENV=test iex -S mix` ([#20](https://github.com/zachallaun/mneme/issues/20)).
 
@@ -68,27 85,27 @@ It is now recommended to use Elixir v1.14.4 or later.
68
85
69
86
## v0.2.2 (2023-03-20)
70
87
71
- ### Fixes
88
### Fixed
72
89
73
90
* Disable a semantic diffing optimization that caused poor diff results in certain cases, usually manifesting as incorrect branches being compared.
74
91
75
92
## v0.2.1 (2023-03-19)
76
93
77
- ### Enhancements
94
### Changed
78
95
79
96
* More consistent formatting between `:semantic` and `:text` diffs.
80
97
81
98
## v0.2.0 (2023-03-18)
82
99
83
- ### Breaking
84
-
85
- * Mneme now requires Elixir v1.14 or later.
86
-
87
- ### Enhancements
100
### Added
88
101
89
102
* Adds semantic diffs which selectively highlight only meaningful changes when updating an assertion. This can be disabled with the `diff: :text` option; see the "Configuration" section of the `Mneme` module doc for more.
90
103
91
- ### Fixes
104
### Changed
105
106
* **Breaking:** Mneme now requires Elixir v1.14 or later.
107
108
### Fixed
92
109
93
110
* Invalid options now cause a warning instead of crashing test process.
94
111
* Internal errors now show an error instead of crashing test process.
 
@@ -96,41 113,41 @@ It is now recommended to use Elixir v1.14.4 or later.
96
113
97
114
## v0.1.6 (2023-03-04)
98
115
99
- ### Enhancements
116
### Changed
100
117
101
118
* Improved compile-time error message when `auto_assert` is used outside of a `test` block ([#9](https://github.com/zachallaun/mneme/issues/9)).
102
119
103
120
## v0.1.5 (2023-02-25)
104
121
105
- ### Enhancements
122
### Changed
106
123
107
124
* More consistent handling of charlists: lists of integers will now generate themselves as a pattern as well as a charlist if the list is ASCII printable ([#6](https://github.com/zachallaun/mneme/issues/6)).
108
125
109
126
## v0.1.4 (2023-02-23)
110
127
111
- ### Fixes
128
### Fixed
112
129
113
130
* Fix a bug that could cause `auto_assert` expressions to revert to the previous value when using `Mneme.start(restart: true)` ([#7](https://github.com/zachallaun/mneme/issues/7)).
114
131
115
132
## v0.1.3 (2023-02-22)
116
133
117
- ### Enhancements
134
### Added
118
135
119
136
* Add a `:default_pattern` configuration option for auto-assertions which controls the pattern that should be selected by default when prompted.
120
137
121
- ### Fixes
138
### Fixed
122
139
123
140
* When converting an auto-assertion to an ExUnit assertion, select the identical pattern when the `:default_pattern` is `:infer` (set by default).
124
141
125
142
## v0.1.2 (2023-02-21)
126
143
127
- ### Enhancements
144
### Added
128
145
129
146
* Add a `:restart` option to `Mneme.start/1` to restart Mneme if called multiple times.
130
147
131
148
## v0.1.1 (2023-02-20)
132
149
133
- ### Enhancements
150
### Changed
134
151
135
152
* Dramatically reduce the performance gap between `auto_assert` and ExUnit's `assert`.
changed hex_metadata.config
 
@@ -6,15 6,16 @@
6
6
[<<"lib">>,<<"lib/mneme">>,<<"lib/mneme/server.ex">>,<<"lib/mneme/diff.ex">>,
7
7
<<"lib/mneme/server">>,<<"lib/mneme/server/ex_unit_formatter.ex">>,
8
8
<<"lib/mneme/diff">>,<<"lib/mneme/diff/syntax_node.ex">>,
9
- <<"lib/mneme/diff/delta.ex">>,<<"lib/mneme/diff/formatter.ex">>,
10
- <<"lib/mneme/diff/ast.ex">>,<<"lib/mneme/diff/pathfinding.ex">>,
11
- <<"lib/mneme/diff/zipper.ex">>,<<"lib/mneme/options.ex">>,
9
<<"lib/mneme/diff/priority_queue.ex">>,<<"lib/mneme/diff/delta.ex">>,
10
<<"lib/mneme/diff/formatter.ex">>,<<"lib/mneme/diff/ast.ex">>,
11
<<"lib/mneme/diff/pathfinding.ex">>,<<"lib/mneme/diff/zipper.ex">>,
12
<<"lib/mneme/utils.ex">>,<<"lib/mneme/options.ex">>,
12
13
<<"lib/mneme/assertion.ex">>,<<"lib/mneme/prompter">>,
13
14
<<"lib/mneme/prompter/terminal.ex">>,<<"lib/mneme/prompter.ex">>,
14
15
<<"lib/mneme/patcher.ex">>,<<"lib/mneme/errors.ex">>,
15
16
<<"lib/mneme/assertion">>,<<"lib/mneme/assertion/pattern_builder.ex">>,
16
- <<"lib/mneme.ex">>,<<"priv">>,<<".formatter.exs">>,<<"mix.exs">>,
17
- <<"README.md">>,<<"CHANGELOG.md">>]}.
17
<<"lib/mneme/assertion/pattern.ex">>,<<"lib/mneme.ex">>,<<"priv">>,
18
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}.
18
19
{<<"licenses">>,[<<"MIT">>]}.
19
20
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/zachallaun/mneme">>}]}.
20
21
{<<"name">>,<<"mneme">>}.
 
@@ -38,10 39,5 @@
38
39
{<<"name">>,<<"rewrite">>},
39
40
{<<"optional">>,false},
40
41
{<<"repository">>,<<"hexpm">>},
41
- {<<"requirement">>,<<"~> 0.6.0">>}],
42
- [{<<"app">>,<<"libgraph">>},
43
- {<<"name">>,<<"libgraph">>},
44
- {<<"optional">>,false},
45
- {<<"repository">>,<<"hexpm">>},
46
- {<<"requirement">>,<<"~> 0.16.0">>}]]}.
47
- {<<"version">>,<<"0.3.0">>}.
42
{<<"requirement">>,<<"~> 0.6.0">>}]]}.
43
{<<"version">>,<<"0.3.1">>}.
changed lib/mneme/assertion.ex
 
@@ -2,6 2,7 @@ defmodule Mneme.Assertion do
2
2
@moduledoc false
3
3
4
4
alias __MODULE__
5
alias Mneme.Assertion.Pattern
5
6
alias Mneme.Assertion.PatternBuilder
6
7
7
8
defstruct [
 
@@ -23,7 24,7 @@ defmodule Mneme.Assertion do
23
24
macro_ast: Macro.t(),
24
25
rich_ast: Macro.t(),
25
26
code: Macro.t(),
26
- patterns: [pattern],
27
patterns: [Pattern.t()],
27
28
pattern_idx: non_neg_integer(),
28
29
context: context
29
30
}
 
@@ -34,8 35,6 @@ defmodule Mneme.Assertion do
34
35
| :auto_assert_receive
35
36
| :auto_assert_received
36
37
37
- @type pattern :: {match :: Macro.t(), guard :: Macro.t() | nil, notes :: [String.t()]}
38
-
39
38
@type context :: %{
40
39
file: String.t(),
41
40
line: non_neg_integer(),
 
@@ -239,7 238,7 @@ defmodule Mneme.Assertion do
239
238
end
240
239
241
240
defp build_and_select_raise(%{value: %exception{} = e, macro_ast: macro_ast}, default) do
242
- patterns = [{{exception, nil}, nil, []}, {{exception, Exception.message(e)}, nil, []}]
241
patterns = [Pattern.new({exception, nil}), Pattern.new({exception, Exception.message(e)})]
243
242
244
243
case {default, macro_ast} do
245
244
{:infer, {_, _, [_, _, _]}} -> {patterns, 1}
 
@@ -273,12 272,8 @@ defmodule Mneme.Assertion do
273
272
|> Enum.map(&simplify_expr/1)
274
273
275
274
PatternBuilder.to_patterns(value, assertion.context)
276
- |> Enum.split_while(fn
277
- {pattern_expr, pattern_guard, _} ->
278
- !(simplify_expr(pattern_expr) == expr && simplify_expr(pattern_guard) == guard)
279
-
280
- _ ->
281
- true
275
|> Enum.split_while(fn %Pattern{expr: pattern_expr, guard: pattern_guard} ->
276
!(simplify_expr(pattern_expr) == expr && simplify_expr(pattern_guard) == guard)
282
277
end)
283
278
|> case do
284
279
{patterns, []} -> {patterns, 0}
 
@@ -331,11 326,6 @@ defmodule Mneme.Assertion do
331
326
"""
332
327
def pattern(%Assertion{pattern_idx: idx, patterns: patterns}), do: Enum.at(patterns, idx)
333
328
334
- @doc """
335
- Returns any notes associated with the current pattern.
336
- """
337
- def notes(%Assertion{} = assertion), do: assertion |> pattern() |> elem(2)
338
-
339
329
@doc """
340
330
Check whether the assertion struct represents the given AST node.
341
331
"""
 
@@ -350,12 340,12 @@ defmodule Mneme.Assertion do
350
340
Generates assertion code for the given target.
351
341
"""
352
342
def to_code(%Assertion{kind: :auto_assert_raise} = assertion, target) do
353
- {expr, _, _} = pattern(assertion)
343
%Pattern{expr: expr} = pattern(assertion)
354
344
build_call(target, assertion, {expr, nil})
355
345
end
356
346
357
347
def to_code(%Assertion{rich_ast: ast} = assertion, target) do
358
- {expr, guard, _} = pattern(assertion)
348
%Pattern{expr: expr, guard: guard} = pattern(assertion)
359
349
build_call(target, assertion, {block_with_line(expr, meta(ast)), guard})
360
350
end
361
351
 
@@ -613,8 603,6 @@ defmodule Mneme.Assertion do
613
603
string
614
604
|> String.replace("\n", "\\n")
615
605
|> String.replace("\#{", "\\\#{")
616
-
617
- # |> String.replace("\"", "\\\"")
618
606
end
619
607
620
608
defp meta({_, meta, _}), do: meta
added lib/mneme/assertion/pattern.ex
 
@@ -0,0 1,21 @@
1
defmodule Mneme.Assertion.Pattern do
2
@moduledoc false
3
4
alias __MODULE__
5
6
@enforce_keys [:expr]
7
defstruct [:expr, guard: nil, notes: []]
8
9
@type t :: %Pattern{
10
expr: term(),
11
guard: Macro.t() | nil,
12
notes: [String.t()]
13
}
14
15
@doc """
16
Creates a new pattern.
17
"""
18
def new(expr, other_attrs \\ []) do
19
struct(Pattern, Keyword.put(other_attrs, :expr, expr))
20
end
21
end
changed lib/mneme/assertion/pattern_builder.ex
 
@@ -2,16 2,18 @@ defmodule Mneme.Assertion.PatternBuilder do
2
2
@moduledoc false
3
3
4
4
alias Mneme.Assertion
5
alias Mneme.Assertion.Pattern
6
alias Mneme.Utils
5
7
6
8
@doc """
7
9
Builds pattern expressions from a runtime value.
8
10
"""
9
- @spec to_patterns(term(), Assertion.context()) :: [Assertion.pattern(), ...]
11
@spec to_patterns(term(), Assertion.context()) :: [Pattern.t(), ...]
10
12
def to_patterns(value, context) do
11
13
patterns = do_to_patterns(value, context)
12
14
13
15
case fetch_pinned(value, context) do
14
- {:ok, pin} -> [{pin, nil, []} | patterns]
16
{:ok, pin} -> [Pattern.new(pin) | patterns]
15
17
:error -> patterns
16
18
end
17
19
end
 
@@ -30,39 32,40 @@ defmodule Mneme.Assertion.PatternBuilder do
30
32
defp fetch_pinned(_, _), do: :error
31
33
32
34
defp do_to_patterns(int, context) when is_integer(int) do
33
- pattern = {{:__block__, with_meta([token: inspect(int)], context), [int]}, nil, []}
34
- [pattern]
35
{:__block__, with_meta([token: inspect(int)], context), [int]}
36
|> Pattern.new()
37
|> List.wrap()
35
38
end
36
39
37
- defp do_to_patterns(value, _context)
38
- when is_atom(value) or is_float(value) do
39
- [{value, nil, []}]
40
defp do_to_patterns(value, _context) when is_atom(value) or is_float(value) do
41
[Pattern.new(value)]
40
42
end
41
43
42
44
defp do_to_patterns(string, context) when is_binary(string) do
43
- block =
44
- cond do
45
- !String.printable?(string) ->
46
- {:<<>>, [], String.to_charlist(string)}
45
newlines = Utils.occurrences(string, ?\n)
47
46
48
- String.contains?(string, "\n") ->
49
- {:__block__, with_meta([delimiter: ~S(""")], context),
50
- [string |> escape() |> format_for_heredoc()]}
47
cond do
48
!String.printable?(string) ->
49
[Pattern.new({:<<>>, [], String.to_charlist(string)})]
51
50
52
- true ->
53
- {:__block__, with_meta([delimiter: ~S(")], context), [escape(string)]}
54
- end
51
newlines >= 2 ->
52
[heredoc_pattern(string, context), string_pattern(string, context)]
55
53
56
- [{block, nil, []}]
54
newlines >= 1 ->
55
[string_pattern(string, context), heredoc_pattern(string, context)]
56
57
true ->
58
[string_pattern(string, context)]
59
end
57
60
end
58
61
59
- defp do_to_patterns([], _), do: [{[], nil, []}]
62
defp do_to_patterns([], _), do: [Pattern.new([])]
60
63
61
64
defp do_to_patterns(list, context) when is_list(list) do
62
65
patterns = enum_to_patterns(list, context)
63
66
64
67
if List.ascii_printable?(list) do
65
- patterns [{list, nil, []}]
68
patterns [Pattern.new(list)]
66
69
else
67
70
patterns
68
71
end
 
@@ -72,20 75,19 @@ defmodule Mneme.Assertion.PatternBuilder do
72
75
tuple
73
76
|> Tuple.to_list()
74
77
|> enum_to_patterns(context)
75
- |> transform_patterns(&tuple_pattern/2, context)
78
|> Enum.map(&to_tuple_pattern(&1, context))
76
79
end
77
80
78
81
for {var_name, guard} <- [ref: :is_reference, pid: :is_pid, port: :is_port] do
79
82
defp do_to_patterns(value, context) when unquote(guard)(value) do
80
- guard_non_serializable(unquote(var_name), unquote(guard), value, context)
83
[guard_pattern(unquote(var_name), unquote(guard), value, context)]
81
84
end
82
85
end
83
86
84
87
for module <- [Range, Regex, DateTime, NaiveDateTime, Date, Time] do
85
88
defp do_to_patterns(%unquote(module){} = value, context) do
86
89
{call, meta, args} = value |> inspect() |> Code.string_to_quoted!()
87
- pattern = {{call, with_meta(meta, context), args}, nil, []}
88
- [pattern]
90
[Pattern.new({call, with_meta(meta, context), args})]
89
91
end
90
92
end
91
93
 
@@ -103,16 105,16 @@ defmodule Mneme.Assertion.PatternBuilder do
103
105
end
104
106
105
107
defp do_to_patterns(%{} = map, context) when map_size(map) == 0 do
106
- [map_pattern(context)]
108
[map_pattern([], context)]
107
109
end
108
110
109
111
defp do_to_patterns(%{} = map, context) do
110
112
patterns =
111
113
map
112
114
|> enum_to_patterns(context)
113
- |> transform_patterns(&map_pattern/2, context)
115
|> Enum.map(&to_map_pattern(&1, context))
114
116
115
- [map_pattern(context) | patterns]
117
[map_pattern([], context) | patterns]
116
118
end
117
119
118
120
defp struct_to_patterns(struct, map, context, extra_notes) do
 
@@ -121,15 123,7 @@ defmodule Mneme.Assertion.PatternBuilder do
121
123
map
122
124
|> Map.filter(fn {k, v} -> v != Map.get(empty, k) end)
123
125
|> to_patterns(context)
124
- |> transform_patterns(&struct_pattern(struct, &1, &2, extra_notes), context)
125
- end
126
-
127
- defp format_for_heredoc(string) when is_binary(string) do
128
- if String.ends_with?(string, "\n") do
129
- string
130
- else
131
- string <> "\\\n"
132
- end
126
|> Enum.map(&to_struct_pattern(struct, &1, context, extra_notes))
133
127
end
134
128
135
129
defp enum_to_patterns(values, context) do
 
@@ -169,48 163,47 @@ defmodule Mneme.Assertion.PatternBuilder do
169
163
170
164
defp combine_patterns(patterns, context) do
171
165
{exprs, {guard, notes}} =
172
- Enum.map_reduce(patterns, {nil, []}, fn {expr, g1, n1}, {g2, n2} ->
173
- {expr, {combine_guards(g1, g2, context), n1 n2}}
174
- end)
166
Enum.map_reduce(
167
patterns,
168
{nil, []},
169
fn %Pattern{expr: expr, guard: g1, notes: n1}, {g2, n2} ->
170
{expr, {combine_guards(g1, g2, context), n1 n2}}
171
end
172
)
175
173
176
- {exprs, guard, notes}
174
Pattern.new(exprs, guard: guard, notes: notes)
177
175
end
178
176
179
177
defp combine_guards(nil, guard, _context), do: guard
180
178
defp combine_guards(guard, nil, _context), do: guard
181
179
defp combine_guards(g1, g2, context), do: {:and, with_meta(context), [g2, g1]}
182
180
183
- defp guard_non_serializable(name, guard, value, context) do
181
defp guard_pattern(name, guard, value, context) do
184
182
var = make_var(name, context)
185
183
186
- pattern =
187
- {var, {guard, with_meta(context), [var]},
188
- ["Using guard for non-serializable value `#{inspect(value)}`"]}
189
-
190
- [pattern]
184
Pattern.new(var,
185
guard: {guard, with_meta(context), [var]},
186
notes: ["Using guard for non-serializable value `#{inspect(value)}`"]
187
)
191
188
end
192
189
193
190
defp make_var(name, context) do
194
191
{name, with_meta(context), nil}
195
192
end
196
193
197
- defp transform_patterns(patterns, transform, context) do
198
- Enum.map(patterns, &transform.(&1, context))
194
defp to_tuple_pattern(%Pattern{expr: [e1, e2]} = pattern, _context) do
195
%{pattern | expr: {e1, e2}}
199
196
end
200
197
201
- defp tuple_pattern({[e1, e2], guard, notes}, _context) do
202
- {{e1, e2}, guard, notes}
198
defp to_tuple_pattern(%Pattern{expr: exprs} = pattern, context) do
199
%{pattern | expr: {:{}, with_meta(context), exprs}}
203
200
end
204
201
205
- defp tuple_pattern({exprs, guard, notes}, context) do
206
- {{:{}, with_meta(context), exprs}, guard, notes}
202
defp to_map_pattern(%Pattern{expr: tuples} = pattern, context) do
203
%{pattern | expr: map_pattern(tuples, context).expr}
207
204
end
208
205
209
- defp map_pattern({tuples, guard, notes} \\ {[], nil, []}, context) do
210
- {{:%{}, with_meta(context), tuples}, guard, notes}
211
- end
212
-
213
- defp struct_pattern(struct, {map_expr, guard, notes}, context, extra_notes) do
206
defp to_struct_pattern(struct, map_pattern, context, extra_notes) do
214
207
{aliased, _} =
215
208
context
216
209
|> Map.get(:aliases, [])
 
@@ -218,8 211,8 @@ defmodule Mneme.Assertion.PatternBuilder do
218
211
219
212
aliases = aliased |> Module.split() |> Enum.map(&String.to_atom/1)
220
213
221
- {{:%, with_meta(context), [{:__aliases__, with_meta(context), aliases}, map_expr]}, guard,
222
- extra_notes notes}
214
{:%, with_meta(context), [{:__aliases__, with_meta(context), aliases}, map_pattern.expr]}
215
|> Pattern.new(guard: map_pattern.guard, notes: extra_notes map_pattern.notes)
223
216
end
224
217
225
218
defp ecto_schema?(module) do
 
@@ -272,6 265,29 @@ defmodule Mneme.Assertion.PatternBuilder do
272
265
end
273
266
end
274
267
268
defp map_pattern(tuples, context) do
269
Pattern.new({:%{}, with_meta(context), tuples})
270
end
271
272
defp string_pattern(string, context) do
273
Pattern.new({:__block__, with_meta([delimiter: ~S(")], context), [escape(string)]})
274
end
275
276
defp heredoc_pattern(string, context) do
277
Pattern.new(
278
{:__block__, with_meta([delimiter: ~S(""")], context),
279
[string |> escape() |> format_for_heredoc()]}
280
)
281
end
282
283
defp format_for_heredoc(string) when is_binary(string) do
284
if String.ends_with?(string, "\n") do
285
string
286
else
287
string <> "\\\n"
288
end
289
end
290
275
291
defp escape(string) when is_binary(string) do
276
292
String.replace(string, "\\", "\\\\")
277
293
end
changed lib/mneme/diff/formatter.ex
 
@@ -3,6 3,15 @@ defmodule Mneme.Diff.Formatter do
3
3
4
4
alias Mneme.Diff
5
5
alias Mneme.Diff.Zipper
6
alias Mneme.Utils
7
8
@type fmt_instruction :: {op, bounds}
9
10
@type op :: :ins | :del | {:ins, :highlight} | {:del, :highlight}
11
12
@type bounds :: {start_bound :: bound, end_bound :: bound}
13
14
@type bound :: {line :: pos_integer(), column :: pos_integer()}
6
15
7
16
@re_newline ~r/\n|\r\n/
8
17
 
@@ -13,8 22,8 @@ defmodule Mneme.Diff.Formatter do
13
22
def highlight_lines(code, instructions) do
14
23
lines = code |> Owl.Data.lines() |> Enum.reverse()
15
24
[last_line | earlier_lines] = lines
16
- hl_instructions = denormalize_all(instructions)
17
- highlighted = highlight(hl_instructions, length(lines), last_line, earlier_lines)
25
fmt_instructions = to_fmt_instructions(instructions)
26
highlighted = highlight(fmt_instructions, length(lines), last_line, earlier_lines)
18
27
19
28
Enum.map(highlighted, fn
20
29
list when is_list(list) ->
 
@@ -25,6 34,15 @@ defmodule Mneme.Diff.Formatter do
25
34
end)
26
35
end
27
36
37
@spec fmt(op, bounds) :: fmt_instruction
38
defp fmt(op, {{l1, c1}, {l2, c2}} = bounds) when l2 >= l1 and c2 >= c1 do
39
{op, bounds}
40
end
41
42
defp fmt(_op, bounds) do
43
raise ArgumentError, "invalid bounds: #{inspect(bounds)}"
44
end
45
28
46
defp highlight(instructions, line_no, current_line, earlier_lines, line_acc \\ [], acc \\ [])
29
47
30
48
defp highlight([], _, current_line, earlier_lines, line_acc, acc) do
 
@@ -71,31 89,32 @@ defmodule Mneme.Diff.Formatter do
71
89
highlight(instructions, l - 1, next, rest_earlier, [], [[line | line_acc] | acc])
72
90
end
73
91
74
- defp denormalize_all(instructions) do
92
@spec to_fmt_instructions([Diff.instruction()]) :: [fmt_instruction]
93
defp to_fmt_instructions(instructions) do
75
94
instructions
76
95
|> Enum.flat_map(fn
77
96
{op, kind, zipper} ->
78
- denormalize(kind, op, Zipper.node(zipper), zipper)
97
to_fmt_instructions(kind, op, Zipper.node(zipper), zipper)
79
98
80
99
{op, :node, zipper, edit_script} ->
81
- denormalize_with_edit_script(op, Zipper.node(zipper), edit_script)
100
edit_script_to_fmt_instructions(op, Zipper.node(zipper), edit_script)
82
101
end)
83
102
|> Enum.sort_by(&elem(&1, 1), :desc)
84
103
end
85
104
86
- defp denormalize(:node, op, {:{}, tuple_meta, [left, right]} = node, _) do
105
defp to_fmt_instructions(:node, op, {:{}, tuple_meta, [left, right]} = node, _) do
87
106
case tuple_meta do
88
107
%{closing: _} ->
89
- [{op, bounds(node)}]
108
[fmt(op, bounds(node))]
90
109
91
110
_ ->
92
- {lc1, _} = bounds(left)
93
- {_, lc2} = bounds(right)
94
- [{op, {lc1, lc2}}]
111
{start_bound, _} = bounds(left)
112
{_, end_bound} = bounds(right)
113
[fmt(op, {start_bound, end_bound})]
95
114
end
96
115
end
97
116
98
- defp denormalize(:node, op, {:var, meta, var}, zipper) do
117
defp to_fmt_instructions(:node, op, {:var, meta, var}, zipper) do
99
118
var_bounds = bounds({:var, meta, var})
100
119
{{l, c}, {l2, c2}} = var_bounds
101
120
 
@@ -103,67 122,67 @@ defmodule Mneme.Diff.Formatter do
103
122
{:__aliases__, _, _} ->
104
123
case Zipper.right(zipper) do
105
124
nil ->
106
- [{op, {{l, c - 1}, {l2, c2}}}]
125
[fmt(op, {{l, c - 1}, {l2, c2}})]
107
126
108
127
_ ->
109
- [{op, {{l, c}, {l2, c2 1}}}]
128
[fmt(op, {{l, c}, {l2, c2 1}})]
110
129
end
111
130
112
131
_ ->
113
- [{op, var_bounds}]
132
[fmt(op, var_bounds)]
114
133
end
115
134
end
116
135
117
- defp denormalize(:node, _op, [], _zipper), do: []
136
defp to_fmt_instructions(:node, _op, [], _zipper), do: []
118
137
119
- defp denormalize(:node, op, node, _zipper) do
138
defp to_fmt_instructions(:node, op, node, _zipper) do
120
139
if bounds = bounds(node) do
121
- [{op, bounds}]
140
[fmt(op, bounds)]
122
141
else
123
142
[]
124
143
end
125
144
end
126
145
127
- defp denormalize(:delimiter, op, {:%, meta, [name, _]}, _) do
128
- [struct_start, struct_end] = denormalize_delimiter(op, meta, 1, 1)
146
defp to_fmt_instructions(:delimiter, op, {:%, meta, [name, _]}, _) do
147
[struct_start, struct_end] = delimiter_to_fmt_instructions(op, meta, 1, 1)
129
148
{_, {name_end_l, name_end_c}} = bounds(name)
130
149
131
150
[
132
151
struct_start,
133
- {op, {{name_end_l, name_end_c}, {name_end_l, name_end_c 1}}},
152
fmt(op, {{name_end_l, name_end_c}, {name_end_l, name_end_c 1}}),
134
153
struct_end
135
154
]
136
155
end
137
156
138
- defp denormalize(:delimiter, op, {:.., %{line: l, column: c}, [_, _]}, _) do
139
- [{op, {{l, c}, {l, c 2}}}]
157
defp to_fmt_instructions(:delimiter, op, {:.., %{line: l, column: c}, [_, _]}, _) do
158
[fmt(op, {{l, c}, {l, c 2}})]
140
159
end
141
160
142
- defp denormalize(:delimiter, op, {:"..//", %{line: l, column: c}, [_, range_end, _]}, _) do
161
defp to_fmt_instructions(:delimiter, op, {:"..//", %{line: l, column: c}, [_, range_end, _]}, _) do
143
162
{_, {l2, c2}} = bounds(range_end)
144
163
145
- [{op, {{l, c}, {l, c 2}}}, {op, {{l2, c2}, {l2, c2 2}}}]
164
[fmt(op, {{l, c}, {l, c 2}}), fmt(op, {{l2, c2}, {l2, c2 2}})]
146
165
end
147
166
148
- defp denormalize(:delimiter, op, {:"[]", meta, _}, _) do
149
- denormalize_delimiter(op, meta, 1, 1)
167
defp to_fmt_instructions(:delimiter, op, {:"[]", meta, _}, _) do
168
delimiter_to_fmt_instructions(op, meta, 1, 1)
150
169
end
151
170
152
- defp denormalize(:delimiter, op, {:{}, meta, _}, _) do
153
- denormalize_delimiter(op, meta, 1, 1)
171
defp to_fmt_instructions(:delimiter, op, {:{}, meta, _}, _) do
172
delimiter_to_fmt_instructions(op, meta, 1, 1)
154
173
end
155
174
156
- defp denormalize(:delimiter, op, {:%{}, meta, _}, zipper) do
175
defp to_fmt_instructions(:delimiter, op, {:%{}, meta, _}, zipper) do
157
176
case zipper |> Zipper.up() |> Zipper.node() do
158
177
{:%, %{line: l, column: c}, _} ->
159
- [{op, {{l, c}, {l, c 1}}} | denormalize_delimiter(op, meta, 1, 1)]
178
[fmt(op, {{l, c}, {l, c 1}}) | delimiter_to_fmt_instructions(op, meta, 1, 1)]
160
179
161
180
_ ->
162
- denormalize_delimiter(op, Map.update!(meta, :column, &(&1 - 1)), 2, 1)
181
delimiter_to_fmt_instructions(op, Map.update!(meta, :column, &(&1 - 1)), 2, 1)
163
182
end
164
183
end
165
184
166
- defp denormalize(
185
defp to_fmt_instructions(
167
186
:delimiter,
168
187
op,
169
188
{call, %{line: l, column: c, closing: %{line: l2, column: c2}}, _},
 
@@ -171,10 190,10 @@ defmodule Mneme.Diff.Formatter do
171
190
)
172
191
when is_atom(call) do
173
192
len = Macro.inspect_atom(:remote_call, call) |> String.length()
174
- [{op, {{l, c}, {l, c len 1}}}, {op, {{l2, c2}, {l2, c2 1}}}]
193
[fmt(op, {{l, c}, {l, c len 1}}), fmt(op, {{l2, c2}, {l2, c2 1}})]
175
194
end
176
195
177
- defp denormalize(
196
defp to_fmt_instructions(
178
197
:delimiter,
179
198
op,
180
199
{call, %{line: l, column: c}, _},
 
@@ -182,53 201,54 @@ defmodule Mneme.Diff.Formatter do
182
201
)
183
202
when is_atom(call) do
184
203
len = Macro.inspect_atom(:remote_call, call) |> String.length()
185
- [{op, {{l, c}, {l, c len}}}]
204
[fmt(op, {{l, c}, {l, c len}})]
186
205
end
187
206
188
- defp denormalize(
207
defp to_fmt_instructions(
189
208
:delimiter,
190
209
op,
191
210
{{:., _, [_left, right]}, %{closing: %{line: l2, column: c2}}, _},
192
211
_
193
212
) do
194
213
{_, {l, c}} = bounds(right)
195
- [{op, {{l, c}, {l, c 1}}}, {op, {{l2, c2}, {l2, c2 1}}}]
214
[fmt(op, {{l, c}, {l, c 1}}), fmt(op, {{l2, c2}, {l2, c2 1}})]
196
215
end
197
216
198
- defp denormalize(
217
defp to_fmt_instructions(
199
218
:delimiter,
200
219
op,
201
220
{{:., _, [var]}, %{closing: %{line: l2, column: c2}}, _},
202
221
_
203
222
) do
204
223
{_, {l, c}} = bounds(var)
205
- [{op, {{l, c}, {l, c 1}}}, {op, {{l2, c2}, {l2, c2 1}}}]
224
[fmt(op, {{l, c}, {l, c 1}}), fmt(op, {{l2, c2}, {l2, c2 1}})]
206
225
end
207
226
208
227
# Dot call delimiter with no parens, nothing to highlight
209
- defp denormalize(:delimiter, _op, {{:., _, _}, _, _}, _), do: []
228
defp to_fmt_instructions(:delimiter, _op, {{:., _, _}, _, _}, _), do: []
210
229
211
- defp denormalize(:delimiter, op, {atom, %{line: l, column: c}, _}, _) when is_atom(atom) do
230
defp to_fmt_instructions(:delimiter, op, {atom, %{line: l, column: c}, _}, _)
231
when is_atom(atom) do
212
232
len = Macro.inspect_atom(:remote_call, atom) |> String.length()
213
- [{op, {{l, c}, {l, c len}}}]
233
[fmt(op, {{l, c}, {l, c len}})]
214
234
end
215
235
216
236
# list literals and 2-tuples are structural only in the AST and cannot
217
237
# be highlighted
218
- defp denormalize(:delimiter, _op, list, _) when is_list(list), do: []
219
- defp denormalize(:delimiter, _op, {_, _}, _), do: []
238
defp to_fmt_instructions(:delimiter, _op, list, _) when is_list(list), do: []
239
defp to_fmt_instructions(:delimiter, _op, {_, _}, _), do: []
220
240
221
- defp denormalize_delimiter(op, meta, start_len, end_len) do
241
defp delimiter_to_fmt_instructions(op, meta, start_len, end_len) do
222
242
case meta do
223
243
%{line: l, column: c, closing: %{line: l2, column: c2}} ->
224
- [{op, {{l, c}, {l, c start_len}}}, {op, {{l2, c2}, {l2, c2 end_len}}}]
244
[fmt(op, {{l, c}, {l, c start_len}}), fmt(op, {{l2, c2}, {l2, c2 end_len}})]
225
245
226
246
_ ->
227
247
[]
228
248
end
229
249
end
230
250
231
- defp denormalize_with_edit_script(op, {_, meta, _}, edit_script) do
251
defp edit_script_to_fmt_instructions(op, {_, meta, _}, edit_script) do
232
252
%{line: l_start, column: c_start, delimiter: del} = meta
233
253
234
254
{l, c} =
 
@@ -247,17 267,18 @@ defmodule Mneme.Diff.Formatter do
247
267
{edit, s}, {l, c, c_start} ->
248
268
c2 =
249
269
if del == ~s(") do
250
- c String.length(s) occurrences(s, ?")
270
c String.length(s) Utils.occurrences(s, ?")
251
271
else
252
272
c String.length(s)
253
273
end
254
274
255
275
op = if edit == :eq, do: op, else: {op, :highlight}
256
- {[{op, {{l, c}, {l, c2}}}], {l, c2, c_start}}
276
277
{[fmt(op, {{l, c}, {l, c2}})], {l, c2, c_start}}
257
278
end)
258
279
259
- del_start = [{op, {{l_start, c_start}, {l_start, c_start String.length(del)}}}]
260
- del_end = [{op, {{l_end, c_end}, {l_end, c_end String.length(del)}}}]
280
del_start = [fmt(op, {{l_start, c_start}, {l_start, c_start String.length(del)}})]
281
del_end = [fmt(op, {{l_end, c_end}, {l_end, c_end String.length(del)}})]
261
282
del_start ops del_end
262
283
end
263
284
 
@@ -273,6 294,9 @@ defmodule Mneme.Diff.Formatter do
273
294
end)
274
295
end
275
296
297
@spec bounds(term()) :: bounds | nil
298
defp bounds(node)
299
276
300
defp bounds({:%{}, %{closing: %{line: l2, column: c2}, line: l, column: c}, _}) do
277
301
{{l, c - 1}, {l2, c2 1}}
278
302
end
 
@@ -295,7 319,7 @@ defmodule Mneme.Diff.Formatter do
295
319
end
296
320
297
321
defp bounds({:string, %{line: l, column: c, delimiter: ~s(")}, string}) do
298
- offset = 2 String.length(string) occurrences(string, ?")
322
offset = 2 String.length(string) Utils.occurrences(string, ?")
299
323
{{l, c}, {l, c offset}}
300
324
end
301
325
 
@@ -309,7 333,7 @@ defmodule Mneme.Diff.Formatter do
309
333
end
310
334
311
335
defp bounds({:charlist, %{line: l, column: c, delimiter: "'"}, string}) do
312
- offset = 2 String.length(string) occurrences(string, ?')
336
offset = 2 String.length(string) Utils.occurrences(string, ?')
313
337
{{l, c}, {l, c offset}}
314
338
end
315
339
 
@@ -400,9 424,4 @@ defmodule Mneme.Diff.Formatter do
400
424
401
425
defp tag(data, {:ins, :highlight}), do: Owl.Data.tag(data, [:bright, :green, :underline])
402
426
defp tag(data, {:del, :highlight}), do: Owl.Data.tag(data, [:bright, :red, :underline])
403
-
404
- defp occurrences(string, char, acc \\ 0)
405
- defp occurrences(<<char, rest::binary>>, char, acc), do: occurrences(rest, char, acc 1)
406
- defp occurrences(<<_, rest::binary>>, char, acc), do: occurrences(rest, char, acc)
407
- defp occurrences(<<>>, _char, acc), do: acc
408
427
end
changed lib/mneme/diff/pathfinding.ex
 
@@ -1,9 1,11 @@
1
- # Derived from bitwalker/libgraph:
2
- # https://github.com/bitwalker/libgraph/blob/main/lib/graph/pathfinding.ex
3
-
4
1
defmodule Mneme.Diff.Pathfinding do
5
2
@moduledoc false
6
3
4
# Originally derived from libgraph:
5
# https://github.com/bitwalker/libgraph/blob/main/lib/graph/pathfinding.ex
6
7
alias Mneme.Diff.PriorityQueue
8
7
9
@doc """
8
10
Finds the shortest path between `v` and a target vertex.
9
11
 
@@ -19,7 21,6 @@ defmodule Mneme.Diff.Pathfinding do
19
21
def lazy_a_star(v, vertex_identifier, next_fun, hfun) do
20
22
v_id = vertex_identifier.(v)
21
23
known = %{v_id => v}
22
- tree = Graph.new(vertex_identifier: &Function.identity/1) |> Graph.add_vertex(v_id)
23
24
q = PriorityQueue.new()
24
25
25
26
with {:cont, vs_out} <- next_fun.(v) do
 
@@ -27,7 28,7 @@ defmodule Mneme.Diff.Pathfinding do
27
28
28
29
q
29
30
|> push_vertices(known, v_id, vs_out, hfun)
30
- |> do_lazy_bfs(known, tree, vertex_identifier, next_fun, hfun)
31
|> do_lazy_bfs(known, %{}, vertex_identifier, next_fun, hfun)
31
32
|> case do
32
33
{:ok, known, path} -> for(id <- path, do: Map.get(known, id))
33
34
:error -> nil
 
@@ -40,33 41,29 @@ defmodule Mneme.Diff.Pathfinding do
40
41
41
42
## Private
42
43
43
- defp do_lazy_bfs(q, known, tree, vertex_identifier, next_fun, hfun) do
44
defp do_lazy_bfs(q, known, history, vertex_identifier, next_fun, hfun) do
44
45
case PriorityQueue.pop(q) do
45
- {{:value, {v1_id, v2_id, acc_cost}}, q} ->
46
{:ok, {v1_id, v2_id, acc_cost}, q} ->
46
47
v2 = Map.fetch!(known, v2_id)
47
48
48
49
case next_fun.(v2) do
49
50
:halt ->
50
- {:ok, known, construct_path(v1_id, tree, [v2_id])}
51
{:ok, known, construct_path(history, v1_id, [v2_id])}
51
52
52
53
{:cont, vs_out} ->
53
- if Map.has_key?(tree.vertices, v2_id) do
54
- do_lazy_bfs(q, known, tree, vertex_identifier, next_fun, hfun)
54
if Map.has_key?(history, v2_id) do
55
do_lazy_bfs(q, known, history, vertex_identifier, next_fun, hfun)
55
56
else
56
57
{v2_out, known} = push_known(known, vs_out, vertex_identifier)
57
-
58
- tree =
59
- tree
60
- |> Graph.add_vertex(v2_id)
61
- |> Graph.add_edge(v2_id, v1_id)
58
history = Map.put(history, v2_id, v1_id)
62
59
63
60
q
64
61
|> push_vertices(known, v2_id, v2_out, hfun, acc_cost)
65
- |> do_lazy_bfs(known, tree, vertex_identifier, next_fun, hfun)
62
|> do_lazy_bfs(known, history, vertex_identifier, next_fun, hfun)
66
63
end
67
64
end
68
65
69
- {:empty, _} ->
66
:error ->
70
67
:error
71
68
end
72
69
end
 
@@ -87,12 84,12 @@ defmodule Mneme.Diff.Pathfinding do
87
84
end)
88
85
end
89
86
90
- defp construct_path(v_id, %Graph{out_edges: oe} = tree, path) do
87
defp construct_path(history, v_id, path) do
91
88
path = [v_id | path]
92
89
93
- case oe |> Map.get(v_id, MapSet.new()) |> MapSet.to_list() do
94
- [] -> path
95
- [next_id] -> construct_path(next_id, tree, path)
90
case history do
91
%{^v_id => next_id} -> construct_path(history, next_id, path)
92
_ -> path
96
93
end
97
94
end
98
95
end
added lib/mneme/diff/priority_queue.ex
 
@@ -0,0 1,52 @@
1
defmodule Mneme.Diff.PriorityQueue do
2
@moduledoc false
3
4
@opaque t :: :gb_trees.tree()
5
6
@type priority :: non_neg_integer()
7
8
@doc """
9
Creates a new priority queue.
10
"""
11
@spec new() :: t
12
def new do
13
:gb_trees.empty()
14
end
15
16
@doc """
17
Push a new element into the queue with a given priority.
18
"""
19
@spec push(t, term(), priority) :: t
20
def push(pqueue, value, priority) do
21
case :gb_trees.lookup(priority, pqueue) do
22
:none ->
23
queue = :queue.new()
24
queue = :queue.in(value, queue)
25
:gb_trees.insert(priority, queue, pqueue)
26
27
{:value, queue} ->
28
new_queue = :queue.in(value, queue)
29
:gb_trees.update(priority, new_queue, pqueue)
30
end
31
end
32
33
@doc """
34
Pop an element out of the queue, returning `{:ok, value, queue}` or
35
`:error` if there is not an element to pop.
36
"""
37
@spec pop(t) :: {:ok, term(), t} | :error
38
def pop(pqueue) do
39
if :gb_trees.is_empty(pqueue) do
40
:error
41
else
42
{priority, queue, pqueue} = :gb_trees.take_smallest(pqueue)
43
{{:value, value}, queue} = :queue.out(queue)
44
45
if :queue.is_empty(queue) do
46
{:ok, value, pqueue}
47
else
48
{:ok, value, :gb_trees.insert(priority, queue, pqueue)}
49
end
50
end
51
end
52
end
changed lib/mneme/prompter.ex
 
@@ -1,8 1,9 @@
1
1
defmodule Mneme.Prompter do
2
- # Behaviour controlling how to prompt acceptance or rejection of an
3
- # assertion patch. This behaviour may become public in the future.
4
2
@moduledoc false
5
3
4
# This module defines a behaviour and utilities for prompting a user
5
# when an assertion should be updated.
6
6
7
@type response :: :accept | :reject | :skip | :prev | :next
7
8
@type diff :: %{left: String.t(), right: String.t()}
8
9
@type options :: map()
changed lib/mneme/prompter/terminal.ex
 
@@ -27,7 27,7 @@ defmodule Mneme.Prompter.Terminal do
27
27
28
28
@doc false
29
29
def message(%Assertion{} = assertion, counter, diff, opts) do
30
- notes = Assertion.notes(assertion)
30
notes = Assertion.pattern(assertion).notes
31
31
32
32
[
33
33
format_header(assertion, counter, opts),
added lib/mneme/utils.ex
 
@@ -0,0 1,23 @@
1
defmodule Mneme.Utils do
2
@moduledoc false
3
4
@doc """
5
Returns the occurrences of a given character in a string.
6
7
## Examples
8
9
iex> occurrences("foo", ?o)
10
2
11
12
iex> occurrences("foo", ?z)
13
0
14
15
"""
16
def occurrences(string, char) when is_binary(string) and is_integer(char) do
17
occurrences(string, char, 0)
18
end
19
20
defp occurrences(<<char, rest::binary>>, char, acc), do: occurrences(rest, char, acc 1)
21
defp occurrences(<<_, rest::binary>>, char, acc), do: occurrences(rest, char, acc)
22
defp occurrences(<<>>, _char, acc), do: acc
23
end
changed mix.exs
 
@@ -4,7 4,7 @@ defmodule Mneme.MixProject do
4
4
@app :mneme
5
5
@source_url "https://github.com/zachallaun/mneme"
6
6
7
- def version, do: "0.3.0"
7
def version, do: "0.3.1"
8
8
9
9
def project do
10
10
[
 
@@ -15,7 15,7 @@ defmodule Mneme.MixProject do
15
15
start_permanent: Mix.env() == :prod,
16
16
deps: deps(),
17
17
dialyzer: dialyzer(),
18
- test_coverage: [tool: ExCoveralls],
18
test_coverage: [tool: ExCoveralls, import_cover: "cover"],
19
19
aliases: aliases(),
20
20
preferred_cli_env: preferred_cli_env(),
21
21
 
@@ -41,10 41,13 @@ defmodule Mneme.MixProject do
41
41
{:nimble_options, "~> 1.0"},
42
42
{:sourceror, "~> 0.12"},
43
43
{:rewrite, "~> 0.6.0"},
44
- {:libgraph, "~> 0.16.0"},
45
44
46
- # Development
47
- {:excoveralls, "~> 0.15", only: :test},
45
# Development / Test
46
{:excoveralls,
47
github: "zachallaun/excoveralls", ref: "import-coverdata-improvements", only: :test},
48
# Go back to using the release version when https://github.com/parroty/excoveralls/pull/309
49
# is merged:
50
# {:excoveralls, "~> 0.15", only: :test},
48
51
{:ecto, "~> 3.9.4", only: :test},
49
52
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
50
53
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
 
@@ -59,14 62,21 @@ defmodule Mneme.MixProject do
59
62
60
63
defp aliases do
61
64
[
62
- t: "coveralls"
65
coveralls: [
66
&export_integration_coverage/1,
67
"coveralls --import-cover cover"
68
],
69
"coveralls.html": [
70
&export_integration_coverage/1,
71
"coveralls.html --import-cover cover"
72
]
63
73
]
64
74
end
65
75
66
76
defp preferred_cli_env do
67
77
[
68
78
coveralls: :test,
69
- t: :test
79
"coveralls.html": :test
70
80
]
71
81
end
72
82
 
@@ -112,4 122,8 @@ defmodule Mneme.MixProject do
112
122
]
113
123
]
114
124
end
125
126
defp export_integration_coverage(_) do
127
Application.put_env(:mneme, :export_integration_coverage, true)
128
end
115
129
end