// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see .
package main
import (
"bufio"
"encoding/json"
"fmt"
"maps"
"os"
"regexp"
"runtime"
"slices"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/tests"
"github.com/urfave/cli/v2"
)
var (
WorkersFlag = &cli.IntFlag{
Name: "workers",
Usage: "Number of parallel workers for processing fixture files",
Value: 1,
}
)
var engineTestCommand = &cli.Command{
Action: engineTestCmd,
Name: "enginetest",
Usage: "Executes the given engine API tests. Filenames can be fed via standard input (batch mode) or as an argument (one-off execution).",
ArgsUsage: "",
Flags: slices.Concat([]cli.Flag{
DumpFlag,
HumanReadableFlag,
RunFlag,
FuzzFlag,
WorkersFlag,
}, traceFlags),
}
func engineTestCmd(ctx *cli.Context) error {
path := ctx.Args().First()
// If path is provided, run the tests at that path.
if len(path) != 0 {
collected := collectFiles(path)
workers := ctx.Int(WorkersFlag.Name)
if workers <= 0 {
workers = runtime.NumCPU()
}
results, err := runEngineTestsParallel(ctx, collected, workers)
if err != nil {
return err
}
report(ctx, results)
return nil
}
// Otherwise, read filenames from stdin and execute back-to-back.
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fname := scanner.Text()
if len(fname) == 0 {
return nil
}
results, err := runEngineTest(ctx, fname)
if err != nil {
return err
}
if !ctx.IsSet(FuzzFlag.Name) {
report(ctx, results)
}
}
return nil
}
// fileResult holds the results from processing a single fixture file.
type fileResult struct {
index int
results []testResult
err error
}
// runEngineTestsParallel processes fixture files using a worker pool.
func runEngineTestsParallel(ctx *cli.Context, files []string, workers int) ([]testResult, error) {
if workers == 1 {
// Fast path: no goroutine overhead for single worker
var results []testResult
for _, fname := range files {
r, err := runEngineTest(ctx, fname)
if err != nil {
return nil, err
}
results = append(results, r...)
}
return results, nil
}
// Parallel execution
var (
wg sync.WaitGroup
fileCh = make(chan struct {
index int
fname string
}, len(files))
resultCh = make(chan fileResult, len(files))
)
// Feed files into the channel
for i, fname := range files {
fileCh <- struct {
index int
fname string
}{i, fname}
}
close(fileCh)
// Start workers
for w := 0; w < workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range fileCh {
r, err := runEngineTest(ctx, item.fname)
resultCh <- fileResult{index: item.index, results: r, err: err}
}
}()
}
// Close result channel when all workers are done
go func() {
wg.Wait()
close(resultCh)
}()
// Collect results in order
ordered := make([]fileResult, len(files))
for fr := range resultCh {
if fr.err != nil {
return nil, fr.err
}
ordered[fr.index] = fr
}
var results []testResult
for _, fr := range ordered {
results = append(results, fr.results...)
}
return results, nil
}
func runEngineTest(ctx *cli.Context, fname string) ([]testResult, error) {
src, err := os.ReadFile(fname)
if err != nil {
return nil, err
}
var testsByName map[string]*tests.EngineTest
if err = json.Unmarshal(src, &testsByName); err != nil {
// Skip non-fixture JSON files (e.g. .meta/index.json)
return nil, nil
}
re, err := regexp.Compile(ctx.String(RunFlag.Name))
if err != nil {
return nil, fmt.Errorf("invalid regex -%s: %v", RunFlag.Name, err)
}
tracer := tracerFromFlags(ctx)
if ctx.IsSet(FuzzFlag.Name) {
log.SetDefault(log.NewLogger(log.DiscardHandler()))
}
keys := slices.Sorted(maps.Keys(testsByName))
var results []testResult
for _, name := range keys {
if !re.MatchString(name) {
continue
}
test := testsByName[name]
result := &testResult{Name: name, Pass: true}
var finalHash *common.Hash
if err := test.Run(rawdb.PathScheme, tracer, func(res error, chain *core.BlockChain) {
if ctx.Bool(DumpFlag.Name) {
if s, _ := chain.State(); s != nil {
result.State = dump(s)
}
}
if chain != nil {
hash := chain.CurrentBlock().Hash()
finalHash = &hash
}
}); err != nil {
result.Pass, result.Error = false, err.Error()
}
result.Fork = test.Network()
if finalHash != nil {
result.BlockHash = finalHash
}
result.PayloadStatus = test.LastPayloadStatus
if result.Pass && test.LastValidationError != "" {
result.Error = test.LastValidationError
}
if ctx.IsSet(FuzzFlag.Name) {
report(ctx, []testResult{*result})
}
results = append(results, *result)
}
return results, nil
}