diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
index 21950459ae8..cbfec5730fc 100644
--- a/.github/workflows/bot.yml
+++ b/.github/workflows/bot.yml
@@ -55,13 +55,15 @@ jobs:
working-directory: contribs/github-bot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: echo "pr-numbers=$(go run . matrix)" >> "$GITHUB_OUTPUT"
+ run: go run . matrix -matrix-key 'pr-numbers' -verbose
# This job processes each pull request in the matrix individually while ensuring
# that a same PR cannot be processed concurrently by mutliple runners
process-pr:
name: Process PR
needs: define-prs-matrix
+ # Just skip this job if PR numbers matrix is empty (prevent failed state)
+ if: ${{ needs.define-prs-matrix.outputs.pr-numbers != '[]' && needs.define-prs-matrix.outputs.pr-numbers != '' }}
runs-on: ubuntu-latest
strategy:
matrix:
diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml
index 8311d113047..7e7586b23d9 100644
--- a/.github/workflows/gnovm.yml
+++ b/.github/workflows/gnovm.yml
@@ -13,8 +13,6 @@ jobs:
uses: ./.github/workflows/main_template.yml
with:
modulepath: "gnovm"
- # in pull requests, append -short so that the CI runs quickly.
- tests-extra-args: ${{ github.event_name == 'pull_request' && '-short' || '' }}
secrets:
codecov-token: ${{ secrets.CODECOV_TOKEN }}
fmt:
diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml
index ccbae792c78..c7956b4caf4 100644
--- a/.github/workflows/test_template.yml
+++ b/.github/workflows/test_template.yml
@@ -41,11 +41,14 @@ jobs:
# Craft a filter flag based on the module path to avoid expanding coverage on unrelated tags.
export filter="-pkg=github.com/gnolang/gno/${{ inputs.modulepath }}/..."
+ # codecov only supports "boolean" coverage (whether a line is
+ # covered or not); so using -covermode=count or atomic would be
+ # pointless here.
# XXX: Simplify coverage of txtar - the current setup is a bit
# confusing and meticulous. There will be some improvements in Go
# 1.23 regarding coverage, so we can use this as a workaround until
# then.
- go test -covermode=atomic -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR
+ go test -covermode=set -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR
# Print results
(set +x; echo 'go coverage results:')
diff --git a/Makefile b/Makefile
index 2bfbe4e05e2..bd67020f236 100644
--- a/Makefile
+++ b/Makefile
@@ -54,7 +54,7 @@ install_gnokey: install.gnokey
install_gno: install.gno
.PHONY: test
-test: test.components test.docker
+test: test.components
.PHONY: test.components
test.components:
@@ -64,14 +64,6 @@ test.components:
$(MAKE) --no-print-directory -C examples test
$(MAKE) --no-print-directory -C misc test
-.PHONY: test.docker
-test.docker:
- @if hash docker 2>/dev/null; then \
- go test --tags=docker -count=1 -v ./misc/docker-integration; \
- else \
- echo "[-] 'docker' is missing, skipping ./misc/docker-integration tests."; \
- fi
-
.PHONY: fmt
fmt:
$(MAKE) --no-print-directory -C tm2 fmt imports
diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md
index 78c9c3c01b8..7932300cb9d 100644
--- a/contribs/github-bot/README.md
+++ b/contribs/github-bot/README.md
@@ -13,7 +13,7 @@ The bot operates by defining a set of rules that are evaluated against each pull
- **Automatic Checks**: These are rules that the bot evaluates automatically. If a pull request meets the conditions specified in the rule, then the corresponding requirements are executed. For example, ensuring that changes to specific directories are reviewed by specific team members.
- **Manual Checks**: These require human intervention. If a pull request meets the conditions specified in the rule, then a checkbox that can be checked only by specified teams is displayed on the bot comment. For example, determining if infrastructure needs to be updated based on changes to specific files.
-The bot configuration is defined in Go and is located in the file [config.go](./config.go).
+The bot configuration is defined in Go and is located in the file [config.go](./internal/config/config.go).
### GitHub Token
@@ -31,7 +31,7 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token
USAGE
github-bot check [flags]
-This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.
+This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.
A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.
FLAGS
diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl
deleted file mode 100644
index ebd07fdd4b9..00000000000
--- a/contribs/github-bot/comment.tmpl
+++ /dev/null
@@ -1,51 +0,0 @@
-# Merge Requirements
-
-The following requirements must be fulfilled before a pull request can be merged.
-Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member.
-
-These requirements are defined in this [configuration file](https://github.com/GnoCheckBot/demo/blob/main/config.go).
-
-## Automated Checks
-
-{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }}
-{{ end }}
-
-{{ if .AutoRules }}Details
-{{ range .AutoRules }}
-{{ .Description | stripLinks }}
-
-### If
-```
-{{ .ConditionDetails | stripLinks }}
-```
-### Then
-```
-{{ .RequirementDetails | stripLinks }}
-```
-
-{{ end }}
-
-{{ else }}*No automated checks match this pull request.*{{ end }}
-
-## Manual Checks
-
-{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }}
-{{ end }}
-
-{{ if .ManualRules }}Details
-{{ range .ManualRules }}
-{{ .Description | stripLinks }}
-
-### If
-```
-{{ .ConditionDetails }}
-```
-### Can be checked by
-{{range $item := .Teams }} - team {{ $item | stripLinks }}
-{{ else }}
-- Any user with comment edit permission
-{{end}}
-
-{{ end }}
-
-{{ else }}*No manual checks match this pull request.*{{ end }}
diff --git a/contribs/github-bot/check.go b/contribs/github-bot/internal/check/check.go
similarity index 78%
rename from contribs/github-bot/check.go
rename to contribs/github-bot/internal/check/check.go
index 8019246d27c..5ca2235e823 100644
--- a/contribs/github-bot/check.go
+++ b/contribs/github-bot/internal/check/check.go
@@ -1,4 +1,4 @@
-package main
+package check
import (
"context"
@@ -9,44 +9,30 @@ import (
"sync/atomic"
"github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/config"
"github.com/gnolang/gno/contribs/github-bot/internal/logger"
- p "github.com/gnolang/gno/contribs/github-bot/internal/params"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
- "github.com/gnolang/gno/tm2/pkg/commands"
"github.com/google/go-github/v64/github"
"github.com/sethvargo/go-githubactions"
"github.com/xlab/treeprint"
)
-func newCheckCmd() *commands.Command {
- params := &p.Params{}
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "check",
- ShortUsage: "github-bot check [flags]",
- ShortHelp: "checks requirements for a pull request to be merged",
- LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
- },
- params,
- func(_ context.Context, _ []string) error {
- params.ValidateFlags()
- return execCheck(params)
- },
- )
-}
-
-func execCheck(params *p.Params) error {
+func execCheck(flags *checkFlags) error {
// Create context with timeout if specified in the parameters.
ctx := context.Background()
- if params.Timeout > 0 {
+ if flags.Timeout > 0 {
var cancel context.CancelFunc
- ctx, cancel = context.WithTimeout(context.Background(), params.Timeout)
+ ctx, cancel = context.WithTimeout(context.Background(), flags.Timeout)
defer cancel()
}
// Init GitHub API client.
- gh, err := client.New(ctx, params)
+ gh, err := client.New(ctx, &client.Config{
+ Owner: flags.Owner,
+ Repo: flags.Repo,
+ Verbose: *flags.Verbose,
+ DryRun: flags.DryRun,
+ })
if err != nil {
return fmt.Errorf("comment update handling failed: %w", err)
}
@@ -69,7 +55,7 @@ func execCheck(params *p.Params) error {
var prs []*github.PullRequest
// If requested, retrieve all open pull requests.
- if params.PRAll {
+ if flags.PRAll {
prs, err = gh.ListPR(utils.PRStateOpen)
if err != nil {
return fmt.Errorf("unable to list all PR: %w", err)
@@ -77,11 +63,11 @@ func execCheck(params *p.Params) error {
} else {
// Otherwise, retrieve only specified pull request(s)
// (flag or GitHub Action context).
- prs = make([]*github.PullRequest, len(params.PRNums))
- for i, prNum := range params.PRNums {
- pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
+ prs = make([]*github.PullRequest, len(flags.PRNums))
+ for i, prNum := range flags.PRNums {
+ pr, err := gh.GetOpenedPullRequest(prNum)
if err != nil {
- return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
+ return fmt.Errorf("unable to process PR list: %w", err)
}
prs[i] = pr
}
@@ -101,7 +87,7 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
}
// Process all pull requests in parallel.
- autoRules, manualRules := config(gh)
+ autoRules, manualRules := config.Config(gh)
var wg sync.WaitGroup
// Used in dry-run mode to log cleanly from different goroutines.
@@ -122,15 +108,15 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
// Check if conditions of this rule are met by this PR.
- if !autoRule.ifC.IsMet(pr, ifDetails) {
+ if !autoRule.If.IsMet(pr, ifDetails) {
continue
}
- c := AutoContent{Description: autoRule.description, Satisfied: false}
+ c := AutoContent{Description: autoRule.Description, Satisfied: false}
thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.Fail))
// Check if requirements of this rule are satisfied by this PR.
- if autoRule.thenR.IsSatisfied(pr, thenDetails) {
+ if autoRule.Then.IsSatisfied(pr, thenDetails) {
thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success))
c.Satisfied = true
} else {
@@ -153,13 +139,13 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
// Check if conditions of this rule are met by this PR.
- if !manualRule.ifC.IsMet(pr, ifDetails) {
+ if !manualRule.If.IsMet(pr, ifDetails) {
continue
}
// Get check status from current comment, if any.
checkedBy := ""
- check, ok := checks[manualRule.description]
+ check, ok := checks[manualRule.Description]
if ok {
checkedBy = check.checkedBy
}
@@ -167,10 +153,10 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
commentContent.ManualRules = append(
commentContent.ManualRules,
ManualContent{
- Description: manualRule.description,
+ Description: manualRule.Description,
ConditionDetails: ifDetails.String(),
CheckedBy: checkedBy,
- Teams: manualRule.teams,
+ Teams: manualRule.Teams,
},
)
diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/check/cmd.go
similarity index 56%
rename from contribs/github-bot/internal/params/params.go
rename to contribs/github-bot/internal/check/cmd.go
index c11d1b62419..7ea6c02795b 100644
--- a/contribs/github-bot/internal/params/params.go
+++ b/contribs/github-bot/internal/check/cmd.go
@@ -1,118 +1,131 @@
-package params
+package check
import (
+ "context"
"flag"
"fmt"
"os"
"time"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/gnolang/gno/tm2/pkg/commands"
"github.com/sethvargo/go-githubactions"
)
-type Params struct {
+type checkFlags struct {
Owner string
Repo string
PRAll bool
- PRNums PRList
- Verbose bool
+ PRNums utils.PRList
+ Verbose *bool
DryRun bool
Timeout time.Duration
flagSet *flag.FlagSet
}
-func (p *Params) RegisterFlags(fs *flag.FlagSet) {
+func NewCheckCmd(verbose *bool) *commands.Command {
+ flags := &checkFlags{Verbose: verbose}
+
+ return commands.NewCommand(
+ commands.Metadata{
+ Name: "check",
+ ShortUsage: "github-bot check [flags]",
+ ShortHelp: "checks requirements for a pull request to be merged",
+ LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
+ },
+ flags,
+ func(_ context.Context, _ []string) error {
+ flags.validateFlags()
+ return execCheck(flags)
+ },
+ )
+}
+
+func (flags *checkFlags) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
- &p.Owner,
+ &flags.Owner,
"owner",
"",
"owner of the repo to process, if empty, will be retrieved from GitHub Actions context",
)
fs.StringVar(
- &p.Repo,
+ &flags.Repo,
"repo",
"",
"repo to process, if empty, will be retrieved from GitHub Actions context",
)
fs.BoolVar(
- &p.PRAll,
+ &flags.PRAll,
"pr-all",
false,
"process all opened pull requests",
)
fs.TextVar(
- &p.PRNums,
+ &flags.PRNums,
"pr-numbers",
- PRList(nil),
+ utils.PRList(nil),
"pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context",
)
fs.BoolVar(
- &p.Verbose,
- "verbose",
- false,
- "set logging level to debug",
- )
-
- fs.BoolVar(
- &p.DryRun,
+ &flags.DryRun,
"dry-run",
false,
"print if pull request requirements are satisfied without updating anything on GitHub",
)
fs.DurationVar(
- &p.Timeout,
+ &flags.Timeout,
"timeout",
0,
"timeout after which the bot execution is interrupted",
)
- p.flagSet = fs
+ flags.flagSet = fs
}
-func (p *Params) ValidateFlags() {
+func (flags *checkFlags) validateFlags() {
// Helper to display an error + usage message before exiting.
errorUsage := func(err string) {
- fmt.Fprintf(p.flagSet.Output(), "Error: %s\n\n", err)
- p.flagSet.Usage()
+ fmt.Fprintf(flags.flagSet.Output(), "Error: %s\n\n", err)
+ flags.flagSet.Usage()
os.Exit(1)
}
// Check if flags are coherent.
- if p.PRAll && len(p.PRNums) != 0 {
+ if flags.PRAll && len(flags.PRNums) != 0 {
errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags.")
}
// If one of these values is empty, it must be retrieved
// from GitHub Actions context.
- if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) {
+ if flags.Owner == "" || flags.Repo == "" || (len(flags.PRNums) == 0 && !flags.PRAll) {
actionCtx, err := githubactions.Context()
if err != nil {
errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err))
}
- if p.Owner == "" {
- if p.Owner, _ = actionCtx.Repo(); p.Owner == "" {
+ if flags.Owner == "" {
+ if flags.Owner, _ = actionCtx.Repo(); flags.Owner == "" {
errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag.")
}
}
- if p.Repo == "" {
- if _, p.Repo = actionCtx.Repo(); p.Repo == "" {
+ if flags.Repo == "" {
+ if _, flags.Repo = actionCtx.Repo(); flags.Repo == "" {
errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag.")
}
}
- if len(p.PRNums) == 0 && !p.PRAll {
+ if len(flags.PRNums) == 0 && !flags.PRAll {
prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
if err != nil {
errorUsage(fmt.Sprintf("Unable to retrieve pull request number from GitHub Actions context: %s\nYou may want to set it using -pr-numbers flag.", err.Error()))
}
- p.PRNums = PRList{prNum}
+ flags.PRNums = utils.PRList{prNum}
}
}
}
diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/internal/check/comment.go
similarity index 90%
rename from contribs/github-bot/comment.go
rename to contribs/github-bot/internal/check/comment.go
index f6605ea8554..434df8f9e76 100644
--- a/contribs/github-bot/comment.go
+++ b/contribs/github-bot/internal/check/comment.go
@@ -1,7 +1,8 @@
-package main
+package check
import (
"bytes"
+ _ "embed"
"errors"
"fmt"
"regexp"
@@ -9,12 +10,15 @@ import (
"text/template"
"github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/config"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
-
"github.com/google/go-github/v64/github"
"github.com/sethvargo/go-githubactions"
)
+//go:embed comment.tmpl
+var tmplString string // Embed template used for comment generation.
+
var errTriggeredByBot = errors.New("event triggered by bot")
// Compile regex only once.
@@ -95,6 +99,18 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
return nil
}
+ // Get PR number from GitHub Actions context.
+ prNumFloat, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64)
+ if !ok || prNumFloat <= 0 {
+ return errors.New("unable to get issue number on issue comment event")
+ }
+ prNum := int(prNumFloat)
+
+ // Ignore if this comment update is not related to an opened PR.
+ if _, err := gh.GetOpenedPullRequest(prNum); err != nil {
+ return nil // May come from an issue or a closed PR
+ }
+
// Return if comment was edited by bot (current authenticated user).
authUser, _, err := gh.Client.Users.Get(gh.Ctx, "")
if err != nil {
@@ -129,17 +145,11 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
return errors.New("unable to get changes body content on issue comment event")
}
- // Get PR number from GitHub Actions context.
- prNum, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64)
- if !ok || prNum <= 0 {
- return errors.New("unable to get issue number on issue comment event")
- }
-
// Check if change is only a checkbox being checked or unckecked.
if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") {
// If not, restore previous comment body.
if !gh.DryRun {
- gh.SetBotComment(previous, int(prNum))
+ gh.SetBotComment(previous, prNum)
}
return errors.New("bot comment edited outside of checkboxes")
}
@@ -157,12 +167,12 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// Get teams allowed to edit this box from config.
var teams []string
found := false
- _, manualRules := config(gh)
+ _, manualRules := config.Config(gh)
for _, manualRule := range manualRules {
- if manualRule.description == key {
+ if manualRule.Description == key {
found = true
- teams = manualRule.teams
+ teams = manualRule.Teams
}
}
@@ -175,9 +185,9 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// If teams specified in rule, check if actor is a member of one of them.
if len(teams) > 0 {
- if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed
+ if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed to check the boxes.
if !gh.DryRun {
- gh.SetBotComment(previous, int(prNum)) // Restore previous state
+ gh.SetBotComment(previous, prNum) // Then restore previous state.
}
return errors.New("checkbox edited by a user not allowed to")
}
@@ -199,7 +209,7 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// Update comment with username.
if edited != "" && !gh.DryRun {
- gh.SetBotComment(edited, int(prNum))
+ gh.SetBotComment(edited, prNum)
gh.Logger.Debugf("Comment manual checks updated successfully")
}
@@ -217,8 +227,7 @@ func generateComment(content CommentContent) (string, error) {
}
// Bind markdown stripping function to template generator.
- const tmplFile = "comment.tmpl"
- tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
+ tmpl, err := template.New("comment").Funcs(funcMap).Parse(tmplString)
if err != nil {
return "", fmt.Errorf("unable to init template: %w", err)
}
diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl
new file mode 100644
index 00000000000..4312019dd2e
--- /dev/null
+++ b/contribs/github-bot/internal/check/comment.tmpl
@@ -0,0 +1,54 @@
+I'm a bot that assists the Gno Core team in maintaining this repository. My role is to ensure that contributors understand and follow our guidelines, helping to streamline the development process.
+
+The following requirements must be fulfilled before a pull request can be merged.
+Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member.
+
+These requirements are defined in this [configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go).
+
+## Automated Checks
+
+{{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }}
+{{ end }}{{ else }}*No automated checks match this pull request.*{{ end }}
+
+## Manual Checks
+
+{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }}
+{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }}
+
+{{ if or .AutoRules .ManualRules }}Debug
+{{ if .AutoRules }}Automated Checks
+{{ range .AutoRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails | stripLinks }}
+```
+### Then
+```
+{{ .RequirementDetails | stripLinks }}
+```
+
+{{ end }}
+
+{{ end }}
+
+{{ if .ManualRules }}Manual Checks
+{{ range .ManualRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails }}
+```
+### Can be checked by
+{{range $item := .Teams }} - team {{ $item | stripLinks }}
+{{ else }}
+- Any user with comment edit permission
+{{end}}
+
+{{ end }}
+
+{{ end }}
+
+{{ end }}
diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/internal/check/comment_test.go
similarity index 86%
rename from contribs/github-bot/comment_test.go
rename to contribs/github-bot/internal/check/comment_test.go
index fd8790dd9e1..0334b76f95c 100644
--- a/contribs/github-bot/comment_test.go
+++ b/contribs/github-bot/internal/check/comment_test.go
@@ -1,4 +1,4 @@
-package main
+package check
import (
"context"
@@ -108,19 +108,34 @@ func TestCommentUpdateHandler(t *testing.T) {
}
gh := newGHClient()
- // Exit without error because EventName is empty
+ // Exit without error because EventName is empty.
assert.NoError(t, handleCommentUpdate(gh, actionCtx))
actionCtx.EventName = utils.EventIssueComment
- // Exit with error because Event.action is not set
+ // Exit with error because Event.action is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event["action"] = ""
- // Exit without error because Event.action is set but not 'deleted'
+ // Exit without error because Event.action is set but not 'deleted'.
assert.NoError(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event["action"] = "deleted"
- // Exit with error because mock not setup to return authUser
+ // Exit with error because Event.issue.number is not set.
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number")
+
+ // Exit without error can't get open pull request associated with PR num.
+ assert.NoError(t, handleCommentUpdate(gh, actionCtx))
+ mockOptions = append(mockOptions, mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/42",
+ Method: "GET",
+ },
+ github.PullRequest{Number: github.Int(42), State: github.String(utils.PRStateOpen)},
+ ))
+ gh = newGHClient()
+
+ // Exit with error because mock not setup to return authUser.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
mockOptions = append(mockOptions, mock.WithRequestMatchPages(
mock.EndpointPattern{
@@ -132,31 +147,27 @@ func TestCommentUpdateHandler(t *testing.T) {
gh = newGHClient()
actionCtx.Actor = bot
- // Exit with error because authUser and action actor is the same user
+ // Exit with error because authUser and action actor is the same user.
assert.ErrorIs(t, handleCommentUpdate(gh, actionCtx), errTriggeredByBot)
actionCtx.Actor = user
- // Exit with error because Event.comment.user.login is not set
+ // Exit with error because Event.comment.user.login is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, user, "comment", "user", "login")
- // Exit without error because comment author is not the bot
+ // Exit without error because comment author is not the bot.
assert.NoError(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, bot, "comment", "user", "login")
- // Exit with error because Event.comment.body is not set
+ // Exit with error because Event.comment.body is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "comment", "body")
- // Exit with error because Event.changes.body.from is not set
+ // Exit with error because Event.changes.body.from is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, "updated_body", "changes", "body", "from")
- // Exit with error because Event.issue.number is not set
- assert.Error(t, handleCommentUpdate(gh, actionCtx))
- actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number")
-
- // Exit with error because checkboxes are differents
+ // Exit with error because checkboxes are differents.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from")
diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go
index 474146ad3da..a5c875e0d22 100644
--- a/contribs/github-bot/internal/client/client.go
+++ b/contribs/github-bot/internal/client/client.go
@@ -7,8 +7,7 @@ import (
"os"
"github.com/gnolang/gno/contribs/github-bot/internal/logger"
- p "github.com/gnolang/gno/contribs/github-bot/internal/params"
-
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
"github.com/google/go-github/v64/github"
)
@@ -32,21 +31,28 @@ type GitHub struct {
Repo string
}
+type Config struct {
+ Owner string
+ Repo string
+ Verbose bool
+ DryRun bool
+}
+
// GetBotComment retrieves the bot's (current user) comment on provided PR number.
func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
- // List existing comments
+ // List existing comments.
const (
sort = "created"
direction = "desc"
)
- // Get current user (bot)
+ // Get current user (bot).
currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "")
if err != nil {
return nil, fmt.Errorf("unable to get current user: %w", err)
}
- // Pagination option
+ // Pagination option.
opts := &github.IssueListCommentsOptions{
Sort: github.String(sort),
Direction: github.String(direction),
@@ -67,7 +73,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err)
}
- // Get the comment created by current user
+ // Get the comment created by current user.
for _, comment := range comments {
if comment.GetUser().GetLogin() == currentUser.GetLogin() {
return comment, nil
@@ -86,7 +92,12 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
// SetBotComment creates a bot's comment on the provided PR number
// or updates it if it already exists.
func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) {
- // Create bot comment if it does not already exist
+ // Prevent updating anything in dry run mode.
+ if gh.DryRun {
+ return nil, errors.New("should not write bot comment in dry run mode")
+ }
+
+ // Create bot comment if it does not already exist.
comment, err := gh.GetBotComment(prNum)
if errors.Is(err, ErrBotCommentNotFound) {
newComment, _, err := gh.Client.Issues.CreateComment(
@@ -119,6 +130,17 @@ func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, e
return editComment, nil
}
+func (gh *GitHub) GetOpenedPullRequest(prNum int) (*github.PullRequest, error) {
+ pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
+ } else if pr.GetState() != utils.PRStateOpen {
+ return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState())
+ }
+
+ return pr, nil
+}
+
// ListTeamMembers lists the members of the specified team.
func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) {
var (
@@ -268,25 +290,25 @@ func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) {
}
// New initializes the API client, the logger, and creates an instance of GitHub.
-func New(ctx context.Context, params *p.Params) (*GitHub, error) {
+func New(ctx context.Context, cfg *Config) (*GitHub, error) {
gh := &GitHub{
Ctx: ctx,
- Owner: params.Owner,
- Repo: params.Repo,
- DryRun: params.DryRun,
+ Owner: cfg.Owner,
+ Repo: cfg.Repo,
+ DryRun: cfg.DryRun,
}
// Detect if the current process was launched by a GitHub Action and return
- // a logger suitable for terminal output or the GitHub Actions web interface
- gh.Logger = logger.NewLogger(params.Verbose)
+ // a logger suitable for terminal output or the GitHub Actions web interface.
+ gh.Logger = logger.NewLogger(cfg.Verbose)
- // Retrieve GitHub API token from env
+ // Retrieve GitHub API token from env.
token, set := os.LookupEnv("GITHUB_TOKEN")
if !set {
return nil, errors.New("GITHUB_TOKEN is not set in env")
}
- // Init GitHub API client using token
+ // Init GitHub API client using token.
gh.Client = github.NewClient(nil).WithAuthToken(token)
return gh, nil
diff --git a/contribs/github-bot/config.go b/contribs/github-bot/internal/config/config.go
similarity index 54%
rename from contribs/github-bot/config.go
rename to contribs/github-bot/internal/config/config.go
index 4a28565ef7f..ac1d185f759 100644
--- a/contribs/github-bot/config.go
+++ b/contribs/github-bot/internal/config/config.go
@@ -1,4 +1,4 @@
-package main
+package config
import (
"github.com/gnolang/gno/contribs/github-bot/internal/client"
@@ -9,37 +9,37 @@ import (
type Teams []string
// Automatic check that will be performed by the bot.
-type automaticCheck struct {
- description string
- ifC c.Condition // If the condition is met, the rule is displayed and the requirement is executed.
- thenR r.Requirement // If the requirement is satisfied, the check passes.
+type AutomaticCheck struct {
+ Description string
+ If c.Condition // If the condition is met, the rule is displayed and the requirement is executed.
+ Then r.Requirement // If the requirement is satisfied, the check passes.
}
// Manual check that will be performed by users.
-type manualCheck struct {
- description string
- ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment.
- teams Teams // Members of these teams can check the checkbox to make the check pass.
+type ManualCheck struct {
+ Description string
+ If c.Condition // If the condition is met, a checkbox will be displayed on bot comment.
+ Teams Teams // Members of these teams can check the checkbox to make the check pass.
}
// This function returns the configuration of the bot consisting of automatic and manual checks
// in which the GitHub client is injected.
-func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
- auto := []automaticCheck{
+func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
+ auto := []AutomaticCheck{
{
- description: "Maintainers must be able to edit this pull request",
- ifC: c.Always(),
- thenR: r.MaintainerCanModify(),
+ Description: "Maintainers must be able to edit this pull request",
+ If: c.Always(),
+ Then: r.MaintainerCanModify(),
},
{
- description: "The pull request head branch must be up-to-date with its base",
- ifC: c.Always(),
- thenR: r.UpToDateWith(gh, r.PR_BASE),
+ Description: "The pull request head branch must be up-to-date with its base",
+ If: c.Always(),
+ Then: r.UpToDateWith(gh, r.PR_BASE),
},
{
- description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff",
- ifC: c.FileChanged(gh, "^docs/"),
- thenR: r.Or(
+ Description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff",
+ If: c.FileChanged(gh, "^docs/"),
+ Then: r.Or(
r.And(
r.AuthorInTeam(gh, "devrels"),
r.ReviewByTeamMembers(gh, "tech-staff", 1),
@@ -52,15 +52,15 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
},
}
- manual := []manualCheck{
+ manual := []ManualCheck{
{
- description: "The pull request description provides enough details",
- ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")),
- teams: Teams{"core-contributors"},
+ Description: "The pull request description provides enough details",
+ If: c.Not(c.AuthorInTeam(gh, "core-contributors")),
+ Teams: Teams{"core-contributors"},
},
{
- description: "Determine if infra needs to be updated before merging",
- ifC: c.And(
+ Description: "Determine if infra needs to be updated before merging",
+ If: c.And(
c.BaseBranch("master"),
c.Or(
c.FileChanged(gh, `Dockerfile`),
@@ -70,17 +70,17 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`),
),
),
- teams: Teams{"devops"},
+ Teams: Teams{"devops"},
},
}
// Check for duplicates in manual rule descriptions (needs to be unique for the bot operations).
unique := make(map[string]struct{})
for _, rule := range manual {
- if _, exists := unique[rule.description]; exists {
- gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.description)
+ if _, exists := unique[rule.Description]; exists {
+ gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.Description)
}
- unique[rule.description] = struct{}{}
+ unique[rule.Description] = struct{}{}
}
return auto, manual
diff --git a/contribs/github-bot/internal/matrix/cmd.go b/contribs/github-bot/internal/matrix/cmd.go
new file mode 100644
index 00000000000..8bcc3a34424
--- /dev/null
+++ b/contribs/github-bot/internal/matrix/cmd.go
@@ -0,0 +1,53 @@
+package matrix
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/gnolang/gno/tm2/pkg/commands"
+)
+
+type matrixFlags struct {
+ verbose *bool
+ matrixKey string
+ flagSet *flag.FlagSet
+}
+
+func NewMatrixCmd(verbose *bool) *commands.Command {
+ flags := &matrixFlags{verbose: verbose}
+
+ return commands.NewCommand(
+ commands.Metadata{
+ Name: "matrix",
+ ShortUsage: "github-bot matrix [flags]",
+ ShortHelp: "parses GitHub Actions event and defines matrix accordingly",
+ LongHelp: "This tool retrieves the GitHub Actions context, parses the attached event, and defines the matrix with the pull request numbers to be processed accordingly",
+ },
+ flags,
+ func(_ context.Context, _ []string) error {
+ flags.validateFlags()
+ return execMatrix(flags)
+ },
+ )
+}
+
+func (flags *matrixFlags) RegisterFlags(fs *flag.FlagSet) {
+ fs.StringVar(
+ &flags.matrixKey,
+ "matrix-key",
+ "",
+ "key of the matrix to set in Github Actions output (required)",
+ )
+
+ flags.flagSet = fs
+}
+
+func (flags *matrixFlags) validateFlags() {
+ if flags.matrixKey == "" {
+ fmt.Fprintf(flags.flagSet.Output(), "Error: no matrix-key provided\n\n")
+ flags.flagSet.Usage()
+ os.Exit(1)
+ }
+}
diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go
new file mode 100644
index 00000000000..9c8f12e4214
--- /dev/null
+++ b/contribs/github-bot/internal/matrix/matrix.go
@@ -0,0 +1,139 @@
+package matrix
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/sethvargo/go-githubactions"
+)
+
+func execMatrix(flags *matrixFlags) error {
+ // Get GitHub Actions context to retrieve event.
+ actionCtx, err := githubactions.Context()
+ if err != nil {
+ return fmt.Errorf("unable to get GitHub Actions context: %w", err)
+ }
+
+ // If verbose is set, print the Github Actions event for debugging purpose.
+ if *flags.verbose {
+ jsonBytes, err := json.MarshalIndent(actionCtx.Event, "", " ")
+ if err != nil {
+ return fmt.Errorf("unable to marshal event to json: %w", err)
+ }
+ fmt.Println("Event:", string(jsonBytes))
+ }
+
+ // Init Github client using only GitHub Actions context.
+ owner, repo := actionCtx.Repo()
+ gh, err := client.New(context.Background(), &client.Config{
+ Owner: owner,
+ Repo: repo,
+ Verbose: *flags.verbose,
+ DryRun: true,
+ })
+ if err != nil {
+ return fmt.Errorf("unable to init GitHub client: %w", err)
+ }
+
+ // Retrieve PR list from GitHub Actions event.
+ prList, err := getPRListFromEvent(gh, actionCtx)
+ if err != nil {
+ return err
+ }
+
+ // Format PR list for GitHub Actions matrix definition.
+ bytes, err := prList.MarshalText()
+ if err != nil {
+ return fmt.Errorf("unable to marshal PR list: %w", err)
+ }
+ matrix := fmt.Sprintf("%s=[%s]", flags.matrixKey, string(bytes))
+
+ // If verbose is set, print the matrix for debugging purpose.
+ if *flags.verbose {
+ fmt.Printf("Matrix: %s\n", matrix)
+ }
+
+ // Get the path of the GitHub Actions environment file used for output.
+ output, ok := os.LookupEnv("GITHUB_OUTPUT")
+ if !ok {
+ return errors.New("unable to get GITHUB_OUTPUT var")
+ }
+
+ // Open GitHub Actions output file
+ file, err := os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ return fmt.Errorf("unable to open GitHub Actions output file: %w", err)
+ }
+ defer file.Close()
+
+ // Append matrix to GitHub Actions output file
+ if _, err := fmt.Fprintf(file, "%s\n", matrix); err != nil {
+ return fmt.Errorf("unable to write matrix in GitHub Actions output file: %w", err)
+ }
+
+ return nil
+}
+
+func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (utils.PRList, error) {
+ var prList utils.PRList
+
+ switch actionCtx.EventName {
+ // Event triggered from GitHub Actions user interface.
+ case utils.EventWorkflowDispatch:
+ // Get input entered by the user.
+ rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string)
+ if !ok {
+ return nil, errors.New("unable to get workflow dispatch input")
+ }
+ input := strings.TrimSpace(rawInput)
+
+ // If all PR are requested, list them from GitHub API.
+ if input == "all" {
+ prs, err := gh.ListPR(utils.PRStateOpen)
+ if err != nil {
+ return nil, fmt.Errorf("unable to list all PR: %w", err)
+ }
+
+ prList = make(utils.PRList, len(prs))
+ for i := range prs {
+ prList[i] = prs[i].GetNumber()
+ }
+ } else {
+ // If a PR list is provided, parse it.
+ if err := prList.UnmarshalText([]byte(input)); err != nil {
+ return nil, fmt.Errorf("invalid PR list provided as input: %w", err)
+ }
+ }
+
+ // Event triggered by an issue / PR comment being created / edited / deleted
+ // or any update on a PR.
+ case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget:
+ // For these events, retrieve the number of the associated PR from the context.
+ prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err)
+ }
+ prList = utils.PRList{prNum}
+
+ default:
+ return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName)
+ }
+
+ // Then only keep provided PR that are opened.
+ var openedPRList utils.PRList = nil
+ for _, prNum := range prList {
+ if _, err := gh.GetOpenedPullRequest(prNum); err != nil {
+ gh.Logger.Warningf("Can't get PR from event: %v", err)
+ } else {
+ openedPRList = append(openedPRList, prNum)
+ }
+ }
+
+ return openedPRList, nil
+}
diff --git a/contribs/github-bot/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go
similarity index 91%
rename from contribs/github-bot/matrix_test.go
rename to contribs/github-bot/internal/matrix/matrix_test.go
index bce4ec1bd8f..fe5b7452a49 100644
--- a/contribs/github-bot/matrix_test.go
+++ b/contribs/github-bot/internal/matrix/matrix_test.go
@@ -1,4 +1,4 @@
-package main
+package matrix
import (
"context"
@@ -9,7 +9,6 @@ import (
"github.com/gnolang/gno/contribs/github-bot/internal/client"
"github.com/gnolang/gno/contribs/github-bot/internal/logger"
- "github.com/gnolang/gno/contribs/github-bot/internal/params"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
"github.com/google/go-github/v64/github"
"github.com/migueleliasweb/go-github-mock/src/mock"
@@ -34,7 +33,7 @@ func TestProcessEvent(t *testing.T) {
name string
gaCtx *githubactions.GitHubContext
prs []*github.PullRequest
- expectedPRList params.PRList
+ expectedPRList utils.PRList
expectedError bool
}{
{
@@ -44,7 +43,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"issue": map[string]any{"number": 1.}},
},
prs,
- params.PRList{1},
+ utils.PRList{1},
false,
}, {
"valid pull_request event",
@@ -53,7 +52,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"pull_request": map[string]any{"number": 1.}},
},
prs,
- params.PRList{1},
+ utils.PRList{1},
false,
}, {
"valid pull_request_target event",
@@ -62,7 +61,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"pull_request": map[string]any{"number": 1.}},
},
prs,
- params.PRList{1},
+ utils.PRList{1},
false,
}, {
"invalid event (PR number not set)",
@@ -71,7 +70,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"issue": nil},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid event name",
@@ -80,7 +79,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"issue": map[string]any{"number": 1.}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"valid workflow_dispatch all",
@@ -89,7 +88,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}},
},
openPRs,
- params.PRList{1, 2, 3},
+ utils.PRList{1, 2, 3},
false,
}, {
"valid workflow_dispatch all (no prs)",
@@ -98,7 +97,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}},
},
nil,
- params.PRList{},
+ utils.PRList(nil),
false,
}, {
"valid workflow_dispatch list",
@@ -107,7 +106,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}},
},
prs,
- params.PRList{1, 2, 3},
+ utils.PRList{1, 2, 3},
false,
}, {
"valid workflow_dispatch list with spaces",
@@ -116,7 +115,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}},
},
prs,
- params.PRList{1, 2, 3},
+ utils.PRList{1, 2, 3},
false,
}, {
"invalid workflow_dispatch list (1 closed)",
@@ -125,8 +124,8 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}},
},
prs,
- params.PRList(nil),
- true,
+ utils.PRList{1, 2, 3},
+ false,
}, {
"invalid workflow_dispatch list (1 doesn't exist)",
&githubactions.GitHubContext{
@@ -134,8 +133,8 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}},
},
prs,
- params.PRList(nil),
- true,
+ utils.PRList(nil),
+ false,
}, {
"invalid workflow_dispatch list (all closed)",
&githubactions.GitHubContext{
@@ -143,8 +142,8 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}},
},
prs,
- params.PRList(nil),
- true,
+ utils.PRList(nil),
+ false,
}, {
"invalid workflow_dispatch list (empty)",
&githubactions.GitHubContext{
@@ -152,7 +151,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid workflow_dispatch list (unset)",
@@ -161,7 +160,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": ""},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid workflow_dispatch list (not a number list)",
@@ -170,7 +169,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid workflow_dispatch list (number list with invalid elem)",
@@ -179,7 +178,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
},
} {
@@ -214,7 +213,7 @@ func TestProcessEvent(t *testing.T) {
prNumStr := parts[len(parts)-1]
prNum, err = strconv.Atoi(prNumStr)
if err != nil {
- panic(err) // Should never happen
+ panic(err) // Should never happen.
}
}
diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go
index d72e8ad2a19..aa86fb0054d 100644
--- a/contribs/github-bot/internal/requirements/assignee_test.go
+++ b/contribs/github-bot/internal/requirements/assignee_test.go
@@ -45,7 +45,7 @@ func TestAssignee(t *testing.T) {
mock.WithRequestMatchHandler(
mock.EndpointPattern{
Pattern: "/repos/issues/0/assignees",
- Method: "GET", // It looks like this mock package doesn't support mocking POST requests
+ Method: "GET", // It looks like this mock package doesn't support mocking POST requests.
},
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
requested = true
diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go
index b686a093015..6481285ae82 100644
--- a/contribs/github-bot/internal/requirements/branch.go
+++ b/contribs/github-bot/internal/requirements/branch.go
@@ -29,7 +29,7 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre
}
head := pr.GetHead().GetRef()
- // If pull request is open from a fork, prepend head ref with fork owner login
+ // If pull request is open from a fork, prepend head ref with fork owner login.
if pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName() {
head = fmt.Sprintf("%s:%s", pr.GetHead().GetRepo().GetOwner().GetLogin(), pr.GetHead().GetRef())
}
diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go
index 7e991b55756..631bff9e64b 100644
--- a/contribs/github-bot/internal/requirements/label_test.go
+++ b/contribs/github-bot/internal/requirements/label_test.go
@@ -45,7 +45,7 @@ func TestLabel(t *testing.T) {
mock.WithRequestMatchHandler(
mock.EndpointPattern{
Pattern: "/repos/issues/0/labels",
- Method: "GET", // It looks like this mock package doesn't support mocking POST requests
+ Method: "GET", // It looks like this mock package doesn't support mocking POST requests.
},
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
requested = true
diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go
index 91b8ac7e6b4..3e08a8e1548 100644
--- a/contribs/github-bot/internal/utils/actions.go
+++ b/contribs/github-bot/internal/utils/actions.go
@@ -23,7 +23,7 @@ func IndexMap(m map[string]any, keys ...string) any {
return nil
}
-// Retrieve PR number from GitHub Actions context
+// Retrieve PR number from GitHub Actions context.
func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) {
firstKey := ""
diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go
index 564b7d3fb38..26d7d54d477 100644
--- a/contribs/github-bot/internal/utils/github_const.go
+++ b/contribs/github-bot/internal/utils/github_const.go
@@ -1,14 +1,14 @@
package utils
-// GitHub const
+// GitHub API const.
const (
- // GitHub Actions Event Names
+ // GitHub Actions Event Names.
EventIssueComment = "issue_comment"
EventPullRequest = "pull_request"
EventPullRequestTarget = "pull_request_target"
EventWorkflowDispatch = "workflow_dispatch"
- // Pull Request States
+ // Pull Request States.
PRStateOpen = "open"
PRStateClosed = "closed"
)
diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/utils/prlist.go
similarity index 91%
rename from contribs/github-bot/internal/params/prlist.go
rename to contribs/github-bot/internal/utils/prlist.go
index ace7bcbe3b6..2893bf802b5 100644
--- a/contribs/github-bot/internal/params/prlist.go
+++ b/contribs/github-bot/internal/utils/prlist.go
@@ -1,4 +1,4 @@
-package params
+package utils
import (
"encoding"
@@ -7,6 +7,7 @@ import (
"strings"
)
+// Type used to (un)marshal input/output for check and matrix subcommands.
type PRList []int
// PRList is both a TextMarshaler and a TextUnmarshaler.
diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go
index 9895f44dc70..e11fe6ffd78 100644
--- a/contribs/github-bot/main.go
+++ b/contribs/github-bot/main.go
@@ -2,25 +2,43 @@ package main
import (
"context"
+ "flag"
"os"
+ "github.com/gnolang/gno/contribs/github-bot/internal/check"
+ "github.com/gnolang/gno/contribs/github-bot/internal/matrix"
"github.com/gnolang/gno/tm2/pkg/commands"
)
+type rootFlags struct {
+ verbose bool
+}
+
func main() {
+ flags := &rootFlags{}
+
cmd := commands.NewCommand(
commands.Metadata{
ShortUsage: "github-bot [flags]",
LongHelp: "Bot that allows for advanced management of GitHub pull requests.",
},
- commands.NewEmptyConfig(),
+ flags,
commands.HelpExec,
)
cmd.AddSubCommands(
- newCheckCmd(),
- newMatrixCmd(),
+ check.NewCheckCmd(&flags.verbose),
+ matrix.NewMatrixCmd(&flags.verbose),
)
cmd.Execute(context.Background(), os.Args[1:])
}
+
+func (flags *rootFlags) RegisterFlags(fs *flag.FlagSet) {
+ fs.BoolVar(
+ &flags.verbose,
+ "verbose",
+ false,
+ "set logging level to debug",
+ )
+}
diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go
deleted file mode 100644
index 56d6667589a..00000000000
--- a/contribs/github-bot/matrix.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "strings"
-
- "github.com/gnolang/gno/contribs/github-bot/internal/client"
- "github.com/gnolang/gno/contribs/github-bot/internal/params"
- "github.com/gnolang/gno/contribs/github-bot/internal/utils"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/sethvargo/go-githubactions"
-)
-
-func newMatrixCmd() *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "matrix",
- ShortUsage: "github-bot matrix",
- ShortHelp: "parses GitHub Actions event and defines matrix accordingly",
- LongHelp: "This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
- },
- commands.NewEmptyConfig(),
- func(_ context.Context, _ []string) error {
- return execMatrix()
- },
- )
-}
-
-func execMatrix() error {
- // Get GitHub Actions context to retrieve event.
- actionCtx, err := githubactions.Context()
- if err != nil {
- return fmt.Errorf("unable to get GitHub Actions context: %w", err)
- }
-
- // Init Github client using only GitHub Actions context
- owner, repo := actionCtx.Repo()
- gh, err := client.New(context.Background(), ¶ms.Params{Owner: owner, Repo: repo})
- if err != nil {
- return fmt.Errorf("unable to init GitHub client: %w", err)
- }
-
- // Retrieve PR list from GitHub Actions event
- prList, err := getPRListFromEvent(gh, actionCtx)
- if err != nil {
- return err
- }
-
- // Print PR list for GitHub Actions matrix definition
- bytes, err := prList.MarshalText()
- if err != nil {
- return fmt.Errorf("unable to marshal PR list: %w", err)
- }
- fmt.Printf("[%s]", string(bytes))
-
- return nil
-}
-
-func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (params.PRList, error) {
- var prList params.PRList
-
- switch actionCtx.EventName {
- // Event triggered from GitHub Actions user interface
- case utils.EventWorkflowDispatch:
- // Get input entered by the user
- rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string)
- if !ok {
- return nil, errors.New("unable to get workflow dispatch input")
- }
- input := strings.TrimSpace(rawInput)
-
- // If all PR are requested, list them from GitHub API
- if input == "all" {
- prs, err := gh.ListPR(utils.PRStateOpen)
- if err != nil {
- return nil, fmt.Errorf("unable to list all PR: %w", err)
- }
-
- prList = make(params.PRList, len(prs))
- for i := range prs {
- prList[i] = prs[i].GetNumber()
- }
- } else {
- // If a PR list is provided, parse it
- if err := prList.UnmarshalText([]byte(input)); err != nil {
- return nil, fmt.Errorf("invalid PR list provided as input: %w", err)
- }
-
- // Then check if all provided PR are opened
- for _, prNum := range prList {
- pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
- if err != nil {
- return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
- } else if pr.GetState() != utils.PRStateOpen {
- return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState())
- }
- }
- }
-
- // Event triggered by an issue / PR comment being created / edited / deleted
- // or any update on a PR
- case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget:
- // For these events, retrieve the number of the associated PR from the context
- prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
- if err != nil {
- return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err)
- }
- prList = params.PRList{prNum}
-
- default:
- return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName)
- }
-
- return prList, nil
-}
diff --git a/examples/gno.land/p/demo/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno
index f8ea433192f..e3a2ba72c5b 100644
--- a/examples/gno.land/p/demo/dao/dao.gno
+++ b/examples/gno.land/p/demo/dao/dao.gno
@@ -15,6 +15,7 @@ const (
// that contains the necessary information to
// log and generate a valid proposal
type ProposalRequest struct {
+ Title string // the title associated with the proposal
Description string // the description associated with the proposal
Executor Executor // the proposal executor
}
diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno
index 5cad679d006..66abcb248c5 100644
--- a/examples/gno.land/p/demo/dao/proposals.gno
+++ b/examples/gno.land/p/demo/dao/proposals.gno
@@ -16,7 +16,7 @@ var (
Accepted ProposalStatus = "accepted" // proposal gathered quorum
NotAccepted ProposalStatus = "not accepted" // proposal failed to gather quorum
ExecutionSuccessful ProposalStatus = "execution successful" // proposal is executed successfully
- ExecutionFailed ProposalStatus = "execution failed" // proposal is failed during execution
+ ExecutionFailed ProposalStatus = "execution failed" // proposal has failed during execution
)
func (s ProposalStatus) String() string {
@@ -42,6 +42,9 @@ type Proposal interface {
// Author returns the author of the proposal
Author() std.Address
+ // Title returns the title of the proposal
+ Title() string
+
// Description returns the description of the proposal
Description() string
diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno
index 7a20237ec3f..837f64a41d6 100644
--- a/examples/gno.land/p/demo/simpledao/dao.gno
+++ b/examples/gno.land/p/demo/simpledao/dao.gno
@@ -3,6 +3,7 @@ package simpledao
import (
"errors"
"std"
+ "strings"
"gno.land/p/demo/avl"
"gno.land/p/demo/dao"
@@ -12,6 +13,7 @@ import (
var (
ErrInvalidExecutor = errors.New("invalid executor provided")
+ ErrInvalidTitle = errors.New("invalid proposal title provided")
ErrInsufficientProposalFunds = errors.New("insufficient funds for proposal")
ErrInsufficientExecuteFunds = errors.New("insufficient funds for executing proposal")
ErrProposalExecuted = errors.New("proposal already executed")
@@ -47,6 +49,11 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {
return 0, ErrInvalidExecutor
}
+ // Make sure the title is set
+ if strings.TrimSpace(request.Title) == "" {
+ return 0, ErrInvalidTitle
+ }
+
var (
caller = getDAOCaller()
sentCoins = std.GetOrigSend() // Get the sent coins, if any
@@ -61,6 +68,7 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {
// Create the wrapped proposal
prop := &proposal{
author: caller,
+ title: request.Title,
description: request.Description,
executor: request.Executor,
status: dao.Active,
diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno
index fb32895e72f..46251e24dad 100644
--- a/examples/gno.land/p/demo/simpledao/dao_test.gno
+++ b/examples/gno.land/p/demo/simpledao/dao_test.gno
@@ -45,6 +45,50 @@ func TestSimpleDAO_Propose(t *testing.T) {
)
})
+ t.Run("invalid title", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ called = false
+ cb = func() error {
+ called = true
+
+ return nil
+ }
+ ex = &mockExecutor{
+ executeFn: cb,
+ }
+
+ sentCoins = std.NewCoins(
+ std.NewCoin(
+ "ugnot",
+ minProposalFeeValue,
+ ),
+ )
+
+ ms = &mockMemberStore{
+ isMemberFn: func(_ std.Address) bool {
+ return false
+ },
+ }
+ s = New(ms)
+ )
+
+ std.TestSetOrigSend(sentCoins, std.Coins{})
+
+ _, err := s.Propose(dao.ProposalRequest{
+ Executor: ex,
+ Title: "", // Set invalid title
+ })
+ uassert.ErrorIs(
+ t,
+ err,
+ ErrInvalidTitle,
+ )
+
+ uassert.False(t, called)
+ })
+
t.Run("caller cannot cover fee", func(t *testing.T) {
t.Parallel()
@@ -58,6 +102,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
ex = &mockExecutor{
executeFn: cb,
}
+ title = "Proposal title"
sentCoins = std.NewCoins(
std.NewCoin(
@@ -80,6 +125,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
_, err := s.Propose(dao.ProposalRequest{
Executor: ex,
+ Title: title,
})
uassert.ErrorIs(
t,
@@ -105,6 +151,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
executeFn: cb,
}
description = "Proposal description"
+ title = "Proposal title"
proposer = testutils.TestAddress("proposer")
sentCoins = std.NewCoins(
@@ -129,6 +176,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
// Make sure the proposal was added
id, err := s.Propose(dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: ex,
})
@@ -141,6 +189,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
uassert.Equal(t, proposer.String(), prop.Author().String())
uassert.Equal(t, description, prop.Description())
+ uassert.Equal(t, title, prop.Title())
uassert.Equal(t, dao.Active.String(), prop.Status().String())
stats := prop.Stats()
diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno
index 06741d397cb..91f2a883047 100644
--- a/examples/gno.land/p/demo/simpledao/propstore.gno
+++ b/examples/gno.land/p/demo/simpledao/propstore.gno
@@ -3,6 +3,7 @@ package simpledao
import (
"errors"
"std"
+ "strings"
"gno.land/p/demo/dao"
"gno.land/p/demo/seqid"
@@ -18,6 +19,7 @@ const maxRequestProposals = 10
// proposal is the internal simpledao proposal implementation
type proposal struct {
author std.Address // initiator of the proposal
+ title string // title of the proposal
description string // description of the proposal
executor dao.Executor // executor for the proposal
@@ -31,6 +33,10 @@ func (p *proposal) Author() std.Address {
return p.author
}
+func (p *proposal) Title() string {
+ return p.title
+}
+
func (p *proposal) Description() string {
return p.description
}
@@ -63,15 +69,20 @@ func (p *proposal) Render() string {
// Fetch the voting stats
stats := p.Stats()
- output := ""
- output += ufmt.Sprintf("Author: %s", p.Author().String())
- output += "\n\n"
- output += p.Description()
- output += "\n\n"
- output += ufmt.Sprintf("Status: %s", p.Status().String())
- output += "\n\n"
- output += ufmt.Sprintf(
- "Voting stats: YES %d (%d%%), NO %d (%d%%), ABSTAIN %d (%d%%), MISSING VOTE %d (%d%%)",
+ var out string
+
+ out += "## Description\n\n"
+ if strings.TrimSpace(p.description) != "" {
+ out += ufmt.Sprintf("%s\n\n", p.description)
+ } else {
+ out += "No description provided.\n\n"
+ }
+
+ out += "## Proposal information\n\n"
+ out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(p.Status().String()))
+
+ out += ufmt.Sprintf(
+ "**Voting stats:**\n- YES %d (%d%%)\n- NO %d (%d%%)\n- ABSTAIN %d (%d%%)\n- MISSING VOTES %d (%d%%)\n",
stats.YayVotes,
stats.YayPercent(),
stats.NayVotes,
@@ -81,10 +92,13 @@ func (p *proposal) Render() string {
stats.MissingVotes(),
stats.MissingVotesPercent(),
)
- output += "\n\n"
- output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3)
- return output
+ out += "\n\n"
+ thresholdOut := strings.ToUpper(ufmt.Sprintf("%t", stats.YayVotes > (2*stats.TotalVotingPower)/3))
+
+ out += ufmt.Sprintf("**Threshold met: %s**\n\n", thresholdOut)
+
+ return out
}
// addProposal adds a new simpledao proposal to the store
diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno
index d99a161bcdf..9263d8d440b 100644
--- a/examples/gno.land/r/gov/dao/v2/dao.gno
+++ b/examples/gno.land/r/gov/dao/v2/dao.gno
@@ -2,12 +2,10 @@ package govdao
import (
"std"
- "strconv"
"gno.land/p/demo/dao"
"gno.land/p/demo/membstore"
"gno.land/p/demo/simpledao"
- "gno.land/p/demo/ufmt"
)
var (
@@ -65,55 +63,3 @@ func GetPropStore() dao.PropStore {
func GetMembStore() membstore.MemberStore {
return members
}
-
-func Render(path string) string {
- if path == "" {
- numProposals := d.Size()
-
- if numProposals == 0 {
- return "No proposals found :(" // corner case
- }
-
- output := ""
-
- offset := uint64(0)
- if numProposals >= 10 {
- offset = uint64(numProposals) - 10
- }
-
- // Fetch the last 10 proposals
- for idx, prop := range d.Proposals(offset, uint64(10)) {
- output += ufmt.Sprintf(
- "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n",
- idx,
- "/r/gov/dao/v2",
- idx,
- prop.Status().String(),
- prop.Author().String(),
- )
- }
-
- return output
- }
-
- // Display the detailed proposal
- idx, err := strconv.Atoi(path)
- if err != nil {
- return "404: Invalid proposal ID"
- }
-
- // Fetch the proposal
- prop, err := d.ProposalByID(uint64(idx))
- if err != nil {
- return ufmt.Sprintf("unable to fetch proposal, %s", err.Error())
- }
-
- // Render the proposal
- output := ""
- output += ufmt.Sprintf("# Prop #%d", idx)
- output += "\n\n"
- output += prop.Render()
- output += "\n\n"
-
- return output
-}
diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod
index bc379bf18df..4da6e0a2484 100644
--- a/examples/gno.land/r/gov/dao/v2/gno.mod
+++ b/examples/gno.land/r/gov/dao/v2/gno.mod
@@ -7,4 +7,6 @@ require (
gno.land/p/demo/simpledao v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/gov/executor v0.0.0-latest
+ gno.land/p/moul/txlink v0.0.0-latest
+ gno.land/r/demo/users v0.0.0-latest
)
diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
index 7b25eeb1db3..7d8975e1fe8 100644
--- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
@@ -42,9 +42,11 @@ func init() {
executor := validators.NewPropExecutor(changesFn)
// Create a proposal
+ title := "Valset change"
description := "manual valset changes proposal example"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: executor,
}
@@ -73,52 +75,98 @@ func main() {
// Output:
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Valset change](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
// No valset changes to apply.
// --
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
index 4eb993b80dc..84a64bc4ee2 100644
--- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
@@ -19,9 +19,11 @@ func init() {
)
// Create a proposal
+ title := "govdao blog post title"
description := "post a new blogpost about govdao"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: ex,
}
@@ -50,35 +52,68 @@ func main() {
// Output:
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - govdao blog post title](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
@@ -87,17 +122,30 @@ func main() {
// No posts.
// --
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
index 546213431e4..068f520e7e2 100644
--- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
@@ -28,9 +28,11 @@ func init() {
}
// Create a proposal
+ title := "new govdao member addition"
description := "add new members to the govdao"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: govdao.NewMemberPropExecutor(memberFn),
}
@@ -65,57 +67,117 @@ func main() {
// --
// 1
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
+//
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
+//
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Threshold met: FALSE**
//
-// Threshold met: false
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**accepted**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACCEPTED**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (25%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 30 (75%)
+//
//
-// Voting stats: YES 10 (25%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 30 (75%)
+// **Threshold met: FALSE**
//
-// Threshold met: false
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**execution successful**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
// 4
diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
index 8eff79ffb5a..13ca572c512 100644
--- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
@@ -9,8 +9,10 @@ import (
func init() {
mExec := params.NewStringPropExecutor("prop1.string", "value1")
+ title := "Setting prop1.string param"
comment := "setting prop1.string param"
prop := dao.ProposalRequest{
+ Title: title,
Description: comment,
Executor: mExec,
}
@@ -36,124 +38,95 @@ func main() {
// Output:
// new prop 0
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Setting prop1.string param](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: execution successful
+// ## Proposal information
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Status: EXECUTION SUCCESSFUL**
//
-// Threshold met: true
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
-
-// Events:
-// [
-// {
-// "type": "ProposalAdded",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "proposal-author",
-// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitProposalAdded"
-// },
-// {
-// "type": "VoteAdded",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "author",
-// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
-// },
-// {
-// "key": "option",
-// "value": "YES"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitVoteAdded"
-// },
-// {
-// "type": "ProposalAccepted",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitProposalAccepted"
-// },
-// {
-// "type": "set",
-// "attrs": [
-// {
-// "key": "k",
-// "value": "prop1.string"
-// }
-// ],
-// "pkg_path": "gno.land/r/sys/params",
-// "func": ""
-// },
-// {
-// "type": "ProposalExecuted",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "exec-status",
-// "value": "accepted"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "ExecuteProposal"
-// }
-// ]
diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno
new file mode 100644
index 00000000000..4cca397e851
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/render.gno
@@ -0,0 +1,123 @@
+package govdao
+
+import (
+ "strconv"
+ "strings"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/txlink"
+ "gno.land/r/demo/users"
+)
+
+func Render(path string) string {
+ var out string
+
+ if path == "" {
+ out += "# GovDAO Proposals\n\n"
+ numProposals := d.Size()
+
+ if numProposals == 0 {
+ out += "No proposals found :(" // corner case
+ return out
+ }
+
+ offset := uint64(0)
+ if numProposals >= 10 {
+ offset = uint64(numProposals) - 10
+ }
+
+ // Fetch the last 10 proposals
+ proposals := d.Proposals(offset, uint64(10))
+ for i := len(proposals) - 1; i >= 0; i-- {
+ prop := proposals[i]
+
+ title := prop.Title()
+ if len(title) > 40 {
+ title = title[:40] + "..."
+ }
+
+ propID := offset + uint64(i)
+ out += ufmt.Sprintf("## [Prop #%d - %s](/r/gov/dao/v2:%d)\n\n", propID, title, propID)
+ out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(prop.Status().String()))
+
+ user := users.GetUserByAddress(prop.Author())
+ authorDisplayText := prop.Author().String()
+ if user != nil {
+ authorDisplayText = ufmt.Sprintf("[%s](/r/demo/users:%s)", user.Name, user.Name)
+ }
+
+ out += ufmt.Sprintf("**Author: %s**\n\n", authorDisplayText)
+
+ if i != 0 {
+ out += "---\n\n"
+ }
+ }
+
+ return out
+ }
+
+ // Display the detailed proposal
+ idx, err := strconv.Atoi(path)
+ if err != nil {
+ return "404: Invalid proposal ID"
+ }
+
+ // Fetch the proposal
+ prop, err := d.ProposalByID(uint64(idx))
+ if err != nil {
+ return ufmt.Sprintf("unable to fetch proposal, %s", err.Error())
+ }
+
+ // Render the proposal page
+ out += renderPropPage(prop, idx)
+
+ return out
+}
+
+func renderPropPage(prop dao.Proposal, idx int) string {
+ var out string
+
+ out += ufmt.Sprintf("# Proposal #%d - %s\n\n", idx, prop.Title())
+ out += prop.Render()
+ out += renderAuthor(prop)
+ out += renderActionBar(prop, idx)
+ out += "\n\n"
+
+ return out
+}
+
+func renderAuthor(p dao.Proposal) string {
+ var out string
+
+ authorUsername := ""
+ user := users.GetUserByAddress(p.Author())
+ if user != nil {
+ authorUsername = user.Name
+ }
+
+ if authorUsername != "" {
+ out += ufmt.Sprintf("**Author: [%s](/r/demo/users:%s)**\n\n", authorUsername, authorUsername)
+ } else {
+ out += ufmt.Sprintf("**Author: %s**\n\n", p.Author().String())
+ }
+
+ return out
+}
+
+func renderActionBar(p dao.Proposal, idx int) string {
+ var out string
+
+ out += "### Actions\n\n"
+ if p.Status() == dao.Active {
+ out += ufmt.Sprintf("#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]",
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"),
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"),
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"),
+ )
+ } else {
+ out += "The voting period for this proposal is over."
+ }
+
+ return out
+}
diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go
index f1bc87d21d8..09be600b198 100644
--- a/gnovm/pkg/gnolang/files_test.go
+++ b/gnovm/pkg/gnolang/files_test.go
@@ -33,19 +33,26 @@ func (nopReader) Read(p []byte) (int, error) { return 0, io.EOF }
// fix a specific test:
// go test -run TestFiles/'^bin1.gno' -short -v -update-golden-tests .
func TestFiles(t *testing.T) {
+ t.Parallel()
+
rootDir, err := filepath.Abs("../../../")
require.NoError(t, err)
- opts := &test.TestOptions{
- RootDir: rootDir,
- Output: io.Discard,
- Error: io.Discard,
- Sync: *withSync,
+ newOpts := func() *test.TestOptions {
+ o := &test.TestOptions{
+ RootDir: rootDir,
+ Output: io.Discard,
+ Error: io.Discard,
+ Sync: *withSync,
+ }
+ o.BaseStore, o.TestStore = test.Store(
+ rootDir, true,
+ nopReader{}, o.WriterForStore(), io.Discard,
+ )
+ return o
}
- opts.BaseStore, opts.TestStore = test.Store(
- rootDir, true,
- nopReader{}, opts.WriterForStore(), io.Discard,
- )
+ // sharedOpts is used for all "short" tests.
+ sharedOpts := newOpts()
dir := "../../tests/"
fsys := os.DirFS(dir)
@@ -59,7 +66,8 @@ func TestFiles(t *testing.T) {
return nil
}
subTestName := path[len("files/"):]
- if strings.HasSuffix(path, "_long.gno") && testing.Short() {
+ isLong := strings.HasSuffix(path, "_long.gno")
+ if isLong && testing.Short() {
t.Run(subTestName, func(t *testing.T) {
t.Skip("skipping in -short")
})
@@ -73,6 +81,12 @@ func TestFiles(t *testing.T) {
var criticalError error
t.Run(subTestName, func(t *testing.T) {
+ opts := sharedOpts
+ if isLong {
+ // Long tests are run in parallel, and with their own store.
+ t.Parallel()
+ opts = newOpts()
+ }
changed, err := opts.RunFiletest(path, content)
if err != nil {
t.Fatal(err.Error())
@@ -94,16 +108,24 @@ func TestFiles(t *testing.T) {
// TestStdlibs tests all the standard library packages.
func TestStdlibs(t *testing.T) {
+ t.Parallel()
+
rootDir, err := filepath.Abs("../../../")
require.NoError(t, err)
- var capture bytes.Buffer
- out := io.Writer(&capture)
- if testing.Verbose() {
- out = os.Stdout
+ newOpts := func() (capture *bytes.Buffer, opts *test.TestOptions) {
+ var out io.Writer
+ if testing.Verbose() {
+ out = os.Stdout
+ } else {
+ capture = new(bytes.Buffer)
+ out = capture
+ }
+ opts = test.NewTestOptions(rootDir, nopReader{}, out, out)
+ opts.Verbose = true
+ return
}
- opts := test.NewTestOptions(rootDir, nopReader{}, out, out)
- opts.Verbose = true
+ sharedCapture, sharedOpts := newOpts()
dir := "../../stdlibs/"
fsys := os.DirFS(dir)
@@ -118,12 +140,31 @@ func TestStdlibs(t *testing.T) {
fp := filepath.Join(dir, path)
memPkg := gnolang.ReadMemPackage(fp, path)
t.Run(strings.ReplaceAll(memPkg.Path, "/", "-"), func(t *testing.T) {
- if testing.Short() {
- switch memPkg.Path {
- case "bytes", "strconv", "regexp/syntax":
+ capture, opts := sharedCapture, sharedOpts
+ switch memPkg.Path {
+ // Excluded in short
+ case
+ "bufio",
+ "bytes",
+ "strconv":
+ if testing.Short() {
t.Skip("Skipped because of -short, and this stdlib is very long currently.")
}
+ fallthrough
+ // Run using separate store, as it's faster
+ case
+ "math/rand",
+ "regexp",
+ "regexp/syntax",
+ "sort":
+ t.Parallel()
+ capture, opts = newOpts()
+ }
+
+ if capture != nil {
+ capture.Reset()
}
+
err := test.Test(memPkg, "", opts)
if !testing.Verbose() {
t.Log(capture.String())
diff --git a/gnovm/pkg/gnolang/softfloat.go b/gnovm/pkg/gnolang/softfloat.go
index 61991b0a005..34c37596893 100644
--- a/gnovm/pkg/gnolang/softfloat.go
+++ b/gnovm/pkg/gnolang/softfloat.go
@@ -29,7 +29,7 @@ func Trunc(x SoftFloat64) SoftFloat64 {
}
func Modf(u SoftFloat64) (it SoftFloat64, frac SoftFloat64) {
- if u.Le(1) {
+ if u.Lt(1) {
switch {
case u.Lt(0):
it, frac = Modf(u.Neg())
diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go
index 9374db263ee..5de37a68405 100644
--- a/gnovm/pkg/test/test.go
+++ b/gnovm/pkg/test/test.go
@@ -284,6 +284,8 @@ func (opts *TestOptions) runTestFiles(
if opts.Metrics {
alloc = gno.NewAllocator(math.MaxInt64)
}
+ // reset store ops, if any - we only need them for some filetests.
+ opts.TestStore.SetLogStoreOps(false)
// Check if we already have the package - it may have been eagerly
// loaded.
diff --git a/gnovm/stdlibs/bytes/compare_test.gno b/gnovm/stdlibs/bytes/compare_test.gno
index f2b1e7c692b..5ebeba33889 100644
--- a/gnovm/stdlibs/bytes/compare_test.gno
+++ b/gnovm/stdlibs/bytes/compare_test.gno
@@ -66,6 +66,8 @@ func TestCompareIdenticalSlice(t *testing.T) {
}
func TestCompareBytes(t *testing.T) {
+ t.Skip("This test takes very long to run on Gno at time of writing, even in its short form")
+
lengths := make([]int, 0) // lengths to test in ascending order
for i := 0; i <= 128; i++ {
lengths = append(lengths, i)
diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go
index 2cc904a9170..db5ecdec05d 100644
--- a/gnovm/tests/stdlibs/generated.go
+++ b/gnovm/tests/stdlibs/generated.go
@@ -9,6 +9,7 @@ import (
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
testlibs_std "github.com/gnolang/gno/gnovm/tests/stdlibs/std"
testlibs_testing "github.com/gnolang/gno/gnovm/tests/stdlibs/testing"
+ testlibs_unicode "github.com/gnolang/gno/gnovm/tests/stdlibs/unicode"
)
// NativeFunc represents a function in the standard library which has a native
@@ -325,6 +326,118 @@ var nativeFuncs = [...]NativeFunc{
func(m *gno.Machine) {
r0 := testlibs_testing.X_unixNano()
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "IsPrint",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("bool")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.IsPrint(p0)
+
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "IsGraphic",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("bool")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.IsGraphic(p0)
+
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "SimpleFold",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("rune")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.SimpleFold(p0)
+
+ m.PushValue(gno.Go2GnoValue(
+ m.Alloc,
+ m.Store,
+ reflect.ValueOf(&r0).Elem(),
+ ))
+ },
+ },
+ {
+ "unicode",
+ "IsUpper",
+ []gno.FieldTypeExpr{
+ {Name: gno.N("p0"), Type: gno.X("rune")},
+ },
+ []gno.FieldTypeExpr{
+ {Name: gno.N("r0"), Type: gno.X("bool")},
+ },
+ false,
+ func(m *gno.Machine) {
+ b := m.LastBlock()
+ var (
+ p0 rune
+ rp0 = reflect.ValueOf(&p0).Elem()
+ )
+
+ gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0)
+
+ r0 := testlibs_unicode.IsUpper(p0)
+
m.PushValue(gno.Go2GnoValue(
m.Alloc,
m.Store,
@@ -337,6 +450,7 @@ var nativeFuncs = [...]NativeFunc{
var initOrder = [...]string{
"std",
"testing",
+ "unicode",
}
// InitOrder returns the initialization order of the standard libraries.
diff --git a/gnovm/tests/stdlibs/unicode/natives.gno b/gnovm/tests/stdlibs/unicode/natives.gno
new file mode 100644
index 00000000000..c7efaac70cc
--- /dev/null
+++ b/gnovm/tests/stdlibs/unicode/natives.gno
@@ -0,0 +1,8 @@
+package unicode
+
+// Optimized as native bindings in tests.
+
+func IsPrint(r rune) bool
+func IsGraphic(r rune) bool
+func SimpleFold(r rune) rune
+func IsUpper(r rune) bool
diff --git a/gnovm/tests/stdlibs/unicode/natives.go b/gnovm/tests/stdlibs/unicode/natives.go
new file mode 100644
index 00000000000..e627f4fe6be
--- /dev/null
+++ b/gnovm/tests/stdlibs/unicode/natives.go
@@ -0,0 +1,8 @@
+package unicode
+
+import "unicode"
+
+func IsPrint(r rune) bool { return unicode.IsPrint(r) }
+func IsGraphic(r rune) bool { return unicode.IsGraphic(r) }
+func SimpleFold(r rune) rune { return unicode.SimpleFold(r) }
+func IsUpper(r rune) bool { return unicode.IsUpper(r) }