Skip to content

Commit c7529f8

Browse files
authored
Merge pull request #7 from edgurgel/cover
Cover
2 parents 42ea17c + e6356e0 commit c7529f8

File tree

9 files changed

+185
-27
lines changed

9 files changed

+185
-27
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
sudo: false
22
language: elixir
33
elixir:
4-
- 1.7
54
- 1.8
65
- 1.9
76
otp_release:
87
- 21.0
98
- 22.0
109
script:
1110
- mix test
11+
- mix test --cover
1212
- mix format --check-formatted
1313
- mix credo --strict

lib/mimic.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,8 @@ defmodule Mimic do
335335
"Module #{inspect(module)} is not available"
336336
end
337337

338-
original_module = Mimic.Module.original(module)
339-
Mimic.Module.replace!(module, original_module)
338+
Mimic.Module.replace!(module)
339+
ExUnit.after_suite(fn _ -> Server.reset(module) end)
340340

341341
:ok
342342
end

lib/mimic/application.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
defmodule Mimic.Application do
22
use Application
3-
alias Mimic.Server
3+
alias Mimic.{Cover, Server}
44
@moduledoc false
55

66
def start(_, _) do
7+
Cover.export_private_functions()
78
children = [Server]
89
Supervisor.start_link(children, name: Mimic.Supervisor, strategy: :one_for_one)
910
end

lib/mimic/cover.ex

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
defmodule Mimic.Cover do
2+
@moduledoc """
3+
Abuse cover private functions to move stuff around
4+
Completely based on meck's solution:
5+
https://github.com/eproxus/meck/blob/2c7ba603416e95401500d7e116c5a829cb558665/src/meck_cover.erl#L67-L91
6+
"""
7+
8+
@doc false
9+
def export_private_functions do
10+
{_, binary, _} = :code.get_object_code(:cover)
11+
{:ok, {_, [{_, {_, abstract_code}}]}} = :beam_lib.chunks(binary, [:abstract_code])
12+
{:ok, module, binary} = :compile.forms(abstract_code, [:export_all])
13+
:code.load_binary(module, '', binary)
14+
end
15+
16+
@doc false
17+
def replace_coverdata!(module, original_beam, original_coverdata) do
18+
original_module = Mimic.Module.original(module)
19+
path = export_coverdata!(original_module)
20+
rewrite_coverdata!(path, module)
21+
Mimic.Module.clear!(module)
22+
:cover.compile_beam(original_beam)
23+
:ok = :cover.import(path)
24+
:ok = :cover.import(original_coverdata)
25+
File.rm(path)
26+
File.rm(original_coverdata)
27+
end
28+
29+
@doc false
30+
def export_coverdata!(module) do
31+
path = Path.expand("#{module}-#{:os.getpid()}.coverdata", ".")
32+
:ok = :cover.export(path, module)
33+
path
34+
end
35+
36+
defp rewrite_coverdata!(path, module) do
37+
terms = get_terms(path)
38+
terms = replace_module_name(terms, module)
39+
write_coverdata!(path, terms)
40+
end
41+
42+
defp replace_module_name(terms, module) do
43+
Enum.map(terms, fn term -> do_replace_module_name(term, module) end)
44+
end
45+
46+
defp do_replace_module_name({:file, old, file}, module) do
47+
{:file, module, String.replace(file, to_string(old), to_string(module))}
48+
end
49+
50+
defp do_replace_module_name({bump = {:bump, _mod, _, _, _, _}, value}, module) do
51+
{put_elem(bump, 1, module), value}
52+
end
53+
54+
defp do_replace_module_name({_mod, clauses}, module) do
55+
{module, replace_module_name(clauses, module)}
56+
end
57+
58+
defp do_replace_module_name(clause = {_mod, _, _, _, _}, module) do
59+
put_elem(clause, 0, module)
60+
end
61+
62+
defp get_terms(path) do
63+
{:ok, resource} = File.open(path, [:binary, :read, :raw])
64+
terms = get_terms(resource, [])
65+
File.close(resource)
66+
terms
67+
end
68+
69+
defp get_terms(resource, terms) do
70+
case apply(:cover, :get_term, [resource]) do
71+
:eof -> terms
72+
term -> get_terms(resource, [term | terms])
73+
end
74+
end
75+
76+
defp write_coverdata!(path, terms) do
77+
{:ok, resource} = File.open(path, [:write, :binary, :raw])
78+
Enum.each(terms, fn term -> apply(:cover, :write, [term, resource]) end)
79+
File.close(resource)
80+
end
81+
end

