lua-users home
lua-l archive

[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.