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

Change Jest preset to "node" for server-side UTs #133017

Merged

Conversation

gsoldevila
Copy link
Contributor

@gsoldevila gsoldevila commented May 26, 2022

Summary

Part of the spacetime-511 initiative. We are updating Jest preset for server-side unit tests, switching from using a "browser-like" environment to a "node-like" one. This makes the testing environment match better the production one.

The idea is to switch the Jest preset for integration tests as well, but doing so breaks the build, as some tests remain hung awaiting for the logic to finish all async tasks. Thus, in order to have a graceful shutdown, we must first make sure there are no loose ends on the stop() lifecycle of the various services and plugins that shape Kibana.

Checklist

@gsoldevila gsoldevila added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc release_note:skip Skip the PR/issue when compiling release notes backport:skip This commit does not require backporting v8.3.0 labels May 26, 2022
@gsoldevila gsoldevila requested review from a team as code owners May 26, 2022 15:07
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

Comment on lines 418 to 428
const subscription = setupContract.esNodesCompatibility$.subscribe(async () => {
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1);
await delay(10);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2);

await elasticsearchService.stop();
await delay(100);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2);
subscription.unsubscribe();
done();
});
Copy link
Contributor

@spalger spalger May 26, 2022

Choose a reason for hiding this comment

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

The .subscribe(next) callback doesn't support promises, so the promise created by the async function passed to subscribe() will be ignored, triggering a unhandled promise rejection and process termination if these assertions ever fails To make sure that errors produced by expect() are propagated properly you should keep assertions in the observable using the proper operator. In this case, concatMap should be used because it will serially call the passed function for each value produced by setupContract.esNodesCompatibility$, wait for the promise to resolve, then produce the resolved value on the observable (undefined in this case because the async function has no return value), which will then trigger the firstValueFrom() helper and cause the observable to be unsubscribed.

Suggested change
const subscription = setupContract.esNodesCompatibility$.subscribe(async () => {
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1);
await delay(10);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2);
await elasticsearchService.stop();
await delay(100);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2);
subscription.unsubscribe();
done();
});
await firstValueFrom(
setupContract.esNodesCompatibility$.pipe(
Rx.concatMap(async () => {
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1);
await delay(10);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2);
await elasticsearchService.stop();
await delay(100);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2);
})
)
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TIL 😲

done();

return new Promise<void>((done) => {
setImmediate(() => {
Copy link
Contributor

@spalger spalger May 26, 2022

Choose a reason for hiding this comment

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

The timers/promises module has a promisified version of setImmediate you could use https://nodejs.org/api/timers.html#timerspromisessetimmediatevalue-options

Like my previous comment, since setImmediate() executes the callback in a separate callstack the error wouldn't propogate properly and would become unhandled, killing the process if this assertion ever failed so using the promisified version of setImmediate is actually important here.

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

I'm probably missing the implications of changing the jest preset, so I have some questions:

Comment on lines 217 to 219
private isListening(): boolean {
return this.httpServer?.isListening?.();
}
Copy link
Contributor

@pgayvallet pgayvallet May 30, 2022

Choose a reason for hiding this comment

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

Why was this addition necessary (and why the optional chaining given this.httpServer is not optional)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright, that's an interesting one. After some research I think I understood what's happening:

The following test in http_service_test.ts is failing as soon as we switch to the "node" preset:

test('register preboot route handler on setup', ...)

It turns out that this test is failing cause the mocked httpServer is not mocking the isListening method.
So, the fix is simple: we can add a new isListening: jest.fn() to the members of the mocked class.

The interesting question here is: Why was it NOT failing with the jsdom/browser-like preset?

Here goes my theory:
According to this GH thread:

RxJS catches the error thrown from the subscriber and rethrows it in the next tick.
This behavior has been in place since at least RxJs 6.0.0, I believe.
You can see this by waiting one tick yourself before finishing the test:

I believe

  • (A) with jest “jsdom” => all async ops are cancelled / aborted automatically when tests finish.
  • (B) with jest “node” => all async operations are awaited for before finishing the tests.

which makes the test (A) succeed, ignoring the RxJS error about to be reported, and gives test (B) enough time to fail.

TLDR: I will fix the test instead of changing prod code.

Comment on lines 150 to 151
return infoPromise
? from(infoPromise).pipe(
Copy link
Contributor

@pgayvallet pgayvallet May 30, 2022

Choose a reason for hiding this comment

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

Why this check? client.nodes.info always return a value. Are we adapting production code to adapt for testing environment rather than the other way around?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I recall correctly, at some point during an integration test, the client.nodes.info() was returning undefined instead of a Promise. I don't remember the exact scenario but that's the reason I changed this. Perhaps it is linked to the fact that we start and stop servers very quickly for some tests, not giving this request enough time to complete.

I'll rollback this change to expose the error again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

UPDATE: No error anywhere 🤷🏼‍♂️ , so I guess we can discard the change!

});
});

describe('#start', () => {
afterEach(async () => await elasticsearchService?.stop());
Copy link
Contributor

@pgayvallet pgayvallet May 30, 2022

Choose a reason for hiding this comment

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

Should we put that at the top-level rather that in the start block?

Copy link
Contributor Author

@gsoldevila gsoldevila May 30, 2022

Choose a reason for hiding this comment

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

Well, there are some #setup tests that don't even start() the ES service, and the #stop ones that are already calling stop(), so for those it is actually not needed.
I've just tried to move the stop() to the top-level as you suggest, and it does not do any harm. I'll update the code, this way we'll make sure we always call it.

@kibana-ci
Copy link
Collaborator

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] FTR Configs #28 / lens app - group 1 lens smokescreen tests should allow to change index pattern

Metrics [docs]

✅ unchanged

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@gsoldevila gsoldevila requested a review from pgayvallet May 30, 2022 13:02
@gsoldevila gsoldevila added auto-backport Deprecated - use backport:version if exact versions are needed v8.4.0 and removed backport:skip This commit does not require backporting labels May 31, 2022
Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

LGTM

@gsoldevila gsoldevila merged commit 0052fd2 into elastic:main May 31, 2022
kibanamachine pushed a commit that referenced this pull request May 31, 2022
* change Jest preset to "node" for server-side UTs

* small code enhancements, graceful stop

(cherry picked from commit 0052fd2)
@kibanamachine
Copy link
Contributor

💚 All backports created successfully

Status Branch Result
8.3

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

kibanamachine added a commit that referenced this pull request May 31, 2022
* change Jest preset to "node" for server-side UTs

* small code enhancements, graceful stop

(cherry picked from commit 0052fd2)

Co-authored-by: Gerard Soldevila <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated - use backport:version if exact versions are needed release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.3.0 v8.4.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants