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 1/7] 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 2/7] 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 3/7] 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:
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" }}
{{ 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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: