[lxc-devel] [lxd/master] Complete cmd.Context support for various AskXXX methods
freeekanayaka on Github
lxc-bot at linuxcontainers.org
Fri Apr 28 08:29:02 UTC 2017
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 727 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20170428/76b1381d/attachment.bin>
-------------- next part --------------
From 70cf6a28a3025f0accc31419045dcea31b95c37e Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free at ekanayaka.io>
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 <free at ekanayaka.io>
---
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)
+}
More information about the lxc-devel
mailing list