diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e67ab81 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +ISC License + +Copyright (c) 2012-2015 Jason Cranmer, 2019-2025 Ole Henrik Dahle and Ann-Karin Kihle +Included D3 library is also under the ISC license, Copyright 2010-2021 Mike Bostock + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..db6245a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +Various code coverage scripts, originally used for the Mozilla (browser) project by Joshua Cranmer. +Forked from https://github.com/jcranmer/mozilla-coverage, since that project is no longer maintained. +The Python scripts rely on lcov and d3.js to produce colored tables and treemaps to illustrate code coverage. diff --git a/ccov.py b/ccov.py index 4a43c90..e16831a 100755 --- a/ccov.py +++ b/ccov.py @@ -6,6 +6,7 @@ import shutil import subprocess import tempfile +import sys def format_set_difference(a, b): if a == b: @@ -35,7 +36,7 @@ def add_line_hit(self, line, hitcount): def lines(self): '''Returns an iterator over (line #, hit count) for this file.''' - for i in xrange(len(self._lines)): + for i in range(len(self._lines)): count = self._lines[i] if count != -1: yield (i, count) @@ -53,8 +54,13 @@ def add_function_hit(self, name, hitcount, lineno=None): def functions(self): '''Returns an iterator over (function name, line #, hit count) for this file.''' - for func, fndata in self._funcs.iteritems(): - yield (func, fndata[0], fndata[1]) + if sys.version_info[0] == 2: + # Python 2 has iteritems, Python 3 has items + for func, fndata in self._funcs.iteritems(): + yield (func, fndata[0], fndata[1]) + else: + for func, fndata in self._funcs.items(): + yield (func, fndata[0], fndata[1]) def add_branch_hit(self, lineno, brno, targetid, count): '''Note that the brno'th branch on the line number going to the targetid @@ -65,10 +71,17 @@ def add_branch_hit(self, lineno, brno, targetid, count): def branches(self): '''Returns an iterator over (line #, branch #, [ids], [counts]) for this file.''' - for tup in self._branches.iteritems(): - items = tup[1].items() - items.sort() - yield (tup[0][0], tup[0][1], [x[0] for x in items], [x[1] for x in items]) + if sys.version_info[0] == 2: + # Python 2 has iteritems, Python 3 has items + for tup in self._branches.iteritems(): + items = tup[1].items() + items.sort() + yield (tup[0][0], tup[0][1], [x[0] for x in items], [x[1] for x in items]) + else: + for tup in self._branches.items(): + items = tup[1].items() + items.sort() + yield (tup[0][0], tup[0][1], [x[0] for x in items], [x[1] for x in items]) def write_lcov_output(self, fd): '''Writes the record for this file to the file descriptor in the LCOV @@ -201,7 +214,7 @@ def loadGcdaTree(self, testname, gcdaDir): gcnodata.add_to_coverage(self, testname, dirpath) return for dirpath, dirnames, filenames in os.walk(gcdaDir): - print 'Processing %s' % dirpath + print("Processing ", dirpath) gcda_files = filter(lambda f: f.endswith('.gcda'), filenames) gcno_files = [f[:-2] + 'no' for f in gcda_files] filepairs = [(da, no) for (da, no) in zip(gcda_files, gcno_files) @@ -301,7 +314,7 @@ def __init__(self, basedir, gcovtool='gcov', table={}): self.table = table def loadDirectory(self, directory, gcda_files): - print 'Processing %s' % directory + print("Processing ", directory) gcda_files = map(lambda f: os.path.join(directory, f), gcda_files) gcovdir = tempfile.mktemp("gcovdir") os.mkdir(gcovdir) @@ -389,7 +402,7 @@ def main(argv): coverage = CoverageData() if opts.more_files == None: opts.more_files = [] for lcovFile in opts.more_files: - print >> sys.stderr, "Reading file %s" % lcovFile + sys.stderr.write("Reading file ", lcovFile) fd = open(lcovFile, 'r') coverage.addFromLcovFile(fd) @@ -404,7 +417,7 @@ def main(argv): coverage.filterFilesByGlob(opts.extract_glob) # Store it to output if opts.outfile != None: - print >> sys.stderr, "Writing to file %s" % opts.outfile + sys.stderr.write("Writing to file", opts.outfile) outfd = open(opts.outfile, 'w') else: outfd = sys.stdout diff --git a/make_ui.py b/make_ui.py index 0c661ca..6616f7d 100755 --- a/make_ui.py +++ b/make_ui.py @@ -1,6 +1,5 @@ #!/usr/bin/python -import cgi import json import os import shutil @@ -9,19 +8,27 @@ def main(argv): from optparse import OptionParser - o = OptionParser() + usage = "Usage: %prog [options] inputfile(s)" + o = OptionParser(usage) o.add_option('-o', '--output', dest="outdir", help="Directory to store all HTML files", metavar="DIRECTORY") o.add_option('-s', '--source-dir', dest="basedir", help="Base directory for source code", metavar="DIRECTORY") + o.add_option('-l', '--limits', dest="limits", + help="Custom limits for medium,high coverage") (opts, args) = o.parse_args(argv) if opts.outdir is None: - print "Need to pass in -o!" + print("Need to pass in -o!") sys.exit(1) + if len(args) < 2: + print("Need to specify at least one input file!") + sys.exit(1) + # Add in all the data cov = CoverageData() for lcovFile in args[1:]: + print("Reading coverage data from", lcovFile) cov.addFromLcovFile(open(lcovFile, 'r')) # Make the output directory @@ -29,19 +36,20 @@ def main(argv): os.makedirs(opts.outdir) print ('Building UI...') - builder = UiBuilder(cov, opts.outdir, opts.basedir) + builder = UiBuilder(cov, opts.outdir, opts.basedir, opts.limits) builder.makeStaticOutput() builder.makeDynamicOutput() class UiBuilder(object): - def __init__(self, covdata, outdir, basedir): + def __init__(self, covdata, outdir, basedir, limits): self.data = covdata self.flatdata = self.data.getFlatData() self.outdir = outdir self.uidir = os.path.dirname(__file__) self.basedir = basedir + self.limits = limits self.relsrc = None - self.tests = ['all'] + self.tests = [] def _loadGlobalData(self): json_data = self.buildJSONData(self.flatdata) @@ -126,7 +134,8 @@ def makeStaticOutput(self): def makeDynamicOutput(self): # Dump out JSON files json_data = self._loadGlobalData() - json.dump(json_data, open(os.path.join(self.outdir, 'all.json'), 'w')) + if 'all' in self.tests : + json.dump(json_data, open(os.path.join(self.outdir, 'all.json'), 'w')) for test in self.data.getTests(): small_data = self.data.getTestData(test) if len(small_data) == 0: @@ -154,6 +163,14 @@ def _readTemplate(self, name): def _makeDirectoryIndex(self, dirname, jsondata): # Utility method for printing out rows of the table + mediumLimit = 75.0 + highLimit = 90.0 + if self.limits: + values = self.limits.split(","); + if len(values) == 2: + mediumLimit = float(values[0]) + highLimit = float(values[1]) + def summary_string(lhs, jsondata): output = '' output += '%s' % lhs @@ -164,15 +181,17 @@ def summary_string(lhs, jsondata): output += '0 / 0-' else: ratio = 100.0 * hit / count - if ratio < 75.0: clazz = "lowcov" - elif ratio < 90.0: clazz = "mediumcov" + if ratio < mediumLimit: clazz = "lowcov" + elif ratio < highLimit: clazz = "mediumcov" else: clazz = "highcov" output += '%d / %d%.1f%%' % ( clazz, hit, count, clazz, ratio) return output + '' - htmltmp = self._readTemplate('directory.html') - - jsondata['files'].sort(lambda x, y: cmp(x['name'], y['name'])) + if dirname: + htmltmp = self._readTemplate('directory.html') + else: + htmltmp = self._readTemplate('root_directory.html') + jsondata['files'].sort(key=lambda x: x['name']) # Parameters for output parameters = {} @@ -185,10 +204,12 @@ def summary_string(lhs, jsondata): ('' % test) for test in self.tests) from datetime import date parameters['date'] = date.today().isoformat() + if not dirname: + parameters['reponame'] = os.getcwd()[os.getcwd().rfind('/')+1:len(os.getcwd())] def htmlname(json): if len(json['files']) > 0: - return json['name'] + return json['name'] + "/index.html" else: return json['name'] + '.html' tablestr = '\n'.join(summary_string( @@ -214,13 +235,28 @@ def htmlname(json): self._makeFileData(dirname, child['name'], child) def _makeFileData(self, dirname, filename, jsondata): - print 'Writing %s/%s.html' % (dirname, filename) - htmltmp = self._readTemplate('file.html') + # Python 2 / 3 compatibility fix + try: + import html + except ImportError: + import cgi as html + if dirname: + if dirname == 'inc': + htmltmp = self._readTemplate('single_test_file.html') + else: + htmltmp = self._readTemplate('file.html') + else: + htmltmp = self._readTemplate('single_test_file.html') parameters = {} parameters['file'] = os.path.join(dirname, filename) parameters['directory'] = dirname - parameters['depth'] = '/'.join('..' for x in dirname.split('/')) + + if dirname: + parameters['depth'] = '/'.join('..' for x in dirname.split('/')) + else: + parameters['depth'] = '.' + parameters['testoptions'] = '\n'.join( '' % s for s in self.tests) from datetime import date @@ -234,8 +270,13 @@ def _makeFileData(self, dirname, filename, jsondata): 'File could not be found') parameters['data'] = '' else: - with open(srcfile, 'r') as fd: - srclines = fd.readlines() + if sys.version_info[0] == 2: + # Python 2 version of open + with open(srcfile, mode="r") as fd: + srclines = fd.readlines() + else: + with open(srcfile, mode="r", encoding="utf-8", errors="ignore") as fd: + srclines = fd.readlines() flatdata = self.flatdata[filekey] del self.flatdata[filekey] # Scavenge memory we don't need anymore. @@ -278,7 +319,7 @@ def _makeFileData(self, dirname, filename, jsondata): outlines.append((' %d' + '%s%s%s\n' ) % (covstatus, lineno, brcount, linecount, - cgi.escape(line.rstrip()))) + html.escape(line.rstrip()))) lineno += 1 parameters['tbody'] = ''.join(outlines) diff --git a/uitemplates/coverage.html b/uitemplates/coverage.html index bff3d7a..2eebc18 100644 --- a/uitemplates/coverage.html +++ b/uitemplates/coverage.html @@ -59,7 +59,7 @@ .style("position", "relative") .style("width", width + "px") .style("height", height + "px"); - d3.json("all.json", loadJsonData); + d3.json(d3.select("#testsuite").node().value + ".json", loadJsonData); // Bind the coverage scale d3.select("#scale").selectAll("rect") @@ -171,6 +171,7 @@ } function get_source_file(data) { + if(!data) return ''; if ("_path" in data) return data._path; if ('parent' in data) { @@ -210,7 +211,22 @@ while(d && d.parent != root) d = d.parent; } if (d) - reroot(d); + { + if (d.files.length > 0) + { + reroot(d); + } else + { + // Single file, open textual view + var url = window.location.href; + if (url.indexOf('?')) + { + url = url.substr(0,url.indexOf('?') -1); + } + var directory = url.substr(0,url.lastIndexOf("/")); + window.location.href = directory + "/" + d._path + ".html" + } + } }).transition().delay(2000).style("opacity", 1); nodes.order(); diff --git a/uitemplates/directory.html b/uitemplates/directory.html index 61260bd..adbc727 100644 --- a/uitemplates/directory.html +++ b/uitemplates/directory.html @@ -9,14 +9,14 @@

