namespace rooibos.Coverage
function saveResults() as dynamic
cc = m.global._rbs_ccn
if cc = invalid then
? "There was no rooibos code coverage component - not generating coverage report"
return invalid
end if
' Observe before requesting the save so the one-time publish can't be missed
port = createObject("roMessagePort")
cc.observeFieldScoped("coverageResults", port)
cc.save = true
coverageResults = cc.coverageResults
if coverageResults = invalid then
msg = wait(0, port)
coverageResults = msg.getData()
end if
cc.unobserveFieldScoped("coverageResults")
return coverageResults
end function
function reportCodeCoverage(coverageResults as dynamic) as void
if m.global._rbs_ccn = invalid then
? "There was no rooibos code coverage component - not generating coverage report"
return
end if
t = createObject("roTimespan")
? ""
? "...Generating code coverage report"
? ""
hitFiles = []
missFiles = []
allLinesCount = 0
allLinesHit = 0
for each file in coverageResults.files
filename = "pkg:/" + file.sourceFile
expectedCount = file.lineTotalFound
allLinesCount += expectedCount
allLinesHit += file.lineTotalHit
if file.lineTotalHit > 0 then
resolvedPercent = (file.lineTotalHit / expectedCount) * 100
hitFiles.push({ percent: resolvedPercent, text: filename + ": " + str(resolvedPercent).trim() + "% (" + stri(file.lineTotalHit).trim() + "/" + stri(expectedCount).trim() + ")" })
else
missFiles.push(filename + ": MISS!")
end if
end for
if allLinesHit = 0 then
allLinesPercent = 0
else
allLinesPercent = (allLinesHit / allLinesCount) * 100
end if
? ""
? ""
? "+++++++++++++++++++++++++++++++++++++++++++"
? "Code Coverage Report"
? "+++++++++++++++++++++++++++++++++++++++++++"
? ""
? "Total Coverage: " ; str(allLinesPercent).trim() ; "% (" ; stri(allLinesHit).trim() ; "/" + stri(allLinesCount).trim() ; ")"
? "Files: " ; hitFiles.count(); "/" ; coverageResults.files.count()
? ""
? "HIT FILES"
? "---------"
hitFiles.SortBy("percent")
for i = 0 to hitFiles.count() - 1
? hitFiles[i].text
end for
? ""
? "MISSED FILES"
? "------------"
for i = 0 to missFiles.count() - 1
? missFiles[i]
end for
? ""
? "+++++++++++++++++++++++++++++++++++++++++++"
? "Code Coverage Report Complete"; t.totalMilliseconds(); "ms"
? "+++++++++++++++++++++++++++++++++++++++++++"
end function
' Prints the condensed hit-counts stream consumed by the rooibos CLI whenever code
' coverage is recorded. One JSON line per file that recorded any hits,
' sparse (zero counts omitted), with every count keyed by its INDEX into the
' corresponding array of components/rooibos/CodeCoverage.json - the CLI re-reads that
' file from the built package and joins the two. The static model never changes order
' at runtime (hits are incremented in place), so index alignment is guaranteed.
' This stream is a fraction of the lcov payload, so it doesn't flood the console.
function printCoverageCounts(coverageResults as dynamic)
?
? "+-=-coverage-counts:start"
? `{"v":1}`
fileIndex = 0
for each file in coverageResults.files
lineHits = {}
i = 0
for each line in file.lines
if line.totalHit > 0 then
lineHits[stri(i).trim()] = line.totalHit
end if
i++
end for
functionHits = {}
i = 0
for each func in file.functions
if func.totalHit > 0 then
functionHits[stri(i).trim()] = func.totalHit
end if
i++
end for
branchHits = {}
i = 0
for each block in file.blocks
armHits = []
anyHit = false
for each branch in block.branches
armHits.push(branch.totalHit)
if branch.totalHit > 0 then
anyHit = true
end if
end for
if anyHit then
branchHits[stri(i).trim()] = armHits
end if
i++
end for
if lineHits.count() > 0 or functionHits.count() > 0 or branchHits.count() > 0 then
? FormatJson({ "i": fileIndex, "l": lineHits, "f": functionHits, "b": branchHits })
' Brief pause so console scrapers can drain the buffer (same reason as the
' lcov printer, but the payload here is far smaller so the pause is too).
sleep(5)
end if
fileIndex++
end for
? "+-=-coverage-counts:end"
end function
function createLCovOutput(coverageResults as dynamic, logToConsole = true as boolean) as string
? "Generating lcov.info file..."
fileResults = []
for each file in coverageResults.files
buffer = `TN:\n`
buffer += `SF:${file.sourceFile}\n`
' buffer += `VER:\n`
' Add all the found functions for the file. Strictly 2-arg FN rows: the 3-arg
' FN:start,end,name form is lcov v2+ and older/strict parsers (Coveralls,
' lcov-parse) reject or mis-read it. The end line still ships to host tooling
' via components/rooibos/CodeCoverage.json.
for each func in file.functions
buffer += `FN:${func.startLine},${func.name}\n`
end for
' Write function related data. Emit FNDA for every function (including unhit ones with 0)
' so consumers like genhtml / istanbul-reports can show missed functions.
for each func in file.functions
buffer += `FNDA:${func.totalHit},${func.name}\n`
end for
buffer += `FNF:${file.functionTotalFound}\n`
buffer += `FNH:${file.functionTotalHit}\n`
' Write branch related data. Emit BRDA for every branch including unhit ones so missed
' branches render in the HTML report. For single-arm ifs (isIfArm=true with only one
' tracked arm), synthesize the implicit-else branch from the if-line's evaluation count
' minus the tracked arm hits. This lets the HTML viewer flag never-taken falsy paths
' even though we don't directly instrument the implicit fall-through.
' One line-hit lookup pass per file - rescanning file.lines for every
' single-arm if is O(blocks x lines) in interpreted code on the device.
hitsByLine = {}
for each line in file.lines
hitsByLine[stri(line.lineNumber).trim()] = line.totalHit
end for
brf = 0
brh = 0
for each block in file.blocks
for each branch in block.branches
brf++
if branch.totalHit > 0 then
brh++
end if
buffer += `BRDA:${branch.line},${block.id},${branch.id},${branch.totalHit}\n`
end for
if block.isIfArm = true and block.branches.count() = 1 then
ifLine = block.branches[0].line
evaluations = hitsByLine[stri(ifLine).trim()] ?? 0
skipHits = evaluations - block.branches[0].totalHit
if skipHits < 0 then
skipHits = 0
end if
brf++
if skipHits > 0 then
brh++
end if
buffer += `BRDA:${ifLine},${block.id},1,${skipHits}\n`
end if
end for
buffer += `BRF:${brf}\n`
buffer += `BRH:${brh}\n`
' Write the per line related data. This output is deliberately plain lcov -
' rich detail (statement spans, branch arm columns) lives in the static
' CodeCoverage.json model and flows through the condensed-counts channel that
' is always printed alongside this report.
for each line in file.lines
buffer += `DA:${line.lineNumber},${line.totalHit}\n`
end for
buffer += `LF:${file.lineTotalFound}\n`
buffer += `LH:${file.lineTotalHit}\n`
buffer += `end_of_record`
if logToConsole then
? buffer
' When logging to the console it is very possible to flood the buffer and cause the application to exit.
' Sleep for a short amount of time so as to give console scrapers time to empty the buffer
sleep(30)
else
fileResults.push(buffer)
end if
end for
if not logToConsole then
return fileResults.join(chr(10))
else
return ""
end if
end function
function printLCovInfo(coverageResults as dynamic)
?
? "+++++++++++++++++++++++++++++++++++++++++++"
? "LCOV.INFO FILE"
? "+++++++++++++++++++++++++++++++++++++++++++"
?
? "+-=-coverage:start"
rooibos.coverage.createLCovOutput(coverageResults)
? "+-=-coverage:end"
end function
end namespace