bug fixes and such, format

This commit is contained in:
Connor Peet
2024-05-09 11:31:51 -07:00
parent 41f6f5ad6e
commit 5447d0db10
19 changed files with 664 additions and 548 deletions

View File

@@ -6,7 +6,7 @@
import { IstanbulCoverageContext } from 'istanbul-to-vscode';
import * as vscode from 'vscode';
import { SourceLocationMapper, SourceMapStore } from './testOutputScanner';
import { ICoverageRange, IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling';
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);
@@ -54,7 +54,7 @@ class Script {
constructor(
public readonly uri: vscode.Uri,
source: string,
private readonly maps: SourceMapStore,
private readonly maps: SourceMapStore
) {
this.converter = new OffsetToPosition(source);
}
@@ -70,7 +70,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;
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) {
@@ -80,28 +80,11 @@ class Script {
}
class ScriptCoverageTracker {
/** 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>();
private coverage = new 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);
}
}
for (const range of RangeCoverageTracker.initializeBlocks(coverage.functions)) {
this.coverage.setCovered(range.start, range.end, range.covered);
}
}
@@ -111,38 +94,44 @@ class ScriptCoverageTracker {
* If a source location mapper is given, it assumes the `uri` is the mapped
* URI, and that any unmapped locations/outside the URI should be ignored.
*/
public report(uri: vscode.Uri, convert: OffsetToPosition, mapper: SourceLocationMapper | undefined, item?: vscode.TestItem): V8CoverageFile {
public report(
uri: vscode.Uri,
convert: OffsetToPosition,
mapper: SourceLocationMapper | undefined,
item?: vscode.TestItem
): V8CoverageFile {
const file = new V8CoverageFile(uri, item);
async function handleBlock(range: ICoverageRange) {
const startLine = convert.getLineOfOffset(range.start);
const endLine = convert.getLineOfOffset(range.end);
for (let i = startLine; i <= endLine; i++) {
const start = new vscode.Position(i, i === startLine ? range.start - convert.lines[i] : 0);
const startMap = mapper?.(start.line, start.line);
const end = new vscode.Position(i, i === endLine ? range.end - convert.lines[i] : 0);
const endMap = startMap && mapper?.(end.line, end.line);
if (mapper && (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase())) {
return;
}
const detail = new vscode.StatementCoverage(range.covered, startMap && endMap
? new vscode.Range(startMap.range.start, endMap.range.end)
: new vscode.Range(start, end)
);
file.add(detail);
for (const range of this.coverage) {
if (range.start === range.end) {
continue;
}
}
for (const range of this.file) {
handleBlock(range);
}
const startCov = convert.toLineColumn(range.start);
let start = new vscode.Position(startCov.line, startCov.column);
for (const block of this.blocks.values()) {
for (const range of block) {
handleBlock(range);
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)
)
)
);
}
}

View File

@@ -25,7 +25,7 @@ const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts';
const getWorkspaceFolderForTestFile = (uri: vscode.Uri) =>
(uri.path.endsWith('.test.ts') || uri.path.endsWith('.integrationTest.ts')) &&
uri.path.includes('/src/vs/')
uri.path.includes('/src/vs/')
? vscode.workspace.getWorkspaceFolder(uri)
: undefined;
@@ -58,7 +58,7 @@ export async function activate(context: vscode.ExtensionContext) {
let startedTrackingFailures = false;
const createRunHandler = (
runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner },
runnerCtor: { new (folder: vscode.WorkspaceFolder): VSCodeTestRunner },
kind: vscode.TestRunProfileKind,
args: string[] = []
) => {
@@ -88,7 +88,10 @@ export async function activate(context: vscode.ExtensionContext) {
if (kind === vscode.TestRunProfileKind.Coverage) {
// todo: browser runs currently don't support per-test coverage
if (args.includes('--browser')) {
coverageDir = path.join(tmpdir(), `vscode-test-coverage-${randomBytes(8).toString('hex')}`);
coverageDir = path.join(
tmpdir(),
`vscode-test-coverage-${randomBytes(8).toString('hex')}`
);
currentArgs = [
...currentArgs,
'--coverage',
@@ -242,7 +245,7 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.workspace.onDidOpenTextDocument(updateNodeForDocument),
vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document)),
registerSnapshotUpdate(ctrl),
new FailingDeepStrictEqualAssertFixer(),
new FailingDeepStrictEqualAssertFixer()
);
}

View File

@@ -5,81 +5,81 @@
import * as ts from 'typescript';
import {
commands,
Disposable,
languages,
Position,
Range,
TestMessage,
TestResultSnapshot,
TestRunResult,
tests,
TextDocument,
Uri,
workspace,
WorkspaceEdit,
commands,
Disposable,
languages,
Position,
Range,
TestMessage,
TestResultSnapshot,
TestRunResult,
tests,
TextDocument,
Uri,
workspace,
WorkspaceEdit,
} from 'vscode';
import { memoizeLast } from './memoize';
import { getTestMessageMetadata } from './metadata';
const enum Constants {
FixCommandId = 'selfhost-test.fix-test',
FixCommandId = 'selfhost-test.fix-test',
}
export class FailingDeepStrictEqualAssertFixer {
private disposables: Disposable[] = [];
private disposables: Disposable[] = [];
constructor() {
this.disposables.push(
commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => {
const document = await workspace.openTextDocument(uri);
constructor() {
this.disposables.push(
commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => {
const document = await workspace.openTextDocument(uri);
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position);
if (!failingAssertion) {
return;
}
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position);
if (!failingAssertion) {
return;
}
const expectedValueNode = failingAssertion.assertion.expectedValue;
if (!expectedValueNode) {
return;
}
const expectedValueNode = failingAssertion.assertion.expectedValue;
if (!expectedValueNode) {
return;
}
const start = document.positionAt(expectedValueNode.getStart());
const end = document.positionAt(expectedValueNode.getEnd());
const start = document.positionAt(expectedValueNode.getStart());
const end = document.positionAt(expectedValueNode.getEnd());
const edit = new WorkspaceEdit();
edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue));
await workspace.applyEdit(edit);
})
);
const edit = new WorkspaceEdit();
edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue));
await workspace.applyEdit(edit);
})
);
this.disposables.push(
languages.registerCodeActionsProvider('typescript', {
provideCodeActions: (document, range) => {
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start);
if (!failingAssertion) {
return undefined;
}
this.disposables.push(
languages.registerCodeActionsProvider('typescript', {
provideCodeActions: (document, range) => {
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start);
if (!failingAssertion) {
return undefined;
}
return [
{
title: 'Fix Expected Value',
command: Constants.FixCommandId,
arguments: [document.uri, range.start],
},
];
},
})
);
return [
{
title: 'Fix Expected Value',
command: Constants.FixCommandId,
arguments: [document.uri, range.start],
},
];
},
})
);
tests.testResults;
}
tests.testResults;
}
dispose() {
for (const d of this.disposables) {
d.dispose();
}
}
dispose() {
for (const d of this.disposables) {
d.dispose();
}
}
}
const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i;
@@ -87,170 +87,170 @@ const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i;
const tsPrinter = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const formatJsonValue = (value: unknown) => {
if (typeof value !== 'object') {
return JSON.stringify(value);
}
if (typeof value !== 'object') {
return JSON.stringify(value);
}
const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true);
const outerExpression = src.statements[0] as ts.ExpressionStatement;
const parenExpression = outerExpression.expression as ts.ParenthesizedExpression;
const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true);
const outerExpression = src.statements[0] as ts.ExpressionStatement;
const parenExpression = outerExpression.expression as ts.ParenthesizedExpression;
const unquoted = ts.transform(parenExpression, [
context => (node: ts.Node) => {
const visitor = (node: ts.Node): ts.Node =>
ts.isPropertyAssignment(node) &&
ts.isStringLiteralLike(node.name) &&
identifierLikeRe.test(node.name.text)
? ts.factory.createPropertyAssignment(
ts.factory.createIdentifier(node.name.text),
ts.visitNode(node.initializer, visitor) as ts.Expression
)
: ts.isStringLiteralLike(node) && node.text === '[undefined]'
? ts.factory.createIdentifier('undefined')
: ts.visitEachChild(node, visitor, context);
const unquoted = ts.transform(parenExpression, [
context => (node: ts.Node) => {
const visitor = (node: ts.Node): ts.Node =>
ts.isPropertyAssignment(node) &&
ts.isStringLiteralLike(node.name) &&
identifierLikeRe.test(node.name.text)
? ts.factory.createPropertyAssignment(
ts.factory.createIdentifier(node.name.text),
ts.visitNode(node.initializer, visitor) as ts.Expression
)
: ts.isStringLiteralLike(node) && node.text === '[undefined]'
? ts.factory.createIdentifier('undefined')
: ts.visitEachChild(node, visitor, context);
return ts.visitNode(node, visitor);
},
]);
return ts.visitNode(node, visitor);
},
]);
return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src);
return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src);
};
/** Parses the source file, memoizing the last document so cursor moves are efficient */
const parseSourceFile = memoizeLast((text: string) =>
ts.createSourceFile('', text, ts.ScriptTarget.ES5, true)
ts.createSourceFile('', text, ts.ScriptTarget.ES5, true)
);
const assertionFailureMessageRe = /^Expected values to be strictly (deep-)?equal:/;
/** Gets information about the failing assertion at the poisition, if any. */
function detectFailingDeepStrictEqualAssertion(
document: TextDocument,
position: Position
document: TextDocument,
position: Position
): { assertion: StrictEqualAssertion; actualJSONValue: unknown } | undefined {
const sf = parseSourceFile(document.getText());
const offset = document.offsetAt(position);
const assertion = StrictEqualAssertion.atPosition(sf, offset);
if (!assertion) {
return undefined;
}
const sf = parseSourceFile(document.getText());
const offset = document.offsetAt(position);
const assertion = StrictEqualAssertion.atPosition(sf, offset);
if (!assertion) {
return undefined;
}
const startLine = document.positionAt(assertion.offsetStart).line;
const messages = getAllTestStatusMessagesAt(document.uri, startLine);
const strictDeepEqualMessage = messages.find(m =>
assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value)
);
const startLine = document.positionAt(assertion.offsetStart).line;
const messages = getAllTestStatusMessagesAt(document.uri, startLine);
const strictDeepEqualMessage = messages.find(m =>
assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value)
);
if (!strictDeepEqualMessage) {
return undefined;
}
if (!strictDeepEqualMessage) {
return undefined;
}
const metadata = getTestMessageMetadata(strictDeepEqualMessage);
if (!metadata) {
return undefined;
}
const metadata = getTestMessageMetadata(strictDeepEqualMessage);
if (!metadata) {
return undefined;
}
return {
assertion: assertion,
actualJSONValue: metadata.actualValue,
};
return {
assertion: assertion,
actualJSONValue: metadata.actualValue,
};
}
class StrictEqualAssertion {
/**
* Extracts the assertion at the current node, if it is one.
*/
public static fromNode(node: ts.Node): StrictEqualAssertion | undefined {
if (!ts.isCallExpression(node)) {
return undefined;
}
/**
* Extracts the assertion at the current node, if it is one.
*/
public static fromNode(node: ts.Node): StrictEqualAssertion | undefined {
if (!ts.isCallExpression(node)) {
return undefined;
}
const expr = node.expression.getText();
if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') {
return undefined;
}
const expr = node.expression.getText();
if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') {
return undefined;
}
return new StrictEqualAssertion(node);
}
return new StrictEqualAssertion(node);
}
/**
* Gets the equals assertion at the given offset in the file.
*/
public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined {
let node = findNodeAt(sf, offset);
/**
* Gets the equals assertion at the given offset in the file.
*/
public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined {
let node = findNodeAt(sf, offset);
while (node.parent) {
const obj = StrictEqualAssertion.fromNode(node);
if (obj) {
return obj;
}
node = node.parent;
}
while (node.parent) {
const obj = StrictEqualAssertion.fromNode(node);
if (obj) {
return obj;
}
node = node.parent;
}
return undefined;
}
return undefined;
}
constructor(private readonly expression: ts.CallExpression) { }
constructor(private readonly expression: ts.CallExpression) {}
/** Gets the expected value */
public get expectedValue(): ts.Expression | undefined {
return this.expression.arguments[1];
}
/** Gets the expected value */
public get expectedValue(): ts.Expression | undefined {
return this.expression.arguments[1];
}
/** Gets the position of the assertion expression. */
public get offsetStart(): number {
return this.expression.getStart();
}
/** Gets the position of the assertion expression. */
public get offsetStart(): number {
return this.expression.getStart();
}
}
function findNodeAt(parent: ts.Node, offset: number): ts.Node {
for (const child of parent.getChildren()) {
if (child.getStart() <= offset && offset <= child.getEnd()) {
return findNodeAt(child, offset);
}
}
return parent;
for (const child of parent.getChildren()) {
if (child.getStart() <= offset && offset <= child.getEnd()) {
return findNodeAt(child, offset);
}
}
return parent;
}
function getAllTestStatusMessagesAt(uri: Uri, lineNumber: number): TestMessage[] {
if (tests.testResults.length === 0) {
return [];
}
if (tests.testResults.length === 0) {
return [];
}
const run = tests.testResults[0];
const snapshots = getTestResultsWithUri(run, uri);
const result: TestMessage[] = [];
const run = tests.testResults[0];
const snapshots = getTestResultsWithUri(run, uri);
const result: TestMessage[] = [];
for (const snapshot of snapshots) {
for (const m of snapshot.taskStates[0].messages) {
if (
m.location &&
m.location.range.start.line <= lineNumber &&
lineNumber <= m.location.range.end.line
) {
result.push(m);
}
}
}
for (const snapshot of snapshots) {
for (const m of snapshot.taskStates[0].messages) {
if (
m.location &&
m.location.range.start.line <= lineNumber &&
lineNumber <= m.location.range.end.line
) {
result.push(m);
}
}
}
return result;
return result;
}
function getTestResultsWithUri(testRun: TestRunResult, uri: Uri): TestResultSnapshot[] {
const results: TestResultSnapshot[] = [];
const results: TestResultSnapshot[] = [];
const walk = (r: TestResultSnapshot) => {
for (const c of r.children) {
walk(c);
}
if (r.uri?.toString() === uri.toString()) {
results.push(r);
}
};
const walk = (r: TestResultSnapshot) => {
for (const c of r.children) {
walk(c);
}
if (r.uri?.toString() === uri.toString()) {
results.push(r);
}
};
for (const r of testRun.results) {
walk(r);
}
for (const r of testRun.results) {
walk(r);
}
return results;
return results;
}

View File

@@ -56,7 +56,12 @@ export class FailureTracker {
const prev = this.lastFailed.get(key);
if (snapshot.taskStates.some(s => s.state === vscode.TestResultState.Failed)) {
// unset the parent to avoid a circular JSON structure:
getGitState().then(s => this.lastFailed.set(key, { snapshot: { ...snapshot, parent: undefined }, failing: s }));
getGitState().then(s =>
this.lastFailed.set(key, {
snapshot: { ...snapshot, parent: undefined },
failing: s,
})
);
} else if (prev) {
this.lastFailed.delete(key);
getGitState().then(s => this.append({ ...prev, passing: s }));

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
export const memoizeLast = <A, T>(fn: (args: A) => T): ((args: A) => T) => {
let last: { arg: A; result: T } | undefined;
return arg => {
if (last && last.arg === arg) {
return last.result;
}
let last: { arg: A; result: T } | undefined;
return arg => {
if (last && last.arg === arg) {
return last.result;
}
const result = fn(arg);
last = { arg, result };
return result;
};
const result = fn(arg);
last = { arg, result };
return result;
};
};

View File

@@ -5,8 +5,8 @@
import { TestMessage } from 'vscode';
export interface TestMessageMetadata {
expectedValue: unknown;
actualValue: unknown;
expectedValue: unknown;
actualValue: unknown;
}
const cache = new Array<{ id: string; metadata: TestMessageMetadata }>();
@@ -14,48 +14,48 @@ const cache = new Array<{ id: string; metadata: TestMessageMetadata }>();
let id = 0;
function getId(): string {
return `msg:${id++}:`;
return `msg:${id++}:`;
}
const regexp = /msg:\d+:/;
export function attachTestMessageMetadata(
message: TestMessage,
metadata: TestMessageMetadata
message: TestMessage,
metadata: TestMessageMetadata
): void {
const existingMetadata = getTestMessageMetadata(message);
if (existingMetadata) {
Object.assign(existingMetadata, metadata);
return;
}
const existingMetadata = getTestMessageMetadata(message);
if (existingMetadata) {
Object.assign(existingMetadata, metadata);
return;
}
const id = getId();
const id = getId();
if (typeof message.message === 'string') {
message.message = `${message.message}\n${id}`;
} else {
message.message.appendText(`\n${id}`);
}
if (typeof message.message === 'string') {
message.message = `${message.message}\n${id}`;
} else {
message.message.appendText(`\n${id}`);
}
cache.push({ id, metadata });
while (cache.length > 100) {
cache.shift();
}
cache.push({ id, metadata });
while (cache.length > 100) {
cache.shift();
}
}
export function getTestMessageMetadata(message: TestMessage): TestMessageMetadata | undefined {
let value: string;
if (typeof message.message === 'string') {
value = message.message;
} else {
value = message.message.value;
}
let value: string;
if (typeof message.message === 'string') {
value = message.message;
} else {
value = message.message.value;
}
const result = regexp.exec(value);
if (!result) {
return undefined;
}
const result = regexp.exec(value);
if (!result) {
return undefined;
}
const id = result[0];
return cache.find(c => c.id === id)?.metadata;
const id = result[0];
return cache.find(c => c.id === id)?.metadata;
}

View File

@@ -9,15 +9,15 @@ import * as vscode from 'vscode';
export const snapshotComment = '\n\n// Snapshot file: ';
export const registerSnapshotUpdate = (ctrl: vscode.TestController) =>
vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => {
const message: vscode.TestMessage = args.message;
const index = message.expectedOutput?.indexOf(snapshotComment);
if (!message.expectedOutput || !message.actualOutput || !index || index === -1) {
vscode.window.showErrorMessage('Could not find snapshot comment in message');
return;
}
vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => {
const message: vscode.TestMessage = args.message;
const index = message.expectedOutput?.indexOf(snapshotComment);
if (!message.expectedOutput || !message.actualOutput || !index || index === -1) {
vscode.window.showErrorMessage('Could not find snapshot comment in message');
return;
}
const file = message.expectedOutput.slice(index + snapshotComment.length);
await fs.writeFile(file, message.actualOutput);
ctrl.invalidateTestResults(args.test);
});
const file = message.expectedOutput.slice(index + snapshotComment.length);
await fs.writeFile(file, message.actualOutput);
ctrl.invalidateTestResults(args.test);
});

