Skip to content

Commit 84d007e

Browse files
authored
fix(e2e): Fix various issues with concurrent E2E and Canary tests (#7805)
1 parent 5d58d52 commit 84d007e

30 files changed

+129
-54
lines changed

.github/workflows/build.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,7 @@ jobs:
749749
yarn test:integration:ci
750750
751751
job_e2e_tests:
752-
name: E2E Tests
752+
name: E2E Tests (Shard ${{ matrix.shard }})
753753
# We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks
754754
# Dependabot PRs sadly also don't have access to secrets, so we skip them as well
755755
if:
@@ -758,6 +758,10 @@ jobs:
758758
needs: [job_get_metadata, job_build]
759759
runs-on: ubuntu-20.04
760760
timeout-minutes: 20
761+
strategy:
762+
fail-fast: false
763+
matrix:
764+
shard: [1, 2, 3]
761765
steps:
762766
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
763767
uses: actions/checkout@v3
@@ -782,6 +786,8 @@ jobs:
782786
E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
783787
E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks'
784788
E2E_TEST_SENTRY_TEST_PROJECT: 'sentry-javascript-e2e-tests'
789+
E2E_TEST_SHARD: ${{ matrix.shard }}
790+
E2E_TEST_SHARD_AMOUNT: 3
785791
run: |
786792
cd packages/e2e-tests
787793
yarn test:e2e

.github/workflows/canary.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
job_canary_test:
2121
name: Canary Tests
2222
runs-on: ubuntu-20.04
23-
timeout-minutes: 30
23+
timeout-minutes: 60
2424
steps:
2525
- name: 'Check out current commit'
2626
uses: actions/checkout@v3

packages/e2e-tests/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ E2E_TEST_AUTH_TOKEN=
22
E2E_TEST_DSN=
33
E2E_TEST_SENTRY_ORG_SLUG=
44
E2E_TEST_SENTRY_TEST_PROJECT=
5+
E2E_TEST_SHARD= # optional
6+
E2E_TEST_SHARD_AMOUNT= # optional
7+
CANARY_E2E_TEST= # optional

packages/e2e-tests/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ To get you started with the recipe, you can copy the following into `test-recipe
5454
{
5555
"$schema": "../../test-recipe-schema.json",
5656
"testApplicationName": "My New Test Application",
57-
"buildCommand": "yarn install --network-concurrency 1",
57+
"buildCommand": "yarn install",
5858
"tests": [
5959
{
6060
"testName": "My new test",

packages/e2e-tests/lib/buildApp.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/* eslint-disable no-console */
22

33
import * as fs from 'fs-extra';
4+
import * as os from 'os';
45
import * as path from 'path';
56

67
import { DEFAULT_BUILD_TIMEOUT_SECONDS } from './constants';
78
import type { Env, RecipeInstance } from './types';
8-
import { spawnAsync } from './utils';
9+
import { prefixObjectKeys, spawnAsync } from './utils';
910

10-
export async function buildApp(appDir: string, recipeInstance: RecipeInstance, env: Env): Promise<void> {
11+
export async function buildApp(appDir: string, recipeInstance: RecipeInstance, envVars: Env): Promise<void> {
1112
const { recipe, label, dependencyOverrides } = recipeInstance;
1213

1314
const packageJsonPath = path.resolve(appDir, 'package.json');
@@ -28,13 +29,23 @@ export async function buildApp(appDir: string, recipeInstance: RecipeInstance, e
2829
if (recipe.buildCommand) {
2930
console.log(`Running build command for test application "${label}"`);
3031

32+
fs.mkdirSync(path.join(os.tmpdir(), 'e2e-test-yarn-caches'), { recursive: true });
33+
const tempYarnCache = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-test-yarn-caches', 'cache-'));
34+
35+
const env = {
36+
...process.env,
37+
...envVars,
38+
YARN_CACHE_FOLDER: tempYarnCache, // Use a separate yarn cache for each build commmand because multiple yarn commands running at the same time may corrupt the cache
39+
};
40+
3141
const buildResult = await spawnAsync(recipe.buildCommand, {
3242
cwd: appDir,
3343
timeout: (recipe.buildTimeoutSeconds ?? DEFAULT_BUILD_TIMEOUT_SECONDS) * 1000,
3444
env: {
35-
...process.env,
3645
...env,
37-
} as unknown as NodeJS.ProcessEnv,
46+
...prefixObjectKeys(env, 'NEXT_PUBLIC_'),
47+
...prefixObjectKeys(env, 'REACT_APP_'),
48+
},
3849
});
3950

4051
if (buildResult.error) {
@@ -57,9 +68,10 @@ export async function buildApp(appDir: string, recipeInstance: RecipeInstance, e
5768
cwd: appDir,
5869
timeout: (recipe.buildTimeoutSeconds ?? DEFAULT_BUILD_TIMEOUT_SECONDS) * 1000,
5970
env: {
60-
...process.env,
6171
...env,
62-
} as unknown as NodeJS.ProcessEnv,
72+
...prefixObjectKeys(env, 'NEXT_PUBLIC_'),
73+
...prefixObjectKeys(env, 'REACT_APP_'),
74+
},
6375
},
6476
buildResult.stdout,
6577
);

packages/e2e-tests/lib/buildRecipeInstances.ts renamed to packages/e2e-tests/lib/constructRecipeInstances.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ import * as fs from 'fs';
22

33
import type { Recipe, RecipeInput, RecipeInstance } from './types';
44

5-
export function buildRecipeInstances(recipePaths: string[]): RecipeInstance[] {
5+
export function constructRecipeInstances(recipePaths: string[]): RecipeInstance[] {
66
const recipes = buildRecipes(recipePaths);
7-
const recipeInstances: RecipeInstance[] = [];
7+
const recipeInstances: Omit<RecipeInstance, 'portModulo' | 'portGap'>[] = [];
88

9-
const basePort = 3001;
10-
11-
recipes.forEach((recipe, i) => {
9+
recipes.forEach(recipe => {
1210
recipe.versions.forEach(version => {
1311
const dependencyOverrides =
1412
Object.keys(version.dependencyOverrides).length > 0 ? version.dependencyOverrides : undefined;
@@ -20,12 +18,19 @@ export function buildRecipeInstances(recipePaths: string[]): RecipeInstance[] {
2018
label: `${recipe.testApplicationName}${dependencyOverridesInformationString}`,
2119
recipe,
2220
dependencyOverrides,
23-
port: basePort + i,
2421
});
2522
});
2623
});
2724

28-
return recipeInstances;
25+
return recipeInstances
26+
.map((instance, i) => ({ ...instance, portModulo: i, portGap: recipeInstances.length }))
27+
.filter((_, i) => {
28+
if (process.env.E2E_TEST_SHARD && process.env.E2E_TEST_SHARD_AMOUNT) {
29+
return (i + Number(process.env.E2E_TEST_SHARD)) % Number(process.env.E2E_TEST_SHARD_AMOUNT) === 0;
30+
} else {
31+
return true;
32+
}
33+
});
2934
}
3035

3136
function buildRecipes(recipePaths: string[]): Recipe[] {

packages/e2e-tests/lib/runAllTestApps.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
/* eslint-disable no-console */
2-
import { buildRecipeInstances } from './buildRecipeInstances';
2+
import * as fs from 'fs';
3+
import * as os from 'os';
4+
import * as path from 'path';
5+
6+
import { constructRecipeInstances } from './constructRecipeInstances';
37
import { buildAndTestApp } from './runTestApp';
48
import type { RecipeInstance, RecipeTestResult } from './types';
59

610
export async function runAllTestApps(
711
recipePaths: string[],
812
envVarsToInject: Record<string, string | undefined>,
913
): Promise<void> {
10-
const maxParallel = process.env.CI ? 2 : 5;
14+
const maxParallel = process.env.CI ? 1 : 1; // For now we are disabling parallel execution because it was causing problems (runners were too slow and timeouts happened)
1115

12-
const recipeInstances = buildRecipeInstances(recipePaths);
16+
const recipeInstances = constructRecipeInstances(recipePaths);
1317

1418
const results = await shardPromises(
1519
recipeInstances,
@@ -33,6 +37,8 @@ export async function runAllTestApps(
3337

3438
const failed = results.filter(result => result.buildFailed || result.testFailed);
3539

40+
fs.rmSync(path.join(os.tmpdir(), 'e2e-test-yarn-caches'), { force: true, recursive: true });
41+
3642
if (failed.length) {
3743
console.log(`${failed.length} test(s) failed.`);
3844
process.exit(1);

packages/e2e-tests/lib/runTestApp.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export async function buildAndTestApp(
1515
recipeInstance: RecipeInstance,
1616
envVarsToInject: Record<string, string | undefined>,
1717
): Promise<RecipeTestResult> {
18-
const { recipe, port } = recipeInstance;
18+
const { recipe, portModulo, portGap } = recipeInstance;
1919
const recipeDirname = path.dirname(recipe.path);
2020

2121
const targetDir = path.join(TMP_DIR, `${recipe.testApplicationName}-${tmpDirCount++}`);
@@ -24,7 +24,8 @@ export async function buildAndTestApp(
2424

2525
const env: Env = {
2626
...envVarsToInject,
27-
PORT: port.toString(),
27+
PORT_MODULO: portModulo.toString(),
28+
PORT_GAP: portGap.toString(),
2829
};
2930

3031
try {

packages/e2e-tests/lib/testApp.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { DEFAULT_TEST_TIMEOUT_SECONDS } from './constants';
44
import type { Env, RecipeInstance, TestDef, TestResult } from './types';
5-
import { spawnAsync } from './utils';
5+
import { prefixObjectKeys, spawnAsync } from './utils';
66

77
export async function testApp(appDir: string, recipeInstance: RecipeInstance, env: Env): Promise<TestResult[]> {
88
const { recipe } = recipeInstance;
@@ -15,17 +15,28 @@ export async function testApp(appDir: string, recipeInstance: RecipeInstance, en
1515
return results;
1616
}
1717

18-
async function runTest(appDir: string, recipeInstance: RecipeInstance, test: TestDef, env: Env): Promise<TestResult> {
18+
async function runTest(
19+
appDir: string,
20+
recipeInstance: RecipeInstance,
21+
test: TestDef,
22+
envVars: Env,
23+
): Promise<TestResult> {
1924
const { recipe, label } = recipeInstance;
2025
console.log(`Running test command for test application "${label}", test "${test.testName}"`);
2126

27+
const env = {
28+
...process.env,
29+
...envVars,
30+
};
31+
2232
const testResult = await spawnAsync(test.testCommand, {
2333
cwd: appDir,
2434
timeout: (recipe.testTimeoutSeconds ?? DEFAULT_TEST_TIMEOUT_SECONDS) * 1000,
2535
env: {
26-
...process.env,
2736
...env,
28-
} as unknown as NodeJS.ProcessEnv,
37+
...prefixObjectKeys(env, 'NEXT_PUBLIC_'),
38+
...prefixObjectKeys(env, 'REACT_APP_'),
39+
},
2940
});
3041

3142
if (testResult.error) {

packages/e2e-tests/lib/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export interface RecipeInstance {
3737
label: string;
3838
recipe: Recipe;
3939
dependencyOverrides?: DependencyOverrides;
40-
port: number;
40+
portModulo: number;
41+
portGap: number;
4142
}
4243

4344
export interface RecipeTestResult extends RecipeInstance {

packages/e2e-tests/lib/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,13 @@ export function spawnAsync(
6767
}
6868
});
6969
}
70+
71+
export function prefixObjectKeys(
72+
obj: Record<string, string | undefined>,
73+
prefix: string,
74+
): Record<string, string | undefined> {
75+
return Object.keys(obj).reduce<Record<string, string | undefined>>((result, key) => {
76+
result[prefix + key] = obj[key];
77+
return result;
78+
}, {});
79+
}

packages/e2e-tests/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ async function run(): Promise<void> {
1818
const envVarsToInject = {
1919
REACT_APP_E2E_TEST_DSN: process.env.E2E_TEST_DSN,
2020
NEXT_PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN,
21+
BASE_PORT: '27496', // just some random port
2122
};
2223

2324
try {

packages/e2e-tests/test-applications/create-next-app/package.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
76
"build": "next build",
8-
"start": "next start",
9-
"lint": "next lint",
10-
"test": "test:prod && test:dev",
11-
"test:prod": "TEST_MODE=prod playwright test",
12-
"test:dev": "TEST_MODE=dev playwright test"
7+
"test:prod": "TEST_ENV=prod playwright test",
8+
"test:dev": "TEST_ENV=dev playwright test"
139
},
1410
"dependencies": {
1511
"@next/font": "13.0.7",

packages/e2e-tests/test-applications/create-next-app/playwright.config.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import type { PlaywrightTestConfig } from '@playwright/test';
22
import { devices } from '@playwright/test';
33

4+
const testEnv = process.env.TEST_ENV;
5+
6+
if (!testEnv) {
7+
throw new Error('No test env defined');
8+
}
9+
10+
const port = Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO);
11+
412
/**
513
* See https://playwright.dev/docs/test-configuration.
614
*/
@@ -59,8 +67,8 @@ const config: PlaywrightTestConfig = {
5967

6068
/* Run your local dev server before starting the tests */
6169
webServer: {
62-
command: process.env.TEST_MODE === 'prod' ? 'yarn start' : 'yarn dev',
63-
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
70+
command: testEnv === 'development' ? `yarn next dev -p ${port}` : `yarn next start -p ${port}`,
71+
port,
6472
},
6573
};
6674

packages/e2e-tests/test-applications/create-next-app/test-recipe.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "../../test-recipe-schema.json",
33
"testApplicationName": "create-next-app",
4-
"buildCommand": "yarn install --network-concurrency 1 && npx playwright install && yarn build",
4+
"buildCommand": "yarn install && npx playwright install && yarn build",
55
"tests": [
66
{
77
"testName": "Playwright tests - Prod Mode",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "../../test-recipe-schema.json",
33
"testApplicationName": "create-react-app",
4-
"buildCommand": "yarn install --network-concurrency 1 && yarn build",
4+
"buildCommand": "yarn install && yarn build",
55
"tests": []
66
}

packages/e2e-tests/test-applications/nextjs-app-dir/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
76
"build": "next build",
8-
"start": "next start",
9-
"lint": "next lint",
107
"test:prod": "TEST_ENV=production playwright test",
118
"test:dev": "TEST_ENV=development playwright test"
129
},

packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ if (!testEnv) {
77
throw new Error('No test env defined');
88
}
99

10-
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
10+
const port = Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO);
1111

1212
/**
1313
* See https://playwright.dev/docs/test-configuration.
@@ -55,12 +55,12 @@ const config: PlaywrightTestConfig = {
5555
/* Run your local dev server before starting the tests */
5656
webServer: [
5757
{
58-
command: testEnv === 'development' ? 'yarn dev' : 'yarn start',
58+
command: testEnv === 'development' ? `yarn next dev -p ${port}` : `yarn next start -p ${port}`,
5959
port,
6060
},
6161
{
6262
command: 'yarn ts-node-script start-event-proxy.ts',
63-
port: 27496,
63+
port: Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO) + Number(process.env.PORT_GAP),
6464
},
6565
],
6666
};

packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import * as Sentry from '@sentry/nextjs';
22

33
Sentry.init({
44
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
5-
tunnel: 'http://localhost:27496/', // proxy server
5+
tunnel: `http://localhost:${
6+
Number(process.env.NEXT_PUBLIC_BASE_PORT) +
7+
Number(process.env.NEXT_PUBLIC_PORT_MODULO) +
8+
Number(process.env.NEXT_PUBLIC_PORT_GAP)
9+
}/`, // proxy server
610
tracesSampleRate: 1.0,
711
});

packages/e2e-tests/test-applications/nextjs-app-dir/sentry.edge.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import * as Sentry from '@sentry/nextjs';
22

33
Sentry.init({
44
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
5-
tunnel: 'http://localhost:27496/', // proxy server
5+
tunnel: `http://localhost:${
6+
Number(process.env.NEXT_PUBLIC_BASE_PORT) +
7+
Number(process.env.NEXT_PUBLIC_PORT_MODULO) +
8+
Number(process.env.NEXT_PUBLIC_PORT_GAP)
9+
}/`, // proxy server
610
tracesSampleRate: 1.0,
711
});

0 commit comments

Comments
 (0)