cli/command/testing.go
vilmibm 36ade42ba3 scriptability improvements: issue commands
This commit is part of work to make gh more scriptable. It includes both
some general purpose helpers towards this goal as well as improvements
to the issue commands. Other commands will follow.

- Adds `utils/terminal.go` for finding out about gh's execution environment
- introduces `stubTerminal` for either faking being attached to a tty or not during tests
- updates issue commands to behave better when not attached to a tty:
  - issue list doesn't print fuzzy dates
  - issue list doesn't print header
  - issue list prints state explicitly
  - issue create no longer hangs
  - issue create fails with clear error unless both -t and -b are specified
  - issue view prints raw issue body
  - issue view prints metadata in a consistent, linewise format
2020-07-14 12:30:53 -05:00

195 lines
4.2 KiB
Go

package command
import (
"bytes"
"errors"
"fmt"
"reflect"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/core"
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/utils"
"github.com/google/shlex"
"github.com/spf13/pflag"
)
const defaultTestConfig = `hosts:
github.com:
user: OWNER
oauth_token: "1234567890"
`
type askStubber struct {
Asks [][]*survey.Question
Count int
Stubs [][]*QuestionStub
}
func initAskStubber() (*askStubber, func()) {
origSurveyAsk := SurveyAsk
as := askStubber{}
SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
as.Asks = append(as.Asks, qs)
count := as.Count
as.Count += 1
if count >= len(as.Stubs) {
panic(fmt.Sprintf("more asks than stubs. most recent call: %v", qs))
}
// actually set response
stubbedQuestions := as.Stubs[count]
for i, sq := range stubbedQuestions {
q := qs[i]
if q.Name != sq.Name {
panic(fmt.Sprintf("stubbed question mismatch: %s != %s", q.Name, sq.Name))
}
if sq.Default {
defaultValue := reflect.ValueOf(q.Prompt).Elem().FieldByName("Default")
_ = core.WriteAnswer(response, q.Name, defaultValue)
} else {
_ = core.WriteAnswer(response, q.Name, sq.Value)
}
}
return nil
}
teardown := func() {
SurveyAsk = origSurveyAsk
}
return &as, teardown
}
type QuestionStub struct {
Name string
Value interface{}
Default bool
}
func (as *askStubber) Stub(stubbedQuestions []*QuestionStub) {
// A call to .Ask takes a list of questions; a stub is then a list of questions in the same order.
as.Stubs = append(as.Stubs, stubbedQuestions)
}
func initBlankContext(cfg, repo, branch string) {
initContext = func() context.Context {
ctx := context.NewBlank()
ctx.SetBaseRepo(repo)
ctx.SetBranch(branch)
ctx.SetRemotes(map[string]string{
"origin": "OWNER/REPO",
})
if cfg == "" {
cfg = defaultTestConfig
}
// NOTE we are not restoring the original readConfig; we never want to touch the config file on
// disk during tests.
config.StubConfig(cfg, "")
return ctx
}
}
func initFakeHTTP() *httpmock.Registry {
http := &httpmock.Registry{}
ensureScopes = func(ctx context.Context, client *api.Client, wantedScopes ...string) (*api.Client, error) {
return client, nil
}
apiClientForContext = func(context.Context) (*api.Client, error) {
return api.NewClient(api.ReplaceTripper(http)), nil
}
return http
}
type cmdOut struct {
outBuf, errBuf *bytes.Buffer
}
func (c cmdOut) String() string {
return c.outBuf.String()
}
func (c cmdOut) Stderr() string {
return c.errBuf.String()
}
func RunCommand(args string) (*cmdOut, error) {
rootCmd := RootCmd
rootArgv, err := shlex.Split(args)
if err != nil {
return nil, err
}
cmd, _, err := rootCmd.Traverse(rootArgv)
if err != nil {
return nil, err
}
rootCmd.SetArgs(rootArgv)
outBuf := bytes.Buffer{}
cmd.SetOut(&outBuf)
errBuf := bytes.Buffer{}
cmd.SetErr(&errBuf)
// Reset flag values so they don't leak between tests
// FIXME: change how we initialize Cobra commands to render this hack unnecessary
cmd.Flags().VisitAll(func(f *pflag.Flag) {
f.Changed = false
switch v := f.Value.(type) {
case pflag.SliceValue:
_ = v.Replace([]string{})
default:
switch v.Type() {
case "bool", "string", "int":
_ = v.Set(f.DefValue)
}
}
})
_, err = rootCmd.ExecuteC()
cmd.SetOut(nil)
cmd.SetErr(nil)
return &cmdOut{&outBuf, &errBuf}, err
}
type errorStub struct {
message string
}
func (s errorStub) Output() ([]byte, error) {
return nil, errors.New(s.message)
}
func (s errorStub) Run() error {
return errors.New(s.message)
}
func stubTerminal(connected bool) func() {
isTerminal := utils.IsTerminal
utils.IsTerminal = func(_ interface{}) bool {
return connected
}
terminalSize := utils.TerminalSize
if connected {
utils.TerminalSize = func(_ interface{}) (int, int, error) {
return 80, 20, nil
}
} else {
utils.TerminalSize = func(_ interface{}) (int, int, error) {
return 0, 0, fmt.Errorf("terminal connection stubbed to false")
}
}
return func() {
utils.IsTerminal = isTerminal
utils.TerminalSize = terminalSize
}
}