blob: 859a49d8a1ba88777886f0361274c5f52f88bab8 [file] [log] [blame]
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Side} from '../../../api/diff';
import {
CoverageRange,
CoverageType,
DiffLayer,
DiffLayerListener,
} from '../../../types/types';
const TOOLTIP_MAP = new Map([
[CoverageType.COVERED, 'Covered by tests.'],
[CoverageType.NOT_COVERED, 'Not covered by tests.'],
[CoverageType.PARTIALLY_COVERED, 'Partially covered by tests.'],
[CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
]);
// Ranges are considered half-open: [start, end)
export type Range = {start: number; end: number};
export function mergeRanges(ranges: Range[]): Range[] {
ranges.sort((a, b) => a.start - b.start);
if (ranges.length <= 1) {
return ranges;
}
const stack: Range[] = [];
stack.push(ranges[0]);
for (let j = 1; j < ranges.length; j++) {
const interval = ranges[j];
const top = stack[stack.length - 1];
if (top.end < interval.start) {
stack.push(interval);
} else if (top.end < interval.end) {
top.end = interval.end;
}
}
return stack;
}
export class GrCoverageLayer implements DiffLayer {
/**
* Must be sorted by code_range.start_line.
* Must only contain ranges that match the side.
*/
private coverageRanges: CoverageRange[] = [];
/**
* 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.
*/
private lastLineNumber = 0;
/**
* See `lastLineNumber` comment.
*/
private index = 0;
/**
* Has any line been annotated already in the lifetime of this layer?
* If not, then `setRanges()` does not have to call `notify()` and thus
* trigger re-rendering of the affected diff rows.
*/
// visible for testing
annotated = false;
private listeners: DiffLayerListener[] = [];
constructor(private readonly side: Side) {}
addListener(listener: DiffLayerListener) {
this.listeners.push(listener);
}
removeListener(listener: DiffLayerListener) {
this.listeners = this.listeners.filter(f => f !== listener);
}
/**
* Must be sorted by code_range.start_line.
* Must only contain ranges that match the side.
*/
setRanges(ranges: CoverageRange[]) {
const oldRanges = this.coverageRanges;
if (oldRanges.length === 0 && ranges.length === 0) return;
this.coverageRanges = ranges;
// If ranges are set before any diff row was rendered, then great, no need
// to notify and re-render.
if (this.annotated) this.notify([...oldRanges, ...ranges]);
}
/**
* Notify listeners (should be just gr-diff triggering a re-render).
*
* We are optimizing the notification calls by converting the coverange ranges
* to an array of [start, end) ranges and then merging them to non-overlapping
* set of ranges.
*/
private notify(ranges: CoverageRange[]) {
const notifyRanges = mergeRanges(
ranges.map(r => {
return {start: r.code_range.start_line, end: r.code_range.end_line + 1};
})
);
for (const r of notifyRanges) {
for (const l of this.listeners) l(r.start, r.end - 1, this.side);
}
}
/**
* Layer method to add annotations to a line.
*
* @param _el Not used for this layer. (unused parameter)
* @param lineNumberEl The <td> element with the line number.
* @param line Not used for this layer.
*/
annotate(_el: HTMLElement, lineNumberEl: HTMLElement) {
if (
!this.side ||
!lineNumberEl ||
!lineNumberEl.classList.contains(this.side)
) {
return;
}
let elementLineNumber;
const dataValue = lineNumberEl.getAttribute('data-value');
if (dataValue) {
elementLineNumber = Number(dataValue);
}
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.lastLineNumber) {
this.index = 0;
}
this.lastLineNumber = elementLineNumber;
this.annotated = true;
// 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.lastLineNumber > 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.lastLineNumber < coverageRange.code_range.start_line) {
return;
}
// The line number is within the current coverage range. Style it!
lineNumberEl.classList.add(coverageRange.type);
lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type) || '';
return;
}
}
}