Add a coverage layer to gr-diff
Change-Id: Iaa768824a0c32694a9f0dbfe29522b15bb7d7198
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
new file mode 100644
index 0000000..56a6fb9
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.html
@@ -0,0 +1,24 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<dom-module id="gr-coverage-layer">
+ <template>
+ </template>
+ <script src="../gr-diff-highlight/gr-annotation.js"></script>
+ <script src="gr-coverage-layer.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
new file mode 100644
index 0000000..5e7cc26
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+ 'use strict';
+
+ /** @enum {string} */
+ Gerrit.CoverageType = {
+ /**
+ * start_character and end_character of the range will be ignored for this
+ * type.
+ */
+ COVERED: 'COVERED',
+ /**
+ * start_character and end_character of the range will be ignored for this
+ * type.
+ */
+ NOT_COVERED: 'NOT_COVERED',
+ PARTIALLY_COVERED: 'PARTIALLY_COVERED',
+ /**
+ * You don't have to use this. If there is no coverage information for a
+ * range, then it implicitly means NOT_INSTRUMENTED. start_character and
+ * end_character of the range will be ignored for this type.
+ */
+ NOT_INSTRUMENTED: 'NOT_INSTRUMENTED',
+ };
+
+ /**
+ * @typedef {{
+ * side: string,
+ * type: Gerrit.CoverageType,
+ * code_range: Gerrit.Range,
+ * }}
+ */
+ Gerrit.CoverageRange;
+
+ Polymer({
+ is: 'gr-coverage-layer',
+
+ properties: {
+ /**
+ * Must be sorted by code_range.start_line.
+ * Must only contain ranges that match the side.
+ *
+ * @type {!Array<!Gerrit.CoverageRange>}
+ */
+ coverageRanges: Array,
+ side: String,
+
+ /**
+ * We keep track of the line number from the previous annotate() call,
+ * and also of the index of the coverage range that had matched.
+ * annotate() calls are coming in with increasing line numbers and
+ * coverage ranges are sorted by line number. So this is a very simple
+ * and efficient way for finding the coverage range that matches a given
+ * line number.
+ */
+ _lineNumber: {
+ type: Number,
+ value: 0,
+ },
+ _index: {
+ type: Number,
+ value: 0,
+ },
+ },
+
+ /**
+ * Layer method to add annotations to a line.
+ *
+ * @param {!HTMLElement} el Not used for this layer.
+ * @param {!HTMLElement} lineNumberEl The <td> element with the line number.
+ * @param {!Object} line Not used for this layer.
+ */
+ annotate(el, lineNumberEl, line) {
+ if (!lineNumberEl || !lineNumberEl.classList.contains(this.side)) {
+ return;
+ }
+ const elementLineNumber = parseInt(
+ lineNumberEl.getAttribute('data-value'), 10);
+ if (!elementLineNumber || elementLineNumber < 1) return;
+
+ // If the line number is smaller than before, then we have to reset our
+ // algorithm and start searching the coverage ranges from the beginning.
+ // That happens for example when you expand diff sections.
+ if (elementLineNumber < this._lineNumber) {
+ this._index = 0;
+ }
+ this._lineNumber = elementLineNumber;
+
+ // We simply loop through all the coverage ranges until we find one that
+ // matches the line number.
+ while (this._index < this.coverageRanges.length) {
+ const coverageRange = this.coverageRanges[this._index];
+
+ // If the line number has moved past the current coverage range, then
+ // try the next coverage range.
+ if (this._lineNumber > coverageRange.code_range.end_line) {
+ this._index++;
+ continue;
+ }
+
+ // If the line number has not reached the next coverage range (and the
+ // range before also did not match), then this line has not been
+ // instrumented. Nothing to do for this line.
+ if (this._lineNumber < coverageRange.code_range.start_line) {
+ return;
+ }
+
+ // The line number is within the current coverage range. Style it!
+ lineNumberEl.classList.add(coverageRange.type);
+ return;
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
new file mode 100644
index 0000000..edd88a2
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-coverage-layer</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../gr-diff/gr-diff-line.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+
+<link rel="import" href="gr-coverage-layer.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-coverage-layer></gr-coverage-layer>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-coverage-layer', () => {
+ let element;
+
+ setup(() => {
+ const initialCoverageRanges = [
+ {
+ type: 'COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 1,
+ end_line: 2,
+ },
+ },
+ {
+ type: 'NOT_COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 3,
+ end_line: 4,
+ },
+ },
+ {
+ type: 'PARTIALLY_COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 5,
+ end_line: 6,
+ },
+ },
+ {
+ type: 'NOT_INSTRUMENTED',
+ side: 'right',
+ code_range: {
+ start_line: 8,
+ end_line: 9,
+ },
+ },
+ ];
+
+ element = fixture('basic');
+ element.coverageRanges = initialCoverageRanges;
+ element.side = 'right';
+ });
+
+ suite('annotate', () => {
+ function createLine(lineNumber) {
+ lineEl = document.createElement('div');
+ lineEl.setAttribute('data-side', 'right');
+ lineEl.setAttribute('data-value', lineNumber);
+ lineEl.className = 'right';
+ return lineEl;
+ }
+
+ function checkLine(lineNumber, className, opt_negated) {
+ const line = createLine(lineNumber);
+ element.annotate(undefined, line, undefined);
+ let contains = line.classList.contains(className);
+ if (opt_negated) contains = !contains;
+ assert.isTrue(contains);
+ }
+
+ test('line 1-2 are covered', () => {
+ checkLine(1, 'COVERED');
+ checkLine(2, 'COVERED');
+ });
+
+ test('line 3-4 are not covered', () => {
+ checkLine(3, 'NOT_COVERED');
+ checkLine(4, 'NOT_COVERED');
+ });
+
+ test('line 5-6 are partially covered', () => {
+ checkLine(5, 'PARTIALLY_COVERED');
+ checkLine(6, 'PARTIALLY_COVERED');
+ });
+
+ test('line 7 is implicitly not instrumented', () => {
+ checkLine(7, 'COVERED', true);
+ checkLine(7, 'NOT_COVERED', true);
+ checkLine(7, 'PARTIALLY_COVERED', true);
+ checkLine(7, 'NOT_INSTRUMENTED', true);
+ });
+
+ test('line 8-9 are not instrumented', () => {
+ checkLine(8, 'NOT_INSTRUMENTED');
+ checkLine(9, 'NOT_INSTRUMENTED');
+ });
+
+ test('coverage correct, if annotate is called out of order', () => {
+ checkLine(8, 'NOT_INSTRUMENTED');
+ checkLine(1, 'COVERED');
+ checkLine(5, 'PARTIALLY_COVERED');
+ checkLine(3, 'NOT_COVERED');
+ checkLine(6, 'PARTIALLY_COVERED');
+ checkLine(4, 'NOT_COVERED');
+ checkLine(9, 'NOT_INSTRUMENTED');
+ checkLine(2, 'COVERED');
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index b5f21b6..7bfec00 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -16,6 +16,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+<link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html">
<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
@@ -31,6 +32,14 @@
<gr-syntax-layer
id="syntaxLayer"
diff="[[diff]]"></gr-syntax-layer>
+ <gr-coverage-layer
+ id="coverageLayerLeft"
+ coverage-ranges="[[_leftCoverageRanges]]"
+ side="left"></gr-coverage-layer>
+ <gr-coverage-layer
+ id="coverageLayerRight"
+ coverage-ranges="[[_rightCoverageRanges]]"
+ side="right"></gr-coverage-layer>
<gr-diff-processor
id="processor"
groups="{{_groups}}"></gr-diff-processor>
@@ -108,6 +117,19 @@
type: Array,
value: () => [],
},
+ /** @type {!Array<!Gerrit.CoverageRange>} */
+ coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
+ _leftCoverageRanges: {
+ type: Array,
+ computed: '_computeLeftCoverageRanges(coverageRanges)',
+ },
+ _rightCoverageRanges: {
+ type: Array,
+ computed: '_computeRightCoverageRanges(coverageRanges)',
+ },
/**
* The promise last returned from `render()` while the asynchronous
* rendering is running - `null` otherwise. Provides a `cancel()`
@@ -125,6 +147,14 @@
'_groupsChanged(_groups.splices)',
],
+ _computeLeftCoverageRanges(coverageRanges) {
+ return coverageRanges.filter(range => range && range.side === 'left');
+ },
+
+ _computeRightCoverageRanges(coverageRanges) {
+ return coverageRanges.filter(range => range && range.side === 'right');
+ },
+
render(keyLocations, prefs) {
// Setting up annotation layers must happen after plugins are
// installed, and |render| satisfies the requirement, however,
@@ -184,6 +214,8 @@
this._createIntralineLayer(),
this._createTabIndicatorLayer(),
this.$.rangeLayer,
+ this.$.coverageLayerLeft,
+ this.$.coverageLayerRight,
];
// Get layers from plugins (if any).
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 61e8603..8a117af 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -270,6 +270,15 @@
.newlineWarning.hidden {
display: none;
}
+ .lineNum.COVERED {
+ background-color: #E0F2F1;
+ }
+ .lineNum.NOT_COVERED {
+ background-color: #FFD1A4;
+ }
+ .lineNum.PARTIALLY_COVERED {
+ background: linear-gradient(to right bottom, #FFD1A4 0%, #FFD1A4 50%, #E0F2F1 50%, #E0F2F1 100%);
+ }
</style>
<style include="gr-syntax-theme"></style>
<div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
@@ -289,6 +298,7 @@
<gr-diff-builder
id="diffBuilder"
comment-ranges="[[_commentRanges]]"
+ coverage-ranges="[[coverageRanges]]"
project-name="[[projectName]]"
diff="[[diff]]"
diff-path="[[path]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 24f167d..40a7abe 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -167,6 +167,11 @@
type: Array,
value: () => [],
},
+ /** @type {!Array<!Gerrit.CoverageRange>} */
+ coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
lineWrapping: {
type: Boolean,
value: false,