prool 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Instance.d.ts +1 -1
- package/dist/Instance.d.ts.map +1 -1
- package/dist/Instance.js +1 -1
- package/dist/Instance.js.map +1 -1
- package/dist/instances/tempo.d.ts +50 -62
- package/dist/instances/tempo.d.ts.map +1 -1
- package/dist/instances/tempo.js +46 -113
- package/dist/instances/tempo.js.map +1 -1
- package/dist/internal/utils.d.ts +5 -5
- package/dist/internal/utils.d.ts.map +1 -1
- package/dist/internal/utils.js +36 -6
- package/dist/internal/utils.js.map +1 -1
- package/dist/testcontainers/Instance.d.ts +48 -0
- package/dist/testcontainers/Instance.d.ts.map +1 -0
- package/dist/testcontainers/Instance.js +83 -0
- package/dist/testcontainers/Instance.js.map +1 -0
- package/dist/testcontainers/index.d.ts +2 -0
- package/dist/testcontainers/index.d.ts.map +1 -0
- package/dist/testcontainers/index.js +2 -0
- package/dist/testcontainers/index.js.map +1 -0
- package/package.json +6 -1
- package/src/Instance.ts +1 -1
- package/src/instances/tempo.ts +76 -147
- package/src/internal/utils.test.ts +14 -0
- package/src/internal/utils.ts +46 -16
- package/src/testcontainers/Instance.test.ts +84 -0
- package/src/testcontainers/Instance.ts +117 -0
- package/src/testcontainers/index.ts +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { GenericContainer, Wait, } from 'testcontainers';
|
|
2
|
+
import * as Instance from '../Instance.js';
|
|
3
|
+
import { command } from '../instances/tempo.js';
|
|
4
|
+
/**
|
|
5
|
+
* Defines a Tempo instance.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const instance = Instance.tempo({ port: 8545 })
|
|
10
|
+
* await instance.start()
|
|
11
|
+
* // ...
|
|
12
|
+
* await instance.stop()
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const tempo = Instance.define((parameters) => {
|
|
16
|
+
const { containerName = `tempo.${crypto.randomUUID()}`, image = 'ghcr.io/tempoxyz/tempo:latest', log: log_, ...args } = parameters || {};
|
|
17
|
+
const log = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(log_);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return log_;
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
const RUST_LOG = log && typeof log !== 'boolean' ? log : '';
|
|
26
|
+
const name = 'tempo';
|
|
27
|
+
let container;
|
|
28
|
+
return {
|
|
29
|
+
_internal: {
|
|
30
|
+
args,
|
|
31
|
+
},
|
|
32
|
+
host: args.host ?? 'localhost',
|
|
33
|
+
name,
|
|
34
|
+
port: args.port ?? 8545,
|
|
35
|
+
async start({ port = args.port }, { emitter }) {
|
|
36
|
+
const promise = Promise.withResolvers();
|
|
37
|
+
const c = new GenericContainer(image)
|
|
38
|
+
.withPlatform('linux/x86_64')
|
|
39
|
+
.withNetworkMode('host')
|
|
40
|
+
.withExtraHosts([
|
|
41
|
+
{ host: 'host.docker.internal', ipAddress: 'host-gateway' },
|
|
42
|
+
{ host: 'localhost', ipAddress: 'host-gateway' },
|
|
43
|
+
])
|
|
44
|
+
.withName(containerName)
|
|
45
|
+
.withEnvironment({ RUST_LOG })
|
|
46
|
+
.withCommand(command({ ...args, port }))
|
|
47
|
+
.withWaitStrategy(Wait.forLogMessage(/RPC HTTP server started/))
|
|
48
|
+
.withLogConsumer((stream) => {
|
|
49
|
+
stream.on('data', (data) => {
|
|
50
|
+
const message = data.toString();
|
|
51
|
+
emitter.emit('message', message);
|
|
52
|
+
emitter.emit('stdout', message);
|
|
53
|
+
if (log)
|
|
54
|
+
console.log(message);
|
|
55
|
+
if (message.includes('shutting down'))
|
|
56
|
+
promise.reject(new Error(`Failed to start: ${message}`));
|
|
57
|
+
});
|
|
58
|
+
stream.on('error', (error) => {
|
|
59
|
+
if (log)
|
|
60
|
+
console.error(error.message);
|
|
61
|
+
emitter.emit('message', error.message);
|
|
62
|
+
emitter.emit('stderr', error.message);
|
|
63
|
+
promise.reject(new Error(`Failed to start: ${error.message}`));
|
|
64
|
+
});
|
|
65
|
+
})
|
|
66
|
+
.withStartupTimeout(10_000);
|
|
67
|
+
c.start()
|
|
68
|
+
.then((c) => {
|
|
69
|
+
container = c;
|
|
70
|
+
promise.resolve();
|
|
71
|
+
})
|
|
72
|
+
.catch(promise.reject);
|
|
73
|
+
return promise.promise;
|
|
74
|
+
},
|
|
75
|
+
async stop() {
|
|
76
|
+
if (!container)
|
|
77
|
+
return;
|
|
78
|
+
await container.stop();
|
|
79
|
+
container = undefined;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=Instance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Instance.js","sourceRoot":"","sources":["../../src/testcontainers/Instance.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testcontainers/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testcontainers/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prool",
|
|
3
3
|
"description": "HTTP testing instances for Ethereum",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"files": [
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"src": "./src/index.ts",
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./testcontainers": {
|
|
21
|
+
"src": "./src/testcontainers/index.ts",
|
|
22
|
+
"types": "./dist/testcontainers/index.d.ts",
|
|
23
|
+
"default": "./dist/testcontainers/index.js"
|
|
19
24
|
}
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
package/src/Instance.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { EventEmitter } from 'eventemitter3'
|
|
|
2
2
|
|
|
3
3
|
export { alto } from './instances/alto.js'
|
|
4
4
|
export { anvil } from './instances/anvil.js'
|
|
5
|
-
export { tempo
|
|
5
|
+
export { tempo } from './instances/tempo.js'
|
|
6
6
|
|
|
7
7
|
type EventTypes = {
|
|
8
8
|
exit: [code: number | null, signal: NodeJS.Signals | null]
|
package/src/instances/tempo.ts
CHANGED
|
@@ -1,48 +1,60 @@
|
|
|
1
1
|
import * as os from 'node:os'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
GenericContainer,
|
|
5
|
-
type StartedTestContainer,
|
|
6
|
-
Wait,
|
|
7
|
-
} from 'testcontainers'
|
|
8
3
|
import * as Instance from '../Instance.js'
|
|
4
|
+
import { deepAssign, toArgs } from '../internal/utils.js'
|
|
9
5
|
import { execa } from '../processes/execa.js'
|
|
10
6
|
|
|
11
7
|
export function command(parameters: tempo.Parameters): string[] {
|
|
12
|
-
const {
|
|
13
|
-
|
|
8
|
+
const { blockMaxTransactions, blockTime, mnemonic, port, ...rest } =
|
|
9
|
+
parameters
|
|
10
|
+
|
|
11
|
+
const datadir = path.join(os.tmpdir(), '.prool', `tempo.${port}`)
|
|
12
|
+
const defaultParameters = {
|
|
13
|
+
authrpc: {
|
|
14
|
+
port: port! + 30,
|
|
15
|
+
},
|
|
16
|
+
datadir,
|
|
17
|
+
dev: [
|
|
18
|
+
true,
|
|
19
|
+
{
|
|
20
|
+
blockTime: blockTime ?? '50ms',
|
|
21
|
+
...(blockMaxTransactions ? { blockMaxTransactions } : {}),
|
|
22
|
+
...(mnemonic ? { mnemonic } : {}),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
engine: {
|
|
26
|
+
disablePrecompileCache: true,
|
|
27
|
+
legacyStateRoot: true,
|
|
28
|
+
},
|
|
29
|
+
faucet: {
|
|
30
|
+
address: [
|
|
31
|
+
'0x20c0000000000000000000000000000000000000',
|
|
32
|
+
'0x20c0000000000000000000000000000000000001',
|
|
33
|
+
'0x20c0000000000000000000000000000000000002',
|
|
34
|
+
'0x20c0000000000000000000000000000000000003',
|
|
35
|
+
],
|
|
36
|
+
amount: '1000000000000',
|
|
37
|
+
enabled: true,
|
|
38
|
+
privateKey:
|
|
39
|
+
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
40
|
+
},
|
|
41
|
+
http: {
|
|
42
|
+
addr: '0.0.0.0',
|
|
43
|
+
api: 'all',
|
|
44
|
+
corsdomain: '*',
|
|
45
|
+
port: port!,
|
|
46
|
+
},
|
|
47
|
+
port: port! + 10,
|
|
48
|
+
ws: {
|
|
49
|
+
port: port! + 20,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
14
53
|
return [
|
|
15
54
|
'node',
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`--dev.block-time=${parameters?.blockTime ?? '50ms'}`,
|
|
20
|
-
'--engine.disable-precompile-cache',
|
|
21
|
-
'--engine.legacy-state-root',
|
|
22
|
-
'--faucet.address',
|
|
23
|
-
...(faucet?.addresses ?? [
|
|
24
|
-
'0x20c0000000000000000000000000000000000000',
|
|
25
|
-
'0x20c0000000000000000000000000000000000001',
|
|
26
|
-
'0x20c0000000000000000000000000000000000002',
|
|
27
|
-
'0x20c0000000000000000000000000000000000003',
|
|
28
|
-
]),
|
|
29
|
-
`--faucet.amount=${faucet?.amount ?? '1000000000000'}`,
|
|
30
|
-
'--faucet.enabled',
|
|
31
|
-
`--faucet.private-key=${faucet?.privateKey ?? '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'}`,
|
|
32
|
-
'--http',
|
|
33
|
-
'--http.addr=0.0.0.0',
|
|
34
|
-
'--http.api=all',
|
|
35
|
-
'--http.corsdomain=*',
|
|
36
|
-
`--http.port=${port!}`,
|
|
37
|
-
`--port=${port! + 10}`,
|
|
38
|
-
'--txpool.basefee-max-count=10000000000000',
|
|
39
|
-
'--txpool.basefee-max-size=10000',
|
|
40
|
-
'--txpool.max-account-slots=500000',
|
|
41
|
-
'--txpool.pending-max-count=10000000000000',
|
|
42
|
-
'--txpool.pending-max-size=10000',
|
|
43
|
-
'--txpool.queued-max-count=10000000000000',
|
|
44
|
-
'--txpool.queued-max-size=10000',
|
|
45
|
-
`--ws.port=${port! + 20}`,
|
|
55
|
+
...toArgs(deepAssign(defaultParameters, rest), {
|
|
56
|
+
arraySeparator: null,
|
|
57
|
+
}),
|
|
46
58
|
]
|
|
47
59
|
}
|
|
48
60
|
|
|
@@ -89,7 +101,7 @@ export const tempo = Instance.define((parameters?: tempo.Parameters) => {
|
|
|
89
101
|
env: {
|
|
90
102
|
RUST_LOG,
|
|
91
103
|
},
|
|
92
|
-
})`${[binary, ...command({ ...
|
|
104
|
+
})`${[binary, ...command({ ...args, port })]}`,
|
|
93
105
|
{
|
|
94
106
|
...options,
|
|
95
107
|
// Resolve when the process is listening via "RPC HTTP server started" message.
|
|
@@ -124,6 +136,28 @@ export declare namespace tempo {
|
|
|
124
136
|
* Interval between blocks.
|
|
125
137
|
*/
|
|
126
138
|
blockTime?: string | undefined
|
|
139
|
+
/**
|
|
140
|
+
* How many transactions to mine per block
|
|
141
|
+
*/
|
|
142
|
+
blockMaxTransactions?: number | undefined
|
|
143
|
+
/**
|
|
144
|
+
* Path to a configuration file.
|
|
145
|
+
*/
|
|
146
|
+
config?: string | undefined
|
|
147
|
+
/**
|
|
148
|
+
* The chain this node is running.
|
|
149
|
+
* Possible values are either a built-in chain or the path to a chain specification file.
|
|
150
|
+
*
|
|
151
|
+
* Built-in chains:
|
|
152
|
+
* - testnet
|
|
153
|
+
*
|
|
154
|
+
* @default "testnet"
|
|
155
|
+
*/
|
|
156
|
+
chain?: string | undefined
|
|
157
|
+
/**
|
|
158
|
+
* The path to the data dir for all reth files and subdirectories.
|
|
159
|
+
*/
|
|
160
|
+
datadir?: string | undefined
|
|
127
161
|
/**
|
|
128
162
|
* Faucet options.
|
|
129
163
|
*/
|
|
@@ -161,118 +195,13 @@ export declare namespace tempo {
|
|
|
161
195
|
*/
|
|
162
196
|
host?: string | undefined
|
|
163
197
|
/**
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
port?: number | undefined
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Defines a Tempo instance.
|
|
172
|
-
*
|
|
173
|
-
* @example
|
|
174
|
-
* ```ts
|
|
175
|
-
* const instance = Instance.tempoDocker({ port: 8545 })
|
|
176
|
-
* await instance.start()
|
|
177
|
-
* // ...
|
|
178
|
-
* await instance.stop()
|
|
179
|
-
* ```
|
|
180
|
-
*/
|
|
181
|
-
export const tempoDocker = Instance.define(
|
|
182
|
-
(parameters?: tempoDocker.Parameters) => {
|
|
183
|
-
const {
|
|
184
|
-
containerName = `tempo.${crypto.randomUUID()}`,
|
|
185
|
-
image = 'ghcr.io/tempoxyz/tempo:latest',
|
|
186
|
-
log: log_,
|
|
187
|
-
...args
|
|
188
|
-
} = parameters || {}
|
|
189
|
-
|
|
190
|
-
const log = (() => {
|
|
191
|
-
try {
|
|
192
|
-
return JSON.parse(log_ as string)
|
|
193
|
-
} catch {
|
|
194
|
-
return log_
|
|
195
|
-
}
|
|
196
|
-
})()
|
|
197
|
-
const RUST_LOG = log && typeof log !== 'boolean' ? log : ''
|
|
198
|
-
|
|
199
|
-
const name = 'tempo'
|
|
200
|
-
let container: StartedTestContainer | undefined
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
_internal: {
|
|
204
|
-
args,
|
|
205
|
-
},
|
|
206
|
-
host: args.host ?? 'localhost',
|
|
207
|
-
name,
|
|
208
|
-
port: args.port ?? 8545,
|
|
209
|
-
async start({ port = args.port }, { emitter }) {
|
|
210
|
-
const promise = Promise.withResolvers<void>()
|
|
211
|
-
|
|
212
|
-
const c = new GenericContainer(image)
|
|
213
|
-
.withPlatform('linux/x86_64')
|
|
214
|
-
.withNetworkMode('host')
|
|
215
|
-
.withExtraHosts([
|
|
216
|
-
{ host: 'host.docker.internal', ipAddress: 'host-gateway' },
|
|
217
|
-
{ host: 'localhost', ipAddress: 'host-gateway' },
|
|
218
|
-
])
|
|
219
|
-
.withName(containerName)
|
|
220
|
-
.withEnvironment({ RUST_LOG })
|
|
221
|
-
.withCommand(command({ ...parameters, port }))
|
|
222
|
-
.withWaitStrategy(Wait.forLogMessage(/RPC HTTP server started/))
|
|
223
|
-
.withLogConsumer((stream) => {
|
|
224
|
-
stream.on('data', (data) => {
|
|
225
|
-
const message = data.toString()
|
|
226
|
-
emitter.emit('message', message)
|
|
227
|
-
emitter.emit('stdout', message)
|
|
228
|
-
if (log) console.log(message)
|
|
229
|
-
if (message.includes('shutting down'))
|
|
230
|
-
promise.reject(new Error(`Failed to start: ${message}`))
|
|
231
|
-
})
|
|
232
|
-
stream.on('error', (error) => {
|
|
233
|
-
if (log) console.error(error.message)
|
|
234
|
-
emitter.emit('message', error.message)
|
|
235
|
-
emitter.emit('stderr', error.message)
|
|
236
|
-
promise.reject(new Error(`Failed to start: ${error.message}`))
|
|
237
|
-
})
|
|
238
|
-
})
|
|
239
|
-
.withStartupTimeout(10_000)
|
|
240
|
-
|
|
241
|
-
c.start()
|
|
242
|
-
.then((c) => {
|
|
243
|
-
container = c
|
|
244
|
-
promise.resolve()
|
|
245
|
-
})
|
|
246
|
-
.catch(promise.reject)
|
|
247
|
-
|
|
248
|
-
return promise.promise
|
|
249
|
-
},
|
|
250
|
-
async stop() {
|
|
251
|
-
if (!container) return
|
|
252
|
-
await container.stop()
|
|
253
|
-
container = undefined
|
|
254
|
-
},
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
export declare namespace tempoDocker {
|
|
260
|
-
export type Parameters = Omit<tempo.Parameters, 'binary'> & {
|
|
261
|
-
/**
|
|
262
|
-
* Name of the container.
|
|
198
|
+
* Derive dev accounts from a fixed mnemonic instead of random ones.
|
|
199
|
+
* @default "test test test test test test test test test test test junk"
|
|
263
200
|
*/
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Docker image to use.
|
|
267
|
-
*/
|
|
268
|
-
image?: string | undefined
|
|
269
|
-
/**
|
|
270
|
-
* Host the server will listen on.
|
|
271
|
-
*/
|
|
272
|
-
host?: string | undefined
|
|
201
|
+
mnemonic?: string | undefined
|
|
273
202
|
/**
|
|
274
203
|
* Port the server will listen on.
|
|
275
204
|
*/
|
|
276
205
|
port?: number | undefined
|
|
277
|
-
}
|
|
206
|
+
} & Record<string, unknown>
|
|
278
207
|
}
|
|
@@ -30,10 +30,12 @@ test.each([
|
|
|
30
30
|
[[{ foo: 0 }], ['--foo', '0']],
|
|
31
31
|
[[{ foo: 1 }], ['--foo', '1']],
|
|
32
32
|
[[{ foo: 1n }], ['--foo', '1']],
|
|
33
|
+
[[{ foo: 1n }], ['--foo', '1']],
|
|
33
34
|
[[{ foo: 'bar', baz: 1 }], ['--foo', 'bar', '--baz', '1']],
|
|
34
35
|
[[{ fooBar: 'test' }], ['--foo-bar', 'test']],
|
|
35
36
|
[[{ foo: ['bar', 'baz'] }], ['--foo', 'bar,baz']],
|
|
36
37
|
[[{ foo: { barBaz: 'test' } }], ['--foo.bar-baz', 'test']],
|
|
38
|
+
[[{ foo: [true, { barBaz: 'test' }] }], ['--foo', '--foo.bar-baz', 'test']],
|
|
37
39
|
[[{ foo: { barBaz: ['test', 'test2'] } }], ['--foo.bar-baz', 'test,test2']],
|
|
38
40
|
[
|
|
39
41
|
[{ fooBar: 'test' }, { casing: 'snake' }],
|
|
@@ -43,6 +45,18 @@ test.each([
|
|
|
43
45
|
[{ foo: { barBaz: 'test' } }, { casing: 'snake' }],
|
|
44
46
|
['--foo.bar_baz', 'test'],
|
|
45
47
|
],
|
|
48
|
+
[
|
|
49
|
+
[{ foo: ['bar', 'baz'] }, { arraySeparator: null }],
|
|
50
|
+
['--foo', 'bar', 'baz'],
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
[{ foo: { bar: ['baz', 'qux'] } }, { arraySeparator: null }],
|
|
54
|
+
['--foo.bar', 'baz', 'qux'],
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
[{ foo: ['bar', 'baz'] }, { arraySeparator: null }],
|
|
58
|
+
['--foo', 'bar', 'baz'],
|
|
59
|
+
],
|
|
46
60
|
] as [Parameters<typeof toArgs>, string[]][])('toArgs(%o) -> %o', ([
|
|
47
61
|
input,
|
|
48
62
|
options,
|
package/src/internal/utils.ts
CHANGED
|
@@ -50,23 +50,26 @@ export function stripColors(message: string) {
|
|
|
50
50
|
* @returns The command line arguments.
|
|
51
51
|
*/
|
|
52
52
|
export function toArgs(
|
|
53
|
-
obj:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
| bigint
|
|
61
|
-
| undefined
|
|
62
|
-
},
|
|
63
|
-
options: { casing: 'kebab' | 'snake' } = { casing: 'kebab' },
|
|
64
|
-
) {
|
|
65
|
-
const { casing } = options
|
|
53
|
+
obj: Record<string, unknown>,
|
|
54
|
+
options: {
|
|
55
|
+
arraySeparator?: string | null | undefined
|
|
56
|
+
casing?: 'kebab' | 'snake' | undefined
|
|
57
|
+
} = {},
|
|
58
|
+
): string[] {
|
|
59
|
+
const { arraySeparator = ',', casing = 'kebab' } = options
|
|
66
60
|
return Object.entries(obj).flatMap(([key, value]) => {
|
|
67
61
|
if (value === undefined) return []
|
|
68
62
|
|
|
69
|
-
if (Array.isArray(value))
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
if (value[0] === true)
|
|
65
|
+
return [toFlagCase(key), ...toArgs({ [key]: value[1] }, options)]
|
|
66
|
+
const arrayValue = (() => {
|
|
67
|
+
if (arraySeparator === null) return value
|
|
68
|
+
return value.join(arraySeparator)
|
|
69
|
+
})()
|
|
70
|
+
|
|
71
|
+
return [toFlagCase(key), arrayValue].flat()
|
|
72
|
+
}
|
|
70
73
|
|
|
71
74
|
if (typeof value === 'object' && value !== null) {
|
|
72
75
|
return Object.entries(value).flatMap(([subKey, subValue]) => {
|
|
@@ -75,7 +78,7 @@ export function toArgs(
|
|
|
75
78
|
`${key}.${subKey}`,
|
|
76
79
|
casing === 'kebab' ? '-' : '_',
|
|
77
80
|
)
|
|
78
|
-
return [flag
|
|
81
|
+
return toArgs({ [flag.slice(2)]: subValue }, options)
|
|
79
82
|
})
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -84,7 +87,7 @@ export function toArgs(
|
|
|
84
87
|
if (value === false) return [flag, 'false']
|
|
85
88
|
if (value === true) return [flag]
|
|
86
89
|
|
|
87
|
-
const stringified = value
|
|
90
|
+
const stringified = value?.toString() ?? ''
|
|
88
91
|
if (stringified === '') return [flag]
|
|
89
92
|
|
|
90
93
|
return [flag, stringified]
|
|
@@ -106,3 +109,30 @@ export function toFlagCase(str: string, separator = '-') {
|
|
|
106
109
|
}
|
|
107
110
|
return `--${keys.join('.')}`
|
|
108
111
|
}
|
|
112
|
+
|
|
113
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
114
|
+
return Object.prototype.toString.call(value) === '[object Object]'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function deepAssign(
|
|
118
|
+
target: Record<string, unknown>,
|
|
119
|
+
...sources: Record<string, unknown>[]
|
|
120
|
+
): Record<string, unknown> {
|
|
121
|
+
for (const source of sources) {
|
|
122
|
+
if (!isPlainObject(source)) continue
|
|
123
|
+
|
|
124
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
125
|
+
const srcVal = source[key as keyof typeof source]
|
|
126
|
+
const tgtVal = target[key as keyof typeof target]
|
|
127
|
+
|
|
128
|
+
// If both are plain objects, merge recursively
|
|
129
|
+
if (isPlainObject(srcVal) && isPlainObject(tgtVal)) {
|
|
130
|
+
deepAssign(tgtVal, srcVal)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
// Otherwise, assign a deep clone of the source value
|
|
134
|
+
;(target as any)[key] = structuredClone(srcVal)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return target
|
|
138
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import getPort from 'get-port'
|
|
2
|
+
import { Instance } from 'prool/testcontainers'
|
|
3
|
+
import { afterEach, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
const instances: Instance.Instance[] = []
|
|
6
|
+
|
|
7
|
+
const port = await getPort()
|
|
8
|
+
|
|
9
|
+
const defineInstance = (parameters: Instance.tempo.Parameters = {}) => {
|
|
10
|
+
const instance = Instance.tempo({ port, ...parameters })
|
|
11
|
+
instances.push(instance)
|
|
12
|
+
return instance
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
for (const instance of instances) await instance.stop().catch(() => {})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('default', async () => {
|
|
20
|
+
const messages: string[] = []
|
|
21
|
+
const stdouts: string[] = []
|
|
22
|
+
|
|
23
|
+
const instance = defineInstance()
|
|
24
|
+
|
|
25
|
+
instance.on('message', (m) => messages.push(m))
|
|
26
|
+
instance.on('stdout', (m) => stdouts.push(m))
|
|
27
|
+
|
|
28
|
+
expect(instance.messages.get()).toMatchInlineSnapshot('[]')
|
|
29
|
+
|
|
30
|
+
await instance.start()
|
|
31
|
+
expect(instance.status).toEqual('started')
|
|
32
|
+
|
|
33
|
+
expect(messages.join('')).toBeDefined()
|
|
34
|
+
expect(stdouts.join('')).toBeDefined()
|
|
35
|
+
expect(instance.messages.get().join('')).toBeDefined()
|
|
36
|
+
|
|
37
|
+
await instance.stop()
|
|
38
|
+
expect(instance.status).toEqual('stopped')
|
|
39
|
+
|
|
40
|
+
expect(messages.join('')).toBeDefined()
|
|
41
|
+
expect(stdouts.join('')).toBeDefined()
|
|
42
|
+
expect(instance.messages.get()).toMatchInlineSnapshot('[]')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('behavior: instance errored (duplicate ports)', async () => {
|
|
46
|
+
const instance_1 = defineInstance({ port: 8546 })
|
|
47
|
+
const instance_2 = defineInstance({ port: 8546 })
|
|
48
|
+
|
|
49
|
+
await instance_1.start()
|
|
50
|
+
await expect(() => instance_2.start()).rejects.toThrowError('Failed to start')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('behavior: start and stop multiple times', async () => {
|
|
54
|
+
const instance = defineInstance()
|
|
55
|
+
|
|
56
|
+
await instance.start()
|
|
57
|
+
await instance.stop()
|
|
58
|
+
await instance.start()
|
|
59
|
+
await instance.stop()
|
|
60
|
+
await instance.start()
|
|
61
|
+
await instance.stop()
|
|
62
|
+
await instance.start()
|
|
63
|
+
await instance.stop()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('behavior: can subscribe to stdout', async () => {
|
|
67
|
+
const messages: string[] = []
|
|
68
|
+
const instance = defineInstance()
|
|
69
|
+
instance.on('stdout', (message) => messages.push(message))
|
|
70
|
+
|
|
71
|
+
await instance.start()
|
|
72
|
+
expect(messages.length).toBeGreaterThanOrEqual(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('behavior: can subscribe to stderr', async () => {
|
|
76
|
+
const messages: string[] = []
|
|
77
|
+
|
|
78
|
+
const instance_1 = defineInstance({ port: 8546 })
|
|
79
|
+
const instance_2 = defineInstance({ port: 8546 })
|
|
80
|
+
|
|
81
|
+
await instance_1.start()
|
|
82
|
+
instance_2.on('stderr', (message) => messages.push(message))
|
|
83
|
+
await expect(instance_2.start()).rejects.toThrow('Failed to start')
|
|
84
|
+
})
|