Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/pandoc/datadir/luacov/reporter.lua
12923 views
1
------------------------
2
-- Report module, will transform statistics file into a report.
3
-- @class module
4
-- @name luacov.reporter
5
local reporter = {}
6
7
local LineScanner = require("luacov.linescanner")
8
local luacov = require("luacov.runner")
9
local util = require("luacov.util")
10
local lfs_ok, lfs = pcall(require, "lfs")
11
12
----------------------------------------------------------------
13
local dir_sep = package.config:sub(1, 1)
14
if not dir_sep:find("[/\\]") then
15
dir_sep = "/"
16
end
17
18
19
--- returns all files inside dir
20
--- @param dir directory to be listed
21
--- @treturn table with filenames and attributes
22
local function dirtree(dir)
23
assert(dir and dir ~= "", "Please pass directory parameter")
24
if dir:sub(-1):match("[/\\]") then
25
dir=string.sub(dir, 1, -2)
26
end
27
28
dir = dir:gsub("[/\\]", dir_sep)
29
30
local function yieldtree(directory)
31
for entry in lfs.dir(directory) do
32
if entry ~= "." and entry ~= ".." then
33
entry=directory..dir_sep..entry
34
local attr=lfs.attributes(entry)
35
coroutine.yield(entry,attr)
36
if attr.mode == "directory" then
37
yieldtree(entry)
38
end
39
end
40
end
41
end
42
43
return coroutine.wrap(function() yieldtree(dir) end)
44
end
45
46
----------------------------------------------------------------
47
--- checks if string 'filename' has pattern 'pattern'
48
--- @param filename
49
--- @param pattern
50
--- @return boolean
51
local function fileMatches(filename, pattern)
52
return string.find(filename, pattern)
53
end
54
55
----------------------------------------------------------------
56
--- Basic reporter class stub.
57
-- Implements 'new', 'run' and 'close' methods required by `report`.
58
-- Provides some helper methods and stubs to be overridden by child classes.
59
-- @usage
60
-- local MyReporter = setmetatable({}, ReporterBase)
61
-- MyReporter.__index = MyReporter
62
-- function MyReporter:on_hit_line(...)
63
-- self:write(("File %s: hit line %s %d times"):format(...))
64
-- end
65
-- @type ReporterBase
66
local ReporterBase = {} do
67
ReporterBase.__index = ReporterBase
68
69
function ReporterBase:new(conf)
70
local stats = require("luacov.stats")
71
local data = stats.load(conf.statsfile)
72
73
if not data then
74
return nil, "Could not load stats file " .. conf.statsfile .. "."
75
end
76
77
local files = {}
78
local filtered_data = {}
79
local max_hits = 0
80
81
-- Several original paths can map to one real path,
82
-- their stats should be merged in this case.
83
for filename, file_stats in pairs(data) do
84
if luacov.file_included(filename) then
85
filename = luacov.real_name(filename)
86
87
if filtered_data[filename] then
88
luacov.update_stats(filtered_data[filename], file_stats)
89
else
90
table.insert(files, filename)
91
filtered_data[filename] = file_stats
92
end
93
94
max_hits = math.max(max_hits, filtered_data[filename].max_hits)
95
end
96
end
97
98
-- including files without tests
99
-- only .lua files
100
if conf.includeuntestedfiles then
101
if not lfs_ok then
102
print("The option includeuntestedfiles requires the lfs module (from luafilesystem) to be installed.")
103
os.exit(1)
104
end
105
106
local function add_empty_file_coverage_data(file_path)
107
108
-- Leading "./" must be trimmed from the file paths because the paths of tested
109
-- files do not have a leading "./" either
110
if (file_path:match("^%.[/\\]")) then
111
file_path = file_path:sub(3)
112
end
113
114
if luacov.file_included(file_path) then
115
local file_stats = {
116
max = 0,
117
max_hits = 0
118
}
119
120
local filename = luacov.real_name(file_path)
121
122
if not filtered_data[filename] then
123
table.insert(files, filename)
124
filtered_data[filename] = file_stats
125
end
126
end
127
128
end
129
130
local function add_empty_dir_coverage_data(directory_path)
131
132
for filename, attr in dirtree(directory_path) do
133
if attr.mode == "file" and fileMatches(filename, '.%.lua$') then
134
add_empty_file_coverage_data(filename)
135
end
136
end
137
138
end
139
140
if (conf.includeuntestedfiles == true) then
141
add_empty_dir_coverage_data("." .. dir_sep)
142
143
elseif (type(conf.includeuntestedfiles) == "table" and conf.includeuntestedfiles[1]) then
144
for _, include_path in ipairs(conf.includeuntestedfiles) do
145
if (fileMatches(include_path, '.%.lua$')) then
146
add_empty_file_coverage_data(include_path)
147
else
148
add_empty_dir_coverage_data(include_path)
149
end
150
end
151
end
152
153
end
154
155
table.sort(files)
156
157
local out, err = io.open(conf.reportfile, "w")
158
if not out then return nil, err end
159
160
local o = setmetatable({
161
_out = out,
162
_cfg = conf,
163
_data = filtered_data,
164
_files = files,
165
_mhit = max_hits,
166
}, self)
167
168
return o
169
end
170
171
--- Returns configuration table.
172
-- @see luacov.defaults
173
function ReporterBase:config()
174
return self._cfg
175
end
176
177
--- Returns maximum number of hits per line in all coverage data.
178
function ReporterBase:max_hits()
179
return self._mhit
180
end
181
182
--- Writes strings to report file.
183
-- @param ... strings.
184
function ReporterBase:write(...)
185
return self._out:write(...)
186
end
187
188
function ReporterBase:close()
189
self._out:close()
190
self._private = nil
191
end
192
193
--- Returns array of filenames to be reported.
194
function ReporterBase:files()
195
return self._files
196
end
197
198
--- Returns coverage data for a file.
199
-- @param filename name of the file.
200
-- @see luacov.stats.load
201
function ReporterBase:stats(filename)
202
return self._data[filename]
203
end
204
205
-- Stub methods follow.
206
-- luacheck: push no unused args
207
208
--- Stub method called before reporting.
209
function ReporterBase:on_start()
210
end
211
212
--- Stub method called before processing a file.
213
-- @param filename name of the file.
214
function ReporterBase:on_new_file(filename)
215
end
216
217
--- Stub method called if a file couldn't be processed due to an error.
218
-- @param filename name of the file.
219
-- @param error_type "open", "read" or "load".
220
-- @param message error message.
221
function ReporterBase:on_file_error(filename, error_type, message)
222
end
223
224
--- Stub method called for each empty source line
225
-- and other lines that can't be hit.
226
-- @param filename name of the file.
227
-- @param lineno line number.
228
-- @param line the line itself as a string.
229
function ReporterBase:on_empty_line(filename, lineno, line)
230
end
231
232
--- Stub method called for each missed source line.
233
-- @param filename name of the file.
234
-- @param lineno line number.
235
-- @param line the line itself as a string.
236
function ReporterBase:on_mis_line(filename, lineno, line)
237
end
238
239
--- Stub method called for each hit source line.
240
-- @param filename name of the file.
241
-- @param lineno line number.
242
-- @param line the line itself as a string.
243
-- @param hits number of times the line was hit. Should be positive.
244
function ReporterBase:on_hit_line(filename, lineno, line, hits)
245
end
246
247
--- Stub method called after a file has been processed.
248
-- @param filename name of the file.
249
-- @param hits total number of hit lines in the file.
250
-- @param miss total number of missed lines in the file.
251
function ReporterBase:on_end_file(filename, hits, miss)
252
end
253
254
--- Stub method called after reporting.
255
function ReporterBase:on_end()
256
end
257
258
-- luacheck: pop
259
260
local cluacov_ok = pcall(require, "cluacov.version")
261
local deepactivelines
262
263
if cluacov_ok then
264
deepactivelines = require("cluacov.deepactivelines")
265
end
266
267
function ReporterBase:_run_file(filename)
268
local file, open_err = io.open(filename)
269
270
if not file then
271
self:on_file_error(filename, "open", util.unprefix(open_err, filename .. ": "))
272
return
273
end
274
275
local active_lines
276
277
if cluacov_ok then
278
local src, read_err = file:read("*a")
279
280
if not src then
281
self:on_file_error(filename, "read", read_err)
282
return
283
end
284
285
src = src:gsub("^#![^\n]*", "")
286
local func, load_err = util.load_string(src, nil, "@file")
287
288
if not func then
289
self:on_file_error(filename, "load", "line " .. util.unprefix(load_err, "file:"))
290
return
291
end
292
293
active_lines = deepactivelines.get(func)
294
file:seek("set")
295
end
296
297
self:on_new_file(filename)
298
local file_hits, file_miss = 0, 0
299
local filedata = self:stats(filename)
300
301
local line_nr = 1
302
local scanner = LineScanner:new()
303
304
while true do
305
local line = file:read("*l")
306
if not line then break end
307
308
local always_excluded, excluded_when_not_hit = scanner:consume(line)
309
local hits = filedata[line_nr] or 0
310
local included = not always_excluded and (not excluded_when_not_hit or hits ~= 0)
311
312
if cluacov_ok then
313
included = included and active_lines[line_nr]
314
end
315
316
if included then
317
if hits == 0 then
318
self:on_mis_line(filename, line_nr, line)
319
file_miss = file_miss + 1
320
else
321
self:on_hit_line(filename, line_nr, line, hits)
322
file_hits = file_hits + 1
323
end
324
else
325
self:on_empty_line(filename, line_nr, line)
326
end
327
328
line_nr = line_nr + 1
329
end
330
331
file:close()
332
self:on_end_file(filename, file_hits, file_miss)
333
end
334
335
function ReporterBase:run()
336
self:on_start()
337
338
for _, filename in ipairs(self:files()) do
339
self:_run_file(filename)
340
end
341
342
self:on_end()
343
end
344
345
end
346
--- @section end
347
----------------------------------------------------------------
348
349
----------------------------------------------------------------
350
local DefaultReporter = setmetatable({}, ReporterBase) do
351
DefaultReporter.__index = DefaultReporter
352
353
function DefaultReporter:on_start()
354
local most_hits = self:max_hits()
355
local most_hits_length = #("%d"):format(most_hits)
356
357
self._summary = {}
358
self._empty_format = (" "):rep(most_hits_length + 1)
359
self._zero_format = ("*"):rep(most_hits_length).."0"
360
self._count_format = ("%% %dd"):format(most_hits_length+1)
361
self._printed_first_header = false
362
end
363
364
function DefaultReporter:on_new_file(filename)
365
self:write(("="):rep(78), "\n")
366
self:write(filename, "\n")
367
self:write(("="):rep(78), "\n")
368
end
369
370
function DefaultReporter:on_file_error(filename, error_type, message) --luacheck: no self
371
io.stderr:write(("Couldn't %s %s: %s\n"):format(error_type, filename, message))
372
end
373
374
function DefaultReporter:on_empty_line(_, _, line)
375
if line == "" then
376
self:write("\n")
377
else
378
self:write(self._empty_format, " ", line, "\n")
379
end
380
end
381
382
function DefaultReporter:on_mis_line(_, _, line)
383
self:write(self._zero_format, " ", line, "\n")
384
end
385
386
function DefaultReporter:on_hit_line(_, _, line, hits)
387
self:write(self._count_format:format(hits), " ", line, "\n")
388
end
389
390
function DefaultReporter:on_end_file(filename, hits, miss)
391
self._summary[filename] = { hits = hits, miss = miss }
392
self:write("\n")
393
end
394
395
local function coverage_to_string(hits, missed)
396
local total = hits + missed
397
398
if total == 0 then
399
total = 1
400
end
401
402
return ("%.2f%%"):format(hits/total*100.0)
403
end
404
405
function DefaultReporter:on_end()
406
self:write(("="):rep(78), "\n")
407
self:write("Summary\n")
408
self:write(("="):rep(78), "\n")
409
self:write("\n")
410
411
local lines = {{"File", "Hits", "Missed", "Coverage"}}
412
local total_hits, total_missed = 0, 0
413
414
for _, filename in ipairs(self:files()) do
415
local summary = self._summary[filename]
416
417
if summary then
418
local hits, missed = summary.hits, summary.miss
419
420
table.insert(lines, {
421
filename,
422
tostring(summary.hits),
423
tostring(summary.miss),
424
coverage_to_string(hits, missed)
425
})
426
427
total_hits = total_hits + hits
428
total_missed = total_missed + missed
429
end
430
end
431
432
table.insert(lines, {
433
"Total",
434
tostring(total_hits),
435
tostring(total_missed),
436
coverage_to_string(total_hits, total_missed)
437
})
438
439
local max_column_lengths = {}
440
441
for _, line in ipairs(lines) do
442
for column_nr, column in ipairs(line) do
443
max_column_lengths[column_nr] = math.max(max_column_lengths[column_nr] or -1, #column)
444
end
445
end
446
447
local table_width = #max_column_lengths - 1
448
449
for _, column_length in ipairs(max_column_lengths) do
450
table_width = table_width + column_length
451
end
452
453
454
for line_nr, line in ipairs(lines) do
455
if line_nr == #lines or line_nr == 2 then
456
self:write(("-"):rep(table_width), "\n")
457
end
458
459
for column_nr, column in ipairs(line) do
460
self:write(column)
461
462
if column_nr == #line then
463
self:write("\n")
464
else
465
self:write((" "):rep(max_column_lengths[column_nr] - #column + 1))
466
end
467
end
468
end
469
end
470
471
end
472
----------------------------------------------------------------
473
474
--- Runs the report generator.
475
-- To load a config, use `luacov.runner.load_config` first.
476
-- @param[opt] reporter_class custom reporter class. Will be
477
-- instantiated using 'new' method with configuration
478
-- (see `luacov.defaults`) as the argument. It should
479
-- return nil + error if something went wrong.
480
-- After acquiring a reporter object its 'run' and 'close'
481
-- methods will be called.
482
-- The easiest way to implement a custom reporter class is to
483
-- extend `ReporterBase`.
484
function reporter.report(reporter_class)
485
local configuration = luacov.load_config()
486
487
reporter_class = reporter_class or DefaultReporter
488
489
local rep, err = reporter_class:new(configuration)
490
491
if not rep then
492
print(err)
493
print("Run your Lua program with -lluacov and then rerun luacov.")
494
os.exit(1)
495
end
496
497
rep:run()
498
499
rep:close()
500
501
if configuration.deletestats then
502
os.remove(configuration.statsfile)
503
end
504
end
505
506
reporter.ReporterBase = ReporterBase
507
508
reporter.DefaultReporter = DefaultReporter
509
510
return reporter
511
512