Skip to content

Commit 7ced72d

Browse files
tpappgoerz
andauthored
Continuation of #38. (#39)
Show relative filenames in coverage table This makes the table significantly more readable. Gaps on demand, relative path, refactoring. Make output more compact, as suggested in #36. Print gaps on demand. Refactor printing code, minor doc fixes. Tweak column widths, don't need sprintf, clean up imports/using, rework gaps display logic, calculate colsize dynamically, fix reductions over empty collections. Add dummy package for testing. Replace report_coverage with report_coverage_and_exit. Kwargs for io redirection, printing coverage summary, gaps Co-authored-by: @gabriel-ss Co-authored-by: Michael Goerz <[email protected]>
1 parent e302127 commit 7ced72d

File tree

8 files changed

+206
-127
lines changed

8 files changed

+206
-127
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
/docs/build
77
/docs/Manifest.toml
88
/test/coverage/Manifest.toml
9+
/test/DummyPackage/Manifest.toml

Project.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LocalCoverage"
22
uuid = "5f6e1e16-694c-5876-87ef-16b5274f298e"
33
authors = ["Tamas K. Papp <[email protected]>"]
4-
version = "0.4.0"
4+
version = "0.5.0"
55

66
[deps]
77
CoverageTools = "c36e975a-824b-4404-a568-ef97ca766997"
@@ -10,13 +10,14 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
1010
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
1111
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
1212
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
13-
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
13+
UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
1414

1515
[compat]
1616
CoverageTools = "1"
1717
DefaultApplication = "1"
1818
DocStringExtensions = "0.8, 0.9"
1919
PrettyTables = "0.12, 1.0, 2"
20+
UnPack = "1"
2021
julia = "1.6"
2122

2223
[extras]

src/LocalCoverage.jl

Lines changed: 148 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
module LocalCoverage
22

3-
using CoverageTools
4-
using DocStringExtensions
5-
using Printf
6-
using PrettyTables
7-
using DefaultApplication
8-
using LibGit2
3+
import CoverageTools
4+
using DocStringExtensions: SIGNATURES, FIELDS
5+
using PrettyTables: pretty_table, Highlighter
6+
import DefaultApplication
7+
import LibGit2
8+
using UnPack: @unpack
99
import Pkg
1010

11-
export generate_coverage, clean_coverage, report_coverage, html_coverage, generate_xml
11+
export generate_coverage, clean_coverage, report_coverage_and_exit, html_coverage, generate_xml
12+
13+
####
14+
#### helper functions and constants
15+
####
1216

1317
"Directory for coverage results."
1418
const COVDIR = "coverage"
1519

1620
"Coverage tracefile."
1721
const LCOVINFO = "lcov.info"
1822

23+
"""
24+
$(SIGNATURES)
25+
26+
Get the root directory of a package. For `nothing`, fall back to the active project.
27+
"""
28+
pkgdir(pkgstr::AbstractString) = abspath(joinpath(dirname(Base.find_package(pkgstr)), ".."))
29+
30+
pkgdir(::Nothing) = dirname(Base.active_project())
31+
32+
####
33+
#### coverage information internals
34+
####
35+
1936
"""
2037
Summarized coverage data about a single file. Evaluated for all files of a package
21-
to compose a `PackageCoverage`.
38+
to compose a [`PackageCoverageSummary`](@ref).
2239
2340
$(FIELDS)
2441
"""
@@ -29,16 +46,14 @@ Base.@kwdef struct FileCoverageSummary
2946
lines_hit::Int
3047
"Number of lines with content to be tested"
3148
lines_tracked::Int
32-
"Percentage of lines covered"
33-
coverage::Float64
3449
"List of all line ranges without coverage"
3550
coverage_gaps::Vector{UnitRange{Int}}
3651
end
3752

