Added a code coverage report.
authorachristensen@apple.com <achristensen@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 26 Jun 2013 20:51:50 +0000 (20:51 +0000)
committerachristensen@apple.com <achristensen@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 26 Jun 2013 20:51:50 +0000 (20:51 +0000)
https://bugs.webkit.org/show_bug.cgi?id=117941

Reviewed by Joseph Pecoraro.

* CodeCoverage: Added.
* CodeCoverage/results-template.html: Added.
* Scripts/generate-coverage-data: Generate and open the new report.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@152012 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Tools/ChangeLog
Tools/CodeCoverage/results-template.html [new file with mode: 0644]
Tools/Scripts/generate-coverage-data

index 92e2ee3..db8ee6d 100644 (file)
@@ -1,3 +1,14 @@
+2013-06-26  Alex Christensen  <achristensen@apple.com>
+
+        Added a code coverage report.
+        https://bugs.webkit.org/show_bug.cgi?id=117941
+
+        Reviewed by Joseph Pecoraro.
+
+        * CodeCoverage: Added.
+        * CodeCoverage/results-template.html: Added.
+        * Scripts/generate-coverage-data: Generate and open the new report.
+
 2013-06-25  Raphael Kubo da Costa  <raphael.kubo.da.costa@intel.com>
 
         gdb: Remove the webcore.py pretty printer.
