Skip to content

Commit

Permalink
Fix: Update spinner constructor and options api (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
chandrareddyp authored Jan 21, 2024
1 parent 778d244 commit 510278b
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 43 deletions.
67 changes: 64 additions & 3 deletions component/output_spinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,49 @@ import (

"github.com/briandowns/spinner"
"github.com/mattn/go-isatty"

"github.com/vmware-tanzu/tanzu-plugin-runtime/log"
)

// OutputWriterSpinner is OutputWriter augmented with a spinner.
type OutputWriterSpinner interface {
OutputWriter
RenderWithSpinner()
StopSpinner()
// SetFinalText sets the spinner final text and prefix
// log indicator (log.LogTypeOUTPUT can be used for no prefix)
SetFinalText(finalText string, prefix log.LogType)
}

// outputwriterspinner is our internal implementation.
type outputwriterspinner struct {
outputwriter
spinnerText string
spinner *spinner.Spinner
spinnerText string
spinnerFinalText string
spinner *spinner.Spinner
}

type OutputWriterSpinnerOptions struct {
OutputWriterOptions []OutputWriterOption
SpinnerOptions []OutputWriterSpinnerOption
}

// OutputWriterSpinnerOption is an option for outputwriterspinner
type OutputWriterSpinnerOption func(*outputwriterspinner)

// WithSpinnerFinalMsg sets the spinner final text and prefix log indicator
// (log.LogTypeOUTPUT can be used for no prefix)
func WithSpinnerFinalMsg(finalText string, prefix log.LogType) OutputWriterSpinnerOption {
finalText = fmt.Sprintf("%s%s", log.GetLogTypeIndicator(prefix), finalText)
return func(ows *outputwriterspinner) {
ows.spinnerFinalText = finalText
}
}

// NewOutputWriterWithSpinner returns implementation of OutputWriterSpinner.
//
// Deprecated: NewOutputWriterWithSpinner is being deprecated in favor of
// NewOutputWriterspinnerWithOptions.
// NewOutputWriterSpinnerWithSpinnerOptions.
// Until it is removed, it will retain the existing behavior of converting
// incoming row values to their golang string representation for backward
// compatibility reasons
Expand All @@ -40,20 +63,42 @@ func NewOutputWriterWithSpinner(output io.Writer, outputFormat, spinnerText stri
}

// NewOutputWriterSpinnerWithOptions returns implementation of OutputWriterSpinner.
//
// Deprecated: NewOutputWriterSpinnerWithOptions is being deprecated in favor of
// NewOutputWriterSpinnerWithSpinnerOptions.
func NewOutputWriterSpinnerWithOptions(output io.Writer, outputFormat, spinnerText string, startSpinner bool, opts []OutputWriterOption, headers ...string) (OutputWriterSpinner, error) {
ows := &outputwriterspinner{}
ows.out = output
ows.outputFormat = OutputType(outputFormat)
ows.keys = headers
ows.applyOptions(opts)

return setAndInitializeSpinner(ows, spinnerText, startSpinner)
}

// NewOutputWriterSpinnerWithSpinnerOptions returns implementation of OutputWriterSpinner.
func NewOutputWriterSpinnerWithSpinnerOptions(output io.Writer, outputFormat OutputType, spinnerText string, startSpinner bool, opts OutputWriterSpinnerOptions, headers ...string) (OutputWriterSpinner, error) {
ows := &outputwriterspinner{}
ows.out = output
ows.outputFormat = outputFormat
ows.keys = headers
ows.applyOptions(opts.OutputWriterOptions)
ows.applyOutputWriterSpinnerOptions(opts.SpinnerOptions)
return setAndInitializeSpinner(ows, spinnerText, startSpinner)
}

// setAndInitializeSpinner sets the spinner text and initializes the spinner
func setAndInitializeSpinner(ows *outputwriterspinner, spinnerText string, startSpinner bool) (OutputWriterSpinner, error) {
if ows.outputFormat != JSONOutputType && ows.outputFormat != YAMLOutputType {
ows.spinnerText = spinnerText
ows.spinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond)
if err := ows.spinner.Color("bgBlack", "bold", "fgWhite"); err != nil {
return nil, err
}
ows.spinner.Suffix = fmt.Sprintf(" %s", spinnerText)
if ows.spinnerFinalText != "" {
spinner.WithFinalMSG(ows.spinnerFinalText)(ows.spinner)
}

// Start the spinner only if attached to terminal
attachedToTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
Expand All @@ -80,3 +125,19 @@ func (ows *outputwriterspinner) StopSpinner() {
fmt.Fprintln(ows.out)
}
}

// SetFinalText sets the spinner final text and prefix log indicator
// (log.LogTypeOUTPUT can be used for no prefix)
func (ows *outputwriterspinner) SetFinalText(finalText string, prefix log.LogType) {
if ows.spinner != nil {
ows.spinnerFinalText = fmt.Sprintf("%s%s", log.GetLogTypeIndicator(prefix), finalText)
spinner.WithFinalMSG(ows.spinnerFinalText)(ows.spinner)
}
}

// applyOutputWriterSpinnerOptions applies the options to the outputwriterspinner
func (ows *outputwriterspinner) applyOutputWriterSpinnerOptions(spinnerOpts []OutputWriterSpinnerOption) {
for i := range spinnerOpts {
spinnerOpts[i](ows)
}
}
107 changes: 107 additions & 0 deletions component/output_spinner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package component

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"

"github.com/vmware-tanzu/tanzu-plugin-runtime/log"
)

