1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview Main module for the Chromium Code Coverage extension. This
7 * extension adds incremental and absolute code coverage stats
8 * to the deprecated Rietveld UI. Stats are added inline with
9 * file names as percentage of lines covered.
12 var coverage = coverage || {};
15 * Contains all required configuration information.
23 * URLs necessary for each project. These are necessary because the Rietveld
24 * sites are used by other projects as well, and is is only possible to find
25 * coverage stats for the projects registered here.
30 coverage.CONFIG.COVERAGE_REPORT_URLS = {
32 prefix: 'https://build.chromium.org/p/tryserver.chromium.linux/builders/' +
33 'android_coverage/builds/',
34 suffix: '/steps/Incremental%20coverage%20report/logs/json.output',
35 botUrl: 'http://build.chromium.org/p/tryserver.chromium.linux/builders/' +
39 prefix: 'https://uberchromegw.corp.google.com/i/internal.bling.tryserver/' +
40 'builders/coverage/builds/',
41 suffix: '/steps/coverage/logs/json.output',
42 botUrl: 'https://uberchromegw.corp.google.com/i/internal.bling.tryserver/' +
48 * URLs where Rietveld apps are served. URLs should be escaped properly so that
49 * they are ready to be used in regular expressions.
51 * @type {Array.<string>}
53 coverage.CONFIG.CODE_REVIEW_URLS = [
54 'https:\\/\\/codereview\\.chromium\\.org',
55 'https:\\/\\/chromereviews\\.googleplex\\.com'
59 * String representing absolute coverage.
64 coverage.ABSOLUTE_COVERAGE = 'absolute';
67 * String representing incremental coverage.
72 coverage.INCREMENTAL_COVERAGE = 'incremental';
75 * String representing patch incremental coverage.
80 coverage.PATCH_COVERAGE = 'patch';
83 * Fetches detailed coverage stats for a given patch set and injects them into
84 * the code review page.
86 * @param {Element} patchElement Div containing a single patch set.
87 * @param {string} botUrl Location of the detailed coverage bot results.
88 * @param {string} projectName The name of project to which code was submitted.
90 coverage.injectCoverageStats = function(patchElement, botUrl, projectName) {
91 var buildNumber = botUrl.split('/').pop();
92 var patch = new coverage.PatchSet(projectName, buildNumber);
93 patch.getCoverageData(function(patchStats) {
94 coverage.updateUi(patchStats, patchElement, patch.getCoverageReportUrl());
99 * Adds coverage stats to the table containing files changed for a given patch.
101 * @param {Object} patchStats Object containing stats for a given patch set.
102 * @param {Element} patchElement Div containing a patch single set.
103 * @param {string} reportUrl Location of the detailed coverage stats for this
106 coverage.updateUi = function(patchStats, patchElement, reportUrl) {
107 // Add absolute and incremental coverage column headers.
108 var patchSetTableBody = patchElement.getElementsByTagName('tbody')[0];
109 var headerRow = patchSetTableBody.firstElementChild;
110 coverage.appendElementBeforeChild(headerRow, 'th', 'ΔCov.', 1);
111 coverage.appendElementBeforeChild(headerRow, 'th', '|Cov.|', 1);
113 // Add absolute and incremental coverage stats for each file.
114 var fileRows = patchElement.querySelectorAll('[name=patch]');
115 for (var i = 0; i < fileRows.length; i++) {
116 var sourceFileRow = fileRows[i];
117 var fileName = sourceFileRow.children[2].textContent.trim();
119 var incrementalPercent = null;
120 var absolutePercent = null;
121 if (patchStats[fileName]) {
122 incrementalPercent = patchStats[fileName][coverage.INCREMENTAL_COVERAGE];
123 absolutePercent = patchStats[fileName][coverage.ABSOLUTE_COVERAGE];
126 coverage.appendElementBeforeChild(
127 sourceFileRow, 'td', coverage.formatPercent(incrementalPercent), 2);
129 coverage.appendElementBeforeChild(
130 sourceFileRow, 'td', coverage.formatPercent(absolutePercent), 2);
132 // Add the overall coverage stats for the patch.
133 coverage.addPatchSummaryStats(
134 patchElement, patchStats[coverage.PATCH_COVERAGE], reportUrl);
138 * Formats percent for presentation on the page.
140 * @param {number} coveragePercent
141 * @return {string} Formatted string ready to be added to the the DOM.
143 coverage.formatPercent = function(coveragePercent) {
144 if (!coveragePercent) {
147 return coveragePercent + '%';
152 * Adds summary line to a patch element: "Cov. for this patch: 45%. Details".
154 * @param {Element} patchElement Div containing a patch single patch set.
155 * @param {number} coveragePercent Incremental coverage for entire patch.
156 * @param {string} coverageReportUrl Location of detailed coverage report.
158 coverage.addPatchSummaryStats = function(
159 patchElement, coveragePercent, coverageReportUrl) {
160 var summaryElement = document.createElement('div');
161 var patchSummaryHtml = 'ΔCov. for this patch: ' +
162 coverage.formatPercent(coveragePercent) + '. ';
163 var detailsHtml = '<a href="' + coverageReportUrl + '">Details</a>';
164 summaryElement.innerHTML = patchSummaryHtml + ' ' + detailsHtml;
166 // Insert the summary line immediately after the table containing the changed
167 // files for the patch.
168 var tableElement = patchElement.getElementsByTagName('table')[0];
169 tableElement.parentNode.insertBefore(
170 summaryElement, tableElement.nextSibling);
174 * Creates and prepends an element before another.
176 * @param {Element} parentElement The parent of the element to prepend a new
178 * @param {string} elementType The tag name for the new element.
179 * @param {string} innerHtml The value to set as the new element's innerHTML
180 * @param {number} childNumber The index of the child to prepend to.
182 coverage.appendElementBeforeChild = function(
183 parentElement, elementType, innerHtml, childNumber) {
184 var newElement = document.createElement(elementType);
185 newElement.innerHTML = innerHtml;
186 parentElement.insertBefore(newElement, parentElement.children[childNumber]);
190 * Checks if the given URL has been registered or not.
192 * @param {string} botUrl The URL to be verified.
193 * @return {boolean} Whether or not the provided URL was valid.
195 coverage.isValidBotUrl = function(botUrl) {
199 for (var project in coverage.CONFIG.COVERAGE_REPORT_URLS) {
200 var candidateUrl = coverage.CONFIG.COVERAGE_REPORT_URLS[project]['botUrl'];
201 if (botUrl.indexOf(candidateUrl) > - 1) {
209 * Returns the project name for the given bot URL. This function expects the bot
212 * @param {botUrl} botUrl
213 * @return {string} The project name for the given bot URL.
214 * @throws {Error} If an invalid bot URL is supplied.
216 coverage.getProjectNameFromBotUrl = function(botUrl) {
218 throw Error(botUrl + ' is an invalid bot url.');
220 for (var project in coverage.CONFIG.COVERAGE_REPORT_URLS) {
221 var candidateUrl = coverage.CONFIG.COVERAGE_REPORT_URLS[project]['botUrl'];
222 if (botUrl.indexOf(candidateUrl) > - 1) {
226 throw Error(botUrl + ' is not registered.');
231 * Finds the coverage bot URL.
233 * @param {Element} patchElement Div to search for bot URL.
234 * @return {string} Returns the URL to the bot details page.
236 coverage.getValidBotUrl = function(patchElement) {
237 var bots = patchElement.getElementsByClassName('build-result');
238 for (var i = 0; i < bots.length; i++) {
239 if (bots[i].getAttribute('status') === 'success' &&
240 coverage.isValidBotUrl(bots[i].href)) {
248 * Checks to see if the URL points to a CL review and not another page on the
249 * code review site (i.e. settings).
251 * @param {string} url The URL to verify.
252 * @return {boolean} Whether or not the URL points to a CL review.
254 coverage.isValidReviewUrl = function(url) {
255 baseUrls = coverage.CONFIG.CODE_REVIEW_URLS.join('|');
256 // Matches baseurl.com/numeric-digits and baseurl.com/numeric-digits/anything
257 var re = new RegExp('(' + baseUrls + ')/[\\d]+(\\/|$)', 'i');
258 return !!url.match(re);
262 * Verifies that the user is using the deprecated UI.
264 * @return {boolean} Whether or not the deprecated UI is being used.
266 coverage.isDeprecatedUi = function() {
267 // The tag is present in the new UI only.
268 return document.getElementsByTagName('cr-app').length == 0;
272 * Returns the newest patch set element.
274 * @return {Element} The main div for the last patch set.
276 coverage.getLastPatchElement = function() {
277 var patchElement = document.querySelectorAll('div[id^="ps-"');
278 return patchElement[patchElement.length - 1];
282 * Model that describes a patch set.
284 * @param {string} projectName The name of the project.
285 * @param {string} buildNumber The build number for the bot run corresponding to
289 coverage.PatchSet = function(projectName, buildNumber) {
291 * Location of the detailed coverage JSON report.
295 this.coverageReportUrl_ = this.getCoverageReportUrl(projectName, buildNumber);
299 * Returns the coverage report URL.
301 * @param {string} projectName The name of the project.
302 * @param {string} buildNumber The build number for the bot run corresponding
304 * @return {string} The URL to the detailed coverage report.
306 coverage.PatchSet.prototype.getCoverageReportUrl = function(
307 projectName, buildNumber) {
308 if (!this.coverageReportUrl_) {
309 var reportUrl = coverage.CONFIG.COVERAGE_REPORT_URLS[projectName];
310 this.coverageReportUrl_ = reportUrl['prefix'] + buildNumber +
313 return this.coverageReportUrl_;
317 * Returns the detailed coverage report. Caller must handle what happens
318 * when the report is received. No side effects if report isn't sent.
320 * @param {function} success The callback to be invoked when the report is
321 * received. Invoked with an object mapping file names to
322 * coverage stats as the only argument.
324 coverage.PatchSet.prototype.getCoverageData = function(success) {
325 var client = new coverage.HttpClient();
326 client.get(this.coverageReportUrl_, (function(data) {
327 var resultDict = JSON.parse(data);
328 var coveragePercentages = this.getCoveragePercentForFiles(resultDict);
329 success(coveragePercentages);
334 * Extracts the coverage percent for each file from the coverage report.
336 * @param {Object} reportDict The detailed coverage report.
337 * @return {Object} An object containing the coverage percent for each file and
338 * the patch coverage percent.
340 coverage.PatchSet.prototype.getCoveragePercentForFiles = function(reportDict) {
341 var fileDict = reportDict['files'];
342 var coveragePercentages = {};
344 for (var fileName in fileDict) {
345 if (fileDict.hasOwnProperty(fileName)) {
346 coveragePercentages[fileName] = {};
347 var coverageDict = fileDict[fileName];
349 coveragePercentages[fileName][coverage.ABSOLUTE_COVERAGE] =
350 this.getCoveragePercent(coverageDict, coverage.ABSOLUTE_COVERAGE);
352 coveragePercentages[fileName][coverage.INCREMENTAL_COVERAGE] =
353 this.getCoveragePercent(coverageDict, coverage.INCREMENTAL_COVERAGE);
356 coveragePercentages[coverage.PATCH_COVERAGE] =
357 this.getCoveragePercent(reportDict[coverage.PATCH_COVERAGE],
358 coverage.INCREMENTAL_COVERAGE);
359 return coveragePercentages;
363 * Returns the coverage percent given the number of total and covered lines.
365 * @param {Object} coverageDict Object containing absolute and incremental
366 * number of lines covered.
367 * @param {string} coverageType Either 'incremental' or 'absolute'.
368 * @return {number} The coverage percent.
370 coverage.PatchSet.prototype.getCoveragePercent = function(
371 coverageDict, coverageType) {
373 (coverageType !== coverage.INCREMENTAL_COVERAGE &&
374 coverageType !== coverage.ABSOLUTE_COVERAGE) ||
375 parseFloat(total) === 0) {
378 var covered = coverageDict[coverageType]['covered'];
379 var total = coverageDict[coverageType]['total'];
381 (parseFloat(covered) / parseFloat(total)) * 100);
385 * Model describing a simple HTTP client. Only supports GET requests.
387 coverage.HttpClient = function() {
391 * HTTP GET that only handles successful requests.
393 * @param {string} url The URL to make a GET request to.
394 * @param {function} success The callback invoked when the request is finished
395 * successfully. Callback is invoked with response text as
398 coverage.HttpClient.prototype.get = function(url, success) {
399 // TODO(estevenson): Handle failure when user isn't authenticated.
400 var http = new XMLHttpRequest();
401 http.onreadystatechange = function() {
402 if (http.readyState === 4 && http.status === 200) {
403 success(http.responseText);
407 http.open('GET', url + '/text', true);
411 // Verifies that page might contain a patch set with a valid coverage bot.
412 if (coverage.isDeprecatedUi() &&
413 coverage.isValidReviewUrl(window.location.href)) {
414 var patchElement = coverage.getLastPatchElement();
415 var botUrl = coverage.getValidBotUrl(patchElement);
417 var projectName = coverage.getProjectNameFromBotUrl(botUrl);
418 coverage.injectCoverageStats(patchElement, botUrl, projectName);