mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-02-21 22:13:07 +00:00
231 lines
7.7 KiB
Nim
231 lines
7.7 KiB
Nim
import std/httpclient
|
|
import std/os
|
|
import std/sequtils
|
|
import std/strutils
|
|
import std/sugar
|
|
import std/times
|
|
import pkg/storage/conf
|
|
import pkg/storage/logutils
|
|
import pkg/chronos/transports/stream
|
|
import pkg/questionable
|
|
import ./storageconfig
|
|
import ./storageprocess
|
|
import ./nodeconfigs
|
|
import ./utils
|
|
import ../asynctest
|
|
import ../checktest
|
|
|
|
export asynctest
|
|
export storageprocess
|
|
export storageconfig
|
|
export nodeconfigs
|
|
|
|
{.push raises: [].}
|
|
|
|
type
|
|
RunningNode* = ref object
|
|
role*: Role
|
|
node*: NodeProcess
|
|
|
|
Role* {.pure.} = enum
|
|
Client
|
|
|
|
MultiNodeSuiteError = object of CatchableError
|
|
SuiteTimeoutError = object of MultiNodeSuiteError
|
|
|
|
const HardhatPort {.intdefine.}: int = 8545
|
|
const StorageApiPort {.intdefine.}: int = 8080
|
|
const StorageDiscPort {.intdefine.}: int = 8090
|
|
const TestId {.strdefine.}: string = "TestId"
|
|
const StorageLogToFile {.booldefine.}: bool = false
|
|
const StorageLogLevel {.strdefine.}: string = ""
|
|
const StorageLogsDir {.strdefine.}: string = ""
|
|
|
|
proc raiseMultiNodeSuiteError(
|
|
msg: string, parent: ref CatchableError = nil
|
|
) {.raises: [MultiNodeSuiteError].} =
|
|
raise newException(MultiNodeSuiteError, msg, parent)
|
|
|
|
template withLock(lock: AsyncLock, body: untyped) =
|
|
if lock.isNil:
|
|
lock = newAsyncLock()
|
|
|
|
await lock.acquire()
|
|
try:
|
|
body
|
|
finally:
|
|
try:
|
|
lock.release()
|
|
except AsyncLockError as parent:
|
|
raiseMultiNodeSuiteError "lock error", parent
|
|
|
|
proc sanitize(pathSegment: string): string =
|
|
var sanitized = pathSegment
|
|
for invalid in invalidFilenameChars.items:
|
|
sanitized = sanitized.replace(invalid, '_').replace(' ', '_')
|
|
sanitized
|
|
|
|
proc getTempDirName*(starttime: string, role: Role, roleIdx: int): string =
|
|
getTempDir() / "Storage" / sanitize($starttime) / sanitize($role & "_" & $roleIdx)
|
|
|
|
template multinodesuite*(suiteName: string, body: untyped) =
|
|
asyncchecksuite suiteName:
|
|
var running {.inject, used.}: seq[RunningNode]
|
|
var bootstrapNodes: seq[string]
|
|
let starttime = now().format("yyyy-MM-dd'_'HH:mm:ss")
|
|
var currentTestName = ""
|
|
var nodeConfigs: NodeConfigs
|
|
var snapshot: JsonNode
|
|
var lastUsedHardhatPort = HardhatPort
|
|
var lastUsedStorageApiPort = StorageApiPort
|
|
var lastUsedStorageDiscPort = StorageDiscPort
|
|
var storagePortLock: AsyncLock
|
|
|
|
template test(tname, startNodeConfigs, tbody) =
|
|
currentTestName = tname
|
|
nodeConfigs = startNodeConfigs
|
|
test tname:
|
|
tbody
|
|
|
|
proc updatePort(url: var string, port: int) =
|
|
let parts = url.split(':')
|
|
url = @[parts[0], parts[1], $port].join(":")
|
|
|
|
proc newStorageProcess(
|
|
roleIdx: int, conf: StorageConfig, role: Role
|
|
): Future[NodeProcess] {.async: (raises: [MultiNodeSuiteError, CancelledError]).} =
|
|
let nodeIdx = running.len
|
|
var config = conf
|
|
let datadir = getDataDir(TestId, currentTestName, $starttime, $role, some roleIdx)
|
|
|
|
try:
|
|
if config.logFile.isSome or StorageLogToFile:
|
|
try:
|
|
let updatedLogFile = getLogFile(
|
|
StorageLogsDir, starttime, suiteName, currentTestName, $role, some roleIdx
|
|
)
|
|
config.withLogFile(updatedLogFile)
|
|
except IOError as e:
|
|
raiseMultiNodeSuiteError(
|
|
"failed to start " & $role &
|
|
" because logfile path could not be obtained: " & e.msg,
|
|
e,
|
|
)
|
|
except OSError as e:
|
|
raiseMultiNodeSuiteError(
|
|
"failed to start " & $role &
|
|
" because logfile path could not be obtained: " & e.msg,
|
|
e,
|
|
)
|
|
|
|
when StorageLogLevel != "":
|
|
config.addCliOption("--log-level", StorageLogLevel)
|
|
|
|
var apiPort, discPort: int
|
|
withLock(storagePortLock):
|
|
apiPort = await nextFreePort(lastUsedStorageApiPort + nodeIdx)
|
|
discPort = await nextFreePort(lastUsedStorageDiscPort + nodeIdx)
|
|
config.addCliOption("--api-port", $apiPort)
|
|
config.addCliOption("--disc-port", $discPort)
|
|
lastUsedStorageApiPort = apiPort
|
|
lastUsedStorageDiscPort = discPort
|
|
|
|
for bootstrapNode in bootstrapNodes:
|
|
config.addCliOption("--bootstrap-node", bootstrapNode)
|
|
|
|
config.addCliOption("--data-dir", datadir)
|
|
config.addCliOption("--nat", "none")
|
|
config.addCliOption("--listen-addrs", "/ip4/127.0.0.1/tcp/0")
|
|
except StorageConfigError as e:
|
|
raiseMultiNodeSuiteError "invalid cli option, error: " & e.msg
|
|
|
|
try:
|
|
let node = await StorageProcess.startNode(
|
|
config.cliArgs, config.debugEnabled, $role & $roleIdx
|
|
)
|
|
await node.waitUntilStarted()
|
|
trace "node started", nodeName = $role & $roleIdx
|
|
return node
|
|
except StorageConfigError as e:
|
|
raiseMultiNodeSuiteError "failed to get cli args from config: " & e.msg, e
|
|
except NodeProcessError as e:
|
|
raiseMultiNodeSuiteError "node not started, error: " & e.msg, e
|
|
|
|
proc clients(): seq[StorageProcess] {.used.} =
|
|
return collect:
|
|
for r in running:
|
|
if r.role == Role.Client:
|
|
StorageProcess(r.node)
|
|
|
|
proc startClientNode(conf: StorageConfig): Future[NodeProcess] {.async.} =
|
|
let clientIdx = clients().len
|
|
return await newStorageProcess(clientIdx, conf, Role.Client)
|
|
|
|
proc teardownImpl() {.async.} =
|
|
for nodes in @[clients()]:
|
|
for node in nodes:
|
|
await node.stop() # also stops rest client
|
|
try:
|
|
node.removeDataDir()
|
|
except StorageProcessError as e:
|
|
error "Failed to remove data dir during teardown", error = e.msg
|
|
|
|
running = @[]
|
|
|
|
template failAndTeardownOnError(message: string, tryBody: untyped) =
|
|
try:
|
|
tryBody
|
|
except CancelledError as e:
|
|
await teardownImpl()
|
|
when declared(teardownAllIMPL):
|
|
teardownAllIMPL()
|
|
fail()
|
|
quit(1)
|
|
except CatchableError as er:
|
|
fatal message, error = er.msg
|
|
echo "[FATAL] ", message, ": ", er.msg
|
|
await teardownImpl()
|
|
when declared(teardownAllIMPL):
|
|
teardownAllIMPL()
|
|
fail()
|
|
quit(1)
|
|
|
|
proc updateBootstrapNodes(
|
|
node: StorageProcess
|
|
): Future[void] {.async: (raises: [MultiNodeSuiteError]).} =
|
|
try:
|
|
without ninfo =? await node.client.info():
|
|
# raise CatchableError instead of Defect (with .get or !) so we
|
|
# can gracefully shutdown and prevent zombies
|
|
raiseMultiNodeSuiteError "Failed to get node info"
|
|
bootstrapNodes.add ninfo["spr"].getStr()
|
|
except CatchableError as e:
|
|
raiseMultiNodeSuiteError "Failed to get node info: " & e.msg, e
|
|
|
|
setupAll:
|
|
# When this file is run with `-d:chronicles_sinks=textlines[file]`, we
|
|
# need to set the log file path at runtime, otherwise chronicles didn't seem to
|
|
# create a log file even when using an absolute path
|
|
when defaultChroniclesStream.outputs is (FileOutput,) and StorageLogsDir.len > 0:
|
|
let logFile =
|
|
StorageLogsDir / sanitize(
|
|
getAppFilename().extractFilename & ".chronicles.log"
|
|
)
|
|
let success = defaultChroniclesStream.outputs[0].open(logFile, fmAppend)
|
|
doAssert success, "Failed to open log file: " & logFile
|
|
|
|
setup:
|
|
trace "Setting up test", suite = suiteName, test = currentTestName, nodeConfigs
|
|
if var clients =? nodeConfigs.clients:
|
|
failAndTeardownOnError "failed to start client nodes":
|
|
for config in clients.configs:
|
|
let node = await startClientNode(config)
|
|
running.add RunningNode(role: Role.Client, node: node)
|
|
await StorageProcess(node).updateBootstrapNodes()
|
|
|
|
teardown:
|
|
await teardownImpl()
|
|
trace "Test completed", suite = suiteName, test = currentTestName
|
|
|
|
body
|