const loading = "Loading..."

func TestNewOutputWriterWithSpinner(t *testing.T) {
output := bytes.Buffer{}
spinnerText := loading
headers := []string{"Name", "Age"}

// Test creating an OutputWriterSpinner with a spinner
ows, err := NewOutputWriterWithSpinner(&output, "table", spinnerText, true, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner without a spinner
ows, err = NewOutputWriterWithSpinner(&output, "table", spinnerText, false, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner with unsupported output format
ows, err = NewOutputWriterWithSpinner(&output, "unsupported", spinnerText, true, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)
}

func TestNewOutputWriterSpinnerWithOptions(t *testing.T) {
output := bytes.Buffer{}
spinnerText := loading
headers := []string{"Name", "Age"}

// Test creating an OutputWriterSpinner with options and a spinner
opts := []OutputWriterOption{WithAutoStringify()}
ows, err := NewOutputWriterSpinnerWithOptions(&output, "table", spinnerText, true, opts, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner with options without a spinner
opts = []OutputWriterOption{WithAutoStringify()}
ows, err = NewOutputWriterSpinnerWithOptions(&output, "table", spinnerText, false, opts, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner with unsupported output format
opts = []OutputWriterOption{WithAutoStringify()}
ows, err = NewOutputWriterSpinnerWithOptions(&output, "unsupported", spinnerText, true, opts, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)
}

func TestNewOutputWriterSpinnerWithSpinnerOptions(t *testing.T) {
output := bytes.Buffer{}
spinnerText := loading
headers := []string{"Name", "Age"}

// Test creating an OutputWriterSpinner with spinner options and a spinner
opts := OutputWriterSpinnerOptions{
OutputWriterOptions: []OutputWriterOption{WithAutoStringify()},
SpinnerOptions: []OutputWriterSpinnerOption{WithSpinnerFinalMsg("Done!", log.LogTypeSUCCESS)},
}
ows, err := NewOutputWriterSpinnerWithSpinnerOptions(&output, "table", spinnerText, true, opts, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner with spinner options without a spinner
opts = OutputWriterSpinnerOptions{
SpinnerOptions: []OutputWriterSpinnerOption{WithSpinnerFinalMsg("Done!", log.LogTypeSUCCESS)},
}
ows, err = NewOutputWriterSpinnerWithSpinnerOptions(&output, "table", spinnerText, false, opts, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner with unsupported output format
opts = OutputWriterSpinnerOptions{}
ows, err = NewOutputWriterSpinnerWithSpinnerOptions(&output, "unsupported", spinnerText, true, opts, headers...)
assert.NoError(t, err)
assert.NotNil(t, ows)
}

func TestOutputWriterSpinnerRenderWithSpinner(t *testing.T) {
output := bytes.Buffer{}
spinnerText := loading
headers := []string{"Name", "Age"}

// Create an OutputWriterSpinner with a spinner
ows, err := NewOutputWriterWithSpinner(&output, "table", spinnerText, true, headers...)
ows.AddRow(map[string]interface{}{"Name": "John", "Age": 30})
assert.NoError(t, err)
assert.NotNil(t, ows)

// Render with spinner
ows.RenderWithSpinner()
assert.Contains(t, output.String(), "NAME")
assert.Contains(t, output.String(), "John")
assert.Contains(t, output.String(), "30")
}
20 changes: 10 additions & 10 deletions log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,58 +85,58 @@ var l = NewLogger()

// Info logs a non-error message with the given key/value pairs as context.
func Info(msg string, kvs ...interface{}) {
l.Print(msg, nil, logTypeINFO, kvs...)
l.Print(msg, nil, string(LogTypeINFO), kvs...)
}

// Infof logs a non-error message with the given key/value pairs as context.
func Infof(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeINFO)
l.Print(msg, nil, string(LogTypeINFO))
}

// Error logs an error message with the given key/value pairs as context.
func Error(err error, msg string, kvs ...interface{}) {
l.Print(msg, err, logTypeERROR, kvs...)
l.Print(msg, err, string(LogTypeERROR), kvs...)
}

// Errorf logs a error message with the given key/value pairs as context.
func Errorf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeERROR)
l.Print(msg, nil, string(LogTypeERROR))
}

// Warning logs a warning messages with the given key/value pairs as context.
func Warning(msg string, kvs ...interface{}) {
l.Print(msg, nil, logTypeWARN, kvs...)
l.Print(msg, nil, string(LogTypeWARN), kvs...)
}

// Warningf logs a warning messages with the given message format with format specifier and arguments.
func Warningf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeWARN)
l.Print(msg, nil, string(LogTypeWARN))
}

// Success logs a success messages with the given key/value pairs as context.
func Success(msg string, kvs ...interface{}) {
l.Print(msg, nil, logTypeSUCCESS, kvs...)
l.Print(msg, nil, string(LogTypeSUCCESS), kvs...)
}

// Successf logs a success messages with the given message format with format specifier and arguments.
func Successf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeSUCCESS)
l.Print(msg, nil, string(LogTypeSUCCESS))
}

