Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add error snippets #919

Merged
merged 1 commit into from May 1, 2024
Merged

feat: add error snippets #919

merged 1 commit into from May 1, 2024

Conversation

KuznetsovRoman
Copy link
Member

@KuznetsovRoman KuznetsovRoman commented Apr 25, 2024

What is done

Add error snippets

Examples

1

image

2

image

3

Notice how stacktrace says example.hermione.tsx?import:12:18, but we are pointing to line 18 because of source map evaluation
image

import type { ResolvedFrame, SufficientStackFrame } from "./types";

// prettier-ignore
const frameRelevance: Record<string, { value: number; matcher: (fileName: string) => boolean }> = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nodejs.org/api/errors.html#errorstack

The location information will be one of:

  • native, if the frame represents a call internal to V8 (as in [].forEach).
  • plain-filename.js:line:column, if the frame represents a call internal to Node.js.
  • /absolute/path/to/file.js:line:column, if the frame represents a call in a user program (using CommonJS module system), or its dependencies.
  • <transport-protocol>:///url/to/module/file.mjs:line:column, if the frame represents a call in a user program (using ES module system), or its dependencies.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ranking them as:

  • 0: can't extract code snippet; useless
  • 1: better than nothing
  • 2: better than wdio internals, but worse, than user code part (these could be testplane parts)
  • 3: user code. If frame of user code appears, always should pick it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ranking them as

this comment should be in the code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
 * @description
 * Rank values:
 * 
 * 0: Can't extract code snippet; useless
 * 
 * 1: WebdriverIO internals: Better than nothing
 * 
 * 2: Project internals: Better than WebdriverIO internals, but worse, than user code part
 * 
 * 3: User code: Best choice
 */
const FRAME_REELVANCE: Record<string, { value: number; matcher: (fileName: string) => boolean }> = {
    repl: { value: 0, matcher: fileName => /^REPL\d+$/.test(fileName) },
    nodeInternals: { value: 0, matcher: fileName => /^node:[a-zA-Z\-_]/.test(fileName) },
    wdioInternals: { value: 1, matcher: fileName => fileName.includes("/node_modules/webdriverio/") },
    projectInternals: { value: 2, matcher: fileName => fileName.includes("/node_modules/") },
    userCode: { value: 3, matcher: () => true },
} as const;

Comment on lines +50 to +64
} catch (findError) {
logger.warn("Unable to find relevant stack frame:", findError);

return null;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If error appers, dont throw any error, just returning null with console warn

Comment on lines 9 to 14
const stackFrameResolver = async (stackFrame: SufficientStackFrame): Promise<ResolvedFrame> => {
const fileContents = await getSourceCodeFile(stackFrame.fileName);
const sourceMaps = await extractSourceMaps(fileContents, stackFrame.fileName);

return sourceMaps ? resolveWithSourceMap(stackFrame, sourceMaps) : resolveWithStackFrame(stackFrame, fileContents);
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSourceCodeFile extracts source code from file and applies source maps, if it is in http headers: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.lmz475t4mvbx

So in other places we dont have to think "is the file from node:fs or browser:fetch?"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source maps will be evaluated, if exists

return sourceMaps ? resolveWithSourceMap(stackFrame, sourceMaps) : resolveWithStackFrame(stackFrame, fileContents);
};

export const extendWithCodeSnippet = async (err: WithSnippetError): Promise<WithSnippetError> => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does not throw exceptions either.
The worst it can do - log a warning into console.

? fileContents.slice(sourceMapsStartIndex + SOURCE_MAP_URL_COMMENT.length)
: fileContents.slice(sourceMapsStartIndex + SOURCE_MAP_URL_COMMENT.length, sourceMapsEndIndex);

const sourceMaps = await getSourceCodeFile(url.resolve(fileName, sourceMapUrl));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceMapUrl could either be relative, absolute or data:application/json;base64,...

https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.lmz475t4mvbx

After url.resolve we could get:

  • absolute path (which we could fs.readFile)
  • network url (which we could fetch)
  • embedded (which would be handled by fetch)

linesBelow: number;
}

export const softFileURLToPath = (fileName: string): string => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For esm, fileName starts with file://

}
};