3853
"""
3954
Summarized coverage data about a specific package. Contains a list of
40-
`FileCoverageSummary` relative to the package files as well as global metrics
41-
about the package coverage.
55+
[`FileCoverageSummary`](@ref) relative to the package files as well as global metrics about
56+
the package coverage.
4257
4358
See [`report_coverage`](@ref).
4459
@@ -53,18 +68,77 @@ Base.@kwdef struct PackageCoverage
5368
lines_hit::Int
5469
"Total number of lines with content to be tested in the package"
5570
lines_tracked::Int
56-
"Percentage of package lines covered"
57-
coverage::Float64
5871
end
5972

60-
"""
61-
$(SIGNATURES)
73+
function Base.getproperty(summary::Union{PackageCoverage,FileCoverageSummary}, sym::Symbol)
74+
if sym :coverage_percentage
75+
100 * summary.lines_hit / summary.lines_tracked
76+
else
77+
getfield(summary, sym)
78+
end
79+
end
6280

63-
Get the root directory of a package. For `nothing`, fall back to the active project.
64-
"""
65-
pkgdir(pkgstr::AbstractString) = abspath(joinpath(dirname(Base.find_package(pkgstr)), ".."))
81+
format_gap(gap) = length(gap) == 1 ? "$(first(gap))" : "$(first(gap))$(last(gap))"
6682

67-
pkgdir(::Nothing) = dirname(Base.active_project())
83+
format_gaps(summary::FileCoverageSummary) = join(map(format_gap, summary.coverage_gaps), ", ")
84+
85+
function format_line(summary::Union{PackageCoverage,FileCoverageSummary})
86+
@unpack lines_hit, lines_tracked, coverage_percentage = summary
87+
is_pkg = summary isa PackageCoverage
88+
(name = is_pkg ? "TOTAL" : summary.filename,
89+
total = lines_tracked,
90+
hit = lines_hit,
91+
missed = lines_tracked - lines_hit,
92+
coverage_percentage,
93+
gaps = is_pkg ? "" : format_gaps(summary))
94+
end
95+
96+
function Base.show(io::IO, summary::PackageCoverage)
97+
@unpack files, package_dir = summary
98+
row_data = map(format_line, files)
99+
push!(row_data, format_line(summary))
100+
row_coverage = map(x -> x.coverage_percentage, row_data)
101+
rows = map(row_data) do row
102+
@unpack name, total, hit, missed, coverage_percentage, gaps = row
103+
percentage = isnan(coverage_percentage) ? "-" : "$(round(Int, coverage_percentage))%"
104+
(; name, total, hit, missed, percentage, gaps)
105+
end
106+
header = ["Filename", "Lines", "Hit", "Miss", "%"]
107+
percentage_column = length(header)
108+
alignment = [:l, :r, :r, :r, :r]
109+
columns_width = fill(0, 5)
110+
if get(io, :print_gaps, false)
111+
push!(header, "Gaps")
112+
push!(alignment, :l)
113+
display_cols = last(get(io, :displaysize, 100))
114+
push!(columns_width, display_cols - 45)
115+
else
116+
rows = map(row -> Base.structdiff(row, NamedTuple{(:gaps,)}), rows)
117+
end
118+
highlighters = (
119+
Highlighter(
120+
(data, i, j) -> j == percentage_column && row_coverage[i] <= 50,
121+
bold = true,
122+
foreground = :red,
123+
),
124+
Highlighter((data, i, j) -> j == percentage_column && row_coverage[i] <= 70, foreground = :yellow),
125+
Highlighter((data, i, j) -> j == percentage_column && row_coverage[i] >= 90, foreground = :green),
126+
)
127+
128+
pretty_table(
129+
io,
130+
rows;
131+
title = "Coverage of $(package_dir)",
132+
header,
133+
alignment,
134+
crop = :none,
135+
linebreaks = true,
136+
columns_width,
137+
autowrap = true,
138+
highlighters,
139+
body_hlines = [length(rows) - 1],
140+
)
141+
end
68142

69143
"""
70144
$(SIGNATURES)
@@ -89,49 +163,52 @@ function find_gaps(coverage)
89163
gaps
90164
end
91165

166+
####
167+
#### API
168+
####
92169

