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
);