mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-23 01:58:53 +01:00
eng: move selfhost test provider as a workspace extension (#208699)
Testing #208184, closes #207756
This commit is contained in:
7
.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts
vendored
Normal file
7
.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { IstanbulCoverageContext } from 'istanbul-to-vscode';
|
||||
|
||||
export const coverageContext = new IstanbulCoverageContext();
|
||||
29
.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts
vendored
Normal file
29
.vscode/extensions/vscode-selfhost-test-provider/src/debounce.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
314
.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts
vendored
Normal file
314
.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts
vendored
Normal file
@@ -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<FileChangeEvent>();
|
||||
|
||||
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<string>();
|
||||
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<FileChangeEvent>
|
||||
) {
|
||||
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<vscode.TestItem>) {
|
||||
const queue = [tests];
|
||||
const titleMap = new Map<string, vscode.TestItem>();
|
||||
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;
|
||||
}
|
||||
255
.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts
vendored
Normal file
255
.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
16
.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts
vendored
Normal file
16
.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
export const memoizeLast = <A, T>(fn: (args: A) => T): ((args: A) => T) => {
|
||||
let last: { arg: A; result: T } | undefined;
|
||||
return arg => {
|
||||
if (last && last.arg === arg) {
|
||||
return last.result;
|
||||
}
|
||||
|
||||
const result = fn(arg);
|
||||
last = { arg, result };
|
||||
return result;
|
||||
};
|
||||
};
|
||||
60
.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts
vendored
Normal file
60
.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
22
.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts
vendored
Normal file
22
.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts
vendored
Normal file
@@ -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);
|
||||
});
|
||||
67
.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts
vendored
Normal file
67
.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts
vendored
Normal file
@@ -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';
|
||||
60
.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts
vendored
Normal file
60
.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts
vendored
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
550
.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts
vendored
Normal file
550
.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts
vendored
Normal file
@@ -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<MochaEventTuple>();
|
||||
protected outputEventEmitter = new vscode.EventEmitter<string>();
|
||||
protected onExitEmitter = new vscode.EventEmitter<string | undefined>();
|
||||
|
||||
/**
|
||||
* 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<string, vscode.TestItem>,
|
||||
task: vscode.TestRun,
|
||||
scanner: TestOutputScanner,
|
||||
coverageDir: string | undefined,
|
||||
cancellation: vscode.CancellationToken
|
||||
): Promise<void> {
|
||||
const exitBlockers: Set<Promise<unknown>> = new Set();
|
||||
const skippedTests = new Set(tests.values());
|
||||
const store = new SourceMapStore();
|
||||
let outputQueue = Promise.resolve();
|
||||
const enqueueOutput = (fn: QueuedOutput | (() => Promise<QueuedOutput>)) => {
|
||||
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 = <T>(prom: Promise<T>): Promise<T> => {
|
||||
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<void>(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(/(?<!\r)\n/gm, '\r\n');
|
||||
|
||||
const sourcemapStack = async (store: SourceMapStore, str: string) => {
|
||||
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</* file uri */ string, Promise<TraceMap | undefined>>();
|
||||
|
||||
async getSourceLocation(fileUri: string, line: number, col = 1) {
|
||||
const sourceMap = await this.loadSourceMap(fileUri);
|
||||
if (!sourceMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const bias of sourceMapBiases) {
|
||||
const position = originalPositionFor(sourceMap, { column: col, line: line + 1, bias });
|
||||
if (position.line !== null && position.column !== null && position.source !== null) {
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, position.source),
|
||||
new vscode.Position(position.line - 1, position.column)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getSourceFile(compiledUri: string) {
|
||||
const sourceMap = await this.loadSourceMap(compiledUri);
|
||||
if (!sourceMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (sourceMap.sources[0]) {
|
||||
return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]);
|
||||
}
|
||||
|
||||
for (const bias of sourceMapBiases) {
|
||||
const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias });
|
||||
if (position.source !== null) {
|
||||
return this.completeSourceMapUrl(sourceMap, position.source);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private completeSourceMapUrl(sm: TraceMap, source: string) {
|
||||
if (sm.sourceRoot) {
|
||||
try {
|
||||
return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString());
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return vscode.Uri.parse(source);
|
||||
}
|
||||
|
||||
private loadSourceMap(fileUri: string) {
|
||||
const existing = this.cache.get(fileUri);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri));
|
||||
const sourcemapMatch = inlineSourcemapRe.exec(contents);
|
||||
if (!sourcemapMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString();
|
||||
return new TraceMap(decoded, fileUri);
|
||||
} catch (e) {
|
||||
console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`);
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
this.cache.set(fileUri, promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g;
|
||||
|
||||
async function replaceAllLocations(store: SourceMapStore, str: string) {
|
||||
const output: (string | Promise<string>)[] = [];
|
||||
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<vscode.Location | undefined>(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));
|
||||
}
|
||||
175
.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts
vendored
Normal file
175
.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts
vendored
Normal file
@@ -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<string>;
|
||||
|
||||
export const itemData = new WeakMap<vscode.TestItem, VSCodeTest>();
|
||||
|
||||
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;
|
||||
306
.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts
vendored
Normal file
306
.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts
vendored
Normal file
@@ -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<string>, filter?: ReadonlyArray<vscode.TestItem>) {
|
||||
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<string>, filter?: ReadonlyArray<vscode.TestItem>) {
|
||||
const port = await this.findOpenPort();
|
||||
const baseConfiguration = vscode.workspace
|
||||
.getConfiguration('launch', this.repoLocation)
|
||||
.get<vscode.DebugConfiguration[]>('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<number> {
|
||||
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<string>,
|
||||
filter?: ReadonlyArray<vscode.TestItem>
|
||||
) {
|
||||
const args = [...this.getDefaultArgs(), ...baseArgs, '--reporter', 'full-json-stream'];
|
||||
if (!filter) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const grepRe: string[] = [];
|
||||
const runPaths = new Set<string>();
|
||||
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<string>;
|
||||
|
||||
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<void>();
|
||||
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<string> {
|
||||
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;
|
||||
Reference in New Issue
Block a user