lib/mimic/module.ex

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
defmodule Mimic.Module do
2-
alias Mimic.Server
2+
alias Mimic.{Cover, Server}
33
@moduledoc false
44

55
def original(module), do: "#{module}.Mimic.Original.Module" |> String.to_atom()
66

77
def clear!(module) do
88
:code.purge(module)
99
:code.delete(module)
10-
{:module, ^module} = :code.ensure_loaded(module)
10+
:code.purge(original(module))
11+
:code.delete(original(module))
1112
:ok
1213
end
1314

14-
def replace!(module, backup_module) do
15+
def replace!(module) do
16+
backup_module = original(module)
17+
18+
case :cover.is_compiled(module) do
19+
{:file, beam_file} ->
20+
coverdata_path = Cover.export_coverdata!(module)
21+
Server.store_beam_and_coverdata(module, beam_file, coverdata_path)
22+
23+
false ->
24+
:ok
25+
end
26+
1527
rename_module(module, backup_module)
1628
Code.compiler_options(ignore_module_conflict: true)
1729
create_mock(module)
@@ -21,42 +33,47 @@ defmodule Mimic.Module do
2133
end
2234

2335
defp rename_module(module, new_module) do
24-
beam_file =
25-
case :code.get_object_code(module) do
26-
{_, binary, _filename} -> binary
27-
_error -> throw({:object_code_not_found, module})
28-
end
36+
beam_code = beam_code(module)
2937

30-
result =
31-
case :beam_lib.chunks(beam_file, [:abstract_code]) do
32-
{:ok, {_, [{:abstract_code, {:raw_abstract_v1, forms}}]}} -> forms
33-
{:ok, {_, [{:abstract_code, :no_abstract_code}]}} -> throw(:no_abstract_code)
34-
end
38+
{:ok, {_, [{:abstract_code, {:raw_abstract_v1, forms}}]}} =
39+
:beam_lib.chunks(beam_code, [:abstract_code])
3540

36-
result =
37-
result
38-
|> rename_attribute(new_module)
41+
forms = rename_attribute(forms, new_module)
3942

40-
case :compile.forms(result, [:return_errors]) do
43+
case :compile.forms(forms, compiler_options(module)) do
4144
{:ok, module_name, binary} ->
4245
load_binary(module_name, binary)
4346
binary
4447

4548
{:ok, module_name, binary, _warnings} ->
4649
load_binary(module_name, binary)
4750
Binary
51+
end
52+
end
4853

49-
error ->
50-
exit({:compile_forms, error})
54+
defp beam_code(module) do
55+
case :code.get_object_code(module) do
56+
{_, binary, _filename} -> binary
57+
_error -> throw({:object_code_not_found, module})
5158
end
5259
end
5360

61+
defp compiler_options(module) do
62+
options =
63+
module.module_info(:compile)
64+
|> Keyword.get(:options)
65+
|> Enum.filter(&(&1 != :from_core))
66+
67+
[:return_errors | [:debug_info | options]]
68+
end
69+
5470
defp load_binary(module, binary) do
5571
case :code.load_binary(module, '', binary) do
5672
{:module, ^module} -> :ok
5773
{:error, reason} -> exit({:error_loading_module, module, reason})
58-
_ -> :yolo
5974
end
75+
76+
apply(:cover, :compile_beams, [[{module, binary}]])
6077
end
6178

