CI coverage: handle Lua code coverage properly

Luacov statistics contained paths to installed files instead of source
files that it was a mess. The stats are now rewritten using hacky
mapping (created from install commands produced by make).

Also, branch and function coverage for Lua was always zero so now it is
turned off not to confuse users.

kresd config for respdiff now enables luacov as well.
parent 0c5e1466
......@@ -13,12 +13,16 @@ lint: $(patsubst %.lua.in,%.lua,$(wildcard */*/*.lua.in))
coverage-c:
@echo "# C coverage in $(COVERAGE_STAGE).c.info"
@$(LCOV) --no-external --capture -d lib -d daemon -d modules -o $(COVERAGE_STAGE).c.info > /dev/null
coverage-lua: $(wildcard */*/luacov.stats.out)
coverage-lua: $(shell find -type f -name 'luacov.stats.out')
# map install paths to source paths
@$(MAKE) PREFIX=$(PREFIX) install --dry-run --always-make | scripts/map_install_src.lua --sed > .luacov_path_map
@find -type f -name 'luacov.stats.out' | xargs sed -i -f .luacov_path_map
@rm .luacov_path_map
@echo "# Lua coverage in $(COVERAGE_STAGE).lua.info"
@if [ ! -z "$^" ]; then ./scripts/luacov_to_info.lua $^ > $(COVERAGE_STAGE).lua.info; fi
@scripts/luacov_to_info.lua $^ > $(COVERAGE_STAGE).lua.info
coverage:
@$(LCOV) $(addprefix --add-tracefile ,$(wildcard $(COVERAGE_STAGE)*.info)) --output-file coverage.info
@$(GENHTML) -q --ignore-errors source -o coverage -p $(realpath $(CURDIR)) -t "Knot DNS Resolver $(VERSION)-$(PLATFORM) coverage report" --legend coverage.info
@$(GENHTML) --no-function-coverage --no-branch-coverage -q -o coverage -p $(realpath $(CURDIR)) -t "Knot DNS Resolver $(VERSION)-$(PLATFORM) coverage report" --legend coverage.info
.PHONY: all install check clean doc info coverage
......
-- Refer to manual: https://knot-resolver.readthedocs.io/en/latest/daemon.html#configuration
-- measure code coverage: kresd must run from $GIT_DIR
require('tests.config.coverage')
-- Refer to manual: https://knot-resolver.readthedocs.io/en/latest/daemon.html#configuration
-- Listen on localhost and external interface
net.listen('127.0.0.1', 5353)
net.listen('::1', 5353)
......
#!/usr/bin/env luajit
-- parse install commands from stdin
-- input: PREFIX=... make install --dry-run --always-make
-- output: <install path> <source path>
-- (or sed commands if --sed was specified)
output = 'list'
if #arg > 1 or arg[1] == '-h' or arg[1] == '--help' then
print(string.format([[
Read install commands and map install paths to paths in source directory.
Usage:
$ PREFIX=... make install --dry-run --always-make | %s
Example output:
/kresd/git/.local/lib/kdns_modules/policy.lua modules/policy/policy.lua
Option --sed will produce output suitable as input suitable for sed.]],
arg[0]))
os.exit(1)
elseif #arg == 0 then
output = 'list'
elseif arg[1] == '--sed' then
output = 'sed'
else
print('Invalid arguments. See --help.')
os.exit(2)
end
-- remove double // from paths and remove trailing /
function normalize_path(path)
assert(path)
repeat
path, changes = path:gsub('//', '/')
until changes == 0
return path:gsub('/$', '')
end
function is_opt(word)
return word:match('^-')
end
-- opts requiring additional argument to be skipped
local ignored_opts_with_arg = {
['--backup'] = true,
['-g'] = true,
['--group'] = true,
['-m'] = true,
['--mode'] = true,
['-o'] = true,
['--owner'] = true,
['--strip-program'] = true,
['--suffix'] = true,
}
-- state machine junctions caused by --opts
-- returns: new state (expect, mode) and target name if any
function parse_opts(word, expect, mode)
if word == '--' then
return 'names', mode, nil -- no options anymore
elseif word == '-d' or word == '--directory' then
return 'opt_or_name', 'newdir', nil
elseif word == '-t' or word == '--target-directory' then
return 'targetdir', mode, nil
elseif word:match('^--target-directory=') then
return 'opt_or_name', mode, string.sub(word, 20)
elseif ignored_opts_with_arg[word] then
return 'ignore', mode, nil -- ignore next word
else
return expect, mode, nil -- unhandled opt
end
end
-- cmd: complete install command line: install -m 0644 -t dest src1 src2
-- dirs: names known to be directories: name => true
-- returns: updated dirs
function process_cmd(cmd, dirs)
-- print('# ' .. cmd)
sanity_check(cmd)
local expect = 'install'
local mode = 'copy' -- copy or newdir
local target -- last argument or argument for install -t
local names = {} -- non-option arguments
for word in cmd:gmatch('%S+') do
if expect == 'install' then -- parsing 'install'
assert(word == 'install')
expect = 'opt_or_name'
elseif expect == 'opt_or_name' then
if is_opt(word) then
expect, mode, newtarget = parse_opts(word, expect, mode)
target = newtarget or target
else
if mode == 'copy' then
table.insert(names, word)
elseif mode == 'newdir' then
local path = normalize_path(word)
dirs[path] = true
else
assert(false, 'bad mode')
end
end
elseif expect == 'targetdir' then
local path = normalize_path(word)
dirs[path] = true
target = word
expect = 'opt_or_name'
elseif expect == 'names' then
table.insert(names, word)
elseif expect == 'ignore' then
expect = 'opt_or_name'
else
assert(false, 'bad expect')
end
end
if mode == 'newdir' then
-- no mapping to print, this cmd just created directory
return dirs
end
if not target then -- last argument is the target
target = table.remove(names)
end
assert(target, 'fatal: no target in install cmd')
target = normalize_path(target)
for _, name in pairs(names) do
basename = string.gsub(name, "(.*/)(.*)", "%2")
if not dirs[target] then
print('fatal: target directory "' .. target .. '" was not created yet!')
os.exit(2)
end
-- mapping installed name -> source name
if output == 'list' then
print(target .. '/' .. basename, name)
elseif output == 'sed' then
print(string.format([[s`%s`%s`g]],
target .. '/' .. basename, name))
else
assert(false, 'unsupported output')
end
end
return dirs
end
function sanity_check(cmd)
-- shell quotation is not supported
assert(not cmd:match('"'), 'quotes " are not supported')
assert(not cmd:match("'"), "quotes ' are not supported")
assert(not cmd:match('\\'), "escapes like \\ are not supported")
assert(cmd:match('^install%s'), 'not an install command')
end
-- remember directories created by install -d so we can expand relative paths
local dirs = {}
while true do
local cmd = io.read("*line")
if not cmd then
break
end
local isinstall = cmd:match('^install%s')
if isinstall then
dirs = process_cmd(cmd, dirs)
end
end
-- optional code coverage
-- include this file into config if you want to generate coverage data
local ok, runner = pcall(require, 'luacov.runner')
if ok then
runner.init({
savestepsize = 2, -- TODO
statsfile = 'luacov.stats.out',
exclude = {'test', 'tapered'},
})
jit.off()
end
package.path = package.path .. ';' .. env.SOURCE_PATH .. '/?.lua'
TEST_DIR = env.TEST_FILE:match('(.*/)')
-- optional code coverage
local ok, runner = pcall(require, 'luacov.runner')
if ok then
runner.init({
savestepsize = 2,
statsfile = TEST_DIR .. '/luacov.stats.out',
exclude = {'test', 'tapered'},
})
jit.off()
end
require('coverage')
-- export testing module in globals
local tapered = require('tapered.src.tapered')
......
Subproject commit a5cd67c98836690e00ab76b7bd220023f7993ee9
Subproject commit 8e533979fe667fdc0bf6975d643d9e98c37f3b41
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment