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:
Connor Peet
2024-07-08 23:19:20 -07:00
parent fecf501d55
commit d7df7e8645
9 changed files with 229 additions and 42 deletions

View 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,
) { }
}

View File

@@ -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) {