diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 5d606a2a663..ffe40c2db7e 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -29,7 +29,7 @@ jobs: with: go-version: ${{ matrix.goversion }} - run: go install -v ./gnovm/cmd/gno - - run: go run ./gnovm/cmd/gno transpile -v --gobuild ./examples + - run: go run ./gnovm/cmd/gno tool transpile -v --gobuild ./examples test: strategy: fail-fast: false 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: diff --git a/.gitignore b/.gitignore index 7d3f3f92b41..82cc109887e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ coverage.out *.swp *.swo *.bak + +.tmp/ +wal/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b58d63c6c75..9562a531227 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -198,7 +198,7 @@ Additionally, it's not possible to use `gofumpt` for code formatting with (flycheck-define-checker gno-lint "A GNO syntax checker using the gno lint tool." - :command ("gno" "lint" source-original) + :command ("gno" "tool" "lint" source-original) :error-patterns (;; ./file.gno:32: error message (code=1) (error line-start (file-name) ":" line ": " (message) " (code=" (id (one-or-more digit)) ")." line-end)) ;; Ensure the file is saved, to work around diff --git a/docs/reference/go-gno-compatibility.md b/docs/reference/go-gno-compatibility.md index 9f9d611e4fd..87c734d935f 100644 --- a/docs/reference/go-gno-compatibility.md +++ b/docs/reference/go-gno-compatibility.md @@ -297,31 +297,31 @@ Legend: ## Tooling (`gno` binary) -| go command | gno command | comment | -|-------------------|---------------------------|-----------------------------------------------------------------------| -| go bug | gno bug | same behavior | -| go build | gno transpile -gobuild | same intention, limited compatibility | -| go clean | gno clean | same intention, limited compatibility | -| go doc | gno doc | limited compatibility; see https://github.com/gnolang/gno/issues/522 | -| go env | gno env | | -| go fix | | | -| go fmt | gno fmt | gofmt (& similar tools, like gofumpt) works on gno code. | -| go generate | | | -| go get | | see `gno mod download`. | -| go help | gno $cmd --help | ie. `gno doc --help` | -| go install | | | -| go list | | | -| go mod | gno mod | | -| + go mod init | gno mod init | same behavior | -| + go mod download | gno mod download | same behavior | -| + go mod tidy | gno mod tidy | same behavior | -| + go mod why | gno mod why | same intention | -| | gno transpile | | -| go work | | | -| | gno repl | | -| go run | gno run | | -| go test | gno test | limited compatibility | -| go tool | | | -| go version | | | -| go vet | | | -| golint | gno lint | same intention | +| go command | gno command | comment | +|-------------------|------------------------------|-----------------------------------------------------------------------| +| go bug | gno bug | same behavior | +| go build | gno tool transpile -gobuild | same intention, limited compatibility | +| go clean | gno clean | same intention, limited compatibility | +| go doc | gno doc | limited compatibility; see https://github.com/gnolang/gno/issues/522 | +| go env | gno env | | +| go fix | | | +| go fmt | gno fmt | gofmt (& similar tools, like gofumpt) works on gno code. | +| go generate | | | +| go get | | see `gno mod download`. | +| go help | gno $cmd --help | ie. `gno doc --help` | +| go install | | | +| go list | | | +| go mod | gno mod | | +| + go mod init | gno mod init | same behavior | +| + go mod download | gno mod download | same behavior | +| + go mod tidy | gno mod tidy | same behavior | +| + go mod why | gno mod why | same intention | +| | gno tool transpile | | +| go work | | | +| | gno tool repl | | +| go run | gno run | | +| go test | gno test | limited compatibility | +| go tool | | | +| go version | | | +| go vet | | | +| golint | gno tool lint | same intention | diff --git a/examples/Makefile b/examples/Makefile index cdc73ee6b3a..63a20f78eb9 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -33,11 +33,11 @@ OFFICIAL_PACKAGES += ./gno.land/r/gov # Dev tools .PHONY: transpile transpile: - go run ../gnovm/cmd/gno transpile -v . + go run ../gnovm/cmd/gno tool transpile -v . .PHONY: build build: - go run ../gnovm/cmd/gno transpile -v --gobuild . + go run ../gnovm/cmd/gno tool transpile -v --gobuild . .PHONY: test test: @@ -45,7 +45,7 @@ test: .PHONY: lint lint: - go run ../gnovm/cmd/gno lint -v $(OFFICIAL_PACKAGES) + go run ../gnovm/cmd/gno tool lint -v $(OFFICIAL_PACKAGES) .PHONY: test.sync test.sync: 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..b4c78205c0a 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -13,7 +13,9 @@ 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. +- [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/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() -} diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index 6fb69c6d984..ce10cae12d5 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.StatusOK, "No Render"}, {"/r/not/found/", notFound, ""}, {"/404/not/found", notFound, ""}, {"/아스키문자가아닌경로", notFound, ""}, diff --git a/gno.land/pkg/gnoweb/components/layout_index.go b/gno.land/pkg/gnoweb/components/layout_index.go index 8b49e8f8ada..f56ddfe1514 100644 --- a/gno.land/pkg/gnoweb/components/layout_index.go +++ b/gno.land/pkg/gnoweb/components/layout_index.go @@ -15,6 +15,8 @@ type HeadData struct { ChromaPath string AssetsPath string Analytics bool + Remote string + ChainId string } type IndexData struct { diff --git a/gno.land/pkg/gnoweb/components/layouts/head.html b/gno.land/pkg/gnoweb/components/layouts/head.html index 1b78eeeb324..72f255ebdf6 100644 --- a/gno.land/pkg/gnoweb/components/layouts/head.html +++ b/gno.land/pkg/gnoweb/components/layouts/head.html @@ -40,5 +40,9 @@ + + + + {{ end }} 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 3fdfc33909c..ac39f4ce0f9 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -90,6 +90,8 @@ func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) { HeadData: components.HeadData{ AssetsPath: h.Static.AssetsPath, ChromaPath: h.Static.ChromaPath, + ChainId: h.Static.ChainId, + Remote: h.Static.RemoteHelp, }, FooterData: components.FooterData{ Analytics: h.Static.Analytics, @@ -112,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) @@ -128,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") } } @@ -158,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.StatusOK, components.StatusNoRenderComponent(gnourl.Path) + } + h.Logger.Error("unable to render realm", "error", err, "path", gnourl.EncodeURL()) return GetClientErrorStatusPage(gnourl, err) } @@ -221,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 @@ -264,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{ @@ -281,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..8321ad24be2 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.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) +} 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", diff --git a/gnovm/Makefile b/gnovm/Makefile index babb7ad74ca..ce745e44aae 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -54,8 +54,8 @@ lint: .PHONY: fmt fmt: - go run ./cmd/gno fmt $(GNOFMT_FLAGS) ./stdlibs/... ./tests/stdlibs/... $(rundep) mvdan.cc/gofumpt $(GOFMT_FLAGS) . + go run ./cmd/gno fmt $(GNOFMT_FLAGS) ./stdlibs/... ./tests/stdlibs/... .PHONY: imports imports: @@ -125,7 +125,13 @@ _test.filetest:; # TODO: move _dev.stringer to go:generate instructions, simplify generate # to just go generate. .PHONY: generate -generate: _dev.stringer _dev.generate +generate: _dev.stringer _dev.generate _dev.docs + +.PHONY: _dev.docs +_dev.docs: + mkdir -p .tmp + (go run ./cmd/gno -h 2>&1 || true) | grep -v "exit status 1" > .tmp/gno-help.txt + $(rundep) github.com/campoy/embedmd -w `find . -name "*.md"` stringer_cmd=$(rundep) golang.org/x/tools/cmd/stringer .PHONY: _dev.stringer diff --git a/gnovm/cmd/gno/README.md b/gnovm/cmd/gno/README.md index f900b164783..81b45622c05 100644 --- a/gnovm/cmd/gno/README.md +++ b/gnovm/cmd/gno/README.md @@ -6,13 +6,25 @@ `gno [arguments]` -## Commands +## Usage -* `gno run` - run a Gno file -* `gno transpile` - transpile .gno to .go -* `gno test` - test a gno package -* `gno mod` - manages dependencies -* `gno repl` start a GnoVM REPL +[embedmd]:#(../../.tmp/gno-help.txt) +```txt +USAGE + gno [arguments] + +SUBCOMMANDS + bug start a bug report + clean remove generated and cached data + doc show documentation for package or symbol + env print gno environment information + fmt gnofmt (reformat) package sources + mod module maintenance + run run gno packages + test test packages + tool run specified gno tool + +``` ## Install diff --git a/gnovm/cmd/gno/bug.go b/gnovm/cmd/gno/bug.go index a1273d9ad59..7a4345fb1ed 100644 --- a/gnovm/cmd/gno/bug.go +++ b/gnovm/cmd/gno/bug.go @@ -61,7 +61,7 @@ func newBugCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "bug", ShortUsage: "bug", - ShortHelp: "Start a bug report", + ShortHelp: "start a bug report", LongHelp: `opens https://github.com/gnolang/gno/issues in a browser. The new issue body is prefilled for you with the following information: diff --git a/gnovm/cmd/gno/clean.go b/gnovm/cmd/gno/clean.go index 0ca2e940d58..7c318c3957e 100644 --- a/gnovm/cmd/gno/clean.go +++ b/gnovm/cmd/gno/clean.go @@ -26,7 +26,7 @@ func newCleanCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "clean", ShortUsage: "clean [flags]", - ShortHelp: "removes generated files and cached data", + ShortHelp: "remove generated and cached data", }, cfg, func(ctx context.Context, args []string) error { diff --git a/gnovm/cmd/gno/doc.go b/gnovm/cmd/gno/doc.go index 794dd1ba7bb..c34ed984d9c 100644 --- a/gnovm/cmd/gno/doc.go +++ b/gnovm/cmd/gno/doc.go @@ -29,7 +29,8 @@ func newDocCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "doc", ShortUsage: "doc [flags] ", - ShortHelp: "get documentation for the specified package or symbol (type, function, method, or variable/constant)", + ShortHelp: "show documentation for package or symbol", + LongHelp: "get documentation for the specified package or symbol (type, function, method, or variable/constant)", }, c, func(_ context.Context, args []string) error { diff --git a/gnovm/cmd/gno/env.go b/gnovm/cmd/gno/env.go index 9c601a270c7..20d32c85ec0 100644 --- a/gnovm/cmd/gno/env.go +++ b/gnovm/cmd/gno/env.go @@ -18,7 +18,7 @@ func newEnvCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "env", ShortUsage: "env [flags] ", - ShortHelp: "`env` prints Gno environment information", + ShortHelp: "print gno environment information", }, c, func(_ context.Context, args []string) error { diff --git a/gnovm/cmd/gno/fmt.go b/gnovm/cmd/gno/fmt.go index de6c28c4df0..e37c50a079f 100644 --- a/gnovm/cmd/gno/fmt.go +++ b/gnovm/cmd/gno/fmt.go @@ -35,7 +35,7 @@ func newFmtCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "fmt", ShortUsage: "gno fmt [flags] [path ...]", - ShortHelp: "Run gno file formatter.", + ShortHelp: "gnofmt (reformat) package sources", LongHelp: "The `gno fmt` tool processes, formats, and cleans up `gno` source files.", }, cfg, diff --git a/gnovm/cmd/gno/main.go b/gnovm/cmd/gno/main.go index 7a5799f2835..5f8bb7b522e 100644 --- a/gnovm/cmd/gno/main.go +++ b/gnovm/cmd/gno/main.go @@ -16,33 +16,32 @@ func main() { func newGnocliCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ - ShortUsage: " [flags] [...]", - LongHelp: "Runs the gno development toolkit", + ShortUsage: "gno [arguments]", }, commands.NewEmptyConfig(), commands.HelpExec, ) cmd.AddSubCommands( - newModCmd(io), - newTestCmd(io), - newLintCmd(io), - newRunCmd(io), - newTranspileCmd(io), + newBugCmd(io), + // build newCleanCmd(io), - newReplCmd(), newDocCmd(io), newEnvCmd(io), - newBugCmd(io), + // fix newFmtCmd(io), - // graph - // vendor -- download deps from the chain in vendor/ - // list -- list packages - // render -- call render()? - // publish/release // generate - // "vm" -- starts an in-memory chain that can be interacted with? + // get + // install + // list -- list packages + newModCmd(io), + // work + newRunCmd(io), + // telemetry + newTestCmd(io), + newToolCmd(io), // version -- show cmd/gno, golang versions + // vet ) return cmd diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 5479d934ce6..e394684561f 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -25,7 +25,7 @@ func newModCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "mod", ShortUsage: "mod ", - ShortHelp: "manage gno.mod", + ShortHelp: "module maintenance", }, commands.NewEmptyConfig(), commands.HelpExec, @@ -33,8 +33,12 @@ func newModCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newModDownloadCmd(io), + // edit + newModGraphCmd(io), newModInitCmd(), newModTidy(io), + // vendor + // verify newModWhy(io), ) @@ -57,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{ @@ -140,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 `, }, } diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index 9a9beac5cd1..489016aa3d4 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -32,7 +32,7 @@ func newRunCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "run", ShortUsage: "run [flags] [...]", - ShortHelp: "runs the specified gno files", + ShortHelp: "run gno packages", }, cfg, func(_ context.Context, args []string) error { diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index ea06b25d8e2..ae67d69bc90 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -35,7 +35,7 @@ func newTestCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "test", ShortUsage: "test [flags] [...]", - ShortHelp: "runs the tests for the specified packages", + ShortHelp: "test packages", LongHelp: `Runs the tests for the specified packages. 'gno test' recompiles each package along with any files with names matching the diff --git a/gnovm/cmd/gno/testdata/lint/bad_import.txtar b/gnovm/cmd/gno/testdata/lint/bad_import.txtar index e2c0431443c..117a699fa6c 100644 --- a/gnovm/cmd/gno/testdata/lint/bad_import.txtar +++ b/gnovm/cmd/gno/testdata/lint/bad_import.txtar @@ -1,6 +1,6 @@ -# testing gno lint command: bad import error +# testing gno tool lint command: bad import error -! gno lint ./bad_file.gno +! gno tool lint ./bad_file.gno cmp stdout stdout.golden cmp stderr stderr.golden diff --git a/gnovm/cmd/gno/testdata/lint/file_error.txtar b/gnovm/cmd/gno/testdata/lint/file_error.txtar index 4fa50c6da81..f3301b46757 100644 --- a/gnovm/cmd/gno/testdata/lint/file_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/file_error.txtar @@ -1,6 +1,6 @@ -# gno lint: test file error +# gno tool lint: test file error -! gno lint ./i_have_error_test.gno +! gno tool lint ./i_have_error_test.gno cmp stdout stdout.golden cmp stderr stderr.golden diff --git a/gnovm/cmd/gno/testdata/lint/no_error.txtar b/gnovm/cmd/gno/testdata/lint/no_error.txtar index 5dd3b164952..5033e054ac6 100644 --- a/gnovm/cmd/gno/testdata/lint/no_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_error.txtar @@ -1,6 +1,6 @@ -# testing simple gno lint command with any error +# testing simple gno tool lint command with any error -gno lint ./good_file.gno +gno tool lint ./good_file.gno cmp stdout stdout.golden cmp stdout stderr.golden diff --git a/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar index b5a046a7095..4564f2d9fff 100644 --- a/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar @@ -1,6 +1,6 @@ -# gno lint: no gnomod +# gno tool lint: no gnomod -! gno lint . +! gno tool lint . cmp stdout stdout.golden cmp stderr stderr.golden diff --git a/gnovm/cmd/gno/testdata/lint/not_declared.txtar b/gnovm/cmd/gno/testdata/lint/not_declared.txtar index ac56b27e0df..3c3c304797e 100644 --- a/gnovm/cmd/gno/testdata/lint/not_declared.txtar +++ b/gnovm/cmd/gno/testdata/lint/not_declared.txtar @@ -1,6 +1,6 @@ -# testing gno lint command: not declared error +# testing gno tool lint command: not declared error -! gno lint ./bad_file.gno +! gno tool lint ./bad_file.gno cmp stdout stdout.golden cmp stderr stderr.golden diff --git a/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar index 145fe796c09..0b41319a190 100644 --- a/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar +++ b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar @@ -1,7 +1,7 @@ -# Run gno transpile with -gobuild flag +# Run gno tool transpile with -gobuild flag # The error messages changed sometime in go1.23, so this avoids errors -! gno transpile -gobuild . +! gno tool transpile -gobuild . ! stdout .+ stderr '^main.gno:4:6: .*declared and not used' diff --git a/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar index 629de2b7f3d..1008d45adb2 100644 --- a/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar +++ b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with -gobuild flag on file with parse error +# Run gno tool transpile with -gobuild flag on file with parse error -! gno transpile -gobuild . +! gno tool transpile -gobuild . ! stdout .+ stderr '^main.gno:3:1: expected declaration, found invalid$' diff --git a/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar b/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar index 0c51012feb7..23e9e805fe5 100644 --- a/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar +++ b/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with gno files with an invalid import path +# Run gno tool transpile with gno files with an invalid import path -! gno transpile . +! gno tool transpile . ! stdout .+ stderr '^main.gno:5:2: import "xxx" does not exist$' diff --git a/gnovm/cmd/gno/testdata/transpile/no_args.txtar b/gnovm/cmd/gno/testdata/transpile/no_args.txtar index 6d8592d4e3b..30ea24fbf73 100644 --- a/gnovm/cmd/gno/testdata/transpile/no_args.txtar +++ b/gnovm/cmd/gno/testdata/transpile/no_args.txtar @@ -1,6 +1,6 @@ -# Run gno transpile without args +# Run gno tool transpile without args -! gno transpile +! gno tool transpile ! stdout .+ stderr 'USAGE' diff --git a/gnovm/cmd/gno/testdata/transpile/parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/parse_error.txtar index b94b86992af..c19276ef273 100644 --- a/gnovm/cmd/gno/testdata/transpile/parse_error.txtar +++ b/gnovm/cmd/gno/testdata/transpile/parse_error.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with gno files with parse errors +# Run gno tool transpile with gno files with parse errors -! gno transpile . +! gno tool transpile . ! stdout .+ stderr '^main.gno:3:1: expected declaration, found invalid$' diff --git a/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar b/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar index 2bd1841d2b4..f9ccd8d8b53 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar @@ -1,6 +1,6 @@ -# Run gno transpile on an empty dir +# Run gno tool transpile on an empty dir -gno transpile . +gno tool transpile . ! stdout .+ ! stderr .+ diff --git a/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar index 40bb1ecb98a..8e95c5994d0 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with -gobuild flag on an individual file +# Run gno tool transpile with -gobuild flag on an individual file -gno transpile -gobuild -v main.gno +gno tool transpile -gobuild -v main.gno ! stdout .+ cmp stderr stderr.golden diff --git a/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar index 2eacfb9de60..72ca7a4f2f4 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with -gobuild flag +# Run gno tool transpile with -gobuild flag -gno transpile -gobuild -v . +gno tool transpile -gobuild -v . ! stdout .+ cmp stderr stderr.golden @@ -12,7 +12,7 @@ cmp sub/sub.gno.gen.go sub/sub.gno.gen.go.golden rm mai.gno.gen.gosub/sub.gno.gen.go # Re-try, but use an absolute path. -gno transpile -gobuild -v $WORK +gno tool transpile -gobuild -v $WORK ! stdout .+ cmpenv stderr stderr2.golden diff --git a/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar index b1a63890f46..62953e96fb7 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with valid gno files, using the -output flag. +# Run gno tool transpile with valid gno files, using the -output flag. -gno transpile -v -output directory/hello/ . +gno tool transpile -v -output directory/hello/ . ! stdout .+ cmp stderr stderr1.golden @@ -9,7 +9,7 @@ exists directory/hello/main.gno.gen.go rm directory # Try running using the absolute path to the directory. -gno transpile -v -output directory/hello $WORK +gno tool transpile -v -output directory/hello $WORK ! stdout .+ cmpenv stderr stderr2.golden @@ -20,7 +20,7 @@ rm directory # Try running in subdirectory, using a "relative non-local path." (ie. has "../") mkdir subdir cd subdir -gno transpile -v -output hello .. +gno tool transpile -v -output hello .. ! stdout .+ cmpenv stderr ../stderr3.golden diff --git a/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar index 3540e865f3e..c948e53ebb3 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with valid gno files, using the -output and -gobuild flags together. +# Run gno tool transpile with valid gno files, using the -output and -gobuild flags together. -gno transpile -v -output directory/hello/ -gobuild . +gno tool transpile -v -output directory/hello/ -gobuild . ! stdout .+ cmp stderr stderr1.golden @@ -9,7 +9,7 @@ exists directory/hello/main.gno.gen.go rm directory # Try running using the absolute path to the directory. -gno transpile -v -output directory/hello -gobuild $WORK +gno tool transpile -v -output directory/hello -gobuild $WORK ! stdout .+ cmpenv stderr stderr2.golden @@ -20,7 +20,7 @@ rm directory # Try running in subdirectory, using a "relative non-local path." (ie. has "../") mkdir subdir cd subdir -gno transpile -v -output hello -gobuild .. +gno tool transpile -v -output hello -gobuild .. ! stdout .+ cmpenv stderr ../stderr3.golden diff --git a/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar index 86cc6f12f7a..906c43cc41b 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar @@ -1,8 +1,8 @@ -# Run gno transpile with an individual file. +# Run gno tool transpile with an individual file. # Running transpile on the current directory should only precompile # main.gno. -gno transpile -v . +gno tool transpile -v . ! stdout .+ stderr ^\.$ @@ -12,7 +12,7 @@ exists main.gno.gen.go rm main.gno.gen.go # Running it using individual filenames should precompile hello_test.gno, as well. -gno transpile -v main.gno hello_test.gno +gno tool transpile -v main.gno hello_test.gno ! stdout .+ cmp stderr transpile-files-stderr.golden diff --git a/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar index 2a24423598a..74923b9846a 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar @@ -1,6 +1,6 @@ -# Run gno transpile with valid gno files +# Run gno tool transpile with valid gno files -gno transpile . +gno tool transpile . ! stdout .+ ! stderr .+ diff --git a/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar index a765ab5093b..e0c42d986e4 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar @@ -1,7 +1,7 @@ -# Run gno transpile with dependencies +# Run gno tool transpile with dependencies env GNOROOT=$WORK -gno transpile -v ./examples +gno tool transpile -v ./examples ! stdout .+ cmpenv stderr stderr.golden diff --git a/gnovm/cmd/gno/tool.go b/gnovm/cmd/gno/tool.go new file mode 100644 index 00000000000..0e4f7ff51d7 --- /dev/null +++ b/gnovm/cmd/gno/tool.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func newToolCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "tool", + ShortUsage: "gno tool command [args...]", + ShortHelp: "run specified gno tool", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + // go equivalent commands: + // + // compile + // transpile + // pprof + // trace + // vet + + // gno specific commands: + // + // ast + newLintCmd(io), + // publish/release + // render -- call render()? + newReplCmd(), + newTranspileCmd(io), + // "vm" -- starts an in-memory chain that can be interacted with? + ) + + return cmd +} diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/tool_lint.go similarity index 100% rename from gnovm/cmd/gno/lint.go rename to gnovm/cmd/gno/tool_lint.go diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/tool_lint_test.go similarity index 70% rename from gnovm/cmd/gno/lint_test.go rename to gnovm/cmd/gno/tool_lint_test.go index 4589fc55f92..85b625fa367 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/tool_lint_test.go @@ -8,26 +8,26 @@ import ( func TestLintApp(t *testing.T) { tc := []testMainCase{ { - args: []string{"lint"}, + args: []string{"tool", "lint"}, errShouldBe: "flag: help requested", }, { - args: []string{"lint", "../../tests/integ/run_main/"}, + args: []string{"tool", "lint", "../../tests/integ/run_main/"}, stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)", errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, + args: []string{"tool", "lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, stderrShouldContain: "undefined_variables_test.gno:6:28: name toto not declared (code=2)", errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"}, + args: []string{"tool", "lint", "../../tests/integ/package_not_declared/main.gno"}, stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2)", errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/several-lint-errors/main.gno"}, + args: []string{"tool", "lint", "../../tests/integ/several-lint-errors/main.gno"}, stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2)\n../../tests/integ/several-lint-errors/main.gno:6", errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, + args: []string{"tool", "lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, stderrShouldContain: func() string { lines := []string{ "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", @@ -39,17 +39,17 @@ func TestLintApp(t *testing.T) { }(), errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/minimalist_gnomod/"}, + args: []string{"tool", "lint", "../../tests/integ/minimalist_gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files }, { - args: []string{"lint", "../../tests/integ/invalid_module_name/"}, + args: []string{"tool", "lint", "../../tests/integ/invalid_module_name/"}, // TODO: raise an error because gno.mod is invalid }, { - args: []string{"lint", "../../tests/integ/invalid_gno_file/"}, + args: []string{"tool", "lint", "../../tests/integ/invalid_gno_file/"}, stderrShouldContain: "../../tests/integ/invalid_gno_file/invalid.gno:1:1: expected 'package', found packag (code=2)", errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/typecheck_missing_return/"}, + args: []string{"tool", "lint", "../../tests/integ/typecheck_missing_return/"}, stderrShouldContain: "../../tests/integ/typecheck_missing_return/main.gno:5:1: missing return (code=4)", errShouldBe: "exit code: 1", }, diff --git a/gnovm/cmd/gno/repl.go b/gnovm/cmd/gno/tool_repl.go similarity index 100% rename from gnovm/cmd/gno/repl.go rename to gnovm/cmd/gno/tool_repl.go diff --git a/gnovm/cmd/gno/repl_test.go b/gnovm/cmd/gno/tool_repl_test.go similarity index 100% rename from gnovm/cmd/gno/repl_test.go rename to gnovm/cmd/gno/tool_repl_test.go diff --git a/gnovm/cmd/gno/transpile.go b/gnovm/cmd/gno/tool_transpile.go similarity index 100% rename from gnovm/cmd/gno/transpile.go rename to gnovm/cmd/gno/tool_transpile.go diff --git a/gnovm/cmd/gno/transpile_test.go b/gnovm/cmd/gno/tool_transpile_test.go similarity index 100% rename from gnovm/cmd/gno/transpile_test.go rename to gnovm/cmd/gno/tool_transpile_test.go diff --git a/gnovm/pkg/gnolang/fuzz_test.go b/gnovm/pkg/gnolang/fuzz_test.go index 977c7453b90..f19c35a3da1 100644 --- a/gnovm/pkg/gnolang/fuzz_test.go +++ b/gnovm/pkg/gnolang/fuzz_test.go @@ -50,8 +50,8 @@ func FuzzConvertUntypedBigdecToFloat(f *testing.F) { func FuzzParseFile(f *testing.F) { // 1. Add the corpra. - parseFileDir := filepath.Join("testdata", "corpra", "parsefile") - paths, err := filepath.Glob(filepath.Join(parseFileDir, "*.go")) + parseFileDir := filepath.Join("testdata", "corpora", "parsefile") + paths, err := filepath.Glob(filepath.Join(parseFileDir, "*.go_fuzz")) if err != nil { f.Fatal(err) } 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 diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/a.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/a.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/a.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/a.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/b.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/b.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/b.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/b.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3013.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3013.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3013.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3013.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine2.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine2.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine2.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine2.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine3.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine3.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine3.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine3.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine4.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine4.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine4.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine4.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine5.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine5.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine5.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine5.go_fuzz diff --git a/gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine6.go b/gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine6.go_fuzz similarity index 100% rename from gnovm/pkg/gnolang/testdata/corpra/parsefile/bug_3014_redefine6.go rename to gnovm/pkg/gnolang/testdata/corpora/parsefile/bug_3014_redefine6.go_fuzz diff --git a/gnovm/tests/challenges/unused0.gno b/gnovm/tests/challenges/unused0.gno index d5b0ff75c17..dc9aa950dec 100644 --- a/gnovm/tests/challenges/unused0.gno +++ b/gnovm/tests/challenges/unused0.gno @@ -1,7 +1,7 @@ package main // NOTE: instead of patching the vm code, this should be handled by an -// integration of `gno transpile --gobuild`, which uses `go build` under +// integration of `gno tool transpile --gobuild`, which uses `go build` under // the hood and already detects unused variables. func main() { var x int 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) + } + }) +}