[lxc-devel] [lxd/master] Add a new shared/cmd package with initial command I/O logic

freeekanayaka on Github lxc-bot at linuxcontainers.org
Thu Apr 27 13:11:32 UTC 2017


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 968 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20170427/7b461756/attachment.bin>
-------------- next part --------------
From c2745ac8116012dc2a8d115dde51fbb2b8f80af9 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free at ekanayaka.io>
Date: Thu, 27 Apr 2017 15:04:46 +0200
Subject: [PATCH] Add a new shared/cmd package with initial command I/O logic

This is a proof of concept of some unit-testable code that I'd like
to later use for making cmdInit testable (as per #2834).

It's small so should it be easy to review and because I wish to have
some feedback on the approach before moving forward. I hope that the
branch gives a feel of what further branches would look like.

As a general note, I feel it'd be important to improve coverage of the
parts that can be unit tested easily (so basically most higher-level
application logic except for system-level interactions). I think this
can be done incrementally without too much disruption, if we agree.

Signed-off-by: Free Ekanayaka <free at ekanayaka.io>
---
 shared/cmd/context.go      | 50 +++++++++++++++++++++++++++++
 shared/cmd/context_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++
 shared/cmd/doc.go          | 11 +++++++
 3 files changed, 141 insertions(+)
 create mode 100644 shared/cmd/context.go
 create mode 100644 shared/cmd/context_test.go
 create mode 100644 shared/cmd/doc.go

diff --git a/shared/cmd/context.go b/shared/cmd/context.go
new file mode 100644
index 0000000..2b7384f
--- /dev/null
+++ b/shared/cmd/context.go
@@ -0,0 +1,50 @@
+package cmd
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/lxc/lxd/shared"
+)
+
+// Context captures the environment the sub-command is being run in,
+// such as in/out/err streams and command line arguments.
+type Context struct {
+	stdin  io.Reader
+	stdout io.Writer
+}
+
+// NewContext creates a new command context with the given parameters.
+func NewContext(stdin io.Reader, stdout io.Writer) *Context {
+	return &Context{
+		stdin:  stdin,
+		stdout: stdout,
+	}
+}
+
+// AskBool asks a question an expect a yes/no answer.
+func (c *Context) AskBool(question string, defaultAnswer string) bool {
+
+	reader := bufio.NewReader(c.stdin)
+
+	for {
+		fmt.Fprintf(c.stdout, question)
+
+		answer, _ := reader.ReadString('\n')
+		answer = strings.TrimSuffix(answer, "\n")
+
+		if answer == "" {
+			answer = defaultAnswer
+		}
+
+		if shared.StringInSlice(strings.ToLower(answer), []string{"yes", "y"}) {
+			return true
+		} else if shared.StringInSlice(strings.ToLower(answer), []string{"no", "n"}) {
+			return false
+		}
+
+		fmt.Fprintf(c.stdout, "Invalid input, try again.\n\n")
+	}
+}
diff --git a/shared/cmd/context_test.go b/shared/cmd/context_test.go
new file mode 100644
index 0000000..162320f
--- /dev/null
+++ b/shared/cmd/context_test.go
@@ -0,0 +1,80 @@
+package cmd_test
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/lxc/lxd/shared/cmd"
+)
+
+// The given question is printed on standard output.
+func TestAskBoolPrintQuestion(t *testing.T) {
+	stdin := strings.NewReader("\n")
+	stdout := new(bytes.Buffer)
+	context := cmd.NewContext(stdin, stdout)
+	context.AskBool("Do you like me?", "yes")
+
+	expected := "Do you like me?"
+	if output := stdout.String(); output != expected {
+		t.Errorf("Expected '%s' got '%s'", expected, output)
+	}
+}
+
+// If no input is given (e.g. the user just presses "enter"), the default
+// answer will be returned.
+func TestAskBoolDefault(t *testing.T) {
+	stdin := strings.NewReader("\n")
+	stdout := new(bytes.Buffer)
+	context := cmd.NewContext(stdin, stdout)
+
+	answer := context.AskBool("Do you like me?", "yes")
+
+	if answer != true {
+		t.Errorf("Expected 'true' answer")
+	}
+}
+
+// If "yes" is given, AskBool returns true.
+func TestAskBoolYes(t *testing.T) {
+	stdin := strings.NewReader("yes\n")
+	stdout := new(bytes.Buffer)
+	context := cmd.NewContext(stdin, stdout)
+
+	answer := context.AskBool("Do you like me?", "yes")
+
+	if answer != true {
+		t.Errorf("Expected 'true' answer")
+	}
+}
+
+// If "no" is given, AskBool returns false.
+func TestAskBoolNo(t *testing.T) {
+	stdin := strings.NewReader("no\n")
+	stdout := new(bytes.Buffer)
+	context := cmd.NewContext(stdin, stdout)
+
+	answer := context.AskBool("Do you like me?", "yes")
+
+	if answer != false {
+		t.Errorf("Expected 'false' answer")
+	}
+}
+
+// If an invalid answer is given, the question gets repeated until
+// it succeeds.
+func TestAskBoolInvalid(t *testing.T) {
+	stdin := strings.NewReader("foo\nyes\n")
+	stdout := new(bytes.Buffer)
+	context := cmd.NewContext(stdin, stdout)
+	answer := context.AskBool("Do you like me?", "yes")
+
+	expected := "Do you like me?Invalid input, try again.\n\nDo you like me?"
+	if output := stdout.String(); output != expected {
+		t.Errorf("Expected '%s?' got '%s'", expected, output)
+	}
+
+	if answer != true {
+		t.Errorf("Expected 'true' answer")
+	}
+}
diff --git a/shared/cmd/doc.go b/shared/cmd/doc.go
new file mode 100644
index 0000000..02dab72
--- /dev/null
+++ b/shared/cmd/doc.go
@@ -0,0 +1,11 @@
+/*
+
+The package cmd implements a simple abstraction around a "sub-command" for
+a main executable (e.g. "lxd init", where "init" is the sub-command).
+
+It is designed to make unit-testing easier, since OS-specific parts like
+standard in/out can be set in tests.
+
+*/
+
+package cmd


More information about the lxc-devel mailing list