mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-23 10:08:49 +01:00
testing: work on stack trace and adoption in selfhost test provider
Builds on #221225, only the last commit needs review. Still a WIP, this sketches things out for proper implementation tomorrow.
This commit is contained in:
60
.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts
vendored
Normal file
60
.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Copied from https://github.com/microsoft/vscode-js-debug/blob/1d104b5184736677ab5cc280c70bbd227403850c/src/common/stackTraceParser.ts#L18
|
||||
|
||||
// Either match lines like
|
||||
// " at fulfilled (/Users/roblou/code/testapp-node2/out/app.js:5:58)"
|
||||
// or
|
||||
// " at /Users/roblou/code/testapp-node2/out/app.js:60:23"
|
||||
// and replace the path in them
|
||||
const re1 = /^(\W*at .*\()(.*):(\d+):(\d+)(\))$/;
|
||||
const re2 = /^(\W*at )(.*):(\d+):(\d+)$/;
|
||||
|
||||
const getLabelRe = /^\W*at (.*) \($/;
|
||||
|
||||
/**
|
||||
* Parses a textual stack trace.
|
||||
*/
|
||||
export class StackTraceParser {
|
||||
/** Gets whether the stacktrace has any locations in it. */
|
||||
public static isStackLike(str: string) {
|
||||
return re1.test(str) || re2.test(str);
|
||||
}
|
||||
constructor(private readonly stack: string) { }
|
||||
|
||||
/** Iterates over segments of text and locations in the stack. */
|
||||
*[Symbol.iterator]() {
|
||||
for (const line of this.stack.split('\n')) {
|
||||
const match = re1.exec(line) || re2.exec(line);
|
||||
if (!match) {
|
||||
yield line + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, prefix, url, lineNo, columnNo, suffix] = match;
|
||||
if (prefix) {
|
||||
yield prefix;
|
||||
}
|
||||
|
||||
yield new StackTraceLocation(getLabelRe.exec(prefix)?.[1], url, Number(lineNo), Number(columnNo));
|
||||
|
||||
if (suffix) {
|
||||
yield suffix;
|
||||
}
|
||||
|
||||
yield '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class StackTraceLocation {
|
||||
constructor(
|
||||
public readonly label: string | undefined,
|
||||
public readonly path: string,
|
||||
public readonly lineBase1: number,
|
||||
public readonly columnBase1: number,
|
||||
) { }
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import * as vscode from 'vscode';
|
||||
import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider';
|
||||
import { attachTestMessageMetadata } from './metadata';
|
||||
import { snapshotComment } from './snapshot';
|
||||
import { StackTraceLocation, StackTraceParser } from './stackTraceParser';
|
||||
import { StreamSplitter } from './streamSplitter';
|
||||
import { getContentFromFilesystem } from './testTree';
|
||||
import { IScriptCoverage } from './v8CoverageWrangling';
|
||||
@@ -288,8 +289,8 @@ export async function scanTestOutput(
|
||||
|
||||
enqueueExitBlocker(
|
||||
(async () => {
|
||||
const location = await tryDeriveStackLocation(store, rawErr, tcase!);
|
||||
let message: vscode.TestMessage;
|
||||
const stackInfo = await deriveStackLocations(store, rawErr, tcase!);
|
||||
let message: vscode.TestMessage2;
|
||||
|
||||
if (hasDiff) {
|
||||
message = new vscode.TestMessage(tryMakeMarkdown(err));
|
||||
@@ -310,7 +311,8 @@ export async function scanTestOutput(
|
||||
);
|
||||
}
|
||||
|
||||
message.location = location ?? testFirstLine;
|
||||
message.location = stackInfo.primary ?? testFirstLine;
|
||||
message.stackTrace = stackInfo.stack;
|
||||
task.failed(tcase!, message, duration);
|
||||
})()
|
||||
);
|
||||
@@ -608,44 +610,38 @@ async function replaceAllLocations(store: SourceMapStore, str: string) {
|
||||
return values.join('');
|
||||
}
|
||||
|
||||
async function tryDeriveStackLocation(
|
||||
async function deriveStackLocations(
|
||||
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);
|
||||
}
|
||||
const locationsRaw = [...new StackTraceParser(stack)].filter(t => t instanceof StackTraceLocation);
|
||||
const locationsMapped = await Promise.all(locationsRaw.map(async location => {
|
||||
const mapped = location.path.startsWith('file:') ? await store.getSourceLocation(location.path, location.lineBase1 - 1, location.columnBase1 - 1) : undefined;
|
||||
const stack = new vscode.TestMessageStackFrame(location.label || '<anonymous>', mapped?.uri, mapped?.range.start || new vscode.Position(location.lineBase1 - 1, location.columnBase1 - 1));
|
||||
return { location: mapped, stack };
|
||||
}));
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
let best: undefined | { location: vscode.Location; score: number };
|
||||
for (const { location } of locationsMapped) {
|
||||
if (!location) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
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) {
|
||||
best = { location, score };
|
||||
}
|
||||
}
|
||||
|
||||
return { stack: locationsMapped.map(s => s.stack), primary: best?.location };
|
||||
}
|
||||
|
||||
async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) {
|
||||
|
||||
Reference in New Issue
Block a user