Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
721036d
Added some FN tracking
chrisdp Feb 11, 2024
79dede2
Worked on new coverage map interface and function hit reporting
chrisdp Feb 12, 2024
3042933
Started working on block and branch injection
chrisdp Feb 12, 2024
2a0b6a3
Update bsc-plugin/src/lib/rooibos/CodeCoverageProcessor.spec.ts
chrisdp Feb 12, 2024
c5138d3
Branching and repotting from framwork
chrisdp Feb 12, 2024
09a8c39
Fixed the cov and lcov output
chrisdp Feb 13, 2024
f551831
Removed some commented out code
chrisdp Feb 13, 2024
6706ed4
code clean up
chrisdp Feb 13, 2024
0b7dd93
More code clean up
chrisdp Feb 13, 2024
7ac087c
removed unused imports
chrisdp Feb 13, 2024
58916e4
Fixed some line cov data being lost
chrisdp Feb 13, 2024
1b1a9aa
Some logging
chrisdp Feb 13, 2024
fe651aa
Merge branch 'lcov-fixes-and-improvments' into feature/more-detailed-…
chrisdp Feb 13, 2024
5dbde22
Merge branch 'lcov-fixes-and-improvments' into feature/more-detailed-…
chrisdp Feb 13, 2024
c474b0a
Merge branch 'lcov-fixes-and-improvments' into feature/more-detailed-…
chrisdp Feb 13, 2024
c437013
Merge branch 'master' into feature/more-detailed-lcov
chrisdp Jul 2, 2024
fdaff8d
Merge branch 'master' into feature/more-detailed-lcov
chrisdp Sep 4, 2024
f48fc9f
Apply suggestions from code review
chrisdp Sep 4, 2024
235e084
Merge branch 'master' into feature/more-detailed-lcov
chrisdp Mar 18, 2025
ea66f41
updated brighterscript version
chrisdp Mar 18, 2025
581b59c
Merge branch 'master' into feature/more-detailed-lcov
chrisdp Apr 15, 2025
759d848
Merge remote-tracking branch 'origin/master' into feature/more-detail…
chrisdp May 3, 2026
0a24c3b
feat: detailed lcov coverage (branches, functions, paired if/else, CL…
chrisdp May 3, 2026
ace05de
feat: try/catch branch coverage
chrisdp May 3, 2026
1c44e98
feat: ternary branch coverage with TS-style arm highlighting
chrisdp May 3, 2026
72ed696
feat: null-coalescing branch coverage
chrisdp May 3, 2026
fbb1f11
feat: logical and/or short-circuit branch coverage
chrisdp May 3, 2026
d2f2ed5
feat: anon function coverage samples + tighter fstat-no wrap
chrisdp May 3, 2026
74fa50d
fix: clear unused-var CI errors + tests for coverage/mocking interaction
chrisdp May 3, 2026
3fe67e5
chore: clear remaining lint errors in CodeCoverageProcessor.ts
chrisdp May 3, 2026
9bbaf6a
brs/bs linting updates and fixes
chrisdp May 3, 2026
f6487f8
feat: --package flag points the rooibos CLI at a pre-built artifact
chrisdp May 3, 2026
cc7b345
docs: handoff for auto-splitting big function bodies
chrisdp May 20, 2026
3f7867b
fix: exclude source maps when zipping a --package folder
chrisdp May 20, 2026
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
535 changes: 392 additions & 143 deletions bsc-plugin/src/lib/rooibos/CodeCoverageProcessor.spec.ts

Large diffs are not rendered by default.

525 changes: 472 additions & 53 deletions bsc-plugin/src/lib/rooibos/CodeCoverageProcessor.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bsc-plugin/src/lib/rooibos/FileFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { standardizePath as s } from 'brighterscript';
import * as path from 'path';
import * as fs from 'fs';
import * as fse from 'fs-extra';
import type { CoverageMap } from './CodeCoverageProcessor';
import type { TestSuite } from './TestSuite';

export class FileFactory {
Expand Down Expand Up @@ -106,10 +107,9 @@ export class FileFactory {
return contents;
}

public createCoverageComponent(program: Program, coverageMap: any, filepathMap: Map<number, string>) {
public createCoverageComponent(program: Program, baseCoverageReport: CoverageMap) {
let template = this.coverageComponentBrsTemplate;
template = template.replace(/\"\#EXPECTED_MAP\#\"/g, JSON.stringify(coverageMap ?? {}));
template = template.replace(/\"\#FILE_PATH_MAP\#\"/g, JSON.stringify(filepathMap ?? {}));
template = template.replace(/\"\#BASE_COVERAGE_REPORT\#\"/g, JSON.stringify(baseCoverageReport ?? { files: [] }));

this.addFileToRootDir(program, path.join('components/rooibos', 'CodeCoverage.brs'), template);
this.addFileToRootDir(program, path.join('components/rooibos', 'CodeCoverage.xml'), this.coverageComponentXmlTemplate);
Expand Down
141 changes: 112 additions & 29 deletions framework/src/source/CodeCoverage.brs
Original file line number Diff line number Diff line change
@@ -1,39 +1,122 @@
function init()
m.resolvedMap = {}
m.top.observeField("entry", "onEntryChange")
m.top.observeField("save", "onSave")
m.results = []
m.coverageMap = "#BASE_COVERAGE_REPORT#"
m.port = createObject("roMessagePort")
m.top.observeFieldScoped("entry", m.port)
m.top.functionName = "runTaskThread"
m.top.control = "RUN"
end function

function setExpectedMap()
m.top.expectedMap = "#EXPECTED_MAP#"
end function
function runTaskThread() as void
while true
events = []
saving = false
message = getMessage(m.port)
if message <> invalid
events.push(message)
end if
if m.top.save = true
saving = true
? "Saving unprocessed code cov events..."
'Get All the unprocessed messages
while true
message = getMessage(m.port, 3)
if message = invalid
exit while
else
events.push(message)
end if
end while

? "Found" events.count() " unprocessed events..."
end if

' enum CodeCoverageLineType
' noCode = 0
' code = 1
' condition = 2
' branch = 3
' function = 4
' end enum

for each event in events
entry = event.getData()
if entry <> invalid
file = m.coverageMap.files[entry.f]
if entry.r = 4 ' CodeCoverageLineType.function
if file.functions[entry.fn].totalHit = 0
file.functionTotalHit ++
end if
file.functions[entry.fn].totalHit ++
else if entry.r = 3 ' CodeCoverageLineType.branch
for each branch in file.blocks[entry.bl].branches
if branch.id = entry.br
if branch.totalHit = 0
file.branchTotalHit ++
end if
branch.totalHit ++
exit for
end if
end for
else if entry.r = 1 ' CodeCoverageLineType.code
for each line in file.lines
if line.lineNumber = entry.l
if line.totalHit = 0
file.lineTotalHit ++
end if
line.totalHit ++
exit for
end if
end for
end if
m.coverageMap.files[entry.f] = file
end if
end for

if saving = true
m.top.coverageResults = m.coverageMap
return
end if
end while

function setFilePathMap()
m.top.filePathMap = "#FILE_PATH_MAP#"
end function

function onEntryChange()
entry = m.top.entry
' defer till later
m.results.push(entry)

' Gets the next message from the message port and applies a short sleep if no message was returned.
' @param {roMessagePort} port - The active message port to get messages from.
' @param {Integer} [sleepInterval] - How long to sleep if there was no message returned.
' @return {Dynamic} Any resulting message from the message port.
function getMessage(port as object, sleepInterval = 20 as integer) as dynamic
message = port.getMessage()
' I know I will always get something from the port so no need for the uninitialized check in isInvalid
if message = invalid then sleep(sleepInterval)
return message
end function

function onSave()
? "saving data"
for each entry in m.results
if entry <> invalid
fileId = entry.f
lineMap = m.resolvedMap[fileId]
#if false
sub test()
player = m.player

if lineMap = invalid
lineMap = {}
m.resolvedMap[fileId] = lineMap
end if
lineMap[entry.l] = entry.r
report = false
if player = invalid or (player.duration > 0 and player.state = "playing") then
report = true
end if

if report = true then
player.control = "stop"
end if
end sub


sub test()
player = m.player

report = false
if RBS_CC_0_reportCondition(109, 1, player = invalid) or RBS_CC_0_reportCondition(109, 2, (RBS_CC_0_reportCondition(109, 3, player.duration > 0) and RBS_CC_0_reportCondition(109, 4, player.state = "playing"))) then
report = true
end if

if report = true then
player.control = "stop"
end if
end for
m.top.resolvedMap = m.resolvedMap
setExpectedMap()
setFilePathMap()
end function
end sub
#end if
7 changes: 3 additions & 4 deletions framework/src/source/CodeCoverage.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<component name="CodeCoverage"
extends="ContentNode"
extends="Task"
>
<script type="text/brightscript" uri="CodeCoverage.brs" />
<interface>
<field id="entry" type="assocarray" />
<field id="coverageResults" type="assocarray" />

<field id="save" type="boolean" />
<field id="expectedMap" type="assocarray" />
<field id="resolvedMap" type="assocarray" />
<field id="filePathMap" type="assocarray" />
</interface>
</component>
126 changes: 71 additions & 55 deletions framework/src/source/Coverage.bs
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
namespace rooibos.Coverage
function reportCodeCoverage() as void

function saveResults() as dynamic
m.global._rbs_ccn.save = true
cc = m.global._rbs_ccn
if cc.coverageResults = invalid
' The results are still being formatted
' Wait for them to be reported
port = createObject("roMessagePort")
cc.observeFieldScoped("coverageResults", port)
msg = wait(0, port)
coverageResults = msg.getData()
else
' Results are ready to go
coverageResults = cc.coverageResults
end if

return coverageResults
end function

function reportCodeCoverage(coverageResults as dynamic) as void

if m.global._rbs_ccn = invalid
? "There was no rooibos code coverage component - not generating coverage report"
Expand All @@ -9,33 +28,23 @@ namespace rooibos.Coverage
? ""
? "...Generating code coverage report"
? ""
m.global._rbs_ccn.save = true
cc = m.global._rbs_ccn
expectedMap = cc.expectedMap
filePathMap = cc.filePathMap
resolvedMap = cc.resolvedMap

hitFiles = []
missFiles = []
allLinesCount = 0
allLinesHit = 0
for each key in expectedMap
filename = filePathMap[key]
expectedCount = expectedMap[key].count()
for each file in coverageResults.files
filename = "pkg:/" + file.sourceFile
expectedCount = file.lineTotalFound
allLinesCount += expectedCount
allLinesHit += file.lineTotalHit
if expectedCount > 0
if resolvedMap[key] <> invalid
resolvedCount = resolvedMap[key].count()
allLinesHit += resolvedCount
if resolvedCount = 0
resolvedPercent = 0
else
resolvedPercent = (resolvedCount / expectedCount) * 100
end if
hitFiles.push({ percent: resolvedPercent, text: filename + ": " + str(resolvedPercent).trim() + "% (" + stri(resolvedCount).trim() + "/" + stri(expectedCount).trim() + ")" })
if file.lineTotalHit = 0
resolvedPercent = 0
else
missFiles.push(filename + ": MISS!")
resolvedPercent = (file.lineTotalHit / expectedCount) * 100
end if
hitFiles.push({ percent: resolvedPercent, text: filename + ": " + str(resolvedPercent).trim() + "% (" + stri(file.lineTotalHit).trim() + "/" + stri(expectedCount).trim() + ")" })
else
missFiles.push(filename + ": MISS!")
end if
Expand All @@ -52,7 +61,7 @@ namespace rooibos.Coverage
? "+++++++++++++++++++++++++++++++++++++++++++"
? ""
? "Total Coverage: " ; str(allLinesPercent).trim() ; "% (" ; stri(allLinesHit).trim() ; "/" + stri(allLinesCount).trim() ; ")"
? "Files: " ; resolvedMap.count(); "/" ; expectedMap.count()
? "Files: " ; hitFiles.count(); "/" ; coverageResults.files.count()
? ""
? "HIT FILES"
? "---------"
Expand All @@ -72,70 +81,77 @@ namespace rooibos.Coverage
? "+++++++++++++++++++++++++++++++++++++++++++"
end function

function createLCovOutput(logToConsole = true as boolean)
function createLCovOutput(coverageResults as dynamic, logToConsole = true)
? "Generating lcov.info file..."
fileResults = []

cc = m.global._rbs_ccn
expectedMap = cc.expectedMap
filePathMap = cc.filePathMap
resolvedMap = cc.resolvedMap
for each file in coverageResults.files
buffer = `TN:\n`
buffer += `SF:${file.sourceFile}\n`
' buffer += `VER:\n`

results = []
' Add all the found functions for the file
for each func in file.functions
buffer += `FN:${func.startLine},${func.endLine},${func.name}\n`
end for

for each module in filePathMap.items()
buffer = ""
moduleNumber = module.key
filePath = module.value
packageName = "."
' Write function related data
for each func in file.functions
if func.totalHit > 0
buffer += `FNDA:${func.totalHit},${func.name}\n`
end if
end for

relativePath = filePath.replace("pkg:", packageName)
sanitizedPath = relativePath.replace("\\", "/")
buffer += `FNF:${file.functionTotalFound}\n`
buffer += `FNH:${file.functionTotalHit}\n`

buffer += "TN:" + chr(10)
buffer += "SF:" + sanitizedPath + chr(10)
' Write branch related data
for each block in file.blocks
for each branch in block.branches
if branch.totalHit > 0
buffer += `BRDA:${branch.line},${block.id},${branch.id},${branch.totalHit}\n`
end if
end for
end for

for each expected in expectedMap[moduleNumber]
lineNumber = val(expected)
SHIFT = 1
buffer += `BRF:${file.branchTotalFound}\n`
buffer += `BRH:${file.branchTotalHit}\n`

if resolvedMap[moduleNumber] <> invalid and resolvedMap[moduleNumber].doesExist(expected)
buffer += "DA:" + str(lineNumber + SHIFT).trim() + "," + str(resolvedMap[moduleNumber][expected]).trim() + chr(10)
else
buffer += "DA:" + str(lineNumber + SHIFT).trim() + ",0" + chr(10)
end if
' Write the per line related data
for each line in file.lines
buffer += `DA:${line.lineNumber},${line.totalHit}\n`
end for

buffer += "LF:" + str(expectedMap[moduleNumber].count()).trim() + chr(10)
buffer += `LF:${file.lineTotalFound}\n`
buffer += `LH:${file.lineTotalHit}\n`
buffer += `end_of_record`

if resolvedMap[moduleNumber] <> invalid
buffer += "LH:" + str(resolvedMap[moduleNumber].count()).trim() + chr(10)
else
buffer += "LH:0" + chr(10)
end if

buffer += "end_of_record"
if logToConsole
? 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
results.push(buffer)
fileResults.push(buffer)
end if
end for

return results.join(chr(10))
if not logToConsole
return fileResults.join(chr(10))
else
return ""
end if
end function

function printLCovInfo()
function printLCovInfo(coverageResults as dynamic)

?
? "+++++++++++++++++++++++++++++++++++++++++++"
? "LCOV.INFO FILE"
? "+++++++++++++++++++++++++++++++++++++++++++"
?
? "+-=-coverage:start"
rooibos.coverage.createLCovOutput()
rooibos.coverage.createLCovOutput(coverageResults)
? "+-=-coverage:end"
end function

Expand Down
5 changes: 3 additions & 2 deletions framework/src/source/TestRunner.bs
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,11 @@ namespace rooibos
m.nodeContext.global.testsScene.rooibosTestResult = rooibosResult

if m.config.isRecordingCodeCoverage
rooibos.Coverage.reportCodeCoverage()
coverageResults = rooibos.Coverage.saveResults()
rooibos.Coverage.reportCodeCoverage(coverageResults)

if m.config.printLcov = true
rooibos.Coverage.printLCovInfo()
rooibos.Coverage.printLCovInfo(coverageResults)
end if
else
? "rooibos.Coverage.reportCodeCoverage is not a function"
Expand Down
Loading