Skip to content

Commit 808280a

Browse files
add debugging and make operations non-blocking
1 parent 8a7bd2c commit 808280a

5 files changed

+185
-84
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
],
3535
"scripts": {
3636
"format": "prettier --write .",
37-
"test": "node --test --test-concurrency=3 --experimental-test-isolation=process \"test/**/*.test.js\""
37+
"test": "node --test \"test/**/*.test.js\""
3838
},
3939
"dependencies": {
4040
"shell-quote": "^1.8.1"

test/next-15-node-18.test.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { suite, test, before, after } from 'node:test';
2-
import { once } from 'node:events';
32
import { Fixture } from './util.js';
43

54
suite('Next.js v15 - Node.js v18', async () => {
65
const ctx = {};
76

87
before(async () => {
98
ctx.fixture = new Fixture({ nextMajor: '15', nodeMajor: '18' });
10-
await once(ctx.fixture, 'ready');
9+
await ctx.fixture.ready;
1110
ctx.rest = `http://${ctx.fixture.portMap.get('9926')}`;
1211
});
1312

@@ -23,7 +22,7 @@ suite('Next.js v15 - Node.js v18', async () => {
2322
t.assert.deepStrictEqual(json, { id: '0', name: 'Lincoln', breed: 'Shepherd' });
2423
});
2524

26-
after(() => {
27-
ctx.fixture.cleanup();
25+
after(async () => {
26+
await ctx.fixture.clear();
2827
});
2928
});

test/next-15-node-20.test.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { suite, test, before, after } from 'node:test';
2-
import { once } from 'node:events';
32
import { Fixture } from './util.js';
43

54
suite('Next.js v15 - Node.js v20', async () => {
65
const ctx = {};
76

87
before(async () => {
98
ctx.fixture = new Fixture({ nextMajor: '15', nodeMajor: '20' });
10-
await once(ctx.fixture, 'ready');
9+
await ctx.fixture.ready;
1110
ctx.rest = `http://${ctx.fixture.portMap.get('9926')}`;
1211
});
1312

@@ -23,7 +22,7 @@ suite('Next.js v15 - Node.js v20', async () => {
2322
t.assert.deepStrictEqual(json, { id: '0', name: 'Lincoln', breed: 'Shepherd' });
2423
});
2524

26-
after(() => {
27-
ctx.fixture.cleanup();
25+
after(async () => {
26+
await ctx.fixture.clear();
2827
});
2928
});

test/next-15-node-22.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { suite, test, before, after } from 'node:test';
2-
import { once } from 'node:events';
2+
33
import { Fixture } from './util.js';
44

55
suite('Next.js v15 - Node.js v22', async () => {
66
const ctx = {};
77

88
before(async () => {
99
ctx.fixture = new Fixture({ nextMajor: '15', nodeMajor: '22' });
10-
await once(ctx.fixture, 'ready');
10+
await ctx.fixture.ready;
1111
ctx.rest = `http://${ctx.fixture.portMap.get('9926')}`;
1212
});
1313

@@ -23,7 +23,7 @@ suite('Next.js v15 - Node.js v22', async () => {
2323
t.assert.deepStrictEqual(json, { id: '0', name: 'Lincoln', breed: 'Shepherd' });
2424
});
2525

26-
after(() => {
27-
ctx.fixture.cleanup();
26+
after(async () => {
27+
await ctx.fixture.clear();
2828
});
2929
});

test/util.js

+174-71
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,208 @@
11
import child_process from 'node:child_process';
2-
import EventEmitter from 'node:events';
2+
import EventEmitter, { once } from 'node:events';
3+
import { Transform } from 'node:stream';
34

4-
const FIXTURE_PATH_URL = new URL('./fixtures/', import.meta.url);
5+
class CollectOutput extends Transform {
6+
constructor() {
7+
super();
8+
this.chunks = [];
9+
}
10+
11+
_transform(chunk, encoding, callback) {
12+
this.chunks.push(chunk);
13+
callback(null, chunk);
14+
}
15+
}
516

6-
const CONTAINER_ENGINE_LIST = ['podman', 'docker'];
17+
export class Fixture {
18+
static CONTAINER_ENGINE_LIST = ['podman', 'docker'];
19+
static FIXTURE_PATH_URL = new URL('./fixtures/', import.meta.url);
720

8-
function getContainerEngine() {
9-
for (const containerEngine of CONTAINER_ENGINE_LIST) {
10-
const { status } = child_process.spawnSync(containerEngine, ['--version'], { stdio: 'ignore' });
11-
if (status === 0) {
12-
return containerEngine;
21+
/** @type {string} */
22+
#containerEngine;
23+
24+
#readyResolve;
25+
#readyReject;
26+
27+
constructor({ nextMajor, nodeMajor, debug = false, autoSetup = true }) {
28+
if (!nextMajor || !nodeMajor) {
29+
throw new Error(`Fixture options nextMajor and nodeMajor are required`);
1330
}
14-
}
1531

16-
throw new Error(`No container engine found in ${CONTAINER_ENGINE_LIST.join(', ')}`);
17-
}
32+
this.nextMajor = nextMajor;
33+
this.nodeMajor = nodeMajor;
1834

19-
function clearContainer({ containerEngine, containerName }) {
20-
const { stdout } = child_process.spawnSync(containerEngine, ['ps', '-aq', '-f', `name=${containerName}`]);
21-
if (stdout !== '') {
22-
child_process.spawnSync(containerEngine, ['stop', containerName], { stdio: 'ignore' });
23-
child_process.spawnSync(containerEngine, ['rm', containerName], { stdio: 'ignore' });
24-
}
25-
}
35+
this.debug = debug;
2636

27-
function buildContainer({ nextMajor, nodeMajor, containerEngine }) {
28-
if (!containerEngine) {
29-
containerEngine = getContainerEngine();
30-
}
37+
this.imageName = `hdb-next-integration-test-image-next-${nextMajor}-node-${nodeMajor}`;
38+
this.containerName = `hdb-next-integration-test-container-next-${nextMajor}-node-${nodeMajor}`;
3139

32-
const imageName = `hdb-next-integration-test-image-next-${nextMajor}-node-${nodeMajor}`;
40+
if (autoSetup) {
41+
this.ready = new Promise((resolve, reject) => {
42+
this.#readyResolve = resolve;
43+
this.#readyReject = reject;
44+
});
45+
this.clear()
46+
.then(() => this.build())
47+
.then(() => this.run())
48+
.then(this.#readyResolve, this.#readyReject);
49+
}
50+
}
3351

34-
// console.log(`🏗️ Building ${imageName}...`);
52+
get containerEngine() {
53+
if (this.#containerEngine) {
54+
return this.#containerEngine;
55+
}
3556

36-
child_process.spawnSync(
37-
containerEngine,
38-
['build', '--build-arg', `NEXT_MAJOR=${nextMajor}`, '--build-arg', `NODE_MAJOR=${nodeMajor}`, '-t', imageName, '.'],
39-
{ cwd: FIXTURE_PATH_URL, stdio: process.env.DEBUG === '1' ? 'inherit' : 'ignore' }
40-
);
57+
for (const containerEngine of Fixture.CONTAINER_ENGINE_LIST) {
58+
const { status } = child_process.spawnSync(containerEngine, ['--version'], { stdio: 'ignore' });
59+
if (status === 0) {
60+
return (this.#containerEngine = containerEngine);
61+
}
62+
}
4163

42-
// console.log(`🏗️ Build complete!`);
64+
throw new Error(`No container engine found in ${CONTAINER_ENGINE_LIST.join(', ')}`);
65+
}
4366

44-
return imageName;
45-
}
67+
get #stdio() {
68+
return ['ignore', this.debug ? 'inherit' : 'ignore', this.debug ? 'inherit' : 'ignore'];
69+
}
4670

47-
function determinePortMapping({ containerName, containerEngine }) {
48-
const portMap = new Map();
49-
for (const port of ['9925', '9926']) {
50-
const { stdout } = child_process.spawnSync(containerEngine, ['port', containerName, port]);
51-
portMap.set(port, stdout.toString().trim());
71+
build() {
72+
return new Promise((resolve, reject) => {
73+
const buildProcess = child_process.spawn(
74+
this.containerEngine,
75+
[
76+
'build',
77+
'--build-arg',
78+
`NEXT_MAJOR=${this.nextMajor}`,
79+
'--build-arg',
80+
`NODE_MAJOR=${this.nodeMajor}`,
81+
'-t',
82+
this.imageName,
83+
'.',
84+
],
85+
{
86+
cwd: Fixture.FIXTURE_PATH_URL,
87+
stdio: this.#stdio,
88+
}
89+
);
90+
91+
buildProcess.on('error', reject);
92+
93+
buildProcess.on('exit', (code) => {
94+
if (code === 0) {
95+
resolve();
96+
} else {
97+
reject(new Error(`\`${this.containerEngine} build\` exited with code ${code}`));
98+
}
99+
});
100+
});
52101
}
53-
return portMap;
54-
}
55102

56-
function runContainer({ nextMajor, nodeMajor, imageName, containerEngine }) {
57-
if (!containerEngine) {
58-
containerEngine = getContainerEngine();
103+
stop() {
104+
return new Promise((resolve, reject) => {
105+
const stopProcess = child_process.spawn(this.containerEngine, ['stop', this.containerName], {
106+
stdio: this.#stdio,
107+
});
108+
109+
stopProcess.on('error', reject);
110+
111+
stopProcess.on('exit', (code) => {
112+
if (code === 0) {
113+
resolve();
114+
} else {
115+
reject(new Error(`\`${this.containerEngine} stop\` exited with code ${code}`));
116+
}
117+
});
118+
});
59119
}
60120

61-
const containerName = `hdb-next-integration-test-container-next-${nextMajor}-node-${nodeMajor}`;
121+
rm() {
122+
return new Promise((resolve, reject) => {
123+
const rmProcess = child_process.spawn(this.containerEngine, ['rm', this.containerName], { stdio: this.#stdio });
62124

63-
clearContainer({ containerEngine, containerName });
125+
rmProcess.on('error', reject);
64126

65-
const runProcess = child_process.spawn(containerEngine, ['run', '-P', '--name', containerName, imageName]);
127+
rmProcess.on('exit', (code) => {
128+
if (code === 0) {
129+
resolve();
130+
} else {
131+
reject(new Error(`\`${this.containerEngine} rm\` exited with code ${code}`));
132+
}
133+
});
134+
});
135+
}
66136

67-
return { containerName, runProcess };
68-
}
137+
clear() {
138+
return new Promise((resolve, reject) => {
139+
const psProcess = child_process.spawn(this.containerEngine, ['ps', '-aq', '-f', `name=${this.containerName}`]);
69140

70-
export class Fixture extends EventEmitter {
71-
constructor({ nextMajor, nodeMajor }) {
72-
super();
141+
psProcess.on('error', reject);
73142

74-
this.containerEngine = getContainerEngine();
143+
const collectedStdout = psProcess.stdout.pipe(new CollectOutput());
75144

76-
this.imageName = buildContainer({ nextMajor, nodeMajor, containerEngine: this.containerEngine });
145+
if (this.debug) {
146+
collectedStdout.pipe(process.stdout);
147+
psProcess.stderr.pipe(process.stderr);
148+
}
77149

78-
const { containerName, runProcess, portMap } = runContainer({
79-
nextMajor,
80-
nodeMajor,
81-
imageName: this.imageName,
82-
containerEngine: this.containerEngine,
150+
psProcess.on('exit', (code) => {
151+
if (code === 0) {
152+
if (collectedStdout.chunks.length !== 0) {
153+
this.stop()
154+
.then(() => this.rm())
155+
.then(resolve, reject);
156+
}
157+
resolve();
158+
} else {
159+
reject(new Error(`\`${this.containerEngine} ps\` exited with code ${code}`));
160+
}
161+
});
83162
});
163+
}
84164

85-
this.containerName = containerName;
86-
this.runProcess = runProcess;
165+
run() {
166+
return new Promise((resolve, reject) => {
167+
const runProcess = child_process.spawn(
168+
this.containerEngine,
169+
['run', '-P', '--name', this.containerName, this.imageName],
170+
{ stdio: ['ignore', 'pipe', this.debug ? 'inherit' : 'ignore'] }
171+
);
172+
const resolveReady = this.#readyResolve;
173+
const stdout = runProcess.stdout.pipe(
174+
new Transform({
175+
transform(chunk, encoding, callback) {
176+
if (chunk.toString().includes('HarperDB 4.4.5 successfully started')) {
177+
resolveReady();
178+
}
179+
callback(null, chunk);
180+
},
181+
})
182+
);
183+
184+
if (this.debug) {
185+
stdout.pipe(process.stdout);
186+
}
87187

88-
this.runProcess.stdout.on('data', (data) => {
89-
if (data.toString().includes('HarperDB 4.4.5 successfully started')) {
90-
this.portMap = determinePortMapping({
91-
containerName: this.containerName,
92-
containerEngine: this.containerEngine,
93-
});
188+
runProcess.on('error', reject);
94189

95-
this.emit('ready');
96-
}
190+
runProcess.on('exit', (code) => {
191+
if (code === 0) {
192+
resolve();
193+
} else {
194+
reject(new Error(`\`${this.containerEngine} run\` exited with code ${code}`));
195+
}
196+
});
97197
});
98198
}
99199

100-
cleanup() {
101-
clearContainer({ containerEngine: this.containerEngine, containerName: this.containerName });
200+
get portMap() {
201+
const portMap = new Map();
202+
for (const port of ['9925', '9926']) {
203+
const { stdout } = child_process.spawnSync(this.containerEngine, ['port', this.containerName, port]);
204+
portMap.set(port, stdout.toString().trim());
205+
}
206+
return portMap;
102207
}
103208
}
104-
105-
// const f = new Fixture({ nextMajor: '15', nodeMajor: '20' });

0 commit comments

Comments
 (0)