1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
type point = { start_ofs : int; end_ofs : int }
let format =
{
Instr.magic = "windtrap-coverage-v3";
kind = "coverage";
dir = "coverage";
ext = "coverage";
remedy =
"delete the stale coverage files, then re-run the instrumented tests";
who = "Windtrap_runtime.Coverage";
}
type error = Data of Instr.error | Point_mismatch of { file : string }
let pp_error ppf = function
| Data e -> Instr.pp_error format ppf e
| Point_mismatch { file } ->
Format.fprintf ppf
"%s: coverage point tables disagree across coverage files (executables \
built from different sources?); re-run every instrumented test \
executable from one build, then merge again; delete the coverage \
files only if leftovers remain"
file
module File_map = Map.Make (String)
type entry = { points : point array; counts : int array }
type t = entry File_map.t
let empty = File_map.empty
let points_equal a b =
Array.length a = Array.length b
&& Array.for_all2
(fun p q -> p.start_ofs = q.start_ofs && p.end_ofs = q.end_ofs)
a b
let saturating_add x y = if x > max_int - y then max_int else x + y
let add t ~file entry =
match File_map.find_opt file t with
| None -> Ok (File_map.add file entry t)
| Some prior when not (points_equal prior.points entry.points) ->
Error (Point_mismatch { file })
| Some prior ->
let counts = Array.map2 saturating_add prior.counts entry.counts in
Ok (File_map.add file { prior with counts } t)
let merge a b =
File_map.fold
(fun file entry acc -> Result.bind acc (fun t -> add t ~file entry))
b (Ok a)
let files t = List.map fst (File_map.bindings t)
type identity = Instr.identity = { exe : string; digest : string }
let to_string ~identity t =
let b = Buffer.create 1024 in
Instr.add_header format b identity;
Printf.bprintf b "%d\n" (File_map.cardinal t);
File_map.iter
(fun file { points; counts } ->
Printf.bprintf b "%d %s\n%d\n" (String.length file) file
(Array.length points);
Array.iteri
(fun i p ->
Printf.bprintf b "%d %d %d\n" p.start_ofs p.end_ofs counts.(i))
points)
t;
Buffer.contents b
let load path =
let read_entry c file =
let read_point _ =
let start_ofs = Instr.read_nat c "extent start" in
let end_ofs = Instr.read_nat c "extent end" in
if end_ofs < start_ofs then
Instr.parse_fail "inverted extent %d-%d in %s" start_ofs end_ofs file;
({ start_ofs; end_ofs }, Instr.read_nat c "count")
in
let rows = Array.init (Instr.read_count c "point count") read_point in
let points, counts = Array.split rows in
{ points; counts }
in
let rec read_files c t n =
if n = 0 then begin
Instr.finish c;
Ok t
end
else
let file = Instr.read_name c "file name" in
Result.bind
(add t ~file (read_entry c file))
(fun t -> read_files c t (n - 1))
in
match Result.bind (Instr.read_file path) (Instr.start format ~path) with
| Error e -> Error (Data e)
| Ok c -> (
try
let identity = Instr.read_identity c in
let file_count = Instr.read_count c "file count" in
Result.map (fun t -> (t, identity)) (read_files c empty file_count)
with Instr.Parse_error reason ->
Error (Data (Instr.Corrupt { path; reason })))
let registry : (point array * int array list) File_map.t ref =
ref File_map.empty
let snapshot () =
File_map.map
(fun (points, live) ->
let zero = Array.make (Array.length points) 0 in
{ points; counts = List.fold_left (Array.map2 saturating_add) zero live })
!registry
type target = File of string | Dir of string
let remove_predecessors dir ~prefix =
match Sys.readdir dir with
| exception Sys_error _ -> ()
| names ->
Array.iter
(fun name ->
if
Filename.check_suffix name ("." ^ format.Instr.ext)
&& not (String.starts_with ~prefix name)
then
try Sys.remove (Filename.concat dir name) with Sys_error _ -> ())
names
let dump target ~exe () =
let t = snapshot () in
let identity =
Option.bind exe (fun exe ->
Option.map
(fun digest -> { exe; digest })
(Instr.file_digest Instr.executable))
in
let data = to_string ~identity t in
match target with
| File path -> (
try Instr.write_file path data
with e ->
Instr.warn "cannot write coverage file %s: %s" path
(Printexc.to_string e))
| Dir dir -> (
let prefix =
match identity with
| None -> ""
| Some { digest; _ } ->
let prefix = digest ^ "-" in
remove_predecessors dir ~prefix;
prefix
in
try ignore (Instr.write_new_file dir ~prefix ~ext:format.Instr.ext data)
with e ->
Instr.warn "cannot write a coverage file under %s: %s" dir
(Printexc.to_string e))
let install_dump () =
let determined f =
match f () with
| v -> Some v
| exception e ->
Instr.warn "cannot determine the coverage output file: %s"
(Printexc.to_string e);
None
in
let target () =
match Sys.getenv_opt "WINDTRAP_COVERAGE_FILE" with
| Some path when path <> "" -> File (Instr.absolute path)
| Some _ | None -> Dir (Instr.output_dir format ~exe:Instr.executable)
in
match determined target with
| None -> ()
| Some target ->
let exe =
determined (fun () -> Instr.exe_identity ~exe:Instr.executable)
in
at_exit (dump target ~exe)
let validate ~file points counts =
let err fmt =
Printf.ksprintf invalid_arg ("Windtrap_runtime.Coverage: %s: " ^^ fmt) file
in
if Array.length points <> Array.length counts then
err "%d points but %d counts" (Array.length points) (Array.length counts);
Array.iter
(fun p ->
if p.start_ofs < 0 || p.end_ofs < p.start_ofs then
err "invalid extent %d-%d" p.start_ofs p.end_ofs)
points;
if Array.exists (fun c -> c < 0) counts then err "negative count"
let register ~file ~points ~counts =
validate ~file points counts;
match File_map.find_opt file !registry with
| Some (table, _) when not (points_equal table points) ->
Instr.warn
"%s: conflicting instrumentation tables in one executable (stale build \
artifacts? rebuild from clean); ignoring one module's coverage data"
file
| registered ->
if File_map.is_empty !registry then install_dump ();
let table, live = Option.value registered ~default:(points, []) in
registry := File_map.add file (table, counts :: live) !registry
let visit counts index =
let count = counts.(index) in
if count < max_int then counts.(index) <- count + 1
type summary = { visited : int; total : int }
let file_summary entry =
let visited =
Array.fold_left (fun n c -> if c > 0 then n + 1 else n) 0 entry.counts
in
{ visited; total = Array.length entry.counts }
let summary t =
File_map.fold
(fun _ entry acc ->
let s = file_summary entry in
{ visited = acc.visited + s.visited; total = acc.total + s.total })
t { visited = 0; total = 0 }
let line_starts source =
let starts = ref [ 0 ] in
for i = 0 to String.length source - 2 do
if source.[i] = '\n' then starts := (i + 1) :: !starts
done;
Array.of_list (List.rev !starts)
let line_index starts ofs =
let rec search lo hi =
if lo >= hi then lo
else
let mid = (lo + hi + 1) / 2 in
if starts.(mid) <= ofs then search mid hi else search lo (mid - 1)
in
search 0 (Array.length starts - 1)
let line_hits ~source entry =
if source = "" then []
else
let starts = line_starts source in
let hits = Array.make (Array.length starts) None in
Array.iteri
(fun i p ->
let count = entry.counts.(i) in
let first = line_index starts p.start_ofs in
let last = line_index starts (max p.start_ofs (p.end_ofs - 1)) in
for line = first to last do
match hits.(line) with
| Some h when h <= count -> ()
| Some _ | None -> hits.(line) <- Some count
done)
entry.points;
Array.to_list hits
|> List.mapi (fun line h -> Option.map (fun h -> (line + 1, h)) h)
|> List.filter_map Fun.id
let uncovered_extents entry =
List.filteri (fun i _ -> entry.counts.(i) = 0) (Array.to_list entry.points)
let find_source ~roots file =
file :: List.map (fun root -> Filename.concat root file) roots
|> List.find_map (fun path ->
match Sys.is_directory path with
| false -> Result.to_option (Instr.read_file path)
| true | (exception Sys_error _) -> None)
let is_stale entry source =
Array.exists (fun p -> p.end_ofs > String.length source) entry.points
type file_report = {
file : string;
summary : summary;
uncovered_extents : point list;
uncovered_lines : int list;
line_hits : (int * int) list;
source : string option;
stale : bool;
}
let file_reports ?(source_roots = [ Filename.current_dir_name ]) t =
let report (file, entry) =
let source, stale =
match find_source ~roots:source_roots file with
| Some source when is_stale entry source -> (None, true)
| source -> (source, false)
in
let line_hits =
match source with None -> [] | Some source -> line_hits ~source entry
in
{
file;
summary = file_summary entry;
uncovered_extents = uncovered_extents entry;
uncovered_lines =
List.filter_map
(fun (line, hits) -> if hits = 0 then Some line else None)
line_hits;
line_hits;
source;
stale;
}
in
List.map report (File_map.bindings t)