mirror of
https://github.com/Microsoft/vscode
synced 2024-07-05 01:08:57 +00:00
just use v8-to-istanbul
This commit is contained in:
parent
afb251a694
commit
2aa1079dbb
|
@ -69,13 +69,12 @@
|
||||||
"test": "npx mocha --ui tdd 'out/*.test.js'"
|
"test": "npx mocha --ui tdd 'out/*.test.js'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mocha": "^10.0.6",
|
"@types/node": "18.x"
|
||||||
"@types/node": "18.x",
|
|
||||||
"v8-to-istanbul": "^9.2.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"ansi-styles": "^5.2.0",
|
"ansi-styles": "^5.2.0",
|
||||||
"istanbul-to-vscode": "^2.0.1"
|
"istanbul-to-vscode": "^2.0.1",
|
||||||
|
"v8-to-istanbul": "^9.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,121 +3,104 @@
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { TraceMap } from '@jridgewell/trace-mapping';
|
||||||
import { IstanbulCoverageContext } from 'istanbul-to-vscode';
|
import { IstanbulCoverageContext } from 'istanbul-to-vscode';
|
||||||
import { SourceMapStore } from './testOutputScanner';
|
import { fileURLToPath } from 'url';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling';
|
import { SourceMapStore } from './sourceMapStore';
|
||||||
import * as v8ToIstanbul from 'v8-to-istanbul';
|
import v8ToIstanbul = require('v8-to-istanbul');
|
||||||
|
|
||||||
export const istanbulCoverageContext = new IstanbulCoverageContext();
|
export const istanbulCoverageContext = new IstanbulCoverageContext();
|
||||||
|
|
||||||
|
export interface ICoverageRange {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
covered: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IV8FunctionCoverage {
|
||||||
|
functionName: string;
|
||||||
|
isBlockCoverage: boolean;
|
||||||
|
ranges: IV8CoverageRange[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IV8CoverageRange {
|
||||||
|
startOffset: number;
|
||||||
|
endOffset: number;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** V8 Script coverage data */
|
||||||
|
export interface IScriptCoverage {
|
||||||
|
scriptId: string;
|
||||||
|
url: string;
|
||||||
|
// Script source added by the runner the first time the script is emitted.
|
||||||
|
source?: string;
|
||||||
|
functions: IV8FunctionCoverage[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks coverage in per-script coverage mode. There are two modes of coverage
|
* Tracks coverage in per-script coverage mode. There are two modes of coverage
|
||||||
* in this extension: generic istanbul reports, and reports from the runtime
|
* in this extension: generic istanbul reports, and V8 reports from the runtime
|
||||||
* sent before and after each test case executes. This handles the latter.
|
* sent before and after each test case executes. This handles the latter.
|
||||||
*/
|
*/
|
||||||
export class PerTestCoverageTracker {
|
export class PerTestCoverageTracker {
|
||||||
private readonly scripts = new Map</* script ID */ string, Script>();
|
private readonly scripts = new Map</* script ID */ string, Script>();
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly maps: SourceMapStore) { }
|
||||||
private readonly initialCoverage: IScriptCoverage,
|
|
||||||
private readonly maps: SourceMapStore,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public add(coverage: IScriptCoverage, test?: vscode.TestItem) {
|
/** Adds new coverage data to the run, optionally for a test item. */
|
||||||
const script = this.scripts.get(coverage.scriptId);
|
public add(run: vscode.TestRun, coverage: IScriptCoverage, test?: vscode.TestItem) {
|
||||||
if (script) {
|
let script = this.scripts.get(coverage.scriptId);
|
||||||
return script.add(coverage, test);
|
if (!script) {
|
||||||
}
|
if (!coverage.source) {
|
||||||
if (!coverage.source) {
|
throw new Error('expected to have source the first time a script is seen');
|
||||||
throw new Error('expected to have source the first time a script is seen');
|
}
|
||||||
|
|
||||||
|
script = new Script(coverage.url, coverage.source, this.maps);
|
||||||
|
this.scripts.set(coverage.scriptId, script);
|
||||||
}
|
}
|
||||||
|
|
||||||
const src = new Script(coverage.url, coverage.source, this.maps);
|
return script.add(run, coverage, test);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Script {
|
class Script {
|
||||||
private converter: OffsetToPosition;
|
private sourceMap?: Promise<TraceMap | undefined>;
|
||||||
|
private originalContent?: Promise<string | undefined>;
|
||||||
/** Tracking the overall coverage for the file */
|
|
||||||
private overall = new ScriptProjection();
|
|
||||||
/** Range tracking per-test item */
|
|
||||||
private readonly perItem = new Map<vscode.TestItem, ScriptProjection>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly url: string,
|
public readonly url: string,
|
||||||
source: string,
|
private readonly source: string,
|
||||||
private readonly maps: SourceMapStore,
|
private readonly maps: SourceMapStore,
|
||||||
) {
|
) {
|
||||||
this.converter = new OffsetToPosition(source);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(coverage: IScriptCoverage, test?: vscode.TestItem) {
|
public async add(run: vscode.TestRun, coverage: IScriptCoverage, test?: vscode.TestItem) {
|
||||||
this.overall.add(coverage);
|
if (!coverage.url.startsWith('file://')) {
|
||||||
if (test) {
|
return;
|
||||||
const p = new ScriptProjection();
|
|
||||||
p.add(coverage);
|
|
||||||
this.perItem.set(test, p);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public report(run: vscode.TestRun) {
|
const sourceMap = await (this.sourceMap ??= this.maps.loadSourceMap(coverage.url));
|
||||||
|
const originalSource = await (this.originalContent ??= this.maps.getSourceFileContents(coverage.url));
|
||||||
}
|
const istanbuled = v8ToIstanbul(fileURLToPath(coverage.url), undefined, sourceMap && originalSource
|
||||||
}
|
? { source: this.source, originalSource, sourceMap: { sourcemap: sourceMap } }
|
||||||
|
: { source: this.source }
|
||||||
class ScriptProjection {
|
);
|
||||||
/** Range tracking for non-block coverage in the file */
|
|
||||||
private file = new RangeCoverageTracker();
|
|
||||||
/** Range tracking for block coverage in the file */
|
|
||||||
private readonly blocks = new Map<string, RangeCoverageTracker>();
|
|
||||||
|
|
||||||
public add(coverage: IScriptCoverage) {
|
|
||||||
|
|
||||||
for (const fn of coverage.functions) {
|
|
||||||
if (fn.isBlockCoverage) {
|
|
||||||
const key = `${fn.ranges[0].startOffset}/${fn.ranges[0].endOffset}`;
|
|
||||||
const block = this.blocks.get(key);
|
|
||||||
if (block) {
|
|
||||||
for (let i = 1; i < fn.ranges.length; i++) {
|
|
||||||
block.setCovered(fn.ranges[i].startOffset, fn.ranges[i].endOffset, fn.ranges[i].count > 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.blocks.set(key, RangeCoverageTracker.initializeBlock(fn.ranges));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const range of fn.ranges) {
|
|
||||||
this.file.setCovered(range.startOffset, range.endOffset, range.count > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public report(run: vscode.TestRun, convert: OffsetToPosition, item?: vscode.TestItem) {
|
|
||||||
const ranges = [...this.file];
|
|
||||||
for (const block of this.blocks.values()) {
|
|
||||||
for (const range of block) {
|
|
||||||
ranges.push(range);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ri = 0;
|
|
||||||
ranges.sort((a, b) => a.end - b.end);
|
|
||||||
|
|
||||||
let offset = 0;
|
|
||||||
for (let i = 0; i < convert.lines.length; i++) {
|
|
||||||
const lineEnd = offset + convert.lines[i] + 1;
|
|
||||||
|
|
||||||
const coverage = new RangeCoverageTracker();
|
|
||||||
for (let i = ri; i < ranges.length && ranges[i].start < lineEnd; i++) {
|
|
||||||
coverage.setCovered(ranges[i].start - offset, ranges[i].end - offset, ranges[i].covered);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (ri < ranges.length && ranges[ri].end < lineEnd) {
|
|
||||||
ri++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
await istanbuled.load();
|
||||||
|
|
||||||
|
const coverages = await istanbulCoverageContext.fromJson(istanbuled.toIstanbul(), {
|
||||||
|
mapFileUri: uri => this.maps.getSourceFile(uri.toString()),
|
||||||
|
mapLocation: (uri, position) =>
|
||||||
|
this.maps.getSourceLocation(uri.toString(), position.line, position.character),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const coverage of coverages) {
|
||||||
|
if (test) {
|
||||||
|
(coverage as vscode.FileCoverage2).testItem = test;
|
||||||
|
}
|
||||||
|
run.addCoverage(coverage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
102
.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts
vendored
Normal file
102
.vscode/extensions/vscode-selfhost-test-provider/src/sourceMapStore.ts
vendored
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GREATEST_LOWER_BOUND,
|
||||||
|
LEAST_UPPER_BOUND,
|
||||||
|
originalPositionFor,
|
||||||
|
TraceMap
|
||||||
|
} from '@jridgewell/trace-mapping';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { getContentFromFilesystem } from './testTree';
|
||||||
|
|
||||||
|
const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m;
|
||||||
|
const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const;
|
||||||
|
|
||||||
|
export class SourceMapStore {
|
||||||
|
private readonly cache = new Map</* file uri */ string, Promise<TraceMap | undefined>>();
|
||||||
|
|
||||||
|
async getSourceLocation(fileUri: string, line: number, col = 1) {
|
||||||
|
const sourceMap = await this.loadSourceMap(fileUri);
|
||||||
|
if (!sourceMap) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bias of sourceMapBiases) {
|
||||||
|
const position = originalPositionFor(sourceMap, { column: col, line: line + 1, 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) {
|
||||||
|
const sourceMap = await this.loadSourceMap(compiledUri);
|
||||||
|
if (!sourceMap) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceMap.sources[0]) {
|
||||||
|
return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bias of sourceMapBiases) {
|
||||||
|
const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias });
|
||||||
|
if (position.source !== null) {
|
||||||
|
return this.completeSourceMapUrl(sourceMap, position.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSourceFileContents(compiledUri: string) {
|
||||||
|
const sourceUri = await this.getSourceFile(compiledUri);
|
||||||
|
return sourceUri ? getContentFromFilesystem(sourceUri) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private completeSourceMapUrl(sm: TraceMap, source: string) {
|
||||||
|
if (sm.sourceRoot) {
|
||||||
|
try {
|
||||||
|
return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString());
|
||||||
|
} catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vscode.Uri.parse(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadSourceMap(fileUri: string) {
|
||||||
|
const existing = this.cache.get(fileUri);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri));
|
||||||
|
const sourcemapMatch = inlineSourcemapRe.exec(contents);
|
||||||
|
if (!sourcemapMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString();
|
||||||
|
return new TraceMap(decoded, fileUri);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.cache.set(fileUri, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,19 +3,13 @@
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import {
|
|
||||||
GREATEST_LOWER_BOUND,
|
|
||||||
LEAST_UPPER_BOUND,
|
|
||||||
originalPositionFor,
|
|
||||||
TraceMap,
|
|
||||||
} from '@jridgewell/trace-mapping';
|
|
||||||
import * as styles from 'ansi-styles';
|
import * as styles from 'ansi-styles';
|
||||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { IScriptCoverage, istanbulCoverageContext } from './coverageProvider';
|
import { IScriptCoverage, PerTestCoverageTracker, istanbulCoverageContext } from './coverageProvider';
|
||||||
import { attachTestMessageMetadata } from './metadata';
|
import { attachTestMessageMetadata } from './metadata';
|
||||||
import { snapshotComment } from './snapshot';
|
import { snapshotComment } from './snapshot';
|
||||||
import { getContentFromFilesystem } from './testTree';
|
import { SourceMapStore } from './sourceMapStore';
|
||||||
import { StreamSplitter } from './streamSplitter';
|
import { StreamSplitter } from './streamSplitter';
|
||||||
|
|
||||||
export const enum MochaEvent {
|
export const enum MochaEvent {
|
||||||
|
@ -174,6 +168,7 @@ export async function scanTestOutput(
|
||||||
|
|
||||||
let lastTest: vscode.TestItem | undefined;
|
let lastTest: vscode.TestItem | undefined;
|
||||||
let ranAnyTest = false;
|
let ranAnyTest = false;
|
||||||
|
let perTestCoverage: PerTestCoverageTracker | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cancellation.isCancellationRequested) {
|
if (cancellation.isCancellationRequested) {
|
||||||
|
@ -319,6 +314,19 @@ export async function scanTestOutput(
|
||||||
case MochaEvent.End:
|
case MochaEvent.End:
|
||||||
// no-op, we wait until the process exits to ensure coverage is written out
|
// no-op, we wait until the process exits to ensure coverage is written out
|
||||||
break;
|
break;
|
||||||
|
case MochaEvent.CoverageInit:
|
||||||
|
perTestCoverage ??= new PerTestCoverageTracker(store);
|
||||||
|
enqueueExitBlocker(perTestCoverage.add(task, evt[1]));
|
||||||
|
break;
|
||||||
|
case MochaEvent.CoverageIncrement: {
|
||||||
|
const { fullTitle, coverage } = evt[1];
|
||||||
|
const tcase = tests.get(fullTitle);
|
||||||
|
if (tcase) {
|
||||||
|
perTestCoverage ??= new PerTestCoverageTracker(store);
|
||||||
|
enqueueExitBlocker(perTestCoverage.add(task, coverage, tcase));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -400,90 +408,6 @@ const tryMakeMarkdown = (message: string) => {
|
||||||
return new vscode.MarkdownString(lines.join('\n'));
|
return new vscode.MarkdownString(lines.join('\n'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m;
|
|
||||||
const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const;
|
|
||||||
|
|
||||||
export class SourceMapStore {
|
|
||||||
private readonly cache = new Map</* file uri */ string, Promise<TraceMap | undefined>>();
|
|
||||||
|
|
||||||
async getSourceLocation(fileUri: string, line: number, col = 1) {
|
|
||||||
const sourceMap = await this.loadSourceMap(fileUri);
|
|
||||||
if (!sourceMap) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const bias of sourceMapBiases) {
|
|
||||||
const position = originalPositionFor(sourceMap, { column: col, line: line + 1, 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) {
|
|
||||||
const sourceMap = await this.loadSourceMap(compiledUri);
|
|
||||||
if (!sourceMap) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceMap.sources[0]) {
|
|
||||||
return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const bias of sourceMapBiases) {
|
|
||||||
const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias });
|
|
||||||
if (position.source !== null) {
|
|
||||||
return this.completeSourceMapUrl(sourceMap, position.source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private completeSourceMapUrl(sm: TraceMap, source: string) {
|
|
||||||
if (sm.sourceRoot) {
|
|
||||||
try {
|
|
||||||
return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString());
|
|
||||||
} catch {
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vscode.Uri.parse(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadSourceMap(fileUri: string) {
|
|
||||||
const existing = this.cache.get(fileUri);
|
|
||||||
if (existing) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = (async () => {
|
|
||||||
try {
|
|
||||||
const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri));
|
|
||||||
const sourcemapMatch = inlineSourcemapRe.exec(contents);
|
|
||||||
if (!sourcemapMatch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString();
|
|
||||||
return new TraceMap(decoded, fileUri);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
this.cache.set(fileUri, promise);
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g;
|
const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g;
|
||||||
|
|
||||||
async function replaceAllLocations(store: SourceMapStore, str: string) {
|
async function replaceAllLocations(store: SourceMapStore, str: string) {
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
/*---------------------------------------------------------------------------------------------
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
||||||
*--------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
import * as assert from 'assert';
|
|
||||||
import { RangeCoverageTracker } from './v8CoverageWrangling';
|
|
||||||
|
|
||||||
suite('v8CoverageWrangling', () => {
|
|
||||||
suite('RangeCoverageTracker', () => {
|
|
||||||
test('covers new range', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.cover(5, 10);
|
|
||||||
assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('non overlapping ranges', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.cover(5, 10);
|
|
||||||
rt.cover(15, 20);
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 5, end: 10, covered: true },
|
|
||||||
{ start: 15, end: 20, covered: true },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('covers exact', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.uncovered(5, 10);
|
|
||||||
rt.cover(5, 10);
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 5, end: 10, covered: true },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('overlap at start', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.uncovered(5, 10);
|
|
||||||
rt.cover(2, 7);
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 2, end: 5, covered: true },
|
|
||||||
{ start: 5, end: 7, covered: true },
|
|
||||||
{ start: 7, end: 10, covered: false },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('overlap at end', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.cover(5, 10);
|
|
||||||
rt.uncovered(2, 7);
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 2, end: 5, covered: false },
|
|
||||||
{ start: 5, end: 7, covered: true },
|
|
||||||
{ start: 7, end: 10, covered: true },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('inner contained', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.cover(5, 10);
|
|
||||||
rt.uncovered(2, 12);
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 2, end: 5, covered: false },
|
|
||||||
{ start: 5, end: 10, covered: true },
|
|
||||||
{ start: 10, end: 12, covered: false },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('outer contained', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.uncovered(5, 10);
|
|
||||||
rt.cover(7, 9);
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 5, end: 7, covered: false },
|
|
||||||
{ start: 7, end: 9, covered: true },
|
|
||||||
{ start: 9, end: 10, covered: false },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('boundary touching', () => {
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
rt.uncovered(5, 10);
|
|
||||||
rt.cover(10, 15);
|
|
||||||
rt.uncovered(15, 20);
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 5, end: 10, covered: false },
|
|
||||||
{ start: 10, end: 15, covered: true },
|
|
||||||
{ start: 15, end: 20, covered: false },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('initializeBlock', () => {
|
|
||||||
const rt = RangeCoverageTracker.initializeBlock([
|
|
||||||
{ count: 1, startOffset: 5, endOffset: 30 },
|
|
||||||
{ count: 1, startOffset: 8, endOffset: 10 },
|
|
||||||
{ count: 0, startOffset: 15, endOffset: 20 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.deepStrictEqual([...rt], [
|
|
||||||
{ start: 5, end: 15, covered: true },
|
|
||||||
{ start: 15, end: 20, covered: false },
|
|
||||||
{ start: 20, end: 30, covered: true },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,156 +0,0 @@
|
||||||
/*---------------------------------------------------------------------------------------------
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
||||||
*--------------------------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
export interface ICoverageRange {
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
covered: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IV8FunctionCoverage {
|
|
||||||
functionName: string;
|
|
||||||
isBlockCoverage: boolean;
|
|
||||||
ranges: IV8CoverageRange[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IV8CoverageRange {
|
|
||||||
startOffset: number;
|
|
||||||
endOffset: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** V8 Script coverage data */
|
|
||||||
export interface IScriptCoverage {
|
|
||||||
scriptId: string;
|
|
||||||
url: string;
|
|
||||||
// Script source added by the runner the first time the script is emitted.
|
|
||||||
source?: string;
|
|
||||||
functions: IV8FunctionCoverage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class RangeCoverageTracker implements Iterable<ICoverageRange> {
|
|
||||||
/**
|
|
||||||
* A noncontiguous, non-overlapping, ordered set of ranges and whether
|
|
||||||
* that range has been covered.
|
|
||||||
*/
|
|
||||||
private ranges: readonly ICoverageRange[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true.
|
|
||||||
*/
|
|
||||||
public static initializeBlock(ranges: IV8CoverageRange[]) {
|
|
||||||
let start = ranges[0].startOffset;
|
|
||||||
const rt = new RangeCoverageTracker();
|
|
||||||
if (!ranges[0].count) {
|
|
||||||
rt.uncovered(start, ranges[0].endOffset);
|
|
||||||
return rt;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i < ranges.length; i++) {
|
|
||||||
const range = ranges[i];
|
|
||||||
if (range.count) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
rt.cover(start, range.startOffset);
|
|
||||||
rt.uncovered(range.startOffset, range.endOffset);
|
|
||||||
start = range.endOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
rt.cover(start, ranges[0].endOffset);
|
|
||||||
return rt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Marks a range covered */
|
|
||||||
public cover(start: number, end: number) {
|
|
||||||
this.setCovered(start, end, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Marks a range as uncovered */
|
|
||||||
public uncovered(start: number, end: number) {
|
|
||||||
this.setCovered(start, end, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Iterates over coverage ranges */
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
return this.ranges[Symbol.iterator]();
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCovered(start: number, end: number, covered: boolean) {
|
|
||||||
const newRanges: ICoverageRange[] = [];
|
|
||||||
let i = 0;
|
|
||||||
for (; i < this.ranges.length && this.ranges[i].end <= start; i++) {
|
|
||||||
newRanges.push(this.ranges[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
newRanges.push({ start, end, covered });
|
|
||||||
for (; i < this.ranges.length; i++) {
|
|
||||||
const range = this.ranges[i];
|
|
||||||
const last = newRanges[newRanges.length - 1];
|
|
||||||
|
|
||||||
if (range.start < last.start && range.end > last.end) {
|
|
||||||
// range contains last:
|
|
||||||
newRanges.pop();
|
|
||||||
newRanges.push({ start: range.start, end: last.start, covered: range.covered });
|
|
||||||
newRanges.push({ start: last.start, end: last.end, covered: range.covered || last.covered });
|
|
||||||
newRanges.push({ start: last.end, end: range.end, covered: range.covered });
|
|
||||||
} else if (range.start > last.start && range.end <= last.end) {
|
|
||||||
// last contains range:
|
|
||||||
newRanges.pop();
|
|
||||||
newRanges.push({ start: last.start, end: range.start, covered: last.covered });
|
|
||||||
newRanges.push({ start: range.start, end: range.end, covered: range.covered || last.covered });
|
|
||||||
newRanges.push({ start: range.end, end: last.end, covered: last.covered });
|
|
||||||
} else if (range.start < last.start && range.end <= last.end) {
|
|
||||||
// range overlaps start of last:
|
|
||||||
newRanges.pop();
|
|
||||||
newRanges.push({ start: range.start, end: last.start, covered: range.covered });
|
|
||||||
newRanges.push({ start: last.start, end: range.end, covered: range.covered || last.covered });
|
|
||||||
newRanges.push({ start: range.end, end: last.end, covered: last.covered });
|
|
||||||
} else if (range.start > last.start && range.end > last.end) {
|
|
||||||
// range overlaps end of last:
|
|
||||||
newRanges.pop();
|
|
||||||
newRanges.push({ start: last.start, end: range.start, covered: last.covered });
|
|
||||||
newRanges.push({ start: range.start, end: last.end, covered: range.covered || last.covered });
|
|
||||||
newRanges.push({ start: last.end, end: range.end, covered: range.covered });
|
|
||||||
} else {
|
|
||||||
// ranges are equal:
|
|
||||||
last.covered ||= range.covered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ranges = newRanges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OffsetToPosition {
|
|
||||||
/** Line numbers to byte offsets. */
|
|
||||||
public readonly lines: number[] = [];
|
|
||||||
|
|
||||||
constructor(public readonly source: string) {
|
|
||||||
this.lines.push(0);
|
|
||||||
for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) {
|
|
||||||
this.lines.push(i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts from a file offset to a base 0 line/column .
|
|
||||||
*/
|
|
||||||
public convert(offset: number): { line: number; column: number } {
|
|
||||||
let low = 0;
|
|
||||||
let high = this.lines.length;
|
|
||||||
while (low < high) {
|
|
||||||
const mid = Math.floor((low + high) / 2);
|
|
||||||
if (this.lines[mid] > offset) {
|
|
||||||
high = mid;
|
|
||||||
} else {
|
|
||||||
low = mid + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { line: low - 1, column: offset - this.lines[low - 1] };
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,12 +4,12 @@
|
||||||
"outDir": "./out",
|
"outDir": "./out",
|
||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
"mocha",
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"../../../src/vscode-dts/vscode.d.ts",
|
"../../../src/vscode-dts/vscode.d.ts",
|
||||||
"../../../src/vscode-dts/vscode.proposed.testObserver.d.ts",
|
"../../../src/vscode-dts/vscode.proposed.testObserver.d.ts",
|
||||||
|
"../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
|
||||||
integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==
|
integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==
|
||||||
|
|
||||||
"@types/mocha@^10.0.6":
|
|
||||||
version "10.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b"
|
|
||||||
integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==
|
|
||||||
|
|
||||||
"@types/node@18.x":
|
"@types/node@18.x":
|
||||||
version "18.19.26"
|
version "18.19.26"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729"
|
||||||
|
|
|
@ -41,12 +41,13 @@ const minimist = require('minimist');
|
||||||
* coverage: boolean;
|
* coverage: boolean;
|
||||||
* coveragePath: string;
|
* coveragePath: string;
|
||||||
* coverageFormats: string | string[];
|
* coverageFormats: string | string[];
|
||||||
|
* 'per-test-coverage': boolean;
|
||||||
* help: boolean;
|
* help: boolean;
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
const args = minimist(process.argv.slice(2), {
|
const args = minimist(process.argv.slice(2), {
|
||||||
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats'],
|
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats'],
|
||||||
boolean: ['build', 'coverage', 'help', 'dev'],
|
boolean: ['build', 'coverage', 'help', 'dev', 'per-test-coverage'],
|
||||||
alias: {
|
alias: {
|
||||||
'grep': ['g', 'f'],
|
'grep': ['g', 'f'],
|
||||||
'runGlob': ['glob', 'runGrep'],
|
'runGlob': ['glob', 'runGrep'],
|
||||||
|
@ -68,10 +69,11 @@ Options:
|
||||||
--runGlob, --glob, --runGrep <file_pattern> only run tests matching <file_pattern>
|
--runGlob, --glob, --runGrep <file_pattern> only run tests matching <file_pattern>
|
||||||
--build run with build output (out-build)
|
--build run with build output (out-build)
|
||||||
--coverage generate coverage report
|
--coverage generate coverage report
|
||||||
|
--per-test-coverage generate a per-test V8 coverage report, only valid with the full-json-stream reporter
|
||||||
--dev, --dev-tools, --devTools <window> open dev tools, keep window open, reuse app data
|
--dev, --dev-tools, --devTools <window> open dev tools, keep window open, reuse app data
|
||||||
--reporter <reporter> the mocha reporter (default: "spec")
|
--reporter <reporter> the mocha reporter (default: "spec")
|
||||||
--reporter-options <options> the mocha reporter options (default: "")
|
--reporter-options <options> the mocha reporter options (default: "")
|
||||||
--waitServer <port> port to connect to and wait before running tests
|
--waitServer <port> port to connect to and wait before running tests
|
||||||
--timeout <ms> timeout for tests
|
--timeout <ms> timeout for tests
|
||||||
--crash-reporter-directory <path> crash reporter directory
|
--crash-reporter-directory <path> crash reporter directory
|
||||||
--tfs <url> TFS server URL
|
--tfs <url> TFS server URL
|
||||||
|
@ -160,7 +162,7 @@ function deserializeError(err) {
|
||||||
|
|
||||||
class IPCRunner extends events.EventEmitter {
|
class IPCRunner extends events.EventEmitter {
|
||||||
|
|
||||||
constructor() {
|
constructor(win) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.didFail = false;
|
this.didFail = false;
|
||||||
|
@ -183,6 +185,34 @@ class IPCRunner extends events.EventEmitter {
|
||||||
this.emit('fail', deserializeRunnable(test), deserializeError(err));
|
this.emit('fail', deserializeRunnable(test), deserializeError(err));
|
||||||
});
|
});
|
||||||
ipcMain.on('pending', (e, test) => this.emit('pending', deserializeRunnable(test)));
|
ipcMain.on('pending', (e, test) => this.emit('pending', deserializeRunnable(test)));
|
||||||
|
|
||||||
|
ipcMain.handle('startCoverage', async () => {
|
||||||
|
win.webContents.debugger.attach();
|
||||||
|
await win.webContents.debugger.sendCommand('Debugger.enable');
|
||||||
|
await win.webContents.debugger.sendCommand('Profiler.enable');
|
||||||
|
await win.webContents.debugger.sendCommand('Profiler.startPreciseCoverage', {
|
||||||
|
detailed: true,
|
||||||
|
allowTriggeredUpdates: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const coverageScriptsReported = new Set();
|
||||||
|
ipcMain.handle('snapshotCoverage', async (_, test) => {
|
||||||
|
const coverage = await win.webContents.debugger.sendCommand('Profiler.takePreciseCoverage');
|
||||||
|
await Promise.all(coverage.result.map(async (r) => {
|
||||||
|
if (!coverageScriptsReported.has(r.scriptId)) {
|
||||||
|
coverageScriptsReported.add(r.scriptId);
|
||||||
|
const src = await win.webContents.debugger.sendCommand('Debugger.getScriptSource', { scriptId: r.scriptId });
|
||||||
|
r.source = src.scriptSource;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!test) {
|
||||||
|
this.emit('coverage init', coverage);
|
||||||
|
} else {
|
||||||
|
this.emit('coverage increment', test, coverage);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,7 +304,7 @@ app.on('ready', () => {
|
||||||
|
|
||||||
win.loadURL(url.format({ pathname: path.join(__dirname, 'renderer.html'), protocol: 'file:', slashes: true }));
|
win.loadURL(url.format({ pathname: path.join(__dirname, 'renderer.html'), protocol: 'file:', slashes: true }));
|
||||||
|
|
||||||
const runner = new IPCRunner();
|
const runner = new IPCRunner(win);
|
||||||
createStatsCollector(runner);
|
createStatsCollector(runner);
|
||||||
|
|
||||||
// Handle renderer crashes, #117068
|
// Handle renderer crashes, #117068
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
/*eslint-env mocha*/
|
/*eslint-env mocha*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const inspector = require('inspector');
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const originals = {};
|
const originals = {};
|
||||||
|
@ -169,9 +170,10 @@ function loadTestModules(opts) {
|
||||||
}).then(loadModules);
|
}).then(loadModules);
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentTestTitle;
|
/** @type Mocha.Test */
|
||||||
|
let currentTest;
|
||||||
|
|
||||||
function loadTests(opts) {
|
async function loadTests(opts) {
|
||||||
|
|
||||||
//#region Unexpected Output
|
//#region Unexpected Output
|
||||||
|
|
||||||
|
@ -185,6 +187,8 @@ function loadTests(opts) {
|
||||||
_allowedTestOutput.push(/Deleting [0-9]+ old snapshots/);
|
_allowedTestOutput.push(/Deleting [0-9]+ old snapshots/);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const perTestCoverage = opts['per-test-coverage'] ? await PerTestCoverage.init() : undefined;
|
||||||
|
|
||||||
const _allowedTestsWithOutput = new Set([
|
const _allowedTestsWithOutput = new Set([
|
||||||
'creates a snapshot', // self-testing
|
'creates a snapshot', // self-testing
|
||||||
'validates a snapshot', // self-testing
|
'validates a snapshot', // self-testing
|
||||||
|
@ -202,7 +206,7 @@ function loadTests(opts) {
|
||||||
|
|
||||||
for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) {
|
for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) {
|
||||||
console[consoleFn.name] = function (msg) {
|
console[consoleFn.name] = function (msg) {
|
||||||
if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTestTitle)) {
|
if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title)) {
|
||||||
_testsWithUnexpectedOutput = true;
|
_testsWithUnexpectedOutput = true;
|
||||||
consoleFn.apply(console, arguments);
|
consoleFn.apply(console, arguments);
|
||||||
}
|
}
|
||||||
|
@ -256,7 +260,7 @@ function loadTests(opts) {
|
||||||
event.preventDefault(); // Do not log to test output, we show an error later when test ends
|
event.preventDefault(); // Do not log to test output, we show an error later when test ends
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (!_allowedTestsWithUnhandledRejections.has(currentTestTitle)) {
|
if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) {
|
||||||
onUnexpectedError(event.reason);
|
onUnexpectedError(event.reason);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -275,7 +279,12 @@ function loadTests(opts) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
teardown(() => {
|
setup(async () => {
|
||||||
|
await perTestCoverage?.startTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(async () => {
|
||||||
|
await perTestCoverage?.finishTest(currentTest.file, currentTest.fullTitle());
|
||||||
|
|
||||||
// should not have unexpected output
|
// should not have unexpected output
|
||||||
if (_testsWithUnexpectedOutput && !opts.dev) {
|
if (_testsWithUnexpectedOutput && !opts.dev) {
|
||||||
|
@ -410,7 +419,7 @@ function runTests(opts) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.on('test', test => currentTestTitle = test.title);
|
runner.on('test', test => currentTest = test);
|
||||||
|
|
||||||
if (opts.dev) {
|
if (opts.dev) {
|
||||||
runner.on('fail', (test, err) => {
|
runner.on('fail', (test, err) => {
|
||||||
|
@ -432,3 +441,21 @@ ipcRenderer.on('run', (e, opts) => {
|
||||||
ipcRenderer.send('error', err);
|
ipcRenderer.send('error', err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class PerTestCoverage {
|
||||||
|
static async init() {
|
||||||
|
await ipcRenderer.invoke('startCoverage');
|
||||||
|
return new PerTestCoverage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTest() {
|
||||||
|
if (!this.didInit) {
|
||||||
|
this.didInit = true;
|
||||||
|
await ipcRenderer.invoke('snapshotCoverage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async finishTest(file, fullTitle) {
|
||||||
|
await ipcRenderer.invoke('snapshotCoverage', { file, fullTitle });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,10 @@ module.exports = class FullJsonStreamReporter extends BaseRunner {
|
||||||
runner.once(EVENT_RUN_BEGIN, () => writeEvent(['start', { total }]));
|
runner.once(EVENT_RUN_BEGIN, () => writeEvent(['start', { total }]));
|
||||||
runner.once(EVENT_RUN_END, () => writeEvent(['end', this.stats]));
|
runner.once(EVENT_RUN_END, () => writeEvent(['end', this.stats]));
|
||||||
|
|
||||||
|
// custom coverage events:
|
||||||
|
runner.on('coverage init', (c) => writeEvent(['coverageInit', c]));
|
||||||
|
runner.on('coverage increment', (context, c) => writeEvent(['coverageIncrement', context, c]));
|
||||||
|
|
||||||
runner.on(EVENT_TEST_BEGIN, test => writeEvent(['testStart', clean(test)]));
|
runner.on(EVENT_TEST_BEGIN, test => writeEvent(['testStart', clean(test)]));
|
||||||
runner.on(EVENT_TEST_PASS, test => writeEvent(['pass', clean(test)]));
|
runner.on(EVENT_TEST_PASS, test => writeEvent(['pass', clean(test)]));
|
||||||
runner.on(EVENT_TEST_FAIL, (test, err) => {
|
runner.on(EVENT_TEST_FAIL, (test, err) => {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user