[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