2024-12-20 16:23:40 +11:00
import std / os
import std / strformat
2025-01-10 23:22:34 +11:00
import std / terminal
2025-02-05 18:31:40 +11:00
from std / times import fromUnix , format , now
2025-01-13 14:38:26 +11:00
from std / unicode import toUpper
2025-01-10 23:22:34 +11:00
import std / unittest
2024-12-20 16:23:40 +11:00
import pkg / chronos
import pkg / chronos / asyncproc
import pkg / codex / logutils
2025-01-22 19:38:58 +11:00
import pkg / codex / utils / trackedfutures
2024-12-20 16:23:40 +11:00
import pkg / questionable
import pkg / questionable / results
import . / hardhatprocess
import . / utils
import .. / examples
type
2025-01-14 17:48:24 +11:00
Hardhat = ref object
process : HardhatProcess
output : seq [ string ]
port : int
2024-12-20 16:23:40 +11:00
TestManager * = ref object
configs : seq [ IntegrationTestConfig ]
tests : seq [ IntegrationTest ]
2025-01-14 17:48:24 +11:00
hardhats : seq [ Hardhat ]
2024-12-20 16:23:40 +11:00
lastHardhatPort : int
lastCodexApiPort : int
lastCodexDiscPort : int
2025-01-16 11:52:02 +11:00
# Echoes stderr if there's a test failure (eg test failed, compilation
# error) or error (eg test manager error)
debugTestHarness : bool
# Echoes stdout from Hardhat process
2024-12-20 16:23:40 +11:00
debugHardhat : bool
2025-01-16 11:52:02 +11:00
# Echoes stdout from the integration test file process. Codex process logs
# can also be output if a test uses a multinodesuite, requires
# CodexConfig.debug to be enabled
debugCodexNodes : bool
2025-01-20 12:54:57 +11:00
# Shows test status updates at regular time intervals. Useful for running
# locally while attended. Set to false for unattended runs, eg CI.
showContinuousStatusUpdates : bool
2025-02-05 18:31:40 +11:00
logsDir : string
2025-01-16 11:52:02 +11:00
timeStart : ? Moment
timeEnd : ? Moment
2024-12-20 16:23:40 +11:00
codexPortLock : AsyncLock
hardhatPortLock : AsyncLock
2025-01-15 10:40:12 +11:00
hardhatProcessLock : AsyncLock
2025-01-10 23:22:34 +11:00
testTimeout : Duration # individual test timeout
2025-01-22 19:38:58 +11:00
trackedFutures : TrackedFutures
2024-12-20 16:23:40 +11:00
IntegrationTestConfig * = object
2025-01-13 14:39:07 +11:00
startHardhat : bool
testFile : string
name : string
2024-12-20 16:23:40 +11:00
2025-01-13 14:30:18 +11:00
IntegrationTestStatus = enum ## The status of a test when it is done.
2025-01-22 19:38:30 +11:00
New # Test not yet run
Running # Test currently running
Ok # Test file launched, and exited with 0. Indicates all tests completed and passed.
Failed
# Test file launched, but exited with a non-zero exit code. Indicates either the test file did not compile, or one or more of the tests in the file failed
Timeout # Test file launched, but the tests did not complete before the timeout.
Error
# Test file did not launch correctly. Indicates an error occurred running the tests (usually an error in the harness).
2025-01-10 23:22:34 +11:00
2024-12-20 16:23:40 +11:00
IntegrationTest = ref object
2025-01-15 10:38:33 +11:00
manager : TestManager
2024-12-20 16:23:40 +11:00
config : IntegrationTestConfig
2025-02-28 22:42:01 +11:00
# process: Future[CommandExResponse].Raising(
# [AsyncProcessError, AsyncProcessTimeoutError, CancelledError]
# )
process : AsyncProcessRef
2025-01-16 11:52:51 +11:00
timeStart : ? Moment
timeEnd : ? Moment
2025-02-28 22:42:01 +11:00
output : ? ! TestOutput
2025-01-22 19:38:30 +11:00
testId : string # when used in datadir path, prevents data dir clashes
2025-01-10 23:22:34 +11:00
status : IntegrationTestStatus
2025-01-16 16:14:25 +11:00
command : string
2025-02-05 18:31:40 +11:00
logsDir : string
2024-12-20 16:23:40 +11:00
2025-02-28 22:42:01 +11:00
TestOutput = ref object
stdOut * : string
stdErr * : string
exitCode * : ? int
2025-01-14 14:56:30 +11:00
TestManagerError * = object of CatchableError
2025-01-13 14:30:18 +11:00
2025-01-14 17:52:33 +11:00
Border {. pure . } = enum
2025-01-22 19:38:30 +11:00
Left
Right
2025-01-14 17:52:33 +11:00
Align {. pure . } = enum
2025-01-22 19:38:30 +11:00
Left
Right
2025-01-14 17:52:33 +11:00
MarkerPosition {. pure . } = enum
2025-01-22 19:38:30 +11:00
Start
2025-01-14 17:52:33 +11:00
Finish
2024-12-20 16:23:40 +11:00
{. push raises : [ ] . }
logScope :
topics = " testing integration testmanager "
2025-01-22 19:38:30 +11:00
proc printOutputMarker (
test : IntegrationTest , position : MarkerPosition , msg : string
) {. gcsafe , raises : [ ] . }
2025-01-15 10:39:41 +11:00
2025-01-22 19:38:30 +11:00
proc raiseTestManagerError (
msg : string , parent : ref CatchableError = nil
) {. raises : [ TestManagerError ] . } =
2025-01-10 23:22:34 +11:00
raise newException ( TestManagerError , msg , parent )
2025-01-14 17:48:24 +11:00
template echoStyled ( args : varargs [ untyped ] ) =
try :
styledEcho args
except CatchableError as parent :
# no need to re-raise this, as it'll eventually have to be logged only
error " failed to print to terminal " , error = parent . msg
2025-02-04 12:35:15 +11:00
template ignoreCancelled ( body ) =
try :
body
except CancelledError :
discard
2025-01-10 23:22:34 +11:00
proc new * (
2025-01-22 19:38:30 +11:00
_ : type TestManager ,
configs : seq [ IntegrationTestConfig ] ,
debugTestHarness = false ,
debugHardhat = false ,
debugCodexNodes = false ,
showContinuousStatusUpdates = false ,
testTimeout = 60 . minutes ,
) : TestManager =
2024-12-20 16:23:40 +11:00
TestManager (
configs : configs ,
lastHardhatPort : 8545 ,
lastCodexApiPort : 8000 ,
lastCodexDiscPort : 9000 ,
debugTestHarness : debugTestHarness ,
2025-01-10 23:22:34 +11:00
debugHardhat : debugHardhat ,
debugCodexNodes : debugCodexNodes ,
2025-01-28 18:32:57 +11:00
showContinuousStatusUpdates : showContinuousStatusUpdates ,
2025-01-22 19:38:58 +11:00
testTimeout : testTimeout ,
trackedFutures : TrackedFutures . new ( ) ,
2024-12-20 16:23:40 +11:00
)
2025-01-13 14:39:07 +11:00
func init * (
2025-01-22 19:38:30 +11:00
_ : type IntegrationTestConfig , testFile : string , startHardhat : bool , name = " "
) : IntegrationTestConfig =
2025-01-13 14:39:07 +11:00
IntegrationTestConfig (
testFile : testFile ,
2025-01-22 19:38:30 +11:00
name : if name = = " " : testFile . extractFilename else : name ,
startHardhat : startHardhat ,
2025-01-13 14:39:07 +11:00
)
2024-12-20 16:23:40 +11:00
template withLock * ( lock : AsyncLock , body : untyped ) =
if lock . isNil :
lock = newAsyncLock ( )
await lock . acquire ( )
try :
body
finally :
try :
lock . release ( )
2025-01-10 23:22:34 +11:00
except AsyncLockError as parent :
raiseTestManagerError " lock error " , parent
proc duration ( manager : TestManager ) : Duration =
2025-01-16 11:52:51 +11:00
let now = Moment . now ( )
( manager . timeEnd | ? now ) - ( manager . timeStart | ? now )
2025-01-10 23:22:34 +11:00
2025-01-28 20:49:12 +11:00
proc allTestsPassed * ( manager : TestManager ) : ? ! bool =
2025-01-28 18:58:43 +11:00
for test in manager . tests :
if test . status in { IntegrationTestStatus . New , IntegrationTestStatus . Running } :
return failure " Integration tests not complete "
if test . status ! = IntegrationTestStatus . Ok :
2025-01-28 20:49:12 +11:00
return success false
2025-01-28 18:58:43 +11:00
2025-01-28 20:49:12 +11:00
return success true
2025-01-28 18:58:43 +11:00
2025-01-10 23:22:34 +11:00
proc duration ( test : IntegrationTest ) : Duration =
2025-01-16 11:52:51 +11:00
let now = Moment . now ( )
( test . timeEnd | ? now ) - ( test . timeStart | ? now )
2024-12-20 16:23:40 +11:00
proc startHardhat (
2025-01-22 19:38:30 +11:00
test : IntegrationTest
) : Future [ Hardhat ] {. async : ( raises : [ CancelledError , TestManagerError ] ) . } =
2024-12-20 16:23:40 +11:00
var args : seq [ string ] = @ [ ]
var port : int
2025-01-14 14:56:30 +11:00
let hardhat = Hardhat . new ( )
proc onOutputLineCaptured ( line : string ) {. raises : [ ] . } =
hardhat . output . add line
2025-01-15 10:38:33 +11:00
withLock ( test . manager . hardhatPortLock ) :
2025-01-20 16:02:44 +11:00
port = await nextFreePort ( test . manager . lastHardhatPort + 1 )
2025-01-15 10:38:33 +11:00
test . manager . lastHardhatPort = port
2024-12-20 16:23:40 +11:00
args . add ( " --port " )
args . add ( $ port )
2025-02-05 18:31:40 +11:00
if test . manager . debugHardhat :
args . add ( " --log-file= " & test . logsDir / " hardhat.log " )
2024-12-20 16:23:40 +11:00
trace " starting hardhat process on port " , port
try :
2025-01-15 10:40:12 +11:00
withLock ( test . manager . hardhatProcessLock ) :
let node = await HardhatProcess . startNode (
2025-01-22 19:38:30 +11:00
args , false , " hardhat for ' " & test . config . name & " ' " , onOutputLineCaptured
)
2025-01-15 10:40:12 +11:00
hardhat . process = node
hardhat . port = port
await node . waitUntilStarted ( )
return hardhat
2024-12-20 16:23:40 +11:00
except CancelledError as e :
raise e
except CatchableError as e :
2025-01-15 10:39:41 +11:00
if not hardhat . isNil :
test . printOutputMarker ( MarkerPosition . Start , " hardhat stdout " )
for line in hardhat . output :
echo line
test . printOutputMarker ( MarkerPosition . Finish , " hardhat stdout " )
2024-12-20 16:23:40 +11:00
raiseTestManagerError " hardhat node failed to start: " & e . msg , e
2025-01-22 19:38:30 +11:00
proc printResult ( test : IntegrationTest , colour : ForegroundColor ) =
echoStyled styleBright ,
colour ,
& " [{toUpper $test .status}] " ,
resetStyle ,
test . config . name ,
resetStyle ,
styleDim ,
& " ({test.duration}) "
proc printOutputMarker ( test : IntegrationTest , position : MarkerPosition , msg : string ) =
2025-01-14 14:56:30 +11:00
if position = = MarkerPosition . Start :
echo " "
2025-01-13 14:38:26 +11:00
2025-01-22 19:38:30 +11:00
echoStyled styleBright ,
bgWhite , fgBlack , & " ----- {toUpper $position } {test.config.name} {msg} ----- "
2025-01-14 14:56:30 +11:00
if position = = MarkerPosition . Finish :
echo " "
2025-01-13 14:38:26 +11:00
2025-02-25 17:54:43 +11:00
proc colorise ( output : string ) : string =
proc setColour ( text : string , colour : ForegroundColor ) : string =
& " {ansiForegroundColorCode(colour, true)}{text}{ansiResetCode} "
let replacements = @ [ ( " [OK] " , fgGreen ) , ( " [FAILED] " , fgRed ) , ( " [Suite] " , fgBlue ) ]
result = output
for ( text , colour ) in replacements :
result = result . replace ( text , text . setColour ( colour ) )
2025-01-10 23:22:34 +11:00
proc printResult (
2025-01-22 19:38:30 +11:00
test : IntegrationTest ,
printStdOut = test . manager . debugCodexNodes ,
printStdErr = test . manager . debugTestHarness ,
) =
case test . status
2025-01-16 11:52:51 +11:00
of IntegrationTestStatus . New :
test . printResult ( fgBlue )
of IntegrationTestStatus . Running :
test . printResult ( fgCyan )
of IntegrationTestStatus . Error :
if error = ? test . output . errorOption :
test . printResult ( fgRed )
test . printOutputMarker ( MarkerPosition . Start , " test harness errors " )
echo " Error during test execution: " , error . msg
echo " Stacktrace: " , error . getStackTrace ( )
test . printOutputMarker ( MarkerPosition . Finish , " test harness errors " )
of IntegrationTestStatus . Failed :
2025-01-10 23:22:34 +11:00
if output = ? test . output :
2025-01-14 17:48:24 +11:00
if printStdErr : #manager.debugTestHarness
2025-02-05 18:31:40 +11:00
test . printOutputMarker ( MarkerPosition . Start , " test file errors (stderr) " )
2025-02-28 22:42:01 +11:00
echo output . stdErr
2025-02-05 18:31:40 +11:00
test . printOutputMarker ( MarkerPosition . Finish , " test file errors (stderr) " )
2025-02-28 22:42:01 +11:00
# if printStdOut:
test . printOutputMarker ( MarkerPosition . Start , " codex node output (stdout) " )
2025-02-25 17:54:43 +11:00
echo output . stdOut . colorise
2025-02-28 22:42:01 +11:00
test . printOutputMarker ( MarkerPosition . Finish , " codex node output (stdout) " )
2025-01-10 23:22:34 +11:00
test . printResult ( fgRed )
2025-01-16 11:52:51 +11:00
of IntegrationTestStatus . Timeout :
2025-01-22 19:38:30 +11:00
if printStdOut and output = ? test . output :
test . printOutputMarker ( MarkerPosition . Start , " codex node output (stdout) " )
2025-02-25 17:54:43 +11:00
echo output . stdOut . colorise
2025-01-22 19:38:30 +11:00
test . printOutputMarker ( MarkerPosition . Finish , " codex node output (stdout) " )
2025-01-10 23:22:34 +11:00
test . printResult ( fgYellow )
2025-01-16 11:52:51 +11:00
of IntegrationTestStatus . Ok :
2025-01-22 19:38:30 +11:00
if printStdOut and output = ? test . output :
test . printOutputMarker ( MarkerPosition . Start , " codex node output (stdout) " )
2025-02-25 17:54:43 +11:00
echo output . stdOut . colorise
2025-01-22 19:38:30 +11:00
test . printOutputMarker ( MarkerPosition . Finish , " codex node output (stdout) " )
2025-01-10 23:22:34 +11:00
test . printResult ( fgGreen )
2025-01-14 14:56:30 +11:00
proc printSummary ( test : IntegrationTest ) =
2025-01-14 17:48:24 +11:00
test . printResult ( printStdOut = false , printStdErr = false )
2025-01-10 23:22:34 +11:00
2025-01-14 14:56:30 +11:00
proc printStart ( test : IntegrationTest ) =
2025-01-22 19:38:30 +11:00
echoStyled styleBright ,
fgMagenta , & " [Integration test started] " , resetStyle , test . config . name
2025-01-14 14:56:30 +11:00
2025-01-10 23:22:34 +11:00
proc buildCommand (
2025-01-22 19:38:30 +11:00
test : IntegrationTest , hardhatPort : ? int
) : Future [ string ] {. async : ( raises : [ CancelledError , TestManagerError ] ) . } =
2025-02-05 18:31:40 +11:00
var logging = string . none
if test . manager . debugTestHarness :
#!fmt: off
logging = some (
2025-01-22 19:38:30 +11:00
" -d:chronicles_log_level=TRACE " &
2025-02-05 18:31:40 +11:00
" -d:chronicles_disabled_topics=websock,JSONRPC-HTTP-CLIENT,JSONRPC-WS-CLIENT " &
" -d:chronicles_default_output_device=stdout " &
" -d:chronicles_sinks=textlines " )
#!fmt: on
2024-12-20 16:23:40 +11:00
2025-02-05 18:31:40 +11:00
var hhPort = string . none
if test . config . startHardhat :
without port = ? hardhatPort :
raiseTestManagerError " hardhatPort required when ' config.startHardhat ' is true "
hhPort = some " -d:HardhatPort= " & $ port
var logDir = string . none
if test . manager . debugCodexNodes :
logDir = some " -d:LogsDir= " & test . logsDir
2025-01-14 14:56:30 +11:00
2024-12-20 16:23:40 +11:00
var testFile : string
try :
testFile = absolutePath (
2025-01-22 19:38:30 +11:00
test . config . testFile , root = currentSourcePath ( ) . parentDir ( ) . parentDir ( )
)
2025-01-10 23:22:34 +11:00
except ValueError as parent :
raiseTestManagerError " bad file name, testFile: " & test . config . testFile , parent
2024-12-20 16:23:40 +11:00
2025-01-20 16:02:44 +11:00
withLock ( test . manager . codexPortLock ) :
2025-02-06 18:07:33 +11:00
# Increase the port by 1000 to allow each test to run 1000 codex nodes
2025-01-20 16:02:44 +11:00
# (clients, SPs, validators) giving a good chance the port will be free. We
# cannot rely on `nextFreePort` in multinodes entirely as there could be a
# concurrency issue where the port is determined free in mulitiple tests and
2025-02-06 18:07:33 +11:00
# then there is a clash during the run. Windows, in particular, does not
# like giving up ports.
2025-02-06 17:50:33 +11:00
let apiPort = await nextFreePort ( test . manager . lastCodexApiPort + 1000 )
2025-01-20 16:02:44 +11:00
test . manager . lastCodexApiPort = apiPort
2025-02-06 17:50:33 +11:00
let discPort = await nextFreePort ( test . manager . lastCodexDiscPort + 1000 )
2025-01-20 16:02:44 +11:00
test . manager . lastCodexDiscPort = discPort
withLock ( test . manager . hardhatPortLock ) :
try :
2025-01-22 19:38:30 +11:00
return
2025-02-05 18:31:40 +11:00
#!fmt: off
" nim c " &
& " -d:CodexApiPort={apiPort} " &
& " -d:CodexDiscPort={discPort} " &
& " -d:DebugCodexNodes={test.manager.debugCodexNodes} " &
& " -d:DebugHardhat={test.manager.debugHardhat} " &
( logDir | ? " " ) & " " &
( hhPort | ? " " ) & " " &
& " -d:TestId={test.testId} " &
( logging | ? " " ) & " " &
" --verbosity:0 " &
" --hints:off " &
" -d:release " &
" -r " &
& " {testFile} "
#!fmt: on
2025-01-20 16:02:44 +11:00
except ValueError as parent :
2025-01-22 19:38:30 +11:00
raiseTestManagerError " bad command -- \n " & " , apiPort: " & $ apiPort &
2025-02-05 18:31:40 +11:00
" , discPort: " & $ discPort & " , logging: " & logging | ? " " & " , testFile: " &
2025-01-22 19:38:30 +11:00
testFile & " , error: " & parent . msg , parent
2025-01-10 23:22:34 +11:00
2025-01-22 19:38:30 +11:00
proc setup (
test : IntegrationTest
) : Future [ ? Hardhat ] {. async : ( raises : [ CancelledError , TestManagerError ] ) . } =
2025-01-16 16:14:25 +11:00
var hardhat = Hardhat . none
2025-01-14 14:56:30 +11:00
var hardhatPort = int . none
2024-12-20 16:23:40 +11:00
2025-01-16 16:14:25 +11:00
if test . config . startHardhat :
let hh = await test . startHardhat ( )
hardhat = some hh
hardhatPort = some hh . port
test . manager . hardhats . add hh
2025-01-10 23:22:34 +11:00
2025-01-16 16:14:25 +11:00
test . command = await test . buildCommand ( hardhatPort )
2025-01-10 23:22:34 +11:00
2025-01-16 16:14:25 +11:00
return hardhat
2025-01-10 23:22:34 +11:00
2025-01-16 16:14:25 +11:00
proc teardown (
2025-01-22 19:38:30 +11:00
test : IntegrationTest , hardhat : ? Hardhat
) {. async : ( raises : [ CancelledError ] ) . } =
2025-01-31 16:21:55 +11:00
if test . config . startHardhat and hardhat = ? hardhat and not hardhat . process . isNil :
2025-01-14 14:56:30 +11:00
try :
2025-01-16 16:14:25 +11:00
trace " Stopping hardhat " , name = test . config . name
2025-01-15 10:38:33 +11:00
await hardhat . process . stop ( )
2025-01-31 16:21:55 +11:00
trace " Hardhat stopped " , name = test . config . name
except CancelledError as e :
raise e
2025-01-15 10:38:33 +11:00
except CatchableError as e :
2025-01-14 14:56:30 +11:00
warn " Failed to stop hardhat node, continuing " ,
error = e . msg , test = test . config . name
2025-01-16 16:14:25 +11:00
if test . manager . debugHardhat :
2025-01-14 14:56:30 +11:00
test . printOutputMarker ( MarkerPosition . Start , " Hardhat stdout " )
for line in hardhat . output :
echo line
test . printOutputMarker ( MarkerPosition . Finish , " Hardhat stdout " )
2025-01-22 19:38:30 +11:00
test . manager . hardhats . keepItIf ( it ! = hardhat )
2025-01-16 16:14:25 +11:00
2025-02-04 12:35:15 +11:00
proc untilTimeout (
2025-02-28 22:42:01 +11:00
fut : InternalRaisesFuture , timeout : Duration
) : Future [ void ] {. async : ( raises : [ CancelledError , AsyncTimeoutError ] ) . } =
## Returns a Future that completes when either fut finishes or timeout elapses,
## or if they finish at the same time. If timeout elapses, an AsyncTimeoutError
## is raised. If fut fails, its error is raised.
2025-02-04 12:35:15 +11:00
let timer = sleepAsync ( timeout )
2025-02-28 22:42:01 +11:00
defer :
# called even when exception raised
2025-02-04 12:35:15 +11:00
# race does not cancel its futures when it's cancelled
2025-02-28 22:42:01 +11:00
await fut . cancelAndWait ( )
await timer . cancelAndWait ( )
try :
discard await race ( fut , timer )
2025-02-04 12:35:15 +11:00
except ValueError as e :
2025-02-28 22:42:01 +11:00
raiseAssert " should not happen "
if fut . finished ( ) : # or fut and timer both finished simultaneously
if fut . failed ( ) :
await fut # raise fut error
return # unreachable, for readability
else : # timeout
raise newException ( AsyncTimeoutError , " Timed out " )
2025-02-04 12:35:15 +11:00
2025-01-16 16:14:25 +11:00
proc start ( test : IntegrationTest ) {. async : ( raises : [ ] ) . } =
logScope :
2025-02-28 22:42:01 +11:00
name = test . config . name
duration = test . duration
2025-01-16 16:14:25 +11:00
trace " Running test "
2025-02-05 18:31:40 +11:00
if test . manager . debugCodexNodes :
test . logsDir = test . manager . logsDir / sanitize ( test . config . name )
try :
createDir ( test . logsDir )
except CatchableError as e :
2025-02-28 22:42:01 +11:00
test . timeEnd = some Moment . now ( )
test . status = IntegrationTestStatus . Error
test . output = TestOutput . failure ( e )
2025-02-05 18:31:40 +11:00
error " failed to create test log dir " , logDir = test . logsDir , error = e . msg
2025-01-16 16:14:25 +11:00
test . timeStart = some Moment . now ( )
test . status = IntegrationTestStatus . Running
var hardhat = none Hardhat
2025-02-04 12:35:15 +11:00
ignoreCancelled :
2025-01-16 16:14:25 +11:00
try :
hardhat = await test . setup ( )
except TestManagerError as e :
test . timeEnd = some Moment . now ( )
test . status = IntegrationTestStatus . Error
2025-02-28 22:42:01 +11:00
test . output = TestOutput . failure ( e )
error " Failed to start hardhat and build command " , error = e . msg
2025-01-16 16:14:25 +11:00
return
2025-02-28 22:42:01 +11:00
trace " Starting parallel integration test " ,
command = test . command , timeout = test . manager . testTimeout
test . printStart ( )
2025-01-16 16:14:25 +11:00
try :
2025-02-28 22:42:01 +11:00
test . process = await startProcess (
command = test . command ,
# arguments = test.command.split(" "),
options = { AsyncProcessOption . EvalCommand } ,
stdoutHandle = AsyncProcess . Pipe ,
stderrHandle = AsyncProcess . Pipe ,
)
except AsyncProcessError as e :
test . timeEnd = some Moment . now ( )
error " Failed to start test process " , error = e . msg
test . output = TestOutput . failure ( e )
test . status = IntegrationTestStatus . Error
return
2025-02-04 12:35:15 +11:00
2025-02-28 22:42:01 +11:00
defer :
trace " Tearing down test "
await noCancel test . teardown ( hardhat )
2025-02-04 12:35:15 +11:00
test . timeEnd = some Moment . now ( )
if test . status = = IntegrationTestStatus . Ok :
info " Test completed " , name = test . config . name , duration = test . duration
2025-02-28 22:42:01 +11:00
if not test . process . isNil :
if test . process . running | ? false :
var output = test . output . expect ( " should have output value " )
trace " Terminating test process "
try :
output . exitCode =
some ( await noCancel test . process . terminateAndWaitForExit ( 500 . millis ) )
test . output = success output
except AsyncProcessError , AsyncProcessTimeoutError :
warn " Test process failed to terminate, check for zombies "
await test . process . closeWait ( )
let outputReader = test . process . stdoutStream . read ( )
let errorReader = test . process . stderrStream . read ( )
var output = TestOutput . new ( )
test . output = success ( output )
output . exitCode =
try :
some ( await test . process . waitForExit ( test . manager . testTimeout ) )
except AsyncProcessTimeoutError as e :
test . timeEnd = some Moment . now ( )
test . status = IntegrationTestStatus . Timeout
error " Test process failed to exit before timeout " ,
timeout = test . manager . testTimeout
return
except AsyncProcessError as e :
test . timeEnd = some Moment . now ( )
test . status = IntegrationTestStatus . Error
test . output = TestOutput . failure ( e )
error " Test failed to complete " , error = e . msg
return
test . status =
if output . exitCode = = some QuitSuccess :
IntegrationTestStatus . Ok
else :
IntegrationTestStatus . Failed
try :
output . stdOut = string . fromBytes ( await outputReader )
output . stdErr = string . fromBytes ( await errorReader )
except AsyncStreamError as e :
2025-01-16 16:14:25 +11:00
test . timeEnd = some Moment . now ( )
2025-02-28 22:42:01 +11:00
error " Failed to read test process output stream " , error = e . msg
test . output = TestOutput . failure ( e )
2025-01-16 16:14:25 +11:00
test . status = IntegrationTestStatus . Error
2025-02-28 22:42:01 +11:00
return
# let processRunning = test.process.waitForExit(test.manager.testTimeout)
# trace "Running test until timeout", timeout = test.manager.testTimeout
# let completedBeforeTimeout =
# await processRunning.withTimeout(test.manager.testTimeout)
# if completedBeforeTimeout:
# else: # timed out
# test.timeEnd = some Moment.now()
# test.status = IntegrationTestStatus.Timeout
# error "Test timed out, terminating process"
# process will be terminated in defer
# try:
# output.exitCode = some(await test.process.terminateAndWaitForExit(100.millis))
# except AsyncProcessError, AsyncProcessTimeoutError:
# warn "Test process failed to terminate, check for zombies"
2025-01-16 16:14:25 +11:00
2025-01-16 11:52:51 +11:00
proc continuallyShowUpdates ( manager : TestManager ) {. async : ( raises : [ ] ) . } =
2025-02-04 12:35:15 +11:00
ignoreCancelled :
2025-01-16 11:52:51 +11:00
while true :
2025-01-22 19:38:30 +11:00
let sleepDuration = if manager . duration < 5 . minutes : 30 . seconds else : 1 . minutes
2025-01-16 11:52:51 +11:00
if manager . tests . len > 0 :
echo " "
2025-01-22 19:38:30 +11:00
echoStyled styleBright ,
bgWhite , fgBlack , & " Integration tests status after {manager.duration} "
2025-01-16 11:52:51 +11:00
for test in manager . tests :
2025-01-16 16:14:25 +11:00
test . printResult ( false , false )
if manager . tests . len > 0 :
echo " "
2025-01-16 11:52:51 +11:00
await sleepAsync ( sleepDuration )
2025-01-16 16:14:25 +11:00
proc run ( test : IntegrationTest ) {. async : ( raises : [ ] ) . } =
2025-02-04 12:35:15 +11:00
ignoreCancelled :
2025-02-28 22:42:01 +11:00
let futStart = test . start ( )
# await futStart
try :
await futStart . untilTimeout ( test . manager . testTimeout )
except AsyncTimeoutError :
# if output =? test.output and output.exitCode.isNone: # timeout
error " Test timed out "
test . timeEnd = some Moment . now ( )
test . status = IntegrationTestStatus . Timeout
# await futStart.cancelAndWait()
2025-01-16 16:14:25 +11:00
test . printResult ( )
2025-01-14 14:56:30 +11:00
proc runTests ( manager : TestManager ) {. async : ( raises : [ CancelledError ] ) . } =
2025-01-16 16:14:25 +11:00
var testFutures : seq [ Future [ void ] ]
2024-12-20 16:23:40 +11:00
2025-01-16 11:52:51 +11:00
manager . timeStart = some Moment . now ( )
2024-12-20 16:23:40 +11:00
2025-01-22 19:38:30 +11:00
echoStyled styleBright ,
bgWhite , fgBlack , " \n [Integration Test Manager] Starting parallel integration tests "
2025-01-10 23:22:34 +11:00
2024-12-20 16:23:40 +11:00
for config in manager . configs :
2025-01-22 19:38:30 +11:00
var test =
IntegrationTest ( manager : manager , config : config , testId : $ uint16 . example )
2025-01-16 16:14:25 +11:00
manager . tests . add test
let futRun = test . run ( )
testFutures . add futRun
2024-12-20 16:23:40 +11:00
2025-02-28 22:42:01 +11:00
defer :
2025-01-31 16:21:55 +11:00
for fut in testFutures :
2025-02-28 22:42:01 +11:00
await fut . cancelAndWait ( )
await allFutures testFutures
2024-12-20 16:23:40 +11:00
2025-01-16 11:52:51 +11:00
manager . timeEnd = some Moment . now ( )
2024-12-20 16:23:40 +11:00
2025-01-10 23:22:34 +11:00
proc withBorder (
2025-01-22 19:38:30 +11:00
msg : string , align = Align . Left , width = 67 , borders = { Border . Left , Border . Right }
) : string =
2025-01-10 23:22:34 +11:00
if borders . contains ( Border . Left ) :
result & = " | "
if align = = Align . Left :
result & = msg . alignLeft ( width )
elif align = = Align . Right :
result & = msg . align ( width )
if borders . contains ( Border . Right ) :
result & = " | "
2025-01-22 19:38:30 +11:00
proc printResult ( manager : TestManager ) {. raises : [ TestManagerError ] . } =
2024-12-20 16:23:40 +11:00
var successes = 0
var totalDurationSerial : Duration
2025-01-22 19:38:30 +11:00
let showSummary =
manager . debugCodexNodes or manager . debugHardhat or manager . debugTestHarness
2025-01-16 16:14:25 +11:00
if showSummary :
echo " "
2025-01-22 19:38:30 +11:00
echoStyled styleBright ,
styleUnderscore , bgWhite , fgBlack , & " INTEGRATION TESTS RESULT "
2025-01-16 16:14:25 +11:00
2024-12-20 16:23:40 +11:00
for test in manager . tests :
2025-01-10 23:22:34 +11:00
totalDurationSerial + = test . duration
2025-01-13 14:30:18 +11:00
if test . status = = IntegrationTestStatus . Ok :
2025-01-10 23:22:34 +11:00
inc successes
2025-01-16 16:14:25 +11:00
# because debug output can really make things hard to read, show a nice
# summary of test results
if showSummary :
test . printResult ( false , false )
2024-12-20 16:23:40 +11:00
# estimated time saved as serial execution with a single hardhat instance
# incurs less overhead
2025-01-22 19:38:30 +11:00
let relativeTimeSaved =
( ( totalDurationSerial - manager . duration ) . nanos * 100 ) div
( totalDurationSerial . nanos )
let passingStyle = if successes < manager . tests . len : fgRed else : fgGreen
2025-01-16 16:14:25 +11:00
2025-01-10 23:22:34 +11:00
echo " \n ▢=====================================================================▢ "
2025-01-22 19:38:30 +11:00
echoStyled " | " ,
styleBright ,
styleUnderscore ,
" INTEGRATION TEST SUMMARY " ,
resetStyle ,
" " . withBorder ( Align . Right , 43 , { Border . Right } )
2025-01-10 23:22:34 +11:00
echo " " . withBorder ( )
2025-01-22 19:38:30 +11:00
echoStyled styleBright ,
" | TOTAL TIME : " ,
resetStyle ,
( $ manager . duration ) . withBorder ( Align . Right , 49 , { Border . Right } )
echoStyled styleBright ,
" | TIME SAVED (EST): " ,
resetStyle ,
( & " {relativeTimeSaved}% " ) . withBorder ( Align . Right , 49 , { Border . Right } )
echoStyled " | " ,
styleBright ,
passingStyle ,
" PASSING : " ,
resetStyle ,
passingStyle ,
( & " {successes} / {manager.tests.len} " ) . align ( 49 ) ,
resetStyle ,
" | "
2024-12-20 16:23:40 +11:00
echo " ▢=====================================================================▢ "
2025-01-22 19:38:30 +11:00
proc start * (
manager : TestManager
) {. async : ( raises : [ CancelledError , TestManagerError ] ) . } =
2025-02-05 18:31:40 +11:00
try :
if manager . debugCodexNodes :
let startTime = now ( ) . format ( " yyyy-MM-dd ' _ ' HH:mm:ss " )
let logsDir =
currentSourcePath . parentDir ( ) / " logs " /
sanitize ( startTime & " __IntegrationTests " )
createDir ( logsDir )
manager . logsDir = logsDir
#!fmt: off
echoStyled bgWhite , fgBlack , styleBright ,
" \n \n " ,
styleUnderscore ,
" ℹ ️ LOGS AVAILABLE ℹ ️ \n \n " ,
resetStyle , bgWhite , fgBlack , styleBright ,
""" Logs for this run will be available at: """ ,
resetStyle , bgWhite , fgBlack ,
& " \n \n {logsDir} \n \n " ,
resetStyle , bgWhite , fgBlack , styleBright ,
" NOTE: For CI runs, logs will be attached as artefacts \n "
#!fmt: on
except IOError as e :
raiseTestManagerError " failed to create hardhat log directory: " & e . msg , e
except OSError as e :
raiseTestManagerError " failed to create hardhat log directory: " & e . msg , e
2025-01-22 19:38:30 +11:00
if manager . showContinuousStatusUpdates :
let fut = manager . continuallyShowUpdates ( )
manager . trackedFutures . track fut
asyncSpawn fut
2025-01-16 16:14:25 +11:00
2025-02-28 15:25:41 +11:00
let futRunTests = manager . runTests ( )
manager . trackedFutures . track futRunTests
await futRunTests
2025-01-16 16:14:25 +11:00
2025-01-10 23:22:34 +11:00
manager . printResult ( )
2024-12-20 16:23:40 +11:00
proc stop * ( manager : TestManager ) {. async : ( raises : [ CancelledError ] ) . } =
2025-02-28 22:42:01 +11:00
trace " [stop] START canelling tracked "
2025-01-22 19:38:58 +11:00
await manager . trackedFutures . cancelTracked ( )
2025-02-28 22:42:01 +11:00
trace " [stop] DONE cancelling tracked "
2025-01-22 19:38:58 +11:00
2025-02-28 22:42:01 +11:00
trace " [stop] stopping running processes "
2024-12-20 16:23:40 +11:00
for test in manager . tests :
2025-02-28 22:42:01 +11:00
if not test . process . isNil and test . process . running | ? false :
try :
trace " [stop] terminating process " , name = test . config . name
discard await test . process . terminateAndWaitForExit ( 100 . millis )
except AsyncProcessError , AsyncProcessTimeoutError :
warn " Test process failed to terminate, ignoring... " , name = test . config . name
finally :
await test . process . closeWait ( )
trace " [stop] stopping hardhats "
2024-12-20 16:23:40 +11:00
for hardhat in manager . hardhats :
try :
2025-02-28 22:42:01 +11:00
trace " [stop] stopping hardhat "
2025-01-29 15:55:17 +11:00
if not hardhat . process . isNil :
2025-02-28 22:42:01 +11:00
await noCancel hardhat . process . stop ( )
2024-12-20 16:23:40 +11:00
except CatchableError as e :
2025-01-22 19:38:30 +11:00
trace " failed to stop hardhat node " , error = e . msg
2025-02-28 22:42:01 +11:00
trace " [stop] done stopping hardhats "