Code coverage report for ${directory}

-
+
Test:
Date compiled: ${date}
Graphical Overview | Detailed Report
- -
+ +
diff --git a/uitemplates/root_directory.html b/uitemplates/root_directory.html new file mode 100644 index 0000000..29db278 --- /dev/null +++ b/uitemplates/root_directory.html @@ -0,0 +1,30 @@ + + + +Code coverage for ${directory} + + + + + + +

Code coverage report for ${reponame}

+
> +
Test: + +
+
Date compiled: ${date}
+
Graphical Overview | Detailed Report
+
+
FilenameLineFunctionBranch
+ + + + + ${tbody} + + + ${tfoot} +
FilenameLineFunctionBranch
+ + diff --git a/uitemplates/single_test_file.html b/uitemplates/single_test_file.html new file mode 100644 index 0000000..23c752f --- /dev/null +++ b/uitemplates/single_test_file.html @@ -0,0 +1,26 @@ + + + +Code coverage + + + + + +

Code coverage report for ${file}

+
+
Test: + +
+
Date compiled: ${date}
+
+ + + + + + ${tbody} + + + + diff --git a/webui/ccov.css b/webui/ccov.css index fed91e8..0e5955e 100644 --- a/webui/ccov.css +++ b/webui/ccov.css @@ -1,39 +1,65 @@ body { - font-family: sans-serif; + font-family: Arial, sans-serif; } h1 { text-align: center; } -/* Styles for the region between the two hrs */ -div.info-panel { - float: left; - margin-bottom: 0.5em; - margin-right: 2em; +a { + color: #3498db; /* Lighter blue for links */ + text-decoration: none; } -div.info-panel + hr { - clear: both; + +/* Styles for the info panel on top of each page */ +div.info-container { + display: flex; + justify-content: center; + align-items: center; + background-color: #4a4a4a; /* Dark gray background */ + margin-bottom: 1em; + width: 66%; /* Align width with coveredtable */ + margin: 0 auto; /* Center the container */ + border-radius: 8px; } +div.info-panel { + flex: 1; + margin: 0 1em; + padding: 1em; + background-color: #4a4a4a; /* Dark gray background */ + border-radius: 8px; + text-align: center; + color: white; /* Ensure text is visible on dark background */ +} + + table#coveredtable { margin: 0 auto; min-width: 66%; + border-collapse: collapse; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; +} + +th, td { + border: 1px solid #ccc; /* Added border to cells */ } th { font-weight: bold; - background-color: #6688D4; + background-color: #4a4a4a; color: white; text-align: center; - padding: 0 1em; + padding: 0.75em 1em; font-size: 110%; } td { - background-color: #DAE7FE; + background-color: #d9d9d9; min-width: 3.5em; - padding: 0 0.5em; + padding: 0.75em 0.75em; text-align: right; } @@ -41,46 +67,62 @@ td { td:first-child { text-align: left; } -tbody td:first-child { + +tbody td:not(:first-child) { font-family: monospace; } /* Coverage colored backgrounds */ .lowcov { - background-color: #FEBAC2; + background-color: #ff6b6b; } + .mediumcov { - background-color: #FEF8B1; + background-color: #f7b731; } + .highcov { - background-color: #C5FEBA; + background-color: #20bf6b; } /* File coverage data styles */ table#filetable { font-family: monospace; border-spacing: 0; + width: 100%; + border-collapse: collapse; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; } + table#filetable td, table#filetable th { background-color: inherit; color: black; - padding: 0 0.5em; + padding: 0.75em; white-space: pre; vertical-align: top; + border: 1px solid #ccc; /* Added border to cells */ } + table#filetable td:first-child { - background-color: #EFE383; + background-color: #f9e79f; text-align: right; } + table#filetable td:not(:first-child):not(:last-child) { text-align: right; border-right: solid black 1px; min-width: 5em; } + table#filetable th { font-size: 90%; text-align: center; + background-color: #4a4a4a; + color: white; } + table#filetable tr > *:last-child { text-align: left; }
Branch dataLine dataSource code