View File

@@ -10,59 +10,59 @@ import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree';
const suiteNames = new Set(['suite', 'flakySuite']);
export const enum Action {
Skip,
Recurse,
Skip,
Recurse,
}
export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: VSCodeTest) => {
if (!ts.isCallExpression(node)) {
return Action.Recurse;
}
if (!ts.isCallExpression(node)) {
return Action.Recurse;
}
let lhs = node.expression;
if (isSkipCall(lhs)) {
return Action.Skip;
}
let lhs = node.expression;
if (isSkipCall(lhs)) {
return Action.Skip;
}
if (isPropertyCall(lhs) && lhs.name.text === 'only') {
lhs = lhs.expression;
}
if (isPropertyCall(lhs) && lhs.name.text === 'only') {
lhs = lhs.expression;
}
const name = node.arguments[0];
const func = node.arguments[1];
if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) {
return Action.Recurse;
}
const name = node.arguments[0];
const func = node.arguments[1];
if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) {
return Action.Recurse;
}
if (!func) {
return Action.Recurse;
}
if (!func) {
return Action.Recurse;
}
const start = src.getLineAndCharacterOfPosition(name.pos);
const end = src.getLineAndCharacterOfPosition(func.end);
const range = new vscode.Range(
new vscode.Position(start.line, start.character),
new vscode.Position(end.line, end.character)
);
const start = src.getLineAndCharacterOfPosition(name.pos);
const end = src.getLineAndCharacterOfPosition(func.end);
const range = new vscode.Range(
new vscode.Position(start.line, start.character),
new vscode.Position(end.line, end.character)
);
const cparent = parent instanceof TestConstruct ? parent : undefined;
if (lhs.escapedText === 'test') {
return new TestCase(name.text, range, cparent);
}
const cparent = parent instanceof TestConstruct ? parent : undefined;
if (lhs.escapedText === 'test') {
return new TestCase(name.text, range, cparent);
}
if (suiteNames.has(lhs.escapedText.toString())) {
return new TestSuite(name.text, range, cparent);
}
if (suiteNames.has(lhs.escapedText.toString())) {
return new TestSuite(name.text, range, cparent);
}
return Action.Recurse;
return Action.Recurse;
};
const isPropertyCall = (
lhs: ts.LeftHandSideExpression
lhs: ts.LeftHandSideExpression
): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } =>
ts.isPropertyAccessExpression(lhs) &&
ts.isIdentifier(lhs.expression) &&
ts.isIdentifier(lhs.name);
ts.isPropertyAccessExpression(lhs) &&
ts.isIdentifier(lhs.expression) &&
ts.isIdentifier(lhs.name);
const isSkipCall = (lhs: ts.LeftHandSideExpression) =>
isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip';
isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip';

