Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
480 lines
12 KiB
Go
480 lines
12 KiB
Go
package envtest
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
chainconfig "github.com/ignite/cli/v29/ignite/config/chain"
|
|
v1 "github.com/ignite/cli/v29/ignite/config/chain/v1"
|
|
"github.com/ignite/cli/v29/ignite/pkg/availableport"
|
|
"github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step"
|
|
"github.com/ignite/cli/v29/ignite/pkg/gocmd"
|
|
"github.com/ignite/cli/v29/ignite/pkg/goenv"
|
|
"github.com/ignite/cli/v29/ignite/pkg/xurl"
|
|
"github.com/ignite/cli/v29/ignite/templates/field"
|
|
)
|
|
|
|
const ServeTimeout = time.Minute * 15
|
|
|
|
const (
|
|
defaultConfigFileName = "config.yml"
|
|
defaultTestTimeout = 30 * time.Minute // Go's default is 10m
|
|
)
|
|
|
|
type (
|
|
// Hosts contains the "hostname:port" addresses for different service hosts.
|
|
Hosts struct {
|
|
RPC string
|
|
P2P string
|
|
Prof string
|
|
GRPC string
|
|
GRPCWeb string
|
|
API string
|
|
Faucet string
|
|
}
|
|
|
|
App struct {
|
|
namespace string
|
|
name string
|
|
path string
|
|
configPath string
|
|
homePath string
|
|
testTimeout time.Duration
|
|
|
|
env Env
|
|
|
|
scaffolded []scaffold
|
|
}
|
|
|
|
scaffold struct {
|
|
fields field.Fields
|
|
index field.Field
|
|
response field.Fields
|
|
params field.Fields
|
|
module string
|
|
name string
|
|
typeName string
|
|
}
|
|
)
|
|
|
|
type AppOption func(*App)
|
|
|
|
func AppConfigPath(path string) AppOption {
|
|
return func(o *App) {
|
|
o.configPath = path
|
|
}
|
|
}
|
|
|
|
func AppHomePath(path string) AppOption {
|
|
return func(o *App) {
|
|
o.homePath = path
|
|
}
|
|
}
|
|
|
|
func AppTestTimeout(d time.Duration) AppOption {
|
|
return func(o *App) {
|
|
o.testTimeout = d
|
|
}
|
|
}
|
|
|
|
// ScaffoldApp scaffolds an app to a unique appPath and returns it.
|
|
func (e Env) ScaffoldApp(namespace string, flags ...string) App {
|
|
root := e.TmpDir()
|
|
|
|
e.Exec("scaffold an app",
|
|
step.NewSteps(step.New(
|
|
step.Exec(
|
|
IgniteApp,
|
|
append([]string{
|
|
"scaffold",
|
|
"chain",
|
|
namespace,
|
|
}, flags...)...,
|
|
),
|
|
step.Workdir(root),
|
|
)),
|
|
)
|
|
|
|
var (
|
|
appDirName = path.Base(namespace)
|
|
appSourcePath = filepath.Join(root, appDirName)
|
|
appHomePath = e.AppHome(appDirName)
|
|
)
|
|
|
|
e.t.Cleanup(func() { os.RemoveAll(appHomePath) })
|
|
|
|
return e.App(namespace, appSourcePath, AppHomePath(appHomePath))
|
|
}
|
|
|
|
func (e Env) App(namespace, appPath string, options ...AppOption) App {
|
|
app := App{
|
|
env: e,
|
|
path: appPath,
|
|
testTimeout: defaultTestTimeout,
|
|
scaffolded: make([]scaffold, 0),
|
|
namespace: namespace,
|
|
name: path.Base(namespace),
|
|
}
|
|
|
|
for _, apply := range options {
|
|
apply(&app)
|
|
}
|
|
|
|
if app.configPath == "" {
|
|
app.configPath = filepath.Join(appPath, defaultConfigFileName)
|
|
}
|
|
|
|
return app
|
|
}
|
|
|
|
func (a *App) SourcePath() string {
|
|
return a.path
|
|
}
|
|
|
|
func (a *App) SetHomePath(homePath string) {
|
|
a.homePath = homePath
|
|
}
|
|
|
|
func (a *App) SetConfigPath(path string) {
|
|
a.configPath = path
|
|
}
|
|
|
|
// Binary returns the binary name of the app. Can be executed directly w/o any
|
|
// path after app.Serve is called, since it should be in the $PATH.
|
|
func (a *App) Binary() string {
|
|
return path.Base(a.path) + "d"
|
|
}
|
|
|
|
// Serve serves an application lives under path with options where msg describes the
|
|
// execution from the serving action.
|
|
// unless calling with Must(), Serve() will not exit test runtime on failure.
|
|
func (a *App) Serve(msg string, options ...ExecOption) (ok bool) {
|
|
serveCommand := []string{
|
|
"chain",
|
|
"serve",
|
|
"-v",
|
|
"--quit-on-fail",
|
|
}
|
|
|
|
if a.homePath != "" {
|
|
serveCommand = append(serveCommand, "--home", a.homePath)
|
|
}
|
|
if a.configPath != "" {
|
|
serveCommand = append(serveCommand, "--config", a.configPath)
|
|
}
|
|
a.env.t.Cleanup(func() {
|
|
// Serve install the app binary in GOBIN, let's clean that.
|
|
appBinary := path.Join(goenv.Bin(), a.Binary())
|
|
os.Remove(appBinary)
|
|
})
|
|
|
|
return a.env.Exec(msg,
|
|
step.NewSteps(step.New(
|
|
step.Exec(IgniteApp, serveCommand...),
|
|
step.Workdir(a.path),
|
|
)),
|
|
options...,
|
|
)
|
|
}
|
|
|
|
// Simulate runs the simulation test for the app.
|
|
func (a *App) Simulate(numBlocks, blockSize int) {
|
|
a.env.Exec("running the simulation tests",
|
|
step.NewSteps(step.New(
|
|
step.Exec(
|
|
IgniteApp, // TODO
|
|
"chain",
|
|
"simulate",
|
|
"--numBlocks",
|
|
strconv.Itoa(numBlocks),
|
|
"--blockSize",
|
|
strconv.Itoa(blockSize),
|
|
),
|
|
step.Workdir(a.path),
|
|
)),
|
|
)
|
|
}
|
|
|
|
// EnsureSteady ensures that app living at the path can compile and its tests are passing.
|
|
func (a *App) EnsureSteady() {
|
|
_, statErr := os.Stat(a.configPath)
|
|
|
|
require.False(a.env.t, os.IsNotExist(statErr), "config.yml cannot be found")
|
|
|
|
a.env.Exec("make sure app is steady",
|
|
step.NewSteps(step.New(
|
|
step.Exec(gocmd.Name(), "test", "-timeout", a.testTimeout.String(), "./..."),
|
|
step.Workdir(a.path),
|
|
)),
|
|
)
|
|
}
|
|
|
|
// EnableFaucet enables faucet by finding a random port for the app faucet and update config.yml
|
|
// with this port and provided coins options.
|
|
func (a *App) EnableFaucet(coins, coinsMax []string) (faucetAddr string) {
|
|
// find a random available port
|
|
port, err := availableport.Find(1)
|
|
require.NoError(a.env.t, err)
|
|
|
|
a.EditConfig(func(c *chainconfig.Config) {
|
|
c.Faucet.Port = port[0]
|
|
c.Faucet.Coins = coins
|
|
c.Faucet.CoinsMax = coinsMax
|
|
})
|
|
|
|
addr, err := xurl.HTTP(fmt.Sprintf("0.0.0.0:%d", port[0]))
|
|
require.NoError(a.env.t, err)
|
|
|
|
return addr
|
|
}
|
|
|
|
// RandomizeServerPorts randomizes server ports for the app at path, updates
|
|
// its config.yml and returns new values.
|
|
func (a *App) RandomizeServerPorts() Hosts {
|
|
// generate random server ports
|
|
ports, err := availableport.Find(7)
|
|
require.NoError(a.env.t, err)
|
|
|
|
genAddr := func(port uint) string {
|
|
return fmt.Sprintf("127.0.0.1:%d", port)
|
|
}
|
|
|
|
hosts := Hosts{
|
|
RPC: genAddr(ports[0]),
|
|
P2P: genAddr(ports[1]),
|
|
Prof: genAddr(ports[2]),
|
|
GRPC: genAddr(ports[3]),
|
|
GRPCWeb: genAddr(ports[4]),
|
|
API: genAddr(ports[5]),
|
|
Faucet: genAddr(ports[6]),
|
|
}
|
|
|
|
a.EditConfig(func(c *chainconfig.Config) {
|
|
c.Faucet.Host = hosts.Faucet
|
|
|
|
s := v1.Servers{}
|
|
s.GRPC.Address = hosts.GRPC
|
|
s.GRPCWeb.Address = hosts.GRPCWeb
|
|
s.API.Address = hosts.API
|
|
s.P2P.Address = hosts.P2P
|
|
s.RPC.Address = hosts.RPC
|
|
s.RPC.PProfAddress = hosts.Prof
|
|
|
|
v := &c.Validators[0]
|
|
require.NoError(a.env.t, v.SetServers(s))
|
|
})
|
|
|
|
return hosts
|
|
}
|
|
|
|
// UseRandomHomeDir sets in the blockchain config files generated temporary directories for home directories.
|
|
// Returns the random home directory.
|
|
func (a *App) UseRandomHomeDir() (homeDirPath string) {
|
|
dir := a.env.TmpDir()
|
|
|
|
a.EditConfig(func(c *chainconfig.Config) {
|
|
c.Validators[0].Home = dir
|
|
})
|
|
|
|
return dir
|
|
}
|
|
|
|
func (a *App) Config() chainconfig.Config {
|
|
bz, err := os.ReadFile(a.configPath)
|
|
require.NoError(a.env.t, err)
|
|
|
|
var conf chainconfig.Config
|
|
err = yaml.Unmarshal(bz, &conf)
|
|
require.NoError(a.env.t, err)
|
|
return conf
|
|
}
|
|
|
|
func (a *App) EditConfig(apply func(*chainconfig.Config)) {
|
|
conf := a.Config()
|
|
apply(&conf)
|
|
|
|
bz, err := yaml.Marshal(conf)
|
|
require.NoError(a.env.t, err)
|
|
err = os.WriteFile(a.configPath, bz, 0o600)
|
|
require.NoError(a.env.t, err)
|
|
}
|
|
|
|
// GenerateTSClient runs the command to generate the Typescript client code.
|
|
func (a *App) GenerateTSClient() bool {
|
|
return a.env.Exec("generate typescript client", step.NewSteps(
|
|
step.New(
|
|
step.Exec(IgniteApp, "g", "ts-client", "--yes", "--clear-cache"),
|
|
step.Workdir(a.path),
|
|
),
|
|
))
|
|
}
|
|
|
|
// MustServe serves the application and ensures success, failing the test if serving fails.
|
|
// It uses the provided context to allow cancellation.
|
|
func (a *App) MustServe(ctx context.Context) {
|
|
a.env.Must(a.Serve("should serve chain", ExecCtx(ctx)))
|
|
}
|
|
|
|
// Scaffold scaffolds a new module or component in the app and optionally
|
|
// validates if it should fail.
|
|
// - msg: description of the scaffolding operation.
|
|
// - shouldFail: whether the scaffolding is expected to fail.
|
|
// - typeName: the type of the scaffold (e.g., "map", "message").
|
|
// - args: additional arguments for the scaffold command.
|
|
func (a *App) Scaffold(msg string, shouldFail bool, typeName string, args ...string) {
|
|
a.generate(msg, "scaffold", shouldFail, append([]string{typeName}, args...)...)
|
|
|
|
if !shouldFail {
|
|
a.addScaffoldCmd(typeName, args...)
|
|
}
|
|
}
|
|
|
|
// Generate executes a code generation command in the app and optionally
|
|
// validates if it should fail.
|
|
// - msg: description of the generation operation.
|
|
// - shouldFail: whether the generation is expected to fail.
|
|
// - args: arguments for the generation command.
|
|
func (a *App) Generate(msg string, shouldFail bool, args ...string) {
|
|
a.generate(msg, "generate", shouldFail, args...)
|
|
}
|
|
|
|
// generate is a helper method to execute a scaffolding or generation command with the specified options.
|
|
// - msg: description of the operation.
|
|
// - command: the command to execute (e.g., "scaffold", "generate").
|
|
// - shouldFail: whether the command is expected to fail.
|
|
// - args: arguments for the command.
|
|
func (a *App) generate(msg, command string, shouldFail bool, args ...string) {
|
|
opts := make([]ExecOption, 0)
|
|
if shouldFail {
|
|
opts = append(opts, ExecShouldError())
|
|
}
|
|
|
|
args = append([]string{command}, args...)
|
|
a.env.Must(a.env.Exec(msg,
|
|
step.NewSteps(step.New(
|
|
step.Exec(IgniteApp, append(args, "--yes")...),
|
|
step.Workdir(a.SourcePath()),
|
|
)),
|
|
opts...,
|
|
))
|
|
}
|
|
|
|
// addScaffoldCmd processes the scaffold arguments and adds the scaffolded command metadata to the app.
|
|
// - typeName: the type of the scaffold (e.g., "map", "message").
|
|
// - args: arguments for the scaffold command.
|
|
func (a *App) addScaffoldCmd(typeName string, args ...string) {
|
|
module := ""
|
|
index := ""
|
|
response := ""
|
|
params := ""
|
|
name := typeName
|
|
|
|
// in the case of scaffolding commands that do no take arguments
|
|
// we can skip the argument parsing
|
|
if len(args) > 0 {
|
|
name = args[0]
|
|
args = args[1:]
|
|
}
|
|
|
|
filteredArgs := make([]string, 0)
|
|
|
|
// remove the flags from the args
|
|
for _, arg := range args {
|
|
if strings.HasPrefix(arg, "-") {
|
|
break
|
|
}
|
|
filteredArgs = append(filteredArgs, arg)
|
|
}
|
|
|
|
// parse the arg flags
|
|
for i, arg := range args {
|
|
// skip tests if the type doesn't need a message
|
|
if arg == "--no-message" {
|
|
return
|
|
}
|
|
if i+1 >= len(args) {
|
|
break
|
|
}
|
|
switch arg {
|
|
case "--module":
|
|
module = args[i+1]
|
|
case "--index":
|
|
index = args[i+1]
|
|
case "--params":
|
|
params = args[i+1]
|
|
case "-r", "--response":
|
|
response = args[i+1]
|
|
}
|
|
}
|
|
|
|
argsFields, err := field.ParseFields(filteredArgs, func(string) error { return nil })
|
|
require.NoError(a.env.t, err)
|
|
|
|
s := scaffold{
|
|
fields: argsFields,
|
|
module: module,
|
|
typeName: typeName,
|
|
name: name,
|
|
}
|
|
|
|
// Handle field specifics based on scaffold type
|
|
switch typeName {
|
|
case "map":
|
|
if index == "" {
|
|
index = "index:string"
|
|
}
|
|
indexFields, err := field.ParseFields(strings.Split(index, ","), func(string) error { return nil })
|
|
require.NoError(a.env.t, err)
|
|
require.Len(a.env.t, indexFields, 1)
|
|
s.index = indexFields[0]
|
|
case "query", "message":
|
|
if response == "" {
|
|
break
|
|
}
|
|
responseFields, err := field.ParseFields(strings.Split(response, ","), func(string) error { return nil })
|
|
require.NoError(a.env.t, err)
|
|
require.Greater(a.env.t, len(responseFields), 0)
|
|
s.response = responseFields
|
|
case "module":
|
|
s.module = name
|
|
if params == "" {
|
|
break
|
|
}
|
|
paramsFields, err := field.ParseFields(strings.Split(params, ","), func(string) error { return nil })
|
|
require.NoError(a.env.t, err)
|
|
require.Greater(a.env.t, len(paramsFields), 0)
|
|
s.params = paramsFields
|
|
case "params":
|
|
s.params = argsFields
|
|
}
|
|
|
|
a.scaffolded = append(a.scaffolded, s)
|
|
}
|
|
|
|
// WaitChainUp waits the chain is up.
|
|
func (a *App) WaitChainUp(ctx context.Context, chainAPI string) {
|
|
// check the chains is up
|
|
env := a.env
|
|
stepsCheckChains := step.NewSteps(
|
|
step.New(
|
|
step.Exec(
|
|
a.Binary(),
|
|
"config",
|
|
"output", "json",
|
|
),
|
|
step.PreExec(func() error {
|
|
return env.IsAppServed(ctx, chainAPI)
|
|
}),
|
|
),
|
|
)
|
|
env.Exec(fmt.Sprintf("waiting the chain (%s) is up", chainAPI), stepsCheckChains, ExecRetry())
|
|
}
|