Skip to content

Commit

Permalink
feat: add txtar driver to gnoland
Browse files Browse the repository at this point in the history
** as requested, this is a squash of the following commits:
- feat: use commands.IO
- chore: cleanup gnoland output using context logger
- chore: cleanup integration test
- chore: cleanup
- feat: add test data example
- chore: cleanup
- fix: concurent run of gnoland
- chore: cleanup
- fix: cleanup
- fix: docker integration test
- chore: cleanup root dir and validate config
- fix: correctly update command io argument
- chore: cleanup

Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
  • Loading branch information
gfanton committed Sep 25, 2023
1 parent 5a7d005 commit ad175a8
Show file tree
Hide file tree
Showing 24 changed files with 556 additions and 64 deletions.
3 changes: 2 additions & 1 deletion gno.land/cmd/gnokey/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Check warning on line 13 in gno.land/cmd/gnokey/main.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnokey/main.go#L13

Added line #L13 was not covered by tests

if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%+v\n", err)
Expand Down
1 change: 1 addition & 0 deletions gno.land/cmd/gnoland/integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package main
365 changes: 365 additions & 0 deletions gno.land/cmd/gnoland/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
package main

import (
"context"
"flag"
"fmt"
"hash/crc32"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"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"
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/commands"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
"github.com/gnolang/gno/tm2/pkg/crypto/keys/client"
"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 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"
)

func TestTestdata(t *testing.T) {
testscript.Run(t, setupGnolandTestScript2(t, "testdata"))
}

func setupGnolandTestScript2(t *testing.T, txtarDir string) testscript.Params {
t.Helper()

goModPath, err := exec.Command("go", "env", "GOMOD").CombinedOutput()
require.NoError(t, err)

gnoRootDir := filepath.Dir(string(goModPath))
gnoHomeDir := filepath.Join(t.TempDir(), "gno")
gnoDataDir := filepath.Join(t.TempDir(), "data")

var muNodes sync.Mutex
nodes := map[string]*node.Node{}
t.Cleanup(func() {
for id, n := range nodes {
if err := n.Stop(); err != nil {
panic(fmt.Errorf("node %q was unable to stop: %w", id, err))
}
}
})

return testscript.Params{
Dir: txtarDir,
Setup: func(env *testscript.Env) error {
kb, err := keys.NewKeyBaseFromDir(gnoHomeDir)
if err != nil {
return err
}

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("use gnoland [start|stop] command"))
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 %q already started", sid)
break
}

dataDir := filepath.Join(gnoDataDir, sid)
var node *node.Node
if node, err = execTestingGnoland(t, dataDir, gnoRootDir, args); err == nil {
// XXX need mutex ?
nodes[sid] = node

// get listen addr environement
// should have been updated with the right port on start
laddr := node.Config().RPC.ListenAddress

// add default environement
ts.Setenv("RPC_ADDR", laddr)
ts.Setenv("GNODATA", gnoDataDir)

// XXX: Use something similar to `require.Eventually` to check for node
// availability. For now, if this sleep duration is too short, the
// subsequent command might fail with an [internal error].
time.Sleep(time.Millisecond * 300)
}
case "stop":
n, ok := nodes[sid]
if !ok {
err = fmt.Errorf("node %q not started cannot be stop", sid)
break
}

if err = n.Stop(); err != nil {
delete(nodes, sid)

// unset env dirs
ts.Setenv("RPC_ADDR", "")
ts.Setenv("GNODATA", "")
}
}

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)
}
}

// 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 execTestingGnoland(t *testing.T, gnoDataDir, gnoRootDir string, args []string) (*node.Node, error) {
t.Helper()

// Setup logger
logger := log.NewNopLogger()

// Setup start config
scfg := &startCfg{}
{
fs := flag.NewFlagSet("start", flag.ExitOnError)
scfg.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, scfg, gnoRootDir); err != nil {
return nil, err
}

// Create application and node
return createAppAndNode(cfg, logger, gnoRootDir, scfg)
}

func getSessionID(ts *testscript.TestScript) string {
works := ts.Getenv("WORK")
sum := crc32.ChecksumIEEE([]byte(works))
return strconv.FormatUint(uint64(sum), 16)
}

func setupTestingGenesis(gnoDataDir string, cfg *config.Config, scfg *startCfg, gnoRootDir string) error {
newPrivValKey := cfg.PrivValidatorKeyFile()
newPrivValState := cfg.PrivValidatorStateFile()
priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState)

genesisFilePath := filepath.Join(gnoDataDir, cfg.Genesis)
osm.EnsureDir(filepath.Dir(genesisFilePath), 0o700)
if !osm.FileExists(genesisFilePath) {
genesisTxs := loadGenesisTxs(scfg.genesisTxsFile, scfg.chainID, scfg.genesisRemote)
pvPub := priv.GetPubKey()

gen := &bft.GenesisDoc{
GenesisTime: time.Now(),
ChainID: scfg.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(scfg.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, scfg *startCfg) (*node.Node, error) {
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 tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) {
if err != nil {
ts.Logf("%s error: %v\n", cmd, err)
if !neg {
ts.Fatalf("unexpected %s command failure", cmd)
}
} else {
if neg {
ts.Fatalf("unexpected %s command success", cmd)
}
}
}
Loading

0 comments on commit ad175a8

Please sign in to comment.