Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / chrome_extensions / chromium_code_coverage / js / app.js
blob322c309857d8e34bf32ed12cd7467fda06f56383
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.
5 /**
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.
10  */
12  var coverage = coverage || {};
14 /**
15  * Contains all required configuration information.
16  *
17  * @type {Object}
18  * @const
19  */
20 coverage.CONFIG = {};
22 /**
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.
26  *
27  * @type {Object}
28  * @const
29  */
30 coverage.CONFIG.COVERAGE_REPORT_URLS = {
31   'Android': {
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/' +
36             'android_coverage'
37   },
38   'iOS': {
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/' +
43             'builders/coverage'
44   }
47 /**
48  * URLs where Rietveld apps are served. URLs should be escaped properly so that
49  * they are ready to be used in regular expressions.
50  *
51  * @type {Array.<string>}
52  */
53 coverage.CONFIG.CODE_REVIEW_URLS = [
54   'https:\\/\\/codereview\\.chromium\\.org',
55   'https:\\/\\/chromereviews\\.googleplex\\.com'
58 /**
59   * String representing absolute coverage.
60   *
61   * @type {string}
62   * @const
64 coverage.ABSOLUTE_COVERAGE = 'absolute';
66 /**
67   * String representing incremental coverage.
68   *
69   * @type {string}
70   * @const
72 coverage.INCREMENTAL_COVERAGE = 'incremental';
74 /**
75  * String representing patch incremental coverage.
76  *
77  * @type {string}
78  * @const
79  */
80 coverage.PATCH_COVERAGE = 'patch';
82 /**
83  * Fetches detailed coverage stats for a given patch set and injects them into
84  * the code review page.
85  *
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.
89  */
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());
95   });
98 /**
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
104  *                  patch.
105  */
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', '&Delta;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];
124     }
126     coverage.appendElementBeforeChild(
127         sourceFileRow, 'td', coverage.formatPercent(incrementalPercent), 2);
129     coverage.appendElementBeforeChild(
130         sourceFileRow, 'td', coverage.formatPercent(absolutePercent), 2);
131   }
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.
142  */
143 coverage.formatPercent = function(coveragePercent) {
144   if (!coveragePercent) {
145     return '-';
146   } else {
147     return coveragePercent + '%';
148   }
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.
157  */
158 coverage.addPatchSummaryStats = function(
159     patchElement, coveragePercent, coverageReportUrl) {
160   var summaryElement = document.createElement('div');
161   var patchSummaryHtml = '&Delta;Cov. for this patch: ' +
162                          coverage.formatPercent(coveragePercent) + '.&nbsp;';
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
177  *                   element to.
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.
181  */
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.
194  */
195 coverage.isValidBotUrl = function(botUrl) {
196   if (!botUrl) {
197     return false;
198   }
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) {
202       return true;
203     }
204   }
205   return false;
209  * Returns the project name for the given bot URL. This function expects the bot
210  * URL to be valid.
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.
215  */
216 coverage.getProjectNameFromBotUrl = function(botUrl) {
217   if (!botUrl) {
218     throw Error(botUrl + ' is an invalid bot url.');
219   }
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) {
223       return project;
224     }
225   }
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.
235  */
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)) {
241       return bots[i].href;
242     }
243   }
244   return null;
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.
253  */
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.
265  */
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.
275  */
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
286  *                 this patch set.
287  * @constructor
288  */
289 coverage.PatchSet = function(projectName, buildNumber) {
290   /**
291    * Location of the detailed coverage JSON report.
292    * @type {string}
293    * @private
294    */
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
303  *                 to this patch set.
304  * @return {string} The URL to the detailed coverage report.
305  */
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 +
311                               reportUrl['suffix'];
312   }
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.
323  */
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);
330   }).bind(this));
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.
339  */
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);
354     }
355   }
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.
369  */
370 coverage.PatchSet.prototype.getCoveragePercent = function(
371     coverageDict, coverageType) {
372   if (!coverageDict ||
373       (coverageType !== coverage.INCREMENTAL_COVERAGE &&
374        coverageType !== coverage.ABSOLUTE_COVERAGE) ||
375       parseFloat(total) === 0) {
376     return null;
377   }
378   var covered = coverageDict[coverageType]['covered'];
379   var total = coverageDict[coverageType]['total'];
380   return Math.round(
381       (parseFloat(covered) / parseFloat(total)) * 100);
385  * Model describing a simple HTTP client. Only supports GET requests.
386  */
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
396  *                    the only argument.
397  */
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);
404     }
405   };
407   http.open('GET', url + '/text', true);
408   http.send(null);
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);
416   if (botUrl) {
417     var projectName = coverage.getProjectNameFromBotUrl(botUrl);
418     coverage.injectCoverageStats(patchElement, botUrl, projectName);
419   }