blob: 2e1cb937aae00e94b7f1e74840261f5ecb7d1822 [file]
<!--
Copyright 2018 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.
-->
<dom-module id='chromium-coverage'>
<script>
(function() {
'use strict'
let coveragePlugin;
// Used to store data related to coverage.
let coverageData = {
// Used to store the fetched and processed uncovered lines.
uncoveredLines: null,
// Used to track the current setTimeout() call to fetch coverage data.
updateTimeoutId: null,
// Change info.
host: null,
project: null,
change: null,
patchset: null,
};
// Url of the service from which to fetch coverage data.
const REQUEST_BASE_URL =
'https://findit-for-me.appspot.com/coverage/api/coverage-data';
// Used to fetch coverage data asynchronously with exponential backoff.
const START_SLEEP_TIME_MS = 0.2 * 1000;
const MAX_SLEEP_TIME_MS = 5 * 60 * 1000;
// Used to identify host that is running on canary. This is needed because
// even though the host name starts with 'canary-', when constructing the
// url to make request to fetch coverage data, the 'host' parameter in the
// query is expected to not contain the 'canary-' prefix.
const CANARY_HOST_PREFIX = 'canary-';
// Regex used to match the URL path of a Gerrit change view or diff view.
// Project, change number and patchset number (optional) are required to
// present in order to match. The URL can be in any of the following forms:
// '/c/chromium/src/+/1369646'
// '/c/chromium/src/+/1369646/'
// '/c/chromium/src/+/1369646/3'
// '/c/chromium/src/+/1369646/3/'
// '/c/chromium/src/+/1369646/3/base/base/test.cc'
// '/c/chromium/src/+/1369646/3/base/base/test.cc/'
// Where the patchset number can also be replaced with '3..4'.
const GERRIT_URL_PATTERN =
/^\/c\/(.+)\/\+\/(\d+)\/?(?:(?:\d+\.\.)?(\d+)\/?)?.*$/;
/**
* Gets and fills infos of the current change such as change number.
*
* This function is no-op if the current page is neither a change view page
* nor a diff view page.
*/
async function getAndFillChangeInfo() {
coverageData.host = window.location.host;
if (coverageData.host.startsWith(CANARY_HOST_PREFIX)) {
coverageData.host =
coverageData.host.substring(CANARY_HOST_PREFIX.length);
}
const match = GERRIT_URL_PATTERN.exec(window.location.pathname);
if (!match) {
console.log('Fail to parse change info from url for code coverage.');
return;
}
coverageData.project = match[1];
coverageData.change = match[2];
if (match[3]) {
coverageData.patchset = match[3];
return;
}
// Current page is the change view instead of diff view, so get patchset
// number via an endpoint request.
const url = '/changes/' + encodeURIComponent(coverageData.project) + '~' +
coverageData.change +
'/detail?o=CURRENT_REVISION&o=ALL_REVISIONS';
coveragePlugin.restApi().get(url).then((change) => {
const currentRevision = change.current_revision;
coverageData.patchset = change.revisions[currentRevision]._number;
}).catch((err) => {
console.log('Failed to get change details for code coverage:', err);
});
}
/**
* The format of the response fetched from the server is the same as the
* following example, and the format is flexible enough to be extended to
* contain additional information such as branch coverage, and how many
* times each line is covered on different platforms.
*
* {
* "data": {
* "files": [
* {
* "path": "base/task/task_scheduler/priority_queue_unittest.cc",
* "lines": [
* {
* "line": 168,
* "count": 10
* },
* {
* "line": 170,
* "count": 0
* },
* {
* "line": 171,
* "count": 0
* }
* ]
* }
* ]
* }
* }
*
* @return {Promise} Resolves to parsed JSON response body if the coverage
* data is successfully retrieved, otherwise, resolves to null.
*/
async function fetchCoverageData() {
if (!coverageData.host || !coverageData.project ||
!coverageData.change || !coverageData.patchset) {
throw 'Need retry, change info is unavailable yet for code coverage.';
}
const requestQuery =
`?host=${coverageData.host}&project=${coverageData.project}&` +
`change=${coverageData.change}&patchset=${coverageData.patchset}&` +
`format=json&concise=1`;
const url = REQUEST_BASE_URL + requestQuery;
const response = await fetch(url);
const responseJson = await response.json();
if (responseJson.error) {
console.error('Parse code coverage response body to JSON returned ' +
'error: ', responseJson.error);
return null;
}
if (response.status == 404 &&
responseJson.is_project_supported == false) {
console.warn(coverageData.project,
' project is not supported for code coverage.');
return null;
}
if (response.status == 500 &&
responseJson.is_service_enabled == false) {
console.warn('Code coverage service is temporarily disabled.');
return null;
}
if (!response.ok) {
throw `Need retry, request code coverage data returned http ` +
`${response.status}.`;
}
return responseJson;
}
/**
* Parses and validates the JSON response.
*
* @param {Object} responseJson The JSON response of coverage data.
* @return {Object} Returns information of the uncovered lines if the JSON
* response has valid format, otherwise, returns null.
*/
function parseAndValidateCoverageData(responseJson) {
if (!responseJson.data) {
console.error('Invalid code coverage response format. ' +
'Expecting "data" in ', responseJson);
return;
}
const responseData = responseJson.data;
if (!responseData.files) {
console.error('Invalid code coverage response format. ' +
'Expecting "files" in ', responseData);
return;
}
const responseFiles = responseData.files;
const uncoveredLines = {};
for (var i = 0; i < responseFiles.length; i++) {
const responseFile = responseFiles[i];
if (!responseFile.path || !responseFile.lines) {
console.error('Invalid code coverage response format. ' +
'Expecting "path" and "lines" in ', responseFile);
return;
}
uncoveredLines[responseFile.path] = new Set();
const responseLines = responseFile.lines
for (var j = 0; j < responseLines.length; j++) {
const responseLine = responseLines[j];
if (!responseLine.line || responseLine.count == null) {
console.error('Invalid code coverage response format. ' +
'Expecting "line" and "count" in ', responseLine);
return;
}
if (responseLine.count == 0) {
uncoveredLines[responseFile.path].add(responseLine.line);
}
}
}
return uncoveredLines;
}
/**
* Repeatedly fetch coverage data in an exponential backoff manner.
*
* @param {number} sleepTime Number of milliseconds to wait before next try.
*/
async function checkAndAnnotateCoverageData(sleepTime) {
let responseJson = null;
try {
responseJson = await fetchCoverageData();
} catch (err) {
if (Math.random() >= 0.8) {
sleepTime = START_SLEEP_TIME_MS;
} else {
sleepTime = Math.min(MAX_SLEEP_TIME_MS, 2 * sleepTime);
}
coverageData.updateTimeoutId = window.setTimeout(function () {
checkAndAnnotateCoverageData(sleepTime);
},
sleepTime);
console.log(err, `Will try fetching code coverage data again after ` +
`${sleepTime}ms.`);
return;
}
if (!responseJson) {
return;
}
coverageData.uncoveredLines = parseAndValidateCoverageData(responseJson);
if (coverageData.uncoveredLines) {
console.log('Code coverage data is ready to be rendered.');
} else {
console.error('Invalid code coverage response data format.');
}
}
/**
* Reload coverage data.
*
* This method is triggered in the following two scenarios:
* 1. When the user first goes to a change view or diff view page.
* 2. When the user goes from one change/patch to another change/patch.
*/
function reloadCoverageData() {
if (coverageData.updateTimeoutId) {
window.clearTimeout(coverageData.updateTimeoutId);
}
coverageData = {};
getAndFillChangeInfo();
checkAndAnnotateCoverageData(START_SLEEP_TIME_MS);
}
Gerrit.install(function(plugin) {
// Css class for coverage annotations.
const ANNOTATION_CSS = Gerrit.css('background-color: #EF9B9B');
coveragePlugin = plugin;
let annotationApi = coveragePlugin.annotationApi();
annotationApi.addLayer(context => {
if (!coverageData.host || !coverageData.project ||
!coverageData.change || !coverageData.patchset) {
return;
}
if (coverageData.change != context.changeNum ||
coverageData.patchset != context.patchNum) {
// When switching from one patchset to another one, the annotation
// layers of both the old and new patchsets are notified, so this
// special condition is required to avoid the annotations of the old
// patchset being incorrectly applied to the new patchset.
return;
}
if (!coverageData.uncoveredLines) {
return;
}
const path = context.path;
const line = context.line;
if (coverageData.uncoveredLines[path] &&
coverageData.uncoveredLines[path].has(line.afterNumber)) {
context.annotateRange(0, line.text.length, ANNOTATION_CSS, 'right');
}
});
reloadCoverageData();
coveragePlugin.on('showchange', reloadCoverageData);
});
})();
</script>
</dom-module>