v / cmd / tools / vcover / main.v
276 lines · 258 sloc · 8.06 KB · 985843fbc9b935f54c5443617044fcaf39780fd4
Raw
1// Copyright (c) 2024 Felipe Pena and Delyan Angelov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module main
5
6import os
7import log
8import flag
9import json
10import arrays
11import encoding.csv
12
13// program options, storage etc
14struct Context {
15mut:
16 show_help bool
17 show_hotspots bool
18 show_percentages bool
19 show_test_files bool
20 use_absolute_paths bool
21 be_verbose bool
22 lcov_output string
23 filter string
24 working_folder string
25
26 targets []string
27 meta map[string]MetaData // aggregated meta data, read from all .json files
28 all_lines_per_file map[string][]int // aggregated by load_meta
29
30 counters map[string]u64 // incremented by process_target, based on each .csv file
31 lines_per_file map[string]map[int]int // incremented by process_target, based on each .csv file
32 processed_points u64
33}
34
35const metadata_extension = '.json'
36const vcounter_glob_pattern = 'vcounters_*.csv'
37
38fn (mut ctx Context) load_meta(folder string) {
39 for omfile in os.walk_ext(folder, metadata_extension) {
40 mfile := omfile.replace('\\', '/')
41 content := os.read_file(mfile) or { '' }
42 meta := os.file_name(mfile.replace(metadata_extension, ''))
43 data := json.decode(MetaData, content) or {
44 log.error('${@METHOD} failed to load ${mfile}')
45 continue
46 }
47 ctx.meta[meta] = data
48 mut lines_per_file := ctx.all_lines_per_file[data.file]
49 lines_per_file << data.points
50 ctx.all_lines_per_file[data.file] = arrays.distinct(lines_per_file)
51 }
52}
53
54fn (mut ctx Context) post_process_all_metas() {
55 ctx.verbose('${@METHOD}')
56 for _, m in ctx.meta {
57 lines_per_file := ctx.all_lines_per_file[m.file]
58 for line in lines_per_file {
59 ctx.counters['${m.file}:${line}:'] = 0
60 }
61 }
62}
63
64fn (mut ctx Context) post_process_all_targets() {
65 ctx.verbose('${@METHOD}')
66 ctx.verbose('ctx.processed_points: ${ctx.processed_points}')
67}
68
69fn (ctx &Context) verbose(msg string) {
70 if ctx.be_verbose {
71 log.info(msg)
72 }
73}
74
75fn (mut ctx Context) process_target(tfile string) ! {
76 ctx.verbose('${@METHOD} ${tfile}')
77 mut reader := csv.new_reader_from_file(tfile)!
78 header := reader.read()!
79 if header != ['meta', 'point', 'hits'] {
80 return error('invalid header in .csv file')
81 }
82 for {
83 row := reader.read() or { break }
84 mut cline := CounterLine{
85 meta: row[0]
86 point: row[1].int()
87 hits: row[2].u64()
88 }
89 m := ctx.meta[cline.meta] or {
90 ctx.verbose('> skipping invalid meta: ${cline.meta} in file: ${cline.file}, csvfile: ${tfile}')
91 continue
92 }
93 cline.file = m.file
94 cline.line = m.points[cline.point] or {
95 ctx.verbose('> skipping invalid point: ${cline.point} in file: ${cline.file}, meta: ${cline.meta}, csvfile: ${tfile}')
96 continue
97 }
98 ctx.counters['${cline.file}:${cline.line}:'] += cline.hits
99 mut lines := ctx.lines_per_file[cline.file].move()
100 lines[cline.line]++
101 ctx.lines_per_file[cline.file] = lines.move()
102 // dump( ctx.lines_per_file[cline.meta][cline.point] )
103 ctx.processed_points++
104 }
105}
106
107fn (mut ctx Context) show_report() ! {
108 filters := ctx.filter.split(',').filter(it != '')
109 if ctx.show_hotspots {
110 mut locations := []string{cap: ctx.counters.len}
111 for location, _ in ctx.counters {
112 if !ctx.matches_filters(location, filters) {
113 continue
114 }
115 locations << location
116 }
117 locations.sort()
118 for location in locations {
119 hits := ctx.counters[location]
120 mut final_path := normalize_path(location)
121 if !ctx.use_absolute_paths {
122 final_path = location.all_after_first('${ctx.working_folder}/')
123 }
124 println('${hits:-8} ${final_path}')
125 }
126 }
127 if ctx.show_percentages {
128 for file in ctx.sorted_hit_files(filters) {
129 total_lines := ctx.all_lines_per_file[file].len
130 executed_points := ctx.lines_per_file[file].len
131 coverage_percent := 100.0 * f64(executed_points) / f64(total_lines)
132 mut final_path := normalize_path(file)
133 if !ctx.use_absolute_paths {
134 final_path = file.all_after_first('${ctx.working_folder}/')
135 }
136 println('${final_path:-80s} | ${executed_points:6} | ${total_lines:6} | ${coverage_percent:6.2f}%')
137 }
138 }
139 if ctx.lcov_output != '' {
140 ctx.write_lcov_report(filters)!
141 }
142}
143
144fn normalize_path(path string) string {
145 return path.replace(os.path_separator, '/')
146}
147
148fn (ctx &Context) matches_filters(path string, filters []string) bool {
149 if filters.len == 0 {
150 return true
151 }
152 return filters.any(path.contains(it))
153}
154
155fn (ctx &Context) should_include_file(file string, filters []string) bool {
156 if !ctx.show_test_files && (file.ends_with('_test.v') || file.ends_with('_test.c.v')) {
157 return false
158 }
159 return ctx.matches_filters(file, filters)
160}
161
162fn (ctx &Context) sorted_hit_files(filters []string) []string {
163 mut files := []string{}
164 for file, _ in ctx.lines_per_file {
165 if !ctx.should_include_file(file, filters) {
166 continue
167 }
168 files << file
169 }
170 files.sort()
171 return files
172}
173
174fn (ctx &Context) sorted_files(filters []string) []string {
175 mut files := []string{}
176 for file, _ in ctx.all_lines_per_file {
177 if !ctx.should_include_file(file, filters) {
178 continue
179 }
180 files << file
181 }
182 files.sort()
183 return files
184}
185
186fn (ctx &Context) write_lcov_report(filters []string) ! {
187 output_path := os.real_path(ctx.lcov_output)
188 output_dir := os.dir(output_path)
189 if output_dir != '' && !os.exists(output_dir) {
190 os.mkdir_all(output_dir)!
191 }
192 mut output := []string{}
193 for file in ctx.sorted_files(filters) {
194 mut lines := ctx.all_lines_per_file[file].clone()
195 lines.sort()
196 hit_lines := ctx.lines_per_file[file].len
197 output << 'TN:'
198 output << 'SF:${normalize_path(file)}'
199 for line in lines {
200 hits := ctx.counters['${file}:${line}:']
201 output << 'DA:${line},${hits}'
202 }
203 output << 'LF:${lines.len}'
204 output << 'LH:${hit_lines}'
205 output << 'end_of_record'
206 }
207 os.write_file(output_path, output.join_lines())!
208 ctx.verbose('Wrote LCOV report to ${output_path}')
209}
210
211fn main() {
212 log.use_stdout()
213 mut ctx := Context{}
214 ctx.working_folder = normalize_path(os.real_path(os.getwd()))
215 mut fp := flag.new_flag_parser(os.args#[1..])
216 fp.application('v cover')
217 fp.version('0.0.2')
218 fp.description('Analyze & make reports, based on cover files, produced by running programs and tests, compiled with `-coverage folder/`')
219 fp.arguments_description('[folder1/ file2 ...]')
220 fp.skip_executable()
221 ctx.show_help = fp.bool('help', `h`, false, 'Show this help text.')
222 ctx.be_verbose = fp.bool('verbose', `v`, false,
223 'Be more verbose while processing the coverages.')
224 ctx.show_hotspots = fp.bool('hotspots', `H`, false,
225 'Show most frequently executed covered lines.')
226 ctx.show_percentages = fp.bool('percentages', `P`, true, 'Show coverage percentage per file.')
227 ctx.lcov_output = fp.string('lcov', 0, '',
228 'Write an LCOV line coverage report to the specified file path.')
229 ctx.show_test_files = fp.bool('show_test_files', `S`, false,
230 'Show `_test.v` files as well (normally filtered).')
231 ctx.use_absolute_paths = fp.bool('absolute', `A`, false,
232 'Use absolute paths for all files, no matter the current folder. By default, files inside the current folder, are shown with a relative path.')
233 ctx.filter = fp.string('filter', `f`, '', 'Filter only the matching source path patterns.')
234 if ctx.show_help {
235 println(fp.usage())
236 exit(0)
237 }
238 targets := fp.finalize() or {
239 log.error(fp.usage())
240 exit(1)
241 }
242 ctx.verbose('Targets: ${targets}')
243 for t in targets {
244 if !os.exists(t) {
245 log.error('Skipping ${t}, since it does not exist')
246 continue
247 }
248 if os.is_dir(t) {
249 found_counter_files := os.walk_ext(t, '.csv')
250 if found_counter_files.len == 0 {
251 log.error('Skipping ${t}, since there are 0 ${vcounter_glob_pattern} files in it')
252 continue
253 }
254 for counterfile in found_counter_files {
255 ctx.targets << counterfile
256 ctx.load_meta(t)
257 }
258 } else {
259 ctx.targets << t
260 ctx.load_meta(os.dir(t))
261 }
262 }
263 ctx.post_process_all_metas()
264 ctx.verbose('Final ctx.targets.len: ${ctx.targets.len}')
265 ctx.verbose('Final ctx.meta.len: ${ctx.meta.len}')
266 ctx.verbose('Final ctx.filter: ${ctx.filter}')
267 if ctx.targets.len == 0 {
268 log.error('0 cover targets')
269 exit(1)
270 }
271 for t in ctx.targets {
272 ctx.process_target(t)!
273 }
274 ctx.post_process_all_targets()
275 ctx.show_report()!
276}
277