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/runner.lua
12923 views
1
---------------------------------------------------
2
-- Statistics collecting module.
3
-- Calling the module table is a shortcut to calling the `init` function.
4
-- @class module
5
-- @name luacov.runner
6
7
local runner = {}
8
--- LuaCov version in `MAJOR.MINOR.PATCH` format.
9
runner.version = "0.15.0"
10
11
local stats = require("luacov.stats")
12
local util = require("luacov.util")
13
runner.defaults = require("luacov.defaults")
14
15
local debug = require("debug")
16
local raw_os_exit = os.exit
17
18
local new_anchor = newproxy or function() return {} end -- luacheck: compat
19
20
-- Returns an anchor that runs fn when collected.
21
local function on_exit_wrap(fn)
22
local anchor = new_anchor()
23
debug.setmetatable(anchor, {__gc = fn})
24
return anchor
25
end
26
27
runner.data = {}
28
runner.paused = true
29
runner.initialized = false
30
runner.tick = false
31
32
-- Checks if a string matches at least one of patterns.
33
-- @param patterns array of patterns or nil
34
-- @param str string to match
35
-- @param on_empty return value in case of empty pattern array
36
local function match_any(patterns, str, on_empty)
37
if not patterns or not patterns[1] then
38
return on_empty
39
end
40
41
for _, pattern in ipairs(patterns) do
42
if string.match(str, pattern) then
43
return true
44
end
45
end
46
47
return false
48
end
49
50
--------------------------------------------------
51
-- Uses LuaCov's configuration to check if a file is included for
52
-- coverage data collection.
53
-- @param filename name of the file.
54
-- @return true if file is included, false otherwise.
55
function runner.file_included(filename)
56
-- Normalize file names before using patterns.
57
filename = string.gsub(filename, "\\", "/")
58
filename = string.gsub(filename, "%.lua$", "")
59
60
-- If include list is empty, everything is included by default.
61
-- If exclude list is empty, nothing is excluded by default.
62
return match_any(runner.configuration.include, filename, true) and
63
not match_any(runner.configuration.exclude, filename, false)
64
end
65
66
--------------------------------------------------
67
-- Adds stats to an existing file stats table.
68
-- @param old_stats stats to be updated.
69
-- @param extra_stats another stats table, will be broken during update.
70
function runner.update_stats(old_stats, extra_stats)
71
old_stats.max = math.max(old_stats.max, extra_stats.max)
72
73
-- Remove string keys so that they do not appear when iterating
74
-- over 'extra_stats'.
75
extra_stats.max = nil
76
extra_stats.max_hits = nil
77
78
for line_nr, run_nr in pairs(extra_stats) do
79
old_stats[line_nr] = (old_stats[line_nr] or 0) + run_nr
80
old_stats.max_hits = math.max(old_stats.max_hits, old_stats[line_nr])
81
end
82
end
83
84
-- Adds accumulated stats to existing stats file or writes a new one, then resets data.
85
function runner.save_stats()
86
local loaded = stats.load(runner.configuration.statsfile) or {}
87
88
for name, file_data in pairs(runner.data) do
89
if loaded[name] then
90
runner.update_stats(loaded[name], file_data)
91
else
92
loaded[name] = file_data
93
end
94
end
95
96
stats.save(runner.configuration.statsfile, loaded)
97
runner.data = {}
98
end
99
100
local cluacov_ok = pcall(require, "cluacov.version")
101
102
--------------------------------------------------
103
-- Debug hook set by LuaCov.
104
-- Acknowledges that a line is executed, but does nothing
105
-- if called manually before coverage gathering is started.
106
-- @param _ event type, should always be "line".
107
-- @param line_nr line number.
108
-- @param[opt] level passed to debug.getinfo to get name of processed file,
109
-- 2 by default. Increase it if this function is called manually
110
-- from another debug hook.
111
-- @usage
112
-- local function custom_hook(_, line)
113
-- runner.debug_hook(_, line, 3)
114
-- extra_processing(line)
115
-- end
116
-- @function debug_hook
117
runner.debug_hook = require(cluacov_ok and "cluacov.hook" or "luacov.hook").new(runner)
118
119
------------------------------------------------------
120
-- Runs the reporter specified in configuration.
121
-- @param[opt] configuration if string, filename of config file (used to call `load_config`).
122
-- If table then config table (see file `luacov.default.lua` for an example).
123
-- If `configuration.reporter` is not set, runs the default reporter;
124
-- otherwise, it must be a module name in 'luacov.reporter' namespace.
125
-- The module must contain 'report' function, which is called without arguments.
126
function runner.run_report(configuration)
127
configuration = runner.load_config(configuration)
128
local reporter = "luacov.reporter"
129
130
if configuration.reporter then
131
reporter = reporter .. "." .. configuration.reporter
132
end
133
134
require(reporter).report()
135
end
136
137
local on_exit_run_once = false
138
139
local function on_exit()
140
-- Lua >= 5.2 could call __gc when user call os.exit
141
-- so this method could be called twice
142
if on_exit_run_once then return end
143
on_exit_run_once = true
144
-- disable hooks before aggregating stats
145
debug.sethook(nil)
146
runner.save_stats()
147
148
if runner.configuration.runreport then
149
runner.run_report(runner.configuration)
150
end
151
end
152
153
local dir_sep = package.config:sub(1, 1)
154
local wildcard_expansion = "[^/]+"
155
156
if not dir_sep:find("[/\\]") then
157
dir_sep = "/"
158
end
159
160
local function escape_module_punctuation(ch)
161
if ch == "." then
162
return "/"
163
elseif ch == "*" then
164
return wildcard_expansion
165
else
166
return "%" .. ch
167
end
168
end
169
170
local function reversed_module_name_parts(name)
171
local parts = {}
172
173
for part in name:gmatch("[^%.]+") do
174
table.insert(parts, 1, part)
175
end
176
177
return parts
178
end
179
180
-- This function is used for sorting module names.
181
-- More specific names should come first.
182
-- E.g. rule for 'foo.bar' should override rule for 'foo.*',
183
-- rule for 'foo.*' should override rule for 'foo.*.*',
184
-- and rule for 'a.b' should override rule for 'b'.
185
-- To be more precise, because names become patterns that are matched
186
-- from the end, the name that has the first (from the end) literal part
187
-- (and the corresponding part for the other name is not literal)
188
-- is considered more specific.
189
local function compare_names(name1, name2)
190
local parts1 = reversed_module_name_parts(name1)
191
local parts2 = reversed_module_name_parts(name2)
192
193
for i = 1, math.max(#parts1, #parts2) do
194
if not parts1[i] then return false end
195
if not parts2[i] then return true end
196
197
local is_literal1 = not parts1[i]:find("%*")
198
local is_literal2 = not parts2[i]:find("%*")
199
200
if is_literal1 ~= is_literal2 then
201
return is_literal1
202
end
203
end
204
205
-- Names are at the same level of specificness,
206
-- fall back to lexicographical comparison.
207
return name1 < name2
208
end
209
210
-- Sets runner.modules using runner.configuration.modules.
211
-- Produces arrays of module patterns and filenames and sets
212
-- them as runner.modules.patterns and runner.modules.filenames.
213
-- Appends these patterns to the include list.
214
local function acknowledge_modules()
215
runner.modules = {patterns = {}, filenames = {}}
216
217
if not runner.configuration.modules then
218
return
219
end
220
221
if not runner.configuration.include then
222
runner.configuration.include = {}
223
end
224
225
local names = {}
226
227
for name in pairs(runner.configuration.modules) do
228
table.insert(names, name)
229
end
230
231
table.sort(names, compare_names)
232
233
for _, name in ipairs(names) do
234
local pattern = name:gsub("%p", escape_module_punctuation) .. "$"
235
local filename = runner.configuration.modules[name]:gsub("[/\\]", dir_sep)
236
table.insert(runner.modules.patterns, pattern)
237
table.insert(runner.configuration.include, pattern)
238
table.insert(runner.modules.filenames, filename)
239
240
if filename:match("init%.lua$") then
241
pattern = pattern:gsub("$$", "/init$")
242
table.insert(runner.modules.patterns, pattern)
243
table.insert(runner.configuration.include, pattern)
244
table.insert(runner.modules.filenames, filename)
245
end
246
end
247
end
248
249
--------------------------------------------------
250
-- Returns real name for a source file name
251
-- using `luacov.defaults.modules` option.
252
-- @param filename name of the file.
253
function runner.real_name(filename)
254
local orig_filename = filename
255
-- Normalize file names before using patterns.
256
filename = filename:gsub("\\", "/"):gsub("%.lua$", "")
257
258
for i, pattern in ipairs(runner.modules.patterns) do
259
local match = filename:match(pattern)
260
261
if match then
262
local new_filename = runner.modules.filenames[i]
263
264
if pattern:find(wildcard_expansion, 1, true) then
265
-- Given a prefix directory, join it
266
-- with matched part of source file name.
267
if not new_filename:match("/$") then
268
new_filename = new_filename .. "/"
269
end
270
271
new_filename = new_filename .. match .. ".lua"
272
end
273
274
-- Switch slashes back to native.
275
return (new_filename:gsub("^%.[/\\]", ""):gsub("[/\\]", dir_sep))
276
end
277
end
278
279
return orig_filename
280
end
281
282
-- Always exclude luacov's own files.
283
local luacov_excludes = {
284
"luacov$",
285
"luacov/hook$",
286
"luacov/reporter$",
287
"luacov/reporter/default$",
288
"luacov/defaults$",
289
"luacov/runner$",
290
"luacov/stats$",
291
"luacov/tick$",
292
"luacov/util$",
293
"cluacov/version$"
294
}
295
296
local function is_absolute(path)
297
if path:sub(1, 1) == dir_sep or path:sub(1, 1) == "/" then
298
return true
299
end
300
301
if dir_sep == "\\" and path:find("^%a:") then
302
return true
303
end
304
305
return false
306
end
307
308
local function get_cur_dir()
309
local pwd_cmd = dir_sep == "\\" and "cd 2>nul" or "pwd 2>/dev/null"
310
local handler = io.popen(pwd_cmd, "r")
311
local cur_dir = handler:read()
312
handler:close()
313
cur_dir = cur_dir:gsub("\r?\n$", "")
314
315
if cur_dir:sub(-1) ~= dir_sep and cur_dir:sub(-1) ~= "/" then
316
cur_dir = cur_dir .. dir_sep
317
end
318
319
return cur_dir
320
end
321
322
-- Sets configuration. If some options are missing, default values are used instead.
323
local function set_config(configuration)
324
runner.configuration = {}
325
326
for option, default_value in pairs(runner.defaults) do
327
runner.configuration[option] = default_value
328
end
329
330
for option, value in pairs(configuration) do
331
runner.configuration[option] = value
332
end
333
334
-- Program using LuaCov may change directory during its execution.
335
-- Convert path options to absolute paths to use correct paths anyway.
336
local cur_dir
337
338
for _, option in ipairs({"statsfile", "reportfile"}) do
339
local path = runner.configuration[option]
340
341
if not is_absolute(path) then
342
cur_dir = cur_dir or get_cur_dir()
343
runner.configuration[option] = cur_dir .. path
344
end
345
end
346
347
acknowledge_modules()
348
349
for _, patt in ipairs(luacov_excludes) do
350
table.insert(runner.configuration.exclude, patt)
351
end
352
353
runner.tick = runner.tick or runner.configuration.tick
354
end
355
356
local function load_config_file(name, is_default)
357
local conf = setmetatable({}, {__index = _G})
358
359
local ok, ret, error_msg = util.load_config(name, conf)
360
361
if ok then
362
if type(ret) == "table" then
363
for key, value in pairs(ret) do
364
if conf[key] == nil then
365
conf[key] = value
366
end
367
end
368
end
369
370
return conf
371
end
372
373
local error_type = ret
374
375
if error_type == "read" and is_default then
376
return nil
377
end
378
379
io.stderr:write(("Error: couldn't %s config file %s: %s\n"):format(error_type, name, error_msg))
380
raw_os_exit(1)
381
end
382
383
local default_config_file = ".luacov"
384
385
------------------------------------------------------
386
-- Loads a valid configuration.
387
-- @param[opt] configuration user provided config (config-table or filename)
388
-- @return existing configuration if already set, otherwise loads a new
389
-- config from the provided data or the defaults.
390
-- When loading a new config, if some options are missing, default values
391
-- from `luacov.defaults` are used instead.
392
function runner.load_config(configuration)
393
if not runner.configuration then
394
if not configuration then
395
-- Nothing provided, load from default location if possible.
396
set_config(load_config_file(default_config_file, true) or runner.defaults)
397
elseif type(configuration) == "string" then
398
set_config(load_config_file(configuration))
399
elseif type(configuration) == "table" then
400
set_config(configuration)
401
else
402
error("Expected filename, config table or nil. Got " .. type(configuration))
403
end
404
end
405
406
return runner.configuration
407
end
408
409
--------------------------------------------------
410
-- Pauses saving data collected by LuaCov's runner.
411
-- Allows other processes to write to the same stats file.
412
-- Data is still collected during pause.
413
function runner.pause()
414
runner.paused = true
415
end
416
417
--------------------------------------------------
418
-- Resumes saving data collected by LuaCov's runner.
419
function runner.resume()
420
runner.paused = false
421
end
422
423
local hook_per_thread
424
425
-- Determines whether debug hooks are separate for each thread.
426
local function has_hook_per_thread()
427
if hook_per_thread == nil then
428
local old_hook, old_mask, old_count = debug.gethook()
429
local noop = function() end
430
debug.sethook(noop, "l")
431
local thread_hook = coroutine.wrap(function() return debug.gethook() end)()
432
hook_per_thread = thread_hook ~= noop
433
debug.sethook(old_hook, old_mask, old_count)
434
end
435
436
return hook_per_thread
437
end
438
439
--------------------------------------------------
440
-- Wraps a function, enabling coverage gathering in it explicitly.
441
-- LuaCov gathers coverage using a debug hook, and patches coroutine
442
-- library to set it on created threads when under standard Lua, where each
443
-- coroutine has its own hook. If a coroutine is created using Lua C API
444
-- or before the monkey-patching, this wrapper should be applied to the
445
-- main function of the coroutine. Under LuaJIT this function is redundant,
446
-- as there is only one, global debug hook.
447
-- @param f a function
448
-- @return a function that enables coverage gathering and calls the original function.
449
-- @usage
450
-- local coro = coroutine.create(runner.with_luacov(func))
451
function runner.with_luacov(f)
452
return function(...)
453
if has_hook_per_thread() then
454
debug.sethook(runner.debug_hook, "l")
455
end
456
457
return f(...)
458
end
459
end
460
461
--------------------------------------------------
462
-- Initializes LuaCov runner to start collecting data.
463
-- @param[opt] configuration if string, filename of config file (used to call `load_config`).
464
-- If table then config table (see file `luacov.default.lua` for an example)
465
function runner.init(configuration)
466
runner.configuration = runner.load_config(configuration)
467
468
-- metatable trick on filehandle won't work if Lua exits through
469
-- os.exit() hence wrap that with exit code as well
470
os.exit = function(...) -- luacheck: no global
471
on_exit()
472
raw_os_exit(...)
473
end
474
475
debug.sethook(runner.debug_hook, "l")
476
477
if has_hook_per_thread() then
478
-- debug must be set for each coroutine separately
479
-- hence wrap coroutine function to set the hook there
480
-- as well
481
local rawcoroutinecreate = coroutine.create
482
coroutine.create = function(...) -- luacheck: no global
483
local co = rawcoroutinecreate(...)
484
debug.sethook(co, runner.debug_hook, "l")
485
return co
486
end
487
488
-- Version of assert which handles non-string errors properly.
489
local function safeassert(ok, ...)
490
if ok then
491
return ...
492
else
493
error(..., 0)
494
end
495
end
496
497
coroutine.wrap = function(...) -- luacheck: no global
498
local co = rawcoroutinecreate(...)
499
debug.sethook(co, runner.debug_hook, "l")
500
return function(...)
501
return safeassert(coroutine.resume(co, ...))
502
end
503
end
504
end
505
506
if not runner.tick then
507
runner.on_exit_trick = on_exit_wrap(on_exit)
508
end
509
510
runner.initialized = true
511
runner.paused = false
512
end
513
514
--------------------------------------------------
515
-- Shuts down LuaCov's runner.
516
-- This should only be called from daemon processes or sandboxes which have
517
-- disabled os.exit and other hooks that are used to determine shutdown.
518
function runner.shutdown()
519
on_exit()
520
end
521
522
-- Gets the sourcefilename from a function.
523
-- @param func function to lookup.
524
-- @return sourcefilename or nil when not found.
525
local function getsourcefile(func)
526
assert(type(func) == "function")
527
local d = debug.getinfo(func).source
528
if d and d:sub(1, 1) == "@" then
529
return d:sub(2)
530
end
531
end
532
533
-- Looks for a function inside a table.
534
-- @param searched set of already checked tables.
535
local function findfunction(t, searched)
536
if searched[t] then
537
return
538
end
539
540
searched[t] = true
541
542
for _, v in pairs(t) do
543
if type(v) == "function" then
544
return v
545
elseif type(v) == "table" then
546
local func = findfunction(v, searched)
547
if func then return func end
548
end
549
end
550
end
551
552
-- Gets source filename from a file name, module name, function or table.
553
-- @param name string; filename,
554
-- string; modulename as passed to require(),
555
-- function; where containing file is looked up,
556
-- table; module table where containing file is looked up
557
-- @raise error message if could not find source filename.
558
-- @return source filename.
559
local function getfilename(name)
560
if type(name) == "function" then
561
local sourcefile = getsourcefile(name)
562
563
if not sourcefile then
564
error("Could not infer source filename")
565
end
566
567
return sourcefile
568
elseif type(name) == "table" then
569
local func = findfunction(name, {})
570
571
if not func then
572
error("Could not find a function within " .. tostring(name))
573
end
574
575
return getfilename(func)
576
else
577
if type(name) ~= "string" then
578
error("Bad argument: " .. tostring(name))
579
end
580
581
if util.file_exists(name) then
582
return name
583
end
584
585
local success, result = pcall(require, name)
586
587
if not success then
588
error("Module/file '" .. name .. "' was not found")
589
end
590
591
if type(result) ~= "table" and type(result) ~= "function" then
592
error("Module '" .. name .. "' did not return a result to lookup its file name")
593
end
594
595
return getfilename(result)
596
end
597
end
598
599
-- Escapes a filename.
600
-- Escapes magic pattern characters, removes .lua extension
601
-- and replaces dir seps by '/'.
602
local function escapefilename(name)
603
return name:gsub("%.lua$", ""):gsub("[%%%^%$%.%(%)%[%]%+%*%-%?]","%%%0"):gsub("\\", "/")
604
end
605
606
local function addfiletolist(name, list)
607
local f = "^"..escapefilename(getfilename(name)).."$"
608
table.insert(list, f)
609
return f
610
end
611
612
local function addtreetolist(name, level, list)
613
local f = escapefilename(getfilename(name))
614
615
if level or f:match("/init$") then
616
-- chop the last backslash and everything after it
617
f = f:match("^(.*)/") or f
618
end
619
620
local t = "^"..f.."/" -- the tree behind the file
621
f = "^"..f.."$" -- the file
622
table.insert(list, f)
623
table.insert(list, t)
624
return f, t
625
end
626
627
-- Returns a pcall result, with the initial 'true' value removed
628
-- and 'false' replaced with nil.
629
local function checkresult(ok, ...)
630
if ok then
631
return ... -- success, strip 'true' value
632
else
633
return nil, ... -- failure; nil + error
634
end
635
end
636
637
-------------------------------------------------------------------
638
-- Adds a file to the exclude list (see `luacov.defaults`).
639
-- If passed a function, then through debuginfo the source filename is collected. In case of a table
640
-- it will recursively search the table for a function, which is then resolved to a filename through debuginfo.
641
-- If the parameter is a string, it will first check if a file by that name exists. If it doesn't exist
642
-- it will call `require(name)` to load a module by that name, and the result of require (function or
643
-- table expected) is used as described above to get the sourcefile.
644
-- @param name
645
-- * string; literal filename,
646
-- * string; modulename as passed to require(),
647
-- * function; where containing file is looked up,
648
-- * table; module table where containing file is looked up
649
-- @return the pattern as added to the list, or nil + error
650
function runner.excludefile(name)
651
return checkresult(pcall(addfiletolist, name, runner.configuration.exclude))
652
end
653
-------------------------------------------------------------------
654
-- Adds a file to the include list (see `luacov.defaults`).
655
-- @param name see `excludefile`
656
-- @return the pattern as added to the list, or nil + error
657
function runner.includefile(name)
658
return checkresult(pcall(addfiletolist, name, runner.configuration.include))
659
end
660
-------------------------------------------------------------------
661
-- Adds a tree to the exclude list (see `luacov.defaults`).
662
-- If `name = 'luacov'` and `level = nil` then
663
-- module 'luacov' (luacov.lua) and the tree 'luacov' (containing `luacov/runner.lua` etc.) is excluded.
664
-- If `name = 'pl.path'` and `level = true` then
665
-- module 'pl' (pl.lua) and the tree 'pl' (containing `pl/path.lua` etc.) is excluded.
666
-- NOTE: in case of an 'init.lua' file, the 'level' parameter will always be set
667
-- @param name see `excludefile`
668
-- @param level if truthy then one level up is added, including the tree
669
-- @return the 2 patterns as added to the list (file and tree), or nil + error
670
function runner.excludetree(name, level)
671
return checkresult(pcall(addtreetolist, name, level, runner.configuration.exclude))
672
end
673
-------------------------------------------------------------------
674
-- Adds a tree to the include list (see `luacov.defaults`).
675
-- @param name see `excludefile`
676
-- @param level see `includetree`
677
-- @return the 2 patterns as added to the list (file and tree), or nil + error
678
function runner.includetree(name, level)
679
return checkresult(pcall(addtreetolist, name, level, runner.configuration.include))
680
end
681
682
683
return setmetatable(runner, {__call = function(_, configfile) runner.init(configfile) end})
684
685