| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Main module for the Chromium Code Coverage extension. This |
| * extension adds incremental and absolute code coverage stats |
| * to the deprecated Rietveld UI. Stats are added inline with |
| * file names as percentage of lines covered. |
| */ |
| |
| var coverage = coverage || {}; |
| |
| /** |
| * Contains all required configuration information. |
| * |
| * @type {Object} |
| * @const |
| */ |
| coverage.CONFIG = {}; |
| |
| /** |
| * URLs necessary for each project. These are necessary because the Rietveld |
| * sites are used by other projects as well, and is is only possible to find |
| * coverage stats for the projects registered here. |
| * |
| * @type {Object} |
| * @const |
| */ |
| coverage.CONFIG.COVERAGE_REPORT_URLS = { |
| 'Android': { |
| prefix: 'https://build.chromium.org/p/tryserver.chromium.linux/builders/' + |
| 'android_coverage/builds/', |
| suffix: '/steps/Incremental%20coverage%20report/logs/json.output', |
| botUrl: 'http://build.chromium.org/p/tryserver.chromium.linux/builders/' + |
| 'android_coverage' |
| }, |
| 'iOS': { |
| prefix: 'https://uberchromegw.corp.google.com/i/internal.bling.tryserver/' + |
| 'builders/coverage/builds/', |
| suffix: '/steps/coverage/logs/json.output', |
| botUrl: 'https://uberchromegw.corp.google.com/i/internal.bling.tryserver/' + |
| 'builders/coverage' |
| } |
| }; |
| |
| /** |
| * URLs where Rietveld apps are served. URLs should be escaped properly so that |
| * they are ready to be used in regular expressions. |
| * |
| * @type {Array.<string>} |
| */ |
| coverage.CONFIG.CODE_REVIEW_URLS = [ |
| 'https:\\/\\/codereview\\.chromium\\.org', |
| 'https:\\/\\/chromereviews\\.googleplex\\.com' |
| ]; |
| |
| /** |
| * String representing absolute coverage. |
| * |
| * @type {string} |
| * @const |
| */ |
| coverage.ABSOLUTE_COVERAGE = 'absolute'; |
| |
| /** |
| * String representing incremental coverage. |
| * |
| * @type {string} |
| * @const |
| */ |
| coverage.INCREMENTAL_COVERAGE = 'incremental'; |
| |
| /** |
| * String representing patch incremental coverage. |
| * |
| * @type {string} |
| * @const |
| */ |
| coverage.PATCH_COVERAGE = 'patch'; |
| |
| /** |
| * Fetches detailed coverage stats for a given patch set and injects them into |
| * the code review page. |
| * |
| * @param {Element} patchElement Div containing a single patch set. |
| * @param {string} botUrl Location of the detailed coverage bot results. |
| * @param {string} projectName The name of project to which code was submitted. |
| */ |
| coverage.injectCoverageStats = function(patchElement, botUrl, projectName) { |
| var buildNumber = botUrl.split('/').pop(); |
| var patch = new coverage.PatchSet(projectName, buildNumber); |
| patch.getCoverageData(function(patchStats) { |
| coverage.updateUi(patchStats, patchElement, patch.getCoverageReportUrl()); |
| }); |
| }; |
| |
| /** |
| * Adds coverage stats to the table containing files changed for a given patch. |
| * |
| * @param {Object} patchStats Object containing stats for a given patch set. |
| * @param {Element} patchElement Div containing a patch single set. |
| * @param {string} reportUrl Location of the detailed coverage stats for this |
| * patch. |
| */ |
| coverage.updateUi = function(patchStats, patchElement, reportUrl) { |
| // Add absolute and incremental coverage column headers. |
| var patchSetTableBody = patchElement.getElementsByTagName('tbody')[0]; |
| var headerRow = patchSetTableBody.firstElementChild; |
| coverage.appendElementBeforeChild(headerRow, 'th', 'ΔCov.', 1); |
| coverage.appendElementBeforeChild(headerRow, 'th', '|Cov.|', 1); |
| |
| // Add absolute and incremental coverage stats for each file. |
| var fileRows = patchElement.querySelectorAll('[name=patch]'); |
| for (var i = 0; i < fileRows.length; i++) { |
| var sourceFileRow = fileRows[i]; |
| var fileName = sourceFileRow.children[2].textContent.trim(); |
| |
| var incrementalPercent = null; |
| var absolutePercent = null; |
| if (patchStats[fileName]) { |
| incrementalPercent = patchStats[fileName][coverage.INCREMENTAL_COVERAGE]; |
| absolutePercent = patchStats[fileName][coverage.ABSOLUTE_COVERAGE]; |
| } |
| |
| coverage.appendElementBeforeChild( |
| sourceFileRow, 'td', coverage.formatPercent(incrementalPercent), 2); |
| |
| coverage.appendElementBeforeChild( |
| sourceFileRow, 'td', coverage.formatPercent(absolutePercent), 2); |
| } |
| // Add the overall coverage stats for the patch. |
| coverage.addPatchSummaryStats( |
| patchElement, patchStats[coverage.PATCH_COVERAGE], reportUrl); |
| }; |
| |
| /** |
| * Formats percent for presentation on the page. |
| * |
| * @param {number} coveragePercent |
| * @return {string} Formatted string ready to be added to the the DOM. |
| */ |
| coverage.formatPercent = function(coveragePercent) { |
| if (!coveragePercent) { |
| return '-'; |
| } else { |
| return coveragePercent + '%'; |
| } |
| }; |
| |
| /** |
| * Adds summary line to a patch element: "Cov. for this patch: 45%. Details". |
| * |
| * @param {Element} patchElement Div containing a patch single patch set. |
| * @param {number} coveragePercent Incremental coverage for entire patch. |
| * @param {string} coverageReportUrl Location of detailed coverage report. |
| */ |
| coverage.addPatchSummaryStats = function( |
| patchElement, coveragePercent, coverageReportUrl) { |
| var summaryElement = document.createElement('div'); |
| var patchSummaryHtml = 'ΔCov. for this patch: ' + |
| coverage.formatPercent(coveragePercent) + '. '; |
| var detailsHtml = '<a href="' + coverageReportUrl + '">Details</a>'; |
| summaryElement.innerHTML = patchSummaryHtml + ' ' + detailsHtml; |
| |
| // Insert the summary line immediately after the table containing the changed |
| // files for the patch. |
| var tableElement = patchElement.getElementsByTagName('table')[0]; |
| tableElement.parentNode.insertBefore( |
| summaryElement, tableElement.nextSibling); |
| }; |
| |
| /** |
| * Creates and prepends an element before another. |
| * |
| * @param {Element} parentElement The parent of the element to prepend a new |
| * element to. |
| * @param {string} elementType The tag name for the new element. |
| * @param {string} innerHtml The value to set as the new element's innerHTML |
| * @param {number} childNumber The index of the child to prepend to. |
| */ |
| coverage.appendElementBeforeChild = function( |
| parentElement, elementType, innerHtml, childNumber) { |
| var newElement = document.createElement(elementType); |
| newElement.innerHTML = innerHtml; |
| parentElement.insertBefore(newElement, parentElement.children[childNumber]); |
| }; |
| |
| /** |
| * Checks if the given URL has been registered or not. |
| * |
| * @param {string} botUrl The URL to be verified. |
| * @return {boolean} Whether or not the provided URL was valid. |
| */ |
| coverage.isValidBotUrl = function(botUrl) { |
| if (!botUrl) { |
| return false; |
| } |
| for (var project in coverage.CONFIG.COVERAGE_REPORT_URLS) { |
| var candidateUrl = coverage.CONFIG.COVERAGE_REPORT_URLS[project]['botUrl']; |
| if (botUrl.indexOf(candidateUrl) > - 1) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| /** |
| * Returns the project name for the given bot URL. This function expects the bot |
| * URL to be valid. |
| * |
| * @param {botUrl} botUrl |
| * @return {string} The project name for the given bot URL. |
| * @throws {Error} If an invalid bot URL is supplied. |
| */ |
| coverage.getProjectNameFromBotUrl = function(botUrl) { |
| if (!botUrl) { |
| throw Error(botUrl + ' is an invalid bot url.'); |
| } |
| for (var project in coverage.CONFIG.COVERAGE_REPORT_URLS) { |
| var candidateUrl = coverage.CONFIG.COVERAGE_REPORT_URLS[project]['botUrl']; |
| if (botUrl.indexOf(candidateUrl) > - 1) { |
| return project; |
| } |
| } |
| throw Error(botUrl + ' is not registered.'); |
| }; |
| |
| |
| /** |
| * Finds the coverage bot URL. |
| * |
| * @param {Element} patchElement Div to search for bot URL. |
| * @return {string} Returns the URL to the bot details page. |
| */ |
| coverage.getValidBotUrl = function(patchElement) { |
| var bots = patchElement.getElementsByClassName('build-result'); |
| for (var i = 0; i < bots.length; i++) { |
| if (bots[i].getAttribute('status') === 'success' && |
| coverage.isValidBotUrl(bots[i].href)) { |
| return bots[i].href; |
| } |
| } |
| return null; |
| }; |
| |
| /** |
| * Checks to see if the URL points to a CL review and not another page on the |
| * code review site (i.e. settings). |
| * |
| * @param {string} url The URL to verify. |
| * @return {boolean} Whether or not the URL points to a CL review. |
| */ |
| coverage.isValidReviewUrl = function(url) { |
| baseUrls = coverage.CONFIG.CODE_REVIEW_URLS.join('|'); |
| // Matches baseurl.com/numeric-digits and baseurl.com/numeric-digits/anything |
| var re = new RegExp('(' + baseUrls + ')/[\\d]+(\\/|$)', 'i'); |
| return !!url.match(re); |
| }; |
| |
| /** |
| * Verifies that the user is using the deprecated UI. |
| * |
| * @return {boolean} Whether or not the deprecated UI is being used. |
| */ |
| coverage.isDeprecatedUi = function() { |
| // The tag is present in the new UI only. |
| return document.getElementsByTagName('cr-app').length == 0; |
| }; |
| |
| /** |
| * Returns the newest patch set element. |
| * |
| * @return {Element} The main div for the last patch set. |
| */ |
| coverage.getLastPatchElement = function() { |
| var patchElement = document.querySelectorAll('div[id^="ps-"'); |
| return patchElement[patchElement.length - 1]; |
| }; |
| |
| /** |
| * Model that describes a patch set. |
| * |
| * @param {string} projectName The name of the project. |
| * @param {string} buildNumber The build number for the bot run corresponding to |
| * this patch set. |
| * @constructor |
| */ |
| coverage.PatchSet = function(projectName, buildNumber) { |
| /** |
| * Location of the detailed coverage JSON report. |
| * @type {string} |
| * @private |
| */ |
| this.coverageReportUrl_ = this.getCoverageReportUrl(projectName, buildNumber); |
| }; |
| |
| /** |
| * Returns the coverage report URL. |
| * |
| * @param {string} projectName The name of the project. |
| * @param {string} buildNumber The build number for the bot run corresponding |
| * to this patch set. |
| * @return {string} The URL to the detailed coverage report. |
| */ |
| coverage.PatchSet.prototype.getCoverageReportUrl = function( |
| projectName, buildNumber) { |
| if (!this.coverageReportUrl_) { |
| var reportUrl = coverage.CONFIG.COVERAGE_REPORT_URLS[projectName]; |
| this.coverageReportUrl_ = reportUrl['prefix'] + buildNumber + |
| reportUrl['suffix']; |
| } |
| return this.coverageReportUrl_; |
| }; |
| |
| /** |
| * Returns the detailed coverage report. Caller must handle what happens |
| * when the report is received. No side effects if report isn't sent. |
| * |
| * @param {function} success The callback to be invoked when the report is |
| * received. Invoked with an object mapping file names to |
| * coverage stats as the only argument. |
| */ |
| coverage.PatchSet.prototype.getCoverageData = function(success) { |
| var client = new coverage.HttpClient(); |
| client.get(this.coverageReportUrl_, (function(data) { |
| var resultDict = JSON.parse(data); |
| var coveragePercentages = this.getCoveragePercentForFiles(resultDict); |
| success(coveragePercentages); |
| }).bind(this)); |
| }; |
| |
| /** |
| * Extracts the coverage percent for each file from the coverage report. |
| * |
| * @param {Object} reportDict The detailed coverage report. |
| * @return {Object} An object containing the coverage percent for each file and |
| * the patch coverage percent. |
| */ |
| coverage.PatchSet.prototype.getCoveragePercentForFiles = function(reportDict) { |
| var fileDict = reportDict['files']; |
| var coveragePercentages = {}; |
| |
| for (var fileName in fileDict) { |
| if (fileDict.hasOwnProperty(fileName)) { |
| coveragePercentages[fileName] = {}; |
| var coverageDict = fileDict[fileName]; |
| |
| coveragePercentages[fileName][coverage.ABSOLUTE_COVERAGE] = |
| this.getCoveragePercent(coverageDict, coverage.ABSOLUTE_COVERAGE); |
| |
| coveragePercentages[fileName][coverage.INCREMENTAL_COVERAGE] = |
| this.getCoveragePercent(coverageDict, coverage.INCREMENTAL_COVERAGE); |
| } |
| } |
| coveragePercentages[coverage.PATCH_COVERAGE] = |
| this.getCoveragePercent(reportDict[coverage.PATCH_COVERAGE], |
| coverage.INCREMENTAL_COVERAGE); |
| return coveragePercentages; |
| }; |
| |
| /** |
| * Returns the coverage percent given the number of total and covered lines. |
| * |
| * @param {Object} coverageDict Object containing absolute and incremental |
| * number of lines covered. |
| * @param {string} coverageType Either 'incremental' or 'absolute'. |
| * @return {number} The coverage percent. |
| */ |
| coverage.PatchSet.prototype.getCoveragePercent = function( |
| coverageDict, coverageType) { |
| if (!coverageDict || |
| (coverageType !== coverage.INCREMENTAL_COVERAGE && |
| coverageType !== coverage.ABSOLUTE_COVERAGE) || |
| parseFloat(total) === 0) { |
| return null; |
| } |
| var covered = coverageDict[coverageType]['covered']; |
| var total = coverageDict[coverageType]['total']; |
| return Math.round( |
| (parseFloat(covered) / parseFloat(total)) * 100); |
| }; |
| |
| /** |
| * Model describing a simple HTTP client. Only supports GET requests. |
| */ |
| coverage.HttpClient = function() { |
| }; |
| |
| /** |
| * HTTP GET that only handles successful requests. |
| * |
| * @param {string} url The URL to make a GET request to. |
| * @param {function} success The callback invoked when the request is finished |
| * successfully. Callback is invoked with response text as |
| * the only argument. |
| */ |
| coverage.HttpClient.prototype.get = function(url, success) { |
| // TODO(estevenson): Handle failure when user isn't authenticated. |
| var http = new XMLHttpRequest(); |
| http.onreadystatechange = function() { |
| if (http.readyState === 4 && http.status === 200) { |
| success(http.responseText); |
| } |
| }; |
| |
| http.open('GET', url + '/text', true); |
| http.send(null); |
| }; |
| |
| // Verifies that page might contain a patch set with a valid coverage bot. |
| if (coverage.isDeprecatedUi() && |
| coverage.isValidReviewUrl(window.location.href)) { |
| var patchElement = coverage.getLastPatchElement(); |
| var botUrl = coverage.getValidBotUrl(patchElement); |
| if (botUrl) { |
| var projectName = coverage.getProjectNameFromBotUrl(botUrl); |
| coverage.injectCoverageStats(patchElement, botUrl, projectName); |
| } |
| } |