Skip to content

Commit 2502844

Browse files
authored
100% test coverage (#246)
1 parent 926d69d commit 2502844

File tree

24 files changed

+923
-158
lines changed

24 files changed

+923
-158
lines changed

README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ Running `bin/elixir_analyzer` on a system with Elixir/Erlang/OTP installed
2929
```text
3030
Usage:
3131
$ elixir_analyzer <exercise-slug> <path the folder containing the solution> <path to folder for output> [options]
32-
33-
You may also pass the following options:
34-
--skip-analysis flag skips running the static analysis
35-
--output-file <filename>
36-
37-
You may also test only individual files :
38-
(assuming analyzer tests are compiled for the named module)
39-
$ exercism_analyzer --analyze-file <full-path-to-.ex>:<module-name>
4032
```
4133

4234
### via IEX

lib/elixir_analyzer.ex

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,14 @@ defmodule ElixirAnalyzer do
2929
3030
* `exercise` is which exercise is submitted to determine proper analysis
3131
32-
* `path` is the path (ending with a '/') to the submitted solution
32+
* `input_path` is the path to the submitted solution
33+
34+
* `output_path` is the path to the output folder
3335
3436
* `opts` is a Keyword List of options, see **options**
3537
3638
## Options
3739
38-
* `:exercise` - name of the exercise, defaults to the `exercise` parameter
39-
40-
* `:path` - path to the submitted solution, defaults to the `path` parameter
41-
42-
* `:output_path` - path to write file output, defaults to the `path` parameter
43-
4440
* `:output_file`, - specifies the name of the output_file, defaults to
4541
`@output_file` (`analysis.json`)
4642
@@ -52,8 +48,6 @@ defmodule ElixirAnalyzer do
5248
5349
* `:puts_summary` - boolean flag if an analysis should print the summary of the
5450
analysis to stdio, defaults to `true`
55-
56-
Any arbitrary keyword-value pair can be passed to `analyze_exercise/3` and these options may be used the other consuming code.
5751
"""
5852
@spec analyze_exercise(String.t(), String.t(), String.t(), keyword()) :: Submission.t()
5953
def analyze_exercise(exercise, input_path, output_path, opts \\ []) do
@@ -147,14 +141,14 @@ defmodule ElixirAnalyzer do
147141
}
148142
rescue
149143
e in File.Error ->
150-
Logger.warning("Unable to decode 'config.json'", error_message: e.message)
144+
Logger.warning("Unable to read config file #{e.path}", error_message: e.reason)
151145

152146
submission
153147
|> Submission.halt()
154148
|> Submission.set_halt_reason("Analysis skipped, not able to read solution config.")
155149

156150
e in Jason.DecodeError ->
157-
Logger.warning("Unable to decode 'config.json'", error_message: e.message)
151+
Logger.warning("Unable to decode 'config.json'", data: e.data)
158152

159153
submission
160154
|> Submission.halt()
@@ -256,7 +250,7 @@ defmodule ElixirAnalyzer do
256250

257251
submission =
258252
submission
259-
|> submission.analysis_module.analyze(submission.source)
253+
|> submission.analysis_module.analyze()
260254
|> Submission.set_analyzed(true)
261255

262256
Logger.info("Analyzing code complete")

lib/elixir_analyzer/cli.ex

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,19 @@ defmodule ElixirAnalyzer.CLI do
66
@usage """
77
Usage:
88
9-
$ elixir_analyzer <exercise-name> <input path> <output path> [options]
9+
$ elixir_analyzer <exercise-name> <input path> <output path> [options]
1010
1111
You may also pass the following options:
12-
--skip-analysis flag skips running the static analysis
13-
--output-file <filename>
14-
15-
You may also test only individual files :
16-
(assuming analyzer tests are compiled for the named module)
17-
18-
$ exercism_analyzer --analyze-file <full-path-to-.ex>:<module-name>
12+
--help see this message
13+
--output-file <filename> output file name (default: analysis.json)
14+
--no-write-results doesn't write to JSON file
15+
--no-puts-summary doesn't print summary to stdio
1916
"""
2017

2118
@options [
22-
{{:skip_analyze, :boolean}, false},
2319
{{:output_file, :string}, "analysis.json"},
24-
{{:analyze_file, :string}, nil},
20+
{{:write_results, :boolean}, true},
21+
{{:puts_summary, :boolean}, true},
2522
{{:help, :boolean}, false}
2623
]
2724

@@ -30,45 +27,27 @@ defmodule ElixirAnalyzer.CLI do
3027
args |> parse_args() |> process()
3128
end
3229

33-
def parse_args(args) do
34-
options = %{
35-
:output_file => "analysis.json"
36-
}
30+
defp parse_args(args) do
31+
default_ops = for({{key, _}, val} <- @options, do: {key, val}, into: %{})
3732

38-
cmd_opts =
39-
OptionParser.parse(args,
40-
strict: for({o, _} <- @options, do: o)
41-
)
33+
cmd_opts = OptionParser.parse(args, strict: for({o, _} <- @options, do: o))
4234

4335
case cmd_opts do
4436
{[help: true], _, _} ->
4537
:help
4638

47-
{[analyze_file: target], _, _} ->
48-
[full_path, module] = String.split(target, ":", trim: true)
49-
path = Path.dirname(full_path)
50-
file = Path.basename(full_path)
51-
{Enum.into([module: module, file: file], options), "undefined", path}
52-
5339
{opts, [exercise, input_path, output_path], _} ->
54-
{Enum.into(opts, options), exercise, input_path, output_path}
40+
{Enum.into(opts, default_ops), exercise, input_path, output_path}
5541
end
5642
rescue
5743
_ -> :help
5844
end
5945

60-
def process(:help), do: IO.puts(@usage)
46+
defp process(:help), do: IO.puts(@usage)
6147

62-
def process({options, exercise, input_path, output_path}) do
63-
opts = get_default_options(options)
64-
ElixirAnalyzer.analyze_exercise(exercise, input_path, output_path, opts)
65-
end
48+
defp process({options, exercise, input_path, output_path}) do
49+
opts = Map.to_list(options)
6650

67-
defp get_default_options(options) do
68-
@options
69-
|> Enum.reduce(options, fn {{option, _}, default}, acc ->
70-
Map.put_new(acc, option, default)
71-
end)
72-
|> Map.to_list()
51+
ElixirAnalyzer.analyze_exercise(exercise, input_path, output_path, opts)
7352
end
7453
end

lib/elixir_analyzer/exercise_test.ex

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ defmodule ElixirAnalyzer.ExerciseTest do
2222

2323
import unquote(__MODULE__)
2424
@before_compile unquote(__MODULE__)
25-
@dialyzer no_match: {:do_analyze, 2}
25+
@dialyzer no_match: {:do_analyze, 1}
2626
end
2727
end
2828

@@ -51,19 +51,20 @@ defmodule ElixirAnalyzer.ExerciseTest do
5151
check_source_tests = Enum.map(check_source_data, &CheckSourceCompiler.compile(&1, source))
5252

5353
quote do
54-
@spec analyze(Submission.t(), Source.t()) :: Submission.t()
55-
def analyze(%Submission{} = submission, %Source{code_string: code_string} = source) do
54+
@spec analyze(Submission.t()) :: Submission.t()
55+
def analyze(%Submission{source: %Source{code_string: code_string} = source} = submission) do
5656
case Code.string_to_quoted(code_string) do
5757
{:ok, code_ast} ->
5858
source = %{source | code_ast: code_ast}
59-
do_analyze(submission, source)
59+
submission = %{submission | source: source}
60+
do_analyze(submission)
6061

6162
{:error, e} ->
6263
append_analysis_failure(submission, e)
6364
end
6465
end
6566

66-
defp do_analyze(%Submission{} = submission, %Source{code_ast: code_ast} = source) do
67+
defp do_analyze(%Submission{source: %Source{code_ast: code_ast} = source} = submission) do
6768
results =
6869
Enum.concat([
6970
unquote(feature_tests),

lib/elixir_analyzer/exercise_test/assert_call/compiler.ex

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,9 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
150150
"""
151151
@spec matching_function_call?(
152152
Macro.t(),
153-
nil | AssertCall.function_signature(),
153+
AssertCall.function_signature(),
154154
%{[atom] => [atom] | keyword()}
155155
) :: boolean()
156-
def matching_function_call?(_node, nil, _), do: false
157156

158157
# For erlang libraries: :math._ or :math.pow
159158
def matching_function_call?(
@@ -209,22 +208,6 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
209208

210209
def matching_function_call?(_, _, _), do: false
211210

212-
@doc """
213-
compare a node to the function_signature, looking for a match for a called function
214-
"""
215-
@spec matching_function_def?(Macro.t(), AssertCall.function_signature()) :: boolean()
216-
def matching_function_def?(_node, nil), do: false
217-
218-
def matching_function_def?(
219-
{def_type, _, [{name, _, _args}, [do: {:__block__, _, [_ | _]}]]},
220-
{_module_path, name}
221-
)
222-
when def_type in ~w[def defp]a do
223-
true
224-
end
225-
226-
def matching_function_def?(_, _), do: false
227-
228211
@doc """
229212
node is a module definition
230213
"""
@@ -237,13 +220,10 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
237220
def extract_module_name({:defmodule, _, [{:__aliases__, _, name}, [do: _]]}),
238221
do: name
239222

240-
def extract_module_name(_), do: nil
241-
242223
@doc """
243224
node is a function definition
244225
"""
245-
def function_def?({def_type, _, [{name, _, _}, [do: _]]})
246-
when is_atom(name) and def_type in ~w[def defp]a do
226+
def function_def?({def_type, _, [_, [do: _]]}) when def_type in ~w[def defp]a do
247227
true
248228
end
249229

@@ -260,8 +240,6 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.Compiler do
260240
when is_atom(name) and def_type in ~w[def defp]a,
261241
do: name
262242

263-
def extract_function_name(_), do: nil
264-
265243
@doc """
266244
compare the name of the function to the function signature, if they match return true
267245
"""

lib/elixir_analyzer/exercise_test/common_checks/function_capture.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.FunctionCapture do
5454
depth = depth - 1
5555

5656
functions =
57-
if depth == 0 and wrong_use? and actual_function?(name) and name not in @exceptions do
57+
if depth <= 0 and wrong_use? and actual_function?(name) and name not in @exceptions do
5858
[{:&, name, length(args)} | functions]
5959
else
6060
functions
6161
end
6262

63-
{node, %{capture_depth: depth - 1, functions: functions}}
63+
{node, %{capture_depth: depth, functions: functions}}
6464
end
6565

6666
# fn -> foo end

lib/elixir_analyzer/exercise_test/feature.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,16 @@ defmodule ElixirAnalyzer.ExerciseTest.Feature do
5353
feature_data = %{feature_data | meta: Map.to_list(feature_data.meta)}
5454
feature_data = Map.to_list(feature_data)
5555

56+
unless Keyword.has_key?(feature_data, :comment) do
57+
raise "Comment must be defined for each feature test"
58+
end
59+
5660
quote do
5761
# Check if the feature is unique
58-
case Enum.filter(@feature_tests, fn {_data, forms} ->
59-
forms == unquote(Macro.escape(feature_forms))
62+
case Enum.filter(@feature_tests, fn {data, forms} ->
63+
{Keyword.get(data, :find), Keyword.get(data, :depth), forms} ==
64+
{Keyword.get(unquote(feature_data), :find),
65+
Keyword.get(unquote(feature_data), :depth), unquote(Macro.escape(feature_forms))}
6066
end) do
6167
[{data, _forms} | _] ->
6268
raise FeatureError,

lib/elixir_analyzer/quote_util.ex

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,6 @@ defmodule ElixirAnalyzer.QuoteUtil do
6868
end)
6969
end
7070

71-
@doc """
72-
Performs a depth-first, pre-order traversal of quoted expressions.
73-
With depth provided to a function
74-
"""
75-
@spec prewalk(Macro.t(), (Macro.t(), non_neg_integer -> Macro.t())) :: Macro.t()
76-
def prewalk(ast, fun) when is_function(fun, 2) do
77-
elem(prewalk(ast, nil, fn x, nil, d -> {fun.(x, d), nil} end), 0)
78-
end
79-
8071
@doc """
8172
Performs a depth-first, pre-order traversal of quoted expressions
8273
using an accumulator.
@@ -86,22 +77,4 @@ defmodule ElixirAnalyzer.QuoteUtil do
8677
def prewalk(ast, acc, fun) when is_function(fun, 3) do
8778
traverse_with_depth(ast, acc, fun, fn x, a, _d -> {x, a} end)
8879
end
89-
90-
@doc """
91-
Performs a depth-first, post-order traversal of quoted expressions.
92-
"""
93-
@spec postwalk(Macro.t(), (Macro.t(), non_neg_integer -> Macro.t())) :: Macro.t()
94-
def postwalk(ast, fun) when is_function(fun, 2) do
95-
elem(postwalk(ast, nil, fn x, nil, d -> {fun.(x, d), nil} end), 0)
96-
end
97-
98-
@doc """
99-
Performs a depth-first, post-order traversal of quoted expressions
100-
using an accumulator.
101-
"""
102-
@spec postwalk(Macro.t(), any, (Macro.t(), any, non_neg_integer -> {Macro.t(), any})) ::
103-
{Macro.t(), any}
104-
def postwalk(ast, acc, fun) when is_function(fun, 3) do
105-
traverse_with_depth(ast, acc, fn x, a, _d -> {x, a} end, fun)
106-
end
10780
end

0 commit comments

Comments
 (0)