From a885c78da2ef749b711241b578b525f9bf4155a1 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:06:05 +0100 Subject: [PATCH 1/9] fix(gnovm/softfloat): replace copy.sh with Go generator (#3584) Co-authored-by: Morgan Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/pkg/gnolang/internal/softfloat/copy.sh | 32 ----- .../gnolang/internal/softfloat/gen/main.go | 116 ++++++++++++++++++ .../internal/softfloat/runtime_softfloat64.go | 2 +- .../softfloat/runtime_softfloat64_test.go | 6 +- .../gnolang/internal/softfloat/softfloat.go | 2 +- 5 files changed, 121 insertions(+), 37 deletions(-) delete mode 100644 gnovm/pkg/gnolang/internal/softfloat/copy.sh create mode 100644 gnovm/pkg/gnolang/internal/softfloat/gen/main.go diff --git a/gnovm/pkg/gnolang/internal/softfloat/copy.sh b/gnovm/pkg/gnolang/internal/softfloat/copy.sh deleted file mode 100644 index 6d2a8f80462..00000000000 --- a/gnovm/pkg/gnolang/internal/softfloat/copy.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh - -# softfloat64.go: -# - add header -# - change package name -cat > runtime_softfloat64.go << EOF -// Code generated by copy.sh. DO NOT EDIT. -// This file is copied from \$GOROOT/src/runtime/softfloat64.go. -// It is the software floating point implementation used by the Go runtime. - -EOF -cat "$GOROOT/src/runtime/softfloat64.go" >> ./runtime_softfloat64.go -sed -i 's/^package runtime$/package softfloat/' runtime_softfloat64.go - -# softfloat64_test.go: -# - add header -# - change package name -# - change import to right package -# - change GOARCH to runtime.GOARCH, and import the "runtime" package -cat > runtime_softfloat64_test.go << EOF -// Code generated by copy.sh. DO NOT EDIT. -// This file is copied from \$GOROOT/src/runtime/softfloat64_test.go. -// It is the tests for the software floating point implementation -// used by the Go runtime. - -EOF -cat "$GOROOT/src/runtime/softfloat64_test.go" >> ./runtime_softfloat64_test.go -sed -i 's/^package runtime_test$/package softfloat_test/ -s#^\t\. "runtime"$#\t. "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat"# -s/GOARCH/runtime.GOARCH/g -16a\ - "runtime"' runtime_softfloat64_test.go \ No newline at end of file diff --git a/gnovm/pkg/gnolang/internal/softfloat/gen/main.go b/gnovm/pkg/gnolang/internal/softfloat/gen/main.go new file mode 100644 index 00000000000..7c89ff9b5a9 --- /dev/null +++ b/gnovm/pkg/gnolang/internal/softfloat/gen/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func main() { + // Process softfloat64.go file + processSoftFloat64File() + + // Process softfloat64_test.go file + processSoftFloat64TestFile() + + // Run mvdan.cc/gofumpt + gofumpt() + + fmt.Println("Files processed successfully.") +} + +func processSoftFloat64File() { + // Read source file + content, err := os.ReadFile(fmt.Sprintf("%s/src/runtime/softfloat64.go", runtime.GOROOT())) + if err != nil { + log.Fatal("Error reading source file:", err) + } + + // Prepare header + header := `// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. +// This file is copied from $GOROOT/src/runtime/softfloat64.go. +// It is the software floating point implementation used by the Go runtime. + +` + + // Combine header with content + newContent := header + string(content) + + // Replace package name + newContent = strings.Replace(newContent, "package runtime", "package softfloat", 1) + + // Write to destination file + err = os.WriteFile("runtime_softfloat64.go", []byte(newContent), 0o644) + if err != nil { + log.Fatal("Error writing to destination file:", err) + } +} + +func processSoftFloat64TestFile() { + // Read source test file + content, err := os.ReadFile(fmt.Sprintf("%s/src/runtime/softfloat64_test.go", runtime.GOROOT())) + if err != nil { + log.Fatal("Error reading source test file:", err) + } + + // Prepare header + header := `// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. +// This file is copied from $GOROOT/src/runtime/softfloat64_test.go. +// It is the tests for the software floating point implementation +// used by the Go runtime. + +` + + // Combine header with content + newContent := header + string(content) + + // Replace package name and imports + newContent = strings.Replace(newContent, "package runtime_test", "package softfloat_test", 1) + newContent = strings.Replace(newContent, "\t. \"runtime\"", "\t\"runtime\"", 1) + newContent = strings.Replace(newContent, "GOARCH", "runtime.GOARCH", 1) + + newContent = strings.Replace(newContent, "import (", "import (\n\t. \"github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat\"", 1) + + // Write to destination file + err = os.WriteFile("runtime_softfloat64_test.go", []byte(newContent), 0o644) + if err != nil { + log.Fatal("Error writing to destination test file:", err) + } +} + +func gitRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + p := wd + for { + if s, e := os.Stat(filepath.Join(p, ".git")); e == nil && s.IsDir() { + return p, nil + } + + if strings.HasSuffix(p, string(filepath.Separator)) { + return "", errors.New("root git not found") + } + + p = filepath.Dir(p) + } +} + +func gofumpt() { + rootPath, err := gitRoot() + if err != nil { + log.Fatal("error finding git root:", err) + } + + cmd := exec.Command("go", "run", "-modfile", filepath.Join(strings.TrimSpace(rootPath), "misc/devdeps/go.mod"), "mvdan.cc/gofumpt", "-w", ".") + _, err = cmd.Output() + if err != nil { + log.Fatal("error gofumpt:", err) + } +} diff --git a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go index cf2ad5afd8a..7623b9c2077 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go +++ b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go @@ -1,4 +1,4 @@ -// Code generated by copy.sh. DO NOT EDIT. +// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. // This file is copied from $GOROOT/src/runtime/softfloat64.go. // It is the software floating point implementation used by the Go runtime. diff --git a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go index c57fe08b0ef..8b5d34650f1 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go +++ b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go @@ -1,4 +1,4 @@ -// Code generated by copy.sh. DO NOT EDIT. +// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. // This file is copied from $GOROOT/src/runtime/softfloat64_test.go. // It is the tests for the software floating point implementation // used by the Go runtime. @@ -10,11 +10,11 @@ package softfloat_test import ( + . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" "math" "math/rand" - . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" + "runtime" "testing" - "runtime" ) // turn uint64 op into float64 op diff --git a/gnovm/pkg/gnolang/internal/softfloat/softfloat.go b/gnovm/pkg/gnolang/internal/softfloat/softfloat.go index 30f66dff620..89dcd04d8fb 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/softfloat.go +++ b/gnovm/pkg/gnolang/internal/softfloat/softfloat.go @@ -17,7 +17,7 @@ package softfloat // This file mostly exports the functions from runtime_softfloat64.go -//go:generate sh copy.sh +//go:generate go run github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen const ( mask = 0x7FF From df4113d75450066a72c627f584686b28d2a10cbc Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:44:49 +0100 Subject: [PATCH 2/9] feat(examples): update leon's config & home (#3603) --- examples/gno.land/r/leon/config/config.gno | 119 +++++++++++++++------ examples/gno.land/r/leon/hof/hof.gno | 22 ++-- examples/gno.land/r/leon/home/home.gno | 42 ++++---- 3 files changed, 119 insertions(+), 64 deletions(-) diff --git a/examples/gno.land/r/leon/config/config.gno b/examples/gno.land/r/leon/config/config.gno index bc800ec8263..bb90a6c21d7 100644 --- a/examples/gno.land/r/leon/config/config.gno +++ b/examples/gno.land/r/leon/config/config.gno @@ -3,61 +3,116 @@ package config import ( "errors" "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/avl" + p "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/realmpath" ) var ( - main std.Address // leon's main address - backup std.Address // backup address + configs = avl.NewTree() + pager = p.NewPager(configs, 10, false) + banner = "---\n[[Leon's Home page]](/r/leon/home) | [[GitHub: @leohhhn]](https://github.com/leohhhn)\n\n---" + absPath = strings.TrimPrefix(std.CurrentRealm().PkgPath(), std.GetChainDomain()) + + // SafeObjects + OwnableMain = ownable.NewWithAddress("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") + OwnableBackup = ownable.NewWithAddress("g1lavlav7zwsjqlzzl3qdl3nl242qtf638vnhdjh") - ErrInvalidAddr = errors.New("leon's config: invalid address") ErrUnauthorized = errors.New("leon's config: unauthorized") ) -func init() { - main = "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5" +type Config struct { + lines string + updated time.Time } -func Address() std.Address { - return main -} +func AddConfig(name, lines string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) + } -func Backup() std.Address { - return backup + configs.Set(name, Config{ + lines: lines, + updated: time.Now(), + }) // no overwrite check } -func SetAddress(a std.Address) error { - if !a.IsValid() { - return ErrInvalidAddr +func RemoveConfig(name string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) } - if err := checkAuthorized(); err != nil { - return err + if _, ok := configs.Remove(name); !ok { + panic("no config with that name") } - - main = a - return nil } -func SetBackup(a std.Address) error { - if !a.IsValid() { - return ErrInvalidAddr +func UpdateBanner(newBanner string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) } - if err := checkAuthorized(); err != nil { - return err - } + banner = newBanner +} - backup = a - return nil +func IsAuthorized(addr std.Address) bool { + return addr == OwnableMain.Owner() || addr == OwnableBackup.Owner() } -func checkAuthorized() error { - caller := std.PrevRealm().Addr() - isAuthorized := caller == main || caller == backup +func Banner() string { + return banner +} + +func Render(path string) (out string) { + req := realmpath.Parse(path) + if req.Path == "" { + out += md.H1("Leon's config package") + + out += ufmt.Sprintf("Leon's main address: %s\n\n", OwnableMain.Owner().String()) + out += ufmt.Sprintf("Leon's backup address: %s\n\n", OwnableBackup.Owner().String()) - if !isAuthorized { - return ErrUnauthorized + out += md.H2("Leon's configs") + + if configs.Size() == 0 { + out += "No configs yet :c\n\n" + } + + page := pager.MustGetPageByPath(path) + for _, item := range page.Items { + out += ufmt.Sprintf("- [%s](%s:%s)\n\n", item.Key, absPath, item.Key) + } + + out += page.Picker() + out += "\n\n" + out += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" + + out += Banner() + + return out } - return nil + return renderConfPage(req.Path) +} + +func renderConfPage(confName string) (out string) { + raw, ok := configs.Get(confName) + if !ok { + out += md.H1("404") + out += "That config does not exist :/" + return out + } + + conf := raw.(Config) + out += md.H1(confName) + out += ufmt.Sprintf("```\n%s\n```\n\n", conf.lines) + out += ufmt.Sprintf("_Last updated on %s_", conf.updated.Format("02 Jan, 2006")) + + return out } diff --git a/examples/gno.land/r/leon/hof/hof.gno b/examples/gno.land/r/leon/hof/hof.gno index 147a0dd1a95..96266ffe380 100644 --- a/examples/gno.land/r/leon/hof/hof.gno +++ b/examples/gno.land/r/leon/hof/hof.gno @@ -10,6 +10,8 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/pausable" "gno.land/p/demo/seqid" + + "gno.land/r/leon/config" ) var ( @@ -24,7 +26,7 @@ type ( Exhibition struct { itemCounter seqid.ID description string - items *avl.Tree // pkgPath > Item + items *avl.Tree // pkgPath > &Item itemsSorted *avl.Tree // same data but sorted, storing pointers } @@ -43,7 +45,7 @@ func init() { itemsSorted: avl.NewTree(), } - Ownable = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) + Ownable = ownable.NewWithAddress(config.OwnableMain.Owner()) // OrigSendOwnable? Pausable = pausable.NewFromOwnable(Ownable) } @@ -85,14 +87,14 @@ func Register() { func Upvote(pkgpath string) { rawItem, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } item := rawItem.(*Item) caller := std.PrevRealm().Addr().String() if item.upvote.Has(caller) { - panic(ErrDoubleUpvote.Error()) + panic(ErrDoubleUpvote) } item.upvote.Set(caller, struct{}{}) @@ -101,14 +103,14 @@ func Upvote(pkgpath string) { func Downvote(pkgpath string) { rawItem, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } item := rawItem.(*Item) caller := std.PrevRealm().Addr().String() if item.downvote.Has(caller) { - panic(ErrDoubleDownvote.Error()) + panic(ErrDoubleDownvote) } item.downvote.Set(caller, struct{}{}) @@ -116,19 +118,19 @@ func Downvote(pkgpath string) { func Delete(pkgpath string) { if !Ownable.CallerIsOwner() { - panic(ownable.ErrUnauthorized.Error()) + panic(ownable.ErrUnauthorized) } i, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } if _, removed := exhibition.items.Remove(pkgpath); !removed { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } } diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index cf33260cc6b..aef261fcd60 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -19,7 +19,24 @@ var ( abtMe [2]string ) +func Render(path string) string { + out := "# Leon's Homepage\n\n" + + out += renderAboutMe() + out += renderBlogPosts() + out += "\n\n" + out += renderArt() + out += "\n\n" + out += config.Banner() + out += "\n\n" + + return out +} + func init() { + hof.Register() + mirror.Register(std.CurrentRealm().PkgPath(), Render) + pfp = "https://i.imgflip.com/91vskx.jpg" pfpCaption = "[My favourite painting & pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)" abtMe = [2]string{ @@ -30,16 +47,12 @@ life-long learner, and sharer of knowledge.`, My contributions to gno.land can mainly be found [here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn). -TODO import r/gh -`, +TODO import r/gh`, } - - hof.Register() - mirror.Register(std.CurrentRealm().PkgPath(), Render) } func UpdatePFP(url, caption string) { - if !isAuthorized(std.PrevRealm().Addr()) { + if !config.IsAuthorized(std.PrevRealm().Addr()) { panic(config.ErrUnauthorized) } @@ -48,7 +61,7 @@ func UpdatePFP(url, caption string) { } func UpdateAboutMe(col1, col2 string) { - if !isAuthorized(std.PrevRealm().Addr()) { + if !config.IsAuthorized(std.PrevRealm().Addr()) { panic(config.ErrUnauthorized) } @@ -56,17 +69,6 @@ func UpdateAboutMe(col1, col2 string) { abtMe[1] = col2 } -func Render(path string) string { - out := "# Leon's Homepage\n\n" - - out += renderAboutMe() - out += renderBlogPosts() - out += "\n\n" - out += renderArt() - - return out -} - func renderBlogPosts() string { out := "" //out += "## Leon's Blog Posts" @@ -130,7 +132,3 @@ func renderMillipede() string { return out } - -func isAuthorized(addr std.Address) bool { - return addr == config.Address() || addr == config.Backup() -} From 21fe65624a39fce3c589c1dd2d897b02b720f292 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:55:05 +0100 Subject: [PATCH 3/9] feat(r/docs): pager + render paths (#3608) --- .../r/docs/avl_pager_with_params/gno.mod | 1 + .../r/docs/avl_pager_with_params/render.gno | 86 +++++++++++++++++++ examples/gno.land/r/docs/docs.gno | 1 + 3 files changed, 88 insertions(+) create mode 100644 examples/gno.land/r/docs/avl_pager_with_params/gno.mod create mode 100644 examples/gno.land/r/docs/avl_pager_with_params/render.gno diff --git a/examples/gno.land/r/docs/avl_pager_with_params/gno.mod b/examples/gno.land/r/docs/avl_pager_with_params/gno.mod new file mode 100644 index 00000000000..aeb5b047762 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager_with_params/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/avl_pager_params diff --git a/examples/gno.land/r/docs/avl_pager_with_params/render.gno b/examples/gno.land/r/docs/avl_pager_with_params/render.gno new file mode 100644 index 00000000000..108f5735b65 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager_with_params/render.gno @@ -0,0 +1,86 @@ +package avl_pager_params + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/realmpath" +) + +// We'll keep some demo data in an AVL tree to showcase pagination. +var ( + items *avl.Tree + idCounter seqid.ID +) + +func init() { + items = avl.NewTree() + // Populate the tree with 15 sample items for demonstration. + for i := 1; i <= 15; i++ { + id := idCounter.Next().String() + items.Set(id, "Some item value: "+id) + } +} + +func Render(path string) string { + // 1) Parse the incoming path to split route vs. query. + req := realmpath.Parse(path) + // - req.Path contains everything *before* ? or $ (? - query params, $ - gnoweb params) + // - The remaining part (page=2, size=5, etc.) is not in req.Path. + + // 2) If no specific route is provided (req.Path == ""), we’ll show a “home” page + // that displays a list of configs in paginated form. + if req.Path == "" { + return renderHome(path) + } + + // 3) If a route *is* provided (e.g. :SomeKey), + // we will interpret it as a request for a specific page. + return renderConfigItem(req.Path) +} + +// renderHome shows a paginated list of config items if route == "". +func renderHome(fullPath string) string { + // Create a Pager for our config tree, with a default page size of 5. + p := pager.NewPager(items, 5, false) + + // MustGetPageByPath uses the *entire* path (including query parts: ?page=2, etc.) + page := p.MustGetPageByPath(fullPath) + + // Start building the output (plain text or markdown). + out := "# AVL Pager + Render paths\n\n" + out += `This realm showcases how to maintain a paginated list while properly parsing render paths. +You can see how a single page can include a paginated element (like the example below), and how clicking +an item can take you to a dedicated page for that specific item. + +No matter how you browse through the paginated list, the introductory text (this section) remains the same. + +` + + out += ufmt.Sprintf("Showing page %d of %d\n\n", page.PageNumber, page.TotalPages) + + // List items for this page. + for _, item := range page.Items { + // Link each item to a details page: e.g. ":Config01" + out += ufmt.Sprintf("- [Item %s](/r/docs/avl_pager_params:%s)\n", item.Key, item.Key) + } + + // Insert pagination controls (previous/next links, etc.). + out += "\n" + page.Picker() + "\n\n" + out += "### [Go back to r/docs](/r/docs)" + + return out +} + +// renderConfigItem shows details for a single item, e.g. ":item001". +func renderConfigItem(itemName string) string { + value, ok := items.Get(itemName) + if !ok { + return ufmt.Sprintf("**No item found** for key: %s", itemName) + } + + out := ufmt.Sprintf("# Item %s\n\n%s\n\n", itemName, value.(string)) + out += "[Go back](/r/docs/avl_pager_params)" + return out +} diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index 28bac4171b5..be9a58e1c53 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -13,6 +13,7 @@ Explore various examples to learn more about Gno functionality and usage. - [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. +- [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. - ... From 4d0000e8e10b13934e18a11b1220c27fd607926f Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:07:26 +0100 Subject: [PATCH 4/9] feat(gnoweb): "No render" page/component (#3611) ## Description Defining a `Render()` function in realms is optional. Currently gnoweb presents an error if a realm that doesn't have a render func is requested. This should not be the case. This PR also adds a VM error, `RenderNotDeclared`, which is to be returned when `vm/qrender` is called on a realm which does not have a `Render()` function declared. I updated the status component to return the following in the aforementioned case: Screenshot 2025-01-25 at 16 30 55 Also adds another `r/docs` realm mentioning that a render function is optional in `r/`. --- examples/gno.land/r/docs/docs.gno | 1 + .../gno.land/r/docs/optional_render/gno.mod | 1 + .../docs/optional_render/optional_render.gno | 7 +++ gno.land/pkg/gnoweb/app_test.go | 1 + gno.land/pkg/gnoweb/components/view_status.go | 34 +++++++++++++-- .../pkg/gnoweb/components/views/status.html | 10 +++-- gno.land/pkg/gnoweb/handler.go | 18 +++++--- gno.land/pkg/gnoweb/handler_test.go | 43 ++++++++++++++++++- gno.land/pkg/gnoweb/webclient.go | 3 +- gno.land/pkg/gnoweb/webclient_html.go | 5 +++ gno.land/pkg/gnoweb/webclient_mock.go | 25 ++++++++++- gno.land/pkg/sdk/vm/errors.go | 2 + gno.land/pkg/sdk/vm/handler.go | 4 ++ gno.land/pkg/sdk/vm/package.go | 1 + 14 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 examples/gno.land/r/docs/optional_render/gno.mod create mode 100644 examples/gno.land/r/docs/optional_render/optional_render.gno diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index be9a58e1c53..b4c78205c0a 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -15,6 +15,7 @@ Explore various examples to learn more about Gno functionality and usage. - [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. - [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. +- [Optional Render](/r/docs/optional_render) - Render() is optional in realms. - ... diff --git a/examples/gno.land/r/docs/optional_render/gno.mod b/examples/gno.land/r/docs/optional_render/gno.mod new file mode 100644 index 00000000000..4c8162ca46d --- /dev/null +++ b/examples/gno.land/r/docs/optional_render/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/optional_render diff --git a/examples/gno.land/r/docs/optional_render/optional_render.gno b/examples/gno.land/r/docs/optional_render/optional_render.gno new file mode 100644 index 00000000000..77da30609b3 --- /dev/null +++ b/examples/gno.land/r/docs/optional_render/optional_render.gno @@ -0,0 +1,7 @@ +package optional_render + +func Info() string { + return `Having a Render() function in your realm is optional! +If you do decide to have a Render() function, it must have the following signature: +func Render(path string) string { ... }` +} diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index 6fb69c6d984..eb17ee4d0e9 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -47,6 +47,7 @@ func TestRoutes(t *testing.T) { {"/game-of-realms", found, "/contribute"}, {"/gor", found, "/contribute"}, {"/blog", found, "/r/gnoland/blog"}, + {"/r/docs/optional_render", http.StatusNoContent, "No Render"}, {"/r/not/found/", notFound, ""}, {"/404/not/found", notFound, ""}, {"/아스키문자가아닌경로", notFound, ""}, diff --git a/gno.land/pkg/gnoweb/components/view_status.go b/gno.land/pkg/gnoweb/components/view_status.go index 46f998c45cb..56477a4db0a 100644 --- a/gno.land/pkg/gnoweb/components/view_status.go +++ b/gno.land/pkg/gnoweb/components/view_status.go @@ -2,10 +2,38 @@ package components const StatusViewType ViewType = "status-view" +// StatusData holds the dynamic fields for the "status" template type StatusData struct { - Message string + Title string + Body string + ButtonURL string + ButtonText string } -func StatusComponent(message string) *View { - return NewTemplateView(StatusViewType, "status", StatusData{message}) +// StatusErrorComponent returns a view for error scenarios +func StatusErrorComponent(message string) *View { + return NewTemplateView( + StatusViewType, + "status", + StatusData{ + Title: "Error: " + message, + Body: "Something went wrong.", + ButtonURL: "/", + ButtonText: "Go Back Home", + }, + ) +} + +// StatusNoRenderComponent returns a view for non-error notifications +func StatusNoRenderComponent(pkgPath string) *View { + return NewTemplateView( + StatusViewType, + "status", + StatusData{ + Title: "No Render", + Body: "This realm does not implement a Render() function.", + ButtonURL: pkgPath + "$source", + ButtonText: "View Realm Source", + }, + ) } diff --git a/gno.land/pkg/gnoweb/components/views/status.html b/gno.land/pkg/gnoweb/components/views/status.html index ab068cbf7e4..f4533275789 100644 --- a/gno.land/pkg/gnoweb/components/views/status.html +++ b/gno.land/pkg/gnoweb/components/views/status.html @@ -1,8 +1,12 @@ {{ define "status" }}
gno land -

Error: {{ .Message }}

-

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

- Go Back Home +

+ {{ .Title }} +

+

{{ .Body }}

+ + {{ .ButtonText }} +
{{ end }} diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index cdaaa63e1bc..822fd50fa1b 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -114,7 +114,7 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components gnourl, err := ParseGnoURL(r.URL) if err != nil { h.Logger.Warn("unable to parse url path", "path", r.URL.Path, "error", err) - return http.StatusNotFound, components.StatusComponent("invalid path") + return http.StatusNotFound, components.StatusErrorComponent("invalid path") } breadcrumb := generateBreadcrumbPaths(gnourl) @@ -130,7 +130,7 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components return h.GetPackageView(gnourl) default: h.Logger.Debug("invalid path: path is neither a pure package or a realm") - return http.StatusBadRequest, components.StatusComponent("invalid path") + return http.StatusBadRequest, components.StatusErrorComponent("invalid path") } } @@ -160,6 +160,10 @@ func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) if err != nil { + if errors.Is(err, ErrRenderNotDeclared) { + return http.StatusNoContent, components.StatusNoRenderComponent(gnourl.Path) + } + h.Logger.Error("unable to render realm", "error", err, "path", gnourl.EncodeURL()) return GetClientErrorStatusPage(gnourl, err) } @@ -223,7 +227,7 @@ func (h *WebHandler) GetSourceView(gnourl *GnoURL) (int, *components.View) { if len(files) == 0 { h.Logger.Debug("no files available", "path", gnourl.Path) - return http.StatusOK, components.StatusComponent("no files available") + return http.StatusOK, components.StatusErrorComponent("no files available") } var fileName string @@ -266,7 +270,7 @@ func (h *WebHandler) GetDirectoryView(gnourl *GnoURL) (int, *components.View) { if len(files) == 0 { h.Logger.Debug("no files available", "path", gnourl.Path) - return http.StatusOK, components.StatusComponent("no files available") + return http.StatusOK, components.StatusErrorComponent("no files available") } return http.StatusOK, components.DirectoryView(components.DirData{ @@ -283,13 +287,13 @@ func GetClientErrorStatusPage(_ *GnoURL, err error) (int, *components.View) { switch { case errors.Is(err, ErrClientPathNotFound): - return http.StatusNotFound, components.StatusComponent(err.Error()) + return http.StatusNotFound, components.StatusErrorComponent(err.Error()) case errors.Is(err, ErrClientBadRequest): - return http.StatusInternalServerError, components.StatusComponent("bad request") + return http.StatusInternalServerError, components.StatusErrorComponent("bad request") case errors.Is(err, ErrClientResponse): fallthrough // XXX: for now fallback as internal error default: - return http.StatusInternalServerError, components.StatusComponent("internal error") + return http.StatusInternalServerError, components.StatusErrorComponent("internal error") } } diff --git a/gno.land/pkg/gnoweb/handler_test.go b/gno.land/pkg/gnoweb/handler_test.go index 624e3390a97..e85434a6f41 100644 --- a/gno.land/pkg/gnoweb/handler_test.go +++ b/gno.land/pkg/gnoweb/handler_test.go @@ -24,12 +24,13 @@ func (t *testingLogger) Write(b []byte) (n int, err error) { // TestWebHandler_Get tests the Get method of WebHandler using table-driven tests. func TestWebHandler_Get(t *testing.T) { + t.Parallel() // Set up a mock package with some files and functions mockPackage := &gnoweb.MockPackage{ Domain: "example.com", Path: "/r/mock/path", Files: map[string]string{ - "render.gno": `package main; func Render(path string) { return "one more time" }`, + "render.gno": `package main; func Render(path string) string { return "one more time" }`, "gno.mod": `module example.com/r/mock/path`, "LicEnse": `my super license`, }, @@ -37,6 +38,10 @@ func TestWebHandler_Get(t *testing.T) { {FuncName: "SuperRenderFunction", Params: []vm.NamedType{ {Name: "my_super_arg", Type: "string"}, }}, + { + FuncName: "Render", Params: []vm.NamedType{{Name: "path", Type: "string"}}, + Results: []vm.NamedType{{Name: "", Type: "string"}}, + }, }, } @@ -82,6 +87,7 @@ func TestWebHandler_Get(t *testing.T) { for _, tc := range cases { t.Run(strings.TrimPrefix(tc.Path, "/"), func(t *testing.T) { + t.Parallel() t.Logf("input: %+v", tc) // Initialize testing logger @@ -110,3 +116,38 @@ func TestWebHandler_Get(t *testing.T) { }) } } + +// TestWebHandler_NoRender checks if gnoweb displays the `No Render` page properly. +// This happens when the render being queried does not have a Render function declared. +func TestWebHandler_NoRender(t *testing.T) { + t.Parallel() + + mockPath := "/r/mock/path" + mockPackage := &gnoweb.MockPackage{ + Domain: "gno.land", + Path: "/r/mock/path", + Files: map[string]string{ + "render.gno": `package main; func init() {}`, + "gno.mod": `module gno.land/r/mock/path`, + }, + } + + webclient := gnoweb.NewMockWebClient(mockPackage) + config := gnoweb.WebHandlerConfig{ + WebClient: webclient, + } + + logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) + handler, err := gnoweb.NewWebHandler(logger, config) + require.NoError(t, err, "failed to create WebHandler") + + req, err := http.NewRequest(http.MethodGet, mockPath, nil) + require.NoError(t, err, "failed to create HTTP request") + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusNoContent, rr.Code, "unexpected status code") + assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "No Render") + assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "This realm does not implement a Render() function.") +} diff --git a/gno.land/pkg/gnoweb/webclient.go b/gno.land/pkg/gnoweb/webclient.go index de44303f352..1def3bc3812 100644 --- a/gno.land/pkg/gnoweb/webclient.go +++ b/gno.land/pkg/gnoweb/webclient.go @@ -10,6 +10,7 @@ import ( var ( ErrClientPathNotFound = errors.New("package not found") + ErrRenderNotDeclared = errors.New("render function not declared") ErrClientBadRequest = errors.New("bad request") ErrClientResponse = errors.New("node response error") ) @@ -23,7 +24,7 @@ type RealmMeta struct { Toc md.Toc } -// WebClient is an interface for interacting with package and node ressources. +// WebClient is an interface for interacting with package and node resources. type WebClient interface { // RenderRealm renders the content of a realm from a given path and // arguments into the giver `writer`. The method should ensures the rendered diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index d856c6f87a0..c04a7f9e457 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -177,6 +177,7 @@ func (s *HTMLWebClient) RenderRealm(w io.Writer, pkgPath string, args string) (* pkgPath = strings.Trim(pkgPath, "/") data := fmt.Sprintf("%s/%s:%s", s.domain, pkgPath, args) + rawres, err := s.query(qpath, []byte(data)) if err != nil { return nil, err @@ -213,6 +214,10 @@ func (s *HTMLWebClient) query(qpath string, data []byte) ([]byte, error) { return nil, ErrClientPathNotFound } + if errors.Is(err, vm.NoRenderDeclError{}) { + return nil, ErrRenderNotDeclared + } + s.logger.Error("response error", "path", qpath, "log", qres.Response.Log) return nil, fmt.Errorf("%w: %s", ErrClientResponse, err.Error()) } diff --git a/gno.land/pkg/gnoweb/webclient_mock.go b/gno.land/pkg/gnoweb/webclient_mock.go index 451f5e237c3..8a037c181e0 100644 --- a/gno.land/pkg/gnoweb/webclient_mock.go +++ b/gno.land/pkg/gnoweb/webclient_mock.go @@ -31,13 +31,18 @@ func NewMockWebClient(pkgs ...*MockPackage) *MockWebClient { return &MockWebClient{Packages: mpkgs} } -// Render simulates rendering a package by writing its content to the writer. +// RenderRealm simulates rendering a package by writing its content to the writer. func (m *MockWebClient) RenderRealm(w io.Writer, path string, args string) (*RealmMeta, error) { pkg, exists := m.Packages[path] if !exists { return nil, ErrClientPathNotFound } + if !pkgHasRender(pkg) { + return nil, ErrRenderNotDeclared + } + + // Write to the realm render fmt.Fprintf(w, "[%s]%s:", pkg.Domain, pkg.Path) // Return a dummy RealmMeta for simplicity @@ -89,3 +94,21 @@ func (m *MockWebClient) Sources(path string) ([]string, error) { return fileNames, nil } + +func pkgHasRender(pkg *MockPackage) bool { + if len(pkg.Functions) == 0 { + return false + } + + for _, fn := range pkg.Functions { + if fn.FuncName == "Render" && + len(fn.Params) == 1 && + len(fn.Results) == 1 && + fn.Params[0].Type == "string" && + fn.Results[0].Type == "string" { + return true + } + } + + return false +} diff --git a/gno.land/pkg/sdk/vm/errors.go b/gno.land/pkg/sdk/vm/errors.go index c8d6da98970..208fb074f7e 100644 --- a/gno.land/pkg/sdk/vm/errors.go +++ b/gno.land/pkg/sdk/vm/errors.go @@ -16,6 +16,7 @@ func (abciError) AssertABCIError() {} // NOTE: these are meant to be used in conjunction with pkgs/errors. type ( InvalidPkgPathError struct{ abciError } + NoRenderDeclError struct{ abciError } PkgExistError struct{ abciError } InvalidStmtError struct{ abciError } InvalidExprError struct{ abciError } @@ -27,6 +28,7 @@ type ( ) func (e InvalidPkgPathError) Error() string { return "invalid package path" } +func (e NoRenderDeclError) Error() string { return "render function not declared" } func (e PkgExistError) Error() string { return "package already exists" } func (e InvalidStmtError) Error() string { return "invalid statement" } func (e InvalidExprError) Error() string { return "invalid expression" } diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index c484e07e887..5aebf1afe46 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -129,9 +129,13 @@ func (vh vmHandler) queryRender(ctx sdk.Context, req abci.RequestQuery) (res abc expr := fmt.Sprintf("Render(%q)", path) result, err := vh.vm.QueryEvalString(ctx, pkgPath, expr) if err != nil { + if strings.Contains(err.Error(), "Render not declared") { + err = NoRenderDeclError{} + } res = sdk.ABCIResponseQueryFromError(err) return } + res.Data = []byte(result) return } diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go index 0359061ccea..95e97648dac 100644 --- a/gno.land/pkg/sdk/vm/package.go +++ b/gno.land/pkg/sdk/vm/package.go @@ -20,6 +20,7 @@ var Package = amino.RegisterPackage(amino.NewPackage( // errors InvalidPkgPathError{}, "InvalidPkgPathError", + NoRenderDeclError{}, "NoRenderDeclError", PkgExistError{}, "PkgExistError", InvalidStmtError{}, "InvalidStmtError", InvalidExprError{}, "InvalidExprError", From 533ae676090c26bc6c9efc81aec5322f10d05e90 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:51:50 +0100 Subject: [PATCH 5/9] fix(gnoweb): NoRender response & test (#3634) ## Description Changes the NoRender response to 200, since 204 does not allow for content body. Also fixes a test that didn't catch this. --- gno.land/pkg/gnoweb/app_test.go | 2 +- gno.land/pkg/gnoweb/handler.go | 2 +- gno.land/pkg/gnoweb/handler_test.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index eb17ee4d0e9..ce10cae12d5 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -47,7 +47,7 @@ func TestRoutes(t *testing.T) { {"/game-of-realms", found, "/contribute"}, {"/gor", found, "/contribute"}, {"/blog", found, "/r/gnoland/blog"}, - {"/r/docs/optional_render", http.StatusNoContent, "No Render"}, + {"/r/docs/optional_render", http.StatusOK, "No Render"}, {"/r/not/found/", notFound, ""}, {"/404/not/found", notFound, ""}, {"/아스키문자가아닌경로", notFound, ""}, diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index 822fd50fa1b..ac39f4ce0f9 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -161,7 +161,7 @@ func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) if err != nil { if errors.Is(err, ErrRenderNotDeclared) { - return http.StatusNoContent, components.StatusNoRenderComponent(gnourl.Path) + return http.StatusOK, components.StatusNoRenderComponent(gnourl.Path) } h.Logger.Error("unable to render realm", "error", err, "path", gnourl.EncodeURL()) diff --git a/gno.land/pkg/gnoweb/handler_test.go b/gno.land/pkg/gnoweb/handler_test.go index e85434a6f41..8321ad24be2 100644 --- a/gno.land/pkg/gnoweb/handler_test.go +++ b/gno.land/pkg/gnoweb/handler_test.go @@ -147,7 +147,7 @@ func TestWebHandler_NoRender(t *testing.T) { rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusNoContent, rr.Code, "unexpected status code") - assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "No Render") - assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "This realm does not implement a Render() function.") + assert.Equal(t, http.StatusOK, rr.Code, "unexpected status code") + expectedBody := "This realm does not implement a Render() function." + assert.Contains(t, rr.Body.String(), expectedBody, "rendered body should contain: %q", expectedBody) } From 15d119fbf21817bd667b9107966a10502e09f605 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:21:07 +0100 Subject: [PATCH 6/9] feat: optimize jitter factor calculation (#3629) --- tm2/pkg/p2p/switch.go | 90 ++++++++++++++++++++------------------ tm2/pkg/p2p/switch_test.go | 67 ++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 43 deletions(-) diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index 0dd087026dd..7d9e768dd4b 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -1,11 +1,12 @@ package p2p import ( + "bytes" "context" "crypto/rand" + "encoding/binary" "fmt" "math" - "math/big" "sync" "time" @@ -356,7 +357,7 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { type backoffItem struct { lastDialTime time.Time - attempts int + attempts uint } var ( @@ -482,65 +483,68 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { } } -// calculateBackoff calculates a backoff time, -// based on the number of attempts and range limits +// calculateBackoff calculates the backoff interval by exponentiating the base interval +// by the number of attempts. The returned interval is capped at maxInterval and has a +// jitter factor applied to it (+/- 10% of interval, max 10 sec). func calculateBackoff( - attempts int, - minTimeout time.Duration, - maxTimeout time.Duration, + attempts uint, + baseInterval time.Duration, + maxInterval time.Duration, ) time.Duration { - var ( - minTime = time.Second * 1 - maxTime = time.Second * 60 - multiplier = float64(2) // exponential + const ( + defaultBaseInterval = time.Second * 1 + defaultMaxInterval = time.Second * 60 ) - // Check the min limit - if minTimeout > 0 { - minTime = minTimeout + // Sanitize base interval parameter. + if baseInterval <= 0 { + baseInterval = defaultBaseInterval } - // Check the max limit - if maxTimeout > 0 { - maxTime = maxTimeout + // Sanitize max interval parameter. + if maxInterval <= 0 { + maxInterval = defaultMaxInterval } - // Sanity check the range - if minTime >= maxTime { - return maxTime + // Calculate the interval by exponentiating the base interval by the number of attempts. + interval := baseInterval << attempts + + // Cap the interval to the maximum interval. + if interval > maxInterval { + interval = maxInterval } - // Calculate the backoff duration - var ( - base = float64(minTime) - calculated = base * math.Pow(multiplier, float64(attempts)) - ) + // Below is the code to add a jitter factor to the interval. + // Read random bytes into an 8 bytes buffer (size of an int64). + var randBytes [8]byte + if _, err := rand.Read(randBytes[:]); err != nil { + return interval + } - // Attempt to calculate the jitter factor - n, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err == nil { - jitterFactor := float64(n.Int64()) / float64(math.MaxInt64) // range [0, 1] + // Convert the random bytes to an int64. + var randInt64 int64 + _ = binary.Read(bytes.NewReader(randBytes[:]), binary.NativeEndian, &randInt64) - calculated = jitterFactor*(calculated-base) + base - } + // Calculate the random jitter multiplier (float between -1 and 1). + jitterMultiplier := float64(randInt64) / float64(math.MaxInt64) - // Prevent overflow for int64 (duration) cast - if calculated > float64(math.MaxInt64) { - return maxTime - } + const ( + maxJitterDuration = 10 * time.Second + maxJitterPercentage = 10 // 10% + ) - duration := time.Duration(calculated) + // Calculate the maximum jitter based on interval percentage. + maxJitter := interval * maxJitterPercentage / 100 - // Clamp the duration within bounds - if duration < minTime { - return minTime + // Cap the maximum jitter to the maximum duration. + if maxJitter > maxJitterDuration { + maxJitter = maxJitterDuration } - if duration > maxTime { - return maxTime - } + // Calculate the jitter. + jitter := time.Duration(float64(maxJitter) * jitterMultiplier) - return duration + return interval + jitter } // DialPeers adds the peers to the dial queue for async dialing. diff --git a/tm2/pkg/p2p/switch_test.go b/tm2/pkg/p2p/switch_test.go index 19a5db2efa5..cf0a0c41bb5 100644 --- a/tm2/pkg/p2p/switch_test.go +++ b/tm2/pkg/p2p/switch_test.go @@ -823,3 +823,70 @@ func TestMultiplexSwitch_DialPeers(t *testing.T) { } }) } + +func TestCalculateBackoff(t *testing.T) { + t.Parallel() + + checkJitterRange := func(t *testing.T, expectedAbs, actual time.Duration) { + t.Helper() + require.LessOrEqual(t, actual, expectedAbs) + require.GreaterOrEqual(t, actual, expectedAbs*-1) + } + + // Test that the default jitter factor is 10% of the backoff duration. + t.Run("percentage jitter", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(0, time.Second, 10*time.Minute)-time.Second) + checkJitterRange(t, 200*time.Millisecond, calculateBackoff(1, time.Second, 10*time.Minute)-2*time.Second) + checkJitterRange(t, 400*time.Millisecond, calculateBackoff(2, time.Second, 10*time.Minute)-4*time.Second) + checkJitterRange(t, 800*time.Millisecond, calculateBackoff(3, time.Second, 10*time.Minute)-8*time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(4, time.Second, 10*time.Minute)-16*time.Second) + } + }) + + // Test that the jitter factor is capped at 10 sec. + t.Run("capped jitter", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 10*time.Second, calculateBackoff(7, time.Second, 10*time.Minute)-128*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(10, time.Second, 20*time.Minute)-1024*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(20, time.Second, 300*time.Hour)-1048576*time.Second) + } + }) + + // Test that the backoff interval is based on the baseInterval. + t.Run("base interval", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 4800*time.Millisecond, calculateBackoff(4, 3*time.Second, 10*time.Minute)-48*time.Second) + checkJitterRange(t, 8*time.Second, calculateBackoff(3, 10*time.Second, 10*time.Minute)-80*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(5, 3*time.Hour, 100*time.Hour)-96*time.Hour) + } + }) + + // Test that the backoff interval is capped at maxInterval +/- jitter factor. + t.Run("max interval", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(10, 10*time.Hour, time.Second)-time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(10, 10*time.Hour, 16*time.Second)-16*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(10, 10*time.Hour, 128*time.Second)-128*time.Second) + } + }) + + // Test parameters sanitization for base and max intervals. + t.Run("parameters sanitization", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(0, -10, -10)-time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(4, -10, -10)-16*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(7, -10, 10*time.Minute)-128*time.Second) + } + }) +} From b392287f0d2c8262b5a020cd045b79a16ccb41fd Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:53:57 +0100 Subject: [PATCH 7/9] feat: gno mod graph (#3588) Basic initial version compatible with `go mod graph` in terms of output, while allowing the specification of folders through an optional argument. - [x] implement - [x] tests - [x] share examples Depends on #3587 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- gnovm/cmd/gno/mod.go | 49 ++++++++++++++++++++++++++++++++++++++- gnovm/cmd/gno/mod_test.go | 28 ++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index f303908d8ee..e394684561f 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -34,7 +34,7 @@ func newModCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newModDownloadCmd(io), // edit - // graph + newModGraphCmd(io), newModInitCmd(), newModTidy(io), // vendor @@ -61,6 +61,21 @@ func newModDownloadCmd(io commands.IO) *commands.Command { ) } +func newModGraphCmd(io commands.IO) *commands.Command { + cfg := &modGraphCfg{} + return commands.NewCommand( + commands.Metadata{ + Name: "graph", + ShortUsage: "graph [path]", + ShortHelp: "print module requirement graph", + }, + cfg, + func(_ context.Context, args []string) error { + return execModGraph(cfg, args, io) + }, + ) +} + func newModInitCmd() *commands.Command { return commands.NewCommand( commands.Metadata{ @@ -144,6 +159,38 @@ func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { ) } +type modGraphCfg struct{} + +func (c *modGraphCfg) RegisterFlags(fs *flag.FlagSet) { + // /out std + // /out remote + // /out _test processing + // ... +} + +func execModGraph(cfg *modGraphCfg, args []string, io commands.IO) error { + // default to current directory if no args provided + if len(args) == 0 { + args = []string{"."} + } + if len(args) > 1 { + return flag.ErrHelp + } + + stdout := io.Out() + + pkgs, err := gnomod.ListPkgs(args[0]) + if err != nil { + return err + } + for _, pkg := range pkgs { + for _, dep := range pkg.Imports { + fmt.Fprintf(stdout, "%s %s\n", pkg.Name, dep) + } + } + return nil +} + func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { if len(args) > 0 { return flag.ErrHelp diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go index afce25597cd..e6fdce50a86 100644 --- a/gnovm/cmd/gno/mod_test.go +++ b/gnovm/cmd/gno/mod_test.go @@ -210,6 +210,34 @@ func TestModApp(t *testing.T) { # gno.land/p/demo/avl valid.gno +`, + }, + + // test `gno mod graph` + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/minimalist_gnomod", + simulateExternalRepo: true, + stdoutShouldBe: ``, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/valid1", + simulateExternalRepo: true, + stdoutShouldBe: ``, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/valid2", + simulateExternalRepo: true, + stdoutShouldBe: `gno.land/p/integ/valid gno.land/p/demo/avl +`, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/require_remote_module", + simulateExternalRepo: true, + stdoutShouldBe: `gno.land/tests/importavl gno.land/p/demo/avl `, }, } From 57da32437daa07a76e9478b1704832c0a211cfd2 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 30 Jan 2025 17:51:43 +0100 Subject: [PATCH 8/9] chore: Trigger CI tests on changes to the main go.mod (#3648) Resolves https://github.com/gnolang/gno/issues/3312 After a dependabot commit, the dependabot-tidy workflow [runs `make tidy`](https://github.com/gnolang/gno/blob/b392287f0d2c8262b5a020cd045b79a16ccb41fd/.github/workflows/dependabot-tidy.yml#L33). This changes `go.mod` in the subfolders of various tools, but these do not trigger the main CI test workflows. However, note that `make tidy` often also changes the main `go.mod` file. In this case, and other cases, it makes sense that a change in the main `go.mod` file should trigger the testing workflows since a change to dependency versions could effect test results. This PR updates workflow yml files for the global tests to trigger on a change the main `go.mod` file. (Thanks to feedback from @zivkovicmilos.) Signed-off-by: Jeff Thompson --- .github/workflows/gnoland.yml | 3 +++ .github/workflows/gnovm.yml | 3 +++ .github/workflows/tm2.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index b02e7b364e6..c4bc26a45fc 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -14,6 +14,9 @@ on: # Changes to examples/ can create failures in gno.land, eg. txtars, # see: https://github.com/gnolang/gno/pull/3590 - examples/** + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 7a015b74e09..08b0b66c4e8 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -8,6 +8,9 @@ on: paths: - gnovm/** - tm2/** # GnoVM has a dependency on TM2 types + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 757391eab8c..d2157eb8828 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -7,6 +7,9 @@ on: pull_request: paths: - tm2/** + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: From d3774cefc766b6e5a9b464e4974e484a876408e9 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <72015889+0xtekgrinder@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:04:56 -0500 Subject: [PATCH 9/9] feat: ownable2step realm (#3594) Addresses: #3520 Add a ownable variant realm to have a transfer of ownership with two functions where the new owner needs to accept ownership to be sure no mistake can be made. --------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../p/oxtekgrinder/ownable2step/errors.gno | 10 ++ .../p/oxtekgrinder/ownable2step/gno.mod | 1 + .../p/oxtekgrinder/ownable2step/ownable.gno | 98 +++++++++++ .../ownable2step/ownable_test.gno | 156 ++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno new file mode 100644 index 00000000000..6d91c9eb24b --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno @@ -0,0 +1,10 @@ +package ownable2step + +import "errors" + +var ( + ErrNoPendingOwner = errors.New("ownable2step: no pending owner") + ErrUnauthorized = errors.New("ownable2step: caller is not owner") + ErrPendingUnauthorized = errors.New("ownable2step: caller is not pending owner") + ErrInvalidAddress = errors.New("ownable2step: new owner address is invalid") +) diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod b/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod new file mode 100644 index 00000000000..0132a03418c --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod @@ -0,0 +1 @@ +module gno.land/p/oxtekgrinder/ownable2step diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno new file mode 100644 index 00000000000..43afa1cd141 --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno @@ -0,0 +1,98 @@ +package ownable2step + +import ( + "std" +) + +const OwnershipTransferEvent = "OwnershipTransfer" + +// Ownable2Step is a two-step ownership transfer package +// It allows the current owner to set a new owner and the new owner will need to accept the ownership before it is transferred +type Ownable2Step struct { + owner std.Address + pendingOwner std.Address +} + +func New() *Ownable2Step { + return &Ownable2Step{ + owner: std.PrevRealm().Addr(), + pendingOwner: "", + } +} + +func NewWithAddress(addr std.Address) *Ownable2Step { + return &Ownable2Step{ + owner: addr, + pendingOwner: "", + } +} + +// TransferOwnership initiate the transfer of the ownership to a new address by setting the PendingOwner +func (o *Ownable2Step) TransferOwnership(newOwner std.Address) error { + if !o.CallerIsOwner() { + return ErrUnauthorized + } + if !newOwner.IsValid() { + return ErrInvalidAddress + } + + o.pendingOwner = newOwner + return nil +} + +// AcceptOwnership accepts the pending ownership transfer +func (o *Ownable2Step) AcceptOwnership() error { + if o.pendingOwner.String() == "" { + return ErrNoPendingOwner + } + if std.PrevRealm().Addr() != o.pendingOwner { + return ErrPendingUnauthorized + } + + o.owner = o.pendingOwner + o.pendingOwner = "" + + return nil +} + +// DropOwnership removes the owner, effectively disabling any owner-related actions +// Top-level usage: disables all only-owner actions/functions, +// Embedded usage: behaves like a burn functionality, removing the owner from the struct +func (o *Ownable2Step) DropOwnership() error { + if !o.CallerIsOwner() { + return ErrUnauthorized + } + + prevOwner := o.owner + o.owner = "" + + std.Emit( + OwnershipTransferEvent, + "from", prevOwner.String(), + "to", "", + ) + + return nil +} + +// Owner returns the owner address from Ownable +func (o *Ownable2Step) Owner() std.Address { + return o.owner +} + +// PendingOwner returns the pending owner address from Ownable2Step +func (o *Ownable2Step) PendingOwner() std.Address { + return o.pendingOwner +} + +// CallerIsOwner checks if the caller of the function is the Realm's owner +func (o *Ownable2Step) CallerIsOwner() bool { + return std.PrevRealm().Addr() == o.owner +} + +// AssertCallerIsOwner panics if the caller is not the owner +func (o *Ownable2Step) AssertCallerIsOwner() { + if std.PrevRealm().Addr() != o.owner { + panic(ErrUnauthorized) + } +} diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno new file mode 100644 index 00000000000..4cca03b6ef5 --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno @@ -0,0 +1,156 @@ +package ownable2step + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") +) + +func TestNew(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + got := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, got, alice) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestNewWithAddress(t *testing.T) { + o := NewWithAddress(alice) + + got := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, got, alice) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestInitiateTransferOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) +} + +func TestTransferOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + err = o.AcceptOwnership() + urequire.NoError(t, err) + + owner = o.Owner() + pendingOwner = o.PendingOwner() + + uassert.Equal(t, owner, bob) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestCallerIsOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + unauthorizedCaller := bob + + std.TestSetRealm(std.NewUserRealm(unauthorizedCaller)) + std.TestSetOrigCaller(unauthorizedCaller) + + uassert.False(t, o.CallerIsOwner()) +} + +func TestDropOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.DropOwnership() + urequire.NoError(t, err, "DropOwnership failed") + + owner := o.Owner() + uassert.Empty(t, owner, "owner should be empty") +} + +// Errors + +func TestErrUnauthorized(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error()) +} + +func TestErrInvalidAddress(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.TransferOwnership("") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) + + err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) +} + +func TestErrNoPendingOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrNoPendingOwner.Error()) +} + +func TestErrPendingUnauthorized(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + std.TestSetRealm(std.NewUserRealm(alice)) + + err = o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrPendingUnauthorized.Error()) +}