mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-28 12:33:35 +01:00
testing: polish attributable coverage API, fix coverage reporting in selfhost (#215559)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IstanbulCoverageContext } from 'istanbul-to-vscode';
|
||||
import * as vscode from 'vscode';
|
||||
import { SourceLocationMapper, SourceMapStore } from './testOutputScanner';
|
||||
import { SearchStrategy, SourceLocationMapper, SourceMapStore } from './testOutputScanner';
|
||||
import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling';
|
||||
|
||||
export const istanbulCoverageContext = new IstanbulCoverageContext();
|
||||
@@ -18,7 +18,7 @@ export const istanbulCoverageContext = new IstanbulCoverageContext();
|
||||
export class PerTestCoverageTracker {
|
||||
private readonly scripts = new Map</* script ID */ string, Script>();
|
||||
|
||||
constructor(private readonly maps: SourceMapStore) {}
|
||||
constructor(private readonly maps: SourceMapStore) { }
|
||||
|
||||
public add(coverage: IScriptCoverage, test?: vscode.TestItem) {
|
||||
const script = this.scripts.get(coverage.scriptId);
|
||||
@@ -71,11 +71,7 @@ class Script {
|
||||
public async report(run: vscode.TestRun) {
|
||||
const mapper = await this.maps.getSourceLocationMapper(this.uri.toString());
|
||||
const originalUri = (await this.maps.getSourceFile(this.uri.toString())) || this.uri;
|
||||
|
||||
run.addCoverage(this.overall.report(originalUri, this.converter, mapper));
|
||||
for (const [test, projection] of this.perItem) {
|
||||
run.addCoverage(projection.report(originalUri, this.converter, mapper, test));
|
||||
}
|
||||
run.addCoverage(this.overall.report(originalUri, this.converter, mapper, this.perItem));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +84,43 @@ class ScriptCoverageTracker {
|
||||
}
|
||||
}
|
||||
|
||||
public *toDetails(
|
||||
uri: vscode.Uri,
|
||||
convert: OffsetToPosition,
|
||||
mapper: SourceLocationMapper | undefined,
|
||||
) {
|
||||
for (const range of this.coverage) {
|
||||
if (range.start === range.end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startCov = convert.toLineColumn(range.start);
|
||||
let start = new vscode.Position(startCov.line, startCov.column);
|
||||
|
||||
const endCov = convert.toLineColumn(range.end);
|
||||
let end = new vscode.Position(endCov.line, endCov.column);
|
||||
if (mapper) {
|
||||
const startMap = mapper(start.line, start.character, SearchStrategy.FirstAfter);
|
||||
const endMap = startMap && mapper(end.line, end.character, SearchStrategy.FirstBefore);
|
||||
if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) {
|
||||
continue;
|
||||
}
|
||||
start = startMap.range.start;
|
||||
end = endMap.range.end;
|
||||
}
|
||||
|
||||
for (let i = start.line; i <= end.line; i++) {
|
||||
yield new vscode.StatementCoverage(
|
||||
range.covered,
|
||||
new vscode.Range(
|
||||
new vscode.Position(i, i === start.line ? start.character : 0),
|
||||
new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the script's coverage for the test run.
|
||||
*
|
||||
@@ -98,53 +131,27 @@ class ScriptCoverageTracker {
|
||||
uri: vscode.Uri,
|
||||
convert: OffsetToPosition,
|
||||
mapper: SourceLocationMapper | undefined,
|
||||
item?: vscode.TestItem
|
||||
items: Map<vscode.TestItem, ScriptCoverageTracker>,
|
||||
): V8CoverageFile {
|
||||
const file = new V8CoverageFile(uri, item);
|
||||
|
||||
for (const range of this.coverage) {
|
||||
if (range.start === range.end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startCov = convert.toLineColumn(range.start);
|
||||
let start = new vscode.Position(startCov.line, startCov.column);
|
||||
|
||||
const endCov = convert.toLineColumn(range.end);
|
||||
let end = new vscode.Position(endCov.line, endCov.column);
|
||||
if (mapper) {
|
||||
const startMap = mapper(start.line, start.character);
|
||||
const endMap = startMap && mapper(end.line, end.character);
|
||||
if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) {
|
||||
continue;
|
||||
}
|
||||
start = startMap.range.start;
|
||||
end = endMap.range.end;
|
||||
}
|
||||
|
||||
for (let i = start.line; i <= end.line; i++) {
|
||||
file.add(
|
||||
new vscode.StatementCoverage(
|
||||
range.covered,
|
||||
new vscode.Range(
|
||||
new vscode.Position(i, i === start.line ? start.character : 0),
|
||||
new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
const file = new V8CoverageFile(uri, items, convert, mapper);
|
||||
for (const detail of this.toDetails(uri, convert, mapper)) {
|
||||
file.add(detail);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
export class V8CoverageFile extends vscode.FileCoverage {
|
||||
export class V8CoverageFile extends vscode.FileCoverage2 {
|
||||
public details: vscode.StatementCoverage[] = [];
|
||||
|
||||
constructor(uri: vscode.Uri, item?: vscode.TestItem) {
|
||||
super(uri, { covered: 0, total: 0 });
|
||||
(this as vscode.FileCoverage2).testItem = item;
|
||||
constructor(
|
||||
uri: vscode.Uri,
|
||||
private readonly perTest: Map<vscode.TestItem, ScriptCoverageTracker>,
|
||||
private readonly convert: OffsetToPosition,
|
||||
private readonly mapper: SourceLocationMapper | undefined,
|
||||
) {
|
||||
super(uri, { covered: 0, total: 0 }, undefined, undefined, [...perTest.keys()]);
|
||||
}
|
||||
|
||||
public add(detail: vscode.StatementCoverage) {
|
||||
@@ -154,4 +161,9 @@ export class V8CoverageFile extends vscode.FileCoverage {
|
||||
this.statementCoverage.covered++;
|
||||
}
|
||||
}
|
||||
|
||||
public testDetails(test: vscode.TestItem): vscode.FileCoverageDetail[] {
|
||||
const t = this.perTest.get(test);
|
||||
return t ? [...t.toDetails(this.uri, this.convert, this.mapper)] : [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,9 +196,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
true
|
||||
);
|
||||
|
||||
coverage.loadDetailedCoverage = async (_run, coverage) => {
|
||||
(coverage as vscode.TestRunProfile2).loadDetailedCoverage = async (_run, coverage, _token, test) => {
|
||||
if (coverage instanceof V8CoverageFile) {
|
||||
return coverage.details;
|
||||
return test ? coverage.testDetails(test) : coverage.details;
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -424,36 +424,62 @@ const tryMakeMarkdown = (message: string) => {
|
||||
const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m;
|
||||
const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const;
|
||||
|
||||
export type SourceLocationMapper = (line: number, col: number) => vscode.Location | undefined;
|
||||
export const enum SearchStrategy {
|
||||
FirstBefore = -1,
|
||||
FirstAfter = 1,
|
||||
}
|
||||
|
||||
export type SourceLocationMapper = (line: number, col: number, strategy: SearchStrategy) => vscode.Location | undefined;
|
||||
|
||||
export class SourceMapStore {
|
||||
private readonly cache = new Map</* file uri */ string, Promise<TraceMap | undefined>>();
|
||||
|
||||
async getSourceLocationMapper(fileUri: string) {
|
||||
async getSourceLocationMapper(fileUri: string): Promise<SourceLocationMapper> {
|
||||
const sourceMap = await this.loadSourceMap(fileUri);
|
||||
return (line: number, col: number) => {
|
||||
return (line, col, strategy) => {
|
||||
if (!sourceMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let smLine = line + 1;
|
||||
// 1. Look for the ideal position on this line if it exists
|
||||
const idealPosition = originalPositionFor(sourceMap, { column: col, line: line + 1, bias: SearchStrategy.FirstAfter ? GREATEST_LOWER_BOUND : LEAST_UPPER_BOUND });
|
||||
if (idealPosition.line !== null && idealPosition.column !== null && idealPosition.source !== null) {
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, idealPosition.source),
|
||||
new vscode.Position(idealPosition.line - 1, idealPosition.column)
|
||||
);
|
||||
}
|
||||
|
||||
// if the range is after the end of mappings, adjust it to the last mapped line
|
||||
// Otherwise get the first/last valid mapping on another line.
|
||||
const decoded = decodedMappings(sourceMap);
|
||||
if (decoded.length <= line) {
|
||||
smLine = decoded.length; // base 1, no -1 needed
|
||||
col = Number.MAX_SAFE_INTEGER;
|
||||
const enum MapField {
|
||||
COLUMN = 0,
|
||||
SOURCES_INDEX = 1,
|
||||
SOURCE_LINE = 2,
|
||||
SOURCE_COLUMN = 3,
|
||||
}
|
||||
|
||||
for (const bias of sourceMapBiases) {
|
||||
const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias });
|
||||
if (position.line !== null && position.column !== null && position.source !== null) {
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, position.source),
|
||||
new vscode.Position(position.line - 1, position.column)
|
||||
);
|
||||
do {
|
||||
line += strategy;
|
||||
const segments = decoded[line];
|
||||
if (!segments?.length) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const index = strategy === SearchStrategy.FirstBefore
|
||||
? findLastIndex(segments, s => s.length !== 1)
|
||||
: segments.findIndex(s => s.length !== 1);
|
||||
const segment = segments[index];
|
||||
|
||||
if (!segment || segment.length === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, sourceMap.sources[segment[MapField.SOURCES_INDEX]]!),
|
||||
new vscode.Position(segment[MapField.SOURCE_LINE] - 1, segment[MapField.SOURCE_COLUMN])
|
||||
);
|
||||
} while (strategy === SearchStrategy.FirstBefore ? line > 0 : line < decoded.length);
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -461,7 +487,31 @@ export class SourceMapStore {
|
||||
|
||||
/** Gets an original location from a base 0 line and column */
|
||||
async getSourceLocation(fileUri: string, line: number, col = 0) {
|
||||
return this.getSourceLocationMapper(fileUri).then(m => m(line, col));
|
||||
const sourceMap = await this.loadSourceMap(fileUri);
|
||||
if (!sourceMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let smLine = line + 1;
|
||||
|
||||
// if the range is after the end of mappings, adjust it to the last mapped line
|
||||
const decoded = decodedMappings(sourceMap);
|
||||
if (decoded.length <= line) {
|
||||
smLine = decoded.length; // base 1, no -1 needed
|
||||
col = Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
for (const bias of sourceMapBiases) {
|
||||
const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias });
|
||||
if (position.line !== null && position.column !== null && position.source !== null) {
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, position.source),
|
||||
new vscode.Position(position.line - 1, position.column)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getSourceFile(compiledUri: string) {
|
||||
@@ -602,3 +652,13 @@ async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArr
|
||||
const [, fileUri, line, col] = parts;
|
||||
return store.getSourceLocation(fileUri, Number(line) - 1, Number(col));
|
||||
}
|
||||
|
||||
function findLastIndex<T>(arr: T[], predicate: (value: T) => boolean) {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (predicate(arr[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
Reference in New Issue
Block a user