Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/execa-stop-exit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"prool": patch
---

Fixed process `stop()` hanging when the process had already exited.
5 changes: 5 additions & 0 deletions .changeset/tempo-faucet-node-address.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"prool": patch
---

Fixed Tempo faucet submitting funding transactions to the node default `http://localhost:8545` instead of the resolved `--http.port`.
87 changes: 87 additions & 0 deletions src/instances/tempo.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as os from 'node:os'
import getPort from 'get-port'
import { Instance } from 'prool'
import { afterEach, expect, test } from 'vitest'
import { command } from './tempo.js'

const instances: Instance.Instance[] = []
const slowTestTimeout = 30_000
Expand Down Expand Up @@ -87,3 +89,88 @@ test('behavior: can subscribe to stderr', async () => {
instance_2.on('stderr', (message) => messages.push(message))
await expect(instance_2.start()).rejects.toThrow('Failed to start')
})

test(
'behavior: faucet funds address',
{ timeout: slowTestTimeout },
async () => {
const instance = defineInstance()
await instance.start()

const rpc = async (method: string, params: unknown[]) => {
const response = await fetch(`http://localhost:${port}`, {
body: JSON.stringify({ id: 0, jsonrpc: '2.0', method, params }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
})
return await response.json()
}

// Funding races the first wall-clock block; expiring-nonce validation rejects until one exists.
let json: { result?: string[] } = {}
for (let i = 0; i < 50; i++) {
json = await rpc('tempo_fundAddress', [
'0x000000000000000000000000000000000000beef',
])
if (json.result) break
await new Promise((resolve) => setTimeout(resolve, 200))
}

expect(
JSON.stringify(json).replaceAll(/0x[0-9a-f]{64}/g, '<hash>'),
).toMatchInlineSnapshot(
`"{"jsonrpc":"2.0","id":0,"result":["<hash>","<hash>","<hash>","<hash>"]}"`,
)

// balanceOf(0x...beef) on the first faucet token.
let balance = 0n
for (let i = 0; i < 50; i++) {
const { result } = await rpc('eth_call', [
{
data: '0x70a08231000000000000000000000000000000000000000000000000000000000000beef',
to: '0x20c0000000000000000000000000000000000000',
},
'latest',
])
balance = BigInt(result ?? 0)
if (balance > 0n) break
await new Promise((resolve) => setTimeout(resolve, 200))
}
expect(balance).toBeGreaterThan(0n)
},
)

const redact = (args: string[]) =>
args.join(' ').replaceAll(os.tmpdir(), '<tmpdir>')

test('command: default', () => {
expect(redact(command({ port: 8545 }))).toMatchInlineSnapshot(
`"node --authrpc.port 8575 --datadir <tmpdir>/.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:8545 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws.port 8565"`,
)
})

test('command: behavior: faucet node address', () => {
expect(
redact(
command({ faucet: { nodeAddress: 'http://localhost:1337' }, port: 8545 }),
),
).toMatchInlineSnapshot(
`"node --authrpc.port 8575 --datadir <tmpdir>/.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:1337 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws.port 8565"`,
)
})

test('command: behavior: faucet disabled', () => {
expect(
redact(command({ faucet: { enabled: false }, port: 8545 })),
).toMatchInlineSnapshot(
`"node --authrpc.port 8575 --datadir <tmpdir>/.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled false --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 8545 --port 8555 --ws.port 8565"`,
)
})

test('command: behavior: http port override', () => {
expect(
redact(command({ http: { port: 1337 }, port: 8545 })),
).toMatchInlineSnapshot(
`"node --authrpc.port 8575 --datadir <tmpdir>/.prool/tempo.8545 --dev --dev.block-time 50ms --engine.disable-precompile-cache --engine.legacy-state-root --faucet.address 0x20c0000000000000000000000000000000000000 0x20c0000000000000000000000000000000000001 0x20c0000000000000000000000000000000000002 0x20c0000000000000000000000000000000000003 --faucet.amount 1000000000000 --faucet.enabled --faucet.private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --faucet.node-address http://localhost:1337 --http.addr 0.0.0.0 --http.api all --http.corsdomain * --http.port 1337 --port 8555 --ws.port 8565"`,
)
})
20 changes: 19 additions & 1 deletion src/instances/tempo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@ export function command(parameters: tempo.Parameters): string[] {
},
}

const args = deepAssign(defaultParameters, rest)

// Faucet submits funding transactions over RPC; point it at the resolved http port (node default: 8545).
const faucet = args['faucet'] as tempo.Parameters['faucet']
const http = args['http'] as { port: number }
if (faucet?.enabled && !faucet.nodeAddress)
faucet.nodeAddress = `http://localhost:${http.port}`

return [
'node',
...toArgs(deepAssign(defaultParameters, rest), {
...toArgs(args, {
arraySeparator: null,
}),
]
Expand Down Expand Up @@ -177,6 +185,16 @@ export declare namespace tempo {
* Amount for each faucet funding transaction
*/
amount?: bigint | undefined
/**
* Whether the faucet is enabled
* @default true
*/
enabled?: boolean | undefined
/**
* Node RPC address the faucet submits funding transactions to
* @default `http://localhost:${port}`
*/
nodeAddress?: string | undefined
/**
* Faucet funding mnemonic
*/
Expand Down
7 changes: 4 additions & 3 deletions src/processes/execa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ export function execa(parameters: execa.Parameters): execa.ReturnType {
let process: Process_internal

async function stop(signal?: keyof SignalConstants | number) {
const killed = process.kill(signal)
if (!killed) return
return new Promise((resolve) => process.on('close', resolve))
process.kill(signal)
// Process may have exited on its own; 'exit' will not re-fire.
if (process.exitCode !== null || process.signalCode !== null) return
return new Promise((resolve) => process.once('exit', resolve))
}

return {
Expand Down
46 changes: 46 additions & 0 deletions src/testcontainers/Instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,49 @@ test('behavior: can subscribe to stdout', async () => {
})

test.skip('behavior: can subscribe to stderr', () => {})

test('behavior: faucet funds address', { timeout: 120_000 }, async () => {
const instance = defineInstance({ startupTimeout: 60_000 })
await instance.start()

const rpc = async (method: string, params: unknown[]) => {
const response = await fetch(`http://${instance.host}:${instance.port}`, {
body: JSON.stringify({ id: 0, jsonrpc: '2.0', method, params }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
})
return await response.json()
}

// Funding races the first wall-clock block; expiring-nonce validation rejects until one exists.
let json: { result?: string[] } = {}
for (let i = 0; i < 100; i++) {
json = await rpc('tempo_fundAddress', [
'0x000000000000000000000000000000000000beef',
])
if (json.result) break
await new Promise((resolve) => setTimeout(resolve, 500))
}

expect(
JSON.stringify(json).replaceAll(/0x[0-9a-f]{64}/g, '<hash>'),
).toMatchInlineSnapshot(
`"{"jsonrpc":"2.0","id":0,"result":["<hash>","<hash>","<hash>","<hash>"]}"`,
)

// balanceOf(0x...beef) on the first faucet token.
let balance = 0n
for (let i = 0; i < 100; i++) {
const { result } = await rpc('eth_call', [
{
data: '0x70a08231000000000000000000000000000000000000000000000000000000000000beef',
to: '0x20c0000000000000000000000000000000000000',
},
'latest',
])
balance = BigInt(result ?? 0)
if (balance > 0n) break
await new Promise((resolve) => setTimeout(resolve, 500))
}
expect(balance).toBeGreaterThan(0n)
})
Loading