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). ![filetests](https://github.com/user-attachments/assets/049680f2-baeb-4f24-8f0f-60ae5fa4bce5) - 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. --- [![Open Source Saturday](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F-open%20source%20saturday-F64060.svg)](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 compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/net&package-manager=go_modules&previous-version=0.0.0-20190813141303-74dc4d7220e7&new-version=0.23.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) 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 You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/gnolang/gno/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- contribs/gnomd/go.mod | 4 ++-- contribs/gnomd/go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod index 8bc352d4848..423e4414a79 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -22,6 +22,6 @@ require ( github.com/mattn/go-runewidth v0.0.12 // indirect github.com/rivo/uniseg v0.1.0 // indirect golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect - golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect - golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/contribs/gnomd/go.sum b/contribs/gnomd/go.sum index b4ad4f5c9bf..0ff70dd99fb 100644 --- a/contribs/gnomd/go.sum +++ b/contribs/gnomd/go.sum @@ -57,13 +57,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s= golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= From d8589b06b14c9b5d4f887faec047813a9d1a1756 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 27 Nov 2024 11:55:39 +0900 Subject: [PATCH 07/86] fix(gnovm): Prevent use of blank identifier as Value or Type (#2699) # Description Closes #1946 The `isNamedConversion` function now includes a safety check to prevent the use of blank identifiers ("_") as values or types. If both `xt` and `t` are nil, the function assumes that a blank identifier is being used inappropriately and panics with an error message that includes the location of the issue. ## Variable Explanations - `xt` (Expression Type): Represents the type of the right-hand side of an assignment or expression. It's the type resulting from evaluating an expression. - `t` (Target Type): Represents the type of the left-hand side of an assignment. It's the variable or field that will receive the value. Checks if a named conversion is needed when assigning a value of type `xt` to a variable of type `t`. ## Preprocess Added some checks to prevent the disallowd usage of blank identifiers in `Preprocess` function level. Theses checks are performed at different stages of the preprocessing: 1. `TRANS_ENTER` for `AssignStmt`: - Checks if both LHS and RHS are blank identifiers in a `DEFINE` statement. 2. `TRANS_LEAVE` for `NameExpr`: - Checks if blank identifier is used as a value in disallowed contexts (excluding `TRANS_ASSIGN_LHS`, `TRANS_RANGE_KEY` and `TRANS_RANGE_VALUE`). 3. `TRANS_LEAVE` for `AssignStmt`: - Checks if RHS is a blank identifier when LHS is not, in a `DEFINE` statement. When any of these conditions are met, the function throws an panics like go message.
Contributors' checklist... - [X] 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 - [X] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- gnovm/pkg/gnolang/nodes.go | 2 +- gnovm/pkg/gnolang/preprocess.go | 24 +++++++++-- gnovm/pkg/gnolang/type_check.go | 3 ++ gnovm/pkg/gnolang/type_check_test.go | 57 ++++++++++++++++++++++++++ gnovm/tests/files/blankidentifier0.gno | 8 ++++ gnovm/tests/files/blankidentifier1.gno | 12 ++++++ gnovm/tests/files/blankidentifier2.gno | 9 ++++ gnovm/tests/files/blankidentifier3.gno | 10 +++++ gnovm/tests/files/blankidentifier4.gno | 9 ++++ gnovm/tests/files/blankidentifier5.gno | 12 ++++++ gnovm/tests/files/blankidentifier6.gno | 24 +++++++++++ gnovm/tests/files/blankidentifier7.gno | 13 ++++++ gnovm/tests/files/typeassert10.gno | 17 ++++++++ 13 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 gnovm/pkg/gnolang/type_check_test.go create mode 100644 gnovm/tests/files/blankidentifier0.gno create mode 100644 gnovm/tests/files/blankidentifier1.gno create mode 100644 gnovm/tests/files/blankidentifier2.gno create mode 100644 gnovm/tests/files/blankidentifier3.gno create mode 100644 gnovm/tests/files/blankidentifier4.gno create mode 100644 gnovm/tests/files/blankidentifier5.gno create mode 100644 gnovm/tests/files/blankidentifier6.gno create mode 100644 gnovm/tests/files/blankidentifier7.gno create mode 100644 gnovm/tests/files/typeassert10.gno diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index dcc1ad41739..3368c7c7bde 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -441,7 +441,7 @@ type IndexExpr struct { // X[Index] Attributes X Expr // expression Index Expr // index expression - HasOK bool // if true, is form: `value, ok := [] + HasOK bool // if true, is form: `value, ok := []` } type SelectorExpr struct { // X.Sel diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 53c187342a6..6e82786b318 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -927,6 +927,14 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { switch n := n.(type) { // TRANS_LEAVE ----------------------- case *NameExpr: + if isBlankIdentifier(n) { + switch ftype { + case TRANS_ASSIGN_LHS, TRANS_RANGE_KEY, TRANS_RANGE_VALUE, TRANS_VAR_NAME: + // can use _ as value or type in these contexts + default: + panic("cannot use _ as value or type") + } + } // Validity: check that name isn't reserved. if isReservedName(n.Name) { panic(fmt.Sprintf( @@ -1645,7 +1653,8 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // Type assertions on the blank identifier are illegal. - if nx, ok := n.X.(*NameExpr); ok && string(nx.Name) == blankIdentifier { + + if isBlankIdentifier(n.X) { panic("cannot use _ as value or type") } @@ -3622,7 +3631,6 @@ func isNamedConversion(xt, t Type) bool { if t == nil { t = xt } - // no conversion case 1: the LHS is an interface _, c1 := t.(*InterfaceType) @@ -3634,7 +3642,6 @@ func isNamedConversion(xt, t Type) bool { _, oktt2 := xt.(*TypeType) c2 := oktt || oktt2 - // if !c1 && !c2 { // carve out above two cases // covert right to the type of left if one side is unnamed type and the other side is not @@ -4216,6 +4223,12 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { }) d.Path = last.GetPathForName(store, d.Name) case *ValueDecl: + // check for blank identifier in type + // e.g., `var x _` + if isBlankIdentifier(d.Type) { + panic("cannot use _ as value or type") + } + un = findUndefined(store, last, d.Type) if un != "" { return @@ -4258,6 +4271,11 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { case *StarExpr: t = &PointerType{} case *NameExpr: + // check for blank identifier in type + // e.g., `type T _` + if isBlankIdentifier(tx) { + panic("cannot use _ as value or type") + } if tv := last.GetValueRef(store, tx.Name, true); tv != nil { t = tv.GetType() if dt, ok := t.(*DeclaredType); ok { diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index e786bed683f..95b1c54ae4b 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -288,6 +288,9 @@ func checkAssignableTo(xt, dt Type, autoNative bool) error { } // case0 if xt == nil { // see test/files/types/eql_0f18 + if dt == nil || dt.Kind() == InterfaceKind { + return nil + } if !maybeNil(dt) { panic(fmt.Sprintf("invalid operation, nil can not be compared to %v", dt)) } diff --git a/gnovm/pkg/gnolang/type_check_test.go b/gnovm/pkg/gnolang/type_check_test.go new file mode 100644 index 00000000000..4b738961e0f --- /dev/null +++ b/gnovm/pkg/gnolang/type_check_test.go @@ -0,0 +1,57 @@ +package gnolang + +import "testing" + +func TestCheckAssignableTo(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + xt Type + dt Type + autoNative bool + wantPanic bool + }{ + { + name: "nil to nil", + xt: nil, + dt: nil, + }, + { + name: "nil and interface", + xt: nil, + dt: &InterfaceType{}, + }, + { + name: "interface to nil", + xt: &InterfaceType{}, + dt: nil, + }, + { + name: "nil to non-nillable", + xt: nil, + dt: PrimitiveType(StringKind), + wantPanic: true, + }, + { + name: "interface to interface", + xt: &InterfaceType{}, + dt: &InterfaceType{}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if tt.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("checkAssignableTo() did not panic, want panic") + } + }() + } + checkAssignableTo(tt.xt, tt.dt, tt.autoNative) + }) + } +} diff --git a/gnovm/tests/files/blankidentifier0.gno b/gnovm/tests/files/blankidentifier0.gno new file mode 100644 index 00000000000..a7447a22a6a --- /dev/null +++ b/gnovm/tests/files/blankidentifier0.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _ = _ +} + +// Error: +// main/files/blankidentifier0.gno:4:6: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier1.gno b/gnovm/tests/files/blankidentifier1.gno new file mode 100644 index 00000000000..9c93ff08f10 --- /dev/null +++ b/gnovm/tests/files/blankidentifier1.gno @@ -0,0 +1,12 @@ +package main + +type zilch interface{} + +func main() { + _ = zilch(nil) + + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/blankidentifier2.gno b/gnovm/tests/files/blankidentifier2.gno new file mode 100644 index 00000000000..a8d06cdabdc --- /dev/null +++ b/gnovm/tests/files/blankidentifier2.gno @@ -0,0 +1,9 @@ +package main + +func main() { + var i _ + println(i) +} + +// Error: +// main/files/blankidentifier2.gno:4:6: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier3.gno b/gnovm/tests/files/blankidentifier3.gno new file mode 100644 index 00000000000..aab1388c92d --- /dev/null +++ b/gnovm/tests/files/blankidentifier3.gno @@ -0,0 +1,10 @@ +package main + +type S _ + +func main() { + println("hey") +} + +// Error: +// main/files/blankidentifier3.gno:3:6: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier4.gno b/gnovm/tests/files/blankidentifier4.gno new file mode 100644 index 00000000000..214102e6c98 --- /dev/null +++ b/gnovm/tests/files/blankidentifier4.gno @@ -0,0 +1,9 @@ +package main + +func main() { + _ = 1 + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/blankidentifier5.gno b/gnovm/tests/files/blankidentifier5.gno new file mode 100644 index 00000000000..0de62bb77c3 --- /dev/null +++ b/gnovm/tests/files/blankidentifier5.gno @@ -0,0 +1,12 @@ +package main + +type foo struct{} + +var _ int = 10 + +func main() { + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/blankidentifier6.gno b/gnovm/tests/files/blankidentifier6.gno new file mode 100644 index 00000000000..a59ea246ad6 --- /dev/null +++ b/gnovm/tests/files/blankidentifier6.gno @@ -0,0 +1,24 @@ +package main + +type Animal interface { + Sound() string +} + +type Dog struct { + name string +} + +func (d Dog) Sound() string { + return "Woof!" +} + +func main() { + var a Animal = Dog{name: "Rex"} + + v := a.(_) + + println(v) +} + +// Error: +// main/files/blankidentifier6.gno:18:13: cannot use _ as value or type diff --git a/gnovm/tests/files/blankidentifier7.gno b/gnovm/tests/files/blankidentifier7.gno new file mode 100644 index 00000000000..4b3a50d2135 --- /dev/null +++ b/gnovm/tests/files/blankidentifier7.gno @@ -0,0 +1,13 @@ +package main + +import "strconv" + +var value int = 0 +func Foo(_ string) string { return strconv.Itoa(value) } + +func main() { + println(Foo("")) +} + +// Output: +// 0 diff --git a/gnovm/tests/files/typeassert10.gno b/gnovm/tests/files/typeassert10.gno new file mode 100644 index 00000000000..5876111b324 --- /dev/null +++ b/gnovm/tests/files/typeassert10.gno @@ -0,0 +1,17 @@ +package main + +type ( + nat []word + word int +) + +func main() { + var a nat + b := []word{0} + a = b + + println(a) +} + +// Output: +// (slice[(0 main.word)] main.nat) From 551bd66d9a42d24886e7966a1aabb456ba6b4e06 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:58:49 +0100 Subject: [PATCH 08/86] fix(gnodev): timestamp issue on reload (#2943) - [x] depends on #2941 resolve #1509 This PR addresses the timestamp issue on gnodev by implementing the `MetadataTX` changes from #2941. Timestamps will now be correctly handled for `Reload` and `import`/`export`. For `Reset`, the timestamp will be updated to the current time. cc @zivkovicmilos @thehowl #### ~TODOs~ - [x] test replays (I've only tested it manually)
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 - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/setup_node.go | 2 +- contribs/gnodev/pkg/dev/node.go | 22 ++- contribs/gnodev/pkg/dev/node_state.go | 2 +- contribs/gnodev/pkg/dev/node_test.go | 217 ++++++++++++++++++++++- contribs/gnodev/pkg/dev/packages.go | 34 ++-- 5 files changed, 247 insertions(+), 30 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 4b3619b4a7d..a2b1970d0ef 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -43,7 +43,7 @@ func setupDevNode( nodeConfig.InitialTxs[index] = nodeTx } - logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(nodeConfig.InitialTxs)) + logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs)) } return gnodev.NewDevNode(ctx, nodeConfig) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 9b3f838b8a0..e0ed64aad36 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "sync" + "time" "unicode" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" @@ -84,6 +85,9 @@ type Node struct { // keep track of number of loaded package to be able to skip them on restore loadedPackages int + // track starting time for genesis + startTime time.Time + // state initialState, state []gnoland.TxWithMetadata currentStateIndex int @@ -97,7 +101,8 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { return nil, fmt.Errorf("unable map pkgs list: %w", err) } - pkgsTxs, err := mpkgs.Load(DefaultFee) + startTime := time.Now() + pkgsTxs, err := mpkgs.Load(DefaultFee, startTime) if err != nil { return nil, fmt.Errorf("unable to load genesis packages: %w", err) } @@ -110,6 +115,7 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { pkgs: mpkgs, logger: cfg.Logger, loadedPackages: len(pkgsTxs), + startTime: startTime, state: cfg.InitialTxs, initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), @@ -173,9 +179,10 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs)) for i, encodedTx := range b.Block.Data.Txs { + // fallback on std tx var tx std.Tx if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { - return nil, fmt.Errorf("unable to unmarshal amino tx, %w", unmarshalErr) + return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr) } txs[i] = gnoland.TxWithMetadata{ @@ -268,8 +275,11 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to stop the node: %w", err) } + // Reset starting time + startTime := time.Now() + // Generate a new genesis state based on the current packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -289,6 +299,7 @@ func (n *Node) Reset(ctx context.Context) error { n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) + n.startTime = startTime n.emitter.Emit(&events.Reset{}) return nil } @@ -358,7 +369,6 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata genesis := n.GenesisDoc().AppState.(gnoland.GnoGenesisState) initialTxs := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages - state := append([]gnoland.TxWithMetadata{}, initialTxs...) lastBlock := n.getLatestBlockNumber() @@ -397,7 +407,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") - txs, err := n.pkgs.Load(DefaultFee) + txs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -413,7 +423,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 7504580b333..73362a5f1c8 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -84,7 +84,7 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 11b0a2090d7..e05e5a996fa 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -2,9 +2,11 @@ package dev import ( "context" + "encoding/json" "os" "path/filepath" "testing" + "time" mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" @@ -15,8 +17,10 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" + tm2events "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -221,6 +225,191 @@ func Render(_ string) string { return str } assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) } +func TestTxTimestampRecover(t *testing.T) { + const ( + // foo package + foobarGnoMod = "module gno.land/r/dev/foo\n" + fooFile = `package foo +import ( + "strconv" + "strings" + "time" +) + +var times = []time.Time{ + time.Now(), // Evaluate at genesis +} + +func SpanTime() { + times = append(times, time.Now()) +} + +func Render(_ string) string { + var strs strings.Builder + + strs.WriteRune('[') + for i, t := range times { + if i > 0 { + strs.WriteRune(',') + } + strs.WriteString(strconv.Itoa(int(t.UnixNano()))) + } + strs.WriteRune(']') + + return strs.String() +} +` + ) + + // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + parseJSONTimesList := func(t *testing.T, render string) []time.Time { + t.Helper() + + var times []time.Time + var nanos []int64 + + err := json.Unmarshal([]byte(render), &nanos) + require.NoError(t, err) + + for _, nano := range nanos { + sec, nsec := nano/int64(time.Second), nano%int64(time.Second) + times = append(times, time.Unix(sec, nsec)) + } + + return times + } + + // Generate package foo + foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + + // Call NewDevNode with no package should work + cfg := createDefaultTestingNodeConfig(foopkg) + + // XXX(gfanton): Setting this to `false` somehow makes the time block + // drift from the time spanned by the VM. + cfg.TMConfig.Consensus.SkipTimeoutCommit = false + cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond + cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond + cfg.TMConfig.Consensus.CreateEmptyBlocks = true + + node, emitter := newTestingDevNodeWithConfig(t, cfg) + + // We need to make sure that blocks are separated by at least 1 second + // (minimal time between blocks). We can ensure this by listening for + // new blocks and comparing timestamps + cc := make(chan types.EventNewBlock) + node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) { + newBlock, ok := evt.(types.EventNewBlock) + if !ok { + return + } + + select { + case cc <- newBlock: + default: + } + }) + + // wait for first block for reference + var refHeight, refTimestamp int64 + + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case res := <-cc: + refTimestamp = res.Block.Time.Unix() + refHeight = res.Block.Height + } + + // number of span to process + const nevents = 3 + + // Span multiple time + for i := 0; i < nevents; i++ { + t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) + for { + var block types.EventNewBlock + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case block = <-cc: + } + + t.Logf("got a block height(%d) and unix(%d)", + block.Block.Height, block.Block.Time.Unix()) + + // Ensure we consume every block before tx block + if refHeight >= block.Block.Height { + continue + } + + // Ensure new block timestamp is before previous reference timestamp + if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp { + refTimestamp = newRefTimestamp + break // break the loop + } + } + + t.Logf("found a valid block(%d)! continue", refHeight) + + // Span a new time + msg := vm.MsgCall{ + PkgPath: "gno.land/r/dev/foo", + Func: "SpanTime", + } + + res, err := testingCallRealm(t, node, msg) + + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + + // Set the new height from the tx as reference + refHeight = res.Height + } + + // Render JSON times list + render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + // Parse times list + timesList1 := parseJSONTimesList(t, render) + t.Logf("list of times: %+v", timesList1) + + // Ensure times are correctly expending. + for i, t2 := range timesList1 { + if i == 0 { + continue + } + + t1 := timesList1[i-1] + require.Greater(t, t2.UnixNano(), t1.UnixNano()) + } + + // Reload the node + err = node.Reload(context.Background()) + require.NoError(t, err) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) + + // Fetch time list again from render + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + timesList2 := parseJSONTimesList(t, render) + + // Times list should be identical from the orignal list + require.Len(t, timesList2, len(timesList1)) + for i := 0; i < len(timesList1); i++ { + t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() + assert.Equal(t, t1nsec, t2nsec, + "comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec) + } +} + func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { t.Helper() @@ -285,25 +474,37 @@ func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { } } +func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { + cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg.PackagesPathList = pkgslist + return cfg +} + func newTestingDevNode(t *testing.T, pkgslist ...PackagePath) (*Node, *mock.ServerEmitter) { t.Helper() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - logger := log.NewTestingLogger(t) + cfg := createDefaultTestingNodeConfig(pkgslist...) + return newTestingDevNodeWithConfig(t, cfg) +} + +func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) + logger := log.NewTestingLogger(t) emitter := &mock.ServerEmitter{} - // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.PackagesPathList = pkgslist cfg.Emitter = emitter cfg.Logger = logger + node, err := NewDevNode(ctx, cfg) require.NoError(t, err) - assert.Len(t, node.ListPkgs(), len(pkgslist)) + assert.Len(t, node.ListPkgs(), len(cfg.PackagesPathList)) - t.Cleanup(func() { node.Close() }) + t.Cleanup(func() { + node.Close() + cancel() + }) return node, emitter } diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index 7ee628ce39e..cccbf316525 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "path/filepath" + "time" "github.com/gnolang/gno/contribs/gnodev/pkg/address" "github.com/gnolang/gno/gno.land/pkg/gnoland" @@ -119,7 +120,7 @@ func (pm PackagesMap) toList() gnomod.PkgList { return list } -func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) { +func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetadata, error) { pkgs := pm.toList() sorted, err := pkgs.Sort() @@ -128,8 +129,8 @@ func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) { } nonDraft := sorted.GetNonDraftPkgs() - txs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) + metatxs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) for _, modPkg := range nonDraft { pkg := pm[modPkg.Dir] if pkg.Creator.IsZero() { @@ -143,22 +144,27 @@ func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) { } // Create transaction - tx := gnoland.TxWithMetadata{ - Tx: std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: pkg.Creator, - Deposit: pkg.Deposit, - Package: memPkg, - }, + tx := std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: pkg.Creator, + Deposit: pkg.Deposit, + Package: memPkg, }, }, } - tx.Tx.Signatures = make([]std.Signature, len(tx.Tx.GetSigners())) - txs = append(txs, tx) + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + metatx := gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: start.Unix(), + }, + } + + metatxs = append(metatxs, metatx) } - return txs, nil + return metatxs, nil } From c64e0df5a6beaeeab29f60c69537087f91b49527 Mon Sep 17 00:00:00 2001 From: Sergio Maria Matone Date: Wed, 27 Nov 2024 13:15:30 +0100 Subject: [PATCH 09/86] feat: Adding an official Gnocontribs image in release process (#3219) --- .github/goreleaser.yaml | 103 ++++++++++++++++++++++++++++------------ Dockerfile.release | 40 +++++++++------- 2 files changed, 96 insertions(+), 47 deletions(-) diff --git a/.github/goreleaser.yaml b/.github/goreleaser.yaml index b5fb07d0578..71a8ba98745 100644 --- a/.github/goreleaser.yaml +++ b/.github/goreleaser.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json project_name: gno version: 2 @@ -86,6 +87,8 @@ builds: goarm: - "6" - "7" + # Gno Contribs + # NOTE: Contribs binary will be added in a single docker image below: gnocontribs - id: gnobro dir: ./contribs/gnodev/cmd/gnobro binary: gnobro @@ -101,6 +104,21 @@ builds: goarm: - "6" - "7" + - id: gnogenesis + dir: ./contribs/gnogenesis + binary: gnogenesis + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - "6" + - "7" gomod: proxy: true @@ -300,6 +318,7 @@ dockers: - gno.land/genesis/genesis_txs.jsonl - examples - gnovm/stdlibs + # gnokey - use: buildx dockerfile: Dockerfile.release @@ -504,73 +523,97 @@ dockers: ids: - gnofaucet - # gnobro + # gnocontribs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: amd64 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-amd64" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: arm64 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-arm64v8" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/arm64/v8" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: arm goarm: 6 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/arm/v6" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs - use: buildx dockerfile: Dockerfile.release goos: linux goarch: arm goarm: 7 image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7" build_flag_templates: - - "--target=gnobro" + - "--target=gnocontribs" - "--platform=linux/arm/v7" - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs docker_manifests: # https://goreleaser.com/customization/docker_manifest/ @@ -645,19 +688,19 @@ docker_manifests: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv6 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv7 - # gnobro - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv7 + # gnocontribs + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7 docker_signs: - cmd: cosign diff --git a/Dockerfile.release b/Dockerfile.release index 481100c85c3..c7bb1b582ed 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -11,11 +11,11 @@ CMD [ "" ] ## ghcr.io/gnolang/gno/gnoland FROM base as gnoland -COPY ./gnoland /usr/bin/gnoland -COPY ./examples /gnoroot/examples/ -COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ -COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt -COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +COPY ./gnoland /usr/bin/gnoland +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl EXPOSE 26656 26657 @@ -44,21 +44,27 @@ COPY ./gnofaucet /usr/bin/gnofaucet EXPOSE 5050 ENTRYPOINT [ "/usr/bin/gnofaucet" ] -# -## ghcr.io/gnolang/gno/gnobro -FROM base as gnobro - -COPY ./gnobro /usr/bin/gnobro -EXPOSE 22 -ENTRYPOINT [ "/usr/bin/gnobro" ] - # ## ghcr.io/gnolang/gno FROM base as gno -COPY ./gno /usr/bin/gno -COPY ./examples /gnoroot/examples/ -COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ -COPY ./gnovm/tests/stdlibs /gnoroot/gnovm/tests/stdlibs/ +COPY ./gno /usr/bin/gno +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gnovm/tests/stdlibs /gnoroot/gnovm/tests/stdlibs/ ENTRYPOINT [ "/usr/bin/gno" ] + +# +## ghcr.io/gnolang/gnocontribs +FROM base as gnocontribs + +COPY ./gnobro /usr/bin/gnobro +COPY ./gnogenesis /usr/bin/gnogenesis +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +EXPOSE 22 + +ENTRYPOINT [ "/bin/sh", "-c" ] From c1ae90b50a0346316c7f106e96b897f502f02824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Wed, 27 Nov 2024 13:19:24 +0100 Subject: [PATCH 10/86] chore: add balances restore to the portal loop (#3220) ## Description This PR allows for a balances restore for the portal loop.
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 - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- misc/loop/cmd/snapshotter.go | 1 + misc/loop/scripts/pull-gh.sh | 18 ++++++++++++++++++ misc/loop/scripts/start.sh | 3 +++ 3 files changed, 22 insertions(+) diff --git a/misc/loop/cmd/snapshotter.go b/misc/loop/cmd/snapshotter.go index 2dda5d568d9..0173f9aad03 100644 --- a/misc/loop/cmd/snapshotter.go +++ b/misc/loop/cmd/snapshotter.go @@ -150,6 +150,7 @@ func (s snapshotter) startPortalLoopContainer(ctx context.Context) (*types.Conta Env: []string{ "MONIKER=the-portal-loop", "GENESIS_BACKUP_FILE=/backups/backup.jsonl", + "GENESIS_BALANCES_FILE=/backups/balances.jsonl", }, Entrypoint: []string{"/scripts/start.sh"}, ExposedPorts: nat.PortSet{ diff --git a/misc/loop/scripts/pull-gh.sh b/misc/loop/scripts/pull-gh.sh index efbb360d551..55ee0f7762c 100755 --- a/misc/loop/scripts/pull-gh.sh +++ b/misc/loop/scripts/pull-gh.sh @@ -6,6 +6,10 @@ TMP_DIR=temp-tx-exports # that the portal loop use when looping (generating the genesis) MASTER_BACKUP_FILE="backup.jsonl" +# The master balances file will contain the ultimate balances +# backup that the portal loop uses when looping (generating the genesis) +MASTER_BALANCES_FILE="balances.jsonl" + # Clones the portal loop backups subdirectory, located in BACKUPS_REPO (tx-exports) pullGHBackups () { BACKUPS_REPO=https://github.com/gnolang/tx-exports.git @@ -32,8 +36,10 @@ pullGHBackups # Combine the pulled backups into a single backup file TXS_BACKUPS_PREFIX="backup_portal_loop_txs_" +BALANCES_BACKUP_NAME="backup_portal_loop_balances.jsonl" find . -type f -name "${TXS_BACKUPS_PREFIX}*.jsonl" | sort | xargs cat > "temp_$MASTER_BACKUP_FILE" +find . -type f -name "${BALANCES_BACKUP_NAME}" | sort | xargs cat > "temp_$MASTER_BALANCES_FILE" BACKUPS_DIR="../backups" TIMESTAMP=$(date +%s) @@ -47,10 +53,22 @@ if [ -e "$BACKUPS_DIR/$MASTER_BACKUP_FILE" ]; then echo "Renamed $MASTER_BACKUP_FILE to ${MASTER_BACKUP_FILE}-legacy-$TIMESTAMP" fi +# Check if the master balances backup file already exists +if [ -e "$BACKUPS_DIR/$MASTER_BALANCES_FILE" ]; then + # Back up the existing master txs file + echo "Master balances backup file exists, backing up..." + mv "$BACKUPS_DIR/$MASTER_BALANCES_FILE" "$BACKUPS_DIR/${MASTER_BALANCES_FILE}-legacy-$TIMESTAMP" + + echo "Renamed $MASTER_BALANCES_FILE to ${MASTER_BALANCES_FILE}-legacy-$TIMESTAMP" +fi + # Use the GitHub state as the canonical backup mv "temp_$MASTER_BACKUP_FILE" "$BACKUPS_DIR/$MASTER_BACKUP_FILE" echo "Moved temp_$MASTER_BACKUP_FILE to $BACKUPS_DIR/$MASTER_BACKUP_FILE" +mv "temp_$MASTER_BALANCES_FILE" "$BACKUPS_DIR/$MASTER_BALANCES_FILE" +echo "Moved temp_$MASTER_BALANCES_FILE to $BACKUPS_DIR/$MASTER_BALANCES_FILE" + # Clean up the temporary directory cd .. rm -rf $TMP_DIR diff --git a/misc/loop/scripts/start.sh b/misc/loop/scripts/start.sh index 6dd57b2c041..db36de39f2a 100755 --- a/misc/loop/scripts/start.sh +++ b/misc/loop/scripts/start.sh @@ -7,12 +7,15 @@ RPC_LADDR=${RPC_LADDR:-"tcp://0.0.0.0:26657"} CHAIN_ID=${CHAIN_ID:-"portal-loop"} GENESIS_BACKUP_FILE=${GENESIS_BACKUP_FILE:-""} +GENESIS_BALANCES_FILE=${GENESIS_BALANCES_FILE:-""} SEEDS=${SEEDS:-""} PERSISTENT_PEERS=${PERSISTENT_PEERS:-""} echo "" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +echo "" >> /gnoroot/gno.land/genesis/genesis_balances.jsonl cat "${GENESIS_BACKUP_FILE}" >> /gnoroot/gno.land/genesis/genesis_txs.jsonl +cat "${GENESIS_BALANCES_FILE}" >> /gnoroot/gno.land/genesis/genesis_balances.jsonl # Initialize the secrets gnoland secrets init From e3e59c268407e1e80a2e8e41d28f058936f3e4e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:19:25 +0100 Subject: [PATCH 11/86] chore(deps): bump the actions group across 1 directory with 2 updates (#3214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions group with 2 updates in the / directory: [tj-actions/changed-files](https://github.com/tj-actions/changed-files) and [anchore/sbom-action](https://github.com/anchore/sbom-action). Updates `tj-actions/changed-files` from 41 to 45
Release notes

Sourced from tj-actions/changed-files's releases.

v45

Changes in v45.0.4

What's Changed

Full Changelog: https://github.com/tj-actions/changed-files/compare/v45...v45.0.4


Changes in v45.0.3

What's Changed

... (truncated)

Changelog

Sourced from tj-actions/changed-files's changelog.

Changelog

45.0.4 - (2024-11-05)

🚀 Features

  • Prevent ignore files warning (#2318) (1f772e9) - (Tonye Jack)

🐛 Bug Fixes

  • deps: Update dependency @​actions/core to v1.11.1 (4d0aab9) - (renovate[bot])

➕ Add

  • Added missing changes and modified dist assets. (9d7201d) - (GitHub Action)
  • Added missing changes and modified dist assets. (0104c75) - (GitHub Action)

📝 Other

⚙️ Miscellaneous Tasks

  • deps: Update dependency eslint-plugin-jest to v28.9.0 (4edd678) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.9.0 (f082558) - (renovate[bot])
  • deps: Lock file maintenance (92c02a0) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.7 (b702211) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.6 (435fd74) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.5 (0626fa3) - (renovate[bot])
  • deps: Update dependency @​types/lodash to v4.17.13 (8817a79) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.4 (5417491) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.2 (84ef162) - (renovate[bot])
  • deps: Lock file maintenance (b672a51) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.1 (678cdc2) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.8.0 (27b7bbb) - (renovate[bot])
  • deps: Update actions/setup-node action to v4.1.0 (8361072) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.9 (21acf46) - (renovate[bot])
  • deps: Update dependency @​types/jest to v29.5.14 (f356b3c) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.8 (66275de) - (renovate[bot])
  • deps: Lock file maintenance (a16702b) - (renovate[bot])
  • deps: Update dependency @​types/lodash to v4.17.12 (aa11897) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.7 (6513fe1) - (renovate[bot])
  • deps: Update dependency @​types/lodash to v4.17.11 (45e0c78) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.6 (a949a83) - (renovate[bot])
  • deps: Lock file maintenance (f93ff33) - (renovate[bot])
  • deps: Update dependency typescript to v5.6.3 (729c704) - (renovate[bot])
  • deps: Update dependency @​types/node to v22.7.5 (2009d44) - (renovate[bot])
  • deps: Lock file maintenance (b693fc2) - (renovate[bot])

... (truncated)

Commits
  • 4edd678 chore(deps): update dependency eslint-plugin-jest to v28.9.0
  • f082558 chore(deps): update dependency @​types/node to v22.9.0
  • 92c02a0 chore(deps): lock file maintenance
  • b702211 chore(deps): update dependency @​types/node to v22.8.7
  • 435fd74 chore(deps): update dependency @​types/node to v22.8.6
  • 0626fa3 chore(deps): update dependency @​types/node to v22.8.5
  • 8817a79 chore(deps): update dependency @​types/lodash to v4.17.13
  • 5417491 chore(deps): update dependency @​types/node to v22.8.4
  • 84ef162 chore(deps): update dependency @​types/node to v22.8.2
  • b672a51 chore(deps): lock file maintenance
  • Additional commits viewable in compare view

Updates `anchore/sbom-action` from 0.17.7 to 0.17.8
Release notes

Sourced from anchore/sbom-action's releases.

v0.17.8

Changes in v0.17.8

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. Screenshot 2024-11-05 at 18 37 34 - 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). Screenshot 2024-11-06 at 01 36 42 #### 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: Screenshot 2024-11-08 at 16 29 49 Home page after (also resolves usernames from `r/demo/users`): Screenshot 2024-11-09 at 13 19 55 Prop page before: Screenshot 2024-11-08 at 16 30 43 Prop page after: Screenshot 2024-11-21 at 13 05 50 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}} +
+{{ end }} +
+{{ end }} +
+{{ end }} diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/internal/check/comment_test.go similarity index 86% rename from contribs/github-bot/comment_test.go rename to contribs/github-bot/internal/check/comment_test.go index fd8790dd9e1..0334b76f95c 100644 --- a/contribs/github-bot/comment_test.go +++ b/contribs/github-bot/internal/check/comment_test.go @@ -1,4 +1,4 @@ -package main +package check import ( "context" @@ -108,19 +108,34 @@ func TestCommentUpdateHandler(t *testing.T) { } gh := newGHClient() - // Exit without error because EventName is empty + // 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 + // 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' + // 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 + // 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 without error can't get open pull request associated with PR num. + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + mockOptions = append(mockOptions, mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/42", + Method: "GET", + }, + github.PullRequest{Number: github.Int(42), State: github.String(utils.PRStateOpen)}, + )) + gh = newGHClient() + + // Exit with error because mock not setup to return authUser. assert.Error(t, handleCommentUpdate(gh, actionCtx)) mockOptions = append(mockOptions, mock.WithRequestMatchPages( mock.EndpointPattern{ @@ -132,31 +147,27 @@ func TestCommentUpdateHandler(t *testing.T) { gh = newGHClient() actionCtx.Actor = bot - // Exit with error because authUser and action actor is the same user + // 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 + // 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 + // 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 + // 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 + // 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 + // Exit with error because checkboxes are differents. assert.Error(t, handleCommentUpdate(gh, actionCtx)) actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from") diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go index 474146ad3da..a5c875e0d22 100644 --- a/contribs/github-bot/internal/client/client.go +++ b/contribs/github-bot/internal/client/client.go @@ -7,8 +7,7 @@ import ( "os" "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/google/go-github/v64/github" ) @@ -32,21 +31,28 @@ type GitHub struct { Repo string } +type Config struct { + Owner string + Repo string + Verbose bool + DryRun bool +} + // GetBotComment retrieves the bot's (current user) comment on provided PR number. func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { - // List existing comments + // List existing comments. const ( sort = "created" direction = "desc" ) - // Get current user (bot) + // 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 + // Pagination option. opts := &github.IssueListCommentsOptions{ Sort: github.String(sort), Direction: github.String(direction), @@ -67,7 +73,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err) } - // Get the comment created by current user + // Get the comment created by current user. for _, comment := range comments { if comment.GetUser().GetLogin() == currentUser.GetLogin() { return comment, nil @@ -86,7 +92,12 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { // 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 + // Prevent updating anything in dry run mode. + if gh.DryRun { + return nil, errors.New("should not write bot comment in dry run mode") + } + + // 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( @@ -119,6 +130,17 @@ func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, e return editComment, nil } +func (gh *GitHub) GetOpenedPullRequest(prNum int) (*github.PullRequest, error) { + 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()) + } + + return pr, nil +} + // ListTeamMembers lists the members of the specified team. func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { var ( @@ -268,25 +290,25 @@ func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) { } // New initializes the API client, the logger, and creates an instance of GitHub. -func New(ctx context.Context, params *p.Params) (*GitHub, error) { +func New(ctx context.Context, cfg *Config) (*GitHub, error) { gh := &GitHub{ Ctx: ctx, - Owner: params.Owner, - Repo: params.Repo, - DryRun: params.DryRun, + Owner: cfg.Owner, + Repo: cfg.Repo, + DryRun: cfg.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) + // a logger suitable for terminal output or the GitHub Actions web interface. + gh.Logger = logger.NewLogger(cfg.Verbose) - // Retrieve GitHub API token from env + // 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 + // Init GitHub API client using token. gh.Client = github.NewClient(nil).WithAuthToken(token) return gh, nil diff --git a/contribs/github-bot/config.go b/contribs/github-bot/internal/config/config.go similarity index 54% rename from contribs/github-bot/config.go rename to contribs/github-bot/internal/config/config.go index 4a28565ef7f..ac1d185f759 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -1,4 +1,4 @@ -package main +package config import ( "github.com/gnolang/gno/contribs/github-bot/internal/client" @@ -9,37 +9,37 @@ import ( type Teams []string // 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. +type AutomaticCheck struct { + Description string + If c.Condition // If the condition is met, the rule is displayed and the requirement is executed. + Then 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 Teams // Members of these teams can check the checkbox to make the check pass. +type ManualCheck struct { + Description string + If c.Condition // If the condition is met, a checkbox will be displayed on bot comment. + 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 // in which the GitHub client is injected. -func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { - auto := []automaticCheck{ +func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { + auto := []AutomaticCheck{ { - description: "Maintainers must be able to edit this pull request", - ifC: c.Always(), - thenR: r.MaintainerCanModify(), + Description: "Maintainers must be able to edit this pull request", + If: c.Always(), + Then: 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: "The pull request head branch must be up-to-date with its base", + If: c.Always(), + Then: 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( + Description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", + If: c.FileChanged(gh, "^docs/"), + Then: r.Or( r.And( r.AuthorInTeam(gh, "devrels"), r.ReviewByTeamMembers(gh, "tech-staff", 1), @@ -52,15 +52,15 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { }, } - manual := []manualCheck{ + manual := []ManualCheck{ { - description: "The pull request description provides enough details", - ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")), - teams: Teams{"core-contributors"}, + Description: "The pull request description provides enough details", + If: c.Not(c.AuthorInTeam(gh, "core-contributors")), + Teams: Teams{"core-contributors"}, }, { - description: "Determine if infra needs to be updated before merging", - ifC: c.And( + Description: "Determine if infra needs to be updated before merging", + If: c.And( c.BaseBranch("master"), c.Or( c.FileChanged(gh, `Dockerfile`), @@ -70,17 +70,17 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`), ), ), - teams: Teams{"devops"}, + Teams: Teams{"devops"}, }, } // 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) + if _, exists := unique[rule.Description]; exists { + gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.Description) } - unique[rule.description] = struct{}{} + unique[rule.Description] = struct{}{} } return auto, manual diff --git a/contribs/github-bot/internal/matrix/cmd.go b/contribs/github-bot/internal/matrix/cmd.go new file mode 100644 index 00000000000..8bcc3a34424 --- /dev/null +++ b/contribs/github-bot/internal/matrix/cmd.go @@ -0,0 +1,53 @@ +package matrix + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type matrixFlags struct { + verbose *bool + matrixKey string + flagSet *flag.FlagSet +} + +func NewMatrixCmd(verbose *bool) *commands.Command { + flags := &matrixFlags{verbose: verbose} + + return commands.NewCommand( + commands.Metadata{ + Name: "matrix", + ShortUsage: "github-bot matrix [flags]", + ShortHelp: "parses GitHub Actions event and defines matrix accordingly", + LongHelp: "This tool retrieves the GitHub Actions context, parses the attached event, and defines the matrix with the pull request numbers to be processed accordingly", + }, + flags, + func(_ context.Context, _ []string) error { + flags.validateFlags() + return execMatrix(flags) + }, + ) +} + +func (flags *matrixFlags) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &flags.matrixKey, + "matrix-key", + "", + "key of the matrix to set in Github Actions output (required)", + ) + + flags.flagSet = fs +} + +func (flags *matrixFlags) validateFlags() { + if flags.matrixKey == "" { + fmt.Fprintf(flags.flagSet.Output(), "Error: no matrix-key provided\n\n") + flags.flagSet.Usage() + os.Exit(1) + } +} diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go new file mode 100644 index 00000000000..9c8f12e4214 --- /dev/null +++ b/contribs/github-bot/internal/matrix/matrix.go @@ -0,0 +1,139 @@ +package matrix + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/sethvargo/go-githubactions" +) + +func execMatrix(flags *matrixFlags) 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) + } + + // If verbose is set, print the Github Actions event for debugging purpose. + if *flags.verbose { + jsonBytes, err := json.MarshalIndent(actionCtx.Event, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal event to json: %w", err) + } + fmt.Println("Event:", string(jsonBytes)) + } + + // Init Github client using only GitHub Actions context. + owner, repo := actionCtx.Repo() + gh, err := client.New(context.Background(), &client.Config{ + Owner: owner, + Repo: repo, + Verbose: *flags.verbose, + DryRun: true, + }) + 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 + } + + // Format PR list for GitHub Actions matrix definition. + bytes, err := prList.MarshalText() + if err != nil { + return fmt.Errorf("unable to marshal PR list: %w", err) + } + matrix := fmt.Sprintf("%s=[%s]", flags.matrixKey, string(bytes)) + + // If verbose is set, print the matrix for debugging purpose. + if *flags.verbose { + fmt.Printf("Matrix: %s\n", matrix) + } + + // Get the path of the GitHub Actions environment file used for output. + output, ok := os.LookupEnv("GITHUB_OUTPUT") + if !ok { + return errors.New("unable to get GITHUB_OUTPUT var") + } + + // Open GitHub Actions output file + file, err := os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("unable to open GitHub Actions output file: %w", err) + } + defer file.Close() + + // Append matrix to GitHub Actions output file + if _, err := fmt.Fprintf(file, "%s\n", matrix); err != nil { + return fmt.Errorf("unable to write matrix in GitHub Actions output file: %w", err) + } + + return nil +} + +func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (utils.PRList, error) { + var prList utils.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(utils.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) + } + } + + // 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 = utils.PRList{prNum} + + default: + return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName) + } + + // Then only keep provided PR that are opened. + var openedPRList utils.PRList = nil + for _, prNum := range prList { + if _, err := gh.GetOpenedPullRequest(prNum); err != nil { + gh.Logger.Warningf("Can't get PR from event: %v", err) + } else { + openedPRList = append(openedPRList, prNum) + } + } + + return openedPRList, nil +} diff --git a/contribs/github-bot/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go similarity index 91% rename from contribs/github-bot/matrix_test.go rename to contribs/github-bot/internal/matrix/matrix_test.go index bce4ec1bd8f..fe5b7452a49 100644 --- a/contribs/github-bot/matrix_test.go +++ b/contribs/github-bot/internal/matrix/matrix_test.go @@ -1,4 +1,4 @@ -package main +package matrix import ( "context" @@ -9,7 +9,6 @@ import ( "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" @@ -34,7 +33,7 @@ func TestProcessEvent(t *testing.T) { name string gaCtx *githubactions.GitHubContext prs []*github.PullRequest - expectedPRList params.PRList + expectedPRList utils.PRList expectedError bool }{ { @@ -44,7 +43,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"issue": map[string]any{"number": 1.}}, }, prs, - params.PRList{1}, + utils.PRList{1}, false, }, { "valid pull_request event", @@ -53,7 +52,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, }, prs, - params.PRList{1}, + utils.PRList{1}, false, }, { "valid pull_request_target event", @@ -62,7 +61,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, }, prs, - params.PRList{1}, + utils.PRList{1}, false, }, { "invalid event (PR number not set)", @@ -71,7 +70,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"issue": nil}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid event name", @@ -80,7 +79,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"issue": map[string]any{"number": 1.}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "valid workflow_dispatch all", @@ -89,7 +88,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, }, openPRs, - params.PRList{1, 2, 3}, + utils.PRList{1, 2, 3}, false, }, { "valid workflow_dispatch all (no prs)", @@ -98,7 +97,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, }, nil, - params.PRList{}, + utils.PRList(nil), false, }, { "valid workflow_dispatch list", @@ -107,7 +106,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}}, }, prs, - params.PRList{1, 2, 3}, + utils.PRList{1, 2, 3}, false, }, { "valid workflow_dispatch list with spaces", @@ -116,7 +115,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}}, }, prs, - params.PRList{1, 2, 3}, + utils.PRList{1, 2, 3}, false, }, { "invalid workflow_dispatch list (1 closed)", @@ -125,8 +124,8 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}}, }, prs, - params.PRList(nil), - true, + utils.PRList{1, 2, 3}, + false, }, { "invalid workflow_dispatch list (1 doesn't exist)", &githubactions.GitHubContext{ @@ -134,8 +133,8 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}}, }, prs, - params.PRList(nil), - true, + utils.PRList(nil), + false, }, { "invalid workflow_dispatch list (all closed)", &githubactions.GitHubContext{ @@ -143,8 +142,8 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}}, }, prs, - params.PRList(nil), - true, + utils.PRList(nil), + false, }, { "invalid workflow_dispatch list (empty)", &githubactions.GitHubContext{ @@ -152,7 +151,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid workflow_dispatch list (unset)", @@ -161,7 +160,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": ""}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid workflow_dispatch list (not a number list)", @@ -170,7 +169,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, { "invalid workflow_dispatch list (number list with invalid elem)", @@ -179,7 +178,7 @@ func TestProcessEvent(t *testing.T) { Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}}, }, prs, - params.PRList(nil), + utils.PRList(nil), true, }, } { @@ -214,7 +213,7 @@ func TestProcessEvent(t *testing.T) { prNumStr := parts[len(parts)-1] prNum, err = strconv.Atoi(prNumStr) if err != nil { - panic(err) // Should never happen + panic(err) // Should never happen. } } diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go index d72e8ad2a19..aa86fb0054d 100644 --- a/contribs/github-bot/internal/requirements/assignee_test.go +++ b/contribs/github-bot/internal/requirements/assignee_test.go @@ -45,7 +45,7 @@ func TestAssignee(t *testing.T) { mock.WithRequestMatchHandler( mock.EndpointPattern{ Pattern: "/repos/issues/0/assignees", - Method: "GET", // It looks like this mock package doesn't support mocking POST requests + Method: "GET", // It looks like this mock package doesn't support mocking POST requests. }, http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { requested = true diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go index b686a093015..6481285ae82 100644 --- a/contribs/github-bot/internal/requirements/branch.go +++ b/contribs/github-bot/internal/requirements/branch.go @@ -29,7 +29,7 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre } head := pr.GetHead().GetRef() - // If pull request is open from a fork, prepend head ref with fork owner login + // 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()) } diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go index 7e991b55756..631bff9e64b 100644 --- a/contribs/github-bot/internal/requirements/label_test.go +++ b/contribs/github-bot/internal/requirements/label_test.go @@ -45,7 +45,7 @@ func TestLabel(t *testing.T) { mock.WithRequestMatchHandler( mock.EndpointPattern{ Pattern: "/repos/issues/0/labels", - Method: "GET", // It looks like this mock package doesn't support mocking POST requests + Method: "GET", // It looks like this mock package doesn't support mocking POST requests. }, http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { requested = true diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go index 91b8ac7e6b4..3e08a8e1548 100644 --- a/contribs/github-bot/internal/utils/actions.go +++ b/contribs/github-bot/internal/utils/actions.go @@ -23,7 +23,7 @@ func IndexMap(m map[string]any, keys ...string) any { return nil } -// Retrieve PR number from GitHub Actions context +// Retrieve PR number from GitHub Actions context. func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) { firstKey := "" diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go index 564b7d3fb38..26d7d54d477 100644 --- a/contribs/github-bot/internal/utils/github_const.go +++ b/contribs/github-bot/internal/utils/github_const.go @@ -1,14 +1,14 @@ package utils -// GitHub const +// GitHub API const. const ( - // GitHub Actions Event Names + // GitHub Actions Event Names. EventIssueComment = "issue_comment" EventPullRequest = "pull_request" EventPullRequestTarget = "pull_request_target" EventWorkflowDispatch = "workflow_dispatch" - // Pull Request States + // Pull Request States. PRStateOpen = "open" PRStateClosed = "closed" ) diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/utils/prlist.go similarity index 91% rename from contribs/github-bot/internal/params/prlist.go rename to contribs/github-bot/internal/utils/prlist.go index ace7bcbe3b6..2893bf802b5 100644 --- a/contribs/github-bot/internal/params/prlist.go +++ b/contribs/github-bot/internal/utils/prlist.go @@ -1,4 +1,4 @@ -package params +package utils import ( "encoding" @@ -7,6 +7,7 @@ import ( "strings" ) +// Type used to (un)marshal input/output for check and matrix subcommands. type PRList []int // PRList is both a TextMarshaler and a TextUnmarshaler. diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 9895f44dc70..e11fe6ffd78 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -2,25 +2,43 @@ package main import ( "context" + "flag" "os" + "github.com/gnolang/gno/contribs/github-bot/internal/check" + "github.com/gnolang/gno/contribs/github-bot/internal/matrix" "github.com/gnolang/gno/tm2/pkg/commands" ) +type rootFlags struct { + verbose bool +} + func main() { + flags := &rootFlags{} + cmd := commands.NewCommand( commands.Metadata{ ShortUsage: "github-bot [flags]", LongHelp: "Bot that allows for advanced management of GitHub pull requests.", }, - commands.NewEmptyConfig(), + flags, commands.HelpExec, ) cmd.AddSubCommands( - newCheckCmd(), - newMatrixCmd(), + check.NewCheckCmd(&flags.verbose), + matrix.NewMatrixCmd(&flags.verbose), ) cmd.Execute(context.Background(), os.Args[1:]) } + +func (flags *rootFlags) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &flags.verbose, + "verbose", + false, + "set logging level to debug", + ) +} diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go deleted file mode 100644 index 56d6667589a..00000000000 --- a/contribs/github-bot/matrix.go +++ /dev/null @@ -1,117 +0,0 @@ -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 - } - - // 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 -} - -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 -} From a6f1aba4f429a79eb9b895faa45bf5ac53ecd242 Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Tue, 3 Dec 2024 17:19:56 +0700 Subject: [PATCH 21/86] fix(gnovm): handle type alias declaration for PrimitiveType (#3222) Fixes: https://github.com/gnolang/gno/issues/3203
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
--- gnovm/pkg/gnolang/preprocess.go | 7 ++--- gnovm/tests/files/type40.gno | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 gnovm/tests/files/type40.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 6e82786b318..78b11a4ebc5 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2352,6 +2352,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // } *dst = *dt2 } + case PrimitiveType: + dst = tmp.(PrimitiveType) + case *PointerType: + *dst = *(tmp.(*PointerType)) default: panic(fmt.Sprintf("unexpected type declaration type %v", reflect.TypeOf(dst))) @@ -4283,9 +4287,6 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { // predefineNow preprocessed dependent types. panic("should not happen") } - } else { - // all names are declared types. - panic("should not happen") } } else if idx, ok := UverseNode().GetLocalIndex(tx.Name); ok { // uverse name diff --git a/gnovm/tests/files/type40.gno b/gnovm/tests/files/type40.gno new file mode 100644 index 00000000000..65210798007 --- /dev/null +++ b/gnovm/tests/files/type40.gno @@ -0,0 +1,46 @@ +package main + +type ( + // PrimitiveType + Number = int32 + Number2 = Number + + // PointerType + Pointer = *int32 + Pointer2 = Pointer + + // Interface + Interface = interface{} + Interface2 = Interface + + // S + Struct = struct{Name string} + Struct2 = Struct +) + +func fNumber(n Number) { println(n) } +func fPointer(p Pointer) { println(*p) } +func fInterface(i Interface) { println(i) } +func fStruct(s Struct) { println(s.Name) } + +func main() { + var n Number2 = 5 + fNumber(n) + + var num int32 = 6 + var p Pointer2 = &num + fPointer(p) + + var i Interface2 + i = 7 + fInterface(i) + + var s Struct2 = Struct2{Name: "yo"} + fStruct(s) +} + +// Output: +// 5 +// 6 +// 7 +// yo \ No newline at end of file From bc44a39b174cfb85a4daba7e10e1f9ab07f3858d Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:23:02 +0100 Subject: [PATCH 22/86] feat: add grc20reg that works... today (#3135) - [x] Switch to storing a `type XXX func() grc20.Token` instead of a `grc20.Token` directly. - [x] Implement `grc20reg`. - [x] Add new tests in `gnovm/tests` to demonstrate the current VM's management of the cross-realm feature and support potential changes in #2743. - [x] Create a demo in `atomicswap` or a similar application. (https://github.com/gnolang/gno/pull/2510#issuecomment-2480500066) - [x] Try using a `Token.Getter()` helper. (Works! f99654e30) - [ ] Demonstrate how to manage "disappearing" functions during garbage collection by checking if the function pointer is nil or non-resolvable. Alternative to #2516 NOT(!) depending on #2743 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/Makefile | 2 +- examples/gno.land/p/demo/grc/grc20/types.gno | 41 +- examples/gno.land/r/demo/bar20/bar20.gno | 4 +- examples/gno.land/r/demo/bar20/gno.mod | 1 + examples/gno.land/r/demo/foo20/foo20.gno | 4 +- examples/gno.land/r/demo/foo20/gno.mod | 1 + examples/gno.land/r/demo/grc20factory/gno.mod | 1 + .../r/demo/grc20factory/grc20factory.gno | 4 +- examples/gno.land/r/demo/grc20reg/gno.mod | 9 + .../gno.land/r/demo/grc20reg/grc20reg.gno | 76 + .../r/demo/grc20reg/grc20reg_test.gno | 59 + .../r/demo/tests/crossrealm/crossrealm.gno | 28 + .../r/demo/tests/crossrealm_b/crossrealm.gno | 25 + .../r/demo/tests/crossrealm_b/gno.mod | 3 + examples/gno.land/r/demo/tests/tests.gno | 2 + examples/gno.land/r/demo/wugnot/gno.mod | 1 + examples/gno.land/r/demo/wugnot/wugnot.gno | 4 +- gnovm/stdlibs/math/overflow/overflow.gno | 6 +- gnovm/tests/files/zrealm_crossrealm15.gno | 27 + gnovm/tests/files/zrealm_crossrealm16.gno | 24 + gnovm/tests/files/zrealm_crossrealm17.gno | 27 + gnovm/tests/files/zrealm_crossrealm18.gno | 35 + .../files/zrealm_crossrealm19_stdlibs.gno | 32 + gnovm/tests/files/zrealm_crossrealm20.gno | 43 + gnovm/tests/files/zrealm_crossrealm21.gno | 723 ++++++ gnovm/tests/files/zrealm_crossrealm22.gno | 2282 +++++++++++++++++ gnovm/tests/files/zrealm_crossrealm4.gno | 12 +- gnovm/tests/files/zrealm_crossrealm5.gno | 8 +- gnovm/tests/files/zrealm_tests0.gno | 73 +- 29 files changed, 3495 insertions(+), 62 deletions(-) create mode 100644 examples/gno.land/r/demo/grc20reg/gno.mod create mode 100644 examples/gno.land/r/demo/grc20reg/grc20reg.gno create mode 100644 examples/gno.land/r/demo/grc20reg/grc20reg_test.gno create mode 100644 examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno create mode 100644 examples/gno.land/r/demo/tests/crossrealm_b/gno.mod create mode 100644 gnovm/tests/files/zrealm_crossrealm15.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm16.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm17.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm18.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm20.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm21.gno create mode 100644 gnovm/tests/files/zrealm_crossrealm22.gno diff --git a/examples/Makefile b/examples/Makefile index 578b4faf15b..cdc73ee6b3a 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -45,7 +45,7 @@ test: .PHONY: lint lint: - go run ../gnovm/cmd/gno lint $(OFFICIAL_PACKAGES) + go run ../gnovm/cmd/gno lint -v $(OFFICIAL_PACKAGES) .PHONY: test.sync test.sync: diff --git a/examples/gno.land/p/demo/grc/grc20/types.gno b/examples/gno.land/p/demo/grc/grc20/types.gno index cf67858ccf3..816bbe8a1d9 100644 --- a/examples/gno.land/p/demo/grc/grc20/types.gno +++ b/examples/gno.land/p/demo/grc/grc20/types.gno @@ -39,11 +39,11 @@ type Teller interface { // // Returns an error if the operation failed. // - // IMPORTANT: Beware that changing an allowance with this method brings the risk - // that someone may use both the old and the new allowance by unfortunate - // transaction ordering. One possible solution to mitigate this race - // condition is to first reduce the spender's allowance to 0 and set the - // desired value afterwards: + // IMPORTANT: Beware that changing an allowance with this method brings + // the risk that someone may use both the old and the new allowance by + // unfortunate transaction ordering. One possible solution to mitigate + // this race condition is to first reduce the spender's allowance to 0 + // and set the desired value afterwards: // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Approve(spender std.Address, amount uint64) error @@ -63,12 +63,23 @@ type Teller interface { // name, symbol, and decimals, as well as methods for interacting with the // ledger, including checking balances and allowances. type Token struct { - name string // Name of the token (e.g., "Dummy Token"). - symbol string // Symbol of the token (e.g., "DUMMY"). - decimals uint // Number of decimal places used for the token's precision. - ledger *PrivateLedger // Pointer to the PrivateLedger that manages balances and allowances. + // Name of the token (e.g., "Dummy Token"). + name string + // Symbol of the token (e.g., "DUMMY"). + symbol string + // Number of decimal places used for the token's precision. + decimals uint + // Pointer to the PrivateLedger that manages balances and allowances. + ledger *PrivateLedger } +// TokenGetter is a function type that returns a Token pointer. This type allows +// bypassing a limitation where we cannot directly pass Token pointers between +// realms. Instead, we pass this function which can then be called to get the +// Token pointer. For more details on this limitation and workaround, see: +// https://github.com/gnolang/gno/pull/3135 +type TokenGetter func() *Token + // PrivateLedger is a struct that holds the balances and allowances for the // token. It provides administrative functions for minting, burning, // transferring tokens, and managing allowances. @@ -77,10 +88,14 @@ type Token struct { // information regarding token balances and allowances, and allows direct, // unrestricted access to all administrative functions. type PrivateLedger struct { - totalSupply uint64 // Total supply of the token managed by this ledger. - balances avl.Tree // std.Address -> uint64 - allowances avl.Tree // owner.(std.Address)+":"+spender.(std.Address)) -> uint64 - token *Token // Pointer to the associated Token struct + // Total supply of the token managed by this ledger. + totalSupply uint64 + // std.Address -> uint64 + balances avl.Tree + // owner.(std.Address)+":"+spender.(std.Address)) -> uint64 + allowances avl.Tree + // Pointer to the associated Token struct + token *Token } var ( diff --git a/examples/gno.land/r/demo/bar20/bar20.gno b/examples/gno.land/r/demo/bar20/bar20.gno index de51b8b47d9..25636fcda78 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -9,6 +9,7 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20reg" ) var ( @@ -17,7 +18,8 @@ var ( ) func init() { - // XXX: grc20reg.Register(Token, "") + getter := func() *grc20.Token { return Token } + grc20reg.Register(getter, "") } func Faucet() string { diff --git a/examples/gno.land/r/demo/bar20/gno.mod b/examples/gno.land/r/demo/bar20/gno.mod index 2ec82d7be0b..9fb0f083e1b 100644 --- a/examples/gno.land/r/demo/bar20/gno.mod +++ b/examples/gno.land/r/demo/bar20/gno.mod @@ -5,4 +5,5 @@ require ( gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/urequire v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 31fa577c515..97b2e52b94b 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -10,6 +10,7 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" + "gno.land/r/demo/grc20reg" "gno.land/r/demo/users" ) @@ -21,7 +22,8 @@ var ( func init() { privateLedger.Mint(owner.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) - // XXX: grc20reg.Register(Token, "") + getter := func() *grc20.Token { return Token } + grc20reg.Register(getter, "") } func TotalSupply() uint64 { diff --git a/examples/gno.land/r/demo/foo20/gno.mod b/examples/gno.land/r/demo/foo20/gno.mod index 4035f9b1200..64b8f90a27d 100644 --- a/examples/gno.land/r/demo/foo20/gno.mod +++ b/examples/gno.land/r/demo/foo20/gno.mod @@ -7,5 +7,6 @@ require ( gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/users v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index bf5e9c9ec96..a2d2a55fdf0 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -7,4 +7,5 @@ require ( gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno index 901a9b9f33c..cfd32479f9d 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -8,6 +8,7 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20reg" ) var instances avl.Tree // symbol -> instance @@ -42,7 +43,8 @@ func NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64 faucet: faucet, } instances.Set(symbol, &inst) - // XXX: grc20reg.Register(token, symbol) + getter := func() *grc20.Token { return token } + grc20reg.Register(getter, symbol) } func (inst instance) Token() *grc20.Token { diff --git a/examples/gno.land/r/demo/grc20reg/gno.mod b/examples/gno.land/r/demo/grc20reg/gno.mod new file mode 100644 index 00000000000..f02ee09c35a --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/gno.mod @@ -0,0 +1,9 @@ +module gno.land/r/demo/grc20reg + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/fqname v0.0.0-latest + gno.land/p/demo/grc/grc20 v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/grc20reg/grc20reg.gno b/examples/gno.land/r/demo/grc20reg/grc20reg.gno new file mode 100644 index 00000000000..ff46ec94860 --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/grc20reg.gno @@ -0,0 +1,76 @@ +package grc20reg + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/fqname" + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ufmt" +) + +var registry = avl.NewTree() // rlmPath[.slug] -> TokenGetter (slug is optional) + +func Register(tokenGetter grc20.TokenGetter, slug string) { + rlmPath := std.PrevRealm().PkgPath() + key := fqname.Construct(rlmPath, slug) + registry.Set(key, tokenGetter) + std.Emit( + registerEvent, + "pkgpath", rlmPath, + "slug", slug, + ) +} + +func Get(key string) grc20.TokenGetter { + tokenGetter, ok := registry.Get(key) + if !ok { + return nil + } + return tokenGetter.(grc20.TokenGetter) +} + +func MustGet(key string) grc20.TokenGetter { + tokenGetter := Get(key) + if tokenGetter == nil { + panic("unknown token: " + key) + } + return tokenGetter +} + +func Render(path string) string { + switch { + case path == "": // home + // TODO: add pagination + s := "" + count := 0 + registry.Iterate("", "", func(key string, tokenI interface{}) bool { + count++ + tokenGetter := tokenI.(grc20.TokenGetter) + token := tokenGetter() + rlmPath, slug := fqname.Parse(key) + rlmLink := fqname.RenderLink(rlmPath, slug) + infoLink := "/r/demo/grc20reg:" + key + s += ufmt.Sprintf("- **%s** - %s - [info](%s)\n", token.GetName(), rlmLink, infoLink) + return false + }) + if count == 0 { + return "No registered token." + } + return s + default: // specific token + key := path + tokenGetter := MustGet(key) + token := tokenGetter() + rlmPath, slug := fqname.Parse(key) + rlmLink := fqname.RenderLink(rlmPath, slug) + s := ufmt.Sprintf("# %s\n", token.GetName()) + s += ufmt.Sprintf("- symbol: **%s**\n", token.GetSymbol()) + s += ufmt.Sprintf("- realm: %s\n", rlmLink) + s += ufmt.Sprintf("- decimals: %d\n", token.GetDecimals()) + s += ufmt.Sprintf("- total supply: %d\n", token.TotalSupply()) + return s + } +} + +const registerEvent = "register" diff --git a/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno b/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno new file mode 100644 index 00000000000..c93365ff7a1 --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno @@ -0,0 +1,59 @@ +package grc20reg + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/urequire" +) + +func TestRegistry(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/foo")) + realmAddr := std.CurrentRealm().PkgPath() + token, ledger := grc20.NewToken("TestToken", "TST", 4) + ledger.Mint(std.CurrentRealm().Addr(), 1234567) + tokenGetter := func() *grc20.Token { return token } + // register + Register(tokenGetter, "") + regTokenGetter := Get(realmAddr) + regToken := regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + expected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo) +` + got := Render("") + urequire.True(t, strings.Contains(got, expected)) + // 404 + invalidToken := Get("0xdeadbeef") + urequire.True(t, invalidToken == nil) + + // register with a slug + Register(tokenGetter, "mySlug") + regTokenGetter = Get(realmAddr + ".mySlug") + regToken = regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + // override + Register(tokenGetter, "") + regTokenGetter = Get(realmAddr + "") + regToken = regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + got = Render("") + urequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo)`)) + urequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).mySlug - [info](/r/demo/grc20reg:gno.land/r/demo/foo.mySlug)`)) + + expected = `# TestToken +- symbol: **TST** +- realm: [gno.land/r/demo/foo](/r/demo/foo).mySlug +- decimals: 4 +- total supply: 1234567 +` + got = Render("gno.land/r/demo/foo.mySlug") + urequire.Equal(t, expected, got) +} diff --git a/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno b/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno index 97273f642de..1cc5a3f8e18 100644 --- a/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno +++ b/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno @@ -27,3 +27,31 @@ func Make1() *p_crossrealm.Container { B: local, } } + +type Fooer interface{ Foo() } + +var fooer Fooer + +func SetFooer(f Fooer) Fooer { + fooer = f + return fooer +} + +func GetFooer() Fooer { return fooer } + +func CallFooerFoo() { fooer.Foo() } + +type FooerGetter func() Fooer + +var fooerGetter FooerGetter + +func SetFooerGetter(fg FooerGetter) FooerGetter { + fooerGetter = fg + return fg +} + +func GetFooerGetter() FooerGetter { + return fooerGetter +} + +func CallFooerGetterFoo() { fooerGetter().Foo() } diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno b/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno new file mode 100644 index 00000000000..d412b6ee6b1 --- /dev/null +++ b/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno @@ -0,0 +1,25 @@ +package crossrealm_b + +import ( + "std" + + "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct { + s string +} + +func (f *fooer) SetS(newVal string) { + f.s = newVal +} + +func (f *fooer) Foo() { + println("hello " + f.s + " cur=" + std.CurrentRealm().PkgPath() + " prev=" + std.PrevRealm().PkgPath()) +} + +var ( + Fooer = &fooer{s: "A"} + FooerGetter = func() crossrealm.Fooer { return Fooer } + FooerGetterBuilder = func() crossrealm.FooerGetter { return func() crossrealm.Fooer { return Fooer } } +) diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod new file mode 100644 index 00000000000..74548712caa --- /dev/null +++ b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/tests/crossrealm_b + +require gno.land/r/demo/tests/crossrealm v0.0.0-latest diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index 421ac6528c9..e7fde94ea08 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -50,6 +50,8 @@ type TestRealmObject struct { Field string } +var TestRealmObjectValue TestRealmObject + func ModifyTestRealmObject(t *TestRealmObject) { t.Field += "_modified" } diff --git a/examples/gno.land/r/demo/wugnot/gno.mod b/examples/gno.land/r/demo/wugnot/gno.mod index f076e90e068..c7081ce6963 100644 --- a/examples/gno.land/r/demo/wugnot/gno.mod +++ b/examples/gno.land/r/demo/wugnot/gno.mod @@ -4,5 +4,6 @@ require ( gno.land/p/demo/grc/grc20 v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/users v0.0.0-latest + gno.land/r/demo/grc20reg v0.0.0-latest gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index bb109644778..09538b860ca 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" + "gno.land/r/demo/grc20reg" "gno.land/r/demo/users" ) @@ -18,7 +19,8 @@ const ( ) func init() { - // XXX: grc20reg.Register(Token, "") + getter := func() *grc20.Token { return Token } + grc20reg.Register(getter, "") } func Deposit() { diff --git a/gnovm/stdlibs/math/overflow/overflow.gno b/gnovm/stdlibs/math/overflow/overflow.gno index 9bdeff0720f..0bc2e03a522 100644 --- a/gnovm/stdlibs/math/overflow/overflow.gno +++ b/gnovm/stdlibs/math/overflow/overflow.gno @@ -223,7 +223,7 @@ func Div8p(a, b int8) int8 { func Quo8(a, b int8) (int8, int8, bool) { if b == 0 { return 0, 0, false - } else if b == -1 && a == math.MinInt8 { + } else if b == -1 && a == int8(math.MinInt8) { return 0, 0, false } c := a / b @@ -313,7 +313,7 @@ func Div16p(a, b int16) int16 { func Quo16(a, b int16) (int16, int16, bool) { if b == 0 { return 0, 0, false - } else if b == -1 && a == math.MinInt16 { + } else if b == -1 && a == int16(math.MinInt16) { return 0, 0, false } c := a / b @@ -403,7 +403,7 @@ func Div32p(a, b int32) int32 { func Quo32(a, b int32) (int32, int32, bool) { if b == 0 { return 0, 0, false - } else if b == -1 && a == math.MinInt32 { + } else if b == -1 && a == int32(math.MinInt32) { return 0, 0, false } c := a / b diff --git a/gnovm/tests/files/zrealm_crossrealm15.gno b/gnovm/tests/files/zrealm_crossrealm15.gno new file mode 100644 index 00000000000..b6f38d81abb --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm15.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct{} + +func (fooer) Foo() { println("hello " + std.CurrentRealm().PkgPath()) } + +var f *fooer + +func init() { + f = &fooer{} + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() +} + +func main() { + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm16.gno b/gnovm/tests/files/zrealm_crossrealm16.gno new file mode 100644 index 00000000000..e1b4001801c --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm16.gno @@ -0,0 +1,24 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct{} + +func (fooer) Foo() { println("hello " + std.CurrentRealm().PkgPath()) } + +var f *fooer + +func main() { + f = &fooer{} + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm17.gno b/gnovm/tests/files/zrealm_crossrealm17.gno new file mode 100644 index 00000000000..9abb918689a --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm17.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type container struct{ *fooer } + +func (container) Foo() { println("hello container " + std.CurrentRealm().PkgPath()) } + +type fooer struct{} + +var f *fooer + +func main() { + f = &fooer{} + c := &container{f} + crossrealm.SetFooer(c) + crossrealm.CallFooerFoo() + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm18.gno b/gnovm/tests/files/zrealm_crossrealm18.gno new file mode 100644 index 00000000000..f7a318ed3a0 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm18.gno @@ -0,0 +1,35 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct{} + +func (fooer) Foo() { println("hello " + std.CurrentRealm().PkgPath()) } + +var f crossrealm.Fooer = crossrealm.SetFooer(&fooer{}) + +func init() { + crossrealm.CallFooerFoo() +} + +func main() { + crossrealm.CallFooerFoo() + print(".") +} + +// Output: +// hello gno.land/r/crossrealm_test +// hello gno.land/r/crossrealm_test +// . + +// Error: + +// Realm: +// switchrealm["gno.land/r/crossrealm_test"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/crossrealm_test"] diff --git a/gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno b/gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno new file mode 100644 index 00000000000..a3b864755fd --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm19_stdlibs.gno @@ -0,0 +1,32 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct { + s string +} + +func (f *fooer) Foo() { + f.s = "B" + println("hello " + f.s + " " + std.CurrentRealm().PkgPath()) +} + +var f *fooer + +func init() { + f = &fooer{s: "A"} + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() +} + +func main() { + print(".") +} + +// Error: +// new escaped mark has no object ID diff --git a/gnovm/tests/files/zrealm_crossrealm20.gno b/gnovm/tests/files/zrealm_crossrealm20.gno new file mode 100644 index 00000000000..32fac2e95b9 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm20.gno @@ -0,0 +1,43 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + crossrealm "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct { + s string +} + +func (f *fooer) Foo() { + f.s = "B" + println("hello " + f.s + " " + std.CurrentRealm().PkgPath()) +} + +var f *fooer + +func init() { + f = &fooer{s: "A"} + fg := func() crossrealm.Fooer { return f } + crossrealm.SetFooerGetter(fg) + crossrealm.CallFooerGetterFoo() + f.s = "C" + crossrealm.CallFooerGetterFoo() +} + +func main() { + print(".") +} + +// Output: +// hello B gno.land/r/crossrealm_test +// hello B gno.land/r/crossrealm_test +// . + +// Realm: +// switchrealm["gno.land/r/crossrealm_test"] + +// Error: +// diff --git a/gnovm/tests/files/zrealm_crossrealm21.gno b/gnovm/tests/files/zrealm_crossrealm21.gno new file mode 100644 index 00000000000..634fbea13c8 --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm21.gno @@ -0,0 +1,723 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + "gno.land/r/demo/tests/crossrealm" + "gno.land/r/demo/tests/crossrealm_b" +) + +func main() { + f := crossrealm_b.Fooer + crossrealm.SetFooer(f) + crossrealm.CallFooerFoo() + f.SetS("B") + crossrealm.CallFooerFoo() + print(".") +} + +// Output: +// hello A cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// hello B cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . + +// Realm: +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "5", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm_b.fooer" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:3" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "B" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/crossrealm_test"] + +// Error: +// diff --git a/gnovm/tests/files/zrealm_crossrealm22.gno b/gnovm/tests/files/zrealm_crossrealm22.gno new file mode 100644 index 00000000000..18985f7719d --- /dev/null +++ b/gnovm/tests/files/zrealm_crossrealm22.gno @@ -0,0 +1,2282 @@ +// PKGPATH: gno.land/r/crossrealm_test +package crossrealm_test + +import ( + "std" + + "gno.land/r/demo/tests/crossrealm" + "gno.land/r/demo/tests/crossrealm_b" +) + +func main() { + f := crossrealm_b.Fooer + crossrealm.SetFooerGetter(func() crossrealm.Fooer { return f }) + crossrealm.CallFooerGetterFoo() + f.SetS("B") + crossrealm.CallFooerGetterFoo() + println(".") + + f.SetS("C") + crossrealm.SetFooerGetter(crossrealm_b.FooerGetter) + crossrealm.CallFooerGetterFoo() + println(".") + + f.SetS("D") + crossrealm.SetFooerGetter(crossrealm_b.FooerGetterBuilder()) + crossrealm.CallFooerGetterFoo() + println(".") +} + +// Output: +// hello A cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// hello B cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . +// hello C cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . +// hello D cur=gno.land/r/demo/tests/crossrealm_b prev=gno.land/r/demo/tests/crossrealm +// . + +// Realm: +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// c[1712ac7adcfdc8e58a67e5615e20fb312394c4df:6]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:6", +// "ModTime": "0", +// "OwnerID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "RefCount": "1" +// }, +// "Parent": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "f5a516808f8976c33939133293d598ce3bca4e8d:3" +// }, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "files/zrealm_crossrealm22.gno", +// "Line": "11", +// "PkgPath": "gno.land/r/crossrealm_test" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm_b.fooer" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:3" +// }, +// "Index": "0", +// "TV": null +// } +// } +// ] +// } +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "5", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Hash": "23de97a577d573252d00394ce9b71c24b0646546", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:6" +// }, +// "FileName": "", +// "IsMethod": false, +// "Name": "", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/crossrealm_test", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "28", +// "File": "files/zrealm_crossrealm22.gno", +// "Line": "13", +// "PkgPath": "gno.land/r/crossrealm_test" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// switchrealm["gno.land/r/crossrealm_test"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "B" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/crossrealm_test"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "C" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "6", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:5" +// }, +// "FileName": "", +// "IsMethod": false, +// "Name": "", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "23", +// "File": "crossrealm.gno", +// "Line": "23", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// d[1712ac7adcfdc8e58a67e5615e20fb312394c4df:6] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// u[0edc46caf30c00efd87b6c272673239eafbd051e:4]={ +// "Fields": [ +// { +// "T": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// }, +// "V": { +// "@type": "/gno.StringValue", +// "value": "D" +// } +// } +// ], +// "ObjectInfo": { +// "ID": "0edc46caf30c00efd87b6c272673239eafbd051e:4", +// "ModTime": "5", +// "OwnerID": "0edc46caf30c00efd87b6c272673239eafbd051e:3", +// "RefCount": "1" +// } +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// c[1712ac7adcfdc8e58a67e5615e20fb312394c4df:7]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:7", +// "ModTime": "0", +// "OwnerID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "RefCount": "1" +// }, +// "Parent": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "0edc46caf30c00efd87b6c272673239eafbd051e:5" +// }, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "23", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// u[1712ac7adcfdc8e58a67e5615e20fb312394c4df:2]={ +// "Blank": {}, +// "ObjectInfo": { +// "ID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:2", +// "IsEscaped": true, +// "ModTime": "6", +// "RefCount": "2" +// }, +// "Parent": null, +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "0", +// "File": "", +// "Line": "0", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Values": [ +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.StructType", +// "Fields": [ +// { +// "Embedded": false, +// "Name": "A", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "32" +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [ +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": null, +// "FileName": "crossrealm.gno", +// "IsMethod": true, +// "Name": "String", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "12", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "ls", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PrimitiveType", +// "value": "16" +// } +// } +// ] +// } +// } +// } +// ], +// "Name": "LocalStruct", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.LocalStruct" +// } +// }, +// "V": { +// "@type": "/gno.PointerValue", +// "Base": { +// "@type": "/gno.RefValue", +// "Hash": "a75fdb389fedfcbbaa7f446d528c1e149726347c", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:4" +// }, +// "Index": "0", +// "TV": null +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "init.2", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "19", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "Make1", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.PointerType", +// "Elt": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/tests/p_crossrealm.Container" +// } +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.InterfaceType", +// "Generic": "", +// "Methods": [ +// { +// "Embedded": false, +// "Name": "Foo", +// "Tag": "", +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// ], +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// }, +// "Methods": [], +// "Name": "Fooer", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "35", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "f", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooer", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "40", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "42", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.TypeType" +// }, +// "V": { +// "@type": "/gno.TypeValue", +// "Type": { +// "@type": "/gno.DeclaredType", +// "Base": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// }, +// "Methods": [], +// "Name": "FooerGetter", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Hash": "89352b352826005a86eee78e6c832b43ae0ab6a6", +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:7" +// }, +// "FileName": "", +// "IsMethod": false, +// "Name": "", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "62", +// "File": "crossrealm.gno", +// "Line": "24", +// "PkgPath": "gno.land/r/demo/tests/crossrealm_b" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.Fooer" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "SetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "48", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [ +// { +// "Embedded": false, +// "Name": "fg", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "GetFooerGetter", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "53", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [ +// { +// "Embedded": false, +// "Name": "", +// "Tag": "", +// "Type": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests/crossrealm.FooerGetter" +// } +// } +// ] +// } +// } +// }, +// { +// "T": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// }, +// "V": { +// "@type": "/gno.FuncValue", +// "Closure": { +// "@type": "/gno.RefValue", +// "Escaped": true, +// "ObjectID": "1712ac7adcfdc8e58a67e5615e20fb312394c4df:3" +// }, +// "FileName": "crossrealm.gno", +// "IsMethod": false, +// "Name": "CallFooerGetterFoo", +// "NativeName": "", +// "NativePkg": "", +// "PkgPath": "gno.land/r/demo/tests/crossrealm", +// "Source": { +// "@type": "/gno.RefNode", +// "BlockNode": null, +// "Location": { +// "Column": "1", +// "File": "crossrealm.gno", +// "Line": "57", +// "PkgPath": "gno.land/r/demo/tests/crossrealm" +// } +// }, +// "Type": { +// "@type": "/gno.FuncType", +// "Params": [], +// "Results": [] +// } +// } +// } +// ] +// } +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm_b"] +// switchrealm["gno.land/r/demo/tests/crossrealm"] +// switchrealm["gno.land/r/crossrealm_test"] + +// Error: +// diff --git a/gnovm/tests/files/zrealm_crossrealm4.gno b/gnovm/tests/files/zrealm_crossrealm4.gno index 6aa9c5247d8..ed73b7ad6bb 100644 --- a/gnovm/tests/files/zrealm_crossrealm4.gno +++ b/gnovm/tests/files/zrealm_crossrealm4.gno @@ -5,18 +5,18 @@ import ( "gno.land/r/demo/tests" ) -// NOTE: it is valid to persist external realm types. -var somevalue tests.TestRealmObject +// NOTE: it is valid to persist a pointer to an external object +var somevalue *tests.TestRealmObject func init() { - somevalue.Field = "test" + somevalue = &tests.TestRealmObjectValue } func main() { - // NOTE: but it is invalid to modify it using an external realm function. + // NOTE: it is valid to modify it using the external realm function. somevalue.Modify() println(somevalue) } -// Error: -// cannot modify external-realm or non-realm object +// Output: +// &(struct{("_modified" string)} gno.land/r/demo/tests.TestRealmObject) diff --git a/gnovm/tests/files/zrealm_crossrealm5.gno b/gnovm/tests/files/zrealm_crossrealm5.gno index 6aa9c5247d8..c7560b21463 100644 --- a/gnovm/tests/files/zrealm_crossrealm5.gno +++ b/gnovm/tests/files/zrealm_crossrealm5.gno @@ -6,15 +6,15 @@ import ( ) // NOTE: it is valid to persist external realm types. -var somevalue tests.TestRealmObject +var somevalue *tests.TestRealmObject func init() { - somevalue.Field = "test" + somevalue = &tests.TestRealmObjectValue } func main() { - // NOTE: but it is invalid to modify it using an external realm function. - somevalue.Modify() + // NOTE: but it is invalid to modify it directly. + somevalue.Field = "test" println(somevalue) } diff --git a/gnovm/tests/files/zrealm_tests0.gno b/gnovm/tests/files/zrealm_tests0.gno index 82e4d418217..afb7e4a7c3b 100644 --- a/gnovm/tests/files/zrealm_tests0.gno +++ b/gnovm/tests/files/zrealm_tests0.gno @@ -26,7 +26,7 @@ func main() { // Realm: // switchrealm["gno.land/r/demo/tests"] -// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18]={ +// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:19]={ // "Fields": [ // { // "T": { @@ -40,17 +40,17 @@ func main() { // } // ], // "ObjectInfo": { -// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18", +// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:19", // "ModTime": "0", -// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", +// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18", // "RefCount": "1" // } // } -// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17]={ +// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18]={ // "ObjectInfo": { -// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", +// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18", // "ModTime": "0", -// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16", +// "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", // "RefCount": "1" // }, // "Value": { @@ -60,12 +60,12 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "d3d6ffa52602f2bc976051d79294d219750aca64", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18" +// "Hash": "6b9b731f6118c2419f23ba57e1481679f17f4a8f", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:19" // } // } // } -// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16]={ +// c[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17]={ // "Data": null, // "List": [ // { @@ -80,8 +80,8 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "4ea1e08156f3849b74a0f41f92cd4b48fb94926b", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:11" +// "Hash": "148d314678615253ee2032d7ecff6b144b474baf", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:12" // }, // "Index": "0", // "TV": null @@ -99,8 +99,8 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "ce86ea1156e75a44cd9d7ba2261819b100aa4ed1", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:14" +// "Hash": "fa414e1770821b8deb8e6d46d97828c47f7d5fa5", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:15" // }, // "Index": "0", // "TV": null @@ -118,8 +118,8 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "b66192fbd8a8dde79b6f854b5cc3c4cc965cfd92", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17" +// "Hash": "aaa64d049cf8660d689780acac9f546f270eaa4e", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:18" // }, // "Index": "0", // "TV": null @@ -127,7 +127,7 @@ func main() { // } // ], // "ObjectInfo": { -// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16", +// "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17", // "ModTime": "0", // "OwnerID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:2", // "RefCount": "1" @@ -138,7 +138,7 @@ func main() { // "ObjectInfo": { // "ID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:2", // "IsEscaped": true, -// "ModTime": "15", +// "ModTime": "16", // "RefCount": "5" // }, // "Parent": null, @@ -207,8 +207,8 @@ func main() { // "@type": "/gno.SliceValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "ad25f70f66c8c53042afd1377e5ff5ab744bf1a5", -// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:16" +// "Hash": "3c58838c5667649add1ff8ee48a1cdc187fcd2ef", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:17" // }, // "Length": "3", // "Maxcap": "3", @@ -1153,7 +1153,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "57", +// "Line": "59", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1185,6 +1185,17 @@ func main() { // }, // { // "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/tests.TestRealmObject" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "5e56ba76fc0add1a3a67f7a8b6709f4f27215f93", +// "ObjectID": "0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:10" +// } +// }, +// { +// "T": { // "@type": "/gno.FuncType", // "Params": [ // { @@ -1221,7 +1232,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "53", +// "Line": "55", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1338,7 +1349,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "75", +// "Line": "77", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1374,7 +1385,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "80", +// "Line": "82", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1410,7 +1421,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "88", +// "Line": "90", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1456,7 +1467,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "92", +// "Line": "94", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1512,7 +1523,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "96", +// "Line": "98", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1569,7 +1580,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "100", +// "Line": "102", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1626,7 +1637,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "104", +// "Line": "106", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1682,7 +1693,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "108", +// "Line": "110", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1738,7 +1749,7 @@ func main() { // "Location": { // "Column": "1", // "File": "tests.gno", -// "Line": "112", +// "Line": "114", // "PkgPath": "gno.land/r/demo/tests" // } // }, @@ -1761,7 +1772,7 @@ func main() { // } // ] // } -// d[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:13] +// d[0ffe7732b4d549b4cf9ec18bd68641cd2c75ad0a:14] // switchrealm["gno.land/r/demo/tests_foo"] // switchrealm["gno.land/r/demo/tests_foo"] // switchrealm["gno.land/r/demo/tests_foo"] From 304222966eccb14b6c77d48b70b0cbe265bf2b4f Mon Sep 17 00:00:00 2001 From: cuibuwei <166905851+cuibuwei@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:00:45 +0800 Subject: [PATCH 23/86] chore: fix some function names in comment (#3254)
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
Signed-off-by: cuibuwei --- tm2/pkg/p2p/netaddress.go | 2 +- tm2/pkg/sdk/baseapp.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tm2/pkg/p2p/netaddress.go b/tm2/pkg/p2p/netaddress.go index 1ce34afff34..77f89b2a4b3 100644 --- a/tm2/pkg/p2p/netaddress.go +++ b/tm2/pkg/p2p/netaddress.go @@ -134,7 +134,7 @@ func NewNetAddressFromStrings(idaddrs []string) ([]*NetAddress, []error) { return netAddrs, errs } -// NewNetAddressIPPort returns a new NetAddress using the provided IP +// NewNetAddressFromIPPort returns a new NetAddress using the provided IP // and port number. func NewNetAddressFromIPPort(id ID, ip net.IP, port uint16) *NetAddress { return &NetAddress{ diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index c11f81d852a..415309eab9a 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -262,7 +262,7 @@ func (app *BaseApp) setConsensusParams(consensusParams *abci.ConsensusParams) { app.consensusParams = consensusParams } -// setConsensusParams stores the consensus params to the main store. +// storeConsensusParams stores the consensus params to the main store. func (app *BaseApp) storeConsensusParams(consensusParams *abci.ConsensusParams) { consensusParamsBz, err := amino.Marshal(consensusParams) if err != nil { From 78f0e200133e9b7cbae930e3e1436d139c183588 Mon Sep 17 00:00:00 2001 From: Miguel Victoria Villaquiran Date: Wed, 4 Dec 2024 03:59:31 -0500 Subject: [PATCH 24/86] feat(genesis): deployerAddress passed as parameter (#3253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/gnolang/gno/issues/2573 Had to change the PR from a personal repository as the pipeline was failing old PR: https://github.com/gnolang/gno/pull/2986
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
--------- Co-authored-by: 6h057 Co-authored-by: Miloš Živković --- contribs/gnogenesis/README.md | 4 ++ .../internal/txs/txs_add_packages.go | 58 +++++++++++++++---- .../internal/txs/txs_add_packages_test.go | 26 +++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/contribs/gnogenesis/README.md b/contribs/gnogenesis/README.md index 32cf3e6bb94..25c82992f8f 100644 --- a/contribs/gnogenesis/README.md +++ b/contribs/gnogenesis/README.md @@ -169,6 +169,10 @@ To clear specific transactions, use the transaction hash: ```shell gnogenesis txs remove "5HuU9LN8WUa2NsjiNxp8Xii9n0zlSGXc9UqzLHB+DPs=" ``` +To specify a deployer address (package creator) on add packages command +```shell +gnogenesis txs add packages ./examples --deployer-address=SOME_ADDRESS +``` The transaction hash is the base64 encoding of the Amino-Binary encoded `std.Tx` transaction hash. diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages.go b/contribs/gnogenesis/internal/txs/txs_add_packages.go index 1b4e6e7cffb..cf863c72116 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -3,26 +3,49 @@ package txs import ( "context" "errors" + "flag" "fmt" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) -var errInvalidPackageDir = errors.New("invalid package directory") +var ( + errInvalidPackageDir = errors.New("invalid package directory") + errInvalidDeployerAddr = errors.New("invalid deployer address") +) +// Keep in sync with gno.land/cmd/start.go var ( - // Keep in sync with gno.land/cmd/start.go - genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) + defaultCreator = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 + genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) ) +type addPkgCfg struct { + txsCfg *txsCfg + deployerAddress string +} + +func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.deployerAddress, + "deployer-address", + defaultCreator.String(), + "the address that will be used to deploy the package", + ) +} + // newTxsAddPackagesCmd creates the genesis txs add packages subcommand func newTxsAddPackagesCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + cfg := &addPkgCfg{ + txsCfg: txsCfg, + } + return commands.NewCommand( commands.Metadata{ Name: "packages", @@ -30,20 +53,20 @@ func newTxsAddPackagesCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { ShortHelp: "imports transactions from the given packages into the genesis.json", LongHelp: "Imports the transactions from a given package directory recursively to the genesis.json", }, - commands.NewEmptyConfig(), + cfg, func(_ context.Context, args []string) error { - return execTxsAddPackages(txsCfg, io, args) + return execTxsAddPackages(cfg, io, args) }, ) } func execTxsAddPackages( - cfg *txsCfg, + cfg *addPkgCfg, io commands.IO, args []string, ) error { // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + genesis, loadErr := types.GenesisDocFromFile(cfg.txsCfg.GenesisPath) if loadErr != nil { return fmt.Errorf("unable to load genesis, %w", loadErr) } @@ -53,10 +76,23 @@ func execTxsAddPackages( return errInvalidPackageDir } + var ( + creator = defaultCreator + err error + ) + + // Check if the deployer address is set + if cfg.deployerAddress != defaultCreator.String() { + creator, err = crypto.AddressFromString(cfg.deployerAddress) + if err != nil { + return fmt.Errorf("%w, %w", errInvalidDeployerAddr, err) + } + } + parsedTxs := make([]gnoland.TxWithMetadata, 0) for _, path := range args { // Generate transactions from the packages (recursively) - txs, err := gnoland.LoadPackagesFromDir(path, genesisDeployAddress, genesisDeployFee) + txs, err := gnoland.LoadPackagesFromDir(path, creator, genesisDeployFee) if err != nil { return fmt.Errorf("unable to load txs from directory, %w", err) } @@ -70,7 +106,7 @@ func execTxsAddPackages( } // Save the updated genesis - if err := genesis.SaveAs(cfg.GenesisPath); err != nil { + if err := genesis.SaveAs(cfg.txsCfg.GenesisPath); err != nil { return fmt.Errorf("unable to save genesis.json, %w", err) } diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go index 12a9287f171..c3405d6ff8d 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go @@ -60,6 +60,32 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { assert.ErrorContains(t, cmdErr, errInvalidPackageDir.Error()) }) + t.Run("invalid deployer address", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + t.TempDir(), // package dir + "--deployer-address", + "beep-boop", // invalid address + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidDeployerAddr) + }) + t.Run("valid package", func(t *testing.T) { t.Parallel() From 6585cad6b8e253602653ac2bcd91f9a752ae5102 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Wed, 4 Dec 2024 10:08:25 +0100 Subject: [PATCH 25/86] fix: impl empty statement exec (#3252) Implement empty statement in the runtime exec. Closes https://github.com/gnolang/gno/issues/3202 --- gnovm/pkg/gnolang/go2gno.go | 2 ++ gnovm/pkg/gnolang/op_exec.go | 1 + gnovm/tests/files/goto_empty_stmt.gno | 10 ++++++++++ 3 files changed, 13 insertions(+) create mode 100644 gnovm/tests/files/goto_empty_stmt.gno diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index 99e051f7913..338efa20fcc 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -471,6 +471,8 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) { PkgName: pkgName, Decls: decls, } + case *ast.EmptyStmt: + return &EmptyStmt{} default: panic(fmt.Sprintf("unknown Go type %v: %s\n", reflect.TypeOf(gon), diff --git a/gnovm/pkg/gnolang/op_exec.go b/gnovm/pkg/gnolang/op_exec.go index 900b5f8e9bb..5f71ffefa0c 100644 --- a/gnovm/pkg/gnolang/op_exec.go +++ b/gnovm/pkg/gnolang/op_exec.go @@ -769,6 +769,7 @@ EXEC_SWITCH: } m.PushOp(OpBody) m.PushStmt(b.GetBodyStmt()) + case *EmptyStmt: default: panic(fmt.Sprintf("unexpected statement %#v", s)) } diff --git a/gnovm/tests/files/goto_empty_stmt.gno b/gnovm/tests/files/goto_empty_stmt.gno new file mode 100644 index 00000000000..fd939de1045 --- /dev/null +++ b/gnovm/tests/files/goto_empty_stmt.gno @@ -0,0 +1,10 @@ +package main + +func main() { + println("Hi") + goto done +done: +} + +// Output: +// Hi \ No newline at end of file From 8c660aca81ab0347769d5f391f9b7c6a6b2b6e6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:10:14 +0100 Subject: [PATCH 26/86] chore(deps): bump coursier/setup-action from 1.3.8 to 1.3.9 in the actions group (#3258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions group with 1 update: [coursier/setup-action](https://github.com/coursier/setup-action). Updates `coursier/setup-action` from 1.3.8 to 1.3.9
Release notes

Sourced from coursier/setup-action's releases.

v1.3.9

What's Changed

Updates / maintenance

Full Changelog: https://github.com/coursier/setup-action/compare/v1...v1.3.9

Commits
  • 039f736 build(deps-dev): bump @​types/node from 22.10.0 to 22.10.1
  • b0150fa build(deps-dev): bump eslint-plugin-github from 5.1.2 to 5.1.3
  • 0329715 build(deps-dev): bump eslint-plugin-github from 5.1.1 to 5.1.2
  • 45da7eb build(deps-dev): bump @​typescript-eslint/parser from 8.15.0 to 8.16.0
  • 049f21e build(deps-dev): bump @​types/node from 22.9.3 to 22.10.0
  • ca73c3e build(deps-dev): bump prettier from 3.3.3 to 3.4.1
  • e3d80af build(deps-dev): bump @​typescript-eslint/eslint-plugin
  • 72f329c bugfix: Migrate to new config format
  • 7441104 build(deps-dev): bump eslint from 8.57.1 to 9.15.0
  • d67c30e Update dist
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coursier/setup-action&package-manager=github_actions&previous-version=1.3.8&new-version=1.3.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) 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> --- .github/workflows/fossa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index c536b428a5c..41d9a2cba94 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -25,7 +25,7 @@ jobs: uses: coursier/cache-action@v6.4.6 - name: Set up JDK 17 - uses: coursier/setup-action@v1.3.8 + uses: coursier/setup-action@v1.3.9 with: jvm: temurin:1.17 From a7a38b6eea44500b259972a3c05d026dce31eabe Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 4 Dec 2024 10:57:29 +0100 Subject: [PATCH 27/86] chore: s/NativeStore/NativeResolver/g (#3262) It's not a "store", this was a misnomer from the beginning. --- gno.land/pkg/sdk/vm/keeper.go | 4 ++-- gnovm/pkg/gnolang/realm.go | 2 +- gnovm/pkg/gnolang/store.go | 24 ++++++++++++------------ gnovm/pkg/gnolang/store_test.go | 2 +- gnovm/pkg/gnolang/values.go | 2 +- gnovm/pkg/test/imports.go | 2 +- gnovm/stdlibs/stdlibs.go | 4 ++-- gnovm/tests/stdlibs/stdlibs.go | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 0dca794ee71..68f784a52e7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -100,7 +100,7 @@ func (vm *VMKeeper) Initialize( alloc := gno.NewAllocator(maxAllocTx) vm.gnoStore = gno.NewStore(alloc, baseStore, iavlStore) - vm.gnoStore.SetNativeStore(stdlibs.NativeStore) + vm.gnoStore.SetNativeResolver(stdlibs.NativeResolver) if vm.gnoStore.NumMemPackages() > 0 { // for now, all mem packages must be re-run after reboot. @@ -146,7 +146,7 @@ func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { } gs := gno.NewStore(nil, cachedStdlib.base, cachedStdlib.iavl) - gs.SetNativeStore(stdlibs.NativeStore) + gs.SetNativeResolver(stdlibs.NativeResolver) loadStdlib(gs, stdlibDir) cachedStdlib.gno = gs }) diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 5913f13a0f7..d25d456edf3 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -1132,7 +1132,7 @@ func copyValueWithRefs(val Value) Value { if cv.Closure != nil { closure = toRefValue(cv.Closure) } - // nativeBody funcs which don't come from NativeStore (and thus don't + // nativeBody funcs which don't come from NativeResolver (and thus don't // have NativePkg/Name) can't be persisted, and should not be able // to get here anyway. if cv.nativeBody != nil && cv.NativePkg == "" { diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 2c0ee05a1d7..b721194823d 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -25,8 +25,8 @@ import ( // cause writes to happen to the store, such as MemPackages to iavlstore. type PackageGetter func(pkgPath string, store Store) (*PackageNode, *PackageValue) -// NativeStore is a function which can retrieve native bodies of native functions. -type NativeStore func(pkgName string, name Name) func(m *Machine) +// NativeResolver is a function which can retrieve native bodies of native functions. +type NativeResolver func(pkgName string, name Name) func(m *Machine) // Store is the central interface that specifies the communications between the // GnoVM and the underlying data store; currently, generally the gno.land @@ -62,7 +62,7 @@ type Store interface { GetMemFile(path string, name string) *gnovm.MemFile IterMemPackage() <-chan *gnovm.MemPackage ClearObjectCache() // run before processing a message - SetNativeStore(NativeStore) // for "new" natives XXX + SetNativeResolver(NativeResolver) // for "new" natives XXX GetNative(pkgPath string, name Name) func(m *Machine) // for "new" natives XXX SetLogStoreOps(enabled bool) SprintStoreOps() string @@ -95,7 +95,7 @@ type defaultStore struct { // store configuration; cannot be modified in a transaction pkgGetter PackageGetter // non-realm packages cacheNativeTypes map[reflect.Type]Type // reflect doc: reflect.Type are comparable - nativeStore NativeStore // for injecting natives + nativeResolver NativeResolver // for injecting natives // transient opslog []StoreOp // for debugging and testing. @@ -116,7 +116,7 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore // store configuration pkgGetter: nil, cacheNativeTypes: make(map[reflect.Type]Type), - nativeStore: nil, + nativeResolver: nil, } InitStoreCaches(ds) return ds @@ -144,7 +144,7 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans // store configuration pkgGetter: ds.pkgGetter, cacheNativeTypes: ds.cacheNativeTypes, - nativeStore: ds.nativeStore, + nativeResolver: ds.nativeResolver, // transient current: nil, @@ -174,8 +174,8 @@ func (transactionStore) SetPackageGetter(pg PackageGetter) { // panic("Go2GnoType may not be called in a transaction store") // } -func (transactionStore) SetNativeStore(ns NativeStore) { - panic("SetNativeStore may not be called in a transaction store") +func (transactionStore) SetNativeResolver(ns NativeResolver) { + panic("SetNativeResolver may not be called in a transaction store") } // CopyCachesFromStore allows to copy a store's internal object, type and @@ -685,13 +685,13 @@ func (ds *defaultStore) ClearObjectCache() { ds.SetCachePackage(Uverse()) } -func (ds *defaultStore) SetNativeStore(ns NativeStore) { - ds.nativeStore = ns +func (ds *defaultStore) SetNativeResolver(ns NativeResolver) { + ds.nativeResolver = ns } func (ds *defaultStore) GetNative(pkgPath string, name Name) func(m *Machine) { - if ds.nativeStore != nil { - return ds.nativeStore(pkgPath, name) + if ds.nativeResolver != nil { + return ds.nativeResolver(pkgPath, name) } return nil } diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index f7f03b947f6..e280032e3d9 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -58,7 +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{}.SetNativeStore(nil) }) + assert.Panics(t, func() { transactionStore{}.SetNativeResolver(nil) }) } func TestCopyFromCachedStore(t *testing.T) { diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 8e27bcbcbdb..e7a6274a780 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -548,7 +548,7 @@ type FuncValue struct { Captures []TypedValue `json:",omitempty"` // HeapItemValues captured from closure. FileName Name // file name where declared PkgPath string - NativePkg string // for native bindings through NativeStore + NativePkg string // for native bindings through NativeResolver NativeName Name // not redundant with Name; this cannot be changed in userspace body []Stmt // function body diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index dabb5644cdd..b57fc6388b1 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -160,7 +160,7 @@ func Store( // Make a new store. resStore = gno.NewStore(nil, baseStore, baseStore) resStore.SetPackageGetter(getPackage) - resStore.SetNativeStore(teststdlibs.NativeStore) + resStore.SetNativeResolver(teststdlibs.NativeResolver) return } diff --git a/gnovm/stdlibs/stdlibs.go b/gnovm/stdlibs/stdlibs.go index c9b16815ab5..3b8b88c1fde 100644 --- a/gnovm/stdlibs/stdlibs.go +++ b/gnovm/stdlibs/stdlibs.go @@ -24,10 +24,10 @@ func FindNative(pkgPath string, name gno.Name) *NativeFunc { return nil } -// NativeStore is used by the GnoVM to determine if the given function, +// NativeResolver is used by the GnoVM to determine if the given function, // specified by its pkgPath and name, has a native implementation; and if so // retrieve it. -func NativeStore(pkgPath string, name gno.Name) func(*gno.Machine) { +func NativeResolver(pkgPath string, name gno.Name) func(*gno.Machine) { nt := FindNative(pkgPath, name) if nt == nil { return nil diff --git a/gnovm/tests/stdlibs/stdlibs.go b/gnovm/tests/stdlibs/stdlibs.go index b0a1050af41..92316bf41fd 100644 --- a/gnovm/tests/stdlibs/stdlibs.go +++ b/gnovm/tests/stdlibs/stdlibs.go @@ -8,11 +8,11 @@ import ( //go:generate go run github.com/gnolang/gno/misc/genstd -func NativeStore(pkgPath string, name gno.Name) func(*gno.Machine) { +func NativeResolver(pkgPath string, name gno.Name) func(*gno.Machine) { for _, nf := range nativeFuncs { if nf.gnoPkg == pkgPath && name == nf.gnoFunc { return nf.f } } - return stdlibs.NativeStore(pkgPath, name) + return stdlibs.NativeResolver(pkgPath, name) } From 7a40481f0f4f97054aa02a5a8211aac01d278a84 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:00:21 +0100 Subject: [PATCH 28/86] docs: update token section in bot README (#3261) Simple update of the bot README to mention the necessary permissions for the bot to operate, see this comment: https://github.com/gnolang/gno/issues/3238#issuecomment-2514895884
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
--- contribs/github-bot/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md index 7932300cb9d..639901c52ee 100644 --- a/contribs/github-bot/README.md +++ b/contribs/github-bot/README.md @@ -19,11 +19,24 @@ The bot configuration is defined in Go and is located in the file [config.go](./ 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: +#### Repository permissions + - `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 +#### Organization permissions + +- `members` scope to read to be able to list the members of a team + +#### Bot account role + +For the bot to create a commit status on a repo - and only for this feature at the time of writing this - the GitHub account of the bot must either: + +- have the `write` role on the repo +- have the `owner` role in the organization that owns the repo + ## Usage ```bash From 2496db78df18d66bad11942312b46ee9016659f3 Mon Sep 17 00:00:00 2001 From: jinoosss <112360739+jinoosss@users.noreply.github.com> Date: Wed, 4 Dec 2024 22:32:01 +0900 Subject: [PATCH 29/86] fix: Modify `app` path method to simulate for ABCI query (#3207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Descriptions To simulate transactions, utilize the `.app/simulate` method for ABCI Query. ### Changes 1. change the path of ABCI Query's `.app` to the result data storage location. - You can receive the query result data as `RequestQuery.ResponseData.Data` instead of `RequestQuery.Value`. - Provide it in a common form with other ABCI Queries. 2. remove the gas-consume logic of mocking signature data that is executed when simulating transactions ([/tm2/pkg/sdk/auth/ante.go#L231-L237](https://github.com/gnolang/gno/blob/master/tm2/pkg/sdk/auth/ante.go#L231-L237)) - We will get the correct value when simulating a real transaction. - We want transactions to run without signatures, but we already have checks in place to see if a signature exists. ([tm2/pkg/sdk/auth/ante.go#L104-L106](https://github.com/gnolang/gno/blob/master/tm2/pkg/sdk/auth/ante.go#L104-L106)) ### Example #### [Request Simulate] ```curl curl --location 'http://localhost:26657' \ --header 'Content-Type: application/json' \ --data '{ "id": 1, "jsonrpc": "2.0", "method": "abci_query", "params": [ ".app/simulate", "CnMKDS9iYW5rLk1zZ1NlbmQSYgooZzFqZzhtdHV0dTlraGhmd2M0bnhtdWhjcGZ0ZjBwYWpkaGZ2c3FmNRIoZzFmZnp4aGE1N2RoMHFndjltYTV2MzkzdXIwemV4ZnZwNmxzanBhZRoMNTAwMDAwMHVnbm90Eg4IgIl6EggzMDB1Z25vdBp+CjoKEy90bS5QdWJLZXlTZWNwMjU2azESIwohA+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2yEkCrIOTBt7YcDGcQ6Ohfv1r3nftAPaTATAtPfYD5zLQf7WDf1KPvWARe//CANtLLtIzcPVl7P/HnHxmfCYEwfGogIgUxMjMxMw==", "0", false ] }' ``` #### [Response] ```curl { "jsonrpc": "2.0", "id": 1, "result": { "response": { "ResponseBase": { "Error": null, "Data": "eyJFcnJvciI6bnVsbCwiRGF0YSI6IiIsIkV2ZW50cyI6W10sIkxvZyI6Im1zZzowLHN1Y2Nlc3M6dHJ1ZSxsb2c6LGV2ZW50czpbXSIsIkluZm8iOiIiLCJHYXNXYW50ZWQiOjEwMDAwMDAsIkdhc1VzZWQiOjQ0NjI5fQ==", "Events": null, "Log": "", "Info": "" }, "Key": null, "Value": null, "Proof": null, "Height": "0" } } } ``` ### Related Issue - https://github.com/gnolang/gno/issues/1826
Contributors' checklist... - [x] 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 - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: n3wbie Co-authored-by: Miloš Živković --- .../cmd/gnoland/testdata/simulate_gas.txtar | 28 ++++++++++++ tm2/pkg/sdk/auth/ante.go | 45 +------------------ tm2/pkg/sdk/auth/ante_test.go | 5 ++- tm2/pkg/sdk/baseapp.go | 10 ++++- tm2/pkg/sdk/baseapp_test.go | 34 +++++++++++++- 5 files changed, 75 insertions(+), 47 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/simulate_gas.txtar diff --git a/gno.land/cmd/gnoland/testdata/simulate_gas.txtar b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar new file mode 100644 index 00000000000..cd58b4ccc8f --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar @@ -0,0 +1,28 @@ +# load the package +loadpkg gno.land/r/simulate $WORK/simulate + +# start a new node +gnoland start + +# simulate only +gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 +stdout 'GAS USED: 50299' + +# simulate skip +gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 +stdout 'GAS USED: 50299' # same as simulate only + + +-- package/package.gno -- +package call_package + +func Render() string { + return "notok" +} + +-- simulate/simulate.gno -- +package simulate + +func Hello() string { + return "Hello" +} diff --git a/tm2/pkg/sdk/auth/ante.go b/tm2/pkg/sdk/auth/ante.go index 49662b47a55..d36b376aa8d 100644 --- a/tm2/pkg/sdk/auth/ante.go +++ b/tm2/pkg/sdk/auth/ante.go @@ -15,11 +15,8 @@ import ( "github.com/gnolang/gno/tm2/pkg/store" ) -var ( - // simulation signature values used to estimate gas consumption - simSecp256k1Pubkey secp256k1.PubKeySecp256k1 - simSecp256k1Sig [64]byte -) +// simulation signature values used to estimate gas consumption +var simSecp256k1Pubkey secp256k1.PubKeySecp256k1 func init() { // This decodes a valid hex string into a sepc256k1Pubkey for use in transaction simulation @@ -228,14 +225,6 @@ func processSig( return nil, abciResult(std.ErrInternal("setting PubKey on signer's account")) } - if simulate { - // Simulated txs should not contain a signature and are not required to - // contain a pubkey, so we must account for tx size of including a - // std.Signature (Amino encoding) and simulate gas consumption - // (assuming a SECP256k1 simulation key). - consumeSimSigGas(ctx.GasMeter(), pubKey, sig, params) - } - if res := sigGasConsumer(ctx.GasMeter(), sig.Signature, pubKey, params); !res.IsOK() { return nil, res } @@ -251,42 +240,12 @@ func processSig( return acc, res } -func consumeSimSigGas(gasmeter store.GasMeter, pubkey crypto.PubKey, sig std.Signature, params Params) { - simSig := std.Signature{PubKey: pubkey} - if len(sig.Signature) == 0 { - simSig.Signature = simSecp256k1Sig[:] - } - - sigBz := amino.MustMarshalSized(simSig) - cost := store.Gas(len(sigBz) + 6) - - // If the pubkey is a multi-signature pubkey, then we estimate for the maximum - // number of signers. - if _, ok := pubkey.(multisig.PubKeyMultisigThreshold); ok { - cost *= params.TxSigLimit - } - - gasmeter.ConsumeGas(params.TxSizeCostPerByte*cost, "txSize") -} - // ProcessPubKey verifies that the given account address matches that of the // std.Signature. In addition, it will set the public key of the account if it // has not been set. func ProcessPubKey(acc std.Account, sig std.Signature, simulate bool) (crypto.PubKey, sdk.Result) { // If pubkey is not known for account, set it from the std.Signature. pubKey := acc.GetPubKey() - if simulate { - // In simulate mode the transaction comes with no signatures, thus if the - // account's pubkey is nil, both signature verification and gasKVStore.Set() - // shall consume the largest amount, i.e. it takes more gas to verify - // secp256k1 keys than ed25519 ones. - if pubKey == nil { - return simSecp256k1Pubkey, sdk.Result{} - } - - return pubKey, sdk.Result{} - } - if pubKey == nil { pubKey = sig.PubKey if pubKey == nil { diff --git a/tm2/pkg/sdk/auth/ante_test.go b/tm2/pkg/sdk/auth/ante_test.go index be4167a6238..86e34391770 100644 --- a/tm2/pkg/sdk/auth/ante_test.go +++ b/tm2/pkg/sdk/auth/ante_test.go @@ -611,10 +611,11 @@ func TestProcessPubKey(t *testing.T) { wantErr bool }{ {"no sigs, simulate off", args{acc1, std.Signature{}, false}, true}, - {"no sigs, simulate on", args{acc1, std.Signature{}, true}, false}, + {"no sigs, simulate on", args{acc1, std.Signature{}, true}, true}, + {"no sigs, account with pub, simulate off", args{acc2, std.Signature{}, false}, false}, {"no sigs, account with pub, simulate on", args{acc2, std.Signature{}, true}, false}, {"pubkey doesn't match addr, simulate off", args{acc1, std.Signature{PubKey: priv2.PubKey()}, false}, true}, - {"pubkey doesn't match addr, simulate on", args{acc1, std.Signature{PubKey: priv2.PubKey()}, true}, false}, + {"pubkey doesn't match addr, simulate on", args{acc1, std.Signature{PubKey: priv2.PubKey()}, true}, true}, } for _, tt := range tests { tt := tt diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 415309eab9a..1802a21f453 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -409,8 +409,16 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc } else { result = app.Simulate(txBytes, tx) } + res.Height = req.Height - res.Value = amino.MustMarshal(result) + + bytes, err := amino.Marshal(result) + if err != nil { + res.Error = ABCIError(std.ErrInternal(fmt.Sprintf("cannot encode to JSON: %s", err))) + } else { + res.Value = bytes + } + return res case "version": res.Height = req.Height diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 08e8191170a..cf944c44f06 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -634,6 +634,38 @@ func TestDeliverTx(t *testing.T) { } } +// Test that the gas used between Simulate and DeliverTx is the same. +func TestGasUsedBetweenSimulateAndDeliver(t *testing.T) { + t.Parallel() + + anteKey := []byte("ante-key") + anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, mainKey, anteKey)) } + + deliverKey := []byte("deliver-key") + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(routeMsgCounter, newMsgCounterHandler(t, mainKey, deliverKey)) + } + + app := setupBaseApp(t, anteOpt, routerOpt) + app.InitChain(abci.RequestInitChain{ChainID: "test-chain"}) + + header := &bft.Header{ChainID: "test-chain", Height: 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + tx := newTxCounter(0, 0) + txBytes, err := amino.Marshal(tx) + require.Nil(t, err) + + simulateRes := app.Simulate(txBytes, tx) + require.True(t, simulateRes.IsOK(), fmt.Sprintf("%v", simulateRes)) + require.Greater(t, simulateRes.GasUsed, int64(0)) // gas used should be greater than 0 + + deliverRes := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) + require.True(t, deliverRes.IsOK(), fmt.Sprintf("%v", deliverRes)) + + require.Equal(t, simulateRes.GasUsed, deliverRes.GasUsed) // gas used should be the same from simulate and deliver +} + // One call to DeliverTx should process all the messages, in order. func TestMultiMsgDeliverTx(t *testing.T) { t.Parallel() @@ -753,7 +785,7 @@ func TestSimulateTx(t *testing.T) { require.True(t, queryResult.IsOK(), queryResult.Log) var res Result - amino.MustUnmarshal(queryResult.Value, &res) + require.NoError(t, amino.Unmarshal(queryResult.Value, &res)) require.Nil(t, err, "Result unmarshalling failed") require.True(t, res.IsOK(), res.Log) require.Equal(t, gasConsumed, res.GasUsed, res.Log) From c1b928faaa1562aa10fd806939ac04ea060a1caa Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:29:40 +0100 Subject: [PATCH 30/86] docs: update test3 mentions, add test5 mentions (#3259) ## Description This PR updates the mentions of test3 after its deprecation, and adds text on test5.
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
--- docs/concepts/testnets.md | 48 +++++++++++++++++------------ docs/reference/network-config.md | 12 ++++---- docs/reference/stdlibs/std/chain.md | 2 +- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/docs/concepts/testnets.md b/docs/concepts/testnets.md index 4df8e3a4b86..b5286eaec57 100644 --- a/docs/concepts/testnets.md +++ b/docs/concepts/testnets.md @@ -21,6 +21,7 @@ gno.land testnets are categorized by 4 main points: Below you can find a breakdown of each existing testnet by these categories. ## Portal Loop + Portal Loop is an always up-to-date rolling testnet. It is meant to be used as a nightly build of the Gno tech stack. The home page of [gno.land](https://gno.land) is the `gnoweb` render of the Portal Loop testnet. @@ -43,8 +44,28 @@ For more information on the Portal Loop, and how it can be best utilized, check out the [Portal Loop concept page](./portal-loop.md). Also, you can find the Portal Loop faucet on [`gno.land/faucet`](https://gno.land/faucet). +## Test5 + +Test5 a permanent multi-node testnet. It bumped the validator set from 7 to 17 +nodes, introduced GovDAO V2, and added lots of bug fixes and quality of life +improvements. + +Test5 was launched in November 2024. + +- **Persistence of state:** + - State is fully persisted unless there are breaking changes in a new release, + where persistence partly depends on implementing a migration strategy +- **Timeliness of code:** + - Pre-deployed packages and realms are at monorepo commit [2e9f5ce](https://github.com/gnolang/gno/tree/2e9f5ce8ecc90ee81eb3ae41c06bab30ab926150) +- **Intended purpose** + - Running a full node, testing validator coordination, deploying stable Gno + dApps, creating tools that require persisted state & transaction history +- **Versioning strategy**: + - Test5 is to be release-based, following releases of the Gno tech stack. + ## Test4 -Test4 a permanent multi-node testnet. + +Test4 is the first permanent multi-node testnet, launched in July 2024. - **Persistence of state:** - State is fully persisted unless there are breaking changes in a new release, @@ -59,6 +80,7 @@ Test4 a permanent multi-node testnet. of the Gno tech stack. ## Staging + Staging is a testnet that is reset once every 60 minutes. - **Persistence of state:** @@ -73,39 +95,25 @@ Staging is a testnet that is reset once every 60 minutes. - Staging is reset every 60 minutes to match the latest monorepo commit ## TestX -These testnets are deprecated and currently serve as archives of previous progress. - -### Test3 -Test3 is the most recent persistent Gno testnet. It is still being used, but -most packages, such as the AVL package, are outdated. -- **Persistence of state:** - - State is fully preserved -- **Timeliness of code:** - - Test3 is at commit [1ca2d97](https://github.com/gnolang/gno/commit/1ca2d973817b174b5b06eb9da011e1fcd2cca575) -of Gno, and it can contain new on-chain code -- **Intended purpose** - - Running a full node, building an indexer, showing demos, persisting history -- **Versioning strategy**: - - There is no versioning strategy for test3. It will stay the way it is, until -the team chooses to shut it down. +These testnets are deprecated and currently serve as archives of previous progress. -Since gno.land is designed with open-source in mind, anyone can see currently -available code by browsing the [test3 homepage](https://test3.gno.land/). +### Test3 (archive) -Test3 is a single-node testnet, ran by the Gno core team. There is no plan to -upgrade test3 to a multi-node testnet. +The third Gno testnet. Archived data for test3 can be found [here](https://github.com/gnolang/tx-exports/tree/main/test3.gno.land). Launch date: November 4th 2022 Release commit: [1ca2d97](https://github.com/gnolang/gno/commit/1ca2d973817b174b5b06eb9da011e1fcd2cca575) ### Test2 (archive) + The second Gno testnet. Find archive data [here](https://github.com/gnolang/tx-exports/tree/main/test2.gno.land). Launch date: July 10th 2022 Release commit: [652dc7a](https://github.com/gnolang/gno/commit/652dc7a3a62ee0438093d598d123a8c357bf2499) ### Test1 (archive) + The first Gno testnet. Find archive data [here](https://github.com/gnolang/tx-exports/tree/main/test1.gno.land). Launch date: May 6th 2022 diff --git a/docs/reference/network-config.md b/docs/reference/network-config.md index 6d4fc9ea14a..45a56b772ae 100644 --- a/docs/reference/network-config.md +++ b/docs/reference/network-config.md @@ -4,12 +4,12 @@ id: network-config # Network configurations -| Network | RPC Endpoint | Chain ID | -|-------------|-----------------------------------|---------------| -| Portal Loop | https://rpc.gno.land:443 | `portal-loop` | -| Test4 | https://rpc.test4.gno.land:443 | `test4` | -| Test3 | https://rpc.test3.gno.land:443 | `test3` | -| Staging | https://rpc.staging.gno.land:443 | `staging` | +| Network | RPC Endpoint | Chain ID | +|-------------|----------------------------------|---------------| +| Portal Loop | https://rpc.gno.land:443 | `portal-loop` | +| Test5 | https://rpc.test5.gno.land:443 | `test5` | +| Test4 | https://rpc.test4.gno.land:443 | `test4` | +| Staging | https://rpc.staging.gno.land:443 | `staging` | ### WebSocket endpoints All networks follow the same pattern for websocket connections: diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 089de682cfd..0e5ead338c5 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -49,7 +49,7 @@ Returns the chain ID. #### Usage ```go -chainID := std.GetChainID() // dev | test3 | main ... +chainID := std.GetChainID() // dev | test5 | main ... ``` --- From ebb49480fc73c4baac13452e0f56c5d8235ad05f Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Wed, 4 Dec 2024 22:27:58 +0100 Subject: [PATCH 31/86] feat: Add handling for float types, and also for %v (#3263) This PR includes the changes to `ufmt.Sprintf()` from #2868 as well as another branch where I had added support for `%v`. These changes add support for a variety of float formatting options to Sprintf, as well as support for the %v flag to automatically chose a default representation for a given type. Tests were added for these additions. This PR is needed for the PRNG PR (#2868) to be fully functional, as the generators include some built in statistical examples/tests which will not function without `ufmt.Sprintf()` support for floats.
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
--------- Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> --- examples/gno.land/p/demo/ufmt/ufmt.gno | 104 +++++++++++++++++--- examples/gno.land/p/demo/ufmt/ufmt_test.gno | 13 +++ 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/examples/gno.land/p/demo/ufmt/ufmt.gno b/examples/gno.land/p/demo/ufmt/ufmt.gno index c2abf43c85a..c9acee1c910 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt.gno @@ -22,6 +22,8 @@ func Println(args ...interface{}) { strs = append(strs, v.String()) case error: strs = append(strs, v.Error()) + case float64: + strs = append(strs, Sprintf("%f", v)) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: strs = append(strs, Sprintf("%d", v)) case bool: @@ -49,21 +51,28 @@ func Println(args ...interface{}) { // // The currently formatted verbs are the following: // -// %s: places a string value directly. -// If the value implements the interface interface{ String() string }, -// the String() method is called to retrieve the value. Same about Error() -// string. -// %c: formats the character represented by Unicode code point -// %d: formats an integer value using package "strconv". -// Currently supports only uint, uint64, int, int64. -// %t: formats a boolean value to "true" or "false". -// %x: formats an integer value as a hexadecimal string. -// Currently supports only uint8, []uint8, [32]uint8. -// %c: formats a rune value as a string. -// Currently supports only rune, int. -// %q: formats a string value as a quoted string. -// %T: formats the type of the value. -// %%: outputs a literal %. Does not consume an argument. +// %s: places a string value directly. +// If the value implements the interface interface{ String() string }, +// the String() method is called to retrieve the value. Same about Error() +// string. +// %c: formats the character represented by Unicode code point +// %d: formats an integer value using package "strconv". +// Currently supports only uint, uint64, int, int64. +// %f: formats a float value, with a default precision of 6. +// %e: formats a float with scientific notation; 1.23456e+78 +// %E: formats a float with scientific notation; 1.23456E+78 +// %F: The same as %f +// %g: formats a float value with %e for large exponents, and %f with full precision for smaller numbers +// %G: formats a float value with %G for large exponents, and %F with full precision for smaller numbers +// %t: formats a boolean value to "true" or "false". +// %x: formats an integer value as a hexadecimal string. +// Currently supports only uint8, []uint8, [32]uint8. +// %c: formats a rune value as a string. +// Currently supports only rune, int. +// %q: formats a string value as a quoted string. +// %T: formats the type of the value. +// %v: formats the value with a default representation appropriate for the value's type +// %%: outputs a literal %. Does not consume an argument. func Sprintf(format string, args ...interface{}) string { // we use runes to handle multi-byte characters sTor := []rune(format) @@ -97,6 +106,51 @@ func Sprintf(format string, args ...interface{}) string { argNum++ switch verb { + case "v": + switch v := arg.(type) { + case nil: + buf += "" + case bool: + if v { + buf += "true" + } else { + buf += "false" + } + case int: + buf += strconv.Itoa(v) + case int8: + buf += strconv.Itoa(int(v)) + case int16: + buf += strconv.Itoa(int(v)) + case int32: + buf += strconv.Itoa(int(v)) + case int64: + buf += strconv.Itoa(int(v)) + case uint: + buf += strconv.FormatUint(uint64(v), 10) + case uint8: + buf += strconv.FormatUint(uint64(v), 10) + case uint16: + buf += strconv.FormatUint(uint64(v), 10) + case uint32: + buf += strconv.FormatUint(uint64(v), 10) + case uint64: + buf += strconv.FormatUint(v, 10) + case float64: + buf += strconv.FormatFloat(v, 'g', -1, 64) + case string: + buf += v + case []byte: + buf += string(v) + case []rune: + buf += string(v) + case interface{ String() string }: + buf += v.String() + case error: + buf += v.Error() + default: + buf += fallback(verb, v) + } case "s": switch v := arg.(type) { case interface{ String() string }: @@ -153,6 +207,24 @@ func Sprintf(format string, args ...interface{}) string { default: buf += fallback(verb, v) } + case "e", "E", "f", "F", "g", "G": + switch v := arg.(type) { + case float64: + switch verb { + case "e": + buf += strconv.FormatFloat(v, byte('e'), -1, 64) + case "E": + buf += strconv.FormatFloat(v, byte('E'), -1, 64) + case "f", "F": + buf += strconv.FormatFloat(v, byte('f'), 6, 64) + case "g": + buf += strconv.FormatFloat(v, byte('g'), -1, 64) + case "G": + buf += strconv.FormatFloat(v, byte('G'), -1, 64) + } + default: + buf += fallback(verb, v) + } case "t": switch v := arg.(type) { case bool: @@ -244,6 +316,8 @@ func fallback(verb string, arg interface{}) string { case error: // note: also "string=" in Go fmt s = "string=" + v.Error() + case float64: + s = "float64=" + Sprintf("%f", v) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: // note: rune, byte would be dups, being aliases if typename, e := typeToString(v); e != nil { diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index 2a583202a93..1cb7231a611 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -20,21 +20,34 @@ func TestSprintf(t *testing.T) { expectedOutput string }{ {"hello %s!", []interface{}{"planet"}, "hello planet!"}, + {"hello %v!", []interface{}{"planet"}, "hello planet!"}, {"hi %%%s!", []interface{}{"worl%d"}, "hi %worl%d!"}, {"%s %c %d %t", []interface{}{"foo", 'α', 421, true}, "foo α 421 true"}, {"string [%s]", []interface{}{"foo"}, "string [foo]"}, {"int [%d]", []interface{}{int(42)}, "int [42]"}, + {"int [%v]", []interface{}{int(42)}, "int [42]"}, {"int8 [%d]", []interface{}{int8(8)}, "int8 [8]"}, + {"int8 [%v]", []interface{}{int8(8)}, "int8 [8]"}, {"int16 [%d]", []interface{}{int16(16)}, "int16 [16]"}, + {"int16 [%v]", []interface{}{int16(16)}, "int16 [16]"}, {"int32 [%d]", []interface{}{int32(32)}, "int32 [32]"}, + {"int32 [%v]", []interface{}{int32(32)}, "int32 [32]"}, {"int64 [%d]", []interface{}{int64(64)}, "int64 [64]"}, + {"int64 [%v]", []interface{}{int64(64)}, "int64 [64]"}, {"uint [%d]", []interface{}{uint(42)}, "uint [42]"}, + {"uint [%v]", []interface{}{uint(42)}, "uint [42]"}, {"uint8 [%d]", []interface{}{uint8(8)}, "uint8 [8]"}, + {"uint8 [%v]", []interface{}{uint8(8)}, "uint8 [8]"}, {"uint16 [%d]", []interface{}{uint16(16)}, "uint16 [16]"}, + {"uint16 [%v]", []interface{}{uint16(16)}, "uint16 [16]"}, {"uint32 [%d]", []interface{}{uint32(32)}, "uint32 [32]"}, + {"uint32 [%v]", []interface{}{uint32(32)}, "uint32 [32]"}, {"uint64 [%d]", []interface{}{uint64(64)}, "uint64 [64]"}, + {"uint64 [%v]", []interface{}{uint64(64)}, "uint64 [64]"}, {"bool [%t]", []interface{}{true}, "bool [true]"}, + {"bool [%v]", []interface{}{true}, "bool [true]"}, {"bool [%t]", []interface{}{false}, "bool [false]"}, + {"bool [%v]", []interface{}{false}, "bool [false]"}, {"no args", nil, "no args"}, {"finish with %", nil, "finish with %"}, {"stringer [%s]", []interface{}{stringer{}}, "stringer [I'm a stringer]"}, From b631207ca737a6b1e3bd30d10d687ccaf9b4918b Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Thu, 5 Dec 2024 10:05:43 +0100 Subject: [PATCH 32/86] feat(examples): Add a useful set of high quality pseudo-random number generators (#2868) I ported a number of my pseudo-random number generator implementations from Ruby to gno while traveling to the retreat last weekend as an exercise in expanding my comfort level with gno code, and expanding my understanding of some of the code internals, while contributing code that others may find interesting or useful. I added two xorshift generators, xorshift64* and xorshiftr128+. These are both many times faster than the PCG generator that is the gno default, and produce high quality randomness with great statistical qualities. In addition to these, I added both the 32-bit ISAAC implementation (with an added function to return 64 bit values), and the 64-bit ISAAC implementation. ISAAC is a stellar pseudo-random number generator. Both implementations are significantly faster than PCG (though not near so fast as the xorshift algorithms), while producing extremely high quality, cryptographically secure randomness that can not be differentiated from real randomness. All of these were built to be compatible with the standard Rand() implementation. This means that any of these can be used as a drop-in replacement for the default PCG algorithm: ``` source = isaac.New() prng := rand.New(source) ``` All of these leverage the `gno.land/p/demo/entropy` package to assist with seeding if no seed is provided. In the case of the ISAAC algorithms, they require 256 uint values for their seed, so they leverage a combination of `entropy` and `xorshiftr128+` to generate any missing numbers in the provided seed. I also added a function to entropy to return uint64, to facilitate using it for seeding. I added tests to entropy, and wrote tests for the other generators, as well. There are a few other things that ended up in this PR. In order to make some fact based assertions about the performance of these generators, I included some code that can be ran via `gno run -expr`. i.e. `gno run -expr 'averageISAAC()' isaac.gno` that can be used to get some benchmarks and some very simple self-statistical-analysis on the results, and when I did so, I discovered that the current `ufmt.Sprintf` implementation didn't support any of the float output flags. I added float support to it's capabilities, which, in turn, required adding `FormatFloat` to the `strconv.gno/strconv.go` implementation in the standard library. I added a test to cover this. I also noticed that there is a test in `tm2/pkg/p2p` that is failing on both master and my branch. Specifically, there is a call to `sw.Logger.Error()` that passes a message and an error, but not `"err"` before the error. Adding that seemed to clear up the build failure. This, specifically, is line 222 of `switch.go`. Currently there is one failing test, which is the code coverage check on tm2, because it is non-obvious to me how to setup a test to properly exercise that one changed line.
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
--------- Co-authored-by: Morgan Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> Co-authored-by: Morgan --- examples/gno.land/p/demo/entropy/entropy.gno | 8 + .../gno.land/p/demo/entropy/entropy_test.gno | 32 ++ .../gno.land/p/demo/entropy/z_filetest.gno | 6 + examples/gno.land/p/demo/ufmt/ufmt_test.gno | 6 + .../gno.land/p/wyhaines/rand/isaac/README.md | 86 ++++ .../gno.land/p/wyhaines/rand/isaac/gno.mod | 7 + .../gno.land/p/wyhaines/rand/isaac/isaac.gno | 435 ++++++++++++++++++ .../p/wyhaines/rand/isaac/isaac_test.gno | 165 +++++++ .../p/wyhaines/rand/isaac64/README.md | 97 ++++ .../gno.land/p/wyhaines/rand/isaac64/gno.mod | 7 + .../p/wyhaines/rand/isaac64/isaac64.gno | 429 +++++++++++++++++ .../p/wyhaines/rand/isaac64/isaac64_test.gno | 165 +++++++ .../p/wyhaines/rand/xorshift64star/README.MD | 69 +++ .../p/wyhaines/rand/xorshift64star/gno.mod | 6 + .../rand/xorshift64star/xorshift64star.gno | 172 +++++++ .../xorshift64star/xorshift64star_test.gno | 134 ++++++ .../wyhaines/rand/xorshiftr128plus/README.MD | 60 +++ .../p/wyhaines/rand/xorshiftr128plus/gno.mod | 6 + .../xorshiftr128plus/xorshiftr128plus.gno | 186 ++++++++ .../xorshiftr128plus_test.gno | 142 ++++++ 20 files changed, 2218 insertions(+) create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/README.md create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/isaac.gno create mode 100644 examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/README.md create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno create mode 100644 examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno create mode 100644 examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno diff --git a/examples/gno.land/p/demo/entropy/entropy.gno b/examples/gno.land/p/demo/entropy/entropy.gno index 5e35b8c7227..9e8f656c21b 100644 --- a/examples/gno.land/p/demo/entropy/entropy.gno +++ b/examples/gno.land/p/demo/entropy/entropy.gno @@ -87,3 +87,11 @@ func (i *Instance) Value() uint32 { i.addEntropy() return i.value } + +func (i *Instance) Value64() uint64 { + i.addEntropy() + high := i.value + i.addEntropy() + + return (uint64(high) << 32) | uint64(i.value) +} diff --git a/examples/gno.land/p/demo/entropy/entropy_test.gno b/examples/gno.land/p/demo/entropy/entropy_test.gno index 0deb3ab9aa2..895bfd1e394 100644 --- a/examples/gno.land/p/demo/entropy/entropy_test.gno +++ b/examples/gno.land/p/demo/entropy/entropy_test.gno @@ -33,6 +33,26 @@ func TestInstanceValue(t *testing.T) { } } +func TestInstanceValue64(t *testing.T) { + baseEntropy := New() + baseResult := computeValue64(t, baseEntropy) + + sameHeightEntropy := New() + sameHeightResult := computeValue64(t, sameHeightEntropy) + + if baseResult != sameHeightResult { + t.Errorf("should have the same result: new=%s, base=%s", sameHeightResult, baseResult) + } + + std.TestSkipHeights(1) + differentHeightEntropy := New() + differentHeightResult := computeValue64(t, differentHeightEntropy) + + if baseResult == differentHeightResult { + t.Errorf("should have different result: new=%s, base=%s", differentHeightResult, baseResult) + } +} + func computeValue(t *testing.T, r *Instance) string { t.Helper() @@ -44,3 +64,15 @@ func computeValue(t *testing.T, r *Instance) string { return out } + +func computeValue64(t *testing.T, r *Instance) string { + t.Helper() + + out := "" + for i := 0; i < 10; i++ { + val := int(r.Value64()) + out += strconv.Itoa(val) + " " + } + + return out +} diff --git a/examples/gno.land/p/demo/entropy/z_filetest.gno b/examples/gno.land/p/demo/entropy/z_filetest.gno index 85ed1b10a3d..ddee29b22fd 100644 --- a/examples/gno.land/p/demo/entropy/z_filetest.gno +++ b/examples/gno.land/p/demo/entropy/z_filetest.gno @@ -15,6 +15,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) // should be the same println("---") @@ -24,6 +25,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) std.TestSkipHeights(1) println("---") @@ -33,6 +35,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) } // Output: @@ -42,15 +45,18 @@ func main() { // 1950222777 // 3348280598 // 438354259 +// 6353385488959065197 // --- // 4129293727 // 2141104956 // 1950222777 // 3348280598 // 438354259 +// 6353385488959065197 // --- // 49506731 // 1539580078 // 2695928529 // 1895482388 // 3462727799 +// 16745038698684748445 diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index 1cb7231a611..1a4d4e7e6f2 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -44,6 +44,12 @@ func TestSprintf(t *testing.T) { {"uint32 [%v]", []interface{}{uint32(32)}, "uint32 [32]"}, {"uint64 [%d]", []interface{}{uint64(64)}, "uint64 [64]"}, {"uint64 [%v]", []interface{}{uint64(64)}, "uint64 [64]"}, + {"float64 [%e]", []interface{}{float64(64.1)}, "float64 [6.41e+01]"}, + {"float64 [%E]", []interface{}{float64(64.1)}, "float64 [6.41E+01]"}, + {"float64 [%f]", []interface{}{float64(64.1)}, "float64 [64.100000]"}, + {"float64 [%F]", []interface{}{float64(64.1)}, "float64 [64.100000]"}, + {"float64 [%g]", []interface{}{float64(64.1)}, "float64 [64.1]"}, + {"float64 [%G]", []interface{}{float64(64.1)}, "float64 [64.1]"}, {"bool [%t]", []interface{}{true}, "bool [true]"}, {"bool [%v]", []interface{}{true}, "bool [true]"}, {"bool [%t]", []interface{}{false}, "bool [false]"}, diff --git a/examples/gno.land/p/wyhaines/rand/isaac/README.md b/examples/gno.land/p/wyhaines/rand/isaac/README.md new file mode 100644 index 00000000000..05f4a94425f --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/README.md @@ -0,0 +1,86 @@ +# package isaac // import "gno.land/p/demo/math/rand/isaac" + +This is a port of the ISAAC cryptographically secure PRNG, +originally based on the reference implementation found at +https://burtleburtle.net/bob/rand/isaacafa.html + +ISAAC has excellent statistical properties, with long cycle times, and +uniformly distributed, unbiased, and unpredictable number generation. It can +not be distinguished from real random data, and in three decades of scrutiny, +no practical attacks have been found. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This +algorithm provides very strong statistical performance, and is cryptographically +secure, while still being substantially faster than the default PCG +implementation in `math/rand`. Note that this package does implement a `Uint64()` +function in order to generate a 64 bit number out of two 32 bit numbers. Doing this +makes the generator only slightly faster than PCG, however, + +Note that the approach to seeing with ISAAC is very important for best results, +and seeding with ISAAC is not as simple as seeding with a single uint64 value. +The ISAAC algorithm requires a 256-element seed. If used for cryptographic +purposes, this will likely require entropy generated off-chain for actual +cryptographically secure seeding. For other purposes, however, one can utilize +the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to +generate any missing seeds if fewer than 256 are provided. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +ISAAC: 1000000 Uint64 generated in 13.23s (uint64) +ISAAC: 1000000 Uint32 generated in 6.43s (uint32) +Ratio: x1.18 times faster than PCG (uint64) +Ratio: x2.42 times faster than PCG (uint32) +``` + +Use it directly: + +``` +prng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest + // will be generated using the xorshiftr128plus PRNG. +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = isaac.New() +prng := rand.New(source) +``` + +# TYPES + +` +type ISAAC struct { + // Has unexported fields. +} +` + +`func New(seeds ...uint32) *ISAAC` + ISAAC requires a large, 256-element seed. This implementation will leverage + the entropy package combined with the the xorshiftr128plus PRNG to generate + any missing seeds of fewer than the required number of arguments are + provided. + +`func (isaac *ISAAC) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (isaac *ISAAC) Seed(seed [256]uint32)` + +`func (isaac *ISAAC) Uint32() uint32` + +`func (isaac *ISAAC) Uint64() uint64` + +`func (isaac *ISAAC) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/isaac/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod new file mode 100644 index 00000000000..0cca6aa5174 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/wyhaines/rand/isaac + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno b/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno new file mode 100644 index 00000000000..4508dd5d5af --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno @@ -0,0 +1,435 @@ +// This is a port of the ISAAC cryptographically secure PRNG, originally based on the reference +// implementation found at https://burtleburtle.net/bob/rand/isaacafa.html +// +// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed, +// unbiased, and unpredictable number generation. It can not be distinguished from real random +// data, and in three decades of scrutiny, no practical attacks have been found. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementation, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This +// algorithm provides very strong statistical performance, and is cryptographically +// secure, while still being substantially faster than the default PCG +// implementation in `math/rand`. Note that this package does implement a `Uint64()` +// function in order to generate a 64 bit number out of two 32 bit numbers. Doing this +// makes the generator only slightly faster than PCG, however, +// +// Note that the approach to seeing with ISAAC is very important for best results, and seeding with +// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a +// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated +// off-chain for actual cryptographically secure seeding. For other purposes, however, one can +// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate +// any missing seeds if fewer than 256 are provided. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// ISAAC: 1000000 Uint64 generated in 13.23s +// ISAAC: 1000000 Uint32 generated in 6.43s +// Ratio: x1.18 times faster than PCG (uint64) +// Ratio: x2.42 times faster than PCG (uint32) +// +// Use it directly: +// +// prng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest +// // will be generated using the xorshiftr128plus PRNG. +// +// Or use it as a drop-in replacement for the default PRNG in Rand: +// +// source = isaac.New() +// prng := rand.New(source) +package isaac + +import ( + "errors" + "math" + "math/rand" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" + "gno.land/p/wyhaines/rand/xorshiftr128plus" +) + +type ISAAC struct { + randrsl [256]uint32 + randcnt uint32 + mm [256]uint32 + aa, bb, cc uint32 + seed [256]uint32 +} + +// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy +// package combined with the the xorshiftr128plus PRNG to generate any missing seeds of +// fewer than the required number of arguments are provided. +func New(seeds ...uint32) *ISAAC { + isaac := &ISAAC{} + seed := [256]uint32{} + + index := 0 + for index = 0; index < len(seeds); index++ { + seed[index] = seeds[index] + } + + if index < 4 { + e := entropy.New() + for ; index < 4; index++ { + seed[index] = e.Value() + } + } + + // Use up to the first four seeds as seeding inputs for xorshiftr128+, in order to + // use it to provide any remaining missing seeds. + prng := xorshiftr128plus.New( + (uint64(seed[0])<<32)|uint64(seed[1]), + (uint64(seed[2])<<32)|uint64(seed[3]), + ) + for ; index < 256; index += 2 { + val := prng.Uint64() + seed[index] = uint32(val & 0xffffffff) + if index+1 < 256 { + seed[index+1] = uint32(val >> 32) + } + } + isaac.Seed(seed) + return isaac +} + +func (isaac *ISAAC) Seed(seed [256]uint32) { + isaac.randrsl = seed + isaac.seed = seed + isaac.randinit(true) +} + +// beUint32() decodes a uint32 from a set of four bytes, assuming big endian encoding. +// binary.bigEndian.Uint32, copied to avoid dependency +func beUint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +} + +// bePutUint32() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint32, copied to avoid dependency +func bePutUint32(b []byte, v uint32) { + _ = b[3] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 24) + b[1] = byte(v >> 16) + b[2] = byte(v >> 8) + b[3] = byte(v) +} + +// A label to identify the marshalled data. +var marshalISAACLabel = []byte("isaac:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (isaac *ISAAC) MarshalBinary() ([]byte, error) { + b := make([]byte, 3094) // 6 + 1024 + 1024 + 1024 + 4 + 4 + 4 + 4 == 3090 + copy(b, marshalISAACLabel) + for i := 0; i < 256; i++ { + bePutUint32(b[6+i*4:], isaac.seed[i]) + } + for i := 256; i < 512; i++ { + bePutUint32(b[6+i*4:], isaac.randrsl[i-256]) + } + for i := 512; i < 768; i++ { + bePutUint32(b[6+i*4:], isaac.mm[i-512]) + } + bePutUint32(b[3078:], isaac.aa) + bePutUint32(b[3082:], isaac.bb) + bePutUint32(b[3086:], isaac.cc) + bePutUint32(b[3090:], isaac.randcnt) + + return b, nil +} + +// errUnmarshalISAAC is returned when unmarshalling fails. +var errUnmarshalISAAC = errors.New("invalid ISAAC encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (isaac *ISAAC) UnmarshalBinary(data []byte) error { + if len(data) != 3094 || string(data[:6]) != string(marshalISAACLabel) { + return errUnmarshalISAAC + } + for i := 0; i < 256; i++ { + isaac.seed[i] = beUint32(data[6+i*4:]) + } + for i := 256; i < 512; i++ { + isaac.randrsl[i-256] = beUint32(data[6+i*4:]) + } + for i := 512; i < 768; i++ { + isaac.mm[i-512] = beUint32(data[6+i*4:]) + } + isaac.aa = beUint32(data[3078:]) + isaac.bb = beUint32(data[3082:]) + isaac.cc = beUint32(data[3086:]) + isaac.randcnt = beUint32(data[3090:]) + return nil +} + +func (isaac *ISAAC) randinit(flag bool) { + isaac.aa = 0 + isaac.bb = 0 + isaac.cc = 0 + + var a, b, c, d, e, f, g, h uint32 = 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9 + + for i := 0; i < 4; i++ { + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + } + + for i := 0; i < 256; i += 8 { + if flag { + a += isaac.randrsl[i] + b += isaac.randrsl[i+1] + c += isaac.randrsl[i+2] + d += isaac.randrsl[i+3] + e += isaac.randrsl[i+4] + f += isaac.randrsl[i+5] + g += isaac.randrsl[i+6] + h += isaac.randrsl[i+7] + } + + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + + if flag { + for i := 0; i < 256; i += 8 { + a += isaac.mm[i] + b += isaac.mm[i+1] + c += isaac.mm[i+2] + d += isaac.mm[i+3] + e += isaac.mm[i+4] + f += isaac.mm[i+5] + g += isaac.mm[i+6] + h += isaac.mm[i+7] + + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + } + + isaac.isaac() + isaac.randcnt = uint32(256) +} + +func (isaac *ISAAC) isaac() { + isaac.cc++ + isaac.bb += isaac.cc + + for i := 0; i < 256; i++ { + x := isaac.mm[i] + switch i % 4 { + case 0: + isaac.aa ^= isaac.aa << 13 + case 1: + isaac.aa ^= isaac.aa >> 6 + case 2: + isaac.aa ^= isaac.aa << 2 + case 3: + isaac.aa ^= isaac.aa >> 16 + } + isaac.aa += isaac.mm[(i+128)&0xff] + + y := isaac.mm[(x>>2)&0xff] + isaac.aa + isaac.bb + isaac.mm[i] = y + isaac.bb = isaac.mm[(y>>10)&0xff] + x + isaac.randrsl[i] = isaac.bb + } +} + +// Returns a random uint32. +func (isaac *ISAAC) Uint32() uint32 { + if isaac.randcnt == uint32(0) { + isaac.isaac() + isaac.randcnt = uint32(256) + } + isaac.randcnt-- + return isaac.randrsl[isaac.randcnt] +} + +// Returns a random uint64 by combining two uint32s. +func (isaac *ISAAC) Uint64() uint64 { + return uint64(isaac.Uint32()) | (uint64(isaac.Uint32()) << 32) +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkISAAC()' xorshift64star.gno +func benchmarkISAAC(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New() + + for i := 0; i < iterations; i++ { + _ = isaac.Uint64() + } + ufmt.Println(ufmt.Sprintf("ISAAC: generate %d uint64\n", iterations)) +} + +// The averageISAAC() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the ISAAC PRNG. +func averageISAAC(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New(987654321, 123456789, 999999999, 111111111) + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("ISAAC average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("ISAAC standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("ISAAC theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} + +func averagePCG(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := rand.NewPCG(987654321, 123456789) + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("PCG average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("PCG standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("PCG theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno new file mode 100644 index 00000000000..b08621e271c --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno @@ -0,0 +1,165 @@ +package isaac + +import ( + "math/rand" + "testing" +) + +type OpenISAAC struct { + Randrsl [256]uint32 + Randcnt uint32 + Mm [256]uint32 + Aa, Bb, Cc uint32 + Seed [256]uint32 +} + +func TestISAACSeeding(t *testing.T) { + isaac := New() +} + +func TestISAACRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.17828173023837635, + 0.7327795780287832, + 0.4850369074875177, + 0.9474842397428482, + 0.6747135561813891, + 0.7522507082868403, + 0.041115261836534356, + 0.7405243709084567, + 0.672863376128768, + 0.11866211399980553, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestISAACUint64(t *testing.T) { + isaac := New() + + expected := []uint64{ + 5986068031949215749, + 10437354066128700566, + 13478007513323023970, + 8969511410255984224, + 3869229557962857982, + 1762449743873204415, + 5292356290662282456, + 7893982194485405616, + 4296136494566588699, + 12414349056998262772, + } + + for i, exp := range expected { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func dupState(i *ISAAC) *OpenISAAC { + state := &OpenISAAC{} + state.Seed = i.seed + state.Randrsl = i.randrsl + state.Mm = i.mm + state.Aa = i.aa + state.Bb = i.bb + state.Cc = i.cc + state.Randcnt = i.randcnt + + return state +} + +func TestISAACMarshalUnmarshal(t *testing.T) { + isaac := New() + + expected1 := []uint64{ + 5986068031949215749, + 10437354066128700566, + 13478007513323023970, + 8969511410255984224, + 3869229557962857982, + } + + expected2 := []uint64{ + 1762449743873204415, + 5292356290662282456, + 7893982194485405616, + 4296136494566588699, + 12414349056998262772, + } + + for i, exp := range expected1 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := isaac.MarshalBinary() + + t.Logf("State: [%v]\n", dupState(isaac)) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := dupState(isaac) + + if err != nil { + t.Errorf("ISAAC.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + isaac.Uint64() + + for i, exp := range expected2 { + val := isaac.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%v]\n", dupState(isaac)) + + // Now restore the state of the PRNG + err = isaac.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%v]\n", dupState(isaac)) + + if state_before.Seed != dupState(isaac).Seed { + t.Errorf("Seed mismatch") + } + if state_before.Randrsl != dupState(isaac).Randrsl { + t.Errorf("Randrsl mismatch") + } + if state_before.Mm != dupState(isaac).Mm { + t.Errorf("Mm mismatch") + } + if state_before.Aa != dupState(isaac).Aa { + t.Errorf("Aa mismatch") + } + if state_before.Bb != dupState(isaac).Bb { + t.Errorf("Bb mismatch") + } + if state_before.Cc != dupState(isaac).Cc { + t.Errorf("Cc mismatch") + } + if state_before.Randcnt != dupState(isaac).Randcnt { + t.Errorf("Randcnt mismatch") + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/README.md b/examples/gno.land/p/wyhaines/rand/isaac64/README.md new file mode 100644 index 00000000000..813b062a5cd --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/README.md @@ -0,0 +1,97 @@ +# package isaac64 // import "gno.land/p/demo/math/rand/isaac64" + +This is a port of the 64-bit version of the ISAAC cryptographically +secure PRNG, originally based on the reference implementation found at +https://burtleburtle.net/bob/rand/isaacafa.html + +ISAAC has excellent statistical properties, with long cycle times, and +uniformly distributed, unbiased, and unpredictable number generation. It can +not be distinguished from real random data, and in three decades of scrutiny, +no practical attacks have been found. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This +algorithm provides very strong statistical performance, and is cryptographically +secure, while still being substantially faster than the default PCG +implementation in `math/rand`. + +Note that the approach to seeing with ISAAC is very important for best results, +and seeding with ISAAC is not as simple as seeding with a single uint64 value. +The ISAAC algorithm requires a 256-element seed. If used for cryptographic +purposes, this will likely require entropy generated off-chain for actual +cryptographically secure seeding. For other purposes, however, one can utilize +the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to +generate any missing seeds if fewer than 256 are provided. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +ISAAC: 1000000 Uint64 generated in 8.95s +ISAAC: 1000000 Uint32 generated in 7.66s +Ratio: x1.74 times faster than PCG (uint64) +Ratio: x2.03 times faster than PCG (uint32) +``` + +Use it directly: + + +``` +prng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest + // will be generated using the xorshiftr128plus PRNG. +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = isaac64.New() +prng := rand.New(source) +``` + +## CONSTANTS + + +``` +const ( + RANDSIZL = 8 + RANDSIZ = 1 << RANDSIZL // 256 +) +``` + +## TYPES + + +``` +type ISAAC struct { + // Has unexported fields. +} +``` + +`func New(seeds ...uint64) *ISAAC` +ISAAC requires a large, 256-element seed. This implementation will leverage +the entropy package combined with the xorshiftr128plus PRNG to generate any +missing seeds if fewer than the required number of arguments are provided. + +`func (isaac *ISAAC) MarshalBinary() ([]byte, error)` +MarshalBinary() returns a byte array that encodes the state of the PRNG. +This can later be used with UnmarshalBinary() to restore the state of the +PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (isaac *ISAAC) Seed(seed [256]uint64)` +Reinitialize the generator with a new seed. A seed must be composed of 256 uint64. + +`func (isaac *ISAAC) Uint32() uint32` +Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result. + +`func (isaac *ISAAC) Uint64() uint64` +Return a 64 bit random integer. + +`func (isaac *ISAAC) UnmarshalBinary(data []byte) error` +UnmarshalBinary() restores the state of the PRNG from a byte array +that was created with MarshalBinary(). UnmarshalBinary implements the +encoding.BinaryUnmarshaler interface. diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod new file mode 100644 index 00000000000..dbc8713094e --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/wyhaines/rand/isaac64 + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno new file mode 100644 index 00000000000..6f2d95150fc --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno @@ -0,0 +1,429 @@ +// This is a port of the 64-bit version of the ISAAC cryptographically secure PRNG, originally +// based on the reference implementation found at https://burtleburtle.net/bob/rand/isaacafa.html +// +// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed, +// unbiased, and unpredictable number generation. It can not be distinguished from real random +// data, and in three decades of scrutiny, no practical attacks have been found. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This algorithm +// provides very strong statistical performance, and is cryptographically secure, while still +// being substantially faster than the default PCG implementation in `math/rand`. +// +// Note that the approach to seeing with ISAAC is very important for best results, and seeding with +// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a +// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated +// off-chain for actual cryptographically secure seeding. For other purposes, however, one can +// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate +// any missing seeds if fewer than 256 are provided. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// ISAAC: 1000000 Uint64 generated in 8.95s +// ISAAC: 1000000 Uint32 generated in 7.66s +// Ratio: x1.74 times faster than PCG (uint64) +// Ratio: x2.03 times faster than PCG (uint32) +// +// Use it directly: +// +// prng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest +// // will be generated using the xorshiftr128plus PRNG. +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = isaac64.New() +// prng := rand.New(source) +package isaac64 + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" + "gno.land/p/wyhaines/rand/xorshiftr128plus" +) + +const ( + RANDSIZL = 8 + RANDSIZ = 1 << RANDSIZL // 256 +) + +type ISAAC struct { + randrsl [256]uint64 + randcnt uint64 + mm [256]uint64 + aa, bb, cc uint64 + seed [256]uint64 +} + +// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy +// package combined with the xorshiftr128plus PRNG to generate any missing seeds if fewer than +// the required number of arguments are provided. +func New(seeds ...uint64) *ISAAC { + isaac := &ISAAC{} + seed := [256]uint64{} + + index := 0 + for index = 0; index < len(seeds) && index < 256; index++ { + seed[index] = seeds[index] + } + + if index < 2 { + e := entropy.New() + for ; index < 2; index++ { + seed[index] = e.Value64() + } + } + + // Use the first two seeds as seeding inputs for xorshiftr128plus, in order to + // use it to provide any remaining missing seeds. + prng := xorshiftr128plus.New( + seed[0], + seed[1], + ) + for ; index < 256; index++ { + seed[index] = prng.Uint64() + } + isaac.Seed(seed) + return isaac +} + +// Reinitialize the generator with a new seed. A seed must be composed of 256 uint64. +func (isaac *ISAAC) Seed(seed [256]uint64) { + isaac.randrsl = seed + isaac.seed = seed + isaac.randinit(true) +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalISAACLabel = []byte("isaac:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (isaac *ISAAC) MarshalBinary() ([]byte, error) { + b := make([]byte, 6+2048*3+8*3+8) // 6 + 2048*3 + 8*3 + 8 == 6182 + copy(b, marshalISAACLabel) + offset := 6 + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.seed[i]) + offset += 8 + } + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.randrsl[i]) + offset += 8 + } + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.mm[i]) + offset += 8 + } + bePutUint64(b[offset:], isaac.aa) + offset += 8 + bePutUint64(b[offset:], isaac.bb) + offset += 8 + bePutUint64(b[offset:], isaac.cc) + offset += 8 + bePutUint64(b[offset:], isaac.randcnt) + return b, nil +} + +// errUnmarshalISAAC is returned when unmarshalling fails. +var errUnmarshalISAAC = errors.New("invalid ISAAC encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (isaac *ISAAC) UnmarshalBinary(data []byte) error { + if len(data) != 6182 || string(data[:6]) != string(marshalISAACLabel) { + return errUnmarshalISAAC + } + offset := 6 + for i := 0; i < 256; i++ { + isaac.seed[i] = beUint64(data[offset:]) + offset += 8 + } + for i := 0; i < 256; i++ { + isaac.randrsl[i] = beUint64(data[offset:]) + offset += 8 + } + for i := 0; i < 256; i++ { + isaac.mm[i] = beUint64(data[offset:]) + offset += 8 + } + isaac.aa = beUint64(data[offset:]) + offset += 8 + isaac.bb = beUint64(data[offset:]) + offset += 8 + isaac.cc = beUint64(data[offset:]) + offset += 8 + isaac.randcnt = beUint64(data[offset:]) + return nil +} + +func (isaac *ISAAC) randinit(flag bool) { + var a, b, c, d, e, f, g, h uint64 + isaac.aa = 0 + isaac.bb = 0 + isaac.cc = 0 + + a = 0x9e3779b97f4a7c13 + b = 0x9e3779b97f4a7c13 + c = 0x9e3779b97f4a7c13 + d = 0x9e3779b97f4a7c13 + e = 0x9e3779b97f4a7c13 + f = 0x9e3779b97f4a7c13 + g = 0x9e3779b97f4a7c13 + h = 0x9e3779b97f4a7c13 + + // scramble it + for i := 0; i < 4; i++ { + mix(&a, &b, &c, &d, &e, &f, &g, &h) + } + + // fill in mm[] with messy stuff + for i := 0; i < RANDSIZ; i += 8 { + if flag { + a += isaac.randrsl[i] + b += isaac.randrsl[i+1] + c += isaac.randrsl[i+2] + d += isaac.randrsl[i+3] + e += isaac.randrsl[i+4] + f += isaac.randrsl[i+5] + g += isaac.randrsl[i+6] + h += isaac.randrsl[i+7] + } + mix(&a, &b, &c, &d, &e, &f, &g, &h) + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + + if flag { + // do a second pass to make all of the seed affect all of mm + for i := 0; i < RANDSIZ; i += 8 { + a += isaac.mm[i] + b += isaac.mm[i+1] + c += isaac.mm[i+2] + d += isaac.mm[i+3] + e += isaac.mm[i+4] + f += isaac.mm[i+5] + g += isaac.mm[i+6] + h += isaac.mm[i+7] + mix(&a, &b, &c, &d, &e, &f, &g, &h) + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + } + + isaac.isaac() + isaac.randcnt = RANDSIZ +} + +func mix(a, b, c, d, e, f, g, h *uint64) { + *a -= *e + *f ^= *h >> 9 + *h += *a + + *b -= *f + *g ^= *a << 9 + *a += *b + + *c -= *g + *h ^= *b >> 23 + *b += *c + + *d -= *h + *a ^= *c << 15 + *c += *d + + *e -= *a + *b ^= *d >> 14 + *d += *e + + *f -= *b + *c ^= *e << 20 + *e += *f + + *g -= *c + *d ^= *f >> 17 + *f += *g + + *h -= *d + *e ^= *g << 14 + *g += *h +} + +func ind(mm []uint64, x uint64) uint64 { + return mm[(x>>3)&(RANDSIZ-1)] +} + +func (isaac *ISAAC) isaac() { + var a, b, x, y uint64 + a = isaac.aa + b = isaac.bb + isaac.cc + 1 + isaac.cc++ + + m := isaac.mm[:] + r := isaac.randrsl[:] + + var i, m2Index int + + // First half + for i = 0; i < RANDSIZ/2; i++ { + m2Index = i + RANDSIZ/2 + switch i % 4 { + case 0: + a = ^(a ^ (a << 21)) + m[m2Index] + case 1: + a = (a ^ (a >> 5)) + m[m2Index] + case 2: + a = (a ^ (a << 12)) + m[m2Index] + case 3: + a = (a ^ (a >> 33)) + m[m2Index] + } + x = m[i] + y = ind(m, x) + a + b + m[i] = y + b = ind(m, y>>RANDSIZL) + x + r[i] = b + } + + // Second half + for i = RANDSIZ / 2; i < RANDSIZ; i++ { + m2Index = i - RANDSIZ/2 + switch i % 4 { + case 0: + a = ^(a ^ (a << 21)) + m[m2Index] + case 1: + a = (a ^ (a >> 5)) + m[m2Index] + case 2: + a = (a ^ (a << 12)) + m[m2Index] + case 3: + a = (a ^ (a >> 33)) + m[m2Index] + } + x = m[i] + y = ind(m, x) + a + b + m[i] = y + b = ind(m, y>>RANDSIZL) + x + r[i] = b + } + + isaac.bb = b + isaac.aa = a +} + +// Return a 64 bit random integer. +func (isaac *ISAAC) Uint64() uint64 { + if isaac.randcnt == 0 { + isaac.isaac() + isaac.randcnt = RANDSIZ + } + isaac.randcnt-- + return isaac.randrsl[isaac.randcnt] +} + +var gencycle int = 0 +var bufferFor32 uint64 = uint64(0) + +// Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result. +func (isaac *ISAAC) Uint32() uint32 { + if gencycle == 0 { + bufferFor32 = isaac.Uint64() + gencycle = 1 + return uint32(bufferFor32 >> 32) + } + + gencycle = 0 + return uint32(bufferFor32 & 0xffffffff) +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkISAAC()' isaac64.gno +func benchmarkISAAC(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New() + + for i := 0; i < iterations; i++ { + _ = isaac.Uint64() + } + ufmt.Println(ufmt.Sprintf("ISAAC: generated %d uint64\n", iterations)) +} + +// The averageISAAC() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the ISAAC PRNG. +func averageISAAC(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New(987654321987654321, 123456789987654321, 1, 997755331886644220) + + var average float64 = 0 + var squares []uint64 = make([]uint64, iterations) + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("ISAAC average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("ISAAC standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("ISAAC theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno new file mode 100644 index 00000000000..239e7f818fb --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno @@ -0,0 +1,165 @@ +package isaac64 + +import ( + "math/rand" + "testing" +) + +type OpenISAAC struct { + Randrsl [256]uint64 + Randcnt uint64 + Mm [256]uint64 + Aa, Bb, Cc uint64 + Seed [256]uint64 +} + +func TestISAACSeeding(t *testing.T) { + isaac := New() +} + +func TestISAACRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.9273376778618531, + 0.327620245173309, + 0.49315436150113456, + 0.9222536383598948, + 0.2999297342641162, + 0.4050531597269049, + 0.5321357451089953, + 0.19478000239059667, + 0.5156043950865713, + 0.9233494881511063, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestISAACUint64(t *testing.T) { + isaac := New() + + expected := []uint64{ + 6781932227698873623, + 14800945299485332986, + 4114322996297394168, + 5328012296808356526, + 12789214124608876433, + 17611101631239575547, + 6877490613942924608, + 15954522518901325556, + 14180160756719376887, + 4977949063252893357, + } + + for i, exp := range expected { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func dupState(i *ISAAC) *OpenISAAC { + state := &OpenISAAC{} + state.Seed = i.seed + state.Randrsl = i.randrsl + state.Mm = i.mm + state.Aa = i.aa + state.Bb = i.bb + state.Cc = i.cc + state.Randcnt = i.randcnt + + return state +} + +func TestISAACMarshalUnmarshal(t *testing.T) { + isaac := New() + + expected1 := []uint64{ + 6781932227698873623, + 14800945299485332986, + 4114322996297394168, + 5328012296808356526, + 12789214124608876433, + } + + expected2 := []uint64{ + 17611101631239575547, + 6877490613942924608, + 15954522518901325556, + 14180160756719376887, + 4977949063252893357, + } + + for i, exp := range expected1 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := isaac.MarshalBinary() + + t.Logf("State: [%v]\n", dupState(isaac)) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := dupState(isaac) + + if err != nil { + t.Errorf("ISAAC.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + isaac.Uint64() + + for i, exp := range expected2 { + val := isaac.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%v]\n", dupState(isaac)) + + // Now restore the state of the PRNG + err = isaac.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%v]\n", dupState(isaac)) + + if state_before.Seed != dupState(isaac).Seed { + t.Errorf("Seed mismatch") + } + if state_before.Randrsl != dupState(isaac).Randrsl { + t.Errorf("Randrsl mismatch") + } + if state_before.Mm != dupState(isaac).Mm { + t.Errorf("Mm mismatch") + } + if state_before.Aa != dupState(isaac).Aa { + t.Errorf("Aa mismatch") + } + if state_before.Bb != dupState(isaac).Bb { + t.Errorf("Bb mismatch") + } + if state_before.Cc != dupState(isaac).Cc { + t.Errorf("Cc mismatch") + } + if state_before.Randcnt != dupState(isaac).Randcnt { + t.Errorf("Randcnt mismatch") + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD b/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD new file mode 100644 index 00000000000..00ed4412db0 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD @@ -0,0 +1,69 @@ +# package xorshift64star // import "gno.land/p/demo/math/rand/xorshift64star" + +Xorshift64* is a very fast psuedo-random number generation algorithm with strong +statistical properties. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the Xorshift64* PRNG algorithm. +This algorithm provides strong statistical performance with most seeds (just +don't seed it with zero), and the performance of this implementation in Gno is +more than four times faster than the default PCG implementation in `math/rand`. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +Xorshift64*: 1000000 Uint64 generated in 3.77s +Ratio: x4.11 times faster than PCG +``` + +Use it directly: + +``` +prng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = xorshift64star.New() +prng := rand.New(source) +``` + +## TYPES + +``` +type Xorshift64Star struct { + // Has unexported fields. +} +``` + +Xorshift64Star is a PRNG that implements the Xorshift64* algorithm. + +`func New(seed ...uint64) *Xorshift64Star` + New() creates a new instance of the PRNG with a given seed, which should + be a uint64. If no seed is provided, the PRNG will be seeded via the + gno.land/p/demo/entropy package. + +`func (xs *Xorshift64Star) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (xs *Xorshift64Star) Seed(seed ...uint64)` + Seed() implements the rand.Source interface. It provides a way to set the + seed for the PRNG. + +`func (xs *Xorshift64Star) Uint64() uint64` + Uint64() generates the next random uint64 value. + +`func (xs *Xorshift64Star) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod new file mode 100644 index 00000000000..bc40b1bc71b --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/wyhaines/rand/xorshift64star + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno new file mode 100644 index 00000000000..4934fe3a878 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno @@ -0,0 +1,172 @@ +// Xorshift64* is a very fast psuedo-random number generation algorithm with strong +// statistical properties. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the Xorshift64* PRNG algorithm. This algorithm provides +// strong statistical performance with most seeds (just don't seed it with zero), and the performance +// of this implementation in Gno is more than four times faster than the default PCG implementation in +// `math/rand`. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// Xorshift64*: 1000000 Uint64 generated in 3.77s +// Ratio: x4.11 times faster than PCG +// +// Use it directly: +// +// prng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = xorshift64star.New() +// prng := rand.New(source) +package xorshift64star + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" +) + +// Xorshift64Star is a PRNG that implements the Xorshift64* algorithm. +type Xorshift64Star struct { + seed uint64 +} + +// New() creates a new instance of the PRNG with a given seed, which +// should be a uint64. If no seed is provided, the PRNG will be seeded via the +// gno.land/p/demo/entropy package. +func New(seed ...uint64) *Xorshift64Star { + xs := &Xorshift64Star{} + xs.Seed(seed...) + return xs +} + +// Seed() implements the rand.Source interface. It provides a way to set the seed for the PRNG. +func (xs *Xorshift64Star) Seed(seed ...uint64) { + if len(seed) == 0 { + e := entropy.New() + xs.seed = e.Value64() + } else { + xs.seed = seed[0] + } +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +// binary.bigEndian.Uint64, copied to avoid dependency +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint64, copied to avoid dependency +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalXorshift64StarLabel = []byte("xorshift64*:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (xs *Xorshift64Star) MarshalBinary() ([]byte, error) { + b := make([]byte, 20) + copy(b, marshalXorshift64StarLabel) + bePutUint64(b[12:], xs.seed) + return b, nil +} + +// errUnmarshalXorshift64Star is returned when unmarshalling fails. +var errUnmarshalXorshift64Star = errors.New("invalid Xorshift64* encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (xs *Xorshift64Star) UnmarshalBinary(data []byte) error { + if len(data) != 20 || string(data[:12]) != string(marshalXorshift64StarLabel) { + return errUnmarshalXorshift64Star + } + xs.seed = beUint64(data[12:]) + return nil +} + +// Uint64() generates the next random uint64 value. +func (xs *Xorshift64Star) Uint64() uint64 { + xs.seed ^= xs.seed >> 12 + xs.seed ^= xs.seed << 25 + xs.seed ^= xs.seed >> 27 + xs.seed *= 2685821657736338717 + return xs.seed // Operations naturally wrap around in uint64 +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkXorshift64Star()' xorshift64star.gno +func benchmarkXorshift64Star(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs64s := New() + + for i := 0; i < iterations; i++ { + _ = xs64s.Uint64() + } + ufmt.Println(ufmt.Sprintf("Xorshift64*: generate %d uint64\n", iterations)) +} + +// The averageXorshift64Star() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the Xorshift64* PRNG. +func averageXorshift64Star(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs64s := New() + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := xs64s.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("Xorshift64* average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("Xorshift64* standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("Xorshift64* theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno new file mode 100644 index 00000000000..8a73bd9718d --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno @@ -0,0 +1,134 @@ +package xorshift64star + +import ( + "math/rand" + "testing" +) + +func TestXorshift64StarSeeding(t *testing.T) { + xs64s := New() + value1 := xs64s.Uint64() + + xs64s = New(987654321) + value2 := xs64s.Uint64() + + if value1 != 5083824587905981259 || value2 != 18211065302896784785 || value1 == value2 { + t.Errorf("Expected 5083824587905981259 to be != to 18211065302896784785; got: %d == %d", value1, value2) + } +} + +func TestXorshift64StarRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + .8344002228310946, + 0.01777174153236205, + 0.23521769507865276, + 0.5387610198576143, + 0.631539862225968, + 0.9369068148346704, + 0.6387002315083188, + 0.5047507613688854, + 0.5208486273732391, + 0.25023746271541747, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestXorshift64StarUint64(t *testing.T) { + xs64s := New() + + expected := []uint64{ + 5083824587905981259, + 4607286371009545754, + 2070557085263023674, + 14094662988579565368, + 2910745910478213381, + 18037409026311016155, + 17169624916429864153, + 10459214929523155306, + 11840179828060641081, + 1198750959721587199, + } + + for i, exp := range expected { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func TestXorshift64StarMarshalUnmarshal(t *testing.T) { + xs64s := New() + + expected1 := []uint64{ + 5083824587905981259, + 4607286371009545754, + 2070557085263023674, + 14094662988579565368, + 2910745910478213381, + } + + expected2 := []uint64{ + 18037409026311016155, + 17169624916429864153, + 10459214929523155306, + 11840179828060641081, + 1198750959721587199, + } + + for i, exp := range expected1 { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := xs64s.MarshalBinary() + + t.Logf("Original State: [%x]\n", xs64s.seed) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := xs64s.seed + + if err != nil { + t.Errorf("Xorshift64Star.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + xs64s.Uint64() + + for i, exp := range expected2 { + val := xs64s.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%x]\n", xs64s.seed) + + // Now restore the state of the PRNG + err = xs64s.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%x]\n", xs64s.seed) + + if state_before != xs64s.seed { + t.Errorf("States before and after marshal/unmarshal are not equal; go %x and %x", state_before, xs64s.seed) + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD new file mode 100644 index 00000000000..444d1e1cdd9 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD @@ -0,0 +1,60 @@ +# package xorshiftr128plus // import "gno.land/p/demo/math/rand/xorshiftr128plus" + +Xorshiftr128+ is a very fast psuedo-random number generation algorithm with +strong statistical properties. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the Xorshiftr128+ PRNG algorithm. +This algorithm provides strong statistical performance with most seeds (just +don't seed it with zeros), and the performance of this implementation in Gno is +more than four times faster than the default PCG implementation in `math/rand`. + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.48s +Xorshiftr128+: 1000000 Uint64 generated in 3.22s +Ratio: x4.81 times faster than PCG +``` + +Use it directly: + +``` +prng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = xorshiftr128plus.New() +prng := rand.New(source) +``` + +## TYPES + +``` +type Xorshiftr128Plus struct { + // Has unexported fields. +} +``` + +`func New(seeds ...uint64) *Xorshiftr128Plus` + +`func (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (x *Xorshiftr128Plus) Seed(s1, s2 uint64)` + +`func (x *Xorshiftr128Plus) Uint64() uint64` + +`func (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod new file mode 100644 index 00000000000..c778fc72550 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/wyhaines/rand/xorshiftr128plus + +require ( + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest +) diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno new file mode 100644 index 00000000000..d950ab5108a --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno @@ -0,0 +1,186 @@ +// Xorshiftr128+ is a very fast psuedo-random number generation algorithm with strong +// statistical properties. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the Xorshiftr128+ PRNG algorithm. This algorithm provides +// strong statistical performance with most seeds (just don't seed it with zeros), and the performance +// of this implementation in Gno is more than four times faster than the default PCG implementation in +// `math/rand`. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.48s +// Xorshiftr128+: 1000000 Uint64 generated in 3.22s +// Ratio: x4.81 times faster than PCG +// +// Use it directly: +// +// prng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = xorshiftr128plus.New() +// prng := rand.New(source) +package xorshiftr128plus + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" +) + +type Xorshiftr128Plus struct { + seed [2]uint64 // Seeds +} + +func New(seeds ...uint64) *Xorshiftr128Plus { + var s1, s2 uint64 + seed_length := len(seeds) + if seed_length < 2 { + e := entropy.New() + if seed_length == 0 { + s1 = e.Value64() + s2 = e.Value64() + } else { + s1 = seeds[0] + s2 = e.Value64() + } + } else { + s1 = seeds[0] + s2 = seeds[1] + } + + prng := &Xorshiftr128Plus{} + prng.Seed(s1, s2) + return prng +} + +func (x *Xorshiftr128Plus) Seed(s1, s2 uint64) { + if s1 == 0 && s2 == 0 { + panic("Seeds must not both be zero") + } + x.seed[0] = s1 + x.seed[1] = s2 +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +// binary.bigEndian.Uint64, copied to avoid dependency +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint64, copied to avoid dependency +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalXorshiftr128PlusLabel = []byte("xorshiftr128+:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error) { + b := make([]byte, 30) + copy(b, marshalXorshiftr128PlusLabel) + bePutUint64(b[14:], xs.seed[0]) + bePutUint64(b[22:], xs.seed[1]) + return b, nil +} + +// errUnmarshalXorshiftr128Plus is returned when unmarshalling fails. +var errUnmarshalXorshiftr128Plus = errors.New("invalid Xorshiftr128Plus encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error { + if len(data) != 30 || string(data[:14]) != string(marshalXorshiftr128PlusLabel) { + return errUnmarshalXorshiftr128Plus + } + xs.seed[0] = beUint64(data[14:]) + xs.seed[1] = beUint64(data[22:]) + return nil +} + +func (x *Xorshiftr128Plus) Uint64() uint64 { + x0 := x.seed[0] + x1 := x.seed[1] + x.seed[0] = x1 + x0 ^= x0 << 23 + x0 ^= x0 >> 17 + x0 ^= x1 + x.seed[1] = x0 + x1 + return x.seed[1] +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkXorshiftr128Plus()' xorshiftr128plus.gno +func benchmarkXorshiftr128Plus(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs128p := New() + + for i := 0; i < iterations; i++ { + _ = xs128p.Uint64() + } + ufmt.Println(ufmt.Sprintf("Xorshiftr128Plus: generate %d uint64\n", iterations)) +} + +// The averageXorshiftr128Plus() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the Xorshiftr128+ PRNG. +func averageXorshiftr128Plus(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs128p := New() + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := xs128p.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno new file mode 100644 index 00000000000..c5d86edd073 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno @@ -0,0 +1,142 @@ +package xorshiftr128plus + +import ( + "math/rand" + "testing" +) + +func TestXorshift64StarSeeding(t *testing.T) { + xs128p := New() + value1 := xs128p.Uint64() + + xs128p = New(987654321) + value2 := xs128p.Uint64() + + xs128p = New(987654321, 9876543210) + value3 := xs128p.Uint64() + + if value1 != 13970141264473760763 || + value2 != 17031892808144362974 || + value3 != 8285073084540510 || + value1 == value2 || + value2 == value3 || + value1 == value3 { + t.Errorf("Expected three different values: 13970141264473760763, 17031892808144362974, and 8285073084540510\n got: %d, %d, %d", value1, value2, value3) + } +} + +func TestXorshiftr128PlusRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.9199548549485674, + 0.0027491282372705816, + 0.31493362274701164, + 0.3531250819119609, + 0.09957852858060356, + 0.731941362705936, + 0.3476937688876708, + 0.1444018086140385, + 0.9106467321832331, + 0.8024870151488901, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestXorshiftr128PlusUint64(t *testing.T) { + xs128p := New(987654321, 9876543210) + + expected := []uint64{ + 8285073084540510, + 97010855169053386, + 11353359435625603792, + 10289232744262291728, + 14019961444418950453, + 15829492476941720545, + 2764732928842099222, + 6871047144273883379, + 16142204260470661970, + 11803223757041229095, + } + + for i, exp := range expected { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func TestXorshiftr128PlusMarshalUnmarshal(t *testing.T) { + xs128p := New(987654321, 9876543210) + + expected1 := []uint64{ + 8285073084540510, + 97010855169053386, + 11353359435625603792, + 10289232744262291728, + 14019961444418950453, + } + + expected2 := []uint64{ + 15829492476941720545, + 2764732928842099222, + 6871047144273883379, + 16142204260470661970, + 11803223757041229095, + } + + for i, exp := range expected1 { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := xs128p.MarshalBinary() + + t.Logf("Original State: [%x]\n", xs128p.seed) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := xs128p.seed + + if err != nil { + t.Errorf("Xorshiftr128Plus.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + xs128p.Uint64() + + for i, exp := range expected2 { + val := xs128p.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%x]\n", xs128p.seed) + + // Now restore the state of the PRNG + err = xs128p.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%x]\n", xs128p.seed) + + if state_before != xs128p.seed { + t.Errorf("States before and after marshal/unmarshal are not equal; go %x and %x", state_before, xs128p.seed) + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} From 7ce29ff1512353d02a3dcd44dd2fd98a4c6ee869 Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Thu, 5 Dec 2024 11:13:06 +0100 Subject: [PATCH 33/86] feat: A fully featured btree implementation (#3126) This is a fully featured btree implementation. A friend gave me some incomplete (and broken) btree code for Go, and when I started reworking it, I discovered that it was a broken semi-copy of an old version of Google's btree for Go. I finished reworking it so that it adhere's to that original Google version's API, though there are some differences internally in places, and I think that my version is much easier to follow and to understand. This implementation is quite a bit faster than the AVL tree. I will add links to some benchmarks that I did in a comment. This implementation supports copy-on-write for the trees, for inexpensively creating copies of a tree that are effectively isolated from each other with respect to changes that happen after the fork.
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 - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--- examples/gno.land/p/demo/btree/btree.gno | 1114 +++++++++++++++++ examples/gno.land/p/demo/btree/btree_test.gno | 678 ++++++++++ examples/gno.land/p/demo/btree/gno.mod | 1 + 3 files changed, 1793 insertions(+) create mode 100644 examples/gno.land/p/demo/btree/btree.gno create mode 100644 examples/gno.land/p/demo/btree/btree_test.gno create mode 100644 examples/gno.land/p/demo/btree/gno.mod diff --git a/examples/gno.land/p/demo/btree/btree.gno b/examples/gno.land/p/demo/btree/btree.gno new file mode 100644 index 00000000000..f909ec6bc91 --- /dev/null +++ b/examples/gno.land/p/demo/btree/btree.gno @@ -0,0 +1,1114 @@ +////////// +// +// Copyright 2014 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// Copyright 2024 New Tendermint +// +// This Gno port of the original Go BTree is substantially rewritten/reimplemented +// from the original, primarily for clarity of code, clarity of documentation, +// and for compatibility with Gno. +// +// Authors: +// Original version authors -- https://github.com/google/btree/graphs/contributors +// Kirk Haines +// +////////// + +// Package btree implements in-memory B-Trees of arbitrary degree. +// +// It has a flatter structure than an equivalent red-black or other binary tree, +// which may yield better memory usage and/or performance. +package btree + +import "sort" + +////////// +// +// Types +// +////////// + +// BTreeOption is a function interface for setting options on a btree with `New()`. +type BTreeOption func(*BTree) + +// BTree is an implementation of a B-Tree. +// +// BTree stores Record instances in an ordered structure, allowing easy insertion, +// removal, and iteration. +type BTree struct { + degree int + length int + root *node + cowCtx *copyOnWriteContext +} + +// Any type that implements this interface can be stored in the BTree. This allows considerable +// +// flexiblity in storage within the BTree. +type Record interface { + // Less compares self to `than`, returning true if self is less than `than` + Less(than Record) bool +} + +// records is the storage within a node. It is expressed as a slice of Record, where a Record +// is any struct that implements the Record interface. +type records []Record + +// node is an internal node in a tree. +// +// It must at all times maintain on of the two conditions: +// - len(children) == 0, len(records) unconstrained +// - len(children) == len(records) + 1 +type node struct { + records records + children children + cowCtx *copyOnWriteContext +} + +// children is the list of child nodes below the current node. It is a slice of nodes. +type children []*node + +// FreeNodeList represents a slice of nodes which are available for reuse. The default +// behavior of New() is for each BTree instance to have its own FreeNodeList. However, +// it is possible for multiple instances of BTree to share the same tree. If one uses +// New(WithFreeNodeList()) to create a tree, one may pass an existing FreeNodeList, allowing +// multiple trees to use a single list. In an application with multiple trees, it might +// be more efficient to allocate a single FreeNodeList with a significant initial capacity, +// and then have all of the trees use that same large FreeNodeList. +type FreeNodeList struct { + nodes []*node +} + +// copyOnWriteContext manages node ownership and ensures that cloned trees +// maintain isolation from each other when a node is changed. +// +// Ownership Rules: +// - Each node is associated with a specific copyOnWriteContext. +// - A tree can modify a node directly only if the tree's context matches the node's context. +// - If a tree attempts to modify a node with a different context, it must create a +// new, writable copy of that node (i.e., perform a clone) before making changes. +// +// Write Operation Invariant: +// - During any write operation, the current node being modified must have the same +// context as the tree requesting the write. +// - To maintain this invariant, before descending into a child node, the system checks +// if the child’s context matches the tree's context. +// - If the contexts match, the node can be modified in place. +// - If the contexts do not match, a mutable copy of the child node is created with the +// correct context before proceeding. +// +// Practical Implications: +// - The node currently being modified inherits the requesting tree's context, allowing +// in-place modifications. +// - Child nodes may initially have different contexts. Before any modification, these +// children are copied to ensure they share the correct context, enabling safe and +// isolated updates without affecting other trees that might be referencing the original nodes. +// +// Example Usage: +// When a tree performs a write operation (e.g., inserting or deleting a node), it uses +// its copyOnWriteContext to determine whether it can modify nodes directly or needs to +// create copies. This mechanism ensures that trees can share nodes efficiently while +// maintaining data integrity. +type copyOnWriteContext struct { + nodes *FreeNodeList +} + +// Record implements an interface with a single function, Less. Any type that implements +// RecordIterator allows callers of all of the iteration functions for the BTree +// to evaluate an element of the tree as it is traversed. The function will receive +// a stored element from the tree. The function must return either a true or a false value. +// True indicates that iteration should continue, while false indicates that it should halt. +type RecordIterator func(i Record) bool + +////////// +// +// Functions +// +////////// + +// NewFreeNodeList creates a new free list. +// size is the maximum size of the returned free list. +func NewFreeNodeList(size int) *FreeNodeList { + return &FreeNodeList{nodes: make([]*node, 0, size)} +} + +func (freeList *FreeNodeList) newNode() (nodeInstance *node) { + index := len(freeList.nodes) - 1 + if index < 0 { + return new(node) + } + nodeInstance = freeList.nodes[index] + freeList.nodes[index] = nil + freeList.nodes = freeList.nodes[:index] + + return nodeInstance +} + +// freeNode adds the given node to the list, returning true if it was added +// and false if it was discarded. + +func (freeList *FreeNodeList) freeNode(nodeInstance *node) (nodeWasAdded bool) { + if len(freeList.nodes) < cap(freeList.nodes) { + freeList.nodes = append(freeList.nodes, nodeInstance) + nodeWasAdded = true + } + return +} + +// A default size for the free node list. We might want to run some benchmarks to see if +// there are any pros or cons to this size versus other sizes. This seems to be a reasonable +// compromise to reduce GC pressure by reusing nodes where possible, without stacking up too +// much baggage in a given tree. +const DefaultFreeNodeListSize = 32 + +// WithDegree sets the degree of the B-Tree. +func WithDegree(degree int) BTreeOption { + return func(bt *BTree) { + if degree <= 1 { + panic("Degrees less than 1 do not make any sense for a BTree. Please provide a degree of 1 or greater.") + } + bt.degree = degree + } +} + +// WithFreeNodeList sets a custom free node list for the B-Tree. +func WithFreeNodeList(freeList *FreeNodeList) BTreeOption { + return func(bt *BTree) { + bt.cowCtx = ©OnWriteContext{nodes: freeList} + } +} + +// New creates a new B-Tree with optional configurations. If configuration is not provided, +// it will default to 16 element nodes. Degree may not be less than 1 (which effectively +// makes the tree into a binary tree). +// +// `New(WithDegree(2))`, for example, will create a 2-3-4 tree (each node contains 1-3 records +// and 2-4 children). +// +// `New(WithFreeNodeList(NewFreeNodeList(64)))` will create a tree with a degree of 16, and +// with a free node list with a size of 64. +func New(options ...BTreeOption) *BTree { + btree := &BTree{ + degree: 16, // default degree + cowCtx: ©OnWriteContext{nodes: NewFreeNodeList(DefaultFreeNodeListSize)}, + } + for _, opt := range options { + opt(btree) + } + return btree +} + +// insertAt inserts a value into the given index, pushing all subsequent values +// forward. +func (recordsSlice *records) insertAt(index int, newRecord Record) { + originalLength := len(*recordsSlice) + + // Extend the slice by one element + *recordsSlice = append(*recordsSlice, nil) + + // Move elements from the end to avoid overwriting during the copy + // TODO: Make this work with slice appends, instead. It should be faster? + if index < originalLength { + for position := originalLength; position > index; position-- { + (*recordsSlice)[position] = (*recordsSlice)[position-1] + } + } + + // Insert the new record + (*recordsSlice)[index] = newRecord +} + +// removeAt removes a Record from the records slice at the specified index. +// It shifts subsequent records to fill the gap and returns the removed Record. +func (recordSlicePointer *records) removeAt(index int) Record { + recordSlice := *recordSlicePointer + removedRecord := recordSlice[index] + copy(recordSlice[index:], recordSlice[index+1:]) + recordSlice[len(recordSlice)-1] = nil + *recordSlicePointer = recordSlice[:len(recordSlice)-1] + + return removedRecord +} + +// Pop removes and returns the last Record from the records slice. +// It also clears the reference to the removed Record to aid garbage collection. +func (r *records) pop() Record { + recordSlice := *r + lastIndex := len(recordSlice) - 1 + removedRecord := recordSlice[lastIndex] + recordSlice[lastIndex] = nil + *r = recordSlice[:lastIndex] + return removedRecord +} + +// This slice is intended only as a supply of records for the truncate function +// that follows, and it should not be changed or altered. +var emptyRecords = make(records, 32) + +// truncate reduces the length of the slice to the specified index, +// and clears the elements beyond that index to prevent memory leaks. +// The index must be less than or equal to the current length of the slice. +func (originalSlice *records) truncate(index int) { + // Split the slice into the part to keep and the part to clear. + recordsToKeep := (*originalSlice)[:index] + recordsToClear := (*originalSlice)[index:] + + // Update the original slice to only contain the records to keep. + *originalSlice = recordsToKeep + + // Clear the memory of the part that was truncated. + for len(recordsToClear) > 0 { + // Copy empty values from `emptyRecords` to the recordsToClear slice. + // This effectively "clears" the memory by overwriting elements. + numCleared := copy(recordsToClear, emptyRecords) + recordsToClear = recordsToClear[numCleared:] + } +} + +// Find determines the appropriate index at which a given Record should be inserted +// into the sorted records slice. If the Record already exists in the slice, +// the method returns its index and sets found to true. +// +// Parameters: +// - record: The Record to search for within the records slice. +// +// Returns: +// - insertIndex: The index at which the Record should be inserted. +// - found: A boolean indicating whether the Record already exists in the slice. +func (recordsSlice records) find(record Record) (insertIndex int, found bool) { + totalRecords := len(recordsSlice) + + // Perform a binary search to find the insertion point for the record + insertionPoint := sort.Search(totalRecords, func(currentIndex int) bool { + return record.Less(recordsSlice[currentIndex]) + }) + + if insertionPoint > 0 { + previousRecord := recordsSlice[insertionPoint-1] + + if !previousRecord.Less(record) { + return insertionPoint - 1, true + } + } + + return insertionPoint, false +} + +// insertAt inserts a value into the given index, pushing all subsequent values +// forward. +func (childSlice *children) insertAt(index int, n *node) { + originalLength := len(*childSlice) + + // Extend the slice by one element + *childSlice = append(*childSlice, nil) + + // Move elements from the end to avoid overwriting during the copy + if index < originalLength { + for i := originalLength; i > index; i-- { + (*childSlice)[i] = (*childSlice)[i-1] + } + } + + // Insert the new record + (*childSlice)[index] = n +} + +// removeAt removes a Record from the records slice at the specified index. +// It shifts subsequent records to fill the gap and returns the removed Record. +func (childSlicePointer *children) removeAt(index int) *node { + childSlice := *childSlicePointer + removedChild := childSlice[index] + copy(childSlice[index:], childSlice[index+1:]) + childSlice[len(childSlice)-1] = nil + *childSlicePointer = childSlice[:len(childSlice)-1] + + return removedChild +} + +// Pop removes and returns the last Record from the records slice. +// It also clears the reference to the removed Record to aid garbage collection. +func (childSlicePointer *children) pop() *node { + childSlice := *childSlicePointer + lastIndex := len(childSlice) - 1 + removedChild := childSlice[lastIndex] + childSlice[lastIndex] = nil + *childSlicePointer = childSlice[:lastIndex] + return removedChild +} + +// This slice is intended only as a supply of records for the truncate function +// that follows, and it should not be changed or altered. +var emptyChildren = make(children, 32) + +// truncate reduces the length of the slice to the specified index, +// and clears the elements beyond that index to prevent memory leaks. +// The index must be less than or equal to the current length of the slice. +func (originalSlice *children) truncate(index int) { + // Split the slice into the part to keep and the part to clear. + childrenToKeep := (*originalSlice)[:index] + childrenToClear := (*originalSlice)[index:] + + // Update the original slice to only contain the records to keep. + *originalSlice = childrenToKeep + + // Clear the memory of the part that was truncated. + for len(childrenToClear) > 0 { + // Copy empty values from `emptyChildren` to the recordsToClear slice. + // This effectively "clears" the memory by overwriting elements. + numCleared := copy(childrenToClear, emptyChildren) + + // Slice recordsToClear to exclude the elements that were just cleared. + childrenToClear = childrenToClear[numCleared:] + } +} + +// mutableFor creates a mutable copy of the node if the current node does not +// already belong to the provided copy-on-write context (COW). If the node is +// already associated with the given COW context, it returns the current node. +// +// Parameters: +// - cowCtx: The copy-on-write context that should own the returned node. +// +// Returns: +// - A pointer to the mutable node associated with the given COW context. +// +// If the current node belongs to a different COW context, this function: +// - Allocates a new node using the provided context. +// - Copies the node’s records and children slices into the newly allocated node. +// - Returns the new node which is now owned by the given COW context. +func (n *node) mutableFor(cowCtx *copyOnWriteContext) *node { + // If the current node is already owned by the provided context, return it as-is. + if n.cowCtx == cowCtx { + return n + } + + // Create a new node in the provided context. + newNode := cowCtx.newNode() + + // Copy the records from the current node into the new node. + newNode.records = append(newNode.records[:0], n.records...) + + // Copy the children from the current node into the new node. + newNode.children = append(newNode.children[:0], n.children...) + + return newNode +} + +// mutableChild ensures that the child node at the given index is mutable and +// associated with the same COW context as the parent node. If the child node +// belongs to a different context, a copy of the child is created and stored in the +// parent node. +// +// Parameters: +// - i: The index of the child node to be made mutable. +// +// Returns: +// - A pointer to the mutable child node. +func (n *node) mutableChild(i int) *node { + // Ensure that the child at index `i` is mutable and belongs to the same context as the parent. + mutableChildNode := n.children[i].mutableFor(n.cowCtx) + // Update the child node reference in the current node to the mutable version. + n.children[i] = mutableChildNode + return mutableChildNode +} + +// split splits the given node at the given index. The current node shrinks, +// and this function returns the record that existed at that index and a new node +// containing all records/children after it. +func (n *node) split(i int) (Record, *node) { + record := n.records[i] + next := n.cowCtx.newNode() + next.records = append(next.records, n.records[i+1:]...) + n.records.truncate(i) + if len(n.children) > 0 { + next.children = append(next.children, n.children[i+1:]...) + n.children.truncate(i + 1) + } + return record, next +} + +// maybeSplitChild checks if a child should be split, and if so splits it. +// Returns whether or not a split occurred. +func (n *node) maybeSplitChild(i, maxRecords int) bool { + if len(n.children[i].records) < maxRecords { + return false + } + first := n.mutableChild(i) + record, second := first.split(maxRecords / 2) + n.records.insertAt(i, record) + n.children.insertAt(i+1, second) + return true +} + +// insert adds a record to the subtree rooted at the current node, ensuring that no node in the subtree +// exceeds the maximum number of allowed records (`maxRecords`). If an equivalent record is already present, +// it replaces the existing one and returns it; otherwise, it returns nil. +// +// Parameters: +// - record: The record to be inserted. +// - maxRecords: The maximum number of records allowed per node. +// +// Returns: +// - The record that was replaced if an equivalent record already existed, otherwise nil. +func (n *node) insert(record Record, maxRecords int) Record { + // Find the position where the new record should be inserted and check if an equivalent record already exists. + insertionIndex, recordExists := n.records.find(record) + + if recordExists { + // If an equivalent record is found, replace it and return the old record. + existingRecord := n.records[insertionIndex] + n.records[insertionIndex] = record + return existingRecord + } + + // If the current node is a leaf (has no children), insert the new record at the calculated index. + if len(n.children) == 0 { + n.records.insertAt(insertionIndex, record) + return nil + } + + // Check if the child node at the insertion index needs to be split due to exceeding maxRecords. + if n.maybeSplitChild(insertionIndex, maxRecords) { + // If a split occurred, compare the new record with the record moved up to the current node. + splitRecord := n.records[insertionIndex] + switch { + case record.Less(splitRecord): + // The new record belongs to the first (left) split node; no change to insertion index. + case splitRecord.Less(record): + // The new record belongs to the second (right) split node; move the insertion index to the next position. + insertionIndex++ + default: + // If the record is equivalent to the split record, replace it and return the old record. + existingRecord := n.records[insertionIndex] + n.records[insertionIndex] = record + return existingRecord + } + } + + // Recursively insert the record into the appropriate child node, now guaranteed to have space. + return n.mutableChild(insertionIndex).insert(record, maxRecords) +} + +// get finds the given key in the subtree and returns it. +func (n *node) get(key Record) Record { + i, found := n.records.find(key) + if found { + return n.records[i] + } else if len(n.children) > 0 { + return n.children[i].get(key) + } + return nil +} + +// min returns the first record in the subtree. +func min(n *node) Record { + if n == nil { + return nil + } + for len(n.children) > 0 { + n = n.children[0] + } + if len(n.records) == 0 { + return nil + } + return n.records[0] +} + +// max returns the last record in the subtree. +func max(n *node) Record { + if n == nil { + return nil + } + for len(n.children) > 0 { + n = n.children[len(n.children)-1] + } + if len(n.records) == 0 { + return nil + } + return n.records[len(n.records)-1] +} + +// toRemove details what record to remove in a node.remove call. +type toRemove int + +const ( + removeRecord toRemove = iota // removes the given record + removeMin // removes smallest record in the subtree + removeMax // removes largest record in the subtree +) + +// remove removes a record from the subtree rooted at the current node. +// +// Parameters: +// - record: The record to be removed (can be nil when the removal type indicates min or max). +// - minRecords: The minimum number of records a node should have after removal. +// - typ: The type of removal operation to perform (removeMin, removeMax, or removeRecord). +// +// Returns: +// - The record that was removed, or nil if no such record was found. +func (n *node) remove(record Record, minRecords int, removalType toRemove) Record { + var targetIndex int + var recordFound bool + + // Determine the index of the record to remove based on the removal type. + switch removalType { + case removeMax: + // If this node is a leaf, remove and return the last record. + if len(n.children) == 0 { + return n.records.pop() + } + targetIndex = len(n.records) // The last record index for removing max. + + case removeMin: + // If this node is a leaf, remove and return the first record. + if len(n.children) == 0 { + return n.records.removeAt(0) + } + targetIndex = 0 // The first record index for removing min. + + case removeRecord: + // Locate the index of the record to be removed. + targetIndex, recordFound = n.records.find(record) + if len(n.children) == 0 { + if recordFound { + return n.records.removeAt(targetIndex) + } + return nil // The record was not found in the leaf node. + } + + default: + panic("invalid removal type") + } + + // If the current node has children, handle the removal recursively. + if len(n.children[targetIndex].records) <= minRecords { + // If the target child node has too few records, grow it before proceeding with removal. + return n.growChildAndRemove(targetIndex, record, minRecords, removalType) + } + + // Get a mutable reference to the child node at the target index. + targetChild := n.mutableChild(targetIndex) + + // If the record to be removed was found in the current node: + if recordFound { + // Replace the current record with its predecessor from the child node, and return the removed record. + replacedRecord := n.records[targetIndex] + n.records[targetIndex] = targetChild.remove(nil, minRecords, removeMax) + return replacedRecord + } + + // Recursively remove the record from the child node. + return targetChild.remove(record, minRecords, removalType) +} + +// growChildAndRemove grows child 'i' to make sure it's possible to remove an +// record from it while keeping it at minRecords, then calls remove to actually +// remove it. +// +// Most documentation says we have to do two sets of special casing: +// 1. record is in this node +// 2. record is in child +// +// In both cases, we need to handle the two subcases: +// +// A) node has enough values that it can spare one +// B) node doesn't have enough values +// +// For the latter, we have to check: +// +// a) left sibling has node to spare +// b) right sibling has node to spare +// c) we must merge +// +// To simplify our code here, we handle cases #1 and #2 the same: +// If a node doesn't have enough records, we make sure it does (using a,b,c). +// We then simply redo our remove call, and the second time (regardless of +// whether we're in case 1 or 2), we'll have enough records and can guarantee +// that we hit case A. +func (n *node) growChildAndRemove(i int, record Record, minRecords int, typ toRemove) Record { + if i > 0 && len(n.children[i-1].records) > minRecords { + // Steal from left child + child := n.mutableChild(i) + stealFrom := n.mutableChild(i - 1) + stolenRecord := stealFrom.records.pop() + child.records.insertAt(0, n.records[i-1]) + n.records[i-1] = stolenRecord + if len(stealFrom.children) > 0 { + child.children.insertAt(0, stealFrom.children.pop()) + } + } else if i < len(n.records) && len(n.children[i+1].records) > minRecords { + // steal from right child + child := n.mutableChild(i) + stealFrom := n.mutableChild(i + 1) + stolenRecord := stealFrom.records.removeAt(0) + child.records = append(child.records, n.records[i]) + n.records[i] = stolenRecord + if len(stealFrom.children) > 0 { + child.children = append(child.children, stealFrom.children.removeAt(0)) + } + } else { + if i >= len(n.records) { + i-- + } + child := n.mutableChild(i) + // merge with right child + mergeRecord := n.records.removeAt(i) + mergeChild := n.children.removeAt(i + 1).mutableFor(n.cowCtx) + child.records = append(child.records, mergeRecord) + child.records = append(child.records, mergeChild.records...) + child.children = append(child.children, mergeChild.children...) + n.cowCtx.freeNode(mergeChild) + } + return n.remove(record, minRecords, typ) +} + +type direction int + +const ( + descend = direction(-1) + ascend = direction(+1) +) + +// iterate provides a simple method for iterating over elements in the tree. +// +// When ascending, the 'start' should be less than 'stop' and when descending, +// the 'start' should be greater than 'stop'. Setting 'includeStart' to true +// will force the iterator to include the first record when it equals 'start', +// thus creating a "greaterOrEqual" or "lessThanEqual" rather than just a +// "greaterThan" or "lessThan" queries. +func (n *node) iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) { + var ok, found bool + var index int + switch dir { + case ascend: + if start != nil { + index, _ = n.records.find(start) + } + for i := index; i < len(n.records); i++ { + if len(n.children) > 0 { + if hit, ok = n.children[i].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + if !includeStart && !hit && start != nil && !start.Less(n.records[i]) { + hit = true + continue + } + hit = true + if stop != nil && !n.records[i].Less(stop) { + return hit, false + } + if !iter(n.records[i]) { + return hit, false + } + } + if len(n.children) > 0 { + if hit, ok = n.children[len(n.children)-1].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + case descend: + if start != nil { + index, found = n.records.find(start) + if !found { + index = index - 1 + } + } else { + index = len(n.records) - 1 + } + for i := index; i >= 0; i-- { + if start != nil && !n.records[i].Less(start) { + if !includeStart || hit || start.Less(n.records[i]) { + continue + } + } + if len(n.children) > 0 { + if hit, ok = n.children[i+1].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + if stop != nil && !stop.Less(n.records[i]) { + return hit, false // continue + } + hit = true + if !iter(n.records[i]) { + return hit, false + } + } + if len(n.children) > 0 { + if hit, ok = n.children[0].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + } + return hit, true +} + +func (tree *BTree) Iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) { + return tree.root.iterate(dir, start, stop, includeStart, hit, iter) +} + +// Clone creates a new BTree instance that shares the current tree's structure using a copy-on-write (COW) approach. +// +// How Cloning Works: +// - The cloned tree (`clonedTree`) shares the current tree’s nodes in a read-only state. This means that no additional memory +// is allocated for shared nodes, and read operations on the cloned tree are as fast as on the original tree. +// - When either the original tree (`t`) or the cloned tree (`clonedTree`) needs to perform a write operation (such as an insert, delete, etc.), +// a new copy of the affected nodes is created on-demand. This ensures that modifications to one tree do not affect the other. +// +// Performance Implications: +// - **Clone Creation:** The creation of a clone is inexpensive since it only involves copying references to the original tree's nodes +// and creating new copy-on-write contexts. +// - **Read Operations:** Reading from either the original tree or the cloned tree has no additional performance overhead compared to the original tree. +// - **Write Operations:** The first write operation on either tree may experience a slight slow-down due to the allocation of new nodes, +// but subsequent write operations will perform at the same speed as if the tree were not cloned. +// +// Returns: +// - A new BTree instance (`clonedTree`) that shares the original tree's structure. +func (t *BTree) Clone() *BTree { + // Create two independent copy-on-write contexts, one for the original tree (`t`) and one for the cloned tree. + originalContext := *t.cowCtx + clonedContext := *t.cowCtx + + // Create a shallow copy of the current tree, which will be the new cloned tree. + clonedTree := *t + + // Assign the new contexts to their respective trees. + t.cowCtx = &originalContext + clonedTree.cowCtx = &clonedContext + + return &clonedTree +} + +// maxRecords returns the max number of records to allow per node. +func (t *BTree) maxRecords() int { + return t.degree*2 - 1 +} + +// minRecords returns the min number of records to allow per node (ignored for the +// root node). +func (t *BTree) minRecords() int { + return t.degree - 1 +} + +func (c *copyOnWriteContext) newNode() (n *node) { + n = c.nodes.newNode() + n.cowCtx = c + return +} + +type freeType int + +const ( + ftFreelistFull freeType = iota // node was freed (available for GC, not stored in nodes) + ftStored // node was stored in the nodes for later use + ftNotOwned // node was ignored by COW, since it's owned by another one +) + +// freeNode frees a node within a given COW context, if it's owned by that +// context. It returns what happened to the node (see freeType const +// documentation). +func (c *copyOnWriteContext) freeNode(n *node) freeType { + if n.cowCtx == c { + // clear to allow GC + n.records.truncate(0) + n.children.truncate(0) + n.cowCtx = nil + if c.nodes.freeNode(n) { + return ftStored + } else { + return ftFreelistFull + } + } else { + return ftNotOwned + } +} + +// Insert adds the given record to the B-tree. If a record already exists in the tree with the same value, +// it is replaced, and the old record is returned. Otherwise, it returns nil. +// +// Notes: +// - The function panics if a nil record is provided as input. +// - If the root node is empty, a new root node is created and the record is inserted. +// +// Parameters: +// - record: The record to be inserted into the B-tree. +// +// Returns: +// - The replaced record if an equivalent record already exists, or nil if no replacement occurred. +func (t *BTree) Insert(record Record) Record { + if record == nil { + panic("nil record cannot be added to BTree") + } + + // If the tree is empty (no root), create a new root node and insert the record. + if t.root == nil { + t.root = t.cowCtx.newNode() + t.root.records = append(t.root.records, record) + t.length++ + return nil + } + + // Ensure that the root node is mutable (associated with the current tree's copy-on-write context). + t.root = t.root.mutableFor(t.cowCtx) + + // If the root node is full (contains the maximum number of records), split the root. + if len(t.root.records) >= t.maxRecords() { + // Split the root node, promoting the middle record and creating a new child node. + middleRecord, newChildNode := t.root.split(t.maxRecords() / 2) + + // Create a new root node to hold the promoted middle record. + oldRoot := t.root + t.root = t.cowCtx.newNode() + t.root.records = append(t.root.records, middleRecord) + t.root.children = append(t.root.children, oldRoot, newChildNode) + } + + // Insert the new record into the subtree rooted at the current root node. + replacedRecord := t.root.insert(record, t.maxRecords()) + + // If no record was replaced, increase the tree's length. + if replacedRecord == nil { + t.length++ + } + + return replacedRecord +} + +// Delete removes an record equal to the passed in record from the tree, returning +// it. If no such record exists, returns nil. +func (t *BTree) Delete(record Record) Record { + return t.deleteRecord(record, removeRecord) +} + +// DeleteMin removes the smallest record in the tree and returns it. +// If no such record exists, returns nil. +func (t *BTree) DeleteMin() Record { + return t.deleteRecord(nil, removeMin) +} + +// Shift is identical to DeleteMin. If the tree is thought of as an ordered list, then Shift() +// removes the element at the start of the list, the smallest element, and returns it. +func (t *BTree) Shift() Record { + return t.deleteRecord(nil, removeMin) +} + +// DeleteMax removes the largest record in the tree and returns it. +// If no such record exists, returns nil. +func (t *BTree) DeleteMax() Record { + return t.deleteRecord(nil, removeMax) +} + +// Pop is identical to DeleteMax. If the tree is thought of as an ordered list, then Shift() +// removes the element at the end of the list, the largest element, and returns it. +func (t *BTree) Pop() Record { + return t.deleteRecord(nil, removeMax) +} + +// deleteRecord removes a record from the B-tree based on the specified removal type (removeMin, removeMax, or removeRecord). +// It returns the removed record if it was found, or nil if no matching record was found. +// +// Parameters: +// - record: The record to be removed (can be nil if the removal type indicates min or max). +// - removalType: The type of removal operation to perform (removeMin, removeMax, or removeRecord). +// +// Returns: +// - The removed record if it existed in the tree, or nil if it was not found. +func (t *BTree) deleteRecord(record Record, removalType toRemove) Record { + // If the tree is empty or the root has no records, return nil. + if t.root == nil || len(t.root.records) == 0 { + return nil + } + + // Ensure the root node is mutable (associated with the tree's copy-on-write context). + t.root = t.root.mutableFor(t.cowCtx) + + // Attempt to remove the specified record from the root node. + removedRecord := t.root.remove(record, t.minRecords(), removalType) + + // Check if the root node has become empty but still has children. + // In this case, the tree height should be reduced, making the first child the new root. + if len(t.root.records) == 0 && len(t.root.children) > 0 { + oldRoot := t.root + t.root = t.root.children[0] + // Free the old root node, as it is no longer needed. + t.cowCtx.freeNode(oldRoot) + } + + // If a record was successfully removed, decrease the tree's length. + if removedRecord != nil { + t.length-- + } + + return removedRecord +} + +// AscendRange calls the iterator for every value in the tree within the range +// [greaterOrEqual, lessThan), until iterator returns false. +func (t *BTree) AscendRange(greaterOrEqual, lessThan Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, greaterOrEqual, lessThan, true, false, iterator) +} + +// AscendLessThan calls the iterator for every value in the tree within the range +// [first, pivot), until iterator returns false. +func (t *BTree) AscendLessThan(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, nil, pivot, false, false, iterator) +} + +// AscendGreaterOrEqual calls the iterator for every value in the tree within +// the range [pivot, last], until iterator returns false. +func (t *BTree) AscendGreaterOrEqual(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, pivot, nil, true, false, iterator) +} + +// Ascend calls the iterator for every value in the tree within the range +// [first, last], until iterator returns false. +func (t *BTree) Ascend(iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, nil, nil, false, false, iterator) +} + +// DescendRange calls the iterator for every value in the tree within the range +// [lessOrEqual, greaterThan), until iterator returns false. +func (t *BTree) DescendRange(lessOrEqual, greaterThan Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, lessOrEqual, greaterThan, true, false, iterator) +} + +// DescendLessOrEqual calls the iterator for every value in the tree within the range +// [pivot, first], until iterator returns false. +func (t *BTree) DescendLessOrEqual(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, pivot, nil, true, false, iterator) +} + +// DescendGreaterThan calls the iterator for every value in the tree within +// the range [last, pivot), until iterator returns false. +func (t *BTree) DescendGreaterThan(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, nil, pivot, false, false, iterator) +} + +// Descend calls the iterator for every value in the tree within the range +// [last, first], until iterator returns false. +func (t *BTree) Descend(iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, nil, nil, false, false, iterator) +} + +// Get looks for the key record in the tree, returning it. It returns nil if +// unable to find that record. +func (t *BTree) Get(key Record) Record { + if t.root == nil { + return nil + } + return t.root.get(key) +} + +// Min returns the smallest record in the tree, or nil if the tree is empty. +func (t *BTree) Min() Record { + return min(t.root) +} + +// Max returns the largest record in the tree, or nil if the tree is empty. +func (t *BTree) Max() Record { + return max(t.root) +} + +// Has returns true if the given key is in the tree. +func (t *BTree) Has(key Record) bool { + return t.Get(key) != nil +} + +// Len returns the number of records currently in the tree. +func (t *BTree) Len() int { + return t.length +} + +// Clear removes all elements from the B-tree. +// +// Parameters: +// - addNodesToFreelist: +// - If true, the tree's nodes are added to the freelist during the clearing process, +// up to the freelist's capacity. +// - If false, the root node is simply dereferenced, allowing Go's garbage collector +// to reclaim the memory. +// +// Benefits: +// - **Performance:** +// - Significantly faster than deleting each element individually, as it avoids the overhead +// of searching and updating the tree structure for each deletion. +// - More efficient than creating a new tree, since it reuses existing nodes by adding them +// to the freelist instead of discarding them to the garbage collector. +// +// Time Complexity: +// - **O(1):** +// - When `addNodesToFreelist` is false. +// - When `addNodesToFreelist` is true but the freelist is already full. +// - **O(freelist size):** +// - When adding nodes to the freelist up to its capacity. +// - **O(tree size):** +// - When iterating through all nodes to add to the freelist, but none can be added due to +// ownership by another tree. + +func (tree *BTree) Clear(addNodesToFreelist bool) { + if tree.root != nil && addNodesToFreelist { + tree.root.reset(tree.cowCtx) + } + tree.root = nil + tree.length = 0 +} + +// reset adds all nodes in the current subtree to the freelist. +// +// The function operates recursively: +// - It first attempts to reset all child nodes. +// - If the freelist becomes full at any point, the process stops immediately. +// +// Parameters: +// - copyOnWriteCtx: The copy-on-write context managing the freelist. +// +// Returns: +// - true: Indicates that the parent node should continue attempting to reset its nodes. +// - false: Indicates that the freelist is full and no further nodes should be added. +// +// Usage: +// This method is called during the `Clear` operation of the B-tree to efficiently reuse +// nodes by adding them to the freelist, thereby avoiding unnecessary allocations and reducing +// garbage collection overhead. +func (currentNode *node) reset(copyOnWriteCtx *copyOnWriteContext) bool { + // Iterate through each child node and attempt to reset it. + for _, childNode := range currentNode.children { + // If any child reset operation signals that the freelist is full, stop the process. + if !childNode.reset(copyOnWriteCtx) { + return false + } + } + + // Attempt to add the current node to the freelist. + // If the freelist is full after this operation, indicate to the parent to stop. + freelistStatus := copyOnWriteCtx.freeNode(currentNode) + return freelistStatus != ftFreelistFull +} diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno new file mode 100644 index 00000000000..5790161c435 --- /dev/null +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -0,0 +1,678 @@ +package btree + +import ( + "fmt" + "sort" + "testing" + + "gno.land/p/demo/btree" +) + +// Content represents a key-value pair where the Key can be either an int or string +// and the Value can be any type. +type Content struct { + Key interface{} + Value interface{} +} + +// Less compares two Content records by their Keys. +// The Key must be either an int or a string. +func (c Content) Less(than Record) bool { + other, ok := than.(Content) + if !ok { + panic("cannot compare: incompatible types") + } + + switch key := c.Key.(type) { + case int: + switch otherKey := other.Key.(type) { + case int: + return key < otherKey + case string: + return true // ints are always less than strings + default: + panic("unsupported key type: must be int or string") + } + case string: + switch otherKey := other.Key.(type) { + case int: + return false // strings are always greater than ints + case string: + return key < otherKey + default: + panic("unsupported key type: must be int or string") + } + default: + panic("unsupported key type: must be int or string") + } +} + +type ContentSlice []Content + +func (s ContentSlice) Len() int { + return len(s) +} + +func (s ContentSlice) Less(i, j int) bool { + return s[i].Less(s[j]) +} + +func (s ContentSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ContentSlice) Copy() ContentSlice { + newSlice := make(ContentSlice, len(s)) + copy(newSlice, s) + return newSlice +} + +// Ensure Content implements the Record interface. +var _ Record = Content{} + +// **************************************************************************** +// Test helpers +// **************************************************************************** + +func genericSeeding(tree *btree.BTree, size int) *btree.BTree { + for i := 0; i < size; i++ { + tree.Insert(Content{Key: i, Value: fmt.Sprintf("Value_%d", i)}) + } + return tree +} + +func intSlicesCompare(left, right []int) int { + if len(left) != len(right) { + if len(left) > len(right) { + return 1 + } else { + return -1 + } + } + + for position, leftInt := range left { + if leftInt != right[position] { + if leftInt > right[position] { + return 1 + } else { + return -1 + } + } + } + + return 0 +} + +// **************************************************************************** +// Tests +// **************************************************************************** + +func TestLen(t *testing.T) { + length := genericSeeding(btree.New(WithDegree(10)), 7).Len() + if length != 7 { + t.Errorf("Length is incorrect. Expected 7, but got %d.", length) + } + + length = genericSeeding(btree.New(WithDegree(5)), 111).Len() + if length != 111 { + t.Errorf("Length is incorrect. Expected 111, but got %d.", length) + } + + length = genericSeeding(btree.New(WithDegree(30)), 123).Len() + if length != 123 { + t.Errorf("Length is incorrect. Expected 123, but got %d.", length) + } + +} + +func TestHas(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 40) + + if tree.Has(Content{Key: 7}) != true { + t.Errorf("Has(7) reported false, but it should be true.") + } + if tree.Has(Content{Key: 39}) != true { + t.Errorf("Has(40) reported false, but it should be true.") + } + if tree.Has(Content{Key: 1111}) == true { + t.Errorf("Has(1111) reported true, but it should be false.") + } +} + +func TestMin(t *testing.T) { + min := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + + if min.Key != 0 { + t.Errorf("Minimum should have been 0, but it was reported as %d.", min) + } +} + +func TestMax(t *testing.T) { + max := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + + if max.Key != 0 { + t.Errorf("Minimum should have been 0, but it was reported as %d.", max) + } +} + +func TestGet(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 40) + + if Content(tree.Get(Content{Key: 7})).Value != "Value_7" { + t.Errorf("Get(7) should have returned 'Value_7', but it returned %v.", tree.Get(Content{Key: 7})) + } + if Content(tree.Get(Content{Key: 39})).Value != "Value_39" { + t.Errorf("Get(40) should have returnd 'Value_39', but it returned %v.", tree.Get(Content{Key: 39})) + } + if tree.Get(Content{Key: 1111}) != nil { + t.Errorf("Get(1111) returned %v, but it should be nil.", Content(tree.Get(Content{Key: 1111}))) + } +} + +func TestDescend(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 5) + + expected := []int{4, 3, 2, 1, 0} + found := []int{} + + tree.Descend(func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("Descend returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendGreaterThan(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{9, 8, 7, 6, 5} + found := []int{} + + tree.DescendGreaterThan(Content{Key: 4}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendGreaterThan returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendLessOrEqual(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{4, 3, 2, 1, 0} + found := []int{} + + tree.DescendLessOrEqual(Content{Key: 4}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendRange(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{6, 5, 4, 3, 2} + found := []int{} + + tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscend(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 5) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + tree.Ascend(func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("Ascend returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendGreaterOrEqual(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{5, 6, 7, 8, 9} + found := []int{} + + tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("AscendGreaterOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendLessThan(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + tree.AscendLessThan(Content{Key: 5}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendRange(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(10)), 10) + + expected := []int{2, 3, 4, 5, 6} + found := []int{} + + tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record btree.Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDeleteMin(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestShift(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of Shift returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestDeleteMax(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{99, 98, 97, 96, 95} + found := []int{} + + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestPop(t *testing.T) { + tree := genericSeeding(btree.New(WithDegree(3)), 100) + + expected := []int{99, 98, 97, 96, 95} + found := []int{} + + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestInsertGet(t *testing.T) { + tree := btree.New(WithDegree(4)) + + expected := []Content{} + + for count := 0; count < 20; count++ { + value := fmt.Sprintf("Value_%d", count) + tree.Insert(Content{Key: count, Value: value}) + expected = append(expected, Content{Key: count, Value: value}) + } + + for count := 0; count < 20; count++ { + if tree.Get(Content{Key: count}) != expected[count] { + t.Errorf("Insert/Get doesn't appear to be working. Expected to retrieve %v with key %d, but got %v.", expected[count], count, tree.Get(Content{Key: count})) + } + } +} + +func TestClone(t *testing.T) { +} + +// ***** The following tests are functional or stress testing type tests. + +func TestBTree(t *testing.T) { + // Create a B-Tree of degree 3 + tree := btree.New(WithDegree(3)) + + //insertData := []Content{} + var insertData ContentSlice + + // Insert integer keys + intKeys := []int{10, 20, 5, 6, 12, 30, 7, 17} + for _, key := range intKeys { + content := Content{Key: key, Value: fmt.Sprintf("Value_%d", key)} + insertData = append(insertData, content) + result := tree.Insert(content) + if result != nil { + t.Errorf("**** Already in the tree? %v", result) + } + } + + // Insert string keys + stringKeys := []string{"apple", "banana", "cherry", "date", "fig", "grape"} + for _, key := range stringKeys { + content := Content{Key: key, Value: fmt.Sprintf("Fruit_%s", key)} + insertData = append(insertData, content) + tree.Insert(content) + } + + if tree.Len() != 14 { + t.Errorf("Tree length wrong. Expected 14 but got %d", tree.Len()) + } + + // Search for existing and non-existing keys + searchTests := []struct { + test Content + expected bool + }{ + {Content{Key: 10, Value: "Value_10"}, true}, + {Content{Key: 15, Value: ""}, false}, + {Content{Key: "banana", Value: "Fruit_banana"}, true}, + {Content{Key: "kiwi", Value: ""}, false}, + } + + t.Logf("Search Tests:\n") + for _, test := range searchTests { + val := tree.Get(test.test) + + if test.expected { + if val != nil && Content(val).Value == test.test.Value { + t.Logf("Found expected key:value %v:%v", test.test.Key, test.test.Value) + } else { + if val == nil { + t.Logf("Didn't find %v, but expected", test.test.Key) + } else { + t.Errorf("Expected key %v:%v, but found %v:%v.", test.test.Key, test.test.Value, Content(val).Key, Content(val).Value) + } + } + } else { + if val != nil { + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.test.Key, Content(val).Key, Content(val).Value) + } else { + t.Logf("Didn't find %v, but wasn't expected", test.test.Key) + } + } + } + + // Iterate in order + t.Logf("\nIn-order Iteration:\n") + pos := 0 + + if tree.Len() != 14 { + t.Errorf("Tree length wrong. Expected 14 but got %d", tree.Len()) + } + + sortedInsertData := insertData.Copy() + sort.Sort(sortedInsertData) + + t.Logf("Insert Data Length: %d", len(insertData)) + t.Logf("Sorted Data Length: %d", len(sortedInsertData)) + t.Logf("Tree Length: %d", tree.Len()) + + tree.Ascend(func(_record btree.Record) bool { + record := Content(_record) + t.Logf("Key:Value == %v:%v", record.Key, record.Value) + if record.Key != sortedInsertData[pos].Key { + t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) + } + pos++ + return true + }) + // // Reverse Iterate + t.Logf("\nReverse-order Iteration:\n") + pos = len(sortedInsertData) - 1 + + tree.Descend(func(_record btree.Record) bool { + record := Content(_record) + t.Logf("Key:Value == %v:%v", record.Key, record.Value) + if record.Key != sortedInsertData[pos].Key { + t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) + } + pos-- + return true + }) + + deleteTests := []Content{ + Content{Key: 10, Value: "Value_10"}, + Content{Key: 15, Value: ""}, + Content{Key: "banana", Value: "Fruit_banana"}, + Content{Key: "kiwi", Value: ""}, + } + for _, test := range deleteTests { + fmt.Printf("\nDeleting %+v\n", test) + tree.Delete(test) + } + + if tree.Len() != 12 { + t.Errorf("Tree length wrong. Expected 12 but got %d", tree.Len()) + } + + for _, test := range deleteTests { + val := tree.Get(test) + if val != nil { + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.Key, Content(val).Key, Content(val).Value) + } else { + t.Logf("Didn't find %v, but wasn't expected", test.Key) + } + } +} + +func TestStress(t *testing.T) { + // Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3. + // Insert 1000 records into each tree, then search for each record. + // Delete half of the records, skipping every other one, then search for each record. + + for degree := 3; degree <= 12; degree += 3 { + t.Logf("Testing B-Tree of degree %d\n", degree) + tree := btree.New(WithDegree(degree)) + + // Insert 1000 records + t.Logf("Inserting 1000 records\n") + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Insert(content) + } + + // Search for all records + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + // Delete half of the records + for i := 0; i < 1000; i += 2 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + } + + // Search for all records + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + if i%2 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + } + } + + // Now create a very large tree, with 100000 records + // Then delete roughly one third of them, using a very basic random number generation scheme + // (implement it right here) to determine which records to delete. + // Print a few lines using Logf to let the user know what's happening. + + t.Logf("Testing B-Tree of degree 10 with 100000 records\n") + tree := btree.New(WithDegree(10)) + + // Insert 100000 records + t.Logf("Inserting 100000 records\n") + for i := 0; i < 100000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Insert(content) + } + + // Implement a very basic random number generator + seed := 0 + random := func() int { + seed = (seed*1103515245 + 12345) & 0x7fffffff + return seed + } + + // Delete one third of the records + t.Logf("Deleting one third of the records\n") + for i := 0; i < 35000; i++ { + content := Content{Key: random() % 100000, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + } +} + +// Write a test that populates a large B-Tree with 10000 records. +// It should then `Clone` the tree, make some changes to both the original and the clone, +// And then clone the clone, and make some changes to all three trees, and then check that the changes are isolated +// to the tree they were made in. + +func TestBTreeCloneIsolation(t *testing.T) { + t.Logf("Creating B-Tree of degree 10 with 10000 records\n") + tree := genericSeeding(btree.New(WithDegree(10)), 10000) + + // Clone the tree + t.Logf("Cloning the tree\n") + clone := tree.Clone() + + // Make some changes to the original and the clone + t.Logf("Making changes to the original and the clone\n") + for i := 0; i < 10000; i += 2 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + content = Content{Key: i + 1, Value: fmt.Sprintf("Value_%d", i+1)} + clone.Delete(content) + } + + // Clone the clone + t.Logf("Cloning the clone\n") + clone2 := clone.Clone() + + // Make some changes to all three trees + t.Logf("Making changes to all three trees\n") + for i := 0; i < 10000; i += 3 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + content = Content{Key: i, Value: fmt.Sprintf("Value_%d", i+1)} + clone.Delete(content) + content = Content{Key: i + 2, Value: fmt.Sprintf("Value_%d", i+2)} + clone2.Delete(content) + } + + // Check that the changes are isolated to the tree they were made in + t.Logf("Checking that the changes are isolated to the tree they were made in\n") + for i := 0; i < 10000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + + if i%3 == 0 || i%2 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + val = clone.Get(content) + if i%2 != 0 || i%3 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + val = clone2.Get(content) + if i%2 != 0 || (i-2)%3 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + } +} diff --git a/examples/gno.land/p/demo/btree/gno.mod b/examples/gno.land/p/demo/btree/gno.mod new file mode 100644 index 00000000000..aed2fe6b730 --- /dev/null +++ b/examples/gno.land/p/demo/btree/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/btree From 87ef6043ba116b18efe402ec3c8360d9dd4a98a9 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Fri, 6 Dec 2024 05:41:54 +0800 Subject: [PATCH 34/86] chore: add filetest in gnovm/makefile (#3272)
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
--------- Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> --- gnovm/Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gnovm/Makefile b/gnovm/Makefile index 31daf942554..3cf2f74276b 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -97,6 +97,9 @@ _test.stdlibs: go run ./cmd/gno test -v ./stdlibs/... +_test.filetest:; + go test pkg/gnolang/files_test.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS) + ######################################## # Code gen # TODO: move _dev.stringer to go:generate instructions, simplify generate From 9a0da98665fbc5a967f4696acc101c63e680f588 Mon Sep 17 00:00:00 2001 From: Kirk Haines Date: Fri, 6 Dec 2024 09:38:08 +0100 Subject: [PATCH 35/86] chore: Fix the linting error. (#3282) --- examples/gno.land/p/demo/btree/btree_test.gno | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno index 5790161c435..a0f7c1c55ca 100644 --- a/examples/gno.land/p/demo/btree/btree_test.gno +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -4,8 +4,6 @@ import ( "fmt" "sort" "testing" - - "gno.land/p/demo/btree" ) // Content represents a key-value pair where the Key can be either an int or string @@ -74,7 +72,7 @@ var _ Record = Content{} // Test helpers // **************************************************************************** -func genericSeeding(tree *btree.BTree, size int) *btree.BTree { +func genericSeeding(tree *BTree, size int) *BTree { for i := 0; i < size; i++ { tree.Insert(Content{Key: i, Value: fmt.Sprintf("Value_%d", i)}) } @@ -108,17 +106,17 @@ func intSlicesCompare(left, right []int) int { // **************************************************************************** func TestLen(t *testing.T) { - length := genericSeeding(btree.New(WithDegree(10)), 7).Len() + length := genericSeeding(New(WithDegree(10)), 7).Len() if length != 7 { t.Errorf("Length is incorrect. Expected 7, but got %d.", length) } - length = genericSeeding(btree.New(WithDegree(5)), 111).Len() + length = genericSeeding(New(WithDegree(5)), 111).Len() if length != 111 { t.Errorf("Length is incorrect. Expected 111, but got %d.", length) } - length = genericSeeding(btree.New(WithDegree(30)), 123).Len() + length = genericSeeding(New(WithDegree(30)), 123).Len() if length != 123 { t.Errorf("Length is incorrect. Expected 123, but got %d.", length) } @@ -126,7 +124,7 @@ func TestLen(t *testing.T) { } func TestHas(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 40) + tree := genericSeeding(New(WithDegree(10)), 40) if tree.Has(Content{Key: 7}) != true { t.Errorf("Has(7) reported false, but it should be true.") @@ -140,7 +138,7 @@ func TestHas(t *testing.T) { } func TestMin(t *testing.T) { - min := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + min := Content(genericSeeding(New(WithDegree(10)), 53).Min()) if min.Key != 0 { t.Errorf("Minimum should have been 0, but it was reported as %d.", min) @@ -148,7 +146,7 @@ func TestMin(t *testing.T) { } func TestMax(t *testing.T) { - max := Content(genericSeeding(btree.New(WithDegree(10)), 53).Min()) + max := Content(genericSeeding(New(WithDegree(10)), 53).Min()) if max.Key != 0 { t.Errorf("Minimum should have been 0, but it was reported as %d.", max) @@ -156,7 +154,7 @@ func TestMax(t *testing.T) { } func TestGet(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 40) + tree := genericSeeding(New(WithDegree(10)), 40) if Content(tree.Get(Content{Key: 7})).Value != "Value_7" { t.Errorf("Get(7) should have returned 'Value_7', but it returned %v.", tree.Get(Content{Key: 7})) @@ -170,12 +168,12 @@ func TestGet(t *testing.T) { } func TestDescend(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 5) + tree := genericSeeding(New(WithDegree(10)), 5) expected := []int{4, 3, 2, 1, 0} found := []int{} - tree.Descend(func(_record btree.Record) bool { + tree.Descend(func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -187,12 +185,12 @@ func TestDescend(t *testing.T) { } func TestDescendGreaterThan(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{9, 8, 7, 6, 5} found := []int{} - tree.DescendGreaterThan(Content{Key: 4}, func(_record btree.Record) bool { + tree.DescendGreaterThan(Content{Key: 4}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -204,12 +202,12 @@ func TestDescendGreaterThan(t *testing.T) { } func TestDescendLessOrEqual(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{4, 3, 2, 1, 0} found := []int{} - tree.DescendLessOrEqual(Content{Key: 4}, func(_record btree.Record) bool { + tree.DescendLessOrEqual(Content{Key: 4}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -221,12 +219,12 @@ func TestDescendLessOrEqual(t *testing.T) { } func TestDescendRange(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{6, 5, 4, 3, 2} found := []int{} - tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record btree.Record) bool { + tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -238,12 +236,12 @@ func TestDescendRange(t *testing.T) { } func TestAscend(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 5) + tree := genericSeeding(New(WithDegree(10)), 5) expected := []int{0, 1, 2, 3, 4} found := []int{} - tree.Ascend(func(_record btree.Record) bool { + tree.Ascend(func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -255,12 +253,12 @@ func TestAscend(t *testing.T) { } func TestAscendGreaterOrEqual(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{5, 6, 7, 8, 9} found := []int{} - tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record btree.Record) bool { + tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -272,12 +270,12 @@ func TestAscendGreaterOrEqual(t *testing.T) { } func TestAscendLessThan(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{0, 1, 2, 3, 4} found := []int{} - tree.AscendLessThan(Content{Key: 5}, func(_record btree.Record) bool { + tree.AscendLessThan(Content{Key: 5}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -289,12 +287,12 @@ func TestAscendLessThan(t *testing.T) { } func TestAscendRange(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(10)), 10) + tree := genericSeeding(New(WithDegree(10)), 10) expected := []int{2, 3, 4, 5, 6} found := []int{} - tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record btree.Record) bool { + tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record Record) bool { record := Content(_record) found = append(found, int(record.Key)) return true @@ -306,7 +304,7 @@ func TestAscendRange(t *testing.T) { } func TestDeleteMin(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{0, 1, 2, 3, 4} found := []int{} @@ -323,7 +321,7 @@ func TestDeleteMin(t *testing.T) { } func TestShift(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{0, 1, 2, 3, 4} found := []int{} @@ -340,7 +338,7 @@ func TestShift(t *testing.T) { } func TestDeleteMax(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{99, 98, 97, 96, 95} found := []int{} @@ -357,7 +355,7 @@ func TestDeleteMax(t *testing.T) { } func TestPop(t *testing.T) { - tree := genericSeeding(btree.New(WithDegree(3)), 100) + tree := genericSeeding(New(WithDegree(3)), 100) expected := []int{99, 98, 97, 96, 95} found := []int{} @@ -374,7 +372,7 @@ func TestPop(t *testing.T) { } func TestInsertGet(t *testing.T) { - tree := btree.New(WithDegree(4)) + tree := New(WithDegree(4)) expected := []Content{} @@ -398,7 +396,7 @@ func TestClone(t *testing.T) { func TestBTree(t *testing.T) { // Create a B-Tree of degree 3 - tree := btree.New(WithDegree(3)) + tree := New(WithDegree(3)) //insertData := []Content{} var insertData ContentSlice @@ -475,7 +473,7 @@ func TestBTree(t *testing.T) { t.Logf("Sorted Data Length: %d", len(sortedInsertData)) t.Logf("Tree Length: %d", tree.Len()) - tree.Ascend(func(_record btree.Record) bool { + tree.Ascend(func(_record Record) bool { record := Content(_record) t.Logf("Key:Value == %v:%v", record.Key, record.Value) if record.Key != sortedInsertData[pos].Key { @@ -488,7 +486,7 @@ func TestBTree(t *testing.T) { t.Logf("\nReverse-order Iteration:\n") pos = len(sortedInsertData) - 1 - tree.Descend(func(_record btree.Record) bool { + tree.Descend(func(_record Record) bool { record := Content(_record) t.Logf("Key:Value == %v:%v", record.Key, record.Value) if record.Key != sortedInsertData[pos].Key { @@ -530,7 +528,7 @@ func TestStress(t *testing.T) { for degree := 3; degree <= 12; degree += 3 { t.Logf("Testing B-Tree of degree %d\n", degree) - tree := btree.New(WithDegree(degree)) + tree := New(WithDegree(degree)) // Insert 1000 records t.Logf("Inserting 1000 records\n") @@ -576,7 +574,7 @@ func TestStress(t *testing.T) { // Print a few lines using Logf to let the user know what's happening. t.Logf("Testing B-Tree of degree 10 with 100000 records\n") - tree := btree.New(WithDegree(10)) + tree := New(WithDegree(10)) // Insert 100000 records t.Logf("Inserting 100000 records\n") @@ -607,7 +605,7 @@ func TestStress(t *testing.T) { func TestBTreeCloneIsolation(t *testing.T) { t.Logf("Creating B-Tree of degree 10 with 10000 records\n") - tree := genericSeeding(btree.New(WithDegree(10)), 10000) + tree := genericSeeding(New(WithDegree(10)), 10000) // Clone the tree t.Logf("Cloning the tree\n") From 08fb49a1010f461a5ce88a013c935ec217a7acaa Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:41:04 +0100 Subject: [PATCH 36/86] fix: bump golangci lint to 1.62 (#3278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #3066 Bump `golangci-lint` to `1.62`, this bump include the following change. - Removing `gopls` from direct dependency as it creates some conflict with the latest version of `golangci-lint`. `gopls` is not meant to be tracked as a direct dependency tool; it's a personal tool and it's dependent on the user's Go version, not the project-specific version. - Update all `printf`-like methods that should not use non-constant format input. Instead, I choose to duplicate those methods into two separate methods: one should be dedicated to formatting, and the other one to simple direct messaging. ex. `errors.Wrap` -> `errors.Wrapf` - ~Ignoring `gosec` issue with `ripemd160` for now, I will open an issue to double-check this one.~ ✅ Double-checked with @zivkovicmilos & @jaekwon, we can ignore it. - Ignoring `gosec` G115 Integer overflow conversion; there is no solution to check the overflow in the time of conversion, so I think the linter shouldn't check for the overflow.
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
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Morgan --- .github/golangci.yml | 1 + .github/workflows/lint_template.yml | 2 +- gno.land/pkg/gnoclient/client_queries.go | 6 +- gno.land/pkg/gnoclient/client_txs.go | 4 +- gno.land/pkg/sdk/vm/keeper.go | 14 +- gnovm/pkg/gnolang/op_expressions.go | 26 +- gnovm/pkg/gnolang/store.go | 2 +- gnovm/pkg/gnolang/types.go | 36 ++- gnovm/pkg/gnolang/values.go | 48 +-- misc/devdeps/deps.go | 1 - misc/devdeps/go.mod | 131 ++++---- misc/devdeps/go.sum | 284 +++++++++--------- tm2/pkg/bft/blockchain/pool.go | 8 +- tm2/pkg/bft/mempool/clist_mempool.go | 10 +- tm2/pkg/bft/mempool/mempool.go | 2 +- tm2/pkg/bft/rpc/core/blocks.go | 6 +- tm2/pkg/bft/rpc/core/blocks_test.go | 18 +- .../bft/rpc/lib/server/http_server_test.go | 10 +- tm2/pkg/bft/types/evidence.go | 4 +- tm2/pkg/bft/types/genesis.go | 2 +- tm2/pkg/bft/types/validator_set.go | 14 +- tm2/pkg/bft/types/vote_set.go | 10 +- tm2/pkg/bft/wal/wal.go | 42 +-- tm2/pkg/crypto/keys/client/maketx.go | 4 +- tm2/pkg/crypto/keys/keybase.go | 4 +- tm2/pkg/crypto/merkle/proof_key_path.go | 4 +- tm2/pkg/crypto/secp256k1/secp256k1.go | 6 +- tm2/pkg/errors/errors.go | 15 +- tm2/pkg/errors/errors_test.go | 4 +- tm2/pkg/iavl/proof_range.go | 2 +- tm2/pkg/os/os.go | 2 +- tm2/pkg/p2p/switch.go | 2 +- tm2/pkg/std/coin.go | 2 +- tm2/pkg/std/gasprice.go | 6 +- tm2/pkg/store/cache/store_test.go | 4 +- 35 files changed, 369 insertions(+), 367 deletions(-) diff --git a/.github/golangci.yml b/.github/golangci.yml index 43cea27a791..b8bd5537135 100644 --- a/.github/golangci.yml +++ b/.github/golangci.yml @@ -51,6 +51,7 @@ linters-settings: excludes: - G204 # Subprocess launched with a potential tainted input or cmd arguments - G306 # Expect WriteFile permissions to be 0600 or less + - G115 # Integer overflow conversion, no solution to check the overflow in time of convert, so linter shouldn't check the overflow. stylecheck: checks: [ "all", "-ST1022", "-ST1003" ] errorlint: diff --git a/.github/workflows/lint_template.yml b/.github/workflows/lint_template.yml index 5b792269c02..b7568d19c41 100644 --- a/.github/workflows/lint_template.yml +++ b/.github/workflows/lint_template.yml @@ -25,4 +25,4 @@ jobs: working-directory: ${{ inputs.modulepath }} args: --config=${{ github.workspace }}/.github/golangci.yml - version: v1.59 # sync with misc/devdeps + version: v1.62 # sync with misc/devdeps diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go index 9d9d7305116..2e09842ae31 100644 --- a/gno.land/pkg/gnoclient/client_queries.go +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -31,7 +31,7 @@ func (c *Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { } if qres.Response.Error != nil { - return qres, errors.Wrap(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) + return qres, errors.Wrapf(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) } return qres, nil @@ -97,7 +97,7 @@ func (c *Client) Render(pkgPath string, args string) (string, *ctypes.ResultABCI return "", nil, errors.Wrap(err, "query render") } if qres.Response.Error != nil { - return "", nil, errors.Wrap(qres.Response.Error, "Render failed: log:%s", qres.Response.Log) + return "", nil, errors.Wrapf(qres.Response.Error, "Render failed: log:%s", qres.Response.Log) } return string(qres.Response.Data), qres, nil @@ -120,7 +120,7 @@ func (c *Client) QEval(pkgPath string, expression string) (string, *ctypes.Resul return "", nil, errors.Wrap(err, "query qeval") } if qres.Response.Error != nil { - return "", nil, errors.Wrap(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log) + return "", nil, errors.Wrapf(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log) } return string(qres.Response.Data), qres, nil diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index 9d3dbde22ae..d7f6f053242 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -283,10 +283,10 @@ func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxC } if bres.CheckTx.IsErr() { - return bres, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + return bres, errors.Wrapf(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) } if bres.DeliverTx.IsErr() { - return bres, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + return bres, errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) } return bres, nil diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 68f784a52e7..52eff20ea95 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -390,7 +390,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM addpkg panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM addpkg panic: %v\n%s\n", r, m2.String()) return } @@ -491,10 +491,10 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) case gno.UnhandledPanicError: - err = errors.Wrap(fmt.Errorf("%v", r.Error()), "VM call panic: %s\nStacktrace: %s\n", + err = errors.Wrapf(fmt.Errorf("%v", r.Error()), "VM call panic: %s\nStacktrace: %s\n", r.Error(), m.ExceptionsStacktrace()) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM call panic: %v\nMachine State:%s\nStacktrace: %s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM call panic: %v\nMachine State:%s\nStacktrace: %s\n", r, m.String(), m.Stacktrace().String()) return } @@ -594,7 +594,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM run main addpkg panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM run main addpkg panic: %v\n%s\n", r, m.String()) return } @@ -620,7 +620,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM run main call panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM run main call panic: %v\n%s\n", r, m2.String()) return } @@ -750,7 +750,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM query eval panic: %v\n%s\n", r, m.String()) return } @@ -816,7 +816,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string case store.OutOfGasException: // panic in consumeGas() panic(r) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM query eval string panic: %v\n%s\n", + err = errors.Wrapf(fmt.Errorf("%v", r), "VM query eval string panic: %v\n%s\n", r, m.String()) return } diff --git a/gnovm/pkg/gnolang/op_expressions.go b/gnovm/pkg/gnolang/op_expressions.go index b614e72e945..c0f6225740b 100644 --- a/gnovm/pkg/gnolang/op_expressions.go +++ b/gnovm/pkg/gnolang/op_expressions.go @@ -88,20 +88,20 @@ func (m *Machine) doOpSelector() { func (m *Machine) doOpSlice() { sx := m.PopExpr().(*SliceExpr) - var low, high, max int = -1, -1, -1 + var lowVal, highVal, maxVal int = -1, -1, -1 // max if sx.Max != nil { - max = m.PopValue().ConvertGetInt() + maxVal = m.PopValue().ConvertGetInt() } // high if sx.High != nil { - high = m.PopValue().ConvertGetInt() + highVal = m.PopValue().ConvertGetInt() } // low if sx.Low != nil { - low = m.PopValue().ConvertGetInt() + lowVal = m.PopValue().ConvertGetInt() } else { - low = 0 + lowVal = 0 } // slice base x xv := m.PopValue() @@ -114,14 +114,14 @@ func (m *Machine) doOpSlice() { } // fill default based on xv if sx.High == nil { - high = xv.GetLength() + highVal = xv.GetLength() } // all low:high:max cases - if max == -1 { - sv := xv.GetSlice(m.Alloc, low, high) + if maxVal == -1 { + sv := xv.GetSlice(m.Alloc, lowVal, highVal) m.PushValue(sv) } else { - sv := xv.GetSlice2(m.Alloc, low, high, max) + sv := xv.GetSlice2(m.Alloc, lowVal, highVal, maxVal) m.PushValue(sv) } } @@ -593,16 +593,16 @@ func (m *Machine) doOpSliceLit2() { // peek slice type. st := m.PeekValue(1).V.(TypeValue).Type // calculate maximum index. - max := 0 + maxVal := 0 for i := 0; i < el; i++ { itv := tvs[i*2+0] idx := itv.ConvertGetInt() - if idx > max { - max = idx + if idx > maxVal { + maxVal = idx } } // construct element buf slice. - es := make([]TypedValue, max+1) + es := make([]TypedValue, maxVal+1) for i := 0; i < el; i++ { itv := tvs[i*2+0] vtv := tvs[i*2+1] diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index b721194823d..4cbc2948f43 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -818,7 +818,7 @@ func backendPackageIndexKey(index uint64) string { } func backendPackagePathKey(path string) string { - return fmt.Sprintf("pkg:" + path) + return "pkg:" + path } // ---------------------------------------- diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index eedb71ffa73..bfc7cc31584 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -41,7 +41,15 @@ func (tid TypeID) String() string { return string(tid) } -func typeid(f string, args ...interface{}) (tid TypeID) { +func typeid(s string) (tid TypeID) { + x := TypeID(s) + if debug { + debug.Println("TYPEID", s) + } + return x +} + +func typeidf(f string, args ...interface{}) (tid TypeID) { fs := fmt.Sprintf(f, args...) x := TypeID(fs) if debug { @@ -521,7 +529,7 @@ func (at *ArrayType) Kind() Kind { func (at *ArrayType) TypeID() TypeID { if at.typeid.IsZero() { - at.typeid = typeid("[%d]%s", at.Len, at.Elt.TypeID().String()) + at.typeid = typeidf("[%d]%s", at.Len, at.Elt.TypeID().String()) } return at.typeid } @@ -564,9 +572,9 @@ func (st *SliceType) Kind() Kind { func (st *SliceType) TypeID() TypeID { if st.typeid.IsZero() { if st.Vrd { - st.typeid = typeid("...%s", st.Elt.TypeID().String()) + st.typeid = typeidf("...%s", st.Elt.TypeID().String()) } else { - st.typeid = typeid("[]%s", st.Elt.TypeID().String()) + st.typeid = typeidf("[]%s", st.Elt.TypeID().String()) } } return st.typeid @@ -607,7 +615,7 @@ func (pt *PointerType) Kind() Kind { func (pt *PointerType) TypeID() TypeID { if pt.typeid.IsZero() { - pt.typeid = typeid("*%s", pt.Elt.TypeID().String()) + pt.typeid = typeidf("*%s", pt.Elt.TypeID().String()) } return pt.typeid } @@ -748,7 +756,7 @@ func (st *StructType) TypeID() TypeID { // may have the same TypeID if and only if neither have // unexported fields. st.PkgPath is only included in field // names that are not uppercase. - st.typeid = typeid( + st.typeid = typeidf( "struct{%s}", FieldTypeList(st.Fields).TypeIDForPackage(st.PkgPath), ) @@ -1078,11 +1086,11 @@ func (ct *ChanType) TypeID() TypeID { if ct.typeid.IsZero() { switch ct.Dir { case SEND | RECV: - ct.typeid = typeid("chan{%s}" + ct.Elt.TypeID().String()) + ct.typeid = typeidf("chan{%s}", ct.Elt.TypeID().String()) case SEND: - ct.typeid = typeid("<-chan{%s}" + ct.Elt.TypeID().String()) + ct.typeid = typeidf("<-chan{%s}", ct.Elt.TypeID().String()) case RECV: - ct.typeid = typeid("chan<-{%s}" + ct.Elt.TypeID().String()) + ct.typeid = typeidf("chan<-{%s}", ct.Elt.TypeID().String()) default: panic("should not happen") } @@ -1298,7 +1306,7 @@ func (ft *FuncType) TypeID() TypeID { } */ if ft.typeid.IsZero() { - ft.typeid = typeid( + ft.typeid = typeidf( "func(%s)(%s)", // pp, ps.UnnamedTypeID(), @@ -1361,7 +1369,7 @@ func (mt *MapType) Kind() Kind { func (mt *MapType) TypeID() TypeID { if mt.typeid.IsZero() { - mt.typeid = typeid( + mt.typeid = typeidf( "map[%s]%s", mt.Key.TypeID().String(), mt.Value.TypeID().String(), @@ -1489,7 +1497,7 @@ func (dt *DeclaredType) TypeID() TypeID { } func DeclaredTypeID(pkgPath string, name Name) TypeID { - return typeid("%s.%s", pkgPath, name) + return typeidf("%s.%s", pkgPath, name) } func (dt *DeclaredType) String() string { @@ -1787,9 +1795,9 @@ func (nt *NativeType) TypeID() TypeID { // > (e.g., base64 instead of "encoding/base64") and is not // > guaranteed to be unique among types. To test for type identity, // > compare the Types directly. - nt.typeid = typeid("go:%s.%s", nt.Type.PkgPath(), nt.Type.String()) + nt.typeid = typeidf("go:%s.%s", nt.Type.PkgPath(), nt.Type.String()) } else { - nt.typeid = typeid("go:%s.%s", nt.Type.PkgPath(), nt.Type.Name()) + nt.typeid = typeidf("go:%s.%s", nt.Type.PkgPath(), nt.Type.Name()) } } return nt.typeid diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index e7a6274a780..4c2e2835f95 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -2248,41 +2248,41 @@ func (tv *TypedValue) GetSlice(alloc *Allocator, low, high int) TypedValue { } } -func (tv *TypedValue) GetSlice2(alloc *Allocator, low, high, max int) TypedValue { - if low < 0 { +func (tv *TypedValue) GetSlice2(alloc *Allocator, lowVal, highVal, maxVal int) TypedValue { + if lowVal < 0 { panic(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - low)) + lowVal)) } - if high < 0 { + if highVal < 0 { panic(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - high)) + highVal)) } - if max < 0 { + if maxVal < 0 { panic(fmt.Sprintf( "invalid slice index %d (index must be non-negative)", - max)) + maxVal)) } - if low > high { + if lowVal > highVal { panic(fmt.Sprintf( "invalid slice index %d > %d", - low, high)) + lowVal, highVal)) } - if high > max { + if highVal > maxVal { panic(fmt.Sprintf( "invalid slice index %d > %d", - high, max)) + highVal, maxVal)) } - if tv.GetCapacity() < high { + if tv.GetCapacity() < highVal { panic(fmt.Sprintf( "slice bounds out of range [%d:%d:%d] with capacity %d", - low, high, max, tv.GetCapacity())) + lowVal, highVal, maxVal, tv.GetCapacity())) } - if tv.GetCapacity() < max { + if tv.GetCapacity() < maxVal { panic(fmt.Sprintf( "slice bounds out of range [%d:%d:%d] with capacity %d", - low, high, max, tv.GetCapacity())) + lowVal, highVal, maxVal, tv.GetCapacity())) } switch bt := baseOf(tv.T).(type) { case *ArrayType: @@ -2294,15 +2294,15 @@ func (tv *TypedValue) GetSlice2(alloc *Allocator, low, high, max int) TypedValue return TypedValue{ T: st, V: alloc.NewSlice( - av, // base - low, // low - high-low, // length - max-low, // maxcap + av, // base + lowVal, // low + highVal-lowVal, // length + maxVal-lowVal, // maxcap ), } case *SliceType: if tv.V == nil { - if low != 0 || high != 0 || max != 0 { + if lowVal != 0 || highVal != 0 || maxVal != 0 { panic("nil slice index out of range") } return TypedValue{ @@ -2314,10 +2314,10 @@ func (tv *TypedValue) GetSlice2(alloc *Allocator, low, high, max int) TypedValue return TypedValue{ T: tv.T, V: alloc.NewSlice( - sv.Base, // base - sv.Offset+low, // offset - high-low, // length - max-low, // maxcap + sv.Base, // base + sv.Offset+lowVal, // offset + highVal-lowVal, // length + maxVal-lowVal, // maxcap ), } default: diff --git a/misc/devdeps/deps.go b/misc/devdeps/deps.go index a011868e4c2..f7da2b10c12 100644 --- a/misc/devdeps/deps.go +++ b/misc/devdeps/deps.go @@ -15,7 +15,6 @@ import ( _ "golang.org/x/tools/cmd/goimports" // required for formatting, linting, pls. - _ "golang.org/x/tools/gopls" _ "mvdan.cc/gofumpt" // protoc, genproto diff --git a/misc/devdeps/go.mod b/misc/devdeps/go.mod index c07b82fd11d..d3b40b73b52 100644 --- a/misc/devdeps/go.mod +++ b/misc/devdeps/go.mod @@ -1,46 +1,42 @@ module github.com/gnolang/gno/misc/devdeps -go 1.22 - -toolchain go1.22.4 +go 1.22.1 require ( - github.com/golangci/golangci-lint v1.59.1 // sync with github action - golang.org/x/tools v0.22.1-0.20240628205440-9c895dd76b34 - golang.org/x/tools/gopls v0.16.1 + github.com/campoy/embedmd v1.0.0 + github.com/golangci/golangci-lint v1.62.2 // sync with github action + golang.org/x/tools v0.27.0 google.golang.org/protobuf v1.35.1 moul.io/testman v1.5.0 - mvdan.cc/gofumpt v0.6.0 + mvdan.cc/gofumpt v0.7.0 ) -require github.com/campoy/embedmd v1.0.0 - require ( 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gochecknoglobals v0.2.1 // indirect github.com/4meepo/tagalign v1.3.4 // indirect - github.com/Abirdcfly/dupword v0.0.14 // indirect - github.com/Antonboom/errname v0.1.13 // indirect - github.com/Antonboom/nilnil v0.1.9 // indirect - github.com/Antonboom/testifylint v1.3.1 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect - github.com/Crocmagnon/fatcontext v0.2.2 // indirect + github.com/Abirdcfly/dupword v0.1.3 // indirect + github.com/Antonboom/errname v1.0.0 // indirect + github.com/Antonboom/nilnil v1.0.0 // indirect + github.com/Antonboom/testifylint v1.5.2 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/Crocmagnon/fatcontext v0.5.3 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect - github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect - github.com/alecthomas/go-check-sumtype v0.1.4 // indirect - github.com/alexkohler/nakedret/v2 v2.0.4 // indirect + github.com/alecthomas/go-check-sumtype v0.2.0 // indirect + github.com/alexkohler/nakedret/v2 v2.0.5 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/ashanbrown/forbidigo v1.6.0 // indirect github.com/ashanbrown/makezero v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bkielbasa/cyclop v1.2.1 // indirect + github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect - github.com/bombsimon/wsl/v4 v4.2.1 // indirect - github.com/breml/bidichk v0.2.7 // indirect - github.com/breml/errchkjson v0.3.6 // indirect + github.com/bombsimon/wsl/v4 v4.4.1 // indirect + github.com/breml/bidichk v0.3.2 // indirect + github.com/breml/errchkjson v0.4.0 // indirect github.com/butuzov/ireturn v0.3.0 // indirect github.com/butuzov/mirror v1.2.0 // indirect github.com/catenacyber/perfsprint v0.7.1 // indirect @@ -48,19 +44,19 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.1.0 // indirect - github.com/ckaznocha/intrange v0.1.2 // indirect + github.com/ckaznocha/intrange v0.2.1 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect - github.com/daixiang0/gci v0.13.4 // indirect + github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/ettle/strcase v0.2.0 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.5 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.6 // indirect - github.com/go-critic/go-critic v0.11.4 // indirect + github.com/ghostiam/protogetter v0.3.8 // indirect + github.com/go-critic/go-critic v0.11.5 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -68,13 +64,14 @@ require ( github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect - github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect + github.com/golangci/go-printf-func-name v0.1.0 // indirect + github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect github.com/golangci/misspell v0.6.0 // indirect github.com/golangci/modinfo v0.3.4 // indirect github.com/golangci/plugin-module-register v0.1.1 // indirect @@ -92,20 +89,18 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jgautheron/goconst v1.7.1 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect - github.com/jjti/go-spancheck v0.6.1 // indirect + github.com/jjti/go-spancheck v0.6.2 // indirect github.com/julz/importas v0.1.0 // indirect github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect - github.com/kisielk/errcheck v1.7.0 // indirect + github.com/kisielk/errcheck v1.8.0 // indirect github.com/kkHAIKE/contextcheck v1.1.5 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/kyoh86/exportloopref v0.1.11 // indirect - github.com/lasiar/canonicalheader v1.1.1 // indirect + github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/gomoddirectives v0.2.4 // indirect github.com/ldez/tagliatelle v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect - github.com/lufeee/execinquery v1.2.1 // indirect github.com/macabu/inamedparam v0.1.3 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/maratori/testableexamples v1.0.0 // indirect @@ -113,91 +108,91 @@ require ( github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.3.7 // indirect + github.com/mgechev/revive v1.5.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moricho/tparallel v0.3.1 // indirect + github.com/moricho/tparallel v0.3.2 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.16.2 // indirect + github.com/nunnatsa/ginkgolinter v0.18.3 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/peterbourgon/ff/v3 v3.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/polyfloyd/go-errorlint v1.5.2 // indirect + github.com/polyfloyd/go-errorlint v1.7.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/quasilyte/go-ruleguard v0.4.2 // indirect + github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/ryancurrah/gomodguard v1.3.2 // indirect + github.com/raeperd/recvcheck v0.1.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/ryancurrah/gomodguard v1.3.5 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.26.0 // indirect - github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 // indirect + github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect + github.com/securego/gosec/v2 v2.21.4 // indirect github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sivchari/tenv v1.7.1 // indirect - github.com/sonatard/noctx v0.0.2 // indirect + github.com/sivchari/tenv v1.12.1 // indirect + github.com/sonatard/noctx v0.1.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.12.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect - github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect - github.com/tetafro/godot v1.4.16 // indirect + github.com/tetafro/godot v1.4.18 // indirect github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect - github.com/timonwong/loggercheck v0.9.4 // indirect - github.com/tomarrell/wrapcheck/v2 v2.8.3 // indirect + github.com/timonwong/loggercheck v0.10.1 // indirect + github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.1.0 // indirect github.com/ultraware/whitespace v0.1.1 // indirect - github.com/uudashr/gocognit v1.1.2 // indirect + github.com/uudashr/gocognit v1.1.3 // indirect + github.com/uudashr/iface v1.2.1 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect github.com/yuin/goldmark v1.4.13 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.12.2 // indirect - go-simpler.org/sloglint v0.7.1 // indirect - go.uber.org/automaxprocs v1.5.3 // indirect + go-simpler.org/musttag v0.13.0 // indirect + go-simpler.org/sloglint v0.7.2 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect - golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/telemetry v0.0.0-20240607193123-221703e18637 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/vuln v1.0.4 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.18.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/tools v0.4.7 // indirect + honnef.co/go/tools v0.5.1 // indirect moul.io/banner v1.0.1 // indirect moul.io/motd v1.0.0 // indirect moul.io/u v1.27.0 // indirect mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect - mvdan.cc/xurls/v2 v2.5.0 // indirect ) diff --git a/misc/devdeps/go.sum b/misc/devdeps/go.sum index e19e47d0c56..fcba3fba624 100644 --- a/misc/devdeps/go.sum +++ b/misc/devdeps/go.sum @@ -37,32 +37,32 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= -github.com/Abirdcfly/dupword v0.0.14 h1:3U4ulkc8EUo+CaT105/GJ1BQwtgyj6+VaBVbAX11Ba8= -github.com/Abirdcfly/dupword v0.0.14/go.mod h1:VKDAbxdY8YbKUByLGg8EETzYSuC4crm9WwI6Y3S0cLI= -github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= -github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= -github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ= -github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= -github.com/Antonboom/testifylint v1.3.1 h1:Uam4q1Q+2b6H7gvk9RQFw6jyVDdpzIirFOOrbs14eG4= -github.com/Antonboom/testifylint v1.3.1/go.mod h1:NV0hTlteCkViPW9mSR4wEMfwp+Hs1T3dY60bkvSfhpM= +github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= +github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= +github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= +github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= +github.com/Antonboom/nilnil v1.0.0 h1:n+v+B12dsE5tbAqRODXmEKfZv9j2KcTBrp+LkoM4HZk= +github.com/Antonboom/nilnil v1.0.0/go.mod h1:fDJ1FSFoLN6yoG65ANb1WihItf6qt9PJVTn/s2IrcII= +github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= +github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Crocmagnon/fatcontext v0.2.2 h1:OrFlsDdOj9hW/oBEJBNSuH7QWf+E9WPVHw+x52bXVbk= -github.com/Crocmagnon/fatcontext v0.2.2/go.mod h1:WSn/c/+MMNiD8Pri0ahRj0o9jVpeowzavOQplBJw6u0= +github.com/Crocmagnon/fatcontext v0.5.3 h1:zCh/wjc9oyeF+Gmp+V60wetm8ph2tlsxocgg/J0hOps= +github.com/Crocmagnon/fatcontext v0.5.3/go.mod h1:XoCQYY1J+XTfyv74qLXvNw4xFunr3L1wkopIIKG7wGM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 h1:sATXp1x6/axKxz2Gjxv8MALP0bXaNRfQinEwyfMcx8c= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0/go.mod h1:Nl76DrGNJTA1KJ0LePKBw/vznBX1EHbAZX8mwjR82nI= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= -github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= +github.com/alecthomas/go-check-sumtype v0.2.0 h1:Bo+e4DFf3rs7ME9w/0SU/g6nmzJaphduP8Cjiz0gbwY= +github.com/alecthomas/go-check-sumtype v0.2.0/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -70,8 +70,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexkohler/nakedret/v2 v2.0.4 h1:yZuKmjqGi0pSmjGpOC016LtPJysIL0WEUiaXW5SUnNg= -github.com/alexkohler/nakedret/v2 v2.0.4/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= +github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= +github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= @@ -84,16 +84,16 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= -github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= +github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= +github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= -github.com/bombsimon/wsl/v4 v4.2.1 h1:Cxg6u+XDWff75SIFFmNsqnIOgob+Q9hG6y/ioKbRFiM= -github.com/bombsimon/wsl/v4 v4.2.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= -github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= -github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= -github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= -github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= +github.com/bombsimon/wsl/v4 v4.4.1 h1:jfUaCkN+aUpobrMO24zwyAMwMAV5eSziCkOKEauOLdw= +github.com/bombsimon/wsl/v4 v4.4.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= +github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= +github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= +github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= +github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0= github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= @@ -115,16 +115,16 @@ github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+U github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/ckaznocha/intrange v0.1.2 h1:3Y4JAxcMntgb/wABQ6e8Q8leMd26JbX2790lIss9MTI= -github.com/ckaznocha/intrange v0.1.2/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE= +github.com/ckaznocha/intrange v0.2.1 h1:M07spnNEQoALOJhwrImSrJLaxwuiQK+hA2DeajBlwYk= +github.com/ckaznocha/intrange v0.2.1/go.mod h1:7NEhVyf8fzZO5Ds7CRaqPEm52Ut83hsTiL5zbER/HYk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= -github.com/daixiang0/gci v0.13.4 h1:61UGkmpoAcxHM2hhNkZEf5SzwQtWJXTSws7jaPyqwlw= -github.com/daixiang0/gci v0.13.4/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= +github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= 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= @@ -136,22 +136,22 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghostiam/protogetter v0.3.6 h1:R7qEWaSgFCsy20yYHNIJsU9ZOb8TziSRRxuAOTVKeOk= -github.com/ghostiam/protogetter v0.3.6/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= -github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU= -github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc= +github.com/ghostiam/protogetter v0.3.8 h1:LYcXbYvybUyTIxN2Mj9h6rHrDZBDwZloPoKctWrFyJY= +github.com/ghostiam/protogetter v0.3.8/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= +github.com/go-critic/go-critic v0.11.5 h1:TkDTOn5v7EEngMxu8KbuFqFR43USaaH8XRJLz1jhVYA= +github.com/go-critic/go-critic v0.11.5/go.mod h1:wu6U7ny9PiaHaZHcvMDmdysMqvDem162Rh3zWTrqk8M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -161,8 +161,10 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -185,14 +187,14 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -226,10 +228,12 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= -github.com/golangci/golangci-lint v1.59.1 h1:CRRLu1JbhK5avLABFJ/OHVSQ0Ie5c4ulsOId1h3TTks= -github.com/golangci/golangci-lint v1.59.1/go.mod h1:jX5Oif4C7P0j9++YB2MMJmoNrb01NJ8ITqKWNLewThg= +github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= +github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= +github.com/golangci/golangci-lint v1.62.2 h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw= +github.com/golangci/golangci-lint v1.62.2/go.mod h1:ILWWyeFUrctpHVGMa1dg2xZPKoMUTc5OIMgW7HZr34g= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= @@ -266,11 +270,9 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= -github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= @@ -299,16 +301,12 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jba/templatecheck v0.7.0 h1:wjTb/VhGgSFeim5zjWVePBdaMo28X74bGLSABZV+zIA= -github.com/jba/templatecheck v0.7.0/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= -github.com/jjti/go-spancheck v0.6.1 h1:ZK/wE5Kyi1VX3PJpUO2oEgeoI4FWOUm7Shb2Gbv5obI= -github.com/jjti/go-spancheck v0.6.1/go.mod h1:vF1QkOO159prdo6mHRxak2CpzDpHAfKiPUDP/NeRnX8= +github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= +github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -323,8 +321,8 @@ github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSX github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= -github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= +github.com/kisielk/errcheck v1.8.0 h1:ZX/URYa7ilESY19ik/vBmCn6zdGQLxACwjAcWbHlYlg= +github.com/kisielk/errcheck v1.8.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= @@ -345,16 +343,14 @@ github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCT github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= -github.com/lasiar/canonicalheader v1.1.1 h1:wC+dY9ZfiqiPwAexUApFush/csSPXeIi4QqyxXmng8I= -github.com/lasiar/canonicalheader v1.1.1/go.mod h1:cXkb3Dlk6XXy+8MVQnF23CYKWlyA7kfQhSw2CcZtZb0= +github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= +github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg= github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g= github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= -github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= @@ -372,12 +368,13 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.3.7 h1:502QY0vQGe9KtYJ9FpxMz9rL+Fc/P13CI5POL4uHCcE= -github.com/mgechev/revive v1.3.7/go.mod h1:RJ16jUbF0OWC3co/+XTxmFNgEpUPwnnA0BRllX2aDNA= +github.com/mgechev/revive v1.5.1 h1:hE+QPeq0/wIzJwOphdVyUJ82njdd8Khp4fUIHGZHW3M= +github.com/mgechev/revive v1.5.1/go.mod h1:lC9AhkJIBs5zwx8wkudyHrU+IJkrEKmpCmGMnIJPk4o= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -387,8 +384,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= -github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= +github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= @@ -397,14 +394,14 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= -github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= +github.com/nunnatsa/ginkgolinter v0.18.3 h1:WgS7X3zzmni3vwHSBhvSgqrRgUecN6PQUcfB0j1noDw= +github.com/nunnatsa/ginkgolinter v0.18.3/go.mod h1:BE1xyB/PNtXXG1azrvrqJW5eFH0hSRylNzFy8QHPwzs= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.17.3 h1:oJcvKpIb7/8uLpDDtnQuf18xVnwKp8DTD7DQ6gTd/MU= -github.com/onsi/ginkgo/v2 v2.17.3/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -415,8 +412,8 @@ github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0= github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24= github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= @@ -427,8 +424,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/polyfloyd/go-errorlint v1.5.2 h1:SJhVik3Umsjh7mte1vE0fVZ5T1gznasQG3PV7U5xFdA= -github.com/polyfloyd/go-errorlint v1.5.2/go.mod h1:sH1QC1pxxi0fFecsVIzBmxtrgd9IF/SkJpA6wqyKAJs= +github.com/polyfloyd/go-errorlint v1.7.0 h1:Zp6lzCK4hpBDj8y8a237YK4EPrMXQWvOe3nGoH4pFrU= +github.com/polyfloyd/go-errorlint v1.7.0/go.mod h1:dGWKu85mGHnegQ2SWpEybFityCg3j7ZbwsVUxAOk9gY= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -453,8 +450,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs= -github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= @@ -463,12 +460,17 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/raeperd/recvcheck v0.1.2 h1:SjdquRsRXJc26eSonWIo8b7IMtKD3OAT2Lb5G3ZX1+4= +github.com/raeperd/recvcheck v0.1.2/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.3.2 h1:CuG27ulzEB1Gu5Dk5gP8PFxSOZ3ptSdP5iI/3IXxM18= -github.com/ryancurrah/gomodguard v1.3.2/go.mod h1:LqdemiFomEjcxOqirbQCb3JFvSxH2JUYMerTFd3sF2o= +github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= +github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= @@ -477,10 +479,10 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.26.0 h1:LONR2hNVKxRmzIrZR0PhSF3mhCAzvnr+DcUiHgREfXE= -github.com/sashamelentyev/usestdlibvars v1.26.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 h1:rnO6Zp1YMQwv8AyxzuwsVohljJgp4L0ZqiCgtACsPsc= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9/go.mod h1:dg7lPlu/xK/Ut9SedURCoZbVCR4yC7fM65DtH9/CDHs= +github.com/sashamelentyev/usestdlibvars v1.27.0 h1:t/3jZpSXtRPRf2xr0m63i32ZrusyurIGT9E5wAvXQnI= +github.com/sashamelentyev/usestdlibvars v1.27.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= +github.com/securego/gosec/v2 v2.21.4 h1:Le8MSj0PDmOnHJgUATjD96PaXRvCpKC+DGJvwyy0Mlk= +github.com/securego/gosec/v2 v2.21.4/go.mod h1:Jtb/MwRQfRxCXyCm1rfM1BEiiiTfUOdyzzAhlr6lUTA= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= @@ -493,18 +495,18 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= -github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= -github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= -github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= +github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= +github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= +github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= +github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -530,12 +532,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= @@ -543,22 +543,24 @@ github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.4.16 h1:4ChfhveiNLk4NveAZ9Pu2AN8QZ2nkUGFuadM9lrr5D0= -github.com/tetafro/godot v1.4.16/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/tetafro/godot v1.4.18 h1:ouX3XGiziKDypbpXqShBfnNLTSjR8r3/HVzrtJ+bHlI= +github.com/tetafro/godot v1.4.18/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= -github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= -github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= -github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3bRDFASUs= -github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= +github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4= +github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ= github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= -github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= -github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= +github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= +github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= +github.com/uudashr/iface v1.2.1 h1:vHHyzAUmWZ64Olq6NZT3vg/z1Ws56kyPdBOd5kTXDF8= +github.com/uudashr/iface v1.2.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -579,18 +581,18 @@ gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go-simpler.org/musttag v0.12.2 h1:J7lRc2ysXOq7eM8rwaTYnNrHd5JwjppzB6mScysB2Cs= -go-simpler.org/musttag v0.12.2/go.mod h1:uN1DVIasMTQKk6XSik7yrJoEysGtR2GRqvWnI9S7TYM= -go-simpler.org/sloglint v0.7.1 h1:qlGLiqHbN5islOxjeLXoPtUdZXb669RW+BDQ+xOSNoU= -go-simpler.org/sloglint v0.7.1/go.mod h1:OlaVDRh/FKKd4X4sIMbsz8st97vomydceL146Fthh/c= +go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= +go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= +go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY= +go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= @@ -617,12 +619,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f h1:WTyX8eCCyfdqiPYkRGm0MqElSfYFH3yR1+rl/mct9sA= +golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -652,8 +654,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -692,8 +694,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -713,8 +715,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -760,7 +762,6 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -769,10 +770,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240607193123-221703e18637 h1:3Wt8mZlbFwG8llny+t18kh7AXxyWePFycXMuVdHxnyM= -golang.org/x/telemetry v0.0.0-20240607193123-221703e18637/go.mod h1:n38mvGdgc4dA684EC4NwQwoPKSw4jyKw8/DgZHDA1Dk= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -789,8 +788,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -851,18 +850,13 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.1-0.20240628205440-9c895dd76b34 h1:Kd+Z5Pm6uwYx3T2KEkeHMHUMZxDPb/q6b1m+zEcy62c= -golang.org/x/tools v0.22.1-0.20240628205440-9c895dd76b34/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools/gopls v0.16.1 h1:1hO/dCeUvjEYx3V0rVvCtOkwnpEpqS29paE+Jw4dcAc= -golang.org/x/tools/gopls v0.16.1/go.mod h1:Mwg8NfkbmP57kHtr/qsiU1+7kyEpuCvlPs7MH6sr988= -golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= -golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -972,8 +966,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= -honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= moul.io/banner v1.0.1 h1:+WsemGLhj2pOajw2eR5VYjLhOIqs0XhIRYchzTyMLk0= moul.io/banner v1.0.1/go.mod h1:XwvIGKkhKRKyN1vIdmR5oaKQLIkMhkMqrsHpS94QzAU= moul.io/godev v1.7.0/go.mod h1:5lgSpI1oH7xWpLl2Ew/Nsgk8DiNM6FzN9WV9+lgW8RQ= @@ -984,12 +978,10 @@ moul.io/testman v1.5.0/go.mod h1:b4/5+lMsMDJtwuh25Cr0eVJ5Y4B2lSPfkzDtfct070g= moul.io/u v1.6.0/go.mod h1:yd3/IoYRIJaZWAJV2rYHvM2EPp/Pp0zSNraB5IPX+hw= moul.io/u v1.27.0 h1:rF0p184mludn2DzL0unA8Gf/mFWMBerdqOh8cyuQYzQ= moul.io/u v1.27.0/go.mod h1:ggYDXxUjoHpfDsMPD3STqkUZTyA741PZiQhSd+7kRnA= -mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= -mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/tm2/pkg/bft/blockchain/pool.go b/tm2/pkg/bft/blockchain/pool.go index 5a82eb4d1d6..b610a0c0e7a 100644 --- a/tm2/pkg/bft/blockchain/pool.go +++ b/tm2/pkg/bft/blockchain/pool.go @@ -330,13 +330,13 @@ func (pool *BlockPool) removePeer(peerID p2p.ID) { // If no peers are left, maxPeerHeight is set to 0. func (pool *BlockPool) updateMaxPeerHeight() { - var max int64 + var maxVal int64 for _, peer := range pool.peers { - if peer.height > max { - max = peer.height + if peer.height > maxVal { + maxVal = peer.height } } - pool.maxPeerHeight = max + pool.maxPeerHeight = maxVal } // Pick an available peer with at least the given minHeight. diff --git a/tm2/pkg/bft/mempool/clist_mempool.go b/tm2/pkg/bft/mempool/clist_mempool.go index 2cad23c68e7..a2bf4301e63 100644 --- a/tm2/pkg/bft/mempool/clist_mempool.go +++ b/tm2/pkg/bft/mempool/clist_mempool.go @@ -505,12 +505,12 @@ func (mem *CListMempool) ReapMaxBytesMaxGas(maxDataBytes, maxGas int64) types.Tx return txs } -func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { +func (mem *CListMempool) ReapMaxTxs(maxVal int) types.Txs { mem.mtx.Lock() defer mem.mtx.Unlock() - if max < 0 { - max = mem.txs.Len() + if maxVal < 0 { + maxVal = mem.txs.Len() } for atomic.LoadInt32(&mem.rechecking) > 0 { @@ -518,8 +518,8 @@ func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { time.Sleep(time.Millisecond * 10) } - txs := make([]types.Tx, 0, min(mem.txs.Len(), max)) - for e := mem.txs.Front(); e != nil && len(txs) <= max; e = e.Next() { + txs := make([]types.Tx, 0, min(mem.txs.Len(), maxVal)) + for e := mem.txs.Front(); e != nil && len(txs) <= maxVal; e = e.Next() { memTx := e.Value.(*mempoolTx) txs = append(txs, memTx.tx) } diff --git a/tm2/pkg/bft/mempool/mempool.go b/tm2/pkg/bft/mempool/mempool.go index 6f822eb99ff..482d8dd2d42 100644 --- a/tm2/pkg/bft/mempool/mempool.go +++ b/tm2/pkg/bft/mempool/mempool.go @@ -30,7 +30,7 @@ type Mempool interface { // ReapMaxTxs reaps up to max transactions from the mempool. // If max is negative, there is no cap on the size of all returned // transactions (~ all available transactions). - ReapMaxTxs(max int) types.Txs + ReapMaxTxs(maxVal int) types.Txs // Lock locks the mempool. The consensus must be able to hold lock to safely update. Lock() diff --git a/tm2/pkg/bft/rpc/core/blocks.go b/tm2/pkg/bft/rpc/core/blocks.go index 53ed25ade11..9ca4e05a46f 100644 --- a/tm2/pkg/bft/rpc/core/blocks.go +++ b/tm2/pkg/bft/rpc/core/blocks.go @@ -421,11 +421,11 @@ func getHeight(currentHeight int64, heightPtr *int64) (int64, error) { return getHeightWithMin(currentHeight, heightPtr, 1) } -func getHeightWithMin(currentHeight int64, heightPtr *int64, min int64) (int64, error) { +func getHeightWithMin(currentHeight int64, heightPtr *int64, minVal int64) (int64, error) { if heightPtr != nil { height := *heightPtr - if height < min { - return 0, fmt.Errorf("height must be greater than or equal to %d", min) + if height < minVal { + return 0, fmt.Errorf("height must be greater than or equal to %d", minVal) } if height > currentHeight { return 0, fmt.Errorf("height must be less than or equal to the current blockchain height") diff --git a/tm2/pkg/bft/rpc/core/blocks_test.go b/tm2/pkg/bft/rpc/core/blocks_test.go index 550cc1542c9..dd55784ada0 100644 --- a/tm2/pkg/bft/rpc/core/blocks_test.go +++ b/tm2/pkg/bft/rpc/core/blocks_test.go @@ -11,11 +11,11 @@ func TestBlockchainInfo(t *testing.T) { t.Parallel() cases := []struct { - min, max int64 - height int64 - limit int64 - resultLength int64 - wantErr bool + minVal, maxVal int64 + height int64 + limit int64 + resultLength int64 + wantErr bool }{ // min > max {0, 0, 0, 10, 0, true}, // min set to 1 @@ -46,12 +46,12 @@ func TestBlockchainInfo(t *testing.T) { for i, c := range cases { caseString := fmt.Sprintf("test %d failed", i) - min, max, err := filterMinMax(c.height, c.min, c.max, c.limit) + minVal, maxVal, err := filterMinMax(c.height, c.minVal, c.maxVal, c.limit) if c.wantErr { require.Error(t, err, caseString) } else { require.NoError(t, err, caseString) - require.Equal(t, 1+max-min, c.resultLength, caseString) + require.Equal(t, 1+maxVal-minVal, c.resultLength, caseString) } } } @@ -62,7 +62,7 @@ func TestGetHeight(t *testing.T) { cases := []struct { currentHeight int64 heightPtr *int64 - min int64 + minVal int64 res int64 wantErr bool }{ @@ -79,7 +79,7 @@ func TestGetHeight(t *testing.T) { for i, c := range cases { caseString := fmt.Sprintf("test %d failed", i) - res, err := getHeightWithMin(c.currentHeight, c.heightPtr, c.min) + res, err := getHeightWithMin(c.currentHeight, c.heightPtr, c.minVal) if c.wantErr { require.Error(t, err, caseString) } else { diff --git a/tm2/pkg/bft/rpc/lib/server/http_server_test.go b/tm2/pkg/bft/rpc/lib/server/http_server_test.go index 6c6d9ad14d6..f089d262a71 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server_test.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server_test.go @@ -22,28 +22,28 @@ import ( func TestMaxOpenConnections(t *testing.T) { t.Parallel() - const max = 5 // max simultaneous connections + const maxVal = 5 // max simultaneous connections // Start the server. var open int32 mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if n := atomic.AddInt32(&open, 1); n > int32(max) { - t.Errorf("%d open connections, want <= %d", n, max) + if n := atomic.AddInt32(&open, 1); n > int32(maxVal) { + t.Errorf("%d open connections, want <= %d", n, maxVal) } defer atomic.AddInt32(&open, -1) time.Sleep(10 * time.Millisecond) fmt.Fprint(w, "some body") }) config := DefaultConfig() - config.MaxOpenConnections = max + config.MaxOpenConnections = maxVal l, err := Listen("tcp://127.0.0.1:0", config) require.NoError(t, err) defer l.Close() go StartHTTPServer(l, mux, log.NewTestingLogger(t), config) // Make N GET calls to the server. - attempts := max * 2 + attempts := maxVal * 2 var wg sync.WaitGroup var failed int32 for i := 0; i < attempts; i++ { diff --git a/tm2/pkg/bft/types/evidence.go b/tm2/pkg/bft/types/evidence.go index c11021e3976..85b08df6ba9 100644 --- a/tm2/pkg/bft/types/evidence.go +++ b/tm2/pkg/bft/types/evidence.go @@ -38,8 +38,8 @@ type EvidenceOverflowError struct { } // NewErrEvidenceOverflow returns a new EvidenceOverflowError where got > max. -func NewErrEvidenceOverflow(max, got int64) *EvidenceOverflowError { - return &EvidenceOverflowError{max, got} +func NewErrEvidenceOverflow(maxVal, got int64) *EvidenceOverflowError { + return &EvidenceOverflowError{maxVal, got} } // Error returns a string representation of the error. diff --git a/tm2/pkg/bft/types/genesis.go b/tm2/pkg/bft/types/genesis.go index c03f7acc09e..b927b7f8f0c 100644 --- a/tm2/pkg/bft/types/genesis.go +++ b/tm2/pkg/bft/types/genesis.go @@ -179,7 +179,7 @@ func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) { } genDoc, err := GenesisDocFromJSON(jsonBlob) if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("Error reading GenesisDoc at %v", genDocFile)) + return nil, errors.Wrapf(err, "Error reading GenesisDoc at %v", genDocFile) } return genDoc, nil } diff --git a/tm2/pkg/bft/types/validator_set.go b/tm2/pkg/bft/types/validator_set.go index 80ed994ca39..c5dc5be1291 100644 --- a/tm2/pkg/bft/types/validator_set.go +++ b/tm2/pkg/bft/types/validator_set.go @@ -162,17 +162,17 @@ func computeMaxMinPriorityDiff(vals *ValidatorSet) int64 { if vals.IsNilOrEmpty() { panic("empty validator set") } - max := int64(math.MinInt64) - min := int64(math.MaxInt64) + maxVal := int64(math.MinInt64) + minVal := int64(math.MaxInt64) for _, v := range vals.Validators { - if v.ProposerPriority < min { - min = v.ProposerPriority + if v.ProposerPriority < minVal { + minVal = v.ProposerPriority } - if v.ProposerPriority > max { - max = v.ProposerPriority + if v.ProposerPriority > maxVal { + maxVal = v.ProposerPriority } } - diff := max - min + diff := maxVal - minVal if diff < 0 { return -1 * diff } else { diff --git a/tm2/pkg/bft/types/vote_set.go b/tm2/pkg/bft/types/vote_set.go index bf6200bff15..496b9b37d60 100644 --- a/tm2/pkg/bft/types/vote_set.go +++ b/tm2/pkg/bft/types/vote_set.go @@ -167,7 +167,7 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { if (vote.Height != voteSet.height) || (vote.Round != voteSet.round) || (vote.Type != voteSet.type_) { - return false, errors.Wrap(ErrVoteUnexpectedStep, "Expected %d/%d/%d, but got %d/%d/%d", + return false, errors.Wrapf(ErrVoteUnexpectedStep, "Expected %d/%d/%d, but got %d/%d/%d", voteSet.height, voteSet.round, voteSet.type_, vote.Height, vote.Round, vote.Type) } @@ -175,13 +175,13 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { // Ensure that signer is a validator. lookupAddr, val := voteSet.valSet.GetByIndex(valIndex) if val == nil { - return false, errors.Wrap(ErrVoteInvalidValidatorIndex, + return false, errors.Wrapf(ErrVoteInvalidValidatorIndex, "Cannot find validator %d in valSet of size %d", valIndex, voteSet.valSet.Size()) } // Ensure that the signer has the right address. if valAddr != lookupAddr { - return false, errors.Wrap(ErrVoteInvalidValidatorAddress, + return false, errors.Wrapf(ErrVoteInvalidValidatorAddress, "vote.ValidatorAddress (%X) does not match address (%X) for vote.ValidatorIndex (%d)\nEnsure the genesis file is correct across all validators.", valAddr, lookupAddr, valIndex) } @@ -191,12 +191,12 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { if bytes.Equal(existing.Signature, vote.Signature) { return false, nil // duplicate } - return false, errors.Wrap(ErrVoteNonDeterministicSignature, "Existing vote: %v; New vote: %v", existing, vote) + return false, errors.Wrapf(ErrVoteNonDeterministicSignature, "Existing vote: %v; New vote: %v", existing, vote) } // Check signature. if err := vote.Verify(voteSet.chainID, val.PubKey); err != nil { - return false, errors.Wrap(err, "Failed to verify vote with ChainID %s and PubKey %s", voteSet.chainID, val.PubKey) + return false, errors.Wrapf(err, "Failed to verify vote with ChainID %s and PubKey %s", voteSet.chainID, val.PubKey) } // Add vote and get conflicting vote if any. diff --git a/tm2/pkg/bft/wal/wal.go b/tm2/pkg/bft/wal/wal.go index 2424f45dfd2..09fed44b2b1 100644 --- a/tm2/pkg/bft/wal/wal.go +++ b/tm2/pkg/bft/wal/wal.go @@ -278,8 +278,8 @@ func (wal *baseWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd // NOTE: starting from the last file in the group because we're usually // searching for the last height. See replay.go - min, max := wal.group.MinIndex(), wal.group.MaxIndex() - wal.Logger.Info("Searching for height", "height", height, "min", min, "max", max) + minVal, maxVal := wal.group.MinIndex(), wal.group.MaxIndex() + wal.Logger.Info("Searching for height", "height", height, "min", minVal, "max", maxVal) var ( mode = WALSearchModeBackwards @@ -293,18 +293,18 @@ func (wal *baseWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd } OUTER_LOOP: - for min <= max { + for minVal <= maxVal { var index int // set index depending on mode. switch mode { case WALSearchModeBackwards: - index = max + backoff + idxoff - if max < index { + index = maxVal + backoff + idxoff + if maxVal < index { // (max+backoff)+ doesn't contain any height. // adjust max & backoff accordingly. idxoff = 0 - max = max + backoff - 1 + maxVal = maxVal + backoff - 1 if backoff == 0 { backoff = -1 } else { @@ -312,16 +312,16 @@ OUTER_LOOP: } continue OUTER_LOOP } - if index < min { + if index < minVal { panic("should not happen") } case WALSearchModeBinary: - index = (min+max+1)/2 + idxoff - if max < index { + index = (minVal+maxVal+1)/2 + idxoff + if maxVal < index { // ((min+max+1)/2)+ doesn't contain any height. // adjust max & binary search accordingly. idxoff = 0 - max = (min+max+1)/2 - 1 + maxVal = (minVal+maxVal+1)/2 - 1 continue OUTER_LOOP } } @@ -360,24 +360,24 @@ OUTER_LOOP: case WALSearchModeBackwards: idxoff = 0 if backoff == 0 { - max-- + maxVal-- backoff = -1 } else { - max += backoff + maxVal += backoff backoff *= 2 } // convert to binary search if backoff is too big. // max+backoff would work but max+(backoff*2) is smoother. - if max+(backoff*2) <= min { + if maxVal+(backoff*2) <= minVal { wal.Logger.Info("Converting to binary search", - "height", height, "min", min, - "max", max, "backoff", backoff) + "height", height, "min", minVal, + "max", maxVal, "backoff", backoff) backoff = 0 mode = WALSearchModeBinary } case WALSearchModeBinary: idxoff = 0 - max = (min+max+1)/2 - 1 + maxVal = (minVal+maxVal+1)/2 - 1 } dec.Close() continue OUTER_LOOP @@ -398,21 +398,21 @@ OUTER_LOOP: } else { // convert to binary search with index as new min. wal.Logger.Info("Converting to binary search with new min", - "height", height, "min", min, - "max", max, "backoff", backoff) + "height", height, "min", minVal, + "max", maxVal, "backoff", backoff) idxoff = 0 backoff = 0 - min = index + minVal = index mode = WALSearchModeBinary dec.Close() continue OUTER_LOOP } case WALSearchModeBinary: - if index < max { + if index < maxVal { // maybe in @index, but first try binary search // between @index and max. idxoff = 0 - min = index + minVal = index dec.Close() continue OUTER_LOOP } else { // index == max diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index 7e67392ebe7..0801fcfe227 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -208,11 +208,11 @@ func ExecSignAndBroadcast( return errors.Wrap(err, "broadcast tx") } if bres.CheckTx.IsErr() { - return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + return errors.Wrapf(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) } if bres.DeliverTx.IsErr() { io.Println("TX HASH: ", base64.StdEncoding.EncodeToString(bres.Hash)) - return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + return errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) } io.Println(string(bres.DeliverTx.Data)) diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index ea3d0546fa0..7f1e152c79c 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -298,7 +298,7 @@ func (kb dbKeybase) ExportPrivateKeyObject(nameOrBech32 string, passphrase strin func (kb dbKeybase) Export(nameOrBech32 string) (astr string, err error) { info, err := kb.GetByNameOrAddress(nameOrBech32) if err != nil { - return "", errors.Wrap(err, "getting info for name %s", nameOrBech32) + return "", errors.Wrapf(err, "getting info for name %s", nameOrBech32) } bz := kb.db.Get(infoKey(info.GetName())) if bz == nil { @@ -313,7 +313,7 @@ func (kb dbKeybase) Export(nameOrBech32 string) (astr string, err error) { func (kb dbKeybase) ExportPubKey(nameOrBech32 string) (astr string, err error) { info, err := kb.GetByNameOrAddress(nameOrBech32) if err != nil { - return "", errors.Wrap(err, "getting info for name %s", nameOrBech32) + return "", errors.Wrapf(err, "getting info for name %s", nameOrBech32) } return armor.ArmorPubKeyBytes(info.GetPubKey().Bytes()), nil } diff --git a/tm2/pkg/crypto/merkle/proof_key_path.go b/tm2/pkg/crypto/merkle/proof_key_path.go index 278f782833c..469a69bf2bc 100644 --- a/tm2/pkg/crypto/merkle/proof_key_path.go +++ b/tm2/pkg/crypto/merkle/proof_key_path.go @@ -96,13 +96,13 @@ func KeyPathToKeys(path string) (keys [][]byte, err error) { hexPart := part[2:] key, err := hex.DecodeString(hexPart) if err != nil { - return nil, errors.Wrap(err, "decoding hex-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding hex-encoded part #%d: /%s", i, part) } keys[i] = key } else { key, err := url.PathUnescape(part) if err != nil { - return nil, errors.Wrap(err, "decoding url-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding url-encoded part #%d: /%s", i, part) } keys[i] = []byte(key) // TODO Test this with random bytes, I'm not sure that it works for arbitrary bytes... } diff --git a/tm2/pkg/crypto/secp256k1/secp256k1.go b/tm2/pkg/crypto/secp256k1/secp256k1.go index 03f51f5ebf9..c9bb3f39c26 100644 --- a/tm2/pkg/crypto/secp256k1/secp256k1.go +++ b/tm2/pkg/crypto/secp256k1/secp256k1.go @@ -8,7 +8,7 @@ import ( "math/big" secp256k1 "github.com/btcsuite/btcd/btcec/v2" - "golang.org/x/crypto/ripemd160" + "golang.org/x/crypto/ripemd160" //nolint:gosec "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -124,8 +124,8 @@ func (pubKey PubKeySecp256k1) Address() crypto.Address { hasherSHA256.Write(pubKey[:]) // does not error sha := hasherSHA256.Sum(nil) - hasherRIPEMD160 := ripemd160.New() - hasherRIPEMD160.Write(sha) // does not error + hasherRIPEMD160 := ripemd160.New() //nolint:gosec + hasherRIPEMD160.Write(sha) // does not error return crypto.AddressFromBytes(hasherRIPEMD160.Sum(nil)) } diff --git a/tm2/pkg/errors/errors.go b/tm2/pkg/errors/errors.go index c72d9c64680..1b40c903c41 100644 --- a/tm2/pkg/errors/errors.go +++ b/tm2/pkg/errors/errors.go @@ -8,19 +8,26 @@ import ( // ---------------------------------------- // Convenience method. -func Wrap(cause interface{}, format string, args ...interface{}) Error { +func Wrap(cause interface{}, msg string) Error { if causeCmnError, ok := cause.(*cmnError); ok { //nolint:gocritic - msg := fmt.Sprintf(format, args...) return causeCmnError.Stacktrace().Trace(1, msg) } else if cause == nil { - return newCmnError(FmtError{format, args}).Stacktrace() + return newCmnError(FmtError{format: msg, args: []interface{}{}}).Stacktrace() } else { // NOTE: causeCmnError is a typed nil here. - msg := fmt.Sprintf(format, args...) return newCmnError(cause).Stacktrace().Trace(1, msg) } } +func Wrapf(cause interface{}, format string, args ...interface{}) Error { + if cause == nil { + return newCmnError(FmtError{format, args}).Stacktrace() + } + + msg := fmt.Sprintf(format, args...) + return Wrap(cause, msg) +} + func Cause(err error) error { if cerr, ok := err.(*cmnError); ok { return cerr.Data().(error) diff --git a/tm2/pkg/errors/errors_test.go b/tm2/pkg/errors/errors_test.go index 21115c21862..ab7a7086ad4 100644 --- a/tm2/pkg/errors/errors_test.go +++ b/tm2/pkg/errors/errors_test.go @@ -35,7 +35,7 @@ func TestErrorPanic(t *testing.T) { func TestWrapSomething(t *testing.T) { t.Parallel() - err := Wrap("something", "formatter%v%v", 0, 1) + err := Wrapf("something", "formatter%v%v", 0, 1) assert.Equal(t, "something", err.Data()) assert.Equal(t, "something", fmt.Sprintf("%v", err)) @@ -46,7 +46,7 @@ func TestWrapSomething(t *testing.T) { func TestWrapNothing(t *testing.T) { t.Parallel() - err := Wrap(nil, "formatter%v%v", 0, 1) + err := Wrapf(nil, "formatter%v%v", 0, 1) assert.Equal(t, FmtError{"formatter%v%v", []interface{}{0, 1}}, diff --git a/tm2/pkg/iavl/proof_range.go b/tm2/pkg/iavl/proof_range.go index ea6bce24fc0..0ce8ebdf057 100644 --- a/tm2/pkg/iavl/proof_range.go +++ b/tm2/pkg/iavl/proof_range.go @@ -273,7 +273,7 @@ func (proof *RangeProof) _computeRootHash() (rootHash []byte, treeEnd bool, err return nil, treeEnd, false, errors.Wrap(err, "recursive COMPUTEHASH call") } if !bytes.Equal(derivedRoot, lpath.Right) { - return nil, treeEnd, false, errors.Wrap(ErrInvalidRoot, "intermediate root hash %X doesn't match, got %X", lpath.Right, derivedRoot) + return nil, treeEnd, false, errors.Wrapf(ErrInvalidRoot, "intermediate root hash %X doesn't match, got %X", lpath.Right, derivedRoot) } if done { return hash, treeEnd, true, nil diff --git a/tm2/pkg/os/os.go b/tm2/pkg/os/os.go index f0e5825cb14..63601ded92a 100644 --- a/tm2/pkg/os/os.go +++ b/tm2/pkg/os/os.go @@ -33,7 +33,7 @@ func Kill() error { } func Exit(s string) { - fmt.Printf(s + "\n") + fmt.Print(s + "\n") os.Exit(1) } diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index b2de68e1ae3..317f34e496b 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -203,7 +203,7 @@ func (sw *Switch) OnStart() error { for _, reactor := range sw.reactors { err := reactor.Start() if err != nil { - return errors.Wrap(err, "failed to start %v", reactor) + return errors.Wrapf(err, "failed to start %v", reactor) } } diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index 4f36757efc0..6457b193a6b 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -658,7 +658,7 @@ func ParseCoin(coinStr string) (coin Coin, err error) { amount, err := strconv.ParseInt(amountStr, 10, 64) if err != nil { - return Coin{}, errors.Wrap(err, "failed to parse coin amount: %s", amountStr) + return Coin{}, errors.Wrapf(err, "failed to parse coin amount: %s", amountStr) } if err := validateDenom(denomStr); err != nil { diff --git a/tm2/pkg/std/gasprice.go b/tm2/pkg/std/gasprice.go index 5acc934fb71..f68ee190e41 100644 --- a/tm2/pkg/std/gasprice.go +++ b/tm2/pkg/std/gasprice.go @@ -19,11 +19,11 @@ func ParseGasPrice(gasprice string) (GasPrice, error) { } price, err := ParseCoin(parts[0]) if err != nil { - return GasPrice{}, errors.Wrap(err, "invalid gas price: %s (invalid price)", gasprice) + return GasPrice{}, errors.Wrapf(err, "invalid gas price: %s (invalid price)", gasprice) } gas, err := ParseCoin(parts[1]) if err != nil { - return GasPrice{}, errors.Wrap(err, "invalid gas price: %s (invalid gas denom)", gasprice) + return GasPrice{}, errors.Wrapf(err, "invalid gas price: %s (invalid gas denom)", gasprice) } if gas.Denom != "gas" { return GasPrice{}, errors.New("invalid gas price: %s (invalid gas denom)", gasprice) @@ -43,7 +43,7 @@ func ParseGasPrices(gasprices string) (res []GasPrice, err error) { for i, part := range parts { res[i], err = ParseGasPrice(part) if err != nil { - return nil, errors.Wrap(err, "invalid gas prices: %s", gasprices) + return nil, errors.Wrapf(err, "invalid gas prices: %s", gasprices) } } return res, nil diff --git a/tm2/pkg/store/cache/store_test.go b/tm2/pkg/store/cache/store_test.go index 1caf51ea52c..1cb1d0b60d9 100644 --- a/tm2/pkg/store/cache/store_test.go +++ b/tm2/pkg/store/cache/store_test.go @@ -359,12 +359,12 @@ func TestCacheKVMergeIteratorRandom(t *testing.T) { truth := memdb.NewMemDB() start, end := 25, 975 - max := 1000 + maxVal := 1000 setRange(st, truth, start, end) // do an op, test the iterator for i := 0; i < 2000; i++ { - doRandomOp(st, truth, max) + doRandomOp(st, truth, maxVal) assertIterateDomainCompare(t, st, truth) } } From 93a30b7268600657bec095d2d5f1ec85c6359e4a Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:51:35 +0100 Subject: [PATCH 37/86] docs: add help links on github-bot rules (#3275) Add help links for each automated github-bot rules.
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
--- contribs/github-bot/internal/check/comment.go | 2 +- contribs/github-bot/internal/config/config.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contribs/github-bot/internal/check/comment.go b/contribs/github-bot/internal/check/comment.go index 434df8f9e76..297395ffe4b 100644 --- a/contribs/github-bot/internal/check/comment.go +++ b/contribs/github-bot/internal/check/comment.go @@ -28,7 +28,7 @@ var ( // Regex for capturing only the checkboxes. checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) // Regex used to capture markdown links. - markdownLink = regexp.MustCompile(`\[(.*)\]\(.*\)`) + markdownLink = regexp.MustCompile(`\[(.*)\]\([^)]*\)`) ) // These structures contain the necessary information to generate diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index ac1d185f759..c1d89e4cde5 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -27,12 +27,12 @@ type ManualCheck struct { func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { auto := []AutomaticCheck{ { - Description: "Maintainers must be able to edit this pull request", + Description: "Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))", If: c.Always(), Then: r.MaintainerCanModify(), }, { - Description: "The pull request head branch must be up-to-date with its base", + Description: "The pull request head branch must be up-to-date with its base ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/keeping-your-pull-request-in-sync-with-the-base-branch))", If: c.Always(), Then: r.UpToDateWith(gh, r.PR_BASE), }, From f30b8816ceb28015de303bafc97a89b9cd69ac96 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:54:05 +0100 Subject: [PATCH 38/86] ci: make github-bot ignore events emitted by codecov (#3274) It's a bit of a "push and pray" since it would take too long to set up to reproduce this on my test repo/org, but I've triple-checked the documentation, the previous runs, etc., and it should be fine. Related to https://github.com/gnolang/gno/issues/3238#issuecomment-2516995329
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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index cbfec5730fc..15ad9c6aa04 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -33,8 +33,8 @@ jobs: # 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 }} + # Prevent bot from retriggering itself and ignore event emitted by codecov + if: ${{ github.actor != vars.GH_BOT_LOGIN && github.actor != "codecov[bot]" }} runs-on: ubuntu-latest permissions: pull-requests: read From f9c4f2aa9395c27a14e9d351ae4f2338867500ce Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Fri, 6 Dec 2024 14:24:31 +0100 Subject: [PATCH 39/86] fix: In keybase writeInfo, enforce one name for address (#3221) The `Keybase` interface was written with the assumption of a one-to-one correspondence between a key's name and address. But this needs to be enforced by the code. PR https://github.com/gnolang/gno/pull/2685 updated `writeInfo` for the case of inserting a new key (address) with the same name as an existing key with a different address. In this case, `writeInfo` uses the name to look up the existing address and deletes the address entry. This PR does the same for the other case: Inserting a key with the same address as an existing key, but a new name. In this case, `writeInfo` uses the address to look up the existing key's name, and deletes the name entry. This is not a breaking change because none of the production code expects to add a key a second time with the same address but a different name. We update the `Keybase` doc comments to this effect. However, some of the tests in keybase_test.go make this assumption, so this PR fixes them: * `TestSignVerify` creates a key with name n2 and exports its public key. It wants to re-import the public key with name n3 and get the public key to check that it is the same public key as n2. We change this test to re-import the public key into a fresh in-memory key store. * `TestExportImportPubKey` creates a key with name "john", exports its public key, then re-imports it with the new name "john-pubkey-only" (but the same address). The current test checks that the key can still be fetched under the old name "john". But this breaks the one-to-one correspondence of key name and address. So the test is changed to confirm that the key with the old name is replaced by the new name.
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 - [ ] Provided any useful hints for running manual tests
Signed-off-by: Jeff Thompson --- tm2/pkg/crypto/keys/keybase.go | 9 ++++++++- tm2/pkg/crypto/keys/keybase_test.go | 15 ++++++++------- tm2/pkg/crypto/keys/types.go | 3 +++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index 7f1e152c79c..23c4237151a 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -523,10 +523,17 @@ func (kb dbKeybase) writeInfo(name string, info Info) error { kb.db.DeleteSync(addrKey(oldInfo.GetAddress())) } + addressKey := addrKey(info.GetAddress()) + nameKeyForAddress := kb.db.Get(addressKey) + if len(nameKeyForAddress) > 0 { + // Enforce 1-to-1 name to address. Remove the info by the old name with the same address + kb.db.DeleteSync(nameKeyForAddress) + } + serializedInfo := writeInfo(info) kb.db.SetSync(key, serializedInfo) // store a pointer to the infokey by address for fast lookup - kb.db.SetSync(addrKey(info.GetAddress()), key) + kb.db.SetSync(addressKey, key) return nil } diff --git a/tm2/pkg/crypto/keys/keybase_test.go b/tm2/pkg/crypto/keys/keybase_test.go index bfb21b46fad..25306e62635 100644 --- a/tm2/pkg/crypto/keys/keybase_test.go +++ b/tm2/pkg/crypto/keys/keybase_test.go @@ -149,11 +149,12 @@ func TestSignVerify(t *testing.T) { i2, err := cstore.CreateAccount(n2, mn2, bip39Passphrase, p2, 0, 0) require.Nil(t, err) - // Import a public key + // Import a public key into a new store armor, err := cstore.ExportPubKey(n2) require.Nil(t, err) - cstore.ImportPubKey(n3, armor) - i3, err := cstore.GetByName(n3) + cstore2 := NewInMemory() + cstore2.ImportPubKey(n3, armor) + i3, err := cstore2.GetByName(n3) require.NoError(t, err) require.Equal(t, i3.GetName(), n3) @@ -174,6 +175,7 @@ func TestSignVerify(t *testing.T) { s21, pub2, err := cstore.Sign(n2, p2, d1) require.Nil(t, err) require.Equal(t, i2.GetPubKey(), pub2) + require.Equal(t, i3.GetPubKey(), pub2) s22, pub2, err := cstore.Sign(n2, p2, d2) require.Nil(t, err) @@ -282,11 +284,10 @@ func TestExportImportPubKey(t *testing.T) { require.NoError(t, err) // Compare the public keys require.True(t, john.GetPubKey().Equals(john2.GetPubKey())) - // Ensure the original key hasn't changed - john, err = cstore.GetByName("john") + // Ensure that storing with the address of "john-pubkey-only" removed the entry for "john" + has, err := cstore.HasByName("john") require.NoError(t, err) - require.Equal(t, john.GetPubKey().Address(), addr) - require.Equal(t, john.GetName(), "john") + require.False(t, has) // Ensure keys cannot be overwritten err = cstore.ImportPubKey("john-pubkey-only", armor) diff --git a/tm2/pkg/crypto/keys/types.go b/tm2/pkg/crypto/keys/types.go index 3865951168e..bdaf39caa54 100644 --- a/tm2/pkg/crypto/keys/types.go +++ b/tm2/pkg/crypto/keys/types.go @@ -27,10 +27,12 @@ type Keybase interface { // CreateAccount creates an account based using the BIP44 path (44'/118'/{account}'/0/{index} // Encrypt the key to disk using encryptPasswd. + // If an account exists with the same address but a different name, it is replaced by the new name. // See https://github.com/tendermint/classic/sdk/issues/2095 CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) // Like CreateAccount but from general bip44 params. + // If an account exists with the same address but a different name, it is replaced by the new name. CreateAccountBip44(name, mnemonic, bip39Passwd, encryptPasswd string, params hd.BIP44Params) (Info, error) // CreateLedger creates, stores, and returns a new Ledger key reference @@ -43,6 +45,7 @@ type Keybase interface { CreateMulti(name string, pubkey crypto.PubKey) (info Info, err error) // The following operations will *only* work on locally-stored keys + // In all import operations, if an account exists with the same address but a different name, it is replaced by the new name. Rotate(name, oldpass string, getNewpass func() (string, error)) error Import(name string, armor string) (err error) ImportPrivKey(name, armor, decryptPassphrase, encryptPassphrase string) error From 6743b8de3b0ff4c54eb6395d859f7e8a67230fe5 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:45:28 +0100 Subject: [PATCH 40/86] ci: fix github-bot workflow (#3286) Fix the github-bot workflow by replacing double quotes by simple ones, see https://github.com/gnolang/gno/actions/runs/12201415150/workflow#L37
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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 15ad9c6aa04..644540c1aaf 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -34,7 +34,7 @@ jobs: define-prs-matrix: name: Define PRs matrix # Prevent bot from retriggering itself and ignore event emitted by codecov - if: ${{ github.actor != vars.GH_BOT_LOGIN && github.actor != "codecov[bot]" }} + if: ${{ github.actor != vars.GH_BOT_LOGIN && github.actor != 'codecov[bot]' }} runs-on: ubuntu-latest permissions: pull-requests: read From 3691956600eb1c7fe17a4d4e02c23a5561ee81a0 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:59:27 +0100 Subject: [PATCH 41/86] chore: refactor txlink in order to extend it (#3289) Signed-off-by: moul <94029+moul@users.noreply.github.com>
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
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/helplink/helplink.gno | 2 +- examples/gno.land/p/moul/txlink/txlink.gno | 12 ++++++------ examples/gno.land/p/moul/txlink/txlink_test.gno | 4 ++-- examples/gno.land/r/demo/boards/board.gno | 2 +- examples/gno.land/r/demo/boards/post.gno | 6 +++--- examples/gno.land/r/docs/adder/adder.gno | 2 +- examples/gno.land/r/gov/dao/v2/render.gno | 6 +++--- examples/gno.land/r/leon/hof/render.gno | 10 +++++----- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/gno.land/p/moul/helplink/helplink.gno b/examples/gno.land/p/moul/helplink/helplink.gno index 0c18f5d0360..14b44622a1e 100644 --- a/examples/gno.land/p/moul/helplink/helplink.gno +++ b/examples/gno.land/p/moul/helplink/helplink.gno @@ -70,7 +70,7 @@ func (r Realm) Func(title string, fn string, args ...string) string { // arguments. func (r Realm) FuncURL(fn string, args ...string) string { tlr := txlink.Realm(r) - return tlr.URL(fn, args...) + return tlr.Call(fn, args...) } // Home returns the base help URL for the specified realm. diff --git a/examples/gno.land/p/moul/txlink/txlink.gno b/examples/gno.land/p/moul/txlink/txlink.gno index 4705161578c..65edda6911e 100644 --- a/examples/gno.land/p/moul/txlink/txlink.gno +++ b/examples/gno.land/p/moul/txlink/txlink.gno @@ -5,7 +5,7 @@ // flexible arguments, allowing users to build dynamic links that integrate // seamlessly with various Gno clients. // -// The primary function, URL, is designed to produce markdown links for +// The primary function, Call, is designed to produce markdown links for // transaction functions in the current "relative realm". By specifying a custom // Realm, you can generate links that either use the current realm path or a // fully qualified path for another realm. @@ -21,10 +21,10 @@ import ( const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) -// URL returns a URL for the specified function with optional key-value +// Call returns a URL for the specified function with optional key-value // arguments, for the current realm. -func URL(fn string, args ...string) string { - return Realm("").URL(fn, args...) +func Call(fn string, args ...string) string { + return Realm("").Call(fn, args...) } // Realm represents a specific realm for generating tx links. @@ -48,9 +48,9 @@ func (r Realm) prefix() string { return "https://" + string(r) } -// URL returns a URL for the specified function with optional key-value +// Call returns a URL for the specified function with optional key-value // arguments. -func (r Realm) URL(fn string, args ...string) string { +func (r Realm) Call(fn string, args ...string) string { // Start with the base query url := r.prefix() + "$help&func=" + fn diff --git a/examples/gno.land/p/moul/txlink/txlink_test.gno b/examples/gno.land/p/moul/txlink/txlink_test.gno index a598a06b154..61b532270d4 100644 --- a/examples/gno.land/p/moul/txlink/txlink_test.gno +++ b/examples/gno.land/p/moul/txlink/txlink_test.gno @@ -6,7 +6,7 @@ import ( "gno.land/p/demo/urequire" ) -func TestURL(t *testing.T) { +func TestCall(t *testing.T) { tests := []struct { fn string args []string @@ -30,7 +30,7 @@ func TestURL(t *testing.T) { for _, tt := range tests { title := tt.fn t.Run(title, func(t *testing.T) { - got := tt.realm.URL(tt.fn, tt.args...) + got := tt.realm.Call(tt.fn, tt.args...) urequire.Equal(t, tt.want, got) }) } diff --git a/examples/gno.land/r/demo/boards/board.gno b/examples/gno.land/r/demo/boards/board.gno index 79b27da84b2..9b9fb730c68 100644 --- a/examples/gno.land/r/demo/boards/board.gno +++ b/examples/gno.land/r/demo/boards/board.gno @@ -135,5 +135,5 @@ func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string } func (board *Board) GetPostFormURL() string { - return txlink.URL("CreateThread", "bid", board.id.String()) + return txlink.Call("CreateThread", "bid", board.id.String()) } diff --git a/examples/gno.land/r/demo/boards/post.gno b/examples/gno.land/r/demo/boards/post.gno index 95d4b2977ba..c6e23cd59d0 100644 --- a/examples/gno.land/r/demo/boards/post.gno +++ b/examples/gno.land/r/demo/boards/post.gno @@ -156,7 +156,7 @@ func (post *Post) GetURL() string { } func (post *Post) GetReplyFormURL() string { - return txlink.URL("CreateReply", + return txlink.Call("CreateReply", "bid", post.board.id.String(), "threadid", post.threadID.String(), "postid", post.id.String(), @@ -164,14 +164,14 @@ func (post *Post) GetReplyFormURL() string { } func (post *Post) GetRepostFormURL() string { - return txlink.URL("CreateRepost", + return txlink.Call("CreateRepost", "bid", post.board.id.String(), "postid", post.id.String(), ) } func (post *Post) GetDeleteFormURL() string { - return txlink.URL("DeletePost", + return txlink.Call("DeletePost", "bid", post.board.id.String(), "threadid", post.threadID.String(), "postid", post.id.String(), diff --git a/examples/gno.land/r/docs/adder/adder.gno b/examples/gno.land/r/docs/adder/adder.gno index cd96d241692..33e971c7c0b 100644 --- a/examples/gno.land/r/docs/adder/adder.gno +++ b/examples/gno.land/r/docs/adder/adder.gno @@ -27,7 +27,7 @@ func Render(path string) string { result += "Last Updated: " + formatTimestamp(lastUpdate) + "\n\n" // Generate a transaction link to call Add with 42 as the default parameter - txLink := txlink.URL("Add", "n", "42") + txLink := txlink.Call("Add", "n", "42") result += "[Increase Number](" + txLink + ")\n" return result diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno index 4cca397e851..57b7b601523 100644 --- a/examples/gno.land/r/gov/dao/v2/render.gno +++ b/examples/gno.land/r/gov/dao/v2/render.gno @@ -111,9 +111,9 @@ func renderActionBar(p dao.Proposal, idx int) 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"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"), ) } else { out += "The voting period for this proposal is over." diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index 6b06ef04051..b4d51d03362 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -64,12 +64,12 @@ func (i Item) Render(dashboard bool) string { out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum) out += ufmt.Sprintf("#### [%d👍](%s) - [%d👎](%s)\n\n", - i.upvote.Size(), txlink.URL("Upvote", "pkgpath", i.pkgpath), - i.downvote.Size(), txlink.URL("Downvote", "pkgpath", i.pkgpath), + i.upvote.Size(), txlink.Call("Upvote", "pkgpath", i.pkgpath), + i.downvote.Size(), txlink.Call("Downvote", "pkgpath", i.pkgpath), ) if dashboard { - out += ufmt.Sprintf("[Delete](%s)", txlink.URL("Delete", "pkgpath", i.pkgpath)) + out += ufmt.Sprintf("[Delete](%s)", txlink.Call("Delete", "pkgpath", i.pkgpath)) } return out @@ -83,9 +83,9 @@ func renderDashboard() string { out += ufmt.Sprintf("Exhibition admin: %s\n\n", owner.Owner().String()) if !exhibition.IsPaused() { - out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.URL("Pause")) + out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.Call("Pause")) } else { - out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.URL("Unpause")) + out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.Call("Unpause")) } out += "---\n\n" From ac899c850d7b234e83d2dd56c3eb7ee1c847c8a0 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 6 Dec 2024 22:13:24 +0100 Subject: [PATCH 42/86] feat: support custom VM domain (#2911) Introducing the concept of "ChainDomain," a local primary domain for packages. The next step, which will be another PR, is to ensure that we can launch a gnoland/gnodev instance while importing a local folder or using a genesis to preload packages from other domains. This will allow users to add packages only to the primary domain while accessing packages from multiple domains. The result will be a preview of the upcoming IBC era, where a single chain can add packages only to its domain but can fetch missing dependencies from other registered zones. - [x] gnovm unaware of gno.land, just accepting valid domains - [x] vmkeeper initialized with a domain - [x] Stdlib to know the current primary domain + new std.ChainDomain - [x] new unit tests around custom domains Depends on #2910 Depends on https://github.com/gnolang/gno/pull/3003 Blocks a new PR that will add multidomain support. Related with https://github.com/gnolang/gno/issues/2904#issuecomment-2395011435 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: n0izn0iz Co-authored-by: Mikael VALLENET Co-authored-by: Morgan --- contribs/gnodev/cmd/gnodev/main.go | 25 +++++++---- contribs/gnodev/cmd/gnodev/setup_node.go | 2 +- contribs/gnodev/pkg/dev/node.go | 13 +++--- contribs/gnodev/pkg/dev/node_test.go | 6 +-- .../cmd/gnoland/testdata/addpkg_domain.txtar | 15 +++++++ .../cmd/gnoland/testdata/genesis_params.txtar | 18 +++++++- .../cmd/gnoland/testdata/simulate_gas.txtar | 4 +- gno.land/genesis/genesis_params.toml | 2 +- gno.land/pkg/gnoland/node_inmemory.go | 9 +++- gno.land/pkg/sdk/vm/gas_test.go | 4 +- gno.land/pkg/sdk/vm/keeper.go | 45 ++++++++++++------- gno.land/pkg/sdk/vm/keeper_test.go | 39 +++++++++++++++- gno.land/pkg/sdk/vm/msgs.go | 4 +- gno.land/pkg/sdk/vm/params.go | 20 +++++++++ gnovm/cmd/gno/test.go | 2 +- gnovm/memfile.go | 2 +- gnovm/memfile_test.go | 4 +- gnovm/pkg/gnolang/helpers.go | 30 ++++++------- gnovm/pkg/test/test.go | 1 + gnovm/stdlibs/generated.go | 20 +++++++++ gnovm/stdlibs/std/context.go | 1 + gnovm/stdlibs/std/native.gno | 5 ++- gnovm/stdlibs/std/native.go | 4 ++ gnovm/tests/files/std5.gno | 4 +- gnovm/tests/files/std8.gno | 4 +- gnovm/tests/files/zrealm_natbind1_stdlibs.gno | 16 +++++++ 26 files changed, 231 insertions(+), 68 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/addpkg_domain.txtar create mode 100644 gno.land/pkg/sdk/vm/params.go create mode 100644 gnovm/tests/files/zrealm_natbind1_stdlibs.gno diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 2c694b608bb..c9d6487d753 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -61,18 +61,20 @@ type devCfg struct { webRemoteHelperAddr string // Node Configuration - minimal bool - verbose bool - noWatch bool - noReplay bool - maxGas int64 - chainId string - serverMode bool - unsafeAPI bool + minimal bool + verbose bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + chainDomain string + serverMode bool + unsafeAPI bool } var defaultDevOptions = &devCfg{ chainId: "dev", + chainDomain: "gno.land", maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", @@ -203,6 +205,13 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "set node ChainID", ) + fs.StringVar( + &c.chainDomain, + "chain-domain", + defaultDevOptions.chainDomain, + "set node ChainDomain", + ) + fs.BoolVar( &c.noWatch, "no-watch", diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index a2b1970d0ef..eaeb89b7e95 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -57,7 +57,7 @@ func setupDevNodeConfig( balances gnoland.Balances, pkgspath []gnodev.PackagePath, ) *gnodev.NodeConfig { - config := gnodev.DefaultNodeConfig(cfg.root) + config := gnodev.DefaultNodeConfig(cfg.root, cfg.chainDomain) config.Logger = logger config.Emitter = emitter diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index e0ed64aad36..0502c03c86f 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -43,9 +43,10 @@ type NodeConfig struct { NoReplay bool MaxGasPerBlock int64 ChainID string + ChainDomain string } -func DefaultNodeConfig(rootdir string) *NodeConfig { +func DefaultNodeConfig(rootdir, domain string) *NodeConfig { tmc := gnoland.NewDefaultTMConfig(rootdir) tmc.Consensus.SkipTimeoutCommit = false // avoid time drifting, see issue #1507 tmc.Consensus.WALDisabled = true @@ -65,6 +66,7 @@ func DefaultNodeConfig(rootdir string) *NodeConfig { DefaultDeployer: defaultDeployer, BalancesList: balances, ChainID: tmc.ChainID(), + ChainDomain: domain, TMConfig: tmc, SkipFailingGenesisTxs: true, MaxGasPerBlock: 10_000_000_000, @@ -487,7 +489,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) } // Setup node config - nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) + nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, n.config.ChainDomain, genesis) nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler // Speed up stdlib loading after first start (saves about 2-3 seconds on each reload). nodeConfig.CacheStdlibLoad = true @@ -566,10 +568,10 @@ func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result return } -func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig { +func newNodeConfig(tmc *tmcfg.Config, chainid, chaindomain string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig { // Create Mocked Identity pv := gnoland.NewMockedPrivValidator() - genesis := gnoland.NewDefaultGenesisConfig(chainid) + genesis := gnoland.NewDefaultGenesisConfig(chainid, chaindomain) genesis.AppState = appstate // Add self as validator @@ -583,10 +585,11 @@ func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesi }, } - return &gnoland.InMemoryNodeConfig{ + cfg := &gnoland.InMemoryNodeConfig{ PrivValidator: pv, TMConfig: tmc, Genesis: genesis, VMOutput: os.Stdout, } + return cfg } diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index e05e5a996fa..4a4acc232b9 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -38,7 +38,7 @@ func TestNewNode_NoPackages(t *testing.T) { logger := log.NewTestingLogger(t) // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.Logger = logger node, err := NewDevNode(ctx, cfg) require.NoError(t, err) @@ -66,7 +66,7 @@ func Render(_ string) string { return "foo" } logger := log.NewTestingLogger(t) // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.PackagesPathList = []PackagePath{pkgpath} cfg.Logger = logger node, err := NewDevNode(ctx, cfg) @@ -475,7 +475,7 @@ func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { } func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.PackagesPathList = pkgslist return cfg } diff --git a/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar b/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar new file mode 100644 index 00000000000..25e4fe0d3a3 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar @@ -0,0 +1,15 @@ +gnoland start + +# addpkg with anotherdomain.land +! gnokey maketx addpkg -pkgdir $WORK -pkgpath anotherdomain.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout 'TX HASH:' +stderr 'invalid package path' +stderr 'invalid domain: anotherdomain.land/r/foobar/bar' + +# addpkg with gno.land +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout 'OK!' + +-- bar.gno -- +package bar +func Render(path string) string { return "hello" } diff --git a/gno.land/cmd/gnoland/testdata/genesis_params.txtar b/gno.land/cmd/gnoland/testdata/genesis_params.txtar index 43ecd8ccacb..d09ededf78a 100644 --- a/gno.land/cmd/gnoland/testdata/genesis_params.txtar +++ b/gno.land/cmd/gnoland/testdata/genesis_params.txtar @@ -1,14 +1,28 @@ -# test for https://github.com/gnolang/gno/pull/3003 +# Test for #3003, #2911. gnoland start +# Query and validate official parameters. +# These parameters should ideally be tested in a txtar format to ensure that a +# default initialization of "gnoland" provides the expected default values. + +# Verify the default chain domain parameter for Gno.land +gnokey query params/vm/gno.land/r/sys/params.vm.chain_domain.string +stdout 'data: "gno.land"$' + +# Test custom parameters to confirm they return the expected values and types. + gnokey query params/vm/gno.land/r/sys/params.test.foo.string stdout 'data: "bar"$' + gnokey query params/vm/gno.land/r/sys/params.test.foo.int64 stdout 'data: "-1337"' + gnokey query params/vm/gno.land/r/sys/params.test.foo.uint64 stdout 'data: "42"' + gnokey query params/vm/gno.land/r/sys/params.test.foo.bool stdout 'data: true' -# XXX: bytes + +# TODO: Consider adding a test case for a byte array parameter diff --git a/gno.land/cmd/gnoland/testdata/simulate_gas.txtar b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar index cd58b4ccc8f..9d3c8abe69f 100644 --- a/gno.land/cmd/gnoland/testdata/simulate_gas.txtar +++ b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar @@ -6,11 +6,11 @@ gnoland start # simulate only gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 -stdout 'GAS USED: 50299' +stdout 'GAS USED: 51299' # simulate skip gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 -stdout 'GAS USED: 50299' # same as simulate only +stdout 'GAS USED: 51299' # same as simulate only -- package/package.gno -- diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml index 5f4d9c5015c..fb080024624 100644 --- a/gno.land/genesis/genesis_params.toml +++ b/gno.land/genesis/genesis_params.toml @@ -8,7 +8,7 @@ ## gnovm ["gno.land/r/sys/params.vm"] - # TODO: chain_domain.string = "gno.land" + chain_domain.string = "gno.land" # TODO: max_gas.int64 = 100_000_000 # TODO: chain_tz.string = "UTC" # TODO: default_storage_allowance.string = "" diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 426a8c780c7..f42166411c8 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -36,7 +36,11 @@ func NewMockedPrivValidator() bft.PrivValidator { } // NewDefaultGenesisConfig creates a default configuration for an in-memory node. -func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { +func NewDefaultGenesisConfig(chainid, chaindomain string) *bft.GenesisDoc { + // custom chain domain + var domainParam Param + _ = domainParam.Parse("gno.land/r/sys/params.vm.chain_domain.string=" + chaindomain) + return &bft.GenesisDoc{ GenesisTime: time.Now(), ChainID: chainid, @@ -46,6 +50,9 @@ func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { AppState: &GnoGenesisState{ Balances: []Balance{}, Txs: []TxWithMetadata{}, + Params: []Param{ + domainParam, + }, }, } } diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 3a11d97c235..677d86a0331 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -75,7 +75,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 100_000 :) - assert.Equal(t, int64(92825), gasDeliver) + assert.Equal(t, int64(93825), gasDeliver) } // Enough gas for a failed transaction. @@ -95,7 +95,7 @@ func TestAddPkgDeliverTxFailed(t *testing.T) { gasDeliver := gctx.GasMeter().GasConsumed() assert.False(t, res.IsOK()) - assert.Equal(t, int64(2231), gasDeliver) + assert.Equal(t, int64(3231), gasDeliver) } // Not enough gas for a failed transaction. diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 52eff20ea95..e4f7a8543a7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -85,6 +85,7 @@ func NewVMKeeper( bank: bank, prmk: prmk, } + return vmk } @@ -192,6 +193,7 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { } m := gno.NewMachineWithOptions(gno.MachineOptions{ + // XXX: gno.land, vm.domain, other? PkgPath: "gno.land/r/stdlibs/" + pkgPath, // PkgPath: pkgPath, XXX why? Store: store, @@ -226,20 +228,22 @@ func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore } // Namespace can be either a user or crypto address. -var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) - -const sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" +var reNamespace = regexp.MustCompile(`^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/(?:r|p)/([\.~_a-zA-Z0-9]+)`) // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { - var sysUsersPkg string - vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) + sysUsersPkg := vm.getSysUsersPkgParam(ctx) if sysUsersPkg == "" { return nil } + chainDomain := vm.getChainDomainParam(ctx) store := vm.getGnoTransactionStore(ctx) + if !strings.HasPrefix(pkgPath, chainDomain+"/") { + return ErrInvalidPkgPath(pkgPath) // no match + } + match := reNamespace.FindStringSubmatch(pkgPath) switch len(match) { case 0: @@ -248,9 +252,6 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add default: panic("invalid pattern while matching pkgpath") } - if len(match) != 2 { - return ErrInvalidPkgPath(pkgPath) - } username := match[1] // if `sysUsersPkg` does not exist -> skip validation. @@ -263,6 +264,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add pkgAddr := gno.DerivePkgAddr(pkgPath) msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: creator.Bech32(), @@ -320,6 +322,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { memPkg := msg.Package deposit := msg.Deposit gnostore := vm.getGnoTransactionStore(ctx) + chainDomain := vm.getChainDomainParam(ctx) // Validate arguments. if creator.IsZero() { @@ -332,6 +335,9 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { if err := msg.Package.Validate(); err != nil { return ErrInvalidPkgPath(err.Error()) } + if !strings.HasPrefix(pkgPath, chainDomain+"/") { + return ErrInvalidPkgPath("invalid domain: " + pkgPath) + } if pv := gnostore.GetPackage(pkgPath, false); pv != nil { return ErrPkgAlreadyExists("package already exists: " + pkgPath) } @@ -363,6 +369,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { // Parse and run the files, construct *PV. msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: creator.Bech32(), @@ -461,8 +468,10 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { // Make context. // NOTE: if this is too expensive, // could it be safely partially memoized? + chainDomain := vm.getChainDomainParam(ctx) msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: caller.Bech32(), @@ -531,11 +540,12 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { gnostore := vm.getGnoTransactionStore(ctx) send := msg.Send memPkg := msg.Package + chainDomain := vm.getChainDomainParam(ctx) // coerce path to right one. // the path in the message must be "" or the following path. // this is already checked in MsgRun.ValidateBasic - memPkg.Path = "gno.land/r/" + msg.Caller.String() + "/run" + memPkg.Path = chainDomain + "/r/" + msg.Caller.String() + "/run" // Validate arguments. callerAcc := vm.acck.GetAccount(ctx, caller) @@ -561,6 +571,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { // Parse and run the files, construct *PV. msgCtx := stdlibs.ExecContext{ ChainID: ctx.ChainID(), + ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), OrigCaller: caller.Bech32(), @@ -722,10 +733,12 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res return "", err } // Construct new machine. + chainDomain := vm.getChainDomainParam(ctx) msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), + ChainID: ctx.ChainID(), + ChainDomain: chainDomain, + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), // OrigCaller: caller, // OrigSend: send, // OrigSendSpent: nil, @@ -788,10 +801,12 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string return "", err } // Construct new machine. + chainDomain := vm.getChainDomainParam(ctx) msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), + ChainID: ctx.ChainID(), + ChainDomain: chainDomain, + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), // OrigCaller: caller, // OrigSend: jsend, // OrigSendSpent: nil, diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index 9afbb3de551..f8144988c44 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -22,7 +22,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/store/types" ) -var coinsString = ugnot.ValueString(10000000) +var coinsString = ugnot.ValueString(10_000_000) func TestVMKeeperAddPackage(t *testing.T) { env := setupTestEnv() @@ -68,6 +68,43 @@ func Echo() string { return "hello world" } assert.Equal(t, expected, memFile.Body) } +func TestVMKeeperAddPackage_InvalidDomain(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) + + // Create test package. + files := []*gnovm.MemFile{ + { + Name: "test.gno", + Body: `package test +func Echo() string {return "hello world"}`, + }, + } + pkgPath := "anotherdomain.land/r/test" + msg1 := NewMsgAddPackage(addr, pkgPath, files) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) + + err := env.vmk.AddPackage(ctx, msg1) + + assert.Error(t, err, ErrInvalidPkgPath("invalid domain: anotherdomain.land/r/test")) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) + + err = env.vmk.AddPackage(ctx, msg1) + assert.Error(t, err, ErrInvalidPkgPath("invalid domain: anotherdomain.land/r/test")) + + // added package is formatted + store := env.vmk.getGnoTransactionStore(ctx) + memFile := store.GetMemFile("gno.land/r/test", "test.gno") + assert.Nil(t, memFile) +} + // Sending total send amount succeeds. func TestVMKeeperOrigSend1(t *testing.T) { env := setupTestEnv() diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index d5b82067a98..1ce648acb19 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -186,8 +186,8 @@ func (msg MsgRun) ValidateBasic() error { } // Force memPkg path to the reserved run path. - wantPath := "gno.land/r/" + msg.Caller.String() + "/run" - if path := msg.Package.Path; path != "" && path != wantPath { + wantSuffix := "/r/" + msg.Caller.String() + "/run" + if path := msg.Package.Path; path != "" && !strings.HasSuffix(path, wantSuffix) { return ErrInvalidPkgPath(fmt.Sprintf("invalid pkgpath for MsgRun: %q", path)) } diff --git a/gno.land/pkg/sdk/vm/params.go b/gno.land/pkg/sdk/vm/params.go new file mode 100644 index 00000000000..248fb8a81fb --- /dev/null +++ b/gno.land/pkg/sdk/vm/params.go @@ -0,0 +1,20 @@ +package vm + +import "github.com/gnolang/gno/tm2/pkg/sdk" + +const ( + sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" + chainDomainParamPath = "gno.land/r/sys/params.chain_domain.string" +) + +func (vm *VMKeeper) getChainDomainParam(ctx sdk.Context) string { + chainDomain := "gno.land" // default + vm.prmk.GetString(ctx, chainDomainParamPath, &chainDomain) + return chainDomain +} + +func (vm *VMKeeper) getSysUsersPkgParam(ctx sdk.Context) string { + var sysUsersPkg string + vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) + return sysUsersPkg +} diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 04a3808718d..511a704dd7d 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -205,7 +205,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { 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 + strings.ToLower(random.RandStr(8)) + gnoPkgPath = "gno.land/r/" + strings.ToLower(random.RandStr(8)) // XXX: gno.land hardcoded for convenience. } } diff --git a/gnovm/memfile.go b/gnovm/memfile.go index a37bba6ef3d..6988c893dd7 100644 --- a/gnovm/memfile.go +++ b/gnovm/memfile.go @@ -41,7 +41,7 @@ const pathLengthLimit = 256 var ( rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) - rePkgOrRlmPath = regexp.MustCompile(`^gno\.land\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`) + rePkgOrRlmPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`) reFileName = regexp.MustCompile(`^([a-zA-Z0-9_]*\.[a-z0-9_\.]*|LICENSE|README)$`) ) diff --git a/gnovm/memfile_test.go b/gnovm/memfile_test.go index c93c251b0e7..5ef70e9e868 100644 --- a/gnovm/memfile_test.go +++ b/gnovm/memfile_test.go @@ -158,13 +158,13 @@ func TestMemPackage_Validate(t *testing.T) { "invalid package/realm path", }, { - "Invalid path", + "Custom domain", &MemPackage{ Name: "hey", Path: "github.com/p/path/path", Files: []*MemFile{{Name: "a.gno"}}, }, - "invalid package/realm path", + "", }, { "Special character", diff --git a/gnovm/pkg/gnolang/helpers.go b/gnovm/pkg/gnolang/helpers.go index d3a8485ee17..ddc1fd2fa55 100644 --- a/gnovm/pkg/gnolang/helpers.go +++ b/gnovm/pkg/gnolang/helpers.go @@ -10,22 +10,21 @@ import ( // ---------------------------------------- // Functions centralizing definitions -// 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/" - PackagePathPrefix = "gno.land/p/" +// ReRealmPath and RePackagePath are the regexes used to identify pkgpaths which are meant to +// be realms with persisted states and pure packages. +var ( + ReRealmPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/[a-z0-9_/]+`) + RePackagePath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/p/[a-z0-9_/]+`) ) // ReGnoRunPath is the path used for realms executed in maketx run. -// These are not considered realms, as an exception to the RealmPathPrefix rule. -var ReGnoRunPath = regexp.MustCompile(`^gno\.land/r/g[a-z0-9]+/run$`) +// These are not considered realms, as an exception to the ReRealmPathPrefix rule. +var ReGnoRunPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/r/g[a-z0-9]+/run$`) // IsRealmPath determines whether the given pkgpath is for a realm, and as such // should persist the global state. func IsRealmPath(pkgPath string) bool { - return strings.HasPrefix(pkgPath, RealmPathPrefix) && - // MsgRun pkgPath aren't realms + return ReRealmPath.MatchString(pkgPath) && !ReGnoRunPath.MatchString(pkgPath) } @@ -33,16 +32,17 @@ func IsRealmPath(pkgPath string) bool { // 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) + return RePackagePath.MatchString(pkgPath) } // 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 - // IBC/ICS and we allow import paths to other chains. It might be good to - // (eventually) follow the same rule as Go, which is: does the first - // element of the import path contain a dot? - return !strings.HasPrefix(s, "gno.land/") + idx := strings.IndexByte(s, '/') + if idx < 0 { + // If no '/' is found, consider the whole string + return strings.IndexByte(s, '.') < 0 + } + return strings.IndexByte(s[:idx+1], '.') < 0 } // ---------------------------------------- diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index 5de37a68405..3ea3d4bc9bd 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -53,6 +53,7 @@ func Context(pkgPath string, send std.Coins) *teststd.TestExecContext { } ctx := stdlibs.ExecContext{ ChainID: "dev", + ChainDomain: "tests.gno.land", Height: DefaultHeight, Timestamp: DefaultTimestamp, OrigCaller: DefaultCaller, diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index a2d82b0bc60..67b492a34b2 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -468,6 +468,26 @@ var nativeFuncs = [...]NativeFunc{ )) }, }, + { + "std", + "GetChainDomain", + []gno.FieldTypeExpr{}, + []gno.FieldTypeExpr{ + {Name: gno.N("r0"), Type: gno.X("string")}, + }, + true, + func(m *gno.Machine) { + r0 := libs_std.GetChainDomain( + m, + ) + + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, { "std", "GetHeight", diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go index 01e763ab82e..a8ef500c346 100644 --- a/gnovm/stdlibs/std/context.go +++ b/gnovm/stdlibs/std/context.go @@ -9,6 +9,7 @@ import ( type ExecContext struct { ChainID string + ChainDomain string Height int64 Timestamp int64 // seconds TimestampNano int64 // nanoseconds, only used for testing. diff --git a/gnovm/stdlibs/std/native.gno b/gnovm/stdlibs/std/native.gno index 5421e231de2..0dcde1148e1 100644 --- a/gnovm/stdlibs/std/native.gno +++ b/gnovm/stdlibs/std/native.gno @@ -10,8 +10,9 @@ func AssertOriginCall() // injected // MsgRun. func IsOriginCall() bool // injected -func GetChainID() string // injected -func GetHeight() int64 // injected +func GetChainID() string // injected +func GetChainDomain() string // injected +func GetHeight() int64 // injected func GetOrigSend() Coins { den, amt := origSend() diff --git a/gnovm/stdlibs/std/native.go b/gnovm/stdlibs/std/native.go index 3fe5fbb9889..fb181d9be31 100644 --- a/gnovm/stdlibs/std/native.go +++ b/gnovm/stdlibs/std/native.go @@ -27,6 +27,10 @@ func GetChainID(m *gno.Machine) string { return GetContext(m).ChainID } +func GetChainDomain(m *gno.Machine) string { + return GetContext(m).ChainDomain +} + func GetHeight(m *gno.Machine) int64 { return GetContext(m).Height } diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 54cfb7846ab..2baba6b5005 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,10 +13,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) -// std/native.gno:44 +// std/native.gno:45 // main() // main/files/std5.gno:10 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index 27545f267ce..4f749c3a6e1 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,10 +23,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) -// std/native.gno:44 +// std/native.gno:45 // fn() // main/files/std8.gno:16 // testutils.WrapCall(inner) diff --git a/gnovm/tests/files/zrealm_natbind1_stdlibs.gno b/gnovm/tests/files/zrealm_natbind1_stdlibs.gno new file mode 100644 index 00000000000..f44b6ab4fcf --- /dev/null +++ b/gnovm/tests/files/zrealm_natbind1_stdlibs.gno @@ -0,0 +1,16 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "std" +) + +func main() { + println(std.GetChainDomain()) +} + +// Output: +// tests.gno.land + +// Realm: +// switchrealm["gno.land/r/test"] From 0a2c447558f8eda925ab3d8f2f6ee4938c7fd003 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:13:39 +0100 Subject: [PATCH 43/86] feat: add r/docs/img_embed (#3241) Embedding an image. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/r/docs/docs.gno | 2 ++ examples/gno.land/r/docs/img_embed/gno.mod | 1 + examples/gno.land/r/docs/img_embed/img_embed.gno | 10 ++++++++++ 3 files changed, 13 insertions(+) create mode 100644 examples/gno.land/r/docs/img_embed/gno.mod create mode 100644 examples/gno.land/r/docs/img_embed/img_embed.gno diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index f796f07bf4a..57d020cd737 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -12,7 +12,9 @@ Explore various examples to learn more about Gno functionality and usage. - [Adder](/r/docs/adder) - An interactive example to update a number with transactions. - [Source](/r/docs/source) - View realm source code. - [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. +- [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. - ... + ## Other resources diff --git a/examples/gno.land/r/docs/img_embed/gno.mod b/examples/gno.land/r/docs/img_embed/gno.mod new file mode 100644 index 00000000000..784914baef5 --- /dev/null +++ b/examples/gno.land/r/docs/img_embed/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/img_embed diff --git a/examples/gno.land/r/docs/img_embed/img_embed.gno b/examples/gno.land/r/docs/img_embed/img_embed.gno new file mode 100644 index 00000000000..b65512d1968 --- /dev/null +++ b/examples/gno.land/r/docs/img_embed/img_embed.gno @@ -0,0 +1,10 @@ +package image_embed + +// Render displays a title and an embedded image from Imgur +func Render(path string) string { + return `# Image Embed Example + +Here’s an example of embedding an image in a Gno realm: + +![Example Image](https://i.imgur.com/So4rBPB.jpeg)` +} From 5f7216d7034fbdf3c9756579b6d2f454afde727a Mon Sep 17 00:00:00 2001 From: Bilog WEB3 <155262265+Bilogweb3@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:58:36 +0100 Subject: [PATCH 44/86] chore: correct typos docs (#3295) I reviewed the entire repository, no more typos found in docs. Hope this helps streamline the project! Best regards, Bilogweb3 --- misc/deployments/test4.gno.land/README.md | 4 ++-- misc/deployments/test5.gno.land/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/deployments/test4.gno.land/README.md b/misc/deployments/test4.gno.land/README.md index 6277ea996ec..7a3a7d06b28 100644 --- a/misc/deployments/test4.gno.land/README.md +++ b/misc/deployments/test4.gno.land/README.md @@ -4,7 +4,7 @@ This deployment folder contains minimal information needed to launch a full test ## `genesis.json` -The initial `genesis.json` validator set is consisted of 3 entities (7 validators in total): +The initial `genesis.json` validator set consisted of 3 entities (7 validators in total): - Gno Core - the gno core team (**4 validators**) - Gno DevX - the gno devX team (**2 validators**) @@ -37,4 +37,4 @@ Some configuration params are required, while others are advised to be set. - `rpc.laddr` - the JSON-RPC listen address, **specific to every node deployment**. - `telemetry.enabled` - flag indicating if telemetry should be turned on. **Advised to be `true`**. - `telemetry.exporter_endpoint` - endpoint for the otel exported. ⚠️ **Required if `telemetry.enabled=true`** ⚠️. -- `telemetry.service_instance_id` - unique ID of the node telemetry instance, **specific to every node deployment**. \ No newline at end of file +- `telemetry.service_instance_id` - unique ID of the node telemetry instance, **specific to every node deployment**. diff --git a/misc/deployments/test5.gno.land/README.md b/misc/deployments/test5.gno.land/README.md index 3dcbf79f2ec..40da91f3b74 100644 --- a/misc/deployments/test5.gno.land/README.md +++ b/misc/deployments/test5.gno.land/README.md @@ -9,7 +9,7 @@ The initial `genesis.json` validator set is consisted of 6 entities (17 validato - Gno Core - the gno core team (**6 validators**) - Gno DevX - the gno devX team (**4 validators**) - AiB - the AiB DevOps team (**3 validators**) -- Onbloc - the [Onbloc](https://onbloc.xyz/) team (**2 validator**) +- Onbloc - the [Onbloc](https://onbloc.xyz/) team (**2 validators**) - Teritori - the [Teritori](https://teritori.com/) team (**1 validator**) - Berty - the [Berty](https://berty.tech/) team (**1 validator**) From 79c9b04b845a6e0e30e657c4d611a53babca3055 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Sat, 7 Dec 2024 12:40:09 -0800 Subject: [PATCH 45/86] fix(contribs): close file in execBalancesExport (#3294) Ensures that the opened file is not leaked and closed after use. Fixes #3032 --- contribs/gnogenesis/internal/balances/balances_export.go | 1 + 1 file changed, 1 insertion(+) diff --git a/contribs/gnogenesis/internal/balances/balances_export.go b/contribs/gnogenesis/internal/balances/balances_export.go index df9d6795805..1970e348b1a 100644 --- a/contribs/gnogenesis/internal/balances/balances_export.go +++ b/contribs/gnogenesis/internal/balances/balances_export.go @@ -60,6 +60,7 @@ func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error { if err != nil { return fmt.Errorf("unable to create output file, %w", err) } + defer outputFile.Close() // Save the balances for _, balance := range state.Balances { From 53cee96236e93e647bc11fedd44729a5618344db Mon Sep 17 00:00:00 2001 From: n0izn0iz Date: Sat, 7 Dec 2024 21:44:26 +0100 Subject: [PATCH 46/86] feat(gnomod)!: forbid require and find dependencies without it (#3123) A step towards the importer package (#2932) and future of `gno.mod` (#2904) - BREAKING CHANGE: remove `require` statement support from `gno.mod` - BREAKING CHANGE: remove `-v` (verbose) and `--remote` flags in `gno mod download` - Don't require version specification in `gno.mod`'s `replace` statements - Use `.gno` files `import` statements to find dependencies - Extract and refacto imports gathering utils in `gnovm/pkg/packages` - Add `gnovm/cmd/gno/internal/pkgdownload.PackageFetcher` interface - Implement `PackageFetcher` using `vm/qfile` queries in `gnovm/cmd/gno/internal/pkgdownload/rpcpackagefetcher` - Rewrite single package download routine in `gnovm/cmd/gno/internal/pkgdownload` - Move and refacto dependencies download routine in `gnovm/cmd/gno/download_deps.go` - Add a `--remote-overrides` flag for `gno mod download` that takes `chain-domain=rpc-url` comma-separated pairs to override endpoints used to fetch packages - Add and use a testing implementation of `PackageFetcher` called `examplesPackageFetcher` that serves package from the `examples` directory for testing purposes (download tests before this PR use the portal loop public endpoint) - Make `ReadMemPackage` and it's dependencies error-out instead of panicking - Create panicking `MemPackage` utils that wrap the erroring ones and use them at existing callsites I decided to do this first to avoid having multiple ways to resolve dependencies lying around in the codebase and causing confusion in subsequent steps
Contributors' checklist... - [x] 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 - [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 - [ ] Provided any useful hints for running manual tests
--------- Signed-off-by: Norman Meier Co-authored-by: Morgan Bazalgette --- contribs/gnodev/pkg/dev/packages.go | 2 +- contribs/gnogenesis/go.mod | 1 - contribs/gnogenesis/go.sum | 2 - contribs/gnomigrate/go.mod | 1 - contribs/gnomigrate/go.sum | 2 - examples/gno.land/p/demo/acl/gno.mod | 7 - examples/gno.land/p/demo/avl/pager/gno.mod | 7 - examples/gno.land/p/demo/avlhelpers/gno.mod | 2 - examples/gno.land/p/demo/blog/gno.mod | 6 - examples/gno.land/p/demo/dao/gno.mod | 2 - examples/gno.land/p/demo/dom/gno.mod | 2 - examples/gno.land/p/demo/fqname/gno.mod | 2 - .../gno.land/p/demo/gnorkle/agent/gno.mod | 5 - .../p/demo/gnorkle/feeds/static/gno.mod | 12 -- .../gno.land/p/demo/gnorkle/gnorkle/gno.mod | 8 - .../p/demo/gnorkle/ingesters/single/gno.mod | 7 - .../gno.land/p/demo/gnorkle/message/gno.mod | 2 - .../p/demo/gnorkle/storage/simple/gno.mod | 8 - examples/gno.land/p/demo/grc/grc1155/gno.mod | 6 - examples/gno.land/p/demo/grc/grc20/gno.mod | 9 - examples/gno.land/p/demo/grc/grc721/gno.mod | 7 - examples/gno.land/p/demo/grc/grc777/gno.mod | 2 - examples/gno.land/p/demo/groups/gno.mod | 2 - examples/gno.land/p/demo/int256/gno.mod | 2 - examples/gno.land/p/demo/json/gno.mod | 2 - .../gno.land/p/demo/math_eval/int32/gno.mod | 2 - examples/gno.land/p/demo/membstore/gno.mod | 8 - examples/gno.land/p/demo/memeland/gno.mod | 9 - examples/gno.land/p/demo/microblog/gno.mod | 5 - .../p/demo/ownable/exts/authorizable/gno.mod | 8 - examples/gno.land/p/demo/ownable/gno.mod | 5 - examples/gno.land/p/demo/pausable/gno.mod | 5 - examples/gno.land/p/demo/seqid/gno.mod | 2 - examples/gno.land/p/demo/simpledao/gno.mod | 11 -- .../p/demo/subscription/lifetime/gno.mod | 7 - .../p/demo/subscription/recurring/gno.mod | 7 - examples/gno.land/p/demo/svg/gno.mod | 2 - examples/gno.land/p/demo/tamagotchi/gno.mod | 2 - examples/gno.land/p/demo/tests/gno.mod | 5 - examples/gno.land/p/demo/todolist/gno.mod | 5 - examples/gno.land/p/demo/uassert/gno.mod | 2 - examples/gno.land/p/demo/urequire/gno.mod | 2 - examples/gno.land/p/demo/watchdog/gno.mod | 2 - examples/gno.land/p/gov/executor/gno.mod | 6 - examples/gno.land/p/moul/helplink/gno.mod | 5 - examples/gno.land/p/moul/mdtable/gno.mod | 2 - .../gno.land/p/moul/printfdebugging/gno.mod | 2 - examples/gno.land/p/moul/realmpath/gno.mod | 5 - examples/gno.land/p/moul/txlink/gno.mod | 2 - examples/gno.land/p/n2p5/haystack/gno.mod | 5 - examples/gno.land/p/n2p5/mgroup/gno.mod | 6 - examples/gno.land/p/nt/poa/gno.mod | 9 - .../gno.land/p/wyhaines/rand/isaac/gno.mod | 6 - .../gno.land/p/wyhaines/rand/isaac64/gno.mod | 6 - .../p/wyhaines/rand/xorshift64star/gno.mod | 5 - .../p/wyhaines/rand/xorshiftr128plus/gno.mod | 5 - examples/gno.land/r/demo/art/gnoface/gno.mod | 6 - .../gno.land/r/demo/art/millipede/gno.mod | 5 - examples/gno.land/r/demo/bar20/gno.mod | 8 - examples/gno.land/r/demo/boards/gno.mod | 6 - examples/gno.land/r/demo/daoweb/gno.mod | 6 - examples/gno.land/r/demo/disperse/gno.mod | 2 - examples/gno.land/r/demo/echo/gno.mod | 2 - examples/gno.land/r/demo/foo1155/gno.mod | 7 - examples/gno.land/r/demo/foo20/gno.mod | 11 -- examples/gno.land/r/demo/foo721/gno.mod | 7 - .../gno.land/r/demo/games/dice_roller/gno.mod | 10 -- .../gno.land/r/demo/games/shifumi/gno.mod | 6 - examples/gno.land/r/demo/grc20factory/gno.mod | 10 -- examples/gno.land/r/demo/grc20reg/gno.mod | 8 - examples/gno.land/r/demo/groups/gno.mod | 5 - examples/gno.land/r/demo/keystore/gno.mod | 7 - examples/gno.land/r/demo/math_eval/gno.mod | 5 - examples/gno.land/r/demo/memeland/gno.mod | 2 - examples/gno.land/r/demo/microblog/gno.mod | 8 - examples/gno.land/r/demo/mirror/gno.mod | 2 - examples/gno.land/r/demo/nft/gno.mod | 5 - examples/gno.land/r/demo/profile/gno.mod | 8 - .../gno.land/r/demo/releases_example/gno.mod | 2 - examples/gno.land/r/demo/tamagotchi/gno.mod | 5 - .../gno.land/r/demo/tests/crossrealm/gno.mod | 5 - .../r/demo/tests/crossrealm_b/gno.mod | 2 - examples/gno.land/r/demo/tests/gno.mod | 5 - examples/gno.land/r/demo/tests_foo/gno.mod | 2 - examples/gno.land/r/demo/todolist/gno.mod | 8 - examples/gno.land/r/demo/types/gno.mod | 2 - examples/gno.land/r/demo/ui/gno.mod | 5 - examples/gno.land/r/demo/userbook/gno.mod | 7 - examples/gno.land/r/demo/users/gno.mod | 8 - examples/gno.land/r/demo/wugnot/gno.mod | 8 - examples/gno.land/r/docs/adder/gno.mod | 2 - examples/gno.land/r/docs/avl_pager/gno.mod | 5 - examples/gno.land/r/gnoland/blog/gno.mod | 7 - examples/gno.land/r/gnoland/events/gno.mod | 8 - examples/gno.land/r/gnoland/faucet/gno.mod | 6 - examples/gno.land/r/gnoland/ghverify/gno.mod | 8 - examples/gno.land/r/gnoland/home/gno.mod | 9 - examples/gno.land/r/gnoland/monit/gno.mod | 7 - examples/gno.land/r/gnoland/pages/gno.mod | 5 - .../gno.land/r/gnoland/valopers/v2/gno.mod | 11 -- examples/gno.land/r/gov/dao/bridge/gno.mod | 10 -- examples/gno.land/r/gov/dao/v2/gno.mod | 11 -- examples/gno.land/r/leon/hof/gno.mod | 14 -- examples/gno.land/r/leon/home/gno.mod | 9 - examples/gno.land/r/morgan/guestbook/gno.mod | 6 - examples/gno.land/r/morgan/home/gno.mod | 2 - examples/gno.land/r/moul/home/gno.mod | 5 - examples/gno.land/r/moul/present/gno.mod | 5 - examples/gno.land/r/n2p5/config/gno.mod | 5 - examples/gno.land/r/n2p5/haystack/gno.mod | 7 - examples/gno.land/r/n2p5/home/gno.mod | 6 - examples/gno.land/r/stefann/home/gno.mod | 8 - examples/gno.land/r/stefann/registry/gno.mod | 2 - examples/gno.land/r/sys/params/gno.mod | 5 - examples/gno.land/r/sys/users/gno.mod | 5 - examples/gno.land/r/sys/validators/v2/gno.mod | 12 -- examples/gno.land/r/x/manfred_outfmt/gno.mod | 2 - gno.land/pkg/gnoland/genesis.go | 2 +- .../pkg/integration/testing_integration.go | 20 ++- gno.land/pkg/keyscli/addpkg.go | 2 +- gno.land/pkg/keyscli/run.go | 2 +- gno.land/pkg/sdk/vm/keeper.go | 2 +- gno.land/pkg/sdk/vm/msgs.go | 4 +- gnovm/cmd/gno/download_deps.go | 86 +++++++++ gnovm/cmd/gno/download_deps_test.go | 152 ++++++++++++++++ .../examplespkgfetcher/examplespkgfetcher.go | 52 ++++++ .../gno/internal/pkgdownload/pkgdownload.go | 30 ++++ .../gno/internal/pkgdownload/pkgfetcher.go | 7 + .../rpcpkgfetcher/rpcpkgfetcher.go | 89 ++++++++++ .../rpcpkgfetcher/rpcpkgfetcher_test.go | 53 ++++++ gnovm/cmd/gno/lint.go | 2 +- gnovm/cmd/gno/main_test.go | 11 +- gnovm/cmd/gno/mod.go | 164 +++++------------ gnovm/cmd/gno/mod_test.go | 158 +---------------- gnovm/cmd/gno/test.go | 2 +- gnovm/pkg/doc/dirs.go | 58 +++++- gnovm/pkg/doc/dirs_test.go | 13 +- gnovm/pkg/doc/testdata/dirsmod/a.gno | 9 + gnovm/pkg/doc/testdata/dirsmod/gno.mod | 6 +- gnovm/pkg/gnolang/files_test.go | 2 +- gnovm/pkg/gnolang/nodes.go | 59 +++++-- gnovm/pkg/gnomod/fetch.go | 30 ---- gnovm/pkg/gnomod/file.go | 129 +------------- gnovm/pkg/gnomod/file_test.go | 142 --------------- gnovm/pkg/gnomod/gnomod.go | 138 +-------------- gnovm/pkg/gnomod/parse.go | 22 +-- gnovm/pkg/gnomod/parse_test.go | 21 ++- gnovm/pkg/gnomod/pkg.go | 52 ++++-- gnovm/pkg/gnomod/pkg_test.go | 142 ++------------- gnovm/pkg/gnomod/preprocess.go | 37 +--- gnovm/pkg/gnomod/preprocess_test.go | 127 ------------- gnovm/pkg/gnomod/read.go | 60 ------- gnovm/pkg/gnomod/read_test.go | 167 ------------------ gnovm/pkg/packages/doc.go | 2 + gnovm/pkg/packages/imports.go | 72 ++++++++ gnovm/pkg/packages/imports_test.go | 127 +++++++++++++ gnovm/pkg/test/imports.go | 28 ++- .../integ/invalid_module_version1/gno.mod | 5 - .../integ/invalid_module_version2/gno.mod | 5 - gnovm/tests/integ/replace_with_dir/gno.mod | 4 - .../integ/replace_with_invalid_module/gno.mod | 6 +- .../replace_with_invalid_module/main.gno | 7 + gnovm/tests/integ/replace_with_module/gno.mod | 6 +- .../tests/integ/replace_with_module/main.gno | 7 + .../integ/require_invalid_module/gno.mod | 6 +- .../integ/require_invalid_module/main.gno | 7 + .../tests/integ/require_remote_module/gno.mod | 4 - gnovm/tests/integ/require_std_lib/gno.mod | 1 + gnovm/tests/integ/require_std_lib/main.gno | 7 + gnovm/tests/integ/valid2/gno.mod | 2 - misc/loop/go.mod | 1 - 171 files changed, 988 insertions(+), 1999 deletions(-) create mode 100644 gnovm/cmd/gno/download_deps.go create mode 100644 gnovm/cmd/gno/download_deps_test.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go create mode 100644 gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go create mode 100644 gnovm/pkg/doc/testdata/dirsmod/a.gno delete mode 100644 gnovm/pkg/gnomod/fetch.go delete mode 100644 gnovm/pkg/gnomod/file_test.go create mode 100644 gnovm/pkg/packages/doc.go create mode 100644 gnovm/pkg/packages/imports.go create mode 100644 gnovm/pkg/packages/imports_test.go delete mode 100644 gnovm/tests/integ/invalid_module_version1/gno.mod delete mode 100644 gnovm/tests/integ/invalid_module_version2/gno.mod create mode 100644 gnovm/tests/integ/replace_with_invalid_module/main.gno create mode 100644 gnovm/tests/integ/replace_with_module/main.gno create mode 100644 gnovm/tests/integ/require_invalid_module/main.gno create mode 100644 gnovm/tests/integ/require_std_lib/gno.mod create mode 100644 gnovm/tests/integ/require_std_lib/main.gno diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index cccbf316525..62c1907b8c9 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -138,7 +138,7 @@ func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetada } // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(modPkg.Dir, modPkg.Name) + memPkg := gno.MustReadMemPackage(modPkg.Dir, modPkg.Name) if err := memPkg.Validate(); err != nil { return nil, fmt.Errorf("invalid package: %w", err) } diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index 393fed0725d..b777cc6e5eb 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -53,7 +53,6 @@ require ( golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index f3161e47bad..3c6127ac216 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -195,8 +195,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index c492ae7c818..a81c2de4ba0 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -48,7 +48,6 @@ require ( golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index f3161e47bad..3c6127ac216 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -195,8 +195,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/gno.land/p/demo/acl/gno.mod b/examples/gno.land/p/demo/acl/gno.mod index 15d9f078048..04fbf9043c4 100644 --- a/examples/gno.land/p/demo/acl/gno.mod +++ b/examples/gno.land/p/demo/acl/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/acl - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/avl/pager/gno.mod b/examples/gno.land/p/demo/avl/pager/gno.mod index 59c961d73f2..020b809b208 100644 --- a/examples/gno.land/p/demo/avl/pager/gno.mod +++ b/examples/gno.land/p/demo/avl/pager/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/avl/pager - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/avlhelpers/gno.mod b/examples/gno.land/p/demo/avlhelpers/gno.mod index 559f60975cf..5adffd13a43 100644 --- a/examples/gno.land/p/demo/avlhelpers/gno.mod +++ b/examples/gno.land/p/demo/avlhelpers/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/avlhelpers - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/demo/blog/gno.mod b/examples/gno.land/p/demo/blog/gno.mod index 65f58e7a0f6..e4e3def299b 100644 --- a/examples/gno.land/p/demo/blog/gno.mod +++ b/examples/gno.land/p/demo/blog/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/blog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/dao/gno.mod b/examples/gno.land/p/demo/dao/gno.mod index ecbab2f7692..fbb23299116 100644 --- a/examples/gno.land/p/demo/dao/gno.mod +++ b/examples/gno.land/p/demo/dao/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/dao - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/dom/gno.mod b/examples/gno.land/p/demo/dom/gno.mod index 83ca827cf66..bd8bba14d06 100644 --- a/examples/gno.land/p/demo/dom/gno.mod +++ b/examples/gno.land/p/demo/dom/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/dom - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/demo/fqname/gno.mod b/examples/gno.land/p/demo/fqname/gno.mod index 1282e262303..afee55e0b7b 100644 --- a/examples/gno.land/p/demo/fqname/gno.mod +++ b/examples/gno.land/p/demo/fqname/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/fqname - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/gnorkle/agent/gno.mod b/examples/gno.land/p/demo/gnorkle/agent/gno.mod index 093ca9cf38e..e784354c35e 100644 --- a/examples/gno.land/p/demo/gnorkle/agent/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/agent/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/gnorkle/agent - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod b/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod index c651c62cb1b..05363a3cd06 100644 --- a/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod @@ -1,13 +1 @@ module gno.land/p/demo/gnorkle/feeds/static - -require ( - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/ingesters/single v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest - gno.land/p/demo/gnorkle/storage/simple v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod b/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod index 88fb202863f..ce2c2c3706d 100644 --- a/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/gnorkle/gnorkle - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/gnorkle/agent v0.0.0-latest - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod b/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod index 71120966a0c..8cf5a9a30d8 100644 --- a/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/gnorkle/ingesters/single - -require ( - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/storage/simple v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/message/gno.mod b/examples/gno.land/p/demo/gnorkle/message/gno.mod index 4baad40ef86..5544d0eb873 100644 --- a/examples/gno.land/p/demo/gnorkle/message/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/message/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/gnorkle/message - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod b/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod index cd673a8771c..b842e2b514c 100644 --- a/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/gnorkle/storage/simple - -require ( - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/storage v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc1155/gno.mod b/examples/gno.land/p/demo/grc/grc1155/gno.mod index d6db0700146..1c3ec6360eb 100644 --- a/examples/gno.land/p/demo/grc/grc1155/gno.mod +++ b/examples/gno.land/p/demo/grc/grc1155/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/grc/grc1155 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc20/gno.mod b/examples/gno.land/p/demo/grc/grc20/gno.mod index 91b430d3d2f..37377b32e73 100644 --- a/examples/gno.land/p/demo/grc/grc20/gno.mod +++ b/examples/gno.land/p/demo/grc/grc20/gno.mod @@ -1,10 +1 @@ module gno.land/p/demo/grc/grc20 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/exts v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc721/gno.mod b/examples/gno.land/p/demo/grc/grc721/gno.mod index 9e1d6f56ffc..f27caee5282 100644 --- a/examples/gno.land/p/demo/grc/grc721/gno.mod +++ b/examples/gno.land/p/demo/grc/grc721/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/grc/grc721 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc777/gno.mod b/examples/gno.land/p/demo/grc/grc777/gno.mod index 9fbf2f2b7cd..da5c762b2ec 100644 --- a/examples/gno.land/p/demo/grc/grc777/gno.mod +++ b/examples/gno.land/p/demo/grc/grc777/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/grc/grc777 - -require gno.land/p/demo/grc/exts v0.0.0-latest diff --git a/examples/gno.land/p/demo/groups/gno.mod b/examples/gno.land/p/demo/groups/gno.mod index cf33d0ce74b..d33df3866fa 100644 --- a/examples/gno.land/p/demo/groups/gno.mod +++ b/examples/gno.land/p/demo/groups/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/groups - -require gno.land/p/demo/rat v0.0.0-latest diff --git a/examples/gno.land/p/demo/int256/gno.mod b/examples/gno.land/p/demo/int256/gno.mod index ef906c83c93..33fb0bc4e72 100644 --- a/examples/gno.land/p/demo/int256/gno.mod +++ b/examples/gno.land/p/demo/int256/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/int256 - -require gno.land/p/demo/uint256 v0.0.0-latest diff --git a/examples/gno.land/p/demo/json/gno.mod b/examples/gno.land/p/demo/json/gno.mod index ef794458c56..831fa56c0f9 100644 --- a/examples/gno.land/p/demo/json/gno.mod +++ b/examples/gno.land/p/demo/json/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/json - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/math_eval/int32/gno.mod b/examples/gno.land/p/demo/math_eval/int32/gno.mod index de57497a699..c4e4bc8f454 100644 --- a/examples/gno.land/p/demo/math_eval/int32/gno.mod +++ b/examples/gno.land/p/demo/math_eval/int32/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/math_eval/int32 - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/membstore/gno.mod b/examples/gno.land/p/demo/membstore/gno.mod index da22a8dcae4..007e7a5d883 100644 --- a/examples/gno.land/p/demo/membstore/gno.mod +++ b/examples/gno.land/p/demo/membstore/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/membstore - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/memeland/gno.mod b/examples/gno.land/p/demo/memeland/gno.mod index 66f22d1ccee..06cc8fbf487 100644 --- a/examples/gno.land/p/demo/memeland/gno.mod +++ b/examples/gno.land/p/demo/memeland/gno.mod @@ -1,10 +1 @@ module gno.land/p/demo/memeland - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/microblog/gno.mod b/examples/gno.land/p/demo/microblog/gno.mod index 9bbcfa19e31..a285ef5f903 100644 --- a/examples/gno.land/p/demo/microblog/gno.mod +++ b/examples/gno.land/p/demo/microblog/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/microblog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod index f36823f3f71..0e8be79f130 100644 --- a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/ownable/exts/authorizable - -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 - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/ownable/gno.mod b/examples/gno.land/p/demo/ownable/gno.mod index 00f7812f6f5..9a9abb1e661 100644 --- a/examples/gno.land/p/demo/ownable/gno.mod +++ b/examples/gno.land/p/demo/ownable/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/ownable - -require ( - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/pausable/gno.mod b/examples/gno.land/p/demo/pausable/gno.mod index 156875f7d85..a741342eb84 100644 --- a/examples/gno.land/p/demo/pausable/gno.mod +++ b/examples/gno.land/p/demo/pausable/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/pausable - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/seqid/gno.mod b/examples/gno.land/p/demo/seqid/gno.mod index d1390012c3c..63e6a1fb551 100644 --- a/examples/gno.land/p/demo/seqid/gno.mod +++ b/examples/gno.land/p/demo/seqid/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/seqid - -require gno.land/p/demo/cford32 v0.0.0-latest diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod index f6f14f379ec..51de621cbec 100644 --- a/examples/gno.land/p/demo/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -1,12 +1 @@ module gno.land/p/demo/simpledao - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/membstore v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/subscription/lifetime/gno.mod b/examples/gno.land/p/demo/subscription/lifetime/gno.mod index 0084aa714c5..59b6c1cf001 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/gno.mod +++ b/examples/gno.land/p/demo/subscription/lifetime/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/subscription/lifetime - -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 - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/subscription/recurring/gno.mod b/examples/gno.land/p/demo/subscription/recurring/gno.mod index d3cf8a044f8..356402978b5 100644 --- a/examples/gno.land/p/demo/subscription/recurring/gno.mod +++ b/examples/gno.land/p/demo/subscription/recurring/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/subscription/recurring - -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 - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/svg/gno.mod b/examples/gno.land/p/demo/svg/gno.mod index 0af7ba0636d..b9dd7f47434 100644 --- a/examples/gno.land/p/demo/svg/gno.mod +++ b/examples/gno.land/p/demo/svg/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/svg - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/tamagotchi/gno.mod b/examples/gno.land/p/demo/tamagotchi/gno.mod index 58441284a6b..a9c6026629e 100644 --- a/examples/gno.land/p/demo/tamagotchi/gno.mod +++ b/examples/gno.land/p/demo/tamagotchi/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/tamagotchi - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/tests/gno.mod b/examples/gno.land/p/demo/tests/gno.mod index 8a19acdbb18..a342a726f61 100644 --- a/examples/gno.land/p/demo/tests/gno.mod +++ b/examples/gno.land/p/demo/tests/gno.mod @@ -1,6 +1 @@ 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 -) diff --git a/examples/gno.land/p/demo/todolist/gno.mod b/examples/gno.land/p/demo/todolist/gno.mod index bbccf357e3b..46d21bf0bc0 100644 --- a/examples/gno.land/p/demo/todolist/gno.mod +++ b/examples/gno.land/p/demo/todolist/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/todolist - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/uassert/gno.mod b/examples/gno.land/p/demo/uassert/gno.mod index f22276564bf..a70e7db825d 100644 --- a/examples/gno.land/p/demo/uassert/gno.mod +++ b/examples/gno.land/p/demo/uassert/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/uassert - -require gno.land/p/demo/diff v0.0.0-latest diff --git a/examples/gno.land/p/demo/urequire/gno.mod b/examples/gno.land/p/demo/urequire/gno.mod index 9689a2222ac..e5336b2c80d 100644 --- a/examples/gno.land/p/demo/urequire/gno.mod +++ b/examples/gno.land/p/demo/urequire/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/urequire - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/watchdog/gno.mod b/examples/gno.land/p/demo/watchdog/gno.mod index 29005441401..96fba14451b 100644 --- a/examples/gno.land/p/demo/watchdog/gno.mod +++ b/examples/gno.land/p/demo/watchdog/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/watchdog - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/gov/executor/gno.mod b/examples/gno.land/p/gov/executor/gno.mod index 99f2ab3610b..5dbb6f7f85e 100644 --- a/examples/gno.land/p/gov/executor/gno.mod +++ b/examples/gno.land/p/gov/executor/gno.mod @@ -1,7 +1 @@ module gno.land/p/gov/executor - -require ( - gno.land/p/demo/context v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/moul/helplink/gno.mod b/examples/gno.land/p/moul/helplink/gno.mod index 1b106749260..cb070b79d6a 100644 --- a/examples/gno.land/p/moul/helplink/gno.mod +++ b/examples/gno.land/p/moul/helplink/gno.mod @@ -1,6 +1 @@ module gno.land/p/moul/helplink - -require ( - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/moul/txlink v0.0.0-latest -) diff --git a/examples/gno.land/p/moul/mdtable/gno.mod b/examples/gno.land/p/moul/mdtable/gno.mod index 0cea0458895..079c935a874 100644 --- a/examples/gno.land/p/moul/mdtable/gno.mod +++ b/examples/gno.land/p/moul/mdtable/gno.mod @@ -1,3 +1 @@ module gno.land/p/moul/mdtable - -require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/p/moul/printfdebugging/gno.mod b/examples/gno.land/p/moul/printfdebugging/gno.mod index 2cf6aa09e61..4b8d0f3256c 100644 --- a/examples/gno.land/p/moul/printfdebugging/gno.mod +++ b/examples/gno.land/p/moul/printfdebugging/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/printfdebugging - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/moul/realmpath/gno.mod b/examples/gno.land/p/moul/realmpath/gno.mod index e391b76390f..0c012a0c3ae 100644 --- a/examples/gno.land/p/moul/realmpath/gno.mod +++ b/examples/gno.land/p/moul/realmpath/gno.mod @@ -1,6 +1 @@ module gno.land/p/moul/realmpath - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/moul/txlink/gno.mod b/examples/gno.land/p/moul/txlink/gno.mod index 6110464316f..ed16b8b74fd 100644 --- a/examples/gno.land/p/moul/txlink/gno.mod +++ b/examples/gno.land/p/moul/txlink/gno.mod @@ -1,3 +1 @@ module gno.land/p/moul/txlink - -require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/p/n2p5/haystack/gno.mod b/examples/gno.land/p/n2p5/haystack/gno.mod index ebd0d07a987..987d62d4565 100644 --- a/examples/gno.land/p/n2p5/haystack/gno.mod +++ b/examples/gno.land/p/n2p5/haystack/gno.mod @@ -1,6 +1 @@ module gno.land/p/n2p5/haystack - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/n2p5/haystack/needle v0.0.0-latest -) diff --git a/examples/gno.land/p/n2p5/mgroup/gno.mod b/examples/gno.land/p/n2p5/mgroup/gno.mod index 95fdbe2f195..132913d9c3d 100644 --- a/examples/gno.land/p/n2p5/mgroup/gno.mod +++ b/examples/gno.land/p/n2p5/mgroup/gno.mod @@ -1,7 +1 @@ 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/nt/poa/gno.mod b/examples/gno.land/p/nt/poa/gno.mod index 5c1b75eb05a..965eeb56aed 100644 --- a/examples/gno.land/p/nt/poa/gno.mod +++ b/examples/gno.land/p/nt/poa/gno.mod @@ -1,10 +1 @@ module gno.land/p/nt/poa - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/isaac/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod index 0cca6aa5174..538f52e6e7e 100644 --- a/examples/gno.land/p/wyhaines/rand/isaac/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod @@ -1,7 +1 @@ module gno.land/p/wyhaines/rand/isaac - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod index dbc8713094e..79772dfe8d8 100644 --- a/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod @@ -1,7 +1 @@ module gno.land/p/wyhaines/rand/isaac64 - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod index bc40b1bc71b..7918a7e7d2d 100644 --- a/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod @@ -1,6 +1 @@ module gno.land/p/wyhaines/rand/xorshift64star - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod index c778fc72550..9f3be9ea8df 100644 --- a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod @@ -1,6 +1 @@ module gno.land/p/wyhaines/rand/xorshiftr128plus - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/art/gnoface/gno.mod b/examples/gno.land/r/demo/art/gnoface/gno.mod index 072c98f3bd6..9465af6216a 100644 --- a/examples/gno.land/r/demo/art/gnoface/gno.mod +++ b/examples/gno.land/r/demo/art/gnoface/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/art/gnoface - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/art/millipede/gno.mod b/examples/gno.land/r/demo/art/millipede/gno.mod index 7cd604206fa..3e5177efdcd 100644 --- a/examples/gno.land/r/demo/art/millipede/gno.mod +++ b/examples/gno.land/r/demo/art/millipede/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/art/millipede - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/bar20/gno.mod b/examples/gno.land/r/demo/bar20/gno.mod index 9fb0f083e1b..e8ede1ea44f 100644 --- a/examples/gno.land/r/demo/bar20/gno.mod +++ b/examples/gno.land/r/demo/bar20/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/bar20 - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/boards/gno.mod b/examples/gno.land/r/demo/boards/gno.mod index 24fea7ce853..dffb96740fc 100644 --- a/examples/gno.land/r/demo/boards/gno.mod +++ b/examples/gno.land/r/demo/boards/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/boards - -require ( - gno.land/p/demo/avl 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/demo/daoweb/gno.mod b/examples/gno.land/r/demo/daoweb/gno.mod index bc781b311dc..74ae149cdb6 100644 --- a/examples/gno.land/r/demo/daoweb/gno.mod +++ b/examples/gno.land/r/demo/daoweb/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/daoweb - -require ( - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/json v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/disperse/gno.mod b/examples/gno.land/r/demo/disperse/gno.mod index 0ba9c88810a..06e81884dfa 100644 --- a/examples/gno.land/r/demo/disperse/gno.mod +++ b/examples/gno.land/r/demo/disperse/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/disperse - -require gno.land/r/demo/grc20factory v0.0.0-latest diff --git a/examples/gno.land/r/demo/echo/gno.mod b/examples/gno.land/r/demo/echo/gno.mod index 4ca5ccab6e0..f07d78943d1 100644 --- a/examples/gno.land/r/demo/echo/gno.mod +++ b/examples/gno.land/r/demo/echo/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/echo - -require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/r/demo/foo1155/gno.mod b/examples/gno.land/r/demo/foo1155/gno.mod index 0a405c5b4a2..eae12bcd1e3 100644 --- a/examples/gno.land/r/demo/foo1155/gno.mod +++ b/examples/gno.land/r/demo/foo1155/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/foo1155 - -require ( - gno.land/p/demo/grc/grc1155 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/foo20/gno.mod b/examples/gno.land/r/demo/foo20/gno.mod index 64b8f90a27d..79dea556e78 100644 --- a/examples/gno.land/r/demo/foo20/gno.mod +++ b/examples/gno.land/r/demo/foo20/gno.mod @@ -1,12 +1 @@ module gno.land/r/demo/foo20 - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/foo721/gno.mod b/examples/gno.land/r/demo/foo721/gno.mod index e013677379d..4779f2fc467 100644 --- a/examples/gno.land/r/demo/foo721/gno.mod +++ b/examples/gno.land/r/demo/foo721/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/foo721 - -require ( - gno.land/p/demo/grc/grc721 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/games/dice_roller/gno.mod b/examples/gno.land/r/demo/games/dice_roller/gno.mod index 75c6473fa3e..3aae9cbe791 100644 --- a/examples/gno.land/r/demo/games/dice_roller/gno.mod +++ b/examples/gno.land/r/demo/games/dice_roller/gno.mod @@ -1,11 +1 @@ module gno.land/r/demo/games/dice_roller - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/games/shifumi/gno.mod b/examples/gno.land/r/demo/games/shifumi/gno.mod index 7a4fc173d3d..e6a428090a9 100644 --- a/examples/gno.land/r/demo/games/shifumi/gno.mod +++ b/examples/gno.land/r/demo/games/shifumi/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/games/shifumi - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index a2d2a55fdf0..f89ee5872a5 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -1,11 +1 @@ module gno.land/r/demo/grc20factory - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/grc20reg/gno.mod b/examples/gno.land/r/demo/grc20reg/gno.mod index f02ee09c35a..c5065c60064 100644 --- a/examples/gno.land/r/demo/grc20reg/gno.mod +++ b/examples/gno.land/r/demo/grc20reg/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/grc20reg - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/fqname v0.0.0-latest - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/groups/gno.mod b/examples/gno.land/r/demo/groups/gno.mod index fc6756e13e2..6f715471ced 100644 --- a/examples/gno.land/r/demo/groups/gno.mod +++ b/examples/gno.land/r/demo/groups/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/groups - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/keystore/gno.mod b/examples/gno.land/r/demo/keystore/gno.mod index 49b0f3494a4..cd07d24adf6 100644 --- a/examples/gno.land/r/demo/keystore/gno.mod +++ b/examples/gno.land/r/demo/keystore/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/keystore - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/math_eval/gno.mod b/examples/gno.land/r/demo/math_eval/gno.mod index 0e3fcfe6e9b..c797becfa7d 100644 --- a/examples/gno.land/r/demo/math_eval/gno.mod +++ b/examples/gno.land/r/demo/math_eval/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/math_eval - -require ( - gno.land/p/demo/math_eval/int32 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/memeland/gno.mod b/examples/gno.land/r/demo/memeland/gno.mod index 5c73379519b..0ccb353659f 100644 --- a/examples/gno.land/r/demo/memeland/gno.mod +++ b/examples/gno.land/r/demo/memeland/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/memeland - -require gno.land/p/demo/memeland v0.0.0-latest diff --git a/examples/gno.land/r/demo/microblog/gno.mod b/examples/gno.land/r/demo/microblog/gno.mod index 26349e481d4..a622200b76d 100644 --- a/examples/gno.land/r/demo/microblog/gno.mod +++ b/examples/gno.land/r/demo/microblog/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/microblog - -require ( - gno.land/p/demo/microblog v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/mirror/gno.mod b/examples/gno.land/r/demo/mirror/gno.mod index 2bf27fd6916..cb53585644a 100644 --- a/examples/gno.land/r/demo/mirror/gno.mod +++ b/examples/gno.land/r/demo/mirror/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/mirror - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/nft/gno.mod b/examples/gno.land/r/demo/nft/gno.mod index 89e0055be51..ad760d186ab 100644 --- a/examples/gno.land/r/demo/nft/gno.mod +++ b/examples/gno.land/r/demo/nft/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/nft - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/grc721 v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/profile/gno.mod b/examples/gno.land/r/demo/profile/gno.mod index e7feac5d680..3e875672a99 100644 --- a/examples/gno.land/r/demo/profile/gno.mod +++ b/examples/gno.land/r/demo/profile/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/profile - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/releases_example/gno.mod b/examples/gno.land/r/demo/releases_example/gno.mod index 22f640fe797..0dc5d6561dc 100644 --- a/examples/gno.land/r/demo/releases_example/gno.mod +++ b/examples/gno.land/r/demo/releases_example/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/releases_example - -require gno.land/p/demo/releases v0.0.0-latest diff --git a/examples/gno.land/r/demo/tamagotchi/gno.mod b/examples/gno.land/r/demo/tamagotchi/gno.mod index b7a2deea2c2..bccf4841666 100644 --- a/examples/gno.land/r/demo/tamagotchi/gno.mod +++ b/examples/gno.land/r/demo/tamagotchi/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tamagotchi - -require ( - gno.land/p/demo/tamagotchi v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests/crossrealm/gno.mod b/examples/gno.land/r/demo/tests/crossrealm/gno.mod index 71a89ec2ec5..2f7f217d288 100644 --- a/examples/gno.land/r/demo/tests/crossrealm/gno.mod +++ b/examples/gno.land/r/demo/tests/crossrealm/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tests/crossrealm - -require ( - gno.land/p/demo/tests/p_crossrealm v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod index 74548712caa..236010c21b3 100644 --- a/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod +++ b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/tests/crossrealm_b - -require gno.land/r/demo/tests/crossrealm v0.0.0-latest diff --git a/examples/gno.land/r/demo/tests/gno.mod b/examples/gno.land/r/demo/tests/gno.mod index c51571e7d04..f04aa5cf7bd 100644 --- a/examples/gno.land/r/demo/tests/gno.mod +++ b/examples/gno.land/r/demo/tests/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tests - -require ( - gno.land/p/demo/nestedpkg v0.0.0-latest - gno.land/r/demo/tests/subtests v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests_foo/gno.mod b/examples/gno.land/r/demo/tests_foo/gno.mod index 226271ae4b0..e5a00113181 100644 --- a/examples/gno.land/r/demo/tests_foo/gno.mod +++ b/examples/gno.land/r/demo/tests_foo/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/tests_foo - -require gno.land/r/demo/tests v0.0.0-latest diff --git a/examples/gno.land/r/demo/todolist/gno.mod b/examples/gno.land/r/demo/todolist/gno.mod index 36909859a6f..acd336f1724 100644 --- a/examples/gno.land/r/demo/todolist/gno.mod +++ b/examples/gno.land/r/demo/todolist/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/todolist - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/todolist v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/types/gno.mod b/examples/gno.land/r/demo/types/gno.mod index 0e86e5d5676..c24f7ddbc93 100644 --- a/examples/gno.land/r/demo/types/gno.mod +++ b/examples/gno.land/r/demo/types/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/types - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/ui/gno.mod b/examples/gno.land/r/demo/ui/gno.mod index 0ef5d9dd40e..591b0b93190 100644 --- a/examples/gno.land/r/demo/ui/gno.mod +++ b/examples/gno.land/r/demo/ui/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/ui - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ui v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/userbook/gno.mod b/examples/gno.land/r/demo/userbook/gno.mod index 213586d12ee..bb709a39ed7 100644 --- a/examples/gno.land/r/demo/userbook/gno.mod +++ b/examples/gno.land/r/demo/userbook/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/userbook - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/users/gno.mod b/examples/gno.land/r/demo/users/gno.mod index f2f88a0f993..4d7fd15d1cd 100644 --- a/examples/gno.land/r/demo/users/gno.mod +++ b/examples/gno.land/r/demo/users/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/users - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/avl/pager v0.0.0-latest - gno.land/p/demo/avlhelpers v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/wugnot/gno.mod b/examples/gno.land/r/demo/wugnot/gno.mod index c7081ce6963..12b6baa7ae2 100644 --- a/examples/gno.land/r/demo/wugnot/gno.mod +++ b/examples/gno.land/r/demo/wugnot/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/wugnot - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/docs/adder/gno.mod b/examples/gno.land/r/docs/adder/gno.mod index f8bbf9d6fe8..f4958c6494d 100644 --- a/examples/gno.land/r/docs/adder/gno.mod +++ b/examples/gno.land/r/docs/adder/gno.mod @@ -1,3 +1 @@ module gno.land/r/docs/adder - -require gno.land/p/moul/txlink v0.0.0-latest diff --git a/examples/gno.land/r/docs/avl_pager/gno.mod b/examples/gno.land/r/docs/avl_pager/gno.mod index 0d05b24bcd0..bc7214f7bc1 100644 --- a/examples/gno.land/r/docs/avl_pager/gno.mod +++ b/examples/gno.land/r/docs/avl_pager/gno.mod @@ -1,6 +1 @@ module gno.land/r/docs/avl_pager - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/avl/pager v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 8a4c5851b4c..b510867c485 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -1,8 +1 @@ module gno.land/r/gnoland/blog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/events/gno.mod b/examples/gno.land/r/gnoland/events/gno.mod index bd3e4652b04..50aa3d8fc27 100644 --- a/examples/gno.land/r/gnoland/events/gno.mod +++ b/examples/gno.land/r/gnoland/events/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoland/events - -require ( - gno.land/p/demo/ownable/exts/authorizable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/faucet/gno.mod b/examples/gno.land/r/gnoland/faucet/gno.mod index 693b0e795cf..6193d111e4f 100644 --- a/examples/gno.land/r/gnoland/faucet/gno.mod +++ b/examples/gno.land/r/gnoland/faucet/gno.mod @@ -1,7 +1 @@ module gno.land/r/gnoland/faucet - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/ghverify/gno.mod b/examples/gno.land/r/gnoland/ghverify/gno.mod index 386bd9293d2..8ffdec663f7 100644 --- a/examples/gno.land/r/gnoland/ghverify/gno.mod +++ b/examples/gno.land/r/gnoland/ghverify/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoland/ghverify - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/gnorkle/feeds/static v0.0.0-latest - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index 52d01c6d38c..09eb0eb19e1 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -1,10 +1 @@ module gno.land/r/gnoland/home - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/ui v0.0.0-latest - gno.land/r/gnoland/blog v0.0.0-latest - gno.land/r/gnoland/events v0.0.0-latest - gno.land/r/leon/hof v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/monit/gno.mod b/examples/gno.land/r/gnoland/monit/gno.mod index e67fdaa7d71..6086a3fa21f 100644 --- a/examples/gno.land/r/gnoland/monit/gno.mod +++ b/examples/gno.land/r/gnoland/monit/gno.mod @@ -1,8 +1 @@ module gno.land/r/gnoland/monit - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/watchdog v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/pages/gno.mod b/examples/gno.land/r/gnoland/pages/gno.mod index 31e9ad2c85b..e041fd948bc 100644 --- a/examples/gno.land/r/gnoland/pages/gno.mod +++ b/examples/gno.land/r/gnoland/pages/gno.mod @@ -1,6 +1 @@ module gno.land/r/gnoland/pages - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod index 099a8406db4..064fe6d811e 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -1,12 +1 @@ module gno.land/r/gnoland/valopers/v2 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest - gno.land/r/sys/validators/v2 v0.0.0-latest -) diff --git a/examples/gno.land/r/gov/dao/bridge/gno.mod b/examples/gno.land/r/gov/dao/bridge/gno.mod index 3382557573a..9f472eaa464 100644 --- a/examples/gno.land/r/gov/dao/bridge/gno.mod +++ b/examples/gno.land/r/gov/dao/bridge/gno.mod @@ -1,11 +1 @@ module gno.land/r/gov/dao/bridge - -require ( - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/membstore v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/gov/dao/v2 v0.0.0-latest -) diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod index 4da6e0a2484..4daf8c600a1 100644 --- a/examples/gno.land/r/gov/dao/v2/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -1,12 +1 @@ module gno.land/r/gov/dao/v2 - -require ( - gno.land/p/demo/combinederr v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/membstore v0.0.0-latest - 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/leon/hof/gno.mod b/examples/gno.land/r/leon/hof/gno.mod index feb31992513..f4720eb2b5a 100644 --- a/examples/gno.land/r/leon/hof/gno.mod +++ b/examples/gno.land/r/leon/hof/gno.mod @@ -1,15 +1 @@ module gno.land/r/leon/hof - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/avl/pager v0.0.0-latest - gno.land/p/demo/fqname v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/pausable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/moul/txlink v0.0.0-latest -) diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod index e7ffc49a37f..56fea265e29 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -1,10 +1 @@ module gno.land/r/leon/home - -require ( - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/demo/art/gnoface v0.0.0-latest - gno.land/r/demo/art/millipede v0.0.0-latest - gno.land/r/demo/mirror v0.0.0-latest - gno.land/r/leon/config v0.0.0-latest - gno.land/r/leon/hof v0.0.0-latest -) diff --git a/examples/gno.land/r/morgan/guestbook/gno.mod b/examples/gno.land/r/morgan/guestbook/gno.mod index 2591643d33d..ac63a4cf8cd 100644 --- a/examples/gno.land/r/morgan/guestbook/gno.mod +++ b/examples/gno.land/r/morgan/guestbook/gno.mod @@ -1,7 +1 @@ module gno.land/r/morgan/guestbook - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest -) diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod index 412666e4171..573a7e139e7 100644 --- a/examples/gno.land/r/morgan/home/gno.mod +++ b/examples/gno.land/r/morgan/home/gno.mod @@ -1,3 +1 @@ module gno.land/r/morgan/home - -require gno.land/r/leon/hof v0.0.0-latest diff --git a/examples/gno.land/r/moul/home/gno.mod b/examples/gno.land/r/moul/home/gno.mod index f42a2c2ced8..91e02df3707 100644 --- a/examples/gno.land/r/moul/home/gno.mod +++ b/examples/gno.land/r/moul/home/gno.mod @@ -1,6 +1 @@ module gno.land/r/moul/home - -require ( - gno.land/r/leon/hof v0.0.0-latest - gno.land/r/moul/config v0.0.0-latest -) diff --git a/examples/gno.land/r/moul/present/gno.mod b/examples/gno.land/r/moul/present/gno.mod index 3ae0bf2e64d..a0a7777d0ed 100644 --- a/examples/gno.land/r/moul/present/gno.mod +++ b/examples/gno.land/r/moul/present/gno.mod @@ -1,6 +1 @@ module gno.land/r/moul/present - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest -) diff --git a/examples/gno.land/r/n2p5/config/gno.mod b/examples/gno.land/r/n2p5/config/gno.mod index 33f9276a409..29d5a74eb0a 100644 --- a/examples/gno.land/r/n2p5/config/gno.mod +++ b/examples/gno.land/r/n2p5/config/gno.mod @@ -1,6 +1 @@ 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/haystack/gno.mod b/examples/gno.land/r/n2p5/haystack/gno.mod index 9203eb2d3b1..17c131b8370 100644 --- a/examples/gno.land/r/n2p5/haystack/gno.mod +++ b/examples/gno.land/r/n2p5/haystack/gno.mod @@ -1,8 +1 @@ module gno.land/r/n2p5/haystack - -require ( - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/n2p5/haystack v0.0.0-latest - gno.land/p/n2p5/haystack/needle v0.0.0-latest -) diff --git a/examples/gno.land/r/n2p5/home/gno.mod b/examples/gno.land/r/n2p5/home/gno.mod index 779aa914989..3b6ddbf86bb 100644 --- a/examples/gno.land/r/n2p5/home/gno.mod +++ b/examples/gno.land/r/n2p5/home/gno.mod @@ -1,7 +1 @@ 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/stefann/home/gno.mod b/examples/gno.land/r/stefann/home/gno.mod index dd556e7f817..89071aa70fb 100644 --- a/examples/gno.land/r/stefann/home/gno.mod +++ b/examples/gno.land/r/stefann/home/gno.mod @@ -1,9 +1 @@ module gno.land/r/stefann/home - -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 - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/stefann/registry v0.0.0-latest -) diff --git a/examples/gno.land/r/stefann/registry/gno.mod b/examples/gno.land/r/stefann/registry/gno.mod index 5ed3e4916e2..7ef0c32030f 100644 --- a/examples/gno.land/r/stefann/registry/gno.mod +++ b/examples/gno.land/r/stefann/registry/gno.mod @@ -1,3 +1 @@ module gno.land/r/stefann/registry - -require gno.land/p/demo/ownable v0.0.0-latest diff --git a/examples/gno.land/r/sys/params/gno.mod b/examples/gno.land/r/sys/params/gno.mod index 4b4c2bf790f..c633412ced7 100644 --- a/examples/gno.land/r/sys/params/gno.mod +++ b/examples/gno.land/r/sys/params/gno.mod @@ -1,6 +1 @@ module gno.land/r/sys/params - -require ( - gno.land/p/demo/dao v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/sys/users/gno.mod b/examples/gno.land/r/sys/users/gno.mod index 774a364a272..e5e84a49faf 100644 --- a/examples/gno.land/r/sys/users/gno.mod +++ b/examples/gno.land/r/sys/users/gno.mod @@ -1,6 +1 @@ module gno.land/r/sys/users - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod index db94a208902..beae6e95d34 100644 --- a/examples/gno.land/r/sys/validators/v2/gno.mod +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -1,13 +1 @@ module gno.land/r/sys/validators/v2 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/nt/poa v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao/bridge v0.0.0-latest -) diff --git a/examples/gno.land/r/x/manfred_outfmt/gno.mod b/examples/gno.land/r/x/manfred_outfmt/gno.mod index 7044f0f72b3..e8165d847c9 100644 --- a/examples/gno.land/r/x/manfred_outfmt/gno.mod +++ b/examples/gno.land/r/x/manfred_outfmt/gno.mod @@ -1,5 +1,3 @@ // Draft module gno.land/r/x/manfred_outfmt - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index ea692bcaf0d..778121d59ed 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -168,7 +168,7 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C var tx std.Tx // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + memPkg := gno.MustReadMemPackage(pkg.Dir, pkg.Name) err := memPkg.Validate() if err != nil { return tx, fmt.Errorf("invalid package: %w", err) diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 235b9581ae0..2a0a4cf1106 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -19,7 +19,9 @@ import ( "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" "github.com/gnolang/gno/tm2/pkg/bft/node" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" @@ -743,8 +745,20 @@ func (pl *pkgsLoader) LoadPackage(modroot string, path, name string) error { // Override package info with mod infos currentPkg.Name = gm.Module.Mod.Path currentPkg.Draft = gm.Draft - for _, req := range gm.Require { - currentPkg.Requires = append(currentPkg.Requires, req.Mod.Path) + + pkg, err := gnolang.ReadMemPackage(currentPkg.Dir, currentPkg.Name) + if err != nil { + return fmt.Errorf("unable to read package at %q: %w", currentPkg.Dir, err) + } + imports, err := packages.Imports(pkg) + if err != nil { + return fmt.Errorf("unable to load package imports in %q: %w", currentPkg.Dir, err) + } + for _, imp := range imports { + if imp == currentPkg.Name || gnolang.IsStdlib(imp) { + continue + } + currentPkg.Imports = append(currentPkg.Imports, imp) } } @@ -758,7 +772,7 @@ func (pl *pkgsLoader) LoadPackage(modroot string, path, name string) error { pl.add(currentPkg) // Add requirements to the queue - for _, pkgPath := range currentPkg.Requires { + for _, pkgPath := range currentPkg.Imports { fullPath := filepath.Join(modroot, pkgPath) queue = append(queue, gnomod.Pkg{Dir: fullPath}) } diff --git a/gno.land/pkg/keyscli/addpkg.go b/gno.land/pkg/keyscli/addpkg.go index 37463d13b5c..eb6e727fedd 100644 --- a/gno.land/pkg/keyscli/addpkg.go +++ b/gno.land/pkg/keyscli/addpkg.go @@ -96,7 +96,7 @@ func execMakeAddPkg(cfg *MakeAddPkgCfg, args []string, io commands.IO) error { } // open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(cfg.PkgDir, cfg.PkgPath) + memPkg := gno.MustReadMemPackage(cfg.PkgDir, cfg.PkgPath) if memPkg.IsEmpty() { panic(fmt.Sprintf("found an empty package %q", cfg.PkgPath)) } diff --git a/gno.land/pkg/keyscli/run.go b/gno.land/pkg/keyscli/run.go index b0e05fe5a84..00b2be585c6 100644 --- a/gno.land/pkg/keyscli/run.go +++ b/gno.land/pkg/keyscli/run.go @@ -92,7 +92,7 @@ func execMakeRun(cfg *MakeRunCfg, args []string, cmdio commands.IO) error { return fmt.Errorf("could not read source path: %q, %w", sourcePath, err) } if info.IsDir() { - memPkg = gno.ReadMemPackage(sourcePath, "") + memPkg = gno.MustReadMemPackage(sourcePath, "") } else { // is file b, err := os.ReadFile(sourcePath) if err != nil { diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index e4f7a8543a7..00a0544cad6 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -186,7 +186,7 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { // does not exist. panic(fmt.Sprintf("failed loading stdlib %q: does not exist", pkgPath)) } - memPkg := gno.ReadMemPackage(stdlibPath, pkgPath) + memPkg := gno.MustReadMemPackage(stdlibPath, pkgPath) if memPkg.IsEmpty() { // no gno files are present panic(fmt.Sprintf("failed loading stdlib %q: not a valid MemPackage", pkgPath)) diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index 1ce648acb19..38f35ab7110 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -29,7 +29,7 @@ func NewMsgAddPackage(creator crypto.Address, pkgPath string, files []*gnovm.Mem var pkgName string for _, file := range files { if strings.HasSuffix(file.Name, ".gno") { - pkgName = string(gno.PackageNameFromFileBody(file.Name, file.Body)) + pkgName = string(gno.MustPackageNameFromFileBody(file.Name, file.Body)) break } } @@ -156,7 +156,7 @@ var _ std.Msg = MsgRun{} func NewMsgRun(caller crypto.Address, send std.Coins, files []*gnovm.MemFile) MsgRun { for _, file := range files { if strings.HasSuffix(file.Name, ".gno") { - pkgName := string(gno.PackageNameFromFileBody(file.Name, file.Body)) + pkgName := string(gno.MustPackageNameFromFileBody(file.Name, file.Body)) if pkgName != "main" { panic("package name should be 'main'") } diff --git a/gnovm/cmd/gno/download_deps.go b/gnovm/cmd/gno/download_deps.go new file mode 100644 index 00000000000..d19de9dd338 --- /dev/null +++ b/gnovm/cmd/gno/download_deps.go @@ -0,0 +1,86 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" + "github.com/gnolang/gno/tm2/pkg/commands" + "golang.org/x/mod/module" +) + +// downloadDeps recursively fetches the imports of a local package while following a given gno.mod replace directives +func downloadDeps(io commands.IO, pkgDir string, gnoMod *gnomod.File, fetcher pkgdownload.PackageFetcher) error { + if fetcher == nil { + return errors.New("fetcher is nil") + } + + pkg, err := gnolang.ReadMemPackage(pkgDir, gnoMod.Module.Mod.Path) + if err != nil { + return fmt.Errorf("read package at %q: %w", pkgDir, err) + } + imports, err := packages.Imports(pkg) + if err != nil { + return fmt.Errorf("read imports at %q: %w", pkgDir, err) + } + + for _, pkgPath := range imports { + resolved := gnoMod.Resolve(module.Version{Path: pkgPath}) + resolvedPkgPath := resolved.Path + + if !isRemotePkgPath(resolvedPkgPath) { + continue + } + + depDir := gnomod.PackageDir("", module.Version{Path: resolvedPkgPath}) + + if err := downloadPackage(io, resolvedPkgPath, depDir, fetcher); err != nil { + return fmt.Errorf("download import %q of %q: %w", resolvedPkgPath, pkgDir, err) + } + + if err := downloadDeps(io, depDir, gnoMod, fetcher); err != nil { + return err + } + } + + return nil +} + +// downloadPackage downloads a remote gno package by pkg path and store it at dst +func downloadPackage(io commands.IO, pkgPath string, dst string, fetcher pkgdownload.PackageFetcher) error { + modFilePath := filepath.Join(dst, "gno.mod") + + if _, err := os.Stat(modFilePath); err == nil { + // modfile exists in modcache, do nothing + return nil + } else if !os.IsNotExist(err) { + return fmt.Errorf("stat downloaded module %q at %q: %w", pkgPath, dst, err) + } + + io.ErrPrintfln("gno: downloading %s", pkgPath) + + if err := pkgdownload.Download(pkgPath, dst, fetcher); err != nil { + return err + } + + // We need to write a marker file for each downloaded package. + // For example: if you first download gno.land/r/foo/bar then download gno.land/r/foo, + // we need to know that gno.land/r/foo is not downloaded yet. + // We do this by checking for the presence of gno.land/r/foo/gno.mod + if err := os.WriteFile(modFilePath, []byte("module "+pkgPath+"\n"), 0o644); err != nil { + return fmt.Errorf("write modfile at %q: %w", modFilePath, err) + } + + return nil +} + +// isRemotePkgPath determines whether s is a remote pkg path, i.e.: not a filepath nor a standard library +func isRemotePkgPath(s string) bool { + return !strings.HasPrefix(s, ".") && !filepath.IsAbs(s) && !gnolang.IsStdlib(s) +} diff --git a/gnovm/cmd/gno/download_deps_test.go b/gnovm/cmd/gno/download_deps_test.go new file mode 100644 index 00000000000..3ccfdb0055e --- /dev/null +++ b/gnovm/cmd/gno/download_deps_test.go @@ -0,0 +1,152 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/mod/modfile" + "golang.org/x/mod/module" +) + +func TestDownloadDeps(t *testing.T) { + for _, tc := range []struct { + desc string + pkgPath string + modFile gnomod.File + errorShouldContain string + requirements []string + ioErrContains []string + }{ + { + desc: "not_exists", + pkgPath: "gno.land/p/demo/does_not_exists", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + }, + errorShouldContain: "query files list for pkg \"gno.land/p/demo/does_not_exists\": package \"gno.land/p/demo/does_not_exists\" is not available", + }, { + desc: "fetch_gno.land/p/demo/avl", + pkgPath: "gno.land/p/demo/avl", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + }, + requirements: []string{"avl"}, + ioErrContains: []string{ + "gno: downloading gno.land/p/demo/avl", + }, + }, { + desc: "fetch_gno.land/p/demo/blog6", + pkgPath: "gno.land/p/demo/blog", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + }, + requirements: []string{"avl", "blog", "ufmt", "mux"}, + ioErrContains: []string{ + "gno: downloading gno.land/p/demo/blog", + "gno: downloading gno.land/p/demo/avl", + "gno: downloading gno.land/p/demo/ufmt", + }, + }, { + desc: "fetch_replace_gno.land/p/demo/avl", + pkgPath: "gno.land/p/demo/replaced_avl", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + Replace: []*modfile.Replace{{ + Old: module.Version{Path: "gno.land/p/demo/replaced_avl"}, + New: module.Version{Path: "gno.land/p/demo/avl"}, + }}, + }, + requirements: []string{"avl"}, + ioErrContains: []string{ + "gno: downloading gno.land/p/demo/avl", + }, + }, { + desc: "fetch_replace_local", + pkgPath: "gno.land/p/demo/foo", + modFile: gnomod.File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + Replace: []*modfile.Replace{{ + Old: module.Version{Path: "gno.land/p/demo/foo"}, + New: module.Version{Path: "../local_foo"}, + }}, + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + mockErr := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + dirPath := t.TempDir() + + err := os.WriteFile(filepath.Join(dirPath, "main.gno"), []byte(fmt.Sprintf("package main\n\n import %q\n", tc.pkgPath)), 0o644) + require.NoError(t, err) + + tmpGnoHome := t.TempDir() + t.Setenv("GNOHOME", tmpGnoHome) + + fetcher := examplespkgfetcher.New() + + // gno: downloading dependencies + err = downloadDeps(io, dirPath, &tc.modFile, fetcher) + if tc.errorShouldContain != "" { + require.ErrorContains(t, err, tc.errorShouldContain) + } else { + require.Nil(t, err) + + // Read dir + entries, err := os.ReadDir(filepath.Join(tmpGnoHome, "pkg", "mod", "gno.land", "p", "demo")) + if !os.IsNotExist(err) { + require.Nil(t, err) + } + + // Check dir entries + assert.Equal(t, len(tc.requirements), len(entries)) + for _, e := range entries { + assert.Contains(t, tc.requirements, e.Name()) + } + + // Check logs + for _, c := range tc.ioErrContains { + assert.Contains(t, mockErr.String(), c) + } + + mockErr.Reset() + + // Try fetching again. Should be cached + downloadDeps(io, dirPath, &tc.modFile, fetcher) + for _, c := range tc.ioErrContains { + assert.NotContains(t, mockErr.String(), c) + } + } + }) + } +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go new file mode 100644 index 00000000000..1642c62d21e --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher/examplespkgfetcher.go @@ -0,0 +1,52 @@ +// Package examplespkgfetcher provides an implementation of [pkgdownload.PackageFetcher] +// to fetch packages from the examples folder at GNOROOT +package examplespkgfetcher + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" +) + +type ExamplesPackageFetcher struct{} + +var _ pkgdownload.PackageFetcher = (*ExamplesPackageFetcher)(nil) + +func New() pkgdownload.PackageFetcher { + return &ExamplesPackageFetcher{} +} + +// FetchPackage implements [pkgdownload.PackageFetcher]. +func (e *ExamplesPackageFetcher) FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) { + pkgDir := filepath.Join(gnoenv.RootDir(), "examples", filepath.FromSlash(pkgPath)) + + entries, err := os.ReadDir(pkgDir) + if os.IsNotExist(err) { + return nil, fmt.Errorf("query files list for pkg %q: package %q is not available", pkgPath, pkgPath) + } else if err != nil { + return nil, err + } + + res := []*gnovm.MemFile{} + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := entry.Name() + filePath := filepath.Join(pkgDir, name) + + body, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("read file at %q: %w", filePath, err) + } + + res = append(res, &gnovm.MemFile{Name: name, Body: string(body)}) + } + + return res, nil +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go b/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go new file mode 100644 index 00000000000..722cab01555 --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/pkgdownload.go @@ -0,0 +1,30 @@ +// Package pkgdownload provides interfaces and utility functions to download gno packages files. +package pkgdownload + +import ( + "fmt" + "os" + "path/filepath" +) + +// Download downloads the package identified by `pkgPath` in the directory at `dst` using the provided [PackageFetcher]. +// The directory at `dst` is created if it does not exists. +func Download(pkgPath string, dst string, fetcher PackageFetcher) error { + files, err := fetcher.FetchPackage(pkgPath) + if err != nil { + return err + } + + if err := os.MkdirAll(dst, 0o744); err != nil { + return err + } + + for _, file := range files { + fileDst := filepath.Join(dst, file.Name) + if err := os.WriteFile(fileDst, []byte(file.Body), 0o644); err != nil { + return fmt.Errorf("write file at %q: %w", fileDst, err) + } + } + + return nil +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go new file mode 100644 index 00000000000..79a7a6a54e2 --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/pkgfetcher.go @@ -0,0 +1,7 @@ +package pkgdownload + +import "github.com/gnolang/gno/gnovm" + +type PackageFetcher interface { + FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go new file mode 100644 index 00000000000..a71c1d43719 --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher.go @@ -0,0 +1,89 @@ +// Package rpcpkgfetcher provides an implementation of [pkgdownload.PackageFetcher] +// to fetch packages from gno.land rpc endpoints +package rpcpkgfetcher + +import ( + "fmt" + "path" + "strings" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type gnoPackageFetcher struct { + remoteOverrides map[string]string +} + +var _ pkgdownload.PackageFetcher = (*gnoPackageFetcher)(nil) + +func New(remoteOverrides map[string]string) pkgdownload.PackageFetcher { + return &gnoPackageFetcher{ + remoteOverrides: remoteOverrides, + } +} + +// FetchPackage implements [pkgdownload.PackageFetcher]. +func (gpf *gnoPackageFetcher) FetchPackage(pkgPath string) ([]*gnovm.MemFile, error) { + rpcURL, err := rpcURLFromPkgPath(pkgPath, gpf.remoteOverrides) + if err != nil { + return nil, fmt.Errorf("get rpc url for pkg path %q: %w", pkgPath, err) + } + + client, err := client.NewHTTPClient(rpcURL) + if err != nil { + return nil, fmt.Errorf("failed to instantiate tm2 client with remote %q: %w", rpcURL, err) + } + defer client.Close() + + data, err := qfile(client, pkgPath) + if err != nil { + return nil, fmt.Errorf("query files list for pkg %q: %w", pkgPath, err) + } + + files := strings.Split(string(data), "\n") + res := make([]*gnovm.MemFile, len(files)) + for i, file := range files { + filePath := path.Join(pkgPath, file) + data, err := qfile(client, filePath) + if err != nil { + return nil, fmt.Errorf("query package file %q: %w", filePath, err) + } + + res[i] = &gnovm.MemFile{Name: file, Body: string(data)} + } + return res, nil +} + +func rpcURLFromPkgPath(pkgPath string, remoteOverrides map[string]string) (string, error) { + parts := strings.Split(pkgPath, "/") + if len(parts) < 2 { + return "", fmt.Errorf("bad pkg path %q", pkgPath) + } + domain := parts[0] + + if override, ok := remoteOverrides[domain]; ok { + return override, nil + } + + // XXX: retrieve host/port from r/sys/zones. + rpcURL := fmt.Sprintf("https://rpc.%s:443", domain) + + return rpcURL, nil +} + +func qfile(c client.Client, pkgPath string) ([]byte, error) { + path := "vm/qfile" + data := []byte(pkgPath) + + qres, err := c.ABCIQuery(path, data) + if err != nil { + return nil, fmt.Errorf("query qfile: %w", err) + } + if qres.Response.Error != nil { + return nil, fmt.Errorf("qfile failed: %w\n%s", qres.Response.Error, qres.Response.Log) + } + + return qres.Response.Data, nil +} diff --git a/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go new file mode 100644 index 00000000000..56db5b796de --- /dev/null +++ b/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher/rpcpkgfetcher_test.go @@ -0,0 +1,53 @@ +package rpcpkgfetcher + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRpcURLFromPkgPath(t *testing.T) { + cases := []struct { + name string + pkgPath string + overrides map[string]string + result string + errorContains string + }{ + { + name: "happy path simple", + pkgPath: "gno.land/p/demo/avl", + result: "https://rpc.gno.land:443", + }, + { + name: "happy path override", + pkgPath: "gno.land/p/demo/avl", + overrides: map[string]string{"gno.land": "https://example.com/rpc:42"}, + result: "https://example.com/rpc:42", + }, + { + name: "happy path override no effect", + pkgPath: "gno.land/p/demo/avl", + overrides: map[string]string{"some.chain": "https://example.com/rpc:42"}, + result: "https://rpc.gno.land:443", + }, + { + name: "error bad pkg path", + pkgPath: "std", + result: "", + errorContains: `bad pkg path "std"`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res, err := rpcURLFromPkgPath(c.pkgPath, c.overrides) + if len(c.errorContains) == 0 { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, c.errorContains) + } + require.Equal(t, c.result, res) + }) + } +} diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index ef35cf9af83..6d5399ca932 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -102,7 +102,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { targetPath = filepath.Dir(pkgPath) } - memPkg := gno.ReadMemPackage(targetPath, targetPath) + memPkg := gno.MustReadMemPackage(targetPath, targetPath) tm := test.Machine(testStore, stdout, memPkg.Path) defer tm.Release() diff --git a/gnovm/cmd/gno/main_test.go b/gnovm/cmd/gno/main_test.go index 76c67f6807b..2ea3e31f977 100644 --- a/gnovm/cmd/gno/main_test.go +++ b/gnovm/cmd/gno/main_test.go @@ -9,9 +9,9 @@ import ( "strings" "testing" - "github.com/stretchr/testify/require" - + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/examplespkgfetcher" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/require" ) func TestMain_Gno(t *testing.T) { @@ -60,10 +60,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { mockErr := bytes.NewBufferString("") if !test.noTmpGnohome { - tmpGnoHome, err := os.MkdirTemp(os.TempDir(), "gnotesthome_") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpGnoHome) }) - t.Setenv("GNOHOME", tmpGnoHome) + t.Setenv("GNOHOME", t.TempDir()) } checkOutputs := func(t *testing.T) { @@ -131,6 +128,8 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { io.SetOut(commands.WriteNopCloser(mockOut)) io.SetErr(commands.WriteNopCloser(mockErr)) + testPackageFetcher = examplespkgfetcher.New() + err := newGnocliCmd(io).ParseAndRun(context.Background(), test.args) if errShouldBeEmpty { diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 67af5631c71..f762b070fe4 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -4,19 +4,22 @@ import ( "context" "flag" "fmt" - "go/parser" - "go/token" "os" "path/filepath" - "sort" "strings" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload" + "github.com/gnolang/gno/gnovm/cmd/gno/internal/pkgdownload/rpcpkgfetcher" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/errors" "go.uber.org/multierr" ) +// testPackageFetcher allows to override the package fetcher during tests. +var testPackageFetcher pkgdownload.PackageFetcher + func newModCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ @@ -123,23 +126,17 @@ For example: } type modDownloadCfg struct { - remote string - verbose bool + remoteOverrides string } +const remoteOverridesArgName = "remote-overrides" + func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( - &c.remote, - "remote", - "gno.land:26657", - "remote for fetching gno modules", - ) - - fs.BoolVar( - &c.verbose, - "v", - false, - "verbose output when running", + &c.remoteOverrides, + remoteOverridesArgName, + "", + "chain-domain=rpc-url comma-separated list", ) } @@ -148,6 +145,17 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { return flag.ErrHelp } + fetcher := testPackageFetcher + if fetcher == nil { + remoteOverrides, err := parseRemoteOverrides(cfg.remoteOverrides) + if err != nil { + return fmt.Errorf("invalid %s flag: %w", remoteOverridesArgName, err) + } + fetcher = rpcpkgfetcher.New(remoteOverrides) + } else if len(cfg.remoteOverrides) != 0 { + return fmt.Errorf("can't use %s flag with a custom package fetcher", remoteOverridesArgName) + } + path, err := os.Getwd() if err != nil { return err @@ -176,23 +184,26 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { return fmt.Errorf("validate: %w", err) } - // fetch dependencies - if err := gnoMod.FetchDeps(gnomod.ModCachePath(), cfg.remote, cfg.verbose); err != nil { - return fmt.Errorf("fetch: %w", err) + if err := downloadDeps(io, path, gnoMod, fetcher); err != nil { + return err } - gomod, err := gnomod.GnoToGoMod(*gnoMod) - if err != nil { - return fmt.Errorf("sanitize: %w", err) - } + return nil +} - // write go.mod file - err = gomod.Write(filepath.Join(path, "go.mod")) - if err != nil { - return fmt.Errorf("write go.mod file: %w", err) +func parseRemoteOverrides(arg string) (map[string]string, error) { + pairs := strings.Split(arg, ",") + res := make(map[string]string, len(pairs)) + for _, pair := range pairs { + parts := strings.Split(pair, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("expected 2 parts in chain-domain=rpc-url pair %q", arg) + } + domain := strings.TrimSpace(parts[0]) + rpcURL := strings.TrimSpace(parts[1]) + res[domain] = rpcURL } - - return nil + return res, nil } func execModInit(args []string) error { @@ -276,26 +287,6 @@ func modTidyOnce(cfg *modTidyCfg, wd, pkgdir string, io commands.IO) error { return err } - // Drop all existing requires - for _, r := range gm.Require { - gm.DropRequire(r.Mod.Path) - } - - imports, err := getGnoPackageImports(pkgdir) - if err != nil { - return err - } - for _, im := range imports { - // skip if importpath is modulepath - if im == gm.Module.Mod.Path { - continue - } - gm.AddRequire(im, "v0.0.0-latest") - if cfg.verbose { - io.ErrPrintfln(" %s", im) - } - } - gm.Write(fname) return nil } @@ -366,79 +357,22 @@ func getImportToFilesMap(pkgPath string) (map[string][]string, error) { if strings.HasSuffix(filename, "_filetest.gno") { continue } - imports, err := getGnoFileImports(filepath.Join(pkgPath, filename)) + + data, err := os.ReadFile(filepath.Join(pkgPath, filename)) if err != nil { return nil, err } - - for _, imp := range imports { - m[imp] = append(m[imp], filename) - } - } - return m, nil -} - -// getGnoPackageImports returns the list of gno imports from a given path. -// Note: It ignores subdirs. Since right now we are still deciding on -// how to handle subdirs. -// See: -// - https://github.com/gnolang/gno/issues/1024 -// - https://github.com/gnolang/gno/issues/852 -// -// TODO: move this to better location. -func getGnoPackageImports(path string) ([]string, error) { - entries, err := os.ReadDir(path) - if err != nil { - return nil, err - } - - allImports := make([]string, 0) - seen := make(map[string]struct{}) - for _, e := range entries { - filename := e.Name() - if ext := filepath.Ext(filename); ext != ".gno" { - continue - } - if strings.HasSuffix(filename, "_filetest.gno") { - continue - } - imports, err := getGnoFileImports(filepath.Join(path, filename)) + imports, _, err := packages.FileImports(filename, string(data)) if err != nil { return nil, err } - for _, im := range imports { - if !strings.HasPrefix(im, "gno.land/") { - continue - } - if _, ok := seen[im]; ok { - continue + + for _, imp := range imports { + if imp.Error != nil { + return nil, err } - allImports = append(allImports, im) - seen[im] = struct{}{} + m[imp.PkgPath] = append(m[imp.PkgPath], filename) } } - sort.Strings(allImports) - - return allImports, nil -} - -func getGnoFileImports(fname string) ([]string, error) { - if !strings.HasSuffix(fname, ".gno") { - return nil, fmt.Errorf("not a gno file: %q", fname) - } - data, err := os.ReadFile(fname) - if err != nil { - return nil, err - } - fs := token.NewFileSet() - f, err := parser.ParseFile(fs, fname, data, parser.ImportsOnly) - if err != nil { - return nil, err - } - res := make([]string, 0) - for _, im := range f.Imports { - importPath := strings.TrimPrefix(strings.TrimSuffix(im.Path.Value, `"`), `"`) - res = append(res, importPath) - } - return res, nil + return m, nil } diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go index d35ab311b6c..afce25597cd 100644 --- a/gnovm/cmd/gno/mod_test.go +++ b/gnovm/cmd/gno/mod_test.go @@ -1,12 +1,7 @@ package main import ( - "os" - "path/filepath" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestModApp(t *testing.T) { @@ -44,24 +39,19 @@ func TestModApp(t *testing.T) { args: []string{"mod", "download"}, testDir: "../../tests/integ/require_remote_module", simulateExternalRepo: true, + stderrShouldContain: "gno: downloading gno.land/p/demo/avl", }, { args: []string{"mod", "download"}, testDir: "../../tests/integ/require_invalid_module", simulateExternalRepo: true, - errShouldContain: "fetch: writepackage: querychain", + stderrShouldContain: "gno: downloading gno.land/p/demo/notexists", + errShouldContain: "query files list for pkg \"gno.land/p/demo/notexists\": package \"gno.land/p/demo/notexists\" is not available", }, { args: []string{"mod", "download"}, - testDir: "../../tests/integ/invalid_module_version1", + testDir: "../../tests/integ/require_std_lib", simulateExternalRepo: true, - errShouldContain: "usage: require module/path v1.2.3", - }, - { - args: []string{"mod", "download"}, - testDir: "../../tests/integ/invalid_module_version2", - simulateExternalRepo: true, - errShouldContain: "invalid: must be of the form v1.2.3", }, { args: []string{"mod", "download"}, @@ -72,12 +62,14 @@ func TestModApp(t *testing.T) { args: []string{"mod", "download"}, testDir: "../../tests/integ/replace_with_module", simulateExternalRepo: true, + stderrShouldContain: "gno: downloading gno.land/p/demo/users", }, { args: []string{"mod", "download"}, testDir: "../../tests/integ/replace_with_invalid_module", simulateExternalRepo: true, - errShouldContain: "fetch: writepackage: querychain", + stderrShouldContain: "gno: downloading gno.land/p/demo/notexists", + errShouldContain: "query files list for pkg \"gno.land/p/demo/notexists\": package \"gno.land/p/demo/notexists\" is not available", }, // test `gno mod init` with no module name @@ -158,12 +150,6 @@ func TestModApp(t *testing.T) { simulateExternalRepo: true, errShouldContain: "could not read gno.mod file", }, - { - args: []string{"mod", "tidy"}, - testDir: "../../tests/integ/invalid_module_version1", - simulateExternalRepo: true, - errShouldContain: "error parsing gno.mod file at", - }, { args: []string{"mod", "tidy"}, testDir: "../../tests/integ/minimalist_gnomod", @@ -179,12 +165,6 @@ func TestModApp(t *testing.T) { testDir: "../../tests/integ/valid2", simulateExternalRepo: true, }, - { - args: []string{"mod", "tidy"}, - testDir: "../../tests/integ/invalid_gno_file", - simulateExternalRepo: true, - errShouldContain: "expected 'package', found packag", - }, // test `gno mod why` { @@ -199,12 +179,6 @@ func TestModApp(t *testing.T) { simulateExternalRepo: true, errShouldContain: "could not read gno.mod file", }, - { - args: []string{"mod", "why", "std"}, - testDir: "../../tests/integ/invalid_module_version1", - simulateExternalRepo: true, - errShouldContain: "error parsing gno.mod file at", - }, { args: []string{"mod", "why", "std"}, testDir: "../../tests/integ/invalid_gno_file", @@ -239,122 +213,6 @@ valid.gno `, }, } - testMainCaseRun(t, tc) -} - -func TestGetGnoImports(t *testing.T) { - workingDir, err := os.Getwd() - require.NoError(t, err) - - // create external dir - tmpDir, cleanUpFn := createTmpDir(t) - defer cleanUpFn() - - // cd to tmp directory - os.Chdir(tmpDir) - defer os.Chdir(workingDir) - - files := []struct { - name, data string - }{ - { - name: "file1.gno", - data: ` - package tmp - - import ( - "std" - - "gno.land/p/demo/pkg1" - ) - `, - }, - { - name: "file2.gno", - data: ` - package tmp - - import ( - "gno.land/p/demo/pkg1" - "gno.land/p/demo/pkg2" - ) - `, - }, - { - name: "file1_test.gno", - data: ` - package tmp - - import ( - "testing" - - "gno.land/p/demo/testpkg" - ) - `, - }, - { - name: "z_0_filetest.gno", - data: ` - package main - - import ( - "gno.land/p/demo/filetestpkg" - ) - `, - }, - - // subpkg files - { - name: filepath.Join("subtmp", "file1.gno"), - data: ` - package subtmp - - import ( - "std" - - "gno.land/p/demo/subpkg1" - ) - `, - }, - { - name: filepath.Join("subtmp", "file2.gno"), - data: ` - package subtmp - - import ( - "gno.land/p/demo/subpkg1" - "gno.land/p/demo/subpkg2" - ) - `, - }, - } - - // Expected list of imports - // - ignore subdirs - // - ignore duplicate - // - ignore *_filetest.gno - // - should be sorted - expected := []string{ - "gno.land/p/demo/pkg1", - "gno.land/p/demo/pkg2", - "gno.land/p/demo/testpkg", - } - - // Create subpkg dir - err = os.Mkdir("subtmp", 0o700) - require.NoError(t, err) - - // Create files - for _, f := range files { - err = os.WriteFile(f.name, []byte(f.data), 0o644) - require.NoError(t, err) - } - imports, err := getGnoPackageImports(tmpDir) - require.NoError(t, err) - - require.Equal(t, len(expected), len(imports)) - for i := range imports { - assert.Equal(t, expected[i], imports[i]) - } + testMainCaseRun(t, tc) } diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 511a704dd7d..fec0de7c221 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -209,7 +209,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { } } - memPkg := gno.ReadMemPackage(pkg.Dir, gnoPkgPath) + memPkg := gno.MustReadMemPackage(pkg.Dir, gnoPkgPath) startedAt := time.Now() hasError := catchRuntimeError(gnoPkgPath, io.Err(), func() { diff --git a/gnovm/pkg/doc/dirs.go b/gnovm/pkg/doc/dirs.go index 19d312f6826..eadbec7d464 100644 --- a/gnovm/pkg/doc/dirs.go +++ b/gnovm/pkg/doc/dirs.go @@ -9,10 +9,15 @@ import ( "os" "path" "path/filepath" + "slices" "sort" "strings" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/pkg/packages" + "golang.org/x/mod/module" ) // A bfsDir describes a directory holding code by specifying @@ -60,25 +65,27 @@ func newDirs(dirs []string, modDirs []string) *bfsDirs { dir: mdir, importPath: gm.Module.Mod.Path, }) - roots = append(roots, getGnoModDirs(gm)...) + roots = append(roots, getGnoModDirs(gm, mdir)...) } go d.walk(roots) return d } -func getGnoModDirs(gm *gnomod.File) []bfsDir { +func getGnoModDirs(gm *gnomod.File, root string) []bfsDir { // cmd/go makes use of the go list command, we don't have that here. - dirs := make([]bfsDir, 0, len(gm.Require)) - for _, r := range gm.Require { - mv := gm.Resolve(r) + imports := packageImportsRecursive(root, gm.Module.Mod.Path) + + dirs := make([]bfsDir, 0, len(imports)) + for _, r := range imports { + mv := gm.Resolve(module.Version{Path: r}) path := gnomod.PackageDir("", mv) if _, err := os.Stat(path); err != nil { // only give directories which actually exist and don't give // an error when accessing if !os.IsNotExist(err) { - log.Println("open source directories from gno.mod:", err) + log.Println("open source directories from import:", err) } continue } @@ -91,6 +98,45 @@ func getGnoModDirs(gm *gnomod.File) []bfsDir { return dirs } +func packageImportsRecursive(root string, pkgPath string) []string { + pkg, err := gnolang.ReadMemPackage(root, pkgPath) + if err != nil { + // ignore invalid packages + pkg = &gnovm.MemPackage{} + } + + res, err := packages.Imports(pkg) + if err != nil { + // ignore packages with invalid imports + res = nil + } + + entries, err := os.ReadDir(root) + if err != nil { + // ignore unreadable dirs + entries = nil + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + dirName := entry.Name() + sub := packageImportsRecursive(filepath.Join(root, dirName), path.Join(pkgPath, dirName)) + + for _, imp := range sub { + if !slices.Contains(res, imp) { + res = append(res, imp) + } + } + } + + sort.Strings(res) + + return res +} + // Reset puts the scan back at the beginning. func (d *bfsDirs) Reset() { d.offset = 0 diff --git a/gnovm/pkg/doc/dirs_test.go b/gnovm/pkg/doc/dirs_test.go index 8659f3cbfcb..3139298a7ae 100644 --- a/gnovm/pkg/doc/dirs_test.go +++ b/gnovm/pkg/doc/dirs_test.go @@ -63,18 +63,9 @@ func TestNewDirs_invalidModDir(t *testing.T) { func tNewDirs(t *testing.T) (string, *bfsDirs) { t.Helper() - // modify GNO_HOME to testdata/dirsdep -- this allows us to test + // modify GNOHOME to testdata/dirsdep -- this allows us to test // dependency lookup by dirs. - old, ex := os.LookupEnv("GNO_HOME") - os.Setenv("GNO_HOME", wdJoin(t, "testdata/dirsdep")) - - t.Cleanup(func() { - if ex { - os.Setenv("GNO_HOME", old) - } else { - os.Unsetenv("GNO_HOME") - } - }) + t.Setenv("GNOHOME", wdJoin(t, "testdata/dirsdep")) return wdJoin(t, "testdata"), newDirs([]string{wdJoin(t, "testdata/dirs")}, []string{wdJoin(t, "testdata/dirsmod")}) diff --git a/gnovm/pkg/doc/testdata/dirsmod/a.gno b/gnovm/pkg/doc/testdata/dirsmod/a.gno new file mode 100644 index 00000000000..ee57c47dff5 --- /dev/null +++ b/gnovm/pkg/doc/testdata/dirsmod/a.gno @@ -0,0 +1,9 @@ +package dirsmod + +import ( + "dirs.mod/dep" +) + +func foo() { + dep.Bar() +} diff --git a/gnovm/pkg/doc/testdata/dirsmod/gno.mod b/gnovm/pkg/doc/testdata/dirsmod/gno.mod index 6c8008b958c..34d825571cc 100644 --- a/gnovm/pkg/doc/testdata/dirsmod/gno.mod +++ b/gnovm/pkg/doc/testdata/dirsmod/gno.mod @@ -1,5 +1 @@ -module dirs.mod/prefix - -require ( - dirs.mod/dep v0.0.0 -) +module dirs.mod/prefix \ No newline at end of file diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go index 09be600b198..2c82f6d8f29 100644 --- a/gnovm/pkg/gnolang/files_test.go +++ b/gnovm/pkg/gnolang/files_test.go @@ -138,7 +138,7 @@ func TestStdlibs(t *testing.T) { } fp := filepath.Join(dir, path) - memPkg := gnolang.ReadMemPackage(fp, path) + memPkg := gnolang.MustReadMemPackage(fp, path) t.Run(strings.ReplaceAll(memPkg.Path, "/", "-"), func(t *testing.T) { capture, opts := sharedCapture, sharedOpts switch memPkg.Path { diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 3368c7c7bde..8d3d6d8a2cc 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -1132,14 +1132,23 @@ type FileSet struct { // PackageNameFromFileBody extracts the package name from the given Gno code body. // The 'name' parameter is used for better error traces, and 'body' contains the Gno code. -func PackageNameFromFileBody(name, body string) Name { +func PackageNameFromFileBody(name, body string) (Name, error) { fset := token.NewFileSet() astFile, err := parser.ParseFile(fset, name, body, parser.PackageClauseOnly) if err != nil { - panic(err) + return "", err } - return Name(astFile.Name.Name) + return Name(astFile.Name.Name), nil +} + +// MustPackageNameFromFileBody is a wrapper around [PackageNameFromFileBody] that panics on error. +func MustPackageNameFromFileBody(name, body string) Name { + pkgName, err := PackageNameFromFileBody(name, body) + if err != nil { + panic(err) + } + return pkgName } // ReadMemPackage initializes a new MemPackage by reading the OS directory @@ -1152,10 +1161,10 @@ func PackageNameFromFileBody(name, body string) Name { // // NOTE: panics if package name is invalid (characters must be alphanumeric or _, // lowercase, and must start with a letter). -func ReadMemPackage(dir string, pkgPath string) *gnovm.MemPackage { +func ReadMemPackage(dir string, pkgPath string) (*gnovm.MemPackage, error) { files, err := os.ReadDir(dir) if err != nil { - panic(err) + return nil, err } allowedFiles := []string{ // make case insensitive? "LICENSE", @@ -1186,24 +1195,36 @@ func ReadMemPackage(dir string, pkgPath string) *gnovm.MemPackage { return ReadMemPackageFromList(list, pkgPath) } +// MustReadMemPackage is a wrapper around [ReadMemPackage] that panics on error. +func MustReadMemPackage(dir string, pkgPath string) *gnovm.MemPackage { + pkg, err := ReadMemPackage(dir, pkgPath) + if err != nil { + panic(err) + } + return pkg +} + // ReadMemPackageFromList creates a new [gnovm.MemPackage] with the specified pkgPath, // containing the contents of all the files provided in the list slice. // No parsing or validation is done on the filenames. // -// NOTE: panics if package name is invalid (characters must be alphanumeric or _, +// NOTE: errors out if package name is invalid (characters must be alphanumeric or _, // lowercase, and must start with a letter). -func ReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage { +func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, error) { memPkg := &gnovm.MemPackage{Path: pkgPath} var pkgName Name for _, fpath := range list { fname := filepath.Base(fpath) bz, err := os.ReadFile(fpath) if err != nil { - panic(err) + return nil, err } // XXX: should check that all pkg names are the same (else package is invalid) if pkgName == "" && strings.HasSuffix(fname, ".gno") { - pkgName = PackageNameFromFileBody(fname, string(bz)) + pkgName, err = PackageNameFromFileBody(fname, string(bz)) + if err != nil { + return nil, err + } if strings.HasSuffix(string(pkgName), "_test") { pkgName = pkgName[:len(pkgName)-len("_test")] } @@ -1217,11 +1238,22 @@ func ReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage { // If no .gno files are present, package simply does not exist. if !memPkg.IsEmpty() { - validatePkgName(string(pkgName)) + if err := validatePkgName(string(pkgName)); err != nil { + return nil, err + } memPkg.Name = string(pkgName) } - return memPkg + return memPkg, nil +} + +// MustReadMemPackageFromList is a wrapper around [ReadMemPackageFromList] that panics on error. +func MustReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage { + pkg, err := ReadMemPackageFromList(list, pkgPath) + if err != nil { + panic(err) + } + return pkg } // ParseMemPackage executes [ParseFile] on each file of the memPkg, excluding @@ -2140,10 +2172,11 @@ var rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]+$`) // TODO: consider length restrictions. // If this function is changed, ReadMemPackage's documentation should be updated accordingly. -func validatePkgName(name string) { +func validatePkgName(name string) error { if !rePkgName.MatchString(name) { - panic(fmt.Sprintf("cannot create package with invalid name %q", name)) + return fmt.Errorf("cannot create package with invalid name %q", name) } + return nil } const hiddenResultVariable = ".res_" diff --git a/gnovm/pkg/gnomod/fetch.go b/gnovm/pkg/gnomod/fetch.go deleted file mode 100644 index 24aaac2f9d4..00000000000 --- a/gnovm/pkg/gnomod/fetch.go +++ /dev/null @@ -1,30 +0,0 @@ -package gnomod - -import ( - "fmt" - - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" -) - -func queryChain(remote string, qpath string, data []byte) (res *abci.ResponseQuery, err error) { - opts2 := client.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX - } - cli, err := client.NewHTTPClient(remote) - if err != nil { - return nil, err - } - - qres, err := cli.ABCIQueryWithOptions(qpath, data, opts2) - if err != nil { - return nil, err - } - if qres.Response.Error != nil { - fmt.Printf("Log: %s\n", qres.Response.Log) - return nil, qres.Response.Error - } - - return &qres.Response, nil -} diff --git a/gnovm/pkg/gnomod/file.go b/gnovm/pkg/gnomod/file.go index b6ee95acac8..a1c77b51e45 100644 --- a/gnovm/pkg/gnomod/file.go +++ b/gnovm/pkg/gnomod/file.go @@ -12,12 +12,8 @@ package gnomod import ( "errors" "fmt" - "log" "os" - "path/filepath" - "strings" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "golang.org/x/mod/modfile" "golang.org/x/mod/module" ) @@ -27,51 +23,11 @@ type File struct { Draft bool Module *modfile.Module Go *modfile.Go - Require []*modfile.Require Replace []*modfile.Replace Syntax *modfile.FileSyntax } -// AddRequire sets the first require line for path to version vers, -// preserving any existing comments for that line and removing all -// other lines for path. -// -// If no line currently exists for path, AddRequire adds a new line -// at the end of the last require block. -func (f *File) AddRequire(path, vers string) error { - need := true - for _, r := range f.Require { - if r.Mod.Path == path { - if need { - r.Mod.Version = vers - updateLine(r.Syntax, "require", modfile.AutoQuote(path), vers) - need = false - } else { - markLineAsRemoved(r.Syntax) - *r = modfile.Require{} - } - } - } - - if need { - f.AddNewRequire(path, vers, false) - } - return nil -} - -// AddNewRequire adds a new require line for path at version vers at the end of -// the last require block, regardless of any existing require lines for path. -func (f *File) AddNewRequire(path, vers string, indirect bool) { - line := addLine(f.Syntax, nil, "require", modfile.AutoQuote(path), vers) - r := &modfile.Require{ - Mod: module.Version{Path: path, Version: vers}, - Syntax: line, - } - setIndirect(r, indirect) - f.Require = append(f.Require, r) -} - func (f *File) AddModuleStmt(path string) error { if f.Syntax == nil { f.Syntax = new(modfile.FileSyntax) @@ -107,16 +63,6 @@ func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) } -func (f *File) DropRequire(path string) error { - for _, r := range f.Require { - if r.Mod.Path == path { - markLineAsRemoved(r.Syntax) - *r = modfile.Require{} - } - } - return nil -} - func (f *File) DropReplace(oldPath, oldVers string) error { for _, r := range f.Replace { if r.Old.Path == oldPath && r.Old.Version == oldVers { @@ -136,76 +82,17 @@ func (f *File) Validate() error { return nil } -// Resolve takes a Require directive from File and returns any adequate replacement +// Resolve takes a module version and returns any adequate replacement // following the Replace directives. -func (f *File) Resolve(r *modfile.Require) module.Version { - mod, replaced := isReplaced(r.Mod, f.Replace) +func (f *File) Resolve(m module.Version) module.Version { + if f == nil { + return m + } + mod, replaced := isReplaced(m, f.Replace) if replaced { return mod } - return r.Mod -} - -// FetchDeps fetches and writes gno.mod packages -// in GOPATH/pkg/gnomod/ -func (f *File) FetchDeps(path string, remote string, verbose bool) error { - for _, r := range f.Require { - mod := f.Resolve(r) - if r.Mod.Path != mod.Path { - if modfile.IsDirectoryPath(mod.Path) { - continue - } - } - indirect := "" - if r.Indirect { - indirect = "// indirect" - } - - _, err := os.Stat(PackageDir(path, mod)) - if !os.IsNotExist(err) { - if verbose { - log.Println("cached", mod.Path, indirect) - } - continue - } - if verbose { - log.Println("fetching", mod.Path, indirect) - } - requirements, err := writePackage(remote, path, mod.Path) - if err != nil { - return fmt.Errorf("writepackage: %w", err) - } - - modFile := new(File) - modFile.AddModuleStmt(mod.Path) - for _, req := range requirements { - path := req[1 : len(req)-1] // trim leading and trailing `"` - if strings.HasSuffix(path, modFile.Module.Mod.Path) { - continue - } - - if !gno.IsStdlib(path) { - modFile.AddNewRequire(path, "v0.0.0-latest", true) - } - } - - err = modFile.FetchDeps(path, remote, verbose) - if err != nil { - return err - } - goMod, err := GnoToGoMod(*modFile) - if err != nil { - return err - } - pkgPath := PackageDir(path, mod) - goModFilePath := filepath.Join(pkgPath, "go.mod") - err = goMod.Write(goModFilePath) - if err != nil { - return err - } - } - - return nil + return m } // writes file to the given absolute file path @@ -220,5 +107,5 @@ func (f *File) Write(fname string) error { } func (f *File) Sanitize() { - removeDups(f.Syntax, &f.Require, &f.Replace) + removeDups(f.Syntax, &f.Replace) } diff --git a/gnovm/pkg/gnomod/file_test.go b/gnovm/pkg/gnomod/file_test.go deleted file mode 100644 index a64c2794a65..00000000000 --- a/gnovm/pkg/gnomod/file_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package gnomod - -import ( - "bytes" - "log" - "os" - "path/filepath" - "testing" - - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/mod/modfile" - "golang.org/x/mod/module" -) - -const testRemote string = "gno.land:26657" // XXX(race condition): test with a local node so that this test is consistent with git and not with a deploy - -func TestFetchDeps(t *testing.T) { - for _, tc := range []struct { - desc string - modFile File - errorShouldContain string - requirements []string - stdOutContains []string - cachedStdOutContains []string - }{ - { - desc: "not_exists", - modFile: File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: "testFetchDeps", - }, - }, - Require: []*modfile.Require{ - { - Mod: module.Version{ - Path: "gno.land/p/demo/does_not_exists", - Version: "v0.0.0", - }, - }, - }, - }, - errorShouldContain: "querychain (gno.land/p/demo/does_not_exists)", - }, { - desc: "fetch_gno.land/p/demo/avl", - modFile: File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: "testFetchDeps", - }, - }, - Require: []*modfile.Require{ - { - Mod: module.Version{ - Path: "gno.land/p/demo/avl", - Version: "v0.0.0", - }, - }, - }, - }, - requirements: []string{"avl"}, - stdOutContains: []string{ - "fetching gno.land/p/demo/avl", - }, - cachedStdOutContains: []string{ - "cached gno.land/p/demo/avl", - }, - }, { - desc: "fetch_gno.land/p/demo/blog6", - modFile: File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: "testFetchDeps", - }, - }, - Require: []*modfile.Require{ - { - Mod: module.Version{ - Path: "gno.land/p/demo/blog", - Version: "v0.0.0", - }, - }, - }, - }, - requirements: []string{"avl", "blog", "ufmt", "mux"}, - stdOutContains: []string{ - "fetching gno.land/p/demo/blog", - "fetching gno.land/p/demo/avl // indirect", - "fetching gno.land/p/demo/ufmt // indirect", - }, - cachedStdOutContains: []string{ - "cached gno.land/p/demo/blog", - }, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - var buf bytes.Buffer - log.SetOutput(&buf) - defer func() { - log.SetOutput(os.Stderr) - }() - - // Create test dir - dirPath, cleanUpFn := testutils.NewTestCaseDir(t) - assert.NotNil(t, dirPath) - defer cleanUpFn() - - // Fetching dependencies - err := tc.modFile.FetchDeps(dirPath, testRemote, true) - if tc.errorShouldContain != "" { - require.ErrorContains(t, err, tc.errorShouldContain) - } else { - require.Nil(t, err) - - // Read dir - entries, err := os.ReadDir(filepath.Join(dirPath, "gno.land", "p", "demo")) - require.Nil(t, err) - - // Check dir entries - assert.Equal(t, len(tc.requirements), len(entries)) - for _, e := range entries { - assert.Contains(t, tc.requirements, e.Name()) - } - - // Check logs - for _, c := range tc.stdOutContains { - assert.Contains(t, buf.String(), c) - } - - buf.Reset() - - // Try fetching again. Should be cached - tc.modFile.FetchDeps(dirPath, testRemote, true) - for _, c := range tc.cachedStdOutContains { - assert.Contains(t, buf.String(), c) - } - } - }) - } -} diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index 9384c41c293..a34caa2e48d 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -3,22 +3,16 @@ package gnomod import ( "errors" "fmt" - "go/parser" - gotoken "go/token" "os" "path/filepath" "strings" - "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/pkg/transpiler" "golang.org/x/mod/modfile" "golang.org/x/mod/module" ) -const queryPathFile = "vm/qfile" - // ModCachePath returns the path for gno modules func ModCachePath() string { return filepath.Join(gnoenv.HomeDir(), "pkg", "mod") @@ -27,127 +21,10 @@ func ModCachePath() string { // PackageDir resolves a given module.Version to the path on the filesystem. // If root is dir, it is defaulted to the value of [ModCachePath]. func PackageDir(root string, v module.Version) string { - // This is also used internally exactly like filepath.Join; but we'll keep - // the calls centralized to make sure we can change the path centrally should - // we start including the module version in the path. - if root == "" { root = ModCachePath() } - return filepath.Join(root, v.Path) -} - -func writePackage(remote, basePath, pkgPath string) (requirements []string, err error) { - res, err := queryChain(remote, queryPathFile, []byte(pkgPath)) - if err != nil { - return nil, fmt.Errorf("querychain (%s): %w", pkgPath, err) - } - - dirPath, fileName := gnovm.SplitFilepath(pkgPath) - if fileName == "" { - // Is Dir - // Create Dir if not exists - dirPath := filepath.Join(basePath, dirPath) - if _, err = os.Stat(dirPath); os.IsNotExist(err) { - if err = os.MkdirAll(dirPath, 0o755); err != nil { - return nil, fmt.Errorf("mkdir %q: %w", dirPath, err) - } - } - - files := strings.Split(string(res.Data), "\n") - for _, file := range files { - reqs, err := writePackage(remote, basePath, filepath.Join(pkgPath, file)) - if err != nil { - return nil, fmt.Errorf("writepackage: %w", err) - } - requirements = append(requirements, reqs...) - } - } else { - // Is File - // Transpile and write generated go file - file, err := parser.ParseFile(gotoken.NewFileSet(), fileName, res.Data, parser.ImportsOnly) - if err != nil { - return nil, fmt.Errorf("parse gno file: %w", err) - } - for _, i := range file.Imports { - requirements = append(requirements, i.Path.Value) - } - - // Write file - fileNameWithPath := filepath.Join(basePath, dirPath, fileName) - err = os.WriteFile(fileNameWithPath, res.Data, 0o644) - if err != nil { - return nil, fmt.Errorf("writefile %q: %w", fileNameWithPath, err) - } - } - - return removeDuplicateStr(requirements), nil -} - -// GnoToGoMod make necessary modifications in the gno.mod -// and return go.mod file. -func GnoToGoMod(f File) (*File, error) { - // TODO(morgan): good candidate to move to pkg/transpiler. - - gnoModPath := ModCachePath() - - if !gnolang.IsStdlib(f.Module.Mod.Path) { - f.AddModuleStmt(transpiler.TranspileImportPath(f.Module.Mod.Path)) - } - - for i := range f.Require { - mod, replaced := isReplaced(f.Require[i].Mod, f.Replace) - if replaced { - if modfile.IsDirectoryPath(mod.Path) { - continue - } - } - path := f.Require[i].Mod.Path - if !gnolang.IsStdlib(path) { - // Add dependency with a modified import path - f.AddRequire(transpiler.TranspileImportPath(path), f.Require[i].Mod.Version) - } - f.AddReplace(path, f.Require[i].Mod.Version, filepath.Join(gnoModPath, path), "") - // Remove the old require since the new dependency was added above - f.DropRequire(path) - } - - // Remove replacements that are not replaced by directories. - // - // Explanation: - // By this stage every replacement should be replace by dir. - // If not replaced by dir, remove it. - // - // e.g: - // - // ``` - // require ( - // gno.land/p/demo/avl v1.2.3 - // ) - // - // replace ( - // gno.land/p/demo/avl v1.2.3 => gno.land/p/demo/avl v3.2.1 - // ) - // ``` - // - // In above case we will fetch `gno.land/p/demo/avl v3.2.1` and - // replace will look something like: - // - // ``` - // replace ( - // gno.land/p/demo/avl v1.2.3 => gno.land/p/demo/avl v3.2.1 - // gno.land/p/demo/avl v3.2.1 => /path/to/avl/version/v3.2.1 - // ) - // ``` - // - // Remove `gno.land/p/demo/avl v1.2.3 => gno.land/p/demo/avl v3.2.1`. - for _, r := range f.Replace { - if !modfile.IsDirectoryPath(r.New.Path) { - f.DropReplace(r.Old.Path, r.Old.Version) - } - } - - return &f, nil + return filepath.Join(root, filepath.FromSlash(v.Path)) } func CreateGnoModFile(rootDir, modPath string) error { @@ -180,7 +57,7 @@ func CreateGnoModFile(rootDir, modPath string) error { return fmt.Errorf("read file %q: %w", fpath, err) } - pn := gnolang.PackageNameFromFileBody(file.Name(), string(bz)) + pn := gnolang.MustPackageNameFromFileBody(file.Name(), string(bz)) if strings.HasSuffix(string(pkgName), "_test") { pkgName = pkgName[:len(pkgName)-len("_test")] } @@ -217,14 +94,3 @@ func isReplaced(mod module.Version, repl []*modfile.Replace) (module.Version, bo } return module.Version{}, false } - -func removeDuplicateStr(str []string) (res []string) { - m := make(map[string]struct{}, len(str)) - for _, s := range str { - if _, ok := m[s]; !ok { - m[s] = struct{}{} - res = append(res, s) - } - } - return -} diff --git a/gnovm/pkg/gnomod/parse.go b/gnovm/pkg/gnomod/parse.go index a6314d5729f..e3a3fbcaeea 100644 --- a/gnovm/pkg/gnomod/parse.go +++ b/gnovm/pkg/gnomod/parse.go @@ -105,7 +105,7 @@ func Parse(file string, data []byte) (*File, error) { Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), }) continue - case "module", "require", "replace": + case "module", "replace": for _, l := range x.Line { f.add(&errs, x, l, x.Token[0], l.Token) } @@ -180,26 +180,6 @@ func (f *File) add(errs *modfile.ErrorList, block *modfile.LineBlock, line *modf } f.Module.Mod = module.Version{Path: s} - case "require": - if len(args) != 2 { - errorf("usage: %s module/path v1.2.3", verb) - return - } - s, err := parseString(&args[0]) - if err != nil { - errorf("invalid quoted string: %v", err) - return - } - v, err := parseVersion(verb, s, &args[1]) - if err != nil { - wrapError(err) - return - } - f.Require = append(f.Require, &modfile.Require{ - Mod: module.Version{Path: s, Version: v}, - Syntax: line, - }) - case "replace": replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args) if wrappederr != nil { diff --git a/gnovm/pkg/gnomod/parse_test.go b/gnovm/pkg/gnomod/parse_test.go index 61aaa83482b..ec54c6424fc 100644 --- a/gnovm/pkg/gnomod/parse_test.go +++ b/gnovm/pkg/gnomod/parse_test.go @@ -194,16 +194,29 @@ func TestParseGnoMod(t *testing.T) { modPath: filepath.Join(pkgDir, "gno.mod"), }, { - desc: "error parsing gno.mod", + desc: "valid gno.mod file with replace", + modData: `module foo + replace bar => ../bar`, + modPath: filepath.Join(pkgDir, "gno.mod"), + }, + { + desc: "error bad module directive", modData: `module foo v0.0.0`, modPath: filepath.Join(pkgDir, "gno.mod"), errShouldContain: "error parsing gno.mod file at", }, { - desc: "error validating gno.mod", - modData: `require bar v0.0.0`, + desc: "error gno.mod without module", + modData: `replace bar => ../bar`, + modPath: filepath.Join(pkgDir, "gno.mod"), + errShouldContain: "requires module", + }, + { + desc: "error gno.mod with require", + modData: `module foo + require bar v0.0.0`, modPath: filepath.Join(pkgDir, "gno.mod"), - errShouldContain: "error validating gno.mod file at", + errShouldContain: "unknown directive: require", }, } { t.Run(tc.desc, func(t *testing.T) { diff --git a/gnovm/pkg/gnomod/pkg.go b/gnovm/pkg/gnomod/pkg.go index f6fe7f60301..35f52e3dded 100644 --- a/gnovm/pkg/gnomod/pkg.go +++ b/gnovm/pkg/gnomod/pkg.go @@ -5,14 +5,19 @@ import ( "io/fs" "os" "path/filepath" + "slices" "strings" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/packages" ) type Pkg struct { - Dir string // absolute path to package dir - Name string // package name - Requires []string // dependencies - Draft bool // whether the package is a draft + Dir string // absolute path to package dir + Name string // package name + Imports []string // direct imports of this pkg + Draft bool // whether the package is a draft } type SubPkg struct { @@ -60,10 +65,10 @@ func visitPackage(pkg Pkg, pkgs []Pkg, visited, onStack map[string]bool, sortedP onStack[pkg.Name] = true // Visit package's dependencies - for _, req := range pkg.Requires { + for _, imp := range pkg.Imports { found := false for _, p := range pkgs { - if p.Name != req { + if p.Name != imp { continue } if err := visitPackage(p, pkgs, visited, onStack, sortedPkgs); err != nil { @@ -73,7 +78,7 @@ func visitPackage(pkg Pkg, pkgs []Pkg, visited, onStack map[string]bool, sortedP break } if !found { - return fmt.Errorf("missing dependency '%s' for package '%s'", req, pkg.Name) + return fmt.Errorf("missing dependency '%s' for package '%s'", imp, pkg.Name) } } @@ -111,17 +116,28 @@ func ListPkgs(root string) (PkgList, error) { return fmt.Errorf("validate: %w", err) } + pkg, err := gnolang.ReadMemPackage(path, gnoMod.Module.Mod.Path) + if err != nil { + // ignore package files on error + pkg = &gnovm.MemPackage{} + } + + imports, err := packages.Imports(pkg) + if err != nil { + // ignore imports on error + imports = []string{} + } + + // remove self and standard libraries from imports + imports = slices.DeleteFunc(imports, func(imp string) bool { + return imp == gnoMod.Module.Mod.Path || gnolang.IsStdlib(imp) + }) + pkgs = append(pkgs, Pkg{ - Dir: path, - Name: gnoMod.Module.Mod.Path, - Draft: gnoMod.Draft, - Requires: func() []string { - var reqs []string - for _, req := range gnoMod.Require { - reqs = append(reqs, req.Mod.Path) - } - return reqs - }(), + Dir: path, + Name: gnoMod.Module.Mod.Path, + Draft: gnoMod.Draft, + Imports: imports, }) return nil }) @@ -144,7 +160,7 @@ func (sp SortedPkgList) GetNonDraftPkgs() SortedPkgList { continue } dependsOnDraft := false - for _, req := range pkg.Requires { + for _, req := range pkg.Imports { if draft[req] { dependsOnDraft = true draft[pkg.Name] = true diff --git a/gnovm/pkg/gnomod/pkg_test.go b/gnovm/pkg/gnomod/pkg_test.go index 587a0bb8f81..7c3035a4b7b 100644 --- a/gnovm/pkg/gnomod/pkg_test.go +++ b/gnovm/pkg/gnomod/pkg_test.go @@ -47,12 +47,6 @@ func TestListAndNonDraftPkgs(t *testing.T) { "foo", `module foo`, }, - { - "bar", - `module bar - - require foo v0.0.0`, - }, { "baz", `module baz`, @@ -64,121 +58,8 @@ func TestListAndNonDraftPkgs(t *testing.T) { module qux`, }, }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{"foo", "bar", "baz"}, - }, - { - desc: "package directly depends on draft package", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - require foo v0.0.0`, - }, - { - "baz", - `module baz`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz"}, - outNonDraftPkgs: []string{"baz"}, - }, - { - desc: "package indirectly depends on draft package", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - - require foo v0.0.0`, - }, - { - "baz", - `module baz - - require bar v0.0.0`, - }, - { - "qux", - `module qux`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{"qux"}, - }, - { - desc: "package indirectly depends on draft package (multiple levels - 1)", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - - require foo v0.0.0`, - }, - { - "baz", - `module baz - - require bar v0.0.0`, - }, - { - "qux", - `module qux - - require baz v0.0.0`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{}, - }, - { - desc: "package indirectly depends on draft package (multiple levels - 2)", - in: []struct{ name, modfile string }{ - { - "foo", - `// Draft - - module foo`, - }, - { - "bar", - `module bar - - require qux v0.0.0`, - }, - { - "baz", - `module baz - - require foo v0.0.0`, - }, - { - "qux", - `module qux - - require baz v0.0.0`, - }, - }, - outListPkgs: []string{"foo", "bar", "baz", "qux"}, - outNonDraftPkgs: []string{}, + outListPkgs: []string{"foo", "baz", "qux"}, + outNonDraftPkgs: []string{"foo", "baz"}, }, } { t.Run(tc.desc, func(t *testing.T) { @@ -224,6 +105,7 @@ func createGnoModPkg(t *testing.T, dirPath, pkgName, modData string) { // Create gno.mod err = os.WriteFile(filepath.Join(pkgDirPath, "gno.mod"), []byte(modData), 0o644) + require.NoError(t, err) } func TestSortPkgs(t *testing.T) { @@ -240,30 +122,30 @@ func TestSortPkgs(t *testing.T) { }, { desc: "no_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{}}, - {Name: "pkg2", Dir: "/path/to/pkg2", Requires: []string{}}, - {Name: "pkg3", Dir: "/path/to/pkg3", Requires: []string{}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{}}, + {Name: "pkg2", Dir: "/path/to/pkg2", Imports: []string{}}, + {Name: "pkg3", Dir: "/path/to/pkg3", Imports: []string{}}, }, expected: []string{"pkg1", "pkg2", "pkg3"}, }, { desc: "circular_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{"pkg2"}}, - {Name: "pkg2", Dir: "/path/to/pkg2", Requires: []string{"pkg1"}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{"pkg2"}}, + {Name: "pkg2", Dir: "/path/to/pkg2", Imports: []string{"pkg1"}}, }, shouldErr: true, }, { desc: "missing_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{"pkg2"}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{"pkg2"}}, }, shouldErr: true, }, { desc: "valid_dependencies", in: []Pkg{ - {Name: "pkg1", Dir: "/path/to/pkg1", Requires: []string{"pkg2"}}, - {Name: "pkg2", Dir: "/path/to/pkg2", Requires: []string{"pkg3"}}, - {Name: "pkg3", Dir: "/path/to/pkg3", Requires: []string{}}, + {Name: "pkg1", Dir: "/path/to/pkg1", Imports: []string{"pkg2"}}, + {Name: "pkg2", Dir: "/path/to/pkg2", Imports: []string{"pkg3"}}, + {Name: "pkg3", Dir: "/path/to/pkg3", Imports: []string{}}, }, expected: []string{"pkg3", "pkg2", "pkg1"}, }, diff --git a/gnovm/pkg/gnomod/preprocess.go b/gnovm/pkg/gnomod/preprocess.go index ec1faaa5c29..df6910f769b 100644 --- a/gnovm/pkg/gnomod/preprocess.go +++ b/gnovm/pkg/gnomod/preprocess.go @@ -3,50 +3,15 @@ package gnomod import ( "golang.org/x/mod/modfile" "golang.org/x/mod/module" - "golang.org/x/mod/semver" ) -func removeDups(syntax *modfile.FileSyntax, require *[]*modfile.Require, replace *[]*modfile.Replace) { - if require != nil { - purged := removeRequireDups(require) - cleanSyntaxTree(syntax, purged) - } +func removeDups(syntax *modfile.FileSyntax, replace *[]*modfile.Replace) { if replace != nil { purged := removeReplaceDups(replace) cleanSyntaxTree(syntax, purged) } } -// removeRequireDups removes duplicate requirements. -// Requirements with higher version takes priority. -func removeRequireDups(require *[]*modfile.Require) map[*modfile.Line]bool { - purge := make(map[*modfile.Line]bool) - - keepRequire := make(map[string]string) - for _, r := range *require { - if v, ok := keepRequire[r.Mod.Path]; ok { - if semver.Compare(r.Mod.Version, v) == 1 { - keepRequire[r.Mod.Path] = r.Mod.Version - } - continue - } - keepRequire[r.Mod.Path] = r.Mod.Version - } - var req []*modfile.Require - added := make(map[string]bool) - for _, r := range *require { - if v, ok := keepRequire[r.Mod.Path]; ok && !added[r.Mod.Path] && v == r.Mod.Version { - req = append(req, r) - added[r.Mod.Path] = true - continue - } - purge[r.Syntax] = true - } - *require = req - - return purge -} - // removeReplaceDups removes duplicate replacements. // Later replacements take priority over earlier ones. func removeReplaceDups(replace *[]*modfile.Replace) map[*modfile.Line]bool { diff --git a/gnovm/pkg/gnomod/preprocess_test.go b/gnovm/pkg/gnomod/preprocess_test.go index 28f42d740e3..6e0a890763c 100644 --- a/gnovm/pkg/gnomod/preprocess_test.go +++ b/gnovm/pkg/gnomod/preprocess_test.go @@ -8,133 +8,6 @@ import ( "golang.org/x/mod/module" ) -func TestRemoveRequireDups(t *testing.T) { - for _, tc := range []struct { - desc string - in []*modfile.Require - expected []*modfile.Require - }{ - { - desc: "no_duplicate", - in: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - expected: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - }, - { - desc: "one_duplicate", - in: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - expected: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - }, - }, - { - desc: "multiple_duplicate", - in: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.0.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.2.0", - }, - }, - }, - expected: []*modfile.Require{ - { - Mod: module.Version{ - Path: "x.y/z", - Version: "v1.1.0", - }, - }, - { - Mod: module.Version{ - Path: "x.y/w", - Version: "v1.2.0", - }, - }, - }, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - in := tc.in - removeRequireDups(&in) - - assert.Equal(t, tc.expected, in) - }) - } -} - func TestRemoveReplaceDups(t *testing.T) { for _, tc := range []struct { desc string diff --git a/gnovm/pkg/gnomod/read.go b/gnovm/pkg/gnomod/read.go index d6d771429d3..bb03ddf6efd 100644 --- a/gnovm/pkg/gnomod/read.go +++ b/gnovm/pkg/gnomod/read.go @@ -770,12 +770,6 @@ func parseReplace(filename string, line *modfile.Line, verb string, args []strin } nv := "" if len(args) == arrow+2 { - if !modfile.IsDirectoryPath(ns) { - if strings.Contains(ns, "@") { - return nil, errorf("replacement module must match format 'path version', not 'path@version'") - } - return nil, errorf("replacement module without version must be directory path (rooted or starting with . or ..)") - } if filepath.Separator == '/' && strings.Contains(ns, `\`) { return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)") } @@ -862,60 +856,6 @@ func updateLine(line *modfile.Line, tokens ...string) { line.Token = tokens } -// setIndirect sets line to have (or not have) a "// indirect" comment. -func setIndirect(r *modfile.Require, indirect bool) { - r.Indirect = indirect - line := r.Syntax - if isIndirect(line) == indirect { - return - } - if indirect { - // Adding comment. - if len(line.Suffix) == 0 { - // New comment. - line.Suffix = []modfile.Comment{{Token: "// indirect", Suffix: true}} - return - } - - com := &line.Suffix[0] - text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash))) - if text == "" { - // Empty comment. - com.Token = "// indirect" - return - } - - // Insert at beginning of existing comment. - com.Token = "// indirect; " + text - return - } - - // Removing comment. - f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) - if f == "indirect" { - // Remove whole comment. - line.Suffix = nil - return - } - - // Remove comment prefix. - com := &line.Suffix[0] - i := strings.Index(com.Token, "indirect;") - com.Token = "//" + com.Token[i+len("indirect;"):] -} - -// isIndirect reports whether line has a "// indirect" comment, -// meaning it is in go.mod only for its effect on indirect dependencies, -// so that it can be dropped entirely once the effective version of the -// indirect dependency reaches the given minimum version. -func isIndirect(line *modfile.Line) bool { - if len(line.Suffix) == 0 { - return false - } - f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) - return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;") -} - // addLine adds a line containing the given tokens to the file. // // If the first token of the hint matches the first token of the diff --git a/gnovm/pkg/gnomod/read_test.go b/gnovm/pkg/gnomod/read_test.go index cf3b6f59076..d9c35205a51 100644 --- a/gnovm/pkg/gnomod/read_test.go +++ b/gnovm/pkg/gnomod/read_test.go @@ -210,85 +210,6 @@ comments before "// e" } } -var addRequireTests = []struct { - desc string - in string - path string - vers string - out string -}{ - { - `existing`, - ` - module m - require x.y/z v1.2.3 - `, - "x.y/z", "v1.5.6", - ` - module m - require x.y/z v1.5.6 - `, - }, - { - `existing2`, - ` - module m - require ( - x.y/z v1.2.3 // first - x.z/a v0.1.0 // first-a - ) - require x.y/z v1.4.5 // second - require ( - x.y/z v1.6.7 // third - x.z/a v0.2.0 // third-a - ) - `, - "x.y/z", "v1.8.9", - ` - module m - - require ( - x.y/z v1.8.9 // first - x.z/a v0.1.0 // first-a - ) - - require x.z/a v0.2.0 // third-a - `, - }, - { - `new`, - ` - module m - require x.y/z v1.2.3 - `, - "x.y/w", "v1.5.6", - ` - module m - require ( - x.y/z v1.2.3 - x.y/w v1.5.6 - ) - `, - }, - { - `new2`, - ` - module m - require x.y/z v1.2.3 - require x.y/q/v2 v2.3.4 - `, - "x.y/w", "v1.5.6", - ` - module m - require x.y/z v1.2.3 - require ( - x.y/q/v2 v2.3.4 - x.y/w v1.5.6 - ) - `, - }, -} - var addModuleStmtTests = []struct { desc string in string @@ -299,12 +220,10 @@ var addModuleStmtTests = []struct { `existing`, ` module m - require x.y/z v1.2.3 `, "n", ` module n - require x.y/z v1.2.3 `, }, { @@ -330,7 +249,6 @@ var addReplaceTests = []struct { `replace_with_module`, ` module m - require x.y/z v1.2.3 `, "x.y/z", "v1.5.6", @@ -338,7 +256,6 @@ var addReplaceTests = []struct { "v1.5.6", ` module m - require x.y/z v1.2.3 replace x.y/z v1.5.6 => a.b/c v1.5.6 `, }, @@ -346,7 +263,6 @@ var addReplaceTests = []struct { `replace_with_dir`, ` module m - require x.y/z v1.2.3 `, "x.y/z", "v1.5.6", @@ -354,66 +270,11 @@ var addReplaceTests = []struct { "", ` module m - require x.y/z v1.2.3 replace x.y/z v1.5.6 => /path/to/dir `, }, } -var dropRequireTests = []struct { - desc string - in string - path string - out string -}{ - { - `existing`, - ` - module m - require x.y/z v1.2.3 - `, - "x.y/z", - ` - module m - `, - }, - { - `existing2`, - ` - module m - require ( - x.y/z v1.2.3 // first - x.z/a v0.1.0 // first-a - ) - require x.y/z v1.4.5 // second - require ( - x.y/z v1.6.7 // third - x.z/a v0.2.0 // third-a - ) - `, - "x.y/z", - ` - module m - - require x.z/a v0.1.0 // first-a - - require x.z/a v0.2.0 // third-a - `, - }, - { - `not_exists`, - ` - module m - require x.y/z v1.2.3 - `, - "a.b/c", - ` - module m - require x.y/z v1.2.3 - `, - }, -} - var dropReplaceTests = []struct { desc string in string @@ -425,7 +286,6 @@ var dropReplaceTests = []struct { `existing`, ` module m - require x.y/z v1.2.3 replace x.y/z v1.2.3 => a.b/c v1.5.6 `, @@ -433,14 +293,12 @@ var dropReplaceTests = []struct { "v1.2.3", ` module m - require x.y/z v1.2.3 `, }, { `not_exists`, ` module m - require x.y/z v1.2.3 replace x.y/z v1.2.3 => a.b/c v1.5.6 `, @@ -448,25 +306,12 @@ var dropReplaceTests = []struct { "v3.2.1", ` module m - require x.y/z v1.2.3 replace x.y/z v1.2.3 => a.b/c v1.5.6 `, }, } -func TestAddRequire(t *testing.T) { - for _, tt := range addRequireTests { - t.Run(tt.desc, func(t *testing.T) { - testEdit(t, tt.in, tt.out, func(f *File) error { - err := f.AddRequire(tt.path, tt.vers) - f.Syntax.Cleanup() - return err - }) - }) - } -} - func TestAddModuleStmt(t *testing.T) { for _, tt := range addModuleStmtTests { t.Run(tt.desc, func(t *testing.T) { @@ -491,18 +336,6 @@ func TestAddReplace(t *testing.T) { } } -func TestDropRequire(t *testing.T) { - for _, tt := range dropRequireTests { - t.Run(tt.desc, func(t *testing.T) { - testEdit(t, tt.in, tt.out, func(f *File) error { - err := f.DropRequire(tt.path) - f.Syntax.Cleanup() - return err - }) - }) - } -} - func TestDropReplace(t *testing.T) { for _, tt := range dropReplaceTests { t.Run(tt.desc, func(t *testing.T) { diff --git a/gnovm/pkg/packages/doc.go b/gnovm/pkg/packages/doc.go new file mode 100644 index 00000000000..fb63ae3838e --- /dev/null +++ b/gnovm/pkg/packages/doc.go @@ -0,0 +1,2 @@ +// Package packages provides utility functions to statically analyze Gno MemPackages +package packages diff --git a/gnovm/pkg/packages/imports.go b/gnovm/pkg/packages/imports.go new file mode 100644 index 00000000000..e72f37276db --- /dev/null +++ b/gnovm/pkg/packages/imports.go @@ -0,0 +1,72 @@ +package packages + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "sort" + "strconv" + "strings" + + "github.com/gnolang/gno/gnovm" +) + +// Imports returns the list of gno imports from a [gnovm.MemPackage]. +func Imports(pkg *gnovm.MemPackage) ([]string, error) { + allImports := make([]string, 0) + seen := make(map[string]struct{}) + for _, file := range pkg.Files { + if !strings.HasSuffix(file.Name, ".gno") { + continue + } + if strings.HasSuffix(file.Name, "_filetest.gno") { + continue + } + imports, _, err := FileImports(file.Name, file.Body) + if err != nil { + return nil, err + } + for _, im := range imports { + if im.Error != nil { + return nil, err + } + if _, ok := seen[im.PkgPath]; ok { + continue + } + allImports = append(allImports, im.PkgPath) + seen[im.PkgPath] = struct{}{} + } + } + sort.Strings(allImports) + + return allImports, nil +} + +type FileImport struct { + PkgPath string + Spec *ast.ImportSpec + Error error +} + +// FileImports returns the list of gno imports in the given file src. +// The given filename is only used when recording position information. +func FileImports(filename string, src string) ([]*FileImport, *token.FileSet, error) { + fs := token.NewFileSet() + f, err := parser.ParseFile(fs, filename, src, parser.ImportsOnly) + if err != nil { + return nil, nil, err + } + res := make([]*FileImport, len(f.Imports)) + for i, im := range f.Imports { + fi := FileImport{Spec: im} + importPath, err := strconv.Unquote(im.Path.Value) + if err != nil { + fi.Error = fmt.Errorf("%v: unexpected invalid import path: %v", fs.Position(im.Pos()).String(), im.Path.Value) + } else { + fi.PkgPath = importPath + } + res[i] = &fi + } + return res, fs, nil +} diff --git a/gnovm/pkg/packages/imports_test.go b/gnovm/pkg/packages/imports_test.go new file mode 100644 index 00000000000..14808dcbd6f --- /dev/null +++ b/gnovm/pkg/packages/imports_test.go @@ -0,0 +1,127 @@ +package packages + +import ( + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/stretchr/testify/require" +) + +func TestImports(t *testing.T) { + workingDir, err := os.Getwd() + require.NoError(t, err) + + // create external dir + tmpDir := t.TempDir() + + // cd to tmp directory + os.Chdir(tmpDir) + defer os.Chdir(workingDir) + + files := []struct { + name, data string + }{ + { + name: "file1.gno", + data: ` + package tmp + + import ( + "std" + + "gno.land/p/demo/pkg1" + ) + `, + }, + { + name: "file2.gno", + data: ` + package tmp + + import ( + "gno.land/p/demo/pkg1" + "gno.land/p/demo/pkg2" + ) + `, + }, + { + name: "file1_test.gno", + data: ` + package tmp + + import ( + "testing" + + "gno.land/p/demo/testpkg" + ) + `, + }, + { + name: "z_0_filetest.gno", + data: ` + package main + + import ( + "gno.land/p/demo/filetestpkg" + ) + `, + }, + + // subpkg files + { + name: filepath.Join("subtmp", "file1.gno"), + data: ` + package subtmp + + import ( + "std" + + "gno.land/p/demo/subpkg1" + ) + `, + }, + { + name: filepath.Join("subtmp", "file2.gno"), + data: ` + package subtmp + + import ( + "gno.land/p/demo/subpkg1" + "gno.land/p/demo/subpkg2" + ) + `, + }, + } + + // Expected list of imports + // - ignore subdirs + // - ignore duplicate + // - ignore *_filetest.gno + // - should be sorted + expected := []string{ + "gno.land/p/demo/pkg1", + "gno.land/p/demo/pkg2", + "gno.land/p/demo/testpkg", + "std", + "testing", + } + + // Create subpkg dir + err = os.Mkdir("subtmp", 0o700) + require.NoError(t, err) + + // Create files + for _, f := range files { + err = os.WriteFile(f.name, []byte(f.data), 0o644) + require.NoError(t, err) + } + + pkg, err := gnolang.ReadMemPackage(tmpDir, "test") + require.NoError(t, err) + imports, err := Imports(pkg) + require.NoError(t, err) + + require.Equal(t, expected, imports) +} diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index b57fc6388b1..731bf9756dd 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -4,18 +4,16 @@ 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" + "github.com/gnolang/gno/gnovm/pkg/packages" 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" @@ -45,7 +43,7 @@ func Store( 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) + memPkg := gno.MustReadMemPackage(baseDir, pkgPath) send := std.Coins{} ctx := Context(pkgPath, send) m2 := gno.NewMachineWithOptions(gno.MachineOptions{ @@ -137,7 +135,7 @@ func Store( // if examples package... examplePath := filepath.Join(rootDir, "examples", pkgPath) if osm.DirExists(examplePath) { - memPkg := gno.ReadMemPackage(examplePath, pkgPath) + memPkg := gno.MustReadMemPackage(examplePath, pkgPath) if memPkg.IsEmpty() { panic(fmt.Sprintf("found an empty package %q", pkgPath)) } @@ -193,7 +191,7 @@ func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gn return nil, nil } - memPkg := gno.ReadMemPackageFromList(files, pkgPath) + memPkg := gno.MustReadMemPackageFromList(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 @@ -241,24 +239,22 @@ func LoadImports(store gno.Store, filename string, content []byte) (err error) { } }() - fset := token.NewFileSet() - fl, err := parser.ParseFile(fset, filename, content, parser.ImportsOnly) + imports, fset, err := packages.FileImports(filename, string(content)) if err != nil { - return fmt.Errorf("parse failure: %w", err) + return 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) + for _, imp := range imports { + if imp.Error != nil { + return imp.Error } - if gno.IsRealmPath(impPath) { + if gno.IsRealmPath(imp.PkgPath) { // Don't eagerly load realms. // Realms persist state and can change the state of other realms in initialization. continue } - pkg := store.GetPackage(impPath, true) + pkg := store.GetPackage(imp.PkgPath, true) if pkg == nil { - return fmt.Errorf("%v: unknown import path %v", fset.Position(imp.Pos()).String(), impPath) + return fmt.Errorf("%v: unknown import path %v", fset.Position(imp.Spec.Pos()).String(), imp.PkgPath) } } return nil diff --git a/gnovm/tests/integ/invalid_module_version1/gno.mod b/gnovm/tests/integ/invalid_module_version1/gno.mod deleted file mode 100644 index e4c64e3106f..00000000000 --- a/gnovm/tests/integ/invalid_module_version1/gno.mod +++ /dev/null @@ -1,5 +0,0 @@ -module tmp - -require ( - "gno.land/p/demo/avl" //missing version -) diff --git a/gnovm/tests/integ/invalid_module_version2/gno.mod b/gnovm/tests/integ/invalid_module_version2/gno.mod deleted file mode 100644 index 0a3088b454a..00000000000 --- a/gnovm/tests/integ/invalid_module_version2/gno.mod +++ /dev/null @@ -1,5 +0,0 @@ -module tmp - -require ( - "gno.land/p/demo/avl" version-2 //invalid versioning -) diff --git a/gnovm/tests/integ/replace_with_dir/gno.mod b/gnovm/tests/integ/replace_with_dir/gno.mod index 6a7b1b664c8..69ae753a58a 100644 --- a/gnovm/tests/integ/replace_with_dir/gno.mod +++ b/gnovm/tests/integ/replace_with_dir/gno.mod @@ -1,9 +1,5 @@ module gno.land/tests/replaceavl -require ( - "gno.land/p/demo/notexists" v0.0.0 -) - replace ( "gno.land/p/demo/notexists" => /path/to/dir ) diff --git a/gnovm/tests/integ/replace_with_invalid_module/gno.mod b/gnovm/tests/integ/replace_with_invalid_module/gno.mod index ee90787ff0e..2a9527da7d6 100644 --- a/gnovm/tests/integ/replace_with_invalid_module/gno.mod +++ b/gnovm/tests/integ/replace_with_invalid_module/gno.mod @@ -1,9 +1,5 @@ module gno.land/tests/replaceavl -require ( - "gno.land/p/demo/avl" v0.0.0 -) - replace ( - "gno.land/p/demo/avl" => "gno.land/p/demo/avlll" v0.0.0 + "gno.land/p/demo/avl" => "gno.land/p/demo/notexists" ) diff --git a/gnovm/tests/integ/replace_with_invalid_module/main.gno b/gnovm/tests/integ/replace_with_invalid_module/main.gno new file mode 100644 index 00000000000..7f78497fa02 --- /dev/null +++ b/gnovm/tests/integ/replace_with_invalid_module/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "gno.land/p/demo/avl" +) + +var foo = avl.Bar diff --git a/gnovm/tests/integ/replace_with_module/gno.mod b/gnovm/tests/integ/replace_with_module/gno.mod index 09c77df7a95..de730c90a53 100644 --- a/gnovm/tests/integ/replace_with_module/gno.mod +++ b/gnovm/tests/integ/replace_with_module/gno.mod @@ -1,9 +1,5 @@ module gno.land/tests/replaceavl -require ( - "gno.land/p/demo/avl" v0.0.2 -) - replace ( - "gno.land/p/demo/avl" v0.0.2 => "gno.land/p/demo/avl" v1.0.0 + "gno.land/p/demo/avl" => "gno.land/p/demo/users" ) diff --git a/gnovm/tests/integ/replace_with_module/main.gno b/gnovm/tests/integ/replace_with_module/main.gno new file mode 100644 index 00000000000..7f78497fa02 --- /dev/null +++ b/gnovm/tests/integ/replace_with_module/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "gno.land/p/demo/avl" +) + +var foo = avl.Bar diff --git a/gnovm/tests/integ/require_invalid_module/gno.mod b/gnovm/tests/integ/require_invalid_module/gno.mod index f0b455f128b..f10dff8c8d5 100644 --- a/gnovm/tests/integ/require_invalid_module/gno.mod +++ b/gnovm/tests/integ/require_invalid_module/gno.mod @@ -1,5 +1 @@ -module gno.land/tests/reqinvalidmodule - -require ( - "gno.land/p/demo/notexists" v1.2.3 -) +module gno.land/tests/reqinvalidmodule \ No newline at end of file diff --git a/gnovm/tests/integ/require_invalid_module/main.gno b/gnovm/tests/integ/require_invalid_module/main.gno new file mode 100644 index 00000000000..703ec65ee5a --- /dev/null +++ b/gnovm/tests/integ/require_invalid_module/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "gno.land/p/demo/notexists" +) + +var foo = notexists.Bar diff --git a/gnovm/tests/integ/require_remote_module/gno.mod b/gnovm/tests/integ/require_remote_module/gno.mod index 4823c72585d..946f41398ba 100644 --- a/gnovm/tests/integ/require_remote_module/gno.mod +++ b/gnovm/tests/integ/require_remote_module/gno.mod @@ -1,5 +1 @@ module gno.land/tests/importavl - -require ( - "gno.land/p/demo/avl" v0.0.0 -) diff --git a/gnovm/tests/integ/require_std_lib/gno.mod b/gnovm/tests/integ/require_std_lib/gno.mod new file mode 100644 index 00000000000..f10dff8c8d5 --- /dev/null +++ b/gnovm/tests/integ/require_std_lib/gno.mod @@ -0,0 +1 @@ +module gno.land/tests/reqinvalidmodule \ No newline at end of file diff --git a/gnovm/tests/integ/require_std_lib/main.gno b/gnovm/tests/integ/require_std_lib/main.gno new file mode 100644 index 00000000000..920d238cccc --- /dev/null +++ b/gnovm/tests/integ/require_std_lib/main.gno @@ -0,0 +1,7 @@ +package main + +import ( + "std" +) + +var foo std.Address diff --git a/gnovm/tests/integ/valid2/gno.mod b/gnovm/tests/integ/valid2/gno.mod index 98a5a0dacc1..3eaaa374994 100644 --- a/gnovm/tests/integ/valid2/gno.mod +++ b/gnovm/tests/integ/valid2/gno.mod @@ -1,3 +1 @@ module gno.land/p/integ/valid - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/misc/loop/go.mod b/misc/loop/go.mod index f1c09cd9f82..a6bbdad3c82 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -73,7 +73,6 @@ require ( golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect From 66c2eb6041f6970d616822e58dff00ac70d0fdce Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Sun, 8 Dec 2024 05:01:51 +0800 Subject: [PATCH 47/86] fix(gnovm): make `Stacktrace` correctly handle panics with `len(Stmts) == 0` (#3273) While finalizing the realm, all statements in the machine have been popped out. A necessary check must be performed during stack trace handling.
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
Co-authored-by: Petar Dambovaliev --- gnovm/pkg/gnolang/machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 4f4c7c188f3..b48c0742e6f 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -401,7 +401,7 @@ func destar(x Expr) Expr { // 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) { - if len(m.Frames) == 0 { + if len(m.Frames) == 0 || len(m.Stmts) == 0 { return } From ca1df8689774cbfcb7b714ed6ac407d0163129d2 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:23:10 +0100 Subject: [PATCH 48/86] ci: use go.mod to determine go version (#3279) Related to https://github.com/gnolang/gno/pull/3229#discussion_r1862370753 This PR replaces most fixed versions of Go in the CI workflows with retrieving the version from the relevant go.mod file. For workflows that do not have an associated go.mod file, the go.mod file at the root of the repository is used. All `*_template.yml` workflows seem designed to use a fixed version of go and do not allow passing a go.mod file to the `setup-go` action. Achieving this would require a more significant refactor.
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
Co-authored-by: Morgan --- .github/workflows/benchmark-master-push.yml | 2 +- .github/workflows/dependabot-tidy.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/genesis-verify.yml | 2 +- .github/workflows/gh-pages.yml | 2 +- .github/workflows/releaser-master.yml | 2 +- .github/workflows/releaser-nightly.yml | 2 +- .github/workflows/releaser.yml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/benchmark-master-push.yml b/.github/workflows/benchmark-master-push.yml index bde6e623a88..622baefc0de 100644 --- a/.github/workflows/benchmark-master-push.yml +++ b/.github/workflows/benchmark-master-push.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod - name: Run benchmark # add more benchmarks by adding additional lines for different packages; diff --git a/.github/workflows/dependabot-tidy.yml b/.github/workflows/dependabot-tidy.yml index 59e9e1c8146..39fed8b0172 100644 --- a/.github/workflows/dependabot-tidy.yml +++ b/.github/workflows/dependabot-tidy.yml @@ -20,7 +20,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: go.mod - name: Tidy all Go mods env: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 262b341276c..c9d9af0fb6f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version-file: go.mod - name: Install dependencies run: go mod download diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml index f870cd0658c..1288d588100 100644 --- a/.github/workflows/genesis-verify.yml +++ b/.github/workflows/genesis-verify.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.22" + go-version-file: contribs/gnogenesis/go.mod - name: Build gnogenesis run: make -C contribs/gnogenesis diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index a8407f57291..1b955b52cd0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod - run: "cd misc/gendocs && make install gen" - uses: actions/configure-pages@v5 id: pages diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index 36a709a242a..3d194e2cb4c 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - uses: sigstore/cosign-installer@v3.7.0 diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index e9a5c15a22d..4308f1c4a7d 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - uses: sigstore/cosign-installer@v3.7.0 diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index d33432bd16d..309664bdcce 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - uses: sigstore/cosign-installer@v3.7.0 From 3de2475bc1d2b12a86e68b150d6d0224909a47da Mon Sep 17 00:00:00 2001 From: Denys Sedchenko Date: Sat, 7 Dec 2024 16:31:18 -0500 Subject: [PATCH 49/86] feat(examples/mux): support query string in path (#3281) This PR adds support for request URLs with query strings for `p/demo/mux` package. Previously, `mux.Router` would fail to find a correct handler if request URL contains query string. ```go r := mux.NewRouter() r.HandleFunc("hello", func (rw *mux.ResponseWriter, req *mux.Request) { ... }) reqUrl := "hello?foo=bar" r.Render(reqUrl) // Fails ``` This PR fixes this behavior and introduces a new `mux.Request.RawPath` field which contains a raw request path including query string. The `RawPath` field is designed to be used for packages like `p/demo/avl/pager` to extract query params from a string.\ The `Path` field, as before, contains just path segment of request, without query strings.
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
CC @moul @thehowl @jeronimoalbi --------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Morgan --- examples/gno.land/p/demo/mux/request.gno | 10 ++- examples/gno.land/p/demo/mux/router.gno | 15 +++- examples/gno.land/p/demo/mux/router_test.gno | 89 +++++++++++++++----- gnovm/cmd/gno/download_deps_test.go | 2 +- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/examples/gno.land/p/demo/mux/request.gno b/examples/gno.land/p/demo/mux/request.gno index f7996fe40fe..7b5b74da91b 100644 --- a/examples/gno.land/p/demo/mux/request.gno +++ b/examples/gno.land/p/demo/mux/request.gno @@ -4,7 +4,15 @@ import "strings" // Request represents an incoming request. type Request struct { - Path string + // Path is request path name. + // + // Note: use RawPath to obtain a raw path with query string. + Path string + + // RawPath contains a whole request path, including query string. + RawPath string + + // HandlerPath is handler rule that matches a request. HandlerPath string } diff --git a/examples/gno.land/p/demo/mux/router.gno b/examples/gno.land/p/demo/mux/router.gno index a2efb3a4ebf..fe6bf70abdf 100644 --- a/examples/gno.land/p/demo/mux/router.gno +++ b/examples/gno.land/p/demo/mux/router.gno @@ -18,7 +18,8 @@ func NewRouter() *Router { // Render renders the output for the given path using the registered route handler. func (r *Router) Render(reqPath string) string { - reqParts := strings.Split(reqPath, "/") + clearPath := stripQueryString(reqPath) + reqParts := strings.Split(clearPath, "/") for _, route := range r.routes { patParts := strings.Split(route.Pattern, "/") @@ -45,7 +46,8 @@ func (r *Router) Render(reqPath string) string { } if match { req := &Request{ - Path: reqPath, + Path: clearPath, + RawPath: reqPath, HandlerPath: route.Pattern, } res := &ResponseWriter{} @@ -66,3 +68,12 @@ func (r *Router) HandleFunc(pattern string, fn HandlerFunc) { route := Handler{Pattern: pattern, Fn: fn} r.routes = append(r.routes, route) } + +func stripQueryString(reqPath string) string { + i := strings.Index(reqPath, "?") + if i == -1 { + return reqPath + } + + return reqPath[:i] +} diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index 13fd5b97955..cc6aad62146 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -1,34 +1,85 @@ package mux -import "testing" +import ( + "testing" -func TestRouter_Render(t *testing.T) { - // Define handlers and route configuration - router := NewRouter() - router.HandleFunc("hello/{name}", func(res *ResponseWriter, req *Request) { - name := req.GetVar("name") - if name != "" { - res.Write("Hello, " + name + "!") - } else { - res.Write("Hello, world!") - } - }) - router.HandleFunc("hi", func(res *ResponseWriter, req *Request) { - res.Write("Hi, earth!") - }) + "gno.land/p/demo/uassert" +) +func TestRouter_Render(t *testing.T) { cases := []struct { + label string path string expectedOutput string + setupHandler func(t *testing.T, r *Router) }{ - {"hello/Alice", "Hello, Alice!"}, - {"hi", "Hi, earth!"}, - {"hello/Bob", "Hello, Bob!"}, + { + label: "route with named parameter", + path: "hello/Alice", + expectedOutput: "Hello, Alice!", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/{name}", func(rw *ResponseWriter, req *Request) { + name := req.GetVar("name") + uassert.Equal(t, "Alice", name) + rw.Write("Hello, " + name + "!") + }) + }, + }, + { + label: "static route", + path: "hi", + expectedOutput: "Hi, earth!", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hi", func(rw *ResponseWriter, req *Request) { + uassert.Equal(t, req.Path, "hi") + rw.Write("Hi, earth!") + }) + }, + }, + { + label: "route with named parameter and query string", + path: "hello/foo/bar?foo=bar&baz", + expectedOutput: "foo bar", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/{key}/{val}", func(rw *ResponseWriter, req *Request) { + key := req.GetVar("key") + val := req.GetVar("val") + uassert.Equal(t, "foo", key) + uassert.Equal(t, "bar", val) + uassert.Equal(t, "hello/foo/bar?foo=bar&baz", req.RawPath) + uassert.Equal(t, "hello/foo/bar", req.Path) + rw.Write(key + " " + val) + }) + }, + }, + { + // TODO: finalize how router should behave with double slash in path. + label: "double slash in nested route", + path: "a/foo//", + expectedOutput: "test foo", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("a/{key}", func(rw *ResponseWriter, req *Request) { + // Assert not called + uassert.False(t, true, "unexpected handler called") + }) + + r.HandleFunc("a/{key}/{val}/", func(rw *ResponseWriter, req *Request) { + key := req.GetVar("key") + val := req.GetVar("val") + uassert.Equal(t, key, "foo") + uassert.Empty(t, val) + rw.Write("test " + key) + }) + }, + }, + // TODO: {"hello", "Hello, world!"}, // TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc } for _, tt := range cases { - t.Run(tt.path, func(t *testing.T) { + t.Run(tt.label, func(t *testing.T) { + router := NewRouter() + tt.setupHandler(t, router) output := router.Render(tt.path) if output != tt.expectedOutput { t.Errorf("Expected output %q, but got %q", tt.expectedOutput, output) diff --git a/gnovm/cmd/gno/download_deps_test.go b/gnovm/cmd/gno/download_deps_test.go index 3ccfdb0055e..0828e9b2245 100644 --- a/gnovm/cmd/gno/download_deps_test.go +++ b/gnovm/cmd/gno/download_deps_test.go @@ -60,7 +60,7 @@ func TestDownloadDeps(t *testing.T) { }, }, }, - requirements: []string{"avl", "blog", "ufmt", "mux"}, + requirements: []string{"avl", "blog", "diff", "uassert", "ufmt", "mux"}, ioErrContains: []string{ "gno: downloading gno.land/p/demo/blog", "gno: downloading gno.land/p/demo/avl", From 727868728204f1a4b46e94a86e7ea524be8a3741 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:57:14 +0100 Subject: [PATCH 50/86] docs: hyperlink buttons demo (#3245) ## Description Adds a `r/docs` that showcases how "buttons" can be added to realm renders.
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 --- examples/gno.land/r/docs/buttons/buttons.gno | 44 +++++++++++++++++++ .../gno.land/r/docs/buttons/buttons_test.gno | 14 ++++++ examples/gno.land/r/docs/buttons/gno.mod | 1 + examples/gno.land/r/docs/docs.gno | 1 + 4 files changed, 60 insertions(+) create mode 100644 examples/gno.land/r/docs/buttons/buttons.gno create mode 100644 examples/gno.land/r/docs/buttons/buttons_test.gno create mode 100644 examples/gno.land/r/docs/buttons/gno.mod diff --git a/examples/gno.land/r/docs/buttons/buttons.gno b/examples/gno.land/r/docs/buttons/buttons.gno new file mode 100644 index 00000000000..cb050b1bc38 --- /dev/null +++ b/examples/gno.land/r/docs/buttons/buttons.gno @@ -0,0 +1,44 @@ +package buttons + +import ( + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +var ( + motd = "The Initial Message\n\n" + lastCaller std.Address +) + +func UpdateMOTD(newmotd string) { + motd = newmotd + lastCaller = std.PrevRealm().Addr() +} + +func Render(path string) string { + if path == "motd" { + out := "# Message of the Day:\n\n" + out += "---\n\n" + out += "# " + motd + "\n\n" + out += "---\n\n" + link := txlink.Call("UpdateMOTD", "newmotd", "Message!") // "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + out += ufmt.Sprintf("Click **[here](%s)** to update the Message of The Day!\n\n", link) + out += "[Go back to home page](/r/docs/buttons)\n\n" + out += "Last updated by " + lastCaller.String() + + return out + } + + out := `# Buttons + +Users can create simple hyperlink buttons to view specific realm pages and +do specific realm actions, such as calling a specific function with some arguments. + +The foundation for this functionality are markdown links; for example, you can +click... +` + "\n## [here](/r/docs/buttons:motd)\n" + `...to view this realm's message of the day.` + + return out +} diff --git a/examples/gno.land/r/docs/buttons/buttons_test.gno b/examples/gno.land/r/docs/buttons/buttons_test.gno new file mode 100644 index 00000000000..2903fa1a858 --- /dev/null +++ b/examples/gno.land/r/docs/buttons/buttons_test.gno @@ -0,0 +1,14 @@ +package buttons + +import ( + "strings" + "testing" +) + +func TestRenderMotdLink(t *testing.T) { + res := Render("motd") + const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + if !strings.Contains(res, wantLink) { + t.Fatalf("%s\ndoes not contain correct help page link: %s", res, wantLink) + } +} diff --git a/examples/gno.land/r/docs/buttons/gno.mod b/examples/gno.land/r/docs/buttons/gno.mod new file mode 100644 index 00000000000..43cc2d773da --- /dev/null +++ b/examples/gno.land/r/docs/buttons/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/buttons diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index 57d020cd737..28bac4171b5 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -11,6 +11,7 @@ Explore various examples to learn more about Gno functionality and usage. - [Hello World](/r/docs/hello) - A simple introductory example. - [Adder](/r/docs/adder) - An interactive example to update a number with transactions. - [Source](/r/docs/source) - View realm source code. +- [Buttons](/r/docs/buttons) - Add buttons to your realm's render. - [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. - ... From 918c9ab88d320a1a01404d91f7131063a4bf54f4 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:21:32 +0100 Subject: [PATCH 51/86] chore(examples): update userbook example to use avl_pager (#3251) ## Description Updates the `r/demo/userbook` example to use the `avl_pager` package.
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
--------- Co-authored-by: Morgan --- examples/gno.land/r/demo/userbook/render.gno | 51 ++++++ .../gno.land/r/demo/userbook/userbook.gno | 148 +++--------------- .../r/demo/userbook/userbook_test.gno | 79 ---------- 3 files changed, 69 insertions(+), 209 deletions(-) create mode 100644 examples/gno.land/r/demo/userbook/render.gno delete mode 100644 examples/gno.land/r/demo/userbook/userbook_test.gno diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno new file mode 100644 index 00000000000..22d7f97eabd --- /dev/null +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -0,0 +1,51 @@ +// Package userbook demonstrates a small userbook system working with gnoweb +package userbook + +import ( + "sort" + "strconv" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +func Render(path string) string { + p := pager.NewPager(signupsTree, 2) + page := p.MustGetPageByPath(path) + + out := "# Welcome to UserBook!\n\n" + + out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp")) + out += "---\n\n" + + var sorted sortedSignups + for _, item := range page.Items { + sorted = append(sorted, item.Value.(*Signup)) + } + + sort.Sort(sorted) + + for _, item := range sorted { + out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", item.ordinal, item.address.String(), item.timestamp.Format("02-01-2006 15:04:05")) + } + + out += "---\n\n" + out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n" + out += page.Selector() // Repeat selector for ease of navigation + return out +} + +type sortedSignups []*Signup + +func (s sortedSignups) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s sortedSignups) Len() int { + return len(s) +} + +func (s sortedSignups) Less(i, j int) bool { + return s[i].timestamp.Before(s[j].timestamp) +} diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno index c49bd90fa42..c958dc9e5b0 100644 --- a/examples/gno.land/r/demo/userbook/userbook.gno +++ b/examples/gno.land/r/demo/userbook/userbook.gno @@ -1,158 +1,46 @@ -// This realm demonstrates a small userbook system working with gnoweb +// Package userbook demonstrates a small userbook system working with gnoweb package userbook import ( "std" - "strconv" + "time" "gno.land/p/demo/avl" - "gno.land/p/demo/mux" "gno.land/p/demo/ufmt" ) type Signup struct { - account string - height int64 + address std.Address + ordinal int + timestamp time.Time } -// signups - keep a slice of signed up addresses efficient pagination -var signups []Signup +var signupsTree = avl.NewTree() -// tracker - keep track of who signed up -var ( - tracker *avl.Tree - router *mux.Router -) - -const ( - defaultPageSize = 20 - pathArgument = "number" - subPath = "page/{" + pathArgument + "}" - signUpEvent = "SignUp" -) +const signUpEvent = "SignUp" func init() { - // Set up tracker tree - tracker = avl.NewTree() - - // Set up route handling - router = mux.NewRouter() - router.HandleFunc("", renderHelper) - router.HandleFunc(subPath, renderHelper) - - // Sign up the deployer - SignUp() + SignUp() // Sign up the deployer } func SignUp() string { // Get transaction caller - caller := std.PrevRealm().Addr().String() - height := std.GetHeight() + caller := std.PrevRealm().Addr() // Check if the user is already signed up - if _, exists := tracker.Get(caller); exists { + if _, exists := signupsTree.Get(caller.String()); exists { panic(caller + " is already signed up!") } + now := time.Now() // Sign up the user - tracker.Set(caller, struct{}{}) - signup := Signup{ - caller, - height, - } - - signups = append(signups, signup) - std.Emit(signUpEvent, "SignedUpAccount", signup.account) + signupsTree.Set(caller.String(), &Signup{ + std.PrevRealm().Addr(), + signupsTree.Size(), + now, + }) - return ufmt.Sprintf("%s added to userbook up at block #%d!", signup.account, signup.height) -} - -func GetSignupsInRange(page, pageSize int) ([]Signup, int) { - if page < 1 { - panic("page number cannot be less than 1") - } - - if pageSize < 1 || pageSize > 50 { - panic("page size must be from 1 to 50") - } - - // Pagination - // Calculate indexes - startIndex := (page - 1) * pageSize - endIndex := startIndex + pageSize - - // If page does not contain any users - if startIndex >= len(signups) { - return nil, -1 - } - - // If page contains fewer users than the page size - if endIndex > len(signups) { - endIndex = len(signups) - } - - return signups[startIndex:endIndex], endIndex -} - -func renderHelper(res *mux.ResponseWriter, req *mux.Request) { - totalSignups := len(signups) - res.Write("# Welcome to UserBook!\n\n") - - // Get URL parameter - page, err := strconv.Atoi(req.GetVar("number")) - if err != nil { - page = 1 // render first page on bad input - } - - // Fetch paginated signups - fetchedSignups, endIndex := GetSignupsInRange(page, defaultPageSize) - // Handle empty page case - if len(fetchedSignups) == 0 { - res.Write("No users on this page!\n\n") - res.Write("---\n\n") - res.Write("[Back to Page #1](/r/demo/userbook:page/1)\n\n") - return - } - - // Write page title - res.Write(ufmt.Sprintf("## UserBook - Page #%d:\n\n", page)) - - // Write signups - pageStartIndex := defaultPageSize * (page - 1) - for i, signup := range fetchedSignups { - out := ufmt.Sprintf("#### User #%d - %s - signed up at Block #%d\n", pageStartIndex+i, signup.account, signup.height) - res.Write(out) - } - - res.Write("---\n\n") - - // Write UserBook info - latestSignupIndex := totalSignups - 1 - res.Write(ufmt.Sprintf("#### Total users: %d\n", totalSignups)) - res.Write(ufmt.Sprintf("#### Latest signup: User #%d at Block #%d\n", latestSignupIndex, signups[latestSignupIndex].height)) - - res.Write("---\n\n") - - // Write page number - res.Write(ufmt.Sprintf("You're viewing page #%d", page)) - - // Write navigation buttons - var prevPage string - var nextPage string - // If we are on any page that is not the first page - if page > 1 { - prevPage = ufmt.Sprintf(" - [Previous page](/r/demo/userbook:page/%d)", page-1) - } - - // If there are more pages after the current one - if endIndex < totalSignups { - nextPage = ufmt.Sprintf(" - [Next page](/r/demo/userbook:page/%d)\n\n", page+1) - } - - res.Write(prevPage) - res.Write(nextPage) -} + std.Emit(signUpEvent, "SignedUpAccount", caller.String()) -func Render(path string) string { - return router.Render(path) + return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z)) } diff --git a/examples/gno.land/r/demo/userbook/userbook_test.gno b/examples/gno.land/r/demo/userbook/userbook_test.gno deleted file mode 100644 index 8d10d381e08..00000000000 --- a/examples/gno.land/r/demo/userbook/userbook_test.gno +++ /dev/null @@ -1,79 +0,0 @@ -package userbook - -import ( - "std" - "strings" - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" -) - -func TestRender(t *testing.T) { - // Sign up 20 users + deployer - for i := 0; i < 20; i++ { - addrName := ufmt.Sprintf("test%d", i) - caller := testutils.TestAddress(addrName) - std.TestSetOrigCaller(caller) - SignUp() - } - - testCases := []struct { - name string - nextPage bool - prevPage bool - path string - expectedNumberOfUsers int - }{ - { - name: "1st page render", - nextPage: true, - prevPage: false, - path: "page/1", - expectedNumberOfUsers: 20, - }, - { - name: "2nd page render", - nextPage: false, - prevPage: true, - path: "page/2", - expectedNumberOfUsers: 1, - }, - { - name: "Invalid path render", - nextPage: true, - prevPage: false, - path: "page/invalidtext", - expectedNumberOfUsers: 20, - }, - { - name: "Empty Page", - nextPage: false, - prevPage: false, - path: "page/1000", - expectedNumberOfUsers: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := Render(tc.path) - numUsers := countUsers(got) - - if tc.prevPage && !strings.Contains(got, "Previous page") { - t.Fatalf("expected to find Previous page, didn't find it") - } - if tc.nextPage && !strings.Contains(got, "Next page") { - t.Fatalf("expected to find Next page, didn't find it") - } - - if tc.expectedNumberOfUsers != numUsers { - t.Fatalf("expected %d, got %d users", tc.expectedNumberOfUsers, numUsers) - } - }) - } -} - -func countUsers(input string) int { - return strings.Count(input, "#### User #") -} From 61a5c020ccf28ce1877aa783b264a6bb6f482fd2 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:33:10 +0100 Subject: [PATCH 52/86] feat: add `p/moul/{md,debug,web25}` + update `r/moul/home` (#2819) Related with https://hackmd.io/a8k09_TeQUu6WawvkpDDpw?both - [x] `p/moul/web25` - displays a link suggesting to view the realm from an external webui, but avoid recursive printing when seen from the external webui - [x] `p/moul/md` - minimal `markdown` helpers library - [x] `p/moul/debug` - displays useful informations when adding `?debug=1` - [x] update `r/moul/home` - [x] markdown table with links example - [x] svg example - [x] use of the new `p/moul/...` packages - [x] Release a static web25 interface somewhere (https://github.com/moul/gno-moul-home-web25) --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 12 +- contribs/gnodev/cmd/gnodev/setup_web.go | 1 + examples/gno.land/p/moul/debug/debug.gno | 92 +++++++ examples/gno.land/p/moul/debug/gno.mod | 1 + .../gno.land/p/moul/debug/z1_filetest.gno | 31 +++ .../gno.land/p/moul/debug/z2_filetest.gno | 37 +++ examples/gno.land/p/moul/md/gno.mod | 1 + examples/gno.land/p/moul/md/md.gno | 242 ++++++++++++++++++ examples/gno.land/p/moul/md/md_test.gno | 88 +++++++ examples/gno.land/p/moul/md/z1_filetest.gno | 87 +++++++ examples/gno.land/p/moul/web25/gno.mod | 1 + examples/gno.land/p/moul/web25/web25.gno | 51 ++++ examples/gno.land/p/moul/web25/web25_test.gno | 1 + .../gno.land/r/moul/config/config_test.gno | 1 + examples/gno.land/r/moul/home/home.gno | 83 ++++-- examples/gno.land/r/moul/home/z1_filetest.gno | 24 +- examples/gno.land/r/moul/home/z2_filetest.gno | 63 ++++- 17 files changed, 778 insertions(+), 38 deletions(-) create mode 100644 examples/gno.land/p/moul/debug/debug.gno create mode 100644 examples/gno.land/p/moul/debug/gno.mod create mode 100644 examples/gno.land/p/moul/debug/z1_filetest.gno create mode 100644 examples/gno.land/p/moul/debug/z2_filetest.gno create mode 100644 examples/gno.land/p/moul/md/gno.mod create mode 100644 examples/gno.land/p/moul/md/md.gno create mode 100644 examples/gno.land/p/moul/md/md_test.gno create mode 100644 examples/gno.land/p/moul/md/z1_filetest.gno create mode 100644 examples/gno.land/p/moul/web25/gno.mod create mode 100644 examples/gno.land/p/moul/web25/web25.gno create mode 100644 examples/gno.land/p/moul/web25/web25_test.gno create mode 100644 examples/gno.land/r/moul/config/config_test.gno diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index c9d6487d753..082d0cb8270 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -59,6 +59,7 @@ type devCfg struct { // Web Configuration webListenerAddr string webRemoteHelperAddr string + webWithHTML bool // Node Configuration minimal bool @@ -126,14 +127,21 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { &c.webListenerAddr, "web-listener", defaultDevOptions.webListenerAddr, - "web server listener address", + "gnoweb: web server listener address", ) fs.StringVar( &c.webRemoteHelperAddr, "web-help-remote", defaultDevOptions.webRemoteHelperAddr, - "web server help page's remote addr (default to )", + "gnoweb: web server help page's remote addr (default to )", + ) + + fs.BoolVar( + &c.webWithHTML, + "web-with-html", + defaultDevOptions.webWithHTML, + "gnoweb: enable HTML parsing in markdown rendering", ) fs.StringVar( diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index 635c27af19d..d55814142a6 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -15,6 +15,7 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) htt webConfig.HelpChainID = cfg.chainId webConfig.RemoteAddr = dnode.GetRemoteAddress() webConfig.HelpRemote = cfg.webRemoteHelperAddr + webConfig.WithHTML = cfg.webWithHTML // If `HelpRemote` is empty default it to `RemoteAddr` if webConfig.HelpRemote == "" { diff --git a/examples/gno.land/p/moul/debug/debug.gno b/examples/gno.land/p/moul/debug/debug.gno new file mode 100644 index 00000000000..9ba3dd36a98 --- /dev/null +++ b/examples/gno.land/p/moul/debug/debug.gno @@ -0,0 +1,92 @@ +// Package debug provides utilities for logging and displaying debug information +// within Gno realms. It supports conditional rendering of logs and metadata, +// toggleable via query parameters. +// +// Key Features: +// - Log collection and display using Markdown formatting. +// - Metadata display for realm path, address, and height. +// - Collapsible debug section for cleaner presentation. +// - Query-based debug toggle using `?debug=1`. +package debug + +import ( + "std" + "time" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/realmpath" +) + +// Debug encapsulates debug information, including logs and metadata. +type Debug struct { + Logs []string + HideMetadata bool +} + +// Log appends a new line of debug information to the Logs slice. +func (d *Debug) Log(line string) { + d.Logs = append(d.Logs, line) +} + +// Render generates the debug content as a collapsible Markdown section. +// It conditionally renders logs and metadata if enabled via the `?debug=1` query parameter. +func (d Debug) Render(path string) string { + if realmpath.Parse(path).Query.Get("debug") != "1" { + return "" + } + + var content string + + if d.Logs != nil { + content += md.H3("Logs") + content += md.BulletList(d.Logs) + } + + if !d.HideMetadata { + content += md.H3("Metadata") + table := mdtable.Table{ + Headers: []string{"Key", "Value"}, + } + table.Append([]string{"`std.CurrentRealm().PkgPath()`", string(std.CurrentRealm().PkgPath())}) + table.Append([]string{"`std.CurrentRealm().Addr()`", string(std.CurrentRealm().Addr())}) + table.Append([]string{"`std.PrevRealm().PkgPath()`", string(std.PrevRealm().PkgPath())}) + table.Append([]string{"`std.PrevRealm().Addr()`", string(std.PrevRealm().Addr())}) + table.Append([]string{"`std.GetHeight()`", ufmt.Sprintf("%d", std.GetHeight())}) + table.Append([]string{"`time.Now().Format(time.RFC3339)`", time.Now().Format(time.RFC3339)}) + content += table.String() + } + + if content == "" { + return "" + } + + return md.CollapsibleSection("debug", content) +} + +// Render displays metadata about the current realm but does not display logs. +// This function uses a default Debug struct with metadata enabled and no logs. +func Render(path string) string { + return Debug{}.Render(path) +} + +// IsEnabled checks if the `?debug=1` query parameter is set in the given path. +// Returns true if debugging is enabled, otherwise false. +func IsEnabled(path string) bool { + req := realmpath.Parse(path) + return req.Query.Get("debug") == "1" +} + +// ToggleURL modifies the given path's query string to toggle the `?debug=1` parameter. +// If debugging is currently enabled, it removes the parameter. +// If debugging is disabled, it adds the parameter. +func ToggleURL(path string) string { + req := realmpath.Parse(path) + if IsEnabled(path) { + req.Query.Del("debug") + } else { + req.Query.Add("debug", "1") + } + return req.String() +} diff --git a/examples/gno.land/p/moul/debug/gno.mod b/examples/gno.land/p/moul/debug/gno.mod new file mode 100644 index 00000000000..eb48ed292ca --- /dev/null +++ b/examples/gno.land/p/moul/debug/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/debug diff --git a/examples/gno.land/p/moul/debug/z1_filetest.gno b/examples/gno.land/p/moul/debug/z1_filetest.gno new file mode 100644 index 00000000000..8203749d3c7 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z1_filetest.gno @@ -0,0 +1,31 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + println("---") + println(debug.Render("")) + println("---") + println(debug.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/debug/z2_filetest.gno b/examples/gno.land/p/moul/debug/z2_filetest.gno new file mode 100644 index 00000000000..32c2fe49951 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z2_filetest.gno @@ -0,0 +1,37 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + var d debug.Debug + d.Log("hello world!") + d.Log("foobar") + println("---") + println(d.Render("")) + println("---") + println(d.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Logs +// - hello world! +// - foobar +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/md/gno.mod b/examples/gno.land/p/moul/md/gno.mod new file mode 100644 index 00000000000..55d124d9e6b --- /dev/null +++ b/examples/gno.land/p/moul/md/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/md diff --git a/examples/gno.land/p/moul/md/md.gno b/examples/gno.land/p/moul/md/md.gno new file mode 100644 index 00000000000..61d6948b997 --- /dev/null +++ b/examples/gno.land/p/moul/md/md.gno @@ -0,0 +1,242 @@ +// Package md provides helper functions for generating Markdown content programmatically. +// +// It includes utilities for text formatting, creating lists, blockquotes, code blocks, +// links, images, and more. +// +// Highlights: +// - Supports basic Markdown syntax such as bold, italic, strikethrough, headers, and lists. +// - Manages multiline support in lists (e.g., bullet, ordered, and todo lists). +// - Includes advanced helpers like inline images with links and nested list prefixes. +package md + +import ( + "strconv" + "strings" +) + +// Bold returns bold text for markdown. +// Example: Bold("foo") => "**foo**" +func Bold(text string) string { + return "**" + text + "**" +} + +// Italic returns italicized text for markdown. +// Example: Italic("foo") => "*foo*" +func Italic(text string) string { + return "*" + text + "*" +} + +// Strikethrough returns strikethrough text for markdown. +// Example: Strikethrough("foo") => "~~foo~~" +func Strikethrough(text string) string { + return "~~" + text + "~~" +} + +// H1 returns a level 1 header for markdown. +// Example: H1("foo") => "# foo\n" +func H1(text string) string { + return "# " + text + "\n" +} + +// H2 returns a level 2 header for markdown. +// Example: H2("foo") => "## foo\n" +func H2(text string) string { + return "## " + text + "\n" +} + +// H3 returns a level 3 header for markdown. +// Example: H3("foo") => "### foo\n" +func H3(text string) string { + return "### " + text + "\n" +} + +// H4 returns a level 4 header for markdown. +// Example: H4("foo") => "#### foo\n" +func H4(text string) string { + return "#### " + text + "\n" +} + +// H5 returns a level 5 header for markdown. +// Example: H5("foo") => "##### foo\n" +func H5(text string) string { + return "##### " + text + "\n" +} + +// H6 returns a level 6 header for markdown. +// Example: H6("foo") => "###### foo\n" +func H6(text string) string { + return "###### " + text + "\n" +} + +// BulletList returns a bullet list for markdown. +// Example: BulletList([]string{"foo", "bar"}) => "- foo\n- bar\n" +func BulletList(items []string) string { + var sb strings.Builder + for _, item := range items { + sb.WriteString(BulletItem(item)) + } + return sb.String() +} + +// BulletItem returns a bullet item for markdown. +// Example: BulletItem("foo") => "- foo\n" +func BulletItem(item string) string { + var sb strings.Builder + lines := strings.Split(item, "\n") + sb.WriteString("- " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// OrderedList returns an ordered list for markdown. +// Example: OrderedList([]string{"foo", "bar"}) => "1. foo\n2. bar\n" +func OrderedList(items []string) string { + var sb strings.Builder + for i, item := range items { + lines := strings.Split(item, "\n") + sb.WriteString(strconv.Itoa(i+1) + ". " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + } + return sb.String() +} + +// TodoList returns a list of todo items with checkboxes for markdown. +// Example: TodoList([]string{"foo", "bar\nmore bar"}, []bool{true, false}) => "- [x] foo\n- [ ] bar\n more bar\n" +func TodoList(items []string, done []bool) string { + var sb strings.Builder + for i, item := range items { + sb.WriteString(TodoItem(item, done[i])) + } + return sb.String() +} + +// TodoItem returns a todo item with checkbox for markdown. +// Example: TodoItem("foo", true) => "- [x] foo\n" +func TodoItem(item string, done bool) string { + var sb strings.Builder + checkbox := " " + if done { + checkbox = "x" + } + lines := strings.Split(item, "\n") + sb.WriteString("- [" + checkbox + "] " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// Nested prefixes each line with a given prefix, enabling nested lists. +// Example: Nested("- foo\n- bar", " ") => " - foo\n - bar\n" +func Nested(content, prefix string) string { + lines := strings.Split(content, "\n") + for i := range lines { + if strings.TrimSpace(lines[i]) != "" { + lines[i] = prefix + lines[i] + } + } + return strings.Join(lines, "\n") +} + +// Blockquote returns a blockquote for markdown. +// Example: Blockquote("foo\nbar") => "> foo\n> bar\n" +func Blockquote(text string) string { + lines := strings.Split(text, "\n") + var sb strings.Builder + for _, line := range lines { + sb.WriteString("> " + line + "\n") + } + return sb.String() +} + +// InlineCode returns inline code for markdown. +// Example: InlineCode("foo") => "`foo`" +func InlineCode(code string) string { + return "`" + strings.ReplaceAll(code, "`", "\\`") + "`" +} + +// CodeBlock creates a markdown code block. +// Example: CodeBlock("foo") => "```\nfoo\n```" +func CodeBlock(content string) string { + return "```\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting. +// Example: LanguageCodeBlock("go", "foo") => "```go\nfoo\n```" +func LanguageCodeBlock(language, content string) string { + return "```" + language + "\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// HorizontalRule returns a horizontal rule for markdown. +// Example: HorizontalRule() => "---\n" +func HorizontalRule() string { + return "---\n" +} + +// Link returns a hyperlink for markdown. +// Example: Link("foo", "http://example.com") => "[foo](http://example.com)" +func Link(text, url string) string { + return "[" + EscapeText(text) + "](" + url + ")" +} + +// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown. +// Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[![alt text](image-url)](link-url)" +func InlineImageWithLink(altText, imageUrl, linkUrl string) string { + return "[" + Image(altText, imageUrl) + "](" + linkUrl + ")" +} + +// Image returns an image for markdown. +// Example: Image("foo", "http://example.com") => "![foo](http://example.com)" +func Image(altText, url string) string { + return "![" + EscapeText(altText) + "](" + url + ")" +} + +// Footnote returns a footnote for markdown. +// Example: Footnote("foo", "bar") => "[foo]: bar" +func Footnote(reference, text string) string { + return "[" + EscapeText(reference) + "]: " + text +} + +// Paragraph wraps the given text in a Markdown paragraph. +// Example: Paragraph("foo") => "foo\n" +func Paragraph(content string) string { + return content + "\n\n" +} + +// CollapsibleSection creates a collapsible section for markdown using +// HTML
and tags. +// Example: +// CollapsibleSection("Click to expand", "Hidden content") +// => +//
Click to expand +// +// Hidden content +//
+func CollapsibleSection(title, content string) string { + return "
" + EscapeText(title) + "\n\n" + content + "\n
\n" +} + +// EscapeText escapes special Markdown characters in regular text where needed. +func EscapeText(text string) string { + replacer := strings.NewReplacer( + `*`, `\*`, + `_`, `\_`, + `[`, `\[`, + `]`, `\]`, + `(`, `\(`, + `)`, `\)`, + `~`, `\~`, + `>`, `\>`, + `|`, `\|`, + `-`, `\-`, + `+`, `\+`, + ".", `\.`, + "!", `\!`, + "`", "\\`", + ) + return replacer.Replace(text) +} diff --git a/examples/gno.land/p/moul/md/md_test.gno b/examples/gno.land/p/moul/md/md_test.gno new file mode 100644 index 00000000000..144ae58d918 --- /dev/null +++ b/examples/gno.land/p/moul/md/md_test.gno @@ -0,0 +1,88 @@ +package md + +import ( + "testing" + + "gno.land/p/moul/md" +) + +func TestHelpers(t *testing.T) { + tests := []struct { + name string + function func() string + expected string + }{ + {"Bold", func() string { return md.Bold("foo") }, "**foo**"}, + {"Italic", func() string { return md.Italic("foo") }, "*foo*"}, + {"Strikethrough", func() string { return md.Strikethrough("foo") }, "~~foo~~"}, + {"H1", func() string { return md.H1("foo") }, "# foo\n"}, + {"HorizontalRule", md.HorizontalRule, "---\n"}, + {"InlineCode", func() string { return md.InlineCode("foo") }, "`foo`"}, + {"CodeBlock", func() string { return md.CodeBlock("foo") }, "```\nfoo\n```"}, + {"LanguageCodeBlock", func() string { return md.LanguageCodeBlock("go", "foo") }, "```go\nfoo\n```"}, + {"Link", func() string { return md.Link("foo", "http://example.com") }, "[foo](http://example.com)"}, + {"Image", func() string { return md.Image("foo", "http://example.com") }, "![foo](http://example.com)"}, + {"InlineImageWithLink", func() string { return md.InlineImageWithLink("alt", "image-url", "link-url") }, "[![alt](image-url)](link-url)"}, + {"Footnote", func() string { return md.Footnote("foo", "bar") }, "[foo]: bar"}, + {"Paragraph", func() string { return md.Paragraph("foo") }, "foo\n\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.function() + if result != tt.expected { + t.Errorf("%s() = %q, want %q", tt.name, result, tt.expected) + } + }) + } +} + +func TestLists(t *testing.T) { + t.Run("BulletList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "- foo\n- bar\n" + result := md.BulletList(items) + if result != expected { + t.Errorf("BulletList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("OrderedList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "1. foo\n2. bar\n" + result := md.OrderedList(items) + if result != expected { + t.Errorf("OrderedList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("TodoList", func(t *testing.T) { + items := []string{"foo", "bar\nmore bar"} + done := []bool{true, false} + expected := "- [x] foo\n- [ ] bar\n more bar\n" + result := md.TodoList(items, done) + if result != expected { + t.Errorf("TodoList(%q, %q) = %q, want %q", items, done, result, expected) + } + }) +} + +func TestNested(t *testing.T) { + t.Run("Nested Single Level", func(t *testing.T) { + content := "- foo\n- bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) + + t.Run("Nested Double Level", func(t *testing.T) { + content := " - foo\n - bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) +} diff --git a/examples/gno.land/p/moul/md/z1_filetest.gno b/examples/gno.land/p/moul/md/z1_filetest.gno new file mode 100644 index 00000000000..077e1732bcb --- /dev/null +++ b/examples/gno.land/p/moul/md/z1_filetest.gno @@ -0,0 +1,87 @@ +package main + +import "gno.land/p/moul/md" + +func main() { + println(md.H1("Header 1")) + println(md.H2("Header 2")) + println(md.H3("Header 3")) + println(md.H4("Header 4")) + println(md.H5("Header 5")) + println(md.H6("Header 6")) + println(md.Bold("bold")) + println(md.Italic("italic")) + println(md.Strikethrough("strikethrough")) + println(md.BulletList([]string{ + "Item 1", + "Item 2\nMore details for item 2", + })) + println(md.OrderedList([]string{"Step 1", "Step 2"})) + println(md.TodoList([]string{"Task 1", "Task 2\nSubtask 2"}, []bool{true, false})) + println(md.Nested(md.BulletList([]string{"Parent Item", md.OrderedList([]string{"Child 1", "Child 2"})}), " ")) + println(md.Blockquote("This is a blockquote\nSpanning multiple lines")) + println(md.InlineCode("inline `code`")) + println(md.CodeBlock("line1\nline2")) + println(md.LanguageCodeBlock("go", "func main() {\nprintln(\"Hello, world!\")\n}")) + println(md.HorizontalRule()) + println(md.Link("Gno", "http://gno.land")) + println(md.Image("Alt Text", "http://example.com/image.png")) + println(md.InlineImageWithLink("Alt Text", "http://example.com/image.png", "http://example.com")) + println(md.Footnote("ref", "This is a footnote")) + println(md.Paragraph("This is a paragraph.")) +} + +// Output: +// # Header 1 +// +// ## Header 2 +// +// ### Header 3 +// +// #### Header 4 +// +// ##### Header 5 +// +// ###### Header 6 +// +// **bold** +// *italic* +// ~~strikethrough~~ +// - Item 1 +// - Item 2 +// More details for item 2 +// +// 1. Step 1 +// 2. Step 2 +// +// - [x] Task 1 +// - [ ] Task 2 +// Subtask 2 +// +// - Parent Item +// - 1. Child 1 +// 2. Child 2 +// +// +// > This is a blockquote +// > Spanning multiple lines +// +// `inline \`code\`` +// ``` +// line1 +// line2 +// ``` +// ```go +// func main() { +// println("Hello, world!") +// } +// ``` +// --- +// +// [Gno](http://gno.land) +// ![Alt Text](http://example.com/image.png) +// [![Alt Text](http://example.com/image.png)](http://example.com) +// [ref]: This is a footnote +// This is a paragraph. +// +// diff --git a/examples/gno.land/p/moul/web25/gno.mod b/examples/gno.land/p/moul/web25/gno.mod new file mode 100644 index 00000000000..f27bc793bf7 --- /dev/null +++ b/examples/gno.land/p/moul/web25/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/web25 diff --git a/examples/gno.land/p/moul/web25/web25.gno b/examples/gno.land/p/moul/web25/web25.gno new file mode 100644 index 00000000000..46d564b70ad --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25.gno @@ -0,0 +1,51 @@ +// Pacakge web25 provides an opinionated way to register an external web2 +// frontend to provide a "better" web2.5 experience. +package web25 + +import ( + "strings" + + "gno.land/p/moul/realmpath" +) + +type Config struct { + CID string + URL string + Text string +} + +func (c *Config) SetRemoteFrontendByURL(url string) { + c.CID = "" + c.URL = url +} + +func (c *Config) SetRemoteFrontendByCID(cid string) { + c.CID = cid + c.URL = "" +} + +func (c Config) GetLink() string { + if c.CID != "" { + return "https://ipfs.io/ipfs/" + c.CID + } + return c.URL +} + +const DefaultText = "Click [here]({link}) to visit the full rendering experience.\n" + +// Render displays a frontend link at the top of your realm's Render function in +// a concistent way to help gno visitors to have a consistent experience. +// +// if query is not nil, then it will check if it's not disable by ?no-web25, so +// that you can call the render function from an external point of view. +func (c Config) Render(path string) string { + if realmpath.Parse(path).Query.Get("no-web25") == "1" { + return "" + } + text := c.Text + if text == "" { + text = DefaultText + } + text = strings.ReplaceAll(text, "{link}", c.GetLink()) + return text +} diff --git a/examples/gno.land/p/moul/web25/web25_test.gno b/examples/gno.land/p/moul/web25/web25_test.gno new file mode 100644 index 00000000000..6d58a586595 --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25_test.gno @@ -0,0 +1 @@ +package web25 diff --git a/examples/gno.land/r/moul/config/config_test.gno b/examples/gno.land/r/moul/config/config_test.gno new file mode 100644 index 00000000000..d912156bec0 --- /dev/null +++ b/examples/gno.land/r/moul/config/config_test.gno @@ -0,0 +1 @@ +package config diff --git a/examples/gno.land/r/moul/home/home.gno b/examples/gno.land/r/moul/home/home.gno index 140e7b5e0c8..1094ce29cc5 100644 --- a/examples/gno.land/r/moul/home/home.gno +++ b/examples/gno.land/r/moul/home/home.gno @@ -1,14 +1,23 @@ package home import ( + "strconv" + + "gno.land/p/demo/svg" + "gno.land/p/moul/debug" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/txlink" + "gno.land/p/moul/web25" "gno.land/r/leon/hof" "gno.land/r/moul/config" ) var ( - todos []string - status string - memeImgURL string + todos []string + status string + memeImgURL string + web25config = web25.Config{URL: "https://moul.github.io/gno-moul-home-web25/"} ) func init() { @@ -19,36 +28,74 @@ func init() { } func Render(path string) string { - content := "# Manfred's (gn)home Dashboard\n\n" + content := web25config.Render(path) + var d debug.Debug + + content += md.H1("Manfred's (gn)home Dashboard") - content += "## Meme\n" - content += "![](" + memeImgURL + ")\n\n" + content += md.H2("Meme") + content += md.Paragraph( + md.Image("meme", memeImgURL), + ) - content += "## Status\n" - content += status + "\n\n" + content += md.H2("Status") + content += md.Paragraph(status) + content += md.Paragraph(md.Link("update", txlink.Call("UpdateStatus"))) - content += "## Personal ToDo List\n" - for _, todo := range todos { - content += "- [ ] " + todo + "\n" + d.Log("hello world!") + + content += md.H2("Personal TODO List (bullet list)") + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("x", txlink.Call("DeleteTodo", "idx", idstr)) + content += md.BulletItem(todo + " " + deleteLink) } - content += "\n" + content += md.BulletItem(md.Link("[new]", txlink.Call("AddTodo"))) + + content += md.H2("Personal TODO List (table)") + table := mdtable.Table{ + Headers: []string{"ID", "Item", "Links"}, + } + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("[del]", txlink.Call("DeleteTodo", "idx", idstr)) + table.Append([]string{"#" + idstr, todo, deleteLink}) + } + content += table.String() + + content += md.H2("SVG Example") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph(svg.Canvas{ + Width: 500, Height: 500, + Elems: []svg.Elem{ + svg.Rectangle{50, 50, 100, 100, "red"}, + svg.Circle{50, 50, 100, "red"}, + svg.Text{100, 100, "hello world!", "magenta"}, + }, + }.String()) - // TODO: Implement a feature to list replies on r/boards on my posts - // TODO: Maybe integrate a calendar feature for upcoming events? + content += md.H2("Debug") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph( + md.Link("toggle debug", debug.ToggleURL(path)), + ) + // TODO: my r/boards posts + // TODO: my r/events events + content += d.Render(path) return content } -func AddNewTodo(todo string) { +func AddTodo(todo string) { config.AssertIsAdmin() todos = append(todos, todo) } -func DeleteTodo(todoIndex int) { +func DeleteTodo(idx int) { config.AssertIsAdmin() - if todoIndex >= 0 && todoIndex < len(todos) { + if idx >= 0 && idx < len(todos) { // Remove the todo from the list by merging slices from before and after the todo - todos = append(todos[:todoIndex], todos[todoIndex+1:]...) + todos = append(todos[:idx], todos[idx+1:]...) } else { panic("Invalid todo index") } diff --git a/examples/gno.land/r/moul/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno index b26c919dd3a..b9d7d91a702 100644 --- a/examples/gno.land/r/moul/home/z1_filetest.gno +++ b/examples/gno.land/r/moul/home/z1_filetest.gno @@ -7,15 +7,31 @@ func main() { } // Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. // # Manfred's (gn)home Dashboard -// // ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) +// ![meme](https://i.imgflip.com/7ze8dc.jpg) // // ## Status // Online // -// ## Personal ToDo List -// - [ ] fill this todo list... +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:?debug=1) // // diff --git a/examples/gno.land/r/moul/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno index 489dc2aeecd..f471280d8ef 100644 --- a/examples/gno.land/r/moul/home/z2_filetest.gno +++ b/examples/gno.land/r/moul/home/z2_filetest.gno @@ -8,30 +8,65 @@ import ( func main() { std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - home.AddNewTodo("aaa") - home.AddNewTodo("bbb") - home.AddNewTodo("ccc") - home.AddNewTodo("ddd") - home.AddNewTodo("eee") + home.AddTodo("aaa") + home.AddTodo("bbb") + home.AddTodo("ccc") + home.AddTodo("ddd") + home.AddTodo("eee") home.UpdateStatus("Lorem Ipsum") home.DeleteTodo(3) - println(home.Render("")) + println(home.Render("?debug=1")) } // Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. // # Manfred's (gn)home Dashboard -// // ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) +// ![meme](https://i.imgflip.com/7ze8dc.jpg) // // ## Status // Lorem Ipsum // -// ## Personal ToDo List -// - [ ] fill this todo list... -// - [ ] aaa -// - [ ] bbb -// - [ ] ddd -// - [ ] eee +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - aaa [x](/r/moul/home$help&func=DeleteTodo&idx=1) +// - bbb [x](/r/moul/home$help&func=DeleteTodo&idx=2) +// - ddd [x](/r/moul/home$help&func=DeleteTodo&idx=3) +// - eee [x](/r/moul/home$help&func=DeleteTodo&idx=4) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// | #1 | aaa | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=1) | +// | #2 | bbb | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=2) | +// | #3 | ddd | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=3) | +// | #4 | eee | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=4) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:) +// +//
debug +// +// ### Logs +// - hello world! +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | gno.land/r/moul/home | +// | `std.CurrentRealm().Addr()` | g1h8h57ntxadcze3f703skymfzdwa6t3ugf0nq3z | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1manfred47kzduec920z88wfr64ylksmdcedlf5 | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | // +//
// From 052a2a1cfdeddbb8a3c6ac291917226aaf46491e Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:06:50 +0100 Subject: [PATCH 53/86] feat(examples): add avl_pager reverse option, update userbook (#3297) ## Description This PR adds the reverse option in the avl_pager package, and updates the rendering in the `r/demo/userbook` realm.
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/avl/pager/pager.gno | 24 +++-- .../gno.land/p/demo/avl/pager/pager_test.gno | 93 +++++++++++++------ .../gno.land/p/demo/avl/pager/z_filetest.gno | 4 +- examples/gno.land/r/demo/userbook/render.gno | 35 +++---- .../gno.land/r/demo/userbook/userbook.gno | 20 ++-- examples/gno.land/r/demo/users/users.gno | 4 +- .../gno.land/r/docs/avl_pager/avl_pager.gno | 6 +- examples/gno.land/r/leon/hof/render.gno | 4 +- 8 files changed, 115 insertions(+), 75 deletions(-) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index 60bb44d97b6..cccdc0df645 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -15,6 +15,7 @@ type Pager struct { PageQueryParam string SizeQueryParam string DefaultPageSize int + Reversed bool } // Page represents a single page of results. @@ -36,12 +37,13 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree *avl.Tree, defaultPageSize int) *Pager { +func NewPager(tree *avl.Tree, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page", SizeQueryParam: "size", DefaultPageSize: defaultPageSize, + Reversed: reversed, } } @@ -86,10 +88,18 @@ func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { } items := []Item{} - p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { - items = append(items, Item{Key: key, Value: value}) - return false - }) + + if p.Reversed { + p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } else { + p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } page.Items = items page.PageNumber = pageNumber @@ -115,8 +125,8 @@ func (p *Pager) GetPageByPath(rawURL string) (*Page, error) { return p.GetPageWithSize(pageNumber, pageSize), nil } -// UI generates the Markdown UI for the page selector. -func (p *Page) Selector() string { +// Picker generates the Markdown UI for the page Picker +func (p *Page) Picker() string { pageNumber := p.PageNumber pageNumber = max(pageNumber, 1) diff --git a/examples/gno.land/p/demo/avl/pager/pager_test.gno b/examples/gno.land/p/demo/avl/pager/pager_test.gno index da4680db8c7..9869924e5b5 100644 --- a/examples/gno.land/p/demo/avl/pager/pager_test.gno +++ b/examples/gno.land/p/demo/avl/pager/pager_test.gno @@ -18,34 +18,67 @@ func TestPager_GetPage(t *testing.T) { tree.Set("d", 4) tree.Set("e", 5) - // Create a new pager. - pager := NewPager(tree, 10) + t.Run("normal ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, + {3, 2, []Item{{Key: "e", Value: 5}}}, + {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {2, 5, []Item{}}, + } - // Define test cases. - tests := []struct { - pageNumber int - pageSize int - expected []Item - }{ - {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, - {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, - {3, 2, []Item{{Key: "e", Value: 5}}}, - {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, - {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {2, 5, []Item{}}, - } + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - for _, tt := range tests { - page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + uassert.Equal(t, len(tt.expected), len(page.Items)) - uassert.Equal(t, len(tt.expected), len(page.Items)) + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } + } + }) + + t.Run("reversed ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, true) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "b", Value: 2}}}, + {3, 2, []Item{{Key: "a", Value: 1}}}, + {1, 3, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {1, 5, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}, {Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {2, 5, []Item{}}, + } - for i, item := range page.Items { - uassert.Equal(t, tt.expected[i].Key, item.Key) - uassert.Equal(t, tt.expected[i].Value, item.Value) + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + uassert.Equal(t, len(tt.expected), len(page.Items)) + + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } } - } + }) } func TestPager_GetPageByPath(t *testing.T) { @@ -56,7 +89,7 @@ func TestPager_GetPageByPath(t *testing.T) { } // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { @@ -80,7 +113,7 @@ func TestPager_GetPageByPath(t *testing.T) { } } -func TestPage_Selector(t *testing.T) { +func TestPage_Picker(t *testing.T) { // Create a new AVL tree and populate it with some key-value pairs. tree := avl.NewTree() tree.Set("a", 1) @@ -90,7 +123,7 @@ func TestPage_Selector(t *testing.T) { tree.Set("e", 5) // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { @@ -106,7 +139,7 @@ func TestPage_Selector(t *testing.T) { for _, tt := range tests { page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - ui := page.Selector() + ui := page.Picker() uassert.Equal(t, tt.expected, ui) } } @@ -119,7 +152,7 @@ func TestPager_UI_WithManyPages(t *testing.T) { } // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases for a large number of pages. tests := []struct { @@ -145,7 +178,7 @@ func TestPager_UI_WithManyPages(t *testing.T) { for _, tt := range tests { page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - ui := page.Selector() + ui := page.Picker() uassert.Equal(t, tt.expected, ui) } } @@ -160,7 +193,7 @@ func TestPager_ParseQuery(t *testing.T) { tree.Set("e", 5) // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { 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 17029f57861..6342888d6b4 100644 --- a/examples/gno.land/p/demo/avl/pager/z_filetest.gno +++ b/examples/gno.land/p/demo/avl/pager/z_filetest.gno @@ -16,7 +16,7 @@ func main() { } // Create a new pager. - pager := pager.NewPager(tree, 7) + pager := pager.NewPager(tree, 7, false) for pn := -1; pn < 8; pn++ { page := pager.GetPage(pn) @@ -25,7 +25,7 @@ func main() { for idx, item := range page.Items { println(ufmt.Sprintf("- idx=%d key=%s value=%d", idx, item.Key, item.Value)) } - println(page.Selector()) + println(page.Picker()) println() } } diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno index 22d7f97eabd..94f7567cbf4 100644 --- a/examples/gno.land/r/demo/userbook/render.gno +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -2,16 +2,19 @@ package userbook import ( - "sort" "strconv" + "gno.land/r/demo/users" + "gno.land/p/demo/avl/pager" "gno.land/p/demo/ufmt" "gno.land/p/moul/txlink" ) +const usersLink = "/r/demo/users" + func Render(path string) string { - p := pager.NewPager(signupsTree, 2) + p := pager.NewPager(signupsTree, 20, true) page := p.MustGetPageByPath(path) out := "# Welcome to UserBook!\n\n" @@ -19,33 +22,19 @@ func Render(path string) string { out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp")) out += "---\n\n" - var sorted sortedSignups for _, item := range page.Items { - sorted = append(sorted, item.Value.(*Signup)) - } + signup := item.Value.(*Signup) + user := signup.address.String() - sort.Sort(sorted) + if data := users.GetUserByAddress(signup.address); data != nil { + user = ufmt.Sprintf("[%s](%s:%s)", data.Name, usersLink, data.Name) + } - for _, item := range sorted { - out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", item.ordinal, item.address.String(), item.timestamp.Format("02-01-2006 15:04:05")) + out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", signup.ordinal, user, signup.timestamp.Format("January 2 2006, 03:04:04 PM")) } out += "---\n\n" out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n" - out += page.Selector() // Repeat selector for ease of navigation + out += page.Picker() return out } - -type sortedSignups []*Signup - -func (s sortedSignups) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s sortedSignups) Len() int { - return len(s) -} - -func (s sortedSignups) Less(i, j int) bool { - return s[i].timestamp.Before(s[j].timestamp) -} diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno index c958dc9e5b0..03027f064b0 100644 --- a/examples/gno.land/r/demo/userbook/userbook.gno +++ b/examples/gno.land/r/demo/userbook/userbook.gno @@ -6,6 +6,7 @@ import ( "time" "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" ) @@ -15,7 +16,11 @@ type Signup struct { timestamp time.Time } -var signupsTree = avl.NewTree() +var ( + signupsTree = avl.NewTree() + tracker = avl.NewTree() + idCounter seqid.ID +) const signUpEvent = "SignUp" @@ -28,19 +33,22 @@ func SignUp() string { caller := std.PrevRealm().Addr() // Check if the user is already signed up - if _, exists := signupsTree.Get(caller.String()); exists { - panic(caller + " is already signed up!") + if _, exists := tracker.Get(caller.String()); exists { + panic(caller.String() + " is already signed up!") } now := time.Now() + // Sign up the user - signupsTree.Set(caller.String(), &Signup{ - std.PrevRealm().Addr(), + signupsTree.Set(idCounter.Next().String(), &Signup{ + caller, signupsTree.Size(), now, }) - std.Emit(signUpEvent, "SignedUpAccount", caller.String()) + tracker.Set(caller.String(), struct{}{}) + + std.Emit(signUpEvent, "account", caller.String()) return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z)) } diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 1f08c9ae08c..8547a6e60e0 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -328,14 +328,14 @@ func Render(fullPath string) string { func renderHome(path string) string { doc := "" - page := pager.NewPager(&name2User, 50).MustGetPageByPath(path) + page := pager.NewPager(&name2User, 50, false).MustGetPageByPath(path) for _, item := range page.Items { user := item.Value.(*users.User) doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" } doc += "\n" - doc += page.Selector() + doc += page.Picker() return doc } diff --git a/examples/gno.land/r/docs/avl_pager/avl_pager.gno b/examples/gno.land/r/docs/avl_pager/avl_pager.gno index 75807b71981..af8a6a10b48 100644 --- a/examples/gno.land/r/docs/avl_pager/avl_pager.gno +++ b/examples/gno.land/r/docs/avl_pager/avl_pager.gno @@ -22,19 +22,19 @@ func init() { // Render paginated content based on the given URL path. // URL format: `...?page=&size=` (default is page 1 and size 10). func Render(path string) string { - p := pager.NewPager(tree, 10) // Default page size is 10 + p := pager.NewPager(tree, 10, false) // Default page size is 10 page := p.MustGetPageByPath(path) // Header and pagination info result := "# Paginated Items\n" result += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" - result += page.Selector() + "\n\n" + result += page.Picker() + "\n\n" // Display items on the current page for _, item := range page.Items { result += "- " + item.Key + ": " + item.Value.(string) + "\n" } - result += "\n" + page.Selector() // Repeat selector for ease of navigation + result += "\n" + page.Picker() // Repeat page picker for ease of navigation return result } diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index b4d51d03362..0721c7d6e72 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -38,7 +38,7 @@ func (e Exhibition) Render(path string, dashboard bool) string { out += "
\n\n" - page := pager.NewPager(e.itemsSorted, pageSize).MustGetPageByPath(path) + page := pager.NewPager(e.itemsSorted, pageSize, false).MustGetPageByPath(path) for i := len(page.Items) - 1; i >= 0; i-- { item := page.Items[i] @@ -52,7 +52,7 @@ func (e Exhibition) Render(path string, dashboard bool) string { out += "
\n\n" - out += page.Selector() + out += page.Picker() return out } From e35fc9a1c6f5e561660a29988bfc3eefa9280c4f Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:32:23 +0100 Subject: [PATCH 54/86] docs: add `std.ChainDomain()` reference (#3301) ## Description Adds the reference docs on `std.ChainDomain()`. Co-authored-by: Antoine Eddi <5222525+aeddi@users.noreply.github.com> --- docs/reference/stdlibs/std/chain.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 0e5ead338c5..b1791e65608 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -28,6 +28,18 @@ std.AssertOriginCall() ``` --- +## ChainDomain +```go +func ChainDomain() string +``` +Returns the chain domain. Currently only `gno.land` is supported. + +#### Usage +```go +domain := std.ChainDomain() // gno.land +``` +--- + ## Emit ```go func Emit(typ string, attrs ...string) From e46f457e70ac2881a8d2c0e3fe625ca9ecdda183 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:51:01 +0100 Subject: [PATCH 55/86] chore: remove PR template (#3300) See https://github.com/gnolang/gno/issues/3238#issuecomment-2526138055 Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/pull_request_template.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 12e07a9cde6..00000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,11 +0,0 @@ - - -
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 -
From 666c54af3d4a7cf354546028de89ec99fe1ce984 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Sun, 8 Dec 2024 19:20:00 +0100 Subject: [PATCH 56/86] chore: import gnolang/overflow into gno/tm2/pkg/overflow (#3302) We remove the dependency on gnolang/overflow, a clone of https://github.com/JohnCGriffin/overflow which is now unmaintained, and import it into gno/tm2/pkg/overflow. We can now have full control on it, fix it and improve it. This PR just changes the import path, no content change is done yet. --------- Co-authored-by: Morgan Bazalgette --- contribs/gnodev/go.mod | 1 - contribs/gnodev/go.sum | 2 - contribs/gnofaucet/go.mod | 1 - contribs/gnofaucet/go.sum | 2 - contribs/gnogenesis/go.mod | 1 - contribs/gnogenesis/go.sum | 2 - contribs/gnohealth/go.mod | 1 - contribs/gnohealth/go.sum | 2 - contribs/gnokeykc/go.mod | 1 - contribs/gnokeykc/go.sum | 2 - contribs/gnomigrate/go.mod | 1 - contribs/gnomigrate/go.sum | 2 - gnovm/pkg/gnolang/machine.go | 3 +- go.mod | 1 - go.sum | 2 - misc/autocounterd/go.mod | 1 - misc/autocounterd/go.sum | 2 - misc/loop/go.mod | 1 - misc/loop/go.sum | 4 - tm2/pkg/overflow/README.md | 66 +++++ tm2/pkg/overflow/overflow.go | 131 ++++++++++ tm2/pkg/overflow/overflow_impl.go | 360 ++++++++++++++++++++++++++ tm2/pkg/overflow/overflow_template.sh | 112 ++++++++ tm2/pkg/overflow/overflow_test.go | 115 ++++++++ tm2/pkg/std/coin.go | 2 +- tm2/pkg/store/gas/store.go | 2 +- tm2/pkg/store/types/gas.go | 2 +- tm2/pkg/store/types/gas_test.go | 2 +- 28 files changed, 789 insertions(+), 35 deletions(-) create mode 100644 tm2/pkg/overflow/README.md create mode 100644 tm2/pkg/overflow/overflow.go create mode 100644 tm2/pkg/overflow/overflow_impl.go create mode 100755 tm2/pkg/overflow/overflow_template.sh create mode 100644 tm2/pkg/overflow/overflow_test.go diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index a315d88591c..2053a61db6c 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -50,7 +50,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index e38c3621483..f9250d34462 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -101,8 +101,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index c5bb1ad0d81..eab9fc90c50 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -20,7 +20,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index f4bdc65d7ec..aabe858e893 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -49,8 +49,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gnolang/faucet v0.3.2 h1:3QBrdmnQszRaAZbxgO5xDDm3czNa0L/RFmhnCkbxy5I= github.com/gnolang/faucet v0.3.2/go.mod h1:/wbw9h4ooMzzyNBuM0X+ol7CiPH2OFjAFF3bYAXqA7U= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index b777cc6e5eb..f1b316c2bee 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -18,7 +18,6 @@ require ( github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index 3c6127ac216..7ba3aede534 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod index e6d9f119c7b..4f5862a0d2e 100644 --- a/contribs/gnohealth/go.mod +++ b/contribs/gnohealth/go.mod @@ -12,7 +12,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index 116cfbff021..dd287d9ca84 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -45,8 +45,6 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 0c794afd54c..479daed22f6 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -21,7 +21,6 @@ require ( github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 50eb5add218..cacf6788d45 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -54,8 +54,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index a81c2de4ba0..cd31adc4f6f 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -17,7 +17,6 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index 3c6127ac216..7ba3aede534 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index b48c0742e6f..a497648dbc8 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -11,10 +11,9 @@ import ( "strings" "sync" - "github.com/gnolang/overflow" - "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/gnolang/gno/tm2/pkg/store" ) diff --git a/go.mod b/go.mod index f73ba1926e6..f389e60b988 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fortytw2/leaktest v1.3.0 - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 github.com/google/gofuzz v1.2.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum index 78d60eeea90..b987535607e 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/misc/autocounterd/go.mod b/misc/autocounterd/go.mod index 5de1d3c2974..30a6f23b458 100644 --- a/misc/autocounterd/go.mod +++ b/misc/autocounterd/go.mod @@ -14,7 +14,6 @@ require ( github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum index b34cbde0c00..5d624ca18cb 100644 --- a/misc/autocounterd/go.sum +++ b/misc/autocounterd/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/misc/loop/go.mod b/misc/loop/go.mod index a6bbdad3c82..70e9d21734b 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -29,7 +29,6 @@ require ( github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 740cc629a21..8e0feb11e4a 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -68,8 +68,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/gnolang/tx-archive v0.4.0 h1:+1Rgo0U0HjLQLq/xqeGdJwtAzo9xWj09t1oZLvrL3bU= github.com/gnolang/tx-archive v0.4.0/go.mod h1:seKHGnvxUnDgH/mSsCEdwG0dHY/FrpbUm6Hd0+KMd9w= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -258,8 +256,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tm2/pkg/overflow/README.md b/tm2/pkg/overflow/README.md new file mode 100644 index 00000000000..55a9ba4c327 --- /dev/null +++ b/tm2/pkg/overflow/README.md @@ -0,0 +1,66 @@ +# overflow + +Check for int/int8/int16/int64/int32 integer overflow in Golang arithmetic. + +Forked from https://github.com/JohnCGriffin/overflow + +### Install +``` +go get github.com/johncgriffin/overflow +``` +Note that because Go has no template types, the majority of repetitive code is +generated by overflow_template.sh. If you have to change an +algorithm, change it there and regenerate the Go code via: +``` +go generate +``` +### Synopsis + +``` +package main + +import "fmt" +import "math" +import "github.com/JohnCGriffin/overflow" + +func main() { + + addend := math.MaxInt64 - 5 + + for i := 0; i < 10; i++ { + sum, ok := overflow.Add(addend, i) + fmt.Printf("%v+%v -> (%v,%v)\n", + addend, i, sum, ok) + } + +} +``` +yields the output +``` +9223372036854775802+0 -> (9223372036854775802,true) +9223372036854775802+1 -> (9223372036854775803,true) +9223372036854775802+2 -> (9223372036854775804,true) +9223372036854775802+3 -> (9223372036854775805,true) +9223372036854775802+4 -> (9223372036854775806,true) +9223372036854775802+5 -> (9223372036854775807,true) +9223372036854775802+6 -> (0,false) +9223372036854775802+7 -> (0,false) +9223372036854775802+8 -> (0,false) +9223372036854775802+9 -> (0,false) +``` + +For int, int64, and int32 types, provide Add, Add32, Add64, Sub, Sub32, Sub64, etc. +Unsigned types not covered at the moment, but such additions are welcome. + +### Stay calm and panic + +There's a good case to be made that a panic is an unidiomatic but proper response. Iff you +believe that there's no valid way to continue your program after math goes wayward, you can +use the easier Addp, Mulp, Subp, and Divp versions which return the normal result or panic. + + + + + + + diff --git a/tm2/pkg/overflow/overflow.go b/tm2/pkg/overflow/overflow.go new file mode 100644 index 00000000000..b476ea5776e --- /dev/null +++ b/tm2/pkg/overflow/overflow.go @@ -0,0 +1,131 @@ +/* +Package overflow offers overflow-checked integer arithmetic operations +for int, int32, and int64. Each of the operations returns a +result,bool combination. This was prompted by the need to know when +to flow into higher precision types from the math.big library. + +For instance, assuing a 64 bit machine: + +10 + 20 -> 30 +int(math.MaxInt64) + 1 -> -9223372036854775808 + +whereas + +overflow.Add(10,20) -> (30, true) +overflow.Add(math.MaxInt64,1) -> (0, false) + +Add, Sub, Mul, Div are for int. Add64, Add32, etc. are specifically sized. + +If anybody wishes an unsigned version, submit a pull request for code +and new tests. +*/ +package overflow + +//go:generate ./overflow_template.sh + +import "math" + +func _is64Bit() bool { + maxU32 := uint(math.MaxUint32) + return ((maxU32 << 1) >> 1) == maxU32 +} + +/********** PARTIAL TEST COVERAGE FROM HERE DOWN ************* + +The only way that I could see to do this is a combination of +my normal 64 bit system and a GopherJS running on Node. My +understanding is that its ints are 32 bit. + +So, FEEL FREE to carefully review the code visually. + +*************************************************************/ + +// Unspecified size, i.e. normal signed int + +// Add sums two ints, returning the result and a boolean status. +func Add(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Add64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Add32(int32(a), int32(b)) + return int(r32), ok +} + +// Sub returns the difference of two ints and a boolean status. +func Sub(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Sub64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Sub32(int32(a), int32(b)) + return int(r32), ok +} + +// Mul returns the product of two ints and a boolean status. +func Mul(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Mul64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Mul32(int32(a), int32(b)) + return int(r32), ok +} + +// Div returns the quotient of two ints and a boolean status +func Div(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Div64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Div32(int32(a), int32(b)) + return int(r32), ok +} + +// Quotient returns the quotient, remainder and status of two ints +func Quotient(a, b int) (int, int, bool) { + if _is64Bit() { + q64, r64, ok := Quotient64(int64(a), int64(b)) + return int(q64), int(r64), ok + } + q32, r32, ok := Quotient32(int32(a), int32(b)) + return int(q32), int(r32), ok +} + +/************* Panic versions for int ****************/ + +// Addp returns the sum of two ints, panicking on overflow +func Addp(a, b int) int { + r, ok := Add(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Subp returns the difference of two ints, panicking on overflow. +func Subp(a, b int) int { + r, ok := Sub(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mulp returns the product of two ints, panicking on overflow. +func Mulp(a, b int) int { + r, ok := Mul(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Divp returns the quotient of two ints, panicking on overflow. +func Divp(a, b int) int { + r, ok := Div(a, b) + if !ok { + panic("division failure") + } + return r +} diff --git a/tm2/pkg/overflow/overflow_impl.go b/tm2/pkg/overflow/overflow_impl.go new file mode 100644 index 00000000000..a9a90c43835 --- /dev/null +++ b/tm2/pkg/overflow/overflow_impl.go @@ -0,0 +1,360 @@ +package overflow + +// This is generated code, created by overflow_template.sh executed +// by "go generate" + +// Add8 performs + operation on two int8 operands +// returning a result and status +func Add8(a, b int8) (int8, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add8p is the unchecked panicing version of Add8 +func Add8p(a, b int8) int8 { + r, ok := Add8(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub8 performs - operation on two int8 operands +// returning a result and status +func Sub8(a, b int8) (int8, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub8p is the unchecked panicing version of Sub8 +func Sub8p(a, b int8) int8 { + r, ok := Sub8(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul8 performs * operation on two int8 operands +// returning a result and status +func Mul8(a, b int8) (int8, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul8p is the unchecked panicing version of Mul8 +func Mul8p(a, b int8) int8 { + r, ok := Mul8(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div8 performs / operation on two int8 operands +// returning a result and status +func Div8(a, b int8) (int8, bool) { + q, _, ok := Quotient8(a, b) + return q, ok +} + +// Div8p is the unchecked panicing version of Div8 +func Div8p(a, b int8) int8 { + r, ok := Div8(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient8 performs + operation on two int8 operands +// returning a quotient, a remainder and status +func Quotient8(a, b int8) (int8, int8, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add16 performs + operation on two int16 operands +// returning a result and status +func Add16(a, b int16) (int16, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add16p is the unchecked panicing version of Add16 +func Add16p(a, b int16) int16 { + r, ok := Add16(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub16 performs - operation on two int16 operands +// returning a result and status +func Sub16(a, b int16) (int16, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub16p is the unchecked panicing version of Sub16 +func Sub16p(a, b int16) int16 { + r, ok := Sub16(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul16 performs * operation on two int16 operands +// returning a result and status +func Mul16(a, b int16) (int16, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul16p is the unchecked panicing version of Mul16 +func Mul16p(a, b int16) int16 { + r, ok := Mul16(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div16 performs / operation on two int16 operands +// returning a result and status +func Div16(a, b int16) (int16, bool) { + q, _, ok := Quotient16(a, b) + return q, ok +} + +// Div16p is the unchecked panicing version of Div16 +func Div16p(a, b int16) int16 { + r, ok := Div16(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient16 performs + operation on two int16 operands +// returning a quotient, a remainder and status +func Quotient16(a, b int16) (int16, int16, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add32 performs + operation on two int32 operands +// returning a result and status +func Add32(a, b int32) (int32, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add32p is the unchecked panicing version of Add32 +func Add32p(a, b int32) int32 { + r, ok := Add32(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub32 performs - operation on two int32 operands +// returning a result and status +func Sub32(a, b int32) (int32, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub32p is the unchecked panicing version of Sub32 +func Sub32p(a, b int32) int32 { + r, ok := Sub32(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul32 performs * operation on two int32 operands +// returning a result and status +func Mul32(a, b int32) (int32, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul32p is the unchecked panicing version of Mul32 +func Mul32p(a, b int32) int32 { + r, ok := Mul32(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div32 performs / operation on two int32 operands +// returning a result and status +func Div32(a, b int32) (int32, bool) { + q, _, ok := Quotient32(a, b) + return q, ok +} + +// Div32p is the unchecked panicing version of Div32 +func Div32p(a, b int32) int32 { + r, ok := Div32(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient32 performs + operation on two int32 operands +// returning a quotient, a remainder and status +func Quotient32(a, b int32) (int32, int32, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add64 performs + operation on two int64 operands +// returning a result and status +func Add64(a, b int64) (int64, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add64p is the unchecked panicing version of Add64 +func Add64p(a, b int64) int64 { + r, ok := Add64(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub64 performs - operation on two int64 operands +// returning a result and status +func Sub64(a, b int64) (int64, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub64p is the unchecked panicing version of Sub64 +func Sub64p(a, b int64) int64 { + r, ok := Sub64(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul64 performs * operation on two int64 operands +// returning a result and status +func Mul64(a, b int64) (int64, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul64p is the unchecked panicing version of Mul64 +func Mul64p(a, b int64) int64 { + r, ok := Mul64(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div64 performs / operation on two int64 operands +// returning a result and status +func Div64(a, b int64) (int64, bool) { + q, _, ok := Quotient64(a, b) + return q, ok +} + +// Div64p is the unchecked panicing version of Div64 +func Div64p(a, b int64) int64 { + r, ok := Div64(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient64 performs + operation on two int64 operands +// returning a quotient, a remainder and status +func Quotient64(a, b int64) (int64, int64, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} diff --git a/tm2/pkg/overflow/overflow_template.sh b/tm2/pkg/overflow/overflow_template.sh new file mode 100755 index 00000000000..a2a85f2c581 --- /dev/null +++ b/tm2/pkg/overflow/overflow_template.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +exec > overflow_impl.go + +echo "package overflow + +// This is generated code, created by overflow_template.sh executed +// by \"go generate\" + +" + + +for SIZE in 8 16 32 64 +do +echo " + +// Add${SIZE} performs + operation on two int${SIZE} operands +// returning a result and status +func Add${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add${SIZE}p is the unchecked panicing version of Add${SIZE} +func Add${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Add${SIZE}(a, b) + if !ok { + panic(\"addition overflow\") + } + return r +} + + +// Sub${SIZE} performs - operation on two int${SIZE} operands +// returning a result and status +func Sub${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub${SIZE}p is the unchecked panicing version of Sub${SIZE} +func Sub${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Sub${SIZE}(a, b) + if !ok { + panic(\"subtraction overflow\") + } + return r +} + + +// Mul${SIZE} performs * operation on two int${SIZE} operands +// returning a result and status +func Mul${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul${SIZE}p is the unchecked panicing version of Mul${SIZE} +func Mul${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Mul${SIZE}(a, b) + if !ok { + panic(\"multiplication overflow\") + } + return r +} + + + +// Div${SIZE} performs / operation on two int${SIZE} operands +// returning a result and status +func Div${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + q, _, ok := Quotient${SIZE}(a, b) + return q, ok +} + +// Div${SIZE}p is the unchecked panicing version of Div${SIZE} +func Div${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Div${SIZE}(a, b) + if !ok { + panic(\"division failure\") + } + return r +} + +// Quotient${SIZE} performs + operation on two int${SIZE} operands +// returning a quotient, a remainder and status +func Quotient${SIZE}(a, b int${SIZE}) (int${SIZE}, int${SIZE}, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} +" +done + +go run -modfile ../../../misc/devdeps/go.mod mvdan.cc/gofumpt -w overflow_impl.go diff --git a/tm2/pkg/overflow/overflow_test.go b/tm2/pkg/overflow/overflow_test.go new file mode 100644 index 00000000000..2b2d345b55d --- /dev/null +++ b/tm2/pkg/overflow/overflow_test.go @@ -0,0 +1,115 @@ +package overflow + +import ( + "fmt" + "math" + "testing" +) + +// sample all possibilities of 8 bit numbers +// by checking against 64 bit numbers + +func TestAlgorithms(t *testing.T) { + errors := 0 + + for a64 := int64(math.MinInt8); a64 <= int64(math.MaxInt8); a64++ { + for b64 := int64(math.MinInt8); b64 <= int64(math.MaxInt8) && errors < 10; b64++ { + a8 := int8(a64) + b8 := int8(b64) + + if int64(a8) != a64 || int64(b8) != b64 { + t.Fatal("LOGIC FAILURE IN TEST") + } + + // ADDITION + { + r64 := a64 + b64 + + // now the verification + result, ok := Add8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v + %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // SUBTRACTION + { + r64 := a64 - b64 + + // now the verification + result, ok := Sub8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v - %v = %v instead of %v\n", + a8, b8, result, r64) + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // MULTIPLICATION + { + r64 := a64 * b64 + + // now the verification + result, ok := Mul8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v * %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // DIVISION + if b8 != 0 { + r64 := a64 / b64 + + // now the verification + result, _, ok := Quotient8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v / %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && result != 0 && int64(result) == r64 { + t.Fail() + errors++ + } + } + } + } +} + +func TestQuotient(t *testing.T) { + q, r, ok := Quotient(100, 3) + if r != 1 || q != 33 || !ok { + t.Errorf("expected 100/3 => 33, r=1") + } + if _, _, ok = Quotient(1, 0); ok { + t.Error("unexpected lack of failure") + } +} + +//func TestAdditionInt(t *testing.T) { +// fmt.Printf("\nminint8 = %v\n", math.MinInt8) +// fmt.Printf("maxint8 = %v\n\n", math.MaxInt8) +// fmt.Printf("maxint32 = %v\n", math.MaxInt32) +// fmt.Printf("minint32 = %v\n\n", math.MinInt32) +// fmt.Printf("maxint64 = %v\n", math.MaxInt64) +// fmt.Printf("minint64 = %v\n\n", math.MinInt64) +//} + +func Test64(t *testing.T) { + fmt.Println("64bit:", _is64Bit()) +} diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index 6457b193a6b..fba20a5ba78 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" ) // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/store/gas/store.go b/tm2/pkg/store/gas/store.go index db5ea7a79b0..81e898a90d8 100644 --- a/tm2/pkg/store/gas/store.go +++ b/tm2/pkg/store/gas/store.go @@ -1,9 +1,9 @@ package gas import ( + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/gnolang/gno/tm2/pkg/store/types" "github.com/gnolang/gno/tm2/pkg/store/utils" - "github.com/gnolang/overflow" ) var _ types.Store = &Store{} diff --git a/tm2/pkg/store/types/gas.go b/tm2/pkg/store/types/gas.go index fd631dd3259..9d1f3d70c28 100644 --- a/tm2/pkg/store/types/gas.go +++ b/tm2/pkg/store/types/gas.go @@ -3,7 +3,7 @@ package types import ( "math" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" ) // Gas consumption descriptors. diff --git a/tm2/pkg/store/types/gas_test.go b/tm2/pkg/store/types/gas_test.go index 410ba0b7e92..115d347bd5e 100644 --- a/tm2/pkg/store/types/gas_test.go +++ b/tm2/pkg/store/types/gas_test.go @@ -4,7 +4,7 @@ import ( "math" "testing" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/stretchr/testify/require" ) From 5f16b8c703867c3311c9023b60105b010a9e97be Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Dec 2024 10:14:40 +0100 Subject: [PATCH 57/86] fix(gnovm): use strconv.UnquoteChar to parse rune literals (#3296) This fixes a bug, as shown in rune3.gno, whereby rune literals which would not be parsed correctly by `strconv.Unquote` are now parsed correctly. Previously, the test would print out 65533, for the unicode invalid code point. --- gnovm/pkg/gnolang/nodes.go | 1 + gnovm/pkg/gnolang/op_eval.go | 10 ++++------ gnovm/tests/files/rune3.gno | 10 ++++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 gnovm/tests/files/rune3.gno diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 8d3d6d8a2cc..0496d37ed72 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -2153,6 +2153,7 @@ type ValuePather interface { // Utility func (x *BasicLitExpr) GetString() string { + // Matches string literal parsing in go/constant.MakeFromLiteral. str, err := strconv.Unquote(x.Value) if err != nil { panic("error in parsing string literal: " + err.Error()) diff --git a/gnovm/pkg/gnolang/op_eval.go b/gnovm/pkg/gnolang/op_eval.go index 1beba1d6e3f..2aa13b21753 100644 --- a/gnovm/pkg/gnolang/op_eval.go +++ b/gnovm/pkg/gnolang/op_eval.go @@ -204,16 +204,14 @@ func (m *Machine) doOpEval() { // and github.com/golang/go/issues/19921 panic("imaginaries are not supported") case CHAR: - cstr, err := strconv.Unquote(x.Value) + // Matching character literal parsing in go/constant.MakeFromLiteral. + val := x.Value + rne, _, _, err := strconv.UnquoteChar(val[1:len(val)-1], '\'') if err != nil { panic("error in parsing character literal: " + err.Error()) } - runes := []rune(cstr) - if len(runes) != 1 { - panic(fmt.Sprintf("error in parsing character literal: 1 rune expected, but got %v (%s)", len(runes), cstr)) - } tv := TypedValue{T: UntypedRuneType} - tv.SetInt32(runes[0]) + tv.SetInt32(rne) m.PushValue(tv) case STRING: m.PushValue(TypedValue{ diff --git a/gnovm/tests/files/rune3.gno b/gnovm/tests/files/rune3.gno new file mode 100644 index 00000000000..e848565e3a4 --- /dev/null +++ b/gnovm/tests/files/rune3.gno @@ -0,0 +1,10 @@ +package main + +const overflow = '\xff' + +func main() { + println(overflow) +} + +// Output: +// 255 From 1fba5cfa840d3499d9ce22507a7d2ada19abbdd4 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Dec 2024 11:13:38 +0100 Subject: [PATCH 58/86] feat(cmd/gno): perform type checking when calling linter (#1730) Depends on (in order): 1. #1700 2. #1702 This PR uses the type checker added in #1702 to perform Gno type checking when calling `gno lint`. Additionally, it adds validation of gno.mod indirectly (the parsed gno mod is used to determine if a package is a draft, and if so skip type checking). Because `gno lint` uses the TestStore, the resulting `MemPackage`s may contain redefinitions, for overwriting standard libraries like `AssertOriginCall`. I changed the type checker to filter out the redefinitions before they reach the Go type checker. Further improvements, which can be done after this: - Add shims for gonative special libraries (`fmt`, `os`...) - This will allow us to fully type check also tests and filetests - Make the type checking on-chain (#1702) also typecheck tests - as a consequence of the above. --- gnovm/cmd/gno/lint.go | 205 ++++++++++++------ gnovm/cmd/gno/lint_test.go | 39 ++-- gnovm/cmd/gno/run_test.go | 19 +- .../gno/testdata/{gno_fmt => fmt}/empty.txtar | 0 .../{gno_fmt => fmt}/import_cleaning.txtar | 0 .../testdata/{gno_fmt => fmt}/include.txtar | 0 .../{gno_fmt => fmt}/multi_import.txtar | 0 .../{gno_fmt => fmt}/noimport_format.txtar | 0 .../{gno_fmt => fmt}/parse_error.txtar | 0 .../{gno_fmt => fmt}/shadow_import.txtar | 0 .../gno/testdata/gno_lint/file_error_txtar | 20 -- .../{gno_lint => lint}/bad_import.txtar | 8 +- .../{gno_lint => lint}/file_error.txtar | 5 +- .../{gno_lint => lint}/no_error.txtar | 9 +- .../{gno_lint => lint}/no_gnomod.txtar | 6 +- .../{gno_lint => lint}/not_declared.txtar | 12 +- .../{gno_test => test}/dir_not_exist.txtar | 0 .../{gno_test => test}/empty_dir.txtar | 0 .../{gno_test => test}/empty_gno1.txtar | 0 .../{gno_test => test}/empty_gno2.txtar | 0 .../{gno_test => test}/empty_gno3.txtar | 0 .../{gno_test => test}/error_correct.txtar | 0 .../{gno_test => test}/error_incorrect.txtar | 0 .../{gno_test => test}/error_sync.txtar | 0 .../{gno_test => test}/failing_filetest.txtar | 0 .../{gno_test => test}/failing_test.txtar | 0 .../{gno_test => test}/filetest_events.txtar | 0 .../flag_print-runtime-metrics.txtar | 0 .../{gno_test => test}/flag_run.txtar | 0 .../{gno_test => test}/flag_timeout.txtar | 0 .../{gno_test => test}/fmt_write_import.txtar | 0 .../testdata/{gno_test => test}/minim1.txtar | 0 .../testdata/{gno_test => test}/minim2.txtar | 0 .../testdata/{gno_test => test}/minim3.txtar | 0 .../{gno_test => test}/multitest_events.txtar | 0 .../testdata/{gno_test => test}/no_args.txtar | 0 .../{gno_test => test}/output_correct.txtar | 0 .../{gno_test => test}/output_incorrect.txtar | 0 .../{gno_test => test}/output_sync.txtar | 0 .../testdata/{gno_test => test}/panic.txtar | 0 .../pkg_underscore_test.txtar | 0 .../realm_boundmethod.txtar | 0 .../{gno_test => test}/realm_correct.txtar | 0 .../{gno_test => test}/realm_incorrect.txtar | 0 .../{gno_test => test}/realm_sync.txtar | 0 .../testdata/{gno_test => test}/recover.txtar | 0 .../testdata/{gno_test => test}/skip.txtar | 0 .../{gno_test => test}/unknown_package.txtar | 0 .../{gno_test => test}/valid_filetest.txtar | 0 .../{gno_test => test}/valid_test.txtar | 0 .../gobuild_flag_build_error.txtar | 0 .../gobuild_flag_parse_error.txtar | 0 .../invalid_import.txtar | 0 .../no_args.txtar | 0 .../parse_error.txtar | 0 .../valid_empty_dir.txtar | 0 .../valid_gobuild_file.txtar | 0 .../valid_gobuild_flag.txtar | 0 .../valid_output_flag.txtar | 0 .../valid_output_gobuild.txtar | 0 .../valid_transpile_file.txtar | 0 .../valid_transpile_package.txtar | 0 .../valid_transpile_tree.txtar | 0 gnovm/cmd/gno/testdata_test.go | 1 - gnovm/cmd/gno/transpile_test.go | 20 -- gnovm/pkg/gnolang/go2gno.go | 59 ++++- .../integ/typecheck_missing_return/gno.mod | 1 + .../integ/typecheck_missing_return/main.gno | 5 + 68 files changed, 263 insertions(+), 146 deletions(-) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/empty.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/import_cleaning.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/include.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/multi_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/noimport_format.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/shadow_import.txtar (100%) delete mode 100644 gnovm/cmd/gno/testdata/gno_lint/file_error_txtar rename gnovm/cmd/gno/testdata/{gno_lint => lint}/bad_import.txtar (54%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/file_error.txtar (88%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/no_error.txtar (68%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/no_gnomod.txtar (60%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/not_declared.txtar (55%) rename gnovm/cmd/gno/testdata/{gno_test => test}/dir_not_exist.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_dir.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno1.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno2.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno3.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/failing_filetest.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/failing_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/filetest_events.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_print-runtime-metrics.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_run.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_timeout.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/fmt_write_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim1.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim2.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim3.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/multitest_events.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/no_args.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/panic.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/pkg_underscore_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_boundmethod.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/recover.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/skip.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/unknown_package.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/valid_filetest.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/valid_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/gobuild_flag_build_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/gobuild_flag_parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/invalid_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/no_args.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_empty_dir.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_gobuild_file.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_gobuild_flag.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_output_flag.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_output_gobuild.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_file.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_package.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_tree.txtar (100%) create mode 100644 gnovm/tests/integ/typecheck_missing_return/gno.mod create mode 100644 gnovm/tests/integ/typecheck_missing_return/main.gno diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 6d5399ca932..a3e7f5310e1 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -6,17 +6,19 @@ import ( "flag" "fmt" "go/scanner" + "go/types" "io" "os" "path/filepath" "regexp" "strings" + "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/pkg/test" "github.com/gnolang/gno/tm2/pkg/commands" - osm "github.com/gnolang/gno/tm2/pkg/os" "go.uber.org/multierr" ) @@ -50,6 +52,31 @@ func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar(&c.rootDir, "root-dir", rootdir, "clone location of github.com/gnolang/gno (gno tries to guess it)") } +type lintCode int + +const ( + lintUnknown lintCode = iota + lintGnoMod + lintGnoError + lintParserError + lintTypeCheckError + + // TODO: add new linter codes here. +) + +type lintIssue struct { + Code lintCode + Msg string + Confidence float64 // 1 is 100% + Location string // file:line, or equivalent + // TODO: consider writing fix suggestions +} + +func (i lintIssue) String() string { + // TODO: consider crafting a doc URL based on Code. + return fmt.Sprintf("%s: %s (code=%d)", i.Location, i.Msg, i.Code) +} + func execLint(cfg *lintCfg, args []string, io commands.IO) error { if len(args) < 1 { return flag.ErrHelp @@ -72,37 +99,55 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { for _, pkgPath := range pkgPaths { if verbose { - fmt.Fprintf(io.Err(), "Linting %q...\n", pkgPath) + io.ErrPrintln(pkgPath) + } + + info, err := os.Stat(pkgPath) + if err == nil && !info.IsDir() { + pkgPath = filepath.Dir(pkgPath) } // Check if 'gno.mod' exists - gnoModPath := filepath.Join(pkgPath, "gno.mod") - if !osm.FileExists(gnoModPath) { - hasError = true + gmFile, err := gnomod.ParseAt(pkgPath) + if err != nil { issue := lintIssue{ - Code: lintNoGnoMod, + Code: lintGnoMod, Confidence: 1, Location: pkgPath, - Msg: "missing 'gno.mod' file", + Msg: err.Error(), } - fmt.Fprint(io.Err(), issue.String()+"\n") + io.ErrPrintln(issue) + hasError = true } - // Handle runtime errors - hasError = catchRuntimeError(pkgPath, io.Err(), func() { - stdout, stdin, stderr := io.Out(), io.In(), io.Err() - _, testStore := test.Store( - rootDir, false, - stdin, stdout, stderr, - ) - - targetPath := pkgPath - info, err := os.Stat(pkgPath) - if err == nil && !info.IsDir() { - targetPath = filepath.Dir(pkgPath) + stdout, stdin, stderr := io.Out(), io.In(), io.Err() + _, testStore := test.Store( + rootDir, false, + stdin, stdout, stderr, + ) + + memPkg, err := gno.ReadMemPackage(pkgPath, pkgPath) + if err != nil { + io.ErrPrintln(issueFromError(pkgPath, err).String()) + hasError = true + continue + } + + // Run type checking + if gmFile == nil || !gmFile.Draft { + foundErr, err := lintTypeCheck(io, memPkg, testStore) + if err != nil { + io.ErrPrintln(err) + hasError = true + } else if foundErr { + hasError = true } + } else if verbose { + io.ErrPrintfln("%s: module is draft, skipping type check", pkgPath) + } - memPkg := gno.MustReadMemPackage(targetPath, targetPath) + // Handle runtime errors + hasRuntimeErr := catchRuntimeError(pkgPath, io.Err(), func() { tm := test.Machine(testStore, stdout, memPkg.Path) defer tm.Release() @@ -110,28 +155,13 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { tm.RunMemPackage(memPkg, true) // Check test files - testfiles := &gno.FileSet{} - for _, mfile := range memPkg.Files { - if !strings.HasSuffix(mfile.Name, ".gno") { - continue // Skip non-GNO files - } + testFiles := lintTestFiles(memPkg) - n, _ := gno.ParseFile(mfile.Name, mfile.Body) - if n == nil { - continue // Skip empty files - } - - // XXX: package ending with `_test` is not supported yet - if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { - // Keep only test files - testfiles.AddFiles(n) - } - } - - tm.RunFiles(testfiles.Files...) - }) || hasError - - // TODO: Add more checkers + tm.RunFiles(testFiles.Files...) + }) + if hasRuntimeErr { + hasError = true + } } if hasError { @@ -141,6 +171,66 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { return nil } +func lintTypeCheck(io commands.IO, memPkg *gnovm.MemPackage, testStore gno.Store) (errorsFound bool, err error) { + tcErr := gno.TypeCheckMemPackageTest(memPkg, testStore) + if tcErr == nil { + return false, nil + } + + errs := multierr.Errors(tcErr) + for _, err := range errs { + switch err := err.(type) { + case types.Error: + io.ErrPrintln(lintIssue{ + Code: lintTypeCheckError, + Msg: err.Msg, + Confidence: 1, + Location: err.Fset.Position(err.Pos).String(), + }) + case scanner.ErrorList: + for _, scErr := range err { + io.ErrPrintln(lintIssue{ + Code: lintParserError, + Msg: scErr.Msg, + Confidence: 1, + Location: scErr.Pos.String(), + }) + } + case scanner.Error: + io.ErrPrintln(lintIssue{ + Code: lintParserError, + Msg: err.Msg, + Confidence: 1, + Location: err.Pos.String(), + }) + default: + return false, fmt.Errorf("unexpected error type: %T", err) + } + } + return true, nil +} + +func lintTestFiles(memPkg *gnovm.MemPackage) *gno.FileSet { + testfiles := &gno.FileSet{} + for _, mfile := range memPkg.Files { + if !strings.HasSuffix(mfile.Name, ".gno") { + continue // Skip non-GNO files + } + + n, _ := gno.ParseFile(mfile.Name, mfile.Body) + if n == nil { + continue // Skip empty files + } + + // XXX: package ending with `_test` is not supported yet + if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { + // Keep only test files + testfiles.AddFiles(n) + } + } + return testfiles +} + func guessSourcePath(pkg, source string) string { if info, err := os.Stat(pkg); !os.IsNotExist(err) && !info.IsDir() { pkg = filepath.Dir(pkg) @@ -174,21 +264,21 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha switch verr := r.(type) { case *gno.PreprocessError: err := verr.Unwrap() - fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, err).String()) case error: errors := multierr.Errors(verr) for _, err := range errors { errList, ok := err.(scanner.ErrorList) if ok { for _, errorInList := range errList { - fmt.Fprint(stderr, issueFromError(pkgPath, errorInList).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, errorInList).String()) } } else { - fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, err).String()) } } case string: - fmt.Fprint(stderr, issueFromError(pkgPath, errors.New(verr)).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, errors.New(verr)).String()) default: panic(r) } @@ -198,29 +288,6 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha return } -type lintCode int - -const ( - lintUnknown lintCode = 0 - lintNoGnoMod lintCode = iota - lintGnoError - - // TODO: add new linter codes here. -) - -type lintIssue struct { - Code lintCode - Msg string - Confidence float64 // 1 is 100% - Location string // file:line, or equivalent - // TODO: consider writing fix suggestions -} - -func (i lintIssue) String() string { - // TODO: consider crafting a doc URL based on Code. - return fmt.Sprintf("%s: %s (code=%d).", i.Location, i.Msg, i.Code) -} - func issueFromError(pkgPath string, err error) lintIssue { var issue lintIssue issue.Confidence = 1 diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 031c252bc79..4589fc55f92 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestLintApp(t *testing.T) { tc := []testMainCase{ @@ -9,7 +12,7 @@ func TestLintApp(t *testing.T) { errShouldBe: "flag: help requested", }, { args: []string{"lint", "../../tests/integ/run_main/"}, - stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", + stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, @@ -17,33 +20,43 @@ func TestLintApp(t *testing.T) { errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"}, - stderrShouldContain: "main.gno:4:2: 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: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: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/"}, - stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", - errShouldBe: "exit code: 1", + args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, + stderrShouldContain: func() string { + lines := []string{ + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + } + return strings.Join(lines, "\n") + "\n" + }(), + errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/minimalist_gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files }, { args: []string{"lint", "../../tests/integ/invalid_module_name/"}, // TODO: raise an error because gno.mod is invalid + }, { + args: []string{"lint", "../../tests/integ/invalid_gno_file/"}, + stderrShouldContain: "../../tests/integ/invalid_gno_file/invalid.gno:1:1: expected 'package', found packag (code=2)", + errShouldBe: "exit code: 1", + }, { + args: []string{"lint", "../../tests/integ/typecheck_missing_return/"}, + stderrShouldContain: "../../tests/integ/typecheck_missing_return/main.gno:5:1: missing return (code=4)", + errShouldBe: "exit code: 1", }, // TODO: 'gno mod' is valid? - // TODO: is gno source valid? // TODO: are dependencies valid? // TODO: is gno source using unsafe/discouraged features? - // TODO: consider making `gno transpile; go lint *gen.go` // TODO: check for imports of native libs from non _test.gno files } testMainCaseRun(t, tc) diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index 74f99f7490c..aa7780c149e 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestRunApp(t *testing.T) { tc := []testMainCase{ @@ -84,9 +87,17 @@ func TestRunApp(t *testing.T) { stdoutShouldContain: "Context worked", }, { - args: []string{"run", "../../tests/integ/several-files-multiple-errors/"}, - 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{"run", "../../tests/integ/several-files-multiple-errors/"}, + stderrShouldContain: func() string { + lines := []string{ + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + } + return strings.Join(lines, "\n") + "\n" + }(), + errShouldBe: "exit code: 1", }, // TODO: a test file // TODO: args diff --git a/gnovm/cmd/gno/testdata/gno_fmt/empty.txtar b/gnovm/cmd/gno/testdata/fmt/empty.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/empty.txtar rename to gnovm/cmd/gno/testdata/fmt/empty.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar b/gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar rename to gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/include.txtar b/gnovm/cmd/gno/testdata/fmt/include.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/include.txtar rename to gnovm/cmd/gno/testdata/fmt/include.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar b/gnovm/cmd/gno/testdata/fmt/multi_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar rename to gnovm/cmd/gno/testdata/fmt/multi_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar b/gnovm/cmd/gno/testdata/fmt/noimport_format.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar rename to gnovm/cmd/gno/testdata/fmt/noimport_format.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar b/gnovm/cmd/gno/testdata/fmt/parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar rename to gnovm/cmd/gno/testdata/fmt/parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar b/gnovm/cmd/gno/testdata/fmt/shadow_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar rename to gnovm/cmd/gno/testdata/fmt/shadow_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar deleted file mode 100644 index 9482eeb1f4f..00000000000 --- a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar +++ /dev/null @@ -1,20 +0,0 @@ -# gno lint: test file error - -! gno lint ./i_have_error_test.gno - -cmp stdout stdout.golden -cmp stderr stderr.golden - --- i_have_error_test.gno -- -package main - -import "fmt" - -func TestIHaveSomeError() { - i := undefined_variable - fmt.Println("Hello", 42) -} - --- stdout.golden -- --- stderr.golden -- -i_have_error_test.gno:6: name undefined_variable not declared (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar b/gnovm/cmd/gno/testdata/lint/bad_import.txtar similarity index 54% rename from gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar rename to gnovm/cmd/gno/testdata/lint/bad_import.txtar index 52141dff09b..b5edbdd0223 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar +++ b/gnovm/cmd/gno/testdata/lint/bad_import.txtar @@ -11,9 +11,13 @@ package main import "python" func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/test + -- stdout.golden -- -- stderr.golden -- -bad_file.gno:3:8: unknown import path python (code=2). +bad_file.gno:3:8: could not import python (import not found: python) (code=4) +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/lint/file_error.txtar similarity index 88% rename from gnovm/cmd/gno/testdata/gno_lint/file_error.txtar rename to gnovm/cmd/gno/testdata/lint/file_error.txtar index 5aa3a3282d5..4fa50c6da81 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/file_error.txtar @@ -15,6 +15,9 @@ func TestIHaveSomeError() { fmt.Println("Hello", 42) } +-- gno.mod -- +module gno.land/p/test + -- stdout.golden -- -- stderr.golden -- -i_have_error_test.gno:6:7: 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/no_error.txtar b/gnovm/cmd/gno/testdata/lint/no_error.txtar similarity index 68% rename from gnovm/cmd/gno/testdata/gno_lint/no_error.txtar rename to gnovm/cmd/gno/testdata/lint/no_error.txtar index 95356b1ba2b..5dd3b164952 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_error.txtar @@ -1,6 +1,6 @@ # testing simple gno lint command with any error -gno lint ./good_file.gno +gno lint ./good_file.gno cmp stdout stdout.golden cmp stdout stderr.golden @@ -8,11 +8,12 @@ cmp stdout stderr.golden -- good_file.gno -- package main -import "fmt" - func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/demo/test + -- stdout.golden -- -- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar similarity index 60% rename from gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar rename to gnovm/cmd/gno/testdata/lint/no_gnomod.txtar index 52daa6f0e9b..b5a046a7095 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar @@ -8,12 +8,10 @@ cmp stderr stderr.golden -- good_file.gno -- package main -import "fmt" - func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } -- stdout.golden -- -- stderr.golden -- -./.: missing 'gno.mod' file (code=1). +./.: parsing gno.mod at ./.: gno.mod file not found in current or any parent directory (code=1) diff --git a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar b/gnovm/cmd/gno/testdata/lint/not_declared.txtar similarity index 55% rename from gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar rename to gnovm/cmd/gno/testdata/lint/not_declared.txtar index b63c5c447e1..ac56b27e0df 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar +++ b/gnovm/cmd/gno/testdata/lint/not_declared.txtar @@ -8,13 +8,15 @@ cmp stderr stderr.golden -- bad_file.gno -- package main -import "fmt" - func main() { - hello.Foo() - fmt.Println("Hello", 42) + hello.Foo() + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/demo/hello + -- stdout.golden -- -- stderr.golden -- -bad_file.gno:6:3: name hello not declared (code=2). +bad_file.gno:4:2: undefined: hello (code=4) +bad_file.gno:4:2: name hello not declared (code=2) diff --git a/gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar b/gnovm/cmd/gno/testdata/test/dir_not_exist.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar rename to gnovm/cmd/gno/testdata/test/dir_not_exist.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar b/gnovm/cmd/gno/testdata/test/empty_dir.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar rename to gnovm/cmd/gno/testdata/test/empty_dir.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar b/gnovm/cmd/gno/testdata/test/empty_gno1.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno1.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar b/gnovm/cmd/gno/testdata/test/empty_gno2.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno2.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar b/gnovm/cmd/gno/testdata/test/empty_gno3.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno3.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/test/error_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_correct.txtar rename to gnovm/cmd/gno/testdata/test/error_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar b/gnovm/cmd/gno/testdata/test/error_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/error_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar b/gnovm/cmd/gno/testdata/test/error_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_sync.txtar rename to gnovm/cmd/gno/testdata/test/error_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/test/failing_filetest.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar rename to gnovm/cmd/gno/testdata/test/failing_filetest.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_test.txtar b/gnovm/cmd/gno/testdata/test/failing_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/failing_test.txtar rename to gnovm/cmd/gno/testdata/test/failing_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar b/gnovm/cmd/gno/testdata/test/filetest_events.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar rename to gnovm/cmd/gno/testdata/test/filetest_events.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar b/gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar rename to gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_run.txtar b/gnovm/cmd/gno/testdata/test/flag_run.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_run.txtar rename to gnovm/cmd/gno/testdata/test/flag_run.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar b/gnovm/cmd/gno/testdata/test/flag_timeout.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar rename to gnovm/cmd/gno/testdata/test/flag_timeout.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar b/gnovm/cmd/gno/testdata/test/fmt_write_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar rename to gnovm/cmd/gno/testdata/test/fmt_write_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar b/gnovm/cmd/gno/testdata/test/minim1.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim1.txtar rename to gnovm/cmd/gno/testdata/test/minim1.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar b/gnovm/cmd/gno/testdata/test/minim2.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim2.txtar rename to gnovm/cmd/gno/testdata/test/minim2.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar b/gnovm/cmd/gno/testdata/test/minim3.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim3.txtar rename to gnovm/cmd/gno/testdata/test/minim3.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar b/gnovm/cmd/gno/testdata/test/multitest_events.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar rename to gnovm/cmd/gno/testdata/test/multitest_events.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/no_args.txtar b/gnovm/cmd/gno/testdata/test/no_args.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/no_args.txtar rename to gnovm/cmd/gno/testdata/test/no_args.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/test/output_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_correct.txtar rename to gnovm/cmd/gno/testdata/test/output_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar b/gnovm/cmd/gno/testdata/test/output_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/output_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/test/output_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_sync.txtar rename to gnovm/cmd/gno/testdata/test/output_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/panic.txtar b/gnovm/cmd/gno/testdata/test/panic.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/panic.txtar rename to gnovm/cmd/gno/testdata/test/panic.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar b/gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar rename to gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar b/gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar rename to gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/test/realm_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar rename to gnovm/cmd/gno/testdata/test/realm_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar b/gnovm/cmd/gno/testdata/test/realm_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/realm_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/test/realm_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar rename to gnovm/cmd/gno/testdata/test/realm_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/recover.txtar b/gnovm/cmd/gno/testdata/test/recover.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/recover.txtar rename to gnovm/cmd/gno/testdata/test/recover.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/skip.txtar b/gnovm/cmd/gno/testdata/test/skip.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/skip.txtar rename to gnovm/cmd/gno/testdata/test/skip.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar b/gnovm/cmd/gno/testdata/test/unknown_package.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar rename to gnovm/cmd/gno/testdata/test/unknown_package.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar rename to gnovm/cmd/gno/testdata/test/valid_filetest.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar b/gnovm/cmd/gno/testdata/test/valid_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/valid_test.txtar rename to gnovm/cmd/gno/testdata/test/valid_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar b/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar rename to gnovm/cmd/gno/testdata/transpile/invalid_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar b/gnovm/cmd/gno/testdata/transpile/no_args.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar rename to gnovm/cmd/gno/testdata/transpile/no_args.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar rename to gnovm/cmd/gno/testdata/transpile/parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar b/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar diff --git a/gnovm/cmd/gno/testdata_test.go b/gnovm/cmd/gno/testdata_test.go index 15bc8d96e26..6b1bbd1d459 100644 --- a/gnovm/cmd/gno/testdata_test.go +++ b/gnovm/cmd/gno/testdata_test.go @@ -24,7 +24,6 @@ func Test_Scripts(t *testing.T) { } name := dir.Name() - t.Logf("testing: %s", name) t.Run(name, func(t *testing.T) { updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) p := testscript.Params{ diff --git a/gnovm/cmd/gno/transpile_test.go b/gnovm/cmd/gno/transpile_test.go index 827c09e23f1..5a03ddc7657 100644 --- a/gnovm/cmd/gno/transpile_test.go +++ b/gnovm/cmd/gno/transpile_test.go @@ -6,29 +6,9 @@ import ( "strconv" "testing" - "github.com/rogpeppe/go-internal/testscript" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/gnovm/pkg/integration" ) -func Test_ScriptsTranspile(t *testing.T) { - p := testscript.Params{ - Dir: "testdata/gno_transpile", - } - - if coverdir, ok := integration.ResolveCoverageDir(); ok { - err := integration.SetupTestscriptsCoverage(&p, coverdir) - require.NoError(t, err) - } - - err := integration.SetupGno(&p, t.TempDir()) - require.NoError(t, err) - - testscript.Run(t, p) -} - func Test_parseGoBuildErrors(t *testing.T) { t.Parallel() diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index 338efa20fcc..82d5c69b08b 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -39,7 +39,9 @@ import ( "go/token" "go/types" "os" + "path" "reflect" + "slices" "strconv" "strings" @@ -499,6 +501,18 @@ type MemPackageGetter interface { // If format is true, the code will be automatically updated with the // formatted source code. func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, format bool) error { + return typeCheckMemPackage(mempkg, getter, false, format) +} + +// TypeCheckMemPackageTest performs the same type checks as [TypeCheckMemPackage], +// but allows re-declarations. +// +// Note: like TypeCheckMemPackage, this function ignores tests and filetests. +func TypeCheckMemPackageTest(mempkg *gnovm.MemPackage, getter MemPackageGetter) error { + return typeCheckMemPackage(mempkg, getter, true, false) +} + +func typeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, testing, format bool) error { var errs error imp := &gnoImporter{ getter: getter, @@ -508,6 +522,7 @@ func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, form errs = multierr.Append(errs, err) }, }, + allowRedefinitions: testing, } imp.cfg.Importer = imp @@ -529,6 +544,9 @@ type gnoImporter struct { getter MemPackageGetter cache map[string]gnoImporterResult cfg *types.Config + + // allow symbol redefinitions? (test standard libraries) + allowRedefinitions bool } // Unused, but satisfies the Importer interface. @@ -559,22 +577,39 @@ func (g *gnoImporter) ImportFrom(path, _ string, _ types.ImportMode) (*types.Pac } func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*types.Package, error) { + // This map is used to allow for function re-definitions, which are allowed + // in Gno (testing context) but not in Go. + // This map links each function identifier with a closure to remove its + // associated declaration. + var delFunc map[string]func() + if g.allowRedefinitions { + delFunc = make(map[string]func()) + } + fset := token.NewFileSet() files := make([]*ast.File, 0, len(mpkg.Files)) var errs error for _, file := range mpkg.Files { + // Ignore non-gno files. + // TODO: support filetest type checking. (should probably handle as each its + // own separate pkg, which should also be typechecked) if !strings.HasSuffix(file.Name, ".gno") || - endsWith(file.Name, []string{"_test.gno", "_filetest.gno"}) { - continue // skip spurious file. + strings.HasSuffix(file.Name, "_test.gno") || + strings.HasSuffix(file.Name, "_filetest.gno") { + continue } const parseOpts = parser.ParseComments | parser.DeclarationErrors | parser.SkipObjectResolution - f, err := parser.ParseFile(fset, file.Name, file.Body, parseOpts) + f, err := parser.ParseFile(fset, path.Join(mpkg.Path, file.Name), file.Body, parseOpts) if err != nil { errs = multierr.Append(errs, err) continue } + if delFunc != nil { + deleteOldIdents(delFunc, f) + } + // enforce formatting if fmt { var buf bytes.Buffer @@ -595,6 +630,24 @@ func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*t return g.cfg.Check(mpkg.Path, fset, files, nil) } +func deleteOldIdents(idents map[string]func(), f *ast.File) { + for _, decl := range f.Decls { + fd, ok := decl.(*ast.FuncDecl) + if !ok || fd.Recv != nil { // ignore methods + continue + } + if del := idents[fd.Name.Name]; del != nil { + del() + } + decl := decl + idents[fd.Name.Name] = func() { + // NOTE: cannot use the index as a file may contain multiple decls to be removed, + // so removing one would make all "later" indexes wrong. + f.Decls = slices.DeleteFunc(f.Decls, func(d ast.Decl) bool { return decl == d }) + } + } +} + //---------------------------------------- // utility methods diff --git a/gnovm/tests/integ/typecheck_missing_return/gno.mod b/gnovm/tests/integ/typecheck_missing_return/gno.mod new file mode 100644 index 00000000000..3eaaa374994 --- /dev/null +++ b/gnovm/tests/integ/typecheck_missing_return/gno.mod @@ -0,0 +1 @@ +module gno.land/p/integ/valid diff --git a/gnovm/tests/integ/typecheck_missing_return/main.gno b/gnovm/tests/integ/typecheck_missing_return/main.gno new file mode 100644 index 00000000000..5d6e547097c --- /dev/null +++ b/gnovm/tests/integ/typecheck_missing_return/main.gno @@ -0,0 +1,5 @@ +package valid + +func Hello() int { + // no return +} From 1bd64192a170fdf7ca904fb6bf27f10e2acc8ed5 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:16:05 +0100 Subject: [PATCH 59/86] feat(github-bot): add a fork condition and handle PR reviews (#3303) This PR improves the bot on two points: - it now handle `pull_request_review` events to address [this concern](https://github.com/gnolang/gno/issues/3238#issuecomment-2526206058) https://github.com/gnolang/gno/commit/4f7b0b80c6e726806a473556b02444fded707254 - a new condition allows to check if a PR was created from a fork to address [this concern](https://github.com/gnolang/gno/issues/3238#issuecomment-2524018469) https://github.com/gnolang/gno/commit/f491d95d68c755d7154cbbee48a9cebc7693fa89 --- .github/workflows/bot.yml | 4 +++ .../github-bot/internal/conditions/fork.go | 27 ++++++++++++++++ .../internal/conditions/fork_test.go | 31 +++++++++++++++++++ contribs/github-bot/internal/config/config.go | 2 +- contribs/github-bot/internal/matrix/matrix.go | 2 +- .../github-bot/internal/matrix/matrix_test.go | 9 ++++++ contribs/github-bot/internal/utils/actions.go | 2 +- .../github-bot/internal/utils/github_const.go | 1 + 8 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 contribs/github-bot/internal/conditions/fork.go create mode 100644 contribs/github-bot/internal/conditions/fork_test.go diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 644540c1aaf..300a5928e25 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -14,6 +14,10 @@ on: - converted_to_draft - ready_for_review + # Watch for changes on PR reviews + pull_request_review: + types: [submitted, edited, dismissed] + # Watch for changes on PR comment issue_comment: types: [created, edited, deleted] diff --git a/contribs/github-bot/internal/conditions/fork.go b/contribs/github-bot/internal/conditions/fork.go new file mode 100644 index 00000000000..72cbae12004 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork.go @@ -0,0 +1,27 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// CreatedFromFork Condition. +type createdFromFork struct{} + +var _ Condition = &createdFromFork{} + +func (b *createdFromFork) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName(), + fmt.Sprintf("The pull request was created from a fork (head branch repo: %s)", pr.GetHead().GetRepo().GetFullName()), + details, + ) +} + +func CreatedFromFork() Condition { + return &createdFromFork{} +} diff --git a/contribs/github-bot/internal/conditions/fork_test.go b/contribs/github-bot/internal/conditions/fork_test.go new file mode 100644 index 00000000000..fe7e9a95bf1 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork_test.go @@ -0,0 +1,31 @@ +package conditions + +import ( + "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 TestCreatedFromFork(t *testing.T) { + t.Parallel() + + var ( + repo = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("main")}, Name: github.String("repo"), FullName: github.String("main/repo")}} + fork = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("fork")}, Name: github.String("repo"), FullName: github.String("fork/repo")}} + ) + + prFromMain := &github.PullRequest{Base: repo, Head: repo} + prFromFork := &github.PullRequest{Base: repo, Head: fork} + + details := treeprint.New() + assert.False(t, CreatedFromFork().IsMet(prFromMain, details)) + assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false") + + details = treeprint.New() + assert.True(t, CreatedFromFork().IsMet(prFromFork, details)) + assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true") +} diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index c1d89e4cde5..2d595c7ce51 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -28,7 +28,7 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { auto := []AutomaticCheck{ { Description: "Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))", - If: c.Always(), + If: c.CreatedFromFork(), Then: r.MaintainerCanModify(), }, { diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go index 9c8f12e4214..02840721c80 100644 --- a/contribs/github-bot/internal/matrix/matrix.go +++ b/contribs/github-bot/internal/matrix/matrix.go @@ -113,7 +113,7 @@ func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContex // Event triggered by an issue / PR comment being created / edited / deleted // or any update on a PR. - case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget: + case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestReview, utils.EventPullRequestTarget: // For these events, retrieve the number of the associated PR from the context. prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) if err != nil { diff --git a/contribs/github-bot/internal/matrix/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go index fe5b7452a49..f6b34f16c24 100644 --- a/contribs/github-bot/internal/matrix/matrix_test.go +++ b/contribs/github-bot/internal/matrix/matrix_test.go @@ -54,6 +54,15 @@ func TestProcessEvent(t *testing.T) { prs, utils.PRList{1}, false, + }, { + "valid pull_request_review event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequestReview, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList{1}, + false, }, { "valid pull_request_target event", &githubactions.GitHubContext{ diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go index 3e08a8e1548..0686e8c29c5 100644 --- a/contribs/github-bot/internal/utils/actions.go +++ b/contribs/github-bot/internal/utils/actions.go @@ -30,7 +30,7 @@ func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) switch actionCtx.EventName { case EventIssueComment: firstKey = "issue" - case EventPullRequest, EventPullRequestTarget: + case EventPullRequest, EventPullRequestReview, EventPullRequestTarget: firstKey = "pull_request" default: return 0, fmt.Errorf("unsupported event: %s", actionCtx.EventName) diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go index 26d7d54d477..f030d9365f7 100644 --- a/contribs/github-bot/internal/utils/github_const.go +++ b/contribs/github-bot/internal/utils/github_const.go @@ -5,6 +5,7 @@ const ( // GitHub Actions Event Names. EventIssueComment = "issue_comment" EventPullRequest = "pull_request" + EventPullRequestReview = "pull_request_review" EventPullRequestTarget = "pull_request_target" EventWorkflowDispatch = "workflow_dispatch" From 9bd9e47e7354d98cbc2980abbd6ee61096efd92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 9 Dec 2024 16:39:22 +0100 Subject: [PATCH 60/86] chore: update portal loop archiver version (#3308) ## Description This PR updates the portal loop tx-archiver version to `v0.4.2`, which eliminates the millisecond timestamp issue --- misc/loop/go.mod | 2 +- misc/loop/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/loop/go.mod b/misc/loop/go.mod index 70e9d21734b..af7783e57bb 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 github.com/gnolang/gno v0.1.0-nightly.20240627 - github.com/gnolang/tx-archive v0.4.0 + github.com/gnolang/tx-archive v0.4.2 github.com/prometheus/client_golang v1.17.0 github.com/sirupsen/logrus v1.9.3 ) diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 8e0feb11e4a..0d235f2cfb1 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -68,8 +68,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/tx-archive v0.4.0 h1:+1Rgo0U0HjLQLq/xqeGdJwtAzo9xWj09t1oZLvrL3bU= -github.com/gnolang/tx-archive v0.4.0/go.mod h1:seKHGnvxUnDgH/mSsCEdwG0dHY/FrpbUm6Hd0+KMd9w= +github.com/gnolang/tx-archive v0.4.2 h1:xBBqLLKY9riv9yxpQgVhItCWxIji2rX6xNFmCY1cEOQ= +github.com/gnolang/tx-archive v0.4.2/go.mod h1:AGUBGO+DCLuKL80a1GJRnpcJ5gxVd9L4jEJXQB9uXp4= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From bb38fb1942aac60999bed90c904711c46fe7b783 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:15:27 +0100 Subject: [PATCH 61/86] fix(gnovm): improve error message for nil assignment in variable declaration (#3068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …aration
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
--------- Co-authored-by: Morgan Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/preprocess.go | 199 ++++++++++++++------------- gnovm/pkg/gnolang/type_check.go | 53 ++++--- gnovm/pkg/gnolang/type_check_test.go | 2 +- gnovm/pkg/gnolang/types.go | 38 ++--- gnovm/tests/files/add3.gno | 9 ++ gnovm/tests/files/assign38.gno | 10 ++ gnovm/tests/files/fun28.gno | 10 ++ gnovm/tests/files/slice3.gno | 9 ++ gnovm/tests/files/var35.gno | 8 ++ 9 files changed, 200 insertions(+), 138 deletions(-) create mode 100644 gnovm/tests/files/add3.gno create mode 100644 gnovm/tests/files/assign38.gno create mode 100644 gnovm/tests/files/fun28.gno create mode 100644 gnovm/tests/files/slice3.gno create mode 100644 gnovm/tests/files/var35.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 78b11a4ebc5..6e749053d72 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -743,7 +743,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { for i, cx := range n.Cases { cx = Preprocess( store, last, cx).(Expr) - checkOrConvertType(store, last, &cx, tt, false) // #nosec G601 + checkOrConvertType(store, last, n, &cx, tt, false) // #nosec G601 n.Cases[i] = cx } } @@ -882,7 +882,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Preprocess and convert tag if const. if n.X != nil { n.X = Preprocess(store, last, n.X).(Expr) - convertIfConst(store, last, n.X) + convertIfConst(store, last, n, n.X) } } return n, TRANS_CONTINUE @@ -1102,10 +1102,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // First, convert untyped as necessary. if !shouldSwapOnSpecificity(lcx.T, rcx.T) { // convert n.Left to right type. - checkOrConvertType(store, last, &n.Left, rcx.T, false) + checkOrConvertType(store, last, n, &n.Left, rcx.T, false) } else { // convert n.Right to left type. - checkOrConvertType(store, last, &n.Right, lcx.T, false) + checkOrConvertType(store, last, n, &n.Right, lcx.T, false) } // Then, evaluate the expression. cx := evalConst(store, last, n) @@ -1125,7 +1125,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { rnt.String())) } // convert n.Left to pt type, - checkOrConvertType(store, last, &n.Left, pt, false) + checkOrConvertType(store, last, n, &n.Left, pt, false) // if check pass, convert n.Right to (gno) pt type, rn := Expr(Call(pt.String(), n.Right)) // and convert result back. @@ -1154,7 +1154,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } if !isUntyped(rt) { // right is typed - checkOrConvertType(store, last, &n.Left, rt, false) + checkOrConvertType(store, last, n, &n.Left, rt, false) } else { if shouldSwapOnSpecificity(lt, rt) { checkUntypedShiftExpr(n.Right) @@ -1165,10 +1165,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } } else if lcx.T == nil { // LHS is nil. // convert n.Left to typed-nil type. - checkOrConvertType(store, last, &n.Left, rt, false) + checkOrConvertType(store, last, n, &n.Left, rt, false) } else { if isUntyped(rt) { - checkOrConvertType(store, last, &n.Right, lt, false) + checkOrConvertType(store, last, n, &n.Right, lt, false) } } } else if ric { // right is const, left is not @@ -1186,7 +1186,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // convert n.Left to (gno) pt type, ln := Expr(Call(pt.String(), n.Left)) // convert n.Right to pt type, - checkOrConvertType(store, last, &n.Right, pt, false) + checkOrConvertType(store, last, n, &n.Right, pt, false) // and convert result back. tx := constType(n, lnt) // reset/create n2 to preprocess left child. @@ -1212,7 +1212,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // both untyped, e.g. 1< float64. // (const) untyped bigint -> int. if !constConverted { - convertConst(store, last, arg0, nil) + convertConst(store, last, n, arg0, nil) } // evaluate the new expression. cx := evalConst(store, last, n) @@ -1397,15 +1397,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if isUntyped(at) { switch arg0.Op { case EQL, NEQ, LSS, GTR, LEQ, GEQ: - assertAssignableTo(at, ct, false) + assertAssignableTo(n, at, ct, false) break default: - checkOrConvertType(store, last, &n.Args[0], ct, false) + checkOrConvertType(store, last, n, &n.Args[0], ct, false) } } case *UnaryExpr: if isUntyped(at) { - checkOrConvertType(store, last, &n.Args[0], ct, false) + checkOrConvertType(store, last, n, &n.Args[0], ct, false) } default: // do nothing @@ -1549,7 +1549,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { panic("should not happen") } // Specify function param/result generics. - sft := ft.Specify(store, argTVs, isVarg) + sft := ft.Specify(store, n, argTVs, isVarg) spts := sft.Params srts := FieldTypeList(sft.Results).Types() // If generics were specified, override attr @@ -1575,12 +1575,12 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { for i, tv := range argTVs { if hasVarg { if (len(spts) - 1) <= i { - assertAssignableTo(tv.T, spts[len(spts)-1].Type.Elem(), true) + assertAssignableTo(n, tv.T, spts[len(spts)-1].Type.Elem(), true) } else { - assertAssignableTo(tv.T, spts[i].Type, true) + assertAssignableTo(n, tv.T, spts[i].Type, true) } } else { - assertAssignableTo(tv.T, spts[i].Type, true) + assertAssignableTo(n, tv.T, spts[i].Type, true) } } } else { @@ -1591,16 +1591,16 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if len(spts) <= i { panic("expected final vargs slice but got many") } - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } else { - checkOrConvertType(store, last, &n.Args[i], + checkOrConvertType(store, last, n, &n.Args[i], spts[len(spts)-1].Type.Elem(), true) } } else { - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } } else { - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } } } @@ -1621,10 +1621,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case StringKind, ArrayKind, SliceKind: // Replace const index with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n.Index) + checkOrConvertIntegerKind(store, last, n, n.Index) case MapKind: mt := baseOf(gnoTypeOf(store, dt)).(*MapType) - checkOrConvertType(store, last, &n.Index, mt.Key, false) + checkOrConvertType(store, last, n, &n.Index, mt.Key, false) default: panic(fmt.Sprintf( "unexpected index base kind for type %s", @@ -1635,15 +1635,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case *SliceExpr: // Replace const L/H/M with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n.Low) - checkOrConvertIntegerKind(store, last, n.High) - checkOrConvertIntegerKind(store, last, n.Max) + checkOrConvertIntegerKind(store, last, n, n.Low) + checkOrConvertIntegerKind(store, last, n, n.High) + checkOrConvertIntegerKind(store, last, n, n.Max) // if n.X is untyped, convert to corresponding type t := evalStaticTypeOf(store, last, n.X) if isUntyped(t) { dt := defaultTypeOf(t) - checkOrConvertType(store, last, &n.X, dt, false) + checkOrConvertType(store, last, n, &n.X, dt, false) } // TRANS_LEAVE ----------------------- @@ -1722,28 +1722,28 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { key := n.Elts[i].Key.(*NameExpr).Name path := cclt.GetPathForName(key) ft := cclt.GetStaticTypeOfAt(path) - checkOrConvertType(store, last, &n.Elts[i].Value, ft, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, ft, false) } } else { for i := 0; i < len(n.Elts); i++ { ft := cclt.Fields[i].Type - checkOrConvertType(store, last, &n.Elts[i].Value, ft, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, ft, false) } } case *ArrayType: for i := 0; i < len(n.Elts); i++ { - convertType(store, last, &n.Elts[i].Key, IntType) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Elt, false) + convertType(store, last, n, &n.Elts[i].Key, IntType) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt, false) } case *SliceType: for i := 0; i < len(n.Elts); i++ { - convertType(store, last, &n.Elts[i].Key, IntType) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Elt, false) + convertType(store, last, n, &n.Elts[i].Key, IntType) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt, false) } case *MapType: for i := 0; i < len(n.Elts); i++ { - checkOrConvertType(store, last, &n.Elts[i].Key, cclt.Key, false) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Value, false) + checkOrConvertType(store, last, n, &n.Elts[i].Key, cclt.Key, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Value, false) } case *NativeType: clt = cclt.GnoType(store) @@ -1943,7 +1943,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *FieldTypeExpr: // Replace const Tag with default *ConstExpr. - convertIfConst(store, last, n.Tag) + convertIfConst(store, last, n, n.Tag) // TRANS_LEAVE ----------------------- case *ArrayTypeExpr: @@ -1952,7 +1952,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } else { // Replace const Len with int *ConstExpr. cx := evalConst(store, last, n.Len) - convertConst(store, last, cx, IntType) + convertConst(store, last, n, cx, IntType) n.Len = cx } // NOTE: For all TypeExprs, the node is not replaced @@ -1993,7 +1993,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Rhs consts become default *ConstExprs. for _, rx := range n.Rhs { // NOTE: does nothing if rx is "nil". - convertIfConst(store, last, rx) + convertIfConst(store, last, n, rx) } nameExprs := make(NameExprs, len(n.Lhs)) @@ -2001,7 +2001,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { nameExprs[i] = *n.Lhs[i].(*NameExpr) } - defineOrDecl(store, last, false, nameExprs, nil, n.Rhs) + defineOrDecl(store, last, n, false, nameExprs, nil, n.Rhs) } else { // ASSIGN, or assignment operation (+=, -=, <<=, etc.) // NOTE: Keep in sync with DEFINE above. if len(n.Lhs) > len(n.Rhs) { @@ -2090,11 +2090,11 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } else { // len(Lhs) == len(Rhs) if n.Op == SHL_ASSIGN || n.Op == SHR_ASSIGN { // Special case if shift assign <<= or >>=. - convertType(store, last, &n.Rhs[0], UintType) + convertType(store, last, n, &n.Rhs[0], UintType) } else if n.Op == ADD_ASSIGN || n.Op == SUB_ASSIGN || n.Op == MUL_ASSIGN || n.Op == QUO_ASSIGN || n.Op == REM_ASSIGN { // e.g. a += b, single value for lhs and rhs, lt := evalStaticTypeOf(store, last, n.Lhs[0]) - checkOrConvertType(store, last, &n.Rhs[0], lt, true) + checkOrConvertType(store, last, n, &n.Rhs[0], lt, true) } else { // all else, like BAND_ASSIGN, etc // General case: a, b = x, y. for i, lx := range n.Lhs { @@ -2104,7 +2104,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // if lt is interface, nothing will happen - checkOrConvertType(store, last, &n.Rhs[i], lt, true) + checkOrConvertType(store, last, n, &n.Rhs[i], lt, true) } } } @@ -2181,12 +2181,12 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *ForStmt: // Cond consts become bool *ConstExprs. - checkOrConvertBoolKind(store, last, n.Cond) + checkOrConvertBoolKind(store, last, n, n.Cond) // TRANS_LEAVE ----------------------- case *IfStmt: // Cond consts become bool *ConstExprs. - checkOrConvertBoolKind(store, last, n.Cond) + checkOrConvertBoolKind(store, last, n, n.Cond) // TRANS_LEAVE ----------------------- case *RangeStmt: @@ -2242,7 +2242,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // XXX how to deal? panic("not yet implemented") } else { - checkOrConvertType(store, last, &n.Results[i], rt, false) + checkOrConvertType(store, last, n, &n.Results[i], rt, false) } } } @@ -2250,7 +2250,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *SendStmt: // Value consts become default *ConstExprs. - checkOrConvertType(store, last, &n.Value, nil, false) + checkOrConvertType(store, last, n, &n.Value, nil, false) // TRANS_LEAVE ----------------------- case *SelectCaseStmt: @@ -2303,7 +2303,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // runDeclaration(), as this uses OpStaticTypeOf. } - defineOrDecl(store, last, n.Const, n.NameExprs, n.Type, n.Values) + defineOrDecl(store, last, n, n.Const, n.NameExprs, n.Type, n.Values) // TODO make note of constance in static block for // future use, or consider "const paths". set as @@ -2383,6 +2383,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { func defineOrDecl( store Store, bn BlockNode, + n Node, isConst bool, nameExprs []NameExpr, typeExpr Expr, @@ -2399,9 +2400,9 @@ func defineOrDecl( tvs := make([]TypedValue, numNames) if numVals == 1 && numNames > 1 { - parseMultipleAssignFromOneExpr(sts, tvs, store, bn, nameExprs, typeExpr, valueExprs[0]) + parseMultipleAssignFromOneExpr(store, bn, n, sts, tvs, nameExprs, typeExpr, valueExprs[0]) } else { - parseAssignFromExprList(sts, tvs, store, bn, isConst, nameExprs, typeExpr, valueExprs) + parseAssignFromExprList(store, bn, n, sts, tvs, isConst, nameExprs, typeExpr, valueExprs) } node := skipFile(bn) @@ -2420,10 +2421,11 @@ func defineOrDecl( // parseAssignFromExprList parses assignment to multiple variables from a list of expressions. // This function will alter the value of sts, tvs. func parseAssignFromExprList( - sts []Type, - tvs []TypedValue, store Store, bn BlockNode, + n Node, + sts []Type, + tvs []TypedValue, isConst bool, nameExprs []NameExpr, typeExpr Expr, @@ -2450,7 +2452,7 @@ func parseAssignFromExprList( } // Convert if const to nt. for i := range valueExprs { - checkOrConvertType(store, bn, &valueExprs[i], nt, false) + checkOrConvertType(store, bn, n, &valueExprs[i], nt, false) } } else if isConst { // Derive static type from values. @@ -2462,10 +2464,10 @@ func parseAssignFromExprList( // Convert n.Value to default type. for i, vx := range valueExprs { if cx, ok := vx.(*ConstExpr); ok { - convertConst(store, bn, cx, nil) + convertConst(store, bn, n, cx, nil) // convertIfConst(store, last, vx) } else { - checkOrConvertType(store, bn, &vx, nil, false) + checkOrConvertType(store, bn, n, &vx, nil, false) } vt := evalStaticTypeOf(store, bn, vx) sts[i] = vt @@ -2506,10 +2508,11 @@ func parseAssignFromExprList( // - a, b := n.(T) // - a, b := n[i], where n is a map func parseMultipleAssignFromOneExpr( - sts []Type, - tvs []TypedValue, store Store, bn BlockNode, + n Node, + sts []Type, + tvs []TypedValue, nameExprs []NameExpr, typeExpr Expr, valueExpr Expr, @@ -2567,7 +2570,7 @@ func parseMultipleAssignFromOneExpr( if st != nil { tt := tuple.Elts[i] - if checkAssignableTo(tt, st, false) != nil { + if checkAssignableTo(n, tt, st, false) != nil { panic( fmt.Sprintf( "cannot use %v (value of type %s) as %s value in assignment", @@ -3491,14 +3494,14 @@ func isConstType(x Expr) bool { } // check before convert type -func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative bool) { +func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type, autoNative bool) { if debug { debug.Printf("checkOrConvertType, *x: %v:, t:%v \n", *x, t) } if cx, ok := (*x).(*ConstExpr); ok { if _, ok := t.(*NativeType); !ok { // not native type, refer to time4_native.gno. // e.g. int(1) == int8(1) - assertAssignableTo(cx.T, t, autoNative) + assertAssignableTo(n, cx.T, t, autoNative) } } else if bx, ok := (*x).(*BinaryExpr); ok && (bx.Op == SHL || bx.Op == SHR) { xt := evalStaticTypeOf(store, last, *x) @@ -3507,22 +3510,22 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative } if isUntyped(xt) { // check assignable first, see: types/shift_b6.gno - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) if t == nil || t.Kind() == InterfaceKind { t = defaultTypeOf(xt) } bx.assertShiftExprCompatible2(t) - checkOrConvertType(store, last, &bx.Left, t, autoNative) + checkOrConvertType(store, last, n, &bx.Left, t, autoNative) } else { - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) } return } else if *x != nil { xt := evalStaticTypeOf(store, last, *x) if t != nil { - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) } if isUntyped(xt) { // Push type into expr if qualifying binary expr. @@ -3534,8 +3537,8 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative rt := evalStaticTypeOf(store, last, bx.Right) if t != nil { // push t into bx.Left and bx.Right - checkOrConvertType(store, last, &bx.Left, t, autoNative) - checkOrConvertType(store, last, &bx.Right, t, autoNative) + checkOrConvertType(store, last, n, &bx.Left, t, autoNative) + checkOrConvertType(store, last, n, &bx.Right, t, autoNative) return } else { if shouldSwapOnSpecificity(lt, rt) { @@ -3546,11 +3549,11 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative // without a specific context type, '1.0< + (const (undefined)) (mismatched types int and untyped nil) diff --git a/gnovm/tests/files/assign38.gno b/gnovm/tests/files/assign38.gno new file mode 100644 index 00000000000..5ef3549ccf6 --- /dev/null +++ b/gnovm/tests/files/assign38.gno @@ -0,0 +1,10 @@ +package main + +func main() { + a := 1 + a = nil + println(a) +} + +// Error: +// main/files/assign38.gno:5:2: cannot use nil as int value in assignment diff --git a/gnovm/tests/files/fun28.gno b/gnovm/tests/files/fun28.gno new file mode 100644 index 00000000000..cf969f9f34b --- /dev/null +++ b/gnovm/tests/files/fun28.gno @@ -0,0 +1,10 @@ +package main + +func f(i int) {} + +func main() { + f(nil) +} + +// Error: +// main/files/fun28.gno:6:2: cannot use nil as int value in argument to f diff --git a/gnovm/tests/files/slice3.gno b/gnovm/tests/files/slice3.gno new file mode 100644 index 00000000000..1132da01420 --- /dev/null +++ b/gnovm/tests/files/slice3.gno @@ -0,0 +1,9 @@ +package main + +func main() { + i := []string{nil} + println(i) +} + +// Error: +// main/files/slice3.gno:4:7: cannot use nil as string value in array, slice literal or map literal diff --git a/gnovm/tests/files/var35.gno b/gnovm/tests/files/var35.gno new file mode 100644 index 00000000000..87b1cc68590 --- /dev/null +++ b/gnovm/tests/files/var35.gno @@ -0,0 +1,8 @@ +package main + +func main() { + var i int = nil +} + +// Error: +// main/files/var35.gno:4:6: cannot use nil as int value in variable declaration From ed4ebe826b24189d0fcaa50c57eab03ff178c9fa Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Tue, 10 Dec 2024 02:36:38 -0800 Subject: [PATCH 62/86] fix(gnovm): do not allow nil as type declaration (#3309) Fix https://github.com/gnolang/gno/issues/3307 --------- Co-authored-by: hieu.ha --- gnovm/pkg/gnolang/preprocess.go | 6 ++++++ gnovm/tests/files/type40.gno | 2 +- gnovm/tests/files/type41.gno | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 gnovm/tests/files/type41.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 6e749053d72..a3e498710bb 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4283,6 +4283,12 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { if isBlankIdentifier(tx) { panic("cannot use _ as value or type") } + + // do not allow nil as type. + if tx.Name == "nil" { + panic("nil is not a type") + } + if tv := last.GetValueRef(store, tx.Name, true); tv != nil { t = tv.GetType() if dt, ok := t.(*DeclaredType); ok { diff --git a/gnovm/tests/files/type40.gno b/gnovm/tests/files/type40.gno index 65210798007..fe312e220e0 100644 --- a/gnovm/tests/files/type40.gno +++ b/gnovm/tests/files/type40.gno @@ -43,4 +43,4 @@ func main() { // 5 // 6 // 7 -// yo \ No newline at end of file +// yo diff --git a/gnovm/tests/files/type41.gno b/gnovm/tests/files/type41.gno new file mode 100644 index 00000000000..ea1a3b1df24 --- /dev/null +++ b/gnovm/tests/files/type41.gno @@ -0,0 +1,9 @@ +package main + +type A nil + +func main() { +} + +// Error: +// main/files/type41.gno:3:6: nil is not a type From 8e7fb503adc7a728ea30fedd0891f27d80a2fe1b Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:02:55 +0100 Subject: [PATCH 63/86] feat(github-bot): refactor comment + add force skip (#3311) This PR significantly modifies the github-bot's comment and adds a button to force the success of its CI check, even it the requirements provided in the config are not met. Related to https://github.com/gnolang/gno/issues/3238#issuecomment-2526174402 **Edit**: I updated [the comment below](https://github.com/gnolang/gno/pull/3311#issuecomment-2528477336) by running the bot on my laptop if you want to see the result (so the skip button is not working yet). --- contribs/github-bot/internal/check/check.go | 32 +++++++++++++++---- contribs/github-bot/internal/check/comment.go | 30 +++++++++-------- .../github-bot/internal/check/comment.tmpl | 32 ++++++++++++++----- .../github-bot/internal/check/comment_test.go | 19 +++++++++-- contribs/github-bot/internal/config/config.go | 9 ++++++ 5 files changed, 91 insertions(+), 31 deletions(-) diff --git a/contribs/github-bot/internal/check/check.go b/contribs/github-bot/internal/check/check.go index 5ca2235e823..cb1848b757c 100644 --- a/contribs/github-bot/internal/check/check.go +++ b/contribs/github-bot/internal/check/check.go @@ -101,7 +101,8 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { go func(pr *github.PullRequest) { defer wg.Done() commentContent := CommentContent{} - commentContent.allSatisfied = true + commentContent.AutoAllSatisfied = true + commentContent.ManualAllSatisfied = true // Iterate over all automatic rules in config. for _, autoRule := range autoRules { @@ -120,7 +121,7 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) c.Satisfied = true } else { - commentContent.allSatisfied = false + commentContent.AutoAllSatisfied = false } c.ConditionDetails = ifDetails.String() @@ -160,8 +161,14 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { }, ) - if checkedBy == "" { - commentContent.allSatisfied = false + // If this check is the special one, store its state in the dedicated var. + if manualRule.Description == config.ForceSkipDescription { + if checkedBy != "" { + commentContent.ForceSkip = true + } + } else if checkedBy == "" { + // Or if its a normal check, just verify if it was checked by someone. + commentContent.ManualAllSatisfied = false } } @@ -224,9 +231,20 @@ func logResults(logger logger.Logger, prNum int, commentContent CommentContent) } logger.Infof("Conclusion:") - if commentContent.allSatisfied { - logger.Infof("%s All requirements are satisfied\n", utils.Success) + + if commentContent.AutoAllSatisfied { + logger.Infof("%s All automated checks are satisfied", utils.Success) + } else { + logger.Infof("%s Some automated checks are not satisfied", utils.Fail) + } + + if commentContent.ManualAllSatisfied { + logger.Infof("%s All manual checks are satisfied\n", utils.Success) } else { - logger.Infof("%s Not all requirements are satisfied\n", utils.Fail) + logger.Infof("%s Some manual checks are not satisfied\n", utils.Fail) + } + + if commentContent.ForceSkip { + logger.Infof("%s Bot checks are force skipped\n", utils.Success) } } diff --git a/contribs/github-bot/internal/check/comment.go b/contribs/github-bot/internal/check/comment.go index 297395ffe4b..d2b386cfa2e 100644 --- a/contribs/github-bot/internal/check/comment.go +++ b/contribs/github-bot/internal/check/comment.go @@ -24,9 +24,9 @@ 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+)\))?$)`) + manualCheckLine = regexp.MustCompile(`(?m:^- \[([ xX])\] (.+?)(?: \(checked by @([A-Za-z0-9-]+)\))?$)`) // Regex for capturing only the checkboxes. - checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) + checkboxes = regexp.MustCompile(`(?m:^- \[[ xX]\])`) // Regex used to capture markdown links. markdownLink = regexp.MustCompile(`\[(.*)\]\([^)]*\)`) ) @@ -46,9 +46,11 @@ type ManualContent struct { Teams []string } type CommentContent struct { - AutoRules []AutoContent - ManualRules []ManualContent - allSatisfied bool + AutoRules []AutoContent + ManualRules []ManualContent + AutoAllSatisfied bool + ManualAllSatisfied bool + ForceSkip bool } type manualCheckDetails struct { @@ -64,10 +66,10 @@ func getCommentManualChecks(commentBody string) 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] + status := strings.ToLower(match[1]) // if X captured, convert it to x. checkedBy := "" - if len(match) > 4 { - checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x. + if len(match) > 3 { + checkedBy = match[3] } checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy} @@ -261,13 +263,15 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen var ( context = "Merge Requirements" targetURL = comment.GetHTMLURL() - state = "failure" - description = "Some requirements are not satisfied yet. See bot comment." + state = "success" + description = "All requirements are satisfied." ) - if content.allSatisfied { - state = "success" - description = "All requirements are satisfied." + if content.ForceSkip { + description = "Bot checks are skipped for this PR." + } else if !content.AutoAllSatisfied || !content.ManualAllSatisfied { + state = "failure" + description = "Some requirements are not satisfied yet. See bot comment." } // Update or create commit status. diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl index 4312019dd2e..d9b633a69d5 100644 --- a/contribs/github-bot/internal/check/comment.tmpl +++ b/contribs/github-bot/internal/check/comment.tmpl @@ -1,19 +1,34 @@ -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. +#### 🛠 PR Checks Summary +{{ if and .AutoRules (not .AutoAllSatisfied) }}{{ range .AutoRules }}{{ if not .Satisfied }} 🔴 {{ .Description }} +{{end}}{{end}}{{ else }}All **Automated Checks** passed. ✅{{end}} -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. +##### Manual Checks (for Reviewers): +{{ 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 }} -These requirements are defined in this [configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). +
Read More -## Automated Checks +🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. +##### ✅ Automated Checks (for Contributors): {{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} {{ end }}{{ else }}*No automated checks match this pull request.*{{ end }} -## Manual Checks +##### ☑️ Contributor Actions: +1. Fix any issues flagged by automated checks. +2. Follow the Contributor Checklist to ensure your PR is ready for review. + - Add new tests, or document why they are unnecessary. + - Provide clear examples/screenshots, if necessary. + - Update documentation, if required. + - Ensure no breaking changes, or include `BREAKING CHANGE` notes. + - Link related issues/PRs, where applicable. -{{ 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 }} +##### ☑️ Reviewer Actions: +1. Complete manual checks for the PR, including the guidelines and additional checks if applicable. + +##### 📚 Resources: +- [Report a bug with the bot](https://github.com/gnolang/gno/issues/3238). +- [View the bot’s configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). {{ if or .AutoRules .ManualRules }}
Debug
{{ if .AutoRules }}
Automated Checks
@@ -52,3 +67,4 @@ These requirements are defined in this [configuration file](https://github.com/g {{ end }}
{{ end }} +
diff --git a/contribs/github-bot/internal/check/comment_test.go b/contribs/github-bot/internal/check/comment_test.go index 0334b76f95c..29886f80f43 100644 --- a/contribs/github-bot/internal/check/comment_test.go +++ b/contribs/github-bot/internal/check/comment_test.go @@ -31,31 +31,44 @@ func TestGeneratedComment(t *testing.T) { {Description: "Test automatic 5", Satisfied: false}, } manualRules := []ManualContent{ - {Description: "Test manual 1", CheckedBy: "user_1"}, + {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"}, + {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") + assert.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") content.AutoRules = autoRules + content.AutoAllSatisfied = true 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.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed 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.AutoAllSatisfied = false + 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.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") + assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") + assert.Equal(t, 3+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") + assert.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") manualChecks := getCommentManualChecks(commentText) assert.Equal(t, len(manualChecks), len(manualRules), "wrong number of manual checks found") diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index 2d595c7ce51..fd29f5e5f57 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -22,6 +22,10 @@ type ManualCheck struct { Teams Teams // Members of these teams can check the checkbox to make the check pass. } +// This is the description for a persistent rule with a non-standard behavior +// that allow maintainer to force the "success" state of the CI check +const ForceSkipDescription = "**SKIP**: Do not block the CI for this PR" + // 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) { @@ -53,6 +57,11 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { } manual := []ManualCheck{ + { + // WARN: Do not edit this special rule which must remain persistent. + Description: ForceSkipDescription, + If: c.Always(), + }, { Description: "The pull request description provides enough details", If: c.Not(c.AuthorInTeam(gh, "core-contributors")), From 4e7305b67ba23f079e33e73e31e0abd90c33d4b9 Mon Sep 17 00:00:00 2001 From: matijamarjanovic <93043005+matijamarjanovic@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:15:51 +0100 Subject: [PATCH 64/86] feat: add Matija's Homepage realm to examples (#2916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This pull request adds a new realm example to the Gno `examples` repository—Matija's Homepage. It showcases a personal homepage built on the Gno chain where users can interact by voting with GNOT tokens to change the page's color. The more tokens users send, the greater influence they have on the color scheme, providing an interactive and dynamic experience. ### Key Features - **Profile Section**: Displays a personal profile with an image and description. - **Color Voting**: Users can vote for the page's color (red, green, blue) by sending GNOT tokens. RGB values are adjusted based on the amount sent. - **Dynamic Updates**: The homepage dynamically updates the color based on votes, showcasing real-time interaction on the Gno blockchain. - **Links to GitHub and LinkedIn**: Includes buttons for GitHub and LinkedIn, making it easy for users to connect. ### Tools & Technologies - Utilizes Gno's native functions to handle voting and token transfers. - Provides a simple, yet effective example of how personal realms can be interactive and engaging on the Gno platform. ### Why this is valuable This example highlights the possibilities of personal realms on Gno, showing how users can create unique and interactive profiles. It’s a fun and approachable entry point for anyone new to Gno development, while also demonstrating the platform's flexibility and potential for creative expression. --------- Co-authored-by: Morgan Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../r/matijamarjanovic/home/config.gno | 64 +++++ .../gno.land/r/matijamarjanovic/home/gno.mod | 1 + .../gno.land/r/matijamarjanovic/home/home.gno | 238 ++++++++++++++++++ .../r/matijamarjanovic/home/home_test.gno | 134 ++++++++++ 4 files changed, 437 insertions(+) create mode 100644 examples/gno.land/r/matijamarjanovic/home/config.gno create mode 100644 examples/gno.land/r/matijamarjanovic/home/gno.mod create mode 100644 examples/gno.land/r/matijamarjanovic/home/home.gno create mode 100644 examples/gno.land/r/matijamarjanovic/home/home_test.gno diff --git a/examples/gno.land/r/matijamarjanovic/home/config.gno b/examples/gno.land/r/matijamarjanovic/home/config.gno new file mode 100644 index 00000000000..2a9669c0b58 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/config.gno @@ -0,0 +1,64 @@ +package home + +import ( + "errors" + "std" +) + +var ( + mainAddr = std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y") // matija's main address + backupAddr std.Address // backup address + + errorInvalidAddr = errors.New("config: invalid address") + errorUnauthorized = errors.New("config: unauthorized") +) + +func Address() std.Address { + return mainAddr +} + +func Backup() std.Address { + return backupAddr +} + +func SetAddress(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + mainAddr = newAddress + return nil +} + +func SetBackup(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + backupAddr = newAddress + return nil +} + +func checkAuthorized() error { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + return errorUnauthorized + } + + return nil +} + +func AssertAuthorized() { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + panic(errorUnauthorized) + } +} diff --git a/examples/gno.land/r/matijamarjanovic/home/gno.mod b/examples/gno.land/r/matijamarjanovic/home/gno.mod new file mode 100644 index 00000000000..0457c947c01 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/matijamarjanovic/home diff --git a/examples/gno.land/r/matijamarjanovic/home/home.gno b/examples/gno.land/r/matijamarjanovic/home/home.gno new file mode 100644 index 00000000000..3757324108a --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home.gno @@ -0,0 +1,238 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/r/leon/hof" +) + +var ( + pfp string // link to profile picture + pfpCaption string // profile picture caption + abtMe string + + modernVotes int64 + classicVotes int64 + minimalVotes int64 + currentTheme string + + modernLink string + classicLink string + minimalLink string +) + +func init() { + pfp = "https://static.artzone.ai/media/38734/conversions/IPF9dR7ro7n05CmMLLrXIojycr1qdLFxgutaaanG-w768.webp" + pfpCaption = "My profile picture - Tarantula Nebula" + abtMe = `Motivated Computer Science student with strong + analytical and problem-solving skills. Proficient in + programming and version control, with a high level of + focus and attention to detail. Eager to apply academic + knowledge to real-world projects and contribute to + innovative technology solutions. + In addition to my academic pursuits, + I enjoy traveling and staying active through weightlifting. + I have a keen interest in electronic music and often explore various genres. + I believe in maintaining a balanced lifestyle that complements my professional development.` + + modernVotes = 0 + classicVotes = 0 + minimalVotes = 0 + currentTheme = "classic" + modernLink = "https://www.google.com" + classicLink = "https://www.google.com" + minimalLink = "https://www.google.com" + hof.Register() +} + +func UpdatePFP(url, caption string) { + AssertAuthorized() + pfp = url + pfpCaption = caption +} + +func UpdateAboutMe(col1 string) { + AssertAuthorized() + abtMe = col1 +} + +func maxOfThree(a, b, c int64) int64 { + max := a + if b > max { + max = b + } + if c > max { + max = c + } + return max +} + +func VoteModern() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + modernVotes += votes + updateCurrentTheme() +} + +func VoteClassic() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + classicVotes += votes + updateCurrentTheme() +} + +func VoteMinimal() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + minimalVotes += votes + updateCurrentTheme() +} + +func updateCurrentTheme() { + maxVotes := maxOfThree(modernVotes, classicVotes, minimalVotes) + + if maxVotes == modernVotes { + currentTheme = "modern" + } else if maxVotes == classicVotes { + currentTheme = "classic" + } else { + currentTheme = "minimal" + } +} + +func CollectBalance() { + AssertAuthorized() + + banker := std.GetBanker(std.BankerTypeRealmSend) + ownerAddr := Address() + + banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr())) +} + +func Render(path string) string { + var sb strings.Builder + + // Theme-specific header styling + switch currentTheme { + case "modern": + // Modern theme - Clean and minimalist with emojis + sb.WriteString(md.H1("🚀 Matija's Space")) + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(md.Italic(pfpCaption)) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(abtMe) + sb.WriteString("\n") + + case "minimal": + // Minimal theme - No emojis, minimal formatting + sb.WriteString(md.H1("Matija Marjanovic")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + + default: // classic + // Classic theme - Traditional blog style with decorative elements + sb.WriteString(md.H1("✨ Welcome to Matija's Homepage ✨")) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("About me")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + } + + // Theme-specific voting section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("🎨 Theme Selector")) + sb.WriteString("Choose your preferred viewing experience:\n") + items := []string{ + md.Link(ufmt.Sprintf("Modern Design (%d votes)", modernVotes), modernLink), + md.Link(ufmt.Sprintf("Classic Style (%d votes)", classicVotes), classicLink), + md.Link(ufmt.Sprintf("Minimal Look (%d votes)", minimalVotes), minimalLink), + } + sb.WriteString(md.BulletList(items)) + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.H3("Theme Selection")) + sb.WriteString(ufmt.Sprintf("Current theme: %s\n", currentTheme)) + sb.WriteString(ufmt.Sprintf("Votes - Modern: %d | Classic: %d | Minimal: %d\n", + modernVotes, classicVotes, minimalVotes)) + sb.WriteString(md.Link("Modern", modernLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Classic", classicLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Minimal", minimalLink)) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("✨ Theme Customization ✨")) + sb.WriteString(md.Bold("Choose Your Preferred Theme:")) + sb.WriteString("\n\n") + items := []string{ + ufmt.Sprintf("Modern 🚀 (%d votes) - %s", modernVotes, md.Link("Vote", modernLink)), + ufmt.Sprintf("Classic ✨ (%d votes) - %s", classicVotes, md.Link("Vote", classicLink)), + ufmt.Sprintf("Minimal ⚡ (%d votes) - %s", minimalVotes, md.Link("Vote", minimalLink)), + } + sb.WriteString(md.BulletList(items)) + } + + // Theme-specific footer/links section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H3("✨ Connect With Me")) + items := []string{ + md.Link("🌟 GitHub", "https://github.com/matijamarjanovic"), + md.Link("💼 LinkedIn", "https://www.linkedin.com/in/matijamarjanovic"), + } + sb.WriteString(md.BulletList(items)) + } + + return sb.String() +} + +func UpdateModernLink(link string) { + AssertAuthorized() + modernLink = link +} + +func UpdateClassicLink(link string) { + AssertAuthorized() + classicLink = link +} + +func UpdateMinimalLink(link string) { + AssertAuthorized() + minimalLink = link +} diff --git a/examples/gno.land/r/matijamarjanovic/home/home_test.gno b/examples/gno.land/r/matijamarjanovic/home/home_test.gno new file mode 100644 index 00000000000..8cc6e6e5608 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home_test.gno @@ -0,0 +1,134 @@ +package home + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +// Helper function to set up test environment +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) +} + +func TestUpdatePFP(t *testing.T) { + setupTest() + pfp = "" + pfpCaption = "" + + UpdatePFP("https://example.com/pic.png", "New Caption") + + urequire.Equal(t, pfp, "https://example.com/pic.png", "Profile picture URL should be updated") + urequire.Equal(t, pfpCaption, "New Caption", "Profile picture caption should be updated") +} + +func TestUpdateAboutMe(t *testing.T) { + setupTest() + abtMe = "" + + UpdateAboutMe("This is my new bio.") + + urequire.Equal(t, abtMe, "This is my new bio.", "About Me should be updated") +} + +func TestVoteModern(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteModern() + + uassert.Equal(t, int64(75000000), modernVotes, "Modern votes should be calculated correctly") + uassert.Equal(t, "modern", currentTheme, "Theme should be updated to modern") +} + +func TestVoteClassic(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteClassic() + + uassert.Equal(t, int64(75000000), classicVotes, "Classic votes should be calculated correctly") + uassert.Equal(t, "classic", currentTheme, "Theme should be updated to classic") +} + +func TestVoteMinimal(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteMinimal() + + uassert.Equal(t, int64(75000000), minimalVotes, "Minimal votes should be calculated correctly") + uassert.Equal(t, "minimal", currentTheme, "Theme should be updated to minimal") +} + +func TestRender(t *testing.T) { + setupTest() + // Reset the state to known values + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + currentTheme = "classic" + pfp = "https://example.com/pic.png" + pfpCaption = "Test Caption" + abtMe = "Test About Me" + + out := Render("") + urequire.NotEqual(t, out, "", "Render output should not be empty") + + // Test classic theme specific content + uassert.True(t, strings.Contains(out, "✨ Welcome to Matija's Homepage ✨"), "Classic theme should have correct header") + uassert.True(t, strings.Contains(out, pfp), "Should contain profile picture URL") + uassert.True(t, strings.Contains(out, pfpCaption), "Should contain profile picture caption") + uassert.True(t, strings.Contains(out, "About me"), "Should contain About me section") + uassert.True(t, strings.Contains(out, abtMe), "Should contain about me content") + uassert.True(t, strings.Contains(out, "Theme Customization"), "Should contain theme customization section") + uassert.True(t, strings.Contains(out, "Connect With Me"), "Should contain connect section") +} + +func TestRenderModernTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 100, 0, 0 + currentTheme = "modern" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "🚀 Matija's Space"), "Modern theme should have correct header") +} + +func TestRenderMinimalTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 100 + currentTheme = "minimal" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "Matija Marjanovic"), "Minimal theme should have correct header") +} + +func TestUpdateLinks(t *testing.T) { + setupTest() + + newLink := "https://example.com/vote" + + UpdateModernLink(newLink) + urequire.Equal(t, modernLink, newLink, "Modern link should be updated") + + UpdateClassicLink(newLink) + urequire.Equal(t, classicLink, newLink, "Classic link should be updated") + + UpdateMinimalLink(newLink) + urequire.Equal(t, minimalLink, newLink, "Minimal link should be updated") +} From 5c31552b05c78575f45876ab07abd73c1d96bf27 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Tue, 10 Dec 2024 21:50:53 +0800 Subject: [PATCH 65/86] fix(gnovm): make static-analysis handle block stmt (#3313) as the title says. it give incorrect error before fix: ``` unexpected panic: main/files/block0.gno:3:1: [function "foo" does not terminate] ``` --- gnovm/pkg/gnolang/static_analysis.go | 2 ++ gnovm/tests/files/block0.gno | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 gnovm/tests/files/block0.gno diff --git a/gnovm/pkg/gnolang/static_analysis.go b/gnovm/pkg/gnolang/static_analysis.go index 311a0d42feb..7094ccbb4c8 100644 --- a/gnovm/pkg/gnolang/static_analysis.go +++ b/gnovm/pkg/gnolang/static_analysis.go @@ -108,6 +108,8 @@ func (s *staticAnalysis) staticAnalysisExpr(expr Expr) bool { // indicating whether a statement is terminating or not func (s *staticAnalysis) staticAnalysisStmt(stmt Stmt) bool { switch n := stmt.(type) { + case *BlockStmt: + return s.staticAnalysisBlockStmt(n.Body) case *BranchStmt: switch n.Op { case BREAK: diff --git a/gnovm/tests/files/block0.gno b/gnovm/tests/files/block0.gno new file mode 100644 index 00000000000..b6d554ce500 --- /dev/null +++ b/gnovm/tests/files/block0.gno @@ -0,0 +1,14 @@ +package main + +func foo() int { + { + return 1 + } +} + +func main() { + println(foo()) +} + +// Output: +// 1 From c33cf676daed03a29ca85f7386daf98e35d2b38f Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:20:52 -0800 Subject: [PATCH 66/86] fix: catch the out of gas exception in preprocess (#2638)
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 - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- .../gnoland/testdata/addpkg_outofgas.txtar | 57 +++++++++++++++++++ gnovm/pkg/gnolang/preprocess.go | 7 +++ 2 files changed, 64 insertions(+) create mode 100644 gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar diff --git a/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar new file mode 100644 index 00000000000..56050f4733b --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar @@ -0,0 +1,57 @@ +# ensure users get proper out of gas errors when they add packages + +# start a new node +gnoland start + +# add foo package +gnokey maketx addpkg -pkgdir $WORK/foo -pkgpath gno.land/r/foo -gas-fee 1000000ugnot -gas-wanted 220000 -broadcast -chainid=tendermint_test test1 + + +# add bar package +# out of gas at store.GetPackage() with gas 60000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 60000 -broadcast -chainid=tendermint_test test1 + +# Out of gas error + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + + +# out of gas at store.store.GetTypeSafe() with gas 63000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 63000 -broadcast -chainid=tendermint_test test1 + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + +-- foo/foo.gno -- +package foo + +type Counter int + +func Inc(i Counter) Counter{ + i = i+1 + return i +} + +-- bar/bar.gno -- +package bar + +import "gno.land/r/foo" + +type NewCounter foo.Counter + +func Add2(i NewCounter) NewCounter{ + i=i+2 + + return i +} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index a3e498710bb..15f268f6321 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -10,6 +10,7 @@ import ( "sync/atomic" "github.com/gnolang/gno/tm2/pkg/errors" + tmstore "github.com/gnolang/gno/tm2/pkg/store" ) const ( @@ -365,6 +366,12 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { func doRecover(stack []BlockNode, n Node) { if r := recover(); r != nil { + // Catch the out-of-gas exception and throw it + if exp, ok := r.(tmstore.OutOfGasException); ok { + exp.Descriptor = fmt.Sprintf("in preprocess: %v", r) + panic(exp) + } + if _, ok := r.(*PreprocessError); ok { // re-panic directly if this is a PreprocessError already. panic(r) From 7185cefe2e091cb1795aef1d3d4c4d938a81778c Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 11 Dec 2024 09:05:18 +0100 Subject: [PATCH 67/86] fix(gnovm): in op_binary, return typed booleans where appropriate (#3298) bool8.gno was failing, because the result of the `==` expression is an untyped boolean, while the first value is a typed boolean. This PR ensures that if either of the values in a binary expression is typed, we return a typed bool instead of an untyped bool. --------- Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/preprocess.go | 4 ++++ gnovm/tests/files/bool8.gno | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 gnovm/tests/files/bool8.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 15f268f6321..4ff182670cd 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3574,6 +3574,10 @@ func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type, au checkOrConvertType(store, last, n, &bx.Left, rt, autoNative) checkOrConvertType(store, last, n, &bx.Right, rt, autoNative) } + // this is not a constant expression; the result here should + // always be a BoolType. (in this scenario, we may have some + // UntypedBoolTypes) + t = BoolType default: // do nothing } diff --git a/gnovm/tests/files/bool8.gno b/gnovm/tests/files/bool8.gno new file mode 100644 index 00000000000..9efbbbe6da2 --- /dev/null +++ b/gnovm/tests/files/bool8.gno @@ -0,0 +1,17 @@ +package main + +// results from comparisons should not be untyped bools + +var a interface{} = true + +func main() { + buf := "hello=" + isEqual(a, (buf[len(buf)-1] == '=')) +} + +func isEqual(v1, v2 interface{}) { + println("v1 == v2", v1 == v2) +} + +// Output: +// v1 == v2 true From 6f48a5b6eb26e6dcb7cd2797c57f4545cb02f744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Wed, 11 Dec 2024 11:42:06 +0100 Subject: [PATCH 68/86] feat: generic datasource package (#3318) The new package is a generic implementation for datasources. It aims to be one possible solution to integrate/aggregate data from different realms. --- .../p/jeronimoalbi/datasource/datasource.gno | 103 +++++++++++ .../datasource/datasource_test.gno | 171 ++++++++++++++++++ .../p/jeronimoalbi/datasource/gno.mod | 1 + .../p/jeronimoalbi/datasource/query.gno | 70 +++++++ .../p/jeronimoalbi/datasource/query_test.gno | 104 +++++++++++ 5 files changed, 449 insertions(+) create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/datasource.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/gno.mod create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/query.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/query_test.gno diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno new file mode 100644 index 00000000000..bf80964a9a0 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno @@ -0,0 +1,103 @@ +// Package datasource defines generic interfaces for datasources. +// +// Datasources contain a set of records which can optionally be +// taggable. Tags can optionally be used to filter records by taxonomy. +// +// Datasources can help in cases where the data sent during +// communication between different realms needs to be generic +// to avoid direct dependencies. +package datasource + +import "errors" + +// ErrInvalidRecord indicates that a datasource contains invalid records. +var ErrInvalidRecord = errors.New("datasource records is not valid") + +type ( + // Fields defines an interface for read-only fields. + Fields interface { + // Has checks whether a field exists. + Has(name string) bool + + // Get retrieves the value associated with the given field. + Get(name string) (value interface{}, found bool) + } + + // Record defines a datasource record. + Record interface { + // ID returns the unique record's identifier. + ID() string + + // String returns a string representation of the record. + String() string + + // Fields returns record fields and values. + Fields() (Fields, error) + } + + // TaggableRecord defines a datasource record that supports tags. + // Tags can be used to build a taxonomy to filter records by category. + TaggableRecord interface { + // Tags returns a list of tags for the record. + Tags() []string + } + + // ContentRecord defines a datasource record that can return content. + ContentRecord interface { + // Content returns the record content. + Content() (string, error) + } + + // Iterator defines an iterator of datasource records. + Iterator interface { + // Next returns true when a new record is available. + Next() bool + + // Err returns any error raised when reading records. + Err() error + + // Record returns the current record. + Record() Record + } + + // Datasource defines a generic datasource. + Datasource interface { + // Records returns a new datasource records iterator. + Records(Query) Iterator + + // Size returns the total number of records in the datasource. + // When -1 is returned it means datasource doesn't support size. + Size() int + + // Record returns a single datasource record. + Record(id string) (Record, error) + } +) + +// NewIterator returns a new record iterator for a datasource query. +func NewIterator(ds Datasource, options ...QueryOption) Iterator { + return ds.Records(NewQuery(options...)) +} + +// QueryRecords return a slice of records for a datasource query. +func QueryRecords(ds Datasource, options ...QueryOption) ([]Record, error) { + var ( + records []Record + query = NewQuery(options...) + iter = ds.Records(query) + ) + + for i := 0; i < query.Count && iter.Next(); i++ { + r := iter.Record() + if r == nil { + return nil, ErrInvalidRecord + } + + records = append(records, r) + } + + if err := iter.Err(); err != nil { + return nil, err + } + return records, nil +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno new file mode 100644 index 00000000000..304a311ced7 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno @@ -0,0 +1,171 @@ +package datasource + +import ( + "errors" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNewIterator(t *testing.T) { + cases := []struct { + name string + records []Record + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + }, + { + name: "error", + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + iter := NewIterator(ds) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, iter.Err()) + return + } + + uassert.NoError(t, iter.Err()) + + for i := 0; iter.Next(); i++ { + r := iter.Record() + urequire.NotEqual(t, nil, r, "valid record") + urequire.True(t, i < len(tc.records), "iteration count") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +func TestQueryRecords(t *testing.T) { + cases := []struct { + name string + records []Record + recordCount int + options []QueryOption + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 3, + }, + { + name: "with count", + options: []QueryOption{WithCount(2)}, + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 2, + }, + { + name: "invalid record", + records: []Record{ + testRecord{id: "1"}, + nil, + testRecord{id: "3"}, + }, + err: ErrInvalidRecord, + }, + { + name: "iterator error", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "3"}, + }, + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + records, err := QueryRecords(ds, tc.options...) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, err) + return + } + + uassert.NoError(t, err) + + urequire.Equal(t, tc.recordCount, len(records), "record count") + for i, r := range records { + urequire.NotEqual(t, nil, r, "valid record") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +type testDatasource struct { + records []Record + err error +} + +func (testDatasource) Size() int { return -1 } +func (testDatasource) Record(string) (Record, error) { return nil, nil } +func (ds testDatasource) Records(Query) Iterator { return &testIter{records: ds.records, err: ds.err} } + +type testRecord struct { + id string + fields Fields + err error +} + +func (r testRecord) ID() string { return r.id } +func (r testRecord) String() string { return "str" + r.id } +func (r testRecord) Fields() (Fields, error) { return r.fields, r.err } + +type testIter struct { + index int + records []Record + current Record + err error +} + +func (it testIter) Err() error { return it.err } +func (it testIter) Record() Record { return it.current } + +func (it *testIter) Next() bool { + count := len(it.records) + if it.err != nil || count == 0 || it.index >= count { + return false + } + it.current = it.records[it.index] + it.index++ + return true +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/gno.mod b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod new file mode 100644 index 00000000000..3b398971b41 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod @@ -0,0 +1 @@ +module gno.land/p/jeronimoalbi/datasource diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query.gno b/examples/gno.land/p/jeronimoalbi/datasource/query.gno new file mode 100644 index 00000000000..f971f9c64db --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query.gno @@ -0,0 +1,70 @@ +package datasource + +import "gno.land/p/demo/avl" + +// DefaultQueryRecords defines the default number of records returned by queries. +const DefaultQueryRecords = 50 + +var defaultQuery = Query{Count: DefaultQueryRecords} + +type ( + // QueryOption configures datasource queries. + QueryOption func(*Query) + + // Query contains datasource query options. + Query struct { + // Offset of the first record to return during iteration. + Offset int + + // Count contains the number to records that query should return. + Count int + + // Tag contains a tag to use as filter for the records. + Tag string + + // Filters contains optional query filters by field value. + Filters avl.Tree + } +) + +// WithOffset configures query to return records starting from an offset. +func WithOffset(offset int) QueryOption { + return func(q *Query) { + q.Offset = offset + } +} + +// WithCount configures the number of records that query returns. +func WithCount(count int) QueryOption { + return func(q *Query) { + if count < 1 { + count = DefaultQueryRecords + } + q.Count = count + } +} + +// ByTag configures query to filter by tag. +func ByTag(tag string) QueryOption { + return func(q *Query) { + q.Tag = tag + } +} + +// WithFilter assigns a new filter argument to a query. +// This option can be used multiple times if more than one +// filter has to be given to the query. +func WithFilter(field string, value interface{}) QueryOption { + return func(q *Query) { + q.Filters.Set(field, value) + } +} + +// NewQuery creates a new datasource query. +func NewQuery(options ...QueryOption) Query { + q := defaultQuery + for _, apply := range options { + apply(&q) + } + return q +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno new file mode 100644 index 00000000000..6f78d41bb35 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno @@ -0,0 +1,104 @@ +package datasource + +import ( + "fmt" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestNewQuery(t *testing.T) { + cases := []struct { + name string + options []QueryOption + setup func() Query + }{ + { + name: "default", + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "with offset", + options: []QueryOption{WithOffset(100)}, + setup: func() Query { + return Query{ + Offset: 100, + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with count", + options: []QueryOption{WithCount(10)}, + setup: func() Query { + return Query{Count: 10} + }, + }, + { + name: "with invalid count", + options: []QueryOption{WithCount(0)}, + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "by tag", + options: []QueryOption{ByTag("foo")}, + setup: func() Query { + return Query{ + Tag: "foo", + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with filter", + options: []QueryOption{WithFilter("foo", 42)}, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + return q + }, + }, + { + name: "with multiple filters", + options: []QueryOption{ + WithFilter("foo", 42), + WithFilter("bar", "baz"), + }, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + q.Filters.Set("bar", "baz") + return q + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + want := tc.setup() + + // Act + q := NewQuery(tc.options...) + + // Assert + uassert.Equal(t, want.Offset, q.Offset) + uassert.Equal(t, want.Count, q.Count) + uassert.Equal(t, want.Tag, q.Tag) + uassert.Equal(t, want.Filters.Size(), q.Filters.Size()) + + want.Filters.Iterate("", "", func(k string, v interface{}) bool { + got, exists := q.Filters.Get(k) + uassert.True(t, exists) + if exists { + uassert.Equal(t, fmt.Sprint(v), fmt.Sprint(got)) + } + return false + }) + }) + } +} From a85a53d5b38f0a21d66262a823a8b07f4f836b68 Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:19:04 -0800 Subject: [PATCH 69/86] fix: prevent false positive return for guarding dao member store (#3121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we want to guard the MemStore by checking the active DAO realm, m.daoPkgPath must first be assigned a realm package path; otherwise, the isCallerDAORealm() method may return a false positive, failing to protect the MemStore.
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
--------- Co-authored-by: Miloš Živković --- examples/gno.land/p/demo/membstore/membstore.gno | 2 +- examples/gno.land/r/gov/dao/v2/dao.gno | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/membstore/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno index 6e1932978d9..ca721d078e6 100644 --- a/examples/gno.land/p/demo/membstore/membstore.gno +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -205,5 +205,5 @@ func (m *MembStore) TotalPower() uint64 { // the API of the member store is public and callable // by anyone who has a reference to the member store instance. func (m *MembStore) isCallerDAORealm() bool { - return m.daoPkgPath == "" || std.CurrentRealm().PkgPath() == m.daoPkgPath + return m.daoPkgPath != "" && std.CurrentRealm().PkgPath() == m.daoPkgPath } diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 9263d8d440b..5ee8e63236a 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -13,6 +13,8 @@ var ( members membstore.MemberStore // the member store ) +const daoPkgPath = "gno.land/r/gov/dao/v2" + func init() { // Example initial member set (just test addresses) set := []membstore.Member{ @@ -23,7 +25,7 @@ func init() { } // Set the member store - members = membstore.NewMembStore(membstore.WithInitialMembers(set)) + members = membstore.NewMembStore(membstore.WithInitialMembers(set), membstore.WithDAOPkgPath(daoPkgPath)) // Set the DAO implementation d = simpledao.New(members) From 79ca9a958dea7c94aaf6c3dc74f522c5f05e791e Mon Sep 17 00:00:00 2001 From: Mikael VALLENET Date: Thu, 12 Dec 2024 16:47:27 +0100 Subject: [PATCH 70/86] fix(std): add full denom in banker issue & remove coin (#3239) fix #2107 -------------------------- ## Problem > From [#875 (review)](https://github.com/gnolang/gno/pull/875#pullrequestreview-2043984930): > > > That said, soon after this is merged, I think we'll need to change this API again. This current implementation creates an inconsistency within the Banker API. All other banker methods now require you to pass in the full realm path to the token you're referring to, but IssueCoin and RemoveCoin do not. > > Thus, I think a few more changes are in order: > > > > 1. There should be a `RealmDenom(pkgpath, denom string)` function in `std`, which creates a realm denomination (ie. `/gno.land/r/morgan:bitcoin`). There can be a helper method `Realm.Denom(denom string)` (so you can do `std.CurrentRealm().Denom("bitcoin")` > > 2. Instead of modifying `denom`'s value in the native function, we should check it matches what we expect. ie. `strings.HasPrefix(denom, RealmDenom(std.CurrentRealm().PkgPath())`, then check the last part of the denom to see that it matches the Gno regex. (This can all be done in gno, without needing to put it in native code) > > Related with #1475 #1576 ------------------------- ## Solution BREAKING CHANGE: All previous realm calling IssueCoin or RemoveCoin are now expected to append the prefix "/" + realmPkgPath + ":" before the denom, it should be done by using ``std.CurrentRealm().CoinDenom(denom string)`` or by using ``std.CoinDenom(pkgPath, denom string)`` For now to avoid to mix coins and fix security issues like being able to issue coins from other realm, when a realm issue a coin, the pkg path of the realm is added as a prefix to the coin. the thing is some function expect only the base denom ``bitcoin`` (issue & remove) but the others like get require the qualified denom ``gno.land/r/demo/banktest:bitcoin``. it can be confusing I also answer the requirements of the comment @thehowl made: - Two functions are now available ``std.CoinDenom(pkgpath, demon string)`` && the method ``std.Realm.CoinDenom(denom string)`` - the denom's value is changed in the `.gno` file and not the native. Here is an example of how it looks like: ```go func IssueNewCoin(denom string, amount int64) string { std.AssertOriginCall() banker := std.GetBanker(std.BankerTypeRealmIssue) addr := std.PrevRealm().Addr() banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) return std.CurrentRealm().Denom(denom) } func RemoveCoin(denom string, amount int64) { std.AssertOriginCall() banker := std.GetBanker(std.BankerTypeRealmIssue) addr := std.PrevRealm().Addr() banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } func GetCoins(denom string) uint64 { banker := std.GetBanker(std.BankerTypeReadonly) addr := std.PrevRealm().Addr() coins := banker.GetCoins(addr) for _, coin := range coins { if coin.Denom == std.CurrentRealm().CoinDenom(denom) { return uint64(coin.Amount) } } return 0 } ```
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 Bazalgette Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- docs/reference/stdlibs/std/banker.md | 4 ++ docs/reference/stdlibs/std/chain.md | 16 ++++++++ docs/reference/stdlibs/std/realm.md | 13 ++++++ .../gnoland/testdata/assertorigincall.txtar | 40 +++++++++---------- .../cmd/gnoland/testdata/grc20_registry.txtar | 8 ++-- gno.land/cmd/gnoland/testdata/prevrealm.txtar | 22 +++++----- .../realm_banker_issued_coin_denom.txtar | 40 ++++++++++++++++++- gno.land/pkg/gnoclient/integration_test.go | 14 +++---- gnovm/stdlibs/std/banker.gno | 32 +++++++++++++++ gnovm/stdlibs/std/banker.go | 30 +------------- gnovm/stdlibs/std/frame.gno | 12 ++++++ gnovm/tests/files/std5.gno | 2 +- gnovm/tests/files/std8.gno | 2 +- gnovm/tests/files/zrealm_natbind0.gno | 2 +- 14 files changed, 162 insertions(+), 75 deletions(-) diff --git a/docs/reference/stdlibs/std/banker.md b/docs/reference/stdlibs/std/banker.md index 71eb3709ea2..b60b55ee93b 100644 --- a/docs/reference/stdlibs/std/banker.md +++ b/docs/reference/stdlibs/std/banker.md @@ -38,6 +38,10 @@ Returns `Banker` of the specified type. ```go banker := std.GetBanker(std.) ``` + +:::info `Banker` methods expect qualified denomination of the coins. Read more [here](./realm.md#coindenom). +::: + --- ## GetCoins diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index b1791e65608..6a1da6483fd 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -162,3 +162,19 @@ Derives the Realm address from its `pkgpath` parameter. ```go realmAddr := std.DerivePkgAddr("gno.land/r/demo/tamagotchi") // g1a3tu874agjlkrpzt9x90xv3uzncapcn959yte4 ``` +--- + +## CoinDenom +```go +func CoinDenom(pkgPath, coinName string) string +``` +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. It can also be used as a method of the `Realm` object, Read more[here](./realm.md#coindenom). + +#### Parameters +- `pkgPath` **string** - package path of the realm +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +denom := std.CoinDenom("gno.land/r/demo/blog", "blgcoin") // /gno.land/r/demo/blog:blgcoin +``` diff --git a/docs/reference/stdlibs/std/realm.md b/docs/reference/stdlibs/std/realm.md index 0c99b7134ea..f69cd874c75 100644 --- a/docs/reference/stdlibs/std/realm.md +++ b/docs/reference/stdlibs/std/realm.md @@ -14,6 +14,7 @@ type Realm struct { func (r Realm) Addr() Address {...} func (r Realm) PkgPath() string {...} func (r Realm) IsUser() bool {...} +func (r Realm) CoinDenom(coinName string) string {...} ``` ## Addr @@ -39,3 +40,15 @@ Checks if the realm it was called upon is a user realm. ```go if r.IsUser() {...} ``` +--- +## CoinDenom +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. + +#### Parameters +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +// in "gno.land/r/gnoland/blog" +denom := r.CoinDenom("blgcoin") // /gno.land/r/gnoland/blog:blgcoin +``` diff --git a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar index 62d660a9215..1a5664d6bef 100644 --- a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar +++ b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar @@ -33,85 +33,85 @@ gnoland start # Test cases ## 1. MsgCall -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## 2. MsgCall -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 3. MsgCall -> myrlm.C: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 4. MsgCall -> r/foo.A -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## 5. MsgCall -> r/foo.B -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 6. MsgCall -> r/foo.C -> myrlm.C: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 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: PANIC -## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' ## 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 +## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stdout 'OK!' ## 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 +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' ## 10. MsgRun -> run.main -> myrlm.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno stderr 'invalid non-origin call' ## 11. MsgRun -> run.main -> myrlm.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno stdout 'OK!' ## 12. MsgRun -> run.main -> myrlm.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno stderr 'invalid non-origin call' ## 13. MsgRun -> run.main -> foo.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno stderr 'invalid non-origin call' ## 14. MsgRun -> run.main -> foo.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno stdout 'OK!' ## 15. MsgRun -> run.main -> foo.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno stderr 'invalid non-origin call' ## 16. MsgRun -> run.main -> bar.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno stderr 'invalid non-origin call' ## 17. MsgRun -> run.main -> bar.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno stdout 'OK!' ## 18. MsgRun -> run.main -> bar.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno stderr 'invalid non-origin call' ## remove testcase 19 due to maketx call forced to call a realm ## 19. MsgCall -> std.AssertOriginCall: pass -## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stdout 'OK!' ## 20. MsgRun -> std.AssertOriginCall: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stderr 'invalid non-origin call' diff --git a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar index a5f7ad5eee3..417ab04539d 100644 --- a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar +++ b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar @@ -6,15 +6,15 @@ loadpkg gno.land/r/registry $WORK/registry gnoland start # we call Transfer with foo20, before it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 stdout 'not found' # add foo20, and foo20wrapper -gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 -gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 # we call Transfer with foo20, after it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 stdout 'same address, success!' -- registry/registry.gno -- diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar index 4a7cece6d62..58b0cdce1d6 100644 --- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar +++ b/gno.land/cmd/gnoland/testdata/prevrealm.txtar @@ -34,19 +34,19 @@ env RFOO_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469 # Test cases ## 1. MsgCall -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${USER_ADDR_test1} ## 2. MsgCall -> myrealm.B -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${USER_ADDR_test1} ## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${RFOO_ADDR} ## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${RFOO_ADDR} ## remove due to update to maketx call can only call realm (case 5, 6, 13) @@ -59,27 +59,27 @@ stdout ${RFOO_ADDR} ## stdout ${USER_ADDR_test1} ## 7. MsgRun -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno stdout ${USER_ADDR_test1} ## 8. MsgRun -> myrealm.B -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno stdout ${USER_ADDR_test1} ## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno stdout ${RFOO_ADDR} ## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno stdout ${RFOO_ADDR} ## 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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno stdout ${USER_ADDR_test1} ## 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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno stdout ${USER_ADDR_test1} ## 13. MsgCall -> std.PrevRealm(): user address @@ -87,7 +87,7 @@ stdout ${USER_ADDR_test1} ## stdout ${USER_ADDR_test1} ## 14. MsgRun -> std.PrevRealm(): user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stdout ${USER_ADDR_test1} -- r/myrlm/myrlm.gno -- diff --git a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar index 71ef6400471..be9a686bac6 100644 --- a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar +++ b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar @@ -12,6 +12,9 @@ gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker - ## add realm_banker with long package_name gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 +## add invalid realm_denom +gnokey maketx addpkg -pkgdir $WORK/invalid_realm_denom -pkgpath gno.land/r/test/invalid_realm_denom -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + ## test2 spend all balance gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 @@ -52,6 +55,22 @@ gnokey maketx call -pkgpath gno.land/r/test/package89_123456789_123456789_123456 gnokey query bank/balances/g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7 stdout '"100/gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890:ugnot"' +## mint invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## burn invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## mint invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + +## burn invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + -- short/realm_banker.gno -- package realm_banker @@ -61,12 +80,12 @@ import ( func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.IssueCoin(addr, denom, amount) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } func Burn(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.RemoveCoin(addr, denom, amount) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } -- long/realm_banker.gno -- @@ -77,6 +96,23 @@ import ( "std" ) +func Mint(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +func Burn(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +-- invalid_realm_denom/realm_banker.gno -- +package invalid_realm_denom + +import ( + "std" +) + func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) banker.IssueCoin(addr, denom, amount) diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index 0a06eb4756a..945121fbacf 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -40,7 +40,7 @@ func TestCallSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -93,7 +93,7 @@ func TestCallMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -155,7 +155,7 @@ func TestSendSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -219,7 +219,7 @@ func TestSendMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -291,7 +291,7 @@ func TestRunSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -452,7 +452,7 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -537,7 +537,7 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", diff --git a/gnovm/stdlibs/std/banker.gno b/gnovm/stdlibs/std/banker.gno index 5412b73281c..4c20e8d4b61 100644 --- a/gnovm/stdlibs/std/banker.gno +++ b/gnovm/stdlibs/std/banker.gno @@ -2,6 +2,7 @@ package std import ( "strconv" + "strings" ) // Realm functions can call std.GetBanker(options) to get @@ -126,6 +127,7 @@ func (b banker) IssueCoin(addr Address, denom string, amount int64) { if b.bt != BankerTypeRealmIssue { panic(b.bt.String() + " cannot issue coins") } + assertCoinDenom(denom) bankerIssueCoin(uint8(b.bt), string(addr), denom, amount) } @@ -133,5 +135,35 @@ func (b banker) RemoveCoin(addr Address, denom string, amount int64) { if b.bt != BankerTypeRealmIssue { panic(b.bt.String() + " cannot remove coins") } + assertCoinDenom(denom) bankerRemoveCoin(uint8(b.bt), string(addr), denom, amount) } + +func assertCoinDenom(denom string) { + prefix := "/" + CurrentRealm().PkgPath() + ":" + if !strings.HasPrefix(denom, prefix) { + panic("invalid denom, can only issue/remove coins with the realm's prefix: " + prefix) + } + + baseDenom := denom[len(prefix):] + if !isValidBaseDenom(baseDenom) { + panic("cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits") + } +} + +// check start by a lowercase letter and be followed by 2-15 lowercase letters or digits +func isValidBaseDenom(denom string) bool { + length := len(denom) + if length < 3 || length > 16 { + return false + } + for i, c := range denom { + switch { + case c >= 'a' && c <= 'z', + i > 0 && (c >= '0' && c <= '9'): // continue + default: + return false + } + } + return true +} diff --git a/gnovm/stdlibs/std/banker.go b/gnovm/stdlibs/std/banker.go index 892af94777f..c57ba8529ed 100644 --- a/gnovm/stdlibs/std/banker.go +++ b/gnovm/stdlibs/std/banker.go @@ -2,7 +2,6 @@ package std import ( "fmt" - "regexp" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -33,9 +32,6 @@ const ( btRealmIssue ) -// regexp for denom format -var reDenom = regexp.MustCompile("[a-z][a-z0-9]{2,15}") - func X_bankerGetCoins(m *gno.Machine, bt uint8, addr string) (denoms []string, amounts []int64) { coins := GetContext(m).Banker.GetCoins(crypto.Bech32Address(addr)) return ExpandCoins(coins) @@ -74,31 +70,9 @@ func X_bankerTotalCoin(m *gno.Machine, bt uint8, denom string) int64 { } func X_bankerIssueCoin(m *gno.Machine, bt uint8, addr string, denom string, amount int64) { - // gno checks for bt == RealmIssue - - // check origin denom format - matched := reDenom.MatchString(denom) - if !matched { - m.Panic(typedString("invalid denom format to issue coin, must be " + reDenom.String())) - return - } - - // Similar to ibc spec - // ibc_denom := 'ibc/' + hash('path' + 'base_denom') - // gno_realm_denom := '/' + 'pkg_path' + ':' + 'base_denom' - newDenom := "/" + m.Realm.Path + ":" + denom - GetContext(m).Banker.IssueCoin(crypto.Bech32Address(addr), newDenom, amount) + GetContext(m).Banker.IssueCoin(crypto.Bech32Address(addr), denom, amount) } func X_bankerRemoveCoin(m *gno.Machine, bt uint8, addr string, denom string, amount int64) { - // gno checks for bt == RealmIssue - - matched := reDenom.MatchString(denom) - if !matched { - m.Panic(typedString("invalid denom format to remove coin, must be " + reDenom.String())) - return - } - - newDenom := "/" + m.Realm.Path + ":" + denom - GetContext(m).Banker.RemoveCoin(crypto.Bech32Address(addr), newDenom, amount) + GetContext(m).Banker.RemoveCoin(crypto.Bech32Address(addr), denom, amount) } diff --git a/gnovm/stdlibs/std/frame.gno b/gnovm/stdlibs/std/frame.gno index bc3a000f5a0..1709f8cb8b5 100644 --- a/gnovm/stdlibs/std/frame.gno +++ b/gnovm/stdlibs/std/frame.gno @@ -16,3 +16,15 @@ func (r Realm) PkgPath() string { func (r Realm) IsUser() bool { return r.pkgPath == "" } + +func (r Realm) CoinDenom(coinName string) string { + return CoinDenom(r.pkgPath, coinName) +} + +func CoinDenom(pkgPath, coinName string) string { + // TODO: Possibly remove after https://github.com/gnolang/gno/issues/3164 + // Similar to ibc spec + // ibc_denom := 'ibc/' + hash('path' + 'base_denom') + // gno_qualified_denom := '/' + 'pkg_path' + ':' + 'base_denom' + return "/" + pkgPath + ":" + coinName +} diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 2baba6b5005..e339d7a6364 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,7 +13,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) // std/native.gno:45 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index 4f749c3a6e1..ee717bf16be 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,7 +23,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) // std/native.gno:45 diff --git a/gnovm/tests/files/zrealm_natbind0.gno b/gnovm/tests/files/zrealm_natbind0.gno index 16a374164d5..8e5f641e734 100644 --- a/gnovm/tests/files/zrealm_natbind0.gno +++ b/gnovm/tests/files/zrealm_natbind0.gno @@ -69,7 +69,7 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:8" +// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:9" // }, // "FileName": "native.gno", // "IsMethod": false, From c48219a1f3782d318e7753e1df7f1da0c5fd10c6 Mon Sep 17 00:00:00 2001 From: Nemanja Aleksic Date: Fri, 13 Dec 2024 07:12:06 +0100 Subject: [PATCH 71/86] docs: add bad contribution section (#3329) Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> Co-authored-by: Morgan --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc125a6da73..b58d63c6c75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -469,6 +469,18 @@ Resources for idiomatic Go docs: - [godoc](https://go.dev/blog/godoc) - [Go Doc Comments](https://tip.golang.org/doc/comment) +## Avoding Unhelpful Contributions + +While we welcome all contributions to the Gno project, it's important to ensure that your changes provide meaningful value or improve the quality of the codebase. Contributions that fail to meet these criteria may not be accepted. Examples of unhelpful contributions include (but not limited to): + +- Airdrop farming & karma farming: Making minimal, superficial changes, with the goal of becoming eligible for airdrops and GovDAO participation. +- Incomplete submissions: Changes that lack adequate context, link to a related issue, documentation, or test coverage. + +Before submitting a pull request, ask yourself: +- Does this change solve a specific problem or add clear value? +- Is the implementation aligned with the gno.land's goals and style guide? +- Have I tested my changes and included relevant documentation? + ## Additional Notes ### Issue and Pull Request Labels @@ -502,3 +514,4 @@ automatic label management. | info needed | Issue is lacking information needed for resolving | | investigating | Issue is still being investigated by the team | | question | Issue starts a discussion or raises a question | + From 705f424470933b45e5666d5939845c2e8306a45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Mon, 16 Dec 2024 12:05:34 +0100 Subject: [PATCH 72/86] feat: datasource for `gno.land/r/leon/hof` integration with Gno.me (#3247) This is quick initial PoC of a Gno.me integration idea. --- examples/gno.land/r/leon/hof/datasource.gno | 77 +++++++++ .../gno.land/r/leon/hof/datasource_test.gno | 157 ++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 examples/gno.land/r/leon/hof/datasource.gno create mode 100644 examples/gno.land/r/leon/hof/datasource_test.gno diff --git a/examples/gno.land/r/leon/hof/datasource.gno b/examples/gno.land/r/leon/hof/datasource.gno new file mode 100644 index 00000000000..180c4880177 --- /dev/null +++ b/examples/gno.land/r/leon/hof/datasource.gno @@ -0,0 +1,77 @@ +package hof + +import ( + "errors" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" + "gno.land/p/jeronimoalbi/datasource" +) + +func NewDatasource() Datasource { + return Datasource{exhibition} +} + +type Datasource struct { + exhibition *Exhibition +} + +func (ds Datasource) Size() int { return ds.exhibition.itemsSorted.Size() } + +func (ds Datasource) Records(q datasource.Query) datasource.Iterator { + return &iterator{ + exhibition: ds.exhibition, + index: q.Offset, + maxIndex: q.Offset + q.Count, + } +} + +func (ds Datasource) Record(id string) (datasource.Record, error) { + v, found := ds.exhibition.itemsSorted.Get(id) + if !found { + return nil, errors.New("realm submission not found") + } + return record{v.(*Item)}, nil +} + +type record struct { + item *Item +} + +func (r record) ID() string { return r.item.id.String() } +func (r record) String() string { return r.item.pkgpath } + +func (r record) Fields() (datasource.Fields, error) { + fields := avl.NewTree() + fields.Set( + "details", + ufmt.Sprintf("Votes: ⏶ %d - ⏷ %d", r.item.upvote.Size(), r.item.downvote.Size()), + ) + return fields, nil +} + +func (r record) Content() (string, error) { + content := ufmt.Sprintf("# Submission #%d\n\n", int(r.item.id)) + content += r.item.Render(false) + return content, nil +} + +type iterator struct { + exhibition *Exhibition + index, maxIndex int + record *record +} + +func (it iterator) Record() datasource.Record { return it.record } +func (it iterator) Err() error { return nil } + +func (it *iterator) Next() bool { + if it.index >= it.maxIndex || it.index >= it.exhibition.itemsSorted.Size() { + return false + } + + _, v := it.exhibition.itemsSorted.GetByIndex(it.index) + it.record = &record{v.(*Item)} + it.index++ + return true +} diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno new file mode 100644 index 00000000000..376f981875f --- /dev/null +++ b/examples/gno.land/r/leon/hof/datasource_test.gno @@ -0,0 +1,157 @@ +package hof + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + "gno.land/p/jeronimoalbi/datasource" +) + +var ( + _ datasource.Datasource = (*Datasource)(nil) + _ datasource.Record = (*record)(nil) + _ datasource.ContentRecord = (*record)(nil) + _ datasource.Iterator = (*iterator)(nil) +) + +func TestDatasourceRecords(t *testing.T) { + cases := []struct { + name string + items []*Item + recordIDs []string + options []datasource.QueryOption + }{ + { + name: "all items", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000001", "0000002", "0000003"}, + }, + { + name: "with offset", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000002", "0000003"}, + options: []datasource.QueryOption{datasource.WithOffset(1)}, + }, + { + name: "with count", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000001", "0000002"}, + options: []datasource.QueryOption{datasource.WithCount(2)}, + }, + { + name: "with offset and count", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000002"}, + options: []datasource.QueryOption{ + datasource.WithOffset(1), + datasource.WithCount(1), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Initialize a local instance of exhibition + exhibition := &Exhibition{itemsSorted: avl.NewTree()} + for _, item := range tc.items { + exhibition.itemsSorted.Set(item.id.String(), item) + } + + // Get a records iterator + ds := Datasource{exhibition} + query := datasource.NewQuery(tc.options...) + iter := ds.Records(query) + + // Start asserting + urequire.Equal(t, len(tc.items), ds.Size(), "datasource size") + + var records []datasource.Record + for iter.Next() { + records = append(records, iter.Record()) + } + urequire.Equal(t, len(tc.recordIDs), len(records), "record count") + + for i, r := range records { + uassert.Equal(t, tc.recordIDs[i], r.ID()) + } + }) + } +} + +func TestDatasourceRecord(t *testing.T) { + cases := []struct { + name string + items []*Item + id string + err string + }{ + { + name: "found", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + id: "0000001", + }, + { + name: "no found", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + id: "42", + err: "realm submission not found", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Initialize a local instance of exhibition + exhibition := &Exhibition{itemsSorted: avl.NewTree()} + for _, item := range tc.items { + exhibition.itemsSorted.Set(item.id.String(), item) + } + + // Get a single record + ds := Datasource{exhibition} + r, err := ds.Record(tc.id) + + // Start asserting + if tc.err != "" { + uassert.ErrorContains(t, err, tc.err) + return + } + + urequire.NoError(t, err, "no error") + urequire.NotEqual(t, nil, r, "record not nil") + uassert.Equal(t, tc.id, r.ID()) + }) + } +} + +func TestItemRecord(t *testing.T) { + pkgpath := "gno.land/r/demo/test" + item := Item{ + id: 1, + pkgpath: pkgpath, + blockNum: 42, + upvote: avl.NewTree(), + downvote: avl.NewTree(), + } + item.downvote.Set("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", struct{}{}) + item.upvote.Set("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", struct{}{}) + item.upvote.Set("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", struct{}{}) + + r := record{&item} + + uassert.Equal(t, "0000001", r.ID()) + uassert.Equal(t, pkgpath, r.String()) + + fields, _ := r.Fields() + details, found := fields.Get("details") + urequire.True(t, found, "details field") + uassert.Equal(t, "Votes: ⏶ 2 - ⏷ 1", details) + + content, _ := r.Content() + wantContent := "# Submission #1\n\n\n```\ngno.land/r/demo/test\n```\n\nby demo\n\n" + + "[View realm](/r/demo/test)\n\nSubmitted at Block #42\n\n" + + "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land/r/demo/test) - " + + "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land/r/demo/test)\n\n" + uassert.Equal(t, wantContent, content) +} From 4b0c341792ad50d4d86ab5ec6926d1309fe5dc9a Mon Sep 17 00:00:00 2001 From: Nathan Toups <612924+n2p5@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:00:27 -0700 Subject: [PATCH 73/86] feat(examples): add {p,r}/n2p5/loci (#3338) # loci (package and realm) This is a realm I've developed as part of a larger project I have in the works. While I have a specific purpose for it, the loci realm is free to be used by anyone who wants to have a mutable data store for placing a byte slice tied to their caller address. This can be useful for pointing to other immutable data. `loci` is a single purpose datastore keyed by the caller's address. It has two functions: Set and Get. loci is plural for locus, which is a central or core place where something is found or from which it originates. In this case, it's a simple key-value store where an address (the key) can store exactly one value (in the form of a byte slice). Only the caller can set the value for their address, but anyone can retrieve the value for any address. --- examples/gno.land/p/n2p5/loci/gno.mod | 1 + examples/gno.land/p/n2p5/loci/loci.gno | 44 +++++++++++ examples/gno.land/p/n2p5/loci/loci_test.gno | 84 +++++++++++++++++++++ examples/gno.land/r/n2p5/loci/gno.mod | 1 + examples/gno.land/r/n2p5/loci/loci.gno | 68 +++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 examples/gno.land/p/n2p5/loci/gno.mod create mode 100644 examples/gno.land/p/n2p5/loci/loci.gno create mode 100644 examples/gno.land/p/n2p5/loci/loci_test.gno create mode 100644 examples/gno.land/r/n2p5/loci/gno.mod create mode 100644 examples/gno.land/r/n2p5/loci/loci.gno diff --git a/examples/gno.land/p/n2p5/loci/gno.mod b/examples/gno.land/p/n2p5/loci/gno.mod new file mode 100644 index 00000000000..ec30d72d752 --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/loci diff --git a/examples/gno.land/p/n2p5/loci/loci.gno b/examples/gno.land/p/n2p5/loci/loci.gno new file mode 100644 index 00000000000..7bd5c29c3af --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/loci.gno @@ -0,0 +1,44 @@ +// loci is a single purpose datastore keyed by the caller's address. It has two +// functions: Set and Get. loci is plural for locus, which is a central or core +// place where something is found or from which it originates. In this case, +// it's a simple key-value store where an address (the key) can store exactly +// one value (in the form of a byte slice). Only the caller can set the value +// for their address, but anyone can retrieve the value for any address. +package loci + +import ( + "std" + + "gno.land/p/demo/avl" +) + +// LociStore is a simple key-value store that uses +// an AVL tree to store the data. +type LociStore struct { + internal *avl.Tree +} + +// New creates a reference to a new LociStore. +func New() *LociStore { + return &LociStore{ + internal: avl.NewTree(), + } +} + +// Set stores a byte slice in the AVL tree using the `std.PrevRealm().Addr()` +// string as the key. +func (s *LociStore) Set(value []byte) { + key := string(std.PrevRealm().Addr()) + s.internal.Set(key, value) +} + +// Get retrieves a byte slice from the AVL tree using the provided address. +// The return values are the byte slice value and a boolean indicating +// whether the value exists. +func (s *LociStore) Get(addr std.Address) []byte { + value, exists := s.internal.Get(string(addr)) + if !exists { + return nil + } + return value.([]byte) +} diff --git a/examples/gno.land/p/n2p5/loci/loci_test.gno b/examples/gno.land/p/n2p5/loci/loci_test.gno new file mode 100644 index 00000000000..bb216a8539e --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/loci_test.gno @@ -0,0 +1,84 @@ +package loci + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestLociStore(t *testing.T) { + t.Parallel() + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u1") + + t.Run("TestSet", func(t *testing.T) { + t.Parallel() + store := New() + u1 := testutils.TestAddress("u1") + + m1 := []byte("hello") + m2 := []byte("world") + std.TestSetOrigCaller(u1) + + // Ensure that the value is nil before setting it. + r1 := store.Get(u1) + if r1 != nil { + t.Errorf("expected value to be nil, got '%s'", r1) + } + store.Set(m1) + // Ensure that the value is correct after setting it. + r2 := store.Get(u1) + if string(r2) != "hello" { + t.Errorf("expected value to be 'hello', got '%s'", r2) + } + store.Set(m2) + // Ensure that the value is correct after overwriting it. + r3 := store.Get(u1) + if string(r3) != "world" { + t.Errorf("expected value to be 'world', got '%s'", r3) + } + }) + t.Run("TestGet", func(t *testing.T) { + t.Parallel() + store := New() + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + u3 := testutils.TestAddress("u3") + u4 := testutils.TestAddress("u4") + + m1 := []byte("hello") + m2 := []byte("world") + m3 := []byte("goodbye") + + std.TestSetOrigCaller(u1) + store.Set(m1) + std.TestSetOrigCaller(u2) + store.Set(m2) + std.TestSetOrigCaller(u3) + store.Set(m3) + + // Ensure that the value is correct after setting it. + r0 := store.Get(u4) + if r0 != nil { + t.Errorf("expected value to be nil, got '%s'", r0) + } + // Ensure that the value is correct after setting it. + r1 := store.Get(u1) + if string(r1) != "hello" { + t.Errorf("expected value to be 'hello', got '%s'", r1) + } + // Ensure that the value is correct after setting it. + r2 := store.Get(u2) + if string(r2) != "world" { + t.Errorf("expected value to be 'world', got '%s'", r2) + } + // Ensure that the value is correct after setting it. + r3 := store.Get(u3) + if string(r3) != "goodbye" { + t.Errorf("expected value to be 'goodbye', got '%s'", r3) + } + }) + +} diff --git a/examples/gno.land/r/n2p5/loci/gno.mod b/examples/gno.land/r/n2p5/loci/gno.mod new file mode 100644 index 00000000000..131e0d73467 --- /dev/null +++ b/examples/gno.land/r/n2p5/loci/gno.mod @@ -0,0 +1 @@ +module gno.land/r/n2p5/loci diff --git a/examples/gno.land/r/n2p5/loci/loci.gno b/examples/gno.land/r/n2p5/loci/loci.gno new file mode 100644 index 00000000000..36f282e729f --- /dev/null +++ b/examples/gno.land/r/n2p5/loci/loci.gno @@ -0,0 +1,68 @@ +package loci + +import ( + "encoding/base64" + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/n2p5/loci" +) + +var store *loci.LociStore + +func init() { + store = loci.New() +} + +// Set takes a base64 encoded string and stores it in the Loci store. +// Keyed by the address of the caller. It also emits a "set" event with +// the address of the caller. +func Set(value string) { + b, err := base64.StdEncoding.DecodeString(value) + if err != nil { + panic(err) + } + store.Set(b) + std.Emit("SetValue", "ForAddr", string(std.PrevRealm().Addr())) +} + +// Get retrieves the value stored at the provided address and +// returns it as a base64 encoded string. +func Get(addr std.Address) string { + return base64.StdEncoding.EncodeToString(store.Get(addr)) +} + +func Render(path string) string { + if path == "" { + return about + } + return renderGet(std.Address(path)) +} + +func renderGet(addr std.Address) string { + value := "```\n" + Get(addr) + "\n```" + + return ufmt.Sprintf(` +# Loci Value Viewer + +**Address:** %s + +%s + +`, addr, value) +} + +const about = ` +# Welcome to Loci + +Loci is a simple key-value store keyed by the caller's gno.land address. +Only the caller can set the value for their address, but anyone can +retrieve the value for any address. There are only two functions: Set and Get. +If you'd like to set a value, simply base64 encode any message you'd like and +it will be stored in in Loci. If you'd like to retrieve a value, simply provide +the address of the value you'd like to retrieve. + +For convenience, you can also use gnoweb to view the value for a given address, +if one exists. For instance append :g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t to +this URL to view the value stored at that address. +` From 3d431887e7ee426afe178f5dba91321edcd9e945 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:32:44 +0100 Subject: [PATCH 74/86] feat(gnoweb): rework & Implement new gnoweb design (#3195) address #3191 Reworking the `gnoweb` package: - Implement `gnoweb` new interface design(cc @alexiscolin). - Move Markdown rendering to the server to enhance speed and security. This change also simplifies the implementation of new components, making it more standardized as a Go library. - Aim to keep dependencies minimal, using only `goldmark` for Markdown and `chroma` for code highlighting, with almost no (in)direct dependencies. - Transition to Tailwind for simplicity and maintainability. - Retain all features from the previous `gnoweb` iteration. ### Preview - Home ![Screenshot 2024-11-25 at 19 39 54](https://github.com/user-attachments/assets/7a4b99d9-c223-49e7-9ae6-6561be85d1d3) - Source ![Screenshot 2024-11-25 at 19 41 25](https://github.com/user-attachments/assets/cb650eca-70d6-48f5-9c25-d247aecf45c3) - Docs ![Screenshot 2024-11-25 at 19 45 16](https://github.com/user-attachments/assets/1d79bb25-e431-42db-bc0e-0fdefca85339) ### TODO: - [x] port and adapt all previous tests to ensure compatibility (it should not take too long) - [x] Some cleanup and restructuring + linting. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: alexiscolin Co-authored-by: Morgan Bazalgette --- .github/workflows/gnoland.yml | 22 + Dockerfile | 1 - contribs/gnodev/cmd/gnodev/main.go | 48 +- contribs/gnodev/cmd/gnodev/setup_web.go | 29 +- contribs/gnodev/go.mod | 10 +- contribs/gnodev/go.sum | 28 +- gno.land/Makefile | 6 + gno.land/cmd/gnoweb/CONTRIBUTING.md | 20 - gno.land/cmd/gnoweb/README.md | 10 +- gno.land/cmd/gnoweb/main.go | 204 ++++- gno.land/cmd/gnoweb/main_test.go | 25 +- gno.land/pkg/gnoweb/.gitignore | 3 + gno.land/pkg/gnoweb/Makefile | 100 ++ gno.land/pkg/gnoweb/README.md | 45 + gno.land/pkg/gnoweb/alias.go | 36 +- gno.land/pkg/gnoweb/app.go | 152 +++ .../gnoweb/{gnoweb_test.go => app_test.go} | 74 +- gno.land/pkg/gnoweb/components/breadcrumb.go | 18 + .../pkg/gnoweb/components/breadcrumb.gohtml | 12 + gno.land/pkg/gnoweb/components/directory.go | 15 + .../pkg/gnoweb/components/directory.gohtml | 39 + gno.land/pkg/gnoweb/components/help.go | 51 ++ gno.land/pkg/gnoweb/components/help.gohtml | 100 ++ gno.land/pkg/gnoweb/components/index.go | 47 + gno.land/pkg/gnoweb/components/index.gohtml | 155 ++++ gno.land/pkg/gnoweb/components/logosvg.gohtml | 21 + gno.land/pkg/gnoweb/components/realm.go | 32 + gno.land/pkg/gnoweb/components/realm.gohtml | 37 + gno.land/pkg/gnoweb/components/redirect.go | 12 + .../pkg/gnoweb/components/redirect.gohtml | 16 + gno.land/pkg/gnoweb/components/source.go | 20 + gno.land/pkg/gnoweb/components/source.gohtml | 51 ++ .../pkg/gnoweb/components/spritesvg.gohtml | 134 +++ gno.land/pkg/gnoweb/components/status.gohtml | 12 + gno.land/pkg/gnoweb/components/template.go | 77 ++ gno.land/pkg/gnoweb/formatter.go | 25 + gno.land/pkg/gnoweb/frontend/css/input.css | 352 +++++++ gno.land/pkg/gnoweb/frontend/css/tx.config.js | 76 ++ gno.land/pkg/gnoweb/frontend/js/copy.ts | 103 +++ gno.land/pkg/gnoweb/frontend/js/index.ts | 42 + gno.land/pkg/gnoweb/frontend/js/realmhelp.ts | 125 +++ gno.land/pkg/gnoweb/frontend/js/searchbar.ts | 74 ++ .../img => frontend/static}/favicon.ico | Bin .../static/fonts/intervar/Inter.var.woff2 | Bin 0 -> 324864 bytes .../fonts/roboto/roboto-mono-normal.woff | Bin 0 -> 15832 bytes .../fonts/roboto/roboto-mono-normal.woff2 | Bin 0 -> 12764 bytes .../gnoweb/frontend/static/imgs/gnoland.svg | 4 + gno.land/pkg/gnoweb/gnoweb.go | 608 ------------ gno.land/pkg/gnoweb/handler.go | 381 ++++++++ gno.land/pkg/gnoweb/markdown/highlighting.go | 588 ++++++++++++ .../pkg/gnoweb/markdown/highlighting_test.go | 568 ++++++++++++ gno.land/pkg/gnoweb/markdown/toc.go | 137 +++ gno.land/pkg/gnoweb/public/favicon.ico | Bin 0 -> 7406 bytes .../public/fonts/intervar/Inter.var.woff2 | Bin 0 -> 324864 bytes .../fonts/roboto/roboto-mono-normal.woff | Bin 0 -> 15832 bytes .../fonts/roboto/roboto-mono-normal.woff2 | Bin 0 -> 12764 bytes gno.land/pkg/gnoweb/public/imgs/gnoland.svg | 4 + gno.land/pkg/gnoweb/public/js/copy.js | 1 + gno.land/pkg/gnoweb/public/js/index.js | 1 + gno.land/pkg/gnoweb/public/js/realmhelp.js | 1 + gno.land/pkg/gnoweb/public/js/searchbar.js | 1 + gno.land/pkg/gnoweb/public/styles.css | 3 + gno.land/pkg/gnoweb/static.go | 28 + gno.land/pkg/gnoweb/static/css/app.css | 862 ------------------ gno.land/pkg/gnoweb/static/css/normalize.css | 379 -------- gno.land/pkg/gnoweb/static/font/README.md | 5 - .../pkg/gnoweb/static/font/roboto/LICENSE.txt | 201 ---- .../static/font/roboto/RobotoMono-Bold.woff | Bin 65396 -> 0 bytes .../font/roboto/RobotoMono-BoldItalic.woff | Bin 72000 -> 0 bytes .../static/font/roboto/RobotoMono-Italic.woff | Bin 71476 -> 0 bytes .../static/font/roboto/RobotoMono-Light.woff | Bin 67428 -> 0 bytes .../font/roboto/RobotoMono-LightItalic.woff | Bin 72748 -> 0 bytes .../static/font/roboto/RobotoMono-Medium.woff | Bin 65392 -> 0 bytes .../font/roboto/RobotoMono-MediumItalic.woff | Bin 72168 -> 0 bytes .../font/roboto/RobotoMono-Regular.woff | Bin 65336 -> 0 bytes .../static/font/roboto/RobotoMono-Thin.woff | Bin 67976 -> 0 bytes .../font/roboto/RobotoMono-ThinItalic.woff | Bin 71144 -> 0 bytes .../gnoweb/static/img/apple-touch-icon.png | Bin 1502 -> 0 bytes .../pkg/gnoweb/static/img/favicon-16x16.png | Bin 172 -> 0 bytes .../pkg/gnoweb/static/img/favicon-32x32.png | Bin 317 -> 0 bytes .../gnoweb/static/img/github-mark-32px.png | Bin 1714 -> 0 bytes .../gnoweb/static/img/github-mark-64px.png | Bin 2625 -> 0 bytes .../pkg/gnoweb/static/img/ico-discord.svg | 3 - gno.land/pkg/gnoweb/static/img/ico-email.svg | 3 - .../pkg/gnoweb/static/img/ico-telegram.svg | 3 - .../pkg/gnoweb/static/img/ico-twitter.svg | 3 - .../pkg/gnoweb/static/img/ico-youtube.svg | 3 - gno.land/pkg/gnoweb/static/img/list-alt.png | Bin 232 -> 0 bytes gno.land/pkg/gnoweb/static/img/list.png | Bin 200 -> 0 bytes .../pkg/gnoweb/static/img/logo-square.png | Bin 13018 -> 0 bytes .../pkg/gnoweb/static/img/logo-square.svg | 6 - gno.land/pkg/gnoweb/static/img/logo-v1.png | Bin 11122 -> 0 bytes gno.land/pkg/gnoweb/static/img/og-gnoland.png | Bin 4739 -> 0 bytes .../gnoweb/static/img/safari-pinned-tab.svg | 29 - gno.land/pkg/gnoweb/static/invites.txt | 48 - .../pkg/gnoweb/static/js/highlight.min.js | 331 ------- gno.land/pkg/gnoweb/static/js/marked.min.js | 14 - gno.land/pkg/gnoweb/static/js/purify.min.js | 3 - gno.land/pkg/gnoweb/static/js/realm_help.js | 111 --- gno.land/pkg/gnoweb/static/js/renderer.js | 225 ----- gno.land/pkg/gnoweb/static/js/umbrella.js | 807 ---------------- gno.land/pkg/gnoweb/static/js/umbrella.min.js | 3 - gno.land/pkg/gnoweb/static/static.go | 8 - gno.land/pkg/gnoweb/status.go | 76 ++ .../pkg/gnoweb/tools/cmd/logname/colors.go | 60 ++ gno.land/pkg/gnoweb/tools/cmd/logname/main.go | 41 + gno.land/pkg/gnoweb/tools/go.mod | 25 + gno.land/pkg/gnoweb/tools/go.sum | 45 + gno.land/pkg/gnoweb/tools/tools.go | 5 + gno.land/pkg/gnoweb/url.go | 148 +++ gno.land/pkg/gnoweb/url_test.go | 135 +++ gno.land/pkg/gnoweb/views/404.html | 18 - gno.land/pkg/gnoweb/views/faucet.html | 139 --- gno.land/pkg/gnoweb/views/funcs.html | 337 ------- gno.land/pkg/gnoweb/views/generic.html | 24 - gno.land/pkg/gnoweb/views/package_dir.html | 37 - gno.land/pkg/gnoweb/views/package_file.html | 28 - gno.land/pkg/gnoweb/views/realm_help.html | 100 -- gno.land/pkg/gnoweb/views/realm_render.html | 40 - gno.land/pkg/gnoweb/views/redirect.html | 16 - gno.land/pkg/gnoweb/webclient.go | 127 +++ go.mod | 7 +- go.sum | 22 +- .../staging.gno.land/docker-compose.yml | 5 +- misc/loop/docker-compose.yml | 5 +- 125 files changed, 4696 insertions(+), 4575 deletions(-) delete mode 100644 gno.land/cmd/gnoweb/CONTRIBUTING.md create mode 100644 gno.land/pkg/gnoweb/.gitignore create mode 100644 gno.land/pkg/gnoweb/Makefile create mode 100644 gno.land/pkg/gnoweb/README.md create mode 100644 gno.land/pkg/gnoweb/app.go rename gno.land/pkg/gnoweb/{gnoweb_test.go => app_test.go} (67%) create mode 100644 gno.land/pkg/gnoweb/components/breadcrumb.go create mode 100644 gno.land/pkg/gnoweb/components/breadcrumb.gohtml create mode 100644 gno.land/pkg/gnoweb/components/directory.go create mode 100644 gno.land/pkg/gnoweb/components/directory.gohtml create mode 100644 gno.land/pkg/gnoweb/components/help.go create mode 100644 gno.land/pkg/gnoweb/components/help.gohtml create mode 100644 gno.land/pkg/gnoweb/components/index.go create mode 100644 gno.land/pkg/gnoweb/components/index.gohtml create mode 100644 gno.land/pkg/gnoweb/components/logosvg.gohtml create mode 100644 gno.land/pkg/gnoweb/components/realm.go create mode 100644 gno.land/pkg/gnoweb/components/realm.gohtml create mode 100644 gno.land/pkg/gnoweb/components/redirect.go create mode 100644 gno.land/pkg/gnoweb/components/redirect.gohtml create mode 100644 gno.land/pkg/gnoweb/components/source.go create mode 100644 gno.land/pkg/gnoweb/components/source.gohtml create mode 100644 gno.land/pkg/gnoweb/components/spritesvg.gohtml create mode 100644 gno.land/pkg/gnoweb/components/status.gohtml create mode 100644 gno.land/pkg/gnoweb/components/template.go create mode 100644 gno.land/pkg/gnoweb/formatter.go create mode 100644 gno.land/pkg/gnoweb/frontend/css/input.css create mode 100644 gno.land/pkg/gnoweb/frontend/css/tx.config.js create mode 100644 gno.land/pkg/gnoweb/frontend/js/copy.ts create mode 100644 gno.land/pkg/gnoweb/frontend/js/index.ts create mode 100644 gno.land/pkg/gnoweb/frontend/js/realmhelp.ts create mode 100644 gno.land/pkg/gnoweb/frontend/js/searchbar.ts rename gno.land/pkg/gnoweb/{static/img => frontend/static}/favicon.ico (100%) create mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Inter.var.woff2 create mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff create mode 100644 gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 create mode 100644 gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg delete mode 100644 gno.land/pkg/gnoweb/gnoweb.go create mode 100644 gno.land/pkg/gnoweb/handler.go create mode 100644 gno.land/pkg/gnoweb/markdown/highlighting.go create mode 100644 gno.land/pkg/gnoweb/markdown/highlighting_test.go create mode 100644 gno.land/pkg/gnoweb/markdown/toc.go create mode 100644 gno.land/pkg/gnoweb/public/favicon.ico create mode 100644 gno.land/pkg/gnoweb/public/fonts/intervar/Inter.var.woff2 create mode 100644 gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff create mode 100644 gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 create mode 100644 gno.land/pkg/gnoweb/public/imgs/gnoland.svg create mode 100644 gno.land/pkg/gnoweb/public/js/copy.js create mode 100644 gno.land/pkg/gnoweb/public/js/index.js create mode 100644 gno.land/pkg/gnoweb/public/js/realmhelp.js create mode 100644 gno.land/pkg/gnoweb/public/js/searchbar.js create mode 100644 gno.land/pkg/gnoweb/public/styles.css create mode 100644 gno.land/pkg/gnoweb/static.go delete mode 100644 gno.land/pkg/gnoweb/static/css/app.css delete mode 100644 gno.land/pkg/gnoweb/static/css/normalize.css delete mode 100644 gno.land/pkg/gnoweb/static/font/README.md delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/LICENSE.txt delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Bold.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Regular.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff delete mode 100644 gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff delete mode 100644 gno.land/pkg/gnoweb/static/img/apple-touch-icon.png delete mode 100644 gno.land/pkg/gnoweb/static/img/favicon-16x16.png delete mode 100644 gno.land/pkg/gnoweb/static/img/favicon-32x32.png delete mode 100644 gno.land/pkg/gnoweb/static/img/github-mark-32px.png delete mode 100644 gno.land/pkg/gnoweb/static/img/github-mark-64px.png delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-discord.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-email.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-telegram.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-twitter.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/ico-youtube.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/list-alt.png delete mode 100644 gno.land/pkg/gnoweb/static/img/list.png delete mode 100644 gno.land/pkg/gnoweb/static/img/logo-square.png delete mode 100644 gno.land/pkg/gnoweb/static/img/logo-square.svg delete mode 100644 gno.land/pkg/gnoweb/static/img/logo-v1.png delete mode 100644 gno.land/pkg/gnoweb/static/img/og-gnoland.png delete mode 100644 gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg delete mode 100644 gno.land/pkg/gnoweb/static/invites.txt delete mode 100644 gno.land/pkg/gnoweb/static/js/highlight.min.js delete mode 100644 gno.land/pkg/gnoweb/static/js/marked.min.js delete mode 100644 gno.land/pkg/gnoweb/static/js/purify.min.js delete mode 100644 gno.land/pkg/gnoweb/static/js/realm_help.js delete mode 100644 gno.land/pkg/gnoweb/static/js/renderer.js delete mode 100644 gno.land/pkg/gnoweb/static/js/umbrella.js delete mode 100644 gno.land/pkg/gnoweb/static/js/umbrella.min.js delete mode 100644 gno.land/pkg/gnoweb/static/static.go create mode 100644 gno.land/pkg/gnoweb/status.go create mode 100644 gno.land/pkg/gnoweb/tools/cmd/logname/colors.go create mode 100644 gno.land/pkg/gnoweb/tools/cmd/logname/main.go create mode 100644 gno.land/pkg/gnoweb/tools/go.mod create mode 100644 gno.land/pkg/gnoweb/tools/go.sum create mode 100644 gno.land/pkg/gnoweb/tools/tools.go create mode 100644 gno.land/pkg/gnoweb/url.go create mode 100644 gno.land/pkg/gnoweb/url_test.go delete mode 100644 gno.land/pkg/gnoweb/views/404.html delete mode 100644 gno.land/pkg/gnoweb/views/faucet.html delete mode 100644 gno.land/pkg/gnoweb/views/funcs.html delete mode 100644 gno.land/pkg/gnoweb/views/generic.html delete mode 100644 gno.land/pkg/gnoweb/views/package_dir.html delete mode 100644 gno.land/pkg/gnoweb/views/package_file.html delete mode 100644 gno.land/pkg/gnoweb/views/realm_help.html delete mode 100644 gno.land/pkg/gnoweb/views/realm_render.html delete mode 100644 gno.land/pkg/gnoweb/views/redirect.html create mode 100644 gno.land/pkg/gnoweb/webclient.go diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 4817e2db0e3..59050f1baa4 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -16,3 +16,25 @@ jobs: tests-extra-args: "-coverpkg=github.com/gnolang/gno/gno.land/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} + + gnoweb_generate: + strategy: + fail-fast: false + matrix: + go-version: ["1.22.x"] + # unittests: TODO: matrix with contracts + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - uses: actions/setup-node@v4 + with: + node-version: lts/Jod # (22.x) https://github.com/nodejs/Release + - uses: actions/checkout@v4 + - run: | + make -C gno.land/pkg/gnoweb fclean generate + # Check if there are changes after running generate.gnoweb + git diff --exit-code || \ + (echo "\`gnoweb generate\` out of date, please run \`make gnoweb.generate\` within './gno.land'" && exit 1) diff --git a/Dockerfile b/Dockerfile index b858589640f..effc30ca32f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,6 @@ ENTRYPOINT ["/usr/bin/gno"] # gnoweb FROM base AS gnoweb COPY --from=build-gno /gnoroot/build/gnoweb /usr/bin/gnoweb -COPY --from=build-gno /opt/gno/src/gno.land/cmd/gnoweb /opt/gno/src/gnoweb EXPOSE 8888 ENTRYPOINT ["/usr/bin/gnoweb"] diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 082d0cb8270..95f1d95e0a6 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -57,9 +57,10 @@ type devCfg struct { txsFile string // Web Configuration + noWeb bool + webHTML bool webListenerAddr string webRemoteHelperAddr string - webWithHTML bool // Node Configuration minimal bool @@ -123,6 +124,20 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "gno root directory", ) + fs.BoolVar( + &c.noWeb, + "no-web", + defaultDevOptions.noWeb, + "disable gnoweb", + ) + + fs.BoolVar( + &c.webHTML, + "web-html", + defaultDevOptions.webHTML, + "gnoweb: enable unsafe HTML parsing in markdown rendering", + ) + fs.StringVar( &c.webListenerAddr, "web-listener", @@ -137,13 +152,6 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "gnoweb: web server help page's remote addr (default to )", ) - fs.BoolVar( - &c.webWithHTML, - "web-with-html", - defaultDevOptions.webWithHTML, - "gnoweb: enable HTML parsing in markdown rendering", - ) - fs.StringVar( &c.nodeRPCListenerAddr, "node-rpc-listener", @@ -323,7 +331,10 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { defer server.Close() // Setup gnoweb - webhandler := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + webhandler, err := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + if err != nil { + return fmt.Errorf("unable to setup gnoweb server: %w", err) + } // Setup unsafe APIs if enabled if cfg.unsafeAPI { @@ -351,14 +362,17 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { mux.Handle("/", webhandler) } - go func() { - err := server.ListenAndServe() - cancel(err) - }() + // Serve gnoweb + if !cfg.noWeb { + go func() { + err := server.ListenAndServe() + cancel(err) + }() - logger.WithGroup(WebLogName). - Info("gnoweb started", - "lisn", fmt.Sprintf("http://%s", server.Addr)) + logger.WithGroup(WebLogName). + Info("gnoweb started", + "lisn", fmt.Sprintf("http://%s", server.Addr)) + } watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) if err != nil { @@ -377,7 +391,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { return runEventLoop(ctx, logger, book, rt, devNode, watcher) } -var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: +var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev P Previous TX - Go to the previous tx diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index d55814142a6..e509768d2a1 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log/slog" "net/http" @@ -9,19 +10,25 @@ import ( ) // setupGnowebServer initializes and starts the Gnoweb server. -func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) http.Handler { - webConfig := gnoweb.NewDefaultConfig() +func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) (http.Handler, error) { + if cfg.noWeb { + return http.HandlerFunc(http.NotFound), nil + } + + remote := dnode.GetRemoteAddress() - webConfig.HelpChainID = cfg.chainId - webConfig.RemoteAddr = dnode.GetRemoteAddress() - webConfig.HelpRemote = cfg.webRemoteHelperAddr - webConfig.WithHTML = cfg.webWithHTML + appcfg := gnoweb.NewDefaultAppConfig() + appcfg.UnsafeHTML = cfg.webHTML + appcfg.NodeRemote = remote + appcfg.ChainID = cfg.chainId + if cfg.webRemoteHelperAddr != "" { + appcfg.RemoteHelp = cfg.webRemoteHelperAddr + } - // If `HelpRemote` is empty default it to `RemoteAddr` - if webConfig.HelpRemote == "" { - webConfig.HelpRemote = webConfig.RemoteAddr + router, err := gnoweb.NewRouter(logger, appcfg) + if err != nil { + return nil, fmt.Errorf("unable to create router app: %w", err) } - app := gnoweb.MakeApp(logger, webConfig) - return app.Router + return router, nil } diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 2053a61db6c..3b895975950 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -27,7 +27,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect - github.com/alecthomas/chroma/v2 v2.8.0 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -48,7 +48,7 @@ require ( github.com/creack/pty v1.1.21 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -57,10 +57,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect - github.com/gorilla/sessions v1.2.1 // indirect - github.com/gotuna/gotuna v0.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -81,7 +77,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark v1.7.2 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index f9250d34462..bab6e5364e8 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -1,12 +1,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= -github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= -github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -91,8 +91,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -128,16 +128,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -233,8 +225,8 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= -github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= +github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/gno.land/Makefile b/gno.land/Makefile index 7b2afd5779f..075560f44a9 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -47,6 +47,12 @@ install.gnoland:; go install ./cmd/gnoland install.gnoweb:; go install ./cmd/gnoweb install.gnokey:; go install ./cmd/gnokey +.PHONY: dev.gnoweb generate.gnoweb +dev.gnoweb: + make -C ./pkg/gnoweb dev +generate.gnoweb: + make -C ./pkg/gnoweb generate + .PHONY: fclean fclean: clean rm -rf gnoland-data genesis.json diff --git a/gno.land/cmd/gnoweb/CONTRIBUTING.md b/gno.land/cmd/gnoweb/CONTRIBUTING.md deleted file mode 100644 index 7d7663e8bf7..00000000000 --- a/gno.land/cmd/gnoweb/CONTRIBUTING.md +++ /dev/null @@ -1,20 +0,0 @@ -# gno.land Website - -The gno.land website has 3 main dependencies: - -1. [UmbrellaJs](https://umbrellajs.com/) for DOM operations -2. [MarkedJs](https://marked.js.org/) for Markdown to html compilation -3. [HighlightJs](https://highlightjs.org/) for golang syntax highlighting -4. [DOMPurify](https://github.com/cure53/DOMPurify) to sanitize html (and avoid xss) - -Some security considerations: -| | Umbrella Js | Marked Js | HighlightJs | DOMPurify | -|---|---|---|---|---| -| dependencies | 0 | 0 | 0 | 0 | -| sanitize content | | [no](https://marked.js.org/#usage) | [throws an error](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741) | [yes](https://github.com/cure53/DOMPurify#readme) | - -Best Practices: - -- **When using MarkedJs**: Always run the output of the marked compiler inside `DOMPurify.sanitize` before inserting it in the dom with `.innerHtml = `. -- **When using DOMPurify**: Preferably use `{ USE_PROFILES: { html: true } }` option to allow html only. Content passed in the sanitizer must not be modified afterwards, and must directly be inserted in the DOM with innerHtml. Do not call `DOMPurify.sanitize` with the output of a previous `DOMPurify.sanitize` to avoid any mutation XSS risks. -- **When using HighlightJs**: always configure it before with `hljs.configure({throwUnescapedHTML: true})` to throw before inserting html in the page if any unexpected html children are detected. The check is done [here](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741). diff --git a/gno.land/cmd/gnoweb/README.md b/gno.land/cmd/gnoweb/README.md index 6379d3f6c43..ccd538c8f70 100644 --- a/gno.land/cmd/gnoweb/README.md +++ b/gno.land/cmd/gnoweb/README.md @@ -2,12 +2,4 @@ The gno.land web interface. -Live demo: https://gno.land/ - -## Install `gnoweb` - -Install and run a local [`gnoland`](../gnoland) instance first. - - $> git clone git@github.com:gnolang/gno.git - $> cd ./gno/gno.land - $> make install.gnoweb +Live demo: [https://gno.land/](https://gno.land/) or using `gnodev` from the directory [gnodev](../../../contribs/gnodev). diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 5cec7257ebe..80a8667ae6b 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -1,61 +1,197 @@ package main import ( + "context" "flag" "fmt" + "net" "net/http" "os" "time" - // for static files "github.com/gnolang/gno/gno.land/pkg/gnoweb" "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/gnolang/gno/tm2/pkg/commands" + "go.uber.org/zap" "go.uber.org/zap/zapcore" - // for error types - // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) ) +type webCfg struct { + chainid string + remote string + remoteHelp string + bind string + faucetURL string + assetsDir string + analytics bool + json bool + html bool + verbose bool +} + +var defaultWebOptions = webCfg{ + chainid: "dev", + assetsDir: "public", + remote: "127.0.0.1:26657", + bind: ":8888", +} + func main() { - err := runMain(os.Args[1:]) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) - os.Exit(1) - } + var cfg webCfg + + stdio := commands.NewDefaultIO() + cmd := commands.NewCommand( + commands.Metadata{ + Name: "gnoweb", + ShortUsage: "gnoweb [flags] [path ...]", + ShortHelp: "runs gno.land web interface", + LongHelp: `gnoweb web interface`, + }, + &cfg, + func(ctx context.Context, args []string) error { + run, err := setupWeb(&cfg, args, stdio) + if err != nil { + return err + } + + return run() + }) + + cmd.Execute(context.Background(), os.Args[1:]) +} + +func (c *webCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remote, + "remote", + defaultWebOptions.remote, + "remote gno.land node address", + ) + + fs.StringVar( + &c.remoteHelp, + "help-remote", + defaultWebOptions.remoteHelp, + "help page's remote address", + ) + + fs.StringVar( + &c.assetsDir, + "assets-dir", + defaultWebOptions.assetsDir, + "if not empty, will be use as assets directory", + ) + + fs.StringVar( + &c.chainid, + "help-chainid", + defaultWebOptions.chainid, + "Deprecated: use `chainid` instead", + ) + + fs.StringVar( + &c.chainid, + "chainid", + defaultWebOptions.chainid, + "target chain id", + ) + + fs.StringVar( + &c.bind, + "bind", + defaultWebOptions.bind, + "gnoweb listener", + ) + + fs.StringVar( + &c.faucetURL, + "faucet-url", + defaultWebOptions.faucetURL, + "The faucet URL will redirect the user when they access `/faucet`.", + ) + + fs.BoolVar( + &c.json, + "json", + defaultWebOptions.json, + "display log in json format", + ) + + fs.BoolVar( + &c.html, + "html", + defaultWebOptions.html, + "enable unsafe html", + ) + + fs.BoolVar( + &c.analytics, + "with-analytics", + defaultWebOptions.analytics, + "nable privacy-first analytics", + ) + + fs.BoolVar( + &c.verbose, + "v", + defaultWebOptions.verbose, + "verbose logging mode", + ) } -func runMain(args []string) error { - var ( - fs = flag.NewFlagSet("gnoweb", flag.ContinueOnError) - cfg = gnoweb.NewDefaultConfig() - bindAddress string - ) - fs.StringVar(&cfg.RemoteAddr, "remote", cfg.RemoteAddr, "remote gnoland node address") - fs.StringVar(&cfg.CaptchaSite, "captcha-site", cfg.CaptchaSite, "recaptcha site key (if empty, captcha are disabled)") - fs.StringVar(&cfg.FaucetURL, "faucet-url", cfg.FaucetURL, "faucet server URL") - fs.StringVar(&cfg.ViewsDir, "views-dir", cfg.ViewsDir, "views directory location") // XXX: replace with goembed - fs.StringVar(&cfg.HelpChainID, "help-chainid", cfg.HelpChainID, "help page's chainid") - fs.StringVar(&cfg.HelpRemote, "help-remote", cfg.HelpRemote, "help page's remote addr") - fs.BoolVar(&cfg.WithAnalytics, "with-analytics", cfg.WithAnalytics, "enable privacy-first analytics") - fs.StringVar(&bindAddress, "bind", "127.0.0.1:8888", "server listening address") - fs.BoolVar(&cfg.WithHTML, "with-html", cfg.WithHTML, "Enable HTML parsing in markdown rendering") - - if err := fs.Parse(args); err != nil { - return err +func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) { + // Setup logger + level := zapcore.InfoLevel + if cfg.verbose { + level = zapcore.DebugLevel + } + + var zapLogger *zap.Logger + if cfg.json { + zapLogger = log.NewZapJSONLogger(io.Out(), level) + } else { + zapLogger = log.NewZapConsoleLogger(io.Out(), level) } + defer zapLogger.Sync() - zapLogger := log.NewZapConsoleLogger(os.Stdout, zapcore.DebugLevel) logger := log.ZapLoggerToSlog(zapLogger) - logger.Info("Running", "listener", "http://"+bindAddress) + appcfg := gnoweb.NewDefaultAppConfig() + appcfg.ChainID = cfg.chainid + appcfg.NodeRemote = cfg.remote + appcfg.RemoteHelp = cfg.remoteHelp + appcfg.Analytics = cfg.analytics + appcfg.UnsafeHTML = cfg.html + appcfg.FaucetURL = cfg.faucetURL + appcfg.AssetsDir = cfg.assetsDir + if appcfg.RemoteHelp == "" { + appcfg.RemoteHelp = appcfg.NodeRemote + } + + app, err := gnoweb.NewRouter(logger, appcfg) + if err != nil { + return nil, fmt.Errorf("unable to start gnoweb app: %w", err) + } + + bindaddr, err := net.ResolveTCPAddr("tcp", cfg.bind) + if err != nil { + return nil, fmt.Errorf("unable to resolve listener %q: %w", cfg.bind, err) + } + + logger.Info("Running", "listener", bindaddr.String()) + server := &http.Server{ - Addr: bindAddress, + Handler: app, + Addr: bindaddr.String(), ReadHeaderTimeout: 60 * time.Second, - Handler: gnoweb.MakeApp(logger, cfg).Router, } - if err := server.ListenAndServe(); err != nil { - logger.Error("HTTP server stopped", " error:", err) - } + return func() error { + if err := server.ListenAndServe(); err != nil { + logger.Error("HTTP server stopped", " error:", err) + return commands.ExitCodeError(1) + } - return zapLogger.Sync() + return nil + }, nil } diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go index 640c4763140..37006c18c93 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/cmd/gnoweb/main_test.go @@ -1,14 +1,25 @@ package main import ( - "errors" - "flag" + "os" "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/require" ) -func TestFlagHelp(t *testing.T) { - err := runMain([]string{"-h"}) - if !errors.Is(err, flag.ErrHelp) { - t.Errorf("should display usage") - } +func TestSetupWeb(t *testing.T) { + opts := defaultWebOptions + opts.bind = "127.0.0.1:0" // random port + stdio := commands.NewDefaultIO() + + // Open /dev/null as a write-only file + devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0o644) + require.NoError(t, err) + defer devNull.Close() + + stdio.SetOut(devNull) + + _, err = setupWeb(&opts, []string{}, stdio) + require.NoError(t, err) } diff --git a/gno.land/pkg/gnoweb/.gitignore b/gno.land/pkg/gnoweb/.gitignore new file mode 100644 index 00000000000..dd09eb49099 --- /dev/null +++ b/gno.land/pkg/gnoweb/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +tmp/ +.cache diff --git a/gno.land/pkg/gnoweb/Makefile b/gno.land/pkg/gnoweb/Makefile new file mode 100644 index 00000000000..61397fef54f --- /dev/null +++ b/gno.land/pkg/gnoweb/Makefile @@ -0,0 +1,100 @@ +# Configurable arguments +DEV_REMOTE ?= 127.0.0.1:26657 +CHAIN_ID ?= test3 +PUBLIC_DIR ?= public + +# Variable Declarations +tools_run := go run -modfile ./tools/go.mod +run_reflex := $(tools_run) github.com/cespare/reflex +run_logname := go -C ./tools run ./cmd/logname + +# css config +input_css := frontend/css/input.css +output_css := $(PUBLIC_DIR)/styles.css +tw_version := 3.4.14 +tw_config_path := frontend/css/tx.config.js + +# static config +src_dir_static := frontend/static +out_dir_static := $(PUBLIC_DIR) +input_static := $(shell find $(src_dir_static) -type f) +output_static := $(patsubst $(src_dir_static)/%, $(out_dir_static)/%, $(input_static)) + +# esbuild config +src_dir_js := frontend/js +out_dir_js := $(PUBLIC_DIR)/js +input_js := $(shell find $(src_dir_js) -name '*.ts') +output_js := $(patsubst $(src_dir_js)/%.ts,$(out_dir_js)/%.js,$(input_js)) +esbuild_version := 0.24.0 + +# cache +cache_dir := .cache + +############# +# Targets +############# +.PHONY: all generate fmt css ts + +# Install dependencies +all: generate + +# Generate process +generate: css ts static + +css: $(output_css) +$(output_css): $(input_css) + npx -y tailwindcss@$(tw_version) -c $(tw_config_path) -i $< -o $@ --minify # tailwind + touch $@ + +ts: $(output_js) +$(out_dir_js)/%.js: $(src_dir_js)/%.ts + npx -y esbuild $< --log-level=error --bundle --outdir=$(out_dir_js) --format=esm --minify + +# Rule to copy static files while preserving directory structure +static: $(output_static) +$(out_dir_static)/%: $(src_dir_static)/% + @mkdir -p $(dir $@) + @cp -v $< $@ + +# Format process +fmt: + go fmt ./... + + ############################### + # Developments + ############################### +.PHONY: dev dev.server dev.css dev.ts deps + +# Run the development dependencies in parallel +dev: + @echo "-- starting development tools" + @PUBLIC_DIR=$(cache_dir)/public $(MAKE) -j 3 \ + dev.gnoweb \ + dev.ts \ + dev.css + +# Go server in development mode +dev.gnoweb: generate + $(run_reflex) -s -r '.*\.go(html)?' -- \ + go run ../../cmd/gnoweb -assets-dir=${PUBLIC_DIR} -chainid=${CHAIN_ID} -remote=${DEV_REMOTE} \ + 2>&1 | $(run_logname) gnoweb + +# Tailwind CSS in development mode +dev.css: generate | $(PUBLIC_DIR) + npx -y tailwindcss@$(tw_version) -c $(tw_config_path) --verbose -i $(input_css) -o $(output_css) --watch \ + 2>&1 | $(run_logname) tailwind + +# XXX: add versioning on esbuild +# TS in development mode +dev.ts: generate | $(PUBLIC_DIR) + npx -y esbuild@$(esbuild_version) $(input_js) --bundle --outdir=$(out_dir_js) --sourcemap --format=esm --watch \ + 2>&1 | $(run_logname) esbuild + +# Cleanup +clean: + rm -rf $(cache_dir) tmp +fclean: clean + rm -rf $(PUBLIC_DIR) + +# Dirs +$(PUBLIC_DIR):; mkdir -p $@ diff --git a/gno.land/pkg/gnoweb/README.md b/gno.land/pkg/gnoweb/README.md new file mode 100644 index 00000000000..287279538d8 --- /dev/null +++ b/gno.land/pkg/gnoweb/README.md @@ -0,0 +1,45 @@ +# gnoweb + +`gnoweb` is a universal web frontend for the gno.land blockchain. + +This README provides instructions on how to set up and run `gnoweb` for development purposes. + +## Prerequisites + +Before you begin, ensure you have the following software installed on your machine: + +- **Node.js**: Required for running JavaScript and CSS build tools. +- **Go**: Required for building `gnoweb` + +## Development + +To start the development environment, which runs multiple development tools in parallel, +use the following command: + +```sh +make dev +``` + +This will: + +- Start a Go server in development mode and watch for any Go files change (targeting [localhost](http://localhost:8888)). +- Enable Tailwind CSS in watch mode to automatically compile CSS changes. +- Use esbuild in watch mode to automatically transpile and bundle TypeScript changes. + +You can customize the behavior of the Go server using the `DEV_REMOTE` and +`CHAIN_ID` environment variables. For example, to use `portal-loop` as the +target, run: + +```sh +CHAIN_ID=portal-loop DEV_REMOTE=https://rpc.gno.land make dev +``` + +## Generate + +To generate the public assets for the project, including static assets (fonts, CSS and JavaScript... +files), run the following command. This should be used while editing CSS, JS, or +any asset files: + +```sh +make generate +``` diff --git a/gno.land/pkg/gnoweb/alias.go b/gno.land/pkg/gnoweb/alias.go index 7fb28d5cbc3..06bb3941e41 100644 --- a/gno.land/pkg/gnoweb/alias.go +++ b/gno.land/pkg/gnoweb/alias.go @@ -1,6 +1,12 @@ package gnoweb -// realm aliases +import ( + "net/http" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" +) + +// Aliases are gnoweb paths that are rewritten using [AliasAndRedirectMiddleware]. var Aliases = map[string]string{ "/": "/r/gnoland/home", "/about": "/r/gnoland/pages:p/about", @@ -14,7 +20,7 @@ var Aliases = map[string]string{ "/events": "/r/gnoland/events", } -// http redirects +// Redirect are gnoweb paths that are redirected using [AliasAndRedirectMiddleware]. var Redirects = map[string]string{ "/r/demo/boards:gnolang/6": "/r/demo/boards:gnolang/3", // XXX: temporary "/blog": "/r/gnoland/blog", @@ -23,5 +29,29 @@ var Redirects = map[string]string{ "/grants": "/partners", "/language": "/gnolang", "/getting-started": "/start", - "/gophercon24": "https://docs.gno.land", +} + +// AliasAndRedirectMiddleware redirects all incoming requests whose path matches +// any of the [Redirects] to the corresponding URL; and rewrites the URL path +// for incoming requests which match any of the [Aliases]. +func AliasAndRedirectMiddleware(next http.Handler, analytics bool) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request path matches a redirect + if newPath, ok := Redirects[r.URL.Path]; ok { + http.Redirect(w, r, newPath, http.StatusFound) + components.RenderRedirectComponent(w, components.RedirectData{ + To: newPath, + WithAnalytics: analytics, + }) + return + } + + // Check if the request path matches an alias + if newPath, ok := Aliases[r.URL.Path]; ok { + r.URL.Path = newPath + } + + // Call the next handler + next.ServeHTTP(w, r) + }) } diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go new file mode 100644 index 00000000000..dc13253468e --- /dev/null +++ b/gno.land/pkg/gnoweb/app.go @@ -0,0 +1,152 @@ +package gnoweb + +import ( + "fmt" + "log/slog" + "net/http" + "path" + "strings" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/styles" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/yuin/goldmark" + mdhtml "github.com/yuin/goldmark/renderer/html" +) + +// AppConfig contains configuration for the gnoweb. +type AppConfig struct { + // UnsafeHTML, if enabled, allows to use HTML in the markdown. + UnsafeHTML bool + // Analytics enables SimpleAnalytics. + Analytics bool + // NodeRemote is the remote address of the gno.land node. + NodeRemote string + // RemoteHelp is the remote of the gno.land node, as used in the help page. + RemoteHelp string + // ChainID is the chain id, used for constructing the help page. + ChainID string + // AssetsPath is the base path to the gnoweb assets. + AssetsPath string + // AssetDir, if set, will be used for assets instead of the embedded public directory + AssetsDir string + // FaucetURL, if specified, will be the URL to which `/faucet` redirects. + FaucetURL string +} + +// NewDefaultAppConfig returns a new default [AppConfig]. The default sets +// 127.0.0.1:26657 as the remote node, "dev" as the chain ID and sets up Assets +// to be served on /public/. +func NewDefaultAppConfig() *AppConfig { + const defaultRemote = "127.0.0.1:26657" + + return &AppConfig{ + // same as Remote by default + NodeRemote: defaultRemote, + RemoteHelp: defaultRemote, + ChainID: "dev", + AssetsPath: "/public/", + } +} + +var chromaStyle = mustGetStyle("friendly") + +func mustGetStyle(name string) *chroma.Style { + s := styles.Get(name) + if s == nil { + panic("unable to get chroma style") + } + return s +} + +// NewRouter initializes the gnoweb router, with the given logger and config. +func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { + chromaOptions := []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + chromahtml.WithLinkableLineNumbers(true, "L"), + chromahtml.WithClasses(true), + chromahtml.ClassPrefix("chroma-"), + } + + mdopts := []goldmark.Option{ + goldmark.WithExtensions( + markdown.NewHighlighting( + markdown.WithFormatOptions(chromaOptions...), + ), + ), + } + if cfg.UnsafeHTML { + mdopts = append(mdopts, goldmark.WithRendererOptions(mdhtml.WithXHTML(), mdhtml.WithUnsafe())) + } + + md := goldmark.New(mdopts...) + + client, err := client.NewHTTPClient(cfg.NodeRemote) + if err != nil { + return nil, fmt.Errorf("unable to create http client: %w", err) + } + webcli := NewWebClient(logger, client, md) + + formatter := chromahtml.New(chromaOptions...) + chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css") + + var webConfig WebHandlerConfig + + webConfig.RenderClient = webcli + webConfig.Formatter = newFormatterWithStyle(formatter, chromaStyle) + + // Static meta + webConfig.Meta.AssetsPath = cfg.AssetsPath + webConfig.Meta.ChromaPath = chromaStylePath + webConfig.Meta.RemoteHelp = cfg.RemoteHelp + webConfig.Meta.ChainId = cfg.ChainID + webConfig.Meta.Analytics = cfg.Analytics + + // Setup main handler + webhandler := NewWebHandler(logger, webConfig) + + mux := http.NewServeMux() + + // Setup Webahndler along Alias Middleware + mux.Handle("/", AliasAndRedirectMiddleware(webhandler, cfg.Analytics)) + + // Register faucet URL to `/faucet` if specified + if cfg.FaucetURL != "" { + mux.Handle("/faucet", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, cfg.FaucetURL, http.StatusFound) + components.RenderRedirectComponent(w, components.RedirectData{ + To: cfg.FaucetURL, + WithAnalytics: cfg.Analytics, + }) + })) + } + + // setup assets + mux.Handle(chromaStylePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Setup Formatter + w.Header().Set("Content-Type", "text/css") + if err := formatter.WriteCSS(w, chromaStyle); err != nil { + logger.Error("unable to write css", "err", err) + http.NotFound(w, r) + } + })) + + // Normalize assets path + assetsBase := "/" + strings.Trim(cfg.AssetsPath, "/") + "/" + + // Handle assets path + if cfg.AssetsDir != "" { + logger.Debug("using assets dir instead of embed assets", "dir", cfg.AssetsDir) + mux.Handle(assetsBase, DevAssetHandler(assetsBase, cfg.AssetsDir)) + } else { + mux.Handle(assetsBase, AssetHandler()) + } + + // Handle status page + mux.Handle("/status.json", handlerStatusJSON(logger, client)) + + return mux, nil +} diff --git a/gno.land/pkg/gnoweb/gnoweb_test.go b/gno.land/pkg/gnoweb/app_test.go similarity index 67% rename from gno.land/pkg/gnoweb/gnoweb_test.go rename to gno.land/pkg/gnoweb/app_test.go index 99eb86ea07e..78fe197a134 100644 --- a/gno.land/pkg/gnoweb/gnoweb_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -4,13 +4,13 @@ import ( "fmt" "net/http" "net/http/httptest" - "strings" "testing" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gotuna/gotuna/test/assert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRoutes(t *testing.T) { @@ -27,12 +27,12 @@ func TestRoutes(t *testing.T) { {"/", ok, "Welcome"}, // assert / gives 200 (OK). assert / contains "Welcome". {"/about", ok, "blockchain"}, {"/r/gnoland/blog", ok, ""}, // whatever content - {"/r/gnoland/blog$help", ok, "exposed"}, + {"/r/gnoland/blog$help", ok, "AdminSetAdminAddr"}, {"/r/gnoland/blog/", ok, "admin.gno"}, - {"/r/gnoland/blog/admin.gno", ok, "func "}, - {"/r/gnoland/blog$help&func=Render", ok, "Render(...)"}, - {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `input type="text" value="foo/bar"`}, - {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, + {"/r/gnoland/blog/admin.gno", ok, ">func<"}, + {"/r/gnoland/blog$help&func=Render", ok, "Render(path)"}, + {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `value="foo/bar"`}, + // {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, // XXX(TODO) {"/r/demo/users:administrator", ok, "address"}, {"/r/demo/users", ok, "moul"}, {"/r/demo/users/users.gno", ok, "// State"}, @@ -40,18 +40,18 @@ func TestRoutes(t *testing.T) { {"/r/demo/deep/very/deep?arg1=val1&arg2=val2", ok, "hi ?arg1=val1&arg2=val2"}, {"/r/demo/deep/very/deep:bob", ok, "hi bob"}, {"/r/demo/deep/very/deep:bob?arg1=val1&arg2=val2", ok, "hi bob?arg1=val1&arg2=val2"}, - {"/r/demo/deep/very/deep$help", ok, "exposed"}, + {"/r/demo/deep/very/deep$help", ok, "Render"}, {"/r/demo/deep/very/deep/", ok, "render.gno"}, - {"/r/demo/deep/very/deep/render.gno", ok, "func Render("}, + {"/r/demo/deep/very/deep/render.gno", ok, ">package<"}, {"/contribute", ok, "Game of Realms"}, {"/game-of-realms", found, "/contribute"}, {"/gor", found, "/contribute"}, {"/blog", found, "/r/gnoland/blog"}, - {"/404-not-found", notFound, "/404-not-found"}, - {"/아스키문자가아닌경로", notFound, "/아스키문자가아닌경로"}, - {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, "/테스트"}, - {"/グノー", notFound, "/グノー"}, - {"/⚛️", notFound, "/⚛️"}, + {"/404/not/found/", notFound, ""}, + {"/아스키문자가아닌경로", notFound, ""}, + {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, ""}, + {"/グノー", notFound, ""}, + {"/⚛️", notFound, ""}, {"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"}, } @@ -61,20 +61,21 @@ func TestRoutes(t *testing.T) { node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) defer node.Stop() - cfg := NewDefaultConfig() + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr logger := log.NewTestingLogger(t) // set the `remoteAddr` of the client to the listening address of the // node, which is randomly assigned. - cfg.RemoteAddr = remoteAddr - app := MakeApp(logger, cfg) + router, err := NewRouter(logger, cfg) + require.NoError(t, err) for _, r := range routes { t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) { request := httptest.NewRequest(http.MethodGet, r.route, nil) response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) + router.ServeHTTP(response, request) assert.Equal(t, r.status, response.Code) assert.Contains(t, response.Body.String(), r.substring) }) @@ -110,34 +111,39 @@ func TestAnalytics(t *testing.T) { node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) defer node.Stop() - cfg := NewDefaultConfig() - cfg.RemoteAddr = remoteAddr - - logger := log.NewTestingLogger(t) - - t.Run("with", func(t *testing.T) { + t.Run("enabled", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { - ccfg := cfg // clone config - ccfg.WithAnalytics = true - app := MakeApp(logger, ccfg) + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + cfg.Analytics = true + logger := log.NewTestingLogger(t) + + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) + router.ServeHTTP(response, request) + fmt.Println("HELLO:", response.Body.String()) assert.Contains(t, response.Body.String(), "sa.gno.services") }) } }) - t.Run("without", func(t *testing.T) { + t.Run("disabled", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { - ccfg := cfg // clone config - ccfg.WithAnalytics = false - app := MakeApp(logger, ccfg) + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + cfg.Analytics = false + logger := log.NewTestingLogger(t) + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) - assert.Equal(t, strings.Contains(response.Body.String(), "sa.gno.services"), false) + router.ServeHTTP(response, request) + assert.NotContains(t, response.Body.String(), "sa.gno.services") }) } }) diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.go b/gno.land/pkg/gnoweb/components/breadcrumb.go new file mode 100644 index 00000000000..9e7a97b2fae --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.go @@ -0,0 +1,18 @@ +package components + +import ( + "io" +) + +type BreadcrumbPart struct { + Name string + Path string +} + +type BreadcrumbData struct { + Parts []BreadcrumbPart +} + +func RenderBreadcrumpComponent(w io.Writer, data BreadcrumbData) error { + return tmpl.ExecuteTemplate(w, "Breadcrumb", data) +} diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.gohtml b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml new file mode 100644 index 00000000000..a3301cb037e --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml @@ -0,0 +1,12 @@ +{{ define "breadcrumb" }} +
    + {{- range $index, $part := .Parts }} + {{- if $index }} +
  1. + {{- else }} +
  2. + {{- end }} + {{ $part.Name }}
  3. + {{- end }} +
+{{ end }} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/components/directory.go b/gno.land/pkg/gnoweb/components/directory.go new file mode 100644 index 00000000000..6e47db3b2c4 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/directory.go @@ -0,0 +1,15 @@ +package components + +import ( + "io" +) + +type DirData struct { + PkgPath string + Files []string + FileCounter int +} + +func RenderDirectoryComponent(w io.Writer, data DirData) error { + return tmpl.ExecuteTemplate(w, "renderDir", data) +} diff --git a/gno.land/pkg/gnoweb/components/directory.gohtml b/gno.land/pkg/gnoweb/components/directory.gohtml new file mode 100644 index 00000000000..4cdeff12a38 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/directory.gohtml @@ -0,0 +1,39 @@ +{{ define "renderDir" }} +
+
+ + + {{ $pkgpath := .PkgPath }} +
+
+
+

{{ $pkgpath }}

+
+
+ Directory · {{ .FileCounter }} Files +
+
+ +
+ +
+
+
+ +
+{{ end }} + diff --git a/gno.land/pkg/gnoweb/components/help.go b/gno.land/pkg/gnoweb/components/help.go new file mode 100644 index 00000000000..e819705006b --- /dev/null +++ b/gno.land/pkg/gnoweb/components/help.go @@ -0,0 +1,51 @@ +package components + +import ( + "html/template" + "io" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types +) + +type HelpData struct { + // Selected function + SelectedFunc string + SelectedArgs map[string]string + + RealmName string + Functions []vm.FunctionSignature + ChainId string + Remote string + PkgPath string +} + +func registerHelpFuncs(funcs template.FuncMap) { + funcs["helpFuncSignature"] = func(fsig vm.FunctionSignature) (string, error) { + var fsigStr strings.Builder + + fsigStr.WriteString(fsig.FuncName) + fsigStr.WriteRune('(') + for i, param := range fsig.Params { + if i > 0 { + fsigStr.WriteString(", ") + } + fsigStr.WriteString(param.Name) + } + fsigStr.WriteRune(')') + + return fsigStr.String(), nil + } + + funcs["getSelectedArgValue"] = func(data HelpData, param vm.NamedType) (string, error) { + if data.SelectedArgs == nil { + return "", nil + } + + return data.SelectedArgs[param.Name], nil + } +} + +func RenderHelpComponent(w io.Writer, data HelpData) error { + return tmpl.ExecuteTemplate(w, "renderHelp", data) +} diff --git a/gno.land/pkg/gnoweb/components/help.gohtml b/gno.land/pkg/gnoweb/components/help.gohtml new file mode 100644 index 00000000000..dea4f683a0a --- /dev/null +++ b/gno.land/pkg/gnoweb/components/help.gohtml @@ -0,0 +1,100 @@ +{{ define "renderHelp" }} + {{ $data := . }} +
+
+
+
+

{{ .RealmName }}

+
+
+
+ +
+
+ + +
+
+
+ +
+ + {{ range .Functions }} +
+

{{ .FuncName }}

+
+
+

Params

+
+ {{ $funcName := .FuncName }} + {{ range .Params }} +
+
+ + +
+
+ {{ end }} +
+
+
+
+

Command

+
+ +
gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .FuncName }}" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid "{{ $.ChainId }}"{{ range .Params }} -args ""{{ end }} -remote "{{ $.Remote }}" ADDRESS
+
+
+
+ {{ end }} + +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/index.go b/gno.land/pkg/gnoweb/components/index.go new file mode 100644 index 00000000000..0cc020ae261 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/index.go @@ -0,0 +1,47 @@ +package components + +import ( + "context" + "html/template" + "io" + "net/url" +) + +type HeadData struct { + Title string + Description string + Canonical string + Image string + URL string + ChromaPath string + AssetsPath string + Analytics bool +} + +type HeaderData struct { + RealmPath string + Breadcrumb BreadcrumbData + WebQuery url.Values +} + +type FooterData struct { + Analytics bool + AssetsPath string +} + +type IndexData struct { + HeadData + HeaderData + FooterData + Body template.HTML +} + +func IndexComponent(data IndexData) Component { + return func(ctx context.Context, tmpl *template.Template, w io.Writer) error { + return tmpl.ExecuteTemplate(w, "index", data) + } +} + +func RenderIndexComponent(w io.Writer, data IndexData) error { + return tmpl.ExecuteTemplate(w, "index", data) +} diff --git a/gno.land/pkg/gnoweb/components/index.gohtml b/gno.land/pkg/gnoweb/components/index.gohtml new file mode 100644 index 00000000000..19fd1e21a6f --- /dev/null +++ b/gno.land/pkg/gnoweb/components/index.gohtml @@ -0,0 +1,155 @@ +{{ define "index" }} + + {{ template "head" .HeadData }} + + {{ template "spritesvg" }} + + + {{ template "header" .HeaderData }} + + + {{ template "main" .Body }} + + + {{ template "footer" .FooterData }} + + +{{ end }} + +{{ define "head" }} + + + + {{ .Title }} + + + + + + {{ if .Canonical }} + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{ end }} + +{{ define "header" }} +
+ +
+{{ end }} + +{{ define "main" }} + {{ . }} +{{ end }} + +{{ define "footer" }} + + +{{- if .Analytics -}} {{- template "analytics" }} {{- end -}} + +{{- end }} + +{{- define "analytics" -}} + + + +{{- end -}} diff --git a/gno.land/pkg/gnoweb/components/logosvg.gohtml b/gno.land/pkg/gnoweb/components/logosvg.gohtml new file mode 100644 index 00000000000..5ebe6460ee3 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/logosvg.gohtml @@ -0,0 +1,21 @@ +{{ define "logosvg" }} + + + + + + + + + + + + + + + + + + + +{{ end }} diff --git a/gno.land/pkg/gnoweb/components/realm.go b/gno.land/pkg/gnoweb/components/realm.go new file mode 100644 index 00000000000..027760bb382 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/realm.go @@ -0,0 +1,32 @@ +package components + +import ( + "context" + "html/template" + "io" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown" +) + +type RealmTOCData struct { + Items []*markdown.TocItem +} + +func RealmTOCComponent(data *RealmTOCData) Component { + return func(ctx context.Context, tmpl *template.Template, w io.Writer) error { + return tmpl.ExecuteTemplate(w, "renderRealmToc", data) + } +} + +func RenderRealmTOCComponent(w io.Writer, data *RealmTOCData) error { + return tmpl.ExecuteTemplate(w, "renderRealmToc", data) +} + +type RealmData struct { + Content template.HTML + TocItems *RealmTOCData +} + +func RenderRealmComponent(w io.Writer, data RealmData) error { + return tmpl.ExecuteTemplate(w, "renderRealm", data) +} diff --git a/gno.land/pkg/gnoweb/components/realm.gohtml b/gno.land/pkg/gnoweb/components/realm.gohtml new file mode 100644 index 00000000000..8cd887b8ac3 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/realm.gohtml @@ -0,0 +1,37 @@ +{{ define "renderRealmToc" }} + +{{ end }} + +{{ define "renderRealm" }} +
+
+ +
+ + {{ .Content }} +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/redirect.go b/gno.land/pkg/gnoweb/components/redirect.go new file mode 100644 index 00000000000..873ddf56ff5 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/redirect.go @@ -0,0 +1,12 @@ +package components + +import "io" + +type RedirectData struct { + To string + WithAnalytics bool +} + +func RenderRedirectComponent(w io.Writer, data RedirectData) error { + return tmpl.ExecuteTemplate(w, "renderRedirect", data) +} diff --git a/gno.land/pkg/gnoweb/components/redirect.gohtml b/gno.land/pkg/gnoweb/components/redirect.gohtml new file mode 100644 index 00000000000..45dac0981cd --- /dev/null +++ b/gno.land/pkg/gnoweb/components/redirect.gohtml @@ -0,0 +1,16 @@ +{{- define "renderRedirect" -}} + + + + + + + + Redirecting to {{.To}} + + + {{.To}} + {{- if .WithAnalytics -}} {{- template "analytics" }} {{- end -}} + + +{{- end -}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/components/source.go b/gno.land/pkg/gnoweb/components/source.go new file mode 100644 index 00000000000..23170776657 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/source.go @@ -0,0 +1,20 @@ +package components + +import ( + "html/template" + "io" +) + +type SourceData struct { + PkgPath string + Files []string + FileName string + FileSize string + FileLines int + FileCounter int + FileSource template.HTML +} + +func RenderSourceComponent(w io.Writer, data SourceData) error { + return tmpl.ExecuteTemplate(w, "renderSource", data) +} diff --git a/gno.land/pkg/gnoweb/components/source.gohtml b/gno.land/pkg/gnoweb/components/source.gohtml new file mode 100644 index 00000000000..ef254bdd313 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/source.gohtml @@ -0,0 +1,51 @@ +{{ define "renderSource" }} +
+
+
+
+

{{ .FileName }}

+
+
+ {{ .FileSize }} · {{ .FileLines }} lines + +
+
+ + +
+
+ {{ .FileSource }} +
+
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/spritesvg.gohtml b/gno.land/pkg/gnoweb/components/spritesvg.gohtml new file mode 100644 index 00000000000..811ad6e6846 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/spritesvg.gohtml @@ -0,0 +1,134 @@ +{{ define "spritesvg" }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{ end }} diff --git a/gno.land/pkg/gnoweb/components/status.gohtml b/gno.land/pkg/gnoweb/components/status.gohtml new file mode 100644 index 00000000000..2321d1110bd --- /dev/null +++ b/gno.land/pkg/gnoweb/components/status.gohtml @@ -0,0 +1,12 @@ +{{ define "status" }} +
+
+
+ gno land +

Error: {{ .Message }}

+

Something went wrong. Let’s find our way back!

+ Go Back Home +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/template.go b/gno.land/pkg/gnoweb/components/template.go new file mode 100644 index 00000000000..9c08703f460 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/template.go @@ -0,0 +1,77 @@ +package components + +import ( + "bytes" + "context" + "embed" + "html/template" + "io" + "net/url" +) + +//go:embed *.gohtml +var gohtml embed.FS + +var funcMap = template.FuncMap{ + // NOTE: this method does NOT escape HTML, use with caution + "noescape_string": func(in string) template.HTML { + return template.HTML(in) //nolint:gosec + }, + // NOTE: this method does NOT escape HTML, use with caution + "noescape_bytes": func(in []byte) template.HTML { + return template.HTML(in) //nolint:gosec + }, + "queryHas": func(vals url.Values, key string) bool { + if vals == nil { + return false + } + + return vals.Has(key) + }, +} + +var tmpl = template.New("web").Funcs(funcMap) + +func init() { + registerHelpFuncs(funcMap) + tmpl.Funcs(funcMap) + + var err error + tmpl, err = tmpl.ParseFS(gohtml, "*.gohtml") + if err != nil { + panic("unable to parse embed tempalates: " + err.Error()) + } +} + +type Component func(ctx context.Context, tmpl *template.Template, w io.Writer) error + +func (c Component) Render(ctx context.Context, w io.Writer) error { + return RenderComponent(ctx, w, c) +} + +func RenderComponent(ctx context.Context, w io.Writer, c Component) error { + var render *template.Template + funcmap := template.FuncMap{ + "render": func(cf Component) (string, error) { + var buf bytes.Buffer + if err := cf(ctx, render, &buf); err != nil { + return "", err + } + + return buf.String(), nil + }, + } + + render = tmpl.Funcs(funcmap) + return c(ctx, render, w) +} + +type StatusData struct { + Message string +} + +func RenderStatusComponent(w io.Writer, message string) error { + return tmpl.ExecuteTemplate(w, "status", StatusData{ + Message: message, + }) +} diff --git a/gno.land/pkg/gnoweb/formatter.go b/gno.land/pkg/gnoweb/formatter.go new file mode 100644 index 00000000000..e172afe9e21 --- /dev/null +++ b/gno.land/pkg/gnoweb/formatter.go @@ -0,0 +1,25 @@ +package gnoweb + +import ( + "io" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/formatters/html" +) + +type Formatter interface { + Format(w io.Writer, iterator chroma.Iterator) error +} + +type formatterWithStyle struct { + *html.Formatter + style *chroma.Style +} + +func newFormatterWithStyle(formater *html.Formatter, style *chroma.Style) Formatter { + return &formatterWithStyle{Formatter: formater, style: style} +} + +func (f *formatterWithStyle) Format(w io.Writer, iterator chroma.Iterator) error { + return f.Formatter.Format(w, f.style, iterator) +} diff --git a/gno.land/pkg/gnoweb/frontend/css/input.css b/gno.land/pkg/gnoweb/frontend/css/input.css new file mode 100644 index 00000000000..2c2e110c27c --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/css/input.css @@ -0,0 +1,352 @@ +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("./fonts/roboto/roboto-mono-normal.woff2") format("woff2"), url("./fonts/roboto/roboto-mono-normal.woff") format("woff"); +} + +@font-face { + font-family: "Inter var"; + font-weight: 100 900; + font-display: swap; + font-style: oblique 0deg 10deg; + src: url("./fonts/intervar/Inter.var.woff2") format("woff2"); +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + @apply font-interNormal text-gray-600 bg-light text-200; + font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; + -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; + text-size-adjust: 100%; + -moz-osx-font-smoothing: grayscale; + font-smoothing: antialiased; + font-variant-ligatures: contextual common-ligatures; + font-kerning: normal; + text-rendering: optimizeLegibility; + overflow-x: hidden; + } + + @supports (font-variation-settings: normal) { + html { + @apply font-interVar; + } + } + + svg { + @apply max-w-full max-h-full; + } + + form { + @apply my-0; + } + + .realm-content { + @apply text-200; + } + + .realm-content a { + @apply text-green-600 font-medium hover:underline; + } + + .realm-content h1, + .realm-content h2, + .realm-content h3, + .realm-content h4 { + @apply text-gray-900 mt-8 leading-tight; + } + + .realm-content h2, + .realm-content h2 * { + @apply font-bold; + } + + .realm-content h3, + .realm-content h3 *, + .realm-content h4, + .realm-content h4 * { + @apply font-semibold; + } + + .realm-content h1 + h2, + .realm-content h2 + h3, + .realm-content h3 + h4 { + @apply mt-1.5; + } + + .realm-content h1 { + @apply text-800 font-bold; + } + + .realm-content h2 { + @apply text-600; + } + + .realm-content h3 { + @apply text-400 text-gray-600 mt-6; + } + + .realm-content h4 { + @apply text-300 text-gray-600 font-medium my-4; + } + + .realm-content p { + @apply my-4; + } + + .realm-content strong { + @apply font-bold text-gray-900; + } + + .realm-content strong * { + @apply font-bold; + } + + .realm-content em { + @apply italic-subtle; + } + + .realm-content blockquote { + @apply border-l-4 border-gray-300 pl-4 text-gray-600 italic-subtle my-4; + } + + .realm-content ul, + .realm-content ol { + @apply pl-4 my-4; + } + + .realm-content ul li, + .realm-content ol li { + @apply mb-1; + } + + .realm-content img { + @apply max-w-full my-6; + } + + .realm-content figure { + @apply my-6 text-center; + } + + .realm-content figcaption { + @apply text-100 text-gray-600; + } + + .realm-content :not(pre) > code { + @apply bg-gray-100 px-1 py-0.5 rounded-sm text-100 font-mono; + } + + .realm-content pre { + @apply bg-gray-50 p-4 rounded overflow-x-auto font-mono; + } + + .realm-content hr { + @apply border-t border-gray-100 my-8; + } + + .realm-content table { + @apply w-full border-collapse my-6; + } + + .realm-content th, + .realm-content td { + @apply border border-gray-300 px-4 py-2; + } + + .realm-content th { + @apply bg-gray-100 font-bold; + } + + .realm-content caption { + @apply mt-2 text-100 text-gray-600 text-left; + } + + .realm-content q { + @apply quotes; + } + + .realm-content q::before { + content: open-quote; + } + + .realm-content q::after { + content: close-quote; + } + + .realm-content ul ul, + .realm-content ul ol, + .realm-content ol ul, + .realm-content ol ol { + @apply mt-2 mb-2 pl-4; + } + + .realm-content ul { + @apply list-disc; + } + + .realm-content ol { + @apply list-decimal; + } + + .realm-content table th:first-child, + .realm-content td:first-child { + @apply pl-0; + } + + .realm-content table th:last-child, + .realm-content td:last-child { + @apply pr-0; + } + + .realm-content abbr[title] { + @apply border-b border-dotted cursor-help; + } + + .realm-content details { + @apply my-4; + } + + .realm-content summary { + @apply font-bold cursor-pointer; + } + + .realm-content a code { + @apply text-inherit; + } + + .realm-content video { + @apply max-w-full my-6; + } + + .realm-content math { + @apply font-mono; + } + + .realm-content small { + @apply text-100; + } + + .realm-content del { + @apply line-through; + } + + .realm-content sub { + @apply text-50 align-sub; + } + + .realm-content sup { + @apply text-50 align-super; + } + + .realm-content input, + .realm-content button { + @apply px-4 py-2 border border-gray-300; + } + + main :is(h1, h2, h3, h4) { + @apply scroll-mt-24; + } + + ::-moz-selection { + @apply bg-green-600 text-light; + } + ::selection { + @apply bg-green-600 text-light; + } +} + +@layer components { + /* header */ + .sidemenu .peer:checked + label > svg { + @apply text-green-600; + } + + /* toc */ + .toc-expend-btn { + @apply after:content-['open'] after:font-normal after:text-100 lg:after:content-none; + } + .toc-expend-btn:has(#toc-expend:checked) { + @apply after:content-['close'] lg:after:content-none; + } + .toc-expend-btn:has(#toc-expend:checked) + nav { + @apply block; + } + + /* sidebar */ + .main-header:has(#sidemenu-summary:checked) + main #sidebar #sidebar-summary, + .main-header:has(#sidemenu-source:checked) + main #sidebar #sidebar-source, + .main-header:has(#sidemenu-docs:checked) + main #sidebar #sidebar-docs, + .main-header:has(#sidemenu-meta:checked) + main #sidebar #sidebar-meta { + @apply block; + } + + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main .realm-content, + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .main-navigation { + @apply md:col-span-6; + } + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar, + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .sidemenu { + @apply md:col-span-4; + } + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar::before { + @apply absolute block content-[''] top-0 w-[50vw] h-full -left-7 bg-gray-100 z-min; + } + + /* chroma */ + main :is(.source-code) > pre { + @apply !bg-light overflow-scroll rounded py-4 md:py-8 px-1 md:px-3 font-mono text-100 md:text-200; + } + main .realm-content > pre a { + @apply hover:no-underline; + } + + main :is(.realm-content, .source-code) > pre .chroma-ln:target { + @apply !bg-transparent; + } + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target), + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover), + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target) .chroma-cl, + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl { + @apply !bg-gray-100 rounded; + } + main :is(.realm-content, .source-code) > pre .chroma-ln { + @apply scroll-mt-24; + } +} + +@layer utilities { + .italic-subtle { + font-style: oblique 10deg; + } + + .quotes { + @apply italic-subtle text-[#555] border-l-4 border-l-[#ccc] pl-4 my-6 [quotes:"“"_"”"_"‘"_"’"]; + } + + .quotes::before, + .quotes::after { + @apply [content:open-quote] text-600 text-gray-300 mr-1 [vertical-align:-0.4rem]; + } + + .quotes::after { + @apply [content:close-quote]; + } + + .text-stroke { + -webkit-text-stroke: currentColor; + -webkit-text-stroke-width: 0.6px; + } + + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} diff --git a/gno.land/pkg/gnoweb/frontend/css/tx.config.js b/gno.land/pkg/gnoweb/frontend/css/tx.config.js new file mode 100644 index 00000000000..198354c700e --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/css/tx.config.js @@ -0,0 +1,76 @@ +const pxToRem = (px) => px / 16; + +export default { + content: ["./components/**/*.{gohtml,ts}"], + theme: { + screens: { + xs: `${pxToRem(360)}rem`, + sm: `${pxToRem(480)}rem`, + md: `${pxToRem(640)}rem`, + lg: `${pxToRem(820)}rem`, + xl: `${pxToRem(1020)}rem`, + xxl: `${pxToRem(1366)}rem`, + max: `${pxToRem(1580)}rem`, + }, + zIndex: { + min: "-1", + 1: "1", + 2: "2", + 100: "100", + max: "9999", + }, + container: { + center: true, + padding: `${pxToRem(40)}rem`, + }, + borderRadius: { + sm: `${pxToRem(4)}rem`, + DEFAULT: `${pxToRem(6)}rem`, + }, + colors: { + light: "#FFFFFF", + gray: { + 50: "#F0F0F0", // Background color + 100: "#E2E2E2", // Title dark color + 200: "#BDBDBD", // Content dark color + 300: "#999999", // Muted color + 400: "#7C7C7C", // Border color + 600: "#54595D", // Content color + 800: "#131313", // Background dark color + 900: "#080809", // Title color + }, + green: { + 400: "#2D8D72", // Primary dark color + 600: "#226C57", // Primary light color + }, + transparent: "transparent", + current: "currentColor", + inherit: "inherit", + }, + fontFamily: { + mono: ["Roboto", 'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace;'], + interVar: [ + '"Inter var"', + 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', + ], + interNormal: [ + "Inter", + 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', + ], + }, + fontSize: { + 0: "0", + 50: `${pxToRem(12)}rem`, + 100: `${pxToRem(14)}rem`, + 200: `${pxToRem(16)}rem`, + 300: `${pxToRem(18)}rem`, + 400: `${pxToRem(20)}rem`, + 500: `${pxToRem(22)}rem`, + 600: `${pxToRem(24)}rem`, + 700: `${pxToRem(32)}rem`, + 800: `${pxToRem(38)}rem`, + 900: `${pxToRem(42)}rem`, + }, + }, + plugins: [], +}; diff --git a/gno.land/pkg/gnoweb/frontend/js/copy.ts b/gno.land/pkg/gnoweb/frontend/js/copy.ts new file mode 100644 index 00000000000..1ba725a9d3a --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/copy.ts @@ -0,0 +1,103 @@ +class Copy { + private DOM: { + el: HTMLElement | null; + }; + private static FEEDBACK_DELAY = 1500; + + private btnClicked: HTMLElement | null = null; + private btnClickedIcons: HTMLElement[] = []; + + private static SELECTORS = { + button: "[data-copy-btn]", + icon: `[data-copy-icon] > use`, + content: (id: string) => `[data-copy-content="${id}"]`, + }; + + constructor() { + this.DOM = { + el: document.querySelector("main"), + }; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("Copy: Main container not found."); + } + } + + private init(): void { + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el?.addEventListener("click", this.handleClick.bind(this)); + } + + private handleClick(event: Event): void { + const target = event.target as HTMLElement; + const button = target.closest(Copy.SELECTORS.button); + + if (!button) return; + + this.btnClicked = button; + this.btnClickedIcons = Array.from(button.querySelectorAll(Copy.SELECTORS.icon)); + + const contentId = button.getAttribute("data-copy-btn"); + if (!contentId) { + console.warn("Copy: No content ID found on the button."); + return; + } + + const codeBlock = this.DOM.el?.querySelector(Copy.SELECTORS.content(contentId)); + if (codeBlock) { + this.copyToClipboard(codeBlock); + } else { + console.warn(`Copy: No content found for ID "${contentId}".`); + } + } + + private sanitizeContent(codeBlock: HTMLElement): string { + const html = codeBlock.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g, ""); + + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = html; + + return tempDiv.textContent?.trim() || ""; + } + + private toggleIcons(): void { + this.btnClickedIcons.forEach((icon) => { + icon.classList.toggle("hidden"); + }); + } + + private showFeedback(): void { + if (!this.btnClicked) return; + + this.toggleIcons(); + window.setTimeout(() => { + this.toggleIcons(); + }, Copy.FEEDBACK_DELAY); + } + + private async copyToClipboard(codeBlock: HTMLElement): Promise { + const sanitizedText = this.sanitizeContent(codeBlock); + + if (!navigator.clipboard) { + console.error("Copy: Clipboard API is not supported in this browser."); + this.showFeedback(); + return; + } + + try { + await navigator.clipboard.writeText(sanitizedText); + console.info("Copy: Text copied successfully."); + this.showFeedback(); + } catch (err) { + console.error("Copy: Error while copying text.", err); + this.showFeedback(); + } + } +} + +export default () => new Copy(); diff --git a/gno.land/pkg/gnoweb/frontend/js/index.ts b/gno.land/pkg/gnoweb/frontend/js/index.ts new file mode 100644 index 00000000000..3927f794b94 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/index.ts @@ -0,0 +1,42 @@ +(() => { + interface Module { + selector: string; + path: string; + } + + const modules: Record = { + copy: { + selector: "[data-copy-btn]", + path: "/public/js/copy.js", + }, + help: { + selector: "#help", + path: "/public/js/realmhelp.js", + }, + searchBar: { + selector: "#header-searchbar", + path: "/public/js/searchbar.js", + }, + }; + + const loadModuleIfExists = async ({ selector, path }: Module): Promise => { + const element = document.querySelector(selector); + if (element) { + try { + const module = await import(path); + module.default(); + } catch (err) { + console.error(`Error while loading script ${path}:`, err); + } + } else { + console.warn(`Module not loaded: no element matches selector "${selector}"`); + } + }; + + const initModules = async (): Promise => { + const promises = Object.values(modules).map((module) => loadModuleIfExists(module)); + await Promise.all(promises); + }; + + document.addEventListener("DOMContentLoaded", initModules); +})(); diff --git a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts new file mode 100644 index 00000000000..980e9625875 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts @@ -0,0 +1,125 @@ +class Help { + private DOM: { + el: HTMLElement | null; + funcs: HTMLElement[]; + addressInput: HTMLInputElement | null; + cmdModeSelect: HTMLSelectElement | null; + }; + + private funcList: HelpFunc[]; + + private static SELECTORS = { + container: "#help", + func: "[data-func]", + addressInput: "[data-role='help-input-addr']", + cmdModeSelect: "[data-role='help-select-mode']", + }; + + constructor() { + this.DOM = { + el: document.querySelector(Help.SELECTORS.container), + funcs: [], + addressInput: null, + cmdModeSelect: null, + }; + + this.funcList = []; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("Help: Main container not found."); + } + } + + private init(): void { + const { el } = this.DOM; + if (!el) return; + + this.DOM.funcs = Array.from(el.querySelectorAll(Help.SELECTORS.func)); + this.DOM.addressInput = el.querySelector(Help.SELECTORS.addressInput); + this.DOM.cmdModeSelect = el.querySelector(Help.SELECTORS.cmdModeSelect); + + console.log(this.DOM); + this.funcList = this.DOM.funcs.map((funcEl) => new HelpFunc(funcEl)); + + this.bindEvents(); + } + + private bindEvents(): void { + const { addressInput, cmdModeSelect } = this.DOM; + + addressInput?.addEventListener("input", () => { + this.funcList.forEach((func) => func.updateAddr(addressInput.value)); + }); + + cmdModeSelect?.addEventListener("change", (e) => { + const target = e.target as HTMLSelectElement; + this.funcList.forEach((func) => func.updateMode(target.value)); + }); + } +} + +class HelpFunc { + private DOM: { + el: HTMLElement; + addrs: HTMLElement[]; + args: HTMLElement[]; + modes: HTMLElement[]; + }; + + private funcName: string | null; + + private static SELECTORS = { + address: "[data-role='help-code-address']", + args: "[data-role='help-code-args']", + mode: "[data-code-mode]", + paramInput: "[data-role='help-param-input']", + }; + + constructor(el: HTMLElement) { + this.DOM = { + el, + addrs: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.address)), + args: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.args)), + modes: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.mode)), + }; + + this.funcName = el.dataset.func || null; + + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el.addEventListener("input", (e) => { + const target = e.target as HTMLInputElement; + if (target.dataset.role === "help-param-input") { + this.updateArg(target.dataset.param || "", target.value); + } + }); + } + + public updateArg(paramName: string, paramValue: string): void { + this.DOM.args + .filter((arg) => arg.dataset.arg === paramName) + .forEach((arg) => { + arg.textContent = paramValue.trim() || ""; + }); + } + + public updateAddr(addr: string): void { + this.DOM.addrs.forEach((DOMaddr) => { + DOMaddr.textContent = addr.trim() || "ADDRESS"; + }); + } + + public updateMode(mode: string): void { + this.DOM.modes.forEach((cmd) => { + const isVisible = cmd.dataset.codeMode === mode; + cmd.className = isVisible ? "inline" : "hidden"; + cmd.dataset.copyContent = isVisible ? `help-cmd-${this.funcName}` : ""; + }); + } +} + +export default () => new Help(); diff --git a/gno.land/pkg/gnoweb/frontend/js/searchbar.ts b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts new file mode 100644 index 00000000000..6cca444aa0f --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts @@ -0,0 +1,74 @@ +class SearchBar { + private DOM: { + el: HTMLElement | null; + inputSearch: HTMLInputElement | null; + breadcrumb: HTMLElement | null; + }; + + private baseUrl: string; + + private static SELECTORS = { + container: "#header-searchbar", + inputSearch: "[data-role='header-input-search']", + breadcrumb: "[data-role='header-breadcrumb-search']", + }; + + constructor() { + this.DOM = { + el: document.querySelector(SearchBar.SELECTORS.container), + inputSearch: null, + breadcrumb: null, + }; + + this.baseUrl = window.location.origin; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("SearchBar: Main container not found."); + } + } + + private init(): void { + const { el } = this.DOM; + + this.DOM.inputSearch = el?.querySelector(SearchBar.SELECTORS.inputSearch) ?? null; + this.DOM.breadcrumb = el?.querySelector(SearchBar.SELECTORS.breadcrumb) ?? null; + + if (!this.DOM.inputSearch) { + console.warn("SearchBar: Input element for search not found."); + } + + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el?.addEventListener("submit", (e) => { + e.preventDefault(); + this.searchUrl(); + }); + } + + public searchUrl(): void { + const input = this.DOM.inputSearch?.value.trim(); + + if (input) { + let url = input; + + // Check if the URL has a proper scheme + if (!/^https?:\/\//i.test(url)) { + url = `${this.baseUrl}${url.startsWith("/") ? "" : "/"}${url}`; + } + + try { + window.location.href = new URL(url).href; + } catch (error) { + console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://."); + } + } else { + console.error("SearchBar: Please enter a URL to search."); + } + } +} + +export default () => new SearchBar(); diff --git a/gno.land/pkg/gnoweb/static/img/favicon.ico b/gno.land/pkg/gnoweb/frontend/static/favicon.ico similarity index 100% rename from gno.land/pkg/gnoweb/static/img/favicon.ico rename to gno.land/pkg/gnoweb/frontend/static/favicon.ico diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Inter.var.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Inter.var.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..365eedc50cd0f46ea35a3176335fc67b51123fb4 GIT binary patch literal 324864 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≫bY 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≥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

6&qaNDxLwZnq2 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

ycBey{rl;kDc{Ai+(=LzE1TWNYW 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

w%k_tC9o+J?jlEWgdzIe0b`MG#H=N&ZEd~ha9 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..53d081f3a538a63578c15a5cc11219b32e6d5795 GIT binary patch literal 12764 zcmV<2F(b}*Pew8T0RR9105RME4gdfE09y0_05O380RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg new file mode 100644 index 00000000000..30d2f3ef56a --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go deleted file mode 100644 index 40d027d84b9..00000000000 --- a/gno.land/pkg/gnoweb/gnoweb.go +++ /dev/null @@ -1,608 +0,0 @@ -package gnoweb - -import ( - "bytes" - "embed" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "log/slog" - "net/http" - "net/url" - "os" - "path/filepath" - "regexp" - "runtime" - "strings" - "time" - - "github.com/gnolang/gno/gnovm" - "github.com/gnolang/gno/tm2/pkg/amino" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gorilla/mux" - "github.com/gotuna/gotuna" - - // for static files - "github.com/gnolang/gno/gno.land/pkg/gnoweb/static" - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types - // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) -) - -const ( - qFileStr = "vm/qfile" - gnowebArgsSeparator = "$" - urlQuerySeparator = "?" -) - -//go:embed views/* -var defaultViewsFiles embed.FS - -type Config struct { - RemoteAddr string - CaptchaSite string - FaucetURL string - ViewsDir string - HelpChainID string - HelpRemote string - WithAnalytics bool - WithHTML bool -} - -func NewDefaultConfig() Config { - return Config{ - RemoteAddr: "127.0.0.1:26657", - CaptchaSite: "", - FaucetURL: "http://localhost:5050", - ViewsDir: "", - HelpChainID: "dev", - HelpRemote: "127.0.0.1:26657", - WithAnalytics: false, - WithHTML: false, - } -} - -func MakeApp(logger *slog.Logger, cfg Config) gotuna.App { - var viewFiles fs.FS - - // Get specific views directory if specified - if cfg.ViewsDir != "" { - viewFiles = os.DirFS(cfg.ViewsDir) - } else { - // Get embed views - var err error - viewFiles, err = fs.Sub(defaultViewsFiles, "views") - if err != nil { - panic("unable to get views directory from embed fs: " + err.Error()) - } - } - - app := gotuna.App{ - ViewFiles: viewFiles, - Router: gotuna.NewMuxRouter(), - Static: static.EmbeddedStatic, - } - - for from, to := range Aliases { - app.Router.Handle(from, handlerRealmAlias(logger, app, &cfg, to)) - } - - for from, to := range Redirects { - app.Router.Handle(from, handlerRedirect(logger, app, &cfg, to)) - } - // realm routes - // NOTE: see rePathPart. - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}/{filename:(?:(?:.*\\.(?:gno|md|txt|mod)$)|(?:LICENSE$))?}", handlerRealmFile(logger, app, &cfg)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}{args:(?:\\$.*)?}", handlerRealmMain(logger, app, &cfg)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}:{querystr:[^$]*}{args:(?:\\$.*)?}", handlerRealmRender(logger, app, &cfg)) - app.Router.Handle("/p/{filepath:.*}", handlerPackageFile(logger, app, &cfg)) - - // other - app.Router.Handle("/faucet", handlerFaucet(logger, app, &cfg)) - app.Router.Handle("/static/{path:.+}", handlerStaticFile(logger, app, &cfg)) - app.Router.Handle("/favicon.ico", handlerFavicon(logger, app, &cfg)) - - // api - app.Router.Handle("/status.json", handlerStatusJSON(logger, app, &cfg)) - - app.Router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.RequestURI - handleNotFound(logger, app, &cfg, path, w, r) - }) - return app -} - -var ( - inlineCodePattern = regexp.MustCompile("`[^`]*`") - htmlTagPattern = regexp.MustCompile(`<\/?\w+[^>]*?>`) -) - -func sanitizeContent(cfg *Config, content string) string { - if cfg.WithHTML { - return content - } - - placeholders := map[string]string{} - contentWithPlaceholders := inlineCodePattern.ReplaceAllStringFunc(content, func(match string) string { - placeholder := fmt.Sprintf("__GNOMDCODE_%d__", len(placeholders)) - placeholders[placeholder] = match - return placeholder - }) - - sanitizedContent := htmlTagPattern.ReplaceAllString(contentWithPlaceholders, "") - - if len(placeholders) > 0 { - for placeholder, code := range placeholders { - sanitizedContent = strings.ReplaceAll(sanitizedContent, placeholder, code) - } - } - - return sanitizedContent -} - -// handlerRealmAlias is used to render official pages from realms. -// url is intended to be shorter. -// UX is intended to be more minimalistic. -// A link to the realm realm is added. -func handlerRealmAlias(logger *slog.Logger, app gotuna.App, cfg *Config, rlmpath string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rlmfullpath := "gno.land" + rlmpath - querystr := "" // XXX: "?gnoweb-alias=1" - parts := strings.Split(rlmpath, ":") - switch len(parts) { - case 1: // continue - case 2: // r/realm:querystr - rlmfullpath = "gno.land" + parts[0] - querystr = parts[1] + querystr - default: - panic("should not happen") - } - rlmname := strings.TrimPrefix(rlmfullpath, "gno.land/r/") - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s:%s", rlmfullpath, querystr)) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, fmt.Errorf("gnoweb failed to query gnoland: %w", err)) - return - } - - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), - Text: part, - }) - } - - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", sanitizeContent(cfg, string(res.Data))) - tmpl.Set("Config", cfg) - tmpl.Set("IsAlias", true) - tmpl.Render(w, r, "realm_render.html", "funcs.html") - }) -} - -func handlerFaucet(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - app.NewTemplatingEngine(). - Set("Config", cfg). - Render(w, r, "faucet.html", "funcs.html") - }) -} - -func handlerStatusJSON(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - startedAt := time.Now() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ret struct { - Gnoland struct { - Connected bool `json:"connected"` - Error *string `json:"error,omitempty"` - Height *int64 `json:"height,omitempty"` - // processed txs - // active connections - - Version *string `json:"version,omitempty"` - // Uptime *float64 `json:"uptime-seconds,omitempty"` - // Goarch *string `json:"goarch,omitempty"` - // Goos *string `json:"goos,omitempty"` - // GoVersion *string `json:"go-version,omitempty"` - // NumCPU *int `json:"num_cpu,omitempty"` - } `json:"gnoland"` - Website struct { - // Version string `json:"version"` - Uptime float64 `json:"uptime-seconds"` - Goarch string `json:"goarch"` - Goos string `json:"goos"` - GoVersion string `json:"go-version"` - NumCPU int `json:"num_cpu"` - } `json:"website"` - } - ret.Website.Uptime = time.Since(startedAt).Seconds() - ret.Website.Goarch = runtime.GOARCH - ret.Website.Goos = runtime.GOOS - ret.Website.NumCPU = runtime.NumCPU() - ret.Website.GoVersion = runtime.Version() - - ret.Gnoland.Connected = true - res, err := makeRequest(logger, cfg, ".app/version", []byte{}) - if err != nil { - ret.Gnoland.Connected = false - errmsg := err.Error() - ret.Gnoland.Error = &errmsg - } else { - version := string(res.Value) - ret.Gnoland.Version = &version - ret.Gnoland.Height = &res.Height - } - - out, _ := json.MarshalIndent(ret, "", " ") - w.Header().Set("Content-Type", "application/json") - w.Write(out) - }) -} - -func handlerRedirect(logger *slog.Logger, app gotuna.App, cfg *Config, to string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, to, http.StatusFound) - tmpl := app.NewTemplatingEngine() - tmpl.Set("To", to) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "redirect.html", "funcs.html") - }) -} - -func handlerRealmMain(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - args, err := parseGnowebArgs(r.RequestURI) - if err != nil { - writeError(logger, w, err) - return - } - - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - - logger.Info("handling", "name", rlmname, "path", rlmpath) - if args.Has("help") { - // Render function helper. - funcName := args.Get("func") - qpath := "vm/qfuncs" - data := []byte(rlmpath) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, fmt.Errorf("request failed: %w", err)) - return - } - var fsigs vm.FunctionSignatures - amino.MustUnmarshalJSON(res.Data, &fsigs) - // Fill fsigs with query parameters. - for i := range fsigs { - fsig := &(fsigs[i]) - for j := range fsig.Params { - param := &(fsig.Params[j]) - value := args.Get(param.Name) - param.Value = value - } - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("FuncName", funcName) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("DirPath", pathOf(rlmpath)) - tmpl.Set("FunctionSignatures", fsigs) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "realm_help.html", "funcs.html") - } else { - // Ensure realm exists. TODO optimize. - qpath := qFileStr - data := []byte(rlmpath) - _, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, errors.New("error querying realm package")) - return - } - // Render blank query path, /r/REALM:. - handleRealmRender(logger, app, cfg, w, r) - } - }) -} - -type pathLink struct { - URL string - Text string -} - -func handlerRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRealmRender(logger, app, cfg, w, r) - }) -} - -func handleRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request) { - gnowebArgs, err := parseGnowebArgs(r.RequestURI) - if err != nil { - writeError(logger, w, err) - return - } - - queryArgs, err := parseQueryArgs(r.RequestURI) - if err != nil { - writeError(logger, w, err) - return - } - - var urlQuery, gnowebQuery string - if len(queryArgs) > 0 { - urlQuery = urlQuerySeparator + queryArgs.Encode() - } - if len(gnowebArgs) > 0 { - gnowebQuery = gnowebArgsSeparator + gnowebArgs.Encode() - } - - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - querystr := vars["querystr"] - if r.URL.Path == "/r/"+rlmname+":" { - // Redirect to /r/REALM if querypath is empty. - http.Redirect(w, r, "/r/"+rlmname+urlQuery+gnowebQuery, http.StatusFound) - return - } - - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s:%s", rlmpath, querystr+urlQuery)) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - // XXX hack - if strings.Contains(err.Error(), "Render not declared") { - res = &abci.ResponseQuery{} - res.Data = []byte("realm package has no Render() function") - } else { - writeError(logger, w, err) - return - } - } - - dirdata := []byte(rlmpath) - dirres, err := makeRequest(logger, cfg, qFileStr, dirdata) - if err != nil { - writeError(logger, w, err) - return - } - hasReadme := bytes.Contains(append(dirres.Data, '\n'), []byte("README.md\n")) - - // linkify querystr. - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - rlmpath := strings.Join(queryParts[:i+1], "/") - - // Add URL query arguments to the last breadcrumb item's URL - if i+1 == len(queryParts) { - rlmpath = rlmpath + urlQuery + gnowebQuery - } - - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + rlmpath, - Text: part, - }) - } - - // Render template. - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", sanitizeContent(cfg, string(res.Data))) - tmpl.Set("Config", cfg) - tmpl.Set("HasReadme", hasReadme) - tmpl.Render(w, r, "realm_render.html", "funcs.html") -} - -func handlerRealmFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - diruri := "gno.land/r/" + vars["rlmname"] - filename := vars["filename"] - renderPackageFile(logger, app, cfg, w, r, diruri, filename) - }) -} - -func handlerPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - pkgpath := "gno.land/p/" + vars["filepath"] - diruri, filename := gnovm.SplitFilepath(pkgpath) - if filename == "" && diruri == pkgpath { - // redirect to diruri + "/" - http.Redirect(w, r, "/p/"+vars["filepath"]+"/", http.StatusFound) - return - } - renderPackageFile(logger, app, cfg, w, r, diruri, filename) - }) -} - -func renderPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request, diruri string, filename string) { - if filename == "" { - // Request is for a folder. - qpath := qFileStr - data := []byte(diruri) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, err) - return - } - files := strings.Split(string(res.Data), "\n") - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("Files", files) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "package_dir.html", "funcs.html") - } else { - // Request is for a file. - filepath := diruri + "/" + filename - qpath := qFileStr - data := []byte(filepath) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, err) - return - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("FileName", filename) - tmpl.Set("FileContents", string(res.Data)) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "package_file.html", "funcs.html") - } -} - -func makeRequest(log *slog.Logger, cfg *Config, qpath string, data []byte) (res *abci.ResponseQuery, err error) { - opts2 := client.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX - } - remote := cfg.RemoteAddr - cli, err := client.NewHTTPClient(remote) - if err != nil { - return nil, fmt.Errorf("unable to create HTTP client, %w", err) - } - - qres, err := cli.ABCIQueryWithOptions( - qpath, data, opts2) - if err != nil { - log.Error("request error", "path", qpath, "error", err) - return nil, fmt.Errorf("unable to query path %q: %w", qpath, err) - } - if qres.Response.Error != nil { - log.Error("response error", "path", qpath, "log", qres.Response.Log) - return nil, qres.Response.Error - } - return &qres.Response, nil -} - -func handlerStaticFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - fs := http.FS(app.Static) - fileapp := http.StripPrefix("/static", http.FileServer(fs)) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - fpath := filepath.Clean(vars["path"]) - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - stat, err := f.Stat() - if err != nil || stat.IsDir() { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - - // TODO: ModTime doesn't work for embed? - // w.Header().Set("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano())) - // w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s", "31536000")) - fileapp.ServeHTTP(w, r) - }) -} - -func handlerFavicon(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - fs := http.FS(app.Static) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fpath := "img/favicon.ico" - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - w.Header().Set("Content-Type", "image/x-icon") - w.Header().Set("Cache-Control", "public, max-age=604800") // 7d - io.Copy(w, f) - }) -} - -func handleNotFound(logger *slog.Logger, app gotuna.App, cfg *Config, path string, w http.ResponseWriter, r *http.Request) { - // decode path for non-ascii characters - decodedPath, err := url.PathUnescape(path) - if err != nil { - logger.Error("failed to decode path", "error", err) - decodedPath = path - } - w.WriteHeader(http.StatusNotFound) - app.NewTemplatingEngine(). - Set("title", "Not found"). - Set("path", decodedPath). - Set("Config", cfg). - Render(w, r, "404.html", "funcs.html") -} - -func writeError(logger *slog.Logger, w http.ResponseWriter, err error) { - if details := errors.Unwrap(err); details != nil { - logger.Error("handler", "error", err, "details", details) - } else { - logger.Error("handler", "error", err) - } - - // XXX: writeError should return an error page template. - w.WriteHeader(500) - w.Write([]byte(err.Error())) -} - -func pathOf(diruri string) string { - parts := strings.Split(diruri, "/") - if parts[0] == "gno.land" { - return "/" + strings.Join(parts[1:], "/") - } - - panic(fmt.Sprintf("invalid dir-URI %q", diruri)) -} - -// parseQueryArgs parses URL query arguments that are not specific to gnoweb. -// These are the standard arguments that comes after the "?" symbol and before -// the special "$" symbol. The "$" symbol can be used within public query -// arguments by using its encoded representation "%24". -func parseQueryArgs(rawURL string) (url.Values, error) { - if i := strings.Index(rawURL, gnowebArgsSeparator); i != -1 { - rawURL = rawURL[:i] - } - - u, err := url.Parse(rawURL) - if err != nil { - return url.Values{}, fmt.Errorf("invalid query arguments: %w", err) - } - return u.Query(), nil -} - -// parseGnowebArgs parses URL query arguments that are specific to gnoweb. -// These arguments are indicated by using the "$" symbol followed by a query -// string with the arguments. -func parseGnowebArgs(rawURL string) (url.Values, error) { - i := strings.Index(rawURL, gnowebArgsSeparator) - if i == -1 { - return url.Values{}, nil - } - - values, err := url.ParseQuery(rawURL[i+1:]) - if err != nil { - return url.Values{}, fmt.Errorf("invalid gnoweb arguments: %w", err) - } - return values, nil -} diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go new file mode 100644 index 00000000000..b3a9fcd143c --- /dev/null +++ b/gno.land/pkg/gnoweb/handler.go @@ -0,0 +1,381 @@ +package gnoweb + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "io" + "log/slog" + "net/http" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types +) + +const DefaultChainDomain = "gno.land" + +type StaticMetadata struct { + AssetsPath string + ChromaPath string + RemoteHelp string + ChainId string + Analytics bool +} + +type WebHandlerConfig struct { + Meta StaticMetadata + RenderClient *WebClient + Formatter Formatter +} + +type WebHandler struct { + formatter Formatter + + logger *slog.Logger + static StaticMetadata + webcli *WebClient +} + +func NewWebHandler(logger *slog.Logger, cfg WebHandlerConfig) *WebHandler { + if cfg.RenderClient == nil { + logger.Error("no renderer has been defined") + } + + return &WebHandler{ + formatter: cfg.Formatter, + webcli: cfg.RenderClient, + logger: logger, + static: cfg.Meta, + } +} + +func (h *WebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.logger.Debug("receiving request", "method", r.Method, "path", r.URL.Path) + + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + h.Get(w, r) +} + +func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) { + var body bytes.Buffer + + start := time.Now() + defer func() { + h.logger.Debug("request completed", + "url", r.URL.String(), + "elapsed", time.Since(start).String()) + }() + + var indexData components.IndexData + indexData.HeadData.AssetsPath = h.static.AssetsPath + indexData.HeadData.ChromaPath = h.static.ChromaPath + indexData.FooterData.Analytics = h.static.Analytics + indexData.FooterData.AssetsPath = h.static.AssetsPath + + // Render the page body into the buffer + var status int + gnourl, err := ParseGnoURL(r.URL) + if err != nil { + h.logger.Warn("page not found", "path", r.URL.Path, "err", err) + status, err = http.StatusNotFound, components.RenderStatusComponent(&body, "page not found") + } else { + // TODO: real data (title & description) + indexData.HeadData.Title = "gno.land - " + gnourl.Path + + // Header + indexData.HeaderData.RealmPath = gnourl.Path + indexData.HeaderData.Breadcrumb.Parts = generateBreadcrumbPaths(gnourl.Path) + indexData.HeaderData.WebQuery = gnourl.WebQuery + + // Render + switch gnourl.Kind() { + case KindRealm, KindPure: + status, err = h.renderPackage(&body, gnourl) + default: + h.logger.Debug("invalid page kind", "kind", gnourl.Kind) + status, err = http.StatusNotFound, components.RenderStatusComponent(&body, "page not found") + } + } + + if err != nil { + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(status) + + // NOTE: HTML escaping should have already been done by markdown rendering package + indexData.Body = template.HTML(body.String()) //nolint:gosec + + // Render the final page with the rendered body + if err = components.RenderIndexComponent(w, indexData); err != nil { + h.logger.Error("failed to render index component", "err", err) + } + + return +} + +func (h *WebHandler) renderPackage(w io.Writer, gnourl *GnoURL) (status int, err error) { + h.logger.Info("component render", "path", gnourl.Path, "args", gnourl.Args) + + kind := gnourl.Kind() + + // Display realm help page? + if kind == KindRealm && gnourl.WebQuery.Has("help") { + return h.renderRealmHelp(w, gnourl) + } + + // Display package source page? + switch { + case gnourl.WebQuery.Has("source"): + return h.renderRealmSource(w, gnourl) + case kind == KindPure, + strings.HasSuffix(gnourl.Path, "/"), + isFile(gnourl.Path): + i := strings.LastIndexByte(gnourl.Path, '/') + if i < 0 { + return http.StatusInternalServerError, fmt.Errorf("unable to get ending slash for %q", gnourl.Path) + } + + // Fill webquery with file infos + gnourl.WebQuery.Set("source", "") // set source + + file := gnourl.Path[i+1:] + if file == "" { + return h.renderRealmDirectory(w, gnourl) + } + + gnourl.WebQuery.Set("file", file) + gnourl.Path = gnourl.Path[:i] + + return h.renderRealmSource(w, gnourl) + } + + // Render content into the content buffer + var content bytes.Buffer + meta, err := h.webcli.Render(&content, gnourl.Path, gnourl.EncodeArgs()) + if err != nil { + if errors.Is(err, vm.InvalidPkgPathError{}) { + return http.StatusNotFound, components.RenderStatusComponent(w, "not found") + } + + h.logger.Error("unable to render markdown", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + err = components.RenderRealmComponent(w, components.RealmData{ + TocItems: &components.RealmTOCData{ + Items: meta.Items, + }, + // NOTE: `content` should have already been escaped by + Content: template.HTML(content.String()), //nolint:gosec + }) + if err != nil { + h.logger.Error("unable to render template", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + // Write the rendered content to the response writer + return http.StatusOK, nil +} + +func (h *WebHandler) renderRealmHelp(w io.Writer, gnourl *GnoURL) (status int, err error) { + fsigs, err := h.webcli.Functions(gnourl.Path) + if err != nil { + h.logger.Error("unable to fetch path functions", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + var selArgs map[string]string + var selFn string + if selFn = gnourl.WebQuery.Get("func"); selFn != "" { + for _, fn := range fsigs { + if selFn != fn.FuncName { + continue + } + + selArgs = make(map[string]string) + for _, param := range fn.Params { + selArgs[param.Name] = gnourl.WebQuery.Get(param.Name) + } + + fsigs = []vm.FunctionSignature{fn} + break + } + } + + // Catch last name of the path + // XXX: we should probably add a helper within the template + realmName := filepath.Base(gnourl.Path) + err = components.RenderHelpComponent(w, components.HelpData{ + SelectedFunc: selFn, + SelectedArgs: selArgs, + RealmName: realmName, + ChainId: h.static.ChainId, + // TODO: get chain domain and use that. + PkgPath: filepath.Join(DefaultChainDomain, gnourl.Path), + Remote: h.static.RemoteHelp, + Functions: fsigs, + }) + if err != nil { + h.logger.Error("unable to render helper", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +func (h *WebHandler) renderRealmSource(w io.Writer, gnourl *GnoURL) (status int, err error) { + pkgPath := gnourl.Path + + files, err := h.webcli.Sources(pkgPath) + if err != nil { + h.logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + if len(files) == 0 { + h.logger.Debug("no files available", "path", gnourl.Path) + return http.StatusOK, components.RenderStatusComponent(w, "no files available") + } + + var fileName string + file := gnourl.WebQuery.Get("file") + if file == "" { + fileName = files[0] + } else if slices.Contains(files, file) { + fileName = file + } else { + h.logger.Error("unable to render source", "file", file, "err", "file does not exist") + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + source, err := h.webcli.SourceFile(pkgPath, fileName) + if err != nil { + h.logger.Error("unable to get source file", "file", fileName, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + // XXX: we should either do this on the front or in the markdown parsing side + fileLines := strings.Count(string(source), "\n") + fileSizeKb := float64(len(source)) / 1024.0 + fileSizeStr := fmt.Sprintf("%.2f Kb", fileSizeKb) + + // Highlight code source + hsource, err := h.highlightSource(fileName, source) + if err != nil { + h.logger.Error("unable to highlight source file", "file", fileName, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + err = components.RenderSourceComponent(w, components.SourceData{ + PkgPath: gnourl.Path, + Files: files, + FileName: fileName, + FileCounter: len(files), + FileLines: fileLines, + FileSize: fileSizeStr, + FileSource: template.HTML(hsource), //nolint:gosec + }) + if err != nil { + h.logger.Error("unable to render helper", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +func (h *WebHandler) renderRealmDirectory(w io.Writer, gnourl *GnoURL) (status int, err error) { + pkgPath := gnourl.Path + + files, err := h.webcli.Sources(pkgPath) + if err != nil { + h.logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + if len(files) == 0 { + h.logger.Debug("no files available", "path", gnourl.Path) + return http.StatusOK, components.RenderStatusComponent(w, "no files available") + } + + err = components.RenderDirectoryComponent(w, components.DirData{ + PkgPath: gnourl.Path, + Files: files, + FileCounter: len(files), + }) + if err != nil { + h.logger.Error("unable to render directory", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +func (h *WebHandler) highlightSource(fileName string, src []byte) ([]byte, error) { + var lexer chroma.Lexer + + switch strings.ToLower(filepath.Ext(fileName)) { + case ".gno": + lexer = lexers.Get("go") + case ".md": + lexer = lexers.Get("markdown") + case ".mod": + lexer = lexers.Get("gomod") + default: + lexer = lexers.Get("txt") // file kind not supported, fallback on `.txt` + } + + if lexer == nil { + return nil, fmt.Errorf("unsupported lexer for file %q", fileName) + } + + iterator, err := lexer.Tokenise(nil, string(src)) + if err != nil { + h.logger.Error("unable to ", "fileName", fileName, "err", err) + } + + var buff bytes.Buffer + if err := h.formatter.Format(&buff, iterator); err != nil { + return nil, fmt.Errorf("unable to format source file %q: %w", fileName, err) + } + + return buff.Bytes(), nil +} + +func generateBreadcrumbPaths(path string) []components.BreadcrumbPart { + split := strings.Split(path, "/") + parts := []components.BreadcrumbPart{} + + var name string + for i := range split { + if name = split[i]; name == "" { + continue + } + + parts = append(parts, components.BreadcrumbPart{ + Name: name, + Path: strings.Join(split[:i+1], "/"), + }) + } + + return parts +} + +// IsFile checks if the last element of the path is a file (has an extension) +func isFile(path string) bool { + base := filepath.Base(path) + ext := filepath.Ext(base) + return ext != "" +} diff --git a/gno.land/pkg/gnoweb/markdown/highlighting.go b/gno.land/pkg/gnoweb/markdown/highlighting.go new file mode 100644 index 00000000000..51c66674df1 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/highlighting.go @@ -0,0 +1,588 @@ +// This file was copied from https://github.com/yuin/goldmark-highlighting +// +// package highlighting is an extension for the goldmark(http://github.com/yuin/goldmark). +// +// This extension adds syntax-highlighting to the fenced code blocks using +// chroma(https://github.com/alecthomas/chroma). +package markdown + +import ( + "bytes" + "io" + "strconv" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/alecthomas/chroma/v2/styles" +) + +// ImmutableAttributes is a read-only interface for ast.Attributes. +type ImmutableAttributes interface { + // Get returns (value, true) if an attribute associated with given + // name exists, otherwise (nil, false) + Get(name []byte) (interface{}, bool) + + // GetString returns (value, true) if an attribute associated with given + // name exists, otherwise (nil, false) + GetString(name string) (interface{}, bool) + + // All returns all attributes. + All() []ast.Attribute +} + +type immutableAttributes struct { + n ast.Node +} + +func (a *immutableAttributes) Get(name []byte) (interface{}, bool) { + return a.n.Attribute(name) +} + +func (a *immutableAttributes) GetString(name string) (interface{}, bool) { + return a.n.AttributeString(name) +} + +func (a *immutableAttributes) All() []ast.Attribute { + if a.n.Attributes() == nil { + return []ast.Attribute{} + } + return a.n.Attributes() +} + +// CodeBlockContext holds contextual information of code highlighting. +type CodeBlockContext interface { + // Language returns (language, true) if specified, otherwise (nil, false). + Language() ([]byte, bool) + + // Highlighted returns true if this code block can be highlighted, otherwise false. + Highlighted() bool + + // Attributes return attributes of the code block. + Attributes() ImmutableAttributes +} + +type codeBlockContext struct { + language []byte + highlighted bool + attributes ImmutableAttributes +} + +func newCodeBlockContext(language []byte, highlighted bool, attrs ImmutableAttributes) CodeBlockContext { + return &codeBlockContext{ + language: language, + highlighted: highlighted, + attributes: attrs, + } +} + +func (c *codeBlockContext) Language() ([]byte, bool) { + if c.language != nil { + return c.language, true + } + return nil, false +} + +func (c *codeBlockContext) Highlighted() bool { + return c.highlighted +} + +func (c *codeBlockContext) Attributes() ImmutableAttributes { + return c.attributes +} + +// WrapperRenderer renders wrapper elements like div, pre, etc. +type WrapperRenderer func(w util.BufWriter, context CodeBlockContext, entering bool) + +// CodeBlockOptions creates Chroma options per code block. +type CodeBlockOptions func(ctx CodeBlockContext) []chromahtml.Option + +// Config struct holds options for the extension. +type Config struct { + html.Config + + // Style is a highlighting style. + // Supported styles are defined under https://github.com/alecthomas/chroma/tree/master/formatters. + Style string + + // Pass in a custom Chroma style. If this is not nil, the Style string will be ignored + CustomStyle *chroma.Style + + // If set, will try to guess language if none provided. + // If the guessing fails, we will fall back to a text lexer. + // Note that while Chroma's API supports language guessing, the implementation + // is not there yet, so you will currently always get the basic text lexer. + GuessLanguage bool + + // FormatOptions is a option related to output formats. + // See https://github.com/alecthomas/chroma#the-html-formatter for details. + FormatOptions []chromahtml.Option + + // CSSWriter is an io.Writer that will be used as CSS data output buffer. + // If WithClasses() is enabled, you can get CSS data corresponds to the style. + CSSWriter io.Writer + + // CodeBlockOptions allows set Chroma options per code block. + CodeBlockOptions CodeBlockOptions + + // WrapperRenderer allows you to change wrapper elements. + WrapperRenderer WrapperRenderer +} + +// NewConfig returns a new Config with defaults. +func NewConfig() Config { + return Config{ + Config: html.NewConfig(), + Style: "github", + FormatOptions: []chromahtml.Option{}, + CSSWriter: nil, + WrapperRenderer: nil, + CodeBlockOptions: nil, + } +} + +// SetOption implements renderer.SetOptioner. +func (c *Config) SetOption(name renderer.OptionName, value interface{}) { + switch name { + case optStyle: + c.Style = value.(string) + case optCustomStyle: + c.CustomStyle = value.(*chroma.Style) + case optFormatOptions: + if value != nil { + c.FormatOptions = value.([]chromahtml.Option) + } + case optCSSWriter: + c.CSSWriter = value.(io.Writer) + case optWrapperRenderer: + c.WrapperRenderer = value.(WrapperRenderer) + case optCodeBlockOptions: + c.CodeBlockOptions = value.(CodeBlockOptions) + case optGuessLanguage: + c.GuessLanguage = value.(bool) + default: + c.Config.SetOption(name, value) + } +} + +// Option interface is a functional option interface for the extension. +type Option interface { + renderer.Option + // SetHighlightingOption sets given option to the extension. + SetHighlightingOption(*Config) +} + +type withHTMLOptions struct { + value []html.Option +} + +func (o *withHTMLOptions) SetConfig(c *renderer.Config) { + if o.value != nil { + for _, v := range o.value { + v.(renderer.Option).SetConfig(c) + } + } +} + +func (o *withHTMLOptions) SetHighlightingOption(c *Config) { + if o.value != nil { + for _, v := range o.value { + v.SetHTMLOption(&c.Config) + } + } +} + +// WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options. +func WithHTMLOptions(opts ...html.Option) Option { + return &withHTMLOptions{opts} +} + +const ( + optStyle renderer.OptionName = "HighlightingStyle" + optCustomStyle renderer.OptionName = "HighlightingCustomStyle" +) + +var highlightLinesAttrName = []byte("hl_lines") + +var ( + styleAttrName = []byte("hl_style") + nohlAttrName = []byte("nohl") + linenosAttrName = []byte("linenos") + linenosTableAttrValue = []byte("table") + linenosInlineAttrValue = []byte("inline") + linenostartAttrName = []byte("linenostart") +) + +type withStyle struct { + value string +} + +func (o *withStyle) SetConfig(c *renderer.Config) { + c.Options[optStyle] = o.value +} + +func (o *withStyle) SetHighlightingOption(c *Config) { + c.Style = o.value +} + +// WithStyle is a functional option that changes highlighting style. +func WithStyle(style string) Option { + return &withStyle{style} +} + +type withCustomStyle struct { + value *chroma.Style +} + +func (o *withCustomStyle) SetConfig(c *renderer.Config) { + c.Options[optCustomStyle] = o.value +} + +func (o *withCustomStyle) SetHighlightingOption(c *Config) { + c.CustomStyle = o.value +} + +// WithStyle is a functional option that changes highlighting style. +func WithCustomStyle(style *chroma.Style) Option { + return &withCustomStyle{style} +} + +const optCSSWriter renderer.OptionName = "HighlightingCSSWriter" + +type withCSSWriter struct { + value io.Writer +} + +func (o *withCSSWriter) SetConfig(c *renderer.Config) { + c.Options[optCSSWriter] = o.value +} + +func (o *withCSSWriter) SetHighlightingOption(c *Config) { + c.CSSWriter = o.value +} + +// WithCSSWriter is a functional option that sets io.Writer for CSS data. +func WithCSSWriter(w io.Writer) Option { + return &withCSSWriter{w} +} + +const optGuessLanguage renderer.OptionName = "HighlightingGuessLanguage" + +type withGuessLanguage struct { + value bool +} + +func (o *withGuessLanguage) SetConfig(c *renderer.Config) { + c.Options[optGuessLanguage] = o.value +} + +func (o *withGuessLanguage) SetHighlightingOption(c *Config) { + c.GuessLanguage = o.value +} + +// WithGuessLanguage is a functional option that toggles language guessing +// if none provided. +func WithGuessLanguage(b bool) Option { + return &withGuessLanguage{value: b} +} + +const optWrapperRenderer renderer.OptionName = "HighlightingWrapperRenderer" + +type withWrapperRenderer struct { + value WrapperRenderer +} + +func (o *withWrapperRenderer) SetConfig(c *renderer.Config) { + c.Options[optWrapperRenderer] = o.value +} + +func (o *withWrapperRenderer) SetHighlightingOption(c *Config) { + c.WrapperRenderer = o.value +} + +// WithWrapperRenderer is a functional option that sets WrapperRenderer that +// renders wrapper elements like div, pre, etc. +func WithWrapperRenderer(w WrapperRenderer) Option { + return &withWrapperRenderer{w} +} + +const optCodeBlockOptions renderer.OptionName = "HighlightingCodeBlockOptions" + +type withCodeBlockOptions struct { + value CodeBlockOptions +} + +func (o *withCodeBlockOptions) SetConfig(c *renderer.Config) { + c.Options[optCodeBlockOptions] = o.value +} + +func (o *withCodeBlockOptions) SetHighlightingOption(c *Config) { + c.CodeBlockOptions = o.value +} + +// WithCodeBlockOptions is a functional option that sets CodeBlockOptions that +// allows setting Chroma options per code block. +func WithCodeBlockOptions(c CodeBlockOptions) Option { + return &withCodeBlockOptions{value: c} +} + +const optFormatOptions renderer.OptionName = "HighlightingFormatOptions" + +type withFormatOptions struct { + value []chromahtml.Option +} + +func (o *withFormatOptions) SetConfig(c *renderer.Config) { + if _, ok := c.Options[optFormatOptions]; !ok { + c.Options[optFormatOptions] = []chromahtml.Option{} + } + c.Options[optFormatOptions] = append(c.Options[optFormatOptions].([]chromahtml.Option), o.value...) +} + +func (o *withFormatOptions) SetHighlightingOption(c *Config) { + c.FormatOptions = append(c.FormatOptions, o.value...) +} + +// WithFormatOptions is a functional option that wraps chroma HTML formatter options. +func WithFormatOptions(opts ...chromahtml.Option) Option { + return &withFormatOptions{opts} +} + +// HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension. +type HTMLRenderer struct { + Config +} + +// NewHTMLRenderer builds a new HTMLRenderer with given options and returns it. +func NewHTMLRenderer(opts ...Option) renderer.NodeRenderer { + r := &HTMLRenderer{ + Config: NewConfig(), + } + for _, opt := range opts { + opt.SetHighlightingOption(&r.Config) + } + return r +} + +// RegisterFuncs implements NodeRenderer.RegisterFuncs. +func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) +} + +func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes { + if node.Attributes() != nil { + return &immutableAttributes{node} + } + if infostr != nil { + attrStartIdx := -1 + + for idx, char := range infostr { + if char == '{' { + attrStartIdx = idx + break + } + } + if attrStartIdx > 0 { + n := ast.NewTextBlock() // dummy node for storing attributes + attrStr := infostr[attrStartIdx:] + if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { + for _, attr := range attrs { + n.SetAttribute(attr.Name, attr.Value) + } + return &immutableAttributes{n} + } + } + } + return nil +} + +func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.FencedCodeBlock) + if !entering { + return ast.WalkContinue, nil + } + language := n.Language(source) + + chromaFormatterOptions := make([]chromahtml.Option, 0, len(r.FormatOptions)) + for _, opt := range r.FormatOptions { + chromaFormatterOptions = append(chromaFormatterOptions, opt) + } + + style := r.CustomStyle + if style == nil { + style = styles.Get(r.Style) + } + nohl := false + + var info []byte + if n.Info != nil { + info = n.Info.Segment.Value(source) + } + attrs := getAttributes(n, info) + if attrs != nil { + baseLineNumber := 1 + if linenostartAttr, ok := attrs.Get(linenostartAttrName); ok { + if linenostart, ok := linenostartAttr.(float64); ok { + baseLineNumber = int(linenostart) + chromaFormatterOptions = append( + chromaFormatterOptions, chromahtml.BaseLineNumber(baseLineNumber), + ) + } + } + if linesAttr, hasLinesAttr := attrs.Get(highlightLinesAttrName); hasLinesAttr { + if lines, ok := linesAttr.([]interface{}); ok { + var hlRanges [][2]int + for _, l := range lines { + if ln, ok := l.(float64); ok { + hlRanges = append(hlRanges, [2]int{int(ln) + baseLineNumber - 1, int(ln) + baseLineNumber - 1}) + } + if rng, ok := l.([]uint8); ok { + slices := strings.Split(string(rng), "-") + lhs, err := strconv.Atoi(slices[0]) + if err != nil { + continue + } + rhs := lhs + if len(slices) > 1 { + rhs, err = strconv.Atoi(slices[1]) + if err != nil { + continue + } + } + hlRanges = append(hlRanges, [2]int{lhs + baseLineNumber - 1, rhs + baseLineNumber - 1}) + } + } + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.HighlightLines(hlRanges)) + } + } + if styleAttr, hasStyleAttr := attrs.Get(styleAttrName); hasStyleAttr { + if st, ok := styleAttr.([]uint8); ok { + styleStr := string(st) + style = styles.Get(styleStr) + } + } + if _, hasNohlAttr := attrs.Get(nohlAttrName); hasNohlAttr { + nohl = true + } + + if linenosAttr, ok := attrs.Get(linenosAttrName); ok { + switch v := linenosAttr.(type) { + case bool: + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(v)) + case []uint8: + if v != nil { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(true)) + } + if bytes.Equal(v, linenosTableAttrValue) { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(true)) + } else if bytes.Equal(v, linenosInlineAttrValue) { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(false)) + } + } + } + } + + var lexer chroma.Lexer + if language != nil { + lexer = lexers.Get(string(language)) + } + if !nohl && (lexer != nil || r.GuessLanguage) { + if style == nil { + style = styles.Fallback + } + var buffer bytes.Buffer + l := n.Lines().Len() + for i := 0; i < l; i++ { + line := n.Lines().At(i) + buffer.Write(line.Value(source)) + } + + if lexer == nil { + lexer = lexers.Analyse(buffer.String()) + if lexer == nil { + lexer = lexers.Fallback + } + language = []byte(strings.ToLower(lexer.Config().Name)) + } + lexer = chroma.Coalesce(lexer) + + iterator, err := lexer.Tokenise(nil, buffer.String()) + if err == nil { + c := newCodeBlockContext(language, true, attrs) + + if r.CodeBlockOptions != nil { + chromaFormatterOptions = append(chromaFormatterOptions, r.CodeBlockOptions(c)...) + } + formatter := chromahtml.New(chromaFormatterOptions...) + if r.WrapperRenderer != nil { + r.WrapperRenderer(w, c, true) + } + _ = formatter.Format(w, style, iterator) == nil + if r.WrapperRenderer != nil { + r.WrapperRenderer(w, c, false) + } + if r.CSSWriter != nil { + _ = formatter.WriteCSS(r.CSSWriter, style) + } + return ast.WalkContinue, nil + } + } + + var c CodeBlockContext + if r.WrapperRenderer != nil { + c = newCodeBlockContext(language, false, attrs) + r.WrapperRenderer(w, c, true) + } else { + _, _ = w.WriteString("

')
+	}
+	l := n.Lines().Len()
+	for i := 0; i < l; i++ {
+		line := n.Lines().At(i)
+		r.Writer.RawWrite(w, line.Value(source))
+	}
+	if r.WrapperRenderer != nil {
+		r.WrapperRenderer(w, c, false)
+	} else {
+		_, _ = w.WriteString("
\n") + } + return ast.WalkContinue, nil +} + +type highlighting struct { + options []Option +} + +// Highlighting is a goldmark.Extender implementation. +var Highlighting = &highlighting{ + options: []Option{}, +} + +// NewHighlighting returns a new extension with given options. +func NewHighlighting(opts ...Option) goldmark.Extender { + return &highlighting{ + options: opts, + } +} + +// Extend implements goldmark.Extender. +func (e *highlighting) Extend(m goldmark.Markdown) { + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(NewHTMLRenderer(e.options...), 200), + )) +} diff --git a/gno.land/pkg/gnoweb/markdown/highlighting_test.go b/gno.land/pkg/gnoweb/markdown/highlighting_test.go new file mode 100644 index 00000000000..25bc4fedd61 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/highlighting_test.go @@ -0,0 +1,568 @@ +// This file was copied from https://github.com/yuin/goldmark-highlighting + +package markdown + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/testutil" + "github.com/yuin/goldmark/util" +) + +func TestHighlighting(t *testing.T) { + var css bytes.Buffer + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithStyle("monokai"), + WithCSSWriter(&css), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + ), + WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { + _, ok := c.Language() + if entering { + if !ok { + w.WriteString("
")
+							return
+						}
+						w.WriteString(`
`) + } else { + if !ok { + w.WriteString("
") + return + } + w.WriteString(`
`) + } + }), + WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { + if language, ok := c.Language(); ok { + // Turn on line numbers for Go only. + if string(language) == "go" { + return []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + } + } + } + return nil + }), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"``` go\n"+`func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
1func main() {
+2    fmt.Println("ok")
+3}
+
+`) { + t.Errorf("failed to render HTML\n%s", buffer.String()) + } + + expected := strings.TrimSpace(`/* Background */ .bg { color: #f8f8f2; background-color: #272822; } +/* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; } +/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #f8f8f2; background-color: #3c3d38 } +/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #f8f8f2; background-color: #3c3d38 } +/* Error */ .chroma .err { color: #960050; background-color: #1e0010 } +/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #3c3d38 } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #66d9ef } +/* KeywordConstant */ .chroma .kc { color: #66d9ef } +/* KeywordDeclaration */ .chroma .kd { color: #66d9ef } +/* KeywordNamespace */ .chroma .kn { color: #f92672 } +/* KeywordPseudo */ .chroma .kp { color: #66d9ef } +/* KeywordReserved */ .chroma .kr { color: #66d9ef } +/* KeywordType */ .chroma .kt { color: #66d9ef } +/* NameAttribute */ .chroma .na { color: #a6e22e } +/* NameClass */ .chroma .nc { color: #a6e22e } +/* NameConstant */ .chroma .no { color: #66d9ef } +/* NameDecorator */ .chroma .nd { color: #a6e22e } +/* NameException */ .chroma .ne { color: #a6e22e } +/* NameFunction */ .chroma .nf { color: #a6e22e } +/* NameOther */ .chroma .nx { color: #a6e22e } +/* NameTag */ .chroma .nt { color: #f92672 } +/* Literal */ .chroma .l { color: #ae81ff } +/* LiteralDate */ .chroma .ld { color: #e6db74 } +/* LiteralString */ .chroma .s { color: #e6db74 } +/* LiteralStringAffix */ .chroma .sa { color: #e6db74 } +/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } +/* LiteralStringChar */ .chroma .sc { color: #e6db74 } +/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } +/* LiteralStringDoc */ .chroma .sd { color: #e6db74 } +/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } +/* LiteralStringEscape */ .chroma .se { color: #ae81ff } +/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } +/* LiteralStringInterpol */ .chroma .si { color: #e6db74 } +/* LiteralStringOther */ .chroma .sx { color: #e6db74 } +/* LiteralStringRegex */ .chroma .sr { color: #e6db74 } +/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } +/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } +/* LiteralNumber */ .chroma .m { color: #ae81ff } +/* LiteralNumberBin */ .chroma .mb { color: #ae81ff } +/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } +/* LiteralNumberHex */ .chroma .mh { color: #ae81ff } +/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } +/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } +/* LiteralNumberOct */ .chroma .mo { color: #ae81ff } +/* Operator */ .chroma .o { color: #f92672 } +/* OperatorWord */ .chroma .ow { color: #f92672 } +/* Comment */ .chroma .c { color: #75715e } +/* CommentHashbang */ .chroma .ch { color: #75715e } +/* CommentMultiline */ .chroma .cm { color: #75715e } +/* CommentSingle */ .chroma .c1 { color: #75715e } +/* CommentSpecial */ .chroma .cs { color: #75715e } +/* CommentPreproc */ .chroma .cp { color: #75715e } +/* CommentPreprocFile */ .chroma .cpf { color: #75715e } +/* GenericDeleted */ .chroma .gd { color: #f92672 } +/* GenericEmph */ .chroma .ge { font-style: italic } +/* GenericInserted */ .chroma .gi { color: #a6e22e } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #75715e }`) + + gotten := strings.TrimSpace(css.String()) + + if expected != gotten { + diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) + t.Errorf("incorrect CSS.\n%s", string(diff)) + } +} + +func TestHighlighting2(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Highlighting, + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"```"+` +func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
func main() {
+    fmt.Println("ok")
+}
+
+`) { + t.Error("failed to render HTML") + } +} + +func TestHighlighting3(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Highlighting, + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= + +`+"```"+`cpp {hl_lines=[1,2]} +#include +int main() { + std::cout<< "hello" << std::endl; +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
#include <iostream>
+int main() {
+    std::cout<< "hello" << std::endl;
+}
+
+`) { + t.Errorf("failed to render HTML:\n%s", buffer.String()) + } +} + +func TestHighlightingCustom(t *testing.T) { + custom := chroma.MustNewStyle("custom", chroma.StyleEntries{ + chroma.Background: "#cccccc bg:#1d1d1d", + chroma.Comment: "#999999", + chroma.CommentSpecial: "#cd0000", + chroma.Keyword: "#cc99cd", + chroma.KeywordDeclaration: "#cc99cd", + chroma.KeywordNamespace: "#cc99cd", + chroma.KeywordType: "#cc99cd", + chroma.Operator: "#67cdcc", + chroma.OperatorWord: "#cdcd00", + chroma.NameClass: "#f08d49", + chroma.NameBuiltin: "#f08d49", + chroma.NameFunction: "#f08d49", + chroma.NameException: "bold #666699", + chroma.NameVariable: "#00cdcd", + chroma.LiteralString: "#7ec699", + chroma.LiteralNumber: "#f08d49", + chroma.LiteralStringBoolean: "#f08d49", + chroma.GenericHeading: "bold #000080", + chroma.GenericSubheading: "bold #800080", + chroma.GenericDeleted: "#e2777a", + chroma.GenericInserted: "#cc99cd", + chroma.GenericError: "#e2777a", + chroma.GenericEmph: "italic", + chroma.GenericStrong: "bold", + chroma.GenericPrompt: "bold #000080", + chroma.GenericOutput: "#888", + chroma.GenericTraceback: "#04D", + chroma.GenericUnderline: "underline", + chroma.Error: "border:#e2777a", + }) + + var css bytes.Buffer + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithStyle("monokai"), // to make sure it is overrided even if present + WithCustomStyle(custom), + WithCSSWriter(&css), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + ), + WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { + _, ok := c.Language() + if entering { + if !ok { + w.WriteString("
")
+							return
+						}
+						w.WriteString(`
`) + } else { + if !ok { + w.WriteString("
") + return + } + w.WriteString(`
`) + } + }), + WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { + if language, ok := c.Language(); ok { + // Turn on line numbers for Go only. + if string(language) == "go" { + return []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + } + } + } + return nil + }), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"``` go\n"+`func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
1func main() {
+2    fmt.Println("ok")
+3}
+
+`) { + t.Error("failed to render HTML", buffer.String()) + } + + expected := strings.TrimSpace(`/* Background */ .bg { color: #cccccc; background-color: #1d1d1d; } +/* PreWrapper */ .chroma { color: #cccccc; background-color: #1d1d1d; } +/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #cccccc; background-color: #333333 } +/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #cccccc; background-color: #333333 } +/* Error */ .chroma .err { } +/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #333333 } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } +/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #cc99cd } +/* KeywordConstant */ .chroma .kc { color: #cc99cd } +/* KeywordDeclaration */ .chroma .kd { color: #cc99cd } +/* KeywordNamespace */ .chroma .kn { color: #cc99cd } +/* KeywordPseudo */ .chroma .kp { color: #cc99cd } +/* KeywordReserved */ .chroma .kr { color: #cc99cd } +/* KeywordType */ .chroma .kt { color: #cc99cd } +/* NameBuiltin */ .chroma .nb { color: #f08d49 } +/* NameClass */ .chroma .nc { color: #f08d49 } +/* NameException */ .chroma .ne { color: #666699; font-weight: bold } +/* NameFunction */ .chroma .nf { color: #f08d49 } +/* NameVariable */ .chroma .nv { color: #00cdcd } +/* LiteralString */ .chroma .s { color: #7ec699 } +/* LiteralStringAffix */ .chroma .sa { color: #7ec699 } +/* LiteralStringBacktick */ .chroma .sb { color: #7ec699 } +/* LiteralStringChar */ .chroma .sc { color: #7ec699 } +/* LiteralStringDelimiter */ .chroma .dl { color: #7ec699 } +/* LiteralStringDoc */ .chroma .sd { color: #7ec699 } +/* LiteralStringDouble */ .chroma .s2 { color: #7ec699 } +/* LiteralStringEscape */ .chroma .se { color: #7ec699 } +/* LiteralStringHeredoc */ .chroma .sh { color: #7ec699 } +/* LiteralStringInterpol */ .chroma .si { color: #7ec699 } +/* LiteralStringOther */ .chroma .sx { color: #7ec699 } +/* LiteralStringRegex */ .chroma .sr { color: #7ec699 } +/* LiteralStringSingle */ .chroma .s1 { color: #7ec699 } +/* LiteralStringSymbol */ .chroma .ss { color: #7ec699 } +/* LiteralNumber */ .chroma .m { color: #f08d49 } +/* LiteralNumberBin */ .chroma .mb { color: #f08d49 } +/* LiteralNumberFloat */ .chroma .mf { color: #f08d49 } +/* LiteralNumberHex */ .chroma .mh { color: #f08d49 } +/* LiteralNumberInteger */ .chroma .mi { color: #f08d49 } +/* LiteralNumberIntegerLong */ .chroma .il { color: #f08d49 } +/* LiteralNumberOct */ .chroma .mo { color: #f08d49 } +/* Operator */ .chroma .o { color: #67cdcc } +/* OperatorWord */ .chroma .ow { color: #cdcd00 } +/* Comment */ .chroma .c { color: #999999 } +/* CommentHashbang */ .chroma .ch { color: #999999 } +/* CommentMultiline */ .chroma .cm { color: #999999 } +/* CommentSingle */ .chroma .c1 { color: #999999 } +/* CommentSpecial */ .chroma .cs { color: #cd0000 } +/* CommentPreproc */ .chroma .cp { color: #999999 } +/* CommentPreprocFile */ .chroma .cpf { color: #999999 } +/* GenericDeleted */ .chroma .gd { color: #e2777a } +/* GenericEmph */ .chroma .ge { font-style: italic } +/* GenericError */ .chroma .gr { color: #e2777a } +/* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold } +/* GenericInserted */ .chroma .gi { color: #cc99cd } +/* GenericOutput */ .chroma .go { color: #888888 } +/* GenericPrompt */ .chroma .gp { color: #000080; font-weight: bold } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold } +/* GenericTraceback */ .chroma .gt { color: #0044dd } +/* GenericUnderline */ .chroma .gl { text-decoration: underline }`) + + gotten := strings.TrimSpace(css.String()) + + if expected != gotten { + diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) + t.Errorf("incorrect CSS.\n%s", string(diff)) + } +} + +func TestHighlightingHlLines(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithFormatOptions( + chromahtml.WithClasses(true), + ), + ), + ), + ) + + for i, test := range []struct { + attributes string + expect []int + }{ + {`hl_lines=["2"]`, []int{2}}, + {`hl_lines=["2-3",5],linenostart=5`, []int{2, 3, 5}}, + {`hl_lines=["2-3"]`, []int{2, 3}}, + {`hl_lines=["2-3",5],linenostart="5"`, []int{2, 3}}, // linenostart must be a number. string values are ignored + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + var buffer bytes.Buffer + codeBlock := fmt.Sprintf(`bash {%s} +LINE1 +LINE2 +LINE3 +LINE4 +LINE5 +LINE6 +LINE7 +LINE8 +`, test.attributes) + + if err := markdown.Convert([]byte(` +`+"```"+codeBlock+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + for _, line := range test.expect { + expectStr := fmt.Sprintf("LINE%d\n", line) + if !strings.Contains(buffer.String(), expectStr) { + t.Fatal("got\n", buffer.String(), "\nexpected\n", expectStr) + } + } + }) + } +} + +type nopPreWrapper struct{} + +// Start is called to write a start
 element.
+func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" }
+
+// End is called to write the end 
element. +func (nopPreWrapper) End(code bool) string { return "" } + +func TestHighlightingLinenos(t *testing.T) { + outputLineNumbersInTable := `
+ +
+1 + +LINE1 +
+
` + + for i, test := range []struct { + attributes string + lineNumbers bool + lineNumbersInTable bool + expect string + }{ + {`linenos=true`, false, false, `1LINE1 +`}, + {`linenos=false`, false, false, `LINE1 +`}, + {``, true, false, `1LINE1 +`}, + {``, true, true, outputLineNumbersInTable}, + {`linenos=inline`, true, true, `1LINE1 +`}, + {`linenos=foo`, false, false, `1LINE1 +`}, + {`linenos=table`, false, false, outputLineNumbersInTable}, + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithFormatOptions( + chromahtml.WithLineNumbers(test.lineNumbers), + chromahtml.LineNumbersInTable(test.lineNumbersInTable), + chromahtml.WithPreWrapper(nopPreWrapper{}), + chromahtml.WithClasses(true), + ), + ), + ), + ) + + var buffer bytes.Buffer + codeBlock := fmt.Sprintf(`bash {%s} +LINE1 +`, test.attributes) + + content := "```" + codeBlock + "```" + + if err := markdown.Convert([]byte(content), &buffer); err != nil { + t.Fatal(err) + } + + s := strings.TrimSpace(buffer.String()) + + if s != test.expect { + t.Fatal("got\n", s, "\nexpected\n", test.expect) + } + }) + } +} + +func TestHighlightingGuessLanguage(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithGuessLanguage(true), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(true), + ), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte("```"+` +LINE +`+"```"), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +
1LINE
+
+`) { + t.Errorf("render mismatch, got\n%s", buffer.String()) + } +} + +func TestCoalesceNeeded(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + // WithGuessLanguage(true), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(true), + ), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte("```http"+` +GET /foo HTTP/1.1 +Content-Type: application/json +User-Agent: foo + +{ + "hello": "world" +} +`+"```"), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +
1GET /foo HTTP/1.1
+2Content-Type: application/json
+3User-Agent: foo
+4
+5{
+6  "hello": "world"
+7}
+
+`) { + t.Errorf("render mismatch, got\n%s", buffer.String()) + } +} diff --git a/gno.land/pkg/gnoweb/markdown/toc.go b/gno.land/pkg/gnoweb/markdown/toc.go new file mode 100644 index 00000000000..59d4941fabf --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/toc.go @@ -0,0 +1,137 @@ +// This file is a minimal version of https://github.com/abhinav/goldmark-toc + +package markdown + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/util" +) + +const MaxDepth = 6 + +type Toc struct { + Items []*TocItem +} + +type TocItem struct { + // Title of this item in the table of contents. + // + // This may be blank for items that don't refer to a heading, and only + // have sub-items. + Title []byte + + // ID is the identifier for the heading that this item refers to. This + // is the fragment portion of the link without the "#". + // + // This may be blank if the item doesn't have an id assigned to it, or + // if it doesn't have a title. + // + // Enable AutoHeadingID in your parser if you expected these to be set + // but they weren't. + ID []byte + + // Items references children of this item. + // + // For a heading at level 3, Items, contains the headings at level 4 + // under that section. + Items []*TocItem +} + +func (i TocItem) Anchor() string { + return "#" + string(i.ID) +} + +type TocOptions struct { + MinDepth, MaxDepth int +} + +func TocInspect(n ast.Node, src []byte, opts TocOptions) (*Toc, error) { + // Appends an empty subitem to the given node + // and returns a reference to it. + appendChild := func(n *TocItem) *TocItem { + child := new(TocItem) + n.Items = append(n.Items, child) + return child + } + + // Returns the last subitem of the given node, + // creating it if necessary. + lastChild := func(n *TocItem) *TocItem { + if len(n.Items) > 0 { + return n.Items[len(n.Items)-1] + } + return appendChild(n) + } + + var root TocItem + + stack := []*TocItem{&root} // inv: len(stack) >= 1 + err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + // Skip non-heading node + heading, ok := n.(*ast.Heading) + if !ok { + return ast.WalkContinue, nil + } + + if opts.MinDepth > 0 && heading.Level < opts.MinDepth { + return ast.WalkSkipChildren, nil + } + + if opts.MaxDepth > 0 && heading.Level > opts.MaxDepth { + return ast.WalkSkipChildren, nil + } + + // The heading is deeper than the current depth. + // Append empty items to match the heading's level. + for len(stack) < heading.Level { + parent := stack[len(stack)-1] + stack = append(stack, lastChild(parent)) + } + + // The heading is shallower than the current depth. + // Move back up the stack until we reach the heading's level. + if len(stack) > heading.Level { + stack = stack[:heading.Level] + } + + parent := stack[len(stack)-1] + target := lastChild(parent) + if len(target.Title) > 0 || len(target.Items) > 0 { + target = appendChild(parent) + } + + target.Title = util.UnescapePunctuations(heading.Text(src)) + if id, ok := n.AttributeString("id"); ok { + target.ID, _ = id.([]byte) + } + + return ast.WalkSkipChildren, nil + }) + + root.Items = compactItems(root.Items) + + return &Toc{Items: root.Items}, err +} + +// compactItems removes items with no titles +// from the given list of items. +// +// Children of removed items will be promoted to the parent item. +func compactItems(items []*TocItem) []*TocItem { + result := make([]*TocItem, 0) + for _, item := range items { + if len(item.Title) == 0 { + result = append(result, compactItems(item.Items)...) + continue + } + + item.Items = compactItems(item.Items) + result = append(result, item) + } + + return result +} diff --git a/gno.land/pkg/gnoweb/public/favicon.ico b/gno.land/pkg/gnoweb/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..528c362c44a80fe29bab63303a6e7a3a05e43ce5 GIT binary patch literal 7406 zcmeHMSxi$|82(D3w6~=!EiEljDr+fFmI4a0l(Ln|(kU1&~ zpL}qMThJ$CjGE|!4<;tQ_@X`;G)9g4miVA?nR9Nrr5)NrW+szNrhjsK&VT;zKezpF zoAZBP0Ur3s$^sdMSdaqw0JPdAUkEIcwXiUUKNtl3t^{-hLsHO9E}F-h$T6_5yybao zYb$DMYEW2Mh>ssXA|xaPM~@!G{{8!rnwpBIPoHAv&Ye)J)ewut=Hxp`FQf=2~tv0uw%y#L`6kme0&@tkqCCX9cRy;MR|ESa&vQ$l$3-=j~-$B z_U%w86u5Ke4s13X78VwelaqsY@7^IHAps8`KE&MI9OQC2Mn*<({rYtT1qGqAvlFLJ zpT^g(UrAlE@aD}M%+AgtA|e8OJ|FGv?WnD-#plnTVK5l*>eVY!pD+v#4uZ$y!D_YQ z)2B~3c<>-zym*1>>1jx%Qna+Rz+$oB*s)_MC@8?QXV0)}*DgdyM?)f!ps%kFSFc`0 zb8|DQs;aPW-#*OG&qJkBp{J(@jg5_{sHni6J$vx@@nhV*dlzG4V-O03xN+kK&YU>| zv)K%z(TLrJIJ4qUi!0d;kCICbh2zI^$D%*;%@e*GHZ;o%q>8bUxo0NUExaQ^&xoH%g;MMXu( z$jHFUmoJwex_{=cfWHF&QUw$h0gAQXYIv~KVo@fhuQ6k)ubZ*in7DZ3S`wz^8wr{k z^NVnL)eK2fj>Q2d2|4xFRT2vGn5Es>J1`kjJ`>Va&dnV+v4r~k)X6&Ty>O$h%hwH} z|Fpf$W+CUKcZ*+%S9wPR#TFZ5aeiPrwa~-at4%~QJzIWul!#&UTl9==&?}e(Bz8+8 zrozg|sbndupF$R6_x%LKn<9WBanshVVKwC8#)N$5YkLwiJH9mJRHjyFW(Th?sj4CO z*op=B@NCjdmCFN7=a2rxcSpE;a#_D=Y~(ksWbEWX-}ssqcl?iH-*_~;<9}EMj;cA=1QU7M zzO8r675}Tt!|;;Vk-eNSIp784vfNv_j1OOZd!SRpC8YTL?mO=hnYol87DsB>=OYhs*P!uQuk`NBD4T@7l|#J0C5Y+chEB0qW3 z!%}nZ;Gg&_@L#9^<*8b7naJhjse#1d%!Q|(J9iGxpFd~$DfvOM+?4Xt$;nBUm%4J% z4((tNr#ycAILj$xV`DKeFaW(?@8pkDQ&TK|JaXg+bUGc&87C$tSgx3t zmj|U%$#TT*?rvPZe3|8l@87>?c_QV995=jp@uC|)q}-5lM9K>(AEaEcq@)D*@82i- zfYyx72%2n1s`YYhCz+bC?-JaqjvAQym;pk*cIEwpq zckfpIO^TJc5iAoorClArfU45_fzgxvW0F)XA!vFvP literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/public/fonts/intervar/Inter.var.woff2 b/gno.land/pkg/gnoweb/public/fonts/intervar/Inter.var.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..365eedc50cd0f46ea35a3176335fc67b51123fb4 GIT binary patch literal 324864 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≫bY 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≥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

6&qaNDxLwZnq2 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

ycBey{rl;kDc{Ai+(=LzE1TWNYW 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

Ub z-^eFvs@QDUKMY=ye9WH=iD^lOiMKCj)@xV4p9J;-Qi7-;L=!ehDoH{TZvI66$v+LZ zaE(@mC3!N+7RIHvyNiD<7!~FXL*KvJ$2j086Cu@+G$5-YTc$f{5iMyh!6|*1=f@i| zSv6ZUnKTPENj2G?iJZjdWlz>klpXI$J4jYeCgHSm?0R}hLYZp6ZddlW_Bit3&k^}_ zAm#5cu<)8T2*w&aQB`i%N zHC7CB(lxD`V^7OM9Ys}1w?W&h0k4W%rcnu7V^fv=K6fXsmEx)@z2tzV@mTXV36=vB zgfT_`q{rO0sMuHy?yPy@v~U#H9Il0!@xf0Zj80sc03tzl&*ud83jNwnpHJT#tJA9R z+;%P9E!QpXW94Jm-PGR!w3WWa;lp-99nLH+TF{|@nvCsA@K{0BS>6D(F=&}-$G5q; z{n0g4GE_@eUtCpOU0j{A`c^{MQQc`Sf>A@wc_G(YWToxK!NSnWFYV zFwIq6XQiCpChgBgQ)h$rT4O6kBg6*D#Vf=*x({NE0L<*SpX{^NTyu?e<4-^Z-BsIh z>FzT0B`6PVGbId_aq|=x^4A1c5O4Yh)%8~z&`M2b9zG$jFBvYV6{ ziqul_p~!Te>+GU!t!=`+&;9ac&XI{TFgf>G6J)v|o>JaT7ClY8VcVCuM zK(YW;ftu2J@$Vu*S;`{$IUq$aMX9tLp?s!X-vXWWmlh;lLaHpcbY)?IB`9`UNx{@? zs)covybH=3eFp(=X09N^AN~B+Ifye9m+&u?&tGpWZxEqSe-rjb!p(}9?y-uaX`=Cm zF@q*%8IQ3hqSaA4P}ETHQHW4L%@A2~Glpo*mKnJ-+{v@bjkeABjK0P`CYen+8$UFe zs$|rVE{H2bLQEAkfm2i2W;{o=jJqLVfw=onAVH7=Z1OS7fY$v5{o%v?sdmi`u-w7C zO|$BAR%I{wo;b2b)EZA#r7zi@-p9JeoRdb!rpFQ_4V3sedD}T&lTrRko}fBaa86|l zPJ@aMw-0&rc?_=|+8wPs@wORmg4(e^!?uQQ4CC#|Igo8;ZHit)KeOF{)+Vb@TOSFgP{A98IZYSbwU63$8-P%6(HjP*@pNA z?A*n?q zo5(egYNmN7Xi4N02h3|&rlJycliH>{Nk%AjBr~W=niosYyPkPv)K3aN<%>OdwYa*$;GCk9PzYk`Ko_)eiVNM4T$U&K`IPV zI-wm!Rz~`Qi4rMnDt!FWP_?0aOkN(}8t)$GFimaH@X%bV1x=PZiguEGExm6XmpCfB zR}HC5RHaZgS9PUsiY(f%V6Et^ps0Xr&RrL~Du-7ZDUU0cUX*Z+%LLp681KPy5b!~) z^i4T1M2Arr5W)Usxvjlr?t<@KIkmoI-Odz{Z9N`)yuB!1aa{3R3AI>gBGW2r5$7%e zYu36NNS;&qP`Ox)Dc?|zEBShv!J+>K&u~KisfM`&Eq%HfXtzFc#O?#yo)g!R$&|GJ zaFAIkBR7j)b=ycU-zFXr-E{8{{@%ar#x>{gqL4jjZH&n3&MfM*r=shoy$*+PfEy3%SyJS zCn3v1-L3mMI)=g`tZIPR^3}e$N4!w7Kkdz|Df66+k|F4Rm>sK6?2M@}_}$Kvr15&c z)-vMfwFs3Y%1TzxyM34#hQjv$KGzhVXt5~N=+)lX1utEumZR#up_yRE>H zTwiRFbapnDzj4Mkv85cjsdJlfc2`ZUdF`nb(>`DIST6dG*1rtg@jyG07UZ`w`^hQu zWvecpyAGQ^*S1^C{h3f(#@umd@%_5CKK3|nJpw68ZaTtXOXJwD3aauRGo7f6p6SKp zt$QyC*~PPV1R=#}CW6XgegMKt(i4Z@!DW{uVQlszz3b5;tD9<#7l21`K`!-@7c zN*6(;Vpb(bB7#LNNVOxKnCD0=QY)wU^^&(`An}}j4t2wQ!BZL&^0=w373aezhNBH% zI3+ngwRe;VLu*b|9Au4if$^(IImAczpjP{GrYJ55H7_M|UnYVKJbu4HS^0GZ0qtg2 zi$7>;w+`gP+PLH&$0*d=nC0}Lh0*Skl2T4NlZb9B!nYGacdMbQ8@+-2bL9H=TX~HR zY;Mg_GY8d7X0pX&`g_v7(_q?}HdcNX(C_N9E9>-gClL1zECZw5C0KH6Unl-rBcphX zruYo>V90SJmF`GIHiD&m{HCeA)y;Tn#dxVm=Xpvn2m`5(Kx-NZNu}^)fO|Gz*ma9p zp~7dxM$>c#99xtKlQ|aBlFUYQ2p!#}1!EC)c@>!j22Gfm04Og#wSd#2qU_egRI^?~ zw_&o3K7+>lPcYIBX%-!yT8*r3*ddOdI$m0?MUm9Q8_n@A`%_ot#2wa^#*aB*{lT3- zC8@c!J({cz&-)%}RkUH&-ZWCVu`*AG8k8}k_jZa(`|54fl(R3Bhq(HB&{bEPe4G}} zj|h`@u(}-sVU9zmPbC$iAk`T;=aq2gkJ;~IiSpo4KA7GU^ydR=FtAi5`;qIk*20eH zoW<^o3z>80Y)5N}KuR!Hh`=uh9$6;PcINjZMAPRA@e4!7L7bx};XL1S1^SXSi(4E~ zXlj*w_vJjn%<&)jngK=#i_&~B6f&@87fMt`XndzJPft~wehLcSFxSkDJQE*{z!usYyro zn{};&5|L34p`Mc#7ZXJdLdV0&a8-OKPf#o4m8;riOyK)Gx~g9uoncawtjH_!A0JDl zcKjsInLTMc2lnm9sT=Uu4DAi4f4uTM`Cd}F=RW`0yb(qUmitP}xTu%}&*hJT3Zq(y zLsmpRS*DJgntS$iUAP+fvvUJNId$h)+1Mr zof{K;5Xl@@uDF$1b$U_~q&5SSzgBd>!k0Vn0skxNz_u6GXZ~>fvd|ppS1Y|&<6aGK zTb23eaVKD6IN>lhRNQ@cjTPm0(m{6J)1QUm=MNyw76`jb3(q(4L*Adl+NZ-gYC4OF zLs9l;3r3gg-6Qtk*t1nz|MfuRi$5Gs#cDi^BtY}8?Fx5OQ7sNjeMB;XDH;gP=yc+? z<)kAsYJsKu%1&Kb@~zp!b~}3U54pOXwxy?(A&K8gcE;yFl@|3xLOB6#L;<>gpjm{P z{m>-mzXrj_EuDaZ)|npMIs`niG~?mW3TXYf1~i%qi@66CwH4*{d~0+smj#b0HHwQQ z6cqPdB4b2>Z7J^N{=M|D_%;0GyoaPhn*F!6mHsW`rHn)-M$lG?cqQ#6#>)u>10B55 zcrEQS^Iq81#LiZ1I`Y?6Fo82o>H@1s4<__p&TPHytP zU~1(N|H#fqjmB>JLJSB1HEy8`Ffxe&NzGXxs#fsMp-^BcVX_m{4WXL{Ar(lZ<~ zu*aqeV+(t3GSKO~&K&v+F|)p>{2dHPVXEm4ZLZC{3CPGNT;_#^WLys?6;&hkAQ6&C zh-b!Hw$dy)2%lG5SJu_k($R>&XrSO~g!BHO^`5A7z+Z9ss>RfZVA6F)8~~g7sj6Gk zC=egRGld?om+4a;U~Kmq^vh~n!zl^Ax3#u$GQ?t&*Z5k!4&eM+K?xs@5|a|wf;l`j zov-2gx=TGS(=?@7It3y>B57oF7xr2JMI@!n6zv?6BJYZ1Y zJnon(^}UASNet!>X%c{Ks;b+IZe2Plcyhdd`Nx$_cdRfo$$uJhsq0a;tWOpY{op^| z2BWB%Sl*N{k%u~wv~DGKIh2UHez^H0(n`mt$5iAYLq2UwO-)N=l&Z}VL0FPb-Qe`h z5JC0pw$gOyg1VHRpc$J)oSv(}cduXy&R}rtRTj*ZgiTE|a{QvAN;fgiFXa0p;myR1 zt8Z8HnL+FQGy@NG#lG#gqMo=Y`|dm^&4l#u1`nWJE|?M9$Y(UsH?TngOzX7{7}ekQ z7|DiZMyStMN`L}M$V3NRNCe@QW1ubN=8z)50xAsikwU8xiF$ITrcSNymq|yCT2TK! zkm%%}I(!yQs)B)QkS~pdBjYU!ds1=PE$3+`>#P1b)03GoX3P`)m%%qYw`v#BA&uz| zIFm04pVc)bWvfV114iyj=Snf`OXL`r7AgK$Zh@nUpr~^%AcVZ{px4Sgj^4QVqt>n# zvnk!(mrs01)SxxhTuz3{>h5~hs01?y56iECZ6(lC*5Fg#K<=WKDIzMeL7delqAm;r zH1F4Wfy6hz7ki-IoGucq-q*;lOI;BK5N$sao6og)1lK}pKT&FlMPkFr5N9Yu;yp=` zvk2J6!s?!uD%P2toD7DCm^eD-M$JOx1AIAi1?WJ={%n+qqHlPqJQsy=+svCBesx83 zowwD)z^s-nXvx#I4w6bbdd`Al+2AXXG4-nL zFzXYIlrhuglT$a8GbKlC6UVHjYIu6>=f|#SnAGa{I_q7ARlHMf2QLl6)-;Sy)m=OW zV;AM)Ky^z~bWbktRhQaVxZb^m?X)m`T5BmM7F4Z5a<0Gf+%RUmC2_EP8WcILZdZ^2 z(7MnHgBX`47Rb#m6aESoQ->NrXzo=#%^Ykls@e?~@v`88+7_p_pijWZgQ zp)G{kctR6}(QgvBv*hX`gMYCqku{Rs*`pp#n(3XBneNOx>A7k8^Q_)OEq}!W3 z&aBv5$CYGW&-@Dadl{t$ewhC3x7g6!AlJgfOoO-QJlJ0*doK6$_Mz|w^cu006I-{N zZ_T5__Pi<&y+*y$n3);$PllO5bt7{t>c9en4HXC%VPg>h+@n{aWI_>%y!B8vC$NZiieA0Rz2#bEYMqjT zvSD8N=ZW5DR7h>u}LjD$Q2Q?{)`DlT}y5K2J{d98xp2)IY<**t&! z;CF=Bf2T-uqh|FGrysnvrs`KzPhMC#SN-z_alcM;3I51(@Zo%&huLkX93yC_QCC1r z@{2@I>89De!ntb6V{GihUR96zqs{Ejpp>M#-b-(97ZIEsx83k{X+oNbUdv@nWTpnK zF!%7!Jy++Om7J_Eboy3`8cuE6Va2Y`RL*PUHtANV*qrA-zpXH7OcJ)zAS8JpCNThU z3}FFzJXtvnor(cRjX~4919)YsH2~P3G?1fEVL0IrBfJSKv{th-A59#IW=)dUeBg~E zQFJsY4u!lL6Ygf-3=teGG8QEBr1%aLAJ=)DW+NBxPD0+zcq2DSn;MCj6Vsh+Y6@;? zV;}=}7k+N3F9d|EFgI0ku%!y{y7V8)_}wvCU>Q3T0);jpQ`zet@5-CXopB&U^| z$h_`PAwLdI%yQze2hD|5+EHF0RE%_8>T1$Kv`7L;^<$LwadxjqRWTv+0HUe2-rACb zRdf5>eoL`>jn#N$Y`J#Z`#FUAB)5;F4?A46afpzLlah{S1!v-ActizO#Wc~QsCJ7j z?maL)-ETGF?-rIW(+jm|HckNUb)PxYSt>`L2i?PQ6NQ?MPJ2EZ#=-pE7KYBYR=4$q ziawEm*!XJ=E^JDaJiq&US3SY^?rtbo^n_STp3f@*@q<)XF023OOVKEYd#b?Wi&n7t zwRre)!>5QK(&*ky1R$~iMlsm`PqC3Tk)UlNM}UJN(y9<>?b!GivL+s{x(`V2+at~S z&SGc(afs1l+Wek9xC6SJ5GdL7=wJOuYu(TKQO&Q{qw|J8rEcG{zNVJ*axlgi?hhY0V&@t7R9$+y%3!Yx(Ye{^G=_(tzIocrWM?SzrRv)ew4-RIR zlM=?E5Q1IRHjt;SqsjL7o=EmKnnsE$l(7L}mFdXeUM*JV?ezWkaU{!5MNATjmpaqt z+ds^`tcv{Ds39+JyCmT(8LKIm28(!NM^&r4m`DJ{ulrrdI|cfVp8I>uwR;9(+v(NP zxERwd@2+r3p{HyrsJ_t9{cb)m4c8b(Op7;tH49zMsRwap_|%16Y>pD-=-ru&kknho z!a>H!2r+UjTrw`s)L&6elK_Dam7F+~u&ryq)No9+muCS7Bd_gqj)6hj`}<{ITAfv7 zWCGE;6063LlmZ9;^WS97$>+n~l7M~|`@!|rFSsjuy&jXYIZ=+EdfnWPbl9Ua6EKu4 zp;Wt7{38Cs)NbyrGm~LMMZ9|TVPRhaymzTr2d@CWBLw=JBh7Xpy`MVQM~^2K@S)ku zj@e7h=<&?*@f)ZxUShFa`VF=obbz*cIe~mq-XXY{)*Ueq{!feICiQd~!F

Oc`#< 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

xBkwJt@8E9&+>-mZSm0{=UvpbD1bp|p`r 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 LF<>E&RPFo=(js6m diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff deleted file mode 100644 index 5beafc7c57e08bb4fd345a80fe126a5163025633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71476 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 QFP0|&_qk`@GEO`H0%fES*#H0l diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff deleted file mode 100644 index 68e61b1298dc17fefa60f27b7026a2086e44ce26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67428 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�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 diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff deleted file mode 100644 index 2a50c6d2fd438d7af1dac13fc59715e890f971d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72748 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%0DWw(Y! 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 diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff deleted file mode 100644 index e65d5310e4526fd16c11e31206634226e6d03ea8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65392 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 lY4M?6^ETWw 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 diff --git a/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff deleted file mode 100644 index 6ca15f36f5c48d9f456be7e99d0515e95e212aea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67976 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%0ArtI)I|QQP4R9kw-lOUH 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 diff --git a/gno.land/pkg/gnoweb/static/img/apple-touch-icon.png b/gno.land/pkg/gnoweb/static/img/apple-touch-icon.png deleted file mode 100644 index dcc70338eaaf61c743c1039bab5d4d6f7dc12cb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1502 zcmV<41tI#0P)qr?$jSU;qFB06^sI)&tBXJphH_bMbR++qP}nwr$(CZQHhO+qSOG+88C% zPVzmm-@493b^8tZodGo?Ktf>#-uChLy!+Ei*u0bqc7}pv{u@mtt`d+;-B(o98gj{a zgUV__D&e+K*`>IU$v?xW>~S?nrNVO+&9xnnNrE%hYYsTydd+C!vR-rQYu0NHvch`J zXyLJ5bINPhYYw>Bdd;ZgvR-rCv;4D1@-_cGjtZ9l57`uaN(B#cLNXz?Qqj5~kV^|M z6}js{D)BB*g;Oyg5$7nXGQbXbWPLzY?xck@{4J(RGyjDwO1z{>&+MpAGx-6s+Y7aHo8VoY1=b;AfYLGyf1Jq<^aQP=%%Bc>uhZ=1Q3el>$snMIV5G=?R zYPLE6#47%Znmx}4q5fM;9cKLvkupA_4);<*Ao~dF(hmZ~xI|q}M}jzwsE@ZMgbB2T zI<58xQL;ayPLI+-l%dqEHv|cPlDZuV2{B5zsN1Uo5W+E;28^&lgh=OT!0|Aa+a2Vq zd=Oz24H*CdLLR0ey92{~nV->+N2y@G)-WsFjyI;8*9v!(5r3^Aq32e#!oUO8=%Fa+RMwP6=~W^6{gaePY_7fj!>fJN(E~ z9OmlC4_?nrZihyWw2R;HgJwd$c$aJWgAM^2T<3fJVXm%xXKO0xW6Q9M@3e%uVs3W5 zoE{OnMVjyxgY$B%q$PH1McK`8%OD#U*#BOQt&ZxRh&n$|xV(9!5+Pdlg1_#@Fpfl~q9T*PcrN|ovn2>i zVrRS2&SA7v+s+FQUw`=U=F!Dn3(^?vxM)AO#}SIuv7E@_CX7_I^Klo=OfoZeKiyM< zkyhtPWe+6~g2b_+NVPX%xU${vN+u4H@#DS<3|IF}r1FR3aBoLWB<*zuLy~Ks6^)~) z^m0QALsCzTq_W%K+=B1oU-l}E0kH+wv&L~)?)Ku?(950}|I+sba4dPt_>bC~(XaMI zyKw^6d8Quy>b{Eq*qs#IT9akJa|!&D+4+Iugrv`V%kfYA1pDRI!KuV8W53+VDj>D` zaZ#LX_-^Bo^qY3V$Um63T_Z4vHvM~U5I}V4ol^BG5aH%nfk*Ll# zxD>x(oHN(^G*)LhXN660D|O#ENA|1?j;-hx=S10ca4mL*bEC@%I9K(|I0yEo4k1|Z z**I(Zz6_yQ@!dE}YF~$tteEXz+qbm{P1{q7GbX)Uj1X0x=@n;4xr;RjReb$>aYpoc za{^(jzm^xLO~qSHh=AmdZ^mh|pL^4YhPL}9aT>%YixC-RhdYf^vhEWVh>+Ow7ZfL_ zH>=}_ma4P8;v|&2REx-oZ}=ciNME)l5Je5w3*xv`ywi+`O6~e?9EY{T$ zh))+I(#no>8Aq_5lNE@#*os#Yhv@B^IHIrmTvqI-{N*~#3yF;%CH^J*vMq`EqVaMk z&DeFl1@lOvb?2#5yB8$jd7kHap67X<=Xsvzd0sI70Q0SE5-Sa1#{d8T07*qoM6N<$ Eg2^c8-v9sr diff --git a/gno.land/pkg/gnoweb/static/img/favicon-16x16.png b/gno.land/pkg/gnoweb/static/img/favicon-16x16.png deleted file mode 100644 index ee407d9a7eaae40fc4b82aba910697a0f7add1e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wBx*Bp9q_EZ7UAialK%Ln;`P6{LQA@%`fXO#0sM z`r~Y!=QqCj|KE3x2h+Rx|Nj4f()Pgk>~DUS;+YSaCN1YXGpF*w6qXo=6H{kToq9Wz zL+`-z#90#(UL9l?pRjsee%7k>)7rTUj+>jBn5Y&guMzyPe(hIrmom-O>*TaI?qgvH WGf_0_Tz9k&&ac>+9EFKx#V&76l3q zK5i)VwX^dr-0<;YI0H62Dw`nk zO_gSh*c33SXF%MLslkLJz6Jej8XIf<1#zco7Q>ueBNn{Lo}Zs_@a07S9596zC*b3N P00000NkvXXu0mjf1D=47 diff --git a/gno.land/pkg/gnoweb/static/img/github-mark-32px.png b/gno.land/pkg/gnoweb/static/img/github-mark-32px.png deleted file mode 100644 index 8b25551a97921681334176ee143b41510a117d86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1714 zcmaJ?X;2eq7*4oFu!ne{XxAht2qc?8LXr|_LPCfTpaBK7K$c{I0Ld=NLIOeuC;@2) zZ$K%a)k+m-s0>xHmKxL%0V&0TRzzznhgyqrIC$F)0{WwLXLrBvd*^wc_uSc%h%m9E z{W5z3f#4_!7RvAyFh6!S_*<8qJ%KOIm?#E|L=rJQq=gB5C6WLG5;c?r%V0>EmEH#X z5eSwPRa6WXBMs#$5H%GtW2go-in9p>zW@UYDNNWc^XOXZQ? z1QjEV00I#$3^1wQUJ8&-2UsjB-G|9y(LDhMNN3PM{APL4eYi{(m*ERcUnJa{R+-3^ z34^A6;U^v`8N*O6ji%S@sd{fJqD`XFIUJ5zgTe5^5nj414F(y!G&=H(f)Lgzv?>%+ zAsWD}2qhpH7>|TU`X&W6IxDNuO_vET7|j5oG&&VDr!)hUO8+0KR?nh!m<)a!?|%yG zqOwq!CWCcIhE{<$E|F|@g>nP6FoYr6C<8>D?ID9%&5J(4oSbR1I^byW*g@__U z4QsF&uJSEcFeleM3~ChjEQGbHOjsGDMbyAl(p=Ttv9RaVo8~I#js@@Y9C^_2U})yn zzSHU%6FxuY?d;&65MyR({^lU*3$z$ZllDb(o&<7d;A_`h2U+3~BJ2Hv`{W}KEU801#cv_B|9Cm!ynR{S`AMsSn z;7E=B;mb!wx$L;S>yGXG^6=&WlQn9$s?&L%Y1D8TI^MlKB1DqsEng$>f4=xYWBoPI z_S1p!sJ#d2?YI4kPA{k}Eby?F=f-J9zIc`YDl^pzjVm~9ebE?Hn?t0Nx+la|D0MB; z9)2xv1G>a1|A9kQ>~DV<=X3-4yC&n!m8-3K#P z{X@0zRuQsy$+N ziSCoLJU{Z$nQy4A4Y5UJ07$5FA~qL2%Q+cLaqDU?Lz3?=BC5;Nk6BbTmmceEaM>-Z zi>O&-dSE=%ex;vcvCOk{*JQ5^_4M z4lW7%l9IqY(z7pV(?I@@8=KPFO82)O{VDI18-*d-k$YmI^XiuPs_LuFw<^ZcD}yP5 c*NrbeloN*74g`U%%F6r~k%+>C^#XapzmV0H-2eap diff --git a/gno.land/pkg/gnoweb/static/img/github-mark-64px.png b/gno.land/pkg/gnoweb/static/img/github-mark-64px.png deleted file mode 100644 index 182a1a3f734fc1b7d712c68b04c29bad9460d6cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2625 zcmaJ@dpuNWA3rl=+=}acf|9E@P=bZCA&+qg7et*|Lo`cMQ4SL!u zv;hFnqx;f=RIA70r>U;`S924)Rm*a*H%lB0$B2{JLJ07ThNB>m&SUR{f*^KuO5#1p z6#!6H+z^(S#qg(aU>=seh`~yD0u>toT-_xCHYXkugHg~ylAk{k$56lW5JxEB2QU{v0O z(J_=Dn$JgHsuL9xD;5hVI9zgaGB()}3k!GR2xKyOQG-ZyP$3*dDSRx+6H zxzS&ah4w`*P8AGpv9Q5%s{48!i53cI)dGsN^YTkva!Csa-!~y{IALumC5XsY* z;oO9fP-D5HNp6GjVXS9_c1V2u^I_zB1-k6a`@n;|eN2-wq}`FLV<<0w=RlfKU9(3Z z?Vv$*-_m{)R9A=k2=5$JrJ5 zd(x-6(zYwCSQA3wWMBj;Lem(jL~x}3pjUMga+Tt=q9Zf4cjQq+R^GwOxB}onmdyq9 zYa}1po)-)mjV-^ZRfS$nm0JP%%2J6zkxp^p8J$PEwHnnPw39eZX}|bwVDI+Gee`@Y zbah4{SeoLiGPW@75vPCvM=#55zb)v1eNE+tfD*T%9$`a#UqDqP6flo7k-aV>IQ3KL z?3H`(H3`?q)i9}4YoPsfZeLPwKtG(KQ-oT2jcN(B%hrz*1V7UCp6GY!F4e!okh(0O znQ=jWE*4#p8`djsr?kI5jXKJRYt>(U){i0emy7~ePChu6oUwefQNQixI-(=d{P1%3 zhx=v2`Ry0lVKW&Jksh#X2ZBp#{a!;N+otQU!S}lvS5Tvvl5Ubd2b5Jj5-;BoY_WOF z_XCPI9rvwO_zYof?DOK%D7k0_M-eMq1#4^uYW@wUg*5e?z1mhW|GkISQ*)gK!lPx| zhZQN7o3b?xTTW$o)&y=wPN6(!-WiNpD#qR}nK9og7lxJS9YRlhEp9)yU^-uiJhow- z`8UtZ449xibZb6f>W1(}6}*;8Q}D4jvc47_zV#=gHPpIg&^BV=sY7Dmal^rQ{Rb1n zUwQSwn=K>Hdns)-UfJcmNaEkVZt&=3p#x^9uRr~)MJC(+R7*|u#l#|6Oe!OSxM_Eu zmB;$9eNW8?oI@Ao1juH&%}d;U z?#98zrD2Iola(vNeqXDEj5{li7yeqImbZr^`ax#dw1QXei_~7G_g(WFx2Du3&m=l? z7h;1<#irByqG9b@3u(qlI+?8(e{@D`x>QxAscV^@j}^G0H9KoHh*`OVvLl5^wL?J< z7)$I5W&Q|c2#?m>)|0U<*(h6S(odPBl0+QpHsP-r8hDCI;Xy;ZB-GTjC{Lh z)^{?@)XZUvU2)|rYeZga0RK+{;)>14TJ^#VgLD29(mB!`H~7S*Fw{zJ%hPczWn=cg z8jH%4)vX%o*KhVWOn7IlqI@$mJZW&H8;wZubZI_Uwrk`&rADaRwb@W?@%Lq;XVYdZ zzbfh08?cyaez+qbJi_UZNiw(*%k&9+amj>L{ED$OWuQs3t3SxwFrj;;X7JtUOggr3 z9_gyPyNb>f4!Q6KY~O5*EcJ8lx!Eo+mu1XJ+Yaf*g#ElRyLa`VS#Nr;#Tl#HQCW>m z{&_c0soAKyl5Hh_n6KLo+?X66U)GDrzLZ!MuKsS1=~Z-jmeYyn9r@L5{%zdITF>DU zc(z0NN5gMd71f1LPTcD_?PI}M(r1raF|bl_rTXz3>u}j*j^Bmd){0~OhHAcdT%96T zl^I$j>vYCuJ?O7Db;K6G{^kavEh#naE`IOB!FIb6?Rl2b>{14>p?RueVYk~ro9y;T zIrcx#*ZIGkiL#&hR%UZ~U8&hb7!h+vGUz&Kgw@+NpF@^rzAM$3da`Mn#XcKJdEb+n z%Ja~1JE|B-plr+1ckkS)J%8tndxzxYNf*b|;HiBz2ekdat!a4bi8!V6uKj*dC6Dra z#ewE=I4u9YXWc$ zFQ)EwjtXc}@pjCV#OF{`{F&M=E0)#J@Tkkfv83XA7q4{3`Po^?`^#!I#t(`mS z?yFbdpa!*s0@tn$0{aDCQgU)Bq;savHLt4{2qzE7+ W4I>>0bz>}E>ge79v - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-email.svg b/gno.land/pkg/gnoweb/static/img/ico-email.svg deleted file mode 100644 index ff397fe664d..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-email.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-telegram.svg b/gno.land/pkg/gnoweb/static/img/ico-telegram.svg deleted file mode 100644 index 32932830dc3..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-telegram.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-twitter.svg b/gno.land/pkg/gnoweb/static/img/ico-twitter.svg deleted file mode 100644 index cf666e3842d..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-twitter.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/ico-youtube.svg b/gno.land/pkg/gnoweb/static/img/ico-youtube.svg deleted file mode 100644 index 36efdd185f0..00000000000 --- a/gno.land/pkg/gnoweb/static/img/ico-youtube.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/gno.land/pkg/gnoweb/static/img/list-alt.png b/gno.land/pkg/gnoweb/static/img/list-alt.png deleted file mode 100644 index 14296a4d28f8dc3c4b97fe44cfda47d840d9a430..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^Y#_`5A|IT2?*XJZ3p^r=85p>QL70(Y)*K0-AY*Zm zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#T2B|pkP1fK(}|Kz4gxI?Z?-bD2uw6HyTCEw zL5_lofbDhM=AMIS`JJtN_QL70(Y)*K0-AY*Zm zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#SWg$nkP61q6Bh*?3MZR4#M}@*ZaD@zkU9Lm+Q*q$}{)#zR!8w=RW6g?sGosXsOcEvD1M-AbPduPxU~c z^WcAeg?rKk;ybPdO`@NCEqnaY~ht%%a(PjGBhNknJ9&;J~oD}nSQ_=8?49q^( zG@b8+A3Qc5{&?}q)qqD@ez1mT)S|D=U&Zgv!H1fPR3%LPlG)f2WbV4zodZ_--{1cb z_#XoQZv?)}ov4C9#XqoIbJ1v27xZLxa$_UZ-92D2)IIQo(q+3ET6uD072h#^wsY{- z=s7EX5gsWnf4pcVe@fXQVQNT^dKWpcJEUXu9CmqfV>t};r|*+2gkLE%h3ozm4MP0n z2=s)yHQwvV1CC|i9{qJPJE;_!ePz^+G?R=%`70}30O6eiK8R_5{1MXX))&$$yfr>R z8_tt^k0GLY4auvq`Y3~Y%~x_Goiooz`8b`P8X4Kspsi)Y`?LB zVz)bIXa#IwS`ZeDgs%!K#S#Vitcx-2Nfd*X08cYqi1Zv>)eh9&x&{sY9JosSVI|%X zrfI!)`gtFb-bBj1&dYr$;QemX0H67vR!7w1lliSh#y=0#-)$a8HU@nNb>QUhA#XKA z9`cd1t4s*ez721QyPuyQd|BA{e;s4J)*E@43_o$p(G#q@22;&kNWQnT1}vf`K9ylUSPb*!cwwKyNV=XA)dUw5Y!q$lI_h(SWV!?{#Dlu*}r z{Bh)=#S2iCjqL&gE|+%HCBAfA??ldCpA#-q5rnOmvqCj`i-pP{Znjwtyk&f4dC=;W zO?_gmFBb|eV@+@B>|NQzYAV|M(9(OVr{-zi1EY!>XftgPFyc%xo)n3%8=ba{78`Yq zd74wu81I?!3|zJLJaRM=Mrd{y0ba%HnT=c`ia*O8_L@RJ)GX*-G7nt+6e!$EC@eqT zBPiTaE;V&>bnjtx7VkA~FKmn!>UnANVW{AsQ5!`x17uZfW7g7gHquyItz(7KN3deX z?=*cRtYUtDePodMD$z~j>0`}f2e-g0OCKGW#v9iS%@C&&XcZsJ(GV7l@{Z}5M1NF|K8h(&0y%e^IzBMeF7bim z4<=BV;wevb1+`ZmIrL_gP!U&IVQheZJUJUoQEb$=E+h)nmQoU_&2?&lkS)^G0 z?~iTVJL~CUS>2u`x%k&k--(6gF2}zPURudX7UsZ%7cUnvD!|ml$LcR>0mCi z(m3gElY!jzM(x{bz+PF5$3I&Rh}20+C13#+_1BCX&6>t-ICO(x#59V+>`uo4pHX#$ zklK8AQp-b1STlWkl4+6eSONHkznI2(DOPc%c$v3k0bB3b7Mg6XKN78*iyfGh)1mVF zV5`Nw=qq)TsNo&RsNGNbJlY42ANrJ1rSdEM`xC**2H_2Krw4*wtvRheRI?Ay=JZ!; z0mQ2!gK(LK1dN$jgl#g9b%%)Ahl0G?9^PBy-00A(O}^G8{S|i)5_ zY>iDd)>IxHHCB$`+nA``<4qOSrkju=s>K%bw6BaPcicyUGdp1HN1q=tJ`t%b z*xxpdnYm$)QPDUs*ku_%Iz^`;qch?^vft-s@y-mqopJ-+3tOoHB-&)~^odeIG( zSBQQ1+Em@6P>n@5eaG_*v!jh_p8-kaEX6D87zg9)MxO!ayLCaI(2P`Gozow(J(GQ- zK~<1h9)+zqiz)|8y6IN}hdmV;CuVM@u?B6C9Rh^l3O|%rocB3)7cf8Cn6MX`hNZ$- zm9rkL>mR&t1;pOL1Lx~uPo}}XLD}$2IY+0@g~pIT!*A54QxAggg5!V2`kBWyF*JuxKxHc%D7o3 z*lAJ%U}LX>Pim^a*8g4E{Uo8~H^fM@>tZLUciitf7$l+o-Sy`Q&h8PHiveMO zC*Rs?)I7xf1xNqW@id8Ih&8MD22(^GMUi2qjvq04KOHn8T!KYdyt#0d=Z!ZeA9xo(gOJ73n5x)Ur5CFqlu_mMSG7!zV90S z0U`JilQ-9oH-q*=9pLjy%3|Cuk}5Q3rYVYKuBtI`cFRY_jj6%`qUqVzA(<HCH(^|B|y%hM^23zz{c^Wx4%ZDnn=#gVw(bXWOLie!Nj z1kp$03SFOL?@v>(n{M7NJ>2}0@p(!{sHs!5+tX!nA0AnQp;#Ih-EjEaZ&{VOYk`OyoR`9{mwC_0pg{)Eglvbt{;Dds61WTSL+ZtS};O(iYz~77(kuM_VA{ zM79xT^Vfa-671wi`uk}grhn#JA-kTpxszwwH16J$ws#Di?!JOK>E2bF51#oc{2K-a zaYoRAXLlIOq7B`sJfZ8zNDa!ngV}f7EnOQ==IQoE?ij6IM`Ao-%R+%<60OY(ZY#lJ->)arr>>h%sCzn~6Cf z+;Dh>hpH3e!6uotW1M(Sf0JPtq4hN~O0zMQ8>wkH-mHYkJj%|L3PZ+q)%jRiMT?Qe z^E>)FjwQFHrh4hcs?7DL>}a5bR%7G`ag7|17n_W%yJPMCnjLYJ0xsw7_4tsw?j&+> z1o+xhF8aOjaf6Dzx(3E*E_C}d$D0jKJGq}9t=nG3;UcmYRSw-v&f2C5ryWc7bS@y$ zQrDMV#2geNYJJy~*bUDk^(zn>Fnb|S!1RPhu}IS35=K`i|2ST=UZ*IG~zL@|vqiQL}C<9wB*)@@rW`fjLo734+1 z{Go*A*T^e1A7GI8vxg5Nc3m>-v+E4I>LbVNe7OE0<$;45&RC`D`tjQ=y#VCi3sfZp!P>t@o#BZU5RO4^SyVGwZ1f1 zj)ST)H6jqf(nqYyv;_|~%2`L+kAboAvF=d8tS`G&hu#8rT--+RD)Np~=s(*32yv5( z!YQ-jIF1T%=h`(sn8k*z?JZN=Al#NGO8q#O8)os-J931{_&tmexQ@RO^(C~dAn3p} z#)*g^G*6?$Qo`$FbU_GpSG}$M!)z?}_XWH)pkZ5G*xjZ3LcbfvZC4CEWQwVkWsQ1Ys#S4!^{F8(%46`6ZyY@T$PJ}DD z;KG)1D^VF%-bwYst4*_tT2E~(0NL3_QUCNL19}z$f;wXH&EVxZev&sDRx#XuW;HZN z-d)B-9&&@YWb70N&!uid$Pzuq7A@P=W`t?P9T`Ln#G7~uV|OA!3H{weOqt)0Tu1QU zjlD~4SDFig_5d}ppXw{Na;!_O0AlK_DBZuGr^#+E)dCpC(H&1N<01eS=5cnbBCGPL zyat;WLHGcVk6jm53@rUHQO+7`B@vApU8;xwR7J#z+Ws&Fwz8%X&oA*DdNhhd1Oloq zD#9%04m!6=_Vh-BgC+GxPcp<{7MtGPuvcw>l5lhHsUocMpy4_e@b5xa1?6P&q?3NS zYnz4{tR#ZJZ)0|ilBa^TDiSFu{(GIKv6VaOOH*83?0i%fx%#Y7S(e}jj?55mn6Iw> z77%p$`XA-qMi0o`_`}{?w)2;LMa<5^7M?zusVvkx@bO(>;vDse+pnZsl_3K6W4HFN zOi$nQ?|U#D=`VO(!a<>swa7)xZaTyV8A7g7{Q8xLe2=y(v`D1>E{#T0>=PvB&;mb) z(Rq(6bX7WPB<0`bvanw2Np50gd_`+7bs$V9D_bd2ovnZ7s$x2w)`a?+``rIx@5^ML z&l#dI%cWw*7NAt=Zq?rpD>KWw7|jw=Yek%e`#M>NoL=y1X|e{&aX3YdUHo!QM)$%X zrOe)?O08`^`8QuwhDcGy!CHKo=&lXRn?P3^=J;U{@imb`upHBS-PvT zzuD`V-ECvq3&p&1zkO_9%=r3HXgN77=c{eD@TxXVvA)Tg0rop2qHNBG=Mku44z9an zsx{`^mqy1RDG092%nhL_CT`?Se42}OB1s#vRLm@Jujipm2(u{hhtk%pBUtfKt8+RX z;yW^zt~ErAB$Nr8;{lO#f+cbLdGkW;EsGOjIYkA*#GaMTg5`r_T zV+~^#3BI^A!JSMop9YPt?(ex-rC)pxF}E3|EAQGX#nM>Uf_WYqve z+a=uUU^OZq0o8Om?#hmSF-uI`{kuVf|MQ79&fGG0EGvY;~ES1u}_ey;3C4t0zV!8vAJfwgG0+btaX*kKW{4%e+2!Cq^? zocyZMBW*zm(-G~@Nr>kFQ15GbY4I~@L$$#uYxU5GLk1srQ_UOYr%Fxm566Jxe5MBO zPayD`Od$3h%j?UzugYU1dBAN0%4)1%lsSj84nI$4)+U+lZ#q$S1*?PSMPBo2Lx>rYQ>R zq_8??DYjLZcxABu6Wtw8U_2`El^kUGF`%1~NSX0>rE|jv$S>}*D)!}BpI$ZqdcAWO zxZ`+yqho-qG8p-KkE;D^N7+^F+QJ@K}7@#}+DNDI~IANcbLuJOdZKZL9SyZol1Hux`v_41WFr7!YR z48k=l9O*7s@zVYg&D@n8QM3!$qM4Qf0#bf4+8fNY=qLa$dipIloaVRQK-~Po z-}l4>E=Y`*XNeRvdxVKnP7m`?*S*IB2B8H-hC_PwKyf-{F;+~3gCn$XKuWShmai8a47Ef3l=Sg z=q+A`U)FxwYc-#OGgYTMOu!u{0dBLJG^q(me@m!9|h?lWn zKj;rri$++Ls?8(|KH{-HRK;T-ob$M*e+2k7zD4*iaW&tNa?26!aWTLVNpC57#OSq* z9mz0-q)1yJ7#`DixnB7Wca=b0!{vErX^L1s>@cicF0fhvtg@aH;;c9%yaTW?La3#g z=8e)a>$K_+rK{g5$;y2eM{^p(z8Qdu$eoq}l|h*q9c*+)QCSy@(@3(QewS$Je3|1b zh5iG!!rdb9cT!je;73r$;T0zvDNNydS%Osx<|b0Gs$z;zIp5WuWWglS0n^`ImquOG zTf*OP`QAe7;`3AP30rwpAmvmBi_!tdT(*@-2PGKHr{W2&=AUCwKZ5&Z8eR7#UV?Zao1 z_w}PPFkd^3W^@W-lVQm&0?r|rG-)C6)H{J?E6IBCMc6uFnZRvX?$Q&+-Xl(6KE8szIU)qhr6+zoCz%+37we6joxcywf zV|L26i%ut5EIhT=B)xq2%t^zRU;z8&EoM)RnAo^tu8LI|ZrC_^#1Mf=!|m5mTLUI# znBrvYr$a_Hbawy>QYznN$SUxv=}uz=)rm);{Z2gmSYuefF*y~zO=V6L(?f%|Eshs1iswkPKU&8(?Lx%KiP4*0as;-;T98ueaaX?{gwxR)52yd zO*&Fb7v4(4aM;O3Y|4}4g81h2i)~pJOHY=y1BHRP$DN4D|9H@@4%%b&vJGe`=l^2s zfLfO~pltf=QN?63z!AsepSC+bClV8w1p|b6z)DgPShiPxs{1AW>smrid6yvOIN>tg zNkVZk1!haAU(!O^-iqM7RFhh|C{lp5yO}CkK#FgMZO@gT17P8-XF0F~JjX=$-{~O5 z0$9s~rL?e|rJ0^8S2eGf#Q43&4))YTH8Ua)7e}I z@Cv|JBhzVp!Ab^lg%)pI`0xK%#i4Kq(+a0qodvtqX0Hb8uWw=gF#~ex)o8p3BbpD~ zvYRuXo2CI#Nx#I_7-t#H964b1?$oF;zaxxUGu?h~V_1PU4EcjgScp^+%h7)nufoUy(X?tePy5coinXMR_603O>Be1Z0PH$Ww2v{YT z@9j?B5)Bu$a8!npO~8lDT6bhPN2VxaTh_PDmd(U!$LiJ3Qx_X+w_Ep2FAUBXYm{0w zsoIlu0d^d&UvBN2^MX^;WC8*(E(GgbZT|R+^hZ^R50lx3Eqq>kKnku9ApBXGa|UDs zKHC1nW_m6$JC4$;q3Rd^WiZCff_Pi|H3tv*9UE)prt<$2y&)Lx7IkGWq$7+zU-hw|HwDY;mCRRi!^ z>;le;9oy5=b{zF3-6gb8HReFdCWck@gl2C72sn#X(fQW2yvrh`ba4#|9o%G-a*86; z-d+(OxJR0Lhybk&?*NIKQW38O764LYO_;1Z=DZCgj@k9RDI!XbY?z9B7*&R|&<`d{`WPe1gY0|OOE9Z!Ks;%n;w$aZES?(Fr!csa>!8YMg=H~W zeDpenNW~_I?iz|pJ)@D-L5csIh#o4*x)6}DJw+9d%BX||d{A;k)R*`~FUtZu+3@6R znfQ|VTY{?%NfJFdzDO17t+NG)5M%JI`J;W6At=6xaIoyn3!Db2=PA8`x8EEkv!p>b z+y9$;+Rd=e7ij73c;Pc1tm=7^5EWrLhZ;xQ1`F4lX+{CI0mp{mT*^f(k=2-1>g!wB zaXs}G>Rs~7J-5%^d|=}40VqWw8VXv`qZ2`l#v+sT?bjtxM`&w+yJ}lQNOo;&e(Ljr zhKny)`_GRNt6b~(XzB>Py6N(enreHMoEJWmAwG@UV2HD&Wl4?6&RdDG@pC8%08jZ; z*g)3axzR%Kx0d0WMU_F@hogXd`XR%VYBXIXZBhwg3YnyMFKV#{n%ncMN9JbJ6zc-{ z2xFk+LP>FYSMoucIMK|I$Pr=7!O0jSed!F$egIxH%~RDP8~fZCOMfK>+f zSaDe9W{0DB4IZ#cWhodsumtiIpjO{q+GEcvrrcA{wMTy6-tVD6@$Q*$@giEJI%}YX zJ%7Y=_BV1c+%Tx;3BWv}lrfDG5&HD$j&b<%5%}E7A@dLzF>_a6Y0>Pq;bnd0&x6L*S^Hn*|8y%%qbu%Xw0c)D`0v6+ znlH^+!meF}Zs@v%gBzx8+*`}(>)jNb z0tIo?b+;~=C51T00ODRN2+i)q`6Wo!F7WLbPm^Pg(u&GeWvt!R=gboJD?S4=UeZuK z)X)6+^Q}GPT?Lviv4J@@-74c?j`C?QFk~g6Tve%VL)*yuV_43_Q`N&n?)?)QOvdc7 ztQ5bez&7XXz=h`Wo94}DkLvo1{Tl7Yn&u=^s<~ke5dYb$h+8z2m3+E2ykJIM%vsOniq!sJ~p)(pn$g$>+y}4 zDXZ8LObFAA{w_^XH{woY@J)N>wv=Z&(0i?FT=&Id(zO#z$&cYEIp7-7Hy9#4W8VM8 zA1Jhh(u*y$O7|I@PTNd2V&vCTm_E3;?Lf{t+_rtZ@-FP{@{hayq@y~f8fne*$NFB` zU=tUiWp0T)9GSyH$fAnfp^oc~c8lLytK-{)N7sb2`*>mE2eJE6mBd3#Z2a@ZLn5KQpNOt;5!Q)xveWxt{S z+EosmRGUcleDg9+?%MZvb9W&H0UMd$^0)7Lu>{s=GoW=^aN2G)dQv=K;F?jQmA;5| zXEgmq`sKq8`O`AUTYlOn%P0F1_O@@Blh3u!aEGv@R%3#Bnu0)k6k$QqH8YJ)9ao9# z&rzMK;P@(~PFP2>$t+jAQjvK`pX<*NufQ`95Z_)!spiRNT9tMhqs0@dKHPUhTJ@@5Z#fMMT2rbUz zz>htQYUdx9OK;t6%ulSlrn3ZZrP|8JhyLZA%-Gf~i9uBN{rc3j{YbW830ngGl{NvrOY zD5E|PJxGesqwyzEy2LT1h)2ZU!1|!Kqvqqja!|7tbCe-~R8&6AmuH!ghx*3KYRsu2 zG$ea1#(p&Qnwq6_g5+@AdQy09L0C@OT_;enF5D!mlc7I*#iGI;;LpADwpahi=w8vJ zs^9({wgAl4@AUZ=2w9_a&Fto`c!dRs3RsIcmmo;!tU-#!ZKMGw-SWUAL@oLsIp<7k zzY3~Lo4x1((XXf(1n!DXv&ms)u!Xf344Tfm1RJL$;%2C+CTkQmA!Pr_xzW5Af0vL* z&t-Rs~TQfYKSqjzZXIoKTTbH?1I$c``;3DqtQHP)~d{MQ(i~f~9evsfU z@gn^`=^C}Lv%u9WyZf?b4XLw%cEi-H^P7>J90$GBi1c!gG@oax$7%!HzFMz{zBaW_ zy8gB|n|@Z9Px3ztk(Og$k+iq;inL5t%d%*!hZ_0&rK2&LD-!mnEoySINL*?w7pK$U zKNz!xs#p?Dmx`ARO!hu^N~H){ySKgGAYXK@JQEDXCiP*s zU&zh;;W(02zXK>#SBUiRjjlx3j-_hFitQc3Ejf?d5hpcxuvlvJ+?=pCaptLum34qy z)YzreDl3DDi${e(tAyg2bl2(AABK)5MXVTEEhj;(Uw?U^13DitXavF7AiymXsL*Is z(bfsp5QXYWY;9j4PP1gyjFY%~y|w~Rgm%92IS!A|b$RP&JMgdC&M}k;d?!n%J2p3E z>y>(}0eu<-#!~u|(bMiq@k?aKq5SbMYxkt4Csc?BXUhOgU6-lV&C2!BMMbxy=D7Fv zvMr-QvqmF<+Q8x2VL8n*?^0bophY4Q9VGuC^g~)3I2CRBr_SVJOSQ4+Kai}?VKZv* zK0ygax!ZADC$_3Y5PCud;MR46C%D&{dP*@r9(;9$G-;b%RGe-0%#@Dj4aNY?TJHh( zZVI%yz*gJ-HGh>=!af!?M(62O_R@8c)jcOdQf=tuC+D4{)IXmKHc$VOer{1}-U+iM zT-GDJbDX}WYQ$q5Kum_$>&-r{iq@hN&f%H1dUkHeezilv8skaygR{T)e;Wnx7{T=s zeD&u;=snYo(K)oJi z8KwDpk`GqbfV2S%u}XBr)Z;1wqgw(}jT6M8zTz2qbtmi)Jp+;d?RV32RVLRvl@1~} zZD04@mv?D{uL4wSGTiQbS(O4ZqwF zIWf)$R80Y4m8_yPjgy$I2)#h-$%1F=*x$O|#^^q|fUz5WU3%PV52t{YiT_r1NUA6N zVZ|>+57_o02+r|R-9YRL0DFAn_TJWU80yEk3SJM^@MQTs0s*RXiXDuZ-=4`hEvmr4 zE=GxwdK<6iXZ9MSJz0mG2TpWSB1^-yzVtR?gR46=A9#m!1FIbTTgCbLZI};Ofz5_t zlLVKWS>o%pZfxBC6bo1XPub(vI|uNUvBX^XFA5}WO9jzZvX2VX%ArTP!E0oYxf38>0d`l8z%Vk8%51@E4?UgZ_-1D zb*|7;R_H4>l2UI&{Fymn@l%ns>1k6P+oAYE>RDlj(&?0a$hGC}&@jWbM_&*OV)D;0 zm^TH`jO{tC<>TkqK*mS}ZZjBM$(@k4J@@xj;K?3iGoQ6F3URsij1n!+%b(~*_-f=Jo-v(~05&L8=&N(YMd;G!N<|+2ieI794NFIK8 zvUp0Om|WY5ckIaK3|)Ve4Uyyif4BJl@0b4}@IM6pe*}uh&Mv2942atL<*597sHURz Kv_kpSyZ;Y(*HHTa diff --git a/gno.land/pkg/gnoweb/static/img/logo-square.svg b/gno.land/pkg/gnoweb/static/img/logo-square.svg deleted file mode 100644 index e6ab42f5ad4..00000000000 --- a/gno.land/pkg/gnoweb/static/img/logo-square.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/gno.land/pkg/gnoweb/static/img/logo-v1.png b/gno.land/pkg/gnoweb/static/img/logo-v1.png deleted file mode 100644 index 702fce47a52b10bf195d8a624f925584162082e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11122 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 diff --git a/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg b/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg deleted file mode 100644 index 0005420c58d..00000000000 --- a/gno.land/pkg/gnoweb/static/img/safari-pinned-tab.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - -Created by potrace 1.14, written by Peter Selinger 2001-2017 - - - - - diff --git a/gno.land/pkg/gnoweb/static/invites.txt b/gno.land/pkg/gnoweb/static/invites.txt deleted file mode 100644 index 7bef15f954f..00000000000 --- a/gno.land/pkg/gnoweb/static/invites.txt +++ /dev/null @@ -1,48 +0,0 @@ -g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1 -g13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1 -g1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1 -g1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1 -g18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1 -g19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1 -g187982000zsc493znqt828s90cmp6hcp2erhu6m:1 -g1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1 -g16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1 -g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1 -g1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1 -g1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1 -g19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1 -g1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1 -g14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1 -g1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1 -g15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1 -g1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1 -g1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1 -g1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1 -g152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1 -g1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1 -g1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1 -g1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1 -g1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1 -g1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1 -g13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1 -g19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1 -g1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1 -g1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1 -g19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1 -g1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1 -g13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1 -g1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1 -g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1 -g1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1 -g1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1 -g1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1 -g14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1 -g19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1 -g1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1 -g1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1 -g1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1 -g1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1 -g1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1 -g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq:10 -g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10 -g14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5 diff --git a/gno.land/pkg/gnoweb/static/js/highlight.min.js b/gno.land/pkg/gnoweb/static/js/highlight.min.js deleted file mode 100644 index 5135b77ab5b..00000000000 --- a/gno.land/pkg/gnoweb/static/js/highlight.min.js +++ /dev/null @@ -1,331 +0,0 @@ -/*! - Highlight.js v11.9.0 (git: b7ec4bfafc) - (c) 2006-2024 undefined and other contributors - License: BSD-3-Clause - */ - var hljs=function(){"use strict";function e(t){ - return t instanceof Map?t.clear=t.delete=t.set=()=>{ - throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{ - throw Error("set is read-only") - }),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{ - const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i) - })),t}class t{constructor(e){ - void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} - ignoreMatch(){this.isMatchIgnored=!0}}function n(e){ - return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") - }function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] - ;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope - ;class o{constructor(e,t){ - this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ - this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{ - if(e.startsWith("language:"))return e.replace("language:","language-") - ;if(e.includes(".")){const n=e.split(".") - ;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") - }return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)} - closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ - this.buffer+=``}}const r=(e={})=>{const t={children:[]} - ;return Object.assign(t,e),t};class a{constructor(){ - this.rootNode=r(),this.stack=[this.rootNode]}get top(){ - return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ - this.top.children.push(e)}openNode(e){const t=r({scope:e}) - ;this.add(t),this.stack.push(t)}closeNode(){ - if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ - for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} - walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ - return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), - t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ - "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ - a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e} - addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ - this.closeNode()}__addSublanguage(e,t){const n=e.root - ;t&&(n.scope="language:"+t),this.add(n)}toHTML(){ - return new o(this,this.options).value()}finalize(){ - return this.closeAllNodes(),!0}}function l(e){ - return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")} - function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")} - function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{ - const t=e[e.length-1] - ;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} - })(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"} - function p(e){return RegExp(e.toString()+"|").exec("").length-1} - const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ - ;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n - ;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break} - s+=i.substring(0,e.index), - i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0], - "("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)} - const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={ - begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'", - illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", - contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t, - contains:[]},n);s.contains.push({scope:"doctag", - begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", - end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) - ;const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) - ;return s.contains.push({begin:h(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s - },S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({ - __proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{ - scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N, - C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number", - begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{ - "on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ - t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E, - MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0}, - NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w, - PHRASAL_WORDS_MODE:{ - begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ - },QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, - end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]}, - RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", - SHEBANG:(e={})=>{const t=/^#![ ]*\// - ;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t, - end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, - TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x, - UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){ - "."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){ - void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){ - t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", - e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, - void 0===e.relevance&&(e.relevance=0))}function L(e,t){ - Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){ - if(e.match){ - if(e.begin||e.end)throw Error("begin & end are not supported with match") - ;e.begin=e.match,delete e.match}}function P(e,t){ - void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return - ;if(e.starts)throw Error("beforeMatch cannot be used with starts") - ;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] - })),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={ - relevance:0,contains:[Object.assign(n,{endsParent:!0})] - },e.relevance=0,delete n.beforeMatch - },H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword" - ;function $(e,t,n=C){const i=Object.create(null) - ;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{ - Object.assign(i,$(e[n],t,n))})),i;function s(e,n){ - t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") - ;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ - return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{ - console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{ - z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) - },K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],o={},r={} - ;for(let e=1;e<=t.length;e++)r[e+i]=s[e],o[e+i]=!0,i+=p(t[e-1]) - ;e[n]=r,e[n]._emit=o,e[n]._multi=!0}function Z(e){(e=>{ - e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, - delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ - _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope - }),(e=>{if(Array.isArray(e.begin)){ - if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), - K - ;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"), - K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{ - if(Array.isArray(e.end)){ - if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"), - K - ;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"), - K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){ - function t(t,n){ - return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) - }class n{constructor(){ - this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} - addRule(e,t){ - t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), - this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) - ;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|" - }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex - ;const t=this.matcherRe.exec(e);if(!t)return null - ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] - ;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){ - this.rules=[],this.multiRegexes=[], - this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ - if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n - ;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), - t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ - return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ - this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ - const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex - ;let n=t.exec(e) - ;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ - const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} - return n&&(this.regexIndex+=n.position+1, - this.regexIndex===this.count&&this.considerAll()),n}} - if(e.compilerExtensions||(e.compilerExtensions=[]), - e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") - ;return e.classNameAliases=i(e.classNameAliases||{}),function n(o,r){const a=o - ;if(o.isCompiled)return a - ;[I,B,Z,D].forEach((e=>e(o,r))),e.compilerExtensions.forEach((e=>e(o,r))), - o.__beforeBegin=null,[T,L,P].forEach((e=>e(o,r))),o.isCompiled=!0;let c=null - ;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords), - c=o.keywords.$pattern, - delete o.keywords.$pattern),c=c||/\w+/,o.keywords&&(o.keywords=$(o.keywords,e.case_insensitive)), - a.keywordPatternRe=t(c,!0), - r&&(o.begin||(o.begin=/\B|\b/),a.beginRe=t(a.begin),o.end||o.endsWithParent||(o.end=/\B|\b/), - o.end&&(a.endRe=t(a.end)), - a.terminatorEnd=l(a.end)||"",o.endsWithParent&&r.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+r.terminatorEnd)), - o.illegal&&(a.illegalRe=t(o.illegal)), - o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{ - variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{ - starts:e.starts?i(e.starts):null - }):Object.isFrozen(e)?i(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,a) - })),o.starts&&n(o.starts,r),a.matcher=(e=>{const t=new s - ;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" - }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" - }),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ - return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ - constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} - const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{ - const i=Object.create(null),s=Object.create(null),o=[];let r=!0 - ;const a="Could not find the language '{}', did you forget to load/include a language module?",l={ - disableAutodetect:!0,name:"Plain text",contains:[]};let p={ - ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, - languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", - cssSelector:"pre code",languages:null,__emitter:c};function b(e){ - return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s="" - ;"object"==typeof t?(i=e, - n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."), - G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), - s=e,i=t),void 0===n&&(n=!0);const o={code:i,language:s};N("before:highlight",o) - ;const r=o.result?o.result:E(o.language,o.code,n) - ;return r.code=o.code,N("after:highlight",r),r}function E(e,n,s,o){ - const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R) - ;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n="" - ;for(;t;){n+=R.substring(e,t.index) - ;const s=_.case_insensitive?t[0].toLowerCase():t[0],o=(i=s,N.keywords[i]);if(o){ - const[e,i]=o - ;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{ - const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0] - ;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i - ;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{ - if(""===R)return;let e=null;if("string"==typeof N.subLanguage){ - if(!i[N.subLanguage])return void M.addText(R) - ;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top - }else e=x(R,N.subLanguage.length?N.subLanguage:null) - ;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language) - })():l(),R=""}function u(e,t){ - ""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1 - ;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue} - const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}} - function h(e,t){ - return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope), - e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), - R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{ - value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t) - ;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e) - ;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){ - for(;e.endsParent&&e.parent;)e=e.parent;return e}} - if(e.endsWithParent)return f(e.parent,n,i)}function b(e){ - return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){ - const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const o=N - ;N.endScope&&N.endScope._wrap?(g(), - u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(), - d(N.endScope,e)):o.skip?R+=t:(o.returnEnd||o.excludeEnd||(R+=t), - g(),o.excludeEnd&&(R=t));do{ - N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent - }while(N!==s.parent);return s.starts&&h(s.starts,e),o.returnEnd?0:t.length} - let w={};function y(i,o){const a=o&&o[0];if(R+=i,null==a)return g(),0 - ;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===a){ - if(R+=n.slice(o.index,o.index+1),!r){const t=Error(`0 width match regex (${e})`) - ;throw t.languageName=e,t.badRule=w.rule,t}return 1} - if(w=o,"begin"===o.type)return(e=>{ - const n=e[0],i=e.rule,s=new t(i),o=[i.__beforeBegin,i["on:begin"]] - ;for(const t of o)if(t&&(t(e,s),s.isMatchIgnored))return b(n) - ;return i.skip?R+=n:(i.excludeBegin&&(R+=n), - g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(o) - ;if("illegal"===o.type&&!s){ - const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') - ;throw e.mode=N,e}if("end"===o.type){const e=m(o);if(e!==ee)return e} - if("illegal"===o.type&&""===a)return 1 - ;if(I>1e5&&I>3*o.index)throw Error("potential infinite loop, way more iterations than matches") - ;return R+=a,a.length}const _=O(e) - ;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"') - ;const v=V(_);let k="",N=o||v;const S={},M=new p.__emitter(p);(()=>{const e=[] - ;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope) - ;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{ - if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){ - I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A - ;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(A,e.index),e) - ;A=e.index+t}y(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e, - value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){ - if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n), - illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A, - context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(r)return{ - language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N} - ;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{ - const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)} - ;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1))) - ;s.unshift(n);const o=s.sort(((e,t)=>{ - if(e.relevance!==t.relevance)return t.relevance-e.relevance - ;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 - ;if(O(t.language).supersetOf===e.language)return-1}return 0})),[r,a]=o,c=r - ;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{ - let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" - ;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1]) - ;return t||(X(a.replace("{}",n[1])), - X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} - return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return - ;if(N("before:highlightElement",{el:e,language:n - }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) - ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), - console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), - console.warn("The element with unescaped HTML:"), - console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) - ;t=e;const i=t.textContent,o=n?m(i,{language:n,ignoreIllegals:!0}):x(i) - ;e.innerHTML=o.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n - ;e.classList.add("hljs"),e.classList.add("language-"+i) - })(e,n,o.language),e.result={language:o.language,re:o.relevance, - relevance:o.relevance},o.secondBest&&(e.secondBest={ - language:o.secondBest.language,relevance:o.secondBest.relevance - }),N("after:highlightElement",{el:e,result:o,text:i})}let y=!1;function _(){ - "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0 - }function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]} - function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ - s[e.toLowerCase()]=t}))}function k(e){const t=O(e) - ;return t&&!t.disableAutodetect}function N(e,t){const n=e;o.forEach((e=>{ - e[n]&&e[n](t)}))} - "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ - y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_, - highlightElement:w, - highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"), - G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)}, - initHighlighting:()=>{ - _(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, - initHighlightingOnLoad:()=>{ - _(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") - },registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){ - if(W("Language definition for '{}' could not be registered.".replace("{}",e)), - !r)throw t;W(t),s=l} - s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{ - languageName:e})},unregisterLanguage:e=>{delete i[e] - ;for(const t of Object.keys(s))s[t]===e&&delete s[t]}, - listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, - autoDetection:k,inherit:Q,addPlugin:e=>{(e=>{ - e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ - e["before:highlightBlock"](Object.assign({block:t.el},t)) - }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ - e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),o.push(e)}, - removePlugin:e=>{const t=o.indexOf(e);-1!==t&&o.splice(t,1)}}),n.debugMode=()=>{ - r=!1},n.safeMode=()=>{r=!0},n.versionString="11.9.0",n.regex={concat:h, - lookahead:g,either:f,optional:d,anyNumberOfTimes:u} - ;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n - },ne=te({});return ne.newInstance=()=>te({}),ne}() - ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `go` grammar compiled for Highlight.js 11.9.0 */ - (()=>{var e=(()=>{"use strict";return e=>{const n={ - keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], - type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], - literal:["true","false","iota","nil"], - built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] - };return{name:"Go",aliases:["golang"],keywords:n,illegal:"{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],n={ - scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",keywords:{ - literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/, - relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0 - },e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], - illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `plaintext` grammar compiled for Highlight.js 11.9.0 */ - (()=>{var t=(()=>{"use strict";return t=>({name:"Plain text", - aliases:["text","txt"],disableAutodetect:!0})})() - ;hljs.registerLanguage("plaintext",t)})(); \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static/js/marked.min.js b/gno.land/pkg/gnoweb/static/js/marked.min.js deleted file mode 100644 index 3cc149db48e..00000000000 --- a/gno.land/pkg/gnoweb/static/js/marked.min.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * marked v12.0.2 - a markdown parser - * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=t[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1");e=x(e.replace(/^ *>[ \t]?/gm,""),"\n");const n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'

'+(n?e:c(e,!0))+"
\n":"
"+(n?e:c(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); -/** - * Minified by jsDelivr using Terser v5.19.2. - * Original file: /npm/marked-highlight@2.1.1/lib/index.umd.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).markedHighlight={})}(this,(function(e){"use strict";function t(e){return(e||"").match(/\S*/)[0]}function n(e){return t=>{"string"==typeof t&&t!==e.text&&(e.escaped=!0,e.text=t)}}const i=/[&<>"']/,o=new RegExp(i.source,"g"),r=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,g=new RegExp(r.source,"g"),h={"&":"&","<":"<",">":">",'"':""","'":"'"},s=e=>h[e];function c(e,t){if(t){if(i.test(e))return e.replace(o,s)}else if(r.test(e))return e.replace(g,s);return e}e.markedHighlight=function(e){if("function"==typeof e&&(e={highlight:e}),!e||"function"!=typeof e.highlight)throw new Error("Must provide highlight function");return"string"!=typeof e.langPrefix&&(e.langPrefix="language-"),{async:!!e.async,walkTokens(i){if("code"!==i.type)return;const o=t(i.lang);if(e.async)return Promise.resolve(e.highlight(i.text,o,i.lang||"")).then(n(i));const r=e.highlight(i.text,o,i.lang||"");if(r instanceof Promise)throw new Error("markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.");n(i)(r)},renderer:{code(n,i,o){const r=t(i),g=r?` class="${e.langPrefix}${c(r)}"`:"";return n=n.replace(/\n$/,""),`
    ${o?n:c(n,!0)}\n
    `}}}}})); -//# sourceMappingURL=/sm/3bfb625a4ed441ddc1f215743851a4b727156eef53b458bd31c51a627ce891c9.map \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static/js/purify.min.js b/gno.land/pkg/gnoweb/static/js/purify.min.js deleted file mode 100644 index ed613fcc36f..00000000000 --- a/gno.land/pkg/gnoweb/static/js/purify.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! @license DOMPurify 2.3.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.6/LICENSE */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,(function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,i=Object.freeze,a=Object.seal,l=Object.create,c="undefined"!=typeof Reflect&&Reflect,s=c.apply,u=c.construct;s||(s=function(e,t,n){return e.apply(t,n)}),i||(i=function(e){return e}),a||(a=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),z=a(/^data-[\-\w.\u00B7-\uFFFF]/),B=a(/^aria-[\-\w]+$/),P=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),G=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),W=a(/^html$/i),q="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function Y(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:K(),n=function(t){return e(t)};if(n.version="2.3.6",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,a=t.DocumentFragment,l=t.HTMLTemplateElement,c=t.Node,s=t.Element,u=t.NodeFilter,m=t.NamedNodeMap,A=void 0===m?t.NamedNodeMap||t.MozNamedAttrMap:m,$=t.HTMLFormElement,X=t.DOMParser,Z=t.trustedTypes,J=s.prototype,Q=w(J,"cloneNode"),ee=w(J,"nextSibling"),te=w(J,"childNodes"),ne=w(J,"parentNode");if("function"==typeof l){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=V(Z,r),ie=oe?oe.createHTML(""):"",ae=o,le=ae.implementation,ce=ae.createNodeIterator,se=ae.createDocumentFragment,ue=ae.getElementsByTagName,me=r.importNode,fe={};try{fe=x(o).documentMode?o.documentMode:{}}catch(e){}var de={};n.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==fe;var pe=H,he=U,ge=z,ye=B,ve=j,be=G,Te=P,Ne=null,Ae=E({},[].concat(Y(k),Y(S),Y(_),Y(O),Y(M))),Ee=null,xe=E({},[].concat(Y(L),Y(R),Y(I),Y(F))),we=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ke=null,Se=null,_e=!0,De=!0,Oe=!1,Ce=!1,Me=!1,Le=!1,Re=!1,Ie=!1,Fe=!1,He=!1,Ue=!0,ze=!0,Be=!1,Pe={},je=null,Ge=E({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),We=null,qe=E({},["audio","video","img","source","image","track"]),Ye=null,Ke=E({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",$e="http://www.w3.org/2000/svg",Xe="http://www.w3.org/1999/xhtml",Ze=Xe,Je=!1,Qe=void 0,et=["application/xhtml+xml","text/html"],tt="text/html",nt=void 0,rt=null,ot=o.createElement("form"),it=function(e){return e instanceof RegExp||e instanceof Function},at=function(e){rt&&rt===e||(e&&"object"===(void 0===e?"undefined":q(e))||(e={}),e=x(e),Ne="ALLOWED_TAGS"in e?E({},e.ALLOWED_TAGS):Ae,Ee="ALLOWED_ATTR"in e?E({},e.ALLOWED_ATTR):xe,Ye="ADD_URI_SAFE_ATTR"in e?E(x(Ke),e.ADD_URI_SAFE_ATTR):Ke,We="ADD_DATA_URI_TAGS"in e?E(x(qe),e.ADD_DATA_URI_TAGS):qe,je="FORBID_CONTENTS"in e?E({},e.FORBID_CONTENTS):Ge,ke="FORBID_TAGS"in e?E({},e.FORBID_TAGS):{},Se="FORBID_ATTR"in e?E({},e.FORBID_ATTR):{},Pe="USE_PROFILES"in e&&e.USE_PROFILES,_e=!1!==e.ALLOW_ARIA_ATTR,De=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ce=e.SAFE_FOR_TEMPLATES||!1,Me=e.WHOLE_DOCUMENT||!1,Ie=e.RETURN_DOM||!1,Fe=e.RETURN_DOM_FRAGMENT||!1,He=e.RETURN_TRUSTED_TYPE||!1,Re=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,ze=!1!==e.KEEP_CONTENT,Be=e.IN_PLACE||!1,Te=e.ALLOWED_URI_REGEXP||Te,Ze=e.NAMESPACE||Xe,e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&it(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Qe=Qe=-1===et.indexOf(e.PARSER_MEDIA_TYPE)?tt:e.PARSER_MEDIA_TYPE,nt="application/xhtml+xml"===Qe?function(e){return e}:h,Ce&&(De=!1),Fe&&(Ie=!0),Pe&&(Ne=E({},[].concat(Y(M))),Ee=[],!0===Pe.html&&(E(Ne,k),E(Ee,L)),!0===Pe.svg&&(E(Ne,S),E(Ee,R),E(Ee,F)),!0===Pe.svgFilters&&(E(Ne,_),E(Ee,R),E(Ee,F)),!0===Pe.mathMl&&(E(Ne,O),E(Ee,I),E(Ee,F))),e.ADD_TAGS&&(Ne===Ae&&(Ne=x(Ne)),E(Ne,e.ADD_TAGS)),e.ADD_ATTR&&(Ee===xe&&(Ee=x(Ee)),E(Ee,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&E(Ye,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(je===Ge&&(je=x(je)),E(je,e.FORBID_CONTENTS)),ze&&(Ne["#text"]=!0),Me&&E(Ne,["html","head","body"]),Ne.table&&(E(Ne,["tbody"]),delete ke.tbody),i&&i(e),rt=e)},lt=E({},["mi","mo","mn","ms","mtext"]),ct=E({},["foreignobject","desc","title","annotation-xml"]),st=E({},S);E(st,_),E(st,D);var ut=E({},O);E(ut,C);var mt=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Xe,tagName:"template"});var n=h(e.tagName),r=h(t.tagName);if(e.namespaceURI===$e)return t.namespaceURI===Xe?"svg"===n:t.namespaceURI===Ve?"svg"===n&&("annotation-xml"===r||lt[r]):Boolean(st[n]);if(e.namespaceURI===Ve)return t.namespaceURI===Xe?"math"===n:t.namespaceURI===$e?"math"===n&&ct[r]:Boolean(ut[n]);if(e.namespaceURI===Xe){if(t.namespaceURI===$e&&!ct[r])return!1;if(t.namespaceURI===Ve&&!lt[r])return!1;var o=E({},["title","style","font","a","script"]);return!ut[n]&&(o[n]||!st[n])}return!1},ft=function(e){p(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},dt=function(e,t){try{p(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(Ie||Fe)try{ft(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},pt=function(e){var t=void 0,n=void 0;if(Re)e=""+e;else{var r=g(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===Qe&&(e=''+e+"");var i=oe?oe.createHTML(e):e;if(Ze===Xe)try{t=(new X).parseFromString(i,Qe)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?"":i}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(o.createTextNode(n),a.childNodes[0]||null),Ze===Xe?ue.call(t,Me?"html":"body")[0]:Me?t.documentElement:a},ht=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},gt=function(e){return e instanceof $&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof A)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},yt=function(e){return"object"===(void 0===c?"undefined":q(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":q(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},vt=function(e,t,r){de[e]&&f(de[e],(function(e){e.call(n,t,r,rt)}))},bt=function(e){var t=void 0;if(vt("beforeSanitizeElements",e,null),gt(e))return ft(e),!0;if(g(e.nodeName,/[\u0080-\uFFFF]/))return ft(e),!0;var r=nt(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:r,allowedTags:Ne}),!yt(e.firstElementChild)&&(!yt(e.content)||!yt(e.content.firstElementChild))&&T(/<[/\w]/g,e.innerHTML)&&T(/<[/\w]/g,e.textContent))return ft(e),!0;if("select"===r&&T(/

    w%k_tC9o+J?jlEWgdzIe0b`MG#H=N&ZEd~ha9 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..53d081f3a538a63578c15a5cc11219b32e6d5795 GIT binary patch literal 12764 zcmV<2F(b}*Pew8T0RR9105RME4gdfE09y0_05O380RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/gno.land/pkg/gnoweb/public/imgs/gnoland.svg b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg new file mode 100644 index 00000000000..30d2f3ef56a --- /dev/null +++ b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gno.land/pkg/gnoweb/public/js/copy.js b/gno.land/pkg/gnoweb/public/js/copy.js new file mode 100644 index 00000000000..73cd1f9fdc2 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/copy.js @@ -0,0 +1 @@ +var s=class n{DOM;static FEEDBACK_DELAY=1500;btnClicked=null;btnClickedIcons=[];static SELECTORS={button:"[data-copy-btn]",icon:"[data-copy-icon] > use",content:t=>`[data-copy-content="${t}"]`};constructor(){this.DOM={el:document.querySelector("main")},this.DOM.el?this.init():console.warn("Copy: Main container not found.")}init(){this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("click",this.handleClick.bind(this))}handleClick(t){let e=t.target.closest(n.SELECTORS.button);if(!e)return;this.btnClicked=e,this.btnClickedIcons=Array.from(e.querySelectorAll(n.SELECTORS.icon));let i=e.getAttribute("data-copy-btn");if(!i){console.warn("Copy: No content ID found on the button.");return}let c=this.DOM.el?.querySelector(n.SELECTORS.content(i));c?this.copyToClipboard(c):console.warn(`Copy: No content found for ID "${i}".`)}sanitizeContent(t){let o=t.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g,""),e=document.createElement("div");return e.innerHTML=o,e.textContent?.trim()||""}toggleIcons(){this.btnClickedIcons.forEach(t=>{t.classList.toggle("hidden")})}showFeedback(){this.btnClicked&&(this.toggleIcons(),window.setTimeout(()=>{this.toggleIcons()},n.FEEDBACK_DELAY))}async copyToClipboard(t){let o=this.sanitizeContent(t);if(!navigator.clipboard){console.error("Copy: Clipboard API is not supported in this browser."),this.showFeedback();return}try{await navigator.clipboard.writeText(o),console.info("Copy: Text copied successfully."),this.showFeedback()}catch(e){console.error("Copy: Error while copying text.",e),this.showFeedback()}}},r=()=>new s;export{r as default}; diff --git a/gno.land/pkg/gnoweb/public/js/index.js b/gno.land/pkg/gnoweb/public/js/index.js new file mode 100644 index 00000000000..e990dd91f5f --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/index.js @@ -0,0 +1 @@ +(()=>{let s={copy:{selector:"[data-copy-btn]",path:"/public/js/copy.js"},help:{selector:"#help",path:"/public/js/realmhelp.js"},searchBar:{selector:"#header-searchbar",path:"/public/js/searchbar.js"}},r=async({selector:e,path:o})=>{if(document.querySelector(e))try{(await import(o)).default()}catch(t){console.error(`Error while loading script ${o}:`,t)}else console.warn(`Module not loaded: no element matches selector "${e}"`)},l=async()=>{let e=Object.values(s).map(o=>r(o));await Promise.all(e)};document.addEventListener("DOMContentLoaded",l)})(); diff --git a/gno.land/pkg/gnoweb/public/js/realmhelp.js b/gno.land/pkg/gnoweb/public/js/realmhelp.js new file mode 100644 index 00000000000..9b045061a00 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/realmhelp.js @@ -0,0 +1 @@ +var s=class a{DOM;funcList;static SELECTORS={container:"#help",func:"[data-func]",addressInput:"[data-role='help-input-addr']",cmdModeSelect:"[data-role='help-select-mode']"};constructor(){this.DOM={el:document.querySelector(a.SELECTORS.container),funcs:[],addressInput:null,cmdModeSelect:null},this.funcList=[],this.DOM.el?this.init():console.warn("Help: Main container not found.")}init(){let{el:e}=this.DOM;e&&(this.DOM.funcs=Array.from(e.querySelectorAll(a.SELECTORS.func)),this.DOM.addressInput=e.querySelector(a.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(a.SELECTORS.cmdModeSelect),console.log(this.DOM),this.funcList=this.DOM.funcs.map(t=>new r(t)),this.bindEvents())}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM;e?.addEventListener("input",()=>{this.funcList.forEach(n=>n.updateAddr(e.value))}),t?.addEventListener("change",n=>{let d=n.target;this.funcList.forEach(l=>l.updateMode(d.value))})}},r=class a{DOM;funcName;static SELECTORS={address:"[data-role='help-code-address']",args:"[data-role='help-code-args']",mode:"[data-code-mode]",paramInput:"[data-role='help-param-input']"};constructor(e){this.DOM={el:e,addrs:Array.from(e.querySelectorAll(a.SELECTORS.address)),args:Array.from(e.querySelectorAll(a.SELECTORS.args)),modes:Array.from(e.querySelectorAll(a.SELECTORS.mode))},this.funcName=e.dataset.func||null,this.bindEvents()}bindEvents(){this.DOM.el.addEventListener("input",e=>{let t=e.target;t.dataset.role==="help-param-input"&&this.updateArg(t.dataset.param||"",t.value)})}updateArg(e,t){this.DOM.args.filter(n=>n.dataset.arg===e).forEach(n=>{n.textContent=t.trim()||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let n=t.dataset.codeMode===e;t.className=n?"inline":"hidden",t.dataset.copyContent=n?`help-cmd-${this.funcName}`:""})}},i=()=>new s;export{i as default}; diff --git a/gno.land/pkg/gnoweb/public/js/searchbar.js b/gno.land/pkg/gnoweb/public/js/searchbar.js new file mode 100644 index 00000000000..e8012b9b6d9 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/searchbar.js @@ -0,0 +1 @@ +var n=class r{DOM;baseUrl;static SELECTORS={container:"#header-searchbar",inputSearch:"[data-role='header-input-search']",breadcrumb:"[data-role='header-breadcrumb-search']"};constructor(){this.DOM={el:document.querySelector(r.SELECTORS.container),inputSearch:null,breadcrumb:null},this.baseUrl=window.location.origin,this.DOM.el?this.init():console.warn("SearchBar: Main container not found.")}init(){let{el:e}=this.DOM;this.DOM.inputSearch=e?.querySelector(r.SELECTORS.inputSearch)??null,this.DOM.breadcrumb=e?.querySelector(r.SELECTORS.breadcrumb)??null,this.DOM.inputSearch||console.warn("SearchBar: Input element for search not found."),this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("submit",e=>{e.preventDefault(),this.searchUrl()})}searchUrl(){let e=this.DOM.inputSearch?.value.trim();if(e){let t=e;/^https?:\/\//i.test(t)||(t=`${this.baseUrl}${t.startsWith("/")?"":"/"}${t}`);try{window.location.href=new URL(t).href}catch{console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://.")}}else console.error("SearchBar: Please enter a URL to search.")}},i=()=>new n;export{i as default}; diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css new file mode 100644 index 00000000000..1bb292e3460 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/styles.css @@ -0,0 +1,3 @@ +@font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-family:Inter var;font-weight:100 900;font-display:swap;font-style:oblique 0deg 10deg;src:url(fonts/intervar/Inter.var.woff2) format("woff2")}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } + +/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility;overflow-x:hidden}@supports (font-variation-settings:normal){html{font-family:Inter var,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{font-size:1rem}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:2rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:.375rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:1.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{font-size:1.125rem;font-weight:500}.realm-content h4,.realm-content p{margin-top:1rem;margin-bottom:1rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content blockquote,.realm-content ol,.realm-content ul{margin-top:1rem;margin-bottom:1rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.25rem}.realm-content img{margin-top:1.5rem;margin-bottom:1.5rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.875rem}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2rem;margin-bottom:2rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:1.5rem;margin-bottom:1.5rem;width:100%;border-collapse:collapse}.realm-content td,.realm-content th{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"“" "”" "‘" "’"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.5rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content table th:first-child,.realm-content td:first-child{padding-left:0}.realm-content table th:last-child,.realm-content td:last-child{padding-right:0}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1rem;margin-bottom:1rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:1.5rem;margin-bottom:1.5rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:after{font-size:.875rem;font-weight:400;--tw-content:"open";content:var(--tw-content)}@media (min-width:51.25rem){.toc-expend-btn:after{--tw-content:none;content:var(--tw-content)}}.toc-expend-btn:has(#toc-expend:checked):after{--tw-content:"close";content:var(--tw-content)}@media (min-width:51.25rem){.toc-expend-btn:has(#toc-expend:checked):after{--tw-content:none;content:var(--tw-content)}}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.top-0{top:0}.top-14{top:3.5rem}.top-2{top:.5rem}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.grid-flow-dense{grid-auto-flow:dense}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.rounded-l-sm{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r-sm{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r-8{border-right-width:8px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(124 124 124/var(--tw-border-opacity))}.border-r-transparent{border-right-color:transparent}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-200{--tw-text-opacity:1;color:rgb(189 189 189/var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:outline-transparent:focus{outline-color:transparent}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static.go b/gno.land/pkg/gnoweb/static.go new file mode 100644 index 00000000000..7900dcd7891 --- /dev/null +++ b/gno.land/pkg/gnoweb/static.go @@ -0,0 +1,28 @@ +package gnoweb + +import ( + "embed" + "net/http" +) + +//go:embed public/* +var assets embed.FS + +func disableCache(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + next.ServeHTTP(w, r) + }) +} + +// AssetHandler returns the handler to serve static assets. If cache is true, +// these will be served using the static files embedded in the binary; otherwise +// they will served from the filesystem. +func AssetHandler() http.Handler { + return http.FileServer(http.FS(assets)) +} + +func DevAssetHandler(path, dir string) http.Handler { + handler := http.StripPrefix(path, http.FileServer(http.Dir(dir))) + return disableCache(handler) +} diff --git a/gno.land/pkg/gnoweb/static/css/app.css b/gno.land/pkg/gnoweb/static/css/app.css deleted file mode 100644 index c10fc8ec0e0..00000000000 --- a/gno.land/pkg/gnoweb/static/css/app.css +++ /dev/null @@ -1,862 +0,0 @@ -/**** ROBOTO ****/ - -@font-face { - font-family: "Roboto Mono"; - font-style: normal; - font-weight: normal; - font-display: swap; - src: local("Roboto Mono Regular"), url("/static/font/roboto/RobotoMono-Regular.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono"; - font-style: italic; - font-weight: normal; - font-display: swap; - src: local("Roboto Mono Italic"), url("/static/font/roboto/RobotoMono-Italic.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono Bold"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: local("Roboto Mono Bold"), url("/static/font/roboto/RobotoMono-Bold.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono"; - font-style: italic; - font-weight: 700; - font-display: swap; - src: local("Roboto Mono Bold Italic"), url("/static/font/roboto/RobotoMono-BoldItalic.woff") format("woff"); - } - - -/*** DARK/LIGHT THEME COLORS ***/ - -html:not([data-theme="dark"]), -html[data-theme="light"] { - --background-color: #eee; - --input-background-color: #eee; - --text-color: #000; - --link-color: #25172a; - --muted-color: #757575; - --border-color: #d7d9db; - --icon-color: #000; - - --quote-background: #ddd; - --quote-2-background: #aaa4; - --code-background: #d7d9db; - --header-background: #373737; - --header-forground: #ffffff; - --logo-hat: #ffffff; - --logo-beard: #808080; - - --realm-help-background-color: #d7d9db9e; - --realm-help-odd-background-color: #d7d9db45; - --realm-help-code-color: #5d5d5d; - - --highlight-color: #2f3337; - --highlight-bg: #f6f6f6; - --highlight-color: #2f3337; - --highlight-comment: #656e77; - --highlight-keyword: #015692; - --highlight-attribute: #015692; - --highlight-symbol: #803378; - --highlight-namespace: #b75501; - --highlight-keyword: #015692; - --highlight-variable: #54790d; - --highlight-keyword: #015692; - --highlight-literal: #b75501; - --highlight-punctuation: #535a60; - --highlight-variable: #54790d; - --highlight-deletion: #c02d2e; - --highlight-addition: #2f6f44; -} - -html[data-theme="dark"] { - --background-color: #1e1e1e; - --input-background-color: #393939; - --text-color: #c7c7c7; - --link-color: #c7c7c7; - --muted-color: #737373; - --border-color: #606060; - --icon-color: #dddddd; - - --quote-background: #404040; - --quote-2-background: #555555; - --code-background: #606060; - --header-background: #373737; - --header-forground: #ffffff; - --logo-hat: #ffffff; - --logo-beard: #808080; - - --realm-help-background-color: #45454545; - --realm-help-odd-background-color: #4545459e; - --realm-help-code-color: #b6b6b6; - - --highlight-color: #ffffff; - --highlight-bg: #1c1b1b; - --highlight-color: #ffffff; - --highlight-comment: #999999; - --highlight-keyword: #88aece; - --highlight-attribute: #88aece; - --highlight-symbol: #c59bc1; - --highlight-namespace: #f08d49; - --highlight-keyword: #88aece; - --highlight-variable: #b5bd68; - --highlight-keyword: #88aece; - --highlight-literal: #f08d49; - --highlight-punctuation: #cccccc; - --highlight-variable: #b5bd68; - --highlight-deletion: #de7176; - --highlight-addition: #76c490; -} - -.logo-wording path {fill: var(--header-forground, #ffffff); } -.logo-beard { fill: var(--logo-beard, #808080); } -.logo-hat {fill: var(--logo-hat, #ffffff); } - -#theme-toggle { - cursor: pointer; - display: inline-block; - padding: 0; - color: var(--header-forground, #ffffff); -} - -html[data-theme="dark"] #theme-toggle-moon, -html[data-theme="light"] #theme-toggle-sun { - display: none; -} - -/*** BASE HTML ELEMENTS ***/ - -* { - box-sizing: border-box; -} - -html { - font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; - -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; - text-size-adjust: 100%; - -moz-osx-font-smoothing: grayscale; - font-smoothing: antialiased; - font-variant-ligatures: contextual common-ligatures; - font-kerning: normal; - text-rendering: optimizeLegibility; - -moz-text-size-adjust: none; - -webkit-text-size-adjust: none; - text-size-adjust: none; -} - -html, -body { - padding: 0; - margin: 0; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol", "Noto Color Emoji"; background-color: var(--background-color, #eee); - color: var(--text-color, #000); - font-size: 15px; - transition: 0.25s all ease; -} - -h1, -h2, -h3, -h4, -nav { - - font-weight: 600; - letter-spacing: 0.08rem; -} - -:is(h1, h2, h3, h4) a { - text-decoration: none; -} - -h1 { - text-align: center; - font-size: 2rem; - margin-block: 4.2rem 2rem; -} - -h2 { - font-size: 1.625rem; - margin-block: 3.4rem 1.2rem; - line-height: 1.4; -} - -h3 { - font-size: 1.467rem; - margin-block: 2.6rem 1rem; -} - -p { - font-size: 1rem; - margin-block: 1.2rem; - line-height: 1.4; -} - -p:last-child:has(a:only-child) { - margin-block-start: 0.8rem; -} -.stack > p:last-child:has(a:only-child) { - margin-block-start: 0; -} - -hr { - border: none; - height: 1px; - background: var(--border-color, #d7d9db); - width: 100%; - margin-block: 1.5rem 2rem; -} - -nav { - font-weight: 400; -} - -button { - color: var(--text-color, #000); -} - -body { - height: 100%; - width: 100%; -} - -input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -a { - color: var(--link-color, #25172a); -} - -a[href="#"] { - color: var(--muted-color, #757575); -} - -.gno-tmpl-section ul { - padding: 0; -} - -.gno-tmpl-section li , -#header li , -.footer li { - list-style: none; -} - -.gno-tmpl-section blockquote { - margin-inline: 0; -} - -li { - margin-bottom: 0.4rem; -} - -li > * { - vertical-align: middle; -} - -input { - background-color: var(--input-background-color, #eee); - border: 1px solid var(--border-color); - color: var(--text-color, #000); - width: 25em; - padding: 0.4rem 0.5rem; - max-width: 100%;x -} - -blockquote { - background-color: var(--quote-background, #ddd); -} - -blockquote blockquote { - margin: 0; - background-color: var(--quote-2-background, #aaa4); -} - -pre, code { - font-family: "Roboto Mono", "Courier New", "sans-serif"; -} -pre { - background-color: var(--code-background, #d7d9db); - margin: 0; - padding: 0.5rem; -} - -label { - margin-block-end: 0.8rem; - display: block; -} - -label > img { - margin-inline-end: 0.8rem; -} - -code { - white-space: pre-wrap; - overflow-wrap: anywhere; -} -/*** COMPOSITION ***/ -.container { - width: 100%; - max-width: 63.75rem; - margin: auto; - padding: 1.25rem; -} - -.container p > img:only-child { - max-width: 100%; -} -.gno-tmpl-page p img:only-child { - margin-inline: auto; - display: block; - max-width: 100%; -} - -.inline-list { - padding: 1rem; - display: flex; - justify-content: space-between; -} - - - -.stack, -.stack > p { - display: flex; - flex-direction: column; -} - -.stack > p { - margin: 0; -} - -.stack > a, -.stack > p > a{ - margin-block-end: 0.4rem; -} - -.column > h1, -.column > h2, -.column > h3, -.column > h4, -.column > h5, -.column > h6 { - margin-block-start: 0; -} - -.columns-2, -.columns-3 { - display: grid; - grid-template-columns: repeat(1, 1fr); - grid-gap: 3.75rem; - margin: 3.75rem auto; -} - -.footer { - text-align: center; - margin-block-start: 2rem; - background-color: var(--header-background, #d7d9db); - border-top: 1px solid var(--border-color); -} - -.footer > .logo { - display: inline-block; - margin: 1rem; - height: 1.2rem; -} - -/** 51.2rem **/ -@media screen and (min-width: 68.75rem) { - .stack, - .stack > p { - flex-direction: row; - } - .stack *:not(:first-child) { - margin-left: 3.75rem; - } - .stack > a, - .stack > p > a{ - margin-block-end: 0; - } - .columns-2 { - grid-template-columns: repeat(2, 1fr); - } - .columns-3 { - grid-template-columns: repeat(3, 1fr); - } -} - -/*** UTILITIES ***/ - -.is-hidden { - display: none; -} - -.is-muted { - color: var(--muted-color, #757575); -} - -.is-finished { - text-decoration: line-through; -} - -.is-underline { - text-decoration: underline; -} - -/*** BLOCKS ***/ -.tabs button { - border: none; - cursor: pointer; - text-decoration: underline; - padding: 0; - background: none; - color: var(--text-color, #000); -} - -.tabs button[aria-selected="true"] { - font-weight: 700; -} - -.tabs + .jumbotron { - margin-top: 2.5rem; -} -.tabs > .columns-2, -.tabs > .columns-3 { - margin-bottom: 2.5rem; -} - -.accordion-trigger { - display: block; - border: none; - cursor: pointer; - padding: 0.4rem 0; - font-size: 1.125rem; - font-weight: 700; - text-align: left; - background: none; -} - -.accordion-trigger ~ div { - padding: 0.875rem 0 2.2rem; -} - -.accordion > p { - margin-block: 0; -} -/** 51.2rem **/ -@media screen and (min-width: 68.75rem) { - .accordion .accordion-trigger ~ div { - padding: 0.875rem 0 2.2rem 2rem; - } -} - -.gor-accordion button::first-letter { - font-size: 1.5em; - color: var(--text-color, #000); -} - -.jumbotron { - border: 1px solid var(--border-color, #d7d9db); - padding: 1.4rem; - margin: 3.75rem auto; -} - -.jumbotron h1 { - text-align: left; -} - -.jumbotron > *:first-child, -.jumbotron > * > *:first-child { - margin-block-start: 0; -} - -.jumbotron > *:last-child, -.jumbotron > * > *:last-child { - margin-block-end: 0; -} - -/** 68.75rem**/ -@media screen and (min-width: 68.75rem) { - .jumbotron { - margin: 3.75rem -3.5rem; - padding: 3.5rem; - } -} - -#root { - display: flex; - flex-direction: column; - border: 1px solid var(--header-background, #d7d9db); - margin: 20px; - overflow: hidden; - /* height: calc(100vh - 40px); */ -} - -#header { - position: relative; - background-color: var(--header-background, #d7d9db); - padding: 1.333rem; - display: flex; - align-items: center; - justify-content: space-between; -} - -#header > nav { - flex-grow: 2; -} - -#header .logo { - display: flex; - align-items: center; - color: var(--link-color, #25172a); - position: absolute; - height: 2.4rem; - z-index: 2; -} - -.logo > svg { - height: 100%; -} - -#logo_path a { - text-decoration: none; -} - -#logo_path { - padding-right: 0.8rem; -} - -#logo_path a:hover { - text-decoration: underline; -} - -#realm_links a { - font-size: 0.8rem; -} - -#header_buttons { - position: relative; - width: 100%; - height: 3rem; -} - -#header_buttons nav { - height: 100%; - display: flex; - justify-content: flex-end; - align-items: center; -} - -/* enabled conditionally with