Activity indicator for init command (#207)

* first version of spinner

- implemented class with basic method
- covered with dummy test
- integrated with init command

* Style and license remarks

* fixed review remarks

* fixed typo + integration of spinner with terminate command

* integration of spinner with create command
This commit is contained in:
Valentyn Yukhymenko 2022-10-04 19:17:05 +03:00 committed by GitHub
parent acdcb535c0
commit abe40de3e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 4 deletions

View File

@ -114,7 +114,10 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler)
} }
} }
spinner := newSpinner(cmd, "Loading ", true)
spinner.Start()
state, err := creator.Create(cmd.Context(), provider, config, flags.name, instanceType, flags.controllerCount, flags.workerCount) state, err := creator.Create(cmd.Context(), provider, config, flags.name, instanceType, flags.controllerCount, flags.workerCount)
spinner.Stop()
if err != nil { if err != nil {
return err return err
} }

View File

@ -124,7 +124,8 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
return fmt.Errorf("parsing or generating master secret from file %s: %w", flags.masterSecretPath, err) return fmt.Errorf("parsing or generating master secret from file %s: %w", flags.masterSecretPath, err)
} }
cmd.Println("Initializing cluster ...") spinner := newSpinner(cmd, "Initializing cluster ", true)
spinner.Start()
req := &initproto.InitRequest{ req := &initproto.InitRequest{
MasterSecret: masterSecret.Key, MasterSecret: masterSecret.Key,
Salt: masterSecret.Salt, Salt: masterSecret.Salt,
@ -141,6 +142,7 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
ConformanceMode: flags.conformance, ConformanceMode: flags.conformance,
} }
resp, err := initCall(cmd.Context(), newDialer(validator), flags.endpoint, req) resp, err := initCall(cmd.Context(), newDialer(validator), flags.endpoint, req)
spinner.Stop()
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,73 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/spf13/cobra"
)
var (
spinnerStates = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}
dotsStates = []string{".", "..", "..."}
)
type spinner struct {
out *cobra.Command
text string
showDots bool
delay time.Duration
wg *sync.WaitGroup
stop int32
}
func newSpinner(c *cobra.Command, text string, showDots bool) *spinner {
return &spinner{
out: c,
text: text,
showDots: showDots,
wg: &sync.WaitGroup{},
delay: 100 * time.Millisecond,
stop: 0,
}
}
func (s *spinner) Start() {
s.wg.Add(1)
go func() {
defer s.wg.Done()
for i := 0; ; i = (i + 1) % len(spinnerStates) {
if atomic.LoadInt32(&s.stop) != 0 {
break
}
dotsState := ""
if s.showDots {
dotsState = dotsStates[i%len(dotsStates)]
}
state := fmt.Sprintf("\r%s %s%s", spinnerStates[i], s.text, dotsState)
s.out.Print(state)
time.Sleep(s.delay)
}
dotsState := ""
if s.showDots {
dotsState = dotsStates[len(dotsStates)-1]
}
finalState := fmt.Sprintf("\r%s%s ", s.text, dotsState)
s.out.Println(finalState)
}()
}
func (s *spinner) Stop() {
atomic.StoreInt32(&s.stop, 1)
s.wg.Wait()
}

View File

@ -0,0 +1,91 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
)
const (
baseWait = 1
baseText = "Loading"
)
func TestSpinnerInitialState(t *testing.T) {
cmd := NewInitCmd()
var out bytes.Buffer
cmd.SetOut(&out)
var errOut bytes.Buffer
cmd.SetErr(&errOut)
s := newSpinner(cmd, baseText, true)
s.Start()
time.Sleep(baseWait * time.Second)
s.Stop()
require.True(t, out.Len() > 0)
require.True(t, errOut.Len() == 0)
outStr := out.String()
require.True(t, strings.HasPrefix(outStr, generateAllStatesAsString(baseText, true)))
}
func TestSpinnerFinalState(t *testing.T) {
cmd := NewInitCmd()
var out bytes.Buffer
cmd.SetOut(&out)
var errOut bytes.Buffer
cmd.SetErr(&errOut)
s := newSpinner(cmd, baseText, true)
s.Start()
time.Sleep(baseWait * time.Second)
s.Stop()
require.True(t, out.Len() > 0)
require.True(t, errOut.Len() == 0)
outStr := out.String()
require.True(t, strings.HasSuffix(outStr, baseText+"... \n"))
}
func TestSpinnerDisabledShowDotsFlag(t *testing.T) {
cmd := NewInitCmd()
var out bytes.Buffer
cmd.SetOut(&out)
var errOut bytes.Buffer
cmd.SetErr(&errOut)
s := newSpinner(cmd, baseText, false)
s.Start()
time.Sleep(baseWait * time.Second)
s.Stop()
require.True(t, out.Len() > 0)
require.True(t, errOut.Len() == 0)
outStr := out.String()
require.True(t, strings.HasPrefix(outStr, generateAllStatesAsString(baseText, false)))
require.True(t, strings.HasSuffix(outStr, baseText+" \n"))
}
func generateAllStatesAsString(text string, showDots bool) string {
var builder strings.Builder
for i := 0; i < len(spinnerStates); i++ {
dotsState := ""
if showDots {
dotsState = dotsStates[i%len(dotsStates)]
}
builder.WriteString(fmt.Sprintf("\r%s %s%s", spinnerStates[i], text, dotsState))
}
return builder.String()
}

View File

@ -47,9 +47,11 @@ func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file.
return fmt.Errorf("reading Constellation state: %w", err) return fmt.Errorf("reading Constellation state: %w", err)
} }
cmd.Println("Terminating ...") spinner := newSpinner(cmd, "Terminating ", true)
spinner.Start()
if err := terminator.Terminate(cmd.Context(), stat); err != nil { err := terminator.Terminate(cmd.Context(), stat)
spinner.Stop()
if err != nil {
return fmt.Errorf("terminating Constellation cluster: %w", err) return fmt.Errorf("terminating Constellation cluster: %w", err)
} }