-
Notifications
You must be signed in to change notification settings - Fork 154
eest: add standalone CLI runners for statetest, blocktest, and enginetest #4101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
dcbe0c0
af026db
83b987d
34a92a7
6d8e295
69154c6
c1efd1c
550ad89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,15 +17,23 @@ import | |
| web3/execution_types, | ||
| json_rpc/rpcclient, | ||
| json_rpc/rpcserver, | ||
| ../../execution_chain/db/core_db/memory_only, | ||
| ../../execution_chain/db/ledger, | ||
| ../../execution_chain/core/chain/forked_chain, | ||
| ../../execution_chain/core/executor, | ||
| ../../execution_chain/core/validate, | ||
| ../../execution_chain/evm/state, | ||
| ../../execution_chain/evm/types, | ||
| ../../execution_chain/beacon/beacon_engine, | ||
| ../../execution_chain/common/common, | ||
| ../../hive_integration/engine_client, | ||
| ./eest_helpers, | ||
| stew/byteutils, | ||
| chronos | ||
|
|
||
| import ../../tools/common/helpers as chp except HardFork | ||
| import ../../tools/evmstate/helpers except HardFork | ||
|
|
||
| proc parseBlocks*(node: JsonNode): seq[BlockDesc] = | ||
| for x in node: | ||
| try: | ||
|
|
@@ -89,14 +97,278 @@ proc processFile*(fileName: string, statelessEnabled = false): bool = | |
|
|
||
| return testPass | ||
|
|
||
| proc runTestFast( | ||
| com: CommonRef, | ||
| parentHeader: Header, | ||
| baseTxFrame: CoreDbTxRef, | ||
| unit: BlockchainUnitEnv): Result[void, string] = | ||
| ## Execute blocks directly through the executor, bypassing ForkedChainRef. | ||
| let blocks = parseBlocks(unit.blocks) | ||
| var | ||
| parent = parentHeader | ||
| currentFrame = baseTxFrame | ||
|
|
||
| for blk in blocks: | ||
| let childFrame = currentFrame.txFrameBegin() | ||
|
|
||
| let vmState = BaseVMState() | ||
| vmState.init( | ||
| parent = parent, | ||
| header = blk.blk.header, | ||
| com = com, | ||
| txFrame = childFrame, | ||
| ) | ||
|
|
||
| # Header + kinship validation (gas limits, timestamps, etc.) | ||
| let valRes = com.validateHeaderAndKinship( | ||
| blk.blk, | ||
| blockAccessList = Opt.none(BlockAccessListRef), | ||
| skipPreExecBalCheck = true, | ||
| parent, | ||
| childFrame) | ||
| if valRes.isErr: | ||
| if blk.badBlock: | ||
| childFrame.dispose() | ||
| continue | ||
| else: | ||
| childFrame.dispose() | ||
| return err("Good block failed validation: " & valRes.error) | ||
|
|
||
| # Execute the block through the executor directly | ||
| let res = vmState.processBlock( | ||
| blk.blk, | ||
| skipValidation = false, | ||
| skipReceipts = false, | ||
| skipUncles = true, | ||
| skipStateRootCheck = false, | ||
| skipPostExecBalCheck = true, | ||
| ) | ||
|
|
||
| if res.isOk: | ||
| if blk.badBlock: | ||
| childFrame.dispose() | ||
| return err("A bug? bad block imported") | ||
| # Persist the header so the next block can look up its parent | ||
| childFrame.persistHeader( | ||
| blk.blk.header.computeBlockHash, blk.blk.header).isOkOr: | ||
| childFrame.dispose() | ||
| return err("Failed to persist header: " & error) | ||
| parent = blk.blk.header | ||
| currentFrame = childFrame | ||
| else: | ||
| childFrame.dispose() | ||
| if not blk.badBlock: | ||
| return err("Good block rejected: " & res.error) | ||
|
|
||
| # Verify final state root | ||
| let stateRoot = currentFrame.getStateRoot().valueOr: | ||
| return err("Failed to get state root") | ||
| if stateRoot != parent.stateRoot: | ||
| return err("Final stateRoot mismatch: got " & $stateRoot & | ||
| " expected " & $parent.stateRoot) | ||
|
|
||
| ok() | ||
|
|
||
| proc processFileFast*(fileName: string): bool = | ||
| let | ||
| fixture = parseFixture(fileName, BlockchainFixture) | ||
|
|
||
| var testPass = true | ||
| for unit in fixture.units: | ||
| try: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a big try/except clause. We prefer to place these around specific areas where they are needed |
||
| let | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason we can't use |
||
| header = unit.unit.genesisBlockHeader.to(Header) | ||
| memDB = newCoreDbRef DefaultDbMemory | ||
| baseTx = memDB.baseTxFrame() | ||
| ledger = LedgerRef.init(baseTx) | ||
| config = getChainConfig(unit.unit.network) | ||
|
|
||
| config.chainId = unit.unit.config.chainid | ||
| config.blobSchedule = unit.unit.config.blobSchedule | ||
|
|
||
| doAssert(unit.unit.genesisBlockHeader.hash == header.computeRlpHash) | ||
|
|
||
| setupLedger(unit.unit.pre, ledger) | ||
| ledger.persist() | ||
|
|
||
| baseTx.persistHeaderAndSetHead(header).isOkOr: | ||
| echo "\nTestName: ", unit.name, " Failed to persist genesis: ", error, "\n" | ||
| testPass = false | ||
| continue | ||
|
|
||
| let com = CommonRef.new(memDB, config) | ||
|
|
||
| let res = runTestFast(com, header, baseTx, unit.unit) | ||
| if res.isErr: | ||
| echo "\nTestName: ", unit.name, " RunTest error: ", res.error, "\n" | ||
| testPass = false | ||
| except ValueError as exc: | ||
| echo "\nTestName: ", unit.name, " Error: ", exc.msg, "\n" | ||
| testPass = false | ||
|
|
||
| return testPass | ||
|
|
||
| when isMainModule: | ||
| import | ||
| os, | ||
| unittest2 | ||
| std/[os, parseopt, strutils] | ||
|
|
||
| type | ||
| TestResult = object | ||
| name: string | ||
| pass: bool | ||
| error: string | ||
|
|
||
| if paramCount() == 0: | ||
| proc collectJsonFiles(path: string): seq[string] = | ||
| if fileExists(path): | ||
| return @[path] | ||
| for entry in walkDirRec(path): | ||
| if entry.endsWith(".json") and "/.meta/" notin entry: | ||
| result.add(entry) | ||
|
|
||
| proc printUsage() = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are to expand this standalone functionality, to a proper tool, we should be using confutils for cli option parsing to be consistent with our other binaries / tools. |
||
| let testFile = getAppFilename().splitPath().tail | ||
| echo "Usage: " & testFile & " vector.json" | ||
| echo "Usage: " & testFile & " [options] <file-or-directory>" | ||
| echo "" | ||
| echo "Options:" | ||
| echo " --fast Bypass ForkedChainRef; call executor directly" | ||
| echo " --run=<pattern> Substring filter on file paths" | ||
| echo " --json Output results as JSON array" | ||
| echo " --workers=<N> Number of workers (accepted, runs sequentially)" | ||
| echo "" | ||
| echo "Examples:" | ||
| echo " " & testFile & " vector.json" | ||
| echo " " & testFile & " --fast /path/to/blockchain_tests/" | ||
| echo " " & testFile & " --json /path/to/blockchain_tests/" | ||
| echo " " & testFile & " --run=eip7702 /path/to/blockchain_tests/" | ||
|
|
||
| var | ||
| fastEnabled = false | ||
| jsonEnabled = false | ||
| runFilter = "" | ||
| workers = 1 | ||
| inputPath = "" | ||
|
|
||
| var p = initOptParser(commandLineParams()) | ||
| while true: | ||
| p.next() | ||
| case p.kind | ||
| of cmdEnd: break | ||
| of cmdLongOption, cmdShortOption: | ||
| case p.key.toLowerAscii | ||
| of "fast": | ||
| fastEnabled = true | ||
| of "json": | ||
| jsonEnabled = true | ||
| of "run": | ||
| runFilter = p.val | ||
| of "workers": | ||
| workers = parseInt(p.val) | ||
| of "help", "h": | ||
| printUsage() | ||
| quit(QuitSuccess) | ||
| else: | ||
| echo "Unknown option: ", p.key | ||
| printUsage() | ||
| quit(QuitFailure) | ||
| of cmdArgument: | ||
| inputPath = p.key | ||
|
|
||
| if inputPath.len == 0: | ||
| printUsage() | ||
| quit(QuitFailure) | ||
|
|
||
| var files = collectJsonFiles(inputPath) | ||
|
|
||
| if runFilter.len > 0: | ||
| var filtered: seq[string] | ||
| for f in files: | ||
| if runFilter in f: | ||
| filtered.add(f) | ||
| files = filtered | ||
|
|
||
| if files.len == 0: | ||
| echo "No matching .json files found." | ||
| quit(QuitFailure) | ||
|
|
||
| check processFile(paramStr(1)) | ||
| type FileResult = object | ||
| path: string | ||
| pass: bool | ||
|
|
||
| var | ||
| results: seq[TestResult] | ||
| passCount = 0 | ||
| failCount = 0 | ||
|
|
||
| if workers > 1: | ||
| discard workers # TODO: in-process parallelism requires GC-safe procs | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need this for your cli compatibility, then perhaps log here that it doesn't do anything when set >1, and also mention this on the cli flag help. |
||
|
|
||
| for f in files: | ||
| if jsonEnabled: | ||
| # Per-test results for JSON output | ||
| let fixture = parseFixture(f, BlockchainFixture) | ||
| for unit in fixture.units: | ||
| let header = unit.unit.genesisBlockHeader.to(Header) | ||
| if fastEnabled: | ||
| try: | ||
| let | ||
| memDB = newCoreDbRef DefaultDbMemory | ||
| baseTx = memDB.baseTxFrame() | ||
| ledger = LedgerRef.init(baseTx) | ||
| config = getChainConfig(unit.unit.network) | ||
| config.chainId = unit.unit.config.chainid | ||
| config.blobSchedule = unit.unit.config.blobSchedule | ||
| setupLedger(unit.unit.pre, ledger) | ||
| ledger.persist() | ||
| let persistRes = baseTx.persistHeaderAndSetHead(header) | ||
| if persistRes.isErr: | ||
| inc failCount | ||
| results.add(TestResult(name: unit.name, pass: false, | ||
| error: "persist genesis: " & persistRes.error)) | ||
| continue | ||
| let com = CommonRef.new(memDB, config) | ||
| let res = runTestFast(com, header, baseTx, unit.unit) | ||
| if res.isOk: | ||
| inc passCount | ||
| results.add(TestResult(name: unit.name, pass: true, error: "")) | ||
| else: | ||
| inc failCount | ||
| results.add(TestResult(name: unit.name, pass: false, error: res.error)) | ||
| except CatchableError as e: | ||
| inc failCount | ||
| results.add(TestResult(name: unit.name, pass: false, error: e.msg)) | ||
| else: | ||
| let env = prepareEnv(unit.unit, header, rpcEnabled = false) | ||
| let res = waitFor env.runTest(unit.unit) | ||
| if res.isOk: | ||
| inc passCount | ||
| results.add(TestResult(name: unit.name, pass: true, error: "")) | ||
| else: | ||
| inc failCount | ||
| results.add(TestResult(name: unit.name, pass: false, error: res.error)) | ||
| env.close() | ||
| else: | ||
| let pass = if fastEnabled: processFileFast(f) | ||
| else: processFile(f) | ||
| let rel = if dirExists(inputPath): | ||
| f.relativePath(inputPath) | ||
| else: | ||
| f.splitPath().tail | ||
| if pass: | ||
| inc passCount | ||
| echo "PASS: ", rel | ||
| else: | ||
| inc failCount | ||
| echo "FAIL: ", rel | ||
|
|
||
| if jsonEnabled: | ||
| var arr = newJArray() | ||
| for r in results: | ||
| arr.add(%*{"name": r.name, "pass": r.pass, "error": r.error}) | ||
| echo $arr | ||
| else: | ||
| echo "" | ||
| echo "Total: ", files.len, " | Passed: ", passCount, | ||
| " | Failed: ", failCount | ||
|
|
||
| if failCount > 0: | ||
| quit(QuitFailure) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This proc contains quite a lot of custom logic for a testing proc. This is not maintainable.