lua-users home
lua-l archive

Getting coverage

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


As I'm hacking away at my Lua xml parser (plxml.lua mentioned a couple of weeks ago) I wanted to check that my tests were giving me the coverage that I wanted. Not finding anything I hacked this up:
-- coverage.lua
-- The name of the source file to match
local match = nil
-- A table to hold the lines being encountered
local lines = {}
-- The highest line number encountered
local max = 0
local function hook()
 local info = debug.getinfo(2,"Sl")
 local source = info.source
 if(string.match(source,match)) then
 local line = info.currentline
 if(lines[line] == nil) then
 lines[line] = 1
 if(line > max) then
 max = line
 end
 else
 lines[line] = lines[line] + 1
 end
 end
end
match = arg[1]
for p = 2,#arg do
 local filename = arg[p]
 local f = assert(loadfile(filename))
 debug.sethook(hook, "l")
 f()
 debug.sethook()
end
local out = io.open('coverage.txt','w')
for p = 1,max do
 if(lines[p] ~= nil) then
 out:write(p .. ' ' .. lines[p] .. "\n")
 end
end
out:close()
Which is used thus:
$ lua coverage.lua plxml.lua tests/*
The second argument 'plxml.lua' is the source I want to match and the rest of the arguments are the tests I run to make sure that my code is still working. This all results in a file called 'coverage.txt' that contains two numbers per line. The first in the line number and the second the number of times that the line was encountered.
-- coverage.txt
15 1
16 1
17 1
18 1
19 1
21 1
27 54
28 54
29 54
30 54
...
This file, coverage.txt, and the source we were checking (plxml.lua) are then run through another program to create a report.
$ lua mc.lua plxml.lua coverage.txt
Which gives this report to standard out:
...
102 true local text = data:sub(start+1,-3)
103 true return newpi( name, text )
104 true end
105 true
106 true local function makedoctype( data )
107 true -- The data is between the '<!' and the final '>'
108 false local text = data:sub(3,-2)
109 false return newdoctype( text )
110 true end
111 true
112 true local function makeelement( data )
113 true local name = ''
114 true local attributes = {}
115 true
116 true local pos = data:find(' ')
117 true if(pos) then
...
439 true end
440 true
441 true return text
442 true end
443 true
444 true ------------------------------------------------------------------------ --------
Total lines in file ..: 444
Total lines of code ..: 319
Code covered .........: 309 (96.87%)
Code missed ..........: 10
The first number is the line number, the second is true when the line is either non code (blank or comment) or has been reported by the coverage tool. Or it says false if it is code and has not been reported in the coverage file. Then you have the source line. Presently cannot handle multiline strings or comments (but then these have yet to appear in my code) and coverage.lua has had to hack a few lines that the debug hook does not seem to catch. I've found it useful, it's pointed out a completely useless function that I had in the source but never called and showed that there were some cases that the tests were not covering so I will have to beef up the tests. Have I just reinvented the wheel or would someone find this useful? I feel a LuaForge project coming on (other than plxml.lua).
Oh here's the mc.lua file:
-- mc.lua
-- Is this line blank or is it just a comment
local function blank( data )
	local text = data
	-- Remove the comments
	local pos = text:find('--', 1, true)
	if(pos) then
		if(pos == 1) then
			text = ''
		else
			text = text:sub(1,pos-1)
		end
	end
	-- Remove whitespace
	text = text:gsub('%s','')
	return text == ''
end
-- Read the source in
local function readsource( filename )
	local inp = io.open(filename,'r')
	local lines = {}
	while true do
		local line = inp:read('*line')
		if not line then break end
		local data = {}
		data.source = line
		data.ignore = blank(line)
		data.count = 0
		lines[#lines+1] = data
	end
	inp:close()
	return lines
end
-- Add the coverage information
local function readcoverage( filename, lines )
	local inp = io.open(filename,'r')
	while true do
		local line, count = inp:read('*number', '*number')
		if not line then break end
		lines[line].count = count
	end
	inp:close()
	
	return lines
end
-- Some lines don't seem to be counted
--
-- 1) end
-- 2) else
-- 3) local function
-- 4) return function
local function patchup( lines )
	for k,v in ipairs(lines) do
		if(v.ignore == false) then
			if(v.count == 0) then
				-- print(v.source)
				if(string.match(v.source, "^%s+end%s*$") ~= nil) then
					v.count = -1
				elseif(string.match(v.source, "^%s+else%s*$") ~= nil) then
					v.count = -1
				elseif(string.match(v.source, "%s*local%s+function%s+") ~= nil) then
					v.count = -1
elseif(string.match(v.source, "%s*return%s+function%s*%(") ~= nil) then
					v.count = -1
				end
			end
		end
	end
	return lines
end
local sourcefilename = arg[1]
local coverfilename = arg[2]
local lines = readsource(sourcefilename)
lines = readcoverage(coverfilename, lines)
lines = patchup(lines)
local total_source_lines = #lines
local total_code_lines = 0
local total_code_covered = 0
for k,v in ipairs(lines) do
	local ok = true
	if(v.ignore == false) then
		total_code_lines = total_code_lines + 1
		if(v.count == 0) then
			ok = false
		else
			total_code_covered = total_code_covered + 1
		end
	end
	print(k,ok,v.source)
end
print()
print("Total lines in file ..: " .. total_source_lines)
print("Total lines of code ..: " .. total_code_lines)
print("Code covered .........: " .. total_code_covered .. " (" .. string.format("%.2f",(total_code_covered / total_code_lines * 100)) .. "%)") print("Code missed ..........: " .. (total_code_lines - total_code_covered))
--
Genius has its limitations. Stupidity is not thus handicapped.

AltStyle によって変換されたページ (->オリジナル) /