diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json index fab178dfaeb..deb2c584c47 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json @@ -1,7 +1,7 @@ { "configurations": [ { - "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--enable-proposed-api=ms-vscode.vscode-selfhost-test-provider"], "name": "Launch Extension", "outFiles": ["${workspaceFolder}/out/**/*.js"], "request": "launch", diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json new file mode 100644 index 00000000000..e4429caeee4 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index ffca4e554a0..852c6c29af2 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -3,7 +3,8 @@ "displayName": "VS Code Selfhost Test Provider", "description": "Test provider for the VS Code project", "enabledApiProposals": [ - "testObserver" + "testObserver", + "attributableCoverage" ], "engines": { "vscode": "^1.88.0" diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index 8059a3d1e1f..7280782c10a 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -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(); - 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(); + 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) + ) + ) + ); } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index c8b0d8f92f3..f68e7f99e89 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -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() ); } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts index b0494471f29..bd2a35d7abf 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts @@ -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; } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts index a3e4c08530d..e232fa133e3 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts @@ -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 })); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts index 52c2aa2c98f..df6c2e77ed2 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ export const memoizeLast = (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; + }; }; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts index 08540b14fbf..8b44c52b72f 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts @@ -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; } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts index 33fbc8fa8bb..5b3a624617e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts @@ -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); + }); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts index 944d3bc6f6d..56b26cafda8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts @@ -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'; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts index fd28b3772da..bb5bc5f28dd 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts @@ -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 { diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index fceb5c3549d..1a1c21fd2c8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -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), diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts index 6ecfb8bc07f..7a54c5c0d32 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts @@ -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; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts index ad22e317860..c2564ca61c3 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts @@ -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 }, + ] + ); + }); }); }); }); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts index 9fbecbd8ba5..ede638430ba 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts @@ -30,7 +30,6 @@ export interface IScriptCoverage { functions: IV8FunctionCoverage[]; } - export class RangeCoverageTracker implements Iterable { /** * A noncontiguous, non-overlapping, ordered set of ranges and whether @@ -41,26 +40,43 @@ export class RangeCoverageTracker implements Iterable { /** * 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 { 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 { 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 { 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] }; - } + } } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts index e8c36118c65..5d73928aed5 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -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, filter?: ReadonlyArray) { const args = this.prepareArguments(baseArgs, filter); @@ -303,5 +303,5 @@ export const PlatformTestRunner = process.platform === 'win32' ? WindowsTestRunner : process.platform === 'darwin' - ? DarwinTestRunner - : PosixTestRunner; + ? DarwinTestRunner + : PosixTestRunner; diff --git a/package.json b/package.json index a421fc39606..05d17c70176 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "75eed367612c65f6edd69c54373da84495e0d0b8", + "distro": "fec0321a3182f40f776709b5ac183c59a120a2b6", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 7e50f1fa9fc..7865e9a2d3d 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -491,8 +491,8 @@ class TestRunTracker extends Disposable { /** Gets details for a previously-emitted coverage object. */ public getCoverageDetails(id: string, token: CancellationToken) { - const [, taskId, covId] = TestId.fromString(id).path; /** runId, taskId, URI */ - const coverage = this.publishedCoverage.get(covId); + const [, taskId] = TestId.fromString(id).path; /** runId, taskId, URI */ + const coverage = this.publishedCoverage.get(id); if (!coverage) { return []; } @@ -573,11 +573,11 @@ class TestRunTracker extends Disposable { } const uriStr = coverage.uri.toString(); - const id = new TestId(testItemIdPart + const id = new TestId(testItemIdPart !== undefined ? [runId, taskId, uriStr, String(testItemIdPart)] : [runId, taskId, uriStr], ).toString(); - this.publishedCoverage.set(uriStr, coverage); + this.publishedCoverage.set(id, coverage); this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(ctrlId, id, coverage)); }, //#region state mutation