From 2b7a2fabf2770b8ad21497989f4abedc226eb0a7 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 12 Sep 2023 23:08:59 +0200 Subject: [PATCH] wip: add txtar driver Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/cmd/gnoland/integration.go | 1 + gno.land/cmd/gnoland/integration_test.go | 459 ++++++++++++++++++ .../gnoland/testdata/getting_started.txtar | 17 + gno.land/pkg/gnoland/app.go | 80 ++- 4 files changed, 545 insertions(+), 12 deletions(-) create mode 100644 gno.land/cmd/gnoland/integration.go create mode 100644 gno.land/cmd/gnoland/integration_test.go create mode 100644 gno.land/cmd/gnoland/testdata/getting_started.txtar diff --git a/gno.land/cmd/gnoland/integration.go b/gno.land/cmd/gnoland/integration.go new file mode 100644 index 000000000000..06ab7d0f9a35 --- /dev/null +++ b/gno.land/cmd/gnoland/integration.go @@ -0,0 +1 @@ +package main diff --git a/gno.land/cmd/gnoland/integration_test.go b/gno.land/cmd/gnoland/integration_test.go new file mode 100644 index 000000000000..72e74d2bc5ae --- /dev/null +++ b/gno.land/cmd/gnoland/integration_test.go @@ -0,0 +1,459 @@ +package main + +import ( + "flag" + "fmt" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + "unicode" + + "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" + 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/crypto/keys" + "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/jaekwon/testify/require" + "github.com/rogpeppe/go-internal/testscript" +) + +// XXX: should be centrilized 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" +) + +func TestTestdata(t *testing.T) { + testscript.Run(t, setupGnolangTestScript(t, "testdata")) +} + +func makeTestingGenesisDoc( + pvPub crypto.PubKey, + chainID string, +) *bft.GenesisDoc { + gen := &bft.GenesisDoc{} + + gen.GenesisTime = time.Now() + gen.ChainID = chainID + gen.ConsensusParams = abci.ConsensusParams{ + Block: &abci.BlockParams{ + // TODO: update limits. + MaxTxBytes: 1000000, // 1MB, + MaxDataBytes: 2000000, // 2MB, + MaxGas: 10000000, // 10M gas + TimeIotaMS: 100, // 100ms + }, + } + gen.Validators = []bft.GenesisValidator{ + { + Address: pvPub.Address(), + PubKey: pvPub, + Power: 10, + Name: "testvalidator", + }, + } + + return gen +} + +func execTestingGnoland(t *testing.T, gnoDataDir, gnoRootDir string, args []string) (*node.Node, error) { + // setup falgs + scfg := &startCfg{} + + // XXX: additionnal testing flags + // var testCfg struct { + // withExamples bool + // } + + { + fs := flag.NewFlagSet("start", flag.ExitOnError) + scfg.RegisterFlags(fs) + + // edit default flags if needed + 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") + + } + f.Value.Set(f.DefValue) + }) + + // XXX: additionnal testing flags + // fs.BoolVar( + // &testCfg.withExamples, + // "with-examples", + // true, + // "pre-add example pkgs", + // ) + + if err := fs.Parse(args); err != nil { + return nil, fmt.Errorf("unable to parse flags: %w", err) + } + } + + // logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) + logger := log.NewNopLogger() + + 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" + + // XXX: setup listener + newPrivValKey := cfg.PrivValidatorKeyFile() + newPrivValState := cfg.PrivValidatorStateFile() + + // create priv validator first. + // need it to generate genesis.json + priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) + + // write genesis file if missing. + // XXX: add genesis support + genesisFilePath := filepath.Join(gnoDataDir, cfg.Genesis) + osm.EnsureDir(filepath.Dir(genesisFilePath), 0o700) + if !osm.FileExists(genesisFilePath) { + genesisTxs := loadGenesisTxs(scfg.genesisTxsFile, scfg.chainID, scfg.genesisRemote) + genDoc := makeTestingGenesisDoc( + priv.GetPubKey(), + scfg.chainID, + ) + + // Load distribution. + balances := loadGenesisBalances(scfg.genesisBalancesFile) + + for _, balance := range balances { + fmt.Println(balance) + } + // 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 { + panic(fmt.Errorf("listing gno packages: %w", err)) + } + + // Sort packages by dependencies. + sortedPkgs, err := pkgs.Sort() + if err != nil { + panic(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 flag for default fee + 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. + genDoc.AppState = gnoland.GnoGenesisState{ + Balances: balances, + Txs: txs, + } + + writeGenesisFile(genDoc, genesisFilePath) + } + + // create application and node. + gnoApp, err := gnoland.NewCustomApp(gnoland.CustomAppConfig{ + Logger: logger, + GnoRootDir: gnoRootDir, + SkipFailingGenesisTxs: scfg.skipFailingGenesisTxs, + MaxCycles: scfg.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 setupGnolangTestScript(t *testing.T, txtarDir string) testscript.Params { + t.Helper() + + // Get root location of github.com/gnolang/gno + goModPath, err := exec.Command("go", "env", "GOMOD").CombinedOutput() + require.NoError(t, err) + + gnoRootDir := filepath.Dir(string(goModPath)) + + // Build a fresh binaries in a temp directory + gnokeyBin := filepath.Join(t.TempDir(), "gnokey") + err = exec.Command("go", "build", "-o", gnokeyBin, filepath.Join(gnoRootDir, "gno.land", "cmd", "gnokey")).Run() + require.NoError(t, err) + + gnoHomeDir := filepath.Join(t.TempDir(), "gno") + gnoDataDir := filepath.Join(t.TempDir(), "data") + + nodes := map[string] /* node_id -> node */ *node.Node{} + + t.Cleanup(func() { + for id, node := range nodes { + // XXX: do we need to check this here ? + if err = node.Stop(); err != nil { + panic(fmt.Errorf("node %q was unable to stop properly: %w", id, err)) + } + } + }) + + // Define script params + return testscript.Params{ + Setup: func(env *testscript.Env) error { + // XXX: add more env to help for test + kb, err := keys.NewKeyBaseFromDir(gnoHomeDir) + if err != nil { + return err + } + + // setup test1 user, great creator of all things + // XXX: setup multiple users + kb.CreateAccount("test1", test1Seed, "", "passphrase", 0, 0) + + env.Setenv("GNOROOT", gnoRootDir) + env.Setenv("GNOHOME", gnoHomeDir) + + return nil + }, + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ + "gnoland-start": func(ts *testscript.TestScript, neg bool, args []string) { + + id, args := extractArgsID(args) + if _, ok := nodes[id]; ok { + if neg { + return + } + + panic(fmt.Sprintf("node %q already started", id)) + } + + dataDir := filepath.Join(gnoDataDir, id) + node, err := execTestingGnoland(t, dataDir, gnoRootDir, args) + if err != nil { + ts.Logf("[%v]\n", err) + if !neg { + ts.Fatalf("unexpected start command failure") + } + } else { + if neg { + ts.Fatalf("unexpected start command success") + } + + // store our node + // XXX does this need mutex ? + nodes[id] = node + + // get listen addr environement + // should have been updated with the right port on start + laddr := node.Config().RPC.ListenAddress + + // set env dirs + if id == "default" { + // add default environement + ts.Setenv("RPC_ADDR", laddr) + ts.Setenv("GNODATA", dataDir) + } + + ts.Setenv("RPC_ADDR_"+id, laddr) + ts.Setenv("GNODATA_"+id, dataDir) + } + }, + + "gnoland-stop": func(ts *testscript.TestScript, neg bool, args []string) { + id, args := extractArgsID(args) + + n, ok := nodes[id] + if !ok { + if neg { + return + } + + panic(fmt.Errorf("node %q not started cannot be stop", id)) + } + + err := n.Stop() + if err != nil { + ts.Logf("[%v]\n", err) + if !neg { + ts.Fatalf("unexpected start command failure") + } + } else { + if neg { + ts.Fatalf("unexpected start command success") + } + + // unset env dirs + if id == "default" { + // add default environement + ts.Setenv("RPC_ADDR", "") + ts.Setenv("GNODATA", "") + + } + + ts.Setenv("RPC_ADDR_"+id, "") + ts.Setenv("GNODATA_"+id, "") + } + }, + + // add a custom "gnoffee" command so txtar files can easily execute "gno" + // without knowing where is the binary or how it is executed. + "gnokey": func(ts *testscript.TestScript, neg bool, args []string) { + flags := map[string]string{"id": "default"} + args = extractFlags(args, flags) + id := flags["id"] + + defaultArgs := []string{"-home", gnoHomeDir} + + if n, ok := nodes[id]; ok { + if raddr := n.Config().RPC.ListenAddress; raddr != "" { + defaultArgs = append(defaultArgs, "-remote", raddr) + } + } + + // inject default argument, if + // duplicate arguments, it + // should be override by the + // ones user provided + args = append(defaultArgs, args...) + + err := ts.Exec(gnokeyBin, args...) + if err != nil { + ts.Logf("[%v]\n", err) + if !neg { + ts.Fatalf("unexpected gnokey command failure") + } + } else { + if neg { + ts.Fatalf("unexpected gnokey command success") + } + } + }, + }, + + Dir: txtarDir, + } +} + +const defaultID = "default" + +// extractFlag extracts the value of the 'id' flag from a list of strings and +// returns the flag value along with a new slice of strings that does not contain the flag or its value. +func extractFlags(args []string, flagsmap map[string]string) []string { + newArgs := make([]string, 0, len(args)) + + i := 0 + for i < len(args) { + var id string + + arg := args[i] + for flag := range flagsmap { + // Check if current arg matches '-id' or '--id' + if strings.HasPrefix(arg, "-"+flag) || strings.HasPrefix(arg, "--"+flag) { + key := strings.TrimLeft(arg, "-") + switch { + case strings.Contains(key, "="): + parts := strings.SplitN(key, "=", 2) + if parts[0] != flag { + continue + } + id = parts[0] + i += 1 + case key == flag && i+1 < len(args): + id = args[i+1] + i += 2 + default: + continue + } + + // we got an id breakout the loop + flagsmap[flag] = id + break + } + } + + if id == "" { + newArgs = append(newArgs, arg) + i++ + } + } + + return args +} + +func extractFlag(args []string, flag, defvalue string) (string, []string) { + mflags := map[string]string{} + mflags[flag] = defvalue + newArgs := extractFlags(args, mflags) + return mflags[flag], newArgs +} + +func extractArgsID(args []string) (string, []string) { + if len(args) == 0 { + return defaultID, args + } + + arg := args[0] + + // Check if the argument consists only of letters and numbers. + for _, r := range arg { + if !unicode.IsLetter(r) && !unicode.IsNumber(r) { + return defaultID, args + } + } + + return arg, args[1:] +} diff --git a/gno.land/cmd/gnoland/testdata/getting_started.txtar b/gno.land/cmd/gnoland/testdata/getting_started.txtar new file mode 100644 index 000000000000..18f99b693fdc --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/getting_started.txtar @@ -0,0 +1,17 @@ +# getting started + +## start gnoland chain +gnoland-start + +## should show test1 +gnokey list + +## query test1 +gnokey query auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 + +# XXX: working but need an invitation +stdin test1_password +gnokey maketx call -insecure-password-stdin -pkgpath=gno.land/r/demo/users -func=Register -args="" -args=ERICCARTMAN -args=Profile -gas-fee=10000000ugnot -gas-wanted=2000000 -send=200000000ugnot -broadcast -chainid=tendermint_test test1 + +-- test1_password -- +passphrase \ No newline at end of file diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index b10f251b115f..860dfd2558e5 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,34 +22,52 @@ import ( "github.com/gnolang/gno/tm2/pkg/store/iavl" ) -// 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) +type CustomAppConfig struct { + DB dbm.DB + GnoRootDir string + SkipFailingGenesisTxs bool + Logger log.Logger + MaxCycles int64 +} + +func (c *CustomAppConfig) applyDefault() { + if c.Logger == nil { + c.Logger = log.NewNopLogger() + } + + if c.DB == nil { + c.DB = dbm.NewMemDB() } + if c.GnoRootDir == "" { + c.GnoRootDir = guessGnoRootDir() + } +} + +// NewApp creates the GnoLand application. +func NewCustomApp(cfg CustomAppConfig) (abci.Application, error) { + cfg.applyDefault() + // Capabilities keys. mainKey := store.NewStoreKey("main") 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 +108,22 @@ func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCy return baseApp, nil } +// NewApp creates the GnoLand application. +func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCycles int64) (abci.Application, error) { + var err error + var cfg CustomAppConfig + + // Get main DB. + cfg.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) + } + + cfg.Logger = logger + + return NewCustomApp(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 { @@ -146,3 +182,23 @@ 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 != "" { + rootdir = filepath.Clean(rootdir) + } else { + rootdir = "{{.Dir}}" + } + + // if GNOROOT is not set, try to guess the root directory using the `go list` command. + cmd := exec.Command("go", "list", "-m", "-mod=mod", "-f", rootdir, "github.com/gnolang/gno") + out, err := cmd.CombinedOutput() + if err != nil { + panic(fmt.Errorf("invalid gno directory: %q", rootdir)) + } + + return strings.TrimSpace(string(out)) +}