fixes to get hardhat process launching at the test-level

- removed ethersuite as the base testing suite for multinodesuite because the need to launch hardhat before ethersuite setup and kill hardhat after ethersuite teardown was required. Removing ethersuite allowed for checking if a connection to hardhat was established. If no connection established, hardhat would need to be started via the test config. If there was a connection established, a snapshot is taken during test setup and reverted during teardown.
- modified the way the hardhat process was launched, because evaluating a command using chronos startProcess would wait for the command to complete before access to the output stream was given. Instead, we now launch the hardhat executable from node_modules/.bin/hardhat.
- modify the way the processes are exited by killing them immediately.
- fix warnings
This commit is contained in:
Eric 2023-11-30 17:14:28 +11:00
parent 109f9461b3
commit cccec6574c
No known key found for this signature in database
8 changed files with 143 additions and 81 deletions

View File

@ -1,6 +1,5 @@
import std/httpclient
import std/strutils
import std/sequtils
from pkg/libp2p import Cid, `$`, init
import pkg/chronicles

View File

@ -33,6 +33,9 @@ method startedOutput(node: CodexProcess): string =
method processOptions(node: CodexProcess): set[AsyncProcessOption] =
return {AsyncProcessOption.StdErrToStdOut}
method outputLineEndings(node: CodexProcess): string =
return "\n"
method onOutputLineCaptured(node: CodexProcess, line: string) =
discard

View File