6279
defp rename_attribute([{:attribute, line, :module, {_, vars}} | t], new_name) do

lib/mimic/server.ex

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule Mimic.Server do
22
use GenServer
3+
alias Mimic.Cover
34
@moduledoc false
45

56
defmodule State do
@@ -8,7 +9,8 @@ defmodule Mimic.Server do
89
mode: :private,
910
global_pid: nil,
1011
stubs: %{},
11-
expectations: %{}
12+
expectations: %{},
13+
modules_beam: %{}
1214
end
1315

1416
defmodule Expectation do
@@ -52,6 +54,14 @@ defmodule Mimic.Server do
5254
GenServer.cast(__MODULE__, {:exit, pid})
5355
end
5456

57+
def store_beam_and_coverdata(module, beam, coverdata) do
58+
GenServer.call(__MODULE__, {:store_beam_and_coverdata, module, beam, coverdata})
59+
end
60+
61+
def reset(module) do
62+
GenServer.call(__MODULE__, {:reset, module})
63+
end
64+
5565
def apply(module, fn_name, args) do
5666
arity = Enum.count(args)
5767
original_module = Mimic.Module.original(module)
@@ -293,6 +303,20 @@ defmodule Mimic.Server do
293303
{:reply, :ok, %{state | verify_on_exit: MapSet.put(state.verify_on_exit, pid)}}
294304
end
295305

306+
def handle_call({:store_beam_and_coverdata, module, beam, coverdata}, _from, state) do
307+
modules_beam = Map.put(state.modules_beam, module, {beam, coverdata})
308+
{:reply, :ok, %{state | modules_beam: modules_beam}}
309+
end
310+
311+
def handle_call({:reset, module}, _from, state) do
312+
case state.modules_beam[module] do
313+
{beam, coverdata} -> Cover.replace_coverdata!(module, beam, coverdata)
314+
_ -> Mimic.Module.clear!(module)
315+
end
316+
317+
{:reply, :ok, state}
318+
end
319+
296320
defp apply_call_to_expectations(
297321
expectations,
298322
expectation = %Expectation{num_applied_calls: num_applied_calls, num_calls: num_calls}

mix.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ defmodule Mimic.Mixfile do
44
def project do
55
[
66
app: :mimic,
7-
version: "1.0.0",
8-
elixir: "~> 1.7",
7+
version: "1.1.0",
8+
elixir: "~> 1.8",
99
build_embedded: Mix.env() == :prod,
1010
start_permanent: Mix.env() == :prod,
1111
elixirc_paths: elixirc_paths(Mix.env()),
1212
name: "Mimic",
1313
description: "Mocks for Elixir functions",
1414
deps: deps(),
1515
package: package(),
16+
test_coverage: [tool: Mimic.TestCover],
1617
docs: [extras: ["README.md"], main: "readme"]
1718
]
1819
end

test/support/test_cover.ex

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule Mimic.TestCover do
2+
@moduledoc false
3+
4+
@doc false
5+
def start(compile_path, _opts) do
6+
:cover.stop()
7+
:cover.start()
8+
:cover.compile_beam_directory(compile_path |> String.to_charlist())
9+
10+
fn ->
11+
execute()
12+
end
13+
end
14+
15+
defp execute do
16+
{:result, results, _fail} = :cover.analyse(:calls, :function)
17+
18+
results =
19+
Enum.filter(results, fn
20+
{{Calculator, _, _}, _} -> true
21+
_ -> false
22+
end)
23+
24+
expected =
25+
{{Calculator, :add, 2}, 5} in results &&
26+
{{Calculator, :mult, 2}, 5} in results
27+
28+
unless expected do
29+
IO.puts("Cover results are incorrect!")
30+
throw(:test_cover_failed)
31+
end
32+
end
33+
end

test/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
Calculator.add(2, 3)
12
Mimic.copy(Calculator)
23
Mimic.copy(Counter)
34
ExUnit.start()

0 commit comments

Comments
 (0)