93170
"""
94171
$(SIGNATURES)
95172
96173
Evaluate the coverage metrics for the given pkg.
97174
"""
98175
function eval_coverage_metrics(coverage, package_dir)
99-
coverage_list = map(coverage) do file
100-
tracked = count(!isnothing, file.coverage)
101-
gaps = find_gaps(file.coverage)
102-
hit = tracked - sum(length.(gaps))
103-
104-
FileCoverageSummary(file.filename, hit, tracked, 100 * hit / tracked, gaps)
176+
files = map(coverage) do file
177+
@unpack coverage = file
178+
lines_tracked = count(!isnothing, coverage)
179+
coverage_gaps = find_gaps(coverage)
180+
lines_hit = lines_tracked - sum(length, coverage_gaps; init = 0)
181+
filename = relpath(file.filename, package_dir)
182+
FileCoverageSummary(; filename, lines_hit, lines_tracked, coverage_gaps)
105183
end
106-
107-
total_hit = sum(getfield.(coverage_list, :lines_hit))
108-
total_tracked = sum(getfield.(coverage_list, :lines_tracked))
109-
110-
PackageCoverage(
111-
package_dir,
112-
coverage_list,
113-
total_hit,
114-
total_tracked,
115-
100 * total_hit / total_tracked,
116-
)
184+
lines_hit = sum(x -> x.lines_hit, files; init = 0)
185+
lines_tracked = sum(x -> x.lines_tracked, files; init = 0)
186+
PackageCoverage(; package_dir, files, lines_hit, lines_tracked)
117187
end
118188

119189
"""
120190
$(SIGNATURES)
121191
122-
Generate a PackageCoverage that summarizes coverage results for package `pkg`.
123-
124-
If no pkg is supplied, the method operates in the currently active pkg.
192+
Generate a summary of coverage results for package `pkg`.
125193
126-
A percentage target_coverage may be specified to control the color coding of the
127-
pretty printed results.
194+
If no `pkg` is supplied, the method operates in the currently active package.
128195
129-
The test execution step may be skipped by passing run_test=false, allowing an
196+
The test execution step may be skipped by passing `run_test = false`, allowing an
130197
easier use in combination with other test packages.
131198
132199
An lcov file is also produced in `Pkg.dir(pkg, \"$(COVDIR)\", \"$(LCOVINFO)\")`.
133200
134201
See [`report_coverage`](@ref), [`clean_coverage`](@ref).
202+
203+
# Printing
204+
205+
Printing of the result can be controlled via `IOContext`.
206+
207+
```julia
208+
cov = generate_coverage(pkg)
209+
show(IOContext(stdout, :print_gaps => true), cov) # print coverage gap information
210+
```
211+
135212
"""
136213
function generate_coverage(pkg = nothing; run_test = true)
137214
if run_test
@@ -161,50 +238,11 @@ function clean_coverage(pkg = nothing; rm_directory::Bool = true)
161238
if rm_directory
162239
rm(COVDIR; force = true, recursive = true)
163240
else
164-
rm(COVDIR, LCOVINFO)
241+
rm(joinpath(COVDIR, LCOVINFO))
165242
end
166243
end
167244
end
168245

169-
format_gap(gap) = length(gap) == 1 ? "$(first(gap))" : "$(first(gap)) - $(last(gap))"
170-
function format_line(summary)
171-
hcat(
172-
summary isa PackageCoverage ? "TOTAL" : summary.filename,
173-
@sprintf("%3d / %3d", summary.lines_hit, summary.lines_tracked),
174-
isnan(summary.coverage) ? "-" : @sprintf("%3.0f%%", summary.coverage),
175-
summary isa PackageCoverage ? "" :
176-
join(map(format_gap, summary.coverage_gaps), ", "),
177-
)
178-
end
179-
180-
function Base.show(io::IO, coverage::PackageCoverage)
181-
table = reduce(vcat, map(format_line, [coverage.files..., coverage]))
182-
row_coverage = [getfield.(coverage.files, :coverage)... coverage.coverage]
183-
184-
highlighters = (
185-
Highlighter(
186-
(data, i, j) -> j == 3 && row_coverage[i] <= 50,
187-
bold = true,
188-
foreground = :red,
189-
),
190-
Highlighter((data, i, j) -> j == 3 && row_coverage[i] <= 70, foreground = :yellow),
191-
Highlighter((data, i, j) -> j == 3 && row_coverage[i] >= 90, foreground = :green),
192-
)
193-
194-
pretty_table(
195-
io,
196-
table,
197-
header = ["File name", "Lines hit", "Coverage", "Missing"],
198-
alignment = [:l, :r, :r, :r],
199-
crop = :none,
200-
linebreaks = true,
201-
columns_width = [min(30, maximum(length.(table[:, 1]))), 11, 8, 35],
202-
autowrap = true,
203-
highlighters = highlighters,
204-
body_hlines = [size(table, 1) - 1],
205-
)
206-
end
207-
208246
"""
209247
$(SIGNATURES)
210248
@@ -251,25 +289,42 @@ the target coverage was met or with a status code 1 otherwise. Useful in command
251289
line, e.g.
252290
253291
```bash
254-
julia --project -e'using LocalCoverage; report_coverage(target_coverage=90)'
292+
julia --project=@. -e'using LocalCoverage; report_coverage_and_exit(;target_coverage=90)'
255293
```
256294
257-
See [`generate_coverage`](@ref).
295+
# Arguments
296+
297+
The only positional argument is either information generated by [`generate_coverage`](@ref),
298+
or a package name (defaults to `nothing`, the active project). For the latter, coverage will
299+
be generated.
300+
301+
# Keyword arguments and defaults
302+
303+
- `target_coverage = 80` determines the threshold for passing coverage
304+
- `print_summary = true` controls whether a detailed summary is printed
305+
- `print_gaps = false` controls whether gaps are printed
306+
- `io` can be used for redirecting the output
258307
"""
259-
function report_coverage(coverage::PackageCoverage, target_coverage = 80)
260-
was_target_met = coverage.coverage >= target_coverage
261-
print(" Target coverage ", was_target_met ? "was met" : "wasn't met", " (")
262-
printstyled("$target_coverage%", color = was_target_met ? :green : :red, bold = true)
263-
println(")")
308+
function report_coverage_and_exit(coverage::PackageCoverage;
309+
target_coverage::Real = 80,
310+
print_summary::Bool = true,
311+
print_gaps::Bool = false,
312+
io::IO = stdout)
313+
was_target_met = coverage.coverage_percentage >= target_coverage
314+
print_summary && show(IOContext(io, :print_gaps => print_gaps), coverage)
315+
print(io, " Target coverage ", was_target_met ? "was met" : "wasn't met", " (")
316+
printstyled(io, "$target_coverage%", color = was_target_met ? :green : :red, bold = true)
317+
println(io, ")")
264318
exit(was_target_met ? 0 : 1)
265319
end
266320

267-
function report_coverage(pkg = nothing; target_coverage = 80)
321+
function report_coverage_and_exit(pkg = nothing; kwargs...)
268322
coverage = generate_coverage(pkg)
269-
show(coverage)
270-
report_coverage(coverage, target_coverage)
323+
report_coverage_and_exit(coverage; kwargs...)
271324
end
272325

326+
Base.@deprecate report_coverage() report_coverage_and_exit()
327+
273328
"""
274329
$(SIGNATURES)
275330

test/DummyPackage/Project.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name = "DummyPackage"
2+
uuid = "d56c877c-c572-4b7f-9f2e-7d24fd6b5b05"
3+
4+
[deps]
5+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"A package for testing coverage generation."
2+
module DummyPackage
3+
4+
export foo, bar
5+
6+
foo() = 42
7+
8+
include("bar.jl")
9+
10+
end

test/DummyPackage/src/bar.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bar() = "a fish"
2+
3+
baz() = "this is untested"

test/DummyPackage/test/runtests.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
using Test, DummyPackage
2+
3+
@test foo() == 42
4+
@test bar() == "a fish"

0 commit comments

Comments
 (0)