From 95450fff471df1adc9ac2115006d87d90c529e4a Mon Sep 17 00:00:00 2001
From: Blake <104744707+r3v4s@users.noreply.github.com>
Date: Tue, 26 Nov 2024 19:33:26 +0900
Subject: [PATCH 01/86] ci: validate genesis.json (#3170)
---
.github/workflows/genesis-verify.yml | 56 ++++++++++++++++++++++++++++
1 file changed, 56 insertions(+)
create mode 100644 .github/workflows/genesis-verify.yml
diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml
new file mode 100644
index 00000000000..6c9955b7178
--- /dev/null
+++ b/.github/workflows/genesis-verify.yml
@@ -0,0 +1,56 @@
+name: genesis-verify
+
+on:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - "misc/deployments/**/genesis.json"
+
+jobs:
+ verify:
+ strategy:
+ fail-fast: false
+ matrix:
+ testnet: ["test5.gno.land"]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v41
+ with:
+ files: "misc/deployments/${{ matrix.testnet }}/genesis.json"
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.22"
+
+ - name: Build gnogenesis
+ run: make -C contribs/gnogenesis
+
+ - name: Verify each genesis file
+ run: |
+ for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
+ echo "Verifying $file"
+ gnogenesis verify -genesis-path $file
+ done
+
+ - name: Build gnoland
+ run: make -C gno.land install.gnoland
+
+ - name: Running latest gnoland with each genesis file
+ run: |
+ for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
+ echo "Running gnoland with $file"
+ timeout 60s gnoland start -lazy --genesis $file || exit_code=$?
+ if [ $exit_code -eq 124 ]; then
+ echo "Gnoland genesis state generated successfully"
+ else
+ echo "Gnoland failed to start with $file"
+ exit 1
+ fi
+ done
From 496bcba5fb5f9743711a37a14b2bdb95371e1244 Mon Sep 17 00:00:00 2001
From: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Date: Tue, 26 Nov 2024 11:58:36 +0100
Subject: [PATCH 02/86] chore(examples/gnoland): fix gnoland/home realm, rename
the blog (#3177)
## Description
Fixes a broken link on the home page and renames the gno.land blog.
Two options for the blog name:
- gno.land's blog
- The Gno Blog
Option 1 seems favorable.
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---
examples/gno.land/r/gnoland/blog/gnoblog.gno | 2 +-
examples/gno.land/r/gnoland/blog/gnoblog_test.gno | 14 +++++++-------
examples/gno.land/r/gnoland/home/home.gno | 2 +-
examples/gno.land/r/gnoland/home/home_filetest.gno | 2 +-
examples/gno.land/r/gov/dao/v2/prop2_filetest.gno | 4 ++--
5 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/examples/gno.land/r/gnoland/blog/gnoblog.gno b/examples/gno.land/r/gnoland/blog/gnoblog.gno
index 1cdc95fe9a8..d2a163543e5 100644
--- a/examples/gno.land/r/gnoland/blog/gnoblog.gno
+++ b/examples/gno.land/r/gnoland/blog/gnoblog.gno
@@ -7,7 +7,7 @@ import (
)
var b = &blog.Blog{
- Title: "Gnoland's Blog",
+ Title: "gno.land's blog",
Prefix: "/r/gnoland/blog:",
}
diff --git a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno
index 328fbe2baa4..b4658db4fb5 100644
--- a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno
+++ b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno
@@ -15,7 +15,7 @@ func TestPackage(t *testing.T) {
{
got := Render("")
expected := `
-# Gnoland's Blog
+# gno.land's blog
No posts.
`
@@ -28,7 +28,7 @@ No posts.
ModAddPost("slug2", "title2", "body2", "2022-05-20T13:17:23Z", "moul", "tag1,tag3")
got := Render("")
expected := `
- # Gnoland's Blog
+ # gno.land's blog
@@ -59,7 +59,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)
Written by moul on 20 May 2022
-Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog
+Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog
---
Comment section
@@ -74,12 +74,12 @@ Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog
// list by tags.
{
got := Render("t/invalid")
- expected := "# [Gnoland's Blog](/r/gnoland/blog:) / t / invalid\n\nNo posts."
+ expected := "# [gno.land's blog](/r/gnoland/blog:) / t / invalid\n\nNo posts."
assertMDEquals(t, got, expected)
got = Render("t/tag2")
expected = `
-# [Gnoland's Blog](/r/gnoland/blog:) / t / tag2
+# [gno.land's blog](/r/gnoland/blog:) / t / tag2
@@ -110,7 +110,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3)
Written by moul on 20 May 2022
-Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog
+Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog
---
Comment section
@@ -152,7 +152,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4)
Written by manfred on 20 May 2022
-Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to Gnoland's Blog
+Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog
---
Comment section
diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno
index 6a520dba394..facb1817fe2 100644
--- a/examples/gno.land/r/gnoland/home/home.gno
+++ b/examples/gno.land/r/gnoland/home/home.gno
@@ -95,7 +95,7 @@ func latestHOFItems(num int) ui.Element {
submissions := hof.RenderExhibWidget(num)
return ui.Element{
- ui.H3("[Hall of Fame](/r/demo/hof)"),
+ ui.H3("[Hall of Fame](/r/leon/hof)"),
ui.Text(submissions),
}
}
diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno
index c587af9b817..4825c9fc588 100644
--- a/examples/gno.land/r/gnoland/home/home_filetest.gno
+++ b/examples/gno.land/r/gnoland/home/home_filetest.gno
@@ -78,7 +78,7 @@ func main() {
//
//
//
-// ### [Hall of Fame](/r/demo/hof)
+// ### [Hall of Fame](/r/leon/hof)
//
//
//
diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
index bfd3f44f6dd..4eb993b80dc 100644
--- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
@@ -82,7 +82,7 @@ func main() {
//
//
// --
-// # Gnoland's Blog
+// # gno.land's blog
//
// No posts.
// --
@@ -101,7 +101,7 @@ func main() {
//
//
// --
-// # Gnoland's Blog
+// # gno.land's blog
//
//
//
From 0e1016b4ba80e99a8768f251b21f0e03609c5498 Mon Sep 17 00:00:00 2001
From: Nathan Toups <612924+n2p5@users.noreply.github.com>
Date: Tue, 26 Nov 2024 06:13:21 -0700
Subject: [PATCH 03/86] feat: adding home realm for r/n2p5/home and supporting
packages (#3183)
# TLDR
There's no place like [home](https://gno.land/r/n2p5/home). This PR is
for `gno` code in [/r/n2p5/home](https://gno.land/r/n2p5/home) and its
supporting packages and realms. This is a more extensive PR than I'd
hoped, but it is because I ended up structuring out two new little
packages [chonk]() and [group](), as well as a [config]() pattern that
works for my home realm, but is reusable as well.
If you like this, vote for me on [Leon's Hall of
Fame](https://gno.land/r/leon/hof)
# Summary of changes
## [/p/n2p5/chonk](https://gno.land/p/n2p5/chonk/)
`chonk` is a linked-list based string chunker. This allows for
arbitrarily large strings to be stored on-chain across multiple
transactions. The original idea for this was to be able to support large
`Render(path string) string` calls. You can think of this as supporting
a "static site generator" pattern, where Markdown can be broken up into
chunks and then rendered as one large payload. It is used directly in
`/r/n2p5/home`.
## [/p/n2p5/mgroup](https://gno.land/p/n2p5/mgroup/)
`mgroup` (Managed Group) is a bit like `authorizable,` but the goals are
a bit different. I wanted a "managed group" with a single `ownable`
owner, but I wanted an arbitrary list of "backup owners," which are
accounts that could "claim" ownership if the owner account became
unavailable. I also wanted to be able to manage the group members
themselves. Another difference here is has the ability to return
information about the group such as
- a list of all backup owners (there is a method for the complete list,
but also a method for an offset and max count iterator for large groups)
- a list of all members (there is a method for the complete list, but
also a method for an offset and max count iterator for large groups)
## [/r/n2p5/config](https://gno.land/r/n2p5/config)
Inspired by the config work done by @moul and @leohhhn for their home
realms, I decided to take a crack at it as well. This config allows me
to use `mgroup` to manage the config auth. This allows me to power
cross-realm auth using this config realm, and it powers the auth for my
home realm.
## [/r/n2p5/home](https://gno.land/r/n2p5/home)
Bringing this all together, the home realm uses `chonk` and
`/r/n2p5/config` to Render the Markdown stored in the chonk data store.
Because you might submit data over multiple transactions, I've added the
ability to "preview" and "promote" render data, you can see this in the
two variations on the URL:
- https://gno.land/r/n2p5/home
- https://gno.land/r/n2p5/home:preview
Contributors' checklist...
- [X] Added new tests, or not needed, or not feasible
- [X] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [X] Updated the official documentation or not needed
- [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [X] Added references to related issues and PRs
- [X] Provided any useful hints for running manual tests
---
examples/gno.land/p/n2p5/chonk/chonk.gno | 84 ++++
examples/gno.land/p/n2p5/chonk/chonk_test.gno | 54 +++
examples/gno.land/p/n2p5/chonk/gno.mod | 1 +
examples/gno.land/p/n2p5/mgroup/gno.mod | 7 +
examples/gno.land/p/n2p5/mgroup/mgroup.gno | 184 ++++++++
.../gno.land/p/n2p5/mgroup/mgroup_test.gno | 420 ++++++++++++++++++
examples/gno.land/r/n2p5/config/config.gno | 120 +++++
examples/gno.land/r/n2p5/config/gno.mod | 6 +
examples/gno.land/r/n2p5/home/gno.mod | 7 +
examples/gno.land/r/n2p5/home/home.gno | 73 +++
10 files changed, 956 insertions(+)
create mode 100644 examples/gno.land/p/n2p5/chonk/chonk.gno
create mode 100644 examples/gno.land/p/n2p5/chonk/chonk_test.gno
create mode 100644 examples/gno.land/p/n2p5/chonk/gno.mod
create mode 100644 examples/gno.land/p/n2p5/mgroup/gno.mod
create mode 100644 examples/gno.land/p/n2p5/mgroup/mgroup.gno
create mode 100644 examples/gno.land/p/n2p5/mgroup/mgroup_test.gno
create mode 100644 examples/gno.land/r/n2p5/config/config.gno
create mode 100644 examples/gno.land/r/n2p5/config/gno.mod
create mode 100644 examples/gno.land/r/n2p5/home/gno.mod
create mode 100644 examples/gno.land/r/n2p5/home/home.gno
diff --git a/examples/gno.land/p/n2p5/chonk/chonk.gno b/examples/gno.land/p/n2p5/chonk/chonk.gno
new file mode 100644
index 00000000000..8b7425eafd0
--- /dev/null
+++ b/examples/gno.land/p/n2p5/chonk/chonk.gno
@@ -0,0 +1,84 @@
+// Package chonk provides a simple way to store arbitrarily large strings
+// in a linked list across transactions for efficient storage and retrieval.
+// A Chonk support three operations: Add, Flush, and Scanner.
+// - Add appends a string to the Chonk.
+// - Flush clears the Chonk.
+// - Scanner is used to iterate over the chunks in the Chonk.
+package chonk
+
+// Chonk is a linked list string storage and
+// retrieval system for fine bois.
+type Chonk struct {
+ first *chunk
+ last *chunk
+}
+
+// chunk is a linked list node for Chonk
+type chunk struct {
+ text string
+ next *chunk
+}
+
+// New creates a reference to a new Chonk
+func New() *Chonk {
+ return &Chonk{}
+}
+
+// Add appends a string to the Chonk. If the Chonk is empty,
+// the string will be the first and last chunk. Otherwise,
+// the string will be appended to the end of the Chonk.
+func (c *Chonk) Add(text string) {
+ next := &chunk{text: text}
+ if c.first == nil {
+ c.first = next
+ c.last = next
+ return
+ }
+ c.last.next = next
+ c.last = next
+}
+
+// Flush clears the Chonk by setting the first and last
+// chunks to nil. This will allow the garbage collector to
+// free the memory used by the Chonk.
+func (c *Chonk) Flush() {
+ c.first = nil
+ c.last = nil
+}
+
+// Scanner returns a new Scanner for the Chonk. The Scanner
+// is used to iterate over the chunks in the Chonk.
+func (c *Chonk) Scanner() *Scanner {
+ return &Scanner{
+ next: c.first,
+ }
+}
+
+// Scanner is a simple string scanner for Chonk. It is used
+// to iterate over the chunks in a Chonk from first to last.
+type Scanner struct {
+ current *chunk
+ next *chunk
+}
+
+// Scan advances the scanner to the next chunk. It returns
+// true if there is a next chunk, and false if there is not.
+func (s *Scanner) Scan() bool {
+ if s.next != nil {
+ s.current = s.next
+ s.next = s.next.next
+ return true
+ }
+ return false
+}
+
+// Text returns the current chunk. It is only valid to call
+// this method after a call to Scan returns true. Expected usage:
+//
+// scanner := chonk.Scanner()
+// for scanner.Scan() {
+// fmt.Println(scanner.Text())
+// }
+func (s *Scanner) Text() string {
+ return s.current.text
+}
diff --git a/examples/gno.land/p/n2p5/chonk/chonk_test.gno b/examples/gno.land/p/n2p5/chonk/chonk_test.gno
new file mode 100644
index 00000000000..7caf1012d39
--- /dev/null
+++ b/examples/gno.land/p/n2p5/chonk/chonk_test.gno
@@ -0,0 +1,54 @@
+package chonk
+
+import (
+ "testing"
+)
+
+func TestChonk(t *testing.T) {
+ t.Parallel()
+ c := New()
+ testTable := []struct {
+ name string
+ chunks []string
+ }{
+ {
+ name: "empty",
+ chunks: []string{},
+ },
+ {
+ name: "single chunk",
+ chunks: []string{"a"},
+ },
+ {
+ name: "multiple chunks",
+ chunks: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
+ },
+ {
+ name: "multiline chunks",
+ chunks: []string{"1a\nb\nc\n\n", "d\ne\nf", "g\nh\ni", "j\nk\nl\n\n\n\n"},
+ },
+ {
+ name: "empty",
+ chunks: []string{},
+ },
+ }
+ testChonk := func(t *testing.T, c *Chonk, chunks []string) {
+ for _, chunk := range chunks {
+ c.Add(chunk)
+ }
+ scanner := c.Scanner()
+ i := 0
+ for scanner.Scan() {
+ if scanner.Text() != chunks[i] {
+ t.Errorf("expected %s, got %s", chunks[i], scanner.Text())
+ }
+ i++
+ }
+ }
+ for _, test := range testTable {
+ t.Run(test.name, func(t *testing.T) {
+ testChonk(t, c, test.chunks)
+ c.Flush()
+ })
+ }
+}
diff --git a/examples/gno.land/p/n2p5/chonk/gno.mod b/examples/gno.land/p/n2p5/chonk/gno.mod
new file mode 100644
index 00000000000..b0dee537b0e
--- /dev/null
+++ b/examples/gno.land/p/n2p5/chonk/gno.mod
@@ -0,0 +1 @@
+module gno.land/p/n2p5/chonk
diff --git a/examples/gno.land/p/n2p5/mgroup/gno.mod b/examples/gno.land/p/n2p5/mgroup/gno.mod
new file mode 100644
index 00000000000..95fdbe2f195
--- /dev/null
+++ b/examples/gno.land/p/n2p5/mgroup/gno.mod
@@ -0,0 +1,7 @@
+module gno.land/p/n2p5/mgroup
+
+require (
+ gno.land/p/demo/avl v0.0.0-latest
+ gno.land/p/demo/ownable v0.0.0-latest
+ gno.land/p/demo/testutils v0.0.0-latest
+)
diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup.gno b/examples/gno.land/p/n2p5/mgroup/mgroup.gno
new file mode 100644
index 00000000000..0c029401ff7
--- /dev/null
+++ b/examples/gno.land/p/n2p5/mgroup/mgroup.gno
@@ -0,0 +1,184 @@
+// Package mgroup is a simple managed group managing ownership and membership
+// for authorization in gno realms. The ManagedGroup struct is used to manage
+// the owner, backup owners, and members of a group. The owner is the primary
+// owner of the group and can add and remove backup owners and members. Backup
+// owners can claim ownership of the group. This is meant to provide backup
+// accounts for the owner in case the owner account is lost or compromised.
+// Members are used to authorize actions across realms.
+package mgroup
+
+import (
+ "errors"
+ "std"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/ownable"
+)
+
+var (
+ ErrCannotRemoveOwner = errors.New("mgroup: cannot remove owner")
+ ErrNotBackupOwner = errors.New("mgroup: not a backup owner")
+ ErrNotMember = errors.New("mgroup: not a member")
+ ErrInvalidAddress = errors.New("mgroup: address is invalid")
+)
+
+type ManagedGroup struct {
+ owner *ownable.Ownable
+ backupOwners *avl.Tree
+ members *avl.Tree
+}
+
+// New creates a new ManagedGroup with the owner set to the provided address.
+// The owner is automatically added as a backup owner and member of the group.
+func New(ownerAddress std.Address) *ManagedGroup {
+ g := &ManagedGroup{
+ owner: ownable.NewWithAddress(ownerAddress),
+ backupOwners: avl.NewTree(),
+ members: avl.NewTree(),
+ }
+ g.AddBackupOwner(ownerAddress)
+ g.AddMember(ownerAddress)
+ return g
+}
+
+// AddBackupOwner adds a backup owner to the group by std.Address.
+// If the caller is not the owner, an error is returned.
+func (g *ManagedGroup) AddBackupOwner(addr std.Address) error {
+ if err := g.owner.CallerIsOwner(); err != nil {
+ return err
+ }
+ if !addr.IsValid() {
+ return ErrInvalidAddress
+ }
+ g.backupOwners.Set(addr.String(), struct{}{})
+ return nil
+}
+
+// RemoveBackupOwner removes a backup owner from the group by std.Address.
+// The owner cannot be removed. If the caller is not the owner, an error is returned.
+func (g *ManagedGroup) RemoveBackupOwner(addr std.Address) error {
+ if err := g.owner.CallerIsOwner(); err != nil {
+ return err
+ }
+ if !addr.IsValid() {
+ return ErrInvalidAddress
+ }
+ if addr == g.Owner() {
+ return ErrCannotRemoveOwner
+ }
+ g.backupOwners.Remove(addr.String())
+ return nil
+}
+
+// ClaimOwnership allows a backup owner to claim ownership of the group.
+// If the caller is not a backup owner, an error is returned.
+// The caller is automatically added as a member of the group.
+func (g *ManagedGroup) ClaimOwnership() error {
+ caller := std.PrevRealm().Addr()
+ // already owner, skip
+ if caller == g.Owner() {
+ return nil
+ }
+ if !g.IsBackupOwner(caller) {
+ return ErrNotMember
+ }
+ g.owner = ownable.NewWithAddress(caller)
+ g.AddMember(caller)
+ return nil
+}
+
+// AddMember adds a member to the group by std.Address.
+// If the caller is not the owner, an error is returned.
+func (g *ManagedGroup) AddMember(addr std.Address) error {
+ if err := g.owner.CallerIsOwner(); err != nil {
+ return err
+ }
+ if !addr.IsValid() {
+ return ErrInvalidAddress
+ }
+ g.members.Set(addr.String(), struct{}{})
+ return nil
+}
+
+// RemoveMember removes a member from the group by std.Address.
+// The owner cannot be removed. If the caller is not the owner,
+// an error is returned.
+func (g *ManagedGroup) RemoveMember(addr std.Address) error {
+ if err := g.owner.CallerIsOwner(); err != nil {
+ return err
+ }
+ if !addr.IsValid() {
+ return ErrInvalidAddress
+ }
+ if addr == g.Owner() {
+ return ErrCannotRemoveOwner
+ }
+ g.members.Remove(addr.String())
+ return nil
+}
+
+// MemberCount returns the number of members in the group.
+func (g *ManagedGroup) MemberCount() int {
+ return g.members.Size()
+}
+
+// BackupOwnerCount returns the number of backup owners in the group.
+func (g *ManagedGroup) BackupOwnerCount() int {
+ return g.backupOwners.Size()
+}
+
+// IsMember checks if an address is a member of the group.
+func (g *ManagedGroup) IsMember(addr std.Address) bool {
+ return g.members.Has(addr.String())
+}
+
+// IsBackupOwner checks if an address is a backup owner in the group.
+func (g *ManagedGroup) IsBackupOwner(addr std.Address) bool {
+ return g.backupOwners.Has(addr.String())
+}
+
+// Owner returns the owner of the group.
+func (g *ManagedGroup) Owner() std.Address {
+ return g.owner.Owner()
+}
+
+// BackupOwners returns a slice of all backup owners in the group, using the underlying
+// avl.Tree to iterate over the backup owners. If you have a large group, you may
+// want to use BackupOwnersWithOffset to iterate over backup owners in chunks.
+func (g *ManagedGroup) BackupOwners() []string {
+ return g.BackupOwnersWithOffset(0, g.BackupOwnerCount())
+}
+
+// Members returns a slice of all members in the group, using the underlying
+// avl.Tree to iterate over the members. If you have a large group, you may
+// want to use MembersWithOffset to iterate over members in chunks.
+func (g *ManagedGroup) Members() []string {
+ return g.MembersWithOffset(0, g.MemberCount())
+}
+
+// BackupOwnersWithOffset returns a slice of backup owners in the group, using the underlying
+// avl.Tree to iterate over the backup owners. The offset and count parameters allow you
+// to iterate over backup owners in chunks to support patterns such as pagination.
+func (g *ManagedGroup) BackupOwnersWithOffset(offset, count int) []string {
+ return sliceWithOffset(g.backupOwners, offset, count)
+}
+
+// MembersWithOffset returns a slice of members in the group, using the underlying
+// avl.Tree to iterate over the members. The offset and count parameters allow you
+// to iterate over members in chunks to support patterns such as pagination.
+func (g *ManagedGroup) MembersWithOffset(offset, count int) []string {
+ return sliceWithOffset(g.members, offset, count)
+}
+
+// sliceWithOffset is a helper function to iterate over an avl.Tree with an offset and count.
+func sliceWithOffset(t *avl.Tree, offset, count int) []string {
+ var result []string
+ t.IterateByOffset(offset, count, func(k string, _ interface{}) bool {
+ if k == "" {
+ return true
+ }
+ result = append(result, k)
+ return false
+ })
+ return result
+}
diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno
new file mode 100644
index 00000000000..7ef0619188f
--- /dev/null
+++ b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno
@@ -0,0 +1,420 @@
+package mgroup
+
+import (
+ "std"
+ "testing"
+
+ "gno.land/p/demo/avl"
+ "gno.land/p/demo/ownable"
+ "gno.land/p/demo/testutils"
+)
+
+func TestManagedGroup(t *testing.T) {
+ t.Parallel()
+
+ u1 := testutils.TestAddress("u1")
+ u2 := testutils.TestAddress("u2")
+ u3 := testutils.TestAddress("u3")
+
+ t.Run("AddBackupOwner", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ // happy path
+ {
+ std.TestSetOrigCaller(u1)
+ err := g.AddBackupOwner(u2)
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ }
+ // ensure checking for authorized caller
+ {
+ std.TestSetOrigCaller(u2)
+ err := g.AddBackupOwner(u3)
+ if err != ownable.ErrUnauthorized {
+ t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error())
+ }
+ }
+ // ensure invalid address is caught
+ {
+ std.TestSetOrigCaller(u1)
+ var badAddr std.Address
+ err := g.AddBackupOwner(badAddr)
+ if err != ErrInvalidAddress {
+ t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error())
+ }
+ }
+ })
+ t.Run("RemoveBackupOwner", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ // happy path
+ {
+ std.TestSetOrigCaller(u1)
+ g.AddBackupOwner(u2)
+ err := g.RemoveBackupOwner(u2)
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ }
+ // running this twice should not error.
+ {
+ std.TestSetOrigCaller(u1)
+ err := g.RemoveBackupOwner(u2)
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ }
+ // ensure checking for authorized caller
+ {
+ std.TestSetOrigCaller(u2)
+ err := g.RemoveBackupOwner(u3)
+ if err != ownable.ErrUnauthorized {
+ t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error())
+ }
+ }
+ {
+ std.TestSetOrigCaller(u1)
+ var badAddr std.Address
+ err := g.RemoveBackupOwner(badAddr)
+ if err != ErrInvalidAddress {
+ t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error())
+ }
+ }
+ {
+ std.TestSetOrigCaller(u1)
+ err := g.RemoveBackupOwner(u1)
+ if err != ErrCannotRemoveOwner {
+ t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error())
+ }
+ }
+ })
+ t.Run("ClaimOwnership", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ g.AddBackupOwner(u2)
+ // happy path
+ {
+ std.TestSetOrigCaller(u2)
+ err := g.ClaimOwnership()
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ if g.Owner() != u2 {
+ t.Errorf("expected %v, got %v", u2, g.Owner())
+ }
+ if !g.IsMember(u2) {
+ t.Errorf("expected %v to be a member", u2)
+ }
+ }
+ // running this twice should not error.
+ {
+ std.TestSetOrigCaller(u2)
+ err := g.ClaimOwnership()
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ }
+ // ensure checking for authorized caller
+ {
+ std.TestSetOrigCaller(u3)
+ err := g.ClaimOwnership()
+ if err != ErrNotMember {
+ t.Errorf("expected %v, got %v", ErrNotMember.Error(), err.Error())
+ }
+ }
+ })
+ t.Run("AddMember", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ // happy path
+ {
+ std.TestSetOrigCaller(u1)
+ err := g.AddMember(u2)
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ if !g.IsMember(u2) {
+ t.Errorf("expected %v to be a member", u2)
+ }
+ }
+ // ensure checking for authorized caller
+ {
+ std.TestSetOrigCaller(u2)
+ err := g.AddMember(u3)
+ if err != ownable.ErrUnauthorized {
+ t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error())
+ }
+ }
+ // ensure invalid address is caught
+ {
+ std.TestSetOrigCaller(u1)
+ var badAddr std.Address
+ err := g.AddMember(badAddr)
+ if err != ErrInvalidAddress {
+ t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error())
+ }
+ }
+ })
+ t.Run("RemoveMember", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ // happy path
+ {
+ std.TestSetOrigCaller(u1)
+ g.AddMember(u2)
+ err := g.RemoveMember(u2)
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ if g.IsMember(u2) {
+ t.Errorf("expected %v to not be a member", u2)
+ }
+ }
+ // running this twice should not error.
+ {
+ std.TestSetOrigCaller(u1)
+ err := g.RemoveMember(u2)
+ if err != nil {
+ t.Errorf("expected nil, got %v", err.Error())
+ }
+ }
+ // ensure checking for authorized caller
+ {
+ std.TestSetOrigCaller(u2)
+ err := g.RemoveMember(u3)
+ if err != ownable.ErrUnauthorized {
+ t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error())
+ }
+ }
+ // ensure invalid address is caught
+ {
+ std.TestSetOrigCaller(u1)
+ var badAddr std.Address
+ err := g.RemoveMember(badAddr)
+ if err != ErrInvalidAddress {
+ t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error())
+ }
+ }
+ // ensure owner cannot be removed
+ {
+ std.TestSetOrigCaller(u1)
+ err := g.RemoveMember(u1)
+ if err != ErrCannotRemoveOwner {
+ t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error())
+ }
+ }
+ })
+ t.Run("MemberCount", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ if g.MemberCount() != 1 {
+ t.Errorf("expected 0, got %v", g.MemberCount())
+ }
+ g.AddMember(u2)
+ if g.MemberCount() != 2 {
+ t.Errorf("expected 1, got %v", g.MemberCount())
+ }
+ g.AddMember(u3)
+ if g.MemberCount() != 3 {
+ t.Errorf("expected 2, got %v", g.MemberCount())
+ }
+ g.RemoveMember(u2)
+ if g.MemberCount() != 2 {
+ t.Errorf("expected 1, got %v", g.MemberCount())
+ }
+ })
+ t.Run("BackupOwnerCount", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ if g.BackupOwnerCount() != 1 {
+ t.Errorf("expected 0, got %v", g.BackupOwnerCount())
+ }
+ g.AddBackupOwner(u2)
+ if g.BackupOwnerCount() != 2 {
+ t.Errorf("expected 1, got %v", g.BackupOwnerCount())
+ }
+ g.AddBackupOwner(u3)
+ if g.BackupOwnerCount() != 3 {
+ t.Errorf("expected 2, got %v", g.BackupOwnerCount())
+ }
+ g.RemoveBackupOwner(u2)
+ if g.BackupOwnerCount() != 2 {
+ t.Errorf("expected 1, got %v", g.BackupOwnerCount())
+ }
+ })
+ t.Run("IsMember", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ if !g.IsMember(u1) {
+ t.Errorf("expected %v to be a member", u1)
+ }
+ if g.IsMember(u2) {
+ t.Errorf("expected %v to not be a member", u2)
+ }
+ g.AddMember(u2)
+ if !g.IsMember(u2) {
+ t.Errorf("expected %v to be a member", u2)
+ }
+ })
+ t.Run("IsBackupOwner", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ if !g.IsBackupOwner(u1) {
+ t.Errorf("expected %v to be a backup owner", u1)
+ }
+ if g.IsBackupOwner(u2) {
+ t.Errorf("expected %v to not be a backup owner", u2)
+ }
+ g.AddBackupOwner(u2)
+ if !g.IsBackupOwner(u2) {
+ t.Errorf("expected %v to be a backup owner", u2)
+ }
+ })
+ t.Run("Owner", func(t *testing.T) {
+ t.Parallel()
+ g := New(u1)
+ if g.Owner() != u1 {
+ t.Errorf("expected %v, got %v", u1, g.Owner())
+ }
+ g.AddBackupOwner(u2)
+ if g.Owner() != u1 {
+ t.Errorf("expected %v, got %v", u1, g.Owner())
+ }
+ std.TestSetOrigCaller(u2)
+ g.ClaimOwnership()
+ if g.Owner() != u2 {
+ t.Errorf("expected %v, got %v", u2, g.Owner())
+ }
+ })
+ t.Run("BackupOwners", func(t *testing.T) {
+ t.Parallel()
+ std.TestSetOrigCaller(u1)
+ g := New(u1)
+ g.AddBackupOwner(u2)
+ g.AddBackupOwner(u3)
+ owners := g.BackupOwners()
+ if len(owners) != 3 {
+ t.Errorf("expected 2, got %v", len(owners))
+ }
+ if owners[0] != u2.String() {
+ t.Errorf("expected %v, got %v", u2, owners[0])
+ }
+ if owners[1] != u3.String() {
+ t.Errorf("expected %v, got %v", u3, owners[1])
+ }
+ if owners[2] != u1.String() {
+ t.Errorf("expected %v, got %v", u3, owners[1])
+ }
+ })
+ t.Run("Members", func(t *testing.T) {
+ t.Parallel()
+ std.TestSetOrigCaller(u1)
+ g := New(u1)
+ g.AddMember(u2)
+ g.AddMember(u3)
+ members := g.Members()
+ if len(members) != 3 {
+ t.Errorf("expected 2, got %v", len(members))
+ }
+ if members[0] != u2.String() {
+ t.Errorf("expected %v, got %v", u2, members[0])
+ }
+ if members[1] != u3.String() {
+ t.Errorf("expected %v, got %v", u3, members[1])
+ }
+ if members[2] != u1.String() {
+ t.Errorf("expected %v, got %v", u3, members[1])
+ }
+ })
+}
+
+func TestSliceWithOffset(t *testing.T) {
+ t.Parallel()
+ testTable := []struct {
+ name string
+ slice []string
+ offset int
+ count int
+ expected []string
+ expectedCount int
+ }{
+ {
+ name: "empty",
+ slice: []string{},
+ offset: 0,
+ count: 0,
+ expected: []string{},
+ expectedCount: 0,
+ },
+ {
+ name: "single",
+ slice: []string{"a"},
+ offset: 0,
+ count: 1,
+ expected: []string{"a"},
+ expectedCount: 1,
+ },
+ {
+ name: "single offset",
+ slice: []string{"a"},
+ offset: 1,
+ count: 1,
+ expected: []string{},
+ expectedCount: 0,
+ },
+ {
+ name: "multiple",
+ slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
+ offset: 0,
+ count: 10,
+ expected: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
+ expectedCount: 10,
+ },
+ {
+ name: "multiple offset",
+ slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
+ offset: 5,
+ count: 5,
+ expected: []string{"f", "g", "h", "i", "j"},
+ expectedCount: 5,
+ },
+ {
+ name: "multiple offset end",
+ slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
+ offset: 10,
+ count: 5,
+ expected: []string{},
+ expectedCount: 0,
+ },
+ {
+ name: "multiple offset past end",
+ slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
+ offset: 11,
+ count: 5,
+ expected: []string{},
+ expectedCount: 0,
+ },
+ {
+ name: "multiple offset count past end",
+ slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
+ offset: 5,
+ count: 20,
+ expected: []string{"f", "g", "h", "i", "j"},
+ expectedCount: 5,
+ },
+ }
+ for _, test := range testTable {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+ tree := avl.NewTree()
+ for _, s := range test.slice {
+ tree.Set(s, struct{}{})
+ }
+ slice := sliceWithOffset(tree, test.offset, test.count)
+ if len(slice) != test.expectedCount {
+ t.Errorf("expected %v, got %v", test.expectedCount, len(slice))
+ }
+ })
+ }
+}
diff --git a/examples/gno.land/r/n2p5/config/config.gno b/examples/gno.land/r/n2p5/config/config.gno
new file mode 100644
index 00000000000..42cb587eaf5
--- /dev/null
+++ b/examples/gno.land/r/n2p5/config/config.gno
@@ -0,0 +1,120 @@
+package config
+
+import (
+ "std"
+
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/n2p5/mgroup"
+)
+
+const (
+ originalOwner = "g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t" // n2p5
+)
+
+var (
+ adminGroup = mgroup.New(originalOwner)
+ description = ""
+)
+
+// AddBackupOwner adds a backup owner to the Owner Group.
+// A backup owner can claim ownership of the contract.
+func AddBackupOwner(addr std.Address) {
+ err := adminGroup.AddBackupOwner(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// RemoveBackupOwner removes a backup owner from the Owner Group.
+// The primary owner cannot be removed.
+func RemoveBackupOwner(addr std.Address) {
+ err := adminGroup.RemoveBackupOwner(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// ClaimOwnership allows an authorized user in the ownerGroup
+// to claim ownership of the contract.
+func ClaimOwnership() {
+ err := adminGroup.ClaimOwnership()
+ if err != nil {
+ panic(err)
+ }
+}
+
+// AddAdmin adds an admin to the Admin Group.
+func AddAdmin(addr std.Address) {
+ err := adminGroup.AddMember(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// RemoveAdmin removes an admin from the Admin Group.
+// The primary owner cannot be removed.
+func RemoveAdmin(addr std.Address) {
+ err := adminGroup.RemoveMember(addr)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// Owner returns the current owner of the claims contract.
+func Owner() std.Address {
+ return adminGroup.Owner()
+}
+
+// BackupOwners returns the current backup owners of the claims contract.
+func BackupOwners() []string {
+ return adminGroup.BackupOwners()
+}
+
+// Admins returns the current admin members of the claims contract.
+func Admins() []string {
+ return adminGroup.Members()
+}
+
+// IsAdmin checks if an address is in the config adminGroup.
+func IsAdmin(addr std.Address) bool {
+ return adminGroup.IsMember(addr)
+}
+
+// toMarkdownList formats a slice of strings as a markdown list.
+func toMarkdownList(items []string) string {
+ var result string
+ for _, item := range items {
+ result += ufmt.Sprintf("- %s\n", item)
+ }
+ return result
+}
+
+func Render(path string) string {
+ owner := adminGroup.Owner().String()
+ backupOwners := toMarkdownList(BackupOwners())
+ adminMembers := toMarkdownList(Admins())
+ return ufmt.Sprintf(`
+# Config Dashboard
+
+This dashboard shows the current configuration owner, backup owners, and admin members.
+- The owner has the exclusive ability to manage the backup owners and admin members.
+- Backup owners can claim ownership of the contract and become the owner.
+- Admin members are used to authorize actions in other realms, such as [my home realm](/r/n2p5/home).
+
+#### Owner
+
+%s
+
+#### Backup Owners
+
+%s
+
+#### Admin Members
+
+%s
+
+`,
+ owner,
+ backupOwners,
+ adminMembers)
+}
diff --git a/examples/gno.land/r/n2p5/config/gno.mod b/examples/gno.land/r/n2p5/config/gno.mod
new file mode 100644
index 00000000000..33f9276a409
--- /dev/null
+++ b/examples/gno.land/r/n2p5/config/gno.mod
@@ -0,0 +1,6 @@
+module gno.land/r/n2p5/config
+
+require (
+ gno.land/p/demo/ufmt v0.0.0-latest
+ gno.land/p/n2p5/mgroup v0.0.0-latest
+)
diff --git a/examples/gno.land/r/n2p5/home/gno.mod b/examples/gno.land/r/n2p5/home/gno.mod
new file mode 100644
index 00000000000..779aa914989
--- /dev/null
+++ b/examples/gno.land/r/n2p5/home/gno.mod
@@ -0,0 +1,7 @@
+module gno.land/r/n2p5/home
+
+require (
+ gno.land/p/n2p5/chonk v0.0.0-latest
+ gno.land/r/leon/hof v0.0.0-latest
+ gno.land/r/n2p5/config v0.0.0-latest
+)
diff --git a/examples/gno.land/r/n2p5/home/home.gno b/examples/gno.land/r/n2p5/home/home.gno
new file mode 100644
index 00000000000..69b82e86d68
--- /dev/null
+++ b/examples/gno.land/r/n2p5/home/home.gno
@@ -0,0 +1,73 @@
+package home
+
+import (
+ "std"
+ "strings"
+
+ "gno.land/p/n2p5/chonk"
+
+ "gno.land/r/leon/hof"
+ "gno.land/r/n2p5/config"
+)
+
+var (
+ active = chonk.New()
+ preview = chonk.New()
+)
+
+func init() {
+ hof.Register()
+}
+
+// Add appends a string to the preview Chonk.
+func Add(chunk string) {
+ assertAdmin()
+ preview.Add(chunk)
+}
+
+// Flush clears the preview Chonk.
+func Flush() {
+ assertAdmin()
+ preview.Flush()
+}
+
+// Promote promotes the preview Chonk to the active Chonk
+// and creates a new preview Chonk.
+func Promote() {
+ assertAdmin()
+ active = preview
+ preview = chonk.New()
+}
+
+// Render returns the contents of the scanner for the active or preview Chonk
+// based on the path provided.
+func Render(path string) string {
+ var result string
+ scanner := getScanner(path)
+ for scanner.Scan() {
+ result += scanner.Text()
+ }
+ return result
+}
+
+// assertAdmin panics if the caller is not an admin as defined in the config realm.
+func assertAdmin() {
+ caller := std.PrevRealm().Addr()
+ if !config.IsAdmin(caller) {
+ panic("forbidden: must be admin")
+ }
+}
+
+// getScanner returns the scanner for the active or preview Chonk based
+// on the path provided.
+func getScanner(path string) *chonk.Scanner {
+ if isPreview(path) {
+ return preview.Scanner()
+ }
+ return active.Scanner()
+}
+
+// isPreview returns true if the path prefix is "preview".
+func isPreview(path string) bool {
+ return strings.HasPrefix(path, "preview")
+}
From 18e4eb9d7e19fed97b093697e12a6820fa14aaca Mon Sep 17 00:00:00 2001
From: Morgan
Date: Tue, 26 Nov 2024 15:19:51 +0100
Subject: [PATCH 04/86] feat(gnovm): test execution refactor + speedup (#3119)
This PR tackles a large amount of improvements in our current internal
testing systems for the gnovm, as well as those for `gno test`. The
primary target is the execution of filetests; however many improvements
have been implemented to remove dead or duplicate code, and bring over
some of the improvements that have been implemented in tests to
filetests, when they come at little added "cost".
The biggest headline concerns the execution of filetests. I wrote the
specific improvements undertaken in a [blog post on "diary of a
gnome"](https://gno.howl.moe/faster-tests/), but here's a side-by-side
comparison of the execution in this PR (left) and the execution on
master (right).

- Fixes #1084
- Fixes #2826 (by addressing root cause of slowness)
- Fixes #588, only running `_long` filetests on master and generally
speeding up test execution.
## Impact
- Test context (tests and filetests)
- Tests and filetests now share the same `test.Store` when running.
Coupled with `test.LoadImports`, it allows us to "eagerly load" imports
ahead of the test execution, and save it in the store. This is the
primary performance improvement of this PR, as all pure packages can be
run and preprocessed only once, whereas before it was once per package,
and once per filetest. (More info in the blog post.)
- One of the consequences of this is that package initialization happens
outside of the test; so a filetest can no longer check the output of a
`println` used in package initialization.
- The default user no longer has 200 gnot by default. There are two
mechanisms that make this unnecessary: `// SEND:`, and
`std.TestIssueCoin`. Some test balances had to be updated.
- Filetests
- Running filetests in `gno test -v` now also prints their output.
- Realm tests are no longer executed in `main.gno`, but in a filename
following the name of the filetest; this changed some `// Realm:`
directives.
- The sytem to read directives and update them in the filetests has been
re-written, and should now be more resilient and with fewer
"exceptions". This means that leading and trailing whitespace in output
now is correctly considered as output, leading to the addition of many
empty `//` in the tests.
- `// Error:` directives are now treated the same as everything else;
and are updated if the `-update-golden-tests` flag is passed.
- Removed the `imports` metric from the runtime metrics, as it's now no
longer straightforward to calculate (given that imports are loaded
"eagerly").
- Removed support for different "modes" of importing stdlibs; further
removing support for gonative (#1361). The remaining gonative libraries
are `os`, `fmt`, `encoding/json`, `internal/os_test` and `math/big`.
This removes the `-with-native-fallback` flag from `gno test`.
- Consequently, filetests ending with `_native.gno` have largely been
removed, and those ending with `_stdlibs.go` have had their suffix
removed.
- Some files testing `gonative` types and functions which were only used
in a small subset of these tests, have been removed.
- Tests
- `gno test`, for testing packages, created a `main_test.gno` file that
is then called directly from the machine on each test. This creates two
identifiers, `tests` and `runtest`, which could come into conflict with
those defined by the tests themselves. We now call `testing.RunTest`
directly, without an intermediary.
- Exports in the normal tests (ie. defined in `pkg`) are now visible in
the tests of `pkg_test`. This is most useful for some standard library
tests, where there often is an `export_test.go` with the sole scope of
exporting some internal functions and methods for testing in the "real"
testing file.
- `gno lint`, and other occasions where we use `issueFromError` (like
when parsing errors in `gno test`), will now also print the column of
the error if given.
## Summary of internal changes
- pkg/gnolang
- `TestFiles` is the new function running the filetests.
- `eval_test.go` has been removed, moving some of its improvements over
to `TestFiles`, and reducing the scope of the corresponding tests in
`debugger.go`, to what it's actually supposed to test for.
- As a consequence of removing all of type mappings in `imports.go`, I
removed `SetStrictGo2GnoMapping` in favour of always being "strict", and
not allowing defining native types of defined types any more.
- The tests in `gonative_test` where primarily related to this, so I
removed them. Similarly for `preprocess_test`.
- `TestFunc` / `TestMemPackage` have been removed as redundant,
including some of their features in `cmd/go/test.go` (like supporting
exporting symbols in `_test.gno` files)
- The `Store` no longer has `ClearCache` and `SetStrictGo2GnoMapping`;
`ClearCache` now has a better substitute in `BeginTransaction`.
- the `testing` stdlib no longer caches `matchPat` and `matchString` as
pure packages should not be able to modify their values during
execution, and as a result of other changes this was failing.
- The tests/ directory has been removed / moved to `gnovm/pkg/test`.
This directory should eventually contain the internal code for `gno
test` as well; but for now I wanted to clean `tests` to ensure it is a
directory just for integration tests, rather than code and testing
systems.
- `TestMachine` and `TestStore` have been renamed to Machine and Store,
to avoid redundancy with the `test` package name.
- Removed plenty instructions in `gnovm/Makefile` as now most tests are
just in `pkg/gnolang`.
- I removed `MsgContext.Msg` as unused. It can be re-added later if
necessary.
- In the CI, tests are now run with `-short`, at least until we figure
out how to make the VM efficient enough to run these tests. Aside from
some filetests, this now excludes some stdlibs: `bytes`, `strconv`,
`regexp/syntax`. I accept other proposals that could make us have fast
tests on PR's, while still testing (mostly) everything.
---
[](https://lu.ma/open-source-saturday-torino)
---------
Co-authored-by: Marc Vertes
---
.github/workflows/examples.yml | 6 +-
.github/workflows/gnovm.yml | 2 +
.../gno.land/p/demo/avl/pager/z_filetest.gno | 1 +
examples/gno.land/p/demo/avl/z_0_filetest.gno | 8 +-
examples/gno.land/p/demo/avl/z_1_filetest.gno | 8 +-
.../p/demo/tamagotchi/z0_filetest.gno | 1 +
.../gno.land/r/demo/banktest/z_0_filetest.gno | 4 +-
.../gno.land/r/demo/banktest/z_2_filetest.gno | 4 +-
.../gno.land/r/demo/boards/z_0_filetest.gno | 2 +
.../r/demo/boards/z_10_c_filetest.gno | 1 +
.../r/demo/boards/z_11_d_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_11_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_12_filetest.gno | 2 +
.../gno.land/r/demo/boards/z_1_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_2_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_3_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_4_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_5_c_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_5_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_6_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_7_filetest.gno | 2 +
.../gno.land/r/demo/boards/z_8_filetest.gno | 1 +
.../gno.land/r/demo/boards/z_9_filetest.gno | 1 +
.../gno.land/r/demo/disperse/z_0_filetest.gno | 4 +-
.../gno.land/r/demo/disperse/z_1_filetest.gno | 4 +-
.../gno.land/r/demo/groups/z_0_c_filetest.gno | 1 +
.../gno.land/r/demo/groups/z_1_a_filetest.gno | 2 +
.../gno.land/r/demo/groups/z_2_a_filetest.gno | 2 +
.../gno.land/r/demo/groups/z_2_e_filetest.gno | 2 +
.../releases_example/releases0_filetest.gno | 1 +
.../r/demo/tamagotchi/z0_filetest.gno | 1 +
.../gno.land/r/demo/users/z_5_filetest.gno | 2 +-
.../gno.land/r/demo/wugnot/z0_filetest.gno | 6 +-
.../gno.land/r/gnoland/faucet/faucet_test.gno | 6 +-
.../gno.land/r/gnoland/faucet/z0_filetest.gno | 2 +
.../gno.land/r/gnoland/faucet/z1_filetest.gno | 2 +
.../gno.land/r/gnoland/faucet/z2_filetest.gno | 2 +
.../gno.land/r/gnoland/faucet/z3_filetest.gno | 2 +
.../gno.land/r/gov/dao/v2/prop1_filetest.gno | 1 +
.../gno.land/r/gov/dao/v2/prop4_filetest.gno | 2 +
examples/gno.land/r/moul/home/z1_filetest.gno | 2 +
examples/gno.land/r/moul/home/z2_filetest.gno | 2 +
gno.land/pkg/sdk/vm/keeper.go | 5 -
gnovm/Makefile | 20 +-
gnovm/cmd/gno/lint.go | 16 +-
gnovm/cmd/gno/lint_test.go | 8 +-
gnovm/cmd/gno/run.go | 10 +-
gnovm/cmd/gno/run_test.go | 2 +-
gnovm/cmd/gno/test.go | 497 ++----------
.../gno/testdata/gno_lint/bad_import.txtar | 2 +-
.../gno/testdata/gno_lint/file_error.txtar | 2 +-
.../gno/testdata/gno_lint/not_declared.txtar | 2 +-
.../gno/testdata/gno_test/error_correct.txtar | 1 -
.../testdata/gno_test/error_incorrect.txtar | 5 +-
.../gno/testdata/gno_test/error_sync.txtar | 7 +-
.../testdata/gno_test/failing_filetest.txtar | 3 +-
.../testdata/gno_test/filetest_events.txtar | 2 +-
.../gno_test/flag_print-runtime-metrics.txtar | 3 +-
.../testdata/gno_test/output_correct.txtar | 3 +-
.../testdata/gno_test/output_incorrect.txtar | 7 +-
.../gno/testdata/gno_test/output_sync.txtar | 6 +-
.../gno_test/pkg_underscore_test.txtar | 3 +-
.../gno/testdata/gno_test/realm_correct.txtar | 23 +-
.../testdata/gno_test/realm_incorrect.txtar | 12 +-
.../gno/testdata/gno_test/realm_sync.txtar | 28 +-
.../gno_test/test_with-native-fallback.txtar | 32 -
.../gno/testdata/gno_test/unknow_lib.txtar | 32 -
.../testdata/gno_test/unknown_package.txtar | 24 +
.../testdata/gno_test/valid_filetest.txtar | 2 +-
.../gobuild_flag_build_error.txtar | 5 +-
gnovm/cmd/gno/util.go | 14 -
gnovm/pkg/gnolang/debugger_test.go | 33 +-
gnovm/pkg/gnolang/eval_test.go | 132 ----
gnovm/pkg/gnolang/files_test.go | 141 ++++
gnovm/pkg/gnolang/gonative.go | 84 +--
gnovm/pkg/gnolang/gonative_test.go | 149 ----
gnovm/pkg/gnolang/machine.go | 157 +---
gnovm/pkg/gnolang/nodes.go | 33 -
gnovm/pkg/gnolang/preprocess.go | 27 +-
gnovm/pkg/gnolang/preprocess_test.go | 62 --
gnovm/pkg/gnolang/store.go | 25 +-
gnovm/pkg/gnolang/store_test.go | 2 -
gnovm/pkg/repl/repl.go | 6 +-
gnovm/pkg/test/filetest.go | 407 ++++++++++
gnovm/pkg/test/imports.go | 265 +++++++
gnovm/pkg/test/test.go | 483 ++++++++++++
gnovm/{cmd/gno => pkg/test}/util_match.go | 2 +-
gnovm/stdlibs/io/example_test.gno | 37 +-
gnovm/stdlibs/io/multi_test.gno | 12 +-
gnovm/stdlibs/std/context.go | 1 -
gnovm/stdlibs/strconv/example_test.gno | 3 +-
gnovm/stdlibs/testing/match.gno | 37 +-
gnovm/stdlibs/testing/testing.gno | 2 +-
gnovm/tests/README.md | 36 +-
gnovm/tests/file.go | 713 ------------------
gnovm/tests/file_test.go | 144 ----
.../{access0_stdlibs.gno => access0.gno} | 0
.../{access1_stdlibs.gno => access1.gno} | 2 +-
.../{access2_stdlibs.gno => access2.gno} | 0
.../{access3_stdlibs.gno => access3.gno} | 0
.../{access4_stdlibs.gno => access4.gno} | 2 +-
.../{access5_stdlibs.gno => access5.gno} | 0
.../{access6_stdlibs.gno => access6.gno} | 2 +-
.../{access7_stdlibs.gno => access7.gno} | 2 +-
.../files/{addr0b_stdlibs.gno => addr0b.gno} | 0
gnovm/tests/files/addr0b_native.gno | 25 -
gnovm/tests/files/addr2b.gno | 10 +-
.../{assign0b_stdlibs.gno => assign0b.gno} | 0
gnovm/tests/files/assign0b_native.gno | 19 -
... => cross_realm_compositelit_filetest.gno} | 0
.../more/realm_compositelit_filetest.gno | 4 +-
gnovm/tests/files/bin1.gno | 8 +-
gnovm/tests/files/bin5.gno | 15 -
gnovm/tests/files/binstruct_ptr_map0.gno | 7 +-
gnovm/tests/files/binstruct_ptr_slice0.gno | 17 -
gnovm/tests/files/binstruct_slice0.gno | 7 +-
gnovm/tests/files/composite11.gno | 7 +-
gnovm/tests/files/const14.gno | 8 +-
gnovm/tests/files/const22.gno | 2 +-
gnovm/tests/files/context.gno | 19 -
gnovm/tests/files/context2.gno | 22 -
gnovm/tests/files/defer4.gno | 23 -
gnovm/tests/files/extern/p1/s1.gno | 5 -
.../timtadh/data_structures/types/string.gno | 18 +-
.../files/{float5_stdlibs.gno => float5.gno} | 0
gnovm/tests/files/fun6.gno | 21 -
gnovm/tests/files/fun6b.gno | 20 -
gnovm/tests/files/fun7.gno | 18 -
gnovm/tests/files/heap_alloc_forloop9_1.gno | 2 +-
gnovm/tests/files/heap_item_value.gno | 4 +-
gnovm/tests/files/heap_item_value_init.gno | 8 +-
gnovm/tests/files/import3.gno | 5 +-
gnovm/tests/files/import5.gno | 2 -
gnovm/tests/files/interp.gi | 13 -
gnovm/tests/files/interp2.gi | 16 -
.../tests/files/{io0_stdlibs.gno => io0.gno} | 0
gnovm/tests/files/io0_native.gno | 18 -
gnovm/tests/files/io2.gno | 5 +-
...{issue_558b_stdlibs.gno => issue_558b.gno} | 4 +-
gnovm/tests/files/issue_782.gno | 2 +-
gnovm/tests/files/l3_long.gno | 163 ----
gnovm/tests/files/l4_long.gno | 7 -
gnovm/tests/files/l5_long.gno | 164 ----
gnovm/tests/files/{l2_long.gno => loop0.gno} | 0
gnovm/tests/files/loop1.gno | 51 ++
gnovm/tests/files/map27.gno | 3 +-
.../files/{map29_stdlibs.gno => map29.gno} | 0
gnovm/tests/files/map29_native.gno | 26 -
.../files/{math0_stdlibs.gno => math0.gno} | 0
gnovm/tests/files/math3.gno | 25 +-
.../files/{math_native.gno => math5.gno} | 0
gnovm/tests/files/method16b.gno | 2 +-
gnovm/tests/files/method18.gno | 29 -
gnovm/tests/files/method20.gno | 7 +-
gnovm/tests/files/method24.gno | 33 -
gnovm/tests/files/method25.gno | 33 -
gnovm/tests/files/op0.gno | 2 +-
gnovm/tests/files/print0.gno | 1 +
gnovm/tests/files/sample.plugin | 19 -
gnovm/tests/files/secure.gi | 33 -
.../files/{std0_stdlibs.gno => std0.gno} | 0
.../files/{std10_stdlibs.gno => std10.gno} | 0
.../files/{std11_stdlibs.gno => std11.gno} | 0
.../files/{std2_stdlibs.gno => std2.gno} | 0
.../files/{std3_stdlibs.gno => std3.gno} | 0
.../files/{std4_stdlibs.gno => std4.gno} | 0
.../files/{std5_stdlibs.gno => std5.gno} | 2 +-
.../files/{std6_stdlibs.gno => std6.gno} | 0
.../files/{std7_stdlibs.gno => std7.gno} | 0
.../files/{std8_stdlibs.gno => std8.gno} | 4 +-
.../files/{std9_stdlibs.gno => std9.gno} | 0
.../{stdbanker_stdlibs.gno => stdbanker.gno} | 0
.../{stdlibs_stdlibs.gno => stdlibs.gno} | 0
.../{struct13_stdlibs.gno => struct13.gno} | 0
gnovm/tests/files/struct13_native.gno | 19 -
gnovm/tests/files/switch21.gno | 2 +-
.../files/{time0_stdlibs.gno => time0.gno} | 0
gnovm/tests/files/time0_native.gno | 13 -
.../files/{time1_stdlibs.gno => time1.gno} | 0
.../files/{time11_stdlibs.gno => time11.gno} | 0
gnovm/tests/files/time11_native.gno | 15 -
.../files/{time12_stdlibs.gno => time12.gno} | 0
gnovm/tests/files/time12_native.gno | 15 -
.../files/{time13_stdlibs.gno => time13.gno} | 0
gnovm/tests/files/time13_native.gno | 18 -
.../files/{time14_stdlibs.gno => time14.gno} | 0
gnovm/tests/files/time14_native.gno | 20 -
gnovm/tests/files/time16_native.gno | 14 -
gnovm/tests/files/time17_native.gno | 20 -
gnovm/tests/files/time1_native.gno | 15 -
.../files/{time2_stdlibs.gno => time2.gno} | 0
gnovm/tests/files/time2_native.gno | 15 -
.../files/{time3_stdlibs.gno => time3.gno} | 0
gnovm/tests/files/time3_native.gno | 15 -
.../files/{time4_stdlibs.gno => time4.gno} | 0
gnovm/tests/files/time4_native.gno | 15 -
.../files/{time6_stdlibs.gno => time6.gno} | 0
gnovm/tests/files/time6_native.gno | 16 -
.../files/{time7_stdlibs.gno => time7.gno} | 0
gnovm/tests/files/time7_native.gno | 13 -
.../files/{time9_stdlibs.gno => time9.gno} | 0
gnovm/tests/files/time9_native.gno | 13 -
gnovm/tests/files/type11.gno | 11 +-
.../files/{type2_stdlibs.gno => type2.gno} | 0
gnovm/tests/files/type2_native.gno | 24 -
...typeassert7_native.gno => typeassert7.gno} | 0
...peassert7a_native.gno => typeassert7a.gno} | 2 +-
...ssign_f0_stdlibs.gno => add_assign_f0.gno} | 2 +-
...ssign_f1_stdlibs.gno => add_assign_f1.gno} | 2 +-
...ssign_f2_stdlibs.gno => add_assign_f2.gno} | 2 +-
.../types/{add_f0_stdlibs.gno => add_f0.gno} | 2 +-
.../types/{add_f1_stdlibs.gno => add_f1.gno} | 2 +-
.../types/{and_f0_stdlibs.gno => and_f0.gno} | 2 +-
.../types/{and_f1_stdlibs.gno => and_f1.gno} | 2 +-
...mp_iface_0_stdlibs.gno => cmp_iface_0.gno} | 0
...mp_iface_3_stdlibs.gno => cmp_iface_3.gno} | 0
.../{eql_0f8_stdlibs.gno => cmp_iface_5.gno} | 2 +-
.../types/{eql_0b4_native.gno => eql_0b4.gno} | 2 +-
gnovm/tests/files/types/eql_0b4_stdlibs.gno | 13 -
.../types/{eql_0f0_native.gno => eql_0f0.gno} | 2 +-
gnovm/tests/files/types/eql_0f0_stdlibs.gno | 28 -
.../{eql_0f1_stdlibs.gno => eql_0f1.gno} | 2 +-
.../{eql_0f27_stdlibs.gno => eql_0f27.gno} | 2 +-
.../{eql_0f2b_native.gno => eql_0f2b.gno} | 2 +-
gnovm/tests/files/types/eql_0f2b_stdlibs.gno | 28 -
.../{eql_0f2c_native.gno => eql_0f2c.gno} | 2 +-
gnovm/tests/files/types/eql_0f2c_stdlibs.gno | 28 -
.../{eql_0f40_stdlibs.gno => eql_0f40.gno} | 0
.../{eql_0f41_stdlibs.gno => eql_0f41.gno} | 2 +-
.../{cmp_iface_5_stdlibs.gno => eql_0f8.gno} | 2 +-
.../files/types/explicit_conversion_0.gno | 2 +-
.../files/types/explicit_conversion_1.gno | 2 +-
.../files/types/explicit_conversion_2.gno | 2 +-
.../types/{or_f0_stdlibs.gno => or_f0.gno} | 2 +-
.../types/{or_f1_stdlibs.gno => or_f1.gno} | 2 +-
gnovm/tests/files/types/shift_b0.gno | 2 +-
gnovm/tests/files/types/shift_b1.gno | 2 +-
gnovm/tests/files/types/shift_b10.gno | 2 +-
gnovm/tests/files/types/shift_b11.gno | 2 +-
gnovm/tests/files/types/shift_b2.gno | 2 +-
gnovm/tests/files/types/shift_b3.gno | 2 +-
gnovm/tests/files/types/shift_b4.gno | 2 +-
gnovm/tests/files/types/shift_b5.gno | 2 +-
gnovm/tests/files/types/shift_b6.gno | 2 +-
gnovm/tests/files/types/shift_b6a.gno | 2 +-
gnovm/tests/files/types/shift_b7.gno | 2 +-
gnovm/tests/files/types/shift_b8.gno | 2 +-
gnovm/tests/files/types/shift_b9.gno | 2 +-
gnovm/tests/files/types/shift_c3.gno | 2 +-
gnovm/tests/files/types/shift_c4.gno | 2 +-
gnovm/tests/files/types/shift_c6.gno | 2 +-
gnovm/tests/files/types/shift_c7.gno | 2 +-
gnovm/tests/files/types/shift_c8.gno | 2 +-
gnovm/tests/files/types/shift_c9.gno | 2 +-
gnovm/tests/files/types/shift_d12.gno | 2 +-
gnovm/tests/files/types/shift_d13.gno | 2 +-
gnovm/tests/files/types/shift_d29.gno | 2 +-
gnovm/tests/files/types/shift_d30.gno | 2 +-
gnovm/tests/files/types/shift_d32.gno | 2 +-
gnovm/tests/files/types/shift_d32a.gno | 2 +-
gnovm/tests/files/types/shift_d33.gno | 2 +-
gnovm/tests/files/types/shift_d34.gno | 2 +-
gnovm/tests/files/types/shift_d35.gno | 2 +-
gnovm/tests/files/types/shift_d39.gno | 2 +-
gnovm/tests/files/types/shift_d50.gno | 2 +-
gnovm/tests/files/types/shift_d53.gno | 2 +-
gnovm/tests/files/types/shift_d54.gno | 2 +-
gnovm/tests/files/types/shift_d55.gno | 2 +-
gnovm/tests/files/types/shift_d56.gno | 2 +-
gnovm/tests/files/types/shift_f5.gno | 2 +-
gnovm/tests/files/types/time_native.gno | 13 -
gnovm/tests/files/zrealm0.gno | 4 +-
gnovm/tests/files/zrealm1.gno | 4 +-
gnovm/tests/files/zrealm12_stdlibs.gno | 29 -
gnovm/tests/files/zrealm2.gno | 8 +-
gnovm/tests/files/zrealm3.gno | 8 +-
gnovm/tests/files/zrealm4.gno | 8 +-
gnovm/tests/files/zrealm5.gno | 8 +-
gnovm/tests/files/zrealm6.gno | 8 +-
gnovm/tests/files/zrealm7.gno | 8 +-
gnovm/tests/files/zrealm_avl0.gno | 8 +-
gnovm/tests/files/zrealm_avl1.gno | 8 +-
...alm_const_stdlibs.gno => zrealm_const.gno} | 0
...lm0_stdlibs.gno => zrealm_crossrealm0.gno} | 0
...lm1_stdlibs.gno => zrealm_crossrealm1.gno} | 0
...10_stdlibs.gno => zrealm_crossrealm10.gno} | 0
...11_stdlibs.gno => zrealm_crossrealm11.gno} | 0
...12_stdlibs.gno => zrealm_crossrealm12.gno} | 0
...13_stdlibs.gno => zrealm_crossrealm13.gno} | 0
...a_stdlibs.gno => zrealm_crossrealm13a.gno} | 0
...lm2_stdlibs.gno => zrealm_crossrealm2.gno} | 0
...lm3_stdlibs.gno => zrealm_crossrealm3.gno} | 0
...lm4_stdlibs.gno => zrealm_crossrealm4.gno} | 0
...lm5_stdlibs.gno => zrealm_crossrealm5.gno} | 0
...lm6_stdlibs.gno => zrealm_crossrealm6.gno} | 0
...lm7_stdlibs.gno => zrealm_crossrealm7.gno} | 0
...lm8_stdlibs.gno => zrealm_crossrealm8.gno} | 0
...lm9_stdlibs.gno => zrealm_crossrealm9.gno} | 0
...initctx_stdlibs.gno => zrealm_initctx.gno} | 0
...tbind0_stdlibs.gno => zrealm_natbind0.gno} | 8 +-
gnovm/tests/files/zrealm_panic.gno | 7 +-
...realm_std0_stdlibs.gno => zrealm_std0.gno} | 0
...realm_std1_stdlibs.gno => zrealm_std1.gno} | 0
...realm_std2_stdlibs.gno => zrealm_std2.gno} | 0
...realm_std3_stdlibs.gno => zrealm_std3.gno} | 0
...realm_std4_stdlibs.gno => zrealm_std4.gno} | 0
...realm_std5_stdlibs.gno => zrealm_std5.gno} | 0
...realm_std6_stdlibs.gno => zrealm_std6.gno} | 0
...m_tests0_stdlibs.gno => zrealm_tests0.gno} | 3 +
...ils0_stdlibs.gno => zrealm_testutils0.gno} | 0
.../{zregexp_stdlibs.gno => zregexp.gno} | 0
gnovm/tests/imports.go | 492 ------------
gnovm/tests/machine_test.go | 65 --
gnovm/tests/package_test.go | 90 ---
gnovm/tests/selector_test.go | 174 -----
gnovm/tests/stdlibs/generated.go | 12 -
gnovm/tests/stdlibs/std/std.gno | 1 -
gnovm/tests/stdlibs/std/std.go | 85 ++-
.../TestMemPackage/fail/file_test.gno | 7 -
.../TestMemPackage/success/file_test.gno | 5 -
320 files changed, 1928 insertions(+), 4452 deletions(-)
delete mode 100644 gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar
delete mode 100644 gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar
create mode 100644 gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar
delete mode 100644 gnovm/pkg/gnolang/eval_test.go
create mode 100644 gnovm/pkg/gnolang/files_test.go
delete mode 100644 gnovm/pkg/gnolang/gonative_test.go
delete mode 100644 gnovm/pkg/gnolang/preprocess_test.go
create mode 100644 gnovm/pkg/test/filetest.go
create mode 100644 gnovm/pkg/test/imports.go
create mode 100644 gnovm/pkg/test/test.go
rename gnovm/{cmd/gno => pkg/test}/util_match.go (99%)
delete mode 100644 gnovm/tests/file.go
delete mode 100644 gnovm/tests/file_test.go
rename gnovm/tests/files/{access0_stdlibs.gno => access0.gno} (100%)
rename gnovm/tests/files/{access1_stdlibs.gno => access1.gno} (52%)
rename gnovm/tests/files/{access2_stdlibs.gno => access2.gno} (100%)
rename gnovm/tests/files/{access3_stdlibs.gno => access3.gno} (100%)
rename gnovm/tests/files/{access4_stdlibs.gno => access4.gno} (56%)
rename gnovm/tests/files/{access5_stdlibs.gno => access5.gno} (100%)
rename gnovm/tests/files/{access6_stdlibs.gno => access6.gno} (61%)
rename gnovm/tests/files/{access7_stdlibs.gno => access7.gno} (67%)
rename gnovm/tests/files/{addr0b_stdlibs.gno => addr0b.gno} (100%)
delete mode 100644 gnovm/tests/files/addr0b_native.gno
rename gnovm/tests/files/{assign0b_stdlibs.gno => assign0b.gno} (100%)
delete mode 100644 gnovm/tests/files/assign0b_native.gno
rename gnovm/tests/files/assign_unnamed_type/more/{cross_realm_compositelit_filetest_stdlibs.gno => cross_realm_compositelit_filetest.gno} (100%)
delete mode 100644 gnovm/tests/files/bin5.gno
delete mode 100644 gnovm/tests/files/binstruct_ptr_slice0.gno
delete mode 100644 gnovm/tests/files/context.gno
delete mode 100644 gnovm/tests/files/context2.gno
delete mode 100644 gnovm/tests/files/defer4.gno
delete mode 100644 gnovm/tests/files/extern/p1/s1.gno
rename gnovm/tests/files/{float5_stdlibs.gno => float5.gno} (100%)
delete mode 100644 gnovm/tests/files/fun6.gno
delete mode 100644 gnovm/tests/files/fun6b.gno
delete mode 100644 gnovm/tests/files/fun7.gno
delete mode 100644 gnovm/tests/files/interp.gi
delete mode 100644 gnovm/tests/files/interp2.gi
rename gnovm/tests/files/{io0_stdlibs.gno => io0.gno} (100%)
delete mode 100644 gnovm/tests/files/io0_native.gno
rename gnovm/tests/files/{issue_558b_stdlibs.gno => issue_558b.gno} (97%)
delete mode 100644 gnovm/tests/files/l3_long.gno
delete mode 100644 gnovm/tests/files/l4_long.gno
delete mode 100644 gnovm/tests/files/l5_long.gno
rename gnovm/tests/files/{l2_long.gno => loop0.gno} (100%)
create mode 100644 gnovm/tests/files/loop1.gno
rename gnovm/tests/files/{map29_stdlibs.gno => map29.gno} (100%)
delete mode 100644 gnovm/tests/files/map29_native.gno
rename gnovm/tests/files/{math0_stdlibs.gno => math0.gno} (100%)
rename gnovm/tests/files/{math_native.gno => math5.gno} (100%)
delete mode 100644 gnovm/tests/files/method18.gno
delete mode 100644 gnovm/tests/files/method24.gno
delete mode 100644 gnovm/tests/files/method25.gno
delete mode 100644 gnovm/tests/files/sample.plugin
delete mode 100644 gnovm/tests/files/secure.gi
rename gnovm/tests/files/{std0_stdlibs.gno => std0.gno} (100%)
rename gnovm/tests/files/{std10_stdlibs.gno => std10.gno} (100%)
rename gnovm/tests/files/{std11_stdlibs.gno => std11.gno} (100%)
rename gnovm/tests/files/{std2_stdlibs.gno => std2.gno} (100%)
rename gnovm/tests/files/{std3_stdlibs.gno => std3.gno} (100%)
rename gnovm/tests/files/{std4_stdlibs.gno => std4.gno} (100%)
rename gnovm/tests/files/{std5_stdlibs.gno => std5.gno} (90%)
rename gnovm/tests/files/{std6_stdlibs.gno => std6.gno} (100%)
rename gnovm/tests/files/{std7_stdlibs.gno => std7.gno} (100%)
rename gnovm/tests/files/{std8_stdlibs.gno => std8.gno} (89%)
rename gnovm/tests/files/{std9_stdlibs.gno => std9.gno} (100%)
rename gnovm/tests/files/{stdbanker_stdlibs.gno => stdbanker.gno} (100%)
rename gnovm/tests/files/{stdlibs_stdlibs.gno => stdlibs.gno} (100%)
rename gnovm/tests/files/{struct13_stdlibs.gno => struct13.gno} (100%)
delete mode 100644 gnovm/tests/files/struct13_native.gno
rename gnovm/tests/files/{time0_stdlibs.gno => time0.gno} (100%)
delete mode 100644 gnovm/tests/files/time0_native.gno
rename gnovm/tests/files/{time1_stdlibs.gno => time1.gno} (100%)
rename gnovm/tests/files/{time11_stdlibs.gno => time11.gno} (100%)
delete mode 100644 gnovm/tests/files/time11_native.gno
rename gnovm/tests/files/{time12_stdlibs.gno => time12.gno} (100%)
delete mode 100644 gnovm/tests/files/time12_native.gno
rename gnovm/tests/files/{time13_stdlibs.gno => time13.gno} (100%)
delete mode 100644 gnovm/tests/files/time13_native.gno
rename gnovm/tests/files/{time14_stdlibs.gno => time14.gno} (100%)
delete mode 100644 gnovm/tests/files/time14_native.gno
delete mode 100644 gnovm/tests/files/time16_native.gno
delete mode 100644 gnovm/tests/files/time17_native.gno
delete mode 100644 gnovm/tests/files/time1_native.gno
rename gnovm/tests/files/{time2_stdlibs.gno => time2.gno} (100%)
delete mode 100644 gnovm/tests/files/time2_native.gno
rename gnovm/tests/files/{time3_stdlibs.gno => time3.gno} (100%)
delete mode 100644 gnovm/tests/files/time3_native.gno
rename gnovm/tests/files/{time4_stdlibs.gno => time4.gno} (100%)
delete mode 100644 gnovm/tests/files/time4_native.gno
rename gnovm/tests/files/{time6_stdlibs.gno => time6.gno} (100%)
delete mode 100644 gnovm/tests/files/time6_native.gno
rename gnovm/tests/files/{time7_stdlibs.gno => time7.gno} (100%)
delete mode 100644 gnovm/tests/files/time7_native.gno
rename gnovm/tests/files/{time9_stdlibs.gno => time9.gno} (100%)
delete mode 100644 gnovm/tests/files/time9_native.gno
rename gnovm/tests/files/{type2_stdlibs.gno => type2.gno} (100%)
delete mode 100644 gnovm/tests/files/type2_native.gno
rename gnovm/tests/files/{typeassert7_native.gno => typeassert7.gno} (100%)
rename gnovm/tests/files/{typeassert7a_native.gno => typeassert7a.gno} (87%)
rename gnovm/tests/files/types/{add_assign_f0_stdlibs.gno => add_assign_f0.gno} (71%)
rename gnovm/tests/files/types/{add_assign_f1_stdlibs.gno => add_assign_f1.gno} (81%)
rename gnovm/tests/files/types/{add_assign_f2_stdlibs.gno => add_assign_f2.gno} (75%)
rename gnovm/tests/files/types/{add_f0_stdlibs.gno => add_f0.gno} (74%)
rename gnovm/tests/files/types/{add_f1_stdlibs.gno => add_f1.gno} (75%)
rename gnovm/tests/files/types/{and_f0_stdlibs.gno => and_f0.gno} (74%)
rename gnovm/tests/files/types/{and_f1_stdlibs.gno => and_f1.gno} (75%)
rename gnovm/tests/files/types/{cmp_iface_0_stdlibs.gno => cmp_iface_0.gno} (100%)
rename gnovm/tests/files/types/{cmp_iface_3_stdlibs.gno => cmp_iface_3.gno} (100%)
rename gnovm/tests/files/types/{eql_0f8_stdlibs.gno => cmp_iface_5.gno} (75%)
rename gnovm/tests/files/types/{eql_0b4_native.gno => eql_0b4.gno} (50%)
delete mode 100644 gnovm/tests/files/types/eql_0b4_stdlibs.gno
rename gnovm/tests/files/types/{eql_0f0_native.gno => eql_0f0.gno} (75%)
delete mode 100644 gnovm/tests/files/types/eql_0f0_stdlibs.gno
rename gnovm/tests/files/types/{eql_0f1_stdlibs.gno => eql_0f1.gno} (76%)
rename gnovm/tests/files/types/{eql_0f27_stdlibs.gno => eql_0f27.gno} (75%)
rename gnovm/tests/files/types/{eql_0f2b_native.gno => eql_0f2b.gno} (80%)
delete mode 100644 gnovm/tests/files/types/eql_0f2b_stdlibs.gno
rename gnovm/tests/files/types/{eql_0f2c_native.gno => eql_0f2c.gno} (80%)
delete mode 100644 gnovm/tests/files/types/eql_0f2c_stdlibs.gno
rename gnovm/tests/files/types/{eql_0f40_stdlibs.gno => eql_0f40.gno} (100%)
rename gnovm/tests/files/types/{eql_0f41_stdlibs.gno => eql_0f41.gno} (77%)
rename gnovm/tests/files/types/{cmp_iface_5_stdlibs.gno => eql_0f8.gno} (75%)
rename gnovm/tests/files/types/{or_f0_stdlibs.gno => or_f0.gno} (75%)
rename gnovm/tests/files/types/{or_f1_stdlibs.gno => or_f1.gno} (75%)
delete mode 100644 gnovm/tests/files/types/time_native.gno
delete mode 100644 gnovm/tests/files/zrealm12_stdlibs.gno
rename gnovm/tests/files/{zrealm_const_stdlibs.gno => zrealm_const.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm0_stdlibs.gno => zrealm_crossrealm0.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm1_stdlibs.gno => zrealm_crossrealm1.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm10_stdlibs.gno => zrealm_crossrealm10.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm11_stdlibs.gno => zrealm_crossrealm11.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm12_stdlibs.gno => zrealm_crossrealm12.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm13_stdlibs.gno => zrealm_crossrealm13.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm13a_stdlibs.gno => zrealm_crossrealm13a.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm2_stdlibs.gno => zrealm_crossrealm2.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm3_stdlibs.gno => zrealm_crossrealm3.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm4_stdlibs.gno => zrealm_crossrealm4.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm5_stdlibs.gno => zrealm_crossrealm5.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm6_stdlibs.gno => zrealm_crossrealm6.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm7_stdlibs.gno => zrealm_crossrealm7.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm8_stdlibs.gno => zrealm_crossrealm8.gno} (100%)
rename gnovm/tests/files/{zrealm_crossrealm9_stdlibs.gno => zrealm_crossrealm9.gno} (100%)
rename gnovm/tests/files/{zrealm_initctx_stdlibs.gno => zrealm_initctx.gno} (100%)
rename gnovm/tests/files/{zrealm_natbind0_stdlibs.gno => zrealm_natbind0.gno} (95%)
rename gnovm/tests/files/{zrealm_std0_stdlibs.gno => zrealm_std0.gno} (100%)
rename gnovm/tests/files/{zrealm_std1_stdlibs.gno => zrealm_std1.gno} (100%)
rename gnovm/tests/files/{zrealm_std2_stdlibs.gno => zrealm_std2.gno} (100%)
rename gnovm/tests/files/{zrealm_std3_stdlibs.gno => zrealm_std3.gno} (100%)
rename gnovm/tests/files/{zrealm_std4_stdlibs.gno => zrealm_std4.gno} (100%)
rename gnovm/tests/files/{zrealm_std5_stdlibs.gno => zrealm_std5.gno} (100%)
rename gnovm/tests/files/{zrealm_std6_stdlibs.gno => zrealm_std6.gno} (100%)
rename gnovm/tests/files/{zrealm_tests0_stdlibs.gno => zrealm_tests0.gno} (99%)
rename gnovm/tests/files/{zrealm_testutils0_stdlibs.gno => zrealm_testutils0.gno} (100%)
rename gnovm/tests/files/{zregexp_stdlibs.gno => zregexp.gno} (100%)
delete mode 100644 gnovm/tests/imports.go
delete mode 100644 gnovm/tests/machine_test.go
delete mode 100644 gnovm/tests/package_test.go
delete mode 100644 gnovm/tests/selector_test.go
delete mode 100644 gnovm/tests/testdata/TestMemPackage/fail/file_test.gno
delete mode 100644 gnovm/tests/testdata/TestMemPackage/success/file_test.gno
diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml
index 7c4bb35526f..41d579c4567 100644
--- a/.github/workflows/examples.yml
+++ b/.github/workflows/examples.yml
@@ -3,7 +3,7 @@ name: examples
on:
pull_request:
push:
- branches: [ "master" ]
+ branches: ["master"]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -70,7 +70,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- goversion: [ "1.22.x" ]
+ goversion: ["1.22.x"]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
@@ -86,7 +86,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- go-version: [ "1.22.x" ]
+ go-version: ["1.22.x"]
# unittests: TODO: matrix with contracts
runs-on: ubuntu-latest
timeout-minutes: 10
diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml
index 7e7586b23d9..8311d113047 100644
--- a/.github/workflows/gnovm.yml
+++ b/.github/workflows/gnovm.yml
@@ -13,6 +13,8 @@ jobs:
uses: ./.github/workflows/main_template.yml
with:
modulepath: "gnovm"
+ # in pull requests, append -short so that the CI runs quickly.
+ tests-extra-args: ${{ github.event_name == 'pull_request' && '-short' || '' }}
secrets:
codecov-token: ${{ secrets.CODECOV_TOKEN }}
fmt:
diff --git a/examples/gno.land/p/demo/avl/pager/z_filetest.gno b/examples/gno.land/p/demo/avl/pager/z_filetest.gno
index 91c20115469..17029f57861 100644
--- a/examples/gno.land/p/demo/avl/pager/z_filetest.gno
+++ b/examples/gno.land/p/demo/avl/pager/z_filetest.gno
@@ -99,3 +99,4 @@ func main() {
//
// ## Page 7 of 6
// [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_
+//
diff --git a/examples/gno.land/p/demo/avl/z_0_filetest.gno b/examples/gno.land/p/demo/avl/z_0_filetest.gno
index aff79ffabc6..2dce5e7f1ac 100644
--- a/examples/gno.land/p/demo/avl/z_0_filetest.gno
+++ b/examples/gno.land/p/demo/avl/z_0_filetest.gno
@@ -267,7 +267,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "z_0.gno",
// "IsMethod": false,
// "Name": "init.1",
// "NativeName": "",
@@ -278,7 +278,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "z_0.gno",
// "Line": "10",
// "PkgPath": "gno.land/r/test"
// }
@@ -303,7 +303,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "z_0.gno",
// "IsMethod": false,
// "Name": "main",
// "NativeName": "",
@@ -314,7 +314,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "z_0.gno",
// "Line": "15",
// "PkgPath": "gno.land/r/test"
// }
diff --git a/examples/gno.land/p/demo/avl/z_1_filetest.gno b/examples/gno.land/p/demo/avl/z_1_filetest.gno
index 3b6d40d5ecd..97ca5ed2135 100644
--- a/examples/gno.land/p/demo/avl/z_1_filetest.gno
+++ b/examples/gno.land/p/demo/avl/z_1_filetest.gno
@@ -340,7 +340,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "z_1.gno",
// "IsMethod": false,
// "Name": "init.1",
// "NativeName": "",
@@ -351,7 +351,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "z_1.gno",
// "Line": "10",
// "PkgPath": "gno.land/r/test"
// }
@@ -376,7 +376,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "z_1.gno",
// "IsMethod": false,
// "Name": "main",
// "NativeName": "",
@@ -387,7 +387,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "z_1.gno",
// "Line": "15",
// "PkgPath": "gno.land/r/test"
// }
diff --git a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno
index 4b2c04b6d5c..17d6c466ed5 100644
--- a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno
+++ b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno
@@ -44,6 +44,7 @@ func main() {
}
// Output:
+//
// -- INITIAL
//
// # Gnome 😃
diff --git a/examples/gno.land/r/demo/banktest/z_0_filetest.gno b/examples/gno.land/r/demo/banktest/z_0_filetest.gno
index 61289dfe071..5a8c8d70a48 100644
--- a/examples/gno.land/r/demo/banktest/z_0_filetest.gno
+++ b/examples/gno.land/r/demo/banktest/z_0_filetest.gno
@@ -42,9 +42,9 @@ func main() {
}
// Output:
-// main before: 300000000ugnot
+// main before: 100000000ugnot
// Deposit(): returned!
-// main after: 250000000ugnot
+// main after: 50000000ugnot
// ## recent activity
//
// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC
diff --git a/examples/gno.land/r/demo/banktest/z_2_filetest.gno b/examples/gno.land/r/demo/banktest/z_2_filetest.gno
index 2dc9c84e767..e839f60354a 100644
--- a/examples/gno.land/r/demo/banktest/z_2_filetest.gno
+++ b/examples/gno.land/r/demo/banktest/z_2_filetest.gno
@@ -39,9 +39,9 @@ func main() {
}
// Output:
-// main before: 200000000ugnot
+// main before:
// Deposit(): returned!
-// main after: 255000000ugnot
+// main after: 55000000ugnot
// ## recent activity
//
// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC
diff --git a/examples/gno.land/r/demo/boards/z_0_filetest.gno b/examples/gno.land/r/demo/boards/z_0_filetest.gno
index 4fc224da9b2..a649895cb01 100644
--- a/examples/gno.land/r/demo/boards/z_0_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno
@@ -37,3 +37,5 @@ func main() {
//
// Body of the second post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts)
+//
+//
diff --git a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno
index f746877b5c7..7dd460500d6 100644
--- a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno
@@ -46,3 +46,4 @@ func main() {
//
// Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno
index de2b6aa463b..f64b4c84bba 100644
--- a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno
@@ -50,3 +50,4 @@ func main() {
// > Edited: First reply of the First post
// >
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_11_filetest.gno b/examples/gno.land/r/demo/boards/z_11_filetest.gno
index 49ee0ee0273..3f56293b3bd 100644
--- a/examples/gno.land/r/demo/boards/z_11_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno
@@ -40,3 +40,4 @@ func main() {
//
// Edited: Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_12_filetest.gno b/examples/gno.land/r/demo/boards/z_12_filetest.gno
index 02953352dd2..ac4adf6ee7b 100644
--- a/examples/gno.land/r/demo/boards/z_12_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno
@@ -38,3 +38,5 @@ func main() {
//
// Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts)
+//
+//
diff --git a/examples/gno.land/r/demo/boards/z_1_filetest.gno b/examples/gno.land/r/demo/boards/z_1_filetest.gno
index ba0a277e2f1..4d46c81b83d 100644
--- a/examples/gno.land/r/demo/boards/z_1_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_1_filetest.gno
@@ -26,3 +26,4 @@ func main() {
//
// * [/r/demo/boards:test_board_1](/r/demo/boards:test_board_1)
// * [/r/demo/boards:test_board_2](/r/demo/boards:test_board_2)
+//
diff --git a/examples/gno.land/r/demo/boards/z_2_filetest.gno b/examples/gno.land/r/demo/boards/z_2_filetest.gno
index 53c0a1965da..31b39644b24 100644
--- a/examples/gno.land/r/demo/boards/z_2_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno
@@ -36,3 +36,4 @@ func main() {
//
// > Reply of the second post
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_3_filetest.gno b/examples/gno.land/r/demo/boards/z_3_filetest.gno
index 89e5a3f12ff..0b2a2df2f91 100644
--- a/examples/gno.land/r/demo/boards/z_3_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno
@@ -38,3 +38,4 @@ func main() {
//
// > Reply of the second post
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno
index fa0b9e20be5..c6cf6397b3a 100644
--- a/examples/gno.land/r/demo/boards/z_4_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno
@@ -44,6 +44,7 @@ func main() {
//
// > Second reply of the second post
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)]
+//
// Realm:
// switchrealm["gno.land/r/demo/users"]
diff --git a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno
index b20d8cdfed8..723e6a10204 100644
--- a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno
@@ -37,3 +37,4 @@ func main() {
//
// > Reply of the first post
// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_5_filetest.gno b/examples/gno.land/r/demo/boards/z_5_filetest.gno
index c0614bb9da3..712af483891 100644
--- a/examples/gno.land/r/demo/boards/z_5_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno
@@ -41,3 +41,4 @@ func main() {
// > Second reply of the second post
// >
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_6_filetest.gno b/examples/gno.land/r/demo/boards/z_6_filetest.gno
index 6ddd8b9cf3f..ec40cf5f8e9 100644
--- a/examples/gno.land/r/demo/boards/z_6_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno
@@ -47,3 +47,4 @@ func main() {
// > Second reply of the second post
// >
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_7_filetest.gno b/examples/gno.land/r/demo/boards/z_7_filetest.gno
index 534095b99cf..353b84f6d87 100644
--- a/examples/gno.land/r/demo/boards/z_7_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno
@@ -29,3 +29,5 @@ func main() {
//
// Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts)
+//
+//
diff --git a/examples/gno.land/r/demo/boards/z_8_filetest.gno b/examples/gno.land/r/demo/boards/z_8_filetest.gno
index f5477026805..4896dfcfccf 100644
--- a/examples/gno.land/r/demo/boards/z_8_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno
@@ -42,3 +42,4 @@ func main() {
// > First reply of the first reply
// >
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)]
+//
diff --git a/examples/gno.land/r/demo/boards/z_9_filetest.gno b/examples/gno.land/r/demo/boards/z_9_filetest.gno
index 4be9d2bdfa6..ca37e306bda 100644
--- a/examples/gno.land/r/demo/boards/z_9_filetest.gno
+++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno
@@ -35,3 +35,4 @@ func main() {
//
// Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&threadid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&threadid=1&postid=1)]
+//
diff --git a/examples/gno.land/r/demo/disperse/z_0_filetest.gno b/examples/gno.land/r/demo/disperse/z_0_filetest.gno
index e54b34ae3bf..ca1e9ea0ce8 100644
--- a/examples/gno.land/r/demo/disperse/z_0_filetest.gno
+++ b/examples/gno.land/r/demo/disperse/z_0_filetest.gno
@@ -30,5 +30,5 @@ func main() {
}
// Output:
-// main before: 200000200ugnot
-// main after: 200000000ugnot
+// main before: 200ugnot
+// main after:
diff --git a/examples/gno.land/r/demo/disperse/z_1_filetest.gno b/examples/gno.land/r/demo/disperse/z_1_filetest.gno
index 62018fe965e..4c27c50749f 100644
--- a/examples/gno.land/r/demo/disperse/z_1_filetest.gno
+++ b/examples/gno.land/r/demo/disperse/z_1_filetest.gno
@@ -30,5 +30,5 @@ func main() {
}
// Output:
-// main before: 200000300ugnot
-// main after: 200000100ugnot
+// main before: 300ugnot
+// main after: 100ugnot
diff --git a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno
index cf5902928db..60600e38b78 100644
--- a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno
+++ b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno
@@ -22,3 +22,4 @@ func main() {
// List of all Groups:
//
// * [test_group](/r/demo/groups:test_group)
+//
diff --git a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno
index 18799e31a67..71da1b966ec 100644
--- a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno
+++ b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno
@@ -76,3 +76,5 @@ func main() {
// Group Members:
//
// [0000000000, g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy, 32, i am from UAE, 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001],
+//
+//
diff --git a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno
index 7c97b01ccf5..0c482e1b52f 100644
--- a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno
+++ b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno
@@ -76,3 +76,5 @@ func main() {
// Group Last MemberID: 0000000001
//
// Group Members:
+//
+//
diff --git a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno
index cbfff97c7a7..ff38acf45a4 100644
--- a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno
+++ b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno
@@ -21,3 +21,5 @@ func main() {
// Output:
// 1
// List of all Groups:
+//
+//
diff --git a/examples/gno.land/r/demo/releases_example/releases0_filetest.gno b/examples/gno.land/r/demo/releases_example/releases0_filetest.gno
index 193f9bdbc90..ca599a54892 100644
--- a/examples/gno.land/r/demo/releases_example/releases0_filetest.gno
+++ b/examples/gno.land/r/demo/releases_example/releases0_filetest.gno
@@ -49,3 +49,4 @@ func main() {
//
// * various improvements
// * new shiny logo
+//
diff --git a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno
index 1ea56b4a3f9..4072c0b30d7 100644
--- a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno
+++ b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno
@@ -23,3 +23,4 @@ func main() {
// * [Play](/r/demo/tamagotchi$help&func=Play)
// * [Heal](/r/demo/tamagotchi$help&func=Heal)
// * [Reset](/r/demo/tamagotchi$help&func=Reset)
+//
diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno
index dcb957f2155..6465cc9c378 100644
--- a/examples/gno.land/r/demo/users/z_5_filetest.gno
+++ b/examples/gno.land/r/demo/users/z_5_filetest.gno
@@ -38,7 +38,7 @@ func main() {
}
// Output:
-// * [archives](/r/demo/users:archives)
+// * [archives](/r/demo/users:archives)
// * [demo](/r/demo/users:demo)
// * [gno](/r/demo/users:gno)
// * [gnoland](/r/demo/users:gnoland)
diff --git a/examples/gno.land/r/demo/wugnot/z0_filetest.gno b/examples/gno.land/r/demo/wugnot/z0_filetest.gno
index bef65c03b68..264bc8f19aa 100644
--- a/examples/gno.land/r/demo/wugnot/z0_filetest.gno
+++ b/examples/gno.land/r/demo/wugnot/z0_filetest.gno
@@ -55,17 +55,17 @@ func printBalances() {
// Output:
// -----------
-// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=200000000 |
+// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=0 |
// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |
// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |
// -----------
// -----------
-// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=200000000 |
+// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=0 |
// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |
// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |
// -----------
// -----------
-// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=200004242 |
+// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=4242 |
// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=99995759 |
// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |
// -----------
diff --git a/examples/gno.land/r/gnoland/faucet/faucet_test.gno b/examples/gno.land/r/gnoland/faucet/faucet_test.gno
index 1f492adb2dc..cecbb2ebcd6 100644
--- a/examples/gno.land/r/gnoland/faucet/faucet_test.gno
+++ b/examples/gno.land/r/gnoland/faucet/faucet_test.gno
@@ -28,7 +28,7 @@ func TestPackage(t *testing.T) {
)
// deposit 1000gnot to faucet contract
std.TestIssueCoins(faucetaddr, std.Coins{{"ugnot", 1000000000}})
- assertBalance(t, faucetaddr, 1200000000)
+ assertBalance(t, faucetaddr, 1000000000)
// by default, balance is empty, and as a user I cannot call Transfer, or Admin commands.
@@ -43,7 +43,7 @@ func TestPackage(t *testing.T) {
// as an admin, add the controller to contract and deposit more 2000gnot to contract
std.TestSetOrigCaller(adminaddr)
assertNoErr(t, faucet.AdminAddController(controlleraddr1))
- assertBalance(t, faucetaddr, 1200000000)
+ assertBalance(t, faucetaddr, 1000000000)
// now, send some tokens as controller.
std.TestSetOrigCaller(controlleraddr1)
@@ -51,7 +51,7 @@ func TestPackage(t *testing.T) {
assertBalance(t, test1addr, 1000000)
assertNoErr(t, faucet.Transfer(test1addr, 1000000))
assertBalance(t, test1addr, 2000000)
- assertBalance(t, faucetaddr, 1198000000)
+ assertBalance(t, faucetaddr, 998000000)
// remove controller
// as an admin, remove controller
diff --git a/examples/gno.land/r/gnoland/faucet/z0_filetest.gno b/examples/gno.land/r/gnoland/faucet/z0_filetest.gno
index bcc75897c85..7e729bdd358 100644
--- a/examples/gno.land/r/gnoland/faucet/z0_filetest.gno
+++ b/examples/gno.land/r/gnoland/faucet/z0_filetest.gno
@@ -33,3 +33,5 @@ func main() {
//
//
// Per request limit: 350000000ugnot
+//
+//
diff --git a/examples/gno.land/r/gnoland/faucet/z1_filetest.gno b/examples/gno.land/r/gnoland/faucet/z1_filetest.gno
index 6afb14b024b..c6fd6298488 100644
--- a/examples/gno.land/r/gnoland/faucet/z1_filetest.gno
+++ b/examples/gno.land/r/gnoland/faucet/z1_filetest.gno
@@ -33,3 +33,5 @@ func main() {
//
//
// Per request limit: 350000000ugnot
+//
+//
diff --git a/examples/gno.land/r/gnoland/faucet/z2_filetest.gno b/examples/gno.land/r/gnoland/faucet/z2_filetest.gno
index 054e5329476..d0616b3afcd 100644
--- a/examples/gno.land/r/gnoland/faucet/z2_filetest.gno
+++ b/examples/gno.land/r/gnoland/faucet/z2_filetest.gno
@@ -48,3 +48,5 @@ func main() {
// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v
//
// Per request limit: 350000000ugnot
+//
+//
diff --git a/examples/gno.land/r/gnoland/faucet/z3_filetest.gno b/examples/gno.land/r/gnoland/faucet/z3_filetest.gno
index 4a48ca390e2..0da06593710 100644
--- a/examples/gno.land/r/gnoland/faucet/z3_filetest.gno
+++ b/examples/gno.land/r/gnoland/faucet/z3_filetest.gno
@@ -60,3 +60,5 @@ func main() {
// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v
//
// Per request limit: 350000000ugnot
+//
+//
diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
index e889dde4f48..7b25eeb1db3 100644
--- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
@@ -126,6 +126,7 @@ func main() {
// - #123: g12345678 (10)
// - #123: g000000000 (10)
// - #123: g000000000 (0)
+//
// Events:
// [
diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
index c90e76727da..8eff79ffb5a 100644
--- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
@@ -80,6 +80,8 @@ func main() {
// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
//
// Threshold met: true
+//
+//
// Events:
// [
diff --git a/examples/gno.land/r/moul/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno
index 5203e07ada7..b26c919dd3a 100644
--- a/examples/gno.land/r/moul/home/z1_filetest.gno
+++ b/examples/gno.land/r/moul/home/z1_filetest.gno
@@ -17,3 +17,5 @@ func main() {
//
// ## Personal ToDo List
// - [ ] fill this todo list...
+//
+//
diff --git a/examples/gno.land/r/moul/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno
index 02d08cd591e..489dc2aeecd 100644
--- a/examples/gno.land/r/moul/home/z2_filetest.gno
+++ b/examples/gno.land/r/moul/home/z2_filetest.gno
@@ -33,3 +33,5 @@ func main() {
// - [ ] bbb
// - [ ] ddd
// - [ ] eee
+//
+//
diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go
index 5fa2075b8f7..0dca794ee71 100644
--- a/gno.land/pkg/sdk/vm/keeper.go
+++ b/gno.land/pkg/sdk/vm/keeper.go
@@ -365,7 +365,6 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) {
ChainID: ctx.ChainID(),
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- Msg: msg,
OrigCaller: creator.Bech32(),
OrigSend: deposit,
OrigSendSpent: new(std.Coins),
@@ -466,7 +465,6 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) {
ChainID: ctx.ChainID(),
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- Msg: msg,
OrigCaller: caller.Bech32(),
OrigSend: send,
OrigSendSpent: new(std.Coins),
@@ -565,7 +563,6 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) {
ChainID: ctx.ChainID(),
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- Msg: msg,
OrigCaller: caller.Bech32(),
OrigSend: send,
OrigSendSpent: new(std.Coins),
@@ -729,7 +726,6 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res
ChainID: ctx.ChainID(),
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- // Msg: msg,
// OrigCaller: caller,
// OrigSend: send,
// OrigSendSpent: nil,
@@ -796,7 +792,6 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string
ChainID: ctx.ChainID(),
Height: ctx.BlockHeight(),
Timestamp: ctx.BlockTime().Unix(),
- // Msg: msg,
// OrigCaller: caller,
// OrigSend: jsend,
// OrigSendSpent: nil,
diff --git a/gnovm/Makefile b/gnovm/Makefile
index d27395d9cd1..31daf942554 100644
--- a/gnovm/Makefile
+++ b/gnovm/Makefile
@@ -64,7 +64,7 @@ imports:
########################################
# Test suite
.PHONY: test
-test: _test.cmd _test.pkg _test.gnolang
+test: _test.cmd _test.pkg _test.stdlibs
.PHONY: _test.cmd
_test.cmd:
@@ -92,20 +92,10 @@ test.cmd.coverage_view: test.cmd.coverage
_test.pkg:
go test ./pkg/... $(GOTEST_FLAGS)
-.PHONY: _test.gnolang
-_test.gnolang: _test.gnolang.native _test.gnolang.stdlibs _test.gnolang.realm _test.gnolang.pkg0 _test.gnolang.pkg1 _test.gnolang.pkg2 _test.gnolang.other
-_test.gnolang.other:; go test tests/*.go -run "(TestFileStr|TestSelectors)" $(GOTEST_FLAGS)
-_test.gnolang.realm:; go test tests/*.go -run "TestFiles/^zrealm" $(GOTEST_FLAGS)
-_test.gnolang.pkg0:; go test tests/*.go -run "TestStdlibs/(bufio|crypto|encoding|errors|internal|io|math|sort|std|strconv|strings|testing|unicode)" $(GOTEST_FLAGS)
-_test.gnolang.pkg1:; go test tests/*.go -run "TestStdlibs/regexp" $(GOTEST_FLAGS)
-_test.gnolang.pkg2:; go test tests/*.go -run "TestStdlibs/bytes" $(GOTEST_FLAGS)
-_test.gnolang.native:; go test tests/*.go -test.short -run "TestFilesNative/" $(GOTEST_FLAGS)
-_test.gnolang.stdlibs:; go test tests/*.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS)
-_test.gnolang.native.sync:; go test tests/*.go -test.short -run "TestFilesNative/" --update-golden-tests $(GOTEST_FLAGS)
-_test.gnolang.stdlibs.sync:; go test tests/*.go -test.short -run 'TestFiles$$/' --update-golden-tests $(GOTEST_FLAGS)
-# NOTE: challenges are current GnoVM bugs which are supposed to fail.
-# If any of these tests pass, it should be moved to a normal test.
-_test.gnolang.challenges:; go test tests/*.go -test.short -run 'TestChallenges$$/' $(GOTEST_FLAGS)
+.PHONY: _test.stdlibs
+_test.stdlibs:
+ go run ./cmd/gno test -v ./stdlibs/...
+
########################################
# Code gen
diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go
index c6008117f13..ef35cf9af83 100644
--- a/gnovm/cmd/gno/lint.go
+++ b/gnovm/cmd/gno/lint.go
@@ -14,7 +14,7 @@ import (
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/gnovm/tests"
+ "github.com/gnolang/gno/gnovm/pkg/test"
"github.com/gnolang/gno/tm2/pkg/commands"
osm "github.com/gnolang/gno/tm2/pkg/os"
"go.uber.org/multierr"
@@ -91,10 +91,9 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error {
// Handle runtime errors
hasError = catchRuntimeError(pkgPath, io.Err(), func() {
stdout, stdin, stderr := io.Out(), io.In(), io.Err()
- testStore := tests.TestStore(
- rootDir, "",
+ _, testStore := test.Store(
+ rootDir, false,
stdin, stdout, stderr,
- tests.ImportModeStdlibsOnly,
)
targetPath := pkgPath
@@ -104,7 +103,8 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error {
}
memPkg := gno.ReadMemPackage(targetPath, targetPath)
- tm := tests.TestMachine(testStore, stdout, memPkg.Name)
+ tm := test.Machine(testStore, stdout, memPkg.Path)
+ defer tm.Release()
// Check package
tm.RunMemPackage(memPkg, true)
@@ -161,7 +161,7 @@ func guessSourcePath(pkg, source string) string {
// reParseRecover is a regex designed to parse error details from a string.
// It extracts the file location, line number, and error message from a formatted error string.
// XXX: Ideally, error handling should encapsulate location details within a dedicated error type.
-var reParseRecover = regexp.MustCompile(`^([^:]+):(\d+)(?::\d+)?:? *(.*)$`)
+var reParseRecover = regexp.MustCompile(`^([^:]+)((?::(?:\d+)){1,2}):? *(.*)$`)
func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (hasError bool) {
defer func() {
@@ -230,9 +230,9 @@ func issueFromError(pkgPath string, err error) lintIssue {
parsedError = strings.TrimPrefix(parsedError, pkgPath+"/")
matches := reParseRecover.FindStringSubmatch(parsedError)
- if len(matches) == 4 {
+ if len(matches) > 0 {
sourcepath := guessSourcePath(pkgPath, matches[1])
- issue.Location = fmt.Sprintf("%s:%s", sourcepath, matches[2])
+ issue.Location = sourcepath + matches[2]
issue.Msg = strings.TrimSpace(matches[3])
} else {
issue.Location = fmt.Sprintf("%s:0", filepath.Clean(pkgPath))
diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go
index 20d21c05d05..031c252bc79 100644
--- a/gnovm/cmd/gno/lint_test.go
+++ b/gnovm/cmd/gno/lint_test.go
@@ -13,19 +13,19 @@ func TestLintApp(t *testing.T) {
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"},
- stderrShouldContain: "undefined_variables_test.gno:6: name toto not declared (code=2)",
+ stderrShouldContain: "undefined_variables_test.gno:6:28: name toto not declared (code=2)",
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"},
- stderrShouldContain: "main.gno:4: name fmt not declared (code=2).",
+ stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2).",
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/several-lint-errors/main.gno"},
- stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6",
+ stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6",
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"},
- stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6: expected '}', found 'EOF' (code=2).\n",
+ stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n",
errShouldBe: "exit code: 1",
}, {
args: []string{"lint", "../../tests/integ/run_main/"},
diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go
index f174c2b4cc5..9a9beac5cd1 100644
--- a/gnovm/cmd/gno/run.go
+++ b/gnovm/cmd/gno/run.go
@@ -12,7 +12,7 @@ import (
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/gnovm/tests"
+ "github.com/gnolang/gno/gnovm/pkg/test"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/std"
)
@@ -92,9 +92,9 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error {
stderr := io.Err()
// init store and machine
- testStore := tests.TestStore(cfg.rootDir,
- "", stdin, stdout, stderr,
- tests.ImportModeStdlibsPreferred)
+ _, testStore := test.Store(
+ cfg.rootDir, false,
+ stdin, stdout, stderr)
if cfg.verbose {
testStore.SetLogStoreOps(true)
}
@@ -115,7 +115,7 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error {
var send std.Coins
pkgPath := string(files[0].PkgName)
- ctx := tests.TestContext(pkgPath, send)
+ ctx := test.Context(pkgPath, send)
m := gno.NewMachineWithOptions(gno.MachineOptions{
PkgPath: pkgPath,
Output: stdout,
diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go
index e5aa1bd6279..74f99f7490c 100644
--- a/gnovm/cmd/gno/run_test.go
+++ b/gnovm/cmd/gno/run_test.go
@@ -85,7 +85,7 @@ func TestRunApp(t *testing.T) {
},
{
args: []string{"run", "../../tests/integ/several-files-multiple-errors/"},
- stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6: expected '}', found 'EOF' (code=2).",
+ stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n",
errShouldBe: "exit code: 1",
},
// TODO: a test file
diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go
index d54b12f6a4f..04a3808718d 100644
--- a/gnovm/cmd/gno/test.go
+++ b/gnovm/cmd/gno/test.go
@@ -1,33 +1,21 @@
package main
import (
- "bytes"
"context"
- "encoding/json"
"flag"
"fmt"
+ goio "io"
"log"
- "math"
- "os"
"path/filepath"
- "runtime/debug"
- "sort"
"strings"
- "text/template"
"time"
- "go.uber.org/multierr"
-
- "github.com/gnolang/gno/gnovm"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
- "github.com/gnolang/gno/gnovm/tests"
- teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
+ "github.com/gnolang/gno/gnovm/pkg/test"
"github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/gnolang/gno/tm2/pkg/errors"
"github.com/gnolang/gno/tm2/pkg/random"
- "github.com/gnolang/gno/tm2/pkg/testutils"
)
type testCfg struct {
@@ -38,7 +26,6 @@ type testCfg struct {
updateGoldenTests bool
printRuntimeMetrics bool
printEvents bool
- withNativeFallback bool
}
func newTestCmd(io commands.IO) *commands.Command {
@@ -66,34 +53,38 @@ module name found in 'gno.mod', or else it is randomly generated like
- "*_filetest.gno" files on the other hand are kind of unique. They exist to
provide a way to interact and assert a gno contract, thanks to a set of
-specific instructions that can be added using code comments.
+specific directives that can be added using code comments.
"*_filetest.gno" must be declared in the 'main' package and so must have a
'main' function, that will be executed to test the target contract.
-List of available instructions that can be used in "*_filetest.gno" files:
- - "PKGPATH:" is a single line instruction that can be used to define the
+These single-line directives can set "input parameters" for the machine used
+to perform the test:
+ - "PKGPATH:" is a single line directive that can be used to define the
package used to interact with the tested package. If not specified, "main" is
used.
- - "MAXALLOC:" is a signle line instruction that can be used to define a limit
+ - "MAXALLOC:" is a single line directive that can be used to define a limit
to the VM allocator. If this limit is exceeded, the VM will panic. Default to
0, no limit.
- - "SEND:" is a single line instruction that can be used to send an amount of
+ - "SEND:" is a single line directive that can be used to send an amount of
token along with the transaction. The format is for example "1000000ugnot".
Default is empty.
- - "Output:\n" (*) is a multiple lines instruction that can be used to assert
- the output of the "*_filetest.gno" file. Any prints executed inside the
- 'main' function must match the lines that follows the "Output:\n"
- instruction, or else the test fails.
- - "Error:\n" works similarly to "Output:\n", except that it asserts the
- stderr of the program, which in that case, comes from the VM because of a
- panic, rather than the 'main' function.
- - "Realm:\n" (*) is a multiple lines instruction that can be used to assert
- what has been recorded in the store following the execution of the 'main'
- function.
-
-(*) The 'update-golden-tests' flag can be set to fill out the content of the
-instruction with the actual content of the test instead of failing.
+
+These directives, instead, match the comment that follows with the result
+of the GnoVM, acting as a "golden test":
+ - "Output:" tests the following comment with the standard output of the
+ filetest.
+ - "Error:" tests the following comment with any panic, or other kind of
+ error that the filetest generates (like a parsing or preprocessing error).
+ - "Realm:" tests the following comment against the store log, which can show
+ what realm information is stored.
+ - "Stacktrace:" can be used to verify the following lines against the
+ stacktrace of the error.
+ - "Events:" can be used to verify the emitted events against a JSON.
+
+To speed up execution, imports of pure packages are processed separately from
+the execution of the tests. This makes testing faster, but means that the
+initialization of imported pure packages cannot be checked in filetests.
`,
},
cfg,
@@ -115,7 +106,7 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) {
&c.updateGoldenTests,
"update-golden-tests",
false,
- `writes actual as wanted for "Output:" and "Realm:" instructions`,
+ `writes actual as wanted for "golden" directives in filetests`,
)
fs.StringVar(
@@ -139,13 +130,6 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) {
"max execution time",
)
- fs.BoolVar(
- &c.withNativeFallback,
- "with-native-fallback",
- false,
- "use stdlibs/* if present, otherwise use supported native Go packages",
- )
-
fs.BoolVar(
&c.printRuntimeMetrics,
"print-runtime-metrics",
@@ -192,6 +176,18 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
return fmt.Errorf("list sub packages: %w", err)
}
+ // Set up options to run tests.
+ stdout := goio.Discard
+ if cfg.verbose {
+ stdout = io.Out()
+ }
+ opts := test.NewTestOptions(cfg.rootDir, io.In(), stdout, io.Err())
+ opts.RunFlag = cfg.run
+ opts.Sync = cfg.updateGoldenTests
+ opts.Verbose = cfg.verbose
+ opts.Metrics = cfg.printRuntimeMetrics
+ opts.Events = cfg.printEvents
+
buildErrCount := 0
testErrCount := 0
for _, pkg := range subPkgs {
@@ -199,200 +195,48 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error {
io.ErrPrintfln("? %s \t[no test files]", pkg.Dir)
continue
}
-
- sort.Strings(pkg.TestGnoFiles)
- sort.Strings(pkg.FiletestGnoFiles)
-
- startedAt := time.Now()
- err = gnoTestPkg(pkg.Dir, pkg.TestGnoFiles, pkg.FiletestGnoFiles, cfg, io)
- duration := time.Since(startedAt)
- dstr := fmtDuration(duration)
-
- if err != nil {
- io.ErrPrintfln("%s: test pkg: %v", pkg.Dir, err)
- io.ErrPrintfln("FAIL")
- io.ErrPrintfln("FAIL %s \t%s", pkg.Dir, dstr)
- io.ErrPrintfln("FAIL")
- testErrCount++
- } else {
- io.ErrPrintfln("ok %s \t%s", pkg.Dir, dstr)
- }
- }
- if testErrCount > 0 || buildErrCount > 0 {
- io.ErrPrintfln("FAIL")
- return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount)
- }
-
- return nil
-}
-
-func gnoTestPkg(
- pkgPath string,
- unittestFiles,
- filetestFiles []string,
- cfg *testCfg,
- io commands.IO,
-) error {
- var (
- verbose = cfg.verbose
- rootDir = cfg.rootDir
- runFlag = cfg.run
- printRuntimeMetrics = cfg.printRuntimeMetrics
- printEvents = cfg.printEvents
-
- stdin = io.In()
- stdout = io.Out()
- stderr = io.Err()
- errs error
- )
-
- mode := tests.ImportModeStdlibsOnly
- if cfg.withNativeFallback {
- // XXX: display a warn?
- mode = tests.ImportModeStdlibsPreferred
- }
- if !verbose {
- // TODO: speedup by ignoring if filter is file/*?
- mockOut := bytes.NewBufferString("")
- stdout = commands.WriteNopCloser(mockOut)
- }
-
- // testing with *_test.gno
- if len(unittestFiles) > 0 {
// Determine gnoPkgPath by reading gno.mod
var gnoPkgPath string
- modfile, err := gnomod.ParseAt(pkgPath)
+ modfile, err := gnomod.ParseAt(pkg.Dir)
if err == nil {
gnoPkgPath = modfile.Module.Mod.Path
} else {
- gnoPkgPath = pkgPathFromRootDir(pkgPath, rootDir)
+ gnoPkgPath = pkgPathFromRootDir(pkg.Dir, cfg.rootDir)
if gnoPkgPath == "" {
// unable to read pkgPath from gno.mod, generate a random realm path
io.ErrPrintfln("--- WARNING: unable to read package path from gno.mod or gno root directory; try creating a gno.mod file")
- gnoPkgPath = gno.RealmPathPrefix + random.RandStr(8)
+ gnoPkgPath = gno.RealmPathPrefix + strings.ToLower(random.RandStr(8))
}
}
- memPkg := gno.ReadMemPackage(pkgPath, gnoPkgPath)
- // tfiles, ifiles := gno.ParseMemPackageTests(memPkg)
- var tfiles, ifiles *gno.FileSet
+ memPkg := gno.ReadMemPackage(pkg.Dir, gnoPkgPath)
- hasError := catchRuntimeError(gnoPkgPath, stderr, func() {
- tfiles, ifiles = parseMemPackageTests(memPkg)
+ startedAt := time.Now()
+ hasError := catchRuntimeError(gnoPkgPath, io.Err(), func() {
+ err = test.Test(memPkg, pkg.Dir, opts)
})
- if hasError {
- return commands.ExitCodeError(1)
- }
- testPkgName := getPkgNameFromFileset(ifiles)
-
- // run test files in pkg
- if len(tfiles.Files) > 0 {
- testStore := tests.TestStore(
- rootDir, "",
- stdin, stdout, stderr,
- mode,
- )
- if verbose {
- testStore.SetLogStoreOps(true)
- }
-
- m := tests.TestMachine(testStore, stdout, gnoPkgPath)
- if printRuntimeMetrics {
- // from tm2/pkg/sdk/vm/keeper.go
- // XXX: make maxAllocTx configurable.
- maxAllocTx := int64(math.MaxInt64)
-
- m.Alloc = gno.NewAllocator(maxAllocTx)
- }
- m.RunMemPackage(memPkg, true)
- err := runTestFiles(m, tfiles, memPkg.Name, verbose, printRuntimeMetrics, printEvents, runFlag, io)
- if err != nil {
- errs = multierr.Append(errs, err)
- }
- }
-
- // test xxx_test pkg
- if len(ifiles.Files) > 0 {
- testStore := tests.TestStore(
- rootDir, "",
- stdin, stdout, stderr,
- mode,
- )
- if verbose {
- testStore.SetLogStoreOps(true)
- }
-
- m := tests.TestMachine(testStore, stdout, testPkgName)
-
- memFiles := make([]*gnovm.MemFile, 0, len(ifiles.FileNames())+1)
- for _, f := range memPkg.Files {
- for _, ifileName := range ifiles.FileNames() {
- if f.Name == "gno.mod" || f.Name == ifileName {
- memFiles = append(memFiles, f)
- break
- }
- }
- }
-
- memPkg.Files = memFiles
- memPkg.Name = testPkgName
- memPkg.Path = memPkg.Path + "_test"
- m.RunMemPackage(memPkg, true)
+ duration := time.Since(startedAt)
+ dstr := fmtDuration(duration)
- err := runTestFiles(m, ifiles, testPkgName, verbose, printRuntimeMetrics, printEvents, runFlag, io)
+ if hasError || err != nil {
if err != nil {
- errs = multierr.Append(errs, err)
+ io.ErrPrintfln("%s: test pkg: %v", pkg.Dir, err)
}
+ io.ErrPrintfln("FAIL")
+ io.ErrPrintfln("FAIL %s \t%s", pkg.Dir, dstr)
+ io.ErrPrintfln("FAIL")
+ testErrCount++
+ } else {
+ io.ErrPrintfln("ok %s \t%s", pkg.Dir, dstr)
}
}
-
- // testing with *_filetest.gno
- {
- filter := splitRegexp(runFlag)
- for _, testFile := range filetestFiles {
- testFileName := filepath.Base(testFile)
- testName := "file/" + testFileName
- if !shouldRun(filter, testName) {
- continue
- }
-
- startedAt := time.Now()
- if verbose {
- io.ErrPrintfln("=== RUN %s", testName)
- }
-
- var closer func() (string, error)
- if !verbose {
- closer = testutils.CaptureStdoutAndStderr()
- }
-
- testFilePath := filepath.Join(pkgPath, testFileName)
- err := tests.RunFileTest(rootDir, testFilePath, tests.WithSyncWanted(cfg.updateGoldenTests))
- duration := time.Since(startedAt)
- dstr := fmtDuration(duration)
-
- if err != nil {
- errs = multierr.Append(errs, err)
- io.ErrPrintfln("--- FAIL: %s (%s)", testName, dstr)
- if verbose {
- stdouterr, err := closer()
- if err != nil {
- panic(err)
- }
- fmt.Fprintln(os.Stderr, stdouterr)
- }
- continue
- }
-
- if verbose {
- io.ErrPrintfln("--- PASS: %s (%s)", testName, dstr)
- }
- // XXX: add per-test metrics
- }
+ if testErrCount > 0 || buildErrCount > 0 {
+ io.ErrPrintfln("FAIL")
+ return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount)
}
- return errs
+ return nil
}
// attempts to determine the full gno pkg path by analyzing the directory.
@@ -423,228 +267,3 @@ func pkgPathFromRootDir(pkgPath, rootDir string) string {
}
return ""
}
-
-func runTestFiles(
- m *gno.Machine,
- files *gno.FileSet,
- pkgName string,
- verbose bool,
- printRuntimeMetrics bool,
- printEvents bool,
- runFlag string,
- io commands.IO,
-) (errs error) {
- defer func() {
- if r := recover(); r != nil {
- errs = multierr.Append(fmt.Errorf("panic: %v\nstack:\n%v\ngno machine: %v", r, string(debug.Stack()), m.String()), errs)
- }
- }()
-
- testFuncs := &testFuncs{
- PackageName: pkgName,
- Verbose: verbose,
- RunFlag: runFlag,
- }
- loadTestFuncs(pkgName, testFuncs, files)
-
- // before/after statistics
- numPackagesBefore := m.Store.NumMemPackages()
-
- testmain, err := formatTestmain(testFuncs)
- if err != nil {
- log.Fatal(err)
- }
-
- m.RunFiles(files.Files...)
- n := gno.MustParseFile("main_test.gno", testmain)
- m.RunFiles(n)
-
- for _, test := range testFuncs.Tests {
- // cleanup machine between tests
- tests.CleanupMachine(m)
-
- testFuncStr := fmt.Sprintf("%q", test.Name)
-
- eval := m.Eval(gno.Call("runtest", testFuncStr))
-
- if printEvents {
- events := m.Context.(*teststd.TestExecContext).EventLogger.Events()
- if events != nil {
- res, err := json.Marshal(events)
- if err != nil {
- panic(err)
- }
- io.ErrPrintfln("EVENTS: %s", string(res))
- }
- }
-
- ret := eval[0].GetString()
- if ret == "" {
- err := errors.New("failed to execute unit test: %q", test.Name)
- errs = multierr.Append(errs, err)
- io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name)
- continue
- }
-
- // TODO: replace with amino or send native type?
- var rep report
- err = json.Unmarshal([]byte(ret), &rep)
- if err != nil {
- errs = multierr.Append(errs, err)
- io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name)
- continue
- }
-
- if rep.Failed {
- err := errors.New("failed: %q", test.Name)
- errs = multierr.Append(errs, err)
- }
-
- if printRuntimeMetrics {
- imports := m.Store.NumMemPackages() - numPackagesBefore - 1
- // XXX: store changes
- // XXX: max mem consumption
- allocsVal := "n/a"
- if m.Alloc != nil {
- maxAllocs, allocs := m.Alloc.Status()
- allocsVal = fmt.Sprintf("%s(%.2f%%)",
- prettySize(allocs),
- float64(allocs)/float64(maxAllocs)*100,
- )
- }
- io.ErrPrintfln("--- runtime: cycle=%s imports=%d allocs=%s",
- prettySize(m.Cycles),
- imports,
- allocsVal,
- )
- }
- }
-
- return errs
-}
-
-// mirror of stdlibs/testing.Report
-type report struct {
- Failed bool
- Skipped bool
-}
-
-var testmainTmpl = template.Must(template.New("testmain").Parse(`
-package {{ .PackageName }}
-
-import (
- "testing"
-)
-
-var tests = []testing.InternalTest{
-{{range .Tests}}
- {"{{.Name}}", {{.Name}}},
-{{end}}
-}
-
-func runtest(name string) (report string) {
- for _, test := range tests {
- if test.Name == name {
- return testing.RunTest({{printf "%q" .RunFlag}}, {{.Verbose}}, test)
- }
- }
- panic("no such test: " + name)
- return ""
-}
-`))
-
-type testFuncs struct {
- Tests []testFunc
- PackageName string
- Verbose bool
- RunFlag string
-}
-
-type testFunc struct {
- Package string
- Name string
-}
-
-func getPkgNameFromFileset(files *gno.FileSet) string {
- if len(files.Files) <= 0 {
- return ""
- }
- return string(files.Files[0].PkgName)
-}
-
-func formatTestmain(t *testFuncs) (string, error) {
- var buf bytes.Buffer
- if err := testmainTmpl.Execute(&buf, t); err != nil {
- return "", err
- }
- return buf.String(), nil
-}
-
-func loadTestFuncs(pkgName string, t *testFuncs, tfiles *gno.FileSet) *testFuncs {
- for _, tf := range tfiles.Files {
- for _, d := range tf.Decls {
- if fd, ok := d.(*gno.FuncDecl); ok {
- fname := string(fd.Name)
- if strings.HasPrefix(fname, "Test") {
- tf := testFunc{
- Package: pkgName,
- Name: fname,
- }
- t.Tests = append(t.Tests, tf)
- }
- }
- }
- }
- return t
-}
-
-// parseMemPackageTests is copied from gno.ParseMemPackageTests
-// for except to _filetest.gno
-func parseMemPackageTests(memPkg *gnovm.MemPackage) (tset, itset *gno.FileSet) {
- tset = &gno.FileSet{}
- itset = &gno.FileSet{}
- var errs error
- for _, mfile := range memPkg.Files {
- if !strings.HasSuffix(mfile.Name, ".gno") {
- continue // skip this file.
- }
- if strings.HasSuffix(mfile.Name, "_filetest.gno") {
- continue
- }
- n, err := gno.ParseFile(mfile.Name, mfile.Body)
- if err != nil {
- errs = multierr.Append(errs, err)
- continue
- }
- if n == nil {
- panic("should not happen")
- }
- if strings.HasSuffix(mfile.Name, "_test.gno") {
- // add test file.
- if memPkg.Name+"_test" == string(n.PkgName) {
- itset.AddFiles(n)
- } else {
- tset.AddFiles(n)
- }
- } else if memPkg.Name == string(n.PkgName) {
- // skip package file.
- } else {
- panic(fmt.Sprintf(
- "expected package name [%s] or [%s_test] but got [%s] file [%s]",
- memPkg.Name, memPkg.Name, n.PkgName, mfile))
- }
- }
- if errs != nil {
- panic(errs)
- }
- return tset, itset
-}
-
-func shouldRun(filter filterMatch, path string) bool {
- if filter == nil {
- return true
- }
- elem := strings.Split(path, "/")
- ok, _ := filter.matches(elem, matchString)
- return ok
-}
diff --git a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar b/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar
index fc4039d38c6..52141dff09b 100644
--- a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar
+++ b/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar
@@ -16,4 +16,4 @@ func main() {
-- stdout.golden --
-- stderr.golden --
-bad_file.gno:3: unknown import path python (code=2).
+bad_file.gno:3:8: unknown import path python (code=2).
diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar
index 9482eeb1f4f..5aa3a3282d5 100644
--- a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar
+++ b/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar
@@ -17,4 +17,4 @@ func TestIHaveSomeError() {
-- stdout.golden --
-- stderr.golden --
-i_have_error_test.gno:6: name undefined_variable not declared (code=2).
+i_have_error_test.gno:6:7: name undefined_variable not declared (code=2).
diff --git a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar b/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar
index 7bd74a34855..b63c5c447e1 100644
--- a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar
+++ b/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar
@@ -17,4 +17,4 @@ func main() {
-- stdout.golden --
-- stderr.golden --
-bad_file.gno:6: name hello not declared (code=2).
+bad_file.gno:6:3: name hello not declared (code=2).
diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar
index 20a399881be..f9ce4dd9028 100644
--- a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar
@@ -2,7 +2,6 @@
gno test -v .
-stdout 'Machine\.RunMain\(\) panic: oups'
stderr '=== RUN file/x_filetest.gno'
stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
diff --git a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar
index 00737d8dd67..621397d8d1f 100644
--- a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar
@@ -2,9 +2,10 @@
! gno test -v .
-stdout 'Machine\.RunMain\(\) panic: oups'
stderr '=== RUN file/x_filetest.gno'
-stderr 'panic: fail on x_filetest.gno: got "oups", want: "xxx"'
+stderr 'Error diff:'
+stderr '-xxx'
+stderr '\+oups'
-- x_filetest.gno --
package main
diff --git a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar
index e2b67cb3333..067489c41f2 100644
--- a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar
@@ -3,9 +3,9 @@
# by the '-update-golden-tests' flag. The Error is only updated when it is
# empty.
-! gno test -v .
+gno test -update-golden-tests -v .
-stdout 'Machine\.RunMain\(\) panic: oups'
+! stdout .+
stderr '=== RUN file/x_filetest.gno'
cmp x_filetest.gno x_filetest.gno.golden
@@ -18,7 +18,6 @@ func main() {
}
// Error:
-
-- x_filetest.gno.golden --
package main
@@ -28,5 +27,3 @@ func main() {
// Error:
// oups
-// *** CHECK THE ERR MESSAGES ABOVE, MAKE SURE IT'S WHAT YOU EXPECTED, DELETE THIS LINE AND RUN TEST AGAIN ***
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar
index 91431e4f7bb..7b57729ee91 100644
--- a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar
@@ -2,9 +2,8 @@
! gno test -v .
-stdout 'Machine.RunMain\(\) panic: beep boop'
stderr '=== RUN file/failing_filetest.gno'
-stderr 'panic: fail on failing_filetest.gno: got unexpected error: beep boop'
+stderr 'unexpected panic: beep boop'
-- failing.gno --
package failing
diff --git a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar b/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar
index 0236872e78a..34da5fe2ff0 100644
--- a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar
@@ -7,7 +7,7 @@ stderr 'ok \. \d\.\d\ds'
gno test -print-events -v .
-! stdout .+
+stdout 'test'
stderr '=== RUN file/valid_filetest.gno'
stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar b/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar
index e065d00d55a..99747a0a241 100644
--- a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar
@@ -3,7 +3,7 @@
gno test --print-runtime-metrics .
! stdout .+
-stderr '--- runtime: cycle=[\d\.kM]+ imports=\d+ allocs=[\d\.kM]+\(\d\.\d\d%\)'
+stderr '--- runtime: cycle=[\d\.kM]+ allocs=[\d\.kM]+\(\d\.\d\d%\)'
-- metrics.gno --
package metrics
@@ -20,4 +20,3 @@ func TestTimeout(t *testing.T) {
println("plop")
}
}
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar
index e734dad7934..a8aa878e0a4 100644
--- a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar
@@ -2,7 +2,8 @@
gno test -v .
-! stdout .+ # stdout should be empty
+stdout 'hey'
+stdout 'hru?'
stderr '=== RUN file/x_filetest.gno'
stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
diff --git a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar
index 009d09623a0..60a38933d47 100644
--- a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar
@@ -1,13 +1,14 @@
# Test Output instruction incorrect
+# with -v, stdout should contain output (unmodified).
! gno test -v .
-! stdout .+ # stdout should be empty
+stdout 'hey'
+
stderr '=== RUN file/x_filetest.gno'
-stderr 'panic: fail on x_filetest.gno: diff:'
stderr '--- Expected'
stderr '\+\+\+ Actual'
-stderr '@@ -1,2 \+1 @@'
+stderr '@@ -1,3 \+1,2 @@'
stderr 'hey'
stderr '-hru?'
diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar
index 45e6e5c79be..45385a7eef9 100644
--- a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar
@@ -2,7 +2,9 @@
gno test -v . -update-golden-tests
-! stdout .+ # stdout should be empty
+stdout 'hey'
+stdout '^hru\?'
+
stderr '=== RUN file/x_filetest.gno'
stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
@@ -19,7 +21,6 @@ func main() {
// Output:
// hey
-
-- x_filetest.gno.golden --
package main
@@ -31,4 +32,3 @@ func main() {
// Output:
// hey
// hru?
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar b/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar
index b38683adf81..7d204bdb98d 100644
--- a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar
@@ -66,4 +66,5 @@ func main() {
println("filetest " + hello.Name)
}
-// Output: filetest foo
+// Output:
+// filetest foo
diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar
index 99e6fccd42d..ced183bec67 100644
--- a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar
@@ -8,8 +8,8 @@ stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
-- x_filetest.gno --
-// PKGPATH: gno.land/r/x
-package x
+// PKGPATH: gno.land/r/xx
+package xx
var x int
@@ -18,11 +18,11 @@ func main() {
}
// Realm:
-// switchrealm["gno.land/r/x"]
-// u[58cde29876a8d185e30c727361981efb068f4726:2]={
+// switchrealm["gno.land/r/xx"]
+// u[aea84df38908f9569d0f552575606e6e6e7e22dd:2]={
// "Blank": {},
// "ObjectInfo": {
-// "ID": "58cde29876a8d185e30c727361981efb068f4726:2",
+// "ID": "aea84df38908f9569d0f552575606e6e6e7e22dd:2",
// "IsEscaped": true,
// "ModTime": "3",
// "RefCount": "2"
@@ -35,7 +35,7 @@ func main() {
// "Column": "0",
// "File": "",
// "Line": "0",
-// "PkgPath": "gno.land/r/x"
+// "PkgPath": "gno.land/r/xx"
// }
// },
// "Values": [
@@ -57,22 +57,22 @@ func main() {
// "Closure": {
// "@type": "/gno.RefValue",
// "Escaped": true,
-// "ObjectID": "58cde29876a8d185e30c727361981efb068f4726:3"
+// "ObjectID": "aea84df38908f9569d0f552575606e6e6e7e22dd:3"
// },
-// "FileName": "main.gno",
+// "FileName": "x.gno",
// "IsMethod": false,
// "Name": "main",
// "NativeName": "",
// "NativePkg": "",
-// "PkgPath": "gno.land/r/x",
+// "PkgPath": "gno.land/r/xx",
// "Source": {
// "@type": "/gno.RefNode",
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "x.gno",
// "Line": "6",
-// "PkgPath": "gno.land/r/x"
+// "PkgPath": "gno.land/r/xx"
// }
// },
// "Type": {
@@ -84,4 +84,3 @@ func main() {
// }
// ]
// }
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar
index 6dfd6d70bb9..234d0f81e77 100644
--- a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar
@@ -4,16 +4,17 @@
! stdout .+ # stdout should be empty
stderr '=== RUN file/x_filetest.gno'
-stderr 'panic: fail on x_filetest.gno: diff:'
+stderr 'Realm diff:'
stderr '--- Expected'
stderr '\+\+\+ Actual'
-stderr '@@ -1 \+1,66 @@'
+stderr '@@ -1,2 \+1,67 @@'
stderr '-xxx'
-stderr '\+switchrealm\["gno.land/r/x"\]'
+stderr '\+switchrealm\["gno.land/r/xx"\]'
+stderr 'x_filetest.gno failed'
-- x_filetest.gno --
-// PKGPATH: gno.land/r/x
-package x
+// PKGPATH: gno.land/r/xx
+package xx
var x int
@@ -23,4 +24,3 @@ func main() {
// Realm:
// xxxx
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar
index 3d27ab4fde0..c93e6d86e8f 100644
--- a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar
@@ -10,8 +10,8 @@ stderr 'ok \. \d\.\d\ds'
cmp x_filetest.gno x_filetest.gno.golden
-- x_filetest.gno --
-// PKGPATH: gno.land/r/x
-package x
+// PKGPATH: gno.land/r/xx
+package xx
var x int
@@ -21,10 +21,9 @@ func main() {
// Realm:
// xxx
-
-- x_filetest.gno.golden --
-// PKGPATH: gno.land/r/x
-package x
+// PKGPATH: gno.land/r/xx
+package xx
var x int
@@ -33,11 +32,11 @@ func main() {
}
// Realm:
-// switchrealm["gno.land/r/x"]
-// u[58cde29876a8d185e30c727361981efb068f4726:2]={
+// switchrealm["gno.land/r/xx"]
+// u[aea84df38908f9569d0f552575606e6e6e7e22dd:2]={
// "Blank": {},
// "ObjectInfo": {
-// "ID": "58cde29876a8d185e30c727361981efb068f4726:2",
+// "ID": "aea84df38908f9569d0f552575606e6e6e7e22dd:2",
// "IsEscaped": true,
// "ModTime": "3",
// "RefCount": "2"
@@ -50,7 +49,7 @@ func main() {
// "Column": "0",
// "File": "",
// "Line": "0",
-// "PkgPath": "gno.land/r/x"
+// "PkgPath": "gno.land/r/xx"
// }
// },
// "Values": [
@@ -72,22 +71,22 @@ func main() {
// "Closure": {
// "@type": "/gno.RefValue",
// "Escaped": true,
-// "ObjectID": "58cde29876a8d185e30c727361981efb068f4726:3"
+// "ObjectID": "aea84df38908f9569d0f552575606e6e6e7e22dd:3"
// },
-// "FileName": "main.gno",
+// "FileName": "x.gno",
// "IsMethod": false,
// "Name": "main",
// "NativeName": "",
// "NativePkg": "",
-// "PkgPath": "gno.land/r/x",
+// "PkgPath": "gno.land/r/xx",
// "Source": {
// "@type": "/gno.RefNode",
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "x.gno",
// "Line": "6",
-// "PkgPath": "gno.land/r/x"
+// "PkgPath": "gno.land/r/xx"
// }
// },
// "Type": {
@@ -99,4 +98,3 @@ func main() {
// }
// ]
// }
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar b/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar
deleted file mode 100644
index 6099788a9a1..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/test_with-native-fallback.txtar
+++ /dev/null
@@ -1,32 +0,0 @@
-# Test native lib
-
-! gno test -v .
-
-! stdout .+
-stderr 'panic: unknown import path net \[recovered\]'
-stderr ' panic: gno.land/r/\w{8}/contract.gno:3:8: unknown import path net'
-
-gno test -v --with-native-fallback .
-
-! stdout .+
-stderr '=== RUN TestFoo'
-stderr '--- PASS: TestFoo'
-
--- contract.gno --
-package contract
-
-import "net"
-
-func Foo() {
- _ = net.IPv4
-}
-
--- contract_test.gno --
-package contract
-
-import "testing"
-
-func TestFoo(t *testing.T) {
- Foo()
-}
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar b/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar
deleted file mode 100644
index 37ef68f3d91..00000000000
--- a/gnovm/cmd/gno/testdata/gno_test/unknow_lib.txtar
+++ /dev/null
@@ -1,32 +0,0 @@
-# Test unknow lib
-
-! gno test -v .
-
-! stdout .+
-stderr 'panic: unknown import path foobarbaz \[recovered\]'
-stderr ' panic: gno.land/r/\w{8}/contract.gno:3:8: unknown import path foobarbaz'
-
-! gno test -v --with-native-fallback .
-
-! stdout .+
-stderr 'panic: unknown import path foobarbaz \[recovered\]'
-stderr ' panic: gno.land/r/\w{8}/contract.gno:3:8: unknown import path foobarbaz'
-
--- contract.gno --
-package contract
-
-import "foobarbaz"
-
-func Foo() {
- _ = foobarbaz.Gnognogno
-}
-
--- contract_test.gno --
-package contract
-
-import "testing"
-
-func TestFoo(t *testing.T) {
- Foo()
-}
-
diff --git a/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar b/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar
new file mode 100644
index 00000000000..0611d3440a4
--- /dev/null
+++ b/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar
@@ -0,0 +1,24 @@
+# Test for loading an unknown package
+
+! gno test -v .
+
+! stdout .+
+stderr 'contract.gno:3:8: unknown import path foobarbaz'
+
+-- contract.gno --
+package contract
+
+import "foobarbaz"
+
+func Foo() {
+ _ = foobarbaz.Gnognogno
+}
+
+-- contract_test.gno --
+package contract
+
+import "testing"
+
+func TestFoo(t *testing.T) {
+ Foo()
+}
diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar
index 02ae3f72304..4e24ad9ab08 100644
--- a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar
+++ b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar
@@ -7,7 +7,7 @@ stderr 'ok \. \d\.\d\ds'
gno test -v .
-! stdout .+
+stdout 'test'
stderr '=== RUN file/valid_filetest.gno'
stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)'
stderr 'ok \. \d\.\d\ds'
diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar b/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar
index d21390f9472..145fe796c09 100644
--- a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar
+++ b/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar
@@ -1,10 +1,11 @@
# Run gno transpile with -gobuild flag
+# The error messages changed sometime in go1.23, so this avoids errors
! gno transpile -gobuild .
! stdout .+
-stderr '^main.gno:4:6: x declared and not used$'
-stderr '^main.gno:5:6: y declared and not used$'
+stderr '^main.gno:4:6: .*declared and not used'
+stderr '^main.gno:5:6: .*declared and not used'
stderr '^2 transpile error\(s\)$'
cmp main.gno.gen.go main.gno.gen.go.golden
diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go
index 90aedd5d27a..697aa94b3c6 100644
--- a/gnovm/cmd/gno/util.go
+++ b/gnovm/cmd/gno/util.go
@@ -338,17 +338,3 @@ func copyFile(src, dst string) error {
return nil
}
-
-// Adapted from https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
-func prettySize(nb int64) string {
- const unit = 1000
- if nb < unit {
- return fmt.Sprintf("%d", nb)
- }
- div, exp := int64(unit), 0
- for n := nb / unit; n >= unit; n /= unit {
- div *= unit
- exp++
- }
- return fmt.Sprintf("%.1f%c", float64(nb)/float64(div), "kMGTPE"[exp])
-}
diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go
index 63a3ee74675..926ff0595e6 100644
--- a/gnovm/pkg/gnolang/debugger_test.go
+++ b/gnovm/pkg/gnolang/debugger_test.go
@@ -12,7 +12,7 @@ import (
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/gnovm/tests"
+ "github.com/gnolang/gno/gnovm/pkg/test"
)
type dtest struct{ in, out string }
@@ -24,17 +24,12 @@ type writeNopCloser struct{ io.Writer }
func (writeNopCloser) Close() error { return nil }
// TODO (Marc): move evalTest to gnovm/tests package and remove code duplicates
-func evalTest(debugAddr, in, file string) (out, err, stacktrace string) {
+func evalTest(debugAddr, in, file string) (out, err string) {
bout := bytes.NewBufferString("")
berr := bytes.NewBufferString("")
stdin := bytes.NewBufferString(in)
stdout := writeNopCloser{bout}
stderr := writeNopCloser{berr}
- debug := in != "" || debugAddr != ""
- mode := tests.ImportModeStdlibsPreferred
- if strings.HasSuffix(file, "_native.gno") {
- mode = tests.ImportModeNativePreferred
- }
defer func() {
if r := recover(); r != nil {
@@ -44,7 +39,7 @@ func evalTest(debugAddr, in, file string) (out, err, stacktrace string) {
err = strings.TrimSpace(strings.ReplaceAll(err, "../../tests/files/", "files/"))
}()
- testStore := tests.TestStore(gnoenv.RootDir(), "../../tests/files", stdin, stdout, stderr, mode)
+ _, testStore := test.Store(gnoenv.RootDir(), false, stdin, stdout, stderr)
f := gnolang.MustReadFile(file)
@@ -53,23 +48,11 @@ func evalTest(debugAddr, in, file string) (out, err, stacktrace string) {
Input: stdin,
Output: stdout,
Store: testStore,
- Context: tests.TestContext(string(f.PkgName), nil),
- Debug: debug,
+ Context: test.Context(string(f.PkgName), nil),
+ Debug: true,
})
defer m.Release()
- defer func() {
- if r := recover(); r != nil {
- switch r.(type) {
- case gnolang.UnhandledPanicError:
- stacktrace = m.ExceptionsStacktrace()
- default:
- stacktrace = m.Stacktrace().String()
- }
- stacktrace = strings.TrimSpace(strings.ReplaceAll(stacktrace, "../../tests/files/", "files/"))
- panic(r)
- }
- }()
if debugAddr != "" {
if e := m.Debugger.Serve(debugAddr); e != nil {
@@ -81,7 +64,7 @@ func evalTest(debugAddr, in, file string) (out, err, stacktrace string) {
m.RunFiles(f)
ex, _ := gnolang.ParseExpr("main()")
m.Eval(ex)
- out, err, stacktrace = bout.String(), berr.String(), m.ExceptionsStacktrace()
+ out, err = bout.String(), berr.String()
return
}
@@ -90,7 +73,7 @@ func runDebugTest(t *testing.T, targetPath string, tests []dtest) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
- out, err, _ := evalTest("", test.in, targetPath)
+ out, err := evalTest("", test.in, targetPath)
t.Log("in:", test.in, "out:", out, "err:", err)
if !strings.Contains(out, test.out) {
t.Errorf("unexpected output\nwant\"%s\"\n got \"%s\"", test.out, out)
@@ -206,7 +189,7 @@ func TestRemoteDebug(t *testing.T) {
}
func TestRemoteError(t *testing.T) {
- _, err, _ := evalTest(":xxx", "", debugTarget)
+ _, err := evalTest(":xxx", "", debugTarget)
t.Log("err:", err)
if !strings.Contains(err, "tcp/xxx: unknown port") &&
!strings.Contains(err, "tcp/xxx: nodename nor servname provided, or not known") {
diff --git a/gnovm/pkg/gnolang/eval_test.go b/gnovm/pkg/gnolang/eval_test.go
deleted file mode 100644
index 9b83d673767..00000000000
--- a/gnovm/pkg/gnolang/eval_test.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package gnolang_test
-
-import (
- "io/fs"
- "os"
- "path/filepath"
- "regexp"
- "sort"
- "strings"
- "testing"
-)
-
-func TestEvalFiles(t *testing.T) {
- dir := "../../tests/files"
- fsys := os.DirFS(dir)
- err := fs.WalkDir(fsys, ".", func(path string, de fs.DirEntry, err error) error {
- switch {
- case err != nil:
- return err
- case path == "extern":
- return fs.SkipDir
- case de.IsDir():
- return nil
- }
-
- fullPath := filepath.Join(dir, path)
- wantOut, wantErr, wantStacktrace, ok := testData(fullPath)
- if !ok {
- return nil
- }
-
- t.Run(path, func(t *testing.T) {
- out, err, stacktrace := evalTest("", "", fullPath)
-
- if wantErr != "" && !strings.Contains(err, wantErr) ||
- wantErr == "" && err != "" {
- t.Fatalf("unexpected error\nWant: %s\n Got: %s", wantErr, err)
- }
-
- if wantStacktrace != "" && !strings.Contains(stacktrace, wantStacktrace) {
- t.Fatalf("unexpected stacktrace\nWant: %s\n Got: %s", wantStacktrace, stacktrace)
- }
- if wantOut != "" && strings.TrimSpace(out) != strings.TrimSpace(wantOut) {
- t.Fatalf("unexpected output\nWant: \"%s\"\n Got: \"%s\"", wantOut, out)
- }
- })
-
- return nil
- })
- if err != nil {
- t.Fatal(err)
- }
-}
-
-// testData returns the expected output and error string, and true if entry is valid.
-func testData(name string) (testOut, testErr, testStacktrace string, ok bool) {
- if !strings.HasSuffix(name, ".gno") || strings.HasSuffix(name, "_long.gno") {
- return
- }
- buf, err := os.ReadFile(name)
- if err != nil {
- return
- }
- str := string(buf)
- if strings.Contains(str, "// PKGPATH:") {
- return
- }
- res := commentFrom(str, []string{
- "// Output:",
- "// Error:",
- "// Stacktrace:",
- })
-
- return res[0], res[1], res[2], true
-}
-
-type directive struct {
- delim string
- res string
- index int
-}
-
-// (?m) makes ^ and $ match start/end of string.
-// Used to substitute from a comment all the //.
-// Using a regex allows us to parse lines only containing "//" as an empty line.
-var reCommentPrefix = regexp.MustCompile("(?m)^//(?: |$)")
-
-// commentFrom returns the comments from s that are between the delimiters.
-// delims is a list of delimiters like "// Output:", which should be on a
-// single line to mark the beginning of a directive.
-// The return value is the content of each directive, matching the indexes
-// of delims, ie. len(result) == len(delims).
-func commentFrom(s string, delims []string) []string {
- directives := make([]directive, len(delims))
- directivesFound := make([]*directive, 0, len(delims))
-
- // Find directives
- for i, delim := range delims {
- // must find delim isolated on one line
- delim = "\n" + delim + "\n"
- index := strings.Index(s, delim)
- directives[i] = directive{delim: delim, index: index}
- if index >= 0 {
- directivesFound = append(directivesFound, &directives[i])
- }
- }
- sort.Slice(directivesFound, func(i, j int) bool {
- return directivesFound[i].index < directivesFound[j].index
- })
-
- for i := range directivesFound {
- next := len(s)
- if i != len(directivesFound)-1 {
- next = directivesFound[i+1].index
- }
-
- // Mark beginning of directive content from the line after the directive.
- contentStart := directivesFound[i].index + len(directivesFound[i].delim)
- content := s[contentStart:next]
-
- // Remove comment prefixes.
- parsed := reCommentPrefix.ReplaceAllLiteralString(content, "")
- directivesFound[i].res = strings.TrimSuffix(parsed, "\n")
- }
-
- res := make([]string, len(directives))
- for i, d := range directives {
- res[i] = d.res
- }
-
- return res
-}
diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go
new file mode 100644
index 00000000000..f1bc87d21d8
--- /dev/null
+++ b/gnovm/pkg/gnolang/files_test.go
@@ -0,0 +1,141 @@
+package gnolang_test
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/gnolang/gno/gnovm/pkg/test"
+ "github.com/stretchr/testify/require"
+)
+
+var withSync = flag.Bool("update-golden-tests", false, "rewrite tests updating Realm: and Output: with new values where changed")
+
+type nopReader struct{}
+
+func (nopReader) Read(p []byte) (int, error) { return 0, io.EOF }
+
+// TestFiles tests all the files in "gnovm/tests/files".
+//
+// Cheatsheet:
+//
+// fail on the first test:
+// go test -run TestFiles -failfast
+// run a specific test:
+// go test -run TestFiles/addr0b
+// fix a specific test:
+// go test -run TestFiles/'^bin1.gno' -short -v -update-golden-tests .
+func TestFiles(t *testing.T) {
+ rootDir, err := filepath.Abs("../../../")
+ require.NoError(t, err)
+
+ opts := &test.TestOptions{
+ RootDir: rootDir,
+ Output: io.Discard,
+ Error: io.Discard,
+ Sync: *withSync,
+ }
+ opts.BaseStore, opts.TestStore = test.Store(
+ rootDir, true,
+ nopReader{}, opts.WriterForStore(), io.Discard,
+ )
+
+ dir := "../../tests/"
+ fsys := os.DirFS(dir)
+ err = fs.WalkDir(fsys, "files", func(path string, de fs.DirEntry, err error) error {
+ switch {
+ case err != nil:
+ return err
+ case path == "files/extern":
+ return fs.SkipDir
+ case de.IsDir():
+ return nil
+ }
+ subTestName := path[len("files/"):]
+ if strings.HasSuffix(path, "_long.gno") && testing.Short() {
+ t.Run(subTestName, func(t *testing.T) {
+ t.Skip("skipping in -short")
+ })
+ return nil
+ }
+
+ content, err := fs.ReadFile(fsys, path)
+ if err != nil {
+ return err
+ }
+
+ var criticalError error
+ t.Run(subTestName, func(t *testing.T) {
+ changed, err := opts.RunFiletest(path, content)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ if changed != "" {
+ err = os.WriteFile(filepath.Join(dir, path), []byte(changed), de.Type())
+ if err != nil {
+ criticalError = fmt.Errorf("could not fix golden file: %w", err)
+ }
+ }
+ })
+
+ return criticalError
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestStdlibs tests all the standard library packages.
+func TestStdlibs(t *testing.T) {
+ rootDir, err := filepath.Abs("../../../")
+ require.NoError(t, err)
+
+ var capture bytes.Buffer
+ out := io.Writer(&capture)
+ if testing.Verbose() {
+ out = os.Stdout
+ }
+ opts := test.NewTestOptions(rootDir, nopReader{}, out, out)
+ opts.Verbose = true
+
+ dir := "../../stdlibs/"
+ fsys := os.DirFS(dir)
+ err = fs.WalkDir(fsys, ".", func(path string, de fs.DirEntry, err error) error {
+ switch {
+ case err != nil:
+ return err
+ case !de.IsDir() || path == ".":
+ return nil
+ }
+
+ fp := filepath.Join(dir, path)
+ memPkg := gnolang.ReadMemPackage(fp, path)
+ t.Run(strings.ReplaceAll(memPkg.Path, "/", "-"), func(t *testing.T) {
+ if testing.Short() {
+ switch memPkg.Path {
+ case "bytes", "strconv", "regexp/syntax":
+ t.Skip("Skipped because of -short, and this stdlib is very long currently.")
+ }
+ }
+ err := test.Test(memPkg, "", opts)
+ if !testing.Verbose() {
+ t.Log(capture.String())
+ }
+ if err != nil {
+ t.Error(err)
+ }
+ })
+
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/gnovm/pkg/gnolang/gonative.go b/gnovm/pkg/gnolang/gonative.go
index fe92f5bcd23..5a39c76b5e1 100644
--- a/gnovm/pkg/gnolang/gonative.go
+++ b/gnovm/pkg/gnolang/gonative.go
@@ -83,11 +83,6 @@ func go2GnoBaseType(rt reflect.Type) Type {
}
}
-// Implements Store.
-func (ds *defaultStore) SetStrictGo2GnoMapping(strict bool) {
- ds.go2gnoStrict = strict
-}
-
// Implements Store.
// See go2GnoValue2(). Like go2GnoType() but also converts any
// top-level complex types (or pointers to them). The result gets
@@ -109,54 +104,9 @@ func (ds *defaultStore) Go2GnoType(rt reflect.Type) (t Type) {
// wrap t with declared type.
pkgPath := rt.PkgPath()
if pkgPath != "" {
- // mappings have been removed, so for any non-builtin type in strict mode,
- // this will panic.
- if ds.go2gnoStrict {
- // mapping failed and strict: error.
- gokey := pkgPath + "." + rt.Name()
- panic(fmt.Sprintf("native type does not exist for %s", gokey))
- }
-
- // generate a new gno type for testing.
- mtvs := []TypedValue(nil)
- if t.Kind() == InterfaceKind {
- // methods already set on t.Methods.
- // *DT.Methods not used in Go for interfaces.
- } else {
- prt := rt
- if rt.Kind() != reflect.Ptr {
- // NOTE: go reflect requires ptr kind
- // for methods with ptr receivers,
- // whereas gno methods are all
- // declared on the *DeclaredType.
- prt = reflect.PointerTo(rt)
- }
- nm := prt.NumMethod()
- mtvs = make([]TypedValue, nm)
- for i := 0; i < nm; i++ {
- mthd := prt.Method(i)
- ft := ds.go2GnoFuncType(mthd.Type)
- fv := &FuncValue{
- Type: ft,
- IsMethod: true,
- Source: nil,
- Name: Name(mthd.Name),
- Closure: nil,
- PkgPath: pkgPath,
- body: nil, // XXX
- nativeBody: nil,
- }
- mtvs[i] = TypedValue{T: ft, V: fv}
- }
- }
- dt := &DeclaredType{
- PkgPath: pkgPath,
- Name: Name(rt.Name()),
- Base: t,
- Methods: mtvs,
- }
- dt.Seal()
- t = dt
+ // mappings have been removed, so this should never happen.
+ gokey := pkgPath + "." + rt.Name()
+ panic(fmt.Sprintf("native type does not exist for %s", gokey))
}
// memoize t to cache.
if debug {
@@ -1230,34 +1180,6 @@ func gno2GoValue(tv *TypedValue, rv reflect.Value) (ret reflect.Value) {
// ----------------------------------------
// PackageNode methods
-func (x *PackageNode) DefineGoNativeType(rt reflect.Type) {
- if debug {
- debug.Printf("*PackageNode.DefineGoNativeType(%s)\n", rt.String())
- }
- pkgp := rt.PkgPath()
- if pkgp == "" {
- // DefineGoNativeType can only work with defined exported types.
- // Unexported types should be composed, and primitive types
- // should just use Gno types.
- panic(fmt.Sprintf(
- "reflect.Type %s has no package path",
- rt.String()))
- }
- name := rt.Name()
- if name == "" {
- panic(fmt.Sprintf(
- "reflect.Type %s is not named",
- rt.String()))
- }
- if rt.PkgPath() == "" {
- panic(fmt.Sprintf(
- "reflect.Type %s is not defined/exported",
- rt.String()))
- }
- nt := &NativeType{Type: rt}
- x.Define(Name(name), asValue(nt))
-}
-
func (x *PackageNode) DefineGoNativeValue(name Name, nv interface{}) {
x.defineGoNativeValue(false, name, nv)
}
diff --git a/gnovm/pkg/gnolang/gonative_test.go b/gnovm/pkg/gnolang/gonative_test.go
deleted file mode 100644
index fa5415a8068..00000000000
--- a/gnovm/pkg/gnolang/gonative_test.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package gnolang
-
-import (
- "bytes"
- "fmt"
- "reflect"
- "testing"
-
- "github.com/gnolang/gno/tm2/pkg/crypto"
- "github.com/stretchr/testify/assert"
-)
-
-// args is an even number of elements,
-// the even index items are package nodes,
-// and the odd index items are corresponding package values.
-func gonativeTestStore(args ...interface{}) Store {
- store := NewStore(nil, nil, nil)
- store.SetPackageGetter(func(pkgPath string, _ Store) (*PackageNode, *PackageValue) {
- for i := 0; i < len(args)/2; i++ {
- pn := args[i*2].(*PackageNode)
- pv := args[i*2+1].(*PackageValue)
- if pkgPath == pv.PkgPath {
- return pn, pv
- }
- }
- return nil, nil
- })
- store.SetStrictGo2GnoMapping(false)
- return store
-}
-
-type Foo struct {
- A int
- B int32
- C int64
- D string
-}
-
-func TestGoNativeDefine(t *testing.T) {
- // Create package foo and define Foo.
- pkg := NewPackageNode("foo", "test.foo", nil)
- rt := reflect.TypeOf(Foo{})
- pkg.DefineGoNativeType(rt)
- nt := pkg.GetValueRef(nil, Name("Foo"), true).GetType().(*NativeType)
- assert.Equal(t, rt, nt.Type)
- path := pkg.GetPathForName(nil, Name("Foo"))
- assert.Equal(t, uint8(1), path.Depth)
- assert.Equal(t, uint16(0), path.Index)
- pv := pkg.NewPackage()
- nt = pv.GetBlock(nil).GetPointerTo(nil, path).TV.GetType().(*NativeType)
- assert.Equal(t, rt, nt.Type)
- store := gonativeTestStore(pkg, pv)
-
- // Import above package and evaluate foo.Foo.
- m := NewMachineWithOptions(MachineOptions{
- PkgPath: "test",
- Store: store,
- })
- m.RunDeclaration(ImportD("foo", "test.foo"))
- tvs := m.Eval(Sel(Nx("foo"), "Foo"))
- assert.Equal(t, 1, len(tvs))
- assert.Equal(t, nt, tvs[0].V.(TypeValue).Type)
-}
-
-func TestGoNativeDefine2(t *testing.T) {
- // Create package foo and define Foo.
- pkg := NewPackageNode("foo", "test.foo", nil)
- rt := reflect.TypeOf(Foo{})
- pkg.DefineGoNativeType(rt)
- pv := pkg.NewPackage()
- store := gonativeTestStore(pkg, pv)
-
- // Import above package and run file.
- out := new(bytes.Buffer)
- m := NewMachineWithOptions(MachineOptions{
- PkgPath: "main",
- Output: out,
- Store: store,
- })
-
- c := `package main
-import foo "test.foo"
-func main() {
- f := foo.Foo{A:1}
- println("A:", f.A)
- println("B:", f.B)
- println("C:", f.C)
- println("D:", f.D)
-}`
- n := MustParseFile("main.go", c)
- m.RunFiles(n)
- m.RunMain()
- // weird `+` is used to place a space, without having editors strip it away.
- assert.Equal(t, `A: 1
-B: 0
-C: 0
-D: `+`
-`, string(out.Bytes()))
-}
-
-func TestGoNativeDefine3(t *testing.T) {
- t.Parallel()
-
- // Create package foo and define Foo.
- out := new(bytes.Buffer)
- pkg := NewPackageNode("foo", "test.foo", nil)
- pkg.DefineGoNativeType(reflect.TypeOf(Foo{}))
- pkg.DefineGoNativeValue("PrintFoo", func(f Foo) {
- out.Write([]byte(fmt.Sprintf("A: %v\n", f.A)))
- out.Write([]byte(fmt.Sprintf("B: %v\n", f.B)))
- out.Write([]byte(fmt.Sprintf("C: %v\n", f.C)))
- out.Write([]byte(fmt.Sprintf("D: %v\n", f.D)))
- })
- pv := pkg.NewPackage()
- store := gonativeTestStore(pkg, pv)
-
- // Import above package and run file.
- m := NewMachineWithOptions(MachineOptions{
- PkgPath: "main",
- Output: out,
- Store: store,
- })
-
- c := `package main
-import foo "test.foo"
-func main() {
- f := foo.Foo{A:1}
- foo.PrintFoo(f)
-}`
- n := MustParseFile("main.go", c)
- m.RunFiles(n)
- m.RunMain()
- assert.Equal(t, `A: 1
-B: 0
-C: 0
-D: `+`
-`, out.String())
-}
-
-func TestCrypto(t *testing.T) {
- t.Parallel()
-
- addr := crypto.Address{}
- store := gonativeTestStore()
- tv := Go2GnoValue(nilAllocator, store, reflect.ValueOf(addr))
- assert.Equal(t,
- `(array[0x0000000000000000000000000000000000000000] github.com/gnolang/gno/tm2/pkg/crypto.Address)`,
- tv.String())
-}
diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go
index e341ef8e9f1..4f4c7c188f3 100644
--- a/gnovm/pkg/gnolang/machine.go
+++ b/gnovm/pkg/gnolang/machine.go
@@ -3,7 +3,6 @@ package gnolang
// XXX rename file to machine.go.
import (
- "encoding/json"
"fmt"
"io"
"reflect"
@@ -11,7 +10,6 @@ import (
"strconv"
"strings"
"sync"
- "testing"
"github.com/gnolang/overflow"
@@ -299,7 +297,7 @@ func (m *Machine) runMemPackage(memPkg *gnovm.MemPackage, save, overrides bool)
}
m.SetActivePackage(pv)
// run files.
- updates := m.RunFileDecls(files.Files...)
+ updates := m.runFileDecls(files.Files...)
// save package value and mempackage.
// XXX save condition will be removed once gonative is removed.
var throwaway *Realm
@@ -400,103 +398,6 @@ func destar(x Expr) Expr {
return x
}
-// Tests all test files in a mempackage.
-// Assumes that the importing of packages is handled elsewhere.
-// The resulting package value and node become injected with TestMethods and
-// other declarations, so it is expected that non-test code will not be run
-// afterwards from the same store.
-func (m *Machine) TestMemPackage(t *testing.T, memPkg *gnovm.MemPackage) {
- defer m.injectLocOnPanic()
- DisableDebug()
- fmt.Println("DEBUG DISABLED (FOR TEST DEPENDENCIES INIT)")
- // parse test files.
- tfiles, itfiles := ParseMemPackageTests(memPkg)
- { // first, tfiles which run in the same package.
- pv := m.Store.GetPackage(memPkg.Path, false)
- pvBlock := pv.GetBlock(m.Store)
- pvSize := len(pvBlock.Values)
- m.SetActivePackage(pv)
- // run test files.
- m.RunFiles(tfiles.Files...)
- // run all tests in test files.
- for i := pvSize; i < len(pvBlock.Values); i++ {
- tv := pvBlock.Values[i]
- m.TestFunc(t, tv)
- }
- }
- { // run all (import) tests in test files.
- pn := NewPackageNode(Name(memPkg.Name+"_test"), memPkg.Path+"_test", itfiles)
- pv := pn.NewPackage()
- m.Store.SetBlockNode(pn)
- m.Store.SetCachePackage(pv)
- pvBlock := pv.GetBlock(m.Store)
- m.SetActivePackage(pv)
- m.RunFiles(itfiles.Files...)
- pn.PrepareNewValues(pv)
- EnableDebug()
- fmt.Println("DEBUG ENABLED")
- for i := 0; i < len(pvBlock.Values); i++ {
- tv := pvBlock.Values[i]
- m.TestFunc(t, tv)
- }
- }
-}
-
-// TestFunc calls tv with testing.RunTest, if tv is a function with a name that
-// starts with `Test`.
-func (m *Machine) TestFunc(t *testing.T, tv TypedValue) {
- if !(tv.T.Kind() == FuncKind &&
- strings.HasPrefix(string(tv.V.(*FuncValue).Name), "Test")) {
- return // not a test function.
- }
- // XXX ensure correct func type.
- name := string(tv.V.(*FuncValue).Name)
- // prefetch the testing package.
- testingpv := m.Store.GetPackage("testing", false)
- testingtv := TypedValue{T: gPackageType, V: testingpv}
- testingcx := &ConstExpr{TypedValue: testingtv}
-
- t.Run(name, func(t *testing.T) {
- defer m.injectLocOnPanic()
- x := Call(
- Sel(testingcx, "RunTest"), // Call testing.RunTest
- Str(name), // First param, the name of the test
- X("true"), // Second Param, verbose bool
- &CompositeLitExpr{ // Third param, the testing.InternalTest
- Type: Sel(testingcx, "InternalTest"),
- Elts: KeyValueExprs{
- {Key: X("Name"), Value: Str(name)},
- {Key: X("F"), Value: X(name)},
- },
- },
- )
- res := m.Eval(x)
- ret := res[0].GetString()
- if ret == "" {
- t.Errorf("failed to execute unit test: %q", name)
- return
- }
-
- // mirror of stdlibs/testing.Report
- var report struct {
- Skipped bool
- Failed bool
- }
- err := json.Unmarshal([]byte(ret), &report)
- if err != nil {
- t.Errorf("failed to parse test output %q", name)
- return
- }
-
- switch {
- case report.Skipped:
- t.SkipNow()
- case report.Failed:
- t.Fail()
- }
- })
-}
-
// Stacktrace returns the stack trace of the machine.
// It collects the executions and frames from the machine's frames and statements.
func (m *Machine) Stacktrace() (stacktrace Stacktrace) {
@@ -534,58 +435,6 @@ func (m *Machine) Stacktrace() (stacktrace Stacktrace) {
return
}
-// in case of panic, inject location information to exception.
-func (m *Machine) injectLocOnPanic() {
- if r := recover(); r != nil {
- // Show last location information.
- // First, determine the line number of expression or statement if any.
- lastLine := 0
- lastColumn := 0
- if len(m.Exprs) > 0 {
- for i := len(m.Exprs) - 1; i >= 0; i-- {
- expr := m.Exprs[i]
- if expr.GetLine() > 0 {
- lastLine = expr.GetLine()
- lastColumn = expr.GetColumn()
- break
- }
- }
- }
- if lastLine == 0 && len(m.Stmts) > 0 {
- for i := len(m.Stmts) - 1; i >= 0; i-- {
- stmt := m.Stmts[i]
- if stmt.GetLine() > 0 {
- lastLine = stmt.GetLine()
- lastColumn = stmt.GetColumn()
- break
- }
- }
- }
- // Append line number to block location.
- lastLoc := Location{}
- for i := len(m.Blocks) - 1; i >= 0; i-- {
- block := m.Blocks[i]
- src := block.GetSource(m.Store)
- loc := src.GetLocation()
- if !loc.IsZero() {
- lastLoc = loc
- if lastLine > 0 {
- lastLoc.Line = lastLine
- lastLoc.Column = lastColumn
- }
- break
- }
- }
- // wrap panic with location information.
- if !lastLoc.IsZero() {
- fmt.Printf("%s: %v\n", lastLoc.String(), r)
- panic(errors.Wrap(r, fmt.Sprintf("location: %s", lastLoc.String())))
- } else {
- panic(r)
- }
- }
-}
-
// Convenience for tests.
// Production must not use this, because realm package init
// must happen after persistence and realm finalization,
@@ -602,10 +451,6 @@ func (m *Machine) RunFiles(fns ...*FileNode) {
// Add files to the package's *FileSet and run decls in them.
// This will also run each init function encountered.
// Returns the updated typed values of package.
-func (m *Machine) RunFileDecls(fns ...*FileNode) []TypedValue {
- return m.runFileDecls(fns...)
-}
-
func (m *Machine) runFileDecls(fns ...*FileNode) []TypedValue {
// Files' package names must match the machine's active one.
// if there is one.
diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go
index 45062f8e14c..dcc1ad41739 100644
--- a/gnovm/pkg/gnolang/nodes.go
+++ b/gnovm/pkg/gnolang/nodes.go
@@ -13,7 +13,6 @@ import (
"strings"
"github.com/gnolang/gno/gnovm"
- "github.com/gnolang/gno/tm2/pkg/errors"
"go.uber.org/multierr"
)
@@ -1257,38 +1256,6 @@ func ParseMemPackage(memPkg *gnovm.MemPackage) (fset *FileSet) {
return fset
}
-func ParseMemPackageTests(memPkg *gnovm.MemPackage) (tset, itset *FileSet) {
- tset = &FileSet{}
- itset = &FileSet{}
- for _, mfile := range memPkg.Files {
- if !strings.HasSuffix(mfile.Name, ".gno") {
- continue // skip this file.
- }
- n, err := ParseFile(mfile.Name, mfile.Body)
- if err != nil {
- panic(errors.Wrap(err, "parsing file "+mfile.Name))
- }
- if n == nil {
- panic("should not happen")
- }
- if strings.HasSuffix(mfile.Name, "_test.gno") {
- // add test file.
- if memPkg.Name+"_test" == string(n.PkgName) {
- itset.AddFiles(n)
- } else {
- tset.AddFiles(n)
- }
- } else if memPkg.Name == string(n.PkgName) {
- // skip package file.
- } else {
- panic(fmt.Sprintf(
- "expected package name [%s] or [%s_test] but got [%s] file [%s]",
- memPkg.Name, memPkg.Name, n.PkgName, mfile))
- }
- }
- return tset, itset
-}
-
func (fs *FileSet) AddFiles(fns ...*FileNode) {
fs.Files = append(fs.Files, fns...)
}
diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go
index 7198d4f6a98..4b556604f0b 100644
--- a/gnovm/pkg/gnolang/preprocess.go
+++ b/gnovm/pkg/gnolang/preprocess.go
@@ -358,6 +358,11 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) {
func doRecover(stack []BlockNode, n Node) {
if r := recover(); r != nil {
+ if _, ok := r.(*PreprocessError); ok {
+ // re-panic directly if this is a PreprocessError already.
+ panic(r)
+ }
+
// before re-throwing the error, append location information to message.
last := stack[len(stack)-1]
loc := last.GetLocation()
@@ -4018,23 +4023,7 @@ func checkIntegerKind(xt Type) {
// preprocess-able before other file-level declarations are
// preprocessed).
func predefineNow(store Store, last BlockNode, d Decl) (Decl, bool) {
- defer func() {
- if r := recover(); r != nil {
- // before re-throwing the error, append location information to message.
- loc := last.GetLocation()
- if nline := d.GetLine(); nline > 0 {
- loc.Line = nline
- loc.Column = d.GetColumn()
- }
- if rerr, ok := r.(error); ok {
- // NOTE: gotuna/gorilla expects error exceptions.
- panic(errors.Wrap(rerr, loc.String()))
- } else {
- // NOTE: gotuna/gorilla expects error exceptions.
- panic(fmt.Errorf("%s: %v", loc.String(), r))
- }
- }
- }()
+ defer doRecover([]BlockNode{last}, d)
stack := &[]Name{}
return predefineNow2(store, last, d, stack)
}
@@ -4099,10 +4088,10 @@ func predefineNow2(store Store, last BlockNode, d Decl, stack *[]Name) (Decl, bo
// check base type of receiver type, should not be pointer type or interface type
assertValidReceiverType := func(t Type) {
if _, ok := t.(*PointerType); ok {
- panic(fmt.Sprintf("invalid receiver type %v (base type is pointer type)\n", rt))
+ panic(fmt.Sprintf("invalid receiver type %v (base type is pointer type)", rt))
}
if _, ok := t.(*InterfaceType); ok {
- panic(fmt.Sprintf("invalid receiver type %v (base type is interface type)\n", rt))
+ panic(fmt.Sprintf("invalid receiver type %v (base type is interface type)", rt))
}
}
diff --git a/gnovm/pkg/gnolang/preprocess_test.go b/gnovm/pkg/gnolang/preprocess_test.go
deleted file mode 100644
index 53ad97dd972..00000000000
--- a/gnovm/pkg/gnolang/preprocess_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package gnolang
-
-import (
- "fmt"
- "reflect"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestPreprocess_BinaryExpressionOneNative(t *testing.T) {
- pn := NewPackageNode("time", "time", nil)
- pn.DefineGoNativeConstValue("Millisecond", time.Millisecond)
- pn.DefineGoNativeConstValue("Second", time.Second)
- pn.DefineGoNativeType(reflect.TypeOf(time.Duration(0)))
- pv := pn.NewPackage()
- store := gonativeTestStore(pn, pv)
- store.SetBlockNode(pn)
-
- const src = `package main
- import "time"
-func main() {
- var a int64 = 2
- println(time.Second * a)
-
-}`
- n := MustParseFile("main.go", src)
-
- defer func() {
- err := recover()
- assert.Contains(t, fmt.Sprint(err), "incompatible operands in binary expression")
- }()
- initStaticBlocks(store, pn, n)
- Preprocess(store, pn, n)
-}
-
-func TestPreprocess_BinaryExpressionBothNative(t *testing.T) {
- pn := NewPackageNode("time", "time", nil)
- pn.DefineGoNativeConstValue("March", time.March)
- pn.DefineGoNativeConstValue("Wednesday", time.Wednesday)
- pn.DefineGoNativeType(reflect.TypeOf(time.Month(0)))
- pn.DefineGoNativeType(reflect.TypeOf(time.Weekday(0)))
- pv := pn.NewPackage()
- store := gonativeTestStore(pn, pv)
- store.SetBlockNode(pn)
-
- const src = `package main
- import "time"
-func main() {
- println(time.March * time.Wednesday)
-
-}`
- n := MustParseFile("main.go", src)
-
- defer func() {
- err := recover()
- assert.Contains(t, fmt.Sprint(err), "incompatible operands in binary expression")
- }()
- initStaticBlocks(store, pn, n)
- Preprocess(store, pn, n)
-}
diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go
index 9410eede29e..2c0ee05a1d7 100644
--- a/gnovm/pkg/gnolang/store.go
+++ b/gnovm/pkg/gnolang/store.go
@@ -51,7 +51,6 @@ type Store interface {
GetBlockNodeSafe(Location) BlockNode
SetBlockNode(BlockNode)
// UNSTABLE
- SetStrictGo2GnoMapping(bool)
Go2GnoType(rt reflect.Type) Type
GetAllocator() *Allocator
NumMemPackages() int64
@@ -68,7 +67,6 @@ type Store interface {
SetLogStoreOps(enabled bool)
SprintStoreOps() string
LogSwitchRealm(rlmpath string) // to mark change of realm boundaries
- ClearCache()
Print()
}
@@ -98,7 +96,6 @@ type defaultStore struct {
pkgGetter PackageGetter // non-realm packages
cacheNativeTypes map[reflect.Type]Type // reflect doc: reflect.Type are comparable
nativeStore NativeStore // for injecting natives
- go2gnoStrict bool // if true, native->gno type conversion must be registered.
// transient
opslog []StoreOp // for debugging and testing.
@@ -120,7 +117,6 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore
pkgGetter: nil,
cacheNativeTypes: make(map[reflect.Type]Type),
nativeStore: nil,
- go2gnoStrict: true,
}
InitStoreCaches(ds)
return ds
@@ -149,7 +145,6 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans
pkgGetter: ds.pkgGetter,
cacheNativeTypes: ds.cacheNativeTypes,
nativeStore: ds.nativeStore,
- go2gnoStrict: ds.go2gnoStrict,
// transient
current: nil,
@@ -171,10 +166,6 @@ func (transactionStore) SetPackageGetter(pg PackageGetter) {
panic("SetPackageGetter may not be called in a transaction store")
}
-func (transactionStore) ClearCache() {
- panic("ClearCache may not be called in a transaction store")
-}
-
// XXX: we should block Go2GnoType, because it uses a global cache map;
// but it's called during preprocess and thus breaks some testing code.
// let's wait until we remove Go2Gno entirely.
@@ -187,10 +178,6 @@ func (transactionStore) SetNativeStore(ns NativeStore) {
panic("SetNativeStore may not be called in a transaction store")
}
-func (transactionStore) SetStrictGo2GnoMapping(strict bool) {
- panic("SetStrictGo2GnoMapping may not be called in a transaction store")
-}
-
// CopyCachesFromStore allows to copy a store's internal object, type and
// BlockNode cache into the dst store.
// This is mostly useful for testing, where many stores have to be initialized.
@@ -498,8 +485,7 @@ func (ds *defaultStore) SetCacheType(tt Type) {
tid := tt.TypeID()
if tt2, exists := ds.cacheTypes.Get(tid); exists {
if tt != tt2 {
- // NOTE: not sure why this would happen.
- panic("should not happen")
+ panic(fmt.Sprintf("cannot re-register %q with different type", tid))
} else {
// already set.
}
@@ -778,15 +764,6 @@ func (ds *defaultStore) LogSwitchRealm(rlmpath string) {
StoreOp{Type: StoreOpSwitchRealm, RlmPath: rlmpath})
}
-func (ds *defaultStore) ClearCache() {
- ds.cacheObjects = make(map[ObjectID]Object)
- ds.cacheTypes = txlog.GoMap[TypeID, Type](map[TypeID]Type{})
- ds.cacheNodes = txlog.GoMap[Location, BlockNode](map[Location]BlockNode{})
- ds.cacheNativeTypes = make(map[reflect.Type]Type)
- // restore builtin types to cache.
- InitStoreCaches(ds)
-}
-
// for debugging
func (ds *defaultStore) Print() {
fmt.Println(colors.Yellow("//----------------------------------------"))
diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go
index 40f84b65375..f7f03b947f6 100644
--- a/gnovm/pkg/gnolang/store_test.go
+++ b/gnovm/pkg/gnolang/store_test.go
@@ -58,9 +58,7 @@ func TestTransactionStore_blockedMethods(t *testing.T) {
// These methods should panic as they modify store settings, which should
// only be changed in the root store.
assert.Panics(t, func() { transactionStore{}.SetPackageGetter(nil) })
- assert.Panics(t, func() { transactionStore{}.ClearCache() })
assert.Panics(t, func() { transactionStore{}.SetNativeStore(nil) })
- assert.Panics(t, func() { transactionStore{}.SetStrictGo2GnoMapping(false) })
}
func TestCopyFromCachedStore(t *testing.T) {
diff --git a/gnovm/pkg/repl/repl.go b/gnovm/pkg/repl/repl.go
index e7b5ecea96f..b0944d21646 100644
--- a/gnovm/pkg/repl/repl.go
+++ b/gnovm/pkg/repl/repl.go
@@ -13,8 +13,9 @@ import (
"os"
"text/template"
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/gnovm/tests"
+ "github.com/gnolang/gno/gnovm/pkg/test"
)
const (
@@ -124,7 +125,8 @@ func NewRepl(opts ...ReplOption) *Repl {
r.stderr = &b
r.storeFunc = func() gno.Store {
- return tests.TestStore("teststore", "", r.stdin, r.stdout, r.stderr, tests.ImportModeStdlibsOnly)
+ _, st := test.Store(gnoenv.RootDir(), false, r.stdin, r.stdout, r.stderr)
+ return st
}
for _, o := range opts {
diff --git a/gnovm/pkg/test/filetest.go b/gnovm/pkg/test/filetest.go
new file mode 100644
index 00000000000..12bc9ed7f28
--- /dev/null
+++ b/gnovm/pkg/test/filetest.go
@@ -0,0 +1,407 @@
+package test
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "regexp"
+ "runtime/debug"
+ "strconv"
+ "strings"
+
+ "github.com/gnolang/gno/gnovm"
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/pmezard/go-difflib/difflib"
+ "go.uber.org/multierr"
+)
+
+// RunFiletest executes the program in source as a filetest.
+// If opts.Sync is enabled, and the filetest's golden output has changed,
+// the first string is set to the new generated content of the file.
+func (opts *TestOptions) RunFiletest(filename string, source []byte) (string, error) {
+ opts.outWriter.w = opts.Output
+
+ return opts.runFiletest(filename, source)
+}
+
+var reEndOfLineSpaces = func() *regexp.Regexp {
+ re := regexp.MustCompile(" +\n")
+ re.Longest()
+ return re
+}()
+
+func (opts *TestOptions) runFiletest(filename string, source []byte) (string, error) {
+ dirs, err := ParseDirectives(bytes.NewReader(source))
+ if err != nil {
+ return "", fmt.Errorf("error parsing directives: %w", err)
+ }
+
+ // Initialize Machine.Context and Machine.Alloc according to the input directives.
+ pkgPath := dirs.FirstDefault(DirectivePkgPath, "main")
+ coins, err := std.ParseCoins(dirs.FirstDefault(DirectiveSend, ""))
+ if err != nil {
+ return "", err
+ }
+ ctx := Context(
+ pkgPath,
+ coins,
+ )
+ maxAllocRaw := dirs.FirstDefault(DirectiveMaxAlloc, "0")
+ maxAlloc, err := strconv.ParseInt(maxAllocRaw, 10, 64)
+ if err != nil {
+ return "", fmt.Errorf("could not parse MAXALLOC directive: %w", err)
+ }
+
+ // Create machine for execution and run test
+ cw := opts.BaseStore.CacheWrap()
+ m := gno.NewMachineWithOptions(gno.MachineOptions{
+ Output: &opts.outWriter,
+ Store: opts.TestStore.BeginTransaction(cw, cw),
+ Context: ctx,
+ MaxAllocBytes: maxAlloc,
+ })
+ defer m.Release()
+ result := opts.runTest(m, pkgPath, filename, source)
+
+ // updated tells whether the directives have been updated, and as such
+ // a new generated filetest should be returned.
+ // returnErr is used as the return value, and may be a MultiError if
+ // multiple mismatches occurred.
+ updated := false
+ var returnErr error
+ // match verifies the content against dir.Content; if different,
+ // either updates dir.Content (for opts.Sync) or appends a new returnErr.
+ match := func(dir *Directive, actual string) {
+ // Remove end-of-line spaces, as these are removed from `fmt` in the filetests anyway.
+ actual = reEndOfLineSpaces.ReplaceAllString(actual, "\n")
+ if dir.Content != actual {
+ if opts.Sync {
+ dir.Content = actual
+ updated = true
+ } else {
+ returnErr = multierr.Append(
+ returnErr,
+ fmt.Errorf("%s diff:\n%s", dir.Name, unifiedDiff(dir.Content, actual)),
+ )
+ }
+ }
+ }
+
+ // First, check if we have an error, whether we're supposed to get it.
+ if result.Error != "" {
+ // Ensure this error was supposed to happen.
+ errDirective := dirs.First(DirectiveError)
+ if errDirective == nil {
+ return "", fmt.Errorf("unexpected panic: %s\noutput:\n%s\nstack:\n%v",
+ result.Error, result.Output, string(result.GoPanicStack))
+ }
+
+ // The Error directive (and many others) will have one trailing newline,
+ // which is not in the output - so add it there.
+ match(errDirective, result.Error+"\n")
+ } else {
+ err = m.CheckEmpty()
+ if err != nil {
+ return "", fmt.Errorf("machine not empty after main: %w", err)
+ }
+ if gno.HasDebugErrors() {
+ return "", fmt.Errorf("got unexpected debug error(s): %v", gno.GetDebugErrors())
+ }
+ }
+
+ // Check through each directive and verify it against the values from the test.
+ for idx := range dirs {
+ dir := &dirs[idx]
+ switch dir.Name {
+ case DirectiveOutput:
+ if !strings.HasSuffix(result.Output, "\n") {
+ result.Output += "\n"
+ }
+ match(dir, result.Output)
+ case DirectiveRealm:
+ sops := m.Store.SprintStoreOps() + "\n"
+ match(dir, sops)
+ case DirectiveEvents:
+ events := m.Context.(*teststd.TestExecContext).EventLogger.Events()
+ evtjson, err := json.MarshalIndent(events, "", " ")
+ if err != nil {
+ panic(err)
+ }
+ evtstr := string(evtjson) + "\n"
+ match(dir, evtstr)
+ case DirectivePreprocessed:
+ pn := m.Store.GetBlockNode(gno.PackageNodeLocation(pkgPath))
+ pre := pn.(*gno.PackageNode).FileSet.Files[0].String() + "\n"
+ match(dir, pre)
+ case DirectiveStacktrace:
+ match(dir, result.GnoStacktrace)
+ }
+ }
+
+ if updated { // only true if sync == true
+ return dirs.FileTest(), returnErr
+ }
+
+ return "", returnErr
+}
+
+func unifiedDiff(wanted, actual string) string {
+ diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
+ A: difflib.SplitLines(wanted),
+ B: difflib.SplitLines(actual),
+ FromFile: "Expected",
+ FromDate: "",
+ ToFile: "Actual",
+ ToDate: "",
+ Context: 1,
+ })
+ if err != nil {
+ panic(fmt.Errorf("error generating unified diff: %w", err))
+ }
+ return diff
+}
+
+type runResult struct {
+ Output string
+ Error string
+ // Set if there was a panic within gno code.
+ GnoStacktrace string
+ // Set if this was recovered from a panic.
+ GoPanicStack []byte
+}
+
+func (opts *TestOptions) runTest(m *gno.Machine, pkgPath, filename string, content []byte) (rr runResult) {
+ // Eagerly load imports.
+ // This is executed using opts.Store, rather than the transaction store;
+ // it allows us to only have to load the imports once (and re-use the cached
+ // versions). Running the tests in separate "transactions" means that they
+ // don't get the parent store dirty.
+ if err := LoadImports(opts.TestStore, filename, content); err != nil {
+ // NOTE: we perform this here, so we can capture the runResult.
+ return runResult{Error: err.Error()}
+ }
+
+ // Reset and start capturing stdout.
+ opts.filetestBuffer.Reset()
+ revert := opts.outWriter.tee(&opts.filetestBuffer)
+ defer revert()
+
+ defer func() {
+ if r := recover(); r != nil {
+ rr.Output = opts.filetestBuffer.String()
+ rr.GoPanicStack = debug.Stack()
+ switch v := r.(type) {
+ case *gno.TypedValue:
+ rr.Error = v.Sprint(m)
+ case *gno.PreprocessError:
+ rr.Error = v.Unwrap().Error()
+ case gno.UnhandledPanicError:
+ rr.Error = v.Error()
+ rr.GnoStacktrace = m.ExceptionsStacktrace()
+ default:
+ rr.Error = fmt.Sprint(v)
+ rr.GnoStacktrace = m.Stacktrace().String()
+ }
+ }
+ }()
+
+ // Use last element after / (works also if slash is missing).
+ pkgName := gno.Name(pkgPath[strings.LastIndexByte(pkgPath, '/')+1:])
+ if !gno.IsRealmPath(pkgPath) {
+ // Simple case - pure package.
+ pn := gno.NewPackageNode(pkgName, pkgPath, &gno.FileSet{})
+ pv := pn.NewPackage()
+ m.Store.SetBlockNode(pn)
+ m.Store.SetCachePackage(pv)
+ m.SetActivePackage(pv)
+ n := gno.MustParseFile(filename, string(content))
+ m.RunFiles(n)
+ m.RunStatement(gno.S(gno.Call(gno.X("main"))))
+ } else {
+ // Realm case.
+ gno.DisableDebug() // until main call.
+
+ // Remove filetest from name, as that can lead to the package not being
+ // parsed correctly when using RunMemPackage.
+ filename = strings.ReplaceAll(filename, "_filetest", "")
+
+ // save package using realm crawl procedure.
+ memPkg := &gnovm.MemPackage{
+ Name: string(pkgName),
+ Path: pkgPath,
+ Files: []*gnovm.MemFile{
+ {
+ Name: filename,
+ Body: string(content),
+ },
+ },
+ }
+ orig, tx := m.Store, m.Store.BeginTransaction(nil, nil)
+ m.Store = tx
+ // Run decls and init functions.
+ m.RunMemPackage(memPkg, true)
+ // Clear store cache and reconstruct machine from committed info
+ // (mimicking on-chain behaviour).
+ tx.Write()
+ m.Store = orig
+
+ pv2 := m.Store.GetPackage(pkgPath, false)
+ m.SetActivePackage(pv2)
+ gno.EnableDebug()
+ // clear store.opslog from init function(s).
+ m.Store.SetLogStoreOps(true) // resets.
+ m.RunStatement(gno.S(gno.Call(gno.X("main"))))
+ }
+
+ return runResult{
+ Output: opts.filetestBuffer.String(),
+ GnoStacktrace: m.Stacktrace().String(),
+ }
+}
+
+// ---------------------------------------
+// directives and directive parsing
+
+const (
+ // These directives are used to set input variables, which should change for
+ // the specific filetest. They must be specified on a single line.
+ DirectivePkgPath = "PKGPATH"
+ DirectiveMaxAlloc = "MAXALLOC"
+ DirectiveSend = "SEND"
+
+ // These are used to match the result of the filetest against known golden
+ // values.
+ DirectiveOutput = "Output"
+ DirectiveError = "Error"
+ DirectiveRealm = "Realm"
+ DirectiveEvents = "Events"
+ DirectivePreprocessed = "Preprocessed"
+ DirectiveStacktrace = "Stacktrace"
+)
+
+// Directives contains the directives of a file.
+// It may also contains directives with empty names, to indicate parts of the
+// original source file (used to re-construct the filetest at the end).
+type Directives []Directive
+
+// First returns the first directive with the corresponding name.
+func (d Directives) First(name string) *Directive {
+ if name == "" {
+ return nil
+ }
+ for i := range d {
+ if d[i].Name == name {
+ return &d[i]
+ }
+ }
+ return nil
+}
+
+// FirstDefault returns the [Directive.Content] of First(name); if First(name)
+// returns nil, then defaultValue is returned.
+func (d Directives) FirstDefault(name, defaultValue string) string {
+ v := d.First(name)
+ if v == nil {
+ return defaultValue
+ }
+ return v.Content
+}
+
+// FileTest re-generates the filetest from the given directives; the inverse of ParseDirectives.
+func (d Directives) FileTest() string {
+ var bld strings.Builder
+ for _, dir := range d {
+ switch {
+ case dir.Name == "":
+ bld.WriteString(dir.Content)
+ case strings.ToUpper(dir.Name) == dir.Name: // is it all uppercase?
+ bld.WriteString("// " + dir.Name + ": " + dir.Content + "\n")
+ default:
+ bld.WriteString("// " + dir.Name + ":\n")
+ cnt := strings.TrimSuffix(dir.Content, "\n")
+ lines := strings.Split(cnt, "\n")
+ for _, line := range lines {
+ if line == "" {
+ bld.WriteString("//\n")
+ continue
+ }
+ bld.WriteString("// ")
+ bld.WriteString(strings.TrimRight(line, " "))
+ bld.WriteByte('\n')
+ }
+ }
+ }
+ return bld.String()
+}
+
+// Directive represents a directive in a filetest.
+// A [Directives] slice may also contain directives with empty Names:
+// these compose the source file itself, and are used to re-construct the file
+// when a directive is changed.
+type Directive struct {
+ Name string
+ Content string
+}
+
+// Allows either a `ALLCAPS: content` on a single line, or a `PascalCase:`,
+// with content on the following lines.
+var reDirectiveLine = regexp.MustCompile("^(?:([A-Z][a-z]*):|([A-Z]+): ?(.*))$")
+
+// ParseDirectives parses all the directives in the filetest given at source.
+func ParseDirectives(source io.Reader) (Directives, error) {
+ sc := bufio.NewScanner(source)
+ parsed := make(Directives, 0, 8)
+ for sc.Scan() {
+ // Re-append trailing newline.
+ // Useful as we always use it anyway.
+ txt := sc.Text() + "\n"
+ if !strings.HasPrefix(txt, "//") {
+ if len(parsed) == 0 || parsed[len(parsed)-1].Name != "" {
+ parsed = append(parsed, Directive{Content: txt})
+ continue
+ }
+ parsed[len(parsed)-1].Content += txt
+ continue
+ }
+
+ comment := txt[2 : len(txt)-1] // leading double slash, trailing \n
+ comment = strings.TrimPrefix(comment, " ") // leading space (if any)
+
+ // If we're already in a directive, simply append there.
+ if len(parsed) > 0 && parsed[len(parsed)-1].Name != "" {
+ parsed[len(parsed)-1].Content += comment + "\n"
+ continue
+ }
+
+ // Find if there is a colon (indicating a possible directive).
+ subm := reDirectiveLine.FindStringSubmatch(comment)
+ switch {
+ case subm == nil:
+ // Not found; append to parsed as a line, or to the previous
+ // directive if it exists.
+ if len(parsed) == 0 {
+ parsed = append(parsed, Directive{Content: txt})
+ continue
+ }
+ last := &parsed[len(parsed)-1]
+ if last.Name == "" {
+ last.Content += txt
+ } else {
+ last.Content += comment + "\n"
+ }
+ case subm[1] != "": // output directive, with content on newlines
+ parsed = append(parsed, Directive{Name: subm[1]})
+ default: // subm[2] != "", all caps
+ parsed = append(parsed,
+ Directive{Name: subm[2], Content: subm[3]},
+ // enforce new directive later
+ Directive{},
+ )
+ }
+ }
+ return parsed, sc.Err()
+}
diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go
new file mode 100644
index 00000000000..dabb5644cdd
--- /dev/null
+++ b/gnovm/pkg/test/imports.go
@@ -0,0 +1,265 @@
+package test
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "go/parser"
+ "go/token"
+ "io"
+ "math/big"
+ "os"
+ "path/filepath"
+ "runtime/debug"
+ "strconv"
+ "strings"
+ "time"
+
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ teststdlibs "github.com/gnolang/gno/gnovm/tests/stdlibs"
+ teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
+ "github.com/gnolang/gno/tm2/pkg/db/memdb"
+ osm "github.com/gnolang/gno/tm2/pkg/os"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ "github.com/gnolang/gno/tm2/pkg/store/dbadapter"
+ storetypes "github.com/gnolang/gno/tm2/pkg/store/types"
+)
+
+// NOTE: this isn't safe, should only be used for testing.
+func Store(
+ rootDir string,
+ withExtern bool,
+ stdin io.Reader,
+ stdout, stderr io.Writer,
+) (
+ baseStore storetypes.CommitStore,
+ resStore gno.Store,
+) {
+ getPackage := func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) {
+ if pkgPath == "" {
+ panic(fmt.Sprintf("invalid zero package path in testStore().pkgGetter"))
+ }
+
+ if withExtern {
+ // if _test package...
+ const testPath = "github.com/gnolang/gno/_test/"
+ if strings.HasPrefix(pkgPath, testPath) {
+ baseDir := filepath.Join(rootDir, "gnovm", "tests", "files", "extern", pkgPath[len(testPath):])
+ memPkg := gno.ReadMemPackage(baseDir, pkgPath)
+ send := std.Coins{}
+ ctx := Context(pkgPath, send)
+ m2 := gno.NewMachineWithOptions(gno.MachineOptions{
+ PkgPath: "test",
+ Output: stdout,
+ Store: store,
+ Context: ctx,
+ })
+ return m2.RunMemPackage(memPkg, true)
+ }
+ }
+
+ // gonative exceptions.
+ // These are values available using gonative; eventually they should all be removed.
+ switch pkgPath {
+ case "os":
+ pkg := gno.NewPackageNode("os", pkgPath, nil)
+ pkg.DefineGoNativeValue("Stdin", stdin)
+ pkg.DefineGoNativeValue("Stdout", stdout)
+ pkg.DefineGoNativeValue("Stderr", stderr)
+ return pkg, pkg.NewPackage()
+ case "fmt":
+ pkg := gno.NewPackageNode("fmt", pkgPath, nil)
+ pkg.DefineGoNativeValue("Println", func(a ...interface{}) (n int, err error) {
+ // NOTE: uncomment to debug long running tests
+ // fmt.Println(a...)
+ res := fmt.Sprintln(a...)
+ return stdout.Write([]byte(res))
+ })
+ pkg.DefineGoNativeValue("Printf", func(format string, a ...interface{}) (n int, err error) {
+ res := fmt.Sprintf(format, a...)
+ return stdout.Write([]byte(res))
+ })
+ pkg.DefineGoNativeValue("Print", func(a ...interface{}) (n int, err error) {
+ res := fmt.Sprint(a...)
+ return stdout.Write([]byte(res))
+ })
+ pkg.DefineGoNativeValue("Sprint", fmt.Sprint)
+ pkg.DefineGoNativeValue("Sprintf", fmt.Sprintf)
+ pkg.DefineGoNativeValue("Sprintln", fmt.Sprintln)
+ pkg.DefineGoNativeValue("Sscanf", fmt.Sscanf)
+ pkg.DefineGoNativeValue("Errorf", fmt.Errorf)
+ pkg.DefineGoNativeValue("Fprintln", fmt.Fprintln)
+ pkg.DefineGoNativeValue("Fprintf", fmt.Fprintf)
+ pkg.DefineGoNativeValue("Fprint", fmt.Fprint)
+ return pkg, pkg.NewPackage()
+ case "encoding/json":
+ pkg := gno.NewPackageNode("json", pkgPath, nil)
+ pkg.DefineGoNativeValue("Unmarshal", json.Unmarshal)
+ pkg.DefineGoNativeValue("Marshal", json.Marshal)
+ return pkg, pkg.NewPackage()
+ case "internal/os_test":
+ pkg := gno.NewPackageNode("os_test", pkgPath, nil)
+ pkg.DefineNative("Sleep",
+ gno.Flds( // params
+ "d", gno.AnyT(), // NOTE: should be time.Duration
+ ),
+ gno.Flds( // results
+ ),
+ func(m *gno.Machine) {
+ // For testing purposes here, nanoseconds are separately kept track.
+ arg0 := m.LastBlock().GetParams1().TV
+ d := arg0.GetInt64()
+ sec := d / int64(time.Second)
+ nano := d % int64(time.Second)
+ ctx := m.Context.(*teststd.TestExecContext)
+ ctx.Timestamp += sec
+ ctx.TimestampNano += nano
+ if ctx.TimestampNano >= int64(time.Second) {
+ ctx.Timestamp += 1
+ ctx.TimestampNano -= int64(time.Second)
+ }
+ m.Context = ctx
+ },
+ )
+ return pkg, pkg.NewPackage()
+ case "math/big":
+ pkg := gno.NewPackageNode("big", pkgPath, nil)
+ pkg.DefineGoNativeValue("NewInt", big.NewInt)
+ return pkg, pkg.NewPackage()
+ }
+
+ // Load normal stdlib.
+ pn, pv = loadStdlib(rootDir, pkgPath, store, stdout)
+ if pn != nil {
+ return
+ }
+
+ // if examples package...
+ examplePath := filepath.Join(rootDir, "examples", pkgPath)
+ if osm.DirExists(examplePath) {
+ memPkg := gno.ReadMemPackage(examplePath, pkgPath)
+ if memPkg.IsEmpty() {
+ panic(fmt.Sprintf("found an empty package %q", pkgPath))
+ }
+
+ send := std.Coins{}
+ ctx := Context(pkgPath, send)
+ m2 := gno.NewMachineWithOptions(gno.MachineOptions{
+ PkgPath: "test",
+ Output: stdout,
+ Store: store,
+ Context: ctx,
+ })
+ pn, pv = m2.RunMemPackage(memPkg, true)
+ return
+ }
+ return nil, nil
+ }
+ db := memdb.NewMemDB()
+ baseStore = dbadapter.StoreConstructor(db, storetypes.StoreOptions{})
+ // Make a new store.
+ resStore = gno.NewStore(nil, baseStore, baseStore)
+ resStore.SetPackageGetter(getPackage)
+ resStore.SetNativeStore(teststdlibs.NativeStore)
+ return
+}
+
+func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) {
+ dirs := [...]string{
+ // Normal stdlib path.
+ filepath.Join(rootDir, "gnovm", "stdlibs", pkgPath),
+ // Override path. Definitions here override the previous if duplicate.
+ filepath.Join(rootDir, "gnovm", "tests", "stdlibs", pkgPath),
+ }
+ files := make([]string, 0, 32) // pre-alloc 32 as a likely high number of files
+ for _, path := range dirs {
+ dl, err := os.ReadDir(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ continue
+ }
+ panic(fmt.Errorf("could not access dir %q: %w", path, err))
+ }
+
+ for _, f := range dl {
+ // NOTE: RunMemPackage has other rules; those should be mostly useful
+ // for on-chain packages (ie. include README and gno.mod).
+ if !f.IsDir() && strings.HasSuffix(f.Name(), ".gno") {
+ files = append(files, filepath.Join(path, f.Name()))
+ }
+ }
+ }
+ if len(files) == 0 {
+ return nil, nil
+ }
+
+ memPkg := gno.ReadMemPackageFromList(files, pkgPath)
+ m2 := gno.NewMachineWithOptions(gno.MachineOptions{
+ // NOTE: see also pkgs/sdk/vm/builtins.go
+ // Needs PkgPath != its name because TestStore.getPackage is the package
+ // getter for the store, which calls loadStdlib, so it would be recursively called.
+ PkgPath: "stdlibload",
+ Output: stdout,
+ Store: store,
+ })
+ return m2.RunMemPackageWithOverrides(memPkg, true)
+}
+
+type stackWrappedError struct {
+ err error
+ stack []byte
+}
+
+func (e *stackWrappedError) Error() string { return e.err.Error() }
+func (e *stackWrappedError) Unwrap() error { return e.err }
+func (e *stackWrappedError) String() string {
+ return fmt.Sprintf("%v\nstack:\n%v", e.err, string(e.stack))
+}
+
+// LoadImports parses the given file and attempts to retrieve all pure packages
+// from the store. This is mostly useful for "eager import loading", whereby all
+// imports are pre-loaded in a permanent store, so that the tests can use
+// ephemeral transaction stores.
+func LoadImports(store gno.Store, filename string, content []byte) (err error) {
+ defer func() {
+ // This is slightly different from the handling below; we do not have a
+ // machine to work with, as this comes from an import; so we need
+ // "machine-less" alternatives. (like v.String instead of v.Sprint)
+ if r := recover(); r != nil {
+ switch v := r.(type) {
+ case *gno.TypedValue:
+ err = errors.New(v.String())
+ case *gno.PreprocessError:
+ err = v.Unwrap()
+ case gno.UnhandledPanicError:
+ err = v
+ case error:
+ err = &stackWrappedError{v, debug.Stack()}
+ default:
+ err = &stackWrappedError{fmt.Errorf("%v", v), debug.Stack()}
+ }
+ }
+ }()
+
+ fset := token.NewFileSet()
+ fl, err := parser.ParseFile(fset, filename, content, parser.ImportsOnly)
+ if err != nil {
+ return fmt.Errorf("parse failure: %w", err)
+ }
+ for _, imp := range fl.Imports {
+ impPath, err := strconv.Unquote(imp.Path.Value)
+ if err != nil {
+ return fmt.Errorf("%v: unexpected invalid import path: %v", fset.Position(imp.Pos()).String(), imp.Path.Value)
+ }
+ if gno.IsRealmPath(impPath) {
+ // Don't eagerly load realms.
+ // Realms persist state and can change the state of other realms in initialization.
+ continue
+ }
+ pkg := store.GetPackage(impPath, true)
+ if pkg == nil {
+ return fmt.Errorf("%v: unknown import path %v", fset.Position(imp.Pos()).String(), impPath)
+ }
+ }
+ return nil
+}
diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go
new file mode 100644
index 00000000000..9374db263ee
--- /dev/null
+++ b/gnovm/pkg/test/test.go
@@ -0,0 +1,483 @@
+// Package test contains the code to parse and execute Gno tests and filetests.
+package test
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime/debug"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gnolang/gno/gnovm"
+ gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
+ "github.com/gnolang/gno/gnovm/stdlibs"
+ teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
+ "github.com/gnolang/gno/tm2/pkg/crypto"
+ "github.com/gnolang/gno/tm2/pkg/sdk"
+ "github.com/gnolang/gno/tm2/pkg/std"
+ storetypes "github.com/gnolang/gno/tm2/pkg/store/types"
+ "go.uber.org/multierr"
+)
+
+const (
+ // DefaultHeight is the default height used in the [Context].
+ DefaultHeight = 123
+ // DefaultTimestamp is the Timestamp value used by default in [Context].
+ DefaultTimestamp = 1234567890
+ // DefaultCaller is the result of gno.DerivePkgAddr("user1.gno"),
+ // used as the default caller in [Context].
+ DefaultCaller crypto.Bech32Address = "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
+)
+
+// Context returns a TestExecContext. Usable for test purpose only.
+// The returned context has a mock banker, params and event logger. It will give
+// the pkgAddr the coins in `send` by default, and only that.
+// The Height and Timestamp parameters are set to the [DefaultHeight] and
+// [DefaultTimestamp].
+func Context(pkgPath string, send std.Coins) *teststd.TestExecContext {
+ // FIXME: create a better package to manage this, with custom constructors
+ pkgAddr := gno.DerivePkgAddr(pkgPath) // the addr of the pkgPath called.
+
+ banker := &teststd.TestBanker{
+ CoinTable: map[crypto.Bech32Address]std.Coins{
+ pkgAddr.Bech32(): send,
+ },
+ }
+ ctx := stdlibs.ExecContext{
+ ChainID: "dev",
+ Height: DefaultHeight,
+ Timestamp: DefaultTimestamp,
+ OrigCaller: DefaultCaller,
+ OrigPkgAddr: pkgAddr.Bech32(),
+ OrigSend: send,
+ OrigSendSpent: new(std.Coins),
+ Banker: banker,
+ Params: newTestParams(),
+ EventLogger: sdk.NewEventLogger(),
+ }
+ return &teststd.TestExecContext{
+ ExecContext: ctx,
+ RealmFrames: make(map[*gno.Frame]teststd.RealmOverride),
+ }
+}
+
+// Machine is a minimal machine, set up with just the Store, Output and Context.
+func Machine(testStore gno.Store, output io.Writer, pkgPath string) *gno.Machine {
+ return gno.NewMachineWithOptions(gno.MachineOptions{
+ Store: testStore,
+ Output: output,
+ Context: Context(pkgPath, nil),
+ })
+}
+
+// ----------------------------------------
+// testParams
+
+type testParams struct{}
+
+func newTestParams() *testParams {
+ return &testParams{}
+}
+
+func (tp *testParams) SetBool(key string, val bool) { /* noop */ }
+func (tp *testParams) SetBytes(key string, val []byte) { /* noop */ }
+func (tp *testParams) SetInt64(key string, val int64) { /* noop */ }
+func (tp *testParams) SetUint64(key string, val uint64) { /* noop */ }
+func (tp *testParams) SetString(key string, val string) { /* noop */ }
+
+// ----------------------------------------
+// main test function
+
+// TestOptions is a list of options that must be passed to [Test].
+type TestOptions struct {
+ // BaseStore / TestStore to use for the tests.
+ BaseStore storetypes.CommitStore
+ TestStore gno.Store
+ // Gno root dir.
+ RootDir string
+ // Used for printing program output, during verbose logging.
+ Output io.Writer
+ // Used for os.Stderr, and for printing errors.
+ Error io.Writer
+
+ // Not set by NewTestOptions:
+
+ // Flag to filter tests to run.
+ RunFlag string
+ // Whether to update filetest directives.
+ Sync bool
+ // Uses Error to print when starting a test, and prints test output directly,
+ // unbuffered.
+ Verbose bool
+ // Uses Error to print runtime metrics for tests.
+ Metrics bool
+ // Uses Error to print the events emitted.
+ Events bool
+
+ filetestBuffer bytes.Buffer
+ outWriter proxyWriter
+}
+
+// WriterForStore is the writer that should be passed to [Store], so that
+// [Test] is then able to swap it when needed.
+func (opts *TestOptions) WriterForStore() io.Writer {
+ return &opts.outWriter
+}
+
+// NewTestOptions sets up TestOptions, filling out all "required" parameters.
+func NewTestOptions(rootDir string, stdin io.Reader, stdout, stderr io.Writer) *TestOptions {
+ opts := &TestOptions{
+ RootDir: rootDir,
+ Output: stdout,
+ Error: stderr,
+ }
+ opts.BaseStore, opts.TestStore = Store(
+ rootDir, false,
+ stdin, opts.WriterForStore(), stderr,
+ )
+ return opts
+}
+
+// proxyWriter is a simple wrapper around a io.Writer, it exists so that the
+// underlying writer can be swapped with another when necessary.
+type proxyWriter struct {
+ w io.Writer
+}
+
+func (p *proxyWriter) Write(b []byte) (int, error) {
+ return p.w.Write(b)
+}
+
+// tee temporarily appends the writer w to an underlying MultiWriter, which
+// should then be reverted using revert().
+func (p *proxyWriter) tee(w io.Writer) (revert func()) {
+ save := p.w
+ if save == io.Discard {
+ p.w = w
+ } else {
+ p.w = io.MultiWriter(save, w)
+ }
+ return func() {
+ p.w = save
+ }
+}
+
+// Test runs tests on the specified memPkg.
+// fsDir is the directory on filesystem of package; it's used in case opts.Sync
+// is enabled, and points to the directory where the files are contained if they
+// are to be updated.
+// opts is a required set of options, which is often shared among different
+// tests; you can use [NewTestOptions] for a common base configuration.
+func Test(memPkg *gnovm.MemPackage, fsDir string, opts *TestOptions) error {
+ opts.outWriter.w = opts.Output
+
+ var errs error
+
+ // Stands for "test", "integration test", and "filetest".
+ // "integration test" are the test files with `package xxx_test` (they are
+ // not necessarily integration tests, it's just for our internal reference.)
+ tset, itset, itfiles, ftfiles := parseMemPackageTests(opts.TestStore, memPkg)
+
+ // Testing with *_test.gno
+ if len(tset.Files)+len(itset.Files) > 0 {
+ // Create a common cw/gs for both the `pkg` tests as well as the `pkg_test`
+ // tests. This allows us to "export" symbols from the pkg tests and
+ // import them from the `pkg_test` tests.
+ cw := opts.BaseStore.CacheWrap()
+ gs := opts.TestStore.BeginTransaction(cw, cw)
+
+ // Run test files in pkg.
+ if len(tset.Files) > 0 {
+ err := opts.runTestFiles(memPkg, tset, cw, gs)
+ if err != nil {
+ errs = multierr.Append(errs, err)
+ }
+ }
+
+ // Test xxx_test pkg.
+ if len(itset.Files) > 0 {
+ itPkg := &gnovm.MemPackage{
+ Name: memPkg.Name + "_test",
+ Path: memPkg.Path + "_test",
+ Files: itfiles,
+ }
+
+ err := opts.runTestFiles(itPkg, itset, cw, gs)
+ if err != nil {
+ errs = multierr.Append(errs, err)
+ }
+ }
+ }
+
+ // Testing with *_filetest.gno.
+ if len(ftfiles) > 0 {
+ filter := splitRegexp(opts.RunFlag)
+ for _, testFile := range ftfiles {
+ testFileName := testFile.Name
+ testFilePath := filepath.Join(fsDir, testFileName)
+ testName := "file/" + testFileName
+ if !shouldRun(filter, testName) {
+ continue
+ }
+
+ startedAt := time.Now()
+ if opts.Verbose {
+ fmt.Fprintf(opts.Error, "=== RUN %s\n", testName)
+ }
+
+ changed, err := opts.runFiletest(testFileName, []byte(testFile.Body))
+ if changed != "" {
+ // Note: changed always == "" if opts.Sync == false.
+ err = os.WriteFile(testFilePath, []byte(changed), 0o644)
+ if err != nil {
+ panic(fmt.Errorf("could not fix golden file: %w", err))
+ }
+ }
+
+ duration := time.Since(startedAt)
+ dstr := fmtDuration(duration)
+ if err != nil {
+ fmt.Fprintf(opts.Error, "--- FAIL: %s (%s)\n", testName, dstr)
+ fmt.Fprintln(opts.Error, err.Error())
+ errs = multierr.Append(errs, fmt.Errorf("%s failed", testName))
+ } else if opts.Verbose {
+ fmt.Fprintf(opts.Error, "--- PASS: %s (%s)\n", testName, dstr)
+ }
+
+ // XXX: add per-test metrics
+ }
+ }
+
+ return errs
+}
+
+func (opts *TestOptions) runTestFiles(
+ memPkg *gnovm.MemPackage,
+ files *gno.FileSet,
+ cw storetypes.Store, gs gno.TransactionStore,
+) (errs error) {
+ var m *gno.Machine
+ defer func() {
+ if r := recover(); r != nil {
+ if st := m.ExceptionsStacktrace(); st != "" {
+ errs = multierr.Append(errors.New(st), errs)
+ }
+ errs = multierr.Append(
+ fmt.Errorf("panic: %v\ngo stacktrace:\n%v\ngno machine: %v\ngno stacktrace:\n%v",
+ r, string(debug.Stack()), m.String(), m.Stacktrace()),
+ errs,
+ )
+ }
+ }()
+
+ tests := loadTestFuncs(memPkg.Name, files)
+
+ var alloc *gno.Allocator
+ if opts.Metrics {
+ alloc = gno.NewAllocator(math.MaxInt64)
+ }
+
+ // Check if we already have the package - it may have been eagerly
+ // loaded.
+ m = Machine(gs, opts.WriterForStore(), memPkg.Path)
+ m.Alloc = alloc
+ if opts.TestStore.GetMemPackage(memPkg.Path) == nil {
+ m.RunMemPackage(memPkg, true)
+ } else {
+ m.SetActivePackage(gs.GetPackage(memPkg.Path, false))
+ }
+ pv := m.Package
+
+ m.RunFiles(files.Files...)
+
+ for _, tf := range tests {
+ // TODO(morgan): we could theoretically use wrapping on the baseStore
+ // and gno store to achieve per-test isolation. However, that requires
+ // some deeper changes, as ideally we'd:
+ // - Run the MemPackage independently (so it can also be run as a
+ // consequence of an import)
+ // - Run the test files before this for loop (but persist it to store;
+ // RunFiles doesn't do that currently)
+ // - Wrap here.
+ m = Machine(gs, opts.Output, memPkg.Path)
+ m.Alloc = alloc
+ m.SetActivePackage(pv)
+
+ testingpv := m.Store.GetPackage("testing", false)
+ testingtv := gno.TypedValue{T: &gno.PackageType{}, V: testingpv}
+ testingcx := &gno.ConstExpr{TypedValue: testingtv}
+
+ eval := m.Eval(gno.Call(
+ gno.Sel(testingcx, "RunTest"), // Call testing.RunTest
+ gno.Str(opts.RunFlag), // run flag
+ gno.Nx(strconv.FormatBool(opts.Verbose)), // is verbose?
+ &gno.CompositeLitExpr{ // Third param, the testing.InternalTest
+ Type: gno.Sel(testingcx, "InternalTest"),
+ Elts: gno.KeyValueExprs{
+ {Key: gno.X("Name"), Value: gno.Str(tf.Name)},
+ {Key: gno.X("F"), Value: gno.Nx(tf.Name)},
+ },
+ },
+ ))
+
+ if opts.Events {
+ events := m.Context.(*teststd.TestExecContext).EventLogger.Events()
+ if events != nil {
+ res, err := json.Marshal(events)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Fprintf(opts.Error, "EVENTS: %s\n", string(res))
+ }
+ }
+
+ ret := eval[0].GetString()
+ if ret == "" {
+ err := fmt.Errorf("failed to execute unit test: %q", tf.Name)
+ errs = multierr.Append(errs, err)
+ fmt.Fprintf(opts.Error, "--- FAIL: %s [internal gno testing error]", tf.Name)
+ continue
+ }
+
+ // TODO: replace with amino or send native type?
+ var rep report
+ err := json.Unmarshal([]byte(ret), &rep)
+ if err != nil {
+ errs = multierr.Append(errs, err)
+ fmt.Fprintf(opts.Error, "--- FAIL: %s [internal gno testing error]", tf.Name)
+ continue
+ }
+
+ if rep.Failed {
+ err := fmt.Errorf("failed: %q", tf.Name)
+ errs = multierr.Append(errs, err)
+ }
+
+ if opts.Metrics {
+ // XXX: store changes
+ // XXX: max mem consumption
+ allocsVal := "n/a"
+ if m.Alloc != nil {
+ maxAllocs, allocs := m.Alloc.Status()
+ allocsVal = fmt.Sprintf("%s(%.2f%%)",
+ prettySize(allocs),
+ float64(allocs)/float64(maxAllocs)*100,
+ )
+ }
+ fmt.Fprintf(opts.Error, "--- runtime: cycle=%s allocs=%s\n",
+ prettySize(m.Cycles),
+ allocsVal,
+ )
+ }
+ }
+
+ return errs
+}
+
+// report is a mirror of Gno's stdlibs/testing.Report.
+type report struct {
+ Failed bool
+ Skipped bool
+}
+
+type testFunc struct {
+ Package string
+ Name string
+}
+
+func loadTestFuncs(pkgName string, tfiles *gno.FileSet) (rt []testFunc) {
+ for _, tf := range tfiles.Files {
+ for _, d := range tf.Decls {
+ if fd, ok := d.(*gno.FuncDecl); ok {
+ fname := string(fd.Name)
+ if strings.HasPrefix(fname, "Test") {
+ tf := testFunc{
+ Package: pkgName,
+ Name: fname,
+ }
+ rt = append(rt, tf)
+ }
+ }
+ }
+ }
+ return
+}
+
+// parseMemPackageTests parses test files (skipping filetests) in the memPkg.
+func parseMemPackageTests(store gno.Store, memPkg *gnovm.MemPackage) (tset, itset *gno.FileSet, itfiles, ftfiles []*gnovm.MemFile) {
+ tset = &gno.FileSet{}
+ itset = &gno.FileSet{}
+ var errs error
+ for _, mfile := range memPkg.Files {
+ if !strings.HasSuffix(mfile.Name, ".gno") {
+ continue // skip this file.
+ }
+
+ if err := LoadImports(store, path.Join(memPkg.Path, mfile.Name), []byte(mfile.Body)); err != nil {
+ errs = multierr.Append(errs, err)
+ }
+
+ n, err := gno.ParseFile(mfile.Name, mfile.Body)
+ if err != nil {
+ errs = multierr.Append(errs, err)
+ continue
+ }
+ if n == nil {
+ panic("should not happen")
+ }
+ switch {
+ case strings.HasSuffix(mfile.Name, "_filetest.gno"):
+ ftfiles = append(ftfiles, mfile)
+ case strings.HasSuffix(mfile.Name, "_test.gno") && memPkg.Name == string(n.PkgName):
+ tset.AddFiles(n)
+ case strings.HasSuffix(mfile.Name, "_test.gno") && memPkg.Name+"_test" == string(n.PkgName):
+ itset.AddFiles(n)
+ itfiles = append(itfiles, mfile)
+ case memPkg.Name == string(n.PkgName):
+ // normal package file
+ default:
+ panic(fmt.Sprintf(
+ "expected package name [%s] or [%s_test] but got [%s] file [%s]",
+ memPkg.Name, memPkg.Name, n.PkgName, mfile))
+ }
+ }
+ if errs != nil {
+ panic(errs)
+ }
+ return
+}
+
+func shouldRun(filter filterMatch, path string) bool {
+ if filter == nil {
+ return true
+ }
+ elem := strings.Split(path, "/")
+ ok, _ := filter.matches(elem, matchString)
+ return ok
+}
+
+// Adapted from https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
+func prettySize(nb int64) string {
+ const unit = 1000
+ if nb < unit {
+ return fmt.Sprintf("%d", nb)
+ }
+ div, exp := int64(unit), 0
+ for n := nb / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f%c", float64(nb)/float64(div), "kMGTPE"[exp])
+}
+
+func fmtDuration(d time.Duration) string {
+ return fmt.Sprintf("%.2fs", d.Seconds())
+}
diff --git a/gnovm/cmd/gno/util_match.go b/gnovm/pkg/test/util_match.go
similarity index 99%
rename from gnovm/cmd/gno/util_match.go
rename to gnovm/pkg/test/util_match.go
index 34181f61254..a3fec22f389 100644
--- a/gnovm/cmd/gno/util_match.go
+++ b/gnovm/pkg/test/util_match.go
@@ -1,4 +1,4 @@
-package main
+package test
// Most of the code in this file is extracted from golang's src/testing/match.go.
//
diff --git a/gnovm/stdlibs/io/example_test.gno b/gnovm/stdlibs/io/example_test.gno
index c781fb9166e..54a9e74f55a 100644
--- a/gnovm/stdlibs/io/example_test.gno
+++ b/gnovm/stdlibs/io/example_test.gno
@@ -8,7 +8,6 @@ import (
"bytes"
"fmt"
"io"
- "log"
"os"
"strings"
)
@@ -17,7 +16,7 @@ func ExampleCopy() {
r := strings.NewReader("some io.Reader stream to be read\n")
if _, err := io.Copy(os.Stdout, r); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -31,12 +30,12 @@ func ExampleCopyBuffer() {
// buf is used here...
if _, err := io.CopyBuffer(os.Stdout, r1, buf); err != nil {
- log.Fatal(err)
+ panic(err)
}
// ... reused here also. No need to allocate an extra buffer.
if _, err := io.CopyBuffer(os.Stdout, r2, buf); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -48,7 +47,7 @@ func ExampleCopyN() {
r := strings.NewReader("some io.Reader stream to be read")
if _, err := io.CopyN(os.Stdout, r, 4); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -60,7 +59,7 @@ func ExampleReadAtLeast() {
buf := make([]byte, 14)
if _, err := io.ReadAtLeast(r, buf, 4); err != nil {
- log.Fatal(err)
+ panic(err)
}
fmt.Printf("%s\n", buf)
@@ -87,7 +86,7 @@ func ExampleReadFull() {
buf := make([]byte, 4)
if _, err := io.ReadFull(r, buf); err != nil {
- log.Fatal(err)
+ panic(err)
}
fmt.Printf("%s\n", buf)
@@ -104,7 +103,7 @@ func ExampleReadFull() {
func ExampleWriteString() {
if _, err := io.WriteString(os.Stdout, "Hello World"); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output: Hello World
@@ -115,7 +114,7 @@ func ExampleLimitReader() {
lr := io.LimitReader(r, 4)
if _, err := io.Copy(os.Stdout, lr); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -129,7 +128,7 @@ func ExampleMultiReader() {
r := io.MultiReader(r1, r2, r3)
if _, err := io.Copy(os.Stdout, r); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -153,7 +152,7 @@ func ExampleSectionReader() {
s := io.NewSectionReader(r, 5, 17)
if _, err := io.Copy(os.Stdout, s); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -166,7 +165,7 @@ func ExampleSectionReader_ReadAt() {
buf := make([]byte, 6)
if _, err := s.ReadAt(buf, 10); err != nil {
- log.Fatal(err)
+ panic(err)
}
fmt.Printf("%s\n", buf)
@@ -180,11 +179,11 @@ func ExampleSectionReader_Seek() {
s := io.NewSectionReader(r, 5, 17)
if _, err := s.Seek(10, io.SeekStart); err != nil {
- log.Fatal(err)
+ panic(err)
}
if _, err := io.Copy(os.Stdout, s); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -196,12 +195,12 @@ func ExampleSeeker_Seek() {
r.Seek(5, io.SeekStart) // move to the 5th char from the start
if _, err := io.Copy(os.Stdout, r); err != nil {
- log.Fatal(err)
+ panic(err)
}
r.Seek(-5, io.SeekEnd)
if _, err := io.Copy(os.Stdout, r); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -216,7 +215,7 @@ func ExampleMultiWriter() {
w := io.MultiWriter(&buf1, &buf2)
if _, err := io.Copy(w, r); err != nil {
- log.Fatal(err)
+ panic(err)
}
fmt.Print(buf1.String())
@@ -237,7 +236,7 @@ func ExamplePipe() {
}()
if _, err := io.Copy(os.Stdout, r); err != nil {
- log.Fatal(err)
+ panic(err)
}
// Output:
@@ -250,7 +249,7 @@ func ExampleReadAll() {
b, err := io.ReadAll(r)
if err != nil {
- log.Fatal(err)
+ panic(err)
}
fmt.Printf("%s", b)
diff --git a/gnovm/stdlibs/io/multi_test.gno b/gnovm/stdlibs/io/multi_test.gno
index 8932ace2e59..f39895ea776 100644
--- a/gnovm/stdlibs/io/multi_test.gno
+++ b/gnovm/stdlibs/io/multi_test.gno
@@ -6,7 +6,7 @@ package io
import (
"bytes"
- "crypto/sha1"
+ "crypto/sha256"
"fmt"
"strings"
"testing"
@@ -119,8 +119,8 @@ func testMultiWriter(t *testing.T, sink interface {
Stringer
},
) {
- sha1 := sha1.New()
- mw := MultiWriter(sha1, sink)
+ var buf bytes.Buffer
+ mw := MultiWriter(&buf, sink)
sourceString := "My input text."
source := strings.NewReader(sourceString)
@@ -134,9 +134,9 @@ func testMultiWriter(t *testing.T, sink interface {
t.Errorf("unexpected error: %v", err)
}
- sha1hex := fmt.Sprintf("%x", sha1.Sum(nil))
- if sha1hex != "01cb303fa8c30a64123067c5aa6284ba7ec2d31b" {
- t.Error("incorrect sha1 value")
+ sha1hex := fmt.Sprintf("%x", sha256.Sum256(buf.Bytes()))
+ if sha1hex != "d3e9e78d2a7e9c4756a4e8e57db6a57ccfd84c6d656d66b9d2bd2620b4ab81b8" {
+ t.Error("incorrect sha256 value")
}
if sink.String() != sourceString {
diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go
index a0dafe5dc44..01e763ab82e 100644
--- a/gnovm/stdlibs/std/context.go
+++ b/gnovm/stdlibs/std/context.go
@@ -12,7 +12,6 @@ type ExecContext struct {
Height int64
Timestamp int64 // seconds
TimestampNano int64 // nanoseconds, only used for testing.
- Msg sdk.Msg
OrigCaller crypto.Bech32Address
OrigPkgAddr crypto.Bech32Address
OrigSend std.Coins
diff --git a/gnovm/stdlibs/strconv/example_test.gno b/gnovm/stdlibs/strconv/example_test.gno
index 428fde4e660..d3ef2cc4244 100644
--- a/gnovm/stdlibs/strconv/example_test.gno
+++ b/gnovm/stdlibs/strconv/example_test.gno
@@ -6,7 +6,6 @@ package strconv_test
import (
"fmt"
- "log"
"strconv"
)
@@ -409,7 +408,7 @@ func ExampleUnquote() {
func ExampleUnquoteChar() {
v, mb, t, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"')
if err != nil {
- log.Fatal(err)
+ panic(err)
}
fmt.Println("value:", string(v))
diff --git a/gnovm/stdlibs/testing/match.gno b/gnovm/stdlibs/testing/match.gno
index 3b22602890d..8b099f37624 100644
--- a/gnovm/stdlibs/testing/match.gno
+++ b/gnovm/stdlibs/testing/match.gno
@@ -16,11 +16,11 @@ import (
type filterMatch interface {
// matches checks the name against the receiver's pattern strings using the
// given match function.
- matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool)
+ matches(name []string) (ok, partial bool)
// verify checks that the receiver's pattern strings are valid filters by
// calling the given match function.
- verify(name string, matchString func(pat, str string) (bool, error)) error
+ verify(name string) error
}
// simpleMatch matches a test name if all of the pattern strings match in
@@ -30,43 +30,43 @@ type simpleMatch []string
// alternationMatch matches a test name if one of the alternations match.
type alternationMatch []filterMatch
-func (m simpleMatch) matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool) {
+func (m simpleMatch) matches(name []string) (ok, partial bool) {
for i, s := range name {
if i >= len(m) {
break
}
- if ok, _ := matchString(m[i], s); !ok {
+ if ok, _ := regexp.MatchString(m[i], s); !ok {
return false, false
}
}
return true, len(name) < len(m)
}
-func (m simpleMatch) verify(name string, matchString func(pat, str string) (bool, error)) error {
+func (m simpleMatch) verify(name string) error {
for i, s := range m {
m[i] = rewrite(s)
}
// Verify filters before doing any processing.
for i, s := range m {
- if _, err := matchString(s, "non-empty"); err != nil {
+ if _, err := regexp.MatchString(s, "non-empty"); err != nil {
return fmt.Errorf("element %d of %s (%q): %s", i, name, s, err)
}
}
return nil
}
-func (m alternationMatch) matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool) {
+func (m alternationMatch) matches(name []string) (ok, partial bool) {
for _, m := range m {
- if ok, partial = m.matches(name, matchString); ok {
+ if ok, partial = m.matches(name); ok {
return ok, partial
}
}
return false, false
}
-func (m alternationMatch) verify(name string, matchString func(pat, str string) (bool, error)) error {
+func (m alternationMatch) verify(name string) error {
for i, m := range m {
- if err := m.verify(name, matchString); err != nil {
+ if err := m.verify(name); err != nil {
return fmt.Errorf("alternation %d of %s", i, err)
}
}
@@ -164,20 +164,3 @@ func isSpace(r rune) bool {
}
return false
}
-
-var (
- matchPat string
- matchRe *regexp.Regexp
-)
-
-// based on testing/internal/testdeps.TestDeps.MatchString.
-func matchString(pat, str string) (result bool, err error) {
- if matchRe == nil || matchPat != pat {
- matchPat = pat
- matchRe, err = regexp.Compile(matchPat)
- if err != nil {
- return
- }
- }
- return matchRe.MatchString(str), nil
-}
diff --git a/gnovm/stdlibs/testing/testing.gno b/gnovm/stdlibs/testing/testing.gno
index 6e55c5cc283..fdafd9652ba 100644
--- a/gnovm/stdlibs/testing/testing.gno
+++ b/gnovm/stdlibs/testing/testing.gno
@@ -280,7 +280,7 @@ func (t *T) shouldRun(name string) bool {
}
elem := strings.Split(name, "/")
- ok, partial := t.runFilter.matches(elem, matchString)
+ ok, partial := t.runFilter.matches(elem)
_ = partial // we don't care right now
return ok
}
diff --git a/gnovm/tests/README.md b/gnovm/tests/README.md
index 378d5d9dc1b..d35c6590e2f 100644
--- a/gnovm/tests/README.md
+++ b/gnovm/tests/README.md
@@ -1 +1,35 @@
-All the files in the ./files directory are meant to be those derived/borrowed from Yaegi, Apache2.0.
+# tests
+
+This directory contains integration tests for the GnoVM. This file aims to provide a brief overview.
+
+GnoVM tests and filetests run in a special context relating to its imports.
+You can see the additional Gonative functions in [gnovm/pkg/test/imports.go](../pkg/test/imports.go).
+You can see additional standard libraries and standard library functions
+available in testing in [gnovm/tests/stdlibs](./stdlibs).
+
+## `files`: GnoVM filetests
+
+The most important directory is `files`, which contains filetests for the Gno
+project. These are executed by the `TestFiles` test in the `gnovm/pkg/gnolang`
+directory.
+
+The `files/extern` directory contains several packages used to test the import
+system. The packages here are imported with the prefix
+`github.com/gnolang/gno/_test/`, exclusively within these filetests.
+
+Tests with the `_long` suffix are skipped when the `-short` flag is passed.
+
+These tests are largely derived from Yaegi, licensed under Apache 2.0.
+
+## `stdlibs`: testing standard libraries
+
+These contain standard libraries which are only available in testing, and
+extensions of them, like `std.TestSkipHeights`.
+
+## other directories
+
+- `backup` has been here since forever; and somebody should come around and delete it at some point.
+- `challenges` contains code that supposedly doesn't work, but should.
+- `integ` contains some files for integration tests which likely should have
+ been in some `testdata` directory to begin with. You guessed it,
+ they're here until someone bothers to move them out.
diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go
deleted file mode 100644
index 98dbab6ac0e..00000000000
--- a/gnovm/tests/file.go
+++ /dev/null
@@ -1,713 +0,0 @@
-package tests
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "go/ast"
- "go/parser"
- "go/token"
- "io"
- "os"
- "regexp"
- rtdb "runtime/debug"
- "strconv"
- "strings"
-
- "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
- "github.com/gnolang/gno/gnovm"
- gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
- "github.com/gnolang/gno/gnovm/stdlibs"
- teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
- "github.com/gnolang/gno/tm2/pkg/crypto"
- osm "github.com/gnolang/gno/tm2/pkg/os"
- "github.com/gnolang/gno/tm2/pkg/sdk"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/pmezard/go-difflib/difflib"
-)
-
-type loggerFunc func(args ...interface{})
-
-func TestMachine(store gno.Store, stdout io.Writer, pkgPath string) *gno.Machine {
- // default values
- var (
- send std.Coins
- maxAlloc int64
- )
-
- return testMachineCustom(store, pkgPath, stdout, maxAlloc, send)
-}
-
-func testMachineCustom(store gno.Store, pkgPath string, stdout io.Writer, maxAlloc int64, send std.Coins) *gno.Machine {
- ctx := TestContext(pkgPath, send)
- m := gno.NewMachineWithOptions(gno.MachineOptions{
- PkgPath: "", // set later.
- Output: stdout,
- Store: store,
- Context: ctx,
- MaxAllocBytes: maxAlloc,
- })
- return m
-}
-
-// TestContext returns a TestExecContext. Usable for test purpose only.
-func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext {
- // FIXME: create a better package to manage this, with custom constructors
- pkgAddr := gno.DerivePkgAddr(pkgPath) // the addr of the pkgPath called.
- caller := gno.DerivePkgAddr("user1.gno")
-
- pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(send) // >= send.
- banker := newTestBanker(pkgAddr.Bech32(), pkgCoins)
- params := newTestParams()
- ctx := stdlibs.ExecContext{
- ChainID: "dev",
- Height: 123,
- Timestamp: 1234567890,
- Msg: nil,
- OrigCaller: caller.Bech32(),
- OrigPkgAddr: pkgAddr.Bech32(),
- OrigSend: send,
- OrigSendSpent: new(std.Coins),
- Banker: banker,
- Params: params,
- EventLogger: sdk.NewEventLogger(),
- }
- return &teststd.TestExecContext{
- ExecContext: ctx,
- RealmFrames: make(map[*gno.Frame]teststd.RealmOverride),
- }
-}
-
-// CleanupMachine can be called during two tests while reusing the same Machine instance.
-func CleanupMachine(m *gno.Machine) {
- prevCtx := m.Context.(*teststd.TestExecContext)
- prevSend := prevCtx.OrigSend
-
- newCtx := TestContext("", prevCtx.OrigSend)
- pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(prevSend) // >= send.
- banker := newTestBanker(prevCtx.OrigPkgAddr, pkgCoins)
- newCtx.OrigPkgAddr = prevCtx.OrigPkgAddr
- newCtx.Banker = banker
- m.Context = newCtx
-}
-
-type runFileTestOptions struct {
- nativeLibs bool
- logger loggerFunc
- syncWanted bool
-}
-
-// RunFileTestOptions specify changing options in [RunFileTest], deviating
-// from the zero value.
-type RunFileTestOption func(*runFileTestOptions)
-
-// WithNativeLibs enables using go native libraries (ie, [ImportModeNativePreferred])
-// instead of using stdlibs/*.
-func WithNativeLibs() RunFileTestOption {
- return func(r *runFileTestOptions) { r.nativeLibs = true }
-}
-
-// WithLoggerFunc sets a logging function for [RunFileTest].
-func WithLoggerFunc(f func(args ...interface{})) RunFileTestOption {
- return func(r *runFileTestOptions) { r.logger = f }
-}
-
-// WithSyncWanted sets the syncWanted flag to true.
-// It rewrites tests files so that the values of Output: and of Realm:
-// comments match the actual output or realm state after the test.
-func WithSyncWanted(v bool) RunFileTestOption {
- return func(r *runFileTestOptions) { r.syncWanted = v }
-}
-
-// RunFileTest executes the filetest at the given path, using rootDir as
-// the directory where to find the "stdlibs" directory.
-func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error {
- var f runFileTestOptions
- for _, opt := range opts {
- opt(&f)
- }
-
- directives, pkgPath, resWanted, errWanted, rops, eventsWanted, stacktraceWanted, maxAlloc, send, preWanted := wantedFromComment(path)
- if pkgPath == "" {
- pkgPath = "main"
- }
- pkgName := DefaultPkgName(pkgPath)
- stdin := new(bytes.Buffer)
- stdout := new(bytes.Buffer)
- stderr := new(bytes.Buffer)
- mode := ImportModeStdlibsPreferred
- if f.nativeLibs {
- mode = ImportModeNativePreferred
- }
- store := TestStore(rootDir, "./files", stdin, stdout, stderr, mode)
- store.SetLogStoreOps(true)
- m := testMachineCustom(store, pkgPath, stdout, maxAlloc, send)
- checkMachineIsEmpty := true
-
- // TODO support stdlib groups, but make testing safe;
- // e.g. not be able to make network connections.
- // interp.New(interp.Options{GoPath: goPath, Stdout: &stdout, Stderr: &stderr})
- // m.Use(interp.Symbols)
- // m.Use(stdlib.Symbols)
- // m.Use(unsafe.Symbols)
- bz, err := os.ReadFile(path)
- if err != nil {
- return err
- }
- { // Validate result, errors, etc.
- var pnc interface{}
- func() {
- defer func() {
- if r := recover(); r != nil {
- // print output.
- fmt.Printf("OUTPUT:\n%s\n", stdout.String())
- pnc = r
- err := strings.TrimSpace(fmt.Sprintf("%v", pnc))
- // print stack if unexpected error.
- if errWanted == "" ||
- !strings.Contains(err, errWanted) {
- fmt.Printf("ERROR:\n%s\n", err)
- // error didn't match: print stack
- // NOTE: will fail testcase later.
- rtdb.PrintStack()
- }
- }
- }()
- if f.logger != nil {
- f.logger("========================================")
- f.logger("RUN FILES & INIT")
- f.logger("========================================")
- }
- if !gno.IsRealmPath(pkgPath) {
- // simple case.
- pn := gno.NewPackageNode(pkgName, pkgPath, &gno.FileSet{})
- pv := pn.NewPackage()
- store.SetBlockNode(pn)
- store.SetCachePackage(pv)
- m.SetActivePackage(pv)
- n := gno.MustParseFile(path, string(bz)) // "main.gno", string(bz))
- m.RunFiles(n)
- if f.logger != nil {
- f.logger("========================================")
- f.logger("RUN MAIN")
- f.logger("========================================")
- }
- m.RunMain()
- if f.logger != nil {
- f.logger("========================================")
- f.logger("RUN MAIN END")
- f.logger("========================================")
- }
- } else {
- // realm case.
- store.SetStrictGo2GnoMapping(true) // in gno.land, natives must be registered.
- gno.DisableDebug() // until main call.
- // save package using realm crawl procedure.
- memPkg := &gnovm.MemPackage{
- Name: string(pkgName),
- Path: pkgPath,
- Files: []*gnovm.MemFile{
- {
- Name: "main.gno", // dontcare
- Body: string(bz),
- },
- },
- }
- // run decls and init functions.
- m.RunMemPackage(memPkg, true)
- // reconstruct machine and clear store cache.
- // whether package is realm or not, since non-realm
- // may call realm packages too.
- if f.logger != nil {
- f.logger("========================================")
- f.logger("CLEAR STORE CACHE")
- f.logger("========================================")
- }
- store.ClearCache()
- /*
- m = gno.NewMachineWithOptions(gno.MachineOptions{
- PkgPath: "",
- Output: stdout,
- Store: store,
- Context: ctx,
- MaxAllocBytes: maxAlloc,
- })
- */
- if f.logger != nil {
- store.Print()
- f.logger("========================================")
- f.logger("PREPROCESS ALL FILES")
- f.logger("========================================")
- }
- m.PreprocessAllFilesAndSaveBlockNodes()
- if f.logger != nil {
- f.logger("========================================")
- f.logger("RUN MAIN")
- f.logger("========================================")
- store.Print()
- }
- pv2 := store.GetPackage(pkgPath, false)
- m.SetActivePackage(pv2)
- gno.EnableDebug()
- if rops != "" {
- // clear store.opslog from init function(s),
- // and PreprocessAllFilesAndSaveBlockNodes().
- store.SetLogStoreOps(true) // resets.
- }
- m.RunMain()
- if f.logger != nil {
- f.logger("========================================")
- f.logger("RUN MAIN END")
- f.logger("========================================")
- }
- }
- }()
-
- for _, directive := range directives {
- switch directive {
- case "Error":
- // errWanted given
- if errWanted != "" {
- if pnc == nil {
- panic(fmt.Sprintf("fail on %s: got nil error, want: %q", path, errWanted))
- }
-
- errstr := ""
- switch v := pnc.(type) {
- case *gno.TypedValue:
- errstr = v.Sprint(m)
- case *gno.PreprocessError:
- errstr = v.Unwrap().Error()
- case gno.UnhandledPanicError:
- errstr = v.Error()
- default:
- errstr = strings.TrimSpace(fmt.Sprintf("%v", pnc))
- }
-
- parts := strings.SplitN(errstr, ":\n--- preprocess stack ---", 2)
- if len(parts) == 2 {
- fmt.Println(parts[0])
- errstr = parts[0]
- }
- if errstr != errWanted {
- if f.syncWanted {
- // write error to file
- replaceWantedInPlace(path, "Error", errstr)
- } else {
- panic(fmt.Sprintf("fail on %s: got %q, want: %q", path, errstr, errWanted))
- }
- }
-
- // NOTE: ignores any gno.GetDebugErrors().
- gno.ClearDebugErrors()
- checkMachineIsEmpty = false // nothing more to do.
- } else {
- // record errors when errWanted is empty and pnc not nil
- if pnc != nil {
- errstr := ""
- if tv, ok := pnc.(*gno.TypedValue); ok {
- errstr = tv.Sprint(m)
- } else {
- errstr = strings.TrimSpace(fmt.Sprintf("%v", pnc))
- }
- parts := strings.SplitN(errstr, ":\n--- preprocess stack ---", 2)
- if len(parts) == 2 {
- fmt.Println(parts[0])
- errstr = parts[0]
- }
- // check tip line, write to file
- ctl := errstr +
- "\n*** CHECK THE ERR MESSAGES ABOVE, MAKE SURE IT'S WHAT YOU EXPECTED, " +
- "DELETE THIS LINE AND RUN TEST AGAIN ***"
- // write error to file
- replaceWantedInPlace(path, "Error", ctl)
- panic(fmt.Sprintf("fail on %s: err recorded, check the message and run test again", path))
- }
- // check gno debug errors when errWanted is empty, pnc is nil
- if gno.HasDebugErrors() {
- panic(fmt.Sprintf("fail on %s: got unexpected debug error(s): %v", path, gno.GetDebugErrors()))
- }
- // pnc is nil, errWanted empty, no gno debug errors
- checkMachineIsEmpty = false
- }
- case "Output":
- // panic if got unexpected error
- if pnc != nil {
- if tv, ok := pnc.(*gno.TypedValue); ok {
- panic(fmt.Sprintf("fail on %s: got unexpected error: %s", path, tv.Sprint(m)))
- } else { // happens on 'unknown import path ...'
- panic(fmt.Sprintf("fail on %s: got unexpected error: %v", path, pnc))
- }
- }
- // check result
- res := strings.TrimSpace(stdout.String())
- res = trimTrailingSpaces(res)
- if res != resWanted {
- if f.syncWanted {
- // write output to file.
- replaceWantedInPlace(path, "Output", res)
- } else {
- // panic so tests immediately fail (for now).
- if resWanted == "" {
- panic(fmt.Sprintf("fail on %s: got unexpected output: %s", path, res))
- } else {
- diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
- A: difflib.SplitLines(resWanted),
- B: difflib.SplitLines(res),
- FromFile: "Expected",
- FromDate: "",
- ToFile: "Actual",
- ToDate: "",
- Context: 1,
- })
- panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff))
- }
- }
- }
- case "Events":
- // panic if got unexpected error
-
- if pnc != nil {
- if tv, ok := pnc.(*gno.TypedValue); ok {
- panic(fmt.Sprintf("fail on %s: got unexpected error: %s", path, tv.Sprint(m)))
- } else { // happens on 'unknown import path ...'
- panic(fmt.Sprintf("fail on %s: got unexpected error: %v", path, pnc))
- }
- }
- // check result
- events := m.Context.(*teststd.TestExecContext).EventLogger.Events()
- evtjson, err := json.MarshalIndent(events, "", " ")
- if err != nil {
- panic(err)
- }
- evtstr := trimTrailingSpaces(string(evtjson))
- if evtstr != eventsWanted {
- if f.syncWanted {
- // write output to file.
- replaceWantedInPlace(path, "Events", evtstr)
- } else {
- // panic so tests immediately fail (for now).
- if eventsWanted == "" {
- panic(fmt.Sprintf("fail on %s: got unexpected events: %s", path, evtstr))
- } else {
- diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
- A: difflib.SplitLines(eventsWanted),
- B: difflib.SplitLines(evtstr),
- FromFile: "Expected",
- FromDate: "",
- ToFile: "Actual",
- ToDate: "",
- Context: 1,
- })
- panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff))
- }
- }
- }
- case "Realm":
- // panic if got unexpected error
- if pnc != nil {
- if tv, ok := pnc.(*gno.TypedValue); ok {
- panic(fmt.Sprintf("fail on %s: got unexpected error: %s", path, tv.Sprint(m)))
- } else { // TODO: does this happen?
- panic(fmt.Sprintf("fail on %s: got unexpected error: %v", path, pnc))
- }
- }
- // check realm ops
- if rops != "" {
- rops2 := strings.TrimSpace(store.SprintStoreOps())
- if rops != rops2 {
- if f.syncWanted {
- // write output to file.
- replaceWantedInPlace(path, "Realm", rops2)
- } else {
- diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
- A: difflib.SplitLines(rops),
- B: difflib.SplitLines(rops2),
- FromFile: "Expected",
- FromDate: "",
- ToFile: "Actual",
- ToDate: "",
- Context: 1,
- })
- panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff))
- }
- }
- }
- case "Preprocessed":
- // check preprocessed AST.
- pn := store.GetBlockNode(gno.PackageNodeLocation(pkgPath))
- pre := pn.(*gno.PackageNode).FileSet.Files[0].String()
- if pre != preWanted {
- if f.syncWanted {
- // write error to file
- replaceWantedInPlace(path, "Preprocessed", pre)
- } else {
- // panic so tests immediately fail (for now).
- diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
- A: difflib.SplitLines(preWanted),
- B: difflib.SplitLines(pre),
- FromFile: "Expected",
- FromDate: "",
- ToFile: "Actual",
- ToDate: "",
- Context: 1,
- })
- panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff))
- }
- }
- case "Stacktrace":
- if stacktraceWanted != "" {
- var stacktrace string
-
- switch pnc.(type) {
- case gno.UnhandledPanicError:
- stacktrace = m.ExceptionsStacktrace()
- default:
- stacktrace = m.Stacktrace().String()
- }
-
- if f.syncWanted {
- // write stacktrace to file
- replaceWantedInPlace(path, "Stacktrace", stacktrace)
- } else {
- if !strings.Contains(stacktrace, stacktraceWanted) {
- diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
- A: difflib.SplitLines(stacktraceWanted),
- B: difflib.SplitLines(stacktrace),
- FromFile: "Expected",
- FromDate: "",
- ToFile: "Actual",
- ToDate: "",
- Context: 1,
- })
- panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff))
- }
- }
- }
- checkMachineIsEmpty = false
- default:
- return nil
- }
- }
- }
-
- if checkMachineIsEmpty {
- // Check that machine is empty.
- err = m.CheckEmpty()
- if err != nil {
- if f.logger != nil {
- f.logger("last state: \n", m.String())
- }
- panic(fmt.Sprintf("fail on %s: machine not empty after main: %v", path, err))
- }
- }
- return nil
-}
-
-func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops, events, stacktrace string, maxAlloc int64, send std.Coins, pre string) {
- fset := token.NewFileSet()
- f, err2 := parser.ParseFile(fset, p, nil, parser.ParseComments)
- if err2 != nil {
- panic(err2)
- }
- if len(f.Comments) == 0 {
- return
- }
- for _, comments := range f.Comments {
- text := readComments(comments)
- if strings.HasPrefix(text, "PKGPATH:") {
- line := strings.SplitN(text, "\n", 2)[0]
- pkgPath = strings.TrimSpace(strings.TrimPrefix(line, "PKGPATH:"))
- } else if strings.HasPrefix(text, "MAXALLOC:") {
- line := strings.SplitN(text, "\n", 2)[0]
- maxstr := strings.TrimSpace(strings.TrimPrefix(line, "MAXALLOC:"))
- maxint, err := strconv.Atoi(maxstr)
- if err != nil {
- panic(fmt.Sprintf("invalid maxalloc amount: %v", maxstr))
- }
- maxAlloc = int64(maxint)
- } else if strings.HasPrefix(text, "SEND:") {
- line := strings.SplitN(text, "\n", 2)[0]
- sendstr := strings.TrimSpace(strings.TrimPrefix(line, "SEND:"))
- send = std.MustParseCoins(sendstr)
- } else if strings.HasPrefix(text, "Output:\n") {
- res = strings.TrimPrefix(text, "Output:\n")
- res = strings.TrimSpace(res)
- directives = append(directives, "Output")
- } else if strings.HasPrefix(text, "Error:\n") {
- err = strings.TrimPrefix(text, "Error:\n")
- err = strings.TrimSpace(err)
- // XXX temporary until we support line:column.
- // If error starts with line:column, trim it.
- re := regexp.MustCompile(`^[0-9]+:[0-9]+: `)
- err = re.ReplaceAllString(err, "")
- directives = append(directives, "Error")
- } else if strings.HasPrefix(text, "Realm:\n") {
- rops = strings.TrimPrefix(text, "Realm:\n")
- rops = strings.TrimSpace(rops)
- directives = append(directives, "Realm")
- } else if strings.HasPrefix(text, "Events:\n") {
- events = strings.TrimPrefix(text, "Events:\n")
- events = strings.TrimSpace(events)
- directives = append(directives, "Events")
- } else if strings.HasPrefix(text, "Preprocessed:\n") {
- pre = strings.TrimPrefix(text, "Preprocessed:\n")
- pre = strings.TrimSpace(pre)
- directives = append(directives, "Preprocessed")
- } else if strings.HasPrefix(text, "Stacktrace:\n") {
- stacktrace = strings.TrimPrefix(text, "Stacktrace:\n")
- stacktrace = strings.TrimSpace(stacktrace)
- directives = append(directives, "Stacktrace")
- } else {
- // ignore unexpected.
- }
- }
- return
-}
-
-// readComments returns //-style comments from cg, but without truncating empty
-// lines like cg.Text().
-func readComments(cg *ast.CommentGroup) string {
- var b strings.Builder
- for _, c := range cg.List {
- if len(c.Text) < 2 || c.Text[:2] != "//" {
- // ignore no //-style comment
- break
- }
- s := strings.TrimPrefix(c.Text[2:], " ")
- b.WriteString(s + "\n")
- }
- return b.String()
-}
-
-// Replace comment in file with given output given directive.
-func replaceWantedInPlace(path string, directive string, output string) {
- bz := osm.MustReadFile(path)
- body := string(bz)
- lines := strings.Split(body, "\n")
- isReplacing := false
- wroteDirective := false
- newlines := []string(nil)
- for _, line := range lines {
- if line == "// "+directive+":" {
- if wroteDirective {
- isReplacing = true
- continue
- } else {
- wroteDirective = true
- isReplacing = true
- newlines = append(newlines, "// "+directive+":")
- outlines := strings.Split(output, "\n")
- for _, outline := range outlines {
- newlines = append(newlines,
- strings.TrimRight("// "+outline, " "))
- }
- continue
- }
- } else if isReplacing {
- if strings.HasPrefix(line, "//") {
- continue
- } else {
- isReplacing = false
- }
- }
- newlines = append(newlines, line)
- }
- osm.MustWriteFile(path, []byte(strings.Join(newlines, "\n")), 0o644)
-}
-
-func DefaultPkgName(gopkgPath string) gno.Name {
- parts := strings.Split(gopkgPath, "/")
- last := parts[len(parts)-1]
- parts = strings.Split(last, "-")
- name := parts[len(parts)-1]
- name = strings.ToLower(name)
- return gno.Name(name)
-}
-
-// go comments strip trailing spaces.
-func trimTrailingSpaces(result string) string {
- lines := strings.Split(result, "\n")
- for i, line := range lines {
- lines[i] = strings.TrimRight(line, " \t")
- }
- return strings.Join(lines, "\n")
-}
-
-// ----------------------------------------
-// testParams
-type testParams struct{}
-
-func newTestParams() *testParams {
- return &testParams{}
-}
-
-func (tp *testParams) SetBool(key string, val bool) { /* noop */ }
-func (tp *testParams) SetBytes(key string, val []byte) { /* noop */ }
-func (tp *testParams) SetInt64(key string, val int64) { /* noop */ }
-func (tp *testParams) SetUint64(key string, val uint64) { /* noop */ }
-func (tp *testParams) SetString(key string, val string) { /* noop */ }
-
-// ----------------------------------------
-// testBanker
-
-type testBanker struct {
- coinTable map[crypto.Bech32Address]std.Coins
-}
-
-func newTestBanker(args ...interface{}) *testBanker {
- coinTable := make(map[crypto.Bech32Address]std.Coins)
- if len(args)%2 != 0 {
- panic("newTestBanker requires even number of arguments; addr followed by coins")
- }
- for i := 0; i < len(args); i += 2 {
- addr := args[i].(crypto.Bech32Address)
- amount := args[i+1].(std.Coins)
- coinTable[addr] = amount
- }
- return &testBanker{
- coinTable: coinTable,
- }
-}
-
-func (tb *testBanker) GetCoins(addr crypto.Bech32Address) (dst std.Coins) {
- return tb.coinTable[addr]
-}
-
-func (tb *testBanker) SendCoins(from, to crypto.Bech32Address, amt std.Coins) {
- fcoins, fexists := tb.coinTable[from]
- if !fexists {
- panic(fmt.Sprintf(
- "source address %s does not exist",
- from.String()))
- }
- if !fcoins.IsAllGTE(amt) {
- panic(fmt.Sprintf(
- "source address %s has %s; cannot send %s",
- from.String(), fcoins, amt))
- }
- // First, subtract from 'from'.
- frest := fcoins.Sub(amt)
- tb.coinTable[from] = frest
- // Second, add to 'to'.
- // NOTE: even works when from==to, due to 2-step isolation.
- tcoins, _ := tb.coinTable[to]
- tsum := tcoins.Add(amt)
- tb.coinTable[to] = tsum
-}
-
-func (tb *testBanker) TotalCoin(denom string) int64 {
- panic("not yet implemented")
-}
-
-func (tb *testBanker) IssueCoin(addr crypto.Bech32Address, denom string, amt int64) {
- coins, _ := tb.coinTable[addr]
- sum := coins.Add(std.Coins{{Denom: denom, Amount: amt}})
- tb.coinTable[addr] = sum
-}
-
-func (tb *testBanker) RemoveCoin(addr crypto.Bech32Address, denom string, amt int64) {
- coins, _ := tb.coinTable[addr]
- rest := coins.Sub(std.Coins{{Denom: denom, Amount: amt}})
- tb.coinTable[addr] = rest
-}
diff --git a/gnovm/tests/file_test.go b/gnovm/tests/file_test.go
deleted file mode 100644
index 4313fd88645..00000000000
--- a/gnovm/tests/file_test.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package tests
-
-import (
- "flag"
- "io/fs"
- "os"
- "path"
- "path/filepath"
- "strings"
- "testing"
-
- gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
-)
-
-var withSync = flag.Bool("update-golden-tests", false, "rewrite tests updating Realm: and Output: with new values where changed")
-
-func TestFileStr(t *testing.T) {
- filePath := filepath.Join(".", "files", "str.gno")
- runFileTest(t, filePath, WithNativeLibs())
-}
-
-// Run tests in the `files` directory using shims from stdlib
-// to native go standard library.
-func TestFilesNative(t *testing.T) {
- baseDir := filepath.Join(".", "files")
- runFileTests(t, baseDir, []string{"*_stdlibs*"}, WithNativeLibs())
-}
-
-// Test files using standard library in stdlibs/.
-func TestFiles(t *testing.T) {
- baseDir := filepath.Join(".", "files")
- runFileTests(t, baseDir, []string{"*_native*"})
-}
-
-func TestChallenges(t *testing.T) {
- t.Skip("Challenge tests, skipping.")
- baseDir := filepath.Join(".", "challenges")
- runFileTests(t, baseDir, nil)
-}
-
-type testFile struct {
- path string
- fs.DirEntry
-}
-
-// ignore are glob patterns to ignore
-func runFileTests(t *testing.T, baseDir string, ignore []string, opts ...RunFileTestOption) {
- t.Helper()
-
- opts = append([]RunFileTestOption{WithSyncWanted(*withSync)}, opts...)
-
- files, err := readFiles(t, baseDir)
- if err != nil {
- t.Fatal(err)
- }
-
- files = filterFileTests(t, files, ignore)
- var path string
- var name string
- for _, file := range files {
- path = file.path
- name = strings.TrimPrefix(file.path, baseDir+string(os.PathSeparator))
- t.Run(name, func(t *testing.T) {
- runFileTest(t, path, opts...)
- })
- }
-}
-
-// it reads all files recursively in the directory
-func readFiles(t *testing.T, dir string) ([]testFile, error) {
- t.Helper()
- var files []testFile
-
- err := filepath.WalkDir(dir, func(path string, de fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
- if de.IsDir() && de.Name() == "extern" {
- return filepath.SkipDir
- }
- f := testFile{path: path, DirEntry: de}
-
- files = append(files, f)
- return nil
- })
- return files, err
-}
-
-func filterFileTests(t *testing.T, files []testFile, ignore []string) []testFile {
- t.Helper()
- filtered := make([]testFile, 0, 1000)
- var name string
-
- for _, f := range files {
- // skip none .gno files
- name = f.DirEntry.Name()
- if filepath.Ext(name) != ".gno" {
- continue
- }
- // skip ignored files
- if isIgnored(t, name, ignore) {
- continue
- }
- // skip _long file if we only want to test regular file.
- if testing.Short() && strings.Contains(name, "_long") {
- t.Logf("skipping test %s in short mode.", name)
- continue
- }
- filtered = append(filtered, f)
- }
- return filtered
-}
-
-func isIgnored(t *testing.T, name string, ignore []string) bool {
- t.Helper()
- isIgnore := false
- for _, is := range ignore {
- match, err := path.Match(is, name)
- if err != nil {
- t.Fatalf("error parsing glob pattern %q: %v", is, err)
- }
- if match {
- isIgnore = true
- break
- }
- }
- return isIgnore
-}
-
-func runFileTest(t *testing.T, path string, opts ...RunFileTestOption) {
- t.Helper()
-
- opts = append([]RunFileTestOption{WithSyncWanted(*withSync)}, opts...)
-
- var logger loggerFunc
- if gno.IsDebug() && testing.Verbose() {
- logger = t.Log
- }
- rootDir := filepath.Join("..", "..")
- err := RunFileTest(rootDir, path, append(opts, WithLoggerFunc(logger))...)
- if err != nil {
- t.Fatalf("got error: %v", err)
- }
-}
diff --git a/gnovm/tests/files/access0_stdlibs.gno b/gnovm/tests/files/access0.gno
similarity index 100%
rename from gnovm/tests/files/access0_stdlibs.gno
rename to gnovm/tests/files/access0.gno
diff --git a/gnovm/tests/files/access1_stdlibs.gno b/gnovm/tests/files/access1.gno
similarity index 52%
rename from gnovm/tests/files/access1_stdlibs.gno
rename to gnovm/tests/files/access1.gno
index 5a1bf4cc12e..bcbfdb2829c 100644
--- a/gnovm/tests/files/access1_stdlibs.gno
+++ b/gnovm/tests/files/access1.gno
@@ -9,4 +9,4 @@ func main() {
}
// Error:
-// main/files/access1_stdlibs.gno:8:10: cannot access gno.land/p/demo/testutils.testVar2 from main
+// main/files/access1.gno:8:10: cannot access gno.land/p/demo/testutils.testVar2 from main
diff --git a/gnovm/tests/files/access2_stdlibs.gno b/gnovm/tests/files/access2.gno
similarity index 100%
rename from gnovm/tests/files/access2_stdlibs.gno
rename to gnovm/tests/files/access2.gno
diff --git a/gnovm/tests/files/access3_stdlibs.gno b/gnovm/tests/files/access3.gno
similarity index 100%
rename from gnovm/tests/files/access3_stdlibs.gno
rename to gnovm/tests/files/access3.gno
diff --git a/gnovm/tests/files/access4_stdlibs.gno b/gnovm/tests/files/access4.gno
similarity index 56%
rename from gnovm/tests/files/access4_stdlibs.gno
rename to gnovm/tests/files/access4.gno
index e38a6d2ea4a..72c4f926ce4 100644
--- a/gnovm/tests/files/access4_stdlibs.gno
+++ b/gnovm/tests/files/access4.gno
@@ -10,4 +10,4 @@ func main() {
}
// Error:
-// main/files/access4_stdlibs.gno:9:10: cannot access gno.land/p/demo/testutils.TestAccessStruct.privateField from main
+// main/files/access4.gno:9:10: cannot access gno.land/p/demo/testutils.TestAccessStruct.privateField from main
diff --git a/gnovm/tests/files/access5_stdlibs.gno b/gnovm/tests/files/access5.gno
similarity index 100%
rename from gnovm/tests/files/access5_stdlibs.gno
rename to gnovm/tests/files/access5.gno
diff --git a/gnovm/tests/files/access6_stdlibs.gno b/gnovm/tests/files/access6.gno
similarity index 61%
rename from gnovm/tests/files/access6_stdlibs.gno
rename to gnovm/tests/files/access6.gno
index 443f2f5291d..04778a8f5bb 100644
--- a/gnovm/tests/files/access6_stdlibs.gno
+++ b/gnovm/tests/files/access6.gno
@@ -16,4 +16,4 @@ func main() {
}
// Error:
-// main/files/access6_stdlibs.gno:15:2: main.mystruct does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod)
+// main/files/access6.gno:15:2: main.mystruct does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod)
diff --git a/gnovm/tests/files/access7_stdlibs.gno b/gnovm/tests/files/access7.gno
similarity index 67%
rename from gnovm/tests/files/access7_stdlibs.gno
rename to gnovm/tests/files/access7.gno
index 01c9ed83fa0..3874ad98971 100644
--- a/gnovm/tests/files/access7_stdlibs.gno
+++ b/gnovm/tests/files/access7.gno
@@ -20,4 +20,4 @@ func main() {
}
// Error:
-// main/files/access7_stdlibs.gno:19:2: main.PrivateInterface2 does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod)
+// main/files/access7.gno:19:2: main.PrivateInterface2 does not implement gno.land/p/demo/testutils.PrivateInterface (missing method privateMethod)
diff --git a/gnovm/tests/files/addr0b_stdlibs.gno b/gnovm/tests/files/addr0b.gno
similarity index 100%
rename from gnovm/tests/files/addr0b_stdlibs.gno
rename to gnovm/tests/files/addr0b.gno
diff --git a/gnovm/tests/files/addr0b_native.gno b/gnovm/tests/files/addr0b_native.gno
deleted file mode 100644
index 86846500e42..00000000000
--- a/gnovm/tests/files/addr0b_native.gno
+++ /dev/null
@@ -1,25 +0,0 @@
-package main
-
-import (
- "fmt"
-
- "github.com/gnolang/gno/_test/net/http"
-)
-
-type extendedRequest struct {
- Request http.Request
-
- Data string
-}
-
-func main() {
- r := extendedRequest{}
- req := &r.Request
-
- fmt.Println(r)
- fmt.Println(req)
-}
-
-// Output:
-// {{ 0 0 map[] 0 [] false map[] map[] map[] } }
-// &{ 0 0 map[] 0 [] false map[] map[] map[] }
diff --git a/gnovm/tests/files/addr2b.gno b/gnovm/tests/files/addr2b.gno
index 04342c00574..59a18904bea 100644
--- a/gnovm/tests/files/addr2b.gno
+++ b/gnovm/tests/files/addr2b.gno
@@ -1,24 +1,22 @@
package main
import (
- "encoding/xml"
+ "encoding/json"
"fmt"
)
type Email struct {
- Where string `xml:"where,attr"`
+ Where string
Addr string
}
func f(s string, r interface{}) interface{} {
- return xml.Unmarshal([]byte(s), &r)
+ return json.Unmarshal([]byte(s), &r)
}
func main() {
data := `
-
- bob@work.com
-
+ {"Where": "work", "Addr": "bob@work.com"}
`
v := Email{}
err := f(data, &v)
diff --git a/gnovm/tests/files/assign0b_stdlibs.gno b/gnovm/tests/files/assign0b.gno
similarity index 100%
rename from gnovm/tests/files/assign0b_stdlibs.gno
rename to gnovm/tests/files/assign0b.gno
diff --git a/gnovm/tests/files/assign0b_native.gno b/gnovm/tests/files/assign0b_native.gno
deleted file mode 100644
index 42faa57634d..00000000000
--- a/gnovm/tests/files/assign0b_native.gno
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-
- "github.com/gnolang/gno/_test/net/http"
-)
-
-func main() {
- http.DefaultClient.Timeout = time.Second * 10
- fmt.Println(http.DefaultClient)
- http.DefaultClient = &http.Client{}
- fmt.Println(http.DefaultClient)
-}
-
-// Output:
-// &{ 10s}
-// &{ 0s}
diff --git a/gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest_stdlibs.gno b/gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest.gno
similarity index 100%
rename from gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest_stdlibs.gno
rename to gnovm/tests/files/assign_unnamed_type/more/cross_realm_compositelit_filetest.gno
diff --git a/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno b/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno
index d61170334d7..45f83bade5f 100644
--- a/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno
+++ b/gnovm/tests/files/assign_unnamed_type/more/realm_compositelit_filetest.gno
@@ -210,7 +210,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "files/assign_unnamed_type/more/realm_compositelit.gno",
// "IsMethod": false,
// "Name": "main",
// "NativeName": "",
@@ -221,7 +221,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "files/assign_unnamed_type/more/realm_compositelit.gno",
// "Line": "16",
// "PkgPath": "gno.land/r/test"
// }
diff --git a/gnovm/tests/files/bin1.gno b/gnovm/tests/files/bin1.gno
index 792651f60bf..e0e5a6d663a 100644
--- a/gnovm/tests/files/bin1.gno
+++ b/gnovm/tests/files/bin1.gno
@@ -1,16 +1,14 @@
package main
import (
- "crypto/sha1"
+ "crypto/sha256"
"fmt"
)
func main() {
- d := sha1.New()
- d.Write([]byte("password"))
- a := d.Sum(nil)
+ a := sha256.Sum256([]byte("password"))
fmt.Println(a)
}
// Output:
-// [91 170 97 228 201 185 63 63 6 130 37 11 108 248 51 27 126 230 143 216]
+// [94 136 72 152 218 40 4 113 81 208 229 111 141 198 41 39 115 96 61 13 106 171 189 214 42 17 239 114 29 21 66 216]
diff --git a/gnovm/tests/files/bin5.gno b/gnovm/tests/files/bin5.gno
deleted file mode 100644
index d471d4e0fd2..00000000000
--- a/gnovm/tests/files/bin5.gno
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "net"
-)
-
-func main() {
- addr := net.TCPAddr{IP: net.IPv4(1, 1, 1, 1), Port: 80}
- var s fmt.Stringer = &addr
- fmt.Println(s.String())
-}
-
-// Output:
-// 1.1.1.1:80
diff --git a/gnovm/tests/files/binstruct_ptr_map0.gno b/gnovm/tests/files/binstruct_ptr_map0.gno
index 329ece209e4..5eddca44f6e 100644
--- a/gnovm/tests/files/binstruct_ptr_map0.gno
+++ b/gnovm/tests/files/binstruct_ptr_map0.gno
@@ -2,11 +2,12 @@ package main
import (
"fmt"
- "image"
)
+type Point struct{ X, Y int }
+
func main() {
- v := map[string]*image.Point{
+ v := map[string]*Point{
"foo": {X: 3, Y: 2},
"bar": {X: 4, Y: 5},
}
@@ -14,4 +15,4 @@ func main() {
}
// Output:
-// (3,2) (4,5)
+// &{3 2} &{4 5}
diff --git a/gnovm/tests/files/binstruct_ptr_slice0.gno b/gnovm/tests/files/binstruct_ptr_slice0.gno
deleted file mode 100644
index 1ceea6cab70..00000000000
--- a/gnovm/tests/files/binstruct_ptr_slice0.gno
+++ /dev/null
@@ -1,17 +0,0 @@
-package main
-
-import (
- "fmt"
- "image"
-)
-
-func main() {
- v := []*image.Point{
- {X: 3, Y: 2},
- {X: 4, Y: 5},
- }
- fmt.Println(v)
-}
-
-// Output:
-// [(3,2) (4,5)]
diff --git a/gnovm/tests/files/binstruct_slice0.gno b/gnovm/tests/files/binstruct_slice0.gno
index 211f60faf01..3cdc455a66f 100644
--- a/gnovm/tests/files/binstruct_slice0.gno
+++ b/gnovm/tests/files/binstruct_slice0.gno
@@ -2,15 +2,16 @@ package main
import (
"fmt"
- "image"
)
+type Point struct{ X, Y int }
+
func main() {
- v := []image.Point{
+ v := []Point{
{X: 3, Y: 2},
}
fmt.Println(v)
}
// Output:
-// [(3,2)]
+// [{3 2}]
diff --git a/gnovm/tests/files/composite11.gno b/gnovm/tests/files/composite11.gno
index 85f71018202..2d989022f25 100644
--- a/gnovm/tests/files/composite11.gno
+++ b/gnovm/tests/files/composite11.gno
@@ -2,11 +2,14 @@ package main
import (
"fmt"
- "image/color"
)
+type NRGBA64 struct {
+ R, G, B, A uint16
+}
+
func main() {
- c := color.NRGBA64{1, 1, 1, 1}
+ c := NRGBA64{1, 1, 1, 1}
fmt.Println(c)
}
diff --git a/gnovm/tests/files/const14.gno b/gnovm/tests/files/const14.gno
index 835858f712d..93d7975e20f 100644
--- a/gnovm/tests/files/const14.gno
+++ b/gnovm/tests/files/const14.gno
@@ -1,13 +1,13 @@
package main
-import "compress/flate"
+import "math"
-func f1(i int) { println("i:", i) }
+func f1(i float64) { println("i:", i) }
func main() {
- i := flate.BestSpeed
+ i := math.Pi
f1(i)
}
// Output:
-// i: 1
+// i: 3.141592653589793
diff --git a/gnovm/tests/files/const22.gno b/gnovm/tests/files/const22.gno
index 42842066265..f92fcd4d910 100644
--- a/gnovm/tests/files/const22.gno
+++ b/gnovm/tests/files/const22.gno
@@ -31,7 +31,7 @@ func main() {
fmt.Printf("%x", ha)
fmt.Printf("%x", hb)
- fmt.Printf("%x", ho)
+ fmt.Printf("%x\n", ho)
}
// Output:
diff --git a/gnovm/tests/files/context.gno b/gnovm/tests/files/context.gno
deleted file mode 100644
index 0dcd8d73a22..00000000000
--- a/gnovm/tests/files/context.gno
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import "context"
-
-func get(ctx context.Context, k string) string {
- var r string
- if v := ctx.Value(k); v != nil {
- r = v.(string)
- }
- return r
-}
-
-func main() {
- ctx := context.WithValue(context.Background(), "hello", "world")
- println(get(ctx, "hello"))
-}
-
-// Output:
-// world
diff --git a/gnovm/tests/files/context2.gno b/gnovm/tests/files/context2.gno
deleted file mode 100644
index 457fb03b735..00000000000
--- a/gnovm/tests/files/context2.gno
+++ /dev/null
@@ -1,22 +0,0 @@
-package main
-
-import "context"
-
-func get(ctx context.Context, k string) string {
- var r string
- var ok bool
- if v := ctx.Value(k); v != nil {
- r, ok = v.(string)
- println(ok)
- }
- return r
-}
-
-func main() {
- ctx := context.WithValue(context.Background(), "hello", "world")
- println(get(ctx, "hello"))
-}
-
-// Output:
-// true
-// world
diff --git a/gnovm/tests/files/defer4.gno b/gnovm/tests/files/defer4.gno
deleted file mode 100644
index e9baa5ac250..00000000000
--- a/gnovm/tests/files/defer4.gno
+++ /dev/null
@@ -1,23 +0,0 @@
-package main
-
-import "sync"
-
-type T struct {
- mu sync.RWMutex
- name string
-}
-
-func (t *T) get() string {
- t.mu.RLock()
- defer t.mu.RUnlock()
- return t.name
-}
-
-var d = T{name: "test"}
-
-func main() {
- println(d.get())
-}
-
-// Output:
-// test
diff --git a/gnovm/tests/files/extern/p1/s1.gno b/gnovm/tests/files/extern/p1/s1.gno
deleted file mode 100644
index ff2d89d4462..00000000000
--- a/gnovm/tests/files/extern/p1/s1.gno
+++ /dev/null
@@ -1,5 +0,0 @@
-package p1
-
-import "crypto/rand"
-
-var Prime = rand.Prime
diff --git a/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno b/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno
index 2411bd2081f..13f94950dfa 100644
--- a/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno
+++ b/gnovm/tests/files/extern/timtadh/data_structures/types/string.gno
@@ -2,7 +2,7 @@ package types
import (
"bytes"
- "hash/fnv"
+ "crypto/sha256"
)
type (
@@ -36,9 +36,7 @@ func (self String) Less(other Sortable) bool {
}
func (self String) Hash() int {
- h := fnv.New32a()
- h.Write([]byte(string(self)))
- return int(h.Sum32())
+ return int(hash([]byte(self)))
}
func (self *ByteSlice) MarshalBinary() ([]byte, error) {
@@ -67,7 +65,13 @@ func (self ByteSlice) Less(other Sortable) bool {
}
func (self ByteSlice) Hash() int {
- h := fnv.New32a()
- h.Write([]byte(self))
- return int(h.Sum32())
+ return int(hash([]byte(self)))
+}
+
+func hash(s []byte) int {
+ res := sha256.Sum256(s)
+ return int(s[0]) |
+ int(s[1]<<8) |
+ int(s[2]<<16) |
+ int(s[3]<<24)
}
diff --git a/gnovm/tests/files/float5_stdlibs.gno b/gnovm/tests/files/float5.gno
similarity index 100%
rename from gnovm/tests/files/float5_stdlibs.gno
rename to gnovm/tests/files/float5.gno
diff --git a/gnovm/tests/files/fun6.gno b/gnovm/tests/files/fun6.gno
deleted file mode 100644
index c5ec644afd5..00000000000
--- a/gnovm/tests/files/fun6.gno
+++ /dev/null
@@ -1,21 +0,0 @@
-package main
-
-import (
- "fmt"
- "sync"
-)
-
-func NewPool() Pool { return Pool{} }
-
-type Pool struct {
- P *sync.Pool
-}
-
-var _pool = NewPool()
-
-func main() {
- fmt.Println(_pool)
-}
-
-// Output:
-// {}
diff --git a/gnovm/tests/files/fun6b.gno b/gnovm/tests/files/fun6b.gno
deleted file mode 100644
index 17b0473b33b..00000000000
--- a/gnovm/tests/files/fun6b.gno
+++ /dev/null
@@ -1,20 +0,0 @@
-package main
-
-import (
- "sync"
-)
-
-func NewPool() Pool { return Pool{} }
-
-type Pool struct {
- p *sync.Pool
-}
-
-var _pool = NewPool()
-
-func main() {
- println(_pool)
-}
-
-// Output:
-// (struct{(gonative{} gonative{*sync.Pool})} main.Pool)
diff --git a/gnovm/tests/files/fun7.gno b/gnovm/tests/files/fun7.gno
deleted file mode 100644
index ee8f813a527..00000000000
--- a/gnovm/tests/files/fun7.gno
+++ /dev/null
@@ -1,18 +0,0 @@
-package main
-
-import (
- goflag "flag"
- "fmt"
-)
-
-func Foo(goflag *goflag.Flag) {
- fmt.Println(goflag)
-}
-
-func main() {
- g := &goflag.Flag{}
- Foo(g)
-}
-
-// Output:
-// &{ }
diff --git a/gnovm/tests/files/heap_alloc_forloop9_1.gno b/gnovm/tests/files/heap_alloc_forloop9_1.gno
index 5e3b9af74f6..2576b9b4da6 100644
--- a/gnovm/tests/files/heap_alloc_forloop9_1.gno
+++ b/gnovm/tests/files/heap_alloc_forloop9_1.gno
@@ -19,7 +19,7 @@ func main() {
// file{ package main; func Search(n (const-type int), f func(.arg_0 (const-type int)) (const-type bool)) (const-type int) { f((const (1 int))); return (const (0 int)) }; func main() { for x := (const (0 int)); x<~VPBlock(1,0)> < (const (2 int)); x<~VPBlock(1,0)>++ { count := (const (0 int)); (const (println func(xs ...interface{})()))((const (" first: count: " string)), count<~VPBlock(1,1)>); Search((const (1 int)), func func(i (const-type int)) (const-type bool){ count<~VPBlock(1,2)>++; return (const-type bool)(i >= x<~VPBlock(1,3)>) }, x<()~VPBlock(1,0)>>); (const (println func(xs ...interface{})()))((const ("second: count: " string)), count<~VPBlock(1,1)>) } } }
// Output:
-// first: count: 0
+// first: count: 0
// second: count: 1
// first: count: 0
// second: count: 1
diff --git a/gnovm/tests/files/heap_item_value.gno b/gnovm/tests/files/heap_item_value.gno
index 40ec05d3ba1..80bf702bec2 100644
--- a/gnovm/tests/files/heap_item_value.gno
+++ b/gnovm/tests/files/heap_item_value.gno
@@ -151,7 +151,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "files/heap_item_value.gno",
// "IsMethod": false,
// "Name": "main",
// "NativeName": "",
@@ -162,7 +162,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "files/heap_item_value.gno",
// "Line": "10",
// "PkgPath": "gno.land/r/test"
// }
diff --git a/gnovm/tests/files/heap_item_value_init.gno b/gnovm/tests/files/heap_item_value_init.gno
index 72f065326f1..2722cce8675 100644
--- a/gnovm/tests/files/heap_item_value_init.gno
+++ b/gnovm/tests/files/heap_item_value_init.gno
@@ -122,7 +122,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "files/heap_item_value_init.gno",
// "IsMethod": false,
// "Name": "init.3",
// "NativeName": "",
@@ -133,7 +133,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "files/heap_item_value_init.gno",
// "Line": "10",
// "PkgPath": "gno.land/r/test"
// }
@@ -158,7 +158,7 @@ func main() {
// "Escaped": true,
// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3"
// },
-// "FileName": "main.gno",
+// "FileName": "files/heap_item_value_init.gno",
// "IsMethod": false,
// "Name": "main",
// "NativeName": "",
@@ -169,7 +169,7 @@ func main() {
// "BlockNode": null,
// "Location": {
// "Column": "1",
-// "File": "main.gno",
+// "File": "files/heap_item_value_init.gno",
// "Line": "16",
// "PkgPath": "gno.land/r/test"
// }
diff --git a/gnovm/tests/files/import3.gno b/gnovm/tests/files/import3.gno
index c16ac626299..c63ed8a055c 100644
--- a/gnovm/tests/files/import3.gno
+++ b/gnovm/tests/files/import3.gno
@@ -4,7 +4,8 @@ import "github.com/gnolang/gno/_test/foo"
func main() { println(foo.Bar, foo.Boo) }
+// Init functions of dependencies are executed separatedly from the test itself,
+// so they don't print with the test proper.
+
// Output:
-// init boo
-// init foo
// BARR Boo
diff --git a/gnovm/tests/files/import5.gno b/gnovm/tests/files/import5.gno
index 609364d85b1..b270d0b0d3c 100644
--- a/gnovm/tests/files/import5.gno
+++ b/gnovm/tests/files/import5.gno
@@ -5,6 +5,4 @@ import boo "github.com/gnolang/gno/_test/foo"
func main() { println(boo.Bar, boo.Boo, boo.Bir) }
// Output:
-// init boo
-// init foo
// BARR Boo Boo22
diff --git a/gnovm/tests/files/interp.gi b/gnovm/tests/files/interp.gi
deleted file mode 100644
index ace895a356c..00000000000
--- a/gnovm/tests/files/interp.gi
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
- "github.com/gnolang/gno/interp"
-)
-
-func main() {
- i := interp.New(interp.Opt{})
- i.Eval(`println("Hello")`)
-}
-
-// Output:
-// Hello
diff --git a/gnovm/tests/files/interp2.gi b/gnovm/tests/files/interp2.gi
deleted file mode 100644
index af3ffd75f18..00000000000
--- a/gnovm/tests/files/interp2.gi
+++ /dev/null
@@ -1,16 +0,0 @@
-package main
-
-import (
- "github.com/gnolang/gno/interp"
-)
-
-func main() {
- i := interp.New(interp.Opt{})
- i.Use(interp.ExportValue, interp.ExportType)
- i.Eval(`import "github.com/gnolang/gno/interp"`)
- i.Eval(`i := interp.New(interp.Opt{})`)
- i.Eval(`i.Eval("println(42)")`)
-}
-
-// Output:
-// 42
diff --git a/gnovm/tests/files/io0_stdlibs.gno b/gnovm/tests/files/io0.gno
similarity index 100%
rename from gnovm/tests/files/io0_stdlibs.gno
rename to gnovm/tests/files/io0.gno
diff --git a/gnovm/tests/files/io0_native.gno b/gnovm/tests/files/io0_native.gno
deleted file mode 100644
index 6486a9ba558..00000000000
--- a/gnovm/tests/files/io0_native.gno
+++ /dev/null
@@ -1,18 +0,0 @@
-package main
-
-import (
- "crypto/rand"
- "fmt"
- "io"
-)
-
-func main() {
- var buf [16]byte
- fmt.Println(buf)
- io.ReadFull(rand.Reader, buf[:])
- fmt.Println(buf)
-}
-
-// Output:
-// [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
-// [100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115]
diff --git a/gnovm/tests/files/io2.gno b/gnovm/tests/files/io2.gno
index 24655f5040c..d0637c44e16 100644
--- a/gnovm/tests/files/io2.gno
+++ b/gnovm/tests/files/io2.gno
@@ -3,7 +3,6 @@ package main
import (
"fmt"
"io"
- "log"
"strings"
)
@@ -12,9 +11,9 @@ func main() {
b, err := io.ReadAll(r)
if err != nil {
- log.Fatal(err)
+ panic(err)
}
- fmt.Printf("%s", b)
+ fmt.Printf("%s\n", b)
}
// Output:
diff --git a/gnovm/tests/files/issue_558b_stdlibs.gno b/gnovm/tests/files/issue_558b.gno
similarity index 97%
rename from gnovm/tests/files/issue_558b_stdlibs.gno
rename to gnovm/tests/files/issue_558b.gno
index 55eba88c985..51ddee56e0a 100644
--- a/gnovm/tests/files/issue_558b_stdlibs.gno
+++ b/gnovm/tests/files/issue_558b.gno
@@ -3,8 +3,6 @@ package main
import (
"fmt"
"io"
- "io"
- "log"
"strings"
)
@@ -68,7 +66,7 @@ func main() {
p.Reader = newReadAutoCloser(strings.NewReader("test"))
b, err := ReadAll(p.Reader)
if err != nil {
- log.Fatal(err)
+ panic(err)
}
fmt.Println(string(b))
}
diff --git a/gnovm/tests/files/issue_782.gno b/gnovm/tests/files/issue_782.gno
index 9d89a90bd30..4dc938ceaec 100644
--- a/gnovm/tests/files/issue_782.gno
+++ b/gnovm/tests/files/issue_782.gno
@@ -7,7 +7,7 @@ func main() {
from := uint32(2)
to := uint32(4)
b := a[from:to]
- fmt.Print(b)
+ fmt.Println(b)
}
// Output:
diff --git a/gnovm/tests/files/l3_long.gno b/gnovm/tests/files/l3_long.gno
deleted file mode 100644
index 64e75f522b2..00000000000
--- a/gnovm/tests/files/l3_long.gno
+++ /dev/null
@@ -1,163 +0,0 @@
-package main
-
-func main() {
- for a := 0; a < 20000000; a++ {
- if a&0x8ffff == 0x80000 {
- println(a)
- }
- }
-}
-
-// Output:
-// 524288
-// 589824
-// 655360
-// 720896
-// 786432
-// 851968
-// 917504
-// 983040
-// 1572864
-// 1638400
-// 1703936
-// 1769472
-// 1835008
-// 1900544
-// 1966080
-// 2031616
-// 2621440
-// 2686976
-// 2752512
-// 2818048
-// 2883584
-// 2949120
-// 3014656
-// 3080192
-// 3670016
-// 3735552
-// 3801088
-// 3866624
-// 3932160
-// 3997696
-// 4063232
-// 4128768
-// 4718592
-// 4784128
-// 4849664
-// 4915200
-// 4980736
-// 5046272
-// 5111808
-// 5177344
-// 5767168
-// 5832704
-// 5898240
-// 5963776
-// 6029312
-// 6094848
-// 6160384
-// 6225920
-// 6815744
-// 6881280
-// 6946816
-// 7012352
-// 7077888
-// 7143424
-// 7208960
-// 7274496
-// 7864320
-// 7929856
-// 7995392
-// 8060928
-// 8126464
-// 8192000
-// 8257536
-// 8323072
-// 8912896
-// 8978432
-// 9043968
-// 9109504
-// 9175040
-// 9240576
-// 9306112
-// 9371648
-// 9961472
-// 10027008
-// 10092544
-// 10158080
-// 10223616
-// 10289152
-// 10354688
-// 10420224
-// 11010048
-// 11075584
-// 11141120
-// 11206656
-// 11272192
-// 11337728
-// 11403264
-// 11468800
-// 12058624
-// 12124160
-// 12189696
-// 12255232
-// 12320768
-// 12386304
-// 12451840
-// 12517376
-// 13107200
-// 13172736
-// 13238272
-// 13303808
-// 13369344
-// 13434880
-// 13500416
-// 13565952
-// 14155776
-// 14221312
-// 14286848
-// 14352384
-// 14417920
-// 14483456
-// 14548992
-// 14614528
-// 15204352
-// 15269888
-// 15335424
-// 15400960
-// 15466496
-// 15532032
-// 15597568
-// 15663104
-// 16252928
-// 16318464
-// 16384000
-// 16449536
-// 16515072
-// 16580608
-// 16646144
-// 16711680
-// 17301504
-// 17367040
-// 17432576
-// 17498112
-// 17563648
-// 17629184
-// 17694720
-// 17760256
-// 18350080
-// 18415616
-// 18481152
-// 18546688
-// 18612224
-// 18677760
-// 18743296
-// 18808832
-// 19398656
-// 19464192
-// 19529728
-// 19595264
-// 19660800
-// 19726336
-// 19791872
-// 19857408
diff --git a/gnovm/tests/files/l4_long.gno b/gnovm/tests/files/l4_long.gno
deleted file mode 100644
index f91542999b3..00000000000
--- a/gnovm/tests/files/l4_long.gno
+++ /dev/null
@@ -1,7 +0,0 @@
-package main
-
-func main() { println(f(5)) }
-func f(i int) int { return i + 1 }
-
-// Output:
-// 6
diff --git a/gnovm/tests/files/l5_long.gno b/gnovm/tests/files/l5_long.gno
deleted file mode 100644
index c72357d2e15..00000000000
--- a/gnovm/tests/files/l5_long.gno
+++ /dev/null
@@ -1,164 +0,0 @@
-package main
-
-func main() {
- for a := 0; a < 20000000; {
- if a&0x8ffff == 0x80000 {
- println(a)
- }
- a = a + 1
- }
-}
-
-// Output:
-// 524288
-// 589824
-// 655360
-// 720896
-// 786432
-// 851968
-// 917504
-// 983040
-// 1572864
-// 1638400
-// 1703936
-// 1769472
-// 1835008
-// 1900544
-// 1966080
-// 2031616
-// 2621440
-// 2686976
-// 2752512
-// 2818048
-// 2883584
-// 2949120
-// 3014656
-// 3080192
-// 3670016
-// 3735552
-// 3801088
-// 3866624
-// 3932160
-// 3997696
-// 4063232
-// 4128768
-// 4718592
-// 4784128
-// 4849664
-// 4915200
-// 4980736
-// 5046272
-// 5111808
-// 5177344
-// 5767168
-// 5832704
-// 5898240
-// 5963776
-// 6029312
-// 6094848
-// 6160384
-// 6225920
-// 6815744
-// 6881280
-// 6946816
-// 7012352
-// 7077888
-// 7143424
-// 7208960
-// 7274496
-// 7864320
-// 7929856
-// 7995392
-// 8060928
-// 8126464
-// 8192000
-// 8257536
-// 8323072
-// 8912896
-// 8978432
-// 9043968
-// 9109504
-// 9175040
-// 9240576
-// 9306112
-// 9371648
-// 9961472
-// 10027008
-// 10092544
-// 10158080
-// 10223616
-// 10289152
-// 10354688
-// 10420224
-// 11010048
-// 11075584
-// 11141120
-// 11206656
-// 11272192
-// 11337728
-// 11403264
-// 11468800
-// 12058624
-// 12124160
-// 12189696
-// 12255232
-// 12320768
-// 12386304
-// 12451840
-// 12517376
-// 13107200
-// 13172736
-// 13238272
-// 13303808
-// 13369344
-// 13434880
-// 13500416
-// 13565952
-// 14155776
-// 14221312
-// 14286848
-// 14352384
-// 14417920
-// 14483456
-// 14548992
-// 14614528
-// 15204352
-// 15269888
-// 15335424
-// 15400960
-// 15466496
-// 15532032
-// 15597568
-// 15663104
-// 16252928
-// 16318464
-// 16384000
-// 16449536
-// 16515072
-// 16580608
-// 16646144
-// 16711680
-// 17301504
-// 17367040
-// 17432576
-// 17498112
-// 17563648
-// 17629184
-// 17694720
-// 17760256
-// 18350080
-// 18415616
-// 18481152
-// 18546688
-// 18612224
-// 18677760
-// 18743296
-// 18808832
-// 19398656
-// 19464192
-// 19529728
-// 19595264
-// 19660800
-// 19726336
-// 19791872
-// 19857408
diff --git a/gnovm/tests/files/l2_long.gno b/gnovm/tests/files/loop0.gno
similarity index 100%
rename from gnovm/tests/files/l2_long.gno
rename to gnovm/tests/files/loop0.gno
diff --git a/gnovm/tests/files/loop1.gno b/gnovm/tests/files/loop1.gno
new file mode 100644
index 00000000000..4a61fbfba5b
--- /dev/null
+++ b/gnovm/tests/files/loop1.gno
@@ -0,0 +1,51 @@
+package main
+
+func main() {
+ for a := 0; a < 20000; {
+ if (a & 0x8ff) == 0x800 {
+ println(a)
+ }
+ a = a + 1
+ }
+}
+
+// Output:
+// 2048
+// 2304
+// 2560
+// 2816
+// 3072
+// 3328
+// 3584
+// 3840
+// 6144
+// 6400
+// 6656
+// 6912
+// 7168
+// 7424
+// 7680
+// 7936
+// 10240
+// 10496
+// 10752
+// 11008
+// 11264
+// 11520
+// 11776
+// 12032
+// 14336
+// 14592
+// 14848
+// 15104
+// 15360
+// 15616
+// 15872
+// 16128
+// 18432
+// 18688
+// 18944
+// 19200
+// 19456
+// 19712
+// 19968
diff --git a/gnovm/tests/files/map27.gno b/gnovm/tests/files/map27.gno
index 5d76ffc21c7..578788d144e 100644
--- a/gnovm/tests/files/map27.gno
+++ b/gnovm/tests/files/map27.gno
@@ -2,7 +2,6 @@ package main
import (
"fmt"
- "text/template"
)
type fm map[string]interface{}
@@ -14,7 +13,7 @@ func main() {
a["foo"] = &foo{}
fmt.Println(a["foo"])
- b := make(template.FuncMap) // type FuncMap map[string]interface{}
+ b := make(map[string]interface{})
b["foo"] = &foo{}
fmt.Println(b["foo"])
}
diff --git a/gnovm/tests/files/map29_stdlibs.gno b/gnovm/tests/files/map29.gno
similarity index 100%
rename from gnovm/tests/files/map29_stdlibs.gno
rename to gnovm/tests/files/map29.gno
diff --git a/gnovm/tests/files/map29_native.gno b/gnovm/tests/files/map29_native.gno
deleted file mode 100644
index b4a4129cd39..00000000000
--- a/gnovm/tests/files/map29_native.gno
+++ /dev/null
@@ -1,26 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-type Item struct {
- Object interface{}
- Expiry time.Duration
-}
-
-func main() {
- items := map[string]Item{}
-
- items["test"] = Item{
- Object: "test",
- Expiry: time.Second,
- }
-
- item := items["test"]
- fmt.Println(item)
-}
-
-// Output:
-// {test 1s}
diff --git a/gnovm/tests/files/math0_stdlibs.gno b/gnovm/tests/files/math0.gno
similarity index 100%
rename from gnovm/tests/files/math0_stdlibs.gno
rename to gnovm/tests/files/math0.gno
diff --git a/gnovm/tests/files/math3.gno b/gnovm/tests/files/math3.gno
index 592af0aa89d..a0ed2e1aa1e 100644
--- a/gnovm/tests/files/math3.gno
+++ b/gnovm/tests/files/math3.gno
@@ -1,32 +1,27 @@
package main
import (
- "crypto/md5"
+ "crypto/sha256"
"fmt"
)
-func md5Crypt(password, salt, magic []byte) []byte {
- d := md5.New()
- d.Write(password)
- d.Write(magic)
- d.Write(salt)
+func sha256Crypt(password, salt, magic string) []byte {
+ toHash := password + magic + salt
+ mixin := sha256.Sum256([]byte(password + salt))
- d2 := md5.New()
- d2.Write(password)
- d2.Write(salt)
-
- for i, mixin := 0, d2.Sum(nil); i < len(password); i++ {
- d.Write([]byte{mixin[i%16]})
+ for i := 0; i < len(password); i++ {
+ toHash += string(mixin[i%32])
}
- return ([]byte)(d.Sum(nil)) // gonative{[]byte} -> []byte
+ res := sha256.Sum256([]byte(toHash))
+ return res[:]
}
func main() {
- b := md5Crypt([]byte("1"), []byte("2"), []byte("3"))
+ b := sha256Crypt("1", "2", "3")
fmt.Println(b)
}
// Output:
-// [187 141 73 89 101 229 33 106 226 63 117 234 117 149 230 21]
+// [172 65 148 29 23 72 77 86 46 80 184 188 192 158 154 11 145 11 197 253 206 210 141 253 188 27 157 126 89 142 179 143]
diff --git a/gnovm/tests/files/math_native.gno b/gnovm/tests/files/math5.gno
similarity index 100%
rename from gnovm/tests/files/math_native.gno
rename to gnovm/tests/files/math5.gno
diff --git a/gnovm/tests/files/method16b.gno b/gnovm/tests/files/method16b.gno
index 421a9f44e7b..4f36f48aa37 100644
--- a/gnovm/tests/files/method16b.gno
+++ b/gnovm/tests/files/method16b.gno
@@ -9,7 +9,7 @@ type Cheese struct {
}
func (t *Cheese) Hello(param string) {
- fmt.Printf("%+v %+v", t, param)
+ fmt.Printf("%+v %+v\n", t, param)
}
func main() {
diff --git a/gnovm/tests/files/method18.gno b/gnovm/tests/files/method18.gno
deleted file mode 100644
index 3da9580dc02..00000000000
--- a/gnovm/tests/files/method18.gno
+++ /dev/null
@@ -1,29 +0,0 @@
-package main
-
-import (
- "compress/gzip"
- "fmt"
-
- "github.com/gnolang/gno/_test/net/http"
-)
-
-type GzipResponseWriter struct {
- http.ResponseWriter
- index int
- gw *gzip.Writer
-}
-
-type GzipResponseWriterWithCloseNotify struct {
- *GzipResponseWriter
-}
-
-func (w GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool {
- return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
-}
-
-func main() {
- fmt.Println("hello")
-}
-
-// Output:
-// hello
diff --git a/gnovm/tests/files/method20.gno b/gnovm/tests/files/method20.gno
index 7561451b699..7f3bce4c806 100644
--- a/gnovm/tests/files/method20.gno
+++ b/gnovm/tests/files/method20.gno
@@ -2,16 +2,11 @@ package main
import (
"fmt"
- "sync"
)
-type Hello struct {
- mu sync.Mutex
-}
+type Hello struct{}
func (h *Hello) Hi() string {
- h.mu.Lock()
- h.mu.Unlock()
return "hi"
}
diff --git a/gnovm/tests/files/method24.gno b/gnovm/tests/files/method24.gno
deleted file mode 100644
index 624f4397b68..00000000000
--- a/gnovm/tests/files/method24.gno
+++ /dev/null
@@ -1,33 +0,0 @@
-package main
-
-import (
- "fmt"
- "sync"
-)
-
-type Pool struct {
- P *sync.Pool
-}
-
-func (p Pool) Get() *Buffer { return &Buffer{} }
-
-func NewPool() Pool { return Pool{} }
-
-type Buffer struct {
- Bs []byte
- Pool Pool
-}
-
-var (
- _pool = NewPool()
- Get = _pool.Get
-)
-
-func main() {
- fmt.Println(_pool)
- fmt.Println(Get())
-}
-
-// Output:
-// {}
-// &{[] {}}
diff --git a/gnovm/tests/files/method25.gno b/gnovm/tests/files/method25.gno
deleted file mode 100644
index a9dff18b6fb..00000000000
--- a/gnovm/tests/files/method25.gno
+++ /dev/null
@@ -1,33 +0,0 @@
-package main
-
-import (
- "fmt"
- "sync"
-)
-
-func (p Pool) Get() *Buffer { return &Buffer{} }
-
-func NewPool() Pool { return Pool{} }
-
-type Buffer struct {
- Bs []byte
- Pool Pool
-}
-
-type Pool struct {
- P *sync.Pool
-}
-
-var (
- _pool = NewPool()
- Get = _pool.Get
-)
-
-func main() {
- fmt.Println(_pool)
- fmt.Println(Get())
-}
-
-// Output:
-// {}
-// &{[] {}}
diff --git a/gnovm/tests/files/op0.gno b/gnovm/tests/files/op0.gno
index 860f525a3bd..3c599928be6 100644
--- a/gnovm/tests/files/op0.gno
+++ b/gnovm/tests/files/op0.gno
@@ -7,7 +7,7 @@ func main() {
a = 64
b = 64
c = a * b
- fmt.Printf("c: %v %T", c, c)
+ fmt.Printf("c: %v %T\n", c, c)
}
// Output:
diff --git a/gnovm/tests/files/print0.gno b/gnovm/tests/files/print0.gno
index 43cdcf19d39..cab6a7943d1 100644
--- a/gnovm/tests/files/print0.gno
+++ b/gnovm/tests/files/print0.gno
@@ -2,6 +2,7 @@ package main
func main() {
print("hello")
+ println()
}
// Output:
diff --git a/gnovm/tests/files/sample.plugin b/gnovm/tests/files/sample.plugin
deleted file mode 100644
index cbe637b73af..00000000000
--- a/gnovm/tests/files/sample.plugin
+++ /dev/null
@@ -1,19 +0,0 @@
-package sample
-
-import (
- "fmt"
- "net/http"
-)
-
-type Sample struct{}
-
-func (s *Sample) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- r.Header.Set("X-sample-test", "Hello")
- if next != nil {
- next(w, r)
- }
-}
-
-func Test() {
- fmt.Println("Hello from toto.Test()")
-}
diff --git a/gnovm/tests/files/secure.gi b/gnovm/tests/files/secure.gi
deleted file mode 100644
index 3ac731a85ff..00000000000
--- a/gnovm/tests/files/secure.gi
+++ /dev/null
@@ -1,33 +0,0 @@
-package main
-
-import (
- "net/http"
-
- "github.com/unrolled/secure" // or "gopkg.in/unrolled/secure.v1"
-)
-
-var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("hello world"))
-})
-
-func main() {
- secureMiddleware := secure.New(secure.Options{
- AllowedHosts: []string{"example.com", "ssl.example.com"},
- HostsProxyHeaders: []string{"X-Forwarded-Host"},
- SSLRedirect: true,
- SSLHost: "ssl.example.com",
- SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
- STSSeconds: 315360000,
- STSIncludeSubdomains: true,
- STSPreload: true,
- FrameDeny: true,
- ContentTypeNosniff: true,
- BrowserXssFilter: true,
- ContentSecurityPolicy: "script-src $NONCE",
- PublicKey: `pin-sha256="base64+primary=="; pin-sha256="base64+backup=="; max-age=5184000; includeSubdomains; report-uri="https://www.example.com/hpkp-report"`,
- IsDevelopment: false,
- })
-
- app := secureMiddleware.Handler(myHandler)
- http.ListenAndServe("127.0.0.1:3000", app)
-}
diff --git a/gnovm/tests/files/std0_stdlibs.gno b/gnovm/tests/files/std0.gno
similarity index 100%
rename from gnovm/tests/files/std0_stdlibs.gno
rename to gnovm/tests/files/std0.gno
diff --git a/gnovm/tests/files/std10_stdlibs.gno b/gnovm/tests/files/std10.gno
similarity index 100%
rename from gnovm/tests/files/std10_stdlibs.gno
rename to gnovm/tests/files/std10.gno
diff --git a/gnovm/tests/files/std11_stdlibs.gno b/gnovm/tests/files/std11.gno
similarity index 100%
rename from gnovm/tests/files/std11_stdlibs.gno
rename to gnovm/tests/files/std11.gno
diff --git a/gnovm/tests/files/std2_stdlibs.gno b/gnovm/tests/files/std2.gno
similarity index 100%
rename from gnovm/tests/files/std2_stdlibs.gno
rename to gnovm/tests/files/std2.gno
diff --git a/gnovm/tests/files/std3_stdlibs.gno b/gnovm/tests/files/std3.gno
similarity index 100%
rename from gnovm/tests/files/std3_stdlibs.gno
rename to gnovm/tests/files/std3.gno
diff --git a/gnovm/tests/files/std4_stdlibs.gno b/gnovm/tests/files/std4.gno
similarity index 100%
rename from gnovm/tests/files/std4_stdlibs.gno
rename to gnovm/tests/files/std4.gno
diff --git a/gnovm/tests/files/std5_stdlibs.gno b/gnovm/tests/files/std5.gno
similarity index 90%
rename from gnovm/tests/files/std5_stdlibs.gno
rename to gnovm/tests/files/std5.gno
index 4afa09da8d3..54cfb7846ab 100644
--- a/gnovm/tests/files/std5_stdlibs.gno
+++ b/gnovm/tests/files/std5.gno
@@ -18,7 +18,7 @@ func main() {
// std.GetCallerAt(2)
// std/native.gno:44
// main()
-// main/files/std5_stdlibs.gno:10
+// main/files/std5.gno:10
// Error:
// frame not found
diff --git a/gnovm/tests/files/std6_stdlibs.gno b/gnovm/tests/files/std6.gno
similarity index 100%
rename from gnovm/tests/files/std6_stdlibs.gno
rename to gnovm/tests/files/std6.gno
diff --git a/gnovm/tests/files/std7_stdlibs.gno b/gnovm/tests/files/std7.gno
similarity index 100%
rename from gnovm/tests/files/std7_stdlibs.gno
rename to gnovm/tests/files/std7.gno
diff --git a/gnovm/tests/files/std8_stdlibs.gno b/gnovm/tests/files/std8.gno
similarity index 89%
rename from gnovm/tests/files/std8_stdlibs.gno
rename to gnovm/tests/files/std8.gno
index ab5e15bd618..27545f267ce 100644
--- a/gnovm/tests/files/std8_stdlibs.gno
+++ b/gnovm/tests/files/std8.gno
@@ -28,11 +28,11 @@ func main() {
// std.GetCallerAt(4)
// std/native.gno:44
// fn()
-// main/files/std8_stdlibs.gno:16
+// main/files/std8.gno:16
// testutils.WrapCall(inner)
// gno.land/p/demo/testutils/misc.gno:5
// main()
-// main/files/std8_stdlibs.gno:21
+// main/files/std8.gno:21
// Error:
// frame not found
diff --git a/gnovm/tests/files/std9_stdlibs.gno b/gnovm/tests/files/std9.gno
similarity index 100%
rename from gnovm/tests/files/std9_stdlibs.gno
rename to gnovm/tests/files/std9.gno
diff --git a/gnovm/tests/files/stdbanker_stdlibs.gno b/gnovm/tests/files/stdbanker.gno
similarity index 100%
rename from gnovm/tests/files/stdbanker_stdlibs.gno
rename to gnovm/tests/files/stdbanker.gno
diff --git a/gnovm/tests/files/stdlibs_stdlibs.gno b/gnovm/tests/files/stdlibs.gno
similarity index 100%
rename from gnovm/tests/files/stdlibs_stdlibs.gno
rename to gnovm/tests/files/stdlibs.gno
diff --git a/gnovm/tests/files/struct13_stdlibs.gno b/gnovm/tests/files/struct13.gno
similarity index 100%
rename from gnovm/tests/files/struct13_stdlibs.gno
rename to gnovm/tests/files/struct13.gno
diff --git a/gnovm/tests/files/struct13_native.gno b/gnovm/tests/files/struct13_native.gno
deleted file mode 100644
index 85515555f50..00000000000
--- a/gnovm/tests/files/struct13_native.gno
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import (
- "fmt"
-
- "github.com/gnolang/gno/_test/net/http"
-)
-
-type Fromage struct {
- http.Server
-}
-
-func main() {
- a := Fromage{}
- fmt.Println(a.Server.WriteTimeout)
-}
-
-// Output:
-// 0s
diff --git a/gnovm/tests/files/switch21.gno b/gnovm/tests/files/switch21.gno
index b13867d4512..5dd70e2a188 100644
--- a/gnovm/tests/files/switch21.gno
+++ b/gnovm/tests/files/switch21.gno
@@ -6,7 +6,7 @@ func main() {
var err error
switch v := err.(type) {
- case fmt.Formatter:
+ case interface{ Format() string }:
println("formatter")
default:
fmt.Println(v)
diff --git a/gnovm/tests/files/time0_stdlibs.gno b/gnovm/tests/files/time0.gno
similarity index 100%
rename from gnovm/tests/files/time0_stdlibs.gno
rename to gnovm/tests/files/time0.gno
diff --git a/gnovm/tests/files/time0_native.gno b/gnovm/tests/files/time0_native.gno
deleted file mode 100644
index 52c5b4d6727..00000000000
--- a/gnovm/tests/files/time0_native.gno
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- fmt.Println(time.Now())
-}
-
-// Output:
-// 1970-01-01 00:00:00 +0000 UTC
diff --git a/gnovm/tests/files/time1_stdlibs.gno b/gnovm/tests/files/time1.gno
similarity index 100%
rename from gnovm/tests/files/time1_stdlibs.gno
rename to gnovm/tests/files/time1.gno
diff --git a/gnovm/tests/files/time11_stdlibs.gno b/gnovm/tests/files/time11.gno
similarity index 100%
rename from gnovm/tests/files/time11_stdlibs.gno
rename to gnovm/tests/files/time11.gno
diff --git a/gnovm/tests/files/time11_native.gno b/gnovm/tests/files/time11_native.gno
deleted file mode 100644
index 641ab4e6e4d..00000000000
--- a/gnovm/tests/files/time11_native.gno
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-const df = time.Minute * 30
-
-func main() {
- fmt.Printf("df: %v %T\n", df, df)
-}
-
-// Output:
-// df: 30m0s time.Duration
diff --git a/gnovm/tests/files/time12_stdlibs.gno b/gnovm/tests/files/time12.gno
similarity index 100%
rename from gnovm/tests/files/time12_stdlibs.gno
rename to gnovm/tests/files/time12.gno
diff --git a/gnovm/tests/files/time12_native.gno b/gnovm/tests/files/time12_native.gno
deleted file mode 100644
index 890e49cd1f0..00000000000
--- a/gnovm/tests/files/time12_native.gno
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-var twentyFourHours = time.Duration(24 * time.Hour)
-
-func main() {
- fmt.Println(twentyFourHours.Hours())
-}
-
-// Output:
-// 24
diff --git a/gnovm/tests/files/time13_stdlibs.gno b/gnovm/tests/files/time13.gno
similarity index 100%
rename from gnovm/tests/files/time13_stdlibs.gno
rename to gnovm/tests/files/time13.gno
diff --git a/gnovm/tests/files/time13_native.gno b/gnovm/tests/files/time13_native.gno
deleted file mode 100644
index a2eedafe880..00000000000
--- a/gnovm/tests/files/time13_native.gno
+++ /dev/null
@@ -1,18 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-var dummy = 1
-
-var t time.Time = time.Date(2007, time.November, 10, 23, 4, 5, 0, time.UTC)
-
-func main() {
- t = time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC)
- fmt.Println(t.Clock())
-}
-
-// Output:
-// 23 4 5
diff --git a/gnovm/tests/files/time14_stdlibs.gno b/gnovm/tests/files/time14.gno
similarity index 100%
rename from gnovm/tests/files/time14_stdlibs.gno
rename to gnovm/tests/files/time14.gno
diff --git a/gnovm/tests/files/time14_native.gno b/gnovm/tests/files/time14_native.gno
deleted file mode 100644
index 9f28c57d006..00000000000
--- a/gnovm/tests/files/time14_native.gno
+++ /dev/null
@@ -1,20 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-var t time.Time
-
-func f() time.Time {
- time := t
- return time
-}
-
-func main() {
- fmt.Println(f())
-}
-
-// Output:
-// 0001-01-01 00:00:00 +0000 UTC
diff --git a/gnovm/tests/files/time16_native.gno b/gnovm/tests/files/time16_native.gno
deleted file mode 100644
index 4010667b41c..00000000000
--- a/gnovm/tests/files/time16_native.gno
+++ /dev/null
@@ -1,14 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- var a int64 = 2
- fmt.Println(time.Second * a)
-}
-
-// Error:
-// main/files/time16_native.gno:10:14: incompatible operands in binary expression: go:time.Duration MUL int64
diff --git a/gnovm/tests/files/time17_native.gno b/gnovm/tests/files/time17_native.gno
deleted file mode 100644
index 6733c1381cb..00000000000
--- a/gnovm/tests/files/time17_native.gno
+++ /dev/null
@@ -1,20 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- now := time.Now()
- now.In(nil)
-}
-
-// Error:
-// time: missing Location in call to Time.In
-
-// Stacktrace:
-// now.In(gonative{*time.Location})
-// gofunction:func(*time.Location) time.Time
-// main()
-// main/files/time17_native.gno:10
diff --git a/gnovm/tests/files/time1_native.gno b/gnovm/tests/files/time1_native.gno
deleted file mode 100644
index 9749d472e08..00000000000
--- a/gnovm/tests/files/time1_native.gno
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- t := time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC)
- m := t.Minute()
- fmt.Println(t, m)
-}
-
-// Output:
-// 2009-11-10 23:04:05 +0000 UTC 4
diff --git a/gnovm/tests/files/time2_stdlibs.gno b/gnovm/tests/files/time2.gno
similarity index 100%
rename from gnovm/tests/files/time2_stdlibs.gno
rename to gnovm/tests/files/time2.gno
diff --git a/gnovm/tests/files/time2_native.gno b/gnovm/tests/files/time2_native.gno
deleted file mode 100644
index 03ea3a2be96..00000000000
--- a/gnovm/tests/files/time2_native.gno
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- t := time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC)
- h, m, s := t.Clock()
- fmt.Println(h, m, s)
-}
-
-// Output:
-// 23 4 5
diff --git a/gnovm/tests/files/time3_stdlibs.gno b/gnovm/tests/files/time3.gno
similarity index 100%
rename from gnovm/tests/files/time3_stdlibs.gno
rename to gnovm/tests/files/time3.gno
diff --git a/gnovm/tests/files/time3_native.gno b/gnovm/tests/files/time3_native.gno
deleted file mode 100644
index 0848abd9a13..00000000000
--- a/gnovm/tests/files/time3_native.gno
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-// FIXME related to named returns
-func main() {
- t := time.Date(2009, time.November, 10, 23, 4, 5, 0, time.UTC)
- fmt.Println(t.Clock())
-}
-
-// Output:
-// 23 4 5
diff --git a/gnovm/tests/files/time4_stdlibs.gno b/gnovm/tests/files/time4.gno
similarity index 100%
rename from gnovm/tests/files/time4_stdlibs.gno
rename to gnovm/tests/files/time4.gno
diff --git a/gnovm/tests/files/time4_native.gno b/gnovm/tests/files/time4_native.gno
deleted file mode 100644
index 3662e35cb01..00000000000
--- a/gnovm/tests/files/time4_native.gno
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- var m time.Month
- m = 9
- fmt.Println(m)
-}
-
-// Output:
-// September
diff --git a/gnovm/tests/files/time6_stdlibs.gno b/gnovm/tests/files/time6.gno
similarity index 100%
rename from gnovm/tests/files/time6_stdlibs.gno
rename to gnovm/tests/files/time6.gno
diff --git a/gnovm/tests/files/time6_native.gno b/gnovm/tests/files/time6_native.gno
deleted file mode 100644
index c88d3ab8115..00000000000
--- a/gnovm/tests/files/time6_native.gno
+++ /dev/null
@@ -1,16 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- t := &time.Time{}
- t.UnmarshalText([]byte("1985-04-12T23:20:50.52Z"))
-
- fmt.Println(t)
-}
-
-// Output:
-// 1985-04-12 23:20:50.52 +0000 UTC
diff --git a/gnovm/tests/files/time7_stdlibs.gno b/gnovm/tests/files/time7.gno
similarity index 100%
rename from gnovm/tests/files/time7_stdlibs.gno
rename to gnovm/tests/files/time7.gno
diff --git a/gnovm/tests/files/time7_native.gno b/gnovm/tests/files/time7_native.gno
deleted file mode 100644
index 1e02defc80d..00000000000
--- a/gnovm/tests/files/time7_native.gno
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-var d = 2 * time.Second
-
-func main() { fmt.Println(d) }
-
-// Output:
-// 2s
diff --git a/gnovm/tests/files/time9_stdlibs.gno b/gnovm/tests/files/time9.gno
similarity index 100%
rename from gnovm/tests/files/time9_stdlibs.gno
rename to gnovm/tests/files/time9.gno
diff --git a/gnovm/tests/files/time9_native.gno b/gnovm/tests/files/time9_native.gno
deleted file mode 100644
index a87b4560d1a..00000000000
--- a/gnovm/tests/files/time9_native.gno
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- fmt.Println((5 * time.Minute).Seconds())
-}
-
-// Output:
-// 300
diff --git a/gnovm/tests/files/type11.gno b/gnovm/tests/files/type11.gno
index a95be962a59..28aaee838dc 100644
--- a/gnovm/tests/files/type11.gno
+++ b/gnovm/tests/files/type11.gno
@@ -1,16 +1,17 @@
package main
import (
- "compress/gzip"
"fmt"
- "sync"
+ "time"
)
-var gzipWriterPools [gzip.BestCompression - gzip.BestSpeed + 2]*sync.Pool
+const i1 = int(time.Nanosecond)
+
+var weirdArray [i1 + len("123456789")]time.Duration
func main() {
- fmt.Printf("%T\n", gzipWriterPools)
+ fmt.Printf("%T\n", weirdArray)
}
// Output:
-// [10]*sync.Pool
+// [10]int64
diff --git a/gnovm/tests/files/type2_stdlibs.gno b/gnovm/tests/files/type2.gno
similarity index 100%
rename from gnovm/tests/files/type2_stdlibs.gno
rename to gnovm/tests/files/type2.gno
diff --git a/gnovm/tests/files/type2_native.gno b/gnovm/tests/files/type2_native.gno
deleted file mode 100644
index 453fd4b64e7..00000000000
--- a/gnovm/tests/files/type2_native.gno
+++ /dev/null
@@ -1,24 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-)
-
-type Options struct {
- debug bool
-}
-
-type T1 struct {
- opt Options
- time time.Time
-}
-
-func main() {
- t := T1{}
- t.time = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
- fmt.Println(t.time)
-}
-
-// Output:
-// 2009-11-10 23:00:00 +0000 UTC
diff --git a/gnovm/tests/files/typeassert7_native.gno b/gnovm/tests/files/typeassert7.gno
similarity index 100%
rename from gnovm/tests/files/typeassert7_native.gno
rename to gnovm/tests/files/typeassert7.gno
diff --git a/gnovm/tests/files/typeassert7a_native.gno b/gnovm/tests/files/typeassert7a.gno
similarity index 87%
rename from gnovm/tests/files/typeassert7a_native.gno
rename to gnovm/tests/files/typeassert7a.gno
index cafb27b6a6b..6e0aa0e8dca 100644
--- a/gnovm/tests/files/typeassert7a_native.gno
+++ b/gnovm/tests/files/typeassert7a.gno
@@ -39,5 +39,5 @@ func main() {
// Output:
// ok
-// interface conversion: interface is nil, not gonative{io.Reader}
+// interface conversion: interface is nil, not io.Reader
// ok
diff --git a/gnovm/tests/files/types/add_assign_f0_stdlibs.gno b/gnovm/tests/files/types/add_assign_f0.gno
similarity index 71%
rename from gnovm/tests/files/types/add_assign_f0_stdlibs.gno
rename to gnovm/tests/files/types/add_assign_f0.gno
index 67c6777d085..a3df217aa7d 100644
--- a/gnovm/tests/files/types/add_assign_f0_stdlibs.gno
+++ b/gnovm/tests/files/types/add_assign_f0.gno
@@ -22,4 +22,4 @@ func main() {
}
// Error:
-// main/files/types/add_assign_f0_stdlibs.gno:20:2: invalid operation: mismatched types int and .uverse.error
+// main/files/types/add_assign_f0.gno:20:2: invalid operation: mismatched types int and .uverse.error
diff --git a/gnovm/tests/files/types/add_assign_f1_stdlibs.gno b/gnovm/tests/files/types/add_assign_f1.gno
similarity index 81%
rename from gnovm/tests/files/types/add_assign_f1_stdlibs.gno
rename to gnovm/tests/files/types/add_assign_f1.gno
index d83a66359c9..195d8ab1c1c 100644
--- a/gnovm/tests/files/types/add_assign_f1_stdlibs.gno
+++ b/gnovm/tests/files/types/add_assign_f1.gno
@@ -25,4 +25,4 @@ func main() {
}
// Error:
-// main/files/types/add_assign_f1_stdlibs.gno:21:2: invalid operation: mismatched types main.Error and .uverse.error
+// main/files/types/add_assign_f1.gno:21:2: invalid operation: mismatched types main.Error and .uverse.error
diff --git a/gnovm/tests/files/types/add_assign_f2_stdlibs.gno b/gnovm/tests/files/types/add_assign_f2.gno
similarity index 75%
rename from gnovm/tests/files/types/add_assign_f2_stdlibs.gno
rename to gnovm/tests/files/types/add_assign_f2.gno
index 8be6b3cfb7b..2603ee273d6 100644
--- a/gnovm/tests/files/types/add_assign_f2_stdlibs.gno
+++ b/gnovm/tests/files/types/add_assign_f2.gno
@@ -22,4 +22,4 @@ func main() {
}
// Error:
-// main/files/types/add_assign_f2_stdlibs.gno:20:2: operator += not defined on: InterfaceKind
+// main/files/types/add_assign_f2.gno:20:2: operator += not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/add_f0_stdlibs.gno b/gnovm/tests/files/types/add_f0.gno
similarity index 74%
rename from gnovm/tests/files/types/add_f0_stdlibs.gno
rename to gnovm/tests/files/types/add_f0.gno
index 33e0346d44f..4497efd41f1 100644
--- a/gnovm/tests/files/types/add_f0_stdlibs.gno
+++ b/gnovm/tests/files/types/add_f0.gno
@@ -20,4 +20,4 @@ func main() {
}
// Error:
-// main/files/types/add_f0_stdlibs.gno:19:10: operator + not defined on: InterfaceKind
+// main/files/types/add_f0.gno:19:10: operator + not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/add_f1_stdlibs.gno b/gnovm/tests/files/types/add_f1.gno
similarity index 75%
rename from gnovm/tests/files/types/add_f1_stdlibs.gno
rename to gnovm/tests/files/types/add_f1.gno
index e46d67e93d7..0c403aff6a4 100644
--- a/gnovm/tests/files/types/add_f1_stdlibs.gno
+++ b/gnovm/tests/files/types/add_f1.gno
@@ -20,4 +20,4 @@ func main() {
}
// Error:
-// main/files/types/add_f1_stdlibs.gno:19:10: operator + not defined on: InterfaceKind
+// main/files/types/add_f1.gno:19:10: operator + not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/and_f0_stdlibs.gno b/gnovm/tests/files/types/and_f0.gno
similarity index 74%
rename from gnovm/tests/files/types/and_f0_stdlibs.gno
rename to gnovm/tests/files/types/and_f0.gno
index e80f69332a8..2c82610b932 100644
--- a/gnovm/tests/files/types/and_f0_stdlibs.gno
+++ b/gnovm/tests/files/types/and_f0.gno
@@ -20,4 +20,4 @@ func main() {
}
// Error:
-// main/files/types/and_f0_stdlibs.gno:19:10: operator & not defined on: InterfaceKind
+// main/files/types/and_f0.gno:19:10: operator & not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/and_f1_stdlibs.gno b/gnovm/tests/files/types/and_f1.gno
similarity index 75%
rename from gnovm/tests/files/types/and_f1_stdlibs.gno
rename to gnovm/tests/files/types/and_f1.gno
index 42a6aa4b466..41a72899ee2 100644
--- a/gnovm/tests/files/types/and_f1_stdlibs.gno
+++ b/gnovm/tests/files/types/and_f1.gno
@@ -20,4 +20,4 @@ func main() {
}
// Error:
-// main/files/types/and_f1_stdlibs.gno:19:10: operator & not defined on: InterfaceKind
+// main/files/types/and_f1.gno:19:10: operator & not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/cmp_iface_0_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_0.gno
similarity index 100%
rename from gnovm/tests/files/types/cmp_iface_0_stdlibs.gno
rename to gnovm/tests/files/types/cmp_iface_0.gno
diff --git a/gnovm/tests/files/types/cmp_iface_3_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_3.gno
similarity index 100%
rename from gnovm/tests/files/types/cmp_iface_3_stdlibs.gno
rename to gnovm/tests/files/types/cmp_iface_3.gno
diff --git a/gnovm/tests/files/types/eql_0f8_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_5.gno
similarity index 75%
rename from gnovm/tests/files/types/eql_0f8_stdlibs.gno
rename to gnovm/tests/files/types/cmp_iface_5.gno
index a6e24110432..7d748bacef3 100644
--- a/gnovm/tests/files/types/eql_0f8_stdlibs.gno
+++ b/gnovm/tests/files/types/cmp_iface_5.gno
@@ -24,4 +24,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0f8_stdlibs.gno:19:5: int64 does not implement .uverse.error (missing method Error)
+// main/files/types/cmp_iface_5.gno:19:5: int64 does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/eql_0b4_native.gno b/gnovm/tests/files/types/eql_0b4.gno
similarity index 50%
rename from gnovm/tests/files/types/eql_0b4_native.gno
rename to gnovm/tests/files/types/eql_0b4.gno
index 7c7baf01924..14a719c41d0 100644
--- a/gnovm/tests/files/types/eql_0b4_native.gno
+++ b/gnovm/tests/files/types/eql_0b4.gno
@@ -10,4 +10,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0b4_native.gno:9:10: unexpected type pair: cannot use bigint as gonative{error}
+// main/files/types/eql_0b4.gno:9:10: bigint does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/eql_0b4_stdlibs.gno b/gnovm/tests/files/types/eql_0b4_stdlibs.gno
deleted file mode 100644
index eac923c6d31..00000000000
--- a/gnovm/tests/files/types/eql_0b4_stdlibs.gno
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
- "errors"
-)
-
-func main() {
- errCmp := errors.New("xxx")
- println(5 == errCmp)
-}
-
-// Error:
-// main/files/types/eql_0b4_stdlibs.gno:9:10: bigint does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/eql_0f0_native.gno b/gnovm/tests/files/types/eql_0f0.gno
similarity index 75%
rename from gnovm/tests/files/types/eql_0f0_native.gno
rename to gnovm/tests/files/types/eql_0f0.gno
index e32325f5cf6..f609c0b5ced 100644
--- a/gnovm/tests/files/types/eql_0f0_native.gno
+++ b/gnovm/tests/files/types/eql_0f0.gno
@@ -25,4 +25,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0f0_native.gno:19:5: unexpected type pair: cannot use bigint as gonative{error}
+// main/files/types/eql_0f0.gno:19:5: bigint does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/eql_0f0_stdlibs.gno b/gnovm/tests/files/types/eql_0f0_stdlibs.gno
deleted file mode 100644
index 4947627cba4..00000000000
--- a/gnovm/tests/files/types/eql_0f0_stdlibs.gno
+++ /dev/null
@@ -1,28 +0,0 @@
-package main
-
-import (
- "errors"
- "strconv"
-)
-
-type Error int64
-
-func (e Error) Error() string {
- return "error: " + strconv.Itoa(int(e))
-}
-
-var errCmp = errors.New("XXXX")
-
-// special case:
-// one is interface
-func main() {
- if 1 == errCmp {
- //if errCmp == 1 {
- println("what the firetruck?")
- } else {
- println("something else")
- }
-}
-
-// Error:
-// main/files/types/eql_0f0_stdlibs.gno:19:5: bigint does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/eql_0f1_stdlibs.gno b/gnovm/tests/files/types/eql_0f1.gno
similarity index 76%
rename from gnovm/tests/files/types/eql_0f1_stdlibs.gno
rename to gnovm/tests/files/types/eql_0f1.gno
index cab7fcfab33..fd40dfcd29b 100644
--- a/gnovm/tests/files/types/eql_0f1_stdlibs.gno
+++ b/gnovm/tests/files/types/eql_0f1.gno
@@ -25,4 +25,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0f1_stdlibs.gno:19:5: int64 does not implement .uverse.error (missing method Error)
+// main/files/types/eql_0f1.gno:19:5: int64 does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/eql_0f27_stdlibs.gno b/gnovm/tests/files/types/eql_0f27.gno
similarity index 75%
rename from gnovm/tests/files/types/eql_0f27_stdlibs.gno
rename to gnovm/tests/files/types/eql_0f27.gno
index 188153aeb51..e90bbab9ca5 100644
--- a/gnovm/tests/files/types/eql_0f27_stdlibs.gno
+++ b/gnovm/tests/files/types/eql_0f27.gno
@@ -18,4 +18,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0f27_stdlibs.gno:13:5: operator > not defined on: InterfaceKind
+// main/files/types/eql_0f27.gno:13:5: operator > not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/eql_0f2b_native.gno b/gnovm/tests/files/types/eql_0f2b.gno
similarity index 80%
rename from gnovm/tests/files/types/eql_0f2b_native.gno
rename to gnovm/tests/files/types/eql_0f2b.gno
index 9de6155c5be..14a94dfde9a 100644
--- a/gnovm/tests/files/types/eql_0f2b_native.gno
+++ b/gnovm/tests/files/types/eql_0f2b.gno
@@ -25,4 +25,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0f2b_native.gno:19:5: operator <= not defined on: InterfaceKind
+// main/files/types/eql_0f2b.gno:19:5: operator <= not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/eql_0f2b_stdlibs.gno b/gnovm/tests/files/types/eql_0f2b_stdlibs.gno
deleted file mode 100644
index ac3616d163d..00000000000
--- a/gnovm/tests/files/types/eql_0f2b_stdlibs.gno
+++ /dev/null
@@ -1,28 +0,0 @@
-package main
-
-import (
- "errors"
- "strconv"
-)
-
-type Error int64
-
-func (e Error) Error() string {
- return "error: " + strconv.Itoa(int(e))
-}
-
-var errCmp = errors.New("XXXX")
-
-// special case:
-// one is interface
-func main() {
- if Error(0) <= errCmp {
- //if errCmp == 1 {
- println("what the firetruck?")
- } else {
- println("something else")
- }
-}
-
-// Error:
-// main/files/types/eql_0f2b_stdlibs.gno:19:5: operator <= not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/eql_0f2c_native.gno b/gnovm/tests/files/types/eql_0f2c.gno
similarity index 80%
rename from gnovm/tests/files/types/eql_0f2c_native.gno
rename to gnovm/tests/files/types/eql_0f2c.gno
index edd5ac3f23a..3374357a145 100644
--- a/gnovm/tests/files/types/eql_0f2c_native.gno
+++ b/gnovm/tests/files/types/eql_0f2c.gno
@@ -25,4 +25,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0f2c_native.gno:19:5: operator < not defined on: InterfaceKind
+// main/files/types/eql_0f2c.gno:19:5: operator < not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/eql_0f2c_stdlibs.gno b/gnovm/tests/files/types/eql_0f2c_stdlibs.gno
deleted file mode 100644
index 3a6ac3395b6..00000000000
--- a/gnovm/tests/files/types/eql_0f2c_stdlibs.gno
+++ /dev/null
@@ -1,28 +0,0 @@
-package main
-
-import (
- "errors"
- "strconv"
-)
-
-type Error int64
-
-func (e Error) Error() string {
- return "error: " + strconv.Itoa(int(e))
-}
-
-var errCmp = errors.New("XXXX")
-
-// special case:
-// one is interface
-func main() {
- if Error(0) < errCmp {
- //if errCmp == 1 {
- println("what the firetruck?")
- } else {
- println("something else")
- }
-}
-
-// Error:
-// main/files/types/eql_0f2c_stdlibs.gno:19:5: operator < not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/eql_0f40_stdlibs.gno b/gnovm/tests/files/types/eql_0f40.gno
similarity index 100%
rename from gnovm/tests/files/types/eql_0f40_stdlibs.gno
rename to gnovm/tests/files/types/eql_0f40.gno
diff --git a/gnovm/tests/files/types/eql_0f41_stdlibs.gno b/gnovm/tests/files/types/eql_0f41.gno
similarity index 77%
rename from gnovm/tests/files/types/eql_0f41_stdlibs.gno
rename to gnovm/tests/files/types/eql_0f41.gno
index be78ea6ed79..162586e2977 100644
--- a/gnovm/tests/files/types/eql_0f41_stdlibs.gno
+++ b/gnovm/tests/files/types/eql_0f41.gno
@@ -32,4 +32,4 @@ func main() {
}
// Error:
-// main/files/types/eql_0f41_stdlibs.gno:27:5: main.animal does not implement .uverse.error (missing method Error)
+// main/files/types/eql_0f41.gno:27:5: main.animal does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/cmp_iface_5_stdlibs.gno b/gnovm/tests/files/types/eql_0f8.gno
similarity index 75%
rename from gnovm/tests/files/types/cmp_iface_5_stdlibs.gno
rename to gnovm/tests/files/types/eql_0f8.gno
index e706c74808e..17bb5f9002d 100644
--- a/gnovm/tests/files/types/cmp_iface_5_stdlibs.gno
+++ b/gnovm/tests/files/types/eql_0f8.gno
@@ -24,4 +24,4 @@ func main() {
}
// Error:
-// main/files/types/cmp_iface_5_stdlibs.gno:19:5: int64 does not implement .uverse.error (missing method Error)
+// main/files/types/eql_0f8.gno:19:5: int64 does not implement .uverse.error (missing method Error)
diff --git a/gnovm/tests/files/types/explicit_conversion_0.gno b/gnovm/tests/files/types/explicit_conversion_0.gno
index ac5e8c2eb94..be7800590e5 100644
--- a/gnovm/tests/files/types/explicit_conversion_0.gno
+++ b/gnovm/tests/files/types/explicit_conversion_0.gno
@@ -5,7 +5,7 @@ import "fmt"
func main() {
r := int(uint(1))
println(r)
- fmt.Printf("%T \n", r)
+ fmt.Printf("%T\n", r)
}
// Output:
diff --git a/gnovm/tests/files/types/explicit_conversion_1.gno b/gnovm/tests/files/types/explicit_conversion_1.gno
index 60fc7b95b64..472a430fdc5 100644
--- a/gnovm/tests/files/types/explicit_conversion_1.gno
+++ b/gnovm/tests/files/types/explicit_conversion_1.gno
@@ -6,7 +6,7 @@ import "fmt"
func main() {
r := int(uint(string("hello")))
println(r)
- fmt.Printf("%T \n", r)
+ fmt.Printf("%T\n", r)
}
// Error:
diff --git a/gnovm/tests/files/types/explicit_conversion_2.gno b/gnovm/tests/files/types/explicit_conversion_2.gno
index f932a970a9a..30da1d31154 100644
--- a/gnovm/tests/files/types/explicit_conversion_2.gno
+++ b/gnovm/tests/files/types/explicit_conversion_2.gno
@@ -6,7 +6,7 @@ func main() {
x := 1
r := uint(+x)
println(r)
- fmt.Printf("%T \n", r)
+ fmt.Printf("%T\n", r)
}
// Output:
diff --git a/gnovm/tests/files/types/or_f0_stdlibs.gno b/gnovm/tests/files/types/or_f0.gno
similarity index 75%
rename from gnovm/tests/files/types/or_f0_stdlibs.gno
rename to gnovm/tests/files/types/or_f0.gno
index 8e6fb54772a..34ffdaa87fe 100644
--- a/gnovm/tests/files/types/or_f0_stdlibs.gno
+++ b/gnovm/tests/files/types/or_f0.gno
@@ -20,4 +20,4 @@ func main() {
}
// Error:
-// main/files/types/or_f0_stdlibs.gno:19:10: operator | not defined on: InterfaceKind
+// main/files/types/or_f0.gno:19:10: operator | not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/or_f1_stdlibs.gno b/gnovm/tests/files/types/or_f1.gno
similarity index 75%
rename from gnovm/tests/files/types/or_f1_stdlibs.gno
rename to gnovm/tests/files/types/or_f1.gno
index 5013126c9fa..96a68632320 100644
--- a/gnovm/tests/files/types/or_f1_stdlibs.gno
+++ b/gnovm/tests/files/types/or_f1.gno
@@ -20,4 +20,4 @@ func main() {
}
// Error:
-// main/files/types/or_f1_stdlibs.gno:19:10: operator | not defined on: InterfaceKind
+// main/files/types/or_f1.gno:19:10: operator | not defined on: InterfaceKind
diff --git a/gnovm/tests/files/types/shift_b0.gno b/gnovm/tests/files/types/shift_b0.gno
index fa9ee4ed2a0..9717f04a56d 100644
--- a/gnovm/tests/files/types/shift_b0.gno
+++ b/gnovm/tests/files/types/shift_b0.gno
@@ -6,7 +6,7 @@ func main() {
x := 2
r := uint64(1 << x)
println(r)
- fmt.Printf("%T \n", r)
+ fmt.Printf("%T\n", r)
}
// Output:
diff --git a/gnovm/tests/files/types/shift_b1.gno b/gnovm/tests/files/types/shift_b1.gno
index 403887269c0..8f2615ba93d 100644
--- a/gnovm/tests/files/types/shift_b1.gno
+++ b/gnovm/tests/files/types/shift_b1.gno
@@ -6,7 +6,7 @@ func main() {
x := 2
r := uint64(1<.Panic()
-// gno.land/r/test/main.gno:7
+// gno.land/r/test/files/zrealm_panic.gno:7
// main()
-// gno.land/r/test/main.gno:12
+// gno.land/r/test/files/zrealm_panic.gno:12
diff --git a/gnovm/tests/files/zrealm_std0_stdlibs.gno b/gnovm/tests/files/zrealm_std0.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_std0_stdlibs.gno
rename to gnovm/tests/files/zrealm_std0.gno
diff --git a/gnovm/tests/files/zrealm_std1_stdlibs.gno b/gnovm/tests/files/zrealm_std1.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_std1_stdlibs.gno
rename to gnovm/tests/files/zrealm_std1.gno
diff --git a/gnovm/tests/files/zrealm_std2_stdlibs.gno b/gnovm/tests/files/zrealm_std2.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_std2_stdlibs.gno
rename to gnovm/tests/files/zrealm_std2.gno
diff --git a/gnovm/tests/files/zrealm_std3_stdlibs.gno b/gnovm/tests/files/zrealm_std3.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_std3_stdlibs.gno
rename to gnovm/tests/files/zrealm_std3.gno
diff --git a/gnovm/tests/files/zrealm_std4_stdlibs.gno b/gnovm/tests/files/zrealm_std4.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_std4_stdlibs.gno
rename to gnovm/tests/files/zrealm_std4.gno
diff --git a/gnovm/tests/files/zrealm_std5_stdlibs.gno b/gnovm/tests/files/zrealm_std5.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_std5_stdlibs.gno
rename to gnovm/tests/files/zrealm_std5.gno
diff --git a/gnovm/tests/files/zrealm_std6_stdlibs.gno b/gnovm/tests/files/zrealm_std6.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_std6_stdlibs.gno
rename to gnovm/tests/files/zrealm_std6.gno
diff --git a/gnovm/tests/files/zrealm_tests0_stdlibs.gno b/gnovm/tests/files/zrealm_tests0.gno
similarity index 99%
rename from gnovm/tests/files/zrealm_tests0_stdlibs.gno
rename to gnovm/tests/files/zrealm_tests0.gno
index d11701505e5..82e4d418217 100644
--- a/gnovm/tests/files/zrealm_tests0_stdlibs.gno
+++ b/gnovm/tests/files/zrealm_tests0.gno
@@ -14,12 +14,15 @@ func init() {
func main() {
tests_foo.AddFooStringer("three")
println(tests.Render(""))
+ println("end")
}
// Output:
// 0: &FooStringer{one}
// 1: &FooStringer{two}
// 2: &FooStringer{three}
+//
+// end
// Realm:
// switchrealm["gno.land/r/demo/tests"]
diff --git a/gnovm/tests/files/zrealm_testutils0_stdlibs.gno b/gnovm/tests/files/zrealm_testutils0.gno
similarity index 100%
rename from gnovm/tests/files/zrealm_testutils0_stdlibs.gno
rename to gnovm/tests/files/zrealm_testutils0.gno
diff --git a/gnovm/tests/files/zregexp_stdlibs.gno b/gnovm/tests/files/zregexp.gno
similarity index 100%
rename from gnovm/tests/files/zregexp_stdlibs.gno
rename to gnovm/tests/files/zregexp.gno
diff --git a/gnovm/tests/imports.go b/gnovm/tests/imports.go
deleted file mode 100644
index 66398ba5f50..00000000000
--- a/gnovm/tests/imports.go
+++ /dev/null
@@ -1,492 +0,0 @@
-package tests
-
-import (
- "bufio"
- "bytes"
- "compress/flate"
- "compress/gzip"
- "context"
- "crypto/md5" //nolint:gosec
- crand "crypto/rand"
- "crypto/sha1" //nolint:gosec
- "encoding/base64"
- "encoding/binary"
- "encoding/json"
- "encoding/xml"
- "errors"
- "flag"
- "fmt"
- "hash/fnv"
- "image"
- "image/color"
- "io"
- "log"
- "math"
- "math/big"
- "math/rand/v2"
- "net"
- "net/url"
- "os"
- "path/filepath"
- "reflect"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- "text/template"
- "time"
- "unicode/utf8"
-
- gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
- teststdlibs "github.com/gnolang/gno/gnovm/tests/stdlibs"
- teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
- "github.com/gnolang/gno/tm2/pkg/db/memdb"
- osm "github.com/gnolang/gno/tm2/pkg/os"
- "github.com/gnolang/gno/tm2/pkg/std"
- "github.com/gnolang/gno/tm2/pkg/store/dbadapter"
- "github.com/gnolang/gno/tm2/pkg/store/iavl"
- stypes "github.com/gnolang/gno/tm2/pkg/store/types"
-)
-
-type importMode uint64
-
-// Import modes to control the import behaviour of TestStore.
-const (
- // use stdlibs/* only (except a few exceptions). for stdlibs/* and examples/* testing.
- ImportModeStdlibsOnly importMode = iota
- // use stdlibs/* if present, otherwise use native. used in files/tests, excluded for *_native.go
- ImportModeStdlibsPreferred
- // do not use stdlibs/* if native registered. used in files/tests, excluded for *_stdlibs.go
- ImportModeNativePreferred
-)
-
-// NOTE: this isn't safe, should only be used for testing.
-func TestStore(rootDir, filesPath string, stdin io.Reader, stdout, stderr io.Writer, mode importMode) (resStore gno.Store) {
- getPackage := func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) {
- if pkgPath == "" {
- panic(fmt.Sprintf("invalid zero package path in testStore().pkgGetter"))
- }
- if mode != ImportModeStdlibsOnly &&
- mode != ImportModeStdlibsPreferred &&
- mode != ImportModeNativePreferred {
- panic(fmt.Sprintf("unrecognized import mode"))
- }
-
- if filesPath != "" {
- // if _test package...
- const testPath = "github.com/gnolang/gno/_test/"
- if strings.HasPrefix(pkgPath, testPath) {
- baseDir := filepath.Join(filesPath, "extern", pkgPath[len(testPath):])
- memPkg := gno.ReadMemPackage(baseDir, pkgPath)
- send := std.Coins{}
- ctx := TestContext(pkgPath, send)
- m2 := gno.NewMachineWithOptions(gno.MachineOptions{
- PkgPath: "test",
- Output: stdout,
- Store: store,
- Context: ctx,
- })
- // pkg := gno.NewPackageNode(gno.Name(memPkg.Name), memPkg.Path, nil)
- // pv := pkg.NewPackage()
- // m2.SetActivePackage(pv)
- // XXX remove second arg 'false' and remove all gonative stuff.
- return m2.RunMemPackage(memPkg, false)
- }
- }
-
- // if stdlibs package is preferred , try to load it first.
- if mode == ImportModeStdlibsOnly ||
- mode == ImportModeStdlibsPreferred {
- pn, pv = loadStdlib(rootDir, pkgPath, store, stdout)
- if pn != nil {
- return
- }
- }
-
- // if native package is allowed, return it.
- if pkgPath == "os" || // special cases even when StdlibsOnly (for tests).
- pkgPath == "fmt" || // TODO: try to minimize these exceptions over time.
- pkgPath == "log" ||
- pkgPath == "crypto/rand" ||
- pkgPath == "crypto/md5" ||
- pkgPath == "crypto/sha1" ||
- pkgPath == "encoding/binary" ||
- pkgPath == "encoding/json" ||
- pkgPath == "encoding/xml" ||
- pkgPath == "internal/os_test" ||
- pkgPath == "math/big" ||
- mode == ImportModeStdlibsPreferred ||
- mode == ImportModeNativePreferred {
- switch pkgPath {
- case "os":
- pkg := gno.NewPackageNode("os", pkgPath, nil)
- pkg.DefineGoNativeValue("Stdin", stdin)
- pkg.DefineGoNativeValue("Stdout", stdout)
- pkg.DefineGoNativeValue("Stderr", stderr)
- return pkg, pkg.NewPackage()
- case "fmt":
- pkg := gno.NewPackageNode("fmt", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf((*fmt.Stringer)(nil)).Elem())
- pkg.DefineGoNativeType(reflect.TypeOf((*fmt.Formatter)(nil)).Elem())
- pkg.DefineGoNativeValue("Println", func(a ...interface{}) (n int, err error) {
- // NOTE: uncomment to debug long running tests
- // fmt.Println(a...)
- res := fmt.Sprintln(a...)
- return stdout.Write([]byte(res))
- })
- pkg.DefineGoNativeValue("Printf", func(format string, a ...interface{}) (n int, err error) {
- res := fmt.Sprintf(format, a...)
- return stdout.Write([]byte(res))
- })
- pkg.DefineGoNativeValue("Print", func(a ...interface{}) (n int, err error) {
- res := fmt.Sprint(a...)
- return stdout.Write([]byte(res))
- })
- pkg.DefineGoNativeValue("Sprint", fmt.Sprint)
- pkg.DefineGoNativeValue("Sprintf", fmt.Sprintf)
- pkg.DefineGoNativeValue("Sprintln", fmt.Sprintln)
- pkg.DefineGoNativeValue("Sscanf", fmt.Sscanf)
- pkg.DefineGoNativeValue("Errorf", fmt.Errorf)
- pkg.DefineGoNativeValue("Fprintln", fmt.Fprintln)
- pkg.DefineGoNativeValue("Fprintf", fmt.Fprintf)
- pkg.DefineGoNativeValue("Fprint", fmt.Fprint)
- return pkg, pkg.NewPackage()
- case "encoding/base64":
- pkg := gno.NewPackageNode("base64", pkgPath, nil)
- pkg.DefineGoNativeValue("RawStdEncoding", base64.RawStdEncoding)
- pkg.DefineGoNativeValue("StdEncoding", base64.StdEncoding)
- pkg.DefineGoNativeValue("NewDecoder", base64.NewDecoder)
- return pkg, pkg.NewPackage()
- case "encoding/binary":
- pkg := gno.NewPackageNode("binary", pkgPath, nil)
- pkg.DefineGoNativeValue("LittleEndian", binary.LittleEndian)
- pkg.DefineGoNativeValue("BigEndian", binary.BigEndian)
- pkg.DefineGoNativeValue("Write", binary.BigEndian) // warn: use reflection
- return pkg, pkg.NewPackage()
- case "encoding/json":
- pkg := gno.NewPackageNode("json", pkgPath, nil)
- pkg.DefineGoNativeValue("Unmarshal", json.Unmarshal)
- pkg.DefineGoNativeValue("Marshal", json.Marshal)
- return pkg, pkg.NewPackage()
- case "encoding/xml":
- pkg := gno.NewPackageNode("xml", pkgPath, nil)
- pkg.DefineGoNativeValue("Unmarshal", xml.Unmarshal)
- return pkg, pkg.NewPackage()
- case "internal/os_test":
- pkg := gno.NewPackageNode("os_test", pkgPath, nil)
- pkg.DefineNative("Sleep",
- gno.Flds( // params
- "d", gno.AnyT(), // NOTE: should be time.Duration
- ),
- gno.Flds( // results
- ),
- func(m *gno.Machine) {
- // For testing purposes here, nanoseconds are separately kept track.
- arg0 := m.LastBlock().GetParams1().TV
- d := arg0.GetInt64()
- sec := d / int64(time.Second)
- nano := d % int64(time.Second)
- ctx := m.Context.(*teststd.TestExecContext)
- ctx.Timestamp += sec
- ctx.TimestampNano += nano
- if ctx.TimestampNano >= int64(time.Second) {
- ctx.Timestamp += 1
- ctx.TimestampNano -= int64(time.Second)
- }
- m.Context = ctx
- },
- )
- return pkg, pkg.NewPackage()
- case "net":
- pkg := gno.NewPackageNode("net", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(net.TCPAddr{}))
- pkg.DefineGoNativeValue("IPv4", net.IPv4)
- return pkg, pkg.NewPackage()
- case "net/url":
- pkg := gno.NewPackageNode("url", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(url.Values{}))
- return pkg, pkg.NewPackage()
- case "bufio":
- pkg := gno.NewPackageNode("bufio", pkgPath, nil)
- pkg.DefineGoNativeValue("NewScanner", bufio.NewScanner)
- pkg.DefineGoNativeType(reflect.TypeOf(bufio.SplitFunc(nil)))
- return pkg, pkg.NewPackage()
- case "bytes":
- pkg := gno.NewPackageNode("bytes", pkgPath, nil)
- pkg.DefineGoNativeValue("Equal", bytes.Equal)
- pkg.DefineGoNativeValue("Compare", bytes.Compare)
- pkg.DefineGoNativeValue("NewReader", bytes.NewReader)
- pkg.DefineGoNativeValue("NewBuffer", bytes.NewBuffer)
- pkg.DefineGoNativeValue("Repeat", bytes.Repeat)
- pkg.DefineGoNativeType(reflect.TypeOf(bytes.Buffer{}))
- return pkg, pkg.NewPackage()
- case "time":
- pkg := gno.NewPackageNode("time", pkgPath, nil)
- pkg.DefineGoNativeConstValue("Millisecond", time.Millisecond)
- pkg.DefineGoNativeConstValue("Second", time.Second)
- pkg.DefineGoNativeConstValue("Minute", time.Minute)
- pkg.DefineGoNativeConstValue("Hour", time.Hour)
- pkg.DefineGoNativeConstValue("Date", time.Date)
- pkg.DefineGoNativeConstValue("Now", func() time.Time { return time.Unix(0, 0).UTC() }) // deterministic
- pkg.DefineGoNativeConstValue("January", time.January)
- pkg.DefineGoNativeConstValue("February", time.February)
- pkg.DefineGoNativeConstValue("March", time.March)
- pkg.DefineGoNativeConstValue("April", time.April)
- pkg.DefineGoNativeConstValue("May", time.May)
- pkg.DefineGoNativeConstValue("June", time.June)
- pkg.DefineGoNativeConstValue("July", time.July)
- pkg.DefineGoNativeConstValue("August", time.August)
- pkg.DefineGoNativeConstValue("September", time.September)
- pkg.DefineGoNativeConstValue("November", time.November)
- pkg.DefineGoNativeConstValue("December", time.December)
- pkg.DefineGoNativeValue("UTC", time.UTC)
- pkg.DefineGoNativeValue("Unix", time.Unix)
- pkg.DefineGoNativeType(reflect.TypeOf(time.Time{}))
- pkg.DefineGoNativeType(reflect.TypeOf(time.Duration(0)))
- pkg.DefineGoNativeType(reflect.TypeOf(time.Month(0)))
- pkg.DefineGoNativeValue("LoadLocation", time.LoadLocation)
- return pkg, pkg.NewPackage()
- case "strconv":
- pkg := gno.NewPackageNode("strconv", pkgPath, nil)
- pkg.DefineGoNativeValue("Itoa", strconv.Itoa)
- pkg.DefineGoNativeValue("Atoi", strconv.Atoi)
- pkg.DefineGoNativeValue("ParseInt", strconv.ParseInt)
- pkg.DefineGoNativeValue("Quote", strconv.Quote)
- pkg.DefineGoNativeValue("FormatUint", strconv.FormatUint)
- pkg.DefineGoNativeType(reflect.TypeOf(strconv.NumError{}))
- return pkg, pkg.NewPackage()
- case "strings":
- pkg := gno.NewPackageNode("strings", pkgPath, nil)
- pkg.DefineGoNativeValue("Split", strings.Split)
- pkg.DefineGoNativeValue("SplitN", strings.SplitN)
- pkg.DefineGoNativeValue("Contains", strings.Contains)
- pkg.DefineGoNativeValue("TrimSpace", strings.TrimSpace)
- pkg.DefineGoNativeValue("HasPrefix", strings.HasPrefix)
- pkg.DefineGoNativeValue("NewReader", strings.NewReader)
- pkg.DefineGoNativeValue("Index", strings.Index)
- pkg.DefineGoNativeValue("IndexRune", strings.IndexRune)
- pkg.DefineGoNativeValue("Join", strings.Join)
- pkg.DefineGoNativeType(reflect.TypeOf(strings.Builder{}))
- return pkg, pkg.NewPackage()
- case "math":
- pkg := gno.NewPackageNode("math", pkgPath, nil)
- pkg.DefineGoNativeValue("Abs", math.Abs)
- pkg.DefineGoNativeValue("Cos", math.Cos)
- pkg.DefineGoNativeConstValue("Pi", math.Pi)
- pkg.DefineGoNativeValue("Float64bits", math.Float64bits)
- pkg.DefineGoNativeConstValue("MaxFloat32", math.MaxFloat32)
- pkg.DefineGoNativeConstValue("MaxFloat64", math.MaxFloat64)
- pkg.DefineGoNativeConstValue("MaxUint32", uint32(math.MaxUint32))
- pkg.DefineGoNativeConstValue("MaxUint64", uint64(math.MaxUint64))
- pkg.DefineGoNativeConstValue("MinInt8", math.MinInt8)
- pkg.DefineGoNativeConstValue("MinInt16", math.MinInt16)
- pkg.DefineGoNativeConstValue("MinInt32", math.MinInt32)
- pkg.DefineGoNativeConstValue("MinInt64", int64(math.MinInt64))
- pkg.DefineGoNativeConstValue("MaxInt8", math.MaxInt8)
- pkg.DefineGoNativeConstValue("MaxInt16", math.MaxInt16)
- pkg.DefineGoNativeConstValue("MaxInt32", math.MaxInt32)
- pkg.DefineGoNativeConstValue("MaxInt64", int64(math.MaxInt64))
- return pkg, pkg.NewPackage()
- case "math/rand":
- // XXX only expose for tests.
- pkg := gno.NewPackageNode("rand", pkgPath, nil)
- // make native rand same as gno rand.
- rnd := rand.New(rand.NewPCG(0, 0)) //nolint:gosec
- pkg.DefineGoNativeValue("IntN", rnd.IntN)
- pkg.DefineGoNativeValue("Uint32", rnd.Uint32)
- return pkg, pkg.NewPackage()
- case "crypto/rand":
- pkg := gno.NewPackageNode("rand", pkgPath, nil)
- pkg.DefineGoNativeValue("Prime", crand.Prime)
- // for determinism:
- // pkg.DefineGoNativeValue("Reader", crand.Reader)
- pkg.DefineGoNativeValue("Reader", &dummyReader{})
- return pkg, pkg.NewPackage()
- case "crypto/md5":
- pkg := gno.NewPackageNode("md5", pkgPath, nil)
- pkg.DefineGoNativeValue("New", md5.New)
- return pkg, pkg.NewPackage()
- case "crypto/sha1":
- pkg := gno.NewPackageNode("sha1", pkgPath, nil)
- pkg.DefineGoNativeValue("New", sha1.New)
- return pkg, pkg.NewPackage()
- case "image":
- pkg := gno.NewPackageNode("image", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(image.Point{}))
- return pkg, pkg.NewPackage()
- case "image/color":
- pkg := gno.NewPackageNode("color", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(color.NRGBA64{}))
- return pkg, pkg.NewPackage()
- case "compress/flate":
- pkg := gno.NewPackageNode("flate", pkgPath, nil)
- pkg.DefineGoNativeConstValue("BestSpeed", flate.BestSpeed)
- return pkg, pkg.NewPackage()
- case "compress/gzip":
- pkg := gno.NewPackageNode("gzip", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(gzip.Writer{}))
- pkg.DefineGoNativeConstValue("BestCompression", gzip.BestCompression)
- pkg.DefineGoNativeConstValue("BestSpeed", gzip.BestSpeed)
- return pkg, pkg.NewPackage()
- case "context":
- pkg := gno.NewPackageNode("context", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf((*context.Context)(nil)).Elem())
- pkg.DefineGoNativeValue("WithValue", context.WithValue)
- pkg.DefineGoNativeValue("Background", context.Background)
- return pkg, pkg.NewPackage()
- case "sync":
- pkg := gno.NewPackageNode("sync", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(sync.Mutex{}))
- pkg.DefineGoNativeType(reflect.TypeOf(sync.RWMutex{}))
- pkg.DefineGoNativeType(reflect.TypeOf(sync.Pool{}))
- return pkg, pkg.NewPackage()
- case "sync/atomic":
- pkg := gno.NewPackageNode("atomic", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(atomic.Value{}))
- return pkg, pkg.NewPackage()
- case "math/big":
- pkg := gno.NewPackageNode("big", pkgPath, nil)
- pkg.DefineGoNativeValue("NewInt", big.NewInt)
- return pkg, pkg.NewPackage()
- case "flag":
- pkg := gno.NewPackageNode("flag", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(flag.Flag{}))
- return pkg, pkg.NewPackage()
- case "io":
- pkg := gno.NewPackageNode("io", pkgPath, nil)
- pkg.DefineGoNativeValue("EOF", io.EOF)
- pkg.DefineGoNativeValue("NopCloser", io.NopCloser)
- pkg.DefineGoNativeValue("ReadFull", io.ReadFull)
- pkg.DefineGoNativeValue("ReadAll", io.ReadAll)
- pkg.DefineGoNativeType(reflect.TypeOf((*io.ReadCloser)(nil)).Elem())
- pkg.DefineGoNativeType(reflect.TypeOf((*io.Closer)(nil)).Elem())
- pkg.DefineGoNativeType(reflect.TypeOf((*io.Reader)(nil)).Elem())
- return pkg, pkg.NewPackage()
- case "log":
- pkg := gno.NewPackageNode("log", pkgPath, nil)
- pkg.DefineGoNativeValue("Fatal", log.Fatal)
- return pkg, pkg.NewPackage()
- case "text/template":
- pkg := gno.NewPackageNode("template", pkgPath, nil)
- pkg.DefineGoNativeType(reflect.TypeOf(template.FuncMap{}))
- return pkg, pkg.NewPackage()
- case "unicode/utf8":
- pkg := gno.NewPackageNode("utf8", pkgPath, nil)
- pkg.DefineGoNativeValue("DecodeRuneInString", utf8.DecodeRuneInString)
- tv := gno.TypedValue{T: gno.UntypedRuneType} // TODO dry
- tv.SetInt32(utf8.RuneSelf) // ..
- pkg.Define("RuneSelf", tv) // ..
- return pkg, pkg.NewPackage()
- case "errors":
- pkg := gno.NewPackageNode("errors", pkgPath, nil)
- pkg.DefineGoNativeValue("New", errors.New)
- return pkg, pkg.NewPackage()
- case "hash/fnv":
- pkg := gno.NewPackageNode("fnv", pkgPath, nil)
- pkg.DefineGoNativeValue("New32a", fnv.New32a)
- return pkg, pkg.NewPackage()
- default:
- // continue on...
- }
- }
-
- // if native package is preferred, try to load stdlibs/* as backup.
- if mode == ImportModeNativePreferred {
- pn, pv = loadStdlib(rootDir, pkgPath, store, stdout)
- if pn != nil {
- return
- }
- }
-
- // if examples package...
- examplePath := filepath.Join(rootDir, "examples", pkgPath)
- if osm.DirExists(examplePath) {
- memPkg := gno.ReadMemPackage(examplePath, pkgPath)
- if memPkg.IsEmpty() {
- panic(fmt.Sprintf("found an empty package %q", pkgPath))
- }
-
- send := std.Coins{}
- ctx := TestContext(pkgPath, send)
- m2 := gno.NewMachineWithOptions(gno.MachineOptions{
- PkgPath: "test",
- Output: stdout,
- Store: store,
- Context: ctx,
- })
- pn, pv = m2.RunMemPackage(memPkg, true)
- return
- }
- return nil, nil
- }
- db := memdb.NewMemDB()
- baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{})
- iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{})
- // make a new store
- resStore = gno.NewStore(nil, baseStore, iavlStore)
- resStore.SetPackageGetter(getPackage)
- resStore.SetNativeStore(teststdlibs.NativeStore)
- resStore.SetStrictGo2GnoMapping(false)
- return
-}
-
-func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) {
- dirs := [...]string{
- // normal stdlib path.
- filepath.Join(rootDir, "gnovm", "stdlibs", pkgPath),
- // override path. definitions here override the previous if duplicate.
- filepath.Join(rootDir, "gnovm", "tests", "stdlibs", pkgPath),
- }
- files := make([]string, 0, 32) // pre-alloc 32 as a likely high number of files
- for _, path := range dirs {
- dl, err := os.ReadDir(path)
- if err != nil {
- if os.IsNotExist(err) {
- continue
- }
- panic(fmt.Errorf("could not access dir %q: %w", path, err))
- }
-
- for _, f := range dl {
- // NOTE: RunMemPackage has other rules; those should be mostly useful
- // for on-chain packages (ie. include README and gno.mod).
- if !f.IsDir() && strings.HasSuffix(f.Name(), ".gno") {
- files = append(files, filepath.Join(path, f.Name()))
- }
- }
- }
- if len(files) == 0 {
- return nil, nil
- }
-
- memPkg := gno.ReadMemPackageFromList(files, pkgPath)
- m2 := gno.NewMachineWithOptions(gno.MachineOptions{
- // NOTE: see also pkgs/sdk/vm/builtins.go
- // Needs PkgPath != its name because TestStore.getPackage is the package
- // getter for the store, which calls loadStdlib, so it would be recursively called.
- PkgPath: "stdlibload",
- Output: stdout,
- Store: store,
- })
- save := pkgPath != "testing" // never save the "testing" package
- return m2.RunMemPackageWithOverrides(memPkg, save)
-}
-
-type dummyReader struct{}
-
-func (*dummyReader) Read(b []byte) (n int, err error) {
- for i := 0; i < len(b); i++ {
- b[i] = byte((100 + i) % 256)
- }
- return len(b), nil
-}
-
-// ----------------------------------------
-
-type TestReport struct {
- Name string
- Verbose bool
- Failed bool
- Skipped bool
- Output string
-}
diff --git a/gnovm/tests/machine_test.go b/gnovm/tests/machine_test.go
deleted file mode 100644
index a67d67f1ff2..00000000000
--- a/gnovm/tests/machine_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package tests
-
-import (
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
-)
-
-func TestMachineTestMemPackage(t *testing.T) {
- matchFunc := func(pat, str string) (bool, error) { return true, nil }
-
- tests := []struct {
- name string
- path string
- shouldSucceed bool
- }{
- {
- name: "TestSuccess",
- path: "testdata/TestMemPackage/success",
- shouldSucceed: true,
- },
- {
- name: "TestFail",
- path: "testdata/TestMemPackage/fail",
- shouldSucceed: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // NOTE: Because the purpose of this test is to ensure testing.T.Failed()
- // returns true if a gno test is failing, and because we don't want this
- // to affect the current testing.T, we are creating an other one thanks
- // to testing.RunTests() function.
- testing.RunTests(matchFunc, []testing.InternalTest{
- {
- Name: tt.name,
- F: func(t2 *testing.T) { //nolint:thelper
- rootDir := filepath.Join("..", "..")
- store := TestStore(rootDir, "test", os.Stdin, os.Stdout, os.Stderr, ImportModeStdlibsOnly)
- store.SetLogStoreOps(true)
- m := gno.NewMachineWithOptions(gno.MachineOptions{
- PkgPath: "test",
- Output: os.Stdout,
- Store: store,
- Context: nil,
- })
- memPkg := gno.ReadMemPackage(tt.path, "test")
-
- m.TestMemPackage(t2, memPkg)
-
- if tt.shouldSucceed {
- assert.False(t, t2.Failed(), "test %q should have succeed", tt.name)
- } else {
- assert.True(t, t2.Failed(), "test %q should have failed", tt.name)
- }
- },
- },
- })
- })
- }
-}
diff --git a/gnovm/tests/package_test.go b/gnovm/tests/package_test.go
deleted file mode 100644
index d4ddfc9a4f0..00000000000
--- a/gnovm/tests/package_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package tests
-
-import (
- "bytes"
- "fmt"
- "io/fs"
- "log"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
-)
-
-func TestStdlibs(t *testing.T) {
- t.Parallel()
-
- // NOTE: this test only works using _test.gno files;
- // filetests are not meant to be used for testing standard libraries.
- // The examples directory is tested directly using `gno test`u
-
- // find all packages with *_test.gno files.
- rootDirs := []string{
- filepath.Join("..", "stdlibs"),
- }
- testDirs := map[string]string{} // aggregate here, pkgPath -> dir
- pkgPaths := []string{}
- for _, rootDir := range rootDirs {
- fileSystem := os.DirFS(rootDir)
- fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- log.Fatal(err)
- }
- if d.IsDir() {
- return nil
- }
- if strings.HasSuffix(path, "_test.gno") {
- dirPath := filepath.Dir(path)
- if _, exists := testDirs[dirPath]; exists {
- // already exists.
- } else {
- testDirs[dirPath] = filepath.Join(rootDir, dirPath)
- pkgPaths = append(pkgPaths, dirPath)
- }
- }
- return nil
- })
- }
- // For each package with testfiles (in testDirs), call Machine.TestMemPackage.
- for _, pkgPath := range pkgPaths {
- testDir := testDirs[pkgPath]
- t.Run(pkgPath, func(t *testing.T) {
- pkgPath := pkgPath
- t.Parallel()
- runPackageTest(t, testDir, pkgPath)
- })
- }
-}
-
-func runPackageTest(t *testing.T, dir string, path string) {
- t.Helper()
-
- memPkg := gno.ReadMemPackage(dir, path)
- require.False(t, memPkg.IsEmpty())
-
- stdin := new(bytes.Buffer)
- // stdout := new(bytes.Buffer)
- stdout := os.Stdout
- stderr := new(bytes.Buffer)
- rootDir := filepath.Join("..", "..")
- store := TestStore(rootDir, path, stdin, stdout, stderr, ImportModeStdlibsOnly)
- store.SetLogStoreOps(true)
- m := gno.NewMachineWithOptions(gno.MachineOptions{
- PkgPath: "test",
- Output: stdout,
- Store: store,
- Context: nil,
- })
- m.TestMemPackage(t, memPkg)
-
- // Check that machine is empty.
- err := m.CheckEmpty()
- if err != nil {
- t.Log("last state: \n", m.String())
- panic(fmt.Sprintf("machine not empty after main: %v", err))
- }
-}
diff --git a/gnovm/tests/selector_test.go b/gnovm/tests/selector_test.go
deleted file mode 100644
index 1f0b400555b..00000000000
--- a/gnovm/tests/selector_test.go
+++ /dev/null
@@ -1,174 +0,0 @@
-package tests
-
-import (
- "fmt"
- "reflect"
- "testing"
-)
-
-/*
-This attempts to show a sufficiently exhaustive list of ValuePaths for
-different types of selectors. As can be seen, even a simple selector
-expression can represent a number of different types of selectors.
-*/
-
-// S1 struct
-type S1 struct {
- F0 int
-}
-
-func (S1) Hello() {
-}
-
-func (*S1) Bye() {
-}
-
-// Pointer to S1
-type S1P *S1
-
-// Like S1 but pointer struct
-type PS1 *struct {
- F0 int
-}
-
-type S7 struct {
- S1
-}
-
-type S9 struct {
- *S1
-}
-
-type S10PD *struct {
- S1
-}
-
-func _printValue(x interface{}) {
- if reflect.TypeOf(x).Kind() == reflect.Func {
- fmt.Println("function")
- } else {
- fmt.Println(x)
- }
-}
-
-func TestSelectors(t *testing.T) {
- t.Parallel()
-
- x0 := struct{ F0 int }{1}
- _printValue(x0.F0) // *ST.F0
- // F:0
- // VPField{depth:0,index:0}
- x1 := S1{1}
- _printValue(x1.F0) // *DT(S1)>*ST.F0
- // +1 F:0
- // VPField{depth:1,index:0}
- _printValue(x1.Hello) // *DT(S1).Hello
- // +1 M:0
- // VPValMethod{index:0}
- _printValue(x1.Bye) // *PT(implied)>*DT(S1).Bye
- // +D +1 *M:1
- // VPDerefPtrMethod{index:1}
- x2 := &x0
- _printValue(x2.F0) // *PT>*ST.F0
- // +D F:0
- // VPDerefField{depth:0,index:0}
- var x3 PS1 = &struct{ F0 int }{1}
- _printValue(x3.F0) // *DT(S1P)>*PT>*ST.F0
- // +1 +D F:0
- // VPDerefField{depth:1,index:0}
- x4 := &S1{1}
- _printValue(x4.F0) // *PT>*DT(S1P)>*ST.F0
- // +D +1 F:0
- // VPDerefField{depth:2,index:0}
- var x5 S1P = &S1{1}
- _printValue(x5.F0) // *DT(S1P)>*PT>*DT(S1)>*ST.F0
- // +1 +D +1 F:0
- // VPDerefField{depth:3,index:0}
- x6 := &x5
- _printValue(x6)
- // _printValue(x6.F0) *PT>*DT(S1P)??? > *PT>*DT(S1)>*ST.F0
- // +D +1 +D +1 F:0
- // VPDerefField{depth:1,index:0}(WRONG!!!) > VPDerefField{depth:1,index:0} XXX ERROR
- x7 := S7{S1{1}}
- _printValue(x7.F0) // *DT(S7)>*ST.S1 > *DT(S1)>*ST.F0
- // +1 F:0 +1 F:0
- // VPField{depth:1,index:0} > VPField{depth:1,index:0}
- x8 := &x7
- _printValue(x8.F0) // *PT>*DT(S7)>*ST.S1 > *DT(S1)>*ST.F0
- // +D +1 F:0 +1 F:0
- // VPDerefField{depth:1,index:0} > VPField{depth:1,index:0}
- x9 := S9{x5}
- _printValue(x9.F0) // *DT(S9)>*ST.S1 > *PT>*DT(S1)>*ST.F0
- // +1 F:0 +D +1 F:0
- // VPField{depth:1,index:0} > VPDerefField{depth:1,index:0}
- x10 := struct{ S1 }{S1{1}}
- _printValue(x10.F0) // *ST.S1 > *DT(S1)>*ST.F0
- // F:0 +1 F:0
- // VPField{depth:0,index:0} > VPField{depth:1,index:0}
- _printValue(x10.Hello) // *ST.S1 > *DT(S1).Hello
- // F:0 +1 M:0
- // VPField{depth:0,index:0} > VPValMethod{index:0}
- _printValue(x10.Bye) // (*PT>)*ST.S1 > *DT(S1).Bye
- // +S F:0 +1 *M:1
- // VPSubrefField{depth:0,index:0} > VPDerefPtrMethod{index:1}
- x10p := &x10
- _printValue(x10p.F0) // *PT>*ST.S1 > *DT(S1)>*ST.F0
- // +D F:0 +1 F:0
- // VPDerefField{depth:0,index:0} > VPField{depth:1,index:0}
- _printValue(x10p.Hello) // *PT>*ST.S1 > *DT(S1).Hello
- // +D F:0 +1 M:0
- // VPDerefField{depth:0,index:0} > VPValMethod{index:0}
- _printValue(x10p.Bye) // *PT>*ST.S1 > *DT(S1).Bye
- // +D F:0 +1 *M:1
- // VPSubrefField{depth:0,index:0} > VPDerefPtrMethod{index:1}
- var x10pd S10PD = &struct{ S1 }{S1{1}}
- _printValue(x10pd.F0) // *DT(S10PD)>*PT>*ST.S1 > *DT(S1)>*ST.F0
- // +1 +D F:0 +1 F:0
- // VPDerefField{depth:1,index:0} > VPField{depth:1,index:0}
- // _printValue(x10pd.Hello) *DT(S10PD)>*PT>*ST.S1 > *DT(S1).Hello XXX weird, doesn't work.
- // +1 +D F:0 +1 M:0
- // VPDerefField{depth:1,index:0} > VPValMethod{index:0}
- _printValue(x10p.Bye) // *DT(S10PD)>*PT>*ST.S1 > *DT(S1).Bye
- // +1 +D F:0 +1 *M:1
- // VPSubrefField{depth:1,index:0} > VPDerefPtrMethod{index:1}
- x11 := S7{S1{1}}
- _printValue(x11.F0) // *DT(S7)>*ST.S1 > *DT(S1)>*ST.F0 NOTE same as x7.
- // +1 F:0 +1 F:0
- // VPField{depth:1,index:0} > VPField{depth:1,index:0}
- _printValue(x11.Hello) // *DT(S7)>*ST.S1 > *DT(S1)>*ST.Hello
- // +1 F:0 +1 M:0
- // VPField{depth:1,index:0} > VPValMethod{index:0}
- _printValue(x11.Bye) // (*PT>)*DT(S7)>*ST.S1 > *DT(S1).Bye
- // +S +1 F:0 +1 *M:1
- // VPSubrefField{depth:2,index:0} > VPDerefPtrMethod{index:1}
- x11p := &S7{S1{1}}
- _printValue(x11p.F0) // *PT>*DT(S7)>*ST.S1 > *DT(S1)>*ST.F0
- // +1 F:0 +1 F:0
- // VPDerefField{depth:2,index:0} > VPField{depth:1,index:0}
- _printValue(x11p.Hello) // *PT>*DT(S7)>*ST.S1 > *DT(S1).Hello
- // +1 F:0 +1 M:0
- // VPDerefField{depth:2,index:0} > VPValMethod{index:0}
- _printValue(x11p.Bye) // *PT>*DT(S7)>*ST.S1 > *DT(S1).Bye
- // +1 F:0 +1 *M:1
- // VPSubrefField{depth:2,index:0} > VPDerefPtrMethod{index:1}
- x12 := struct{ *S1 }{&S1{1}}
- _printValue(x12.F0) // *ST.S1 > *PT>*DT(S1)>*ST.F0
- // F:0 +D +1 F:0
- // VPField{depth:0,index:0} > VPDerefField{depth:1,index:0}
- _printValue(x12.Hello) // *ST.S1 > *PT>*DT(S1).Hello
- // F:0 +D +1 M:0
- // VPField{depth:0,index:0} > VPDerefValMethod{index:0}
- _printValue(x12.Bye) // *ST.S1 > *PT>*DT(S1).Bye
- // F:0 +D +1 *M:1
- // VPField{depth:0,index:0} > VPDerefPtrMethod{index:1}
- x13 := &x12
- _printValue(x13.F0) // *PT>*ST.S1 > *PT>*DT(S1)>*ST.F0
- // +D F:0 +D +1 F:0
- // VPDerefField{depth:0,index:0} > VPDerefField{depth:1,index:0}
- _printValue(x13.Hello) // *PT>*ST.S1 > *PT>*DT(S1).Hello
- // +D F:0 +D +1 M:0
- // VPDerefField{depth:0,index:0} > VPDerefValMethod{index:0}
- _printValue(x13.Bye) // *PT>*ST.S1 > *PT>*DT(S1).Bye
- // +D F:0 +D +1 *M:1
- // VPDerefField{depth:0,index:0} > VPDerefPtrMethod{index:1}
-}
diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go
index f3d74e214eb..2cc904a9170 100644
--- a/gnovm/tests/stdlibs/generated.go
+++ b/gnovm/tests/stdlibs/generated.go
@@ -84,18 +84,6 @@ var nativeFuncs = [...]NativeFunc{
p0)
},
},
- {
- "std",
- "ClearStoreCache",
- []gno.FieldTypeExpr{},
- []gno.FieldTypeExpr{},
- true,
- func(m *gno.Machine) {
- testlibs_std.ClearStoreCache(
- m,
- )
- },
- },
{
"std",
"callerAt",
diff --git a/gnovm/tests/stdlibs/std/std.gno b/gnovm/tests/stdlibs/std/std.gno
index 3a56ecc1c47..dcb5a64dbb3 100644
--- a/gnovm/tests/stdlibs/std/std.gno
+++ b/gnovm/tests/stdlibs/std/std.gno
@@ -3,7 +3,6 @@ package std
func AssertOriginCall() // injected
func IsOriginCall() bool // injected
func TestSkipHeights(count int64) // injected
-func ClearStoreCache() // injected
func TestSetOrigCaller(addr Address) { testSetOrigCaller(string(addr)) }
func TestSetOrigPkgAddr(addr Address) { testSetOrigPkgAddr(string(addr)) }
diff --git a/gnovm/tests/stdlibs/std/std.go b/gnovm/tests/stdlibs/std/std.go
index d580572e9c5..675194b252f 100644
--- a/gnovm/tests/stdlibs/std/std.go
+++ b/gnovm/tests/stdlibs/std/std.go
@@ -3,11 +3,11 @@ package std
import (
"fmt"
"strings"
- "testing"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/stdlibs/std"
"github.com/gnolang/gno/tm2/pkg/crypto"
+ tm2std "github.com/gnolang/gno/tm2/pkg/std"
)
// TestExecContext is the testing extension of the exec context.
@@ -41,9 +41,17 @@ func IsOriginCall(m *gno.Machine) bool {
tname := m.Frames[0].Func.Name
switch tname {
case "main": // test is a _filetest
+ // 0. main
+ // 1. $RealmFuncName
+ // 2. std.IsOriginCall
return len(m.Frames) == 3
- case "runtest": // test is a _test
- return len(m.Frames) == 7
+ case "RunTest": // test is a _test
+ // 0. testing.RunTest
+ // 1. tRunner
+ // 2. $TestFuncName
+ // 3. $RealmFuncName
+ // 4. std.IsOriginCall
+ return len(m.Frames) == 5
}
// support init() in _filetest
// XXX do we need to distinguish from 'runtest'/_test?
@@ -61,23 +69,6 @@ func TestSkipHeights(m *gno.Machine, count int64) {
m.Context = ctx
}
-func ClearStoreCache(m *gno.Machine) {
- if gno.IsDebug() && testing.Verbose() {
- m.Store.Print()
- fmt.Println("========================================")
- fmt.Println("CLEAR CACHE (RUNTIME)")
- fmt.Println("========================================")
- }
- m.Store.ClearCache()
- m.PreprocessAllFilesAndSaveBlockNodes()
- if gno.IsDebug() && testing.Verbose() {
- m.Store.Print()
- fmt.Println("========================================")
- fmt.Println("CLEAR CACHE DONE")
- fmt.Println("========================================")
- }
-}
-
func X_callerAt(m *gno.Machine, n int) string {
if n <= 0 {
m.Panic(typedString("GetCallerAt requires positive arg"))
@@ -188,6 +179,60 @@ func X_testSetOrigSend(m *gno.Machine,
m.Context = ctx
}
+// TestBanker is a banker that can be used as a mock banker in test contexts.
+type TestBanker struct {
+ CoinTable map[crypto.Bech32Address]tm2std.Coins
+}
+
+var _ std.BankerInterface = &TestBanker{}
+
+// GetCoins implements the Banker interface.
+func (tb *TestBanker) GetCoins(addr crypto.Bech32Address) (dst tm2std.Coins) {
+ return tb.CoinTable[addr]
+}
+
+// SendCoins implements the Banker interface.
+func (tb *TestBanker) SendCoins(from, to crypto.Bech32Address, amt tm2std.Coins) {
+ fcoins, fexists := tb.CoinTable[from]
+ if !fexists {
+ panic(fmt.Sprintf(
+ "source address %s does not exist",
+ from.String()))
+ }
+ if !fcoins.IsAllGTE(amt) {
+ panic(fmt.Sprintf(
+ "source address %s has %s; cannot send %s",
+ from.String(), fcoins, amt))
+ }
+ // First, subtract from 'from'.
+ frest := fcoins.Sub(amt)
+ tb.CoinTable[from] = frest
+ // Second, add to 'to'.
+ // NOTE: even works when from==to, due to 2-step isolation.
+ tcoins, _ := tb.CoinTable[to]
+ tsum := tcoins.Add(amt)
+ tb.CoinTable[to] = tsum
+}
+
+// TotalCoin implements the Banker interface.
+func (tb *TestBanker) TotalCoin(denom string) int64 {
+ panic("not yet implemented")
+}
+
+// IssueCoin implements the Banker interface.
+func (tb *TestBanker) IssueCoin(addr crypto.Bech32Address, denom string, amt int64) {
+ coins, _ := tb.CoinTable[addr]
+ sum := coins.Add(tm2std.Coins{{Denom: denom, Amount: amt}})
+ tb.CoinTable[addr] = sum
+}
+
+// RemoveCoin implements the Banker interface.
+func (tb *TestBanker) RemoveCoin(addr crypto.Bech32Address, denom string, amt int64) {
+ coins, _ := tb.CoinTable[addr]
+ rest := coins.Sub(tm2std.Coins{{Denom: denom, Amount: amt}})
+ tb.CoinTable[addr] = rest
+}
+
func X_testIssueCoins(m *gno.Machine, addr string, denom []string, amt []int64) {
ctx := m.Context.(*TestExecContext)
banker := ctx.Banker
diff --git a/gnovm/tests/testdata/TestMemPackage/fail/file_test.gno b/gnovm/tests/testdata/TestMemPackage/fail/file_test.gno
deleted file mode 100644
index b202c40bc46..00000000000
--- a/gnovm/tests/testdata/TestMemPackage/fail/file_test.gno
+++ /dev/null
@@ -1,7 +0,0 @@
-package test
-
-import "testing"
-
-func TestFail(t *testing.T) {
- t.Errorf("OUPS")
-}
diff --git a/gnovm/tests/testdata/TestMemPackage/success/file_test.gno b/gnovm/tests/testdata/TestMemPackage/success/file_test.gno
deleted file mode 100644
index 0fc1d898199..00000000000
--- a/gnovm/tests/testdata/TestMemPackage/success/file_test.gno
+++ /dev/null
@@ -1,5 +0,0 @@
-package test
-
-import "testing"
-
-func TestSucess(t *testing.T) {}
From 4004ba139ab1fccb2987faea9c244f8eb46f181a Mon Sep 17 00:00:00 2001
From: Mikael VALLENET
Date: Tue, 26 Nov 2024 16:06:40 +0100
Subject: [PATCH 05/86] feat(gnovm): forbid importing realms in packages
(#3042)
Closes #3040
50% of the work comes from @harry-hov's PR #1393 (let's repay to Caesar
what belongs to Caesar) :rocket:
Notable additions:
- handle different domains (e.g github.com/p/demo/...)
- skip non ``.gno`` files (LICENSE, README, ...) or empty files
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---------
Co-authored-by: n0izn0iz
Co-authored-by: Morgan
Co-authored-by: Morgan
---
examples/gno.land/p/demo/groups/gno.mod | 5 +---
examples/gno.land/p/demo/groups/groups.gno | 8 -----
examples/gno.land/p/demo/tests/gno.mod | 1 -
examples/gno.land/p/demo/tests/tests.gno | 13 ---------
.../gno.land/p/demo/tests/z0_filetest.gno | 16 ----------
.../gnoland/testdata/assertorigincall.txtar | 29 ++++++++++---------
gno.land/cmd/gnoland/testdata/prevrealm.txtar | 22 +++++++-------
gnovm/pkg/gnolang/helpers.go | 12 +++++++-
gnovm/pkg/gnolang/preprocess.go | 7 +++++
gnovm/tests/files/import11.gno | 13 +++++++++
gnovm/tests/files/zrealm_crossrealm11.gno | 11 ++-----
11 files changed, 60 insertions(+), 77 deletions(-)
delete mode 100644 examples/gno.land/p/demo/groups/groups.gno
delete mode 100644 examples/gno.land/p/demo/tests/z0_filetest.gno
create mode 100644 gnovm/tests/files/import11.gno
diff --git a/examples/gno.land/p/demo/groups/gno.mod b/examples/gno.land/p/demo/groups/gno.mod
index f0749e3f411..cf33d0ce74b 100644
--- a/examples/gno.land/p/demo/groups/gno.mod
+++ b/examples/gno.land/p/demo/groups/gno.mod
@@ -1,6 +1,3 @@
module gno.land/p/demo/groups
-require (
- gno.land/p/demo/rat v0.0.0-latest
- gno.land/r/demo/boards v0.0.0-latest
-)
+require gno.land/p/demo/rat v0.0.0-latest
diff --git a/examples/gno.land/p/demo/groups/groups.gno b/examples/gno.land/p/demo/groups/groups.gno
deleted file mode 100644
index fcf77dd2a74..00000000000
--- a/examples/gno.land/p/demo/groups/groups.gno
+++ /dev/null
@@ -1,8 +0,0 @@
-package groups
-
-import "gno.land/r/demo/boards"
-
-// TODO implement something and test.
-type Group struct {
- Board *boards.Board
-}
diff --git a/examples/gno.land/p/demo/tests/gno.mod b/examples/gno.land/p/demo/tests/gno.mod
index d3d796f76f8..8a19acdbb18 100644
--- a/examples/gno.land/p/demo/tests/gno.mod
+++ b/examples/gno.land/p/demo/tests/gno.mod
@@ -3,5 +3,4 @@ module gno.land/p/demo/tests
require (
gno.land/p/demo/tests/subtests v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
- gno.land/r/demo/tests v0.0.0-latest
)
diff --git a/examples/gno.land/p/demo/tests/tests.gno b/examples/gno.land/p/demo/tests/tests.gno
index 43732d82dac..ffad5b8c8cd 100644
--- a/examples/gno.land/p/demo/tests/tests.gno
+++ b/examples/gno.land/p/demo/tests/tests.gno
@@ -4,19 +4,10 @@ import (
"std"
psubtests "gno.land/p/demo/tests/subtests"
- "gno.land/r/demo/tests"
- rtests "gno.land/r/demo/tests"
)
const World = "world"
-// IncCounter demonstrates that it's possible to call a realm function from
-// a package. So a package can potentially write into the store, by calling
-// an other realm.
-func IncCounter() {
- tests.IncCounter()
-}
-
func CurrentRealmPath() string {
return std.CurrentRealm().PkgPath()
}
@@ -64,10 +55,6 @@ func GetPSubtestsPrevRealm() std.Realm {
return psubtests.GetPrevRealm()
}
-func GetRTestsGetPrevRealm() std.Realm {
- return rtests.GetPrevRealm()
-}
-
// Warning: unsafe pattern.
func Exec(fn func()) {
fn()
diff --git a/examples/gno.land/p/demo/tests/z0_filetest.gno b/examples/gno.land/p/demo/tests/z0_filetest.gno
deleted file mode 100644
index b788eaf398f..00000000000
--- a/examples/gno.land/p/demo/tests/z0_filetest.gno
+++ /dev/null
@@ -1,16 +0,0 @@
-package main
-
-import (
- ptests "gno.land/p/demo/tests"
- rtests "gno.land/r/demo/tests"
-)
-
-func main() {
- println(rtests.Counter())
- ptests.IncCounter()
- println(rtests.Counter())
-}
-
-// Output:
-// 0
-// 1
diff --git a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar
index 1315f23cc95..62d660a9215 100644
--- a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar
+++ b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar
@@ -9,18 +9,18 @@
# | 4 | | through /r/foo | myrealm.A() | PANIC |
# | 5 | | | myrealm.B() | pass |
# | 6 | | | myrealm.C() | PANIC |
-# | 7 | | through /p/demo/bar | myrealm.A() | PANIC |
-# | 8 | | | myrealm.B() | pass |
-# | 9 | | | myrealm.C() | PANIC |
+# | 7 | | through /p/demo/bar | bar.A() | PANIC |
+# | 8 | | | bar.B() | pass |
+# | 9 | | | bar.C() | PANIC |
# | 10 | MsgRun | wallet direct | myrealm.A() | PANIC |
# | 11 | | | myrealm.B() | pass |
# | 12 | | | myrealm.C() | PANIC |
# | 13 | | through /r/foo | myrealm.A() | PANIC |
# | 14 | | | myrealm.B() | pass |
# | 15 | | | myrealm.C() | PANIC |
-# | 16 | | through /p/demo/bar | myrealm.A() | PANIC |
-# | 17 | | | myrealm.B() | pass |
-# | 18 | | | myrealm.C() | PANIC |
+# | 16 | | through /p/demo/bar | bar.A() | PANIC |
+# | 17 | | | bar.B() | pass |
+# | 18 | | | bar.C() | PANIC |
# | 19 | MsgCall | wallet direct | std.AssertOriginCall() | pass |
# | 20 | MsgRun | wallet direct | std.AssertOriginCall() | PANIC |
@@ -57,15 +57,15 @@ stdout 'OK!'
stderr 'invalid non-origin call'
## remove due to update to maketx call can only call realm (case 7,8,9)
-## 7. MsgCall -> p/demo/bar.A -> myrlm.A: PANIC
+## 7. MsgCall -> p/demo/bar.A: PANIC
## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
## stderr 'invalid non-origin call'
-## 8. MsgCall -> p/demo/bar.B -> myrlm.B: PASS
+## 8. MsgCall -> p/demo/bar.B: PASS
## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
## stdout 'OK!'
-## 9. MsgCall -> p/demo/bar.C -> myrlm.C: PANIC
+## 9. MsgCall -> p/demo/bar.C: PANIC
## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
## stderr 'invalid non-origin call'
@@ -152,18 +152,19 @@ func C() {
-- p/demo/bar/bar.gno --
package bar
-import "gno.land/r/myrlm"
+import "std"
func A() {
- myrlm.A()
+ C()
}
func B() {
- myrlm.B()
+ if false {
+ C()
+ }
}
-
func C() {
- myrlm.C()
+ std.AssertOriginCall()
}
-- run/myrlmA.gno --
package main
diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar
index 7a0d994a686..4a7cece6d62 100644
--- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar
+++ b/gno.land/cmd/gnoland/testdata/prevrealm.txtar
@@ -8,14 +8,14 @@
# | 2 | | | myrlm.B() | user address |
# | 3 | | through /r/foo | myrlm.A() | r/foo |
# | 4 | | | myrlm.B() | r/foo |
-# | 5 | | through /p/demo/bar | myrlm.A() | user address |
-# | 6 | | | myrlm.B() | user address |
+# | 5 | | through /p/demo/bar | bar.A() | user address |
+# | 6 | | | bar.B() | user address |
# | 7 | MsgRun | wallet direct | myrlm.A() | user address |
# | 8 | | | myrlm.B() | user address |
# | 9 | | through /r/foo | myrlm.A() | r/foo |
# | 10 | | | myrlm.B() | r/foo |
-# | 11 | | through /p/demo/bar | myrlm.A() | user address |
-# | 12 | | | myrlm.B() | user address |
+# | 11 | | through /p/demo/bar | bar.A() | user address |
+# | 12 | | | bar.B() | user address |
# | 13 | MsgCall | wallet direct | std.PrevRealm() | user address |
# | 14 | MsgRun | wallet direct | std.PrevRealm() | user address |
@@ -50,11 +50,11 @@ gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wan
stdout ${RFOO_ADDR}
## remove due to update to maketx call can only call realm (case 5, 6, 13)
-## 5. MsgCall -> p/demo/bar.A -> myrlm.A: user address
+## 5. MsgCall -> p/demo/bar.A: user address
## gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
## stdout ${USER_ADDR_test1}
-## 6. MsgCall -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address
+## 6. MsgCall -> p/demo/bar.B: user address
## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1
## stdout ${USER_ADDR_test1}
@@ -74,11 +74,11 @@ stdout ${RFOO_ADDR}
gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno
stdout ${RFOO_ADDR}
-## 11. MsgRun -> p/demo/bar.A -> myrlm.A: user address
+## 11. MsgRun -> p/demo/bar.A: user address
gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno
stdout ${USER_ADDR_test1}
-## 12. MsgRun -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address
+## 12. MsgRun -> p/demo/bar.B: user address
gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno
stdout ${USER_ADDR_test1}
@@ -117,14 +117,14 @@ func B() string {
-- p/demo/bar/bar.gno --
package bar
-import "gno.land/r/myrlm"
+import "std"
func A() string {
- return myrlm.A()
+ return std.PrevRealm().Addr().String()
}
func B() string {
- return myrlm.B()
+ return A()
}
-- run/myrlmA.gno --
package main
diff --git a/gnovm/pkg/gnolang/helpers.go b/gnovm/pkg/gnolang/helpers.go
index c6f7e696ea4..d3a8485ee17 100644
--- a/gnovm/pkg/gnolang/helpers.go
+++ b/gnovm/pkg/gnolang/helpers.go
@@ -12,7 +12,10 @@ import (
// RealmPathPrefix is the prefix used to identify pkgpaths which are meant to
// be realms and as such to have their state persisted. This is used by [IsRealmPath].
-const RealmPathPrefix = "gno.land/r/"
+const (
+ RealmPathPrefix = "gno.land/r/"
+ PackagePathPrefix = "gno.land/p/"
+)
// ReGnoRunPath is the path used for realms executed in maketx run.
// These are not considered realms, as an exception to the RealmPathPrefix rule.
@@ -26,6 +29,13 @@ func IsRealmPath(pkgPath string) bool {
!ReGnoRunPath.MatchString(pkgPath)
}
+// IsPurePackagePath determines whether the given pkgpath is for a published Gno package.
+// It only considers "pure" those starting with gno.land/p/, so it returns false for
+// stdlib packages and MsgRun paths.
+func IsPurePackagePath(pkgPath string) bool {
+ return strings.HasPrefix(pkgPath, PackagePathPrefix)
+}
+
// IsStdlib determines whether s is a pkgpath for a standard library.
func IsStdlib(s string) bool {
// NOTE(morgan): this is likely to change in the future as we add support for
diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go
index 4b556604f0b..53c187342a6 100644
--- a/gnovm/pkg/gnolang/preprocess.go
+++ b/gnovm/pkg/gnolang/preprocess.go
@@ -174,6 +174,13 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) {
case *ImportDecl:
nx := &n.NameExpr
nn := nx.Name
+ loc := last.GetLocation()
+ // NOTE: imports from "pure packages" are actually sometimes
+ // allowed, most notably in MsgRun and filetests; IsPurePackagePath
+ // returns false in these cases.
+ if IsPurePackagePath(loc.PkgPath) && IsRealmPath(n.PkgPath) {
+ panic(fmt.Sprintf("pure package path %q cannot import realm path %q", loc.PkgPath, n.PkgPath))
+ }
if nn == "." {
panic("dot imports not allowed in gno")
}
diff --git a/gnovm/tests/files/import11.gno b/gnovm/tests/files/import11.gno
new file mode 100644
index 00000000000..594e9f10698
--- /dev/null
+++ b/gnovm/tests/files/import11.gno
@@ -0,0 +1,13 @@
+// PKGPATH: gno.land/p/demo/bar
+package bar
+
+import (
+ "gno.land/r/demo/tests"
+)
+
+func main() {
+ println(tests.Counter())
+}
+
+// Error:
+// gno.land/p/demo/bar/files/import11.gno:5:2: pure package path "gno.land/p/demo/bar" cannot import realm path "gno.land/r/demo/tests"
diff --git a/gnovm/tests/files/zrealm_crossrealm11.gno b/gnovm/tests/files/zrealm_crossrealm11.gno
index e6f33c50654..5936743ddc6 100644
--- a/gnovm/tests/files/zrealm_crossrealm11.gno
+++ b/gnovm/tests/files/zrealm_crossrealm11.gno
@@ -2,10 +2,11 @@
package crossrealm_test
import (
+ "std"
+
ptests "gno.land/p/demo/tests"
"gno.land/p/demo/ufmt"
rtests "gno.land/r/demo/tests"
- "std"
)
func getPrevRealm() std.Realm {
@@ -64,10 +65,6 @@ func main() {
callStackAdd: " -> r/demo/tests -> r/demo/tests/subtests",
callerFn: rtests.GetRSubtestsPrevRealm,
},
- {
- callStackAdd: " -> p/demo/tests -> r/demo/tests",
- callerFn: ptests.GetRTestsGetPrevRealm,
- },
}
println("---") // needed to have space prefixes
@@ -140,7 +137,3 @@ func printColumns(left, right string) {
// user1.gno -> r/crossrealm_test.main -> r/crossrealm_test.Exec -> r/demo/tests -> r/demo/tests/subtests = gno.land/r/demo/tests
// user1.gno -> r/crossrealm_test.main -> r/demo/tests.Exec -> r/demo/tests -> r/demo/tests/subtests = gno.land/r/demo/tests
// user1.gno -> r/crossrealm_test.main -> p/demo/tests.Exec -> r/demo/tests -> r/demo/tests/subtests = gno.land/r/demo/tests
-// user1.gno -> r/crossrealm_test.main -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test
-// user1.gno -> r/crossrealm_test.main -> r/crossrealm_test.Exec -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test
-// user1.gno -> r/crossrealm_test.main -> r/demo/tests.Exec -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test
-// user1.gno -> r/crossrealm_test.main -> p/demo/tests.Exec -> p/demo/tests -> r/demo/tests = gno.land/r/crossrealm_test
From 2093d8a43c1ec632707f28021860ac52f5a80c26 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 18:25:20 +0100
Subject: [PATCH 06/86] chore(deps): bump golang.org/x/net from
0.0.0-20190813141303-74dc4d7220e7 to 0.23.0 in /contribs/gnomd in the
go_modules group across 1 directory (#3154)
Bumps the go_modules group with 1 update in the /contribs/gnomd
directory: [golang.org/x/net](https://github.com/golang/net).
Updates `golang.org/x/net` from 0.0.0-20190813141303-74dc4d7220e7 to
0.23.0
Commits
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Morgan Bazalgette
---
.github/workflows/genesis-verify.yml | 3 ++-
.github/workflows/releaser-master.yml | 2 +-
.github/workflows/releaser-nightly.yml | 2 +-
.github/workflows/releaser.yml | 2 +-
4 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml
index 6c9955b7178..f870cd0658c 100644
--- a/.github/workflows/genesis-verify.yml
+++ b/.github/workflows/genesis-verify.yml
@@ -6,6 +6,7 @@ on:
- master
paths:
- "misc/deployments/**/genesis.json"
+ - ".github/workflows/genesis-verify.yml"
jobs:
verify:
@@ -20,7 +21,7 @@ jobs:
- name: Get changed files
id: changed-files
- uses: tj-actions/changed-files@v41
+ uses: tj-actions/changed-files@v45
with:
files: "misc/deployments/${{ matrix.testnet }}/genesis.json"
diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml
index eb5698e9d8f..36a709a242a 100644
--- a/.github/workflows/releaser-master.yml
+++ b/.github/workflows/releaser-master.yml
@@ -27,7 +27,7 @@ jobs:
cache: true
- uses: sigstore/cosign-installer@v3.7.0
- - uses: anchore/sbom-action/download-syft@v0.17.7
+ - uses: anchore/sbom-action/download-syft@v0.17.8
- uses: docker/login-action@v3
with:
diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml
index aed56526a2f..e9a5c15a22d 100644
--- a/.github/workflows/releaser-nightly.yml
+++ b/.github/workflows/releaser-nightly.yml
@@ -24,7 +24,7 @@ jobs:
cache: true
- uses: sigstore/cosign-installer@v3.7.0
- - uses: anchore/sbom-action/download-syft@v0.17.7
+ - uses: anchore/sbom-action/download-syft@v0.17.8
- uses: docker/login-action@v3
with:
diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml
index aeda7ed2c7e..d33432bd16d 100644
--- a/.github/workflows/releaser.yml
+++ b/.github/workflows/releaser.yml
@@ -24,7 +24,7 @@ jobs:
cache: true
- uses: sigstore/cosign-installer@v3.7.0
- - uses: anchore/sbom-action/download-syft@v0.17.7
+ - uses: anchore/sbom-action/download-syft@v0.17.8
- uses: docker/login-action@v3
with:
From 8125041024044caf5f7f61a4067e700f533e077d Mon Sep 17 00:00:00 2001
From: Kristov Atlas <7227529+kristovatlas@users.noreply.github.com>
Date: Wed, 27 Nov 2024 10:20:42 -0600
Subject: [PATCH 12/86] docs: Add security policy (#3217)
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---
SECURITY.md | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 SECURITY.md
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000000..8380267dacf
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,14 @@
+# Security Policy
+
+The gno.land community strives to contribute toward the security of our ecosystem through internal security practices, and by working with external security researchers from the community.
+
+## Reporting a Vulnerability
+If you've identified a vulnerability, please report it through one of the following venues:
+
+* Submit an advisory through GitHub: https://github.com/gnolang/gno/security/advisories/new
+* Email security [at-symbol] tendermint [dot] com. If you are concerned about confidentiality e.g. because of a high-severity issue, you may email us for PGP or Signal contact details.
+* A security bug bounty platform for gno.land will be available Soonᵀᴹ. You will need to report via our bug bounty platform in order to be eligible for rewards.
+
+We will respond within 3 business days to all received reports.
+
+Thank you for helping to keep our ecosystem safe!
From 310938d8a737403f6d4d695cb3342db8f7e6ba9b Mon Sep 17 00:00:00 2001
From: Antoine Eddi <5222525+aeddi@users.noreply.github.com>
Date: Wed, 27 Nov 2024 18:53:04 +0100
Subject: [PATCH 13/86] ci: add a github bot to support advanced PR review
workflows (#3037)
This pull request aims to add a bot that extends GitHub's
functionalities like codeowners file and other merge protection
mechanisms. Interaction with the bot is done via a comment. You can test
it on the demo repo here : https://github.com/GnoCheckBot/demo/pull/1
Fixes #1007
Related to #1466, #2788
- The `config.go` file contains all the conditions and requirements in
an 'If - Then' format.
```go
// Automatic check
{
Description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams",
If: c.And(
c.FileChanged(gh, "tm2"),
c.BaseBranch("main"),
),
Then: r.And(
r.Or(
r.ReviewByTeamMembers(gh, "eu", 1),
r.AuthorInTeam(gh, "eu"),
),
r.Or(
r.ReviewByTeamMembers(gh, "us", 1),
r.AuthorInTeam(gh, "us"),
),
),
}
```
- There are two types of checks: some are automatic and managed by the
bot (like the one above), while others are manual and need to be
verified by a specific org team member (like the one below). If no team
is specified, anyone with comment editing permission can check it.
```go
// Manual check
{
Description: "The documentation is accurate and relevant",
If: c.FileChanged(gh, `.*\.md`),
Teams: []string{
"tech-staff",
"devrels",
},
},
```
- The conditions (If) allow checking, among other things, who the author
is, who is assigned, what labels are applied, the modified files, etc.
The list is available in the `condition` folder.
- The requirements (Then) allow, among other things, assigning a member,
verifying that a review is done by a specific user, applying a label,
etc. (List in `requirement` folder).
- A PR Check (the icon at the bottom with all the CI checks) will remain
orange/pending until all checks are validated, after which it will turn
green.
- The Github Actions workflow associated with the bot ensures that PRs
are processed concurrently, while ensuring that the same PR is not
processed by two runners at the same time.
- We can manually process a PR by launching the workflow directly from
the [GitHub Actions
interface](https://github.com/GnoCheckBot/demo/actions/workflows/bot.yml).
#### To do
- [x] implement base version of the bot
- [x] cleanup code / comments
- [x] setup a demo repo
- [x] add debug printing on dry run
- [x] add some tests on requirements and conditions
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---
.github/workflows/bot.yml | 79 +++++
contribs/github-bot/README.md | 48 +++
contribs/github-bot/check.go | 246 +++++++++++++++
contribs/github-bot/comment.go | 282 +++++++++++++++++
contribs/github-bot/comment.tmpl | 51 +++
contribs/github-bot/comment_test.go | 164 ++++++++++
contribs/github-bot/config.go | 100 ++++++
contribs/github-bot/go.mod | 28 ++
contribs/github-bot/go.sum | 38 +++
contribs/github-bot/internal/client/client.go | 293 ++++++++++++++++++
.../internal/conditions/assignee.go | 66 ++++
.../internal/conditions/assignee_test.go | 100 ++++++
.../github-bot/internal/conditions/author.go | 60 ++++
.../internal/conditions/author_test.go | 93 ++++++
.../github-bot/internal/conditions/boolean.go | 98 ++++++
.../internal/conditions/boolean_test.go | 96 ++++++
.../github-bot/internal/conditions/branch.go | 49 +++
.../internal/conditions/branch_test.go | 49 +++
.../internal/conditions/condition.go | 12 +
.../internal/conditions/constant.go | 34 ++
.../internal/conditions/constant_test.go | 25 ++
.../github-bot/internal/conditions/file.go | 58 ++++
.../internal/conditions/file_test.go | 68 ++++
.../github-bot/internal/conditions/label.go | 34 ++
.../internal/conditions/label_test.go | 48 +++
contribs/github-bot/internal/logger/action.go | 43 +++
contribs/github-bot/internal/logger/logger.go | 40 +++
contribs/github-bot/internal/logger/noop.go | 27 ++
.../github-bot/internal/logger/terminal.go | 55 ++++
contribs/github-bot/internal/params/params.go | 118 +++++++
contribs/github-bot/internal/params/prlist.go | 49 +++
.../internal/requirements/assignee.go | 53 ++++
.../internal/requirements/assignee_test.go | 72 +++++
.../internal/requirements/author.go | 39 +++
.../internal/requirements/author_test.go | 93 ++++++
.../internal/requirements/boolean.go | 98 ++++++
.../internal/requirements/boolean_test.go | 96 ++++++
.../internal/requirements/branch.go | 53 ++++
.../internal/requirements/branch_test.go | 62 ++++
.../internal/requirements/constant.go | 34 ++
.../internal/requirements/constant_test.go | 25 ++
.../github-bot/internal/requirements/label.go | 53 ++++
.../internal/requirements/label_test.go | 79 +++++
.../internal/requirements/maintainer.go | 25 ++
.../internal/requirements/maintener_test.go | 34 ++
.../internal/requirements/requirement.go | 12 +
.../internal/requirements/reviewer.go | 156 ++++++++++
.../internal/requirements/reviewer_test.go | 215 +++++++++++++
contribs/github-bot/internal/utils/actions.go | 45 +++
.../github-bot/internal/utils/actions_test.go | 43 +++
.../github-bot/internal/utils/github_const.go | 14 +
contribs/github-bot/internal/utils/testing.go | 21 ++
contribs/github-bot/internal/utils/tree.go | 24 ++
contribs/github-bot/main.go | 26 ++
contribs/github-bot/matrix.go | 111 +++++++
contribs/github-bot/matrix_test.go | 248 +++++++++++++++
56 files changed, 4282 insertions(+)
create mode 100644 .github/workflows/bot.yml
create mode 100644 contribs/github-bot/README.md
create mode 100644 contribs/github-bot/check.go
create mode 100644 contribs/github-bot/comment.go
create mode 100644 contribs/github-bot/comment.tmpl
create mode 100644 contribs/github-bot/comment_test.go
create mode 100644 contribs/github-bot/config.go
create mode 100644 contribs/github-bot/go.mod
create mode 100644 contribs/github-bot/go.sum
create mode 100644 contribs/github-bot/internal/client/client.go
create mode 100644 contribs/github-bot/internal/conditions/assignee.go
create mode 100644 contribs/github-bot/internal/conditions/assignee_test.go
create mode 100644 contribs/github-bot/internal/conditions/author.go
create mode 100644 contribs/github-bot/internal/conditions/author_test.go
create mode 100644 contribs/github-bot/internal/conditions/boolean.go
create mode 100644 contribs/github-bot/internal/conditions/boolean_test.go
create mode 100644 contribs/github-bot/internal/conditions/branch.go
create mode 100644 contribs/github-bot/internal/conditions/branch_test.go
create mode 100644 contribs/github-bot/internal/conditions/condition.go
create mode 100644 contribs/github-bot/internal/conditions/constant.go
create mode 100644 contribs/github-bot/internal/conditions/constant_test.go
create mode 100644 contribs/github-bot/internal/conditions/file.go
create mode 100644 contribs/github-bot/internal/conditions/file_test.go
create mode 100644 contribs/github-bot/internal/conditions/label.go
create mode 100644 contribs/github-bot/internal/conditions/label_test.go
create mode 100644 contribs/github-bot/internal/logger/action.go
create mode 100644 contribs/github-bot/internal/logger/logger.go
create mode 100644 contribs/github-bot/internal/logger/noop.go
create mode 100644 contribs/github-bot/internal/logger/terminal.go
create mode 100644 contribs/github-bot/internal/params/params.go
create mode 100644 contribs/github-bot/internal/params/prlist.go
create mode 100644 contribs/github-bot/internal/requirements/assignee.go
create mode 100644 contribs/github-bot/internal/requirements/assignee_test.go
create mode 100644 contribs/github-bot/internal/requirements/author.go
create mode 100644 contribs/github-bot/internal/requirements/author_test.go
create mode 100644 contribs/github-bot/internal/requirements/boolean.go
create mode 100644 contribs/github-bot/internal/requirements/boolean_test.go
create mode 100644 contribs/github-bot/internal/requirements/branch.go
create mode 100644 contribs/github-bot/internal/requirements/branch_test.go
create mode 100644 contribs/github-bot/internal/requirements/constant.go
create mode 100644 contribs/github-bot/internal/requirements/constant_test.go
create mode 100644 contribs/github-bot/internal/requirements/label.go
create mode 100644 contribs/github-bot/internal/requirements/label_test.go
create mode 100644 contribs/github-bot/internal/requirements/maintainer.go
create mode 100644 contribs/github-bot/internal/requirements/maintener_test.go
create mode 100644 contribs/github-bot/internal/requirements/requirement.go
create mode 100644 contribs/github-bot/internal/requirements/reviewer.go
create mode 100644 contribs/github-bot/internal/requirements/reviewer_test.go
create mode 100644 contribs/github-bot/internal/utils/actions.go
create mode 100644 contribs/github-bot/internal/utils/actions_test.go
create mode 100644 contribs/github-bot/internal/utils/github_const.go
create mode 100644 contribs/github-bot/internal/utils/testing.go
create mode 100644 contribs/github-bot/internal/utils/tree.go
create mode 100644 contribs/github-bot/main.go
create mode 100644 contribs/github-bot/matrix.go
create mode 100644 contribs/github-bot/matrix_test.go
diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
new file mode 100644
index 00000000000..975f39f29dc
--- /dev/null
+++ b/.github/workflows/bot.yml
@@ -0,0 +1,79 @@
+name: GitHub Bot
+
+on:
+ # Watch for changes on PR state, assignees, labels, head branch and draft/ready status
+ pull_request_target:
+ types:
+ - assigned
+ - unassigned
+ - labeled
+ - unlabeled
+ - opened
+ - reopened
+ - synchronize # PR head updated
+ - converted_to_draft
+ - ready_for_review
+
+ # Watch for changes on PR comment
+ issue_comment:
+ types: [created, edited, deleted]
+
+ # Manual run from GitHub Actions interface
+ workflow_dispatch:
+ inputs:
+ pull-request-list:
+ description: "PR(s) to process: specify 'all' or a comma separated list of PR numbers, e.g. '42,1337,7890'"
+ required: true
+ default: all
+ type: string
+
+jobs:
+ # This job creates a matrix of PR numbers based on the inputs from the various
+ # events that can trigger this workflow so that the process-pr job below can
+ # handle the parallel processing of the pull-requests
+ define-prs-matrix:
+ name: Define PRs matrix
+ # Prevent bot from retriggering itself
+ if: ${{ github.actor != vars.GH_BOT_LOGIN }}
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ outputs:
+ pr-numbers: ${{ steps.pr-numbers.outputs.pr-numbers }}
+
+ steps:
+ - name: Generate matrix from event
+ id: pr-numbers
+ working-directory: contribs/github-bot
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: go run . matrix >> "$GITHUB_OUTPUT"
+
+ # This job processes each pull request in the matrix individually while ensuring
+ # that a same PR cannot be processed concurrently by mutliple runners
+ process-pr:
+ name: Process PR
+ needs: define-prs-matrix
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ # Run one job for each PR to process
+ pr-number: ${{ fromJSON(needs.define-prs-matrix.outputs.pr-numbers) }}
+ concurrency:
+ # Prevent running concurrent jobs for a given PR number
+ group: ${{ matrix.pr-number }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Run GitHub Bot
+ working-directory: contribs/github-bot
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }}
+ run: go run . -pr-numbers '${{ matrix.pr-number }}' -verbose
diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md
new file mode 100644
index 00000000000..e3cc12fe01a
--- /dev/null
+++ b/contribs/github-bot/README.md
@@ -0,0 +1,48 @@
+# GitHub Bot
+
+## Overview
+
+The GitHub Bot is designed to automate and streamline the process of managing pull requests. It can automate certain tasks such as requesting reviews, assigning users or applying labels, but it also ensures that certain requirements are satisfied before allowing a pull request to be merged. Interaction with the bot occurs through a comment on the pull request, providing all the information to the user and allowing them to check boxes for the manual validation of certain rules.
+
+## How It Works
+
+### Configuration
+
+The bot operates by defining a set of rules that are evaluated against each pull request passed as parameter. These rules are categorized into automatic and manual checks:
+
+- **Automatic Checks**: These are rules that the bot evaluates automatically. If a pull request meets the conditions specified in the rule, then the corresponding requirements are executed. For example, ensuring that changes to specific directories are reviewed by specific team members.
+- **Manual Checks**: These require human intervention. If a pull request meets the conditions specified in the rule, then a checkbox that can be checked only by specified teams is displayed on the bot comment. For example, determining if infrastructure needs to be updated based on changes to specific files.
+
+The bot configuration is defined in Go and is located in the file [config.go](./config.go).
+
+### GitHub Token
+
+For the bot to make requests to the GitHub API, it needs a Personal Access Token. The fine-grained permissions to assign to the token for the bot to function are:
+
+- `pull_requests` scope to read is the bare minimum to run the bot in dry-run mode
+- `pull_requests` scope to write to be able to update bot comment, assign user, apply label and request review
+- `contents` scope to read to be able to check if the head branch is up to date with another one
+- `commit_statuses` scope to write to be able to update pull request bot status check
+
+## Usage
+
+```bash
+> go install github.com/gnolang/gno/contribs/github-bot@latest
+// (go: downloading ...)
+
+> github-bot --help
+USAGE
+ github-bot [flags]
+
+This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.
+A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.
+
+FLAGS
+ -dry-run=false print if pull request requirements are satisfied without updating anything on GitHub
+ -owner ... owner of the repo to process, if empty, will be retrieved from GitHub Actions context
+ -pr-all=false process all opened pull requests
+ -pr-numbers ... pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context
+ -repo ... repo to process, if empty, will be retrieved from GitHub Actions context
+ -timeout 0s timeout after which the bot execution is interrupted
+ -verbose=false set logging level to debug
+```
diff --git a/contribs/github-bot/check.go b/contribs/github-bot/check.go
new file mode 100644
index 00000000000..8019246d27c
--- /dev/null
+++ b/contribs/github-bot/check.go
@@ -0,0 +1,246 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "sync"
+ "sync/atomic"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ p "github.com/gnolang/gno/contribs/github-bot/internal/params"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/gnolang/gno/tm2/pkg/commands"
+ "github.com/google/go-github/v64/github"
+ "github.com/sethvargo/go-githubactions"
+ "github.com/xlab/treeprint"
+)
+
+func newCheckCmd() *commands.Command {
+ params := &p.Params{}
+
+ return commands.NewCommand(
+ commands.Metadata{
+ Name: "check",
+ ShortUsage: "github-bot check [flags]",
+ ShortHelp: "checks requirements for a pull request to be merged",
+ LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
+ },
+ params,
+ func(_ context.Context, _ []string) error {
+ params.ValidateFlags()
+ return execCheck(params)
+ },
+ )
+}
+
+func execCheck(params *p.Params) error {
+ // Create context with timeout if specified in the parameters.
+ ctx := context.Background()
+ if params.Timeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(context.Background(), params.Timeout)
+ defer cancel()
+ }
+
+ // Init GitHub API client.
+ gh, err := client.New(ctx, params)
+ if err != nil {
+ return fmt.Errorf("comment update handling failed: %w", err)
+ }
+
+ // Get GitHub Actions context to retrieve comment update.
+ actionCtx, err := githubactions.Context()
+ if err != nil {
+ gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err)
+ return nil
+ }
+
+ // Handle comment update, if any.
+ if err := handleCommentUpdate(gh, actionCtx); errors.Is(err, errTriggeredByBot) {
+ return nil // Ignore if this run was triggered by a previous run.
+ } else if err != nil {
+ return fmt.Errorf("comment update handling failed: %w", err)
+ }
+
+ // Retrieve a slice of pull requests to process.
+ var prs []*github.PullRequest
+
+ // If requested, retrieve all open pull requests.
+ if params.PRAll {
+ prs, err = gh.ListPR(utils.PRStateOpen)
+ if err != nil {
+ return fmt.Errorf("unable to list all PR: %w", err)
+ }
+ } else {
+ // Otherwise, retrieve only specified pull request(s)
+ // (flag or GitHub Action context).
+ prs = make([]*github.PullRequest, len(params.PRNums))
+ for i, prNum := range params.PRNums {
+ pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
+ if err != nil {
+ return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
+ }
+ prs[i] = pr
+ }
+ }
+
+ return processPRList(gh, prs)
+}
+
+func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
+ if len(prs) > 1 {
+ prNums := make([]int, len(prs))
+ for i, pr := range prs {
+ prNums[i] = pr.GetNumber()
+ }
+
+ gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums)
+ }
+
+ // Process all pull requests in parallel.
+ autoRules, manualRules := config(gh)
+ var wg sync.WaitGroup
+
+ // Used in dry-run mode to log cleanly from different goroutines.
+ logMutex := sync.Mutex{}
+
+ // Used in regular-run mode to return an error if one PR processing failed.
+ var failed atomic.Bool
+
+ for _, pr := range prs {
+ wg.Add(1)
+ go func(pr *github.PullRequest) {
+ defer wg.Done()
+ commentContent := CommentContent{}
+ commentContent.allSatisfied = true
+
+ // Iterate over all automatic rules in config.
+ for _, autoRule := range autoRules {
+ ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
+
+ // Check if conditions of this rule are met by this PR.
+ if !autoRule.ifC.IsMet(pr, ifDetails) {
+ continue
+ }
+
+ c := AutoContent{Description: autoRule.description, Satisfied: false}
+ thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.Fail))
+
+ // Check if requirements of this rule are satisfied by this PR.
+ if autoRule.thenR.IsSatisfied(pr, thenDetails) {
+ thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success))
+ c.Satisfied = true
+ } else {
+ commentContent.allSatisfied = false
+ }
+
+ c.ConditionDetails = ifDetails.String()
+ c.RequirementDetails = thenDetails.String()
+ commentContent.AutoRules = append(commentContent.AutoRules, c)
+ }
+
+ // Retrieve manual check states.
+ checks := make(map[string]manualCheckDetails)
+ if comment, err := gh.GetBotComment(pr.GetNumber()); err == nil {
+ checks = getCommentManualChecks(comment.GetBody())
+ }
+
+ // Iterate over all manual rules in config.
+ for _, manualRule := range manualRules {
+ ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
+
+ // Check if conditions of this rule are met by this PR.
+ if !manualRule.ifC.IsMet(pr, ifDetails) {
+ continue
+ }
+
+ // Get check status from current comment, if any.
+ checkedBy := ""
+ check, ok := checks[manualRule.description]
+ if ok {
+ checkedBy = check.checkedBy
+ }
+
+ commentContent.ManualRules = append(
+ commentContent.ManualRules,
+ ManualContent{
+ Description: manualRule.description,
+ ConditionDetails: ifDetails.String(),
+ CheckedBy: checkedBy,
+ Teams: manualRule.teams,
+ },
+ )
+
+ if checkedBy == "" {
+ commentContent.allSatisfied = false
+ }
+ }
+
+ // Logs results or write them in bot PR comment.
+ if gh.DryRun {
+ logMutex.Lock()
+ logResults(gh.Logger, pr.GetNumber(), commentContent)
+ logMutex.Unlock()
+ } else {
+ if err := updatePullRequest(gh, pr, commentContent); err != nil {
+ gh.Logger.Errorf("unable to update pull request: %v", err)
+ failed.Store(true)
+ }
+ }
+ }(pr)
+ }
+ wg.Wait()
+
+ if failed.Load() {
+ return errors.New("error occurred while processing pull requests")
+ }
+
+ return nil
+}
+
+// logResults is called in dry-run mode and outputs the status of each check
+// and a conclusion.
+func logResults(logger logger.Logger, prNum int, commentContent CommentContent) {
+ logger.Infof("Pull request #%d requirements", prNum)
+ if len(commentContent.AutoRules) > 0 {
+ logger.Infof("Automated Checks:")
+ }
+
+ for _, rule := range commentContent.AutoRules {
+ status := utils.Fail
+ if rule.Satisfied {
+ status = utils.Success
+ }
+ logger.Infof("%s %s", status, rule.Description)
+ logger.Debugf("If:\n%s", rule.ConditionDetails)
+ logger.Debugf("Then:\n%s", rule.RequirementDetails)
+ }
+
+ if len(commentContent.ManualRules) > 0 {
+ logger.Infof("Manual Checks:")
+ }
+
+ for _, rule := range commentContent.ManualRules {
+ status := utils.Fail
+ checker := "any user with comment edit permission"
+ if rule.CheckedBy != "" {
+ status = utils.Success
+ }
+ if len(rule.Teams) == 0 {
+ checker = fmt.Sprintf("a member of one of these teams: %s", strings.Join(rule.Teams, ", "))
+ }
+ logger.Infof("%s %s", status, rule.Description)
+ logger.Debugf("If:\n%s", rule.ConditionDetails)
+ logger.Debugf("Can be checked by %s", checker)
+ }
+
+ logger.Infof("Conclusion:")
+ if commentContent.allSatisfied {
+ logger.Infof("%s All requirements are satisfied\n", utils.Success)
+ } else {
+ logger.Infof("%s Not all requirements are satisfied\n", utils.Fail)
+ }
+}
diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go
new file mode 100644
index 00000000000..8bf4a158745
--- /dev/null
+++ b/contribs/github-bot/comment.go
@@ -0,0 +1,282 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "regexp"
+ "strings"
+ "text/template"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/sethvargo/go-githubactions"
+)
+
+var errTriggeredByBot = errors.New("event triggered by bot")
+
+// Compile regex only once.
+var (
+ // Regex for capturing the entire line of a manual check.
+ manualCheckLine = regexp.MustCompile(`(?m:^-\s\[([ xX])\]\s+(.+?)\s*(\(checked by @(\w+)\))?$)`)
+ // Regex for capturing only the checkboxes.
+ checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`)
+ // Regex used to capture markdown links.
+ markdownLink = regexp.MustCompile(`\[(.*)\]\(.*\)`)
+)
+
+// These structures contain the necessary information to generate
+// the bot's comment from the template file.
+type AutoContent struct {
+ Description string
+ Satisfied bool
+ ConditionDetails string
+ RequirementDetails string
+}
+type ManualContent struct {
+ Description string
+ CheckedBy string
+ ConditionDetails string
+ Teams []string
+}
+type CommentContent struct {
+ AutoRules []AutoContent
+ ManualRules []ManualContent
+ allSatisfied bool
+}
+
+type manualCheckDetails struct {
+ status string
+ checkedBy string
+}
+
+// getCommentManualChecks parses the bot comment to get the checkbox status,
+// the check description and the username who checked it.
+func getCommentManualChecks(commentBody string) map[string]manualCheckDetails {
+ checks := make(map[string]manualCheckDetails)
+
+ // For each line that matches the "Manual check" regex.
+ for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) {
+ description := match[2]
+ status := match[1]
+ checkedBy := ""
+ if len(match) > 4 {
+ checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x.
+ }
+
+ checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy}
+ }
+
+ return checks
+}
+
+// handleCommentUpdate checks if:
+// - the current run was triggered by GitHub Actions
+// - the triggering event is an edit of the bot comment
+// - the comment was not edited by the bot itself (prevent infinite loop)
+// - the comment change is only a checkbox being checked or unckecked (or restore it)
+// - the actor / comment editor has permission to modify this checkbox (or restore it)
+func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubContext) error {
+ // Ignore if it's not a comment related event.
+ if actionCtx.EventName != utils.EventIssueComment {
+ gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName)
+ return nil
+ }
+
+ // Ignore if the action type is not deleted or edited.
+ actionType, ok := actionCtx.Event["action"].(string)
+ if !ok {
+ return errors.New("unable to get type on issue comment event")
+ }
+
+ if actionType != "deleted" && actionType != "edited" {
+ return nil
+ }
+
+ // Return if comment was edited by bot (current authenticated user).
+ authUser, _, err := gh.Client.Users.Get(gh.Ctx, "")
+ if err != nil {
+ return fmt.Errorf("unable to get authenticated user: %w", err)
+ }
+
+ if actionCtx.Actor == authUser.GetLogin() {
+ gh.Logger.Debugf("Prevent infinite loop if the bot comment was edited by the bot itself")
+ return errTriggeredByBot
+ }
+
+ // Get login of the author of the edited comment.
+ login, ok := utils.IndexMap(actionCtx.Event, "comment", "user", "login").(string)
+ if !ok {
+ return errors.New("unable to get comment user login on issue comment event")
+ }
+
+ // If the author is not the bot, return.
+ if login != authUser.GetLogin() {
+ return nil
+ }
+
+ // Get comment updated body.
+ current, ok := utils.IndexMap(actionCtx.Event, "comment", "body").(string)
+ if !ok {
+ return errors.New("unable to get comment body on issue comment event")
+ }
+
+ // Get comment previous body.
+ previous, ok := utils.IndexMap(actionCtx.Event, "changes", "body", "from").(string)
+ if !ok {
+ return errors.New("unable to get changes body content on issue comment event")
+ }
+
+ // Get PR number from GitHub Actions context.
+ prNum, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64)
+ if !ok || prNum <= 0 {
+ return errors.New("unable to get issue number on issue comment event")
+ }
+
+ // Check if change is only a checkbox being checked or unckecked.
+ if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") {
+ // If not, restore previous comment body.
+ if !gh.DryRun {
+ gh.SetBotComment(previous, int(prNum))
+ }
+ return errors.New("bot comment edited outside of checkboxes")
+ }
+
+ // Check if actor / comment editor has permission to modify changed boxes.
+ currentChecks := getCommentManualChecks(current)
+ previousChecks := getCommentManualChecks(previous)
+ edited := ""
+ for key := range currentChecks {
+ // If there is no diff for this check, ignore it.
+ if currentChecks[key].status == previousChecks[key].status {
+ continue
+ }
+
+ // Get teams allowed to edit this box from config.
+ var teams []string
+ found := false
+ _, manualRules := config(gh)
+
+ for _, manualRule := range manualRules {
+ if manualRule.description == key {
+ found = true
+ teams = manualRule.teams
+ }
+ }
+
+ // If rule were not found, return to reprocess the bot comment entirely
+ // (maybe bot config was updated since last run?).
+ if !found {
+ gh.Logger.Debugf("Updated rule not found in config: %s", key)
+ return nil
+ }
+
+ // If teams specified in rule, check if actor is a member of one of them.
+ if len(teams) > 0 {
+ if gh.IsUserInTeams(actionCtx.Actor, teams) {
+ if !gh.DryRun {
+ gh.SetBotComment(previous, int(prNum))
+ }
+ return errors.New("checkbox edited by a user not allowed to")
+ }
+ }
+
+ // This regex capture only the line of the current check.
+ specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key].status, regexp.QuoteMeta(key)))
+
+ // If the box is checked, append the username of the user who checked it.
+ if strings.TrimSpace(currentChecks[key].status) == "x" {
+ replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key].status, key, actionCtx.Actor)
+ edited = specificManualCheck.ReplaceAllString(current, replacement)
+ } else {
+ // Else, remove the username of the user.
+ replacement := fmt.Sprintf("- [%s] %s", currentChecks[key].status, key)
+ edited = specificManualCheck.ReplaceAllString(current, replacement)
+ }
+ }
+
+ // Update comment with username.
+ if edited != "" && !gh.DryRun {
+ gh.SetBotComment(edited, int(prNum))
+ gh.Logger.Debugf("Comment manual checks updated successfully")
+ }
+
+ return nil
+}
+
+// generateComment generates a comment using the template file and the
+// content passed as parameter.
+func generateComment(content CommentContent) (string, error) {
+ // Custom function to strip markdown links.
+ funcMap := template.FuncMap{
+ "stripLinks": func(input string) string {
+ return markdownLink.ReplaceAllString(input, "$1")
+ },
+ }
+
+ // Bind markdown stripping function to template generator.
+ const tmplFile = "comment.tmpl"
+ tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
+ if err != nil {
+ return "", fmt.Errorf("unable to init template: %w", err)
+ }
+
+ // Generate bot comment using template file.
+ var commentBytes bytes.Buffer
+ if err := tmpl.Execute(&commentBytes, content); err != nil {
+ return "", fmt.Errorf("unable to execute template: %w", err)
+ }
+
+ return commentBytes.String(), nil
+}
+
+// updatePullRequest updates or creates both the bot comment and the commit status.
+func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content CommentContent) error {
+ // Generate comment text content.
+ commentText, err := generateComment(content)
+ if err != nil {
+ return fmt.Errorf("unable to generate comment on PR %d: %w", pr.GetNumber(), err)
+ }
+
+ // Update comment on pull request.
+ comment, err := gh.SetBotComment(commentText, pr.GetNumber())
+ if err != nil {
+ return fmt.Errorf("unable to update comment on PR %d: %w", pr.GetNumber(), err)
+ } else {
+ gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber())
+ }
+
+ // Prepare commit status content.
+ var (
+ context = "Merge Requirements"
+ targetURL = comment.GetHTMLURL()
+ state = "failure"
+ description = "Some requirements are not satisfied yet. See bot comment."
+ )
+
+ if content.allSatisfied {
+ state = "success"
+ description = "All requirements are satisfied."
+ }
+
+ // Update or create commit status.
+ if _, _, err := gh.Client.Repositories.CreateStatus(
+ gh.Ctx,
+ gh.Owner,
+ gh.Repo,
+ pr.GetHead().GetSHA(),
+ &github.RepoStatus{
+ Context: &context,
+ State: &state,
+ TargetURL: &targetURL,
+ Description: &description,
+ }); err != nil {
+ return fmt.Errorf("unable to create status on PR %d: %w", pr.GetNumber(), err)
+ } else {
+ gh.Logger.Infof("Commit status successfully updated on PR %d", pr.GetNumber())
+ }
+
+ return nil
+}
diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl
new file mode 100644
index 00000000000..ebd07fdd4b9
--- /dev/null
+++ b/contribs/github-bot/comment.tmpl
@@ -0,0 +1,51 @@
+# Merge Requirements
+
+The following requirements must be fulfilled before a pull request can be merged.
+Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member.
+
+These requirements are defined in this [configuration file](https://github.com/GnoCheckBot/demo/blob/main/config.go).
+
+## Automated Checks
+
+{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }}
+{{ end }}
+
+{{ if .AutoRules }}Details
+{{ range .AutoRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails | stripLinks }}
+```
+### Then
+```
+{{ .RequirementDetails | stripLinks }}
+```
+
+{{ end }}
+
+{{ else }}*No automated checks match this pull request.*{{ end }}
+
+## Manual Checks
+
+{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }}
+{{ end }}
+
+{{ if .ManualRules }}Details
+{{ range .ManualRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails }}
+```
+### Can be checked by
+{{range $item := .Teams }} - team {{ $item | stripLinks }}
+{{ else }}
+- Any user with comment edit permission
+{{end}}
+
+{{ end }}
+
+{{ else }}*No manual checks match this pull request.*{{ end }}
diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go
new file mode 100644
index 00000000000..fd8790dd9e1
--- /dev/null
+++ b/contribs/github-bot/comment_test.go
@@ -0,0 +1,164 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/google/go-github/v64/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/sethvargo/go-githubactions"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGeneratedComment(t *testing.T) {
+ t.Parallel()
+
+ autoCheckSuccessLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Success))
+ autoCheckFailLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Fail))
+
+ content := CommentContent{}
+ autoRules := []AutoContent{
+ {Description: "Test automatic 1", Satisfied: false},
+ {Description: "Test automatic 2", Satisfied: false},
+ {Description: "Test automatic 3", Satisfied: true},
+ {Description: "Test automatic 4", Satisfied: true},
+ {Description: "Test automatic 5", Satisfied: false},
+ }
+ manualRules := []ManualContent{
+ {Description: "Test manual 1", CheckedBy: "user_1"},
+ {Description: "Test manual 2", CheckedBy: ""},
+ {Description: "Test manual 3", CheckedBy: ""},
+ {Description: "Test manual 4", CheckedBy: "user_4"},
+ {Description: "Test manual 5", CheckedBy: "user_5"},
+ }
+
+ commentText, err := generateComment(content)
+ assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err))
+ assert.True(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should contains automated check placeholder")
+ assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder")
+
+ content.AutoRules = autoRules
+ commentText, err = generateComment(content)
+ assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err))
+ assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder")
+ assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder")
+ assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check")
+ assert.Equal(t, 3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check")
+
+ content.ManualRules = manualRules
+ commentText, err = generateComment(content)
+ assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err))
+ assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder")
+ assert.False(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should not contains manual check placeholder")
+
+ manualChecks := getCommentManualChecks(commentText)
+ assert.Equal(t, len(manualChecks), len(manualRules), "wrong number of manual checks found")
+ for _, rule := range manualRules {
+ val, ok := manualChecks[rule.Description]
+ assert.True(t, ok, "manual check should exist")
+ if rule.CheckedBy == "" {
+ assert.Equal(t, " ", val.status, "manual rule should not be checked")
+ } else {
+ assert.Equal(t, "x", val.status, "manual rule should be checked")
+ }
+ assert.Equal(t, rule.CheckedBy, val.checkedBy, "invalid username found for CheckedBy")
+ }
+}
+
+func setValue(t *testing.T, m map[string]any, value any, keys ...string) map[string]any {
+ t.Helper()
+
+ if len(keys) > 1 {
+ currMap, ok := m[keys[0]].(map[string]any)
+ if !ok {
+ currMap = map[string]any{}
+ }
+ m[keys[0]] = setValue(t, currMap, value, keys[1:]...)
+ } else if len(keys) == 1 {
+ m[keys[0]] = value
+ }
+
+ return m
+}
+
+func TestCommentUpdateHandler(t *testing.T) {
+ t.Parallel()
+
+ const (
+ user = "user"
+ bot = "bot"
+ )
+ actionCtx := &githubactions.GitHubContext{
+ Event: make(map[string]any),
+ }
+
+ mockOptions := []mock.MockBackendOption{}
+ newGHClient := func() *client.GitHub {
+ return &client.GitHub{
+ Client: github.NewClient(mock.NewMockedHTTPClient(mockOptions...)),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+ }
+ gh := newGHClient()
+
+ // Exit without error because EventName is empty
+ assert.NoError(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.EventName = utils.EventIssueComment
+
+ // Exit with error because Event.action is not set
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event["action"] = ""
+
+ // Exit without error because Event.action is set but not 'deleted'
+ assert.NoError(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event["action"] = "deleted"
+
+ // Exit with error because mock not setup to return authUser
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ mockOptions = append(mockOptions, mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/user",
+ Method: "GET",
+ },
+ github.User{Login: github.String(bot)},
+ ))
+ gh = newGHClient()
+ actionCtx.Actor = bot
+
+ // Exit with error because authUser and action actor is the same user
+ assert.ErrorIs(t, handleCommentUpdate(gh, actionCtx), errTriggeredByBot)
+ actionCtx.Actor = user
+
+ // Exit with error because Event.comment.user.login is not set
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, user, "comment", "user", "login")
+
+ // Exit without error because comment author is not the bot
+ assert.NoError(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, bot, "comment", "user", "login")
+
+ // Exit with error because Event.comment.body is not set
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "comment", "body")
+
+ // Exit with error because Event.changes.body.from is not set
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, "updated_body", "changes", "body", "from")
+
+ // Exit with error because Event.issue.number is not set
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number")
+
+ // Exit with error because checkboxes are differents
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from")
+
+ assert.Nil(t, handleCommentUpdate(gh, actionCtx))
+}
diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go
new file mode 100644
index 00000000000..4504844e289
--- /dev/null
+++ b/contribs/github-bot/config.go
@@ -0,0 +1,100 @@
+package main
+
+import (
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ c "github.com/gnolang/gno/contribs/github-bot/internal/conditions"
+ r "github.com/gnolang/gno/contribs/github-bot/internal/requirements"
+)
+
+// Automatic check that will be performed by the bot.
+type automaticCheck struct {
+ description string
+ ifC c.Condition // If the condition is met, the rule is displayed and the requirement is executed.
+ thenR r.Requirement // If the requirement is satisfied, the check passes.
+}
+
+// Manual check that will be performed by users.
+type manualCheck struct {
+ description string
+ ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment.
+ teams []string // Members of these teams can check the checkbox to make the check pass.
+}
+
+// This function returns the configuration of the bot consisting of automatic and manual checks
+// in which the GitHub client is injected.
+func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
+ auto := []automaticCheck{
+ {
+ description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams",
+ ifC: c.And(
+ c.FileChanged(gh, "tm2"),
+ c.BaseBranch("master"),
+ ),
+ thenR: r.And(
+ r.Or(
+ r.ReviewByTeamMembers(gh, "eu", 1),
+ r.AuthorInTeam(gh, "eu"),
+ ),
+ r.Or(
+ r.ReviewByTeamMembers(gh, "us", 1),
+ r.AuthorInTeam(gh, "us"),
+ ),
+ ),
+ },
+ {
+ description: "A maintainer must be able to edit this pull request",
+ ifC: c.Always(),
+ thenR: r.MaintainerCanModify(),
+ },
+ {
+ description: "The pull request head branch must be up-to-date with its base",
+ ifC: c.Always(), // Or only if c.BaseBranch("main") ?
+ thenR: r.UpToDateWith(gh, r.PR_BASE),
+ },
+ }
+
+ manual := []manualCheck{
+ {
+ description: "Determine if infra needs to be updated",
+ ifC: c.And(
+ c.BaseBranch("master"),
+ c.Or(
+ c.FileChanged(gh, "misc/deployments"),
+ c.FileChanged(gh, `misc/docker-\.*`),
+ c.FileChanged(gh, "tm2/pkg/p2p"),
+ ),
+ ),
+ teams: []string{"tech-staff"},
+ },
+ {
+ description: "Ensure the code style is satisfactory",
+ ifC: c.And(
+ c.BaseBranch("master"),
+ c.Or(
+ c.FileChanged(gh, `.*\.go`),
+ c.FileChanged(gh, `.*\.js`),
+ ),
+ ),
+ teams: []string{"tech-staff"},
+ },
+ {
+ description: "Ensure the documentation is accurate and relevant",
+ ifC: c.FileChanged(gh, `.*\.md`),
+ teams: []string{
+ "tech-staff",
+ "devrels",
+ },
+ },
+ }
+
+ // Check for duplicates in manual rule descriptions (needs to be unique for the bot operations).
+ unique := make(map[string]struct{})
+ for _, rule := range manual {
+ if _, exists := unique[rule.description]; exists {
+ gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.description)
+ }
+ unique[rule.description] = struct{}{}
+ }
+
+ return auto, manual
+}
diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod
new file mode 100644
index 00000000000..8df55e3f282
--- /dev/null
+++ b/contribs/github-bot/go.mod
@@ -0,0 +1,28 @@
+module github.com/gnolang/gno/contribs/github-bot
+
+go 1.22
+
+toolchain go1.22.2
+
+replace github.com/gnolang/gno => ../..
+
+require (
+ github.com/gnolang/gno v0.0.0-00010101000000-000000000000
+ github.com/google/go-github/v64 v64.0.0
+ github.com/migueleliasweb/go-github-mock v1.0.1
+ github.com/sethvargo/go-githubactions v1.3.0
+ github.com/stretchr/testify v1.9.0
+ github.com/xlab/treeprint v1.2.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/gorilla/mux v1.8.1 // indirect
+ github.com/peterbourgon/ff/v3 v3.4.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ golang.org/x/term v0.23.0 // indirect
+ golang.org/x/time v0.3.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum
new file mode 100644
index 00000000000..2dae4e83e72
--- /dev/null
+++ b/contribs/github-bot/go.sum
@@ -0,0 +1,38 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg=
+github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/migueleliasweb/go-github-mock v1.0.1 h1:amLEECVny28RCD1ElALUpQxrAimamznkg9rN2O7t934=
+github.com/migueleliasweb/go-github-mock v1.0.1/go.mod h1:8PJ7MpMoIiCBBNpuNmvndHm0QicjsE+hjex1yMGmjYQ=
+github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
+github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A=
+github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
+github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go
new file mode 100644
index 00000000000..229c3e90631
--- /dev/null
+++ b/contribs/github-bot/internal/client/client.go
@@ -0,0 +1,293 @@
+package client
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ p "github.com/gnolang/gno/contribs/github-bot/internal/params"
+
+ "github.com/google/go-github/v64/github"
+)
+
+// PageSize is the number of items to load for each iteration when fetching a list.
+const PageSize = 100
+
+var ErrBotCommentNotFound = errors.New("bot comment not found")
+
+// GitHub contains everything necessary to interact with the GitHub API,
+// including the client, a context (which must be passed with each request),
+// a logger, etc. This object will be passed to each condition or requirement
+// that requires fetching additional information or modifying things on GitHub.
+// The object also provides methods for performing more complex operations than
+// a simple API call.
+type GitHub struct {
+ Client *github.Client
+ Ctx context.Context
+ DryRun bool
+ Logger logger.Logger
+ Owner string
+ Repo string
+}
+
+// GetBotComment retrieves the bot's (current user) comment on provided PR number.
+func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
+ // List existing comments
+ const (
+ sort = "created"
+ direction = "desc"
+ )
+
+ // Get current user (bot)
+ currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "")
+ if err != nil {
+ return nil, fmt.Errorf("unable to get current user: %w", err)
+ }
+
+ // Pagination option
+ opts := &github.IssueListCommentsOptions{
+ Sort: github.String(sort),
+ Direction: github.String(direction),
+ ListOptions: github.ListOptions{
+ PerPage: PageSize,
+ },
+ }
+
+ for {
+ comments, response, err := gh.Client.Issues.ListComments(
+ gh.Ctx,
+ gh.Owner,
+ gh.Repo,
+ prNum,
+ opts,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err)
+ }
+
+ // Get the comment created by current user
+ for _, comment := range comments {
+ if comment.GetUser().GetLogin() == currentUser.GetLogin() {
+ return comment, nil
+ }
+ }
+
+ if response.NextPage == 0 {
+ break
+ }
+ opts.Page = response.NextPage
+ }
+
+ return nil, errors.New("bot comment not found")
+}
+
+// SetBotComment creates a bot's comment on the provided PR number
+// or updates it if it already exists.
+func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) {
+ // Create bot comment if it does not already exist
+ comment, err := gh.GetBotComment(prNum)
+ if errors.Is(err, ErrBotCommentNotFound) {
+ newComment, _, err := gh.Client.Issues.CreateComment(
+ gh.Ctx,
+ gh.Owner,
+ gh.Repo,
+ prNum,
+ &github.IssueComment{Body: &body},
+ )
+ if err != nil {
+ return nil, fmt.Errorf("unable to create bot comment for PR %d: %w", prNum, err)
+ }
+ return newComment, nil
+ } else if err != nil {
+ return nil, fmt.Errorf("unable to get bot comment: %w", err)
+ }
+
+ comment.Body = &body
+ editComment, _, err := gh.Client.Issues.EditComment(
+ gh.Ctx,
+ gh.Owner,
+ gh.Repo,
+ comment.GetID(),
+ comment,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("unable to edit bot comment with ID %d: %w", comment.GetID(), err)
+ }
+
+ return editComment, nil
+}
+
+// ListTeamMembers lists the members of the specified team.
+func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) {
+ var (
+ allMembers []*github.User
+ opts = &github.TeamListTeamMembersOptions{
+ ListOptions: github.ListOptions{
+ PerPage: PageSize,
+ },
+ }
+ )
+
+ for {
+ members, response, err := gh.Client.Teams.ListTeamMembersBySlug(
+ gh.Ctx,
+ gh.Owner,
+ team,
+ opts,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("unable to list members for team %s: %w", team, err)
+ }
+
+ allMembers = append(allMembers, members...)
+
+ if response.NextPage == 0 {
+ break
+ }
+ opts.Page = response.NextPage
+ }
+
+ return allMembers, nil
+}
+
+// IsUserInTeams checks if the specified user is a member of any of the
+// provided teams.
+func (gh *GitHub) IsUserInTeams(user string, teams []string) bool {
+ for _, team := range teams {
+ teamMembers, err := gh.ListTeamMembers(team)
+ if err != nil {
+ gh.Logger.Errorf("unable to check if user %s in team %s", user, team)
+ continue
+ }
+
+ for _, member := range teamMembers {
+ if member.GetLogin() == user {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// ListPRReviewers returns the list of reviewers for the specified PR number.
+func (gh *GitHub) ListPRReviewers(prNum int) (*github.Reviewers, error) {
+ var (
+ allReviewers = &github.Reviewers{}
+ opts = &github.ListOptions{
+ PerPage: PageSize,
+ }
+ )
+
+ for {
+ reviewers, response, err := gh.Client.PullRequests.ListReviewers(
+ gh.Ctx,
+ gh.Owner,
+ gh.Repo,
+ prNum,
+ opts,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("unable to list reviewers for PR %d: %w", prNum, err)
+ }
+
+ allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...)
+ allReviewers.Users = append(allReviewers.Users, reviewers.Users...)
+
+ if response.NextPage == 0 {
+ break
+ }
+ opts.Page = response.NextPage
+ }
+
+ return allReviewers, nil
+}
+
+// ListPRReviewers returns the list of reviews for the specified PR number.
+func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) {
+ var (
+ allReviews []*github.PullRequestReview
+ opts = &github.ListOptions{
+ PerPage: PageSize,
+ }
+ )
+
+ for {
+ reviews, response, err := gh.Client.PullRequests.ListReviews(
+ gh.Ctx,
+ gh.Owner,
+ gh.Repo,
+ prNum,
+ opts,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("unable to list reviews for PR %d: %w", prNum, err)
+ }
+
+ allReviews = append(allReviews, reviews...)
+
+ if response.NextPage == 0 {
+ break
+ }
+ opts.Page = response.NextPage
+ }
+
+ return allReviews, nil
+}
+
+// ListPR returns the list of pull requests in the specified state.
+func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) {
+ var prs []*github.PullRequest
+
+ opts := &github.PullRequestListOptions{
+ State: state,
+ Sort: "updated",
+ Direction: "desc",
+ ListOptions: github.ListOptions{
+ PerPage: PageSize,
+ },
+ }
+
+ for {
+ prsPage, response, err := gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts)
+ if err != nil {
+ return nil, fmt.Errorf("unable to list pull requests with state %s: %w", state, err)
+ }
+
+ prs = append(prs, prsPage...)
+
+ if response.NextPage == 0 {
+ break
+ }
+ opts.Page = response.NextPage
+ }
+
+ return prs, nil
+}
+
+// New initializes the API client, the logger, and creates an instance of GitHub.
+func New(ctx context.Context, params *p.Params) (*GitHub, error) {
+ gh := &GitHub{
+ Ctx: ctx,
+ Owner: params.Owner,
+ Repo: params.Repo,
+ DryRun: params.DryRun,
+ }
+
+ // Detect if the current process was launched by a GitHub Action and return
+ // a logger suitable for terminal output or the GitHub Actions web interface
+ gh.Logger = logger.NewLogger(params.Verbose)
+
+ // Retrieve GitHub API token from env
+ token, set := os.LookupEnv("GITHUB_TOKEN")
+ if !set {
+ return nil, errors.New("GITHUB_TOKEN is not set in env")
+ }
+
+ // Init GitHub API client using token
+ gh.Client = github.NewClient(nil).WithAuthToken(token)
+
+ return gh, nil
+}
diff --git a/contribs/github-bot/internal/conditions/assignee.go b/contribs/github-bot/internal/conditions/assignee.go
new file mode 100644
index 00000000000..7024259909c
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/assignee.go
@@ -0,0 +1,66 @@
+package conditions
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Assignee Condition.
+type assignee struct {
+ user string
+}
+
+var _ Condition = &assignee{}
+
+func (a *assignee) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("A pull request assignee is user: %s", a.user)
+
+ for _, assignee := range pr.Assignees {
+ if a.user == assignee.GetLogin() {
+ return utils.AddStatusNode(true, detail, details)
+ }
+ }
+
+ return utils.AddStatusNode(false, detail, details)
+}
+
+func Assignee(user string) Condition {
+ return &assignee{user: user}
+}
+
+// AssigneeInTeam Condition.
+type assigneeInTeam struct {
+ gh *client.GitHub
+ team string
+}
+
+var _ Condition = &assigneeInTeam{}
+
+func (a *assigneeInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("A pull request assignee is a member of the team: %s", a.team)
+
+ teamMembers, err := a.gh.ListTeamMembers(a.team)
+ if err != nil {
+ a.gh.Logger.Errorf("unable to check if assignee is in team %s: %v", a.team, err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ for _, member := range teamMembers {
+ for _, assignee := range pr.Assignees {
+ if member.GetLogin() == assignee.GetLogin() {
+ return utils.AddStatusNode(true, fmt.Sprintf("%s (member: %s)", detail, member.GetLogin()), details)
+ }
+ }
+ }
+
+ return utils.AddStatusNode(false, detail, details)
+}
+
+func AssigneeInTeam(gh *client.GitHub, team string) Condition {
+ return &assigneeInTeam{gh: gh, team: team}
+}
diff --git a/contribs/github-bot/internal/conditions/assignee_test.go b/contribs/github-bot/internal/conditions/assignee_test.go
new file mode 100644
index 00000000000..9207e4604b7
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/assignee_test.go
@@ -0,0 +1,100 @@
+package conditions
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestAssignee(t *testing.T) {
+ t.Parallel()
+
+ assignees := []*github.User{
+ {Login: github.String("notTheRightOne")},
+ {Login: github.String("user")},
+ {Login: github.String("anotherOne")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ assignees []*github.User
+ isMet bool
+ }{
+ {"empty assignee list", "user", []*github.User{}, false},
+ {"assignee list contains user", "user", assignees, true},
+ {"assignee list doesn't contain user", "user2", assignees, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{Assignees: testCase.assignees}
+ details := treeprint.New()
+ condition := Assignee(testCase.user)
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
+
+func TestAssigneeInTeam(t *testing.T) {
+ t.Parallel()
+
+ members := []*github.User{
+ {Login: github.String("notTheRightOne")},
+ {Login: github.String("user")},
+ {Login: github.String("anotherOne")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ members []*github.User
+ isMet bool
+ }{
+ {"empty assignee list", "user", []*github.User{}, false},
+ {"assignee list contains user", "user", members, true},
+ {"assignee list doesn't contain user", "user2", members, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/orgs/teams/team/members",
+ Method: "GET",
+ },
+ testCase.members,
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ pr := &github.PullRequest{
+ Assignees: []*github.User{
+ {Login: github.String(testCase.user)},
+ },
+ }
+ details := treeprint.New()
+ condition := AssigneeInTeam(gh, "team")
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/conditions/author.go b/contribs/github-bot/internal/conditions/author.go
new file mode 100644
index 00000000000..9052f781bd5
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/author.go
@@ -0,0 +1,60 @@
+package conditions
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Author Condition.
+type author struct {
+ user string
+}
+
+var _ Condition = &author{}
+
+func (a *author) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(
+ a.user == pr.GetUser().GetLogin(),
+ fmt.Sprintf("Pull request author is user: %v", a.user),
+ details,
+ )
+}
+
+func Author(user string) Condition {
+ return &author{user: user}
+}
+
+// AuthorInTeam Condition.
+type authorInTeam struct {
+ gh *client.GitHub
+ team string
+}
+
+var _ Condition = &authorInTeam{}
+
+func (a *authorInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team)
+
+ teamMembers, err := a.gh.ListTeamMembers(a.team)
+ if err != nil {
+ a.gh.Logger.Errorf("unable to check if author is in team %s: %v", a.team, err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ for _, member := range teamMembers {
+ if member.GetLogin() == pr.GetUser().GetLogin() {
+ return utils.AddStatusNode(true, detail, details)
+ }
+ }
+
+ return utils.AddStatusNode(false, detail, details)
+}
+
+func AuthorInTeam(gh *client.GitHub, team string) Condition {
+ return &authorInTeam{gh: gh, team: team}
+}
diff --git a/contribs/github-bot/internal/conditions/author_test.go b/contribs/github-bot/internal/conditions/author_test.go
new file mode 100644
index 00000000000..c5836f1ea76
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/author_test.go
@@ -0,0 +1,93 @@
+package conditions
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/xlab/treeprint"
+)
+
+func TestAuthor(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ author string
+ isMet bool
+ }{
+ {"author match", "user", "user", true},
+ {"author doesn't match", "user", "author", false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{
+ User: &github.User{Login: github.String(testCase.author)},
+ }
+ details := treeprint.New()
+ condition := Author(testCase.user)
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
+
+func TestAuthorInTeam(t *testing.T) {
+ t.Parallel()
+
+ members := []*github.User{
+ {Login: github.String("notTheRightOne")},
+ {Login: github.String("user")},
+ {Login: github.String("anotherOne")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ members []*github.User
+ isMet bool
+ }{
+ {"empty member list", "user", []*github.User{}, false},
+ {"member list contains user", "user", members, true},
+ {"member list doesn't contain user", "user2", members, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/orgs/teams/team/members",
+ Method: "GET",
+ },
+ testCase.members,
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ pr := &github.PullRequest{
+ User: &github.User{Login: github.String(testCase.user)},
+ }
+ details := treeprint.New()
+ condition := AuthorInTeam(gh, "team")
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/conditions/boolean.go b/contribs/github-bot/internal/conditions/boolean.go
new file mode 100644
index 00000000000..2fa3a25f7ac
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/boolean.go
@@ -0,0 +1,98 @@
+package conditions
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// And Condition.
+type and struct {
+ conditions []Condition
+}
+
+var _ Condition = &and{}
+
+func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ met := utils.Success
+ branch := details.AddBranch("")
+
+ for _, condition := range a.conditions {
+ if !condition.IsMet(pr, branch) {
+ met = utils.Fail
+ // We don't break here because we need to call IsMet on all conditions
+ // to populate the details tree.
+ }
+ }
+
+ branch.SetValue(fmt.Sprintf("%s And", met))
+
+ return (met == utils.Success)
+}
+
+func And(conditions ...Condition) Condition {
+ if len(conditions) < 2 {
+ panic("You should pass at least 2 conditions to And()")
+ }
+
+ return &and{conditions}
+}
+
+// Or Condition.
+type or struct {
+ conditions []Condition
+}
+
+var _ Condition = &or{}
+
+func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ met := utils.Fail
+ branch := details.AddBranch("")
+
+ for _, condition := range o.conditions {
+ if condition.IsMet(pr, branch) {
+ met = utils.Success
+ // We don't break here because we need to call IsMet on all conditions
+ // to populate the details tree.
+ }
+ }
+
+ branch.SetValue(fmt.Sprintf("%s Or", met))
+
+ return (met == utils.Success)
+}
+
+func Or(conditions ...Condition) Condition {
+ if len(conditions) < 2 {
+ panic("You should pass at least 2 conditions to Or()")
+ }
+
+ return &or{conditions}
+}
+
+// Not Condition.
+type not struct {
+ cond Condition
+}
+
+var _ Condition = ¬{}
+
+func (n *not) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ met := n.cond.IsMet(pr, details)
+ node := details.FindLastNode()
+
+ if met {
+ node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string)))
+ } else {
+ node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string)))
+ }
+
+ return !met
+}
+
+func Not(cond Condition) Condition {
+ return ¬{cond}
+}
diff --git a/contribs/github-bot/internal/conditions/boolean_test.go b/contribs/github-bot/internal/conditions/boolean_test.go
new file mode 100644
index 00000000000..52f028cf2b4
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/boolean_test.go
@@ -0,0 +1,96 @@
+package conditions
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestAnd(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ conditions []Condition
+ isMet bool
+ }{
+ {"and is true", []Condition{Always(), Always()}, true},
+ {"and is false", []Condition{Always(), Always(), Never()}, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ condition := And(testCase.conditions...)
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
+
+func TestAndPanic(t *testing.T) {
+ t.Parallel()
+
+ assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided")
+}
+
+func TestOr(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ conditions []Condition
+ isMet bool
+ }{
+ {"or is true", []Condition{Never(), Always()}, true},
+ {"or is false", []Condition{Never(), Never(), Never()}, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ condition := Or(testCase.conditions...)
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
+
+func TestOrPanic(t *testing.T) {
+ t.Parallel()
+
+ assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided")
+}
+
+func TestNot(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ condition Condition
+ isMet bool
+ }{
+ {"not is true", Never(), true},
+ {"not is false", Always(), false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ condition := Not(testCase.condition)
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/conditions/branch.go b/contribs/github-bot/internal/conditions/branch.go
new file mode 100644
index 00000000000..6977d633d98
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/branch.go
@@ -0,0 +1,49 @@
+package conditions
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// BaseBranch Condition.
+type baseBranch struct {
+ pattern *regexp.Regexp
+}
+
+var _ Condition = &baseBranch{}
+
+func (b *baseBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(
+ b.pattern.MatchString(pr.GetBase().GetRef()),
+ fmt.Sprintf("The base branch matches this pattern: %s", b.pattern.String()),
+ details,
+ )
+}
+
+func BaseBranch(pattern string) Condition {
+ return &baseBranch{pattern: regexp.MustCompile(pattern)}
+}
+
+// HeadBranch Condition.
+type headBranch struct {
+ pattern *regexp.Regexp
+}
+
+var _ Condition = &headBranch{}
+
+func (h *headBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(
+ h.pattern.MatchString(pr.GetHead().GetRef()),
+ fmt.Sprintf("The head branch matches this pattern: %s", h.pattern.String()),
+ details,
+ )
+}
+
+func HeadBranch(pattern string) Condition {
+ return &headBranch{pattern: regexp.MustCompile(pattern)}
+}
diff --git a/contribs/github-bot/internal/conditions/branch_test.go b/contribs/github-bot/internal/conditions/branch_test.go
new file mode 100644
index 00000000000..3e53ef2db1c
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/branch_test.go
@@ -0,0 +1,49 @@
+package conditions
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestHeadBaseBranch(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ pattern string
+ base string
+ isMet bool
+ }{
+ {"perfectly match", "base", "base", true},
+ {"prefix match", "^dev/", "dev/test-bot", true},
+ {"prefix doesn't match", "dev/$", "dev/test-bot", false},
+ {"suffix match", "/test-bot$", "dev/test-bot", true},
+ {"suffix doesn't match", "^/test-bot", "dev/test-bot", false},
+ {"doesn't match", "base", "notatall", false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{
+ Base: &github.PullRequestBranch{Ref: github.String(testCase.base)},
+ Head: &github.PullRequestBranch{Ref: github.String(testCase.base)},
+ }
+ conditions := []Condition{
+ BaseBranch(testCase.pattern),
+ HeadBranch(testCase.pattern),
+ }
+
+ for _, condition := range conditions {
+ details := treeprint.New()
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ }
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/conditions/condition.go b/contribs/github-bot/internal/conditions/condition.go
new file mode 100644
index 00000000000..8c2fa5a2948
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/condition.go
@@ -0,0 +1,12 @@
+package conditions
+
+import (
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+type Condition interface {
+ // Check if the Condition is met and add the details
+ // to the tree passed as a parameter.
+ IsMet(pr *github.PullRequest, details treeprint.Tree) bool
+}
diff --git a/contribs/github-bot/internal/conditions/constant.go b/contribs/github-bot/internal/conditions/constant.go
new file mode 100644
index 00000000000..26bbe9e8110
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/constant.go
@@ -0,0 +1,34 @@
+package conditions
+
+import (
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Always Condition.
+type always struct{}
+
+var _ Condition = &always{}
+
+func (*always) IsMet(_ *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(true, "On every pull request", details)
+}
+
+func Always() Condition {
+ return &always{}
+}
+
+// Never Condition.
+type never struct{}
+
+var _ Condition = &never{}
+
+func (*never) IsMet(_ *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(false, "On no pull request", details)
+}
+
+func Never() Condition {
+ return &never{}
+}
diff --git a/contribs/github-bot/internal/conditions/constant_test.go b/contribs/github-bot/internal/conditions/constant_test.go
new file mode 100644
index 00000000000..92bbe9b318a
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/constant_test.go
@@ -0,0 +1,25 @@
+package conditions
+
+import (
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+ "github.com/xlab/treeprint"
+)
+
+func TestAlways(t *testing.T) {
+ t.Parallel()
+
+ details := treeprint.New()
+ assert.True(t, Always().IsMet(nil, details), "condition should have a met status: true")
+ assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true")
+}
+
+func TestNever(t *testing.T) {
+ t.Parallel()
+
+ details := treeprint.New()
+ assert.False(t, Never().IsMet(nil, details), "condition should have a met status: false")
+ assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false")
+}
diff --git a/contribs/github-bot/internal/conditions/file.go b/contribs/github-bot/internal/conditions/file.go
new file mode 100644
index 00000000000..e3854a7734a
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/file.go
@@ -0,0 +1,58 @@
+package conditions
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// FileChanged Condition.
+type fileChanged struct {
+ gh *client.GitHub
+ pattern *regexp.Regexp
+}
+
+var _ Condition = &fileChanged{}
+
+func (fc *fileChanged) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("A changed file matches this pattern: %s", fc.pattern.String())
+ opts := &github.ListOptions{
+ PerPage: client.PageSize,
+ }
+
+ for {
+ files, response, err := fc.gh.Client.PullRequests.ListFiles(
+ fc.gh.Ctx,
+ fc.gh.Owner,
+ fc.gh.Repo,
+ pr.GetNumber(),
+ opts,
+ )
+ if err != nil {
+ fc.gh.Logger.Errorf("Unable to list changed files for PR %d: %v", pr.GetNumber(), err)
+ break
+ }
+
+ for _, file := range files {
+ if fc.pattern.MatchString(file.GetFilename()) {
+ return utils.AddStatusNode(true, fmt.Sprintf("%s (filename: %s)", detail, file.GetFilename()), details)
+ }
+ }
+
+ if response.NextPage == 0 {
+ break
+ }
+ opts.Page = response.NextPage
+ }
+
+ return utils.AddStatusNode(false, detail, details)
+}
+
+func FileChanged(gh *client.GitHub, pattern string) Condition {
+ return &fileChanged{gh: gh, pattern: regexp.MustCompile(pattern)}
+}
diff --git a/contribs/github-bot/internal/conditions/file_test.go b/contribs/github-bot/internal/conditions/file_test.go
new file mode 100644
index 00000000000..3fd7a33fa4a
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/file_test.go
@@ -0,0 +1,68 @@
+package conditions
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestFileChanged(t *testing.T) {
+ t.Parallel()
+
+ filenames := []*github.CommitFile{
+ {Filename: github.String("foo")},
+ {Filename: github.String("bar")},
+ {Filename: github.String("baz")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ pattern string
+ files []*github.CommitFile
+ isMet bool
+ }{
+ {"empty file list", "foo", []*github.CommitFile{}, false},
+ {"file list contains exact match", "foo", filenames, true},
+ {"file list contains prefix match", "^fo", filenames, true},
+ {"file list contains prefix doesn't match", "fo$", filenames, false},
+ {"file list contains suffix match", "oo$", filenames, true},
+ {"file list contains suffix doesn't match", "^oo", filenames, false},
+ {"file list doesn't contains match", "foobar", filenames, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/0/files",
+ Method: "GET",
+ },
+ testCase.files,
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ condition := FileChanged(gh, testCase.pattern)
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/conditions/label.go b/contribs/github-bot/internal/conditions/label.go
new file mode 100644
index 00000000000..ace94ed436c
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/label.go
@@ -0,0 +1,34 @@
+package conditions
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Label Condition.
+type label struct {
+ pattern *regexp.Regexp
+}
+
+var _ Condition = &label{}
+
+func (l *label) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("A label matches this pattern: %s", l.pattern.String())
+
+ for _, label := range pr.Labels {
+ if l.pattern.MatchString(label.GetName()) {
+ return utils.AddStatusNode(true, fmt.Sprintf("%s (label: %s)", detail, label.GetName()), details)
+ }
+ }
+
+ return utils.AddStatusNode(false, detail, details)
+}
+
+func Label(pattern string) Condition {
+ return &label{pattern: regexp.MustCompile(pattern)}
+}
diff --git a/contribs/github-bot/internal/conditions/label_test.go b/contribs/github-bot/internal/conditions/label_test.go
new file mode 100644
index 00000000000..ea895b28ad1
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/label_test.go
@@ -0,0 +1,48 @@
+package conditions
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestLabel(t *testing.T) {
+ t.Parallel()
+
+ labels := []*github.Label{
+ {Name: github.String("notTheRightOne")},
+ {Name: github.String("label")},
+ {Name: github.String("anotherOne")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ pattern string
+ labels []*github.Label
+ isMet bool
+ }{
+ {"empty label list", "label", []*github.Label{}, false},
+ {"label list contains exact match", "label", labels, true},
+ {"label list contains prefix match", "^lab", labels, true},
+ {"label list contains prefix doesn't match", "lab$", labels, false},
+ {"label list contains suffix match", "bel$", labels, true},
+ {"label list contains suffix doesn't match", "^bel", labels, false},
+ {"label list doesn't contains match", "baleb", labels, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{Labels: testCase.labels}
+ details := treeprint.New()
+ condition := Label(testCase.pattern)
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/logger/action.go b/contribs/github-bot/internal/logger/action.go
new file mode 100644
index 00000000000..c6d10429e62
--- /dev/null
+++ b/contribs/github-bot/internal/logger/action.go
@@ -0,0 +1,43 @@
+package logger
+
+import (
+ "github.com/sethvargo/go-githubactions"
+)
+
+type actionLogger struct{}
+
+var _ Logger = &actionLogger{}
+
+// Debugf implements Logger.
+func (a *actionLogger) Debugf(msg string, args ...any) {
+ githubactions.Debugf(msg, args...)
+}
+
+// Errorf implements Logger.
+func (a *actionLogger) Errorf(msg string, args ...any) {
+ githubactions.Errorf(msg, args...)
+}
+
+// Fatalf implements Logger.
+func (a *actionLogger) Fatalf(msg string, args ...any) {
+ githubactions.Fatalf(msg, args...)
+}
+
+// Infof implements Logger.
+func (a *actionLogger) Infof(msg string, args ...any) {
+ githubactions.Infof(msg, args...)
+}
+
+// Noticef implements Logger.
+func (a *actionLogger) Noticef(msg string, args ...any) {
+ githubactions.Noticef(msg, args...)
+}
+
+// Warningf implements Logger.
+func (a *actionLogger) Warningf(msg string, args ...any) {
+ githubactions.Warningf(msg, args...)
+}
+
+func newActionLogger() Logger {
+ return &actionLogger{}
+}
diff --git a/contribs/github-bot/internal/logger/logger.go b/contribs/github-bot/internal/logger/logger.go
new file mode 100644
index 00000000000..570ca027e5c
--- /dev/null
+++ b/contribs/github-bot/internal/logger/logger.go
@@ -0,0 +1,40 @@
+package logger
+
+import (
+ "os"
+)
+
+// All Logger methods follow the standard fmt.Printf convention.
+type Logger interface {
+ // Debugf prints a debug-level message.
+ Debugf(msg string, args ...any)
+
+ // Noticef prints a notice-level message.
+ Noticef(msg string, args ...any)
+
+ // Warningf prints a warning-level message.
+ Warningf(msg string, args ...any)
+
+ // Errorf prints a error-level message.
+ Errorf(msg string, args ...any)
+
+ // Fatalf prints a error-level message and exits.
+ Fatalf(msg string, args ...any)
+
+ // Infof prints message to stdout without any level annotations.
+ Infof(msg string, args ...any)
+}
+
+// Returns a logger suitable for Github Actions or terminal output.
+func NewLogger(verbose bool) Logger {
+ if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction {
+ return newActionLogger()
+ }
+
+ return newTermLogger(verbose)
+}
+
+// NewNoopLogger returns a logger that does not log anything.
+func NewNoopLogger() Logger {
+ return newNoopLogger()
+}
diff --git a/contribs/github-bot/internal/logger/noop.go b/contribs/github-bot/internal/logger/noop.go
new file mode 100644
index 00000000000..629ed9d52d9
--- /dev/null
+++ b/contribs/github-bot/internal/logger/noop.go
@@ -0,0 +1,27 @@
+package logger
+
+type noopLogger struct{}
+
+var _ Logger = &noopLogger{}
+
+// Debugf implements Logger.
+func (*noopLogger) Debugf(_ string, _ ...any) {}
+
+// Errorf implements Logger.
+func (*noopLogger) Errorf(_ string, _ ...any) {}
+
+// Fatalf implements Logger.
+func (*noopLogger) Fatalf(_ string, _ ...any) {}
+
+// Infof implements Logger.
+func (*noopLogger) Infof(_ string, _ ...any) {}
+
+// Noticef implements Logger.
+func (*noopLogger) Noticef(_ string, _ ...any) {}
+
+// Warningf implements Logger.
+func (*noopLogger) Warningf(_ string, _ ...any) {}
+
+func newNoopLogger() Logger {
+ return &noopLogger{}
+}
diff --git a/contribs/github-bot/internal/logger/terminal.go b/contribs/github-bot/internal/logger/terminal.go
new file mode 100644
index 00000000000..d0e5671a3c8
--- /dev/null
+++ b/contribs/github-bot/internal/logger/terminal.go
@@ -0,0 +1,55 @@
+package logger
+
+import (
+ "fmt"
+ "log/slog"
+ "os"
+)
+
+type termLogger struct{}
+
+var _ Logger = &termLogger{}
+
+// Debugf implements Logger.
+func (s *termLogger) Debugf(msg string, args ...any) {
+ msg = fmt.Sprintf("%s\n", msg)
+ slog.Debug(fmt.Sprintf(msg, args...))
+}
+
+// Errorf implements Logger.
+func (s *termLogger) Errorf(msg string, args ...any) {
+ msg = fmt.Sprintf("%s\n", msg)
+ slog.Error(fmt.Sprintf(msg, args...))
+}
+
+// Fatalf implements Logger.
+func (s *termLogger) Fatalf(msg string, args ...any) {
+ s.Errorf(msg, args...)
+ os.Exit(1)
+}
+
+// Infof implements Logger.
+func (s *termLogger) Infof(msg string, args ...any) {
+ msg = fmt.Sprintf("%s\n", msg)
+ slog.Info(fmt.Sprintf(msg, args...))
+}
+
+// Noticef implements Logger.
+func (s *termLogger) Noticef(msg string, args ...any) {
+ // Alias to info on terminal since notice level only exists on GitHub Actions.
+ s.Infof(msg, args...)
+}
+
+// Warningf implements Logger.
+func (s *termLogger) Warningf(msg string, args ...any) {
+ msg = fmt.Sprintf("%s\n", msg)
+ slog.Warn(fmt.Sprintf(msg, args...))
+}
+
+func newTermLogger(verbose bool) Logger {
+ if verbose {
+ slog.SetLogLoggerLevel(slog.LevelDebug)
+ }
+
+ return &termLogger{}
+}
diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/params/params.go
new file mode 100644
index 00000000000..c11d1b62419
--- /dev/null
+++ b/contribs/github-bot/internal/params/params.go
@@ -0,0 +1,118 @@
+package params
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/sethvargo/go-githubactions"
+)
+
+type Params struct {
+ Owner string
+ Repo string
+ PRAll bool
+ PRNums PRList
+ Verbose bool
+ DryRun bool
+ Timeout time.Duration
+ flagSet *flag.FlagSet
+}
+
+func (p *Params) RegisterFlags(fs *flag.FlagSet) {
+ fs.StringVar(
+ &p.Owner,
+ "owner",
+ "",
+ "owner of the repo to process, if empty, will be retrieved from GitHub Actions context",
+ )
+
+ fs.StringVar(
+ &p.Repo,
+ "repo",
+ "",
+ "repo to process, if empty, will be retrieved from GitHub Actions context",
+ )
+
+ fs.BoolVar(
+ &p.PRAll,
+ "pr-all",
+ false,
+ "process all opened pull requests",
+ )
+
+ fs.TextVar(
+ &p.PRNums,
+ "pr-numbers",
+ PRList(nil),
+ "pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context",
+ )
+
+ fs.BoolVar(
+ &p.Verbose,
+ "verbose",
+ false,
+ "set logging level to debug",
+ )
+
+ fs.BoolVar(
+ &p.DryRun,
+ "dry-run",
+ false,
+ "print if pull request requirements are satisfied without updating anything on GitHub",
+ )
+
+ fs.DurationVar(
+ &p.Timeout,
+ "timeout",
+ 0,
+ "timeout after which the bot execution is interrupted",
+ )
+
+ p.flagSet = fs
+}
+
+func (p *Params) ValidateFlags() {
+ // Helper to display an error + usage message before exiting.
+ errorUsage := func(err string) {
+ fmt.Fprintf(p.flagSet.Output(), "Error: %s\n\n", err)
+ p.flagSet.Usage()
+ os.Exit(1)
+ }
+
+ // Check if flags are coherent.
+ if p.PRAll && len(p.PRNums) != 0 {
+ errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags.")
+ }
+
+ // If one of these values is empty, it must be retrieved
+ // from GitHub Actions context.
+ if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) {
+ actionCtx, err := githubactions.Context()
+ if err != nil {
+ errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err))
+ }
+
+ if p.Owner == "" {
+ if p.Owner, _ = actionCtx.Repo(); p.Owner == "" {
+ errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag.")
+ }
+ }
+ if p.Repo == "" {
+ if _, p.Repo = actionCtx.Repo(); p.Repo == "" {
+ errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag.")
+ }
+ }
+
+ if len(p.PRNums) == 0 && !p.PRAll {
+ prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
+ if err != nil {
+ errorUsage(fmt.Sprintf("Unable to retrieve pull request number from GitHub Actions context: %s\nYou may want to set it using -pr-numbers flag.", err.Error()))
+ }
+
+ p.PRNums = PRList{prNum}
+ }
+ }
+}
diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/params/prlist.go
new file mode 100644
index 00000000000..51aed8dc457
--- /dev/null
+++ b/contribs/github-bot/internal/params/prlist.go
@@ -0,0 +1,49 @@
+package params
+
+import (
+ "encoding"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type PRList []int
+
+// PRList is both a TextMarshaler and a TextUnmarshaler.
+var (
+ _ encoding.TextMarshaler = PRList{}
+ _ encoding.TextUnmarshaler = &PRList{}
+)
+
+// MarshalText implements encoding.TextMarshaler.
+func (p PRList) MarshalText() (text []byte, err error) {
+ prNumsStr := make([]string, len(p))
+
+ for i, prNum := range p {
+ prNumsStr[i] = strconv.Itoa(prNum)
+ }
+
+ return []byte(strings.Join(prNumsStr, ",")), nil
+}
+
+// UnmarshalText implements encoding.TextUnmarshaler.
+func (p *PRList) UnmarshalText(text []byte) error {
+ prNumsStr := strings.Split(string(text), ",")
+ prNums := make([]int, len(prNumsStr))
+
+ for i := range prNumsStr {
+ prNum, err := strconv.Atoi(strings.TrimSpace(prNumsStr[i]))
+ if err != nil {
+ return err
+ }
+
+ if prNum <= 0 {
+ return fmt.Errorf("invalid pull request number (<= 0): original(%s) parsed(%d)", prNumsStr[i], prNum)
+ }
+
+ prNums[i] = prNum
+ }
+ *p = prNums
+
+ return nil
+}
diff --git a/contribs/github-bot/internal/requirements/assignee.go b/contribs/github-bot/internal/requirements/assignee.go
new file mode 100644
index 00000000000..9a2723ad18f
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/assignee.go
@@ -0,0 +1,53 @@
+package requirements
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Assignee Requirement.
+type assignee struct {
+ gh *client.GitHub
+ user string
+}
+
+var _ Requirement = &assignee{}
+
+func (a *assignee) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("This user is assigned to pull request: %s", a.user)
+
+ // Check if user was already assigned to PR.
+ for _, assignee := range pr.Assignees {
+ if a.user == assignee.GetLogin() {
+ return utils.AddStatusNode(true, detail, details)
+ }
+ }
+
+ // If in a dry run, skip assigning the user.
+ if a.gh.DryRun {
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ // If user not already assigned, assign it.
+ if _, _, err := a.gh.Client.Issues.AddAssignees(
+ a.gh.Ctx,
+ a.gh.Owner,
+ a.gh.Repo,
+ pr.GetNumber(),
+ []string{a.user},
+ ); err != nil {
+ a.gh.Logger.Errorf("Unable to assign user %s to PR %d: %v", a.user, pr.GetNumber(), err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ return utils.AddStatusNode(true, detail, details)
+}
+
+func Assignee(gh *client.GitHub, user string) Requirement {
+ return &assignee{gh: gh, user: user}
+}
diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go
new file mode 100644
index 00000000000..df6ffdf0cd3
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/assignee_test.go
@@ -0,0 +1,72 @@
+package requirements
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestAssignee(t *testing.T) {
+ t.Parallel()
+
+ assignees := []*github.User{
+ {Login: github.String("notTheRightOne")},
+ {Login: github.String("user")},
+ {Login: github.String("anotherOne")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ assignees []*github.User
+ dryRun bool
+ exists bool
+ }{
+ {"empty assignee list", "user", []*github.User{}, false, false},
+ {"empty assignee list with dry-run", "user", []*github.User{}, true, false},
+ {"assignee list contains user", "user", assignees, false, true},
+ {"assignee list doesn't contain user", "user2", assignees, false, false},
+ {"assignee list doesn't contain user with dry-run", "user2", assignees, true, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ requested := false
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/repos/issues/0/assignees",
+ Method: "GET", // It looks like this mock package doesn't support mocking POST requests
+ },
+ http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
+ requested = true
+ }),
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ DryRun: testCase.dryRun,
+ }
+
+ pr := &github.PullRequest{Assignees: testCase.assignees}
+ details := treeprint.New()
+ requirement := Assignee(gh, testCase.user)
+
+ assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true")
+ assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true")
+ assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item")
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/requirements/author.go b/contribs/github-bot/internal/requirements/author.go
new file mode 100644
index 00000000000..eed2c510b97
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/author.go
@@ -0,0 +1,39 @@
+package requirements
+
+import (
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/conditions"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Author Requirement.
+type author struct {
+ c conditions.Condition // Alias Author requirement to identical condition.
+}
+
+var _ Requirement = &author{}
+
+func (a *author) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ return a.c.IsMet(pr, details)
+}
+
+func Author(user string) Requirement {
+ return &author{conditions.Author(user)}
+}
+
+// AuthorInTeam Requirement.
+type authorInTeam struct {
+ c conditions.Condition // Alias AuthorInTeam requirement to identical condition.
+}
+
+var _ Requirement = &authorInTeam{}
+
+func (a *authorInTeam) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ return a.c.IsMet(pr, details)
+}
+
+func AuthorInTeam(gh *client.GitHub, team string) Requirement {
+ return &authorInTeam{conditions.AuthorInTeam(gh, team)}
+}
diff --git a/contribs/github-bot/internal/requirements/author_test.go b/contribs/github-bot/internal/requirements/author_test.go
new file mode 100644
index 00000000000..768ca44f24e
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/author_test.go
@@ -0,0 +1,93 @@
+package requirements
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/xlab/treeprint"
+)
+
+func TestAuthor(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ author string
+ isSatisfied bool
+ }{
+ {"author match", "user", "user", true},
+ {"author doesn't match", "user", "author", false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{
+ User: &github.User{Login: github.String(testCase.author)},
+ }
+ details := treeprint.New()
+ requirement := Author(testCase.user)
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ })
+ }
+}
+
+func TestAuthorInTeam(t *testing.T) {
+ t.Parallel()
+
+ members := []*github.User{
+ {Login: github.String("notTheRightOne")},
+ {Login: github.String("user")},
+ {Login: github.String("anotherOne")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ members []*github.User
+ isSatisfied bool
+ }{
+ {"empty member list", "user", []*github.User{}, false},
+ {"member list contains user", "user", members, true},
+ {"member list doesn't contain user", "user2", members, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/orgs/teams/team/members",
+ Method: "GET",
+ },
+ testCase.members,
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ pr := &github.PullRequest{
+ User: &github.User{Login: github.String(testCase.user)},
+ }
+ details := treeprint.New()
+ requirement := AuthorInTeam(gh, "team")
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/requirements/boolean.go b/contribs/github-bot/internal/requirements/boolean.go
new file mode 100644
index 00000000000..6b441c92f80
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/boolean.go
@@ -0,0 +1,98 @@
+package requirements
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// And Requirement.
+type and struct {
+ requirements []Requirement
+}
+
+var _ Requirement = &and{}
+
+func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ satisfied := utils.Success
+ branch := details.AddBranch("")
+
+ for _, requirement := range a.requirements {
+ if !requirement.IsSatisfied(pr, branch) {
+ satisfied = utils.Fail
+ // We don't break here because we need to call IsSatisfied on all
+ // requirements to populate the details tree.
+ }
+ }
+
+ branch.SetValue(fmt.Sprintf("%s And", satisfied))
+
+ return (satisfied == utils.Success)
+}
+
+func And(requirements ...Requirement) Requirement {
+ if len(requirements) < 2 {
+ panic("You should pass at least 2 requirements to And()")
+ }
+
+ return &and{requirements}
+}
+
+// Or Requirement.
+type or struct {
+ requirements []Requirement
+}
+
+var _ Requirement = &or{}
+
+func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ satisfied := utils.Fail
+ branch := details.AddBranch("")
+
+ for _, requirement := range o.requirements {
+ if requirement.IsSatisfied(pr, branch) {
+ satisfied = utils.Success
+ // We don't break here because we need to call IsSatisfied on all
+ // requirements to populate the details tree.
+ }
+ }
+
+ branch.SetValue(fmt.Sprintf("%s Or", satisfied))
+
+ return (satisfied == utils.Success)
+}
+
+func Or(requirements ...Requirement) Requirement {
+ if len(requirements) < 2 {
+ panic("You should pass at least 2 requirements to Or()")
+ }
+
+ return &or{requirements}
+}
+
+// Not Requirement.
+type not struct {
+ req Requirement
+}
+
+var _ Requirement = ¬{}
+
+func (n *not) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ satisfied := n.req.IsSatisfied(pr, details)
+ node := details.FindLastNode()
+
+ if satisfied {
+ node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string)))
+ } else {
+ node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string)))
+ }
+
+ return !satisfied
+}
+
+func Not(req Requirement) Requirement {
+ return ¬{req}
+}
diff --git a/contribs/github-bot/internal/requirements/boolean_test.go b/contribs/github-bot/internal/requirements/boolean_test.go
new file mode 100644
index 00000000000..0043a44985c
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/boolean_test.go
@@ -0,0 +1,96 @@
+package requirements
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestAnd(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ requirements []Requirement
+ isSatisfied bool
+ }{
+ {"and is true", []Requirement{Always(), Always()}, true},
+ {"and is false", []Requirement{Always(), Always(), Never()}, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ requirement := And(testCase.requirements...)
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ })
+ }
+}
+
+func TestAndPanic(t *testing.T) {
+ t.Parallel()
+
+ assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided")
+}
+
+func TestOr(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ requirements []Requirement
+ isSatisfied bool
+ }{
+ {"or is true", []Requirement{Never(), Always()}, true},
+ {"or is false", []Requirement{Never(), Never(), Never()}, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ requirement := Or(testCase.requirements...)
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ })
+ }
+}
+
+func TestOrPanic(t *testing.T) {
+ t.Parallel()
+
+ assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided")
+}
+
+func TestNot(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ requirement Requirement
+ isSatisfied bool
+ }{
+ {"not is true", Never(), true},
+ {"not is false", Always(), false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ requirement := Not(testCase.requirement)
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go
new file mode 100644
index 00000000000..65d00d06ae8
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/branch.go
@@ -0,0 +1,53 @@
+package requirements
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Pass this to UpToDateWith constructor to check the PR head branch
+// against its base branch.
+const PR_BASE = "PR_BASE"
+
+// UpToDateWith Requirement.
+type upToDateWith struct {
+ gh *client.GitHub
+ base string
+}
+
+var _ Requirement = &author{}
+
+func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ base := u.base
+ if u.base == PR_BASE {
+ base = pr.GetBase().GetRef()
+ }
+ head := pr.GetHead().GetRef()
+
+ cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil)
+ if err != nil {
+ u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s): %v", head, base, err)
+ return false
+ }
+
+ return utils.AddStatusNode(
+ cmp.GetBehindBy() == 0,
+ fmt.Sprintf(
+ "Head branch (%s) is up to date with base (%s): behind by %d / ahead by %d",
+ head,
+ base,
+ cmp.GetBehindBy(),
+ cmp.GetAheadBy(),
+ ),
+ details,
+ )
+}
+
+func UpToDateWith(gh *client.GitHub, base string) Requirement {
+ return &upToDateWith{gh, base}
+}
diff --git a/contribs/github-bot/internal/requirements/branch_test.go b/contribs/github-bot/internal/requirements/branch_test.go
new file mode 100644
index 00000000000..54387beb605
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/branch_test.go
@@ -0,0 +1,62 @@
+package requirements
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/xlab/treeprint"
+)
+
+func TestUpToDateWith(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ behind int
+ ahead int
+ isSatisfied bool
+ }{
+ {"up-to-date without commit ahead", 0, 0, true},
+ {"up-to-date with commits ahead", 0, 3, true},
+ {"not up-to-date with commits behind", 3, 0, false},
+ {"not up-to-date with commits behind and ahead", 3, 3, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/repos/compare/base...",
+ Method: "GET",
+ },
+ github.CommitsComparison{
+ AheadBy: &testCase.ahead,
+ BehindBy: &testCase.behind,
+ },
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ requirement := UpToDateWith(gh, "base")
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/requirements/constant.go b/contribs/github-bot/internal/requirements/constant.go
new file mode 100644
index 00000000000..cbe932da830
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/constant.go
@@ -0,0 +1,34 @@
+package requirements
+
+import (
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Always Requirement.
+type always struct{}
+
+var _ Requirement = &always{}
+
+func (*always) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(true, "On every pull request", details)
+}
+
+func Always() Requirement {
+ return &always{}
+}
+
+// Never Requirement.
+type never struct{}
+
+var _ Requirement = &never{}
+
+func (*never) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(false, "On no pull request", details)
+}
+
+func Never() Requirement {
+ return &never{}
+}
diff --git a/contribs/github-bot/internal/requirements/constant_test.go b/contribs/github-bot/internal/requirements/constant_test.go
new file mode 100644
index 00000000000..b04addcb672
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/constant_test.go
@@ -0,0 +1,25 @@
+package requirements
+
+import (
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+ "github.com/xlab/treeprint"
+)
+
+func TestAlways(t *testing.T) {
+ t.Parallel()
+
+ details := treeprint.New()
+ assert.True(t, Always().IsSatisfied(nil, details), "requirement should have a satisfied status: true")
+ assert.True(t, utils.TestLastNodeStatus(t, true, details), "requirement details should have a status: true")
+}
+
+func TestNever(t *testing.T) {
+ t.Parallel()
+
+ details := treeprint.New()
+ assert.False(t, Never().IsSatisfied(nil, details), "requirement should have a satisfied status: false")
+ assert.True(t, utils.TestLastNodeStatus(t, false, details), "requirement details should have a status: false")
+}
diff --git a/contribs/github-bot/internal/requirements/label.go b/contribs/github-bot/internal/requirements/label.go
new file mode 100644
index 00000000000..d1ee475db92
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/label.go
@@ -0,0 +1,53 @@
+package requirements
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Label Requirement.
+type label struct {
+ gh *client.GitHub
+ name string
+}
+
+var _ Requirement = &label{}
+
+func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("This label is applied to pull request: %s", l.name)
+
+ // Check if label was already applied to PR.
+ for _, label := range pr.Labels {
+ if l.name == label.GetName() {
+ return utils.AddStatusNode(true, detail, details)
+ }
+ }
+
+ // If in a dry run, skip applying the label.
+ if l.gh.DryRun {
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ // If label not already applied, apply it.
+ if _, _, err := l.gh.Client.Issues.AddLabelsToIssue(
+ l.gh.Ctx,
+ l.gh.Owner,
+ l.gh.Repo,
+ pr.GetNumber(),
+ []string{l.name},
+ ); err != nil {
+ l.gh.Logger.Errorf("Unable to add label %s to PR %d: %v", l.name, pr.GetNumber(), err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ return utils.AddStatusNode(true, detail, details)
+}
+
+func Label(gh *client.GitHub, name string) Requirement {
+ return &label{gh, name}
+}
diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go
new file mode 100644
index 00000000000..6fbe8ff7f25
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/label_test.go
@@ -0,0 +1,79 @@
+package requirements
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+func TestLabel(t *testing.T) {
+ t.Parallel()
+
+ labels := []*github.Label{
+ {Name: github.String("notTheRightOne")},
+ {Name: github.String("label")},
+ {Name: github.String("anotherOne")},
+ }
+
+ for _, testCase := range []struct {
+ name string
+ pattern string
+ labels []*github.Label
+ dryRun bool
+ exists bool
+ }{
+ {"empty label list", "label", []*github.Label{}, false, false},
+ {"empty label list with dry-run", "label", []*github.Label{}, true, false},
+ {"label list contains exact match", "label", labels, false, true},
+ {"label list contains prefix match", "^lab", labels, false, true},
+ {"label list contains prefix doesn't match", "lab$", labels, false, false},
+ {"label list contains prefix doesn't match with dry-run", "lab$", labels, true, false},
+ {"label list contains suffix match", "bel$", labels, false, true},
+ {"label list contains suffix match with dry-run", "bel$", labels, true, true},
+ {"label list contains suffix doesn't match", "^bel", labels, false, false},
+ {"label list contains suffix doesn't match with dry-run", "^bel", labels, true, false},
+ {"label list doesn't contains match", "baleb", labels, false, false},
+ {"label list doesn't contains match with dry-run", "baleb", labels, true, false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ requested := false
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/repos/issues/0/labels",
+ Method: "GET", // It looks like this mock package doesn't support mocking POST requests
+ },
+ http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
+ requested = true
+ }),
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ DryRun: testCase.dryRun,
+ }
+
+ pr := &github.PullRequest{Labels: testCase.labels}
+ details := treeprint.New()
+ requirement := Label(gh, testCase.pattern)
+
+ assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true")
+ assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true")
+ assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item")
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/requirements/maintainer.go b/contribs/github-bot/internal/requirements/maintainer.go
new file mode 100644
index 00000000000..8e3f356bebf
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/maintainer.go
@@ -0,0 +1,25 @@
+package requirements
+
+import (
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// MaintainerCanModify Requirement.
+type maintainerCanModify struct{}
+
+var _ Requirement = &maintainerCanModify{}
+
+func (a *maintainerCanModify) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(
+ pr.GetMaintainerCanModify(),
+ "Maintainer can modify this pull request",
+ details,
+ )
+}
+
+func MaintainerCanModify() Requirement {
+ return &maintainerCanModify{}
+}
diff --git a/contribs/github-bot/internal/requirements/maintener_test.go b/contribs/github-bot/internal/requirements/maintener_test.go
new file mode 100644
index 00000000000..5b71803b468
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/maintener_test.go
@@ -0,0 +1,34 @@
+package requirements
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/google/go-github/v64/github"
+ "github.com/stretchr/testify/assert"
+ "github.com/xlab/treeprint"
+)
+
+func TestMaintenerCanModify(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ isSatisfied bool
+ }{
+ {"modify is true", true},
+ {"modify is false", false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{MaintainerCanModify: &testCase.isSatisfied}
+ details := treeprint.New()
+ requirement := MaintainerCanModify()
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/requirements/requirement.go b/contribs/github-bot/internal/requirements/requirement.go
new file mode 100644
index 00000000000..296c4a1461d
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/requirement.go
@@ -0,0 +1,12 @@
+package requirements
+
+import (
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+type Requirement interface {
+ // Check if the Requirement is satisfied and add the detail
+ // to the tree passed as a parameter.
+ IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool
+}
diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go
new file mode 100644
index 00000000000..aa3914d4c4a
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/reviewer.go
@@ -0,0 +1,156 @@
+package requirements
+
+import (
+ "fmt"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Reviewer Requirement.
+type reviewByUser struct {
+ gh *client.GitHub
+ user string
+}
+
+var _ Requirement = &reviewByUser{}
+
+func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("This user approved pull request: %s", r.user)
+
+ // If not a dry run, make the user a reviewer if he's not already.
+ if !r.gh.DryRun {
+ requested := false
+ reviewers, err := r.gh.ListPRReviewers(pr.GetNumber())
+ if err != nil {
+ r.gh.Logger.Errorf("unable to check if user %s review is already requested: %v", r.user, err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ for _, user := range reviewers.Users {
+ if user.GetLogin() == r.user {
+ requested = true
+ break
+ }
+ }
+
+ if requested {
+ r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber())
+ } else {
+ r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber())
+ if _, _, err := r.gh.Client.PullRequests.RequestReviewers(
+ r.gh.Ctx,
+ r.gh.Owner,
+ r.gh.Repo,
+ pr.GetNumber(),
+ github.ReviewersRequest{
+ Reviewers: []string{r.user},
+ },
+ ); err != nil {
+ r.gh.Logger.Errorf("Unable to request review from user %s on PR %d: %v", r.user, pr.GetNumber(), err)
+ }
+ }
+ }
+
+ // Check if user already approved this PR.
+ reviews, err := r.gh.ListPRReviews(pr.GetNumber())
+ if err != nil {
+ r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ for _, review := range reviews {
+ if review.GetUser().GetLogin() == r.user {
+ r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState())
+ return utils.AddStatusNode(review.GetState() == "APPROVED", detail, details)
+ }
+ }
+ r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber())
+
+ return utils.AddStatusNode(false, detail, details)
+}
+
+func ReviewByUser(gh *client.GitHub, user string) Requirement {
+ return &reviewByUser{gh, user}
+}
+
+// Reviewer Requirement.
+type reviewByTeamMembers struct {
+ gh *client.GitHub
+ team string
+ count uint
+}
+
+var _ Requirement = &reviewByTeamMembers{}
+
+func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool {
+ detail := fmt.Sprintf("At least %d user(s) of the team %s approved pull request", r.count, r.team)
+
+ // If not a dry run, make the user a reviewer if he's not already.
+ if !r.gh.DryRun {
+ requested := false
+ reviewers, err := r.gh.ListPRReviewers(pr.GetNumber())
+ if err != nil {
+ r.gh.Logger.Errorf("unable to check if team %s review is already requested: %v", r.team, err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ for _, team := range reviewers.Teams {
+ if team.GetSlug() == r.team {
+ requested = true
+ break
+ }
+ }
+
+ if requested {
+ r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber())
+ } else {
+ r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber())
+ if _, _, err := r.gh.Client.PullRequests.RequestReviewers(
+ r.gh.Ctx,
+ r.gh.Owner,
+ r.gh.Repo,
+ pr.GetNumber(),
+ github.ReviewersRequest{
+ TeamReviewers: []string{r.team},
+ },
+ ); err != nil {
+ r.gh.Logger.Errorf("Unable to request review from team %s on PR %d: %v", r.team, pr.GetNumber(), err)
+ }
+ }
+ }
+
+ // Check how many members of this team already approved this PR.
+ approved := uint(0)
+ reviews, err := r.gh.ListPRReviews(pr.GetNumber())
+ if err != nil {
+ r.gh.Logger.Errorf("unable to check if a member of team %s already approved this PR: %v", r.team, err)
+ return utils.AddStatusNode(false, detail, details)
+ }
+
+ for _, review := range reviews {
+ teamMembers, err := r.gh.ListTeamMembers(r.team)
+ if err != nil {
+ r.gh.Logger.Errorf(err.Error())
+ continue
+ }
+
+ for _, member := range teamMembers {
+ if review.GetUser().GetLogin() == member.GetLogin() {
+ if review.GetState() == "APPROVED" {
+ approved += 1
+ }
+ r.gh.Logger.Debugf("Member %s from team %s already reviewed PR %d with state %s (%d/%d required approval(s))", member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), approved, r.count)
+ }
+ }
+ }
+
+ return utils.AddStatusNode(approved >= r.count, detail, details)
+}
+
+func ReviewByTeamMembers(gh *client.GitHub, team string, count uint) Requirement {
+ return &reviewByTeamMembers{gh, team, count}
+}
diff --git a/contribs/github-bot/internal/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go
new file mode 100644
index 00000000000..16c50e13743
--- /dev/null
+++ b/contribs/github-bot/internal/requirements/reviewer_test.go
@@ -0,0 +1,215 @@
+package requirements
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/xlab/treeprint"
+)
+
+func TestReviewByUser(t *testing.T) {
+ t.Parallel()
+
+ reviewers := github.Reviewers{
+ Users: []*github.User{
+ {Login: github.String("notTheRightOne")},
+ {Login: github.String("user")},
+ {Login: github.String("anotherOne")},
+ },
+ }
+
+ reviews := []*github.PullRequestReview{
+ {
+ User: &github.User{Login: github.String("notTheRightOne")},
+ State: github.String("APPROVED"),
+ }, {
+ User: &github.User{Login: github.String("user")},
+ State: github.String("APPROVED"),
+ }, {
+ User: &github.User{Login: github.String("anotherOne")},
+ State: github.String("REQUEST_CHANGES"),
+ },
+ }
+
+ for _, testCase := range []struct {
+ name string
+ user string
+ isSatisfied bool
+ create bool
+ }{
+ {"reviewer matches", "user", true, false},
+ {"reviewer matches without approval", "anotherOne", false, false},
+ {"reviewer doesn't match", "user2", false, true},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ firstRequest := true
+ requested := false
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/0/requested_reviewers",
+ Method: "GET",
+ },
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ if firstRequest {
+ w.Write(mock.MustMarshal(reviewers))
+ firstRequest = false
+ } else {
+ requested = true
+ }
+ }),
+ ),
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/0/reviews",
+ Method: "GET",
+ },
+ reviews,
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ requirement := ReviewByUser(gh, testCase.user)
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ assert.Equal(t, testCase.create, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.create))
+ })
+ }
+}
+
+func TestReviewByTeamMembers(t *testing.T) {
+ t.Parallel()
+
+ reviewers := github.Reviewers{
+ Teams: []*github.Team{
+ {Slug: github.String("team1")},
+ {Slug: github.String("team2")},
+ {Slug: github.String("team3")},
+ },
+ }
+
+ members := map[string][]*github.User{
+ "team1": {
+ {Login: github.String("user1")},
+ {Login: github.String("user2")},
+ {Login: github.String("user3")},
+ },
+ "team2": {
+ {Login: github.String("user3")},
+ {Login: github.String("user4")},
+ {Login: github.String("user5")},
+ },
+ "team3": {
+ {Login: github.String("user4")},
+ {Login: github.String("user5")},
+ },
+ }
+
+ reviews := []*github.PullRequestReview{
+ {
+ User: &github.User{Login: github.String("user1")},
+ State: github.String("APPROVED"),
+ }, {
+ User: &github.User{Login: github.String("user2")},
+ State: github.String("APPROVED"),
+ }, {
+ User: &github.User{Login: github.String("user3")},
+ State: github.String("APPROVED"),
+ }, {
+ User: &github.User{Login: github.String("user4")},
+ State: github.String("REQUEST_CHANGES"),
+ }, {
+ User: &github.User{Login: github.String("user5")},
+ State: github.String("REQUEST_CHANGES"),
+ },
+ }
+
+ for _, testCase := range []struct {
+ name string
+ team string
+ count uint
+ isSatisfied bool
+ testRequest bool
+ }{
+ {"3/3 team members approved;", "team1", 3, true, false},
+ {"1/1 team member approved", "team2", 1, true, false},
+ {"1/2 team member approved", "team2", 2, false, false},
+ {"0/1 team member approved", "team3", 1, false, false},
+ {"0/1 team member approved with request", "team3", 1, false, true},
+ {"team doesn't exist with request", "team4", 1, false, true},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ firstRequest := true
+ requested := false
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/0/requested_reviewers",
+ Method: "GET",
+ },
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ if firstRequest {
+ if testCase.testRequest {
+ w.Write(mock.MustMarshal(github.Reviewers{}))
+ } else {
+ w.Write(mock.MustMarshal(reviewers))
+ }
+ firstRequest = false
+ } else {
+ requested = true
+ }
+ }),
+ ),
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.team),
+ Method: "GET",
+ },
+ members[testCase.team],
+ ),
+ mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/0/reviews",
+ Method: "GET",
+ },
+ reviews,
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ pr := &github.PullRequest{}
+ details := treeprint.New()
+ requirement := ReviewByTeamMembers(gh, testCase.team, testCase.count)
+
+ assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
+ assert.Equal(t, testCase.testRequest, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.testRequest))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go
new file mode 100644
index 00000000000..91b8ac7e6b4
--- /dev/null
+++ b/contribs/github-bot/internal/utils/actions.go
@@ -0,0 +1,45 @@
+package utils
+
+import (
+ "fmt"
+
+ "github.com/sethvargo/go-githubactions"
+)
+
+// Recursively search for nested values using the keys provided.
+func IndexMap(m map[string]any, keys ...string) any {
+ if len(keys) == 0 {
+ return m
+ }
+
+ if val, ok := m[keys[0]]; ok {
+ if keys = keys[1:]; len(keys) == 0 {
+ return val
+ }
+ subMap, _ := val.(map[string]any)
+ return IndexMap(subMap, keys...)
+ }
+
+ return nil
+}
+
+// Retrieve PR number from GitHub Actions context
+func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) {
+ firstKey := ""
+
+ switch actionCtx.EventName {
+ case EventIssueComment:
+ firstKey = "issue"
+ case EventPullRequest, EventPullRequestTarget:
+ firstKey = "pull_request"
+ default:
+ return 0, fmt.Errorf("unsupported event: %s", actionCtx.EventName)
+ }
+
+ num, ok := IndexMap(actionCtx.Event, firstKey, "number").(float64)
+ if !ok || num <= 0 {
+ return 0, fmt.Errorf("invalid value: %d", int(num))
+ }
+
+ return int(num), nil
+}
diff --git a/contribs/github-bot/internal/utils/actions_test.go b/contribs/github-bot/internal/utils/actions_test.go
new file mode 100644
index 00000000000..3114bb8a061
--- /dev/null
+++ b/contribs/github-bot/internal/utils/actions_test.go
@@ -0,0 +1,43 @@
+package utils
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIndexMap(t *testing.T) {
+ t.Parallel()
+
+ m := map[string]any{
+ "Key1": map[string]any{
+ "Key2": map[string]any{
+ "Key3": 1,
+ },
+ },
+ }
+
+ test := IndexMap(m)
+ assert.NotNil(t, test, "should return m")
+ _, ok := test.(map[string]any)
+ assert.True(t, ok, "returned m should be a map")
+
+ test = IndexMap(m, "Key1")
+ assert.NotNil(t, test, "should return Key1 value")
+ _, ok = test.(map[string]any)
+ assert.True(t, ok, "Key1 value type should be a map")
+
+ test = IndexMap(m, "Key1", "Key2")
+ assert.NotNil(t, test, "should return Key2 value")
+ _, ok = test.(map[string]any)
+ assert.True(t, ok, "Key2 value type should be a map")
+
+ test = IndexMap(m, "Key1", "Key2", "Key3")
+ assert.NotNil(t, test, "should return Key3 value")
+ val, ok := test.(int)
+ assert.True(t, ok, "Key3 value type should be an int")
+ assert.Equal(t, 1, val, "Key3 value should be a 1")
+
+ test = IndexMap(m, "Key1", "Key2", "Key3", "Key4")
+ assert.Nil(t, test, "Key4 value should not exist")
+}
diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go
new file mode 100644
index 00000000000..564b7d3fb38
--- /dev/null
+++ b/contribs/github-bot/internal/utils/github_const.go
@@ -0,0 +1,14 @@
+package utils
+
+// GitHub const
+const (
+ // GitHub Actions Event Names
+ EventIssueComment = "issue_comment"
+ EventPullRequest = "pull_request"
+ EventPullRequestTarget = "pull_request_target"
+ EventWorkflowDispatch = "workflow_dispatch"
+
+ // Pull Request States
+ PRStateOpen = "open"
+ PRStateClosed = "closed"
+)
diff --git a/contribs/github-bot/internal/utils/testing.go b/contribs/github-bot/internal/utils/testing.go
new file mode 100644
index 00000000000..3c7f7bfef88
--- /dev/null
+++ b/contribs/github-bot/internal/utils/testing.go
@@ -0,0 +1,21 @@
+package utils
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/xlab/treeprint"
+)
+
+func TestLastNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool {
+ t.Helper()
+
+ detail := details.FindLastNode().(*treeprint.Node).Value.(string)
+ status := Fail
+
+ if success {
+ status = Success
+ }
+
+ return strings.HasPrefix(detail, string(status))
+}
diff --git a/contribs/github-bot/internal/utils/tree.go b/contribs/github-bot/internal/utils/tree.go
new file mode 100644
index 00000000000..c6ff57bcd99
--- /dev/null
+++ b/contribs/github-bot/internal/utils/tree.go
@@ -0,0 +1,24 @@
+package utils
+
+import (
+ "fmt"
+
+ "github.com/xlab/treeprint"
+)
+
+type Status string
+
+const (
+ Success Status = "🟢"
+ Fail Status = "🔴"
+)
+
+func AddStatusNode(b bool, desc string, details treeprint.Tree) bool {
+ if b {
+ details.AddNode(fmt.Sprintf("%s %s", Success, desc))
+ } else {
+ details.AddNode(fmt.Sprintf("%s %s", Fail, desc))
+ }
+
+ return b
+}
diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go
new file mode 100644
index 00000000000..9895f44dc70
--- /dev/null
+++ b/contribs/github-bot/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "context"
+ "os"
+
+ "github.com/gnolang/gno/tm2/pkg/commands"
+)
+
+func main() {
+ cmd := commands.NewCommand(
+ commands.Metadata{
+ ShortUsage: "github-bot [flags]",
+ LongHelp: "Bot that allows for advanced management of GitHub pull requests.",
+ },
+ commands.NewEmptyConfig(),
+ commands.HelpExec,
+ )
+
+ cmd.AddSubCommands(
+ newCheckCmd(),
+ newMatrixCmd(),
+ )
+
+ cmd.Execute(context.Background(), os.Args[1:])
+}
diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go
new file mode 100644
index 00000000000..2442a6d94d6
--- /dev/null
+++ b/contribs/github-bot/matrix.go
@@ -0,0 +1,111 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/params"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/gnolang/gno/tm2/pkg/commands"
+ "github.com/sethvargo/go-githubactions"
+)
+
+func newMatrixCmd() *commands.Command {
+ return commands.NewCommand(
+ commands.Metadata{
+ Name: "matrix",
+ ShortUsage: "github-bot matrix",
+ ShortHelp: "parses GitHub Actions event and defines matrix accordingly",
+ LongHelp: "This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
+ },
+ commands.NewEmptyConfig(),
+ func(_ context.Context, _ []string) error {
+ return execMatrix()
+ },
+ )
+}
+
+func execMatrix() error {
+ // Get GitHub Actions context to retrieve event.
+ actionCtx, err := githubactions.Context()
+ if err != nil {
+ return fmt.Errorf("unable to get GitHub Actions context: %w", err)
+ }
+
+ // Init Github client using only GitHub Actions context
+ owner, repo := actionCtx.Repo()
+ gh, err := client.New(context.Background(), ¶ms.Params{Owner: owner, Repo: repo})
+ if err != nil {
+ return fmt.Errorf("unable to init GitHub client: %w", err)
+ }
+
+ // Retrieve PR list from GitHub Actions event
+ prList, err := getPRListFromEvent(gh, actionCtx)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(prList)
+ return nil
+}
+
+func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (params.PRList, error) {
+ var prList params.PRList
+
+ switch actionCtx.EventName {
+ // Event triggered from GitHub Actions user interface
+ case utils.EventWorkflowDispatch:
+ // Get input entered by the user
+ rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string)
+ if !ok {
+ return nil, errors.New("unable to get workflow dispatch input")
+ }
+ input := strings.TrimSpace(rawInput)
+
+ // If all PR are requested, list them from GitHub API
+ if input == "all" {
+ prs, err := gh.ListPR(utils.PRStateOpen)
+ if err != nil {
+ return nil, fmt.Errorf("unable to list all PR: %w", err)
+ }
+
+ prList = make(params.PRList, len(prs))
+ for i := range prs {
+ prList[i] = prs[i].GetNumber()
+ }
+ } else {
+ // If a PR list is provided, parse it
+ if err := prList.UnmarshalText([]byte(input)); err != nil {
+ return nil, fmt.Errorf("invalid PR list provided as input: %w", err)
+ }
+
+ // Then check if all provided PR are opened
+ for _, prNum := range prList {
+ pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
+ } else if pr.GetState() != utils.PRStateOpen {
+ return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState())
+ }
+ }
+ }
+
+ // Event triggered by an issue / PR comment being created / edited / deleted
+ // or any update on a PR
+ case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget:
+ // For these events, retrieve the number of the associated PR from the context
+ prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err)
+ }
+ prList = params.PRList{prNum}
+
+ default:
+ return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName)
+ }
+
+ return prList, nil
+}
diff --git a/contribs/github-bot/matrix_test.go b/contribs/github-bot/matrix_test.go
new file mode 100644
index 00000000000..bce4ec1bd8f
--- /dev/null
+++ b/contribs/github-bot/matrix_test.go
@@ -0,0 +1,248 @@
+package main
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/logger"
+ "github.com/gnolang/gno/contribs/github-bot/internal/params"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/google/go-github/v64/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/sethvargo/go-githubactions"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestProcessEvent(t *testing.T) {
+ t.Parallel()
+
+ prs := []*github.PullRequest{
+ {Number: github.Int(1), State: github.String(utils.PRStateOpen)},
+ {Number: github.Int(2), State: github.String(utils.PRStateOpen)},
+ {Number: github.Int(3), State: github.String(utils.PRStateOpen)},
+ {Number: github.Int(4), State: github.String(utils.PRStateClosed)},
+ {Number: github.Int(5), State: github.String(utils.PRStateClosed)},
+ {Number: github.Int(6), State: github.String(utils.PRStateClosed)},
+ }
+ openPRs := prs[:3]
+
+ for _, testCase := range []struct {
+ name string
+ gaCtx *githubactions.GitHubContext
+ prs []*github.PullRequest
+ expectedPRList params.PRList
+ expectedError bool
+ }{
+ {
+ "valid issue_comment event",
+ &githubactions.GitHubContext{
+ EventName: utils.EventIssueComment,
+ Event: map[string]any{"issue": map[string]any{"number": 1.}},
+ },
+ prs,
+ params.PRList{1},
+ false,
+ }, {
+ "valid pull_request event",
+ &githubactions.GitHubContext{
+ EventName: utils.EventPullRequest,
+ Event: map[string]any{"pull_request": map[string]any{"number": 1.}},
+ },
+ prs,
+ params.PRList{1},
+ false,
+ }, {
+ "valid pull_request_target event",
+ &githubactions.GitHubContext{
+ EventName: utils.EventPullRequestTarget,
+ Event: map[string]any{"pull_request": map[string]any{"number": 1.}},
+ },
+ prs,
+ params.PRList{1},
+ false,
+ }, {
+ "invalid event (PR number not set)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventIssueComment,
+ Event: map[string]any{"issue": nil},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "invalid event name",
+ &githubactions.GitHubContext{
+ EventName: "invalid_event",
+ Event: map[string]any{"issue": map[string]any{"number": 1.}},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "valid workflow_dispatch all",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}},
+ },
+ openPRs,
+ params.PRList{1, 2, 3},
+ false,
+ }, {
+ "valid workflow_dispatch all (no prs)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}},
+ },
+ nil,
+ params.PRList{},
+ false,
+ }, {
+ "valid workflow_dispatch list",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}},
+ },
+ prs,
+ params.PRList{1, 2, 3},
+ false,
+ }, {
+ "valid workflow_dispatch list with spaces",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}},
+ },
+ prs,
+ params.PRList{1, 2, 3},
+ false,
+ }, {
+ "invalid workflow_dispatch list (1 closed)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "invalid workflow_dispatch list (1 doesn't exist)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "invalid workflow_dispatch list (all closed)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "invalid workflow_dispatch list (empty)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "invalid workflow_dispatch list (unset)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": ""},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "invalid workflow_dispatch list (not a number list)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ }, {
+ "invalid workflow_dispatch list (number list with invalid elem)",
+ &githubactions.GitHubContext{
+ EventName: utils.EventWorkflowDispatch,
+ Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}},
+ },
+ prs,
+ params.PRList(nil),
+ true,
+ },
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls",
+ Method: "GET",
+ },
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ if testCase.expectedPRList != nil {
+ w.Write(mock.MustMarshal(testCase.prs))
+ }
+ }),
+ ),
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/{number}",
+ Method: "GET",
+ },
+ http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ var (
+ err error
+ prNum int
+ parts = strings.Split(req.RequestURI, "/")
+ )
+
+ if len(parts) > 0 {
+ prNumStr := parts[len(parts)-1]
+ prNum, err = strconv.Atoi(prNumStr)
+ if err != nil {
+ panic(err) // Should never happen
+ }
+ }
+
+ for _, pr := range prs {
+ if pr.GetNumber() == prNum {
+ w.Write(mock.MustMarshal(pr))
+ return
+ }
+ }
+
+ w.Write(mock.MustMarshal(nil))
+ }),
+ ),
+ )
+
+ gh := &client.GitHub{
+ Client: github.NewClient(mockedHTTPClient),
+ Ctx: context.Background(),
+ Logger: logger.NewNoopLogger(),
+ }
+
+ prList, err := getPRListFromEvent(gh, testCase.gaCtx)
+ if testCase.expectedError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, testCase.expectedPRList, prList)
+ })
+ }
+}
From 4da7fdf2bf3643cadbeae8ef8490d0c56df9094d Mon Sep 17 00:00:00 2001
From: Morgan
Date: Wed, 27 Nov 2024 19:09:14 +0100
Subject: [PATCH 14/86] ci: in bot.yml, add checkout code and install go
(#3224)
Contributors' checklist...
- [ ] 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
---
.github/workflows/bot.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
index 975f39f29dc..5beac27c07e 100644
--- a/.github/workflows/bot.yml
+++ b/.github/workflows/bot.yml
@@ -42,6 +42,12 @@ jobs:
pr-numbers: ${{ steps.pr-numbers.outputs.pr-numbers }}
steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
- name: Generate matrix from event
id: pr-numbers
working-directory: contribs/github-bot
From 6433b86e7b10760fa647910c592d745c30f12b25 Mon Sep 17 00:00:00 2001
From: Kristov Atlas <7227529+kristovatlas@users.noreply.github.com>
Date: Wed, 27 Nov 2024 21:25:27 -0600
Subject: [PATCH 15/86] docs: Clarify security policy (#3225)
---
SECURITY.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 8380267dacf..409c3867e57 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,12 +1,10 @@
# Security Policy
-The gno.land community strives to contribute toward the security of our ecosystem through internal security practices, and by working with external security researchers from the community.
-
## Reporting a Vulnerability
-If you've identified a vulnerability, please report it through one of the following venues:
+If you've identified a vulnerability, please **DO NOT** open a new public issue. Instead, report it through one of the following venues:
* Submit an advisory through GitHub: https://github.com/gnolang/gno/security/advisories/new
-* Email security [at-symbol] tendermint [dot] com. If you are concerned about confidentiality e.g. because of a high-severity issue, you may email us for PGP or Signal contact details.
+* Email security [at-symbol] tendermint [dot] com. If you are concerned about confidentiality e.g. because of a high-severity issue, you may email us for PGP or Signal contact details. If you’ve found multiple vulnerabilities, please submit one per email.
* A security bug bounty platform for gno.land will be available Soonᵀᴹ. You will need to report via our bug bounty platform in order to be eligible for rewards.
We will respond within 3 business days to all received reports.
From 97b21590624feadea07ccfe4257b609010e4b4e9 Mon Sep 17 00:00:00 2001
From: Antoine Eddi <5222525+aeddi@users.noreply.github.com>
Date: Thu, 28 Nov 2024 17:28:02 +0100
Subject: [PATCH 16/86] ci: fixes bot workflow and comment update (#3229)
This PR should (normally) fix the issues with the bot on this repo.
In addition to the fixes, I also replaced the old config with a config
that has much simpler rules while we decide what to add later on, once
we've verified that everything is working properly.
Here is the current config, If nothing seems off to you, we can merge it
as it is then improve it incrementaly.
```go
auto := []automaticCheck{
{
description: "Maintainers must be able to edit this pull request",
ifC: c.Always(),
thenR: r.MaintainerCanModify(),
},
{
description: "The pull request head branch must be up-to-date with its base",
ifC: c.Always(),
thenR: r.UpToDateWith(gh, r.PR_BASE),
},
{
description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff",
ifC: c.FileChanged(gh, "^docs/"),
thenR: r.Or(
r.And(
r.AuthorInTeam(gh, "devrels"),
r.ReviewByTeamMembers(gh, "tech-staff", 1),
),
r.And(
r.AuthorInTeam(gh, "tech-staff"),
r.ReviewByTeamMembers(gh, "devrels", 1),
),
),
},
}
manual := []manualCheck{
{
description: "The pull request description provides enough details",
ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")),
teams: Teams{"core-contributors"},
},
{
description: "Determine if infra needs to be updated before merging",
ifC: c.And(
c.BaseBranch("master"),
c.Or(
c.FileChanged(gh, `Dockerfile`),
c.FileChanged(gh, `^misc/deployments`),
c.FileChanged(gh, `^misc/docker-`),
c.FileChanged(gh, `^.github/workflows/releaser.*\.yml$`),
c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`),
),
),
teams: Teams{"devops"},
},
}
```
Contributors' checklist...
- [ ] 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
---
.github/workflows/bot.yml | 12 ++--
contribs/github-bot/README.md | 9 +--
contribs/github-bot/comment.go | 4 +-
contribs/github-bot/config.go | 71 ++++++++-----------
contribs/github-bot/internal/client/client.go | 2 +-
.../internal/conditions/branch_test.go | 4 +-
.../github-bot/internal/conditions/draft.go | 21 ++++++
.../internal/conditions/draft_test.go | 34 +++++++++
.../internal/conditions/file_test.go | 4 +-
.../internal/conditions/label_test.go | 4 +-
contribs/github-bot/internal/params/prlist.go | 2 +-
.../internal/requirements/assignee_test.go | 6 +-
.../internal/requirements/branch.go | 5 ++
.../internal/requirements/label_test.go | 21 ++----
contribs/github-bot/matrix.go | 8 ++-
15 files changed, 126 insertions(+), 81 deletions(-)
create mode 100644 contribs/github-bot/internal/conditions/draft.go
create mode 100644 contribs/github-bot/internal/conditions/draft_test.go
diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
index 5beac27c07e..21950459ae8 100644
--- a/.github/workflows/bot.yml
+++ b/.github/workflows/bot.yml
@@ -44,16 +44,18 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+
- name: Install Go
uses: actions/setup-go@v5
with:
- go-version-file: go.mod
+ go-version-file: contribs/github-bot/go.mod
+
- name: Generate matrix from event
id: pr-numbers
working-directory: contribs/github-bot
env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: go run . matrix >> "$GITHUB_OUTPUT"
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: echo "pr-numbers=$(go run . matrix)" >> "$GITHUB_OUTPUT"
# This job processes each pull request in the matrix individually while ensuring
# that a same PR cannot be processed concurrently by mutliple runners
@@ -76,10 +78,10 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
- go-version-file: go.mod
+ go-version-file: contribs/github-bot/go.mod
- name: Run GitHub Bot
working-directory: contribs/github-bot
env:
GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }}
- run: go run . -pr-numbers '${{ matrix.pr-number }}' -verbose
+ run: go run . check -pr-numbers '${{ matrix.pr-number }}' -verbose
diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md
index e3cc12fe01a..78c9c3c01b8 100644
--- a/contribs/github-bot/README.md
+++ b/contribs/github-bot/README.md
@@ -27,14 +27,11 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token
## Usage
```bash
-> go install github.com/gnolang/gno/contribs/github-bot@latest
-// (go: downloading ...)
-
-> github-bot --help
+> github-bot check --help
USAGE
- github-bot [flags]
+ github-bot check [flags]
-This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.
+This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.
A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.
FLAGS
diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go
index 8bf4a158745..f6605ea8554 100644
--- a/contribs/github-bot/comment.go
+++ b/contribs/github-bot/comment.go
@@ -175,9 +175,9 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// If teams specified in rule, check if actor is a member of one of them.
if len(teams) > 0 {
- if gh.IsUserInTeams(actionCtx.Actor, teams) {
+ if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed
if !gh.DryRun {
- gh.SetBotComment(previous, int(prNum))
+ gh.SetBotComment(previous, int(prNum)) // Restore previous state
}
return errors.New("checkbox edited by a user not allowed to")
}
diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go
index 4504844e289..4a28565ef7f 100644
--- a/contribs/github-bot/config.go
+++ b/contribs/github-bot/config.go
@@ -6,6 +6,8 @@ import (
r "github.com/gnolang/gno/contribs/github-bot/internal/requirements"
)
+type Teams []string
+
// Automatic check that will be performed by the bot.
type automaticCheck struct {
description string
@@ -17,7 +19,7 @@ type automaticCheck struct {
type manualCheck struct {
description string
ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment.
- teams []string // Members of these teams can check the checkbox to make the check pass.
+ teams Teams // Members of these teams can check the checkbox to make the check pass.
}
// This function returns the configuration of the bot consisting of automatic and manual checks
@@ -25,65 +27,50 @@ type manualCheck struct {
func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
auto := []automaticCheck{
{
- description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams",
- ifC: c.And(
- c.FileChanged(gh, "tm2"),
- c.BaseBranch("master"),
- ),
- thenR: r.And(
- r.Or(
- r.ReviewByTeamMembers(gh, "eu", 1),
- r.AuthorInTeam(gh, "eu"),
- ),
- r.Or(
- r.ReviewByTeamMembers(gh, "us", 1),
- r.AuthorInTeam(gh, "us"),
- ),
- ),
- },
- {
- description: "A maintainer must be able to edit this pull request",
+ description: "Maintainers must be able to edit this pull request",
ifC: c.Always(),
thenR: r.MaintainerCanModify(),
},
{
description: "The pull request head branch must be up-to-date with its base",
- ifC: c.Always(), // Or only if c.BaseBranch("main") ?
+ ifC: c.Always(),
thenR: r.UpToDateWith(gh, r.PR_BASE),
},
+ {
+ description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff",
+ ifC: c.FileChanged(gh, "^docs/"),
+ thenR: r.Or(
+ r.And(
+ r.AuthorInTeam(gh, "devrels"),
+ r.ReviewByTeamMembers(gh, "tech-staff", 1),
+ ),
+ r.And(
+ r.AuthorInTeam(gh, "tech-staff"),
+ r.ReviewByTeamMembers(gh, "devrels", 1),
+ ),
+ ),
+ },
}
manual := []manualCheck{
{
- description: "Determine if infra needs to be updated",
- ifC: c.And(
- c.BaseBranch("master"),
- c.Or(
- c.FileChanged(gh, "misc/deployments"),
- c.FileChanged(gh, `misc/docker-\.*`),
- c.FileChanged(gh, "tm2/pkg/p2p"),
- ),
- ),
- teams: []string{"tech-staff"},
+ description: "The pull request description provides enough details",
+ ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")),
+ teams: Teams{"core-contributors"},
},
{
- description: "Ensure the code style is satisfactory",
+ description: "Determine if infra needs to be updated before merging",
ifC: c.And(
c.BaseBranch("master"),
c.Or(
- c.FileChanged(gh, `.*\.go`),
- c.FileChanged(gh, `.*\.js`),
+ c.FileChanged(gh, `Dockerfile`),
+ c.FileChanged(gh, `^misc/deployments`),
+ c.FileChanged(gh, `^misc/docker-`),
+ c.FileChanged(gh, `^.github/workflows/releaser.*\.yml$`),
+ c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`),
),
),
- teams: []string{"tech-staff"},
- },
- {
- description: "Ensure the documentation is accurate and relevant",
- ifC: c.FileChanged(gh, `.*\.md`),
- teams: []string{
- "tech-staff",
- "devrels",
- },
+ teams: Teams{"devops"},
},
}
diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go
index 229c3e90631..474146ad3da 100644
--- a/contribs/github-bot/internal/client/client.go
+++ b/contribs/github-bot/internal/client/client.go
@@ -80,7 +80,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
opts.Page = response.NextPage
}
- return nil, errors.New("bot comment not found")
+ return nil, ErrBotCommentNotFound
}
// SetBotComment creates a bot's comment on the provided PR number
diff --git a/contribs/github-bot/internal/conditions/branch_test.go b/contribs/github-bot/internal/conditions/branch_test.go
index 3e53ef2db1c..81ed96f8314 100644
--- a/contribs/github-bot/internal/conditions/branch_test.go
+++ b/contribs/github-bot/internal/conditions/branch_test.go
@@ -22,9 +22,9 @@ func TestHeadBaseBranch(t *testing.T) {
}{
{"perfectly match", "base", "base", true},
{"prefix match", "^dev/", "dev/test-bot", true},
- {"prefix doesn't match", "dev/$", "dev/test-bot", false},
+ {"prefix doesn't match", "^/test-bot", "dev/test-bot", false},
{"suffix match", "/test-bot$", "dev/test-bot", true},
- {"suffix doesn't match", "^/test-bot", "dev/test-bot", false},
+ {"suffix doesn't match", "dev/$", "dev/test-bot", false},
{"doesn't match", "base", "notatall", false},
} {
t.Run(testCase.name, func(t *testing.T) {
diff --git a/contribs/github-bot/internal/conditions/draft.go b/contribs/github-bot/internal/conditions/draft.go
new file mode 100644
index 00000000000..2c263f2ae75
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/draft.go
@@ -0,0 +1,21 @@
+package conditions
+
+import (
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+
+ "github.com/google/go-github/v64/github"
+ "github.com/xlab/treeprint"
+)
+
+// Draft Condition.
+type draft struct{}
+
+var _ Condition = &baseBranch{}
+
+func (*draft) IsMet(pr *github.PullRequest, details treeprint.Tree) bool {
+ return utils.AddStatusNode(pr.GetDraft(), "This pull request is a draft", details)
+}
+
+func Draft() Condition {
+ return &draft{}
+}
diff --git a/contribs/github-bot/internal/conditions/draft_test.go b/contribs/github-bot/internal/conditions/draft_test.go
new file mode 100644
index 00000000000..a31b4eaca4c
--- /dev/null
+++ b/contribs/github-bot/internal/conditions/draft_test.go
@@ -0,0 +1,34 @@
+package conditions
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/google/go-github/v64/github"
+ "github.com/stretchr/testify/assert"
+ "github.com/xlab/treeprint"
+)
+
+func TestDraft(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ isMet bool
+ }{
+ {"draft is true", true},
+ {"draft is false", false},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ pr := &github.PullRequest{Draft: &testCase.isMet}
+ details := treeprint.New()
+ condition := Draft()
+
+ assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet))
+ assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet))
+ })
+ }
+}
diff --git a/contribs/github-bot/internal/conditions/file_test.go b/contribs/github-bot/internal/conditions/file_test.go
index 3fd7a33fa4a..8571ffea7d0 100644
--- a/contribs/github-bot/internal/conditions/file_test.go
+++ b/contribs/github-bot/internal/conditions/file_test.go
@@ -33,9 +33,9 @@ func TestFileChanged(t *testing.T) {
{"empty file list", "foo", []*github.CommitFile{}, false},
{"file list contains exact match", "foo", filenames, true},
{"file list contains prefix match", "^fo", filenames, true},
- {"file list contains prefix doesn't match", "fo$", filenames, false},
+ {"file list contains prefix doesn't match", "^oo", filenames, false},
{"file list contains suffix match", "oo$", filenames, true},
- {"file list contains suffix doesn't match", "^oo", filenames, false},
+ {"file list contains suffix doesn't match", "fo$", filenames, false},
{"file list doesn't contains match", "foobar", filenames, false},
} {
t.Run(testCase.name, func(t *testing.T) {
diff --git a/contribs/github-bot/internal/conditions/label_test.go b/contribs/github-bot/internal/conditions/label_test.go
index ea895b28ad1..00a3a8e3457 100644
--- a/contribs/github-bot/internal/conditions/label_test.go
+++ b/contribs/github-bot/internal/conditions/label_test.go
@@ -29,9 +29,9 @@ func TestLabel(t *testing.T) {
{"empty label list", "label", []*github.Label{}, false},
{"label list contains exact match", "label", labels, true},
{"label list contains prefix match", "^lab", labels, true},
- {"label list contains prefix doesn't match", "lab$", labels, false},
+ {"label list contains prefix doesn't match", "^bel", labels, false},
{"label list contains suffix match", "bel$", labels, true},
- {"label list contains suffix doesn't match", "^bel", labels, false},
+ {"label list contains suffix doesn't match", "lab$", labels, false},
{"label list doesn't contains match", "baleb", labels, false},
} {
t.Run(testCase.name, func(t *testing.T) {
diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/params/prlist.go
index 51aed8dc457..ace7bcbe3b6 100644
--- a/contribs/github-bot/internal/params/prlist.go
+++ b/contribs/github-bot/internal/params/prlist.go
@@ -23,7 +23,7 @@ func (p PRList) MarshalText() (text []byte, err error) {
prNumsStr[i] = strconv.Itoa(prNum)
}
- return []byte(strings.Join(prNumsStr, ",")), nil
+ return []byte(strings.Join(prNumsStr, ", ")), nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go
index df6ffdf0cd3..d72e8ad2a19 100644
--- a/contribs/github-bot/internal/requirements/assignee_test.go
+++ b/contribs/github-bot/internal/requirements/assignee_test.go
@@ -64,9 +64,9 @@ func TestAssignee(t *testing.T) {
details := treeprint.New()
requirement := Assignee(gh, testCase.user)
- assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true")
- assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true")
- assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item")
+ assert.True(t, requirement.IsSatisfied(pr, details) || testCase.dryRun, "requirement should have a satisfied status: true")
+ assert.True(t, utils.TestLastNodeStatus(t, true, details) || testCase.dryRun, "requirement details should have a status: true")
+ assert.True(t, testCase.exists || requested || testCase.dryRun, "requirement should have requested to create item")
})
}
}
diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go
index 65d00d06ae8..b686a093015 100644
--- a/contribs/github-bot/internal/requirements/branch.go
+++ b/contribs/github-bot/internal/requirements/branch.go
@@ -27,7 +27,12 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre
if u.base == PR_BASE {
base = pr.GetBase().GetRef()
}
+
head := pr.GetHead().GetRef()
+ // If pull request is open from a fork, prepend head ref with fork owner login
+ if pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName() {
+ head = fmt.Sprintf("%s:%s", pr.GetHead().GetRepo().GetOwner().GetLogin(), pr.GetHead().GetRef())
+ }
cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil)
if err != nil {
diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go
index 6fbe8ff7f25..7e991b55756 100644
--- a/contribs/github-bot/internal/requirements/label_test.go
+++ b/contribs/github-bot/internal/requirements/label_test.go
@@ -32,17 +32,10 @@ func TestLabel(t *testing.T) {
exists bool
}{
{"empty label list", "label", []*github.Label{}, false, false},
- {"empty label list with dry-run", "label", []*github.Label{}, true, false},
- {"label list contains exact match", "label", labels, false, true},
- {"label list contains prefix match", "^lab", labels, false, true},
- {"label list contains prefix doesn't match", "lab$", labels, false, false},
- {"label list contains prefix doesn't match with dry-run", "lab$", labels, true, false},
- {"label list contains suffix match", "bel$", labels, false, true},
- {"label list contains suffix match with dry-run", "bel$", labels, true, true},
- {"label list contains suffix doesn't match", "^bel", labels, false, false},
- {"label list contains suffix doesn't match with dry-run", "^bel", labels, true, false},
- {"label list doesn't contains match", "baleb", labels, false, false},
- {"label list doesn't contains match with dry-run", "baleb", labels, true, false},
+ {"empty label list with dry-run", "user", []*github.Label{}, true, false},
+ {"label list contains label", "label", labels, false, true},
+ {"label list doesn't contain label", "label2", labels, false, false},
+ {"label list doesn't contain label with dry-run", "label", labels, true, false},
} {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
@@ -71,9 +64,9 @@ func TestLabel(t *testing.T) {
details := treeprint.New()
requirement := Label(gh, testCase.pattern)
- assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true")
- assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true")
- assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item")
+ assert.True(t, requirement.IsSatisfied(pr, details) || testCase.dryRun, "requirement should have a satisfied status: true")
+ assert.True(t, utils.TestLastNodeStatus(t, true, details) || testCase.dryRun, "requirement details should have a status: true")
+ assert.True(t, testCase.exists || requested || testCase.dryRun, "requirement should have requested to create item")
})
}
}
diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go
index 2442a6d94d6..56d6667589a 100644
--- a/contribs/github-bot/matrix.go
+++ b/contribs/github-bot/matrix.go
@@ -48,7 +48,13 @@ func execMatrix() error {
return err
}
- fmt.Println(prList)
+ // Print PR list for GitHub Actions matrix definition
+ bytes, err := prList.MarshalText()
+ if err != nil {
+ return fmt.Errorf("unable to marshal PR list: %w", err)
+ }
+ fmt.Printf("[%s]", string(bytes))
+
return nil
}
From 2c060704b0225f54975d2a015174b207a3c33221 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?=
Date: Fri, 29 Nov 2024 10:05:14 +0100
Subject: [PATCH 17/86] chore: remove leftover `docker-integration` make
directive (#3243)
---
Makefile | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/Makefile b/Makefile
index 2bfbe4e05e2..bd67020f236 100644
--- a/Makefile
+++ b/Makefile
@@ -54,7 +54,7 @@ install_gnokey: install.gnokey
install_gno: install.gno
.PHONY: test
-test: test.components test.docker
+test: test.components
.PHONY: test.components
test.components:
@@ -64,14 +64,6 @@ test.components:
$(MAKE) --no-print-directory -C examples test
$(MAKE) --no-print-directory -C misc test
-.PHONY: test.docker
-test.docker:
- @if hash docker 2>/dev/null; then \
- go test --tags=docker -count=1 -v ./misc/docker-integration; \
- else \
- echo "[-] 'docker' is missing, skipping ./misc/docker-integration tests."; \
- fi
-
.PHONY: fmt
fmt:
$(MAKE) --no-print-directory -C tm2 fmt imports
From 7b7e7585540b495d5f0052ae280f3e876d91854a Mon Sep 17 00:00:00 2001
From: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Date: Mon, 2 Dec 2024 09:31:45 +0100
Subject: [PATCH 18/86] feat(govdao): better rendering (#3096)
## Description
Introduces better `gnoweb` rendering for the GovDAO suite, and better
help page actions for voting on proposals.
Home page before:
Home page after (also resolves usernames from `r/demo/users`):
Prop page before:
Prop page after:
The actions bar notifies the user when the proposal is no longer active
as well.
Continuation of #2579
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---
examples/gno.land/p/demo/dao/dao.gno | 1 +
examples/gno.land/p/demo/dao/proposals.gno | 5 +-
examples/gno.land/p/demo/simpledao/dao.gno | 8 +
.../gno.land/p/demo/simpledao/dao_test.gno | 49 ++++++
.../gno.land/p/demo/simpledao/propstore.gno | 38 +++--
examples/gno.land/r/gov/dao/v2/dao.gno | 54 ------
examples/gno.land/r/gov/dao/v2/gno.mod | 2 +
.../gno.land/r/gov/dao/v2/prop1_filetest.gno | 80 +++++++--
.../gno.land/r/gov/dao/v2/prop2_filetest.gno | 80 +++++++--
.../gno.land/r/gov/dao/v2/prop3_filetest.gno | 98 +++++++++--
.../gno.land/r/gov/dao/v2/prop4_filetest.gno | 155 ++++++++----------
examples/gno.land/r/gov/dao/v2/render.gno | 123 ++++++++++++++
12 files changed, 485 insertions(+), 208 deletions(-)
create mode 100644 examples/gno.land/r/gov/dao/v2/render.gno
diff --git a/examples/gno.land/p/demo/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno
index f8ea433192f..e3a2ba72c5b 100644
--- a/examples/gno.land/p/demo/dao/dao.gno
+++ b/examples/gno.land/p/demo/dao/dao.gno
@@ -15,6 +15,7 @@ const (
// that contains the necessary information to
// log and generate a valid proposal
type ProposalRequest struct {
+ Title string // the title associated with the proposal
Description string // the description associated with the proposal
Executor Executor // the proposal executor
}
diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno
index 5cad679d006..66abcb248c5 100644
--- a/examples/gno.land/p/demo/dao/proposals.gno
+++ b/examples/gno.land/p/demo/dao/proposals.gno
@@ -16,7 +16,7 @@ var (
Accepted ProposalStatus = "accepted" // proposal gathered quorum
NotAccepted ProposalStatus = "not accepted" // proposal failed to gather quorum
ExecutionSuccessful ProposalStatus = "execution successful" // proposal is executed successfully
- ExecutionFailed ProposalStatus = "execution failed" // proposal is failed during execution
+ ExecutionFailed ProposalStatus = "execution failed" // proposal has failed during execution
)
func (s ProposalStatus) String() string {
@@ -42,6 +42,9 @@ type Proposal interface {
// Author returns the author of the proposal
Author() std.Address
+ // Title returns the title of the proposal
+ Title() string
+
// Description returns the description of the proposal
Description() string
diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno
index 7a20237ec3f..837f64a41d6 100644
--- a/examples/gno.land/p/demo/simpledao/dao.gno
+++ b/examples/gno.land/p/demo/simpledao/dao.gno
@@ -3,6 +3,7 @@ package simpledao
import (
"errors"
"std"
+ "strings"
"gno.land/p/demo/avl"
"gno.land/p/demo/dao"
@@ -12,6 +13,7 @@ import (
var (
ErrInvalidExecutor = errors.New("invalid executor provided")
+ ErrInvalidTitle = errors.New("invalid proposal title provided")
ErrInsufficientProposalFunds = errors.New("insufficient funds for proposal")
ErrInsufficientExecuteFunds = errors.New("insufficient funds for executing proposal")
ErrProposalExecuted = errors.New("proposal already executed")
@@ -47,6 +49,11 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {
return 0, ErrInvalidExecutor
}
+ // Make sure the title is set
+ if strings.TrimSpace(request.Title) == "" {
+ return 0, ErrInvalidTitle
+ }
+
var (
caller = getDAOCaller()
sentCoins = std.GetOrigSend() // Get the sent coins, if any
@@ -61,6 +68,7 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {
// Create the wrapped proposal
prop := &proposal{
author: caller,
+ title: request.Title,
description: request.Description,
executor: request.Executor,
status: dao.Active,
diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno
index fb32895e72f..46251e24dad 100644
--- a/examples/gno.land/p/demo/simpledao/dao_test.gno
+++ b/examples/gno.land/p/demo/simpledao/dao_test.gno
@@ -45,6 +45,50 @@ func TestSimpleDAO_Propose(t *testing.T) {
)
})
+ t.Run("invalid title", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ called = false
+ cb = func() error {
+ called = true
+
+ return nil
+ }
+ ex = &mockExecutor{
+ executeFn: cb,
+ }
+
+ sentCoins = std.NewCoins(
+ std.NewCoin(
+ "ugnot",
+ minProposalFeeValue,
+ ),
+ )
+
+ ms = &mockMemberStore{
+ isMemberFn: func(_ std.Address) bool {
+ return false
+ },
+ }
+ s = New(ms)
+ )
+
+ std.TestSetOrigSend(sentCoins, std.Coins{})
+
+ _, err := s.Propose(dao.ProposalRequest{
+ Executor: ex,
+ Title: "", // Set invalid title
+ })
+ uassert.ErrorIs(
+ t,
+ err,
+ ErrInvalidTitle,
+ )
+
+ uassert.False(t, called)
+ })
+
t.Run("caller cannot cover fee", func(t *testing.T) {
t.Parallel()
@@ -58,6 +102,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
ex = &mockExecutor{
executeFn: cb,
}
+ title = "Proposal title"
sentCoins = std.NewCoins(
std.NewCoin(
@@ -80,6 +125,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
_, err := s.Propose(dao.ProposalRequest{
Executor: ex,
+ Title: title,
})
uassert.ErrorIs(
t,
@@ -105,6 +151,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
executeFn: cb,
}
description = "Proposal description"
+ title = "Proposal title"
proposer = testutils.TestAddress("proposer")
sentCoins = std.NewCoins(
@@ -129,6 +176,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
// Make sure the proposal was added
id, err := s.Propose(dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: ex,
})
@@ -141,6 +189,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
uassert.Equal(t, proposer.String(), prop.Author().String())
uassert.Equal(t, description, prop.Description())
+ uassert.Equal(t, title, prop.Title())
uassert.Equal(t, dao.Active.String(), prop.Status().String())
stats := prop.Stats()
diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno
index 06741d397cb..91f2a883047 100644
--- a/examples/gno.land/p/demo/simpledao/propstore.gno
+++ b/examples/gno.land/p/demo/simpledao/propstore.gno
@@ -3,6 +3,7 @@ package simpledao
import (
"errors"
"std"
+ "strings"
"gno.land/p/demo/dao"
"gno.land/p/demo/seqid"
@@ -18,6 +19,7 @@ const maxRequestProposals = 10
// proposal is the internal simpledao proposal implementation
type proposal struct {
author std.Address // initiator of the proposal
+ title string // title of the proposal
description string // description of the proposal
executor dao.Executor // executor for the proposal
@@ -31,6 +33,10 @@ func (p *proposal) Author() std.Address {
return p.author
}
+func (p *proposal) Title() string {
+ return p.title
+}
+
func (p *proposal) Description() string {
return p.description
}
@@ -63,15 +69,20 @@ func (p *proposal) Render() string {
// Fetch the voting stats
stats := p.Stats()
- output := ""
- output += ufmt.Sprintf("Author: %s", p.Author().String())
- output += "\n\n"
- output += p.Description()
- output += "\n\n"
- output += ufmt.Sprintf("Status: %s", p.Status().String())
- output += "\n\n"
- output += ufmt.Sprintf(
- "Voting stats: YES %d (%d%%), NO %d (%d%%), ABSTAIN %d (%d%%), MISSING VOTE %d (%d%%)",
+ var out string
+
+ out += "## Description\n\n"
+ if strings.TrimSpace(p.description) != "" {
+ out += ufmt.Sprintf("%s\n\n", p.description)
+ } else {
+ out += "No description provided.\n\n"
+ }
+
+ out += "## Proposal information\n\n"
+ out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(p.Status().String()))
+
+ out += ufmt.Sprintf(
+ "**Voting stats:**\n- YES %d (%d%%)\n- NO %d (%d%%)\n- ABSTAIN %d (%d%%)\n- MISSING VOTES %d (%d%%)\n",
stats.YayVotes,
stats.YayPercent(),
stats.NayVotes,
@@ -81,10 +92,13 @@ func (p *proposal) Render() string {
stats.MissingVotes(),
stats.MissingVotesPercent(),
)
- output += "\n\n"
- output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3)
- return output
+ out += "\n\n"
+ thresholdOut := strings.ToUpper(ufmt.Sprintf("%t", stats.YayVotes > (2*stats.TotalVotingPower)/3))
+
+ out += ufmt.Sprintf("**Threshold met: %s**\n\n", thresholdOut)
+
+ return out
}
// addProposal adds a new simpledao proposal to the store
diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno
index d99a161bcdf..9263d8d440b 100644
--- a/examples/gno.land/r/gov/dao/v2/dao.gno
+++ b/examples/gno.land/r/gov/dao/v2/dao.gno
@@ -2,12 +2,10 @@ package govdao
import (
"std"
- "strconv"
"gno.land/p/demo/dao"
"gno.land/p/demo/membstore"
"gno.land/p/demo/simpledao"
- "gno.land/p/demo/ufmt"
)
var (
@@ -65,55 +63,3 @@ func GetPropStore() dao.PropStore {
func GetMembStore() membstore.MemberStore {
return members
}
-
-func Render(path string) string {
- if path == "" {
- numProposals := d.Size()
-
- if numProposals == 0 {
- return "No proposals found :(" // corner case
- }
-
- output := ""
-
- offset := uint64(0)
- if numProposals >= 10 {
- offset = uint64(numProposals) - 10
- }
-
- // Fetch the last 10 proposals
- for idx, prop := range d.Proposals(offset, uint64(10)) {
- output += ufmt.Sprintf(
- "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n",
- idx,
- "/r/gov/dao/v2",
- idx,
- prop.Status().String(),
- prop.Author().String(),
- )
- }
-
- return output
- }
-
- // Display the detailed proposal
- idx, err := strconv.Atoi(path)
- if err != nil {
- return "404: Invalid proposal ID"
- }
-
- // Fetch the proposal
- prop, err := d.ProposalByID(uint64(idx))
- if err != nil {
- return ufmt.Sprintf("unable to fetch proposal, %s", err.Error())
- }
-
- // Render the proposal
- output := ""
- output += ufmt.Sprintf("# Prop #%d", idx)
- output += "\n\n"
- output += prop.Render()
- output += "\n\n"
-
- return output
-}
diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod
index bc379bf18df..4da6e0a2484 100644
--- a/examples/gno.land/r/gov/dao/v2/gno.mod
+++ b/examples/gno.land/r/gov/dao/v2/gno.mod
@@ -7,4 +7,6 @@ require (
gno.land/p/demo/simpledao v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/gov/executor v0.0.0-latest
+ gno.land/p/moul/txlink v0.0.0-latest
+ gno.land/r/demo/users v0.0.0-latest
)
diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
index 7b25eeb1db3..7d8975e1fe8 100644
--- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
@@ -42,9 +42,11 @@ func init() {
executor := validators.NewPropExecutor(changesFn)
// Create a proposal
+ title := "Valset change"
description := "manual valset changes proposal example"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: executor,
}
@@ -73,52 +75,98 @@ func main() {
// Output:
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Valset change](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
// No valset changes to apply.
// --
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
index 4eb993b80dc..84a64bc4ee2 100644
--- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
@@ -19,9 +19,11 @@ func init() {
)
// Create a proposal
+ title := "govdao blog post title"
description := "post a new blogpost about govdao"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: ex,
}
@@ -50,35 +52,68 @@ func main() {
// Output:
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - govdao blog post title](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
@@ -87,17 +122,30 @@ func main() {
// No posts.
// --
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
index 546213431e4..068f520e7e2 100644
--- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
@@ -28,9 +28,11 @@ func init() {
}
// Create a proposal
+ title := "new govdao member addition"
description := "add new members to the govdao"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: govdao.NewMemberPropExecutor(memberFn),
}
@@ -65,57 +67,117 @@ func main() {
// --
// 1
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
+//
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
+//
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Threshold met: FALSE**
//
-// Threshold met: false
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**accepted**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACCEPTED**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (25%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 30 (75%)
+//
//
-// Voting stats: YES 10 (25%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 30 (75%)
+// **Threshold met: FALSE**
//
-// Threshold met: false
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**execution successful**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
// 4
diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
index 8eff79ffb5a..13ca572c512 100644
--- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
@@ -9,8 +9,10 @@ import (
func init() {
mExec := params.NewStringPropExecutor("prop1.string", "value1")
+ title := "Setting prop1.string param"
comment := "setting prop1.string param"
prop := dao.ProposalRequest{
+ Title: title,
Description: comment,
Executor: mExec,
}
@@ -36,124 +38,95 @@ func main() {
// Output:
// new prop 0
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Setting prop1.string param](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: execution successful
+// ## Proposal information
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Status: EXECUTION SUCCESSFUL**
//
-// Threshold met: true
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
-
-// Events:
-// [
-// {
-// "type": "ProposalAdded",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "proposal-author",
-// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitProposalAdded"
-// },
-// {
-// "type": "VoteAdded",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "author",
-// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
-// },
-// {
-// "key": "option",
-// "value": "YES"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitVoteAdded"
-// },
-// {
-// "type": "ProposalAccepted",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitProposalAccepted"
-// },
-// {
-// "type": "set",
-// "attrs": [
-// {
-// "key": "k",
-// "value": "prop1.string"
-// }
-// ],
-// "pkg_path": "gno.land/r/sys/params",
-// "func": ""
-// },
-// {
-// "type": "ProposalExecuted",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "exec-status",
-// "value": "accepted"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "ExecuteProposal"
-// }
-// ]
diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno
new file mode 100644
index 00000000000..4cca397e851
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/render.gno
@@ -0,0 +1,123 @@
+package govdao
+
+import (
+ "strconv"
+ "strings"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/txlink"
+ "gno.land/r/demo/users"
+)
+
+func Render(path string) string {
+ var out string
+
+ if path == "" {
+ out += "# GovDAO Proposals\n\n"
+ numProposals := d.Size()
+
+ if numProposals == 0 {
+ out += "No proposals found :(" // corner case
+ return out
+ }
+
+ offset := uint64(0)
+ if numProposals >= 10 {
+ offset = uint64(numProposals) - 10
+ }
+
+ // Fetch the last 10 proposals
+ proposals := d.Proposals(offset, uint64(10))
+ for i := len(proposals) - 1; i >= 0; i-- {
+ prop := proposals[i]
+
+ title := prop.Title()
+ if len(title) > 40 {
+ title = title[:40] + "..."
+ }
+
+ propID := offset + uint64(i)
+ out += ufmt.Sprintf("## [Prop #%d - %s](/r/gov/dao/v2:%d)\n\n", propID, title, propID)
+ out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(prop.Status().String()))
+
+ user := users.GetUserByAddress(prop.Author())
+ authorDisplayText := prop.Author().String()
+ if user != nil {
+ authorDisplayText = ufmt.Sprintf("[%s](/r/demo/users:%s)", user.Name, user.Name)
+ }
+
+ out += ufmt.Sprintf("**Author: %s**\n\n", authorDisplayText)
+
+ if i != 0 {
+ out += "---\n\n"
+ }
+ }
+
+ return out
+ }
+
+ // Display the detailed proposal
+ idx, err := strconv.Atoi(path)
+ if err != nil {
+ return "404: Invalid proposal ID"
+ }
+
+ // Fetch the proposal
+ prop, err := d.ProposalByID(uint64(idx))
+ if err != nil {
+ return ufmt.Sprintf("unable to fetch proposal, %s", err.Error())
+ }
+
+ // Render the proposal page
+ out += renderPropPage(prop, idx)
+
+ return out
+}
+
+func renderPropPage(prop dao.Proposal, idx int) string {
+ var out string
+
+ out += ufmt.Sprintf("# Proposal #%d - %s\n\n", idx, prop.Title())
+ out += prop.Render()
+ out += renderAuthor(prop)
+ out += renderActionBar(prop, idx)
+ out += "\n\n"
+
+ return out
+}
+
+func renderAuthor(p dao.Proposal) string {
+ var out string
+
+ authorUsername := ""
+ user := users.GetUserByAddress(p.Author())
+ if user != nil {
+ authorUsername = user.Name
+ }
+
+ if authorUsername != "" {
+ out += ufmt.Sprintf("**Author: [%s](/r/demo/users:%s)**\n\n", authorUsername, authorUsername)
+ } else {
+ out += ufmt.Sprintf("**Author: %s**\n\n", p.Author().String())
+ }
+
+ return out
+}
+
+func renderActionBar(p dao.Proposal, idx int) string {
+ var out string
+
+ out += "### Actions\n\n"
+ if p.Status() == dao.Active {
+ out += ufmt.Sprintf("#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]",
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"),
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"),
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"),
+ )
+ } else {
+ out += "The voting period for this proposal is over."
+ }
+
+ return out
+}
From 43c9116c22de8518055b3eaf94c112ee99377a1e Mon Sep 17 00:00:00 2001
From: Morgan
Date: Mon, 2 Dec 2024 16:23:37 +0100
Subject: [PATCH 19/86] test(gnovm): test performance improvements (#3210)
Fix master CI runs, and miscellaneous improvements locally, too.
- ci: switch to using `-covermode=set` rather than atomic, as it
significantly degrades performance while not being shown on codecov.
[more
info](https://github.com/gnolang/gno/pull/3210#issuecomment-2511455953)
- gnolang tests: use `t.Parallel()` to parallelize known "long" tests,
both in `-short` and long versions.
- stdlibs: provide `unicode` native shims for some common functions used
in some standard library tests. This may lead to some small
inconsistencies between on-chain behaviour and off-chain should the
`unicode` packages diverge; but I think we might we might want to
consider a native-based `unicode` stdlib, anyway.
- thanks to these improvements, there is no longer the need to run
`-short` on PRs, as the CI runs in ~9 mins, ie. 8 minutes less than the
gno.land tests.
---
.github/workflows/gnovm.yml | 2 -
.github/workflows/test_template.yml | 5 +-
gnovm/pkg/gnolang/files_test.go | 79 ++++++++++++----
gnovm/pkg/test/test.go | 2 +
gnovm/stdlibs/bytes/compare_test.gno | 2 +
gnovm/tests/stdlibs/generated.go | 114 ++++++++++++++++++++++++
gnovm/tests/stdlibs/unicode/natives.gno | 8 ++
gnovm/tests/stdlibs/unicode/natives.go | 8 ++
8 files changed, 198 insertions(+), 22 deletions(-)
create mode 100644 gnovm/tests/stdlibs/unicode/natives.gno
create mode 100644 gnovm/tests/stdlibs/unicode/natives.go
diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml
index 8311d113047..7e7586b23d9 100644
--- a/.github/workflows/gnovm.yml
+++ b/.github/workflows/gnovm.yml
@@ -13,8 +13,6 @@ jobs:
uses: ./.github/workflows/main_template.yml
with:
modulepath: "gnovm"
- # in pull requests, append -short so that the CI runs quickly.
- tests-extra-args: ${{ github.event_name == 'pull_request' && '-short' || '' }}
secrets:
codecov-token: ${{ secrets.CODECOV_TOKEN }}
fmt:
diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml
index ccbae792c78..c7956b4caf4 100644
--- a/.github/workflows/test_template.yml
+++ b/.github/workflows/test_template.yml
@@ -41,11 +41,14 @@ jobs:
# Craft a filter flag based on the module path to avoid expanding coverage on unrelated tags.
export filter="-pkg=github.com/gnolang/gno/${{ inputs.modulepath }}/..."
+ # codecov only supports "boolean" coverage (whether a line is
+ # covered or not); so using -covermode=count or atomic would be
+ # pointless here.
# XXX: Simplify coverage of txtar - the current setup is a bit
# confusing and meticulous. There will be some improvements in Go
# 1.23 regarding coverage, so we can use this as a workaround until
# then.
- go test -covermode=atomic -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR
+ go test -covermode=set -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR
# Print results
(set +x; echo 'go coverage results:')
diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go
index f1bc87d21d8..09be600b198 100644
--- a/gnovm/pkg/gnolang/files_test.go
+++ b/gnovm/pkg/gnolang/files_test.go
@@ -33,19 +33,26 @@ func (nopReader) Read(p []byte) (int, error) { return 0, io.EOF }
// fix a specific test:
// go test -run TestFiles/'^bin1.gno' -short -v -update-golden-tests .
func TestFiles(t *testing.T) {
+ t.Parallel()
+
rootDir, err := filepath.Abs("../../../")
require.NoError(t, err)
- opts := &test.TestOptions{
- RootDir: rootDir,
- Output: io.Discard,
- Error: io.Discard,
- Sync: *withSync,
+ newOpts := func() *test.TestOptions {
+ o := &test.TestOptions{
+ RootDir: rootDir,
+ Output: io.Discard,
+ Error: io.Discard,
+ Sync: *withSync,
+ }
+ o.BaseStore, o.TestStore = test.Store(
+ rootDir, true,
+ nopReader{}, o.WriterForStore(), io.Discard,
+ )
+ return o
}
- opts.BaseStore, opts.TestStore = test.Store(
- rootDir, true,
- nopReader{}, opts.WriterForStore(), io.Discard,
- )
+ // sharedOpts is used for all "short" tests.
+ sharedOpts := newOpts()
dir := "../../tests/"
fsys := os.DirFS(dir)
@@ -59,7 +66,8 @@ func TestFiles(t *testing.T) {
return nil
}
subTestName := path[len("files/"):]
- if strings.HasSuffix(path, "_long.gno") && testing.Short() {
+ isLong := strings.HasSuffix(path, "_long.gno")
+ if isLong && testing.Short() {
t.Run(subTestName, func(t *testing.T) {
t.Skip("skipping in -short")
})
@@ -73,6 +81,12 @@ func TestFiles(t *testing.T) {
var criticalError error
t.Run(subTestName, func(t *testing.T) {
+ opts := sharedOpts
+ if isLong {
+ // Long tests are run in parallel, and with their own store.
+ t.Parallel()
+ opts = newOpts()
+ }
changed, err := opts.RunFiletest(path, content)
if err != nil {
t.Fatal(err.Error())
@@ -94,16 +108,24 @@ func TestFiles(t *testing.T) {
// TestStdlibs tests all the standard library packages.
func TestStdlibs(t *testing.T) {
+ t.Parallel()
+
rootDir, err := filepath.Abs("../../../")
require.NoError(t, err)
- var capture bytes.Buffer
- out := io.Writer(&capture)
- if testing.Verbose() {
- out = os.Stdout
+ newOpts := func() (capture *bytes.Buffer, opts *test.TestOptions) {
+ var out io.Writer
+ if testing.Verbose() {
+ out = os.Stdout
+ } else {
+ capture = new(bytes.Buffer)
+ out = capture
+ }
+ opts = test.NewTestOptions(rootDir, nopReader{}, out, out)
+ opts.Verbose = true
+ return
}
- opts := test.NewTestOptions(rootDir, nopReader{}, out, out)
- opts.Verbose = true
+ sharedCapture, sharedOpts := newOpts()
dir := "../../stdlibs/"
fsys := os.DirFS(dir)
@@ -118,12 +140,31 @@ func TestStdlibs(t *testing.T) {
fp := filepath.Join(dir, path)
memPkg := gnolang.ReadMemPackage(fp, path)
t.Run(strings.ReplaceAll(memPkg.Path, "/", "-"), func(t *testing.T) {
- if testing.Short() {
- switch memPkg.Path {
- case "bytes", "strconv", "regexp/syntax":
+ capture, opts := sharedCapture, sharedOpts
+ switch memPkg.Path {
+ // Excluded in short
+ case
+ "bufio",
+ "bytes",
+ "strconv":
+ if testing.Short() {
t.Skip("Skipped because of -short, and this stdlib is very long currently.")
}
+ fallthrough
+ // Run using separate store, as it's faster
+ case
+ "math/rand",
+ "regexp",
+ "regexp/syntax",
+ "sort":
+ t.Parallel()
+ capture, opts = newOpts()
+ }
+
+ if capture != nil {
+ capture.Reset()
}
+
err := test.Test(memPkg, "", opts)
if !testing.Verbose() {
t.Log(capture.String())
diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go
index 9374db263ee..5de37a68405 100644
--- a/gnovm/pkg/test/test.go
+++ b/gnovm/pkg/test/test.go
@@ -284,6 +284,8 @@ func (opts *TestOptions) runTestFiles(
if opts.Metrics {
alloc = gno.NewAllocator(math.MaxInt64)
}
+ // reset store ops, if any - we only need them for some filetests.
+ opts.TestStore.SetLogStoreOps(false)
// Check if we already have the package - it may have been eagerly
// loaded.
diff --git a/gnovm/stdlibs/bytes/compare_test.gno b/gnovm/stdlibs/bytes/compare_test.gno
index f2b1e7c692b..5ebeba33889 100644
--- a/gnovm/stdlibs/bytes/compare_test.gno
+++ b/gnovm/stdlibs/bytes/compare_test.gno
@@ -66,6 +66,8 @@ func TestCompareIdenticalSlice(t *testing.T) {
}
func TestCompareBytes(t *testing.T) {
+ t.Skip("This test takes very long to run on Gno at time of writing, even in its short form")
+
lengths := make([]int, 0) // lengths to test in ascending order
for i := 0; i <= 128; i++ {
lengths = append(lengths, i)
diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go
index 2cc904a9170..db5ecdec05d 100644
--- a/gnovm/tests/stdlibs/generated.go
+++ b/gnovm/tests/stdlibs/generated.go
@@ -9,6 +9,7 @@ import (
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
testlibs_std "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
testlibs_testing "github.com/gnolang/gno/gnovm/tests/stdlibs/testing"
+ testlibs_unicode "github.com/gnolang/gno/gnovm/tests/stdlibs/unicode"
)
// NativeFunc represents a function in the standard library which has a native
@@ -325,6 +326,118 @@ var nativeFuncs = [...]NativeFunc{
func(m *gno.Machine) {
r0 := testlibs_testing.X_unixNano()
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "IsPrint",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("bool")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.IsPrint(p0)
+
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "IsGraphic",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("bool")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.IsGraphic(p0)
+
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "SimpleFold",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("rune")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.SimpleFold(p0)
+
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "IsUpper",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("bool")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.IsUpper(p0)
+
m.PushValue(gno.Go2GnoValue(
m.Alloc,
m.Store,
@@ -337,6 +450,7 @@ var nativeFuncs = [...]NativeFunc{
var initOrder = [...]string{
"std",
"testing",
+ "unicode",
}
// InitOrder returns the initialization order of the standard libraries.
diff --git a/gnovm/tests/stdlibs/unicode/natives.gno b/gnovm/tests/stdlibs/unicode/natives.gno
new file mode 100644
index 00000000000..c7efaac70cc
--- /dev/null
+++ b/gnovm/tests/stdlibs/unicode/natives.gno
@@ -0,0 +1,8 @@
+package unicode
+
+// Optimized as native bindings in tests.
+
+func IsPrint(r rune) bool
+func IsGraphic(r rune) bool
+func SimpleFold(r rune) rune
+func IsUpper(r rune) bool
diff --git a/gnovm/tests/stdlibs/unicode/natives.go b/gnovm/tests/stdlibs/unicode/natives.go
new file mode 100644
index 00000000000..e627f4fe6be
--- /dev/null
+++ b/gnovm/tests/stdlibs/unicode/natives.go
@@ -0,0 +1,8 @@
+package unicode
+
+import "unicode"
+
+func IsPrint(r rune) bool { return unicode.IsPrint(r) }
+func IsGraphic(r rune) bool { return unicode.IsGraphic(r) }
+func SimpleFold(r rune) rune { return unicode.SimpleFold(r) }
+func IsUpper(r rune) bool { return unicode.IsUpper(r) }
From 8fa4997cafa486fda99b899436ef9805eb59313d Mon Sep 17 00:00:00 2001
From: Antoine Eddi <5222525+aeddi@users.noreply.github.com>
Date: Mon, 2 Dec 2024 18:57:35 +0100
Subject: [PATCH 20/86] ci: add debug on github-bot matrix subcommand + fixes
(#3244)
This PR will allow debugging errors of [this
type](https://github.com/gnolang/gno/actions/runs/12072757244) that
unfortunately cannot be tested locally since they rely on the context of
GitHub Actions.
Since I also had to add flags to the matrix subcommand, I moved the two
matrix and check subcommands into subfolders.
This PR also modify the comment to stick to moul's request and fixes
several Github Actions errors.
Related to #3238
Changes:
-
https://github.com/gnolang/gno/pull/3244/commits/d11ad5a08e457921907e3db32b8576921dde8563
moves matrix and check subcommands to their own packages in internal
-
https://github.com/gnolang/gno/pull/3244/commits/462ac01321ff15e34cbe956a7ecc07096e665e28
https://github.com/gnolang/gno/pull/3244/commits/5c1edda51950c74c8bccb7eb8c16c036df3bd1f7
https://github.com/gnolang/gno/pull/3244/commits/ffdce936c39c1ad587f0ed17158f579b4ded067e
adds a debug to matrix subcommand (print event input / matrix output) +
direct output of matrix to GitHub Actions using a matrix-key flag
-
https://github.com/gnolang/gno/pull/3244/commits/6af501d4cd923c122e8ea6791ab58f394e2bbf1f
embed comment template file as a string at compile time instead of
opening it at runtime
-
https://github.com/gnolang/gno/pull/3244/commits/59c3ad6835191cae92dc811de4484b6a6793ea74
modifies bot comment to meet [this
requirements](https://github.com/gnolang/gno/issues/3238#issuecomment-2506520120)
-
https://github.com/gnolang/gno/pull/3244/commits/241a75532ce5e035ac745b4cd66f3bea2d9a420f
filter out from the matrix generation and the PR processing all issues
or closed PRs (process / list only opened PRs)
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---------
Co-authored-by: Morgan
---
.github/workflows/bot.yml | 4 +-
contribs/github-bot/README.md | 4 +-
contribs/github-bot/comment.tmpl | 51 -------
.../github-bot/{ => internal/check}/check.go | 62 +++-----
.../{params/params.go => check/cmd.go} | 75 ++++++----
.../{ => internal/check}/comment.go | 43 +++---
.../github-bot/internal/check/comment.tmpl | 54 +++++++
.../{ => internal/check}/comment_test.go | 41 ++++--
contribs/github-bot/internal/client/client.go | 52 +++++--
.../{ => internal/config}/config.go | 60 ++++----
contribs/github-bot/internal/matrix/cmd.go | 53 +++++++
contribs/github-bot/internal/matrix/matrix.go | 139 ++++++++++++++++++
.../{ => internal/matrix}/matrix_test.go | 45 +++---
.../internal/requirements/assignee_test.go | 2 +-
.../internal/requirements/branch.go | 2 +-
.../internal/requirements/label_test.go | 2 +-
contribs/github-bot/internal/utils/actions.go | 2 +-
.../github-bot/internal/utils/github_const.go | 6 +-
.../internal/{params => utils}/prlist.go | 3 +-
contribs/github-bot/main.go | 24 ++-
contribs/github-bot/matrix.go | 117 ---------------
21 files changed, 490 insertions(+), 351 deletions(-)
delete mode 100644 contribs/github-bot/comment.tmpl
rename contribs/github-bot/{ => internal/check}/check.go (78%)
rename contribs/github-bot/internal/{params/params.go => check/cmd.go} (56%)
rename contribs/github-bot/{ => internal/check}/comment.go (90%)
create mode 100644 contribs/github-bot/internal/check/comment.tmpl
rename contribs/github-bot/{ => internal/check}/comment_test.go (86%)
rename contribs/github-bot/{ => internal/config}/config.go (54%)
create mode 100644 contribs/github-bot/internal/matrix/cmd.go
create mode 100644 contribs/github-bot/internal/matrix/matrix.go
rename contribs/github-bot/{ => internal/matrix}/matrix_test.go (91%)
rename contribs/github-bot/internal/{params => utils}/prlist.go (91%)
delete mode 100644 contribs/github-bot/matrix.go
diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
index 21950459ae8..cbfec5730fc 100644
--- a/.github/workflows/bot.yml
+++ b/.github/workflows/bot.yml
@@ -55,13 +55,15 @@ jobs:
working-directory: contribs/github-bot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: echo "pr-numbers=$(go run . matrix)" >> "$GITHUB_OUTPUT"
+ run: go run . matrix -matrix-key 'pr-numbers' -verbose
# This job processes each pull request in the matrix individually while ensuring
# that a same PR cannot be processed concurrently by mutliple runners
process-pr:
name: Process PR
needs: define-prs-matrix
+ # Just skip this job if PR numbers matrix is empty (prevent failed state)
+ if: ${{ needs.define-prs-matrix.outputs.pr-numbers != '[]' && needs.define-prs-matrix.outputs.pr-numbers != '' }}
runs-on: ubuntu-latest
strategy:
matrix:
diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md
index 78c9c3c01b8..7932300cb9d 100644
--- a/contribs/github-bot/README.md
+++ b/contribs/github-bot/README.md
@@ -13,7 +13,7 @@ The bot operates by defining a set of rules that are evaluated against each pull
- **Automatic Checks**: These are rules that the bot evaluates automatically. If a pull request meets the conditions specified in the rule, then the corresponding requirements are executed. For example, ensuring that changes to specific directories are reviewed by specific team members.
- **Manual Checks**: These require human intervention. If a pull request meets the conditions specified in the rule, then a checkbox that can be checked only by specified teams is displayed on the bot comment. For example, determining if infrastructure needs to be updated based on changes to specific files.
-The bot configuration is defined in Go and is located in the file [config.go](./config.go).
+The bot configuration is defined in Go and is located in the file [config.go](./internal/config/config.go).
### GitHub Token
@@ -31,7 +31,7 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token
USAGE
github-bot check [flags]
-This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.
+This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.
A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.
FLAGS
diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl
deleted file mode 100644
index ebd07fdd4b9..00000000000
--- a/contribs/github-bot/comment.tmpl
+++ /dev/null
@@ -1,51 +0,0 @@
-# Merge Requirements
-
-The following requirements must be fulfilled before a pull request can be merged.
-Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member.
-
-These requirements are defined in this [configuration file](https://github.com/GnoCheckBot/demo/blob/main/config.go).
-
-## Automated Checks
-
-{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }}
-{{ end }}
-
-{{ if .AutoRules }}Details
-{{ range .AutoRules }}
-{{ .Description | stripLinks }}
-
-### If
-```
-{{ .ConditionDetails | stripLinks }}
-```
-### Then
-```
-{{ .RequirementDetails | stripLinks }}
-```
-
-{{ end }}
-
-{{ else }}*No automated checks match this pull request.*{{ end }}
-
-## Manual Checks
-
-{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }}
-{{ end }}
-
-{{ if .ManualRules }}Details
-{{ range .ManualRules }}
-{{ .Description | stripLinks }}
-
-### If
-```
-{{ .ConditionDetails }}
-```
-### Can be checked by
-{{range $item := .Teams }} - team {{ $item | stripLinks }}
-{{ else }}
-- Any user with comment edit permission
-{{end}}
-
-{{ end }}
-
-{{ else }}*No manual checks match this pull request.*{{ end }}
diff --git a/contribs/github-bot/check.go b/contribs/github-bot/internal/check/check.go
similarity index 78%
rename from contribs/github-bot/check.go
rename to contribs/github-bot/internal/check/check.go
index 8019246d27c..5ca2235e823 100644
--- a/contribs/github-bot/check.go
+++ b/contribs/github-bot/internal/check/check.go
@@ -1,4 +1,4 @@
-package main
+package check
import (
"context"
@@ -9,44 +9,30 @@ import (
"sync/atomic"
"github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/config"
"github.com/gnolang/gno/contribs/github-bot/internal/logger"
- p "github.com/gnolang/gno/contribs/github-bot/internal/params"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
- "github.com/gnolang/gno/tm2/pkg/commands"
"github.com/google/go-github/v64/github"
"github.com/sethvargo/go-githubactions"
"github.com/xlab/treeprint"
)
-func newCheckCmd() *commands.Command {
- params := &p.Params{}
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "check",
- ShortUsage: "github-bot check [flags]",
- ShortHelp: "checks requirements for a pull request to be merged",
- LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
- },
- params,
- func(_ context.Context, _ []string) error {
- params.ValidateFlags()
- return execCheck(params)
- },
- )
-}
-
-func execCheck(params *p.Params) error {
+func execCheck(flags *checkFlags) error {
// Create context with timeout if specified in the parameters.
ctx := context.Background()
- if params.Timeout > 0 {
+ if flags.Timeout > 0 {
var cancel context.CancelFunc
- ctx, cancel = context.WithTimeout(context.Background(), params.Timeout)
+ ctx, cancel = context.WithTimeout(context.Background(), flags.Timeout)
defer cancel()
}
// Init GitHub API client.
- gh, err := client.New(ctx, params)
+ gh, err := client.New(ctx, &client.Config{
+ Owner: flags.Owner,
+ Repo: flags.Repo,
+ Verbose: *flags.Verbose,
+ DryRun: flags.DryRun,
+ })
if err != nil {
return fmt.Errorf("comment update handling failed: %w", err)
}
@@ -69,7 +55,7 @@ func execCheck(params *p.Params) error {
var prs []*github.PullRequest
// If requested, retrieve all open pull requests.
- if params.PRAll {
+ if flags.PRAll {
prs, err = gh.ListPR(utils.PRStateOpen)
if err != nil {
return fmt.Errorf("unable to list all PR: %w", err)
@@ -77,11 +63,11 @@ func execCheck(params *p.Params) error {
} else {
// Otherwise, retrieve only specified pull request(s)
// (flag or GitHub Action context).
- prs = make([]*github.PullRequest, len(params.PRNums))
- for i, prNum := range params.PRNums {
- pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
+ prs = make([]*github.PullRequest, len(flags.PRNums))
+ for i, prNum := range flags.PRNums {
+ pr, err := gh.GetOpenedPullRequest(prNum)
if err != nil {
- return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
+ return fmt.Errorf("unable to process PR list: %w", err)
}
prs[i] = pr
}
@@ -101,7 +87,7 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
}
// Process all pull requests in parallel.
- autoRules, manualRules := config(gh)
+ autoRules, manualRules := config.Config(gh)
var wg sync.WaitGroup
// Used in dry-run mode to log cleanly from different goroutines.
@@ -122,15 +108,15 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
// Check if conditions of this rule are met by this PR.
- if !autoRule.ifC.IsMet(pr, ifDetails) {
+ if !autoRule.If.IsMet(pr, ifDetails) {
continue
}
- c := AutoContent{Description: autoRule.description, Satisfied: false}
+ c := AutoContent{Description: autoRule.Description, Satisfied: false}
thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.Fail))
// Check if requirements of this rule are satisfied by this PR.
- if autoRule.thenR.IsSatisfied(pr, thenDetails) {
+ if autoRule.Then.IsSatisfied(pr, thenDetails) {
thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success))
c.Satisfied = true
} else {
@@ -153,13 +139,13 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
// Check if conditions of this rule are met by this PR.
- if !manualRule.ifC.IsMet(pr, ifDetails) {
+ if !manualRule.If.IsMet(pr, ifDetails) {
continue
}
// Get check status from current comment, if any.
checkedBy := ""
- check, ok := checks[manualRule.description]
+ check, ok := checks[manualRule.Description]
if ok {
checkedBy = check.checkedBy
}
@@ -167,10 +153,10 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
commentContent.ManualRules = append(
commentContent.ManualRules,
ManualContent{
- Description: manualRule.description,
+ Description: manualRule.Description,
ConditionDetails: ifDetails.String(),
CheckedBy: checkedBy,
- Teams: manualRule.teams,
+ Teams: manualRule.Teams,
},
)
diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/check/cmd.go
similarity index 56%
rename from contribs/github-bot/internal/params/params.go
rename to contribs/github-bot/internal/check/cmd.go
index c11d1b62419..7ea6c02795b 100644
--- a/contribs/github-bot/internal/params/params.go
+++ b/contribs/github-bot/internal/check/cmd.go
@@ -1,118 +1,131 @@
-package params
+package check
import (
+ "context"
"flag"
"fmt"
"os"
"time"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/gnolang/gno/tm2/pkg/commands"
"github.com/sethvargo/go-githubactions"
)
-type Params struct {
+type checkFlags struct {
Owner string
Repo string
PRAll bool
- PRNums PRList
- Verbose bool
+ PRNums utils.PRList
+ Verbose *bool
DryRun bool
Timeout time.Duration
flagSet *flag.FlagSet
}
-func (p *Params) RegisterFlags(fs *flag.FlagSet) {
+func NewCheckCmd(verbose *bool) *commands.Command {
+ flags := &checkFlags{Verbose: verbose}
+
+ return commands.NewCommand(
+ commands.Metadata{
+ Name: "check",
+ ShortUsage: "github-bot check [flags]",
+ ShortHelp: "checks requirements for a pull request to be merged",
+ LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
+ },
+ flags,
+ func(_ context.Context, _ []string) error {
+ flags.validateFlags()
+ return execCheck(flags)
+ },
+ )
+}
+
+func (flags *checkFlags) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
- &p.Owner,
+ &flags.Owner,
"owner",
"",
"owner of the repo to process, if empty, will be retrieved from GitHub Actions context",
)
fs.StringVar(
- &p.Repo,
+ &flags.Repo,
"repo",
"",
"repo to process, if empty, will be retrieved from GitHub Actions context",
)
fs.BoolVar(
- &p.PRAll,
+ &flags.PRAll,
"pr-all",
false,
"process all opened pull requests",
)
fs.TextVar(
- &p.PRNums,
+ &flags.PRNums,
"pr-numbers",
- PRList(nil),
+ utils.PRList(nil),
"pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context",
)
fs.BoolVar(
- &p.Verbose,
- "verbose",
- false,
- "set logging level to debug",
- )
-
- fs.BoolVar(
- &p.DryRun,
+ &flags.DryRun,
"dry-run",
false,
"print if pull request requirements are satisfied without updating anything on GitHub",
)
fs.DurationVar(
- &p.Timeout,
+ &flags.Timeout,
"timeout",
0,
"timeout after which the bot execution is interrupted",
)
- p.flagSet = fs
+ flags.flagSet = fs
}
-func (p *Params) ValidateFlags() {
+func (flags *checkFlags) validateFlags() {
// Helper to display an error + usage message before exiting.
errorUsage := func(err string) {
- fmt.Fprintf(p.flagSet.Output(), "Error: %s\n\n", err)
- p.flagSet.Usage()
+ fmt.Fprintf(flags.flagSet.Output(), "Error: %s\n\n", err)
+ flags.flagSet.Usage()
os.Exit(1)
}
// Check if flags are coherent.
- if p.PRAll && len(p.PRNums) != 0 {
+ if flags.PRAll && len(flags.PRNums) != 0 {
errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags.")
}
// If one of these values is empty, it must be retrieved
// from GitHub Actions context.
- if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) {
+ if flags.Owner == "" || flags.Repo == "" || (len(flags.PRNums) == 0 && !flags.PRAll) {
actionCtx, err := githubactions.Context()
if err != nil {
errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err))
}
- if p.Owner == "" {
- if p.Owner, _ = actionCtx.Repo(); p.Owner == "" {
+ if flags.Owner == "" {
+ if flags.Owner, _ = actionCtx.Repo(); flags.Owner == "" {
errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag.")
}
}
- if p.Repo == "" {
- if _, p.Repo = actionCtx.Repo(); p.Repo == "" {
+ if flags.Repo == "" {
+ if _, flags.Repo = actionCtx.Repo(); flags.Repo == "" {
errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag.")
}
}
- if len(p.PRNums) == 0 && !p.PRAll {
+ if len(flags.PRNums) == 0 && !flags.PRAll {
prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
if err != nil {
errorUsage(fmt.Sprintf("Unable to retrieve pull request number from GitHub Actions context: %s\nYou may want to set it using -pr-numbers flag.", err.Error()))
}
- p.PRNums = PRList{prNum}
+ flags.PRNums = utils.PRList{prNum}
}
}
}
diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/internal/check/comment.go
similarity index 90%
rename from contribs/github-bot/comment.go
rename to contribs/github-bot/internal/check/comment.go
index f6605ea8554..434df8f9e76 100644
--- a/contribs/github-bot/comment.go
+++ b/contribs/github-bot/internal/check/comment.go
@@ -1,7 +1,8 @@
-package main
+package check
import (
"bytes"
+ _ "embed"
"errors"
"fmt"
"regexp"
@@ -9,12 +10,15 @@ import (
"text/template"
"github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/config"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
-
"github.com/google/go-github/v64/github"
"github.com/sethvargo/go-githubactions"
)
+//go:embed comment.tmpl
+var tmplString string // Embed template used for comment generation.
+
var errTriggeredByBot = errors.New("event triggered by bot")
// Compile regex only once.
@@ -95,6 +99,18 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
return nil
}
+ // Get PR number from GitHub Actions context.
+ prNumFloat, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64)
+ if !ok || prNumFloat <= 0 {
+ return errors.New("unable to get issue number on issue comment event")
+ }
+ prNum := int(prNumFloat)
+
+ // Ignore if this comment update is not related to an opened PR.
+ if _, err := gh.GetOpenedPullRequest(prNum); err != nil {
+ return nil // May come from an issue or a closed PR
+ }
+
// Return if comment was edited by bot (current authenticated user).
authUser, _, err := gh.Client.Users.Get(gh.Ctx, "")
if err != nil {
@@ -129,17 +145,11 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
return errors.New("unable to get changes body content on issue comment event")
}
- // Get PR number from GitHub Actions context.
- prNum, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64)
- if !ok || prNum <= 0 {
- return errors.New("unable to get issue number on issue comment event")
- }
-
// Check if change is only a checkbox being checked or unckecked.
if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") {
// If not, restore previous comment body.
if !gh.DryRun {
- gh.SetBotComment(previous, int(prNum))
+ gh.SetBotComment(previous, prNum)
}
return errors.New("bot comment edited outside of checkboxes")
}
@@ -157,12 +167,12 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// Get teams allowed to edit this box from config.
var teams []string
found := false
- _, manualRules := config(gh)
+ _, manualRules := config.Config(gh)
for _, manualRule := range manualRules {
- if manualRule.description == key {
+ if manualRule.Description == key {
found = true
- teams = manualRule.teams
+ teams = manualRule.Teams
}
}
@@ -175,9 +185,9 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// If teams specified in rule, check if actor is a member of one of them.
if len(teams) > 0 {
- if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed
+ if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed to check the boxes.
if !gh.DryRun {
- gh.SetBotComment(previous, int(prNum)) // Restore previous state
+ gh.SetBotComment(previous, prNum) // Then restore previous state.
}
return errors.New("checkbox edited by a user not allowed to")
}
@@ -199,7 +209,7 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// Update comment with username.
if edited != "" && !gh.DryRun {
- gh.SetBotComment(edited, int(prNum))
+ gh.SetBotComment(edited, prNum)
gh.Logger.Debugf("Comment manual checks updated successfully")
}
@@ -217,8 +227,7 @@ func generateComment(content CommentContent) (string, error) {
}
// Bind markdown stripping function to template generator.
- const tmplFile = "comment.tmpl"
- tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
+ tmpl, err := template.New("comment").Funcs(funcMap).Parse(tmplString)
if err != nil {
return "", fmt.Errorf("unable to init template: %w", err)
}
diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl
new file mode 100644
index 00000000000..4312019dd2e
--- /dev/null
+++ b/contribs/github-bot/internal/check/comment.tmpl
@@ -0,0 +1,54 @@
+I'm a bot that assists the Gno Core team in maintaining this repository. My role is to ensure that contributors understand and follow our guidelines, helping to streamline the development process.
+
+The following requirements must be fulfilled before a pull request can be merged.
+Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member.
+
+These requirements are defined in this [configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go).
+
+## Automated Checks
+
+{{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }}
+{{ end }}{{ else }}*No automated checks match this pull request.*{{ end }}
+
+## Manual Checks
+
+{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }}
+{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }}
+
+{{ if or .AutoRules .ManualRules }}Debug
+{{ if .AutoRules }}Automated Checks
+{{ range .AutoRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails | stripLinks }}
+```
+### Then
+```
+{{ .RequirementDetails | stripLinks }}
+```
+
+{{ end }}
+
+{{ end }}
+
+{{ if .ManualRules }}Manual Checks
+{{ range .ManualRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails }}
+```
+### Can be checked by
+{{range $item := .Teams }} - team {{ $item | stripLinks }}
+{{ else }}
+- Any user with comment edit permission
+{{end}}
+