View File

@@ -28,7 +28,11 @@ export class StreamSplitter extends Transform {
}
}
override _transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: any) => void): void {
override _transform(
chunk: Buffer,
_encoding: string,
callback: (error?: Error | null, data?: any) => void
): void {
if (!this.buffer) {
this.buffer = chunk;
} else {

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import {
decodedMappings,
GREATEST_LOWER_BOUND,
LEAST_UPPER_BOUND,
originalPositionFor,
@@ -15,8 +16,8 @@ import * as vscode from 'vscode';
import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider';
import { attachTestMessageMetadata } from './metadata';
import { snapshotComment } from './snapshot';
import { getContentFromFilesystem } from './testTree';
import { StreamSplitter } from './streamSplitter';
import { getContentFromFilesystem } from './testTree';
import { IScriptCoverage } from './v8CoverageWrangling';
export const enum MochaEvent {
@@ -435,8 +436,17 @@ export class SourceMapStore {
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: line + 1, bias });
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),

View File

@@ -22,155 +22,155 @@ export const clearFileDiagnostics = (uri: vscode.Uri) => diagnosticCollection.de
* Tries to guess which workspace folder VS Code is in.
*/
export const guessWorkspaceFolder = async () => {
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
if (!vscode.workspace.workspaceFolders) {
return undefined;
}
if (vscode.workspace.workspaceFolders.length < 2) {
return vscode.workspace.workspaceFolders[0];
}
if (vscode.workspace.workspaceFolders.length < 2) {
return vscode.workspace.workspaceFolders[0];
}
for (const folder of vscode.workspace.workspaceFolders) {
try {
await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js'));
return folder;
} catch {
// ignored
}
}
for (const folder of vscode.workspace.workspaceFolders) {
try {
await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js'));
return folder;
} catch {
// ignored
}
}
return undefined;
return undefined;
};
export const getContentFromFilesystem: ContentGetter = async uri => {
try {
const rawContent = await vscode.workspace.fs.readFile(uri);
return textDecoder.decode(rawContent);
} catch (e) {
console.warn(`Error providing tests for ${uri.fsPath}`, e);
return '';
}
try {
const rawContent = await vscode.workspace.fs.readFile(uri);
return textDecoder.decode(rawContent);
} catch (e) {
console.warn(`Error providing tests for ${uri.fsPath}`, e);
return '';
}
};
export class TestFile {
public hasBeenRead = false;
public hasBeenRead = false;
constructor(
public readonly uri: vscode.Uri,
public readonly workspaceFolder: vscode.WorkspaceFolder
) { }
constructor(
public readonly uri: vscode.Uri,
public readonly workspaceFolder: vscode.WorkspaceFolder
) {}
public getId() {
return this.uri.toString().toLowerCase();
}
public getId() {
return this.uri.toString().toLowerCase();
}
public getLabel() {
return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath);
}
public getLabel() {
return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath);
}
public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) {
try {
const content = await getContentFromFilesystem(item.uri!);
item.error = undefined;
this.updateFromContents(controller, content, item);
} catch (e) {
item.error = (e as Error).stack;
}
}
public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) {
try {
const content = await getContentFromFilesystem(item.uri!);
item.error = undefined;
this.updateFromContents(controller, content, item);
} catch (e) {
item.error = (e as Error).stack;
}
}
/**
* Refreshes all tests in this file, `sourceReader` provided by the root.
*/
public updateFromContents(
controller: vscode.TestController,
content: string,
file: vscode.TestItem
) {
try {
const diagnostics: vscode.Diagnostic[] = [];
const ast = ts.createSourceFile(
this.uri.path.split('/').pop()!,
content,
ts.ScriptTarget.ESNext,
false,
ts.ScriptKind.TS
);
/**
* Refreshes all tests in this file, `sourceReader` provided by the root.
*/
public updateFromContents(
controller: vscode.TestController,
content: string,
file: vscode.TestItem
) {
try {
const diagnostics: vscode.Diagnostic[] = [];
const ast = ts.createSourceFile(
this.uri.path.split('/').pop()!,
content,
ts.ScriptTarget.ESNext,
false,
ts.ScriptKind.TS
);
const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [
{ item: file, children: [] },
];
const traverse = (node: ts.Node) => {
const parent = parents[parents.length - 1];
const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!);
if (childData === Action.Skip) {
return;
}
const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [
{ item: file, children: [] },
];
const traverse = (node: ts.Node) => {
const parent = parents[parents.length - 1];
const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!);
if (childData === Action.Skip) {
return;
}
if (childData === Action.Recurse) {
ts.forEachChild(node, traverse);
return;
}
if (childData === Action.Recurse) {
ts.forEachChild(node, traverse);
return;
}
const id = `${file.uri}/${childData.fullName}`.toLowerCase();
const id = `${file.uri}/${childData.fullName}`.toLowerCase();
// Skip duplicated tests. They won't run correctly with the way
// mocha reports them, and will error if we try to insert them.
const existing = parent.children.find(c => c.id === id);
if (existing) {
const diagnostic = new vscode.Diagnostic(
childData.range,
'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.',
vscode.DiagnosticSeverity.Warning
);
// Skip duplicated tests. They won't run correctly with the way
// mocha reports them, and will error if we try to insert them.
const existing = parent.children.find(c => c.id === id);
if (existing) {
const diagnostic = new vscode.Diagnostic(
childData.range,
'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.',
vscode.DiagnosticSeverity.Warning
);
diagnostic.relatedInformation = [
new vscode.DiagnosticRelatedInformation(
new vscode.Location(existing.uri!, existing.range!),
'First declared here'
),
];
diagnostic.relatedInformation = [
new vscode.DiagnosticRelatedInformation(
new vscode.Location(existing.uri!, existing.range!),
'First declared here'
),
];
diagnostics.push(diagnostic);
return;
}
diagnostics.push(diagnostic);
return;
}
const item = controller.createTestItem(id, childData.name, file.uri);
itemData.set(item, childData);
item.range = childData.range;
parent.children.push(item);
const item = controller.createTestItem(id, childData.name, file.uri);
itemData.set(item, childData);
item.range = childData.range;
parent.children.push(item);
if (childData instanceof TestSuite) {
parents.push({ item: item, children: [] });
ts.forEachChild(node, traverse);
item.children.replace(parents.pop()!.children);
}
};
if (childData instanceof TestSuite) {
parents.push({ item: item, children: [] });
ts.forEachChild(node, traverse);
item.children.replace(parents.pop()!.children);
}
};
ts.forEachChild(ast, traverse);
file.error = undefined;
file.children.replace(parents[0].children);
diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined);
this.hasBeenRead = true;
} catch (e) {
file.error = String((e as Error).stack || (e as Error).message);
}
}
ts.forEachChild(ast, traverse);
file.error = undefined;
file.children.replace(parents[0].children);
diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined);
this.hasBeenRead = true;
} catch (e) {
file.error = String((e as Error).stack || (e as Error).message);
}
}
}
export abstract class TestConstruct {
public fullName: string;
public fullName: string;
constructor(
public readonly name: string,
public readonly range: vscode.Range,
parent?: TestConstruct
) {
this.fullName = parent ? `${parent.fullName} ${name}` : name;
}
constructor(
public readonly name: string,
public readonly range: vscode.Range,
parent?: TestConstruct
) {
this.fullName = parent ? `${parent.fullName} ${name}` : name;
}
}
export class TestSuite extends TestConstruct { }
export class TestSuite extends TestConstruct {}
export class TestCase extends TestConstruct { }
export class TestCase extends TestConstruct {}
export type VSCodeTest = TestFile | TestSuite | TestCase;

