Skip to content

Commit a4e7a7b

Browse files
committed
wip: Implement Test Coverage Reporting
1 parent e0c016c commit a4e7a7b

17 files changed

Lines changed: 221 additions & 19 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ permissions:
2121

2222
jobs:
2323
test_linux:
24-
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}
24+
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
2525
strategy:
2626
fail-fast: false
2727
matrix:
2828
include:
2929
- otp_version: "27.1"
3030
deterministic: true
31+
- otp_version: "27.1"
32+
erlc_opts: "warnings_as_errors"
33+
coverage: true
3134
- otp_version: "27.1"
3235
otp_latest: true
3336
erlc_opts: "warnings_as_errors"
@@ -65,9 +68,14 @@ jobs:
6568
- name: Erlang test suite
6669
run: make test_erlang
6770
continue-on-error: ${{ matrix.development }}
71+
if: "${{ !matrix.coverage }}"
6872
- name: Elixir test suite
6973
run: make test_elixir
7074
continue-on-error: ${{ matrix.development }}
75+
if: "${{ !matrix.coverage }}"
76+
- name: "Calculate Coverage"
77+
run: make cover
78+
if: "${{ matrix.coverage }}"
7179
- name: Build docs (ExDoc main)
7280
if: ${{ matrix.otp_latest }}
7381
run: |
@@ -85,6 +93,12 @@ jobs:
8593
# Recompile System without .git
8694
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
8795
taskset 1 make check_reproducible
96+
- name: "Upload Coverage Artifact"
97+
if: "${{ matrix.coverage }}"
98+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
99+
with:
100+
name: TestCoverage
101+
path: cover/*
88102

89103
test_windows:
90104
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
/.eunit
1616
.elixir.plt
1717
erl_crash.dump
18+
/cover/

Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
5353
test_$(1): test_formatted $(1)
5454
@ echo "==> $(1) (ex_unit)"
5555
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
56+
57+
cover/exunit_$(1).coverdata:
58+
$(Q) mkdir -p cover
59+
$(Q) COVER_FILE="$(realpath cover)/exunit_$(1).coverdata" $(MAKE) test_$(1)
60+
cover/combined.coverdata: cover/exunit_$(1).coverdata
5661
endef
5762

5863
define WRITE_SOURCE_DATE_EPOCH
@@ -175,6 +180,7 @@ clean: clean_man
175180
rm -rf lib/mix/test/fixtures/git_sparse_repo/
176181
rm -rf lib/mix/test/fixtures/archive/ebin/
177182
rm -f erl_crash.dump
183+
rm -rf cover/*
178184

179185
clean_elixir:
180186
$(Q) rm -f lib/*/ebin/Elixir.*.beam
@@ -287,6 +293,17 @@ test_stdlib: compile
287293
cd lib/elixir && ../../bin/elixir --sname primary -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
288294
fi
289295

296+
cover/eunit_stdlib.coverdata:
297+
$(Q) mkdir -p cover
298+
$(Q) COVER_FILE="$(realpath cover)/eunit_stdlib.coverdata" $(MAKE) test_erlang
299+
cover/combined.coverdata: cover/eunit_stdlib.coverdata
300+
301+
cover/combined.coverdata:
302+
$(Q) bin/elixir ./lib/elixir/scripts/cover.exs
303+
304+
.PHONY: cover
305+
cover: cover/combined.coverdata
306+
290307
#==> Dialyzer tasks
291308

292309
DIALYZER_OPTS = --no_check_plt --fullpath -Werror_handling -Wunmatched_returns -Wunderspecs

lib/eex/test/test_helper.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
{line_exclude, line_include} =
66
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
77

