11module 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
99import 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."
1418const COVDIR = " coverage"
1519
1620" Coverage tracefile."
1721const 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"""
2037Summarized 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}}
3651end
3752
3853"""
3954Summarized 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
4358See [`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
5871end
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
90164end
91165
166+ # ###
167+ # ### API
168+ # ###
92169
93170"""
94171$(SIGNATURES)
95172
96173Evaluate the coverage metrics for the given pkg.
97174"""
98175function 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)
117187end
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
130197easier use in combination with other test packages.
131198
132199An lcov file is also produced in `Pkg.dir(pkg, \" $(COVDIR) \" , \" $(LCOVINFO) \" )`.
133200
134201See [`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"""
136213function 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
167244end
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
251289line, 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 )
265319end
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... )
271324end
272325
326+ Base. @deprecate report_coverage () report_coverage_and_exit ()
327+
273328"""
274329$(SIGNATURES)
275330
0 commit comments