Skip to content

Commit 98f973d

Browse files
committed
Finalize feature and testing
1 parent 90be1ad commit 98f973d

File tree

5 files changed

+251
-95
lines changed

5 files changed

+251
-95
lines changed

global.d.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,14 @@ declare global {
2020
*/
2121
BUILD_PLUGINS_ENV?: Env;
2222
/**
23-
* Enable the dev server of our synthetics plugin.
23+
* The port of the dev server of our synthetics plugin.
2424
*
25-
* This is only used by datadog-ci, that will trigger a build,
26-
* using the customer's build command, which, if it includes our plugin,
25+
* This is only used by datadog-ci, in its build'n test workflow,
26+
* using the customer's build command, if it includes our plugin,
2727
* will launch a dev-server over the outdir of the build so datadog-ci
2828
* can trigger a tunnel and a test batch over the branch's code.
2929
*
3030
*/
31-
BUILD_PLUGINS_S8S_LOCAL?: '1';
32-
/**
33-
* The port of the dev server of our synthetics plugin.
34-
*/
3531
BUILD_PLUGINS_S8S_PORT?: string;
3632
/**
3733
* Defined in github actions when running in CI.

packages/plugins/synthetics/src/index.test.ts

+194-35
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
import { runServer } from '@dd/core/helpers/server';
66
import { API_PREFIX, DEFAULT_PORT } from '@dd/synthetics-plugin/constants';
7+
import type { ServerResponse } from '@dd/synthetics-plugin/types';
78
import { getPlugins } from '@dd/synthetics-plugin';
89
import { getContextMock } from '@dd/tests/_jest/helpers/mocks';
10+
import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
11+
import fs from 'fs';
912
import nock from 'nock';
13+
import path from 'path';
1014

1115
jest.mock('@dd/core/helpers/server', () => {
1216
const original = jest.requireActual('@dd/core/helpers/server');
@@ -18,6 +22,48 @@ jest.mock('@dd/core/helpers/server', () => {
1822

1923
const runServerMocked = jest.mocked(runServer);
2024

25+
const getApiUrl = (port: number = DEFAULT_PORT) => `http://127.0.0.1:${port}`;
26+
const getInternalApiUrl = (port: number = DEFAULT_PORT) => `${getApiUrl(port)}/${API_PREFIX}`;
27+
const safeFetch = async (route: string, port: number) => {
28+
try {
29+
return await fetch(`${getInternalApiUrl(port)}${route}`);
30+
} catch (e) {
31+
// Do nothing.
32+
}
33+
};
34+
35+
// Wait for the local server to tell us that the build is complete.
36+
const waitingForBuild = (port: number, cb: (resp: ServerResponse) => void) => {
37+
return new Promise<void>((resolve, reject) => {
38+
// Stop the polling after 10 seconds.
39+
const timeout = setTimeout(() => {
40+
clearInterval(interval);
41+
reject(new Error('Timeout.'));
42+
}, 10000);
43+
44+
// Poll all the local servers until we get the build status.
45+
const interval = setInterval(async () => {
46+
const res = await safeFetch('/build-status', port);
47+
if (res?.ok) {
48+
const data = (await res.json()) as ServerResponse;
49+
cb(data);
50+
51+
if (['success', 'fail'].includes(data.status)) {
52+
clearInterval(interval);
53+
clearTimeout(timeout);
54+
55+
if (data.status === 'success') {
56+
resolve();
57+
}
58+
if (data.status === 'fail') {
59+
reject(new Error('Build failed.'));
60+
}
61+
}
62+
}
63+
}, 100);
64+
});
65+
};
66+
2167
describe('Synthetics Plugin', () => {
2268
describe('getPlugins', () => {
2369
test('Should not initialize the plugin if disabled', async () => {
@@ -40,71 +86,184 @@ describe('Synthetics Plugin', () => {
4086
nock.enableNetConnect('127.0.0.1');
4187
});
4288

43-
afterEach(async () => {
44-
// Kill the server.
45-
try {
46-
await fetch(`http://127.0.0.1:${DEFAULT_PORT}/${API_PREFIX}/kill`);
47-
} catch (e) {
48-
// Do nothing.
49-
}
50-
});
51-
5289
afterAll(() => {
5390
nock.cleanAll();
5491
nock.disableNetConnect();
5592
});
5693

5794
describe('to run or not to run', () => {
58-
afterEach(() => {
95+
afterEach(async () => {
5996
// Remove the variables we've set.
6097
delete process.env.BUILD_PLUGINS_S8S_LOCAL;
6198
delete process.env.BUILD_PLUGINS_S8S_PORT;
99+
100+
// Kill the server.
101+
await safeFetch('/kill', DEFAULT_PORT);
62102
});
103+
63104
const expectations = [
64105
{
65-
description: 'not run with no variables',
106+
description: 'not run with no env and no config',
66107
env: {},
108+
config: {},
67109
shouldRun: false,
68110
},
69111
{
70-
description: 'not run with missing port',
112+
description: 'run with port in env and no config',
71113
env: {
72-
BUILD_PLUGINS_S8S_LOCAL: '1',
114+
BUILD_PLUGINS_S8S_PORT: JSON.stringify(DEFAULT_PORT),
73115
},
74-
shouldRun: false,
116+
config: {},
117+
shouldRun: true,
118+
},
119+
{
120+
description: 'run with no variables and full config',
121+
env: {},
122+
config: {
123+
synthetics: {
124+
server: {
125+
run: true,
126+
port: DEFAULT_PORT,
127+
},
128+
},
129+
},
130+
shouldRun: true,
131+
},
132+
{
133+
description: 'run with no variables and just config.run',
134+
env: {},
135+
config: {
136+
synthetics: {
137+
server: {
138+
run: true,
139+
},
140+
},
141+
},
142+
shouldRun: true,
75143
},
76144
{
77-
description: 'not run with missing local',
145+
description: 'not run with config.run false',
78146
env: {
79147
BUILD_PLUGINS_S8S_PORT: JSON.stringify(DEFAULT_PORT),
80148
},
149+
config: {
150+
synthetics: {
151+
server: {
152+
run: false,
153+
},
154+
},
155+
},
81156
shouldRun: false,
82157
},
83158
{
84-
description: 'run with both variables',
85-
env: {
86-
BUILD_PLUGINS_S8S_PORT: JSON.stringify(DEFAULT_PORT),
87-
BUILD_PLUGINS_S8S_LOCAL: '1',
159+
description: 'not run with disabled and config.run',
160+
env: {},
161+
config: {
162+
synthetics: {
163+
disabled: true,
164+
server: {
165+
run: true,
166+
},
167+
},
88168
},
89-
shouldRun: true,
169+
shouldRun: false,
90170
},
91171
];
92172

93-
test.each(expectations)(
94-
'Should $description.',
95-
async ({ description, env, shouldRun }) => {
96-
// Set the variables.
97-
Object.assign(process.env, env);
98-
// Run the plugin.
99-
getPlugins({}, getContextMock());
100-
// Check the server.
101-
if (shouldRun) {
102-
expect(runServerMocked).toHaveBeenCalled();
103-
} else {
104-
expect(runServerMocked).not.toHaveBeenCalled();
105-
}
106-
},
107-
);
173+
test.each(expectations)('Should $description.', async ({ config, env, shouldRun }) => {
174+
// Set the variables.
175+
Object.assign(process.env, env);
176+
// Run the plugin.
177+
const [plugin] = getPlugins(config, getContextMock());
178+
if (plugin?.bundlerReport) {
179+
// Trigger the bundlerReport hook where the server starts.
180+
plugin.bundlerReport(getContextMock().bundler);
181+
}
182+
// Check the server.
183+
if (shouldRun) {
184+
expect(runServerMocked).toHaveBeenCalled();
185+
} else {
186+
expect(runServerMocked).not.toHaveBeenCalled();
187+
}
188+
});
189+
});
190+
191+
// We need to loop over bundlers because we'll use a different port for each one of them
192+
// to avoid port conflicts.
193+
describe.each(BUNDLERS)('$name', (bundler) => {
194+
// Get an incremental port to prevent conflicts.
195+
const port = DEFAULT_PORT + BUNDLERS.indexOf(bundler);
196+
197+
let buildProm: Promise<any>;
198+
let outDir: string;
199+
const serverResponses: Set<ServerResponse> = new Set();
200+
201+
beforeAll(async () => {
202+
// Run the builds.
203+
// Do not await the promise as the server will be running.
204+
buildProm = runBundlers(
205+
{
206+
synthetics: {
207+
server: {
208+
run: true,
209+
port,
210+
},
211+
},
212+
// Use a custom plugin to get the cwd and outdir of the build.
213+
customPlugins: () => [
214+
{
215+
name: 'get-outdirs',
216+
bundlerReport: (report) => {
217+
outDir = report.outDir;
218+
},
219+
},
220+
],
221+
},
222+
undefined,
223+
[bundler.name],
224+
);
225+
226+
// Instead, wait for the server to tell us that the build is complete.
227+
await waitingForBuild(port, (resp) => {
228+
serverResponses.add(resp);
229+
});
230+
});
231+
232+
afterAll(async () => {
233+
await safeFetch('/kill', port);
234+
// Wait for the build to finish now that the server is killed.
235+
if (buildProm) {
236+
await buildProm;
237+
}
238+
});
239+
240+
test('Should report the build status.', async () => {
241+
// Verify that we have the running and success statuses.
242+
const reportedStatus = Array.from(serverResponses).filter((resp) =>
243+
['fail', 'success', 'running'].includes(resp.status),
244+
);
245+
expect(reportedStatus.length).toBeGreaterThan(0);
246+
});
247+
248+
test('Should report the outDir.', async () => {
249+
// Verify that we have the running and success statuses.
250+
const reportedOutDirs = new Set(
251+
Array.from(serverResponses).map((resp) => resp.outDir),
252+
);
253+
// We should have only one outDir.
254+
expect(reportedOutDirs.size).toBe(1);
255+
// It should be the same as the one we reported from the build.
256+
expect(reportedOutDirs.values().next().value).toEqual(outDir);
257+
});
258+
259+
test('Should actually serve the built files.', async () => {
260+
// Query a file from the server.
261+
const res = await fetch(`${getApiUrl(port)}/main.js`);
262+
expect(res.ok).toBe(true);
263+
const text = await res.text();
264+
// Confirm that the file served by the server is the same as the one on disk.
265+
expect(text).toEqual(fs.readFileSync(path.join(outDir, 'main.js'), 'utf-8'));
266+
});
108267
});
109268
});
110269
});

packages/plugins/synthetics/src/index.ts

+33-32
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { runServer } from '@dd/core/helpers/server';
66
import type { GlobalContext, GetPlugins, Options } from '@dd/core/types';
77
import { CONFIG_KEY as ERROR_TRACKING } from '@dd/error-tracking-plugin';
88
import { API_PREFIX, CONFIG_KEY, PLUGIN_NAME } from '@dd/synthetics-plugin/constants';
9-
import type { BuildStatus, SyntheticsOptions } from '@dd/synthetics-plugin/types';
9+
import type { ServerResponse, SyntheticsOptions } from '@dd/synthetics-plugin/types';
1010
import { validateOptions } from '@dd/synthetics-plugin/validate';
1111
import chalk from 'chalk';
1212

@@ -26,49 +26,50 @@ export const getPlugins: GetPlugins = (opts: Options, context: GlobalContext) =>
2626
return [];
2727
}
2828

29-
const response: { outDir?: string; publicPath?: string; status: BuildStatus } = {
29+
const response: ServerResponse = {
3030
publicPath: opts[ERROR_TRACKING]?.sourcemaps?.minifiedPathPrefix,
3131
status: 'running',
3232
};
33+
3334
const getServerResponse = () => {
3435
return response;
3536
};
3637

37-
if (options.server.run) {
38-
const port = options.server.port;
39-
log.info(
40-
`Starting Synthetics local server on ${chalk.bold.cyan(`http://127.0.0.1:${port}`)}.`,
41-
);
42-
43-
const server = runServer({
44-
port,
45-
root: context.bundler.outDir,
46-
routes: {
47-
[`/${API_PREFIX}/build-status`]: {
48-
get: (req, res) => {
49-
res.writeHead(200, { 'Content-Type': 'application/json' });
50-
res.end(JSON.stringify(getServerResponse()));
51-
},
52-
},
53-
[`/${API_PREFIX}/kill`]: {
54-
get: (req, res) => {
55-
res.writeHead(200, { 'Content-Type': 'text/html' });
56-
res.end('ok');
57-
// kill kill kill.
58-
server.close();
59-
server.closeAllConnections();
60-
server.closeIdleConnections();
61-
},
62-
},
63-
},
64-
});
65-
}
66-
6738
return [
6839
{
6940
name: PLUGIN_NAME,
41+
// Wait for us to have the bundler report to start the server over the outDir.
7042
bundlerReport(bundlerReport) {
7143
response.outDir = bundlerReport.outDir;
44+
if (options.server.run) {
45+
const port = options.server.port;
46+
log.debug(
47+
`Starting Synthetics local server on ${chalk.bold.cyan(`http://127.0.0.1:${port}`)}.`,
48+
);
49+
50+
const server = runServer({
51+
port,
52+
root: options.server.root || response.outDir,
53+
routes: {
54+
[`/${API_PREFIX}/build-status`]: {
55+
get: (req, res) => {
56+
res.writeHead(200, { 'Content-Type': 'application/json' });
57+
res.end(JSON.stringify(getServerResponse()));
58+
},
59+
},
60+
[`/${API_PREFIX}/kill`]: {
61+
get: (req, res) => {
62+
res.writeHead(200, { 'Content-Type': 'text/html' });
63+
res.end('ok');
64+
// kill kill kill.
65+
server.close();
66+
server.closeAllConnections();
67+
server.closeIdleConnections();
68+
},
69+
},
70+
},
71+
});
72+
}
7273
},
7374
buildReport(buildReport) {
7475
if (buildReport.errors.length) {

0 commit comments

Comments
 (0)