View File

@@ -18,63 +18,76 @@ suite('v8CoverageWrangling', () => {
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 },
]);
rt.cover(12, 13);
assert.deepStrictEqual(
[...rt],
[
{ start: 5, end: 10, covered: true },
{ start: 12, end: 13, 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 },
]);
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 },
]);
assert.deepStrictEqual(
[...rt],
[
{ start: 2, 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 },
]);
assert.deepStrictEqual(
[...rt],
[
{ start: 2, end: 5, covered: false },
{ start: 5, 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 },
]);
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 },
]);
assert.deepStrictEqual(
[...rt],
[
{ start: 5, end: 7, covered: false },
{ start: 7, end: 9, covered: true },
{ start: 9, end: 10, covered: false },
]
);
});
test('boundary touching', () => {
@@ -82,25 +95,62 @@ suite('v8CoverageWrangling', () => {
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 },
]);
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 },
]);
suite('initializeBlock', () => {
test('simple tree', () => {
const rt = RangeCoverageTracker.initializeBlocks([
{
functionName: 'outer',
isBlockCoverage: true,
ranges: [
{ 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 },
]);
assert.deepStrictEqual(
[...rt],
[
{ start: 5, end: 15, covered: true },
{ start: 15, end: 20, covered: false },
{ start: 20, end: 30, covered: true },
]
);
});
test('separate branches', () => {
const rt = RangeCoverageTracker.initializeBlocks([
{
functionName: 'outer',
isBlockCoverage: true,
ranges: [
{ count: 1, startOffset: 5, endOffset: 8 },
{ count: 1, startOffset: 10, endOffset: 12 },
{ count: 0, startOffset: 15, endOffset: 20 },
],
},
]);
assert.deepStrictEqual(
[...rt],
[
{ start: 5, end: 8, covered: true },
{ start: 10, end: 12, covered: true },
{ start: 15, end: 20, covered: false },
]
);
});
});
});
});

