Skip to content

Commit

Permalink
fix: improve gno.land/gnoclient integration tests speed
Browse files Browse the repository at this point in the history
Signed-off-by: gfanton <[email protected]>
  • Loading branch information
gfanton committed Dec 21, 2024
1 parent 7ac95fe commit 5dd2a66
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 147 deletions.
72 changes: 62 additions & 10 deletions gno.land/pkg/gnoclient/integration_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package gnoclient

import (
"path/filepath"
"testing"

"github.com/gnolang/gno/gnovm/pkg/gnolang"

"github.com/gnolang/gno/gno.land/pkg/gnoland"
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
"github.com/gnolang/gno/gno.land/pkg/integration"
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
Expand All @@ -21,8 +23,14 @@ import (
)

func TestCallSingle_Integration(t *testing.T) {
// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
// Setup packages
rootdir := gnoenv.RootDir()
config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
meta := loadpkgs(t, rootdir, "gno.land/r/demo/deep/very/deep")
state := config.Genesis.AppState.(gnoland.GnoGenesisState)
state.Txs = append(state.Txs, meta...)
config.Genesis.AppState = state

node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -74,8 +82,14 @@ func TestCallSingle_Integration(t *testing.T) {
}

func TestCallMultiple_Integration(t *testing.T) {
// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
// Setup packages
rootdir := gnoenv.RootDir()
config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
meta := loadpkgs(t, rootdir, "gno.land/r/demo/deep/very/deep")
state := config.Genesis.AppState.(gnoland.GnoGenesisState)
state.Txs = append(state.Txs, meta...)
config.Genesis.AppState = state

node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -137,7 +151,7 @@ func TestCallMultiple_Integration(t *testing.T) {

func TestSendSingle_Integration(t *testing.T) {
// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -201,7 +215,7 @@ func TestSendSingle_Integration(t *testing.T) {

func TestSendMultiple_Integration(t *testing.T) {
// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -273,8 +287,15 @@ func TestSendMultiple_Integration(t *testing.T) {

// Run tests
func TestRunSingle_Integration(t *testing.T) {
// Setup packages
rootdir := gnoenv.RootDir()
config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
meta := loadpkgs(t, rootdir, "gno.land/p/demo/ufmt", "gno.land/r/demo/tests")
state := config.Genesis.AppState.(gnoland.GnoGenesisState)
state.Txs = append(state.Txs, meta...)
config.Genesis.AppState = state

// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -342,7 +363,17 @@ func main() {
// Run tests
func TestRunMultiple_Integration(t *testing.T) {
// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
rootdir := gnoenv.RootDir()
config := integration.TestingMinimalNodeConfig(rootdir)
meta := loadpkgs(t, rootdir,
"gno.land/p/demo/ufmt",
"gno.land/r/demo/tests",
"gno.land/r/demo/deep/very/deep",
)
state := config.Genesis.AppState.(gnoland.GnoGenesisState)
state.Txs = append(state.Txs, meta...)
config.Genesis.AppState = state

node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -434,7 +465,7 @@ func main() {

func TestAddPackageSingle_Integration(t *testing.T) {
// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -519,7 +550,7 @@ func Echo(str string) string {

func TestAddPackageMultiple_Integration(t *testing.T) {
// Set up in-memory node
config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir())
config := integration.TestingMinimalNodeConfig(gnoenv.RootDir())
node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config)
defer node.Stop()

Expand Down Expand Up @@ -670,3 +701,24 @@ func newInMemorySigner(t *testing.T, chainid string) *SignerFromKeybase {
ChainID: chainid, // Chain ID for transaction signing
}
}

func loadpkgs(t *testing.T, rootdir string, paths ...string) []gnoland.TxWithMetadata {
t.Helper()

loader := integration.NewPkgsLoader()
examplesDir := filepath.Join(rootdir, "examples")
for _, path := range paths {
path = filepath.Clean(path)
path = filepath.Join(examplesDir, path)
err := loader.LoadPackage(examplesDir, path, "")
require.NoErrorf(t, err, "`loadpkg` unable to load package(s) from %q: %s", path, err)
}

creator := crypto.MustAddressFromString(integration.DefaultAccount_Address)
defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))

meta, err := loader.LoadPackages(creator, defaultFee, nil)
require.NoError(t, err)

return meta
}
60 changes: 60 additions & 0 deletions gno.land/pkg/integration/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package integration

import (
"context"
"os"
"path/filepath"
"testing"
"time"

"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestGnolandIntegration tests the forking of a Gnoland node.
// XXX: For now keep this test sequential (no parallel execution is allowed).
func TestGnolandIntegration(t *testing.T) {
// Set a timeout of 20 seconds for the test context.
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()

tmpdir := t.TempDir() // Create a temporary directory for the test.

gnoRootDir := gnoenv.RootDir() // Get the root directory for Gnolang.

// Define paths for the build directory and the gnoland binary.
gnolandBuildDir := filepath.Join(tmpdir, "build")
gnolandBin := filepath.Join(gnolandBuildDir, "gnoland")

// Compile the gnoland binary.
err := buildGnoland(t, gnoRootDir, gnolandBin)
require.NoError(t, err)

// Prepare a minimal node configuration for testing.
cfg := TestingMinimalNodeConfig(gnoRootDir)
remoteAddr, cmd, err := ExecuteNode(ctx, gnolandBin, &Config{
Verbose: true,
RootDir: gnoRootDir,
TMConfig: cfg.TMConfig,
Genesis: NewMarshalableGenesisDoc(cfg.Genesis),
}, os.Stderr)
require.NoError(t, err)

defer func() {
// Ensure the process is killed after the test to clean up resources.
if cmd.Process != nil {
cmd.Process.Kill()
}
}()

// Create a new HTTP client to interact with the integration node.
cli, err := client.NewHTTPClient(remoteAddr)
require.NoError(t, err)

// Retreive node info.

Check failure on line 56 in gno.land/pkg/integration/cmd_test.go

View workflow job for this annotation

GitHub Actions / Run gno.land suite / Go Lint / lint

`Retreive` is a misspelling of `Retrieve` (misspell)
info, err := cli.ABCIInfo()
require.NoError(t, err)
assert.NotEmpty(t, info.Response.Data)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func isAllZero(key [64]byte) bool {
return true
}

func ForkableNode(cfg *integration.ForkConfig) error {
func IntegrationNode(cfg *integration.Config) error {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
if cfg.Verbose {
logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
Expand All @@ -45,8 +45,8 @@ func ForkableNode(cfg *integration.ForkConfig) error {

nodecfg := integration.TestingMinimalNodeConfig(cfg.RootDir)

if len(cfg.PrivValidator) > 0 && !isAllZero(cfg.PrivValidator) {
nodecfg.PrivValidator = bft.NewMockPVWithParams(cfg.PrivValidator, false, false)
if len(cfg.ValidatorKey) > 0 && !isAllZero(cfg.ValidatorKey) {
nodecfg.PrivValidator = bft.NewMockPVWithParams(cfg.ValidatorKey, false, false)
}
pv := nodecfg.PrivValidator.GetPubKey()

Expand All @@ -63,22 +63,16 @@ func ForkableNode(cfg *integration.ForkConfig) error {
},
}

fmt.Println("NEW NODE")

node, err := gnoland.NewInMemoryNode(logger, nodecfg)
if err != nil {
return fmt.Errorf("failed to create new in-memory node: %w", err)
}

fmt.Println(">>>STARTING")

err = node.Start()
if err != nil {
return fmt.Errorf("failed to start node: %w", err)
}

fmt.Println(">>>STARTING DONE")

ourAddress := nodecfg.PrivValidator.GetPubKey().Address()
isValidator := slices.ContainsFunc(nodecfg.Genesis.Validators, func(val bft.GenesisValidator) bool {
return val.Address == ourAddress
Expand Down Expand Up @@ -110,15 +104,15 @@ func main() {
}

// Unmarshal the JSON configuration
var cfg integration.ForkConfig
var cfg integration.Config
err = json.Unmarshal(configData, &cfg)
if err != nil {
fmt.Fprintf(os.Stdout, "Error unmarshaling JSON: %v\n", err)
os.Exit(1)
}

// Call the ForkableNode function with the parsed configuration
if err := ForkableNode(&cfg); err != nil {
if err := IntegrationNode(&cfg); err != nil {
fmt.Fprintf(os.Stdout, "Error running ForkableNode: %v\n", err)
os.Exit(1)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"

Expand Down Expand Up @@ -37,25 +39,41 @@ func (m *MarshalableGenesisDoc) UnmarshalJSON(data []byte) (err error) {
return
}

// Function to cast back to the original bft.GenesisDoc
// Cast back to the original bft.GenesisDoc.
func (m *MarshalableGenesisDoc) ToGenesisDoc() *bft.GenesisDoc {
return (*bft.GenesisDoc)(m)
}

type ForkConfig struct {
Verbose bool `json:"verbose"`
PrivValidator ed25519.PrivKeyEd25519 `json:"priv"`
DBDir string `json:"dbdir"`
RootDir string `json:"rootdir"`
Genesis *MarshalableGenesisDoc `json:"genesis"`
TMConfig *tmcfg.Config `json:"tm"`
type Config struct {
ValidatorKey ed25519.PrivKeyEd25519 `json:"priv"`
Verbose bool `json:"verbose"`
DBDir string `json:"dbdir"`
RootDir string `json:"rootdir"`
Genesis *MarshalableGenesisDoc `json:"genesis"`
TMConfig *tmcfg.Config `json:"tm"`
}

// ExecuteForkBinary runs the binary at the given path with the provided configuration.
func (i Config) validate() error {
if i.TMConfig == nil {
return errors.New("no tm config set")
}

if i.Genesis == nil {
return errors.New("no genesis is set")
}

return nil
}

// ExecuteNode runs the binary at the given path with the provided configuration.
// It marshals the configuration to JSON and passes it to the binary via stdin.
// The function waits for "READY:<address>" on stdout and returns the address if successful,
// or kills the process and returns an error if "READY" is not received within 10 seconds.
func ExecuteForkBinary(ctx context.Context, binaryPath string, cfg *ForkConfig) (string, *exec.Cmd, error) {
// or kills the process and returns an error if "READY" is not received within the context deadline.
func ExecuteNode(ctx context.Context, binaryPath string, cfg *Config, out io.Writer) (string, *exec.Cmd, error) {
if err := cfg.validate(); err != nil {
return "", nil, err
}

// Marshal the configuration to JSON
configData, err := json.Marshal(cfg)
if err != nil {
Expand Down Expand Up @@ -85,33 +103,35 @@ func ExecuteForkBinary(ctx context.Context, binaryPath string, cfg *ForkConfig)
readyChan := make(chan error, 1)
var address string

// Goroutine to read stdout and check for "READY"
// Wait for ready signal
go func() {
var scanned bool
var ready bool
scanner := bufio.NewScanner(stdoutPipe)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line) // Print each line to stdout for logging
if scanned {
continue
}
if _, err := fmt.Sscanf(line, "READY:%s", &address); err == nil {
readyChan <- nil
scanned = true
if !ready {
if _, err := fmt.Sscanf(line, "READY:%s", &address); err == nil {
readyChan <- nil
ready = true
continue
}
}

fmt.Fprintln(out, line)
}

if err := scanner.Err(); err != nil {
readyChan <- fmt.Errorf("error reading stdout: %w", err)
} else {
readyChan <- fmt.Errorf("process exited without 'READY'")
}
}()

// Wait for either the "READY" signal or a timeout
// Wait for either the "READY" signal or a context timeout.
select {
case err := <-readyChan:
if err != nil {
fmt.Println("ERR", err)
fmt.Fprintf(out, "err: %q\n", err)
cmd.Process.Kill()
return "", cmd, err
}
Expand Down
Loading

0 comments on commit 5dd2a66

Please sign in to comment.