Skip to content

Commit f7f2b2e

Browse files
authored
Improve VM Test Reporting (#4297)
* Add per-directory reporting to VM test runners (as custom vitest reporter) * Integrate state reporting into blockchain test runner
1 parent 0fb3e18 commit f7f2b2e

4 files changed

Lines changed: 357 additions & 108 deletions

File tree

packages/vm/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@
5454
"test:blockchain:transitionForks": "echo 'ByzantiumToConstantinopleFixAt5 EIP158ToByzantiumAt5 FrontierToHomesteadAt5 HomesteadToDaoAt5 HomesteadToEIP150At5 BerlinToLondonAt5' | xargs -n1 | xargs -I {} sh -c 'VITE_FORK={} VITE_VERIFY_TEST_AMOUNT_ALL_TESTS=1 npx vitest test/tester/blockchain.spec.ts'",
5555
"test:blockchain:buildIntegrity": "npm run test:blockchain -- --file='randomStatetest303'",
5656
"test:buildIntegrity": "npm run test:state -- --test='stackOverflow'",
57-
"test:est:stable:state": "TEST_PATH=../execution-spec-tests/stable/state_tests npx vitest run test/tester/executionSpecState.test.ts",
58-
"test:est:stable:blockchain": "TEST_PATH=../execution-spec-tests/stable/blockchain_tests npx vitest run test/tester/executionSpecBlockchain.test.ts",
59-
"test:est:dev:state": "TEST_PATH=../execution-spec-tests/dev/state_tests npx vitest run test/tester/executionSpecState.test.ts",
57+
"test:est:stable:state": "TEST_PATH=../execution-spec-tests/stable/state_tests npx vitest run --reporter=default --reporter=./test/tester/util/perDirectoryReporter.ts test/tester/executionSpecState.test.ts",
58+
"test:est:stable:blockchain": "TEST_PATH=../execution-spec-tests/stable/blockchain_tests npx vitest run --reporter=default --reporter=./test/tester/util/perDirectoryReporter.ts test/tester/executionSpecBlockchain.test.ts",
59+
"test:est:dev:state": "TEST_PATH=../execution-spec-tests/dev/state_tests npx vitest run --reporter=default --reporter=./test/tester/util/perDirectoryReporter.ts test/tester/executionSpecState.test.ts",
6060
"test:est:dev:blockchain": "npm run test:est:dev:blockchain:v700",
61-
"test:est:dev:blockchain:v700": "TEST_PATH=../execution-spec-tests/dev/blockchain_tests/amsterdam/v700_mixed_with_other_eips npx vitest run test/tester/executionSpecBlockchain.test.ts",
62-
"test:est:dev:blockchain:v301": "TEST_PATH=../execution-spec-tests/dev/blockchain_tests/amsterdam/v301_single_bal_no_bal_defs npx vitest run test/tester/executionSpecBlockchain.test.ts",
63-
"test:est:dev:blockchain:v200": "TEST_PATH=../execution-spec-tests/dev/blockchain_tests/amsterdam/v200_bal_defs_somewhat_outdated npx vitest run test/tester/executionSpecBlockchain.test.ts",
61+
"test:est:dev:blockchain:v700": "TEST_PATH=../execution-spec-tests/dev/blockchain_tests/amsterdam/v700_mixed_with_other_eips npx vitest run --reporter=default --reporter=./test/tester/util/perDirectoryReporter.ts test/tester/executionSpecBlockchain.test.ts",
62+
"test:est:dev:blockchain:v301": "TEST_PATH=../execution-spec-tests/dev/blockchain_tests/amsterdam/v301_single_bal_no_bal_defs npx vitest run --reporter=default --reporter=./test/tester/util/perDirectoryReporter.ts test/tester/executionSpecBlockchain.test.ts",
63+
"test:est:dev:blockchain:v200": "TEST_PATH=../execution-spec-tests/dev/blockchain_tests/amsterdam/v200_bal_defs_somewhat_outdated npx vitest run --reporter=default --reporter=./test/tester/util/perDirectoryReporter.ts test/tester/executionSpecBlockchain.test.ts",
6464
"test:state": "tsx ./test/tester/vitest-wrapper.ts",
6565
"test:state:allForks": "npm run test:state:newForks && npm run test:state:oldForks && npm run test:state:transitionForks",
6666
"test:state:newForks": "echo 'Prague' | xargs -n1 | xargs -I {} sh -c 'VITE_FORK={} VITE_VERIFY_TEST_AMOUNT_ALL_TESTS=1 npx vitest test/tester/state.spec.ts'",

packages/vm/test/tester/executionSpecBlockchain.test.ts

Lines changed: 148 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { createVM, runBlock } from '../../src/index.ts'
2121
import { setupPreConditions } from '../util.ts'
2222
import { createCommonForFork, loadExecutionSpecFixtures } from './executionSpecTestLoader.ts'
2323
import { compareBAL } from './util/balComparatorAI.ts'
24+
import { annotateFixture } from './util/perDirectoryReporter.ts'
2425

2526
const customFixturesPath = process.env.TEST_PATH ?? '../execution-spec-tests'
2627
const fixturesPath = path.resolve(customFixturesPath)
@@ -77,8 +78,9 @@ if (fs.existsSync(fixturesPath) === false) {
7778
return
7879
}
7980

80-
for (const { id, fork, data } of fixtures) {
81-
it(`${fork}: ${id}`, async () => {
81+
for (const { id, fork, filePath, data } of fixtures) {
82+
it(`${fork}: ${id}`, async ({ task }) => {
83+
annotateFixture(task, filePath, fixturesPath, 'blockchain tests')
8284
await runBlockchainTestCase(fork, data, assert, kzg)
8385
}, 360000) // 6 minutes
8486
}
@@ -121,124 +123,170 @@ export async function runBlockchainTestCase(
121123

122124
let parentBlock = genesisBlock
123125

124-
for (const {
125-
rlp,
126-
expectException,
127-
blockHeader,
128-
rlp_decoded,
129-
blockAccessList,
130-
} of testData.blocks) {
131-
const expectedHash = blockHeader?.hash ?? rlp_decoded?.blockHeader?.hash ?? undefined
132-
let block: Block | undefined
133-
try {
134-
block = createBlockFromRLP(hexToBytes(rlp), { common: vm.common, setHardfork: true })
135-
//t.equal(bytesToHex(block.serialize()), rlp, 'correct block RLP')
136-
if (expectedHash !== undefined) {
137-
//t.equal(bytesToHex(block.hash()), expectedHash, 'correct block hash')
138-
}
139-
const result = await runBlock(vm, {
140-
block,
141-
root: parentBlock.header.stateRoot,
142-
setHardfork: true,
143-
})
144-
await vm.blockchain.putBlock(block)
145-
parentBlock = block
146-
t.notExists(expectException, `Should have thrown with: ${expectException}`)
126+
// Capture errors from block processing so post-state checks still run; this
127+
// surfaces state-divergence details even when a block throws unexpectedly.
128+
let runError: Error | undefined
147129

148-
// Check if the block level access list is correct
149-
if (common.isActivatedEIP(7928)) {
150-
let balDiffMessage = ''
151-
if (blockAccessList !== undefined) {
152-
const expectedBAL = createBlockLevelAccessListFromJSON(blockAccessList)
153-
// Use the BAL comparator to show a colored diff of any mismatches
154-
// Pass false to skip console output during test, we'll include it in the assertion
155-
const { diffString } = compareBAL(
156-
expectedBAL.raw(),
157-
result.blockLevelAccessList!.raw(),
158-
false,
159-
)
160-
balDiffMessage = diffString
130+
try {
131+
for (const {
132+
rlp,
133+
expectException,
134+
blockHeader,
135+
rlp_decoded,
136+
blockAccessList,
137+
} of testData.blocks) {
138+
const expectedHash = blockHeader?.hash ?? rlp_decoded?.blockHeader?.hash ?? undefined
139+
let block: Block | undefined
140+
try {
141+
block = createBlockFromRLP(hexToBytes(rlp), { common: vm.common, setHardfork: true })
142+
//t.equal(bytesToHex(block.serialize()), rlp, 'correct block RLP')
143+
if (expectedHash !== undefined) {
144+
//t.equal(bytesToHex(block.hash()), expectedHash, 'correct block hash')
145+
}
146+
const result = await runBlock(vm, {
147+
block,
148+
root: parentBlock.header.stateRoot,
149+
setHardfork: true,
150+
})
151+
await vm.blockchain.putBlock(block)
152+
parentBlock = block
153+
t.notExists(expectException, `Should have thrown with: ${expectException}`)
154+
155+
// Check if the block level access list is correct
156+
if (common.isActivatedEIP(7928)) {
157+
let balDiffMessage = ''
158+
if (blockAccessList !== undefined) {
159+
const expectedBAL = createBlockLevelAccessListFromJSON(blockAccessList)
160+
// Use the BAL comparator to show a colored diff of any mismatches
161+
// Pass false to skip console output during test, we'll include it in the assertion
162+
const { diffString } = compareBAL(
163+
expectedBAL.raw(),
164+
result.blockLevelAccessList!.raw(),
165+
false,
166+
)
167+
balDiffMessage = diffString
168+
t.deepEqual(
169+
bytesToHex(expectedBAL.hash()),
170+
bytesToHex(block.header.blockAccessListHash!),
171+
`expected block level access list correct${balDiffMessage}`,
172+
)
173+
}
161174
t.deepEqual(
162-
bytesToHex(expectedBAL.hash()),
175+
bytesToHex(result.blockLevelAccessList!.hash()),
163176
bytesToHex(block.header.blockAccessListHash!),
164-
`expected block level access list correct${balDiffMessage}`,
177+
`generated block level access list correct${balDiffMessage}`,
165178
)
166179
}
167-
t.deepEqual(
168-
bytesToHex(result.blockLevelAccessList!.hash()),
169-
bytesToHex(block.header.blockAccessListHash!),
170-
`generated block level access list correct${balDiffMessage}`,
180+
} catch (e: any) {
181+
if (expectException === undefined) {
182+
throw e
183+
}
184+
// Check if the block failed due to an expected exception
185+
t.exists(
186+
expectException,
187+
`expectException should be defined. Error: ${e.message}\n${e.stack}`,
171188
)
172-
}
173-
} catch (e: any) {
174-
if (expectException === undefined) {
175-
throw e
176-
}
177-
// Check if the block failed due to an expected exception
178-
t.exists(
179-
expectException,
180-
`expectException should be defined. Error: ${e.message}\n${e.stack}`,
181-
)
182189

183-
if (expectException.includes('|') === true) {
184-
const exceptions = expectException.split('|')
185-
let i = 0
186-
while (i < exceptions.length) {
187-
try {
188-
t.isTrue(
189-
exceptions[i] in exceptionMessages,
190-
`expectException: (${exceptions[i]}) should be in exceptionMessages. Error: ${e.message}\n${e.stack}`,
191-
)
192-
t.match(
193-
e.message,
194-
exceptionMessages[exceptions[i]],
195-
`Should have correct error for ${exceptions[i]}`,
196-
)
197-
break
198-
} catch {
199-
if (i === exceptions.length - 1) {
200-
t.fail(
201-
`Should have thrown one of the following exceptions: ${expectException}. Threw: ${e.message}`,
190+
if (expectException.includes('|') === true) {
191+
const exceptions = expectException.split('|')
192+
let i = 0
193+
while (i < exceptions.length) {
194+
try {
195+
t.isTrue(
196+
exceptions[i] in exceptionMessages,
197+
`expectException: (${exceptions[i]}) should be in exceptionMessages. Error: ${e.message}\n${e.stack}`,
198+
)
199+
t.match(
200+
e.message,
201+
exceptionMessages[exceptions[i]],
202+
`Should have correct error for ${exceptions[i]}`,
202203
)
203204
break
205+
} catch {
206+
if (i === exceptions.length - 1) {
207+
t.fail(
208+
`Should have thrown one of the following exceptions: ${expectException}. Threw: ${e.message}`,
209+
)
210+
break
211+
}
212+
i++
204213
}
205-
i++
206214
}
215+
} else {
216+
t.isTrue(
217+
expectException in exceptionMessages,
218+
`expectException: (${expectException}) should be in exceptionMessages. Error: ${e.message}\n${e.stack}`,
219+
)
220+
// Check if the error message matches the expected exception
221+
t.match(
222+
e.message,
223+
exceptionMessages[expectException],
224+
`Should have correct error for ${expectException} -- got: ${e.message}`,
225+
)
207226
}
208-
} else {
209-
t.isTrue(
210-
expectException in exceptionMessages,
211-
`expectException: (${expectException}) should be in exceptionMessages. Error: ${e.message}\n${e.stack}`,
212-
)
213-
// Check if the error message matches the expected exception
214-
t.match(
215-
e.message,
216-
exceptionMessages[expectException],
217-
`Should have correct error for ${expectException} -- got: ${e.message}`,
218-
)
219227
}
220228
}
229+
} catch (e: any) {
230+
runError = e
221231
}
222232

223-
// Check final state after all blocks are processed
224-
const head = await blockchain.getCanonicalHeadBlock()
225-
t.equal(bytesToHex(head.hash()), testData.lastblockhash, `head block hash matches lastblockhash`)
233+
// Always check final state and post state, even if block processing threw.
234+
// Individual diffs go into `postFailures` so we can show them alongside any
235+
// block-processing error rather than masking them with an early throw.
236+
const postFailures: string[] = []
226237

227-
// Check post state
228-
for (const address of Object.keys(testData.postState)) {
229-
const account = await vm.stateManager.getAccount(createAddressFromString(address))
230-
t.exists(account, `account should be defined. Got: ${address}`)
231-
const accountInfo = testData.postState[address]
232-
t.equal(account.balance, hexToBigInt(accountInfo.balance), 'correct balance')
233-
t.equal(account.nonce, hexToBigInt(accountInfo.nonce), 'correct nonce')
234-
t.deepEqual(account.codeHash, keccak_256(hexToBytes(accountInfo.code)), 'correct code')
238+
try {
239+
const head = await blockchain.getCanonicalHeadBlock()
240+
t.equal(
241+
bytesToHex(head.hash()),
242+
testData.lastblockhash,
243+
`head block hash matches lastblockhash`,
244+
)
245+
} catch (e: any) {
246+
postFailures.push(`head block hash: ${e?.message ?? String(e)}`)
247+
}
235248

236-
for (const [key, value] of Object.entries(accountInfo.storage)) {
237-
const keyBytes = setLengthLeft(hexToBytes(key as `0x${string}`), 32)
238-
const storage = await vm.stateManager.getStorage(createAddressFromString(address), keyBytes)
239-
t.equal(bytesToHex(storage), value, 'correct storage')
249+
const postState = testData.postState ?? {}
250+
for (const address of Object.keys(postState)) {
251+
try {
252+
const account = await vm.stateManager.getAccount(createAddressFromString(address))
253+
t.exists(account, `account should be defined. Got: ${address}`)
254+
const accountInfo = postState[address]
255+
t.equal(account.balance, hexToBigInt(accountInfo.balance), `correct balance (${address})`)
256+
t.equal(account.nonce, hexToBigInt(accountInfo.nonce), `correct nonce (${address})`)
257+
t.deepEqual(
258+
account.codeHash,
259+
keccak_256(hexToBytes(accountInfo.code)),
260+
`correct code (${address})`,
261+
)
262+
263+
for (const [key, value] of Object.entries(accountInfo.storage)) {
264+
const keyBytes = setLengthLeft(hexToBytes(key as `0x${string}`), 32)
265+
const storage = await vm.stateManager.getStorage(createAddressFromString(address), keyBytes)
266+
t.equal(bytesToHex(storage), value, `correct storage[${key}] (${address})`)
267+
}
268+
} catch (e: any) {
269+
postFailures.push(e?.message ?? String(e))
240270
}
241271
}
272+
273+
if (runError === undefined && postFailures.length === 0) return
274+
275+
// Build a combined failure. If a block-run error was the root cause, keep
276+
// its original stack so vitest's source-mapped frames still point there.
277+
const sections: string[] = []
278+
if (runError !== undefined) sections.push(runError.message)
279+
if (postFailures.length > 0) {
280+
const header =
281+
postFailures.length === 1
282+
? `Post-run state issue:`
283+
: `Post-run state issues (${postFailures.length}):`
284+
sections.push([header, ...postFailures.map((m) => ` - ${m}`)].join('\n'))
285+
}
286+
287+
const combined = new Error(sections.join('\n\n'))
288+
if (runError?.stack !== undefined) combined.stack = runError.stack
289+
throw combined
242290
}
243291

244292
// EthJS error messages mapped to expected exception types

packages/vm/test/tester/executionSpecState.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createVM } from '../../src/constructors.ts'
1111
import { runTx } from '../../src/runTx.ts'
1212
import { makeBlockFromEnv, makeTx, setupPreConditions } from '../util.ts'
1313
import { loadExecutionSpecFixtures, parseTest } from './executionSpecTestLoader.ts'
14+
import { annotateFixture } from './util/perDirectoryReporter.ts'
1415

1516
const customFixturesPath = process.env.TEST_PATH ?? '../execution-spec-tests'
1617
const fixturesPath = path.resolve(customFixturesPath)
@@ -52,8 +53,9 @@ if (fs.existsSync(fixturesPath) === false) {
5253
return
5354
}
5455

55-
for (const { id, fork, data } of fixtures) {
56-
it(`${fork}: ${id}`, async () => {
56+
for (const { id, fork, filePath, data } of fixtures) {
57+
it(`${fork}: ${id}`, async ({ task }) => {
58+
annotateFixture(task, filePath, fixturesPath, 'state tests')
5759
const testCase = parseTest(fork, data)
5860
try {
5961
await runStateTestCase(fork, testCase, assert, kzg)

0 commit comments

Comments
 (0)