From d42fad27b278543b0ac92de06b217a67ede40dcc Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 28 Mar 2024 09:08:23 -0700 Subject: [PATCH] eng: move selfhost test provider as a workspace extension (#208699) Testing #208184, closes #207756 --- .vscode/extensions.json | 1 - .../vscode-selfhost-test-provider/icon.png | Bin 0 -> 7702 bytes .../package.json | 78 +++ .../src/coverageProvider.ts | 7 + .../src/debounce.ts | 29 + .../src/extension.ts | 314 ++++++++++ .../src/failingDeepStrictEqualAssertFixer.ts | 255 ++++++++ .../src/memoize.ts | 16 + .../src/metadata.ts | 60 ++ .../src/snapshot.ts | 22 + .../src/sourceUtils.ts | 67 +++ .../src/streamSplitter.ts | 60 ++ .../src/testOutputScanner.ts | 550 ++++++++++++++++++ .../src/testTree.ts | 175 ++++++ .../src/vscodeTestRunner.ts | 306 ++++++++++ .../tsconfig.json | 14 + .../vscode-selfhost-test-provider/yarn.lock | 50 ++ .vscode/settings.json | 1 + build/gulpfile.extensions.js | 4 +- build/npm/dirs.js | 1 + 20 files changed, 2008 insertions(+), 2 deletions(-) create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/icon.png create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/package.json create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/extension.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/tsconfig.json create mode 100644 .vscode/extensions/vscode-selfhost-test-provider/yarn.lock diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3d58135095b19..737efece5a492 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,7 +6,6 @@ "editorconfig.editorconfig", "github.vscode-pull-request-github", "ms-vscode.vscode-github-issue-notebooks", - "ms-vscode.vscode-selfhost-test-provider", "ms-vscode.extension-test-runner", "jrieken.vscode-pr-pinger" ] diff --git a/.vscode/extensions/vscode-selfhost-test-provider/icon.png b/.vscode/extensions/vscode-selfhost-test-provider/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..46bf5e4d3bcf2377c88db3404edd3994f663afc2 GIT binary patch literal 7702 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 YQA~Ghs3I9i_XPlG-OyDlR6#xeACFJL0ssI2 literal 0 HcmV?d00001 diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json new file mode 100644 index 0000000000000..f27953f3cdb4f --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -0,0 +1,78 @@ +{ + "name": "vscode-selfhost-test-provider", + "displayName": "VS Code Selfhost Test Provider", + "description": "Test provider for the VS Code project", + "enabledApiProposals": [ + "testObserver" + ], + "engines": { + "vscode": "^1.88.0" + }, + "contributes": { + "commands": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "title": "Update Snapshot", + "icon": "$(merge)" + } + ], + "menus": { + "commandPalette": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "when": "false" + } + ], + "testing/message/context": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "group": "inline@1", + "when": "testMessage == isSelfhostSnapshotMessage && !testResultOutdated" + } + ], + "testing/message/content": [ + { + "command": "selfhost-test-provider.updateSnapshot", + "when": "testMessage == isSelfhostSnapshotMessage && !testResultOutdated" + } + ] + } + }, + "icon": "icon.png", + "version": "0.4.0", + "publisher": "ms-vscode", + "categories": [ + "Other" + ], + "activationEvents": [ + "workspaceContains:src/vs/loader.js" + ], + "workspaceTrust": { + "request": "onDemand", + "description": "Trust is required to execute tests in the workspace." + }, + "main": "./out/extension.js", + "prettier": { + "printWidth": 100, + "singleQuote": true, + "tabWidth": 2, + "arrowParens": "avoid" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + }, + "license": "MIT", + "scripts": { + "compile": "gulp compile-extension:vscode-selfhost-test-provider", + "watch": "gulp watch-extension:vscode-selfhost-test-provider" + }, + "devDependencies": { + "@types/node": "18.x" + }, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "ansi-styles": "^5.2.0", + "istanbul-to-vscode": "^2.0.1" + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts new file mode 100644 index 0000000000000..1a891523655ab --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { IstanbulCoverageContext } from 'istanbul-to-vscode'; + +export const coverageContext = new IstanbulCoverageContext(); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts new file mode 100644 index 0000000000000..2d2f0ba1bf4b6 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +/** + * Debounces the function call for an interval. + */ +export function debounce(duration: number, fn: () => void): (() => void) & { clear: () => void } { + let timeout: NodeJS.Timeout | void; + const debounced = () => { + if (timeout !== undefined) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + timeout = undefined; + fn(); + }, duration); + }; + + debounced.clear = () => { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + }; + + return debounced; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts new file mode 100644 index 0000000000000..b7269def2e725 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -0,0 +1,314 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { randomBytes } from 'crypto'; +import { tmpdir } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { coverageContext } from './coverageProvider'; +import { FailingDeepStrictEqualAssertFixer } from './failingDeepStrictEqualAssertFixer'; +import { registerSnapshotUpdate } from './snapshot'; +import { scanTestOutput } from './testOutputScanner'; +import { + TestCase, + TestFile, + clearFileDiagnostics, + guessWorkspaceFolder, + itemData, +} from './testTree'; +import { BrowserTestRunner, PlatformTestRunner, VSCodeTestRunner } from './vscodeTestRunner'; + +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/') + ? vscode.workspace.getWorkspaceFolder(uri) + : undefined; + +const browserArgs: [name: string, arg: string][] = [ + ['Chrome', 'chromium'], + ['Firefox', 'firefox'], + ['Webkit', 'webkit'], +]; + +type FileChangeEvent = { uri: vscode.Uri; removed: boolean }; + +export async function activate(context: vscode.ExtensionContext) { + const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests'); + const fileChangedEmitter = new vscode.EventEmitter(); + + ctrl.resolveHandler = async test => { + if (!test) { + context.subscriptions.push(await startWatchingWorkspace(ctrl, fileChangedEmitter)); + return; + } + + const data = itemData.get(test); + if (data instanceof TestFile) { + // No need to watch this, updates will be triggered on file changes + // either by the text document or file watcher. + await data.updateFromDisk(ctrl, test); + } + }; + + const createRunHandler = ( + runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner }, + kind: vscode.TestRunProfileKind, + args: string[] = [] + ) => { + const doTestRun = async ( + req: vscode.TestRunRequest, + cancellationToken: vscode.CancellationToken + ) => { + const folder = await guessWorkspaceFolder(); + if (!folder) { + return; + } + + const runner = new runnerCtor(folder); + const map = await getPendingTestMap(ctrl, req.include ?? gatherTestItems(ctrl.items)); + const task = ctrl.createTestRun(req); + for (const test of map.values()) { + task.enqueued(test); + } + + let coverageDir: string | undefined; + let currentArgs = args; + if (kind === vscode.TestRunProfileKind.Coverage) { + coverageDir = path.join(tmpdir(), `vscode-test-coverage-${randomBytes(8).toString('hex')}`); + currentArgs = [ + ...currentArgs, + '--coverage', + '--coveragePath', + coverageDir, + '--coverageFormats', + 'json', + '--coverageFormats', + 'html', + ]; + } + + return await scanTestOutput( + map, + task, + kind === vscode.TestRunProfileKind.Debug + ? await runner.debug(currentArgs, req.include) + : await runner.run(currentArgs, req.include), + coverageDir, + cancellationToken + ); + }; + + return async (req: vscode.TestRunRequest, cancellationToken: vscode.CancellationToken) => { + if (!req.continuous) { + return doTestRun(req, cancellationToken); + } + + const queuedFiles = new Set(); + let debounced: NodeJS.Timeout | undefined; + + const listener = fileChangedEmitter.event(({ uri, removed }) => { + clearTimeout(debounced); + + if (req.include && !req.include.some(i => i.uri?.toString() === uri.toString())) { + return; + } + + if (removed) { + queuedFiles.delete(uri.toString()); + } else { + queuedFiles.add(uri.toString()); + } + + debounced = setTimeout(() => { + const include = + req.include?.filter(t => t.uri && queuedFiles.has(t.uri?.toString())) ?? + [...queuedFiles] + .map(f => getOrCreateFile(ctrl, vscode.Uri.parse(f))) + .filter((f): f is vscode.TestItem => !!f); + queuedFiles.clear(); + + doTestRun( + new vscode.TestRunRequest(include, req.exclude, req.profile, true), + cancellationToken + ); + }, 1000); + }); + + cancellationToken.onCancellationRequested(() => { + clearTimeout(debounced); + listener.dispose(); + }); + }; + }; + + ctrl.createRunProfile( + 'Run in Electron', + vscode.TestRunProfileKind.Run, + createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Run), + true, + undefined, + true + ); + + ctrl.createRunProfile( + 'Debug in Electron', + vscode.TestRunProfileKind.Debug, + createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Debug), + true, + undefined, + true + ); + + const coverage = ctrl.createRunProfile( + 'Coverage in Electron', + vscode.TestRunProfileKind.Coverage, + createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Coverage), + true, + undefined, + true + ); + + coverage.loadDetailedCoverage = coverageContext.loadDetailedCoverage; + + for (const [name, arg] of browserArgs) { + const cfg = ctrl.createRunProfile( + `Run in ${name}`, + vscode.TestRunProfileKind.Run, + createRunHandler(BrowserTestRunner, vscode.TestRunProfileKind.Run, [' --browser', arg]), + undefined, + undefined, + true + ); + + cfg.configureHandler = () => vscode.window.showInformationMessage(`Configuring ${name}`); + + ctrl.createRunProfile( + `Debug in ${name}`, + vscode.TestRunProfileKind.Debug, + createRunHandler(BrowserTestRunner, vscode.TestRunProfileKind.Debug, [ + '--browser', + arg, + '--debug-browser', + ]), + undefined, + undefined, + true + ); + } + + function updateNodeForDocument(e: vscode.TextDocument) { + const node = getOrCreateFile(ctrl, e.uri); + const data = node && itemData.get(node); + if (data instanceof TestFile) { + data.updateFromContents(ctrl, e.getText(), node!); + } + } + + for (const document of vscode.workspace.textDocuments) { + updateNodeForDocument(document); + } + + context.subscriptions.push( + ctrl, + fileChangedEmitter.event(({ uri, removed }) => { + if (!removed) { + const node = getOrCreateFile(ctrl, uri); + if (node) { + ctrl.invalidateTestResults(); + } + } + }), + vscode.workspace.onDidOpenTextDocument(updateNodeForDocument), + vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document)), + registerSnapshotUpdate(ctrl), + new FailingDeepStrictEqualAssertFixer() + ); +} + +export function deactivate() { + // no-op +} + +function getOrCreateFile( + controller: vscode.TestController, + uri: vscode.Uri +): vscode.TestItem | undefined { + const folder = getWorkspaceFolderForTestFile(uri); + if (!folder) { + return undefined; + } + + const data = new TestFile(uri, folder); + const existing = controller.items.get(data.getId()); + if (existing) { + return existing; + } + + const file = controller.createTestItem(data.getId(), data.getLabel(), uri); + controller.items.add(file); + file.canResolveChildren = true; + itemData.set(file, data); + + return file; +} + +function gatherTestItems(collection: vscode.TestItemCollection) { + const items: vscode.TestItem[] = []; + collection.forEach(item => items.push(item)); + return items; +} + +async function startWatchingWorkspace( + controller: vscode.TestController, + fileChangedEmitter: vscode.EventEmitter +) { + const workspaceFolder = await guessWorkspaceFolder(); + if (!workspaceFolder) { + return new vscode.Disposable(() => undefined); + } + + const pattern = new vscode.RelativePattern(workspaceFolder, TEST_FILE_PATTERN); + const watcher = vscode.workspace.createFileSystemWatcher(pattern); + + watcher.onDidCreate(uri => { + getOrCreateFile(controller, uri); + fileChangedEmitter.fire({ removed: false, uri }); + }); + watcher.onDidChange(uri => fileChangedEmitter.fire({ removed: false, uri })); + watcher.onDidDelete(uri => { + fileChangedEmitter.fire({ removed: true, uri }); + clearFileDiagnostics(uri); + controller.items.delete(uri.toString()); + }); + + for (const file of await vscode.workspace.findFiles(pattern)) { + getOrCreateFile(controller, file); + } + + return watcher; +} + +async function getPendingTestMap(ctrl: vscode.TestController, tests: Iterable) { + const queue = [tests]; + const titleMap = new Map(); + while (queue.length) { + for (const item of queue.pop()!) { + const data = itemData.get(item); + if (data instanceof TestFile) { + if (!data.hasBeenRead) { + await data.updateFromDisk(ctrl, item); + } + queue.push(gatherTestItems(item.children)); + } else if (data instanceof TestCase) { + titleMap.set(data.fullName, item); + } else { + queue.push(gatherTestItems(item.children)); + } + } + } + + return titleMap; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts new file mode 100644 index 0000000000000..c6236c1840284 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts @@ -0,0 +1,255 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as ts from 'typescript'; +import { + 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', +} + +export class FailingDeepStrictEqualAssertFixer { + private disposables: Disposable[] = []; + + 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 expectedValueNode = failingAssertion.assertion.expectedValue; + if (!expectedValueNode) { + return; + } + + 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); + }) + ); + + 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], + }, + ]; + }, + }) + ); + + tests.testResults; + } + + dispose() { + for (const d of this.disposables) { + d.dispose(); + } + } +} + +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); + } + + 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); + + return ts.visitNode(node, visitor); + }, + ]); + + 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) +); + +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 +): { 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 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; + } + + const metadata = getTestMessageMetadata(strictDeepEqualMessage); + if (!metadata) { + return undefined; + } + + 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; + } + + const expr = node.expression.getText(); + if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') { + return undefined; + } + + 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); + + while (node.parent) { + const obj = StrictEqualAssertion.fromNode(node); + if (obj) { + return obj; + } + node = node.parent; + } + + return undefined; + } + + constructor(private readonly expression: ts.CallExpression) {} + + /** 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(); + } +} + +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; +} + +function getAllTestStatusMessagesAt(uri: Uri, lineNumber: number): TestMessage[] { + if (tests.testResults.length === 0) { + return []; + } + + 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); + } + } + } + + return result; +} + +function getTestResultsWithUri(testRun: TestRunResult, uri: Uri): 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); + } + }; + + for (const r of testRun.results) { + walk(r); + } + + return results; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts new file mode 100644 index 0000000000000..f655f58f62a96 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +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; + } + + 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 new file mode 100644 index 0000000000000..0c03614b8bb8c --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +import { TestMessage } from 'vscode'; + +export interface TestMessageMetadata { + expectedValue: unknown; + actualValue: unknown; +} + +const cache = new Array<{ id: string; metadata: TestMessageMetadata }>(); + +let id = 0; + +function getId(): string { + return `msg:${id++}:`; +} + +const regexp = /msg:\d+:/; + +export function attachTestMessageMetadata( + message: TestMessage, + metadata: TestMessageMetadata +): void { + const existingMetadata = getTestMessageMetadata(message); + if (existingMetadata) { + Object.assign(existingMetadata, metadata); + return; + } + + const id = getId(); + + 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(); + } +} + +export function getTestMessageMetadata(message: TestMessage): TestMessageMetadata | undefined { + 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 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 new file mode 100644 index 0000000000000..f6936094c2200 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { promises as fs } from 'fs'; +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; + } + + 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 new file mode 100644 index 0000000000000..334b39f0aa3d8 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as ts from 'typescript'; +import * as vscode from 'vscode'; +import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree'; + +const suiteNames = new Set(['suite', 'flakySuite']); + +export const enum Action { + Skip, + Recurse, +} + +export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: VSCodeTest) => { + if (!ts.isCallExpression(node)) { + return Action.Recurse; + } + + let lhs = node.expression; + if (isSkipCall(lhs)) { + return Action.Skip; + } + + 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; + } + + 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 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); + } + + return Action.Recurse; +}; + +const isPropertyCall = ( + lhs: ts.LeftHandSideExpression +): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } => + 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'; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts new file mode 100644 index 0000000000000..fd28b3772da47 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// DO NOT EDIT DIRECTLY: copied from src/vs/base/node/nodeStreams.ts + +import { Transform } from 'stream'; + +/** + * A Transform stream that splits the input on the "splitter" substring. + * The resulting chunks will contain (and trail with) the splitter match. + * The last chunk when the stream ends will be emitted even if a splitter + * is not encountered. + */ +export class StreamSplitter extends Transform { + private buffer: Buffer | undefined; + private readonly splitter: number; + private readonly spitterLen: number; + + constructor(splitter: string | number | Buffer) { + super(); + if (typeof splitter === 'number') { + this.splitter = splitter; + this.spitterLen = 1; + } else { + throw new Error('not implemented here'); + } + } + + override _transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: any) => void): void { + if (!this.buffer) { + this.buffer = chunk; + } else { + this.buffer = Buffer.concat([this.buffer, chunk]); + } + + let offset = 0; + while (offset < this.buffer.length) { + const index = this.buffer.indexOf(this.splitter, offset); + if (index === -1) { + break; + } + + this.push(this.buffer.slice(offset, index + this.spitterLen)); + offset = index + this.spitterLen; + } + + this.buffer = offset === this.buffer.length ? undefined : this.buffer.slice(offset); + callback(); + } + + override _flush(callback: (error?: Error | null, data?: any) => void): void { + if (this.buffer) { + this.push(this.buffer); + } + + callback(); + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts new file mode 100644 index 0000000000000..8b8de4dc8cc41 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -0,0 +1,550 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { + GREATEST_LOWER_BOUND, + LEAST_UPPER_BOUND, + originalPositionFor, + TraceMap, +} from '@jridgewell/trace-mapping'; +import * as styles from 'ansi-styles'; +import { ChildProcessWithoutNullStreams } from 'child_process'; +import * as vscode from 'vscode'; +import { coverageContext } from './coverageProvider'; +import { attachTestMessageMetadata } from './metadata'; +import { snapshotComment } from './snapshot'; +import { getContentFromFilesystem } from './testTree'; +import { StreamSplitter } from './streamSplitter'; + +export const enum MochaEvent { + Start = 'start', + TestStart = 'testStart', + Pass = 'pass', + Fail = 'fail', + End = 'end', +} + +export interface IStartEvent { + total: number; +} + +export interface ITestStartEvent { + title: string; + fullTitle: string; + file: string; + currentRetry: number; + speed: string; +} + +export interface IPassEvent extends ITestStartEvent { + duration: number; +} + +export interface IFailEvent extends IPassEvent { + err: string; + stack: string | null; + expected?: string; + actual?: string; + expectedJSON?: unknown; + actualJSON?: unknown; + snapshotPath?: string; +} + +export interface IEndEvent { + suites: number; + tests: number; + passes: number; + pending: number; + failures: number; + start: string /* ISO date */; + end: string /* ISO date */; +} + +export type MochaEventTuple = + | [MochaEvent.Start, IStartEvent] + | [MochaEvent.TestStart, ITestStartEvent] + | [MochaEvent.Pass, IPassEvent] + | [MochaEvent.Fail, IFailEvent] + | [MochaEvent.End, IEndEvent]; + +const LF = '\n'.charCodeAt(0); + +export class TestOutputScanner implements vscode.Disposable { + protected mochaEventEmitter = new vscode.EventEmitter(); + protected outputEventEmitter = new vscode.EventEmitter(); + protected onExitEmitter = new vscode.EventEmitter(); + + /** + * Fired when a mocha event comes in. + */ + public readonly onMochaEvent = this.mochaEventEmitter.event; + + /** + * Fired when other output from the process comes in. + */ + public readonly onOtherOutput = this.outputEventEmitter.event; + + /** + * Fired when the process encounters an error, or exits. + */ + public readonly onRunnerExit = this.onExitEmitter.event; + + constructor(private readonly process: ChildProcessWithoutNullStreams, private args?: string[]) { + process.stdout.pipe(new StreamSplitter(LF)).on('data', this.processData); + process.stderr.pipe(new StreamSplitter(LF)).on('data', this.processData); + process.on('error', e => this.onExitEmitter.fire(e.message)); + process.on('exit', code => + this.onExitEmitter.fire(code ? `Test process exited with code ${code}` : undefined) + ); + } + + /** + * @override + */ + public dispose() { + try { + this.process.kill(); + } catch { + // ignored + } + } + + protected readonly processData = (data: string) => { + if (this.args) { + this.outputEventEmitter.fire(`./scripts/test ${this.args.join(' ')}`); + this.args = undefined; + } + + try { + const parsed = JSON.parse(data.trim()) as unknown; + if (parsed instanceof Array && parsed.length === 2 && typeof parsed[0] === 'string') { + this.mochaEventEmitter.fire(parsed as MochaEventTuple); + } else { + this.outputEventEmitter.fire(data); + } + } catch { + this.outputEventEmitter.fire(data); + } + }; +} + +type QueuedOutput = string | [string, vscode.Location | undefined, vscode.TestItem | undefined]; + +export async function scanTestOutput( + tests: Map, + task: vscode.TestRun, + scanner: TestOutputScanner, + coverageDir: string | undefined, + cancellation: vscode.CancellationToken +): Promise { + const exitBlockers: Set> = new Set(); + const skippedTests = new Set(tests.values()); + const store = new SourceMapStore(); + let outputQueue = Promise.resolve(); + const enqueueOutput = (fn: QueuedOutput | (() => Promise)) => { + exitBlockers.delete(outputQueue); + outputQueue = outputQueue.finally(async () => { + const r = typeof fn === 'function' ? await fn() : fn; + typeof r === 'string' ? task.appendOutput(r) : task.appendOutput(...r); + }); + exitBlockers.add(outputQueue); + return outputQueue; + }; + const enqueueExitBlocker = (prom: Promise): Promise => { + exitBlockers.add(prom); + prom.finally(() => exitBlockers.delete(prom)); + return prom; + }; + + let lastTest: vscode.TestItem | undefined; + let ranAnyTest = false; + + try { + if (cancellation.isCancellationRequested) { + return; + } + + await new Promise(resolve => { + cancellation.onCancellationRequested(() => { + resolve(); + }); + + let currentTest: vscode.TestItem | undefined; + + scanner.onRunnerExit(err => { + if (err) { + enqueueOutput(err + crlf); + } + resolve(); + }); + + scanner.onOtherOutput(str => { + const match = spdlogRe.exec(str); + if (!match) { + enqueueOutput(str + crlf); + return; + } + + const logLocation = store.getSourceLocation(match[2], Number(match[3])); + const logContents = replaceAllLocations(store, match[1]); + const test = currentTest; + + enqueueOutput(() => + Promise.all([logLocation, logContents]).then(([location, contents]) => [ + contents + crlf, + location, + test, + ]) + ); + }); + + scanner.onMochaEvent(evt => { + switch (evt[0]) { + case MochaEvent.Start: + break; // no-op + case MochaEvent.TestStart: + currentTest = tests.get(evt[1].fullTitle); + if (!currentTest) { + console.warn(`Could not find test ${evt[1].fullTitle}`); + return; + } + skippedTests.delete(currentTest); + task.started(currentTest); + ranAnyTest = true; + break; + case MochaEvent.Pass: + { + const title = evt[1].fullTitle; + const tcase = tests.get(title); + enqueueOutput(` ${styles.green.open}√${styles.green.close} ${title}\r\n`); + if (tcase) { + lastTest = tcase; + task.passed(tcase, evt[1].duration); + tests.delete(title); + } + } + break; + case MochaEvent.Fail: + { + const { + err, + stack, + duration, + expected, + expectedJSON, + actual, + actualJSON, + snapshotPath, + fullTitle: id, + } = evt[1]; + let tcase = tests.get(id); + // report failures on hook to the last-seen test, or first test if none run yet + if (!tcase && (id.includes('hook for') || id.includes('hook in'))) { + tcase = lastTest ?? tests.values().next().value; + } + + enqueueOutput(`${styles.red.open} x ${id}${styles.red.close}\r\n`); + const rawErr = stack || err; + const locationsReplaced = replaceAllLocations(store, forceCRLF(rawErr)); + if (rawErr) { + enqueueOutput(async () => [await locationsReplaced, undefined, tcase]); + } + + if (!tcase) { + return; + } + + tests.delete(id); + + const hasDiff = + actual !== undefined && + expected !== undefined && + (expected !== '[undefined]' || actual !== '[undefined]'); + const testFirstLine = + tcase.range && + new vscode.Location( + tcase.uri!, + new vscode.Range( + tcase.range.start, + new vscode.Position(tcase.range.start.line, 100) + ) + ); + + enqueueExitBlocker( + (async () => { + const location = await tryDeriveStackLocation(store, rawErr, tcase!); + let message: vscode.TestMessage; + + if (hasDiff) { + message = new vscode.TestMessage(tryMakeMarkdown(err)); + message.actualOutput = outputToString(actual); + message.expectedOutput = outputToString(expected); + if (snapshotPath) { + message.contextValue = 'isSelfhostSnapshotMessage'; + message.expectedOutput += snapshotComment + snapshotPath; + } + + attachTestMessageMetadata(message, { + expectedValue: expectedJSON, + actualValue: actualJSON, + }); + } else { + message = new vscode.TestMessage( + stack ? await sourcemapStack(store, stack) : await locationsReplaced + ); + } + + message.location = location ?? testFirstLine; + task.failed(tcase!, message, duration); + })() + ); + } + break; + case MochaEvent.End: + // no-op, we wait until the process exits to ensure coverage is written out + break; + } + }); + }); + + await Promise.all([...exitBlockers]); + + if (coverageDir) { + try { + await coverageContext.apply(task, coverageDir, { + mapFileUri: uri => store.getSourceFile(uri.toString()), + mapLocation: (uri, position) => + store.getSourceLocation(uri.toString(), position.line, position.character), + }); + } catch (e) { + const msg = `Error loading coverage:\n\n${e}\n`; + task.appendOutput(msg.replace(/\n/g, crlf)); + } + } + + // no tests? Possible crash, show output: + if (!ranAnyTest) { + await vscode.commands.executeCommand('testing.showMostRecentOutput'); + } + } catch (e) { + task.appendOutput((e as Error).stack || (e as Error).message); + } finally { + scanner.dispose(); + for (const test of skippedTests) { + task.skipped(test); + } + task.end(); + } +} + +const spdlogRe = /"(.+)", source: (file:\/\/\/.*?)+ \(([0-9]+)\)/; +const crlf = '\r\n'; + +const forceCRLF = (str: string) => str.replace(/(? { + locationRe.lastIndex = 0; + + const replacements = await Promise.all( + [...str.matchAll(locationRe)].map(async match => { + const location = await deriveSourceLocation(store, match); + if (!location) { + return; + } + return { + from: match[0], + to: location?.uri.with({ + fragment: `L${location.range.start.line + 1}:${location.range.start.character + 1}`, + }), + }; + }) + ); + + for (const replacement of replacements) { + if (replacement) { + str = str.replace(replacement.from, replacement.to.toString(true)); + } + } + + return str; +}; + +const outputToString = (output: unknown) => + typeof output === 'object' ? JSON.stringify(output, null, 2) : String(output); + +const tryMakeMarkdown = (message: string) => { + const lines = message.split('\n'); + const start = lines.findIndex(l => l.includes('+ actual')); + if (start === -1) { + return message; + } + + lines.splice(start, 1, '```diff'); + lines.push('```'); + return new vscode.MarkdownString(lines.join('\n')); +}; + +const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m; +const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const; + +export class SourceMapStore { + private readonly cache = new Map>(); + + async getSourceLocation(fileUri: string, line: number, col = 1) { + const sourceMap = await this.loadSourceMap(fileUri); + if (!sourceMap) { + return undefined; + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: col, line: line + 1, bias }); + if (position.line !== null && position.column !== null && position.source !== null) { + return new vscode.Location( + this.completeSourceMapUrl(sourceMap, position.source), + new vscode.Position(position.line - 1, position.column) + ); + } + } + + return undefined; + } + + async getSourceFile(compiledUri: string) { + const sourceMap = await this.loadSourceMap(compiledUri); + if (!sourceMap) { + return undefined; + } + + if (sourceMap.sources[0]) { + return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]); + } + + for (const bias of sourceMapBiases) { + const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias }); + if (position.source !== null) { + return this.completeSourceMapUrl(sourceMap, position.source); + } + } + + return undefined; + } + + private completeSourceMapUrl(sm: TraceMap, source: string) { + if (sm.sourceRoot) { + try { + return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString()); + } catch { + // ignored + } + } + + return vscode.Uri.parse(source); + } + + private loadSourceMap(fileUri: string) { + const existing = this.cache.get(fileUri); + if (existing) { + return existing; + } + + const promise = (async () => { + try { + const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri)); + const sourcemapMatch = inlineSourcemapRe.exec(contents); + if (!sourcemapMatch) { + return; + } + + const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString(); + return new TraceMap(decoded, fileUri); + } catch (e) { + console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`); + return; + } + })(); + + this.cache.set(fileUri, promise); + return promise; + } +} + +const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g; + +async function replaceAllLocations(store: SourceMapStore, str: string) { + const output: (string | Promise)[] = []; + let lastIndex = 0; + + for (const match of str.matchAll(locationRe)) { + const locationPromise = deriveSourceLocation(store, match); + const startIndex = match.index || 0; + const endIndex = startIndex + match[0].length; + + if (startIndex > lastIndex) { + output.push(str.substring(lastIndex, startIndex)); + } + + output.push( + locationPromise.then(location => + location + ? `${location.uri}:${location.range.start.line + 1}:${location.range.start.character + 1}` + : match[0] + ) + ); + + lastIndex = endIndex; + } + + // Preserve the remaining string after the last match + if (lastIndex < str.length) { + output.push(str.substring(lastIndex)); + } + + const values = await Promise.all(output); + return values.join(''); +} + +async function tryDeriveStackLocation( + store: SourceMapStore, + stack: string, + tcase: vscode.TestItem +) { + locationRe.lastIndex = 0; + + return new Promise(resolve => { + const matches = [...stack.matchAll(locationRe)]; + let todo = matches.length; + if (todo === 0) { + return resolve(undefined); + } + + let best: undefined | { location: vscode.Location; i: number; score: number }; + for (const [i, match] of matches.entries()) { + deriveSourceLocation(store, match) + .catch(() => undefined) + .then(location => { + if (location) { + let score = 0; + if (tcase.uri && tcase.uri.toString() === location.uri.toString()) { + score = 1; + if (tcase.range && tcase.range.contains(location?.range)) { + score = 2; + } + } + if (!best || score > best.score || (score === best.score && i < best.i)) { + best = { location, i, score }; + } + } + + if (!--todo) { + resolve(best?.location); + } + }); + } + }); +} + +async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) { + const [, fileUri, line, col] = parts; + return store.getSourceLocation(fileUri, Number(line), Number(col)); +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts new file mode 100644 index 0000000000000..453740535a3a9 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { join, relative } from 'path'; +import * as ts from 'typescript'; +import { TextDecoder } from 'util'; +import * as vscode from 'vscode'; +import { Action, extractTestFromNode } from './sourceUtils'; + +const textDecoder = new TextDecoder('utf-8'); +const diagnosticCollection = vscode.languages.createDiagnosticCollection('selfhostTestProvider'); + +type ContentGetter = (uri: vscode.Uri) => Promise; + +export const itemData = new WeakMap(); + +export const clearFileDiagnostics = (uri: vscode.Uri) => diagnosticCollection.delete(uri); + +/** + * Tries to guess which workspace folder VS Code is in. + */ +export const guessWorkspaceFolder = async () => { + if (!vscode.workspace.workspaceFolders) { + return undefined; + } + + 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 + } + } + + 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 ''; + } +}; + +export class TestFile { + public hasBeenRead = false; + + constructor( + public readonly uri: vscode.Uri, + public readonly workspaceFolder: vscode.WorkspaceFolder + ) {} + + public getId() { + return this.uri.toString().toLowerCase(); + } + + 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; + } + } + + /** + * 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; + } + + if (childData === Action.Recurse) { + ts.forEachChild(node, traverse); + return; + } + + 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 + ); + + diagnostic.relatedInformation = [ + new vscode.DiagnosticRelatedInformation( + new vscode.Location(existing.uri!, existing.range!), + 'First declared here' + ), + ]; + + 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); + + 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); + } + } +} + +export abstract class TestConstruct { + public fullName: string; + + 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 TestCase extends TestConstruct {} + +export type VSCodeTest = TestFile | TestSuite | TestCase; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts new file mode 100644 index 0000000000000..3870be367ed2a --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -0,0 +1,306 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { spawn } from 'child_process'; +import { promises as fs } from 'fs'; +import { AddressInfo, createServer } from 'net'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { TestOutputScanner } from './testOutputScanner'; +import { TestCase, TestFile, TestSuite, itemData } from './testTree'; + +/** + * From MDN + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping + */ +const escapeRe = (s: string) => s.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); + +const TEST_ELECTRON_SCRIPT_PATH = 'test/unit/electron/index.js'; +const TEST_BROWSER_SCRIPT_PATH = 'test/unit/browser/index.js'; + +const ATTACH_CONFIG_NAME = 'Attach to VS Code'; +const DEBUG_TYPE = 'pwa-chrome'; + +export abstract class VSCodeTestRunner { + constructor(protected readonly repoLocation: vscode.WorkspaceFolder) { } + + public async run(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { + const args = this.prepareArguments(baseArgs, filter); + const cp = spawn(await this.binaryPath(), args, { + cwd: this.repoLocation.uri.fsPath, + stdio: 'pipe', + env: this.getEnvironment(), + }); + + return new TestOutputScanner(cp, args); + } + + public async debug(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { + const port = await this.findOpenPort(); + const baseConfiguration = vscode.workspace + .getConfiguration('launch', this.repoLocation) + .get('configurations', []) + .find(c => c.name === ATTACH_CONFIG_NAME); + + if (!baseConfiguration) { + throw new Error(`Could not find launch configuration ${ATTACH_CONFIG_NAME}`); + } + + const server = this.createWaitServer(); + const args = [ + ...this.prepareArguments(baseArgs, filter), + `--remote-debugging-port=${port}`, + // for breakpoint freeze: https://github.com/microsoft/vscode/issues/122225#issuecomment-885377304 + '--js-flags="--regexp_interpret_all"', + // for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910 + '--disable-features=CalculateNativeWinOcclusion', + '--timeout=0', + `--waitServer=${server.port}`, + ]; + + const cp = spawn(await this.binaryPath(), args, { + cwd: this.repoLocation.uri.fsPath, + stdio: 'pipe', + env: this.getEnvironment(), + }); + + // Register a descriptor factory that signals the server when any + // breakpoint set requests on the debugee have been completed. + const factory = vscode.debug.registerDebugAdapterTrackerFactory(DEBUG_TYPE, { + createDebugAdapterTracker(session) { + if (!session.parentSession || session.parentSession !== rootSession) { + return; + } + + let initRequestId: number | undefined; + + return { + onDidSendMessage(message) { + if (message.type === 'response' && message.request_seq === initRequestId) { + server.ready(); + } + }, + onWillReceiveMessage(message) { + if (initRequestId !== undefined) { + return; + } + + if (message.command === 'launch' || message.command === 'attach') { + initRequestId = message.seq; + } + }, + }; + }, + }); + + vscode.debug.startDebugging(this.repoLocation, { ...baseConfiguration, port }); + + let exited = false; + let rootSession: vscode.DebugSession | undefined; + cp.once('exit', () => { + exited = true; + server.dispose(); + listener.dispose(); + factory.dispose(); + + if (rootSession) { + vscode.debug.stopDebugging(rootSession); + } + }); + + const listener = vscode.debug.onDidStartDebugSession(s => { + if (s.name === ATTACH_CONFIG_NAME && !rootSession) { + if (exited) { + vscode.debug.stopDebugging(rootSession); + } else { + rootSession = s; + } + } + }); + + return new TestOutputScanner(cp, args); + } + + private findOpenPort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, () => { + const address = server.address() as AddressInfo; + const port = address.port; + server.close(() => { + resolve(port); + }); + }); + server.on('error', (error: Error) => { + reject(error); + }); + }); + } + + protected getEnvironment(): NodeJS.ProcessEnv { + return { + ...process.env, + ELECTRON_RUN_AS_NODE: undefined, + ELECTRON_ENABLE_LOGGING: '1', + }; + } + + private prepareArguments( + baseArgs: ReadonlyArray, + filter?: ReadonlyArray + ) { + const args = [...this.getDefaultArgs(), ...baseArgs, '--reporter', 'full-json-stream']; + if (!filter) { + return args; + } + + const grepRe: string[] = []; + const runPaths = new Set(); + const addTestFileRunPath = (data: TestFile) => + runPaths.add( + path.relative(data.workspaceFolder.uri.fsPath, data.uri.fsPath).replace(/\\/g, '/') + ); + + for (const test of filter) { + const data = itemData.get(test); + if (data instanceof TestCase || data instanceof TestSuite) { + grepRe.push(escapeRe(data.fullName) + (data instanceof TestCase ? '$' : ' ')); + for (let p = test.parent; p; p = p.parent) { + const parentData = itemData.get(p); + if (parentData instanceof TestFile) { + addTestFileRunPath(parentData); + } + } + } else if (data instanceof TestFile) { + addTestFileRunPath(data); + } + } + + if (grepRe.length) { + args.push('--grep', `/^(${grepRe.join('|')})/`); + } + + if (runPaths.size) { + args.push(...[...runPaths].flatMap(p => ['--run', p])); + } + + return args; + } + + protected abstract getDefaultArgs(): string[]; + + protected abstract binaryPath(): Promise; + + protected async readProductJson() { + const projectJson = await fs.readFile( + path.join(this.repoLocation.uri.fsPath, 'product.json'), + 'utf-8' + ); + try { + return JSON.parse(projectJson); + } catch (e) { + throw new Error(`Error parsing product.json: ${(e as Error).message}`); + } + } + + private createWaitServer() { + const onReady = new vscode.EventEmitter(); + let ready = false; + + const server = createServer(socket => { + if (ready) { + socket.end(); + } else { + onReady.event(() => socket.end()); + } + }); + + server.listen(0); + + return { + port: (server.address() as AddressInfo).port, + ready: () => { + ready = true; + onReady.fire(); + }, + dispose: () => { + server.close(); + }, + }; + } +} + +export class BrowserTestRunner extends VSCodeTestRunner { + /** @override */ + protected binaryPath(): Promise { + return Promise.resolve(process.execPath); + } + + /** @override */ + protected override getEnvironment() { + return { + ...super.getEnvironment(), + ELECTRON_RUN_AS_NODE: '1', + }; + } + + /** @override */ + protected getDefaultArgs() { + return [TEST_BROWSER_SCRIPT_PATH]; + } +} + +export class WindowsTestRunner extends VSCodeTestRunner { + /** @override */ + protected async binaryPath() { + const { nameShort } = await this.readProductJson(); + return path.join(this.repoLocation.uri.fsPath, `.build/electron/${nameShort}.exe`); + } + + /** @override */ + protected getDefaultArgs() { + return [TEST_ELECTRON_SCRIPT_PATH]; + } +} + +export class PosixTestRunner extends VSCodeTestRunner { + /** @override */ + protected async binaryPath() { + const { applicationName } = await this.readProductJson(); + return path.join(this.repoLocation.uri.fsPath, `.build/electron/${applicationName}`); + } + + /** @override */ + protected getDefaultArgs() { + return [TEST_ELECTRON_SCRIPT_PATH]; + } +} + +export class DarwinTestRunner extends PosixTestRunner { + /** @override */ + protected override getDefaultArgs() { + return [ + TEST_ELECTRON_SCRIPT_PATH, + '--no-sandbox', + '--disable-dev-shm-usage', + '--use-gl=swiftshader', + ]; + } + + /** @override */ + protected override async binaryPath() { + const { nameLong } = await this.readProductJson(); + return path.join( + this.repoLocation.uri.fsPath, + `.build/electron/${nameLong}.app/Contents/MacOS/Electron` + ); + } +} + +export const PlatformTestRunner = + process.platform === 'win32' + ? WindowsTestRunner + : process.platform === 'darwin' + ? DarwinTestRunner + : PosixTestRunner; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json new file mode 100644 index 0000000000000..4bc025b62ba71 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../extensions/tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*", + "../../../src/vscode-dts/vscode.d.ts", + "../../../src/vscode-dts/vscode.proposed.testObserver.d.ts", + ] +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock new file mode 100644 index 0000000000000..bf2295ed7b373 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -0,0 +1,50 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@types/istanbul-lib-coverage@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/node@18.x": + version "18.19.26" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729" + integrity sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw== + dependencies: + undici-types "~5.26.4" + +ansi-styles@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +istanbul-to-vscode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/istanbul-to-vscode/-/istanbul-to-vscode-2.0.1.tgz#84994d06e604b68cac7301840f338b1e74eb888b" + integrity sha512-V9Hhr7kX3UvkvkaT1lK3AmCRPkaIAIogQBrduTpNiLTkp1eVsybnJhWiDSVeCQap/3aGeZ2019oIivhX9MNsCQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.6" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/.vscode/settings.json b/.vscode/settings.json index 121972d6ec6c0..251f8c0617a0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -168,6 +168,7 @@ "[github-issues]": { "editor.wordWrap": "on" }, + "extensions.experimental.supportWorkspaceExtensions": true, "css.format.spaceAroundSelectorSeparator": true, "inlineChat.mode": "live", "typescript.enablePromptUseWorkspaceTsdk": true, diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 32a66abfbc2a0..559049597787e 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -66,7 +66,9 @@ const compilations = [ 'extensions/typescript-language-features/tsconfig.json', 'extensions/vscode-api-tests/tsconfig.json', 'extensions/vscode-colorize-tests/tsconfig.json', - 'extensions/vscode-test-resolver/tsconfig.json' + 'extensions/vscode-test-resolver/tsconfig.json', + + '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', ]; const getBaseUrl = out => `https://ticino.blob.core.windows.net/sourcemaps/${commit}/${out}`; diff --git a/build/npm/dirs.js b/build/npm/dirs.js index faf3a6577a5d8..372d546cd78e3 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -52,6 +52,7 @@ const dirs = [ 'test/integration/browser', 'test/monaco', 'test/smoke', + '.vscode/extensions/vscode-selfhost-test-provider', ]; if (fs.existsSync(`${__dirname}/../../.build/distro/npm`)) {