Skip to content

Commit b32b0c9

Browse files
test: make the generator and dev server tests stable (#4885)
* test: make the generator and dev server tests stable The `create-webpack-app` tests install real packages, and a registry hiccup made npm fail, which the tests only reported as a missing line in stdout. npm now prefers its cache and retries requests more, without the audit, fund and update-notifier requests, and a failed install fails the test with npm's error. The dev server tests took ports from the ephemeral range, where outgoing connections or another Jest worker could take them before the server listened. Each worker now takes ports from its own range below the ephemeral ones. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * test: keep npm's default retries for the generator tests More retries made a failing install outlast the test timeout, which then hid npm's error. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * test: keep the port ranges of Jest workers within valid ports Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * test: give up to 120 Jest workers their own port range Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * fix: don't detect browsers in `webpack info` on Windows On Windows, `envinfo` starts the browsers themselves to get their versions, which can hang, e.g. the `info` tests time out on Windows CI runners. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * test: don't let npm prefer its cache in the generator tests With `prefer-offline`, npm used cached metadata that lacked a version a freshly fetched package depended on, and the install failed with ETARGET (`@babel/generator@^8.0.10`) on Windows CI. Also add a changeset for not detecting browsers in `webpack info` on Windows. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * chore: move the `webpack info` changeset to #4847 Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * revert: detect browsers in `webpack info` on Windows again The `info` tests time out on Windows only at random, so keep detecting browsers there. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL * test: give each Jest worker its own Yarn cache in the generator tests The init, loader and plugin tests install with Yarn in parallel Jest workers. Yarn 1 can't fill one cache from two installs at the same time, and an install failed on macOS CI reading a half-written package.json from the shared cache. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL
1 parent 351f1c9 commit b32b0c9

6 files changed

Lines changed: 77 additions & 24 deletions

File tree

‎test/create-webpack-app/test.utils.js‎

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use strict";
22

3+
const os = require("node:os");
34
const path = require("node:path");
45
const testUtilsPkg = require("../utils/test-utils");
56

@@ -8,6 +9,35 @@ const nodeVersion = Number.parseInt(process.versions.node.split(".")[0], 10);
89
// eslint-disable-next-line jsdoc/reject-any-type
910
/** @typedef {Record<string, any>} TestOptions */
1011

12+
// The generators install real packages. Skip the requests the tests don't need, so fewer
13+
// requests can fail. `prefer-offline` isn't used: with a stale cache, npm misses new
14+
// versions that other packages already depend on.
15+
const packageManagerEnv = {
16+
npm_config_audit: "false",
17+
npm_config_fund: "false",
18+
npm_config_update_notifier: "false",
19+
// Yarn 1 breaks its cache when two installs fill it at the same time, as tests in parallel
20+
// Jest workers do, so each worker gets its own
21+
YARN_CACHE_FOLDER: path.join(
22+
os.tmpdir(),
23+
`webpack-cli-yarn-cache-${process.env.JEST_WORKER_ID || 1}`,
24+
),
25+
};
26+
27+
/**
28+
* Fail with the package manager's error, rather than with the output the test misses.
29+
* @template {{ stderr: string }} T
30+
* @param {T} result result of a CLI run
31+
* @returns {T} the same result
32+
*/
33+
const checkInstall = (result) => {
34+
if (result.stderr && result.stderr.includes("Error occurred while installing packages")) {
35+
throw new Error(`Installing packages failed:\n${result.stderr}`);
36+
}
37+
38+
return result;
39+
};
40+
1141
function createPathDependentUtils(cli) {
1242
const CLI_PATH = path.resolve(__dirname, `../../packages/${cli}/bin/cli.js`);
1343

@@ -19,10 +49,13 @@ function createPathDependentUtils(cli) {
1949
* @returns {Promise<import("execa").Result>} child process
2050
*/
2151
const run = async (cwd, args = [], options = {}) =>
22-
testUtilsPkg.run(cwd, args, {
23-
...options,
24-
executorPath: CLI_PATH,
25-
});
52+
checkInstall(
53+
await testUtilsPkg.run(cwd, args, {
54+
...options,
55+
env: { ...packageManagerEnv, ...options.env },
56+
executorPath: CLI_PATH,
57+
}),
58+
);
2659

2760
/**
2861
* Run the webpack CLI in watch mode for a test case.
@@ -34,6 +67,7 @@ function createPathDependentUtils(cli) {
3467
const runWatch = async (cwd, args = [], options = {}) =>
3568
testUtilsPkg.runWatch(cwd, args, {
3669
...options,
70+
env: { ...packageManagerEnv, ...options.env },
3771
executorPath: CLI_PATH,
3872
});
3973

@@ -46,10 +80,13 @@ function createPathDependentUtils(cli) {
4680
* @returns {Promise<{ stdout: string, stderr: string }>} result
4781
*/
4882
const runPromptWithAnswers = async (cwd, args, answers = [], options = {}) =>
49-
testUtilsPkg.runPromptWithAnswers(cwd, args, answers, {
50-
...options,
51-
executorPath: CLI_PATH,
52-
});
83+
checkInstall(
84+
await testUtilsPkg.runPromptWithAnswers(cwd, args, answers, {
85+
...options,
86+
env: { ...packageManagerEnv, ...options.env },
87+
executorPath: CLI_PATH,
88+
}),
89+
);
5390

5491
return { run, runWatch, runPromptWithAnswers };
5592
}

‎test/serve/basic/multi-dev-server.config.js‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
const { getFreePort } = require("../../utils/test-utils");
12
const WebpackCLITestPlugin = require("../../utils/webpack-cli-test-plugin");
23
const { devServerConfig } = require("./helper/base-dev-server.config");
34

4-
const getGetPort = () => import("get-port");
5-
65
module.exports = async () => {
7-
const port1 = await (await getGetPort()).default();
8-
const port2 = await (await getGetPort()).default();
6+
const port1 = await getFreePort();
7+
const port2 = await getFreePort();
98

109
return [
1110
{

‎test/serve/basic/serve-basic.test.js‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"use strict";
22

33
const path = require("node:path");
4-
const { normalizeStderr, normalizeStdout, runWatch } = require("../../utils/test-utils");
5-
6-
const getGetPort = () => import("get-port");
4+
const {
5+
getFreePort,
6+
normalizeStderr,
7+
normalizeStdout,
8+
runWatch,
9+
} = require("../../utils/test-utils");
710

811
const testPath = path.resolve(__dirname);
912

@@ -16,7 +19,7 @@ describe("basic serve usage", () => {
1619
let port;
1720

1821
beforeEach(async () => {
19-
port = await (await getGetPort()).default();
22+
port = await getFreePort();
2023
});
2124

2225
it("should work", async () => {

‎test/serve/serve-variable/serve-variable.test.js‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"use strict";
22

33
const path = require("node:path");
4-
const { normalizeStderr, runWatch } = require("../../utils/test-utils");
5-
6-
const getGetPort = () => import("get-port");
4+
const { getFreePort, normalizeStderr, runWatch } = require("../../utils/test-utils");
75

86
const testPath = path.resolve(__dirname);
97

108
describe("serve variable", () => {
119
let port;
1210

1311
beforeEach(async () => {
14-
port = await (await getGetPort()).default();
12+
port = await getFreePort();
1513
});
1614

1715
it("compiles without flags and export variable", async () => {

‎test/serve/with-custom-port/serve-custom-config.test.js‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"use strict";
22

33
const path = require("node:path");
4-
const { normalizeStderr, runWatch } = require("../../utils/test-utils");
5-
6-
const getGetPort = () => import("get-port");
4+
const { getFreePort, normalizeStderr, runWatch } = require("../../utils/test-utils");
75

86
const testPath = path.resolve(__dirname);
97

108
describe("serve with devServer in config", () => {
119
let port;
1210

1311
beforeEach(async () => {
14-
port = await (await getGetPort()).default();
12+
port = await getFreePort();
1513
});
1614

1715
it("should pick up the host and port from config", async () => {

‎test/utils/test-utils.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,23 @@ const cleanStderr = (stderr) =>
401401
? stderr.replaceAll(NODE_WARNING_RE, "").replaceAll(INFRASTRUCTURE_INFO_RE, "").trim()
402402
: stderr;
403403

404+
/**
405+
* Get a free port for a dev server.
406+
*
407+
* A port from the OS comes from the ephemeral range, where outgoing connections (e.g. of
408+
* tests that install packages) can take it before the dev server listens, and Jest
409+
* workers running in parallel can get the same one. So each worker takes ports from its
410+
* own range below the ephemeral ones.
411+
* @returns {Promise<number>} free port
412+
*/
413+
const getFreePort = async () => {
414+
const { default: getPort, portNumbers } = await import("get-port");
415+
// 120 ranges keep ports below 32768, where the ephemeral range of Linux starts
416+
const from = 20000 + ((Number(process.env.JEST_WORKER_ID || 1) - 1) % 120) * 100;
417+
418+
return getPort({ port: portNumbers(from, from + 99) });
419+
};
420+
404421
const uniqueDirectoryForTest = async () => {
405422
const result = path.resolve(path.resolve(__dirname, "../create-webpack-app-testing"), uuid());
406423

@@ -413,6 +430,7 @@ const uniqueDirectoryForTest = async () => {
413430

414431
module.exports = {
415432
cleanStderr,
433+
getFreePort,
416434
getWebpackCliArguments,
417435
hyphenToUpperCase,
418436
isWindows,

0 commit comments

Comments
 (0)