Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial JUnit report support. #26

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions telescope.lua
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ local function run(contexts, callbacks, test_filter)
end
end

-- Returns the number of elapsed seconds with decimals.
-- TODO: On POSIX systems it returns CPU time (not what we want), on Windows it returns elapsed time
local function get_seconds()
return os.clock()
end

for i, v in filter(contexts, function(i, v) return v.test and test_filter(v) end) do
env = newEnv() -- Setup a new environment for this test

Expand Down Expand Up @@ -454,7 +460,9 @@ local function run(contexts, callbacks, test_filter)

-- check if it's a function because pending tests will just have "true"
if type(v.test) == "function" then
result.start_time = get_seconds()
result.status_code, result.assertions_invoked, result.message = invoke_test(v.test)
result.end_time = get_seconds()
invoke_callback(status_names[result.status_code], result)
else
result.status_code = status_codes.pending
Expand Down Expand Up @@ -578,12 +586,126 @@ local function summary_report(contexts, results)
return table.concat(buffer, " "), r
end

--- Writes a detailed report with the results of each test, in JUnit XML format.
-- Each context is converted into a test suite.
-- @param contexts The contexts returned by <tt>load_contexts</tt>.
-- @param results The results returned by <tt>run</tt>.
-- @param file_contexts A table that associates the test filenames with the last context corresponding to it.
-- @param out_path The path where the output files will be written
-- @function junit_report
local function junit_report(contexts, results, file_contexts, out_path)
assert(type(out_path) == "string")
local ancestor_separator = " / "

local xml_escapes = {
[">"] = "&gt;",
["<"] = "&lt;",
["&"] = "&amp;",
['"'] = "&quot;"
}
local function escape_xml(str)
return str:gsub("[<>&\"]", function(a) return xml_escapes[a] end)
end

local function write_test(report, test)
-- TODO: classname="" status=""
local test_time = ""
if test.start_time and test.end_time then
test_time = ' time="' .. (test.end_time - test.start_time) .. '"'
end
report:write(' <testcase name="' .. escape_xml(test.name) .. '" assertions="' .. test.assertions_invoked .. '"' .. test_time .. '>', "\n")

if test.status_code == status_codes.err then
-- <error message="my error message">my crash report</error>
-- TODO: message="" type=""
-- Contains as a text node relevant data for the error, e.g., a stack trace
report:write(' <error>' .. escape_xml(table.concat(test.message, "\n")) .. '</error>', "\n")
elseif test.status_code == status_codes.fail then
-- <failure message="my failure message">my stack trace</failure>
-- TODO: message="" type=""
report:write(' <failure>' .. escape_xml(table.concat(test.message, "\n")) .. '</failure>', "\n")
elseif test.status_code == status_codes.pass then
elseif test.status_code == status_codes.pending then
report:write(' <skipped/>', "\n")
elseif test.status_code == status_codes.unassertive then
-- Treat it like passed
end

-- TODO: <system-out/>
-- TODO: <system-err/>
report:write(' </testcase>', "\n")
end

local function write_suite(report, suite)
-- TODO: disabled="" id="" package="" skipped=""
-- timestamp: when the test was executed. Timezone may not be specified.
-- hostname: Host on which the tests were executed. 'localhost' should be used if the hostname cannot be determined.
-- failures: The total number of tests in the suite that failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals
-- errors: The total number of tests in the suite that errorrd. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test.
-- time: Time taken (in seconds) to execute the tests in the suite
-- it can contain a <properties> <property name="" value="">... </properties> block
report:write(' <testsuite name="' .. escape_xml(suite.name) .. '" tests="' .. #suite.cases .. '">', "\n")

for _, case in ipairs(suite.cases) do
write_test(report, results[case])
end

report:write(' </testsuite>', "\n")
end

local function write_file(out_path, filename, suites)
local report = io.open(out_path .. "/TEST-" .. filename .. ".xml", "w")
report:write('<?xml version="1.0" encoding="UTF-8"?>', "\n")
-- TODO: disabled="" errors="" failures="" tests="" time=""
report:write('<testsuites name="' .. filename .. '">', "\n")

for i in ipairs(contexts) do
suite = suites[i]
if suite then
write_suite(report, suite)
end
end

report:write('</testsuites>', "\n")
report:close()
end


-- TODO: Find a portable way to create a directory
os.execute("mkdir " .. out_path)
local i = 1
for _, file_info in ipairs(file_contexts) do
local suites = {}

while i <= file_info.last_context do
local item = contexts[i]

if item.context then
local name = item.name
local ancestors = ancestors(i, contexts)
for _, ancestor in pairs(ancestors) do
name = contexts[ancestor].name .. ancestor_separator .. name
end

suites[i] = { name = name, cases = {} }
else
table.insert(suites[item.parent].cases, i)
end

i = i + 1
end

write_file(out_path, file_info.name, suites)
end
end

_M.after_aliases = after_aliases
_M.make_assertion = make_assertion
_M.assertion_message_prefix = assertion_message_prefix
_M.before_aliases = before_aliases
_M.context_aliases = context_aliases
_M.error_report = error_report
_M.junit_report = junit_report
_M.load_contexts = load_contexts
_M.run = run
_M.test_report = test_report
Expand Down
24 changes: 18 additions & 6 deletions tsc
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Options:
-h,-? --help Show this text
-v --version Show version
-c --luacov Output a coverage file using Luacov (http://luacov.luaforge.net/)
--junit=<path> Generate JUnit compatible .xml report files in the specified path
--load=<file> Load a Lua file before executing command
--name=<pattern> Only run tests whose name matches a Lua string pattern
--shake Use shake as the front-end for tests
Expand Down Expand Up @@ -264,10 +265,11 @@ for _, callback in ipairs(callback_args) do
end

local contexts = {}
if opts["shake"] then
for _, file in ipairs(files) do shake.load_contexts(file, contexts) end
else
for _, file in ipairs(files) do telescope.load_contexts(file, contexts) end
local file_contexts = {}
local load_contexts = opts["shake"] and shake.load_contexts or telescope.load_contexts
for _, file in ipairs(files) do
load_contexts(file, contexts)
table.insert(file_contexts, { name = string.match(file, "(.*)%."), last_context = #contexts })
end

local buffer = {}
Expand Down Expand Up @@ -296,9 +298,19 @@ if opts.c or opts.coverage then
os.remove("luacov.stats.out")
end

if opts.junit then
telescope.junit_report(contexts, results, file_contexts, opts.junit)
end

local fail = false
for _, v in pairs(results) do
if v.status_code == telescope.status_codes.err or
v.status_code == telescope.status_codes.fail then
if v.status_code == telescope.status_codes.err then
os.exit(1)
end
if v.status_code == telescope.status_codes.fail then
fail = true
end
end
if fail then
os.exit(2)
end