Skip to content

Commit

Permalink
Stage support (#297)
Browse files Browse the repository at this point in the history
Initial stage implementation
  • Loading branch information
yuce authored Aug 22, 2023
1 parent 622e2e9 commit 16630d8
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 18 deletions.
20 changes: 18 additions & 2 deletions clc/cmd/exec_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"os"
"os/signal"
"strings"
"time"

"github.com/fatih/color"
Expand Down Expand Up @@ -265,7 +266,7 @@ func (ec *ExecContext) WrapResult(f func() error) error {

func (ec *ExecContext) PrintlnUnnecessary(text string) {
if !ec.Quiet() {
I2(fmt.Fprintln(ec.Stdout(), text))
I2(fmt.Fprintln(ec.Stdout(), colorizeText(text)))
}
}

Expand All @@ -286,6 +287,16 @@ func (ec *ExecContext) ensurePrinter() error {
return nil
}

func colorizeText(text string) string {
if strings.HasPrefix(text, "OK ") {
return fmt.Sprintf(" %s %s", color.GreenString("OK"), text[3:])
}
if strings.HasPrefix(text, "FAIL ") {
return fmt.Sprintf(" %s %s", color.RedString("FAIL"), text[5:])
}
return text
}

func makeErrorStringFromHTTPResponse(text string) string {
m := map[string]any{}
if err := json.Unmarshal([]byte(text), &m); err != nil {
Expand Down Expand Up @@ -314,13 +325,18 @@ func (s *simpleSpinner) Start() {
_ = s.sp.Start()
}

func (s *simpleSpinner) Stop() {
// ignoring the error here
_ = s.sp.Stop()
}

func (s *simpleSpinner) SetText(text string) {
s.text = text
if text == "" {
s.sp.Prefix("")
return
}
s.sp.Prefix(text + cancelMsg)
s.sp.Prefix(" " + text + cancelMsg)
}

func (s *simpleSpinner) SetProgress(progress float32) {
Expand Down
117 changes: 117 additions & 0 deletions clc/ux/stage/stage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package stage

import (
"context"
"fmt"
"time"

"github.com/hazelcast/hazelcast-commandline-client/clc"
"github.com/hazelcast/hazelcast-commandline-client/internal"
"github.com/hazelcast/hazelcast-commandline-client/internal/plug"
"github.com/hazelcast/hazelcast-commandline-client/internal/str"
)

type Statuser interface {
SetProgress(progress float32)
SetRemainingDuration(dur time.Duration)
}

type basicStatuser struct {
text string
textFmtWithRemaining string
indexText string
sp clc.Spinner
}

func (s *basicStatuser) SetProgress(progress float32) {
s.sp.SetProgress(progress)
}

func (s *basicStatuser) SetRemainingDuration(dur time.Duration) {
text := s.text
if dur > 0 {
text = fmt.Sprintf(s.textFmtWithRemaining, dur)
}
s.sp.SetText(s.indexText + " " + text)
}

type Stage struct {
ProgressMsg string
SuccessMsg string
FailureMsg string
Func func(status Statuser) error
}

type Provider internal.Iterator[Stage]

type Counter interface {
StageCount() int
}

type FixedProvider struct {
stages []Stage
offset int
current Stage
err error
}

func NewFixedProvider(stages ...Stage) *FixedProvider {
return &FixedProvider{stages: stages}
}

func (sp *FixedProvider) Next() bool {
if sp.offset >= len(sp.stages) {
return false
}
sp.current = sp.stages[sp.offset]
sp.offset++
return true
}

func (sp *FixedProvider) Value() Stage {
return sp.current
}

func (sp *FixedProvider) Err() error {
return sp.err
}

func (sp *FixedProvider) StageCount() int {
return len(sp.stages)
}

func Execute(ctx context.Context, ec plug.ExecContext, sp Provider) error {
ss := &basicStatuser{}
var index int
var stageCount int
if sc, ok := sp.(Counter); ok {
stageCount = sc.StageCount()
}
for sp.Next() {
if sp.Err() != nil {
return sp.Err()
}
stg := sp.Value()
index++
ss.text = stg.ProgressMsg
ss.textFmtWithRemaining = stg.ProgressMsg + " (%s left)"
if stageCount > 0 {
d := str.SpacePaddedIntFormat(stageCount)
ss.indexText = fmt.Sprintf("["+d+"/%d]", index, stageCount)
} else {
ss.indexText = ""
}
_, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, spinner clc.Spinner) (any, error) {
ss.sp = spinner
ss.SetRemainingDuration(0)
return nil, stg.Func(ss)
})
if err != nil {
ec.PrintlnUnnecessary(fmt.Sprintf("FAIL %s: %s", stg.FailureMsg, err.Error()))
return err
}
stop()
ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg))
}
return nil
}
97 changes: 97 additions & 0 deletions clc/ux/stage/stage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package stage_test

import (
"context"
"fmt"
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage"
"github.com/hazelcast/hazelcast-commandline-client/internal/it"
)

func TestExecute(t *testing.T) {
stages := []stage.Stage{
{
ProgressMsg: "Progressing 1",
SuccessMsg: "Success 1",
FailureMsg: "Failure 1",
Func: func(status stage.Statuser) error {
time.Sleep(1 * time.Millisecond)
return nil
},
},
{
ProgressMsg: "Progressing 2",
SuccessMsg: "Success 2",
FailureMsg: "Failure 2",
Func: func(status stage.Statuser) error {
for i := 0; i < 5; i++ {
status.SetProgress(float32(i+1) / float32(5))
}
time.Sleep(1 * time.Millisecond)
return nil
},
},
{
ProgressMsg: "Progressing 3",
SuccessMsg: "Success 3",
FailureMsg: "Failure 3",
Func: func(status stage.Statuser) error {
for i := 0; i < 5; i++ {
status.SetRemainingDuration(5*time.Second - time.Duration(i+1)*time.Second)
}
time.Sleep(1 * time.Millisecond)
return nil
},
},
}
ec := it.NewExecuteContext(nil)
err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...))
assert.NoError(t, err)
texts := []string{
"[1/3] Progressing 1",
"[2/3] Progressing 2",
"[3/3] Progressing 3",
"[3/3] Progressing 3 (4s left)",
"[3/3] Progressing 3 (3s left)",
"[3/3] Progressing 3 (2s left)",
"[3/3] Progressing 3 (1s left)",
"[3/3] Progressing 3",
}
assert.Equal(t, texts, ec.Spinner.Texts)
progresses := []float32{0.2, 0.4, 0.6, 0.8, 1}
assert.Equal(t, progresses, ec.Spinner.Progresses)
text := "OK [1/3] Success 1.\nOK [2/3] Success 2.\nOK [3/3] Success 3.\n"
assert.Equal(t, text, ec.StdoutText())
}