View File

@@ -30,7 +30,6 @@ export interface IScriptCoverage {
functions: IV8FunctionCoverage[];
}
export class RangeCoverageTracker implements Iterable<ICoverageRange> {
/**
* A noncontiguous, non-overlapping, ordered set of ranges and whether
@@ -41,26 +40,43 @@ export class RangeCoverageTracker implements Iterable<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;
public static initializeBlocks(fns: IV8FunctionCoverage[]) {
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;
let start = 0;
const stack: IV8CoverageRange[] = [];
// note: comes pre-sorted from V8
for (const { ranges } of fns) {
for (const range of ranges) {
while (stack.length && stack[stack.length - 1].endOffset < range.startOffset) {
const last = stack.pop()!;
rt.setCovered(start, last.endOffset, last.count > 0);
start = last.endOffset;
}
if (range.startOffset > start && stack.length) {
rt.setCovered(start, range.startOffset, !!stack[stack.length - 1].count);
}
start = range.startOffset;
stack.push(range);
}
rt.cover(start, range.startOffset);
rt.uncovered(range.startOffset, range.endOffset);
start = range.endOffset;
}
rt.cover(start, ranges[0].endOffset);
while (stack.length) {
const last = stack.pop()!;
rt.setCovered(start, last.endOffset, last.count > 0);
start = last.endOffset;
}
return rt;
}
/** Makes a copy of the range tracker. */
public clone() {
const rt = new RangeCoverageTracker();
rt.ranges = this.ranges.slice();
return rt;
}
@@ -79,6 +95,12 @@ export class RangeCoverageTracker implements Iterable<ICoverageRange> {
return this.ranges[Symbol.iterator]();
}
/**
* Marks the given character range as being covered or uncovered.
*
* todo@connor4312: this is a hot path is could probably be optimized to
* avoid rebuilding the array. Maybe with a nice tree structure?
*/
public setCovered(start: number, end: number, covered: boolean) {
const newRanges: ICoverageRange[] = [];
let i = 0;
@@ -86,38 +108,53 @@ export class RangeCoverageTracker implements Iterable<ICoverageRange> {
newRanges.push(this.ranges[i]);
}
newRanges.push({ start, end, covered });
const push = (range: ICoverageRange) => {
const last = newRanges.length && newRanges[newRanges.length - 1];
if (last && last.end === range.start && last.covered === range.covered) {
last.end = range.end;
} else {
newRanges.push(range);
}
};
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) {
if (range.start === last.start && range.end === last.end) {
// ranges are equal:
last.covered ||= range.covered;
} else if (range.end < last.start || range.start > last.end) {
// ranges don't overlap
push(range);
} else 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) {
push({ start: range.start, end: last.start, covered: range.covered });
push({ start: last.start, end: last.end, covered: range.covered || last.covered });
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 });
push({ start: last.start, end: range.start, covered: last.covered });
push({ start: range.start, end: range.end, covered: range.covered || last.covered });
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) {
push({ start: range.start, end: last.start, covered: range.covered });
push({ start: last.start, end: range.end, covered: range.covered || last.covered });
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 });
push({ start: last.start, end: range.start, covered: last.covered });
push({ start: range.start, end: last.end, covered: range.covered || last.covered });
push({ start: last.end, end: range.end, covered: range.covered });
} else {
// ranges are equal:
last.covered ||= range.covered;
throw new Error('unreachable');
}
}
@@ -127,14 +164,24 @@ export class RangeCoverageTracker implements Iterable<ICoverageRange> {
export class OffsetToPosition {
/** Line numbers to byte offsets. */
public readonly lines: number[] = [];
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);
}
}
public readonly totalLength: number;
constructor(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);
}
this.totalLength = source.length;
}
public getLineLength(lineNumber: number): number {
return (
(lineNumber < this.lines.length - 1 ? this.lines[lineNumber + 1] - 1 : this.totalLength) -
this.lines[lineNumber]
);
}
/**
* Gets the line the offset appears on.
@@ -154,11 +201,11 @@ export class OffsetToPosition {
return low - 1;
}
/**
* Converts from a file offset to a base 0 line/column .
*/
public convert(offset: number): { line: number; column: number } {
/**
* Converts from a file offset to a base 0 line/column .
*/
public toLineColumn(offset: number): { line: number; column: number } {
const line = this.getLineOfOffset(offset);
return { line: line, column: offset - this.lines[line] };
}
}
}

View File

@@ -24,7 +24,7 @@ const ATTACH_CONFIG_NAME = 'Attach to VS Code';
const DEBUG_TYPE = 'pwa-chrome';
export abstract class VSCodeTestRunner {
constructor(protected readonly repoLocation: vscode.WorkspaceFolder) { }
constructor(protected readonly repoLocation: vscode.WorkspaceFolder) {}
public async run(baseArgs: ReadonlyArray<string>, filter?: ReadonlyArray<vscode.TestItem>) {
const args = this.prepareArguments(baseArgs, filter);
@@ -303,5 +303,5 @@ export const PlatformTestRunner =
process.platform === 'win32'
? WindowsTestRunner
: process.platform === 'darwin'
? DarwinTestRunner
: PosixTestRunner;
? DarwinTestRunner
: PosixTestRunner;