export const formatFileNameHeader = (fileName: string, opts: formatFileNameHeaderOpts): string => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formats this kind of header above code snippet:

    . | // tests-dir/test.js
    . |
    9 |     it.only('only test', async ({browser}) => {

Comment on lines +49 to +84
const sourceMapUrl = response.headers.get("SourceMap");

return responseText + "\n" + SOURCE_MAP_URL_COMMENT + sourceMapUrl;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By specification, source maps could be in HTTP Header SourceMap.

If it is, we could just append our file contents and dont think about it elsewhere

@@ -37,6 +37,11 @@ module.exports = class FlatReporter extends BaseReporter {
const icon = testCase.isFailed ? icons.FAIL : icons.RETRY;

this.informer.log(` ${testCase.browserId}`);
if (testCase.errorSnippet) {
this.informer.log("");
testCase.errorSnippet.split("\n").forEach(line => this.informer.log(line));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

align snippet with log prefix

}

if (test.err && test.err.snippet) {
testInfo.errorSnippet = chalk.supportsColor ? test.err.snippet : stripAnsi(test.err.snippet);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way we can use colorfull version for console, and remove control characters if user pipes logs into file

@KuznetsovRoman KuznetsovRoman force-pushed the HERMIONE-1517.snippets branch 6 times, most recently from fe15bf2 to be2a68f Compare April 26, 2024 10:23
src/error-snippets/frames.ts Outdated Show resolved Hide resolved
const frameRelevance: Record<string, { value: number; matcher: (fileName: string) => boolean }> = {
repl: { value: 0, matcher: fileName => /^REPL\d+$/.test(fileName) },
nodeNative: { value: 0, matcher: fileName => fileName === "native" },
nodeInternals: { value: 0, matcher: fileName => /^node:[a-zA-Z\-_]/.test(fileName) || /^[a-zA-Z\-_]+\.[a-zA-Z]+$/.test(fileName)},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you, pls, provide an examples for these errors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took it from https://nodejs.org/api/errors.html#errorstack

The location information will be one of:

- native, if the frame represents a call internal to V8 (as in [].forEach).
- plain-filename.js:line:column, if the frame represents a call internal to Node.js.
- /absolute/path/to/file.js:line:column, if the frame represents a call in a user program (using CommonJS module system), or its dependencies.
- <transport-protocol>:///url/to/module/file.mjs:line:column, if the frame represents a call in a user program (using ES module system), or its dependencies.
The string representing the stack trace is lazily generated when the error.stack property is accessed.

But it looks like they forgot to update it, because internal filename differs from version to version.

In node@8 it was like this:

TypeError: Module.SourceMap is not a constructor
    at myFunc (/Users/kroman512/appiumProj/stacktrace.js:12:9)
    at Immediate.setImmediate [as _onImmediate] (/Users/kroman512/appiumProj/stacktrace.js:15:5)
    at runCallback (timers.js:810:20)
    at tryOnImmediate (timers.js:768:5)
    at processImmediate [as _immediateCallback] (timers.js:745:5)

(so timers.js is node internal)

In node@12 it was:

TypeError [ERR_INVALID_ARG_TYPE]: The "payload" argument must be of type object. Received undefined
    at cloneSourceMapV3 (internal/source_map/source_map.js:315:11)
    at new SourceMap (internal/source_map/source_map.js:141:21)
    at myFunc (/Users/kroman512/appiumProj/stacktrace.js:12:9)
    at Immediate._onImmediate (/Users/kroman512/appiumProj/stacktrace.js:15:5)
    at processImmediate (internal/timers.js:461:21)

so it starts with internal/. But starting from node@16 its this:

TypeError [ERR_INVALID_ARG_TYPE]: The "payload" argument must be of type object. Received undefined
    at cloneSourceMapV3 (node:internal/source_map/source_map:318:3)
    at new SourceMap (node:internal/source_map/source_map:141:21)
    at myFunc (/Users/kroman512/appiumProj/stacktrace.js:12:9)
    at Immediate.<anonymous> (/Users/kroman512/appiumProj/stacktrace.js:15:5)
    at processImmediate (node:internal/timers:466:21)

So it is always node:internal/ in node16+

I will remove second nodeInternals matcher as well as native one (this one can't be produced from javascript and only can appear in node runtime itself. Also turns out, we can't parse it anyways):

/**
 * @description
 * Rank values:
 * 
 * 0: Can't extract code snippet; useless
 * 
 * 1: WebdriverIO internals: Better than nothing
 * 
 * 2: Project internals: Better than WebdriverIO internals, but worse, than user code part
 * 
 * 3: User code: Best choice
 */
const FRAME_REELVANCE: Record<string, { value: number; matcher: (fileName: string) => boolean }> = {
    repl: { value: 0, matcher: fileName => /^REPL\d+$/.test(fileName) },
    nodeInternals: { value: 0, matcher: fileName => /^node:[a-zA-Z\-_]/.test(fileName) },
    wdioInternals: { value: 1, matcher: fileName => fileName.includes("/node_modules/webdriverio/") },
    projectInternals: { value: 2, matcher: fileName => fileName.includes("/node_modules/") },
    userCode: { value: 3, matcher: () => true },
} as const;

import type { ResolvedFrame, SufficientStackFrame } from "./types";

// prettier-ignore
const frameRelevance: Record<string, { value: number; matcher: (fileName: string) => boolean }> = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ranking them as

this comment should be in the code

src/error-snippets/frames.ts Outdated Show resolved Hide resolved
src/error-snippets/frames.ts Outdated Show resolved Hide resolved
test/src/error-snippets/frames.ts Outdated Show resolved Hide resolved
test/src/error-snippets/frames.ts Outdated Show resolved Hide resolved
});
});

it("resolveWithStackFrame", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not the most understandable test name :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stackFrameResolver -> stackFrameLocationResolver
resolveWithSourceMap -> resolveLocationWithSourceMap
resolveWithStackFrame -> resolveLocationWithStackFrame

assert.calledOnceWith(loggerStub.warn, "Unable to apply code snippet:");
});

it("should return the same Object.is object", async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure that the name of the test needs to be specified about Object.is

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not?
{} and {} are the same objects. But there is a nuance)

test/src/error-snippets/utils.ts Outdated Show resolved Hide resolved
@KuznetsovRoman KuznetsovRoman force-pushed the HERMIONE-1517.snippets branch 4 times, most recently from 03b2c0f to 389eb4a Compare April 27, 2024 13:43
@KuznetsovRoman KuznetsovRoman merged commit ddc3e28 into master May 1, 2024
2 checks passed
@KuznetsovRoman KuznetsovRoman deleted the HERMIONE-1517.snippets branch May 1, 2024 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants