src: refactor prompt to use `AlecAivazis/survey` (#397)

* src: refactor prompt to use `AlecAivazis/survey`

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
This commit is contained in:
Zbynek Roubalik 2021-06-21 08:38:26 +02:00 committed by GitHub
parent 83a9ca684f
commit 7a24a103ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 206 additions and 589 deletions

View File

@ -3,13 +3,14 @@ package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/ory/viper"
"github.com/spf13/cobra"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/buildpacks"
"github.com/boson-project/func/progress"
"github.com/boson-project/func/prompt"
)
func init() {
@ -58,7 +59,13 @@ kn func build --builder cnbs/sample-builder:bionic
}
func runBuild(cmd *cobra.Command, _ []string) (err error) {
config := newBuildConfig().Prompt()
config, err := newBuildConfig().Prompt()
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
function, err := functionWithOverrides(config.Path, functionOverrides{Builder: config.Builder, Image: config.Image})
if err != nil {
@ -75,10 +82,16 @@ func runBuild(cmd *cobra.Command, _ []string) (err error) {
// AND a --registry was not provided, then we need to
// prompt for a registry from which we can derive an image name.
if config.Registry == "" {
fmt.Print("A registry for function images is required (e.g. 'quay.io/boson').\n\n")
config.Registry = prompt.ForString("Registry for function images", "")
if config.Registry == "" {
return fmt.Errorf("unable to determine function image name")
fmt.Println("A registry for Function images is required. For example, 'docker.io/tigerteam'.")
err = survey.AskOne(
&survey.Input{Message: "Registry for Function images:"},
&config.Registry, survey.WithValidator(survey.Required))
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
}
@ -154,16 +167,33 @@ func newBuildConfig() buildConfig {
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
// all prompts) was set (default).
func (c buildConfig) Prompt() buildConfig {
func (c buildConfig) Prompt() (buildConfig, error) {
imageName := deriveImage(c.Image, c.Registry, c.Path)
if !interactiveTerminal() || !c.Confirm {
return c
return c, nil
}
return buildConfig{
Path: prompt.ForString("Path to project directory", c.Path),
Image: prompt.ForString("Full image name (e.g. quay.io/boson/node-sample)", imageName, prompt.WithRequired(true)),
Verbose: c.Verbose,
// Registry not prompted for as it would be confusing when combined with explicit image. Instead it is
// inferred by the derived default for Image, which uses Registry for derivation.
bc := buildConfig{Verbose: c.Verbose}
var qs = []*survey.Question{
{
Name: "path",
Prompt: &survey.Input{
Message: "Project path:",
Default: c.Path,
},
Validate: survey.Required,
},
{
Name: "image",
Prompt: &survey.Input{
Message: "Full image name (e.g. quay.io/boson/node-sample):",
Default: imageName,
},
Validate: survey.Required,
},
}
err := survey.Ask(qs, &bc)
return bc, err
}

View File

@ -4,12 +4,13 @@ import (
"fmt"
"path/filepath"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/ory/viper"
"github.com/spf13/cobra"
bosonFunc "github.com/boson-project/func"
"github.com/boson-project/func/buildpacks"
"github.com/boson-project/func/prompt"
"github.com/boson-project/func/utils"
)
@ -52,14 +53,21 @@ kn func create --template events myfunc
// TODO: autocomplate or interactive prompt for runtime and template.
}
func runCreate(cmd *cobra.Command, args []string) error {
func runCreate(cmd *cobra.Command, args []string) (err error) {
config := newCreateConfig(args)
if err := utils.ValidateFunctionName(config.Name); err != nil {
return err
err = utils.ValidateFunctionName(config.Name)
if err != nil {
return
}
config = config.Prompt()
config, err = config.Prompt()
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
function := bosonFunc.Function{
Name: config.Name,
@ -131,30 +139,62 @@ func newCreateConfig(args []string) createConfig {
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
// all prompts) was set (default).
func (c createConfig) Prompt() createConfig {
func (c createConfig) Prompt() (createConfig, error) {
if !interactiveTerminal() || !c.Confirm {
// Just print the basics if not confirming
fmt.Printf("Project path: %v\n", c.Path)
fmt.Printf("Function name: %v\n", c.Name)
fmt.Printf("Runtime: %v\n", c.Runtime)
fmt.Printf("Template: %v\n", c.Template)
return c
return c, nil
}
var derivedName, derivedPath string
for {
derivedName, derivedPath = deriveNameAndAbsolutePathFromPath(prompt.ForString("Project path", c.Path, prompt.WithRequired(true)))
err := utils.ValidateFunctionName(derivedName)
if err == nil {
break
}
fmt.Println("Error:", err)
var qs = []*survey.Question{
{
Name: "path",
Prompt: &survey.Input{
Message: "Project path:",
Default: c.Path,
},
Validate: func(val interface{}) error {
derivedName, _ := deriveNameAndAbsolutePathFromPath(val.(string))
return utils.ValidateFunctionName(derivedName)
},
},
{
Name: "runtime",
Prompt: &survey.Input{
Message: "Runtime:",
Default: c.Runtime,
// TODO add runtime suggestions: https://github.com/AlecAivazis/survey#suggestion-options
},
Validate: survey.Required,
},
{
Name: "template",
Prompt: &survey.Input{
Message: "Template:",
Default: c.Template,
// TODO add template suggestions: https://github.com/AlecAivazis/survey#suggestion-options
},
},
}
answers := struct {
Template string
Runtime string
Path string
}{}
err := survey.Ask(qs, &answers)
if err != nil {
return createConfig{}, err
}
derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(answers.Path)
return createConfig{
Name: derivedName,
Path: derivedPath,
Runtime: prompt.ForString("Runtime", c.Runtime),
Template: prompt.ForString("Template", c.Template),
}
Runtime: answers.Runtime,
Template: answers.Template,
}, nil
}

View File

@ -3,12 +3,13 @@ package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/ory/viper"
"github.com/spf13/cobra"
fn "github.com/boson-project/func"
"github.com/boson-project/func/knative"
"github.com/boson-project/func/prompt"
)
func init() {
@ -38,7 +39,13 @@ kn func delete -n apps myfunc
ValidArgsFunction: CompleteFunctionList,
PreRunE: bindEnv("path", "confirm", "namespace"),
RunE: func(cmd *cobra.Command, args []string) (err error) {
config := newDeleteConfig(args).Prompt()
config, err := newDeleteConfig(args).Prompt()
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
var function fn.Function
@ -122,12 +129,16 @@ func newDeleteConfig(args []string) deleteConfig {
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c deleteConfig) Prompt() deleteConfig {
func (c deleteConfig) Prompt() (deleteConfig, error) {
if !interactiveTerminal() || !viper.GetBool("confirm") {
return c
}
return deleteConfig{
// TODO: Path should be prompted for and set prior to name attempting path derivation. Test/fix this if necessary.
Name: prompt.ForString("Function to remove", deriveName(c.Name, c.Path), prompt.WithRequired(true)),
return c, nil
}
dc := deleteConfig{}
return dc, survey.AskOne(
&survey.Input{
Message: "Function to remove:",
Default: deriveName(c.Name, c.Path)},
&dc.Name, survey.WithValidator(survey.Required))
}

View File

@ -1,19 +1,16 @@
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"syscall"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/containers/image/v5/pkg/docker/config"
containersTypes "github.com/containers/image/v5/types"
"github.com/ory/viper"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/term"
"knative.dev/client/pkg/util"
bosonFunc "github.com/boson-project/func"
@ -21,7 +18,6 @@ import (
"github.com/boson-project/func/docker"
"github.com/boson-project/func/knative"
"github.com/boson-project/func/progress"
"github.com/boson-project/func/prompt"
)
func init() {
@ -72,7 +68,13 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) {
if err != nil {
return err
}
config = config.Prompt()
config, err = config.Prompt()
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace, Image: config.Image})
if err != nil {
@ -94,10 +96,16 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) {
// AND a --registry was not provided, then we need to
// prompt for a registry from which we can derive an image name.
if config.Registry == "" {
fmt.Print("A registry for Function images is required. For example, 'docker.io/tigerteam'.\n\n")
config.Registry = prompt.ForString("Registry for Function images", "")
if config.Registry == "" {
return fmt.Errorf("unable to determine function image name")
fmt.Println("A registry for Function images is required. For example, 'docker.io/tigerteam'.")
err = survey.AskOne(
&survey.Input{Message: "Registry for Function images:"},
&config.Registry, survey.WithValidator(survey.Required))
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return
}
}
@ -117,6 +125,9 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) {
pusher, err := docker.NewPusher(docker.WithCredentialsProvider(credentialsProvider))
if err != nil {
if err == terminal.InterruptErr {
return nil
}
return err
}
pusher.Verbose = config.Verbose
@ -176,72 +187,26 @@ func credentialsProvider(ctx context.Context, registry string) (docker.Credentia
return result, nil
}
fmt.Print("Username: ")
username, err := getUserName(ctx)
if err != nil {
return result, err
fmt.Println("Please provide credentials for image registry.")
var qs = []*survey.Question{
{
Name: "username",
Prompt: &survey.Input{
Message: "Username:",
},
Validate: survey.Required,
},
{
Name: "namespace",
Prompt: &survey.Password{
Message: "Password:",
},
Validate: survey.Required,
},
}
err = survey.Ask(qs, &result)
fmt.Print("Password: ")
bytePassword, err := getPassword(ctx)
if err != nil {
return result, err
}
password := string(bytePassword)
result.Username, result.Password = username, password
return result, nil
}
func getPassword(ctx context.Context) ([]byte, error) {
ch := make(chan struct {
p []byte
e error
})
go func() {
pass, err := term.ReadPassword(int(syscall.Stdin)) // nolint: unconvert
ch <- struct {
p []byte
e error
}{p: pass, e: err}
}()
select {
case res := <-ch:
return res.p, res.e
case <-ctx.Done():
return nil, ctx.Err()
}
}
func getUserName(ctx context.Context) (string, error) {
ch := make(chan struct {
u string
e error
})
go func() {
reader := bufio.NewReader(os.Stdin)
username, err := reader.ReadString('\n')
if err != nil {
ch <- struct {
u string
e error
}{u: "", e: err}
}
ch <- struct {
u string
e error
}{u: strings.TrimRight(username, "\r\n"), e: nil}
}()
select {
case res := <-ch:
return res.u, res.e
case <-ctx.Done():
return "", ctx.Err()
}
return result, err
}
type deployConfig struct {
@ -299,20 +264,56 @@ func newDeployConfig(cmd *cobra.Command) (deployConfig, error) {
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c deployConfig) Prompt() deployConfig {
func (c deployConfig) Prompt() (deployConfig, error) {
if !interactiveTerminal() || !c.Confirm {
return c
return c, nil
}
var qs = []*survey.Question{
{
Name: "registry",
Prompt: &survey.Input{
Message: "Registry for Function images:",
Default: c.buildConfig.Registry,
},
Validate: survey.Required,
},
{
Name: "namespace",
Prompt: &survey.Input{
Message: "Namespace:",
Default: c.Namespace,
},
},
{
Name: "path",
Prompt: &survey.Input{
Message: "Project path:",
Default: c.Path,
},
Validate: survey.Required,
},
}
answers := struct {
Registry string
Namespace string
Path string
}{}
err := survey.Ask(qs, &answers)
if err != nil {
return deployConfig{}, err
}
dc := deployConfig{
buildConfig: buildConfig{
Registry: prompt.ForString("Registry for Function images", c.buildConfig.Registry),
Registry: answers.Registry,
},
Namespace: prompt.ForString("Namespace", c.Namespace),
Path: prompt.ForString("Project path", c.Path),
Namespace: answers.Namespace,
Path: answers.Path,
Verbose: c.Verbose,
}
dc.Image = deriveImage(dc.Image, dc.Registry, dc.Path)
return dc
return dc, nil
}

1
go.mod
View File

@ -17,7 +17,6 @@ require (
github.com/ory/viper v1.7.4
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.1.3
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.19.7
k8s.io/apimachinery v0.19.7

3
go.sum
View File

@ -98,6 +98,7 @@ github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3H
github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns=
github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -547,6 +548,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0=
github.com/heroku/color v0.0.6/go.mod h1:ZBvOcx7cTF2QKOv4LbmoBtNl5uB17qWxGuzZrsi1wLU=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
@ -610,6 +612,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=

View File

@ -1,220 +0,0 @@
package prompt
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
)
// Default delimiter between prompt and user input. Includes a space such that
// overrides have full control of spacing.
const DefaultDelimiter = ": "
// DefaultRetryLimit imposed for a few reasons, not least of which infinite
// loops in tests.
const DefaultRetryLimit = 10
type prompt struct {
in io.Reader
out io.Writer
label string
delim string
required bool
retryLimit int
}
type stringPrompt struct {
prompt
dflt string
}
type boolPrompt struct {
prompt
dflt bool
}
type Option func(*prompt)
func WithInput(in io.Reader) Option {
return func(p *prompt) {
p.in = in
}
}
func WithOutput(out io.Writer) Option {
return func(p *prompt) {
p.out = out
}
}
func WithDelimiter(d string) Option {
return func(p *prompt) {
p.delim = d
}
}
func WithRequired(r bool) Option {
return func(p *prompt) {
p.required = r
}
}
func WithRetryLimit(l int) Option {
return func(p *prompt) {
p.retryLimit = l
}
}
func ForString(label string, dflt string, options ...Option) string {
p := &stringPrompt{
prompt: prompt{
in: os.Stdin,
out: os.Stdout,
label: label,
delim: DefaultDelimiter,
retryLimit: DefaultRetryLimit,
},
dflt: dflt,
}
for _, o := range options {
o(&p.prompt)
}
writeStringLabel(p) // Write the label
input, err := readString(p) // gather the input
var attempt int
for err != nil && attempt < p.retryLimit { // while there are errors
attempt++
writeError(err, &p.prompt) // write the error on its own line
writeStringLabel(p) // re-write the label
input, err = readString(p) // re-read the input
}
return input
}
func writeStringLabel(p *stringPrompt) {
_, err := p.out.Write([]byte(p.label))
if err != nil {
panic(err)
}
if p.dflt != "" {
if p.label != "" {
_, err = p.out.Write([]byte(" "))
if err != nil {
panic(err)
}
}
fmt.Fprintf(p.out, "(%v)", p.dflt)
}
_, err = p.out.Write([]byte(p.delim))
if err != nil {
panic(err)
}
}
func readString(p *stringPrompt) (s string, err error) {
if s, err = bufio.NewReader(p.in).ReadString('\n'); err != nil {
return
}
s = strings.TrimSpace(s)
if s == "" {
s = p.dflt
}
if s == "" && p.required {
err = errors.New("please enter a value")
}
return
}
func ForBool(label string, dflt bool, options ...Option) bool {
p := &boolPrompt{
prompt: prompt{
in: os.Stdin,
out: os.Stdout,
label: label,
delim: DefaultDelimiter,
retryLimit: DefaultRetryLimit,
},
dflt: dflt,
}
for _, o := range options {
o(&p.prompt)
}
writeBoolLabel(p) // write the prompt label
input, err := readBool(p) // gather the input
var attempt int
for err != nil && attempt < p.retryLimit {
attempt++
writeError(err, &p.prompt) // write the error on its own line
writeBoolLabel(p) // re-write the label
input, err = readBool(p) // re-read the input
}
return input
}
func writeBoolLabel(p *boolPrompt) {
_, err := p.out.Write([]byte(p.label))
if err != nil {
panic(err)
}
if p.label != "" {
_, err = p.out.Write([]byte(" "))
if err != nil {
panic(err)
}
}
if p.dflt {
fmt.Fprint(p.out, "(Y/n)")
} else {
fmt.Fprint(p.out, "(y/N)")
}
_, err = p.out.Write([]byte(p.delim))
if err != nil {
panic(err)
}
}
func readBool(p *boolPrompt) (bool, error) {
reader := bufio.NewReader(p.in)
var s string
s, err := reader.ReadString('\n')
if err != nil {
return p.dflt, err
}
s = strings.TrimSpace(s)
if s == "" {
return p.dflt, nil
}
if isTruthy(s) { // variants of 'true'
return true, nil
} else if isFalsy(s) {
return false, nil
}
return strconv.ParseBool(s)
}
var truthy = regexp.MustCompile("(?i)y(?:es)?|1")
var falsy = regexp.MustCompile("(?i)n(?:o)?|0")
func isTruthy(confirm string) bool {
return truthy.MatchString(confirm)
}
func isFalsy(confirm string) bool {
return falsy.MatchString(confirm)
}
func writeError(err error, p *prompt) {
_, _err := p.out.Write([]byte("\n"))
if _err != nil {
panic(_err)
}
fmt.Fprintln(p.out, err)
}

View File

@ -1,247 +0,0 @@
// +build !integration
package prompt_test
import (
"bytes"
"fmt"
"testing"
"github.com/boson-project/func/prompt"
)
// TestForStringLabel ensures that a string prompt with a given label is printed to stdout.
func TestForStringLabel(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
in.Write([]byte("\n"))
// Empty label
_ = prompt.ForString("", "",
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String() != ": " {
t.Fatalf("expected output to be ': ', got '%v'\n", out.String())
}
out.Reset()
in.Reset()
in.Write([]byte("\n"))
// Populated lable
_ = prompt.ForString("Name", "",
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String() != "Name: " {
t.Fatalf("expected 'Name', got '%v'\n", out.String())
}
}
// TestForStringLabelDefault ensures that a default, only if provided, is appended
// to the prompt label.
func TestForStringLabelDefault(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
in.Write([]byte("\n")) // [ENTER]
// No lablel but a default
_ = prompt.ForString("", "Alice",
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String() != "(Alice): " {
t.Fatalf("expected '(Alice): ', got '%v'\n", out.String())
}
out.Reset()
in.Reset()
in.Write([]byte("\n")) // [ENTER]
// Label with default
_ = prompt.ForString("Name", "Alice",
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String() != "Name (Alice): " {
t.Fatalf("expected 'Name (Alice): ', got '%v'\n", out.String())
}
}
// TestForStringLabelDelimiter ensures that a default delimiter override is respected.
func TestWithDelimiter(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
in.Write([]byte("\n")) // [ENTER]
_ = prompt.ForString("", "",
prompt.WithInput(&in),
prompt.WithOutput(&out),
prompt.WithDelimiter("Δ"))
if out.String() != "Δ" {
t.Fatalf("expected output to be 'Δ', got '%v'\n", out.String())
}
}
// TestForStringDefault ensures that the default is returned when enter is
// pressed on a string input.
func TestForStringDefault(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
in.Write([]byte("\n")) // [ENTER]
// Empty default should return an empty value.
s := prompt.ForString("", "",
prompt.WithInput(&in),
prompt.WithOutput(&out))
if s != "" {
t.Fatalf("expected '', got '%v'\n", s)
}
in.Reset()
out.Reset()
in.Write([]byte("\n")) // [ENTER]
// Extant default should be returned
s = prompt.ForString("", "default",
prompt.WithInput(&in),
prompt.WithOutput(&out))
if s != "default" {
t.Fatalf("expected 'default', got '%v'\n", s)
}
}
// TestForStringRequired ensures that an error is generated if a value is not
// provided for a required prompt with no default.
func TestForStringRequired(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
in.Write([]byte("\n")) // [ENTER]
_ = prompt.ForString("", "",
prompt.WithInput(&in),
prompt.WithOutput(&out),
prompt.WithRequired(true),
prompt.WithRetryLimit(1)) // makes the output buffer easier to confirm
output := out.String()
expected := ": \nplease enter a value\n: "
if output != expected {
t.Fatalf("Unexpected prompt received for a required value. expected '%v', got '%v'", expected, output)
}
}
// TestForString ensures that string input is accepted.
func TestForString(t *testing.T) {
var in bytes.Buffer
var out bytes.Buffer
in.Write([]byte("hunter2\n"))
s := prompt.ForString("", "",
prompt.WithInput(&in),
prompt.WithOutput(&out))
if s != "hunter2" {
t.Fatalf("Expected 'hunter2' got '%v'", s)
}
}
// TestForBoolLabel ensures that a prompt for a given boolean prompt prints
// the expected y/n prompt.
func TestForBoolLabel(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
in.Write([]byte("\n"))
// Empty label, default false
_ = prompt.ForBool("", false,
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String() != "(y/N): " {
t.Fatalf("expected output to be '(y/N): ', got '%v'\n", out.String())
}
out.Reset()
in.Reset()
in.Write([]byte("\n"))
// Empty label, default true
_ = prompt.ForBool("", true,
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String() != "(Y/n): " {
t.Fatalf("expected output to be '(Y/n): ', got '%v'\n", out.String())
}
out.Reset()
in.Reset()
in.Write([]byte("\n"))
// Populated lablel default false
_ = prompt.ForBool("Local", false,
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String() != "Local (y/N): " {
t.Fatalf("expected 'Local (y/N): ', got '%v'\n", out.String())
}
}
// TestForBoolDefault ensures that the default is returned when no user input is given.
func TestForBoolDefault(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
in.Write([]byte("\n"))
b := prompt.ForBool("", false,
prompt.WithInput(&in), prompt.WithOutput(&out))
if b != false {
t.Fatal("expected default of false to be returned when user accepts.")
}
out.Reset()
in.Reset()
in.Write([]byte("\n"))
b = prompt.ForBool("", true,
prompt.WithInput(&in), prompt.WithOutput(&out))
if b != true {
t.Fatal("expected default of true to be returned when user accepts.")
}
}
// TestForBool ensures that a truthy value, when entered, is returned as a bool.
func TestForBool(t *testing.T) {
var out bytes.Buffer
var in bytes.Buffer
cases := []struct {
in string
out bool
}{
{"true", true},
{"1", true},
{"y", true},
{"Y", true},
{"yes", true},
{"Yes", true},
{"YES", true},
{"false", false},
{"0", false},
{"n", false},
{"N", false},
{"no", false},
{"No", false},
{"NO", false},
}
for _, c := range cases {
in.Reset()
out.Reset()
fmt.Fprintf(&in, "%v\n", c.in)
// Note the default value is always the oposite of the input
// to ensure it is flipped.
b := prompt.ForBool("", !c.out,
prompt.WithInput(&in), prompt.WithOutput(&out))
if b != c.out {
t.Fatalf("expected '%v' to be an acceptable %v.", c.in, c.out)
}
}
}