diff --git a/Tools/CodeCoverage/results-template.html b/Tools/CodeCoverage/results-template.html
new file mode 100644 (file)
index 0000000..42c5c24
--- /dev/null
@@ -0,0 +1,358 @@
+<!doctype html>
+<html>
+    <head>
+        <style type='text/css'>
+            table, tr, td
+            {
+                border-spacing: 1px;
+                padding: 0px;
+            }
+            td.textColumn
+            {
+                white-space: nowrap;
+                font-family: Courier, monospace;
+            }
+
+            #directories, #codeviewer
+            {
+                overflow: scroll;
+                background-color: white;
+                position: absolute;
+                width: 50%;
+                height: 100%;
+            }
+
+            #directories
+            {
+                left: 0%;
+            }
+
+            #codeviewer
+            {
+                left: 50%;
+            }
+
+            ul
+            {
+                padding: 1px 0px 1px 8px;
+                margin: 0px;
+                list-style-type: none;
+            }
+            
+            li.file
+            {
+                /* 8px is to match the width of downArrow and rightArrow because files don't have an image before them. */
+                padding: 0px 0px 0px 8px; 
+            }
+            
+            div.graphsContainer
+            {
+                right: 0px;
+                width: 300px;
+                position: absolute;
+                display: inline-block;            
+            }
+            div.codeCoverage
+            {
+                right: 0px;
+                width: 150px;
+                position: absolute;
+                display: inline-block;
+            }
+            div.branchCoverage
+            {
+                right: 150px;
+                width: 150px;
+                position: absolute;
+                display: inline-block;
+            }
+
+        </style>
+        <script type='text/javascript'>
+
+            // This is the contents of the images left of directories.
+            var downArrow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYHFioBcCRLNAAAAJhJREFUGNOFzrEKwjAURuHTJHUspekDZGmhdlawCEqoUDK19O1cnX0dFxcnQXyIOAhKSsUzXr4Lf3Q8nf3leuNXy8IgBmeRYh5IAYOziDxLsdv1LNo3K/IsRQAcdhu0TgKgdUJnG4A3ipVk7NoAjV2LkvKLAKrSUBcGgLowVKX5PASTe2eJF4re2XCcn3R/PKcnH3nvPX96AbbuOGLOrLT1AAAAAElFTkSuQmCC';
+            var rightArrow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYHFiojpUQK0AAAAKZJREFUGNNjmL1k/f+379//xweYlXXMGo6dvcDw/x8jg7y0OAMzMzMDOmBiYGBg+PP7P8OO/ccYOqctZLh2+x52RTDw9t0nhtlLNjDMWbqB4d2HD9gVwcDVW/cYumcsZXj56i0DAwMDAws2RdqqCgwBHo4MIiKCmIqEBPgYgrycGbTVFVE0sTAwMDCwMjMyONqYMbjYWjCwsmL6jmH24rX/37zBH04AHAxpneY+98AAAAAASUVORK5CYII=';
+
+            function getHeatBackgroundColor(hits, maxHits)
+            {
+                if (hits === -1)
+                    return 'white'; // Non-code lines are white.
+                else if (hits === 0)
+                    return 'orange'; // Unexecuted lines are orange.
+                else {
+                    // Executed lines are between red and green.
+                    var relativeHeat = Math.floor(hits / maxHits * 255);
+                    return 'rgb(' + relativeHeat + ',' + (255 - relativeHeat) + ', 0)';
+                }
+            }
+
+            function getCoverageBackgroundColor(coverage)
+            {
+                var value = Math.floor(coverage * 255);
+                return 'rgb(' + (255 - value) + ',' + value + ', 0)';
+            }
+            
+            function expandClicked(event)
+            {
+                var children = this.parentNode.lastChild;
+                if (children.style.display === '') {
+                    children.style.display = 'none';
+                    this.src = rightArrow;
+                } else {
+                    children.style.display = '';
+                    this.src = downArrow;
+                }
+            }
+
+            function processFile(fileData, contents)
+            {
+                var lines = contents.split('\n');
+                var hits = new Array();
+                var branchesNumerator = new Array();
+                var branchesDenominator = new Array();
+
+                for (var i = 0; i < lines.length; i++) {
+                    hits[i] = -1;
+                    branchesNumerator[i] = -1;
+                    branchesDenominator[i] = -1;
+                }
+                
+                for (var i = 0; i < fileData.hitLines.length; i++)
+                    hits[fileData.hitLines[i] - 1] = fileData.hits[i];
+                    
+                for (var i = 0; i < fileData.branchLines.length; i++) {
+                    branchesNumerator[fileData.branchLines[i] - 1] = fileData.branchesTaken[i];
+                    branchesDenominator[fileData.branchLines[i] - 1] = fileData.branchesPossible[i];
+                }
+
+                var table = document.createElement('table');
+
+                for (var i = 0; i < lines.length; i++) {
+                    var row = document.createElement('tr');
+                    
+                    var branchesColumn = document.createElement('td');
+                    if (branchesNumerator[i] != -1)
+                        branchesColumn.appendChild(document.createTextNode('(' + branchesNumerator[i] + '/' + branchesDenominator[i] + ')'));
+                    
+                    var hitsColumn = document.createElement('td');
+                    if (hits[i] != -1)
+                        hitsColumn.appendChild(document.createTextNode(hits[i]));
+                    
+                    var textColumn = document.createElement('td');
+                    textColumn.style.background = getHeatBackgroundColor(hits[i], fileData.maxHeat);
+                    textColumn.style.className = 'textColumn';
+                    textColumn.appendChild(document.createTextNode(lines[i]));
+                    
+                    row.appendChild(branchesColumn);
+                    row.appendChild(hitsColumn);
+                    row.appendChild(textColumn);
+                    table.appendChild(row);
+                }
+
+                return table;
+            }
+
+            function fileClicked(event)
+            {
+                var xhr = new XMLHttpRequest();
+                xhr.onreadystatechange = function()
+                {
+                    if (xhr.readyState === XMLHttpRequest.DONE) {
+                        var codeviewer = document.getElementById('codeviewer');
+                        codeviewer.replaceChild(processFile(xhr.fileData, xhr.responseText), codeviewer.firstChild);
+                    }
+                }
+                xhr.fileData = event.target.fileData;
+                xhr.open('GET', '../../' + event.target.fileData.filename.substring(1), true);
+                xhr.send();
+                event.stopPropagation();
+            }
+
+            function makeGraphs(dirOrFile)
+            {
+                var codeCoverage = document.createElement('div');
+                codeCoverage.className = 'codeCoverage';
+                var codeCoveragePercent = dirOrFile.totalLines ? Math.floor(dirOrFile.totalHitLines / dirOrFile.totalLines * 100) + '%' : '-';
+                var codeCoverageText = codeCoveragePercent + ' (' + dirOrFile.totalHitLines + '/' + dirOrFile.totalLines + ')';
+                codeCoverage.appendChild(document.createTextNode(codeCoverageText));
+                codeCoverage.style.backgroundColor = getCoverageBackgroundColor(dirOrFile.coverage);
+
+                var branchCoverage = document.createElement('div');
+                branchCoverage.className = 'branchCoverage';
+                var branchCoveragePercent = dirOrFile.totalBranchesPossible ? Math.floor(dirOrFile.totalBranchesTaken / dirOrFile.totalBranchesPossible * 100) + '%' : '-';
+                branchCoverage.appendChild(document.createTextNode(branchCoveragePercent + ' (' + dirOrFile.totalBranchesTaken + '/' + dirOrFile.totalBranchesPossible + ')'));
+                branchCoverage.style.backgroundColor = getCoverageBackgroundColor(dirOrFile.branchCoverage);
+
+                var graphsContainer = document.createElement('div');
+                graphsContainer.className = 'graphsContainer';
+                graphsContainer.appendChild(codeCoverage);
+                graphsContainer.appendChild(branchCoverage);
+                return graphsContainer;
+            }
+            
+            function makeFileListItem(fileData, filename)
+            {            
+                var li = document.createElement('li');
+                li.className = 'file';
+                var a = document.createElement('a');
+                a.appendChild(document.createTextNode(filename));
+                a.href = '#';
+                a.addEventListener('click', fileClicked.bind(a));
+                a.fileData = fileData;
+                li.appendChild(a);
+                li.appendChild(makeGraphs(fileData));
+                return li;
+            }
+
+            function makeDirectoryListItem(dir, dirName)
+            {
+                var li = document.createElement('li');
+                var children = document.createElement('ul');
+
+                // Recursively add all sorted subdirectories and files.
+                var fileNames = dir.files ? Object.keys(dir.files).sort() : [];
+                var subdirNames = dir.subdirs ? Object.keys(dir.subdirs).sort() : [];
+                for (var i = 0; i < subdirNames.length; i++) {
+                    var subdir = subdirNames[i];
+                    children.appendChild(makeDirectoryListItem(dir.subdirs[subdir], subdir));
+                }
+                for (var i = 0; i < fileNames.length; i++) {
+                    var file = fileNames[i];
+                    children.appendChild(makeFileListItem(dir.files[file], file, dir.maxHeat, dir.totalHeat));
+                }
+                
+                var img = document.createElement('img');
+                img.addEventListener('click', expandClicked.bind(img));
+                
+                // These four directories are expanded by default.
+                if (dirName === '' || dirName === 'Source' || dirName === 'Tools' || dirName === 'WebKitBuild') {
+                    img.src = downArrow;
+                    children.style.display = '';
+                } else {
+                    img.src = rightArrow;
+                    children.style.display = 'none';
+                }
+                    
+                li.appendChild(img);
+                li.appendChild(document.createTextNode(dirName));
+                li.appendChild(makeGraphs(dir));
+                li.appendChild(children);
+                return li;
+            }
+            
+            // Collect total coverage for a directory and its subdirectories.
+            function collectDirectoryTotals(directory)
+            {
+                directory.totalBranchesPossible = 0;
+                directory.totalBranchesTaken = 0;
+                directory.totalHitLines = 0;
+                directory.totalLines = 0;
+                directory.totalHeat = 0;
+                directory.maxHeat = 0;
+                if (directory.subdirs) {
+                    for (var subdirName in directory.subdirs) {
+                        var subdir = directory.subdirs[subdirName];
+
+                        collectDirectoryTotals(subdir);
+
+                        directory.totalBranchesPossible += subdir.totalBranchesPossible;
+                        directory.totalBranchesTaken += subdir.totalBranchesTaken;
+                        directory.totalHitLines += subdir.totalHitLines;
+                        directory.totalLines += subdir.totalLines;
+                        directory.totalHeat += subdir.totalHeat;
+                        directory.maxHeat = Math.max(directory.maxHeat, subdir.maxHeat);
+                    }
+                }
+                if (directory.files) {
+                    for (var fileName in directory.files) {
+                        var file = directory.files[fileName];
+
+                        file.totalBranchesPossible = 0;
+                        file.totalBranchesTaken = 0;
+                        file.totalHitLines = 0;
+                        file.totalLines = file.hitLines.length;
+                        file.totalHeat = 0;
+                        
+                        for (var i = 0; i < file.branchesPossible.length; i++) {
+                            file.totalBranchesPossible += file.branchesPossible[i];
+                            file.totalBranchesTaken += file.branchesTaken[i];
+                        }
+                        for (var i = 0; i < file.hits.length; i++) {
+                            file.totalHeat += file.hits[i];
+                            if (file.hits[i])
+                                file.totalHitLines++;
+                        }
+                        
+                        directory.totalBranchesPossible += file.totalBranchesPossible;
+                        directory.totalBranchesTaken += file.totalBranchesTaken;
+                        directory.totalHitLines += file.totalHitLines;
+                        directory.totalLines += file.totalLines;
+                        directory.totalHeat += file.totalHeat;
+                        directory.maxHeat = Math.max(directory.maxHeat, file.maxHeat);
+                    }
+                }
+                directory.coverage = directory.totalHitLines / directory.totalLines;
+                directory.branchCoverage = directory.totalBranchesPossible ? directory.totalBranchesTaken / directory.totalBranchesPossible : 1;
+            }
+            
+            function addFileToDirectory(filename, filedata, directory)
+            {
+                var slashIndex = filename.indexOf('/', 1);
+                if (slashIndex === -1) {
+                    if (!directory.files)
+                        directory.files = {};
+                    directory.files[filename.substring(1)] = filedata;
+                } else {
+                    if (!directory.subdirs)
+                        directory.subdirs = {};
+                    var subdirName = filename.substring(1, slashIndex);
+                    if (!directory.subdirs[subdirName])
+                        directory.subdirs[subdirName] = {};
+                    addFileToDirectory(filename.substring(slashIndex), filedata, directory.subdirs[subdirName]);
+                }
+            }
+            
+            function updateReport(data)
+            {
+                var rootDirectory = {};
+                for (var i = 0; i < data.length; i++)
+                    addFileToDirectory(data[i].filename, data[i], rootDirectory);
+            
+                collectDirectoryTotals(rootDirectory);
+            
+                var report = document.createElement('div');
+                var codeCoverageHeader = document.createElement('div');
+                codeCoverageHeader.className = 'codeCoverage';
+                codeCoverageHeader.appendChild(document.createTextNode('Code Coverage'));
+                var branchCoverageHeader = document.createElement('div');
+                branchCoverageHeader.className = 'branchCoverage';
+                branchCoverageHeader.appendChild(document.createTextNode('Branch Coverage'));
+                var ul = document.createElement('ul');
+                ul.appendChild(makeDirectoryListItem(rootDirectory, ''));
+                
+                report.appendChild(codeCoverageHeader);
+                report.appendChild(branchCoverageHeader);
+                report.appendChild(document.createTextNode('Directories'));
+                report.appendChild(ul);
+                
+                var directories = document.getElementById('directories');
+                directories.replaceChild(report, directories.firstChild);
+            }
+            
+            function bodyLoaded()
+            {
+                updateReport(JSON.parse(document.getElementById('json').textContent));
+            }
+            
+        </script>
+    </head>
+    <body onload='bodyLoaded();'>
+        <div id='directories'>
+            loading data...
+        </div>
+        <div id='codeviewer'>
+        </div>
+        <script id='json' type='application/json'>%CoverageDataJSON%</script>
+    </body>
+</html>
index 3403d65..497487c 100755 (executable)
@@ -70,9 +70,20 @@ system("mkdir WebKitBuild/Coverage") if ! -d "WebKitBuild/Coverage";
 system("python Tools/Scripts/webkitpy/tool/gcovr --xml --output=WebKitBuild/Coverage/" . $resultName . ".xml") == 0 or die "Cannot run gcovr";
 
 # Collect useful data from xml to json format
-open my $jsonFile, ">", "WebKitBuild/Coverage/$resultName.json" or die "Cannot open $resultName.json";
-print $jsonFile encode_json(parseGcovrOutput("WebKitBuild/Coverage/$resultName.xml"));
-close $jsonFile;
+my $jsonData = encode_json(parseGcovrOutput("WebKitBuild/Coverage/$resultName.xml"));
+open my $templateFile, "<", "Tools/CodeCoverage/results-template.html" or die "Cannot open Tools/CodeCoverage/results-template.html";
+my $templateHtml = join("", <$templateFile>);
+close $templateFile;
+$templateHtml =~ s/%CoverageDataJSON%/$jsonData/;
+
+my $reportFilename = "WebKitBuild/Coverage/$resultName.html";
+open my $reportFile, ">", $reportFilename or die "Cannot open $reportFilename";
+print $reportFile $templateHtml;
+close $reportFile;
+
+# Open the report
+my $url = "file://" . sourceDir() . "/WebKitBuild/Coverage/$resultName.html";
+system "open \"$url\"";
 
 print "Done\n";