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: enhance stacktraces #916

Merged
merged 1 commit into from
Apr 25, 2024
Merged

Conversation

KuznetsovRoman
Copy link
Member

@KuznetsovRoman KuznetsovRoman commented Apr 24, 2024

What is done

First part of HERMIONE-1517
It only does two things:

  • adds prettier stacktrace to WebdriverIO.Browser and WebdriverIO.Element calls
  • removes some underlying wdio frames

Example

// test.testplane.ts
describe('section', () => {
    it('test', async ({browser}) => {
        await browser.url("https://www.npmjs.com/");
        await browser.customWaitForDisplayed("#not-existing");
    });
});
// .testplane.conf.ts
export default {
    ...
    prepareBrowser: browser => {
        browser.addCommand("customWaitForDisplayed", async (selector) => {
            return browser.$(selector).waitForDisplayed();
        });
    }
}

Before

✘ Error: element ("#not-existing") still not displayed after 1000ms
    at file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/build/commands/browser/waitUntil.js:39:23
    at async Element.wrapCommandFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:81:29)
    at async Element.elementErrorHandlerCallbackFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/build/middlewares.js:18:32)
    at async Element.wrapCommandFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:81:29)
    at async Element.wrapCommandFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:81:29)
    at async Element.wrapCommandFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:81:29)
    at async Element.elementErrorHandlerCallbackFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/build/middlewares.js:18:32)
    at async Element.wrapCommandFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:81:29)
    at async Element.wrapCommandFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:81:29)
    at async Browser.wrapCommandFn (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:81:29)

After

