-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Improve integration tests (stability + performance) #15125
Conversation
1ecbc12 to
763ecb8
Compare
This ensures that we run CLI, PostCSS, Vite, ... tests per OS per type of test (sequential or concurrent).
For all shared configuration files instead of using CLI flags
All tests will be concurrent by default, only tests marked using `test.sequential` will be run in sequential order.
Instead of blocks; Also make sure to strip all ANSI escapes. This will be necessary when we are parsing values from the stdout/stderr output.
This is the biggest change. Whenever we start a long running process (via `spawn(…)`), we will ensure to wait for specific message on `stdout` or `stderr` from the process. These typically indicate when a process is ready (e.g.: initial build is done, or watchers are setup and we are waiting for file changes). This fixes race conditions where we already start writing files and expect a rebuild to happen before the process is ready. These messages can differ per command we run, so they are implemented on a per-test basis. Additionally, most long running processes that require a dev server expose the URL (host + port) in the stdout/stderr. We will use that information instead of using the `getFreePort()` that we used to use. The big benefit is that these processes typically already have conflict erroring, and try until a free port is available. Because most tests are concurrent, we also use the `expect` exposed from the test helper.
8bcbff8 to
cfd371a
Compare
With the split in CI, properly waiting for dev servers to be ready and properly handling ANSI escapes, it seems like we don't even need sequential tests at all. Going to undo all of that in this commit, so we can re-enable it later if needed.
24581c4 to
a266124
Compare
182b21f to
6edba4e
Compare
| let process = await spawn( | ||
| 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch', | ||
| ) | ||
| await process.onStderr((m) => m.includes('Done in')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All spawn(…) tests will wait for a specific message to ensure we wait for everything to be ready before making changes. This improves the reliability of the tests.
| @@ -491,7 +492,7 @@ test( | |||
| 'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]', | |||
| }, | |||
| }, | |||
| async ({ fs, exec }) => { | |||
| async ({ fs, exec, expect }) => { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to the concurrency, we have to use the expect provided by the test itself instead of the globally imported expect.
| @@ -4,7 +4,6 @@ | |||
| "private": true, | |||
| "devDependencies": { | |||
| "dedent": "1.5.3", | |||
| "fast-glob": "^3.3.2", | |||
| "kill-port": "^2.0.1" | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We removed the getFreePort() which relied on the kill-port package.
| let url = '' | ||
| await process.onStdout((m) => { | ||
| let match = /Local:\s*(http.*)/.exec(m) | ||
| if (match) url = match[1] | ||
| return Boolean(url) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can potentially abstract this in a function but the regex is different based on the project. It also depends on the project if stdout or stderr is used. Because this is only a handful of lines, I just duplicated them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems fine to me.
| test.skip = (name: string, config: TestConfig, testCallback: TestCallback) => { | ||
| return test(name, config, testCallback, { skip: true }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Introduced this during debugging and while this is not used right now, I think this is still useful (similar to how .only and .debug is not used right now).
| let process = await spawn('pnpm vite build --watch', { | ||
| cwd: path.join(root, 'project-a'), | ||
| }) | ||
| await process.onStdout((m) => m.includes('built in')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example where we weren't properly waiting for the initial build to be ready.
| test('dev mode', SETUP, async ({ fs, spawn, expect }) => { | ||
| let process = await spawn('pnpm nuxt dev', { | ||
| env: { | ||
| TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vitest (I think) sets a process.env.TEST environment variable. If this is set to true, then nuxt dev doesn't show any output. Why? I'm not sure, but it was confusing because it worked while running manually. It also worked with copying the spawn() setup in a custom whatisgoingon.js file...
integrations/vite/resolvers.test.ts
Outdated
| @@ -1,143 +1,148 @@ | |||
| import { describe, expect } from 'vitest' | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test still uses a global expect — this is probably fine but maybe we should use the one from the test for consistency.
This PR improves the integration tests in two ways:
Tried a lot of different things to make sure that these tests are fast and stable.
The biggest issue we noticed is that some tests are flakey, these are tests with long running dev-mode processes where watchers are being used and/or dev servers are created.
To solve this, all the tests that spawn a process look at stdout/stderr and wait for a message from the process to know whether we can start making changes.
For example, in case of an Astro project, you get a
watching for file changesmessage. In case of Nuxt project you can wait for anserver warmed up inand in case of Next.js there is aReady inmessage.These depend on the tools being used, so this is hardcoded per test instead of a magically automatic solution.
These messages allow us to wait until all the initial necessary work, internal watchers and/or dev servers are setup before we start making changes to the files and/or request CSS stylesheets before the server(s) are ready.
Another improvement is how we setup the dev servers. Before, we used to try and get a free port on the system and use a
--portflag or aPORTenvironment variable. Instead of doing this (which is slow), we rely on the process itself to show a URL with a port. Basically all tools will try to find a free port if the default port is in use. We can then use the stdout/stderr messages to get the URL and the port to use.To reduce the amount of potential conflicts in ports, we used to run every test and every file sequentially to basically guarantee that ports are free. With this new approach where we rely on the process, I noticed that we don't really run into this issue again (I reran the tests multiple times and they were always stable)
We do make the tests concurrent by default now, which in theory means that there could be conflicts (which in practice means that the process has to do a few more tries to find a free port). To reduce these conflicts, we split up the integration tests such that Vite, PostCSS, CLI, … tests all run in a separate job in the GitHub actions workflow.
Comparing this branch against the
nextbranch, this is what CI looks like right now:nextfeat/improve-integration-testsThere also was a point in time where I introduced sequential tests such that all spawned processes still run after each other, but so far I didn't run into issues if we keep them concurrent so I dropped that code.
Some small changes I made to make things more reliable:
\nand we strip all the ANSI escapes which allows us to not worry about special ANSI characters when finding the URL or a specific message to wait for.child.kill()the spawned process. If that doesn't work, for whatever reason, we run achild.kill('SIGKILL')to force kill the process. This could technically lead to some memory or files not being cleaned up properly, but once CI is done, everything is thrown away anyway.nextfeat/improve-integration-testsThey also look a bit nicer in the PR overview as well:

The very last commit just filters out Windows and macOS tests again for PRs (but they are executed on the
nextbranch.Nest steps
I think for now we are in a pretty good state, but there are some things we can do to further improve everything (mainly make things faster) but aren't necessary. I also ran into issue while trying it so there is more work to do.
buildstep can be full turbo (e.g.: after a PR is merged innextand we run everything again)