Skip to content

Commit

Permalink
Add combine and testname options
Browse files Browse the repository at this point in the history
Description:
- Fix #39
- Fix #32
- Remove support for gcov-json output (nobody uses it)
- Fix example build
- Add additional test to make sure example builds
  • Loading branch information
RPGillespie6 committed Mar 20, 2020
1 parent 23e40c6 commit eb459ef
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 29 deletions.
4 changes: 2 additions & 2 deletions example/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ ninja
# Run unit tests
ctest

# Run fastcov with smart branch filtering, as well as system header (/usr/include) and test file filtering
${BASE_DIR}/fastcov.py --gcov gcov-9 --branch-coverage --exclude /usr/include test/ --lcov -o example.info
# Run fastcov with smart branch filtering, as well as system header (/usr/include) and cmake project test file filtering
${BASE_DIR}/fastcov.py -t ExampleTest --gcov gcov-9 --branch-coverage --exclude /usr/include cmake_project/test/ --lcov -o example.info

# Generate report with lcov's genhtml
genhtml --branch-coverage example.info -o coverage_report
Expand Down
160 changes: 144 additions & 16 deletions fastcov.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import threading
import subprocess
import multiprocessing
from collections import Counter # For additively merging dictionaries

FASTCOV_VERSION = (1,5)
MINIMUM_PYTHON = (3,5)
Expand Down Expand Up @@ -174,8 +175,7 @@ def dumpToLcovInfo(fastcov_json, output):
sources = fastcov_json["sources"]
for sf in sorted(sources.keys()):
data = sources[sf]
# NOTE: TN stands for "Test Name" and appears to be unimplemented, but lcov includes it, so we do too...
f.write("TN:\n")
f.write("TN:{}\n".format(fastcov_json["testname"]))
f.write("SF:{}\n".format(sf)) #Source File

fn_miss = 0
Expand Down Expand Up @@ -350,13 +350,14 @@ def distillSource(source_raw, sources, include_exceptional_branches):
for line in source_raw["lines"]:
distillLine(line, sources[source_name]["lines"], sources[source_name]["branches"], include_exceptional_branches)

def distillReport(report_raw, include_exceptional_branches):
def distillReport(report_raw, args):
report_json = {
"sources": {}
"sources": {},
"testname": args.testname if args.testname else ""
}

for source in report_raw:
distillSource(source, report_json["sources"], include_exceptional_branches)
distillSource(source, report_json["sources"], args.xbranchcoverage)

return report_json

Expand All @@ -371,6 +372,134 @@ def getGcovFilterOptions(args):
"exclude": args.excludepost,
}

def addDicts(dict1, dict2):
"""Add dicts together by value. i.e. addDicts({"a":1,"b":0}, {"a":2}) == {"a":3,"b":0}"""
result = {k:v for k,v in dict1.items()}
for k,v in dict2.items():
if k in result:
result[k] += v
else:
result[k] = v

return result

def addLists(list1, list2):
"""Add lists together by value. i.e. addLists([1,1], [2,2]) == [3,3]"""
if len(list1) == len(list2):
return [b1 + b2 for b1, b2 in zip(list1, list2)]
else:
# One report had different number branch measurements than the other, print a warning
sys.stderr.write("Warning: possible loss of correctness. Different number of branches for same line when combining reports ({} vs {})\n".format(list1, list2))
return list1 if len(list1) > len(list2) else list2

def combineReports(base, overlay):
if overlay["testname"]:
base["testname"] = overlay["testname"]

for source, scov in overlay["sources"].items():
# Combine Source Coverage
if source not in base["sources"]:
base["sources"][source] = scov
continue

# Combine Line Coverage
base["sources"][source]["lines"] = addDicts(base["sources"][source]["lines"], scov["lines"])

# Combine Branch Coverage
for branch, cov in scov["branches"].items():
if branch not in base["sources"][source]["branches"]:
base["sources"][source]["branches"][branch] = cov
else:
base["sources"][source]["branches"][branch] = addLists(base["sources"][source]["branches"][branch], cov)

# Combine Function Coverage
for function, cov in scov["functions"].items():
if function not in base["sources"][source]["functions"]:
base["sources"][source]["functions"][function] = cov
else:
base["sources"][source]["functions"][function]["execution_count"] += cov["execution_count"]

def parseInfo(path):
"""Parse an lcov .info file into fastcov json"""
fastcov_json = {
"sources": {},
"testname": ""
}

