Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ignite
GitHub Repository: ignite/cli
Path: blob/main/integration/app.go
1007 views
1
package envtest
2
3
import (
4
"context"
5
"fmt"
6
"os"
7
"path"
8
"path/filepath"
9
"strconv"
10
"strings"
11
"time"
12
13
"github.com/stretchr/testify/require"
14
"gopkg.in/yaml.v3"
15
16
chainconfig "github.com/ignite/cli/v29/ignite/config/chain"
17
v1 "github.com/ignite/cli/v29/ignite/config/chain/v1"
18
"github.com/ignite/cli/v29/ignite/pkg/availableport"
19
"github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step"
20
"github.com/ignite/cli/v29/ignite/pkg/gocmd"
21
"github.com/ignite/cli/v29/ignite/pkg/goenv"
22
"github.com/ignite/cli/v29/ignite/pkg/xurl"
23
"github.com/ignite/cli/v29/ignite/templates/field"
24
)
25
26
const ServeTimeout = time.Minute * 15
27
28
const (
29
defaultConfigFileName = "config.yml"
30
defaultTestTimeout = 30 * time.Minute // Go's default is 10m
31
)
32
33
type (
34
// Hosts contains the "hostname:port" addresses for different service hosts.
35
Hosts struct {
36
RPC string
37
P2P string
38
Prof string
39
GRPC string
40
GRPCWeb string
41
API string
42
Faucet string
43
}
44
45
App struct {
46
namespace string
47
name string
48
path string
49
configPath string
50
homePath string
51
testTimeout time.Duration
52
53
env Env
54
55
scaffolded []scaffold
56
}
57
58
scaffold struct {
59
fields field.Fields
60
index field.Field
61
response field.Fields
62
params field.Fields
63
module string
64
name string
65
typeName string
66
}
67
)
68
69
type AppOption func(*App)
70
71
func AppConfigPath(path string) AppOption {
72
return func(o *App) {
73
o.configPath = path
74
}
75
}
76
77
func AppHomePath(path string) AppOption {
78
return func(o *App) {
79
o.homePath = path
80
}
81
}
82
83
func AppTestTimeout(d time.Duration) AppOption {
84
return func(o *App) {
85
o.testTimeout = d
86
}
87
}
88
89
// ScaffoldApp scaffolds an app to a unique appPath and returns it.
90
func (e Env) ScaffoldApp(namespace string, flags ...string) App {
91
root := e.TmpDir()
92
93
e.Exec("scaffold an app",
94
step.NewSteps(step.New(
95
step.Exec(
96
IgniteApp,
97
append([]string{
98
"scaffold",
99
"chain",
100
namespace,
101
}, flags...)...,
102
),
103
step.Workdir(root),
104
)),
105
)
106
107
var (
108
appDirName = path.Base(namespace)
109
appSourcePath = filepath.Join(root, appDirName)
110
appHomePath = e.AppHome(appDirName)
111
)
112
113
e.t.Cleanup(func() { os.RemoveAll(appHomePath) })
114
115
return e.App(namespace, appSourcePath, AppHomePath(appHomePath))
116
}
117
118
func (e Env) App(namespace, appPath string, options ...AppOption) App {
119
app := App{
120
env: e,
121
path: appPath,
122
testTimeout: defaultTestTimeout,
123
scaffolded: make([]scaffold, 0),
124
namespace: namespace,
125
name: path.Base(namespace),
126
}
127
128
for _, apply := range options {
129
apply(&app)
130
}
131
132
if app.configPath == "" {
133
app.configPath = filepath.Join(appPath, defaultConfigFileName)
134
}
135
136
return app
137
}
138
139
func (a *App) SourcePath() string {
140
return a.path
141
}
142
143
func (a *App) SetHomePath(homePath string) {
144
a.homePath = homePath
145
}
146
147
func (a *App) SetConfigPath(path string) {
148
a.configPath = path
149
}
150
151
// Binary returns the binary name of the app. Can be executed directly w/o any
152
// path after app.Serve is called, since it should be in the $PATH.
153
func (a *App) Binary() string {
154
return path.Base(a.path) + "d"
155
}
156
157
// Serve serves an application lives under path with options where msg describes the
158
// execution from the serving action.
159
// unless calling with Must(), Serve() will not exit test runtime on failure.
160
func (a *App) Serve(msg string, options ...ExecOption) (ok bool) {
161
serveCommand := []string{
162
"chain",
163
"serve",
164
"-v",
165
"--quit-on-fail",
166
}
167
168
if a.homePath != "" {
169
serveCommand = append(serveCommand, "--home", a.homePath)
170
}
171
if a.configPath != "" {
172
serveCommand = append(serveCommand, "--config", a.configPath)
173
}
174
a.env.t.Cleanup(func() {
175
// Serve install the app binary in GOBIN, let's clean that.
176
appBinary := path.Join(goenv.Bin(), a.Binary())
177
os.Remove(appBinary)
178
})
179
180
return a.env.Exec(msg,
181
step.NewSteps(step.New(
182
step.Exec(IgniteApp, serveCommand...),
183
step.Workdir(a.path),
184
)),
185
options...,
186
)
187
}
188
189
// Simulate runs the simulation test for the app.
190
func (a *App) Simulate(numBlocks, blockSize int) {
191
a.env.Exec("running the simulation tests",
192
step.NewSteps(step.New(
193
step.Exec(
194
IgniteApp, // TODO
195
"chain",
196
"simulate",
197
"--numBlocks",
198
strconv.Itoa(numBlocks),
199
"--blockSize",
200
strconv.Itoa(blockSize),
201
),
202
step.Workdir(a.path),
203
)),
204
)
205
}
206
207
// EnsureSteady ensures that app living at the path can compile and its tests are passing.
208
func (a *App) EnsureSteady() {
209
_, statErr := os.Stat(a.configPath)
210
211
require.False(a.env.t, os.IsNotExist(statErr), "config.yml cannot be found")
212
213
a.env.Exec("make sure app is steady",
214
step.NewSteps(step.New(
215
step.Exec(gocmd.Name(), "test", "-timeout", a.testTimeout.String(), "./..."),
216
step.Workdir(a.path),
217
)),
218
)
219
}
220
221
// EnableFaucet enables faucet by finding a random port for the app faucet and update config.yml
222
// with this port and provided coins options.
223
func (a *App) EnableFaucet(coins, coinsMax []string) (faucetAddr string) {
224
// find a random available port
225
port, err := availableport.Find(1)
226
require.NoError(a.env.t, err)
227
228
a.EditConfig(func(c *chainconfig.Config) {
229
c.Faucet.Port = port[0]
230
c.Faucet.Coins = coins
231
c.Faucet.CoinsMax = coinsMax
232
})
233
234
addr, err := xurl.HTTP(fmt.Sprintf("0.0.0.0:%d", port[0]))
235
require.NoError(a.env.t, err)
236
237
return addr
238
}
239
240
// RandomizeServerPorts randomizes server ports for the app at path, updates
241
// its config.yml and returns new values.
242
func (a *App) RandomizeServerPorts() Hosts {
243
// generate random server ports
244
ports, err := availableport.Find(7)
245
require.NoError(a.env.t, err)
246
247
genAddr := func(port uint) string {
248
return fmt.Sprintf("127.0.0.1:%d", port)
249
}
250
251
hosts := Hosts{
252
RPC: genAddr(ports[0]),
253
P2P: genAddr(ports[1]),
254
Prof: genAddr(ports[2]),
255
GRPC: genAddr(ports[3]),
256
GRPCWeb: genAddr(ports[4]),
257
API: genAddr(ports[5]),
258
Faucet: genAddr(ports[6]),
259
}
260
261
a.EditConfig(func(c *chainconfig.Config) {
262
c.Faucet.Host = hosts.Faucet
263
264
s := v1.Servers{}
265
s.GRPC.Address = hosts.GRPC
266
s.GRPCWeb.Address = hosts.GRPCWeb
267
s.API.Address = hosts.API
268
s.P2P.Address = hosts.P2P
269
s.RPC.Address = hosts.RPC
270
s.RPC.PProfAddress = hosts.Prof
271
272
v := &c.Validators[0]
273
require.NoError(a.env.t, v.SetServers(s))
274
})
275
276
return hosts
277
}
278
279
// UseRandomHomeDir sets in the blockchain config files generated temporary directories for home directories.
280
// Returns the random home directory.
281
func (a *App) UseRandomHomeDir() (homeDirPath string) {
282
dir := a.env.TmpDir()
283
284
a.EditConfig(func(c *chainconfig.Config) {
285
c.Validators[0].Home = dir
286
})
287
288
return dir
289
}
290
291
func (a *App) Config() chainconfig.Config {
292
bz, err := os.ReadFile(a.configPath)
293
require.NoError(a.env.t, err)
294
295
var conf chainconfig.Config
296
err = yaml.Unmarshal(bz, &conf)
297
require.NoError(a.env.t, err)
298
return conf
299
}
300
301
func (a *App) EditConfig(apply func(*chainconfig.Config)) {
302
conf := a.Config()
303
apply(&conf)
304
305
bz, err := yaml.Marshal(conf)
306
require.NoError(a.env.t, err)
307
err = os.WriteFile(a.configPath, bz, 0o600)
308
require.NoError(a.env.t, err)
309
}
310
311
// GenerateTSClient runs the command to generate the Typescript client code.
312
func (a *App) GenerateTSClient() bool {
313
return a.env.Exec("generate typescript client", step.NewSteps(
314
step.New(
315
step.Exec(IgniteApp, "g", "ts-client", "--yes", "--clear-cache"),
316
step.Workdir(a.path),
317
),
318
))
319
}
320
321
// MustServe serves the application and ensures success, failing the test if serving fails.
322
// It uses the provided context to allow cancellation.
323
func (a *App) MustServe(ctx context.Context) {
324
a.env.Must(a.Serve("should serve chain", ExecCtx(ctx)))
325
}
326
327
// Scaffold scaffolds a new module or component in the app and optionally
328
// validates if it should fail.
329
// - msg: description of the scaffolding operation.
330
// - shouldFail: whether the scaffolding is expected to fail.
331
// - typeName: the type of the scaffold (e.g., "map", "message").
332
// - args: additional arguments for the scaffold command.
333
func (a *App) Scaffold(msg string, shouldFail bool, typeName string, args ...string) {
334
a.generate(msg, "scaffold", shouldFail, append([]string{typeName}, args...)...)
335
336
if !shouldFail {
337
a.addScaffoldCmd(typeName, args...)
338
}
339
}
340
341
// Generate executes a code generation command in the app and optionally
342
// validates if it should fail.
343
// - msg: description of the generation operation.
344
// - shouldFail: whether the generation is expected to fail.
345
// - args: arguments for the generation command.
346
func (a *App) Generate(msg string, shouldFail bool, args ...string) {
347
a.generate(msg, "generate", shouldFail, args...)
348
}
349
350
// generate is a helper method to execute a scaffolding or generation command with the specified options.
351
// - msg: description of the operation.
352
// - command: the command to execute (e.g., "scaffold", "generate").
353
// - shouldFail: whether the command is expected to fail.
354
// - args: arguments for the command.
355
func (a *App) generate(msg, command string, shouldFail bool, args ...string) {
356
opts := make([]ExecOption, 0)
357
if shouldFail {
358
opts = append(opts, ExecShouldError())
359
}
360
361
args = append([]string{command}, args...)
362
a.env.Must(a.env.Exec(msg,
363
step.NewSteps(step.New(
364
step.Exec(IgniteApp, append(args, "--yes")...),
365
step.Workdir(a.SourcePath()),
366
)),
367
opts...,
368
))
369
}
370
371
// addScaffoldCmd processes the scaffold arguments and adds the scaffolded command metadata to the app.
372
// - typeName: the type of the scaffold (e.g., "map", "message").
373
// - args: arguments for the scaffold command.
374
func (a *App) addScaffoldCmd(typeName string, args ...string) {
375
module := ""
376
index := ""
377
response := ""
378
params := ""
379
name := typeName
380
381
// in the case of scaffolding commands that do no take arguments
382
// we can skip the argument parsing
383
if len(args) > 0 {
384
name = args[0]
385
args = args[1:]
386
}
387
388
filteredArgs := make([]string, 0)
389
390
// remove the flags from the args
391
for _, arg := range args {
392
if strings.HasPrefix(arg, "-") {
393
break
394
}
395
filteredArgs = append(filteredArgs, arg)
396
}
397
398
// parse the arg flags
399
for i, arg := range args {
400
// skip tests if the type doesn't need a message
401
if arg == "--no-message" {
402
return
403
}
404
if i+1 >= len(args) {
405
break
406
}
407
switch arg {
408
case "--module":
409
module = args[i+1]
410
case "--index":
411
index = args[i+1]
412
case "--params":
413
params = args[i+1]
414
case "-r", "--response":
415
response = args[i+1]
416
}
417
}
418
419
argsFields, err := field.ParseFields(filteredArgs, func(string) error { return nil })
420
require.NoError(a.env.t, err)
421
422
s := scaffold{
423
fields: argsFields,
424
module: module,
425
typeName: typeName,
426
name: name,
427
}
428
429
// Handle field specifics based on scaffold type
430
switch typeName {
431
case "map":
432
if index == "" {
433
index = "index:string"
434
}
435
indexFields, err := field.ParseFields(strings.Split(index, ","), func(string) error { return nil })
436
require.NoError(a.env.t, err)
437
require.Len(a.env.t, indexFields, 1)
438
s.index = indexFields[0]
439
case "query", "message":
440
if response == "" {
441
break
442
}
443
responseFields, err := field.ParseFields(strings.Split(response, ","), func(string) error { return nil })
444
require.NoError(a.env.t, err)
445
require.Greater(a.env.t, len(responseFields), 0)
446
s.response = responseFields
447
case "module":
448
s.module = name
449
if params == "" {
450
break
451
}
452
paramsFields, err := field.ParseFields(strings.Split(params, ","), func(string) error { return nil })
453
require.NoError(a.env.t, err)
454
require.Greater(a.env.t, len(paramsFields), 0)
455
s.params = paramsFields
456
case "params":
457
s.params = argsFields
458
}
459
460
a.scaffolded = append(a.scaffolded, s)
461
}
462
463
// WaitChainUp waits the chain is up.
464
func (a *App) WaitChainUp(ctx context.Context, chainAPI string) {
465
// check the chains is up
466
env := a.env
467
stepsCheckChains := step.NewSteps(
468
step.New(
469
step.Exec(
470
a.Binary(),
471
"config",
472
"output", "json",
473
),
474
step.PreExec(func() error {
475
return env.IsAppServed(ctx, chainAPI)
476
}),
477
),
478
)
479
env.Exec(fmt.Sprintf("waiting the chain (%s) is up", chainAPI), stepsCheckChains, ExecRetry())
480
}
481
482