// Fatal logs a fatal message with the given key/value pairs as context and returns with os.Exit(1)
func Fatal(err error, msg string, kvs ...interface{}) {
l.Print(msg, err, logTypeERROR, kvs...)
l.Print(msg, err, string(LogTypeERROR), kvs...)
os.Exit(1)
}

// Outputf writes a message to stdout
func Outputf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeOUTPUT)
l.Print(msg, nil, string(LogTypeOUTPUT))
}

// V returns an InfoLogger value for a specific verbosity level.
Expand Down
35 changes: 12 additions & 23 deletions log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,58 +88,58 @@ func (l *logger) Enabled() bool {

// Info logs a non-error message with the given key/value pairs as context.
func (l *logger) Info(msg string, kvs ...interface{}) {
l.Print(msg, nil, logTypeINFO, kvs...)
l.Print(msg, nil, string(LogTypeINFO), kvs...)
}

// Infof logs a non-error messages with the given message format with format specifier and arguments.
func (l *logger) Infof(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeINFO)
l.Print(msg, nil, string(LogTypeINFO))
}

// Error logs an error message with the given key/value pairs as context.
func (l *logger) Error(err error, msg string, kvs ...interface{}) {
l.Print(msg, err, logTypeERROR, kvs...)
l.Print(msg, err, string(LogTypeERROR), kvs...)
}

// Errorf logs a error message with the given key/value pairs as context.
func (l *logger) Errorf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeERROR)
l.Print(msg, nil, string(LogTypeERROR))
}

// Warning logs a warning messages with the given key/value pairs as context.
func (l *logger) Warning(msg string, kvs ...interface{}) {
l.Print(msg, nil, logTypeWARN, kvs...)
l.Print(msg, nil, string(LogTypeWARN), kvs...)
}

// Warningf logs a warning messages with the given message format with format specifier and arguments.
func (l *logger) Warningf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeWARN)
l.Print(msg, nil, string(LogTypeWARN))
}

// Success logs a success messages with the given key/value pairs as context.
func (l *logger) Success(msg string, kvs ...interface{}) {
l.Print(msg, nil, logTypeSUCCESS, kvs...)
l.Print(msg, nil, string(LogTypeSUCCESS), kvs...)
}

// Successf logs a success messages with the given message format with format specifier and arguments.
func (l *logger) Successf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeSUCCESS)
l.Print(msg, nil, string(LogTypeSUCCESS))
}

// Fatal logs a fatal message with the given key/value pairs as context and returns with os.exit(1)
func (l *logger) Fatal(err error, msg string, kvs ...interface{}) {
l.Print(msg, err, logTypeERROR, kvs...)
l.Print(msg, err, string(LogTypeERROR), kvs...)
os.Exit(1)
}

// Outputf writes a message to stdout
func (l *logger) Outputf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
l.Print(msg, nil, logTypeOUTPUT)
l.Print(msg, nil, string(LogTypeOUTPUT))
}

// V returns an InfoLogger value for a specific verbosity level.
Expand Down Expand Up @@ -212,25 +212,14 @@ func (l *logger) getLogString(values []interface{}) string {
}
f, err := flatten(entry)
if err != nil {
_, _ = logWriter.Write([]byte{}, []byte(err.Error()), l.Enabled(), 0, logTypeWARN)
_, _ = logWriter.Write([]byte{}, []byte(err.Error()), l.Enabled(), 0, string(LogTypeWARN))
return ""
}
return f
}

func (l *logger) getLogTypeIndicator(logType string) string {
switch logType {
case logTypeINFO:
return "[i] "
case logTypeWARN:
return "[!] "
case logTypeERROR:
return "[x] "
case logTypeSUCCESS:
return "[ok] "
case logTypeOUTPUT:
}
return ""
return GetLogTypeIndicator(LogType(logType))
}

func copySlice(in []interface{}) []interface{} {
Expand Down
Loading

0 comments on commit 510278b

Please sign in to comment.