@ -4,12 +4,10 @@ import pkg/confutils
import pkg/chronicles
import pkg/chronos
import pkg/stew/io2
import std/osproc
import std/os
import std/sets
import std/streams
import std/sequtils
import std/strutils
import std/sugar
import pkg/codex/conf
import pkg/codex/utils/trackedfutures
import ./codexclient
@ -30,13 +28,16 @@ method workingDir(node: HardhatProcess): string =
return currentSourcePath() / ".." / ".." / ".." / "vendor" / "codex-contracts-eth"
method executable(node: HardhatProcess): string =
return "npm start"
return "node_modules" / ".bin" / "hardhat"
method startedOutput(node: HardhatProcess): string =
return "Started HTTP and WebSocket JSON-RPC server at"
method processOptions(node: HardhatProcess): set[AsyncProcessOption] =
return {AsyncProcessOption.EvalCommand, AsyncProcessOption.StdErrToStdOut}
return {}
method outputLineEndings(node: HardhatProcess): string =
return "\n"
proc openLogFile(node: HardhatProcess, logFilePath: string): IoHandle =
let logFileHandle = openFile(
@ -53,11 +54,31 @@ proc openLogFile(node: HardhatProcess, logFilePath: string): IoHandle =
return fileHandle
method start*(node: HardhatProcess) {.async.} =
let poptions = node.processOptions + {AsyncProcessOption.StdErrToStdOut}
trace "starting node",
args = node.arguments,
executable = node.executable,
workingDir = node.workingDir,
processOptions = poptions
try:
node.process = await startProcess(
node.executable,
node.workingDir,
@["node", "--export", "deployment-localhost.json"].concat(node.arguments),
options = poptions,
stdoutHandle = AsyncProcess.Pipe
)
except CatchableError as e:
error "failed to start node process", error = e.msg
proc startNode*(
_: type HardhatProcess,
args: seq[string] = @[],
args: seq[string],
debug: string | bool = false,
name: string = "hardhat"
name: string
): Future[HardhatProcess] {.async.} =
var logFilePath = ""
@ -70,14 +91,20 @@ proc startNode*(
arguments.add arg
trace "starting hardhat node", arguments
echo ">>> starting hardhat node with args: ", arguments
let node = await NodeProcess.startNode(arguments, debug, "hardhat")
let hardhat = HardhatProcess(node)
## Starts a Hardhat Node with the specified arguments.
## Set debug to 'true' to see output of the node.
let hardhat = HardhatProcess(
arguments: arguments,
debug: ($debug != "false"),
trackedFutures: TrackedFutures.new(),
name: "hardhat"
)
await hardhat.start()
if logFilePath != "":
hardhat.logFile = some hardhat.openLogFile(logFilePath)
# let hardhat = HardhatProcess()
return hardhat
method onOutputLineCaptured(node: HardhatProcess, line: string) =
@ -91,9 +118,10 @@ method onOutputLineCaptured(node: HardhatProcess, line: string) =
method stop*(node: HardhatProcess) {.async.} =
# terminate the process
procCall NodeProcess(node).stop()
await procCall NodeProcess(node).stop()
if logFile =? node.logFile:
trace "closing hardhat log file"
discard logFile.closeFile()
method removeDataDir*(node: HardhatProcess) =

View File

@ -90,7 +90,6 @@ template marketplacesuite*(name: string, body: untyped) =
discard
setup:
echo "[marketplacesuite.setup] setup start"
marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner())
let tokenAddress = await marketplace.token()
token = Erc20Token.new(tokenAddress, ethProvider.getSigner())

View File

@ -4,13 +4,15 @@ import std/strutils
import std/sugar
import std/times
import pkg/chronicles
import ../ethertest
import pkg/ethers
import pkg/asynctest
import ./hardhatprocess
import ./codexprocess
import ./hardhatconfig
import ./codexconfig
export ethertest
export asynctest
export ethers except `%`
export hardhatprocess
export codexprocess
export hardhatconfig
@ -44,25 +46,28 @@ proc nextFreePort(startPort: int): Future[int] {.async.} =
"lsof -ti:"
var port = startPort
while true:
trace "checking if port is free", port
let portInUse = await execCommandEx(cmd & $port)
if portInUse.stdOutput == "":
echo "port ", port, " is free"
trace "port is free", port
return port
else:
inc port
template multinodesuite*(name: string, body: untyped) =
ethersuite name:
asyncchecksuite name:
var running: seq[RunningNode]
var bootstrap: string
let starttime = now().format("yyyy-MM-dd'_'HH:mm:ss")
var currentTestName = ""
var nodeConfigs: NodeConfigs
var ethProvider {.inject, used.}: JsonRpcProvider
var accounts {.inject, used.}: seq[Address]
var snapshot: JsonNode
template test(tname, startNodeConfigs, tbody) =
echo "[multinodes] inside test template, tname: ", tname, ", startNodeConfigs: ", startNodeConfigs
currentTestName = tname
nodeConfigs = startNodeConfigs
test tname:
@ -101,11 +106,11 @@ template multinodesuite*(name: string, body: untyped) =
if config.logFile:
let updatedLogFile = getLogFile(role, none int)
args.add "--log-file=" & updatedLogFile
echo ">>> [multinodes] starting hardhat node with args: ", args
let node = await HardhatProcess.startNode(args, config.debugEnabled, "hardhat")
await node.waitUntilStarted()
debug "started new hardhat node"
trace "hardhat node started"
return node
proc newCodexProcess(roleIdx: int,
@ -148,25 +153,30 @@ template multinodesuite*(name: string, body: untyped) =
"--eth-account=" & $accounts[nodeIdx]])
let node = await CodexProcess.startNode(args, conf.debugEnabled, $role & $roleIdx)
echo "[multinodes.newCodexProcess] waiting until ", role, " node started"
await node.waitUntilStarted()
echo "[multinodes.newCodexProcess] ", role, " NODE STARTED"
trace "node started", nodeName = $role & $roleIdx
return node
proc clients(): seq[CodexProcess] {.used.} =
proc hardhat: HardhatProcess =
for r in running:
if r.role == Role.Hardhat:
return HardhatProcess(r.node)
return nil
proc clients: seq[CodexProcess] {.used.} =
return collect:
for r in running:
if r.role == Role.Client:
CodexProcess(r.node)
proc providers(): seq[CodexProcess] {.used.} =
proc providers: seq[CodexProcess] {.used.} =
return collect:
for r in running:
if r.role == Role.Provider:
CodexProcess(r.node)
proc validators(): seq[CodexProcess] {.used.} =
proc validators: seq[CodexProcess] {.used.} =
return collect:
for r in running:
if r.role == Role.Validator:
@ -204,35 +214,38 @@ template multinodesuite*(name: string, body: untyped) =
return await newCodexProcess(validatorIdx, config, Role.Validator)
setup:
echo "[multinodes.setup] setup start"
if not nodeConfigs.hardhat.isNil:
echo "[multinodes.setup] starting hardhat node "
let node = await startHardhatNode()
running.add RunningNode(role: Role.Hardhat, node: node)
try:
ethProvider = JsonRpcProvider.new("ws://localhost:8545")
# if hardhat was NOT started by the test, take a snapshot so it can be
# reverted in the test teardown
if nodeConfigs.hardhat.isNil:
snapshot = await send(ethProvider, "evm_snapshot")
accounts = await ethProvider.listAccounts()
except CatchableError as e:
fatal "failed to connect to hardhat", error = e.msg
raiseAssert "Hardhat not running. Run hardhat manually before executing tests, or include a HardhatConfig in the test setup."
if not nodeConfigs.clients.isNil:
for i in 0..<nodeConfigs.clients.numNodes:
echo "[multinodes.setup] starting client node ", i
let node = await startClientNode()
running.add RunningNode(
role: Role.Client,
node: node
)
echo "[multinodes.setup] added running client node ", i
if i == 0:
echo "[multinodes.setup] getting client 0 bootstrap spr"
bootstrap = CodexProcess(node).client.info()["spr"].getStr()
echo "[multinodes.setup] got client 0 bootstrap spr: ", bootstrap
if not nodeConfigs.providers.isNil:
for i in 0..<nodeConfigs.providers.numNodes:
echo "[multinodes.setup] starting provider node ", i
let node = await startProviderNode()
running.add RunningNode(
role: Role.Provider,
node: node
)
echo "[multinodes.setup] added running provider node ", i
if not nodeConfigs.validators.isNil:
for i in 0..<nodeConfigs.validators.numNodes:
@ -241,12 +254,21 @@ template multinodesuite*(name: string, body: untyped) =
role: Role.Validator,
node: node
)
echo "[multinodes.setup] added running validator node ", i
teardown:
for r in running:
await r.node.stop() # also stops rest client
r.node.removeDataDir()
for nodes in @[validators(), clients(), providers()]:
for node in nodes:
await node.stop() # also stops rest client
node.removeDataDir()
# if hardhat was started in the test, kill the node
# otherwise revert the snapshot taken in the test setup
let hardhat = hardhat()
if not hardhat.isNil:
await hardhat.stop()
else:
discard await send(ethProvider, "evm_revert", @[snapshot])
running = @[]
body

