diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index e2b099c2b92..2b38f254a13 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -73,7 +73,14 @@ jobs: run: | export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" + export LOG_DIR="${{ runner.temp }}/logs/test-${{ matrix.goversion }}-gnoland" make ${{ matrix.args }} + - name: Upload Test Log + if: always() + uses: actions/upload-artifact@v3 + with: + name: logs-test-gnoland-go${{ matrix.goversion }} + path: ${{ runner.temp }}/logs/**/*.log - uses: actions/upload-artifact@v3 if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} with: diff --git a/Dockerfile b/Dockerfile index 70e2d01bf04..9e7fc48dcb0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,8 @@ RUN rm -rf /opt/gno/src/.git # runtime-base + runtime-tls FROM debian:stable-slim AS runtime-base -ENV PATH="${PATH}:/opt/gno/bin" +ENV PATH="${PATH}:/opt/gno/bin" \ + GNOROOT="/opt/gno/src" WORKDIR /opt/gno/src FROM runtime-base AS runtime-tls RUN apt-get update && apt-get install -y expect ca-certificates && update-ca-certificates diff --git a/docs/testing_guide.md b/docs/testing_guide.md new file mode 100644 index 00000000000..6019f238f01 --- /dev/null +++ b/docs/testing_guide.md @@ -0,0 +1,86 @@ +# Gnoland Testing Guide + +This guide provides an overview of our testing practices and conventions. While most of our testing aligns with typical Go practices, there are exceptions and specifics you should be aware of. + +## Standard Package Testing + +For most packages, tests are written and executed in the standard Go manner: + +- Tests are located alongside the code they test. +- The `go test` command can be used to execute tests. + +However, as mentioned earlier, there are some exceptions. In the following sections, we will explore our specialized tests and how to work with them. + +## Gno Filetests + +**Location:** `gnovm/test/files` + +These are our custom file-based tests tailored specifically for this project. + +**Execution:** + +From the gnovm directory, There are two main commands to run Gno filetests: + +1. To test native files, use: +``` +make _test.gnolang.native +``` + +2. To test standard libraries, use: +``` +make _test.gnolang.stdlibs +``` + +**Golden Files Update:** + +Golden files are references for expected outputs. Sometimes, after certain updates, these need to be synchronized. To do so: + +1. For native tests: +``` +make _test.gnolang.native.sync +``` + +2. For standard library tests: +``` +make _test.gnolang.stdlibs.sync +``` + +## Integration Tests + +**Location:** `gno.land/**/testdata` + +From the gno.land directory, Integration tests are designed to ensure different parts of the project work cohesively. Specifically: + +1. **InMemory Node Integration Testing:** + Found in `gno.land/cmd/gnoland/testdata`, these are dedicated to running integration tests against a genuine `gnoland` node. + +2. **Integration Features Testing:** + Located in `gno.land/pkg/integration/testdata`, these tests target integrations specific commands. + +These integration tests utilize the `testscript` package and follow the `txtar` file specifications. + +**Documentation:** + +- For general `testscript` package documentation, refer to: [testscript documentation](https://github.com/rogpeppe/go-internal/blob/v1.11.0/testscript/doc.go) + +- For more specific details about our integration tests, consult our extended documentation: [gnoland integration documentation](https://github.com/gnolang/gno/blob/master/gno.land/pkg/integration/doc.go) + +**Execution:** + +To run the integration tests (alongside other packages): + +``` +make _test.pkgs +``` + +**Golden Files Update within txtar:** + +For tests utilizing the `cmp` command inside `txtar` files, golden files can be synchronized using: + +``` +make _test.pkgs.sync +``` + +--- + +As the project evolves, this guide might be updated. diff --git a/gno.land/Makefile b/gno.land/Makefile index 117de18fc46..e794bb58174 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -47,7 +47,10 @@ test: _test.gnoland _test.gnoweb _test.gnokey _test.pkgs GOTEST_FLAGS ?= -v -p 1 -timeout=30m -_test.gnoland:; go test $(GOTEST_FLAGS) ./cmd/gnoland -_test.gnoweb:; go test $(GOTEST_FLAGS) ./cmd/gnoweb -_test.gnokey:; go test $(GOTEST_FLAGS) ./cmd/gnokey -_test.pkgs:; go test $(GOTEST_FLAGS) ./pkg/... +_test.gnoland:; go test $(GOTEST_FLAGS) ./cmd/gnoland +_test.gnoweb:; go test $(GOTEST_FLAGS) ./cmd/gnoweb +_test.gnokey:; go test $(GOTEST_FLAGS) ./cmd/gnokey +_test.pkgs:; go test $(GOTEST_FLAGS) ./pkg/... +_test.pkgs.sync:; UPDATE_SCRIPTS=true go test $(GOTEST_FLAGS) ./pkg/... + + diff --git a/gno.land/cmd/gnokey/main.go b/gno.land/cmd/gnokey/main.go index 1f3414dc893..28cb665eac1 100644 --- a/gno.land/cmd/gnokey/main.go +++ b/gno.land/cmd/gnokey/main.go @@ -5,11 +5,12 @@ import ( "fmt" "os" + "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" ) func main() { - cmd := client.NewRootCmd() + cmd := client.NewRootCmd(commands.NewDefaultIO()) if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) diff --git a/gno.land/cmd/gnoland/integration_test.go b/gno.land/cmd/gnoland/integration_test.go new file mode 100644 index 00000000000..78c8e94fa09 --- /dev/null +++ b/gno.land/cmd/gnoland/integration_test.go @@ -0,0 +1,12 @@ +package main + +import ( + "testing" + + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/rogpeppe/go-internal/testscript" +) + +func TestTestdata(t *testing.T) { + testscript.Run(t, integration.SetupGnolandTestScript(t, "testdata")) +} diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar new file mode 100644 index 00000000000..5e871b058ac --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg.txtar @@ -0,0 +1,26 @@ +# test for add package + +## start a new node +gnoland start + +## add bar.gno package located in $WORK directory as gno.land/r/foobar/bar +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 + +## execute Render +gnokey maketx call -pkgpath gno.land/r/foobar/bar -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 + +## compare render +cmp stdout stdout.golden + +-- bar.gno -- +package bar + +func Render(path string) string { + return "hello from foo" +} + +-- stdout.golden -- +("hello from foo" string) +OK! +GAS WANTED: 2000000 +GAS USED: 69163 \ No newline at end of file diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index b10f251b115..3585f99d7de 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -2,6 +2,8 @@ package gnoland import ( "fmt" + "os" + "os/exec" "path/filepath" "strings" @@ -20,12 +22,40 @@ import ( "github.com/gnolang/gno/tm2/pkg/store/iavl" ) +type AppOptions struct { + DB dbm.DB + // `gnoRootDir` should point to the local location of the gno repository. + // It serves as the gno equivalent of GOROOT. + GnoRootDir string + SkipFailingGenesisTxs bool + Logger log.Logger + MaxCycles int64 +} + +func NewAppOptions() *AppOptions { + return &AppOptions{ + Logger: log.NewNopLogger(), + DB: dbm.NewMemDB(), + GnoRootDir: GuessGnoRootDir(), + } +} + +func (c *AppOptions) validate() error { + if c.Logger == nil { + return fmt.Errorf("no logger provided") + } + + if c.DB == nil { + return fmt.Errorf("no db provided") + } + + return nil +} + // NewApp creates the GnoLand application. -func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCycles int64) (abci.Application, error) { - // Get main DB. - db, err := dbm.NewDB("gnolang", dbm.GoLevelDBBackend, filepath.Join(rootDir, "data")) - if err != nil { - return nil, fmt.Errorf("error initializing database %q using path %q: %w", dbm.GoLevelDBBackend, rootDir, err) +func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { + if err := cfg.validate(); err != nil { + return nil, err } // Capabilities keys. @@ -33,21 +63,21 @@ func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCy baseKey := store.NewStoreKey("base") // Create BaseApp. - baseApp := sdk.NewBaseApp("gnoland", logger, db, baseKey, mainKey) + baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey) baseApp.SetAppVersion("dev") // Set mounts for BaseApp's MultiStore. - baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, db) - baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, db) + baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) + baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) - stdlibsDir := filepath.Join("..", "gnovm", "stdlibs") - vmKpr := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, maxCycles) + stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") + vmKpr := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) // Set InitChainer - baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, skipFailingGenesisTxs)) + baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.SkipFailingGenesisTxs)) // Set AnteHandler authOptions := auth.AnteOptions{ @@ -88,6 +118,23 @@ func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCy return baseApp, nil } +// NewApp creates the GnoLand application. +func NewApp(dataRootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCycles int64) (abci.Application, error) { + var err error + + cfg := NewAppOptions() + + // Get main DB. + cfg.DB, err = dbm.NewDB("gnolang", dbm.GoLevelDBBackend, filepath.Join(dataRootDir, "data")) + if err != nil { + return nil, fmt.Errorf("error initializing database %q using path %q: %w", dbm.GoLevelDBBackend, dataRootDir, err) + } + + cfg.Logger = logger + + return NewAppWithOptions(cfg) +} + // InitChainer returns a function that can initialize the chain with genesis. func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank.BankKeeperI, skipFailingGenesisTxs bool) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { @@ -107,14 +154,15 @@ func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank for i, tx := range genState.Txs { res := baseApp.Deliver(tx) if res.IsErr() { - fmt.Println("ERROR LOG:", res.Log) - fmt.Println("#", i, string(amino.MustMarshalJSON(tx))) + ctx.Logger().Error("LOG", res.Log) + ctx.Logger().Error("#", i, string(amino.MustMarshalJSON(tx))) + // NOTE: comment out to ignore. if !skipFailingGenesisTxs { panic(res.Error) } } else { - fmt.Println("SUCCESS:", string(amino.MustMarshalJSON(tx))) + ctx.Logger().Info("SUCCESS:", string(amino.MustMarshalJSON(tx))) } } // Done! @@ -146,3 +194,25 @@ func EndBlocker(vmk vm.VMKeeperI) func(ctx sdk.Context, req abci.RequestEndBlock return abci.ResponseEndBlock{} } } + +func GuessGnoRootDir() string { + var rootdir string + + // First try to get the root directory from the GNOROOT environment variable. + if rootdir = os.Getenv("GNOROOT"); rootdir != "" { + return filepath.Clean(rootdir) + } + + if gobin, err := exec.LookPath("go"); err == nil { + // If GNOROOT is not set, try to guess the root directory using the `go list` command. + cmd := exec.Command(gobin, "list", "-m", "-mod=mod", "-f", "{{.Dir}}", "github.com/gnolang/gno") + out, err := cmd.CombinedOutput() + if err != nil { + panic(fmt.Errorf("invalid gno directory %q: %w", rootdir, err)) + } + + return strings.TrimSpace(string(out)) + } + + panic("no go binary available, unable to determine gno root-dir path") +} diff --git a/gno.land/pkg/integration/doc.go b/gno.land/pkg/integration/doc.go new file mode 100644 index 00000000000..a8b40f9c321 --- /dev/null +++ b/gno.land/pkg/integration/doc.go @@ -0,0 +1,87 @@ +// Package integration offers utilities to run txtar-based tests against the gnoland system +// by extending the functionalities provided by the standard testscript package. This package is +// currently in an experimental phase and may undergo significant changes in the future. +// +// SetupGnolandTestScript, sets up the environment for running txtar tests, introducing additional +// commands like "gnoland" and "gnokey" into the test script ecosystem. Specifically, it allows the +// user to initiate an in-memory gnoland node and interact with it via the `gnokey` command. +// +// Additional Command Overview: +// +// 1. `gnoland [start|stop]`: +// - The gnoland node doesn't start automatically. This enables the user to do some +// pre-configuration or pass custom arguments to the start command. +// +// 2. `gnokey`: +// - Supports most of the common commands. +// - `--remote`, `--insecure-password-stdin`, and `--home` flags are set automatically to +// communicate with the gnoland node. +// +// Logging: +// +// Gnoland logs aren't forwarded to stdout to avoid overwhelming the tests with too much +// information. Instead, a log directory can be specified with `LOG_DIR`, or you +// can set `TESTWORK=true` +// to persist logs in the txtar working directory. In any case, the log file should be printed +// on start if one of these environment variables is set. +// +// Accounts: +// +// By default, only the test1 user will be created in the default keybase directory, +// with no password set. The default gnoland genesis balance file and the genesis +// transaction file are also registered by default. +// +// Examples: +// +// Examples can be found in the `testdata` directory of this package. +// +// Environment Variables: +// +// Input: +// +// - LOG_LEVEL: +// The logging level to be used, which can be one of "error", "debug", "info", or an empty string. +// If empty, the log level defaults to "debug". +// +// - LOG_DIR: +// If set, logs will be directed to the specified directory. +// +// - TESTWORK: +// A boolean that, when enabled, retains working directories after tests for +// inspection. If enabled, gnoland logs will be persisted inside this +// folder. +// +// - UPDATE_SCRIPTS: +// A boolean that, when enabled, updates the test scripts if a `cmp` command +// fails and its second argument refers to a file inside the testscript +// file. The content will be quoted with txtar.Quote if needed, requiring +// manual edits if it's not unquoted in the script. +// +// Output (available inside testscripts files): +// +// - WORK: +// The path to the temporary work directory tree created for each script. +// +// - GNOROOT: +// Points to the local location of the gno repository, serving as the GOROOT equivalent for gno. +// +// - GNOHOME: +// Refers to the local directory where gnokey stores its keys. +// +// - GNODATA: +// The path where the gnoland node stores its configuration and data. It's +// set only if the node has started. +// +// - USER_SEED_test1: +// Contains the seed for the test1 account. +// +// - USER_ADDR_test1: +// Contains the address for the test1 account. +// +// - RPC_ADDR: +// Points to the gnoland node's remote address. It's set only if the node has started. +// +// For a more comprehensive guide on original behaviors, additional commands and environment +// variables, refer to the original documentation of testscripts available here: +// https://github.com/rogpeppe/go-internal/blob/master/testscript/doc.go +package integration diff --git a/gno.land/pkg/integration/gnoland.go b/gno.land/pkg/integration/gnoland.go new file mode 100644 index 00000000000..c4fee341bfc --- /dev/null +++ b/gno.land/pkg/integration/gnoland.go @@ -0,0 +1,336 @@ +package integration + +import ( + "flag" + "fmt" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/node" + "github.com/gnolang/gno/tm2/pkg/bft/privval" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/log" + osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/rogpeppe/go-internal/testscript" +) + +type IntegrationConfig struct { + SkipFailingGenesisTxs bool + SkipStart bool + GenesisBalancesFile string + GenesisTxsFile string + ChainID string + GenesisRemote string + RootDir string + GenesisMaxVMCycles int64 + Config string +} + +// NOTE: This is a copy of gnoland actual flags. +// XXX: A lot this make no sense for integration. +func (c *IntegrationConfig) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.SkipFailingGenesisTxs, + "skip-failing-genesis-txs", + false, + "don't panic when replaying invalid genesis txs", + ) + fs.BoolVar( + &c.SkipStart, + "skip-start", + false, + "quit after initialization, don't start the node", + ) + + fs.StringVar( + &c.GenesisBalancesFile, + "genesis-balances-file", + "./genesis/genesis_balances.txt", + "initial distribution file", + ) + + fs.StringVar( + &c.GenesisTxsFile, + "genesis-txs-file", + "./genesis/genesis_txs.txt", + "initial txs to replay", + ) + + fs.StringVar( + &c.ChainID, + "chainid", + "dev", + "the ID of the chain", + ) + + fs.StringVar( + &c.RootDir, + "root-dir", + "testdir", + "directory for config and data", + ) + + fs.StringVar( + &c.GenesisRemote, + "genesis-remote", + "localhost:26657", + "replacement for '%%REMOTE%%' in genesis", + ) + + fs.Int64Var( + &c.GenesisMaxVMCycles, + "genesis-max-vm-cycles", + 10_000_000, + "set maximum allowed vm cycles per operation. Zero means no limit.", + ) +} + +func execTestingGnoland(t *testing.T, logger log.Logger, gnoDataDir, gnoRootDir string, args []string) (*node.Node, error) { + t.Helper() + + // Setup start config. + icfg := &IntegrationConfig{} + { + fs := flag.NewFlagSet("start", flag.ExitOnError) + icfg.RegisterFlags(fs) + + // Override default value for flags. + fs.VisitAll(func(f *flag.Flag) { + switch f.Name { + case "root-dir": + f.DefValue = gnoDataDir + case "chainid": + f.DefValue = "tendermint_test" + case "genesis-balances-file": + f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_balances.txt") + case "genesis-txs-file": + f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_txs.txt") + default: + return + } + + f.Value.Set(f.DefValue) + }) + + if err := fs.Parse(args); err != nil { + return nil, fmt.Errorf("unable to parse flags: %w", err) + } + } + + // Setup testing config. + cfg := config.TestConfig().SetRootDir(gnoDataDir) + { + cfg.EnsureDirs() + cfg.Consensus.CreateEmptyBlocks = true + cfg.Consensus.CreateEmptyBlocksInterval = time.Duration(0) + cfg.RPC.ListenAddress = "tcp://127.0.0.1:0" + cfg.P2P.ListenAddress = "tcp://127.0.0.1:0" + } + + // Prepare genesis. + if err := setupTestingGenesis(gnoDataDir, cfg, icfg, gnoRootDir); err != nil { + return nil, err + } + + // Create application and node. + return createAppAndNode(cfg, logger, gnoRootDir, icfg) +} + +func setupTestingGenesis(gnoDataDir string, cfg *config.Config, icfg *IntegrationConfig, gnoRootDir string) error { + newPrivValKey := cfg.PrivValidatorKeyFile() + newPrivValState := cfg.PrivValidatorStateFile() + priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) + + genesisFilePath := filepath.Join(gnoDataDir, cfg.Genesis) + genesisDirPath := filepath.Dir(genesisFilePath) + if err := osm.EnsureDir(genesisDirPath, 0o700); err != nil { + return fmt.Errorf("unable to ensure directory %q: %w", genesisDirPath, err) + } + + genesisTxs := loadGenesisTxs(icfg.GenesisTxsFile, icfg.ChainID, icfg.GenesisRemote) + pvPub := priv.GetPubKey() + + gen := &bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: icfg.ChainID, + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + // TODO: update limits. + MaxTxBytes: 1000000, // 1MB, + MaxDataBytes: 2000000, // 2MB, + MaxGas: 10000000, // 10M gas + TimeIotaMS: 100, // 100ms + }, + }, + Validators: []bft.GenesisValidator{ + { + Address: pvPub.Address(), + PubKey: pvPub, + Power: 10, + Name: "testvalidator", + }, + }, + } + + // Load distribution. + balances := loadGenesisBalances(icfg.GenesisBalancesFile) + + // Load initial packages from examples. + // XXX: We should be able to config this. + test1 := crypto.MustAddressFromString(test1Addr) + txs := []std.Tx{} + + // List initial packages to load from examples. + // println(filepath.Join(gnoRootDir, "examples")) + pkgs, err := gnomod.ListPkgs(filepath.Join(gnoRootDir, "examples")) + if err != nil { + return fmt.Errorf("listing gno packages: %w", err) + } + + // Sort packages by dependencies. + sortedPkgs, err := pkgs.Sort() + if err != nil { + return fmt.Errorf("sorting packages: %w", err) + } + + // Filter out draft packages. + nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() + + for _, pkg := range nonDraftPkgs { + // Open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + + var tx std.Tx + tx.Msgs = []std.Msg{ + vmm.MsgAddPackage{ + Creator: test1, + Package: memPkg, + Deposit: nil, + }, + } + + // XXX: Add fee flag ? + // Or maybe reduce fee to the minimum ? + tx.Fee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs = append(txs, tx) + } + + // Load genesis txs from file. + txs = append(txs, genesisTxs...) + + // Construct genesis AppState. + gen.AppState = gnoland.GnoGenesisState{ + Balances: balances, + Txs: txs, + } + + writeGenesisFile(gen, genesisFilePath) + + return nil +} + +func createAppAndNode(cfg *config.Config, logger log.Logger, gnoRootDir string, icfg *IntegrationConfig) (*node.Node, error) { + gnoApp, err := gnoland.NewAppWithOptions(&gnoland.AppOptions{ + Logger: logger, + GnoRootDir: gnoRootDir, + SkipFailingGenesisTxs: icfg.SkipFailingGenesisTxs, + MaxCycles: icfg.GenesisMaxVMCycles, + DB: db.NewMemDB(), + }) + if err != nil { + return nil, fmt.Errorf("error in creating new app: %w", err) + } + + cfg.LocalApp = gnoApp + node, err := node.DefaultNewNode(cfg, logger) + if err != nil { + return nil, fmt.Errorf("error in creating node: %w", err) + } + + return node, node.Start() +} + +func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { + if err != nil { + fmt.Fprintf(ts.Stderr(), "%q error: %v\n", cmd, err) + if !neg { + ts.Fatalf("unexpected %q command failure: %s", cmd, err) + } + } else { + if neg { + ts.Fatalf("unexpected %s command success", cmd) + } + } +} + +func loadGenesisTxs( + path string, + chainID string, + genesisRemote string, +) []std.Tx { + txs := []std.Tx{} + txsBz := osm.MustReadFile(path) + txsLines := strings.Split(string(txsBz), "\n") + for _, txLine := range txsLines { + if txLine == "" { + continue // Skip empty line. + } + + // Patch the TX. + txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) + txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) + + var tx std.Tx + amino.MustUnmarshalJSON([]byte(txLine), &tx) + txs = append(txs, tx) + } + + return txs +} + +func loadGenesisBalances(path string) []string { + // Each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot. + balances := []string{} + content := osm.MustReadFile(path) + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + + // Remove comments. + line = strings.Split(line, "#")[0] + line = strings.TrimSpace(line) + + // Skip empty lines. + if line == "" { + continue + } + + parts := strings.Split(line, "=") + if len(parts) != 2 { + panic("invalid genesis_balance line: " + line) + } + + balances = append(balances, line) + } + return balances +} + +func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { + err := gen.SaveAs(filePath) + if err != nil { + panic(err) + } +} diff --git a/gno.land/pkg/integration/integration_test.go b/gno.land/pkg/integration/integration_test.go new file mode 100644 index 00000000000..3c22a190d64 --- /dev/null +++ b/gno.land/pkg/integration/integration_test.go @@ -0,0 +1,11 @@ +package integration + +import ( + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +func TestTestdata(t *testing.T) { + testscript.Run(t, SetupGnolandTestScript(t, "testdata")) +} diff --git a/gno.land/pkg/integration/testdata/gnokey.txtar b/gno.land/pkg/integration/testdata/gnokey.txtar new file mode 100644 index 00000000000..e4d2c93c0c9 --- /dev/null +++ b/gno.land/pkg/integration/testdata/gnokey.txtar @@ -0,0 +1,32 @@ +# test basic gnokey integrations commands +# golden files have been generated using UPDATE_SCRIPTS=true + +# start gnoland +gnoland start + +## test1 account should be available on default +gnokey query auth/accounts/${USER_ADDR_test1} +cmp stdout gnokey-query-valid.stdout.golden +cmp stderr gnokey-query-valid.stderr.golden + +## invalid gnokey command should raise an error +! gnokey query foo/bar +cmp stdout gnokey-query-invalid.stdout.golden +cmp stderr gnokey-query-invalid.stderr.golden + +-- gnokey-query-valid.stdout.golden -- +height: 0 +data: { + "BaseAccount": { + "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "coins": "9999892000000ugnot", + "public_key": null, + "account_number": "0", + "sequence": "0" + } +} +-- gnokey-query-valid.stderr.golden -- +-- gnokey-query-invalid.stdout.golden -- +Log: +-- gnokey-query-invalid.stderr.golden -- +"gnokey" error: unknown request error diff --git a/gno.land/pkg/integration/testdata/gnoland.txtar b/gno.land/pkg/integration/testdata/gnoland.txtar new file mode 100644 index 00000000000..c675e7578b6 --- /dev/null +++ b/gno.land/pkg/integration/testdata/gnoland.txtar @@ -0,0 +1,43 @@ +# test basic gnoland integrations commands +# golden files have been generated using UPDATE_SCRIPTS=true + +## no arguments should fail +! gnoland +cmp stdout gnoland-no-arguments.stdout.golden +cmp stderr gnoland-no-arguments.stderr.golden + +## should be able to start +gnoland start +cmp stdout gnoland-start.stdout.golden +cmp stderr gnoland-start.stderr.golden + +## should not be able to start a node twice +! gnoland start +cmp stdout gnoland-already-start.stdout.golden +cmp stderr gnoland-already-start.stderr.golden + +## should be able to stop default +gnoland stop +cmp stdout gnoland-stop.stdout.golden +cmp stderr gnoland-stop.stderr.golden + +## should not be able to stop a node twice +! gnoland stop +cmp stdout gnoland-already-stop.stdout.golden +cmp stderr gnoland-already-stop.stderr.golden + +-- gnoland-no-arguments.stdout.golden -- +-- gnoland-no-arguments.stderr.golden -- +"gnoland" error: syntax: gnoland [start|stop] +-- gnoland-start.stdout.golden -- +node started successfully +-- gnoland-start.stderr.golden -- +-- gnoland-already-start.stdout.golden -- +-- gnoland-already-start.stderr.golden -- +"gnoland start" error: node already started +-- gnoland-stop.stdout.golden -- +node stopped successfully +-- gnoland-stop.stderr.golden -- +-- gnoland-already-stop.stdout.golden -- +-- gnoland-already-stop.stderr.golden -- +"gnoland stop" error: node not started cannot be stopped diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go new file mode 100644 index 00000000000..f0a696ddd85 --- /dev/null +++ b/gno.land/pkg/integration/testing_integration.go @@ -0,0 +1,282 @@ +package integration + +import ( + "context" + "fmt" + "hash/crc32" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/node" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/rogpeppe/go-internal/testscript" +) + +// XXX: This should be centralize somewhere. +const ( + test1Addr = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + test1Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" +) + +type testNode struct { + *node.Node + logger log.Logger + nGnoKeyExec uint // Counter for execution of gnokey. +} + +// SetupGnolandTestScript prepares the test environment to execute txtar tests +// using a partial InMemory gnoland node. It initializes key storage, sets up the gnoland node, +// and provides custom commands like "gnoland" and "gnokey" for txtar script execution. +// +// The function returns testscript.Params which contain the test setup and command +// executions to be used with the testscript package. +// +// For a detailed explanation of the commands and their behaviors, as well as +// example txtar scripts, refer to the package documentation in doc.go. +func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { + t.Helper() + + tmpdir := t.TempDir() + + // `gnoRootDir` should point to the local location of the gno repository. + // It serves as the gno equivalent of GOROOT. + gnoRootDir := gnoland.GuessGnoRootDir() + + // `gnoHomeDir` should be the local directory where gnokey stores keys. + gnoHomeDir := filepath.Join(tmpdir, "gno") + + // `gnoDataDir` should refer to the local location where the gnoland node + // stores its configuration and data. + gnoDataDir := filepath.Join(tmpdir, "data") + + // Testscripts run concurrently by default, so we need to be prepared for that. + var muNodes sync.Mutex + nodes := map[string]*testNode{} + + updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) + persistWorkDir, _ := strconv.ParseBool(os.Getenv("TESTWORK")) + return testscript.Params{ + UpdateScripts: updateScripts, + TestWork: persistWorkDir, + Dir: txtarDir, + Setup: func(env *testscript.Env) error { + kb, err := keys.NewKeyBaseFromDir(gnoHomeDir) + if err != nil { + return err + } + + // XXX: Add a command to add custom account. + kb.CreateAccount("test1", test1Seed, "", "", 0, 0) + env.Setenv("USER_SEED_test1", test1Seed) + env.Setenv("USER_ADDR_test1", test1Addr) + + env.Setenv("GNOROOT", gnoRootDir) + env.Setenv("GNOHOME", gnoHomeDir) + + return nil + }, + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ + "gnoland": func(ts *testscript.TestScript, neg bool, args []string) { + muNodes.Lock() + defer muNodes.Unlock() + + if len(args) == 0 { + tsValidateError(ts, "gnoland", neg, fmt.Errorf("syntax: gnoland [start|stop]")) + return + } + + sid := getSessionID(ts) + + var cmd string + cmd, args = args[0], args[1:] + + var err error + switch cmd { + case "start": + if _, ok := nodes[sid]; ok { + err = fmt.Errorf("node already started") + break + } + + logger := log.NewNopLogger() + if persistWorkDir || os.Getenv("LOG_DIR") != "" { + logname := fmt.Sprintf("gnoland-%s.log", sid) + logger = getTestingLogger(ts, logname) + } + + dataDir := filepath.Join(gnoDataDir, sid) + var node *node.Node + if node, err = execTestingGnoland(t, logger, dataDir, gnoRootDir, args); err == nil { + nodes[sid] = &testNode{ + Node: node, + logger: logger, + } + ts.Defer(func() { + muNodes.Lock() + defer muNodes.Unlock() + + if n := nodes[sid]; n != nil { + if err := n.Stop(); err != nil { + panic(fmt.Errorf("node %q was unable to stop: %w", sid, err)) + } + } + }) + + // Get listen address environment. + // It should have been updated with the right port on start. + laddr := node.Config().RPC.ListenAddress + + // Add default environements. + ts.Setenv("RPC_ADDR", laddr) + ts.Setenv("GNODATA", gnoDataDir) + + const listenerID = "testing_listener" + + // Wait for first block by waiting for `EventNewBlock` event. + nb := make(chan struct{}, 1) + node.EventSwitch().AddListener(listenerID, func(ev events.Event) { + if _, ok := ev.(types.EventNewBlock); ok { + select { + case nb <- struct{}{}: + default: + } + } + }) + + if node.BlockStore().Height() == 0 { + select { + case <-nb: // ok + case <-time.After(time.Second * 6): + ts.Fatalf("timeout while waiting for the node to start") + } + } + + node.EventSwitch().RemoveListener(listenerID) + + fmt.Fprintln(ts.Stdout(), "node started successfully") + } + case "stop": + n, ok := nodes[sid] + if !ok { + err = fmt.Errorf("node not started cannot be stopped") + break + } + + if err = n.Stop(); err == nil { + delete(nodes, sid) + + // Unset gnoland environements. + ts.Setenv("RPC_ADDR", "") + ts.Setenv("GNODATA", "") + fmt.Fprintln(ts.Stdout(), "node stopped successfully") + } + default: + err = fmt.Errorf("invalid gnoland subcommand: %q", cmd) + } + + tsValidateError(ts, "gnoland "+cmd, neg, err) + }, + "gnokey": func(ts *testscript.TestScript, neg bool, args []string) { + muNodes.Lock() + defer muNodes.Unlock() + + sid := getSessionID(ts) + + // Setup IO command. + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(ts.Stdout())) + io.SetErr(commands.WriteNopCloser(ts.Stderr())) + cmd := client.NewRootCmd(io) + + io.SetIn(strings.NewReader("\n")) // Inject empty password to stdin. + defaultArgs := []string{ + "-home", gnoHomeDir, + "-insecure-password-stdin=true", // There no use to not have this param by default. + } + + if n, ok := nodes[sid]; ok { + if raddr := n.Config().RPC.ListenAddress; raddr != "" { + defaultArgs = append(defaultArgs, "-remote", raddr) + } + + n.nGnoKeyExec++ + headerlog := fmt.Sprintf("%.02d!EXEC_GNOKEY", n.nGnoKeyExec) + // Log the command inside gnoland logger, so we can better scope errors. + n.logger.Info(headerlog, strings.Join(args, " ")) + defer n.logger.Info(headerlog, "END") + } + + // Inject default argument, if duplicate + // arguments, it should be override by the ones + // user provided. + args = append(defaultArgs, args...) + + err := cmd.ParseAndRun(context.Background(), args) + + tsValidateError(ts, "gnokey", neg, err) + }, + }, + } +} + +func getSessionID(ts *testscript.TestScript) string { + works := ts.Getenv("WORK") + sum := crc32.ChecksumIEEE([]byte(works)) + return strconv.FormatUint(uint64(sum), 16) +} + +func getTestingLogger(ts *testscript.TestScript, logname string) log.Logger { + var path string + if logdir := os.Getenv("LOG_DIR"); logdir != "" { + if err := os.MkdirAll(logdir, 0o755); err != nil { + ts.Fatalf("unable to make log directory %q", logdir) + } + + var err error + if path, err = filepath.Abs(filepath.Join(logdir, logname)); err != nil { + ts.Fatalf("uanble to get absolute path of logdir %q", logdir) + } + } else if workdir := ts.Getenv("WORK"); workdir != "" { + path = filepath.Join(workdir, logname) + } else { + return log.NewNopLogger() + } + + f, err := os.Create(path) + if err != nil { + ts.Fatalf("unable to create log file %q: %s", path, err.Error()) + } + + ts.Defer(func() { + if err := f.Close(); err != nil { + panic(fmt.Errorf("unable to close log file %q: %w", path, err)) + } + }) + + logger := log.NewTMLogger(f) + switch level := os.Getenv("LOG_LEVEL"); strings.ToLower(level) { + case "error": + logger.SetLevel(log.LevelError) + case "debug": + logger.SetLevel(log.LevelDebug) + case "info": + logger.SetLevel(log.LevelInfo) + case "": + default: + ts.Fatalf("invalid log level %q", level) + } + + ts.Logf("starting logger: %q", path) + return logger +} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 28531f0a773..6f695e98558 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -188,7 +188,8 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) error { }) defer m2.Release() m2.RunMemPackage(memPkg, true) - fmt.Println("CPUCYCLES addpkg", m2.Cycles) + + ctx.Logger().Info("CPUCYCLES", "addpkg", m2.Cycles) return nil } @@ -270,7 +271,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { m.Release() }() rtvs := m.Eval(xn) - fmt.Println("CPUCYCLES call", m.Cycles) + ctx.Logger().Info("CPUCYCLES call: ", m.Cycles) for i, rtv := range rtvs { res = res + rtv.String() if i < len(rtvs)-1 { diff --git a/tm2/pkg/crypto/keys/client/add.go b/tm2/pkg/crypto/keys/client/add.go index 5f90a9f874e..30b612a9de2 100644 --- a/tm2/pkg/crypto/keys/client/add.go +++ b/tm2/pkg/crypto/keys/client/add.go @@ -29,7 +29,7 @@ type addCfg struct { index uint64 } -func newAddCmd(rootCfg *baseCfg) *commands.Command { +func newAddCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &addCfg{ rootCfg: rootCfg, } @@ -42,7 +42,7 @@ func newAddCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execAdd(cfg, args, commands.NewDefaultIO()) + return execAdd(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/addpkg.go b/tm2/pkg/crypto/keys/client/addpkg.go index 885e1f123d7..3de9a6de546 100644 --- a/tm2/pkg/crypto/keys/client/addpkg.go +++ b/tm2/pkg/crypto/keys/client/addpkg.go @@ -24,7 +24,7 @@ type addPkgCfg struct { deposit string } -func newAddPkgCmd(rootCfg *makeTxCfg) *commands.Command { +func newAddPkgCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { cfg := &addPkgCfg{ rootCfg: rootCfg, } @@ -37,7 +37,7 @@ func newAddPkgCmd(rootCfg *makeTxCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execAddPkg(cfg, args, commands.NewDefaultIO()) + return execAddPkg(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/broadcast.go b/tm2/pkg/crypto/keys/client/broadcast.go index 039a9557c38..f1d448495a6 100644 --- a/tm2/pkg/crypto/keys/client/broadcast.go +++ b/tm2/pkg/crypto/keys/client/broadcast.go @@ -23,7 +23,7 @@ type broadcastCfg struct { tx *std.Tx } -func newBroadcastCmd(rootCfg *baseCfg) *commands.Command { +func newBroadcastCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &broadcastCfg{ rootCfg: rootCfg, } diff --git a/tm2/pkg/crypto/keys/client/call.go b/tm2/pkg/crypto/keys/client/call.go index bcb7be3e550..29fe9739a36 100644 --- a/tm2/pkg/crypto/keys/client/call.go +++ b/tm2/pkg/crypto/keys/client/call.go @@ -22,7 +22,7 @@ type callCfg struct { args commands.StringArr } -func newCallCmd(rootCfg *makeTxCfg) *commands.Command { +func newCallCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { cfg := &callCfg{ rootCfg: rootCfg, } @@ -35,7 +35,7 @@ func newCallCmd(rootCfg *makeTxCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execCall(cfg, args, commands.NewDefaultIO()) + return execCall(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/delete.go b/tm2/pkg/crypto/keys/client/delete.go index 0f216d3467c..e22ac30988c 100644 --- a/tm2/pkg/crypto/keys/client/delete.go +++ b/tm2/pkg/crypto/keys/client/delete.go @@ -16,7 +16,7 @@ type deleteCfg struct { force bool } -func newDeleteCmd(rootCfg *baseCfg) *commands.Command { +func newDeleteCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &deleteCfg{ rootCfg: rootCfg, } @@ -29,7 +29,7 @@ func newDeleteCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execDelete(cfg, args, commands.NewDefaultIO()) + return execDelete(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/export.go b/tm2/pkg/crypto/keys/client/export.go index eda04a5c92f..6eff8aa97b3 100644 --- a/tm2/pkg/crypto/keys/client/export.go +++ b/tm2/pkg/crypto/keys/client/export.go @@ -18,7 +18,7 @@ type exportCfg struct { unsafe bool } -func newExportCmd(rootCfg *baseCfg) *commands.Command { +func newExportCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &exportCfg{ rootCfg: rootCfg, } @@ -31,7 +31,7 @@ func newExportCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execExport(cfg, commands.NewDefaultIO()) + return execExport(cfg, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/generate.go b/tm2/pkg/crypto/keys/client/generate.go index b721e6704ce..d209bd70bd3 100644 --- a/tm2/pkg/crypto/keys/client/generate.go +++ b/tm2/pkg/crypto/keys/client/generate.go @@ -16,7 +16,7 @@ type generateCfg struct { customEntropy bool } -func newGenerateCmd(rootCfg *baseCfg) *commands.Command { +func newGenerateCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &generateCfg{ rootCfg: rootCfg, } @@ -29,7 +29,7 @@ func newGenerateCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execGenerate(cfg, args, commands.NewDefaultIO()) + return execGenerate(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/import.go b/tm2/pkg/crypto/keys/client/import.go index 5e0eeecabb5..e1d8af55861 100644 --- a/tm2/pkg/crypto/keys/client/import.go +++ b/tm2/pkg/crypto/keys/client/import.go @@ -18,7 +18,7 @@ type importCfg struct { unsafe bool } -func newImportCmd(rootCfg *baseCfg) *commands.Command { +func newImportCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &importCfg{ rootCfg: rootCfg, } @@ -31,7 +31,7 @@ func newImportCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, _ []string) error { - return execImport(cfg, commands.NewDefaultIO()) + return execImport(cfg, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/list.go b/tm2/pkg/crypto/keys/client/list.go index 50be35cef43..cb86feb2395 100644 --- a/tm2/pkg/crypto/keys/client/list.go +++ b/tm2/pkg/crypto/keys/client/list.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) -func newListCmd(rootCfg *baseCfg) *commands.Command { +func newListCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "list", @@ -17,7 +17,7 @@ func newListCmd(rootCfg *baseCfg) *commands.Command { }, nil, func(_ context.Context, args []string) error { - return execList(rootCfg, args, commands.NewDefaultIO()) + return execList(rootCfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index cbcc6def0de..36214a5a983 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -17,7 +17,7 @@ type makeTxCfg struct { chainID string } -func newMakeTxCmd(rootCfg *baseCfg) *commands.Command { +func newMakeTxCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &makeTxCfg{ rootCfg: rootCfg, } @@ -33,9 +33,9 @@ func newMakeTxCmd(rootCfg *baseCfg) *commands.Command { ) cmd.AddSubCommands( - newAddPkgCmd(cfg), - newSendCmd(cfg), - newCallCmd(cfg), + newAddPkgCmd(cfg, io), + newSendCmd(cfg, io), + newCallCmd(cfg, io), ) return cmd diff --git a/tm2/pkg/crypto/keys/client/query.go b/tm2/pkg/crypto/keys/client/query.go index 58923f8787c..8cc5757aba7 100644 --- a/tm2/pkg/crypto/keys/client/query.go +++ b/tm2/pkg/crypto/keys/client/query.go @@ -3,7 +3,6 @@ package client import ( "context" "flag" - "fmt" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" @@ -22,7 +21,7 @@ type queryCfg struct { path string } -func newQueryCmd(rootCfg *baseCfg) *commands.Command { +func newQueryCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &queryCfg{ rootCfg: rootCfg, } @@ -35,7 +34,7 @@ func newQueryCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execQuery(cfg, args) + return execQuery(cfg, args, io) }, ) } @@ -63,7 +62,7 @@ func (c *queryCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execQuery(cfg *queryCfg, args []string) error { +func execQuery(cfg *queryCfg, args []string, io *commands.IO) error { if len(args) != 1 { return flag.ErrHelp } @@ -76,15 +75,16 @@ func execQuery(cfg *queryCfg, args []string) error { } if qres.Response.Error != nil { - fmt.Printf("Log: %s\n", + io.Printf("Log: %s\n", qres.Response.Log) return qres.Response.Error } + resdata := qres.Response.Data // XXX in general, how do we know what to show? // proof := qres.Response.Proof height := qres.Response.Height - fmt.Printf("height: %d\ndata: %s\n", + io.Printf("height: %d\ndata: %s\n", height, string(resdata)) return nil diff --git a/tm2/pkg/crypto/keys/client/root.go b/tm2/pkg/crypto/keys/client/root.go index ad8983f2bb9..550dd408b77 100644 --- a/tm2/pkg/crypto/keys/client/root.go +++ b/tm2/pkg/crypto/keys/client/root.go @@ -18,7 +18,7 @@ type baseCfg struct { BaseOptions } -func NewRootCmd() *commands.Command { +func NewRootCmd(io *commands.IO) *commands.Command { cfg := &baseCfg{} cmd := commands.NewCommand( @@ -35,17 +35,17 @@ func NewRootCmd() *commands.Command { ) cmd.AddSubCommands( - newAddCmd(cfg), - newDeleteCmd(cfg), - newGenerateCmd(cfg), - newExportCmd(cfg), - newImportCmd(cfg), - newListCmd(cfg), - newSignCmd(cfg), - newVerifyCmd(cfg), - newQueryCmd(cfg), - newBroadcastCmd(cfg), - newMakeTxCmd(cfg), + newAddCmd(cfg, io), + newDeleteCmd(cfg, io), + newGenerateCmd(cfg, io), + newExportCmd(cfg, io), + newImportCmd(cfg, io), + newListCmd(cfg, io), + newSignCmd(cfg, io), + newVerifyCmd(cfg, io), + newQueryCmd(cfg, io), + newBroadcastCmd(cfg, io), + newMakeTxCmd(cfg, io), ) return cmd diff --git a/tm2/pkg/crypto/keys/client/send.go b/tm2/pkg/crypto/keys/client/send.go index 8f82778b1e3..6d19ffcb393 100644 --- a/tm2/pkg/crypto/keys/client/send.go +++ b/tm2/pkg/crypto/keys/client/send.go @@ -21,7 +21,7 @@ type sendCfg struct { to string } -func newSendCmd(rootCfg *makeTxCfg) *commands.Command { +func newSendCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { cfg := &sendCfg{ rootCfg: rootCfg, } @@ -34,7 +34,7 @@ func newSendCmd(rootCfg *makeTxCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execSend(cfg, args, commands.NewDefaultIO()) + return execSend(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/sign.go b/tm2/pkg/crypto/keys/client/sign.go index bfc39647141..761e0d7a563 100644 --- a/tm2/pkg/crypto/keys/client/sign.go +++ b/tm2/pkg/crypto/keys/client/sign.go @@ -28,7 +28,7 @@ type signCfg struct { pass string } -func newSignCmd(rootCfg *baseCfg) *commands.Command { +func newSignCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &signCfg{ rootCfg: rootCfg, } @@ -41,7 +41,7 @@ func newSignCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execSign(cfg, args, commands.NewDefaultIO()) + return execSign(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/verify.go b/tm2/pkg/crypto/keys/client/verify.go index 3dcc5f35dee..bb486c1a8fa 100644 --- a/tm2/pkg/crypto/keys/client/verify.go +++ b/tm2/pkg/crypto/keys/client/verify.go @@ -16,7 +16,7 @@ type verifyCfg struct { docPath string } -func newVerifyCmd(rootCfg *baseCfg) *commands.Command { +func newVerifyCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &verifyCfg{ rootCfg: rootCfg, } @@ -29,7 +29,7 @@ func newVerifyCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execVerify(cfg, args, commands.NewDefaultIO()) + return execVerify(cfg, args, io) }, ) }