func TestExecute_WithFailure(t *testing.T) {
stages := []stage.Stage{
{
ProgressMsg: "Progressing 1",
SuccessMsg: "Success 1",
FailureMsg: "Failure 1",
Func: func(status stage.Statuser) error {
return fmt.Errorf("some error")
},
},
{
ProgressMsg: "Progressing 2",
SuccessMsg: "Success 2",
FailureMsg: "Failure 2",
Func: func(status stage.Statuser) error {
return nil
},
},
}
ec := it.NewExecuteContext(nil)
err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...))
assert.Error(t, err)
texts := []string{"[1/2] Progressing 1"}
assert.Equal(t, texts, ec.Spinner.Texts)
text := "FAIL Failure 1: some error\n"
assert.Equal(t, text, ec.StdoutText())
}
57 changes: 41 additions & 16 deletions internal/it/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,31 @@ func (c CommandContext) SetTopLevel(b bool) {
}

type ExecContext struct {
lg *Logger
stdout *bytes.Buffer
stderr *bytes.Buffer
stdin *bytes.Buffer
args []string
props *plug.Properties
Rows []output.Row
lg *Logger
stdout *bytes.Buffer
stderr *bytes.Buffer
stdin *bytes.Buffer
args []string
props *plug.Properties
Rows []output.Row
Spinner *Spinner
}

func NewExecuteContext(args []string) *ExecContext {
return &ExecContext{
lg: NewLogger(),
stdout: &bytes.Buffer{},
stderr: &bytes.Buffer{},
stdin: &bytes.Buffer{},
args: args,
props: plug.NewProperties(),
lg: NewLogger(),
stdout: &bytes.Buffer{},
stderr: &bytes.Buffer{},
stdin: &bytes.Buffer{},
args: args,
props: plug.NewProperties(),
Spinner: NewSpinner(),
}
}
func (ec *ExecContext) ExecuteBlocking(context.Context, func(context.Context, clc.Spinner) (any, error)) (any, context.CancelFunc, error) {
//TODO implement me
panic("implement me")
func (ec *ExecContext) ExecuteBlocking(ctx context.Context, f func(context.Context, clc.Spinner) (any, error)) (any, context.CancelFunc, error) {
v, err := f(ctx, ec.Spinner)
stop := func() {}
return v, stop, err
}

func (ec *ExecContext) Props() plug.ReadOnlyProperties {
Expand Down Expand Up @@ -187,3 +190,25 @@ func (ec *ExecContext) PrintlnUnnecessary(text string) {
func (ec *ExecContext) WrapResult(f func() error) error {
return f()
}

type Spinner struct {
Texts []string
Progresses []float32
}

func NewSpinner() *Spinner {
return &Spinner{}
}

func (s *Spinner) Reset() {
s.Texts = nil
s.Progresses = nil
}

func (s *Spinner) SetText(text string) {
s.Texts = append(s.Texts, text)
}

func (s *Spinner) SetProgress(progress float32) {
s.Progresses = append(s.Progresses, progress)
}
17 changes: 17 additions & 0 deletions internal/iterator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package internal

// Iterator is a generic iterator interface.
// Non thread safe.
type Iterator[T any] interface {
// Next returns false if the iterator is exhausted.
// Otherwise advances the iterator and returns true.
Next() bool
// Value returns the current value in the iterator.
// Next should always be called before Value is called.
// Otherwise may panic.
Value() T
// Err contains the error after advancing the iterator.
// If it is nil, it is safe to call Next.
// Otherwise Next should not be called.
Err() error
}
10 changes: 10 additions & 0 deletions internal/str/str.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package str

import (
"fmt"
"strconv"
"strings"
)

Expand Down Expand Up @@ -37,3 +38,12 @@ func MaybeShorten(s string, l int) string {
}
return fmt.Sprintf("%s...", s[:l])
}

// SpacePaddedIntFormat returns the fmt string that can fit the given integer.
// The padding uses spaces.
func SpacePaddedIntFormat(maxValue int) string {
if maxValue < 0 {
panic("SpacePaddedIntFormat: cannot be negative")
}
return fmt.Sprintf("%%%dd", len(strconv.Itoa(maxValue)))
}
Loading

0 comments on commit 16630d8

Please sign in to comment.