Current section

Files

Jump to
double_down lib double_down dynamic_facade cover.ex
Raw

lib/double_down/dynamic_facade/cover.ex

defmodule DoubleDown.DynamicFacade.Cover do
@moduledoc """
Ensures DynamicFacade-shimmed modules retain `:cover` instrumentation.
When ExCoverall or `:cover` compiles a module with coverage instrumentation,
`DynamicFacade.setup/1` renames the on-disk (non-instrumented) bytecode to
`Module.__dd_original__`, stripping coverage from the backup. This module
preserves coverage by:
1. Exporting pre-shim coverdata
2. Re-instrumenting the backup via `:cover.compile_beams/1`
3. Providing `merge/1` to rewrite backup coverdata back to the original name
"""
@doc false
# Recompiles the `:cover` module with `:export_all` to expose
# `compile_beams/1` (private function that accepts in-memory binaries).
# Idempotent — checks if private functions are already exported.
# Based on meck and Mimic's approach.
def export_private_functions do
if not private_functions_exported?() do
{_, binary, _} = :code.get_object_code(:cover)
{:ok, {_, [{_, {:raw_abstract_v1, abstract_code}}]}} =
:beam_lib.chunks(binary, [:abstract_code])
{:ok, module, binary} = :compile.forms(abstract_code, [:export_all])
{:module, :cover} = :code.load_binary(module, ~c"", binary)
end
:ok
end
@doc false
def enabled_for?(module) do
cover_loaded?() and apply(:cover, :is_compiled, [module]) != false
end
defp cover_loaded? do
is_tuple(:code.is_loaded(:cover))
end
@doc false
def export_coverdata!(module) do
path = Path.expand("#{module}-#{:os.getpid()}.coverdata", ".")
:ok = apply(:cover, :export, [String.to_charlist(path), module])
path
end
@doc """
Merge coverdata from the `__dd_original__` backup into the original module name.
Exports coverdata for `Module.__dd_original__`, rewrites all module-name
references back to `Module`, imports the result, and cleans up the temp file.
Call this after all tests have completed to ensure ExCoverall sees coverage
for DynamicFacade-shimmed modules under their original names.
"""
def merge(module) do
backup = Module.concat(module, :__dd_original__)
if enabled_for?(backup) do
path = export_coverdata!(backup)
rewrite_coverdata!(path, backup, module)
:ok = apply(:cover, :import, [String.to_charlist(path)])
File.rm(path)
end
:ok
end
@doc """
Merge coverdata for all registered DynamicFacade modules.
Iterates all modules set up via `DynamicFacade.setup/1` and calls
`merge/1` for each. No-op if `:cover` is not available or has no
instrumented modules.
Call this from `test/test_helper.exs` after all tests complete:
ExUnit.after_suite(fn _ -> DoubleDown.DynamicFacade.Cover.merge_all() end)
"""
def merge_all do
if cover_loaded?() and apply(:cover, :modules, []) != [] do
Enum.each(DoubleDown.DynamicFacade.registered_modules(), &merge/1)
end
:ok
end
# -- Private --
defp private_functions_exported? do
function_exported?(:cover, :get_term, 1)
end
defp rewrite_coverdata!(path, from_module, to_module) do
terms = get_terms(path)
terms = replace_module_name(terms, from_module, to_module)
write_coverdata!(path, terms)
end
defp replace_module_name(terms, from_module, to_module) do
Enum.map(terms, fn term -> do_replace_module_name(term, from_module, to_module) end)
end
defp do_replace_module_name({:file, _old, file}, from_module, to_module) do
{:file, to_module, String.replace(file, to_string(from_module), to_string(to_module))}
end
defp do_replace_module_name({bump = {:bump, _mod, _, _, _, _}, value}, from_module, to_module) do
{put_elem(bump, 2, bump_module_name(bump, from_module, to_module)), value}
end
defp do_replace_module_name({_mod, clauses}, from_module, to_module) do
{to_module, replace_module_name(clauses, from_module, to_module)}
end
defp do_replace_module_name(clause = {_mod, _, _, _, _}, _from_module, to_module) do
put_elem(clause, 0, to_module)
end
defp do_replace_module_name(other, _from_module, _to_module), do: other
defp bump_module_name(bump, from_module, to_module) do
case elem(bump, 1) do
^from_module -> to_module
other -> other
end
end
defp get_terms(path) do
{:ok, resource} = File.open(path, [:binary, :read, :raw])
terms = get_terms(resource, [])
File.close(resource)
terms
end
defp get_terms(resource, terms) do
case apply(:cover, :get_term, [resource]) do
:eof -> terms
term -> get_terms(resource, [term | terms])
end
end
defp write_coverdata!(path, terms) do
{:ok, resource} = File.open(path, [:write, :binary, :raw])
Enum.each(terms, fn term -> apply(:cover, :write, [term, resource]) end)
File.close(resource)
end
end