8+
Code.eval_file("../../../elixir/scripts/cover_record.exs", __ENV__.file)
9+
810
ExUnit.start(
911
trace: !!System.get_env("TRACE"),
1012
include: line_include,

lib/elixir/scripts/cover.exs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!bin/elixir
2+
3+
# SPDX-License-Identifier: Apache-2.0
4+
# SPDX-FileCopyrightText: 2021 The Elixir Team
5+
6+
root_dir = __ENV__.file |> Path.dirname() |> Path.join("../../..")
7+
cover_dir = Path.join(root_dir, "cover")
8+
coverdata_inputs = cover_dir |> Path.join("{exunit,eunit}_*.coverdata") |> Path.wildcard()
9+
coverdata_output = Path.join(cover_dir, "combined.coverdata")
10+
ebins = root_dir |> Path.join("lib/*/ebin") |> Path.wildcard()
11+
12+
_ = :cover.stop()
13+
{:ok, cover_pid} = :cover.start()
14+
15+
for ebin <- ebins,
16+
result <- :cover.compile_beam_directory(String.to_charlist(ebin)) do
17+
case result do
18+
{:ok, _module} ->
19+
:ok
20+
21+
{:error, reason} ->
22+
raise "Failed to cover compile directory #{ebin} with reason: #{inspect(reason)}"
23+
end
24+
end
25+
26+
for file <- coverdata_inputs do
27+
:ok = :cover.import(String.to_charlist(file))
28+
end
29+
30+
:ok = :cover.export(String.to_charlist(coverdata_output))
31+
32+
{:ok, _} = Application.ensure_all_started(:mix)
33+
34+
# Silence analyse import messages emitted by cover
35+
{:ok, string_io} = StringIO.open("")
36+
Process.group_leader(cover_pid, string_io)
37+
38+
:ok =
39+
Mix.Tasks.Test.Coverage.generate_cover_results(
40+
output: cover_dir,
41+
summary: [threshold: 0]
42+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: 2021 The Elixir Team
3+
4+
root_dir = __ENV__.file |> Path.dirname() |> Path.join("../../..")
5+
ebins = root_dir |> Path.join("lib/*/ebin") |> Path.wildcard()
6+
7+
case System.fetch_env("COVER_FILE") do
8+
{:ok, file} ->
9+
_ = :cover.stop()
10+
{:ok, _pid} = :cover.start()
11+
12+
for ebin <- ebins,
13+
result <- :cover.compile_beam_directory(String.to_charlist(ebin)) do
14+
case result do
15+
{:ok, _module} ->
16+
:ok
17+
18+
{:error, reason} ->
19+
raise "Failed to cover compile directory #{ebin} with reason: #{inspect(reason)}"
20+
end
21+
end
22+
23+
System.at_exit(fn _status ->
24+
:ok = :cover.export(String.to_charlist(file))
25+
end)
26+
27+
true
28+
29+
:error ->
30+
false
31+
end

lib/elixir/test/elixir/test_helper.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ source_exclude =
120120
[]
121121
end
122122

123+
Code.eval_file("../../scripts/cover_record.exs", __ENV__.file)
124+
123125
ExUnit.start(
124126
trace: !!System.get_env("TRACE"),
125127
assert_receive_timeout: assert_timeout,

lib/elixir/test/erlang/test_helper.erl

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
test() ->
1616
application:ensure_all_started(elixir),
17-
case eunit:test(?TESTS) of
18-
error -> erlang:halt(1);
19-
_Res -> erlang:halt(0)
20-
end.
17+
enable_coverage_report(),
18+
ExitCode = case eunit:test(?TESTS) of
19+
error -> 1;
20+
_Res -> 0
21+
end,
22+
write_report_coverage(),
23+
erlang:halt(ExitCode).
2124

2225
% Execute a piece of code and purge given modules right after
2326
run_and_remove(Fun, Modules) ->
@@ -39,3 +42,20 @@ throw_erlang(String) ->
3942
{ok, Tokens, _} = erl_scan:string(String),
4043
{ok, [Form]} = erl_parse:parse_exprs(Tokens),
4144
erlang:error(io:format("~p~n", [Form])).
45+
46+
enable_coverage_report() ->
47+
case os:getenv("COVER_FILE") of
48+
false -> ok;
49+
_File ->
50+
_ = cover:stop(),
51+
{ok, _Pid} = cover:start(),
52+
Ebin = filename:dirname(filename:absname(?FILE)) ++ "/../../ebin",
53+
% TODO: Check Result
54+
cover:compile_beam_directory(Ebin)
55+
end.
56+
57+
write_report_coverage() ->
58+
case os:getenv("COVER_FILE") of
59+
false -> ok;
60+
File -> cover:export(File)
61+
end.

lib/ex_unit/test/ex_unit/formatter_test.exs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ defmodule ExUnit.FormatterTest do
8686

8787
failure = [{:exit, {{error, stack}, {:mod, :fun, []}}, []}]
8888

89-
assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
89+
format = trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2))
90+
91+
assert format =~
9092
"""
9193
1) world (Hello)
9294
test/ex_unit/formatter_test.exs:1
@@ -101,11 +103,16 @@ defmodule ExUnit.FormatterTest do
101103
102104
# 2
103105
:bar
106+
"""
104107

105-
Attempted function clauses (showing 5 out of 5):
108+
if Access not in :cover.modules() do
109+
assert format =~
110+
"""
111+
Attempted function clauses (showing 5 out of 5):
106112
107-
def fetch(%module{} = container, key)
108-
"""
113+
def fetch(%module{} = container, key)
114+
"""
115+
end
109116
end
110117

111118
test "formats test exits with assertion mfa" do
@@ -177,11 +184,16 @@ defmodule ExUnit.FormatterTest do
177184
178185
# 2
179186
:bar
187+
"""
180188

181-
Attempted function clauses (showing 5 out of 5):
189+
if Access not in :cover.modules() do
190+
assert format =~
191+
"""
192+
Attempted function clauses (showing 5 out of 5):
182193
183-
def fetch(%module{} = container, key)
184-
"""
194+
def fetch(%module{} = container, key)
195+
"""
196+
end
185197

186198
assert format =~ ~r"lib/access.ex:\d+: Access.fetch/2"
187199
end
@@ -418,11 +430,16 @@ defmodule ExUnit.FormatterTest do
418430
419431
# 2
420432
:bar
433+
"""
421434

422-
Attempted function clauses (showing 5 out of 5):
435+
if Access not in :cover.modules() do
436+
assert failure =~
437+
"""
438+
Attempted function clauses (showing 5 out of 5):
423439
424-
def fetch(%module{} = container, key)
425-
"""
440+
def fetch(%module{} = container, key)
441+
"""
442+
end
426443

427444
assert failure =~ ~r"\(elixir #{System.version()}\) lib/access\.ex:\d+: Access\.fetch/2"
428445
end

lib/ex_unit/test/test_helper.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Logger.configure_backend(:console, colors: [enabled: false])
77
{line_exclude, line_include} =
88
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
99

10+
Code.eval_file("../../../elixir/scripts/cover_record.exs", __ENV__.file)
11+
1012
ExUnit.start(
1113
trace: !!System.get_env("TRACE"),
1214
include: line_include,

0 commit comments

Comments
 (0)