with open(path) as f:
for line in f:
if line.startswith("TN:"):
fastcov_json["testname"] = line[3:].strip()
elif line.startswith("SF:"):
current_sf = line[3:].strip()
fastcov_json["sources"][current_sf] = {
"functions": {},
"branches": {},
"lines": {},
}
elif line.startswith("FN:"):
line_num, function_name = line[3:].strip().split(",")
fastcov_json["sources"][current_sf]["functions"][function_name] = {}
fastcov_json["sources"][current_sf]["functions"][function_name]["start_line"] = int(line_num)
elif line.startswith("FNDA:"):
count, function_name = line[5:].strip().split(",")
fastcov_json["sources"][current_sf]["functions"][function_name]["execution_count"] = int(count)
elif line.startswith("DA:"):
line_num, count = line[3:].strip().split(",")
fastcov_json["sources"][current_sf]["lines"][line_num] = int(count)
elif line.startswith("BRDA:"):
branch_tokens = line[5:].strip().split(",")
line_num, count = branch_tokens[0], branch_tokens[-1]
if line_num not in fastcov_json["sources"][current_sf]["branches"]:
fastcov_json["sources"][current_sf]["branches"][line_num] = []
fastcov_json["sources"][current_sf]["branches"][line_num].append(int(count))

return fastcov_json

def convertKeysToInt(report):
for source in report["sources"]:
report["sources"][source]["lines"] = {int(k):v for k,v in report["sources"][source]["lines"].items()}
report["sources"][source]["branches"] = {int(k):v for k,v in report["sources"][source]["branches"].items()}

def parseAndCombine(paths):
base_report = {}

for path in paths:
if path.endswith(".json"):
with open(path) as f:
report = json.load(f)
elif path.endswith(".info"):
report = parseInfo(path)
else:
sys.stderr.write("Currently only fastcov .json and lcov .info supported for combine operations, aborting due to {}...\n".format(path))
sys.exit(3)

# In order for sorting to work later when we serialize,
# make sure integer keys are int
convertKeysToInt(report)

if not base_report:
base_report = report
log("Setting {} as base report".format(path))
else:
combineReports(base_report, report)
log("Adding {} to base report".format(path))

return base_report

def combineCoverageFiles(args):
log("Performing combine operation")
fastcov_json = parseAndCombine(args.combine)
dumpFile(fastcov_json, args)

def dumpFile(fastcov_json, args):
if args.lcov:
dumpToLcovInfo(fastcov_json, args.output)
log("Created lcov info file '{}'".format(args.output))
else:
dumpToJson(fastcov_json, args.output)
log("Created fastcov json file '{}'".format(args.output))

def tupleToDotted(tup):
return ".".join(map(str, tup))

Expand Down Expand Up @@ -407,10 +536,12 @@ def parseArgs():
parser.add_argument('-F', '--fallback-encodings', dest='fallback_encodings', nargs="+", metavar='', default=[], help='List of encodings to try if opening a source file with the default fails (i.e. latin1, etc.). This option is not usually needed.')

parser.add_argument('-l', '--lcov', dest='lcov', action="store_true", help='Output in lcov info format instead of fastcov json')
parser.add_argument('-r', '--gcov-raw', dest='gcov_raw', action="store_true", help='Output in gcov raw json instead of fastcov json')
parser.add_argument('-o', '--output', dest='output', default="coverage.json", help='Name of output file (default: coverage.json)')
parser.add_argument('-q', '--quiet', dest='quiet', action="store_true", help='Suppress output to stdout')

parser.add_argument('-t', '--test-name', dest='testname', help='Specify a test name for the coverage. Equivalent to lcov\'s `-t`.')
parser.add_argument('-C', '--add-tracefile', dest='combine', nargs="+", help='Combine multiple coverage files into one. If this flag is specified, fastcov will do a combine operation instead invoking gcov. Equivalent to lcov\'s `-a`.')

parser.add_argument('-v', '--version', action="version", version='%(prog)s {version}'.format(version=__version__), help="Show program's version number and exit")

args = parser.parse_args()
Expand Down Expand Up @@ -442,6 +573,11 @@ def main():
# Need at least python 3.5 because of use of recursive glob
checkPythonVersion(sys.version_info[0:2])

# Combine operation?
if args.combine:
combineCoverageFiles(args)
return

# Need at least gcov 9.0.0 because that's when gcov JSON and stdout streaming was introduced
checkGcovVersion(getGcovVersion(args.gcov))

Expand Down Expand Up @@ -469,23 +605,15 @@ def main():
log("Processed {} .gcov files ({} total, {} skipped)".format(gcov_total - gcov_skipped, gcov_total, gcov_skipped))