✘ Error: element ("#not-existing") still not displayed after 1000ms
    at Browser.customWaitForDisplayed (file:///Users/kroman512/gemini-testing/testplane/node_modules/webdriverio/node_modules/@wdio/utils/build/shim.js:233:24)
    at Browser.customWaitForDisplayed (file:///Users/kroman512/gemini-testing/testplane/node_modules/devtools/node_modules/@wdio/utils/build/monad.js:146:33)
    at Object.<anonymous> (/Users/kroman512/gemini-testing/testing-project/tests/example.hermione.js:8:23)


type AnyFunc = (...args: any[]) => unknown; // eslint-disable-line @typescript-eslint/no-explicit-any

export const runWithStacktraceHooks = ({
Copy link
Member Author

@KuznetsovRoman KuznetsovRoman Apr 24, 2024

Choose a reason for hiding this comment

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

The only function, which is different with history/index.js. The rest is taken from history/index.js with removing shouldNotWrapCommand

Comment on lines 5 to 6
const getErrorTitle = (e: Error): string => `${e.name || "Error"}: ${e.message}`;
const getErrorStackFrames = (e: Error): string | undefined => e.stack?.replace(getErrorTitle(e) + "\n", "");
Copy link
Member Author

Choose a reason for hiding this comment

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

CT could lose e.name due to serialization

Reference: https://nodejs.org/docs/latest-v16.x/api/errors.html#errorcapturestacktracetargetobject-constructoropt

The first line of the trace will be prefixed with ${myObject.name}: ${myObject.message}.

Copy link
Member

Choose a reason for hiding this comment

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

CT could lose e.name due to serialization

Hm, looks like problem in this helper - https://github.com/gemini-testing/testplane/blob/master/src/runner/browser-env/vite/browser-modules/errors/index.ts#L42-L45. Object.getOwnPropertyNames(error) doesn't return name field. I will fix it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Heh, first time i see JSON.stringify with array being used as the second argument.

Error.captureStackTrace(targetObj, filterFunc || captureRawStackFrames);
Error.stackTraceLimit = savedStackTraceLimit;

const rawFramesPosition = targetObj.stack.indexOf("\n") + 1; // crop out error message
Copy link
Member Author

Choose a reason for hiding this comment

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

The first line of the trace will be prefixed with ${myObject.name}: ${myObject.message}.

Comment on lines 40 to 42
if (rawFramesArr.length !== framesParsed.length) {
return error;
}
Copy link
Member Author

@KuznetsovRoman KuznetsovRoman Apr 24, 2024

Choose a reason for hiding this comment

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

We assume "each line of rawFramesArr is equal to framesParsed"
Because we filter "rawFramesArr" using "framesParsed" and we can't build parsed frames back to stacktrace.
I dont know if rawFramesArr.length !== framesParsed.length even could be true, but just in case


isNested(childFrames: RawStackFrames): boolean {
for (const parentFrames of this._framesMap.values()) {
if (childFrames.length !== parentFrames.length && childFrames.endsWith(parentFrames)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

childFrames.length !== parentFrames.length

Because user might have a loop.

childFrames.endsWith(parentFrames)

If true, then we already wrapped parent call

Comment on lines 21 to 20
const frames = captureRawStackFrames(stackFilterFunc || runWithStacktraceHooks);

if (stackFrames.isNested(frames)) {
return fn();
}
Copy link
Member Author

Choose a reason for hiding this comment

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

We dont want to save all of the frames, because with nested calls it would use some memory

@KuznetsovRoman KuznetsovRoman force-pushed the HERMIONE-1517.stacktrace branch 3 times, most recently from b700da5 to da9b2ad Compare April 24, 2024 19:51
@@ -119,6 +120,8 @@ module.exports = class TestRunner extends Runner {
this._browserAgent.freeBrowser(browser);

if (error) {
filterExtraWdioFrames(error);
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved filter to here, because stacktrace hooks are not executed in browser runner.
That way we can filter wdio frames in only one place

Copy link
Member

@DudaGod DudaGod left a comment

Choose a reason for hiding this comment

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

🔥

@@ -1,7 +1,7 @@
import type { Browser } from "../types";
import logger from "../../utils/logger";

export default async (browser: Browser): Promise<void> => {
export default (browser: Browser): void => {
Copy link
Member

Choose a reason for hiding this comment

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

👍

src/browser/stacktrace.ts Outdated Show resolved Hide resolved
Comment on lines 5 to 6
const getErrorTitle = (e: Error): string => `${e.name || "Error"}: ${e.message}`;
const getErrorStackFrames = (e: Error): string | undefined => e.stack?.replace(getErrorTitle(e) + "\n", "");
Copy link
Member

Choose a reason for hiding this comment

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

CT could lose e.name due to serialization

Hm, looks like problem in this helper - https://github.com/gemini-testing/testplane/blob/master/src/runner/browser-env/vite/browser-modules/errors/index.ts#L42-L45. Object.getOwnPropertyNames(error) doesn't return name field. I will fix it.

src/utils/stacktrace.ts Outdated Show resolved Hide resolved
return error;
}

const frames = getErrorStackFrames(error)!;
Copy link
Member

Choose a reason for hiding this comment

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

Why doesn't getErrorStackFrames method return array with frames? By its name, I would expect this behavior.

Copy link
Member Author

Choose a reason for hiding this comment

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

Because it is located near the captureRawStackFrames and captureRawStackFrames returns string. Replaced it with:

type ErrorWithStack = SetRequired<Error, "stack">;

const getErrorRawStackFrames = (e: ErrorWithStack): RawStackFrames => e.stack.replace(getErrorTitle(e) + "\n", "");

so it would be more clear

src/utils/stacktrace.ts Outdated Show resolved Hide resolved
"Element.wrapCommandFn",
"Element.<anonymous>",
"Element.newCommand",
"Element.elementErrorHandlerCallbackFn",
Copy link
Member

Choose a reason for hiding this comment

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

How did you search for all the meaningless frames? Just run few different tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. Those are present in stacktrace of error, produced by any command. So these give us no information.

@KuznetsovRoman KuznetsovRoman merged commit 04c2c2a into master Apr 25, 2024
2 checks passed
@KuznetsovRoman KuznetsovRoman deleted the HERMIONE-1517.stacktrace branch April 25, 2024 12:21
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.

2 participants