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,