8000 ref: Generalize integration tests runner (#3641) · sdemjanenko/sentry-javascript@843bc28 · GitHub
[go: up one dir, main page]

Skip to content

Commit 843bc28

Browse files
authored
ref: Generalize integration tests runner (getsentry#3641)
1 parent 172e478 commit 843bc28

22 files changed

+489
-515
lines changed
Lines changed: 35 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,41 @@
1-
const fs = require('fs').promises;
2-
const { createServer } = require('http');
3-
const { parse } = require('url');
41
const path = require('path');
5-
6-
const yargs = require('yargs/yargs');
7-
const next = require('next');
82
const puppeteer = require('puppeteer');
3+
const { run } = require('./runner');
4+
const { createNextServer, startServer } = require('./utils/common');
5+
const { createRequestInterceptor } = require('./utils/client');
96

10-
const {
11-
colorize,
12-
extractEnvelopeFromRequest,
13-
extractEventFromRequest,
14-
isEventRequest,
15-
isSentryRequest,
16-
isSessionRequest,
17-
isTransactionRequest,
18-
logIf,
19-
} = require('./utils');
20-
const { log } = console;
21-
22-
/**
23-
* client.js
24-
*
25-
* Start the test-runner
26-
*
27-
* Options:
28-
* --filter Filter scenarios based on filename (case-insensitive) [string]
29-
* --silent Hide all stdout and console logs except test results [boolean]
30-
* --debug Log intercepted requests and debug messages [boolean]
31-
* --depth Set the logging depth for intercepted requests [number]
32-
*/
33-
34-
const argv = yargs(process.argv.slice(2))
35-
.command('$0', 'Start the test-runner')
36-
.option('filter', {
37-
type: 'string',
38-
description: 'Filter scenarios based on filename (case-insensitive)',
39-
})
40-
.option('silent', {
41-
type: 'boolean',
42-
description: 'Hide all stdout and console logs except test results',
43-
})
44-
.option('debug', {
45-
type: 'boolean',
46-
description: 'Log intercepted requests and debug messages',
47-
})
48-
.option('depth', {
49-
type: 'number',
50-
description: 'Set the logging depth for intercepted requests',
51-
}).argv;
52-
53-
(async () => {
54-
let scenarios = await fs.readdir(path.resolve(__dirname, './client'));
55-
56-
if (argv.filter) {
57-
scenarios = scenarios.filter(file => file.toLowerCase().includes(argv.filter));
58-
}
59-
60-
if (scenarios.length === 0) {
61-
log('No test suites found');
62-
process.exit(0);
63-
} else {
64-
if (!argv.silent) {
65-
scenarios.forEach(s => log(`⊙ Test suites found: ${s}`));
66-
}
67-
}
68-
69-
// Silence all the unnecessary server noise. We are capturing errors manualy anyway.
70-
if (argv.silent) {
71-
console.log = () => {};
72-
console.error = () => {};
73-
}
74-
75-
const app = next({ dev: false, dir: path.resolve(__dirname, '..') });
76-
const handle = app.getRequestHandler();
77-
await app.prepare();
78-
const server = createServer((req, res) => handle(req, res, parse(req.url, true)));
79-
7+
const setup = async () => {
8+
const server = await createNextServer({ dev: false, dir: path.resolve(__dirname, '..') });
809
const browser = await puppeteer.launch({
8110
devtools: false,
8211
});
83-
84-
const success = await new Promise(resolve => {
85-
server.listen(0, err => {
86-
if (err) throw err;
87-
88-
const cases = scenarios.map(async testCase => {
89-
const page = await browser.newPage();
90-
page.setDefaultTimeout(2000);
91-
92-
page.on('console', msg => logIf(argv.debug, msg.text()));
93-
94-
// Capturing requests this way allows us to have a reproducible order,
95-
// where using `Promise.all([page.waitForRequest(isEventRequest), page.waitForRequest(isEventRequest)])` doesn't guarantee it.
96-
const testInput = {
97-
page,
98-
url: `http://localhost:${server.address().port}`,
99-
requests: {
100-
events: [],
101-
sessions: [],
102-
transactions: [],
103-
},
104-
};
105-
106-
await page.setRequestInterception(true);
107-
page.on('request', request => {
108-
if (
109-
isSentryRequest(request) ||
110-
// Used for testing http tracing
111-
request.url().includes('http://example.com')
112-
) {
113-
request.respond({
114-
status: 200,
115-
contentType: 'application/json',
116-
headers: {
117-
'Access-Control-Allow-Origin': '*',
118-
},
119-
});
120-
} else {
121-
request.continue();
122-
}
123-
124-
if (isEventRequest(request)) {
125-
logIf(argv.debug, 'Intercepted Event', extractEventFromRequest(request), argv.depth);
126-
testInput.requests.events.push(request);
127-
}
128-
129-
if (isSessionRequest(request)) {
130-
logIf(argv.debug, 'Intercepted Session', extractEnvelopeFromRequest(request), argv.depth);
131-
testInput.requests.sessions.push(request);
132-
}
133-
134-
if (isTransactionRequest(request)) {
135-
logIf(argv.debug, 'Intercepted Transaction', extractEnvelopeFromRequest(request), argv.depth);
136-
testInput.requests.transactions.push(request);
137-
}
138-
});
139-
140-
try {
141-
await require(`./client/${testCase}`)(testInput);
142-
log(colorize(`✓ Scenario succeded: ${testCase}`, 'green'));
143-
return true;
144-
} catch (error) {
145-
const testCaseFrames = error.stack.split('\n').filter(l => l.includes(testCase));
146-
if (testCaseFrames.length === 0) {
147-
log(error);
148-
return false;
149-
}
150-
/**
151-
* Find first frame that matches our scenario filename and extract line number from it, eg.:
152-
*
153-
* at assertObjectMatches (/test/integration/test/utils.js:184:7)
154-
* at module.exports.expectEvent (/test/integration/test/utils.js:122:10)
155-
* at module.exports (/test/integration/test/client/errorGlobal.js:6:3)
156-
*/
157-
const line = testCaseFrames[0].match(/.+:(\d+):/)[1];
158-
log(colorize(`X Scenario failed: ${testCase} (line: ${line})`, 'red'));
159-
log(error.message);
160-
return false;
161-
}
162-
});
163-
164-
Promise.all(cases).then(result => {
165-
// Awaiting server being correctly closed and resolving promise in it's callback
166-
// adds ~4-5sec overhead for some reason. It should be safe to skip it though.
167-
server.close();
168-
resolve(result.every(Boolean));
169-
});
170-
});
171-
});
172-
173-
await browser.close();
174-
175-
if (success) {
176-
log(colorize(`✓ All scenarios succeded`, 'green'));
177-
process.exit(0);
178-
} else {
179-
log(colorize(`X Some scenarios failed`, 'red'));
180-
process.exit(1);
181-
}
182-
})();
12+
return startServer(server, { browser });
13+
};
14+
15+
const teardown = async ({ browser, server }) => {
16+
return Promise.all([browser.close(), new Promise(resolve => server.close(resolve))]);
17+
};
18+
19+
const execute = async (scenario, env) => {
20+
// Capturing requests this way allows us to have a reproducible, guaranteed order, as `Promise.all` does not do that.
21+
// Eg. this won't be enough: `const [resp1, resp2] = Promise.all([page.waitForRequest(isEventRequest), page.waitForRequest(isEventRequest)])`
22+
env.requests = {
23+
events: [],
24+
sessions: [],
25+
transactions: [],
26+
};
27+
28+
const page = (env.page = await env.browser.newPage());
29+
await page.setRequestInterception(true);
30+
page.setDefaultTimeout(2000);
31+
page.on('request', createRequestInterceptor(env));
32+
33+
return scenario(env);
34+
};
35+
36+
run({
37+
setup,
38+
teardown,
39+
execute,
40+
scenariosDir: path.resolve(__dirname, './client'),
41+
});

packages/nextjs/test/integration/test/client/errorClick.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
const { expectRequestCount, waitForAll, isEventRequest, expectEvent } = require('../utils');
1+
const { waitForAll } = require('../utils/common');
2+
const { expectRequestCount, isEventRequest, expectEvent } = require('../utils/client');
23

34
module.exports = async ({ page, url, requests }) => {
5+
console.log(page, url, requests);
46
await page.goto(`${url}/errorClick`);
57

68
await waitForAll([page.click('button'), page.waitForRequest(isEventRequest)]);

packages/nextjs/test/integration/test/client/errorGlobal.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const { expectRequestCount, waitForAll, isEventRequest, expectEvent } = require('../utils');
1+
const { waitForAll } = require('../utils/common');
2+
const { expectRequestCount, isEventRequest, expectEvent } = require('../utils/client');
23

34
module.exports = async ({ page, url, requests }) => {
45
await waitForAll([page.goto(`${url}/crashed`), page.waitForRequest(isEventRequest)]);

packages/nextjs/test/integration/test/client/sessionCrashed.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const { expectRequestCount, waitForAll, isSessionRequest, expectSession } = require('../utils');
1+
const { waitForAll } = require('../utils/common');
2+
const { expectRequestCount, isSessionRequest, expectSession } = require('../utils/client');
23

34
module.exports = async ({ page, url, requests }) => {
45
await waitForAll([

packages/nextjs/test/integration/test/client/sessionHealthy.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const { expectRequestCount, waitForAll, expectSession, isSessionRequest } = require('../utils');
1+
const { waitForAll } = require('../utils/common');
2+
const { expectRequestCount, isSessionRequest, expectSession } = require('../utils/client');
23

34
module.exports = async ({ page, url, requests }) => {
45
await waitForAll([page.goto(`${url}/healthy`), page.waitForRequest(isSessionRequest)]);

packages/nextjs/test/integration/test/client/sessionNavigate.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const { expectRequestCount, waitForAll, isSessionRequest, expectSession, sleep } = require('../utils');
1+
const { sleep, waitForAll } = require('../utils/common');
2+
const { expectRequestCount, isSessionRequest, expectSession } = require('../utils/client');
23

34
module.exports = async ({ page, url, requests }) => {
45
await waitForAll([page.goto(`${url}/healthy`), page.waitForRequest(isSessionRequest)]);

packages/nextjs/test/integration/test/client/tracingDynamicRoute.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { expectRequestCount, isTransactionRequest, expectTransaction } = require('../utils');
1+
const { expectRequestCount, isTransactionRequest, expectTransaction } = require('../utils/client');
22

33
module.exports = async ({ page, url, requests }) => {
44
await page.goto(`${url}/users/102`);

packages/nextjs/test/integration/test/client/tracingFetch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { expectRequestCount, expectTransaction, isTransactionRequest } = require('../utils');
1+
const { expectRequestCount, isTransactionRequest, expectTransaction } = require('../utils/client');
22

33
module.exports = async ({ page, url, requests }) => {
44
await page.goto(`${url}/fetch`);

packages/nextjs/test/integration/test/client/tracingNavigate.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const { expectRequestCount, expectTransaction, isTransactionRequest, sleep } = require('../utils');
1+
const { sleep } = require('../utils/common');
2+
const { expectRequestCount, isTransactionRequest, expectTransaction } = require('../utils/client');
23

34
module.exports = async ({ page, url, requests }) => {
45
await page.goto(`${url}/healthy`);

packages/nextjs/test/integration/test/client/tracingPageLoad.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { expectRequestCount, expectTransaction, isTransactionRequest } = require('../utils');
1+
const { expectRequestCount, isTransactionRequest, expectTransaction } = require('../utils/client');
22

33
module.exports = async ({ page, url, requests }) => {
44
await page.goto(`${url}/healthy`);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const fs = require('fs').promises;
2+
const path = require('path');
3+
4+
const yargs = require('yargs/yargs');
5+
6+
const { colorize, verifyDir } = require('./utils/common');
7+
const { error, log } = console;
8+
9+
const argv = yargs(process.argv.slice(2))
10+
.option('filter', {
11+
type: 'string',
12+
description: 'Filter scenarios based on filename (case-insensitive)',
13+
})
14+
.option('silent', {
15+
type: 'boolean',
16+
description: 'Hide all stdout and console logs except test results',
17+
})
18+
.option('debug', {
19+
type: 'boolean',
20+
description: 'Log intercepted requests and debug messages',
21+
})
22+
.option('depth', {
23+
type: 'number',
24+
description: 'Set the logging depth for intercepted requests',
25+
}).argv;
26+
27+
const runScenario = async (scenario, execute, env) => {
28+
try {
29+
await execute(require(scenario), { ...env });
30+
log(colorize(`✓ Scenario succeded: ${path.basename(scenario)}`, 'green'));
31+
return true;
32+
} catch (error) {
33+
const scenarioFrames = error.stack.split('\n').filter(l => l.includes(scenario));
34+
35+
if (scenarioFrames.length === 0) {
36+
log(error);
37+
return false;
38+
}
39+
40+
/**
41+
* Find first frame that matches our scenario filename and extract line number from it, eg.:
42+
*
43+
* at assertObjectMatches (/test/integration/test/utils.js:184:7)
44+
* at module.exports.expectEvent (/test/integration/test/utils.js:122:10)
45+
* at module.exports (/test/integration/test/client/errorGlobal.js:6:3)
46+
*/
47+
const line = scenarioFrames[0].match(/.+:(\d+):/)[1];
48+
log(colorize(`X Scenario failed: ${path.basename(scenario)} (line: ${line})`, 'red'));
49+
log(error.message);
50+
return false;
51+
}
52+
};
53+
54+
const runScenarios = async (scenarios, execute, env) => {
55+
return Promise.all(scenarios.map(scenario => runScenario(scenario, execute, env)));
56+
};
57+
58+
module.exports.run = async ({
59+
setup = async () => {},
60+
teardown = async () => {},
61+
execute = async (scenario, env) => scenario(env),
62+
scenariosDir,
63+
}) => {
64+
try {
65+
await verifyDir(scenariosDir);
66+
67+
let scenarios = await fs.readdir(scenariosDir);
68+
if (argv.filter) {
69+
scenarios = scenarios.filter(file => file.toLowerCase().includes(argv.filter));
70+
}
71+
scenarios = scenarios.map(s => path.resolve(scenariosDir, s));
72+
73+
if (scenarios.length === 0) {
74+
log('No scenarios found');
75+
process.exit(0);
76+
} else {
77+
if (!argv.silent) {
78+
scenarios.forEach(s => log(`⊙ Scenario found: ${path.basename(s)}`));
79+
}
80+
}
81+
// Silence all the unnecessary server noise. We are capturing errors manualy anyway.
82+
if (argv.silent) {
83+
for (const level of ['log', 'warn', 'info', 'error']) {
84+
console[level] = () => {};
85+
}
86+
}
87+
88+
const env = {
89+
argv,
90+
...(await setup({ argv })),
91+
};
92+
const results = await runScenarios(scenarios, execute, env);
93+
const success = results.every(Boolean);
94+
await teardown(env);
95+
96+
if (success) {
97+
log(colorize(`✓ All scenarios succeded`, 'green'));
98+
process.exit(0);
99+
} else {
100+
log(colorize(`X Some scenarios failed`, 'red'));
101+
process.exit(1);
102+
}
103+
} catch (e) {
104+
error(e.message);
105+
process.exit(1);
106+
}
107+
};

0 commit comments

Comments
 (0)
0