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) }