# Distill all the extraneous info gcov gives us down to the core report
fastcov_json = distillReport(intermediate_json_files, args.xbranchcoverage)
fastcov_json = distillReport(intermediate_json_files, args)
log("Aggregated raw gcov JSON into fastcov JSON report")

# Scan for exclusion markers
scanExclusionMarkers(fastcov_json, args.jobs, args.exclude_branches_sw, args.include_branches_sw, args.minimum_chunk, args.fallback_encodings)
log("Scanned {} source files for exclusion markers".format(len(fastcov_json["sources"])))

# Dump to desired file format
if args.lcov:
dumpToLcovInfo(fastcov_json, args.output)
log("Created lcov info file '{}'".format(args.output))
elif args.gcov_raw:
dumpToJson(intermediate_json_files, args.output)
log("Created gcov raw json file '{}'".format(args.output))
else:
dumpToJson(fastcov_json, args.output)
log("Created fastcov json file '{}'".format(args.output))
dumpFile(fastcov_json, args)

# Set package version... it's way down here so that we can call tupleToDotted
__version__ = tupleToDotted(FASTCOV_VERSION)
Expand Down
39 changes: 39 additions & 0 deletions test/functional/expected_results/combine1.expected.info
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
TN:FunctionalTest1
SF:/mnt/workspace/test/functional/cmake_project/src/source1.cpp
FN:3,_Z3foob
FNDA:2,_Z3foob
FNF:1
FNH:1
BRDA:7,0,0,1000
BRDA:7,0,1,1
BRDA:10,0,0,0
BRDA:10,0,1,1000
BRF:4
BRH:3
DA:3,2
DA:5,2
DA:7,2002
DA:8,2000
DA:10,2000
DA:11,0
DA:14,2
LF:7
LH:6
end_of_record
TN:FunctionalTest1
SF:/mnt/workspace/test/functional/cmake_project/src/source2.cpp
FN:3,_Z3barbii
FNDA:20,_Z3barbii
FNF:1
FNH:1
BRDA:5,0,0,10
BRDA:5,0,1,0
BRF:2
BRH:1
DA:3,20
DA:5,20
DA:6,20
DA:8,0
LF:4
LH:3
end_of_record
39 changes: 39 additions & 0 deletions test/functional/expected_results/combine2.expected.info
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
TN:
SF:/mnt/workspace/test/functional/cmake_project/src/source1.cpp
FN:3,_Z3foob
FNDA:1,_Z3foob
FNF:1
FNH:1
BRDA:7,0,0,1000
BRDA:7,0,1,1
BRDA:10,0,0,0
BRDA:10,0,1,1000
BRF:4
BRH:3
DA:3,1
DA:5,1
DA:7,1001
DA:8,1000
DA:10,1000
DA:11,0
DA:14,1
LF:7
LH:6
end_of_record
TN:
SF:/mnt/workspace/test/functional/cmake_project/src/source2.cpp
FN:3,_Z3barbii
FNDA:10,_Z3barbii
FNF:1
FNH:1
BRDA:5,0,0,10
BRDA:5,0,1,0
BRF:2
BRH:1
DA:3,10
DA:5,10
DA:6,10
DA:8,0
LF:4
LH:3
end_of_record
44 changes: 44 additions & 0 deletions test/functional/expected_results/combine3.expected.info
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
TN:
SF:/mnt/workspace/test/functional/cmake_project/src/source1.cpp
FN:3,_Z3foob
FN:55,_Z3barb
FNDA:1,_Z3barb
FNDA:2,_Z3foob
FNF:2
FNH:2
BRDA:7,0,0,2000
BRDA:7,0,1,2
BRDA:10,0,0,0
BRDA:10,0,1,2000
BRDA:71,0,0,5
BRDA:71,0,1,5
BRF:6
BRH:5
DA:3,2
DA:5,2
DA:7,2002
DA:8,2000
DA:10,2000
DA:11,0
DA:14,2
DA:58,10
LF:8
LH:7
end_of_record
TN:
SF:/mnt/workspace/test/functional/cmake_project/src/source2.cpp
FN:3,_Z3barbii
FNDA:20,_Z3barbii
FNF:1
FNH:1
BRDA:5,0,0,20
BRDA:5,0,1,0
BRF:2
BRH:1
DA:3,20
DA:5,20
DA:6,20
DA:8,0
LF:4
LH:3
end_of_record
Loading

0 comments on commit eb459ef

Please sign in to comment.