Skip to content

Commit 3854370

Browse files
fix: assign random ports for e2e tests (#4676)
This is a follow on from #4653 This PR resolves flaky E2E tests caused by port collisions when running tests in parallel. Instead of deriving ports from package names (which can collide), this new approach assigns a random, available port to each test server instance. Key Changes: - Random Port Allocation: Each test server (Playwright, dummy servers) gets a unique, random port at startup. - File-based Sync: The server writes its assigned port to a port.txt file in its test project directory, this is kept unique through using the package name or another input string for the file name. - Test Runner Sync: The test runner reads the port from that file to build the correct URL. - Automatic Cleanup: All port files are deleted at the start of each test:e2e run to ensure fresh ports. Local parallel runs (up to 10) have shown this to be stable and free of collisions. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b3a83c1 commit 3854370

File tree

104 files changed

+141
-140
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+141
-140
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ coverage
2323
# tests
2424
packages/router-generator/tests/**/*.gen.ts
2525
packages/router-generator/tests/**/*.gen.js
26+
e2e/**/port*.txt
2627

2728
# misc
2829
.DS_Store
@@ -77,4 +78,4 @@ vite.config.ts.timestamp_*
7778

7879
**/llms
7980

80-
**/.tanstack
81+
**/.tanstack

e2e/e2e-utils/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@
1414
"./package.json": "./package.json"
1515
},
1616
"dependencies": {},
17-
"devDependencies": {}
17+
"devDependencies": {
18+
"get-port-please": "^3.2.0"
19+
}
1820
}

e2e/e2e-utils/src/derivePort.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,19 @@
1-
import * as crypto from 'node:crypto'
1+
import fs from 'node:fs'
2+
import { getRandomPort } from 'get-port-please'
23

34
/**
4-
* Hash a string and map it to a range [min, max].
5-
* @param {string} input - The string to hash.
6-
* @param {number} min - Minimum port value.
7-
* @param {number} max - Maximum port value.
8-
* @returns {number} A port within the range [min, max].
5+
* Check if a port has been allocated, if it hasn't generate a random port and save it.
6+
* @param {string} input - port test allocation
7+
* @returns {number} A random port.
98
*/
10-
export function derivePort(
11-
input: string,
12-
min: number = 5600,
13-
max: number = 65535,
14-
): number {
15-
// Hash the input using SHA-256
16-
const hash = crypto.createHash('sha256').update(input).digest('hex')
9+
export async function derivePort(input: string): Promise<number> {
10+
const portFile = `port-${input}.txt`
1711

18-
// Convert hash to an integer
19-
const hashInt = parseInt(hash.slice(0, 8), 16) // Use the first 8 characters
12+
if (!fs.existsSync(portFile)) {
13+
fs.writeFileSync(portFile, (await getRandomPort()).toString())
14+
}
2015

21-
// Map hash value to the port range
22-
const port = min + (hashInt % (max - min + 1))
23-
24-
console.info(`Mapped "${input}" to port ${port}`)
25-
26-
return port
16+
const portNumber = parseInt(await fs.promises.readFile(portFile, 'utf-8'))
17+
console.info(`Mapped "${input}" to port ${portNumber}`)
18+
return portNumber
2719
}

e2e/react-router/basic-esbuild-file-based/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"build": "esbuild src/main.tsx --bundle --outfile=dist/main.js && tsc --noEmit",
88
"serve": "esbuild src/main.tsx --bundle --outfile=dist/main.js --servedir=.",
99
"start": "dev",
10-
"test:e2e": "playwright test --project=chromium"
10+
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
1111
},
1212
"dependencies": {
1313
"@tanstack/react-router": "workspace:^",

e2e/react-router/basic-esbuild-file-based/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'
22
import { derivePort } from '@tanstack/router-e2e-utils'
33
import packageJson from './package.json' with { type: 'json' }
44

5-
const PORT = derivePort(packageJson.name)
5+
const PORT = await derivePort(packageJson.name)
66
const baseURL = `http://localhost:${PORT}`
77
/**
88
* See https://playwright.dev/docs/test-configuration.

e2e/react-router/basic-file-based-code-splitting/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"build": "vite build && tsc --noEmit",
99
"serve": "vite preview",
1010
"start": "vite",
11-
"test:e2e:verbose-routes:true": "VERBOSE_FILE_ROUTES=1 playwright test --project=chromium",
12-
"test:e2e:verbose-routes:false": "VERBOSE_FILE_ROUTES=0 playwright test --project=chromium",
13-
"test:e2e": "pnpm run test:e2e:verbose-routes:true && pnpm run test:e2e:verbose-routes:false"
11+
"test:e2e:verbose-routes:true": "rm -rf port*.txt; VERBOSE_FILE_ROUTES=1 playwright test --project=chromium",
12+
"test:e2e:verbose-routes:false": "rm -rf port*.txt; VERBOSE_FILE_ROUTES=0 playwright test --project=chromium",
13+
"test:e2e": "rm -rf port*.txt; pnpm run test:e2e:verbose-routes:true && pnpm run test:e2e:verbose-routes:false"
1414
},
1515
"dependencies": {
1616
"@tanstack/react-router": "workspace:^",

e2e/react-router/basic-file-based-code-splitting/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'
22
import { derivePort } from '@tanstack/router-e2e-utils'
33
import packageJson from './package.json' with { type: 'json' }
44

5-
const PORT = derivePort(packageJson.name)
5+
const PORT = await derivePort(packageJson.name)
66
const baseURL = `http://localhost:${PORT}`
77
/**
88
* See https://playwright.dev/docs/test-configuration.

e2e/react-router/basic-file-based/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build": "vite build && tsc --noEmit",
99
"serve": "vite preview",
1010
"start": "vite",
11-
"test:e2e": "playwright test --project=chromium"
11+
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
1212
},
1313
"dependencies": {
1414
"@tanstack/react-router": "workspace:^",

e2e/react-router/basic-file-based/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test'
22
import { derivePort } from '@tanstack/router-e2e-utils'
33
import packageJson from './package.json' with { type: 'json' }
44

5-
const PORT = derivePort(packageJson.name)
5+
const PORT = await derivePort(packageJson.name)
66
const baseURL = `http://localhost:${PORT}`
77
/**
88
* See https://playwright.dev/docs/test-configuration.

e2e/react-router/basic-file-based/tests/redirect.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import queryString from 'node:querystring'
88
// somehow playwright does not correctly import default exports
99
const combinate = (combinateImport as any).default as typeof combinateImport
1010

11-
const PORT = derivePort(packageJson.name)
12-
const EXTERNAL_HOST_PORT = derivePort(`${packageJson.name}-external`)
11+
const PORT = await derivePort(packageJson.name)
12+
const EXTERNAL_HOST_PORT = await derivePort(`${packageJson.name}-external`)
1313

1414
test.describe('redirects', () => {
1515
let server: Server

0 commit comments

Comments
 (0)