The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/3245
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === This branch is a mechanical follow-up of the first one introducing cmd.Context. It adds the rest of AskXXX methods as defined in cmdInit, and should be a step towards making cmdInit itself testable. The logic is exactly the same as the inline functions currently defined in main_init.go, although some boilerplate could be avoided by factoring common logic together. Signed-off-by: Free Ekanayaka <[email protected]>
From 70cf6a28a3025f0accc31419045dcea31b95c37e Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <[email protected]> Date: Fri, 28 Apr 2017 10:22:52 +0200 Subject: [PATCH] Complete cmd.Context support for various AskXXX methods This branch is a mechanical follow-up of the first one introducing cmd.Context. It adds the rest of AskXXX methods as defined in cmdInit, and should be a step towards making cmdInit itself testable. The logic is exactly the same as the inline functions currently defined in main_init.go, although some boilerplate could be avoided by factoring common logic together. Signed-off-by: Free Ekanayaka <[email protected]> --- shared/cmd/context.go | 92 +++++++++++++++++++++++++++++-- shared/cmd/context_test.go | 131 +++++++++++++++++++++++++++++++++++++++------ shared/cmd/testing.go | 45 ++++++++++++++++ 3 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 shared/cmd/testing.go diff --git a/shared/cmd/context.go b/shared/cmd/context.go index c265f8d..346657c 100644 --- a/shared/cmd/context.go +++ b/shared/cmd/context.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "strconv" "strings" "github.com/lxc/lxd/shared" @@ -29,8 +30,7 @@ func NewContext(stdin io.Reader, stdout, stderr io.Writer) *Context { // AskBool asks a question an expect a yes/no answer. func (c *Context) AskBool(question string, defaultAnswer string) bool { for { - fmt.Fprintf(c.stdout, question) - answer := c.readAnswer(defaultAnswer) + answer := c.askQuestion(question, defaultAnswer) if shared.StringInSlice(strings.ToLower(answer), []string{"yes", "y"}) { return true @@ -38,10 +38,96 @@ func (c *Context) AskBool(question string, defaultAnswer string) bool { return false } - fmt.Fprintf(c.stderr, "Invalid input, try again.\n\n") + c.invalidInput() + } +} + +// AskChoice asks the user to select between a set of choices +func (c *Context) AskChoice(question string, choices []string, defaultAnswer string) string { + for { + answer := c.askQuestion(question, defaultAnswer) + + if shared.StringInSlice(answer, choices) { + return answer + } + + c.invalidInput() + } +} + +// AskInt asks the user to enter an integer between a min and max value +func (c *Context) AskInt(question string, min int64, max int64, defaultAnswer string) int64 { + for { + answer := c.askQuestion(question, defaultAnswer) + + result, err := strconv.ParseInt(answer, 10, 64) + + if err == nil && (min == -1 || result >= min) && (max == -1 || result <= max) { + return result + } + + c.invalidInput() + } +} + +// AskString asks the user to enter a string, which optionally +// conforms to a validation function. +func (c *Context) AskString(question string, defaultAnswer string, validate func(string) error) string { + for { + answer := c.askQuestion(question, defaultAnswer) + + if validate != nil { + error := validate(answer) + if error != nil { + fmt.Fprintf(c.stderr, "Invalid input: %s\n\n", error) + continue + } + } + if len(answer) != 0 { + return answer + } + + c.invalidInput() } } +// AskPassword asks the user to enter a password. The reader function used to +// read the password without echoing characters must be passed (usually +// terminal.ReadPassword from golang.org/x/crypto/ssh/terminal). +func (c *Context) AskPassword(question string, reader func(int) ([]byte, error)) string { + for { + fmt.Fprintf(c.stdout, question) + + pwd, _ := reader(0) + fmt.Fprintf(c.stdout, "\n") + inFirst := string(pwd) + inFirst = strings.TrimSuffix(inFirst, "\n") + + fmt.Fprintf(c.stdout, "Again: ") + pwd, _ = reader(0) + fmt.Fprintf(c.stdout, "\n") + inSecond := string(pwd) + inSecond = strings.TrimSuffix(inSecond, "\n") + + if inFirst == inSecond { + return inFirst + } + + c.invalidInput() + } +} + +// Ask a question on the output stream and read the answer from the input stream +func (c *Context) askQuestion(question, defaultAnswer string) string { + fmt.Fprintf(c.stdout, question) + return c.readAnswer(defaultAnswer) +} + +// Print an invalid input message on the error stream +func (c *Context) invalidInput() { + fmt.Fprintf(c.stderr, "Invalid input, try again.\n\n") +} + // Read the user's answer from the input stream, trimming newline and providing a default. func (c *Context) readAnswer(defaultAnswer string) string { answer, _ := c.stdin.ReadString('\n') diff --git a/shared/cmd/context_test.go b/shared/cmd/context_test.go index b57743f..24006b0 100644 --- a/shared/cmd/context_test.go +++ b/shared/cmd/context_test.go @@ -1,11 +1,11 @@ package cmd_test import ( - "bytes" - "strings" + "fmt" "testing" "github.com/lxc/lxd/shared/cmd" + "github.com/stretchr/testify/assert" ) // AskBool returns a boolean result depending on the user input. @@ -26,22 +26,123 @@ func TestAskBool(t *testing.T) { {"Do you code?", "yes", "Do you code?Do you code?", "Invalid input, try again.\n\n", "foo\nyes\n", true}, } for _, c := range cases { - stdin := strings.NewReader(c.input) - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - context := cmd.NewContext(stdin, stdout, stderr) + streams := cmd.NewMemoryStreams(c.input) + context := cmd.NewMemoryContext(streams) result := context.AskBool(c.question, c.defaultAnswer) - if result != c.result { - t.Errorf("Expected '%v' result got '%v'", c.result, result) - } + assert.Equal(t, c.result, result, "Unexpected answer result") + streams.AssertOutEqual(t, c.output) + streams.AssertErrEqual(t, c.error) + } +} + +// AskChoice returns one of the given choices +func TestAskChoice(t *testing.T) { + cases := []struct { + question string + choices []string + defaultAnswer string + output string + error string + input string + result string + }{ + {"Best food?", []string{"pizza", "rice"}, "rice", "Best food?", "", "\n", "rice"}, + {"Best food?", []string{"pizza", "rice"}, "rice", "Best food?", "", "pizza\n", "pizza"}, + {"Best food?", []string{"pizza", "rice"}, "rice", "Best food?Best food?", "Invalid input, try again.\n\n", "foo\npizza\n", "pizza"}, + } + for _, c := range cases { + streams := cmd.NewMemoryStreams(c.input) + context := cmd.NewMemoryContext(streams) + result := context.AskChoice(c.question, c.choices, c.defaultAnswer) - if output := stdout.String(); output != c.output { - t.Errorf("Expected '%s' output got '%s'", c.output, output) - } + assert.Equal(t, c.result, result, "Unexpected answer result") + streams.AssertOutEqual(t, c.output) + streams.AssertErrEqual(t, c.error) + } +} + +// AskInt returns an integer within the given bounds +func TestAskInt(t *testing.T) { + cases := []struct { + question string + min int64 + max int64 + defaultAnswer string + output string + error string + input string + result int64 + }{ + {"Age?", 0, 100, "30", "Age?", "", "\n", 30}, + {"Age?", 0, 100, "30", "Age?", "", "40\n", 40}, + {"Age?", 0, 100, "30", "Age?Age?", "Invalid input, try again.\n\n", "foo\n40\n", 40}, + {"Age?", 18, 65, "30", "Age?Age?", "Invalid input, try again.\n\n", "10\n30\n", 30}, + {"Age?", 18, 65, "30", "Age?Age?", "Invalid input, try again.\n\n", "70\n30\n", 30}, + {"Age?", 0, -1, "30", "Age?", "", "120\n", 120}, + } + for _, c := range cases { + streams := cmd.NewMemoryStreams(c.input) + context := cmd.NewMemoryContext(streams) + result := context.AskInt(c.question, c.min, c.max, c.defaultAnswer) + + assert.Equal(t, c.result, result, "Unexpected answer result") + streams.AssertOutEqual(t, c.output) + streams.AssertErrEqual(t, c.error) + } +} + +// AskString returns a string conforming the validation function. +func TestAskString(t *testing.T) { + cases := []struct { + question string + defaultAnswer string + validate func(string) error + output string + error string + input string + result string + }{ + {"Name?", "Joe", nil, "Name?", "", "\n", "Joe"}, + {"Name?", "Joe", nil, "Name?", "", "John\n", "John"}, + {"Name?", "Joe", func(s string) error { + if s[0] != 'J' { + return fmt.Errorf("ugly name") + } + return nil + }, "Name?Name?", "Invalid input: ugly name\n\n", "Ted\nJohn", "John"}, + } + for _, c := range cases { + streams := cmd.NewMemoryStreams(c.input) + context := cmd.NewMemoryContext(streams) + result := context.AskString(c.question, c.defaultAnswer, c.validate) + + assert.Equal(t, c.result, result, "Unexpected answer result") + streams.AssertOutEqual(t, c.output) + streams.AssertErrEqual(t, c.error) + } +} + +// AskPassword returns the password entered twice by the user. +func TestAskPassword(t *testing.T) { + cases := []struct { + question string + reader func(int) ([]byte, error) + output string + error string + result string + }{ + {"Pass?", func(int) ([]byte, error) { + return []byte("pwd"), nil + }, "Pass?\nAgain: \n", "", "pwd"}, + } + for _, c := range cases { + streams := cmd.NewMemoryStreams("") + context := cmd.NewMemoryContext(streams) + result := context.AskPassword(c.question, c.reader) - if error := stderr.String(); error != c.error { - t.Errorf("Expected '%s' error got '%s'", c.error, error) - } + assert.Equal(t, c.result, result, "Unexpected answer result") + streams.AssertOutEqual(t, c.output) + streams.AssertErrEqual(t, c.error) } } diff --git a/shared/cmd/testing.go b/shared/cmd/testing.go new file mode 100644 index 0000000..faefb48 --- /dev/null +++ b/shared/cmd/testing.go @@ -0,0 +1,45 @@ +// Utilities for testing cmd-related code. + +package cmd + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// MemoryStreams provide an in-memory version of the system +// stdin/stdout/stderr streams. +type MemoryStreams struct { + in *strings.Reader + out *bytes.Buffer + err *bytes.Buffer +} + +// NewMemoryStreams creates a new set of in-memory streams with the given +// user input. +func NewMemoryStreams(input string) *MemoryStreams { + return &MemoryStreams{ + in: strings.NewReader(input), + out: new(bytes.Buffer), + err: new(bytes.Buffer), + } +} + +// AssertOutEqual checks that the given text matches the the out stream. +func (s *MemoryStreams) AssertOutEqual(t *testing.T, expected string) { + assert.Equal(t, expected, s.out.String(), "Unexpected output stream") +} + +// AssertErrEqual checks that the given text matches the the err stream. +func (s *MemoryStreams) AssertErrEqual(t *testing.T, expected string) { + assert.Equal(t, expected, s.err.String(), "Unexpected error stream") +} + +// NewMemoryContext creates a new command Context using the given in-memory +// streams. +func NewMemoryContext(streams *MemoryStreams) *Context { + return NewContext(streams.in, streams.out, streams.err) +}
_______________________________________________ lxc-devel mailing list [email protected] http://lists.linuxcontainers.org/listinfo/lxc-devel
