-
Couldn't load subscription status.
- Fork 7.3k
Issue commands should parse args early #10811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aaddcb0
8b615ec
6129b26
7744e05
60f2484
9669932
5c67c19
f55138c
455c77a
e474acc
81ecbd8
cfa90a2
55d3b1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| package argparsetest | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "reflect" | ||
| "testing" | ||
|
|
||
| "github.com/cli/cli/v2/internal/ghrepo" | ||
| "github.com/cli/cli/v2/pkg/cmdutil" | ||
| "github.com/google/shlex" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // newCmdFunc represents the typical function signature we use for creating commands e.g. `NewCmdView`. | ||
| // | ||
| // It is generic over `T` as each command construction has their own Options type e.g. `ViewOptions` | ||
| type newCmdFunc[T any] func(f *cmdutil.Factory, runF func(*T) error) *cobra.Command | ||
|
|
||
| // TestArgParsing is a test helper that verifies that issue commands correctly parse the `{issue number | url}` | ||
| // positional arg into an issue number and base repo. | ||
| // | ||
| // Looking through the existing tests, I noticed that the coverage was pretty smattered. | ||
| // Since nearly all issue commands only accept a single positional argument, we are able to reuse this test helper. | ||
| // Commands with no further flags or args can use this solely. | ||
| // Commands with extra flags use this and further table tests. | ||
| // Commands with extra required positional arguments (like `transfer`) cannot use this. They duplicate these cases inline. | ||
| func TestArgParsing[T any](t *testing.T, fn newCmdFunc[T]) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| expectedissueNumber int | ||
| expectedBaseRepo ghrepo.Interface | ||
| expectErr bool | ||
| }{ | ||
| { | ||
| name: "no argument", | ||
| input: "", | ||
| expectErr: true, | ||
| }, | ||
| { | ||
| name: "issue number argument", | ||
| input: "23 --repo owner/repo", | ||
| expectedissueNumber: 23, | ||
| expectedBaseRepo: ghrepo.New("owner", "repo"), | ||
| }, | ||
| { | ||
| name: "argument is hash prefixed number", | ||
| // Escaping is required here to avoid what I think is shellex treating it as a comment. | ||
| input: "\\#23 --repo owner/repo", | ||
| expectedissueNumber: 23, | ||
| expectedBaseRepo: ghrepo.New("owner", "repo"), | ||
| }, | ||
| { | ||
| name: "argument is a URL", | ||
| input: "https://github.com/cli/cli/issues/23", | ||
| expectedissueNumber: 23, | ||
| expectedBaseRepo: ghrepo.New("cli", "cli"), | ||
| }, | ||
| { | ||
| name: "argument cannot be parsed to an issue", | ||
| input: "unparseable", | ||
| expectErr: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| f := &cmdutil.Factory{} | ||
|
|
||
| argv, err := shlex.Split(tt.input) | ||
| assert.NoError(t, err) | ||
|
|
||
| var gotOpts T | ||
| cmd := fn(f, func(opts *T) error { | ||
| gotOpts = *opts | ||
| return nil | ||
| }) | ||
|
|
||
| cmdutil.EnableRepoOverride(cmd, f) | ||
|
|
||
| // TODO: remember why we do this | ||
| cmd.Flags().BoolP("help", "x", false, "") | ||
|
Comment on lines
+82
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather see an explanation like: "this is to avoid collision with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tbh I just didn't remember why we were doing this, but I think you've remembered for me. I think we should probably extract something shared like |
||
|
|
||
| cmd.SetArgs(argv) | ||
| cmd.SetIn(&bytes.Buffer{}) | ||
| cmd.SetOut(&bytes.Buffer{}) | ||
| cmd.SetErr(&bytes.Buffer{}) | ||
|
|
||
| _, err = cmd.ExecuteC() | ||
|
|
||
| if tt.expectErr { | ||
| require.Error(t, err) | ||
| return | ||
| } else { | ||
| require.NoError(t, err) | ||
| } | ||
|
|
||
| actualIssueNumber := issueNumberFromOpts(t, gotOpts) | ||
| assert.Equal(t, tt.expectedissueNumber, actualIssueNumber) | ||
|
|
||
| actualBaseRepo := baseRepoFromOpts(t, gotOpts) | ||
| assert.True( | ||
| t, | ||
| ghrepo.IsSame(tt.expectedBaseRepo, actualBaseRepo), | ||
| "expected base repo %+v, got %+v", tt.expectedBaseRepo, actualBaseRepo, | ||
| ) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func issueNumberFromOpts(t *testing.T, v any) int { | ||
| rv := reflect.ValueOf(v) | ||
| field := rv.FieldByName("IssueNumber") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this probably is the best way of doing this, so I'm just thinking out loud and these are just half-baked and over-engineered solutions. Feel free to ignore with no reply. I'm thinking of two possible solutions to avoid binding to field names 1. Field tagsWe can loop through the fields and pick the one with the right tag. We're still binding to a string literal, but the field name is free of any restriction. type FooOption struct {
BaseRepo func() (ghrepo.Interface, error) `ghopt:"baseRepo"`
IssueNumber int `ghopt:"issueNumber"`
}2. Custom
|
||
| if !field.IsValid() || field.Kind() != reflect.Int { | ||
| t.Fatalf("Type %T does not have IssueNumber int field", v) | ||
| } | ||
| return int(field.Int()) | ||
| } | ||
|
|
||
| func baseRepoFromOpts(t *testing.T, v any) ghrepo.Interface { | ||
| rv := reflect.ValueOf(v) | ||
| field := rv.FieldByName("BaseRepo") | ||
| // check whether the field is valid and of type func() (ghrepo.Interface, error) | ||
| if !field.IsValid() || field.Kind() != reflect.Func { | ||
| t.Fatalf("Type %T does not have BaseRepo func field", v) | ||
| } | ||
| // call the function and check the return value | ||
| results := field.Call([]reflect.Value{}) | ||
| if len(results) != 2 { | ||
| t.Fatalf("%T.BaseRepo() does not return two values", v) | ||
| } | ||
| if !results[1].IsNil() { | ||
| t.Fatalf("%T.BaseRepo() returned an error: %v", v, results[1].Interface()) | ||
| } | ||
| return results[0].Interface().(ghrepo.Interface) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package comment | |
| import ( | ||
| "github.com/MakeNowJust/heredoc" | ||
| "github.com/cli/cli/v2/internal/ghrepo" | ||
| "github.com/cli/cli/v2/pkg/cmd/issue/shared" | ||
| issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared" | ||
| prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" | ||
| "github.com/cli/cli/v2/pkg/cmdutil" | ||
|
|
@@ -37,15 +38,41 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e | |
| Args: cobra.ExactArgs(1), | ||
| PreRunE: func(cmd *cobra.Command, args []string) error { | ||
| opts.RetrieveCommentable = func() (prShared.Commentable, ghrepo.Interface, error) { | ||
| // TODO wm: more testing | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block is totally untested. Not sure how much I want to tackle this for the extra confidence. TBD. |
||
| issueNumber, parsedBaseRepo, err := shared.ParseIssueFromArg(args[0]) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| // If the args provided the base repo then use that directly. | ||
| var baseRepo ghrepo.Interface | ||
|
|
||
| if parsedBaseRepo, present := parsedBaseRepo.Value(); present { | ||
| baseRepo = parsedBaseRepo | ||
| } else { | ||
| // support `-R, --repo` override | ||
| baseRepo, err = f.BaseRepo() | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
| } | ||
|
|
||
| httpClient, err := f.HttpClient() | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| fields := []string{"id", "url"} | ||
| if opts.EditLast { | ||
| fields = append(fields, "comments") | ||
| } | ||
| return issueShared.IssueFromArgWithFields(httpClient, f.BaseRepo, args[0], fields) | ||
|
|
||
| issue, err := issueShared.FindIssueOrPR(httpClient, baseRepo, issueNumber, fields) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| return issue, baseRepo, nil | ||
| } | ||
| return prShared.CommentablePreRun(cmd, opts) | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/issue/Issue?