View File

@ -3,11 +3,7 @@ import pkg/questionable/results
import pkg/confutils
import pkg/chronicles
import pkg/libp2p
import pkg/stew/byteutils
import std/osproc
import std/os
import std/sequtils
import std/streams
import std/strutils
import codex/conf
import codex/utils/exceptions
@ -40,24 +36,35 @@ method startedOutput(node: NodeProcess): string {.base.} =
method processOptions(node: NodeProcess): set[AsyncProcessOption] {.base.} =
raiseAssert "[processOptions] not implemented"
method outputLineEndings(node: NodeProcess): string {.base.} =
raiseAssert "[outputLineEndings] not implemented"
method onOutputLineCaptured(node: NodeProcess, line: string) {.base.} =
raiseAssert "[onOutputLineCaptured] not implemented"
method start(node: NodeProcess) {.base, async.} =
method start*(node: NodeProcess) {.base, async.} =
logScope:
nodeName = node.name
trace "starting node", args = node.arguments
let poptions = node.processOptions + {AsyncProcessOption.StdErrToStdOut}
trace "starting node",
args = node.arguments,
executable = node.executable,
workingDir = node.workingDir,
processOptions = poptions
node.process = await startProcess(
node.executable,
node.workingDir,
node.arguments,
options = node.processOptions,
stdoutHandle = AsyncProcess.Pipe
)
try:
node.process = await startProcess(
node.executable,
node.workingDir,
node.arguments,
options = poptions,
stdoutHandle = AsyncProcess.Pipe
)
except CatchableError as e:
error "failed to start node process", error = e.msg
proc captureOutput*(
proc captureOutput(
node: NodeProcess,
output: string,
started: Future[void]
@ -68,20 +75,23 @@ proc captureOutput*(
trace "waiting for output", output
let stream = node.process.stdOutStream
let stream = node.process.stdoutStream
try:
while(let line = await stream.readLine(0, "\n"); line != ""):
if node.debug:
# would be nice if chronicles could parse and display with colors
echo line
while node.process.running.option == some true:
while(let line = await stream.readLine(0, node.outputLineEndings); line != ""):
if node.debug:
# would be nice if chronicles could parse and display with colors
echo line
if not started.isNil and not started.finished and line.contains(output):
started.complete()
if not started.isNil and not started.finished and line.contains(output):
started.complete()
node.onOutputLineCaptured(line)
node.onOutputLineCaptured(line)
await sleepAsync(1.millis)
await sleepAsync(1.millis)
except AsyncStreamReadError as e:
error "error reading output stream", error = e.msgDetail
@ -110,18 +120,20 @@ method stop*(node: NodeProcess) {.base, async.} =
await node.trackedFutures.cancelTracked()
if node.process != nil:
try:
if err =? node.process.terminate().errorOption:
error "failed to terminate node process", errorCode = err
discard await node.process.waitForExit(timeout=5.seconds)
# close process' streams
trace "waiting for node process to exit"
let exitCode = await node.process.waitForExit(ZeroDuration)
if exitCode > 0:
error "failed to exit process, check for zombies", exitCode
trace "closing node process' streams"
await node.process.closeWait()
except AsyncTimeoutError as e:
error "waiting for process exit timed out", error = e.msgDetail
except CatchableError as e:
error "error stopping node process", error = e.msg
finally:
node.process = nil
trace "node stopped"
proc waitUntilStarted*(node: NodeProcess) {.async.} =

View File

@ -20,7 +20,7 @@ import ./marketplacesuite
# You can also pass a string in same format like for the `--log-level` parameter
# to enable custom logging levels for specific topics like: debug2 = "INFO; TRACE: marketplace"
twonodessuite "Integration tests", debug1 = true, debug2 = true:
twonodessuite "Integration tests", debug1 = false, debug2 = false:
setup:
# Our Hardhat configuration does use automine, which means that time tracked by `ethProvider.currentTime()` is not
# advanced until blocks are mined and that happens only when transaction is submitted.
@ -232,20 +232,20 @@ marketplacesuite "Marketplace payouts":
test "expired request partially pays out for stored time",
NodeConfigs(
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally
# hardhat: HardhatConfig().withLogFile()
clients:
CodexConfig()
.nodes(1)
.debug() # uncomment to enable console log output.debug()
# .debug() # uncomment to enable console log output.debug()
.withLogFile() # uncomment to output log file to tests/integration/logs/<start_datetime> <suite_name>/<test_name>/<node_role>_<node_idx>.log
.withLogTopics("node", "erasure"),
providers:
CodexConfig()
.nodes(1)
.debug() # uncomment to enable console log output
# .debug() # uncomment to enable console log output
.withLogFile() # uncomment to output log file to tests/integration/logs/<start_datetime> <suite_name>/<test_name>/<node_role>_<node_idx>.log
.withLogTopics("marketplace", "sales", "reservations", "node", "proving", "clock"),
):

View File

@ -16,20 +16,20 @@ logScope:
marketplacesuite "Hosts submit regular proofs":
test "hosts submit periodic proofs for slots they fill", NodeConfigs(
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# hardhat: HardhatConfig().debug().withLogFile(),
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally
# hardhat: HardhatConfig().withLogFile(),
clients:
CodexConfig()
.nodes(1)
.debug() # uncomment to enable console log output
# .debug() # uncomment to enable console log output
.withLogFile() # uncomment to output log file to tests/integration/logs/<start_datetime> <suite_name>/<test_name>/<node_role>_<node_idx>.log
.withLogTopics("node"),
providers:
CodexConfig()
.nodes(1)
.debug() # uncomment to enable console log output
# .debug() # uncomment to enable console log output
.withLogFile() # uncomment to output log file to tests/integration/logs/<start_datetime> <suite_name>/<test_name>/<node_role>_<node_idx>.log
.withLogTopics("marketplace", "sales", "reservations", "node"),
):
@ -56,7 +56,6 @@ marketplacesuite "Hosts submit regular proofs":
await subscription.unsubscribe()
marketplacesuite "Simulate invalid proofs":
# TODO: these are very loose tests in that they are not testing EXACTLY how
@ -65,8 +64,8 @@ marketplacesuite "Simulate invalid proofs":
# proofs are being marked as missed by the validator.
test "slot is freed after too many invalid proofs submitted", NodeConfigs(
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# hardhat: HardhatConfig().debug().withLogFile(),
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally
# hardhat: HardhatConfig().withLogFile(),
clients:
CodexConfig()
@ -117,8 +116,8 @@ marketplacesuite "Simulate invalid proofs":
await subscription.unsubscribe()
test "slot is not freed when not enough invalid proofs submitted", NodeConfigs(
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# hardhat: HardhatConfig().debug().withLogFile(),
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally
# hardhat: HardhatConfig().withLogFile(),
clients:
CodexConfig()
@ -170,8 +169,8 @@ marketplacesuite "Simulate invalid proofs":
await subscription.unsubscribe()
test "host that submits invalid proofs is paid out less", NodeConfigs(
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# hardhat: HardhatConfig().debug().withLogFile(),
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally
# hardhat: HardhatConfig().withLogFile(),
clients:
CodexConfig()