|
1 | 1 | 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'; |
3 | 4 |
|
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 | +} |
5 | 16 |
|
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); |
7 | 20 |
|
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`); |
13 | 30 | }
|
14 |
| - } |
15 | 31 |
|
16 |
| - throw new Error(`No container engine found in ${CONTAINER_ENGINE_LIST.join(', ')}`); |
17 |
| -} |
| 32 | + this.nextMajor = nextMajor; |
| 33 | + this.nodeMajor = nodeMajor; |
18 | 34 |
|
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; |
26 | 36 |
|
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}`; |
31 | 39 |
|
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 | + } |
33 | 51 |
|
34 |
| - // console.log(`🏗️ Building ${imageName}...`); |
| 52 | + get containerEngine() { |
| 53 | + if (this.#containerEngine) { |
| 54 | + return this.#containerEngine; |
| 55 | + } |
35 | 56 |
|
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 | + } |
41 | 63 |
|
42 |
| - // console.log(`🏗️ Build complete!`); |
| 64 | + throw new Error(`No container engine found in ${CONTAINER_ENGINE_LIST.join(', ')}`); |
| 65 | + } |
43 | 66 |
|
44 |
| - return imageName; |
45 |
| -} |
| 67 | + get #stdio() { |
| 68 | + return ['ignore', this.debug ? 'inherit' : 'ignore', this.debug ? 'inherit' : 'ignore']; |
| 69 | + } |
46 | 70 |
|
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 | + }); |
52 | 101 | }
|
53 |
| - return portMap; |
54 |
| -} |
55 | 102 |
|
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 | + }); |
59 | 119 | }
|
60 | 120 |
|
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 }); |
62 | 124 |
|
63 |
| - clearContainer({ containerEngine, containerName }); |
| 125 | + rmProcess.on('error', reject); |
64 | 126 |
|
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 | + } |
66 | 136 |
|
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}`]); |
69 | 140 |
|
70 |
| -export class Fixture extends EventEmitter { |
71 |
| - constructor({ nextMajor, nodeMajor }) { |
72 |
| - super(); |
| 141 | + psProcess.on('error', reject); |
73 | 142 |
|
74 |
| - this.containerEngine = getContainerEngine(); |
| 143 | + const collectedStdout = psProcess.stdout.pipe(new CollectOutput()); |
75 | 144 |
|
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 | + } |
77 | 149 |
|
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 | + }); |
83 | 162 | });
|
| 163 | + } |
84 | 164 |
|
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 | + } |
87 | 187 |
|
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); |
94 | 189 |
|
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 | + }); |
97 | 197 | });
|
98 | 198 | }
|
99 | 199 |
|
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; |
102 | 207 | }
|
103 | 208 | }
|
104 |
| - |
105 |
| -// const f = new Fixture({ nextMajor: '15', nodeMajor: '20' }); |
|
0 commit comments