Skip to content

Commit

Permalink
feat: add txtar driver for gnoland integration test (#1117)
Browse files Browse the repository at this point in the history
Resolves #1101.

This PR establishes a base for creating `txtar` tests using a partial
in-memory `gnoland` node. It supports:

- `gnoland [start|stop]`: The node doesn't start automatically, which
enables users to perform pre-configuration or pass custom arguments to
the start command.
- `gnokey`: Most of the commands should work. The `--remote`,
`--insecure-password-stdin`, and `--home` flags are automatically set
with the appropriate values to communicate with the node.
- `sleep`: A simple helper to introduce a delay between actions,
ensuring specific tasks complete before proceeding.

Currently, only the "test1" user is automatically created in the default
keybase directory (without a password). Default `gnoland` genesis
balance and genesis transaction files are also load on start.

You can find some examples of `txtar` in this directory:

https://github.com/gfanton/gno/tree/feat/gnoland-txtar-driver/gno.land/cmd/gnoland/testdata

`gnoland` logs aren't forwarded to stdout to avoid overwhelming
informations in the tests. Instead, you can specify a log directory
using the `LOG_DIR` environment variable or set `TESTWORK=true` to
enable persistence in the `txtar` working directory. In either case, the
path to the log file will be printed at the start if one of these
environment variables is set.
This also enables storing logs as artifacts on the CI for later
examination.

There is still a lot to do, but I believe this provides a good base for
future iterations.

<!-- please provide a detailed description of the changes made in this
pull request. -->

<details><summary>Contributors' checklist...</summary>

- [ ] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
Co-authored-by: Morgan <git@howl.moe>
  • Loading branch information
3 people committed Oct 5, 2023
1 parent ce258b1 commit 924d62d
Show file tree
Hide file tree
Showing 30 changed files with 1,065 additions and 67 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/gnoland.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions docs/testing_guide.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 7 additions & 4 deletions gno.land/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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/...


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

if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%+v\n", err)
Expand Down
12 changes: 12 additions & 0 deletions gno.land/cmd/gnoland/integration_test.go
Original file line number Diff line number Diff line change
@@ -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"))
}
26 changes: 26 additions & 0 deletions gno.land/cmd/gnoland/testdata/addpkg.txtar
Original file line number Diff line number Diff line change
@@ -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
98 changes: 84 additions & 14 deletions gno.land/pkg/gnoland/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package gnoland

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

Expand All @@ -20,34 +22,62 @@ 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.
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{
Expand Down Expand Up @@ -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 {
Expand All @@ -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!
Expand Down Expand Up @@ -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")
}
Loading

0 comments on commit 924d62d

Please sign in to comment.