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

Reply via email to