mirror of
https://github.com/docker/compose.git
synced 2026-02-09 01:59:22 +08:00
decouple Event from tty progress writer
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
committed by
Guillaume Lours
parent
bf50c99193
commit
e5c8b68642
@@ -16,26 +16,9 @@
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventStatus indicates the status of an action
|
||||
type EventStatus int
|
||||
|
||||
func (s EventStatus) colorFn() colorFunc {
|
||||
switch s {
|
||||
case Done:
|
||||
return SuccessColor
|
||||
case Warning:
|
||||
return WarningColor
|
||||
case Error:
|
||||
return ErrorColor
|
||||
default:
|
||||
return nocolor
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// Working means that the current task is working
|
||||
Working EventStatus = iota
|
||||
@@ -56,11 +39,7 @@ type Event struct {
|
||||
StatusText string
|
||||
Current int64
|
||||
Percent int
|
||||
|
||||
Total int64
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
spinner *Spinner
|
||||
Total int64
|
||||
}
|
||||
|
||||
// ErrorMessageEvent creates a new Error Event with message
|
||||
@@ -180,31 +159,3 @@ func NewEvent(id string, status EventStatus, statusText string) Event {
|
||||
StatusText: statusText,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Event) stop() {
|
||||
e.endTime = time.Now()
|
||||
e.spinner.Stop()
|
||||
}
|
||||
|
||||
func (e *Event) hasMore() {
|
||||
e.spinner.Restart()
|
||||
}
|
||||
|
||||
var (
|
||||
spinnerDone = "✔"
|
||||
spinnerWarning = "!"
|
||||
spinnerError = "✘"
|
||||
)
|
||||
|
||||
func (e *Event) Spinner() any {
|
||||
switch e.Status {
|
||||
case Done:
|
||||
return SuccessColor(spinnerDone)
|
||||
case Warning:
|
||||
return WarningColor(spinnerWarning)
|
||||
case Error:
|
||||
return ErrorColor(spinnerError)
|
||||
default:
|
||||
return CountColor(e.spinner.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ import (
|
||||
|
||||
type ttyWriter struct {
|
||||
out io.Writer
|
||||
events map[string]Event
|
||||
eventIDs []string
|
||||
tasks map[string]task
|
||||
ids []string
|
||||
repeated bool
|
||||
numLines int
|
||||
done chan bool
|
||||
@@ -46,6 +46,29 @@ type ttyWriter struct {
|
||||
progressTitle string
|
||||
}
|
||||
|
||||
type task struct {
|
||||
ID string
|
||||
parentID string
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
text string
|
||||
status EventStatus
|
||||
statusText string
|
||||
current int64
|
||||
percent int
|
||||
total int64
|
||||
spinner *Spinner
|
||||
}
|
||||
|
||||
func (t *task) stop() {
|
||||
t.endTime = time.Now()
|
||||
t.spinner.Stop()
|
||||
}
|
||||
|
||||
func (t *task) hasMore() {
|
||||
t.spinner.Restart()
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
@@ -77,44 +100,54 @@ func (w *ttyWriter) Event(e Event) {
|
||||
}
|
||||
|
||||
func (w *ttyWriter) event(e Event) {
|
||||
if !slices.Contains(w.eventIDs, e.ID) {
|
||||
w.eventIDs = append(w.eventIDs, e.ID)
|
||||
if !slices.Contains(w.ids, e.ID) {
|
||||
w.ids = append(w.ids, e.ID)
|
||||
}
|
||||
if _, ok := w.events[e.ID]; ok {
|
||||
last := w.events[e.ID]
|
||||
if _, ok := w.tasks[e.ID]; ok {
|
||||
last := w.tasks[e.ID]
|
||||
switch e.Status {
|
||||
case Done, Error, Warning:
|
||||
if last.Status != e.Status {
|
||||
if last.status != e.Status {
|
||||
last.stop()
|
||||
}
|
||||
case Working:
|
||||
last.hasMore()
|
||||
}
|
||||
last.Status = e.Status
|
||||
last.Text = e.Text
|
||||
last.StatusText = e.StatusText
|
||||
last.status = e.Status
|
||||
last.text = e.Text
|
||||
last.statusText = e.StatusText
|
||||
// progress can only go up
|
||||
if e.Total > last.Total {
|
||||
last.Total = e.Total
|
||||
if e.Total > last.total {
|
||||
last.total = e.Total
|
||||
}
|
||||
if e.Current > last.Current {
|
||||
last.Current = e.Current
|
||||
if e.Current > last.current {
|
||||
last.current = e.Current
|
||||
}
|
||||
if e.Percent > last.Percent {
|
||||
last.Percent = e.Percent
|
||||
if e.Percent > last.percent {
|
||||
last.percent = e.Percent
|
||||
}
|
||||
// allow set/unset of parent, but not swapping otherwise prompt is flickering
|
||||
if last.ParentID == "" || e.ParentID == "" {
|
||||
last.ParentID = e.ParentID
|
||||
if last.parentID == "" || e.ParentID == "" {
|
||||
last.parentID = e.ParentID
|
||||
}
|
||||
w.events[e.ID] = last
|
||||
w.tasks[e.ID] = last
|
||||
} else {
|
||||
e.startTime = time.Now()
|
||||
e.spinner = NewSpinner()
|
||||
if e.Status == Done || e.Status == Error {
|
||||
e.stop()
|
||||
t := task{
|
||||
ID: e.ID,
|
||||
parentID: e.ParentID,
|
||||
startTime: time.Now(),
|
||||
text: e.Text,
|
||||
status: e.Status,
|
||||
statusText: e.StatusText,
|
||||
current: e.Current,
|
||||
percent: e.Percent,
|
||||
total: e.Total,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
w.events[e.ID] = e
|
||||
if e.Status == Done || e.Status == Error {
|
||||
t.stop()
|
||||
}
|
||||
w.tasks[e.ID] = t
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +180,7 @@ func (w *ttyWriter) printTailEvents() {
|
||||
func (w *ttyWriter) print() { //nolint:gocyclo
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
if len(w.eventIDs) == 0 {
|
||||
if len(w.ids) == 0 {
|
||||
return
|
||||
}
|
||||
terminalWidth := goterm.Width()
|
||||
@@ -167,43 +200,43 @@ func (w *ttyWriter) print() { //nolint:gocyclo
|
||||
_, _ = fmt.Fprint(w.out, aec.Show)
|
||||
}()
|
||||
|
||||
firstLine := fmt.Sprintf("[+] %s %d/%d", w.progressTitle, numDone(w.events), len(w.events))
|
||||
if w.numLines != 0 && numDone(w.events) == w.numLines {
|
||||
firstLine := fmt.Sprintf("[+] %s %d/%d", w.progressTitle, numDone(w.tasks), len(w.tasks))
|
||||
if w.numLines != 0 && numDone(w.tasks) == w.numLines {
|
||||
firstLine = DoneColor(firstLine)
|
||||
}
|
||||
_, _ = fmt.Fprintln(w.out, firstLine)
|
||||
|
||||
var statusPadding int
|
||||
for _, v := range w.eventIDs {
|
||||
event := w.events[v]
|
||||
l := len(fmt.Sprintf("%s %s", event.ID, event.Text))
|
||||
for _, v := range w.ids {
|
||||
t := w.tasks[v]
|
||||
l := len(fmt.Sprintf("%s %s", t.ID, t.text))
|
||||
if statusPadding < l {
|
||||
statusPadding = l
|
||||
}
|
||||
if event.ParentID != "" {
|
||||
if t.parentID != "" {
|
||||
statusPadding -= 2
|
||||
}
|
||||
}
|
||||
|
||||
if len(w.eventIDs) > goterm.Height()-2 {
|
||||
if len(w.ids) > goterm.Height()-2 {
|
||||
w.skipChildEvents = true
|
||||
}
|
||||
numLines := 0
|
||||
for _, v := range w.eventIDs {
|
||||
event := w.events[v]
|
||||
if event.ParentID != "" {
|
||||
for _, v := range w.ids {
|
||||
t := w.tasks[v]
|
||||
if t.parentID != "" {
|
||||
continue
|
||||
}
|
||||
line := w.lineText(event, "", terminalWidth, statusPadding, w.dryRun)
|
||||
line := w.lineText(t, "", terminalWidth, statusPadding, w.dryRun)
|
||||
_, _ = fmt.Fprint(w.out, line)
|
||||
numLines++
|
||||
for _, v := range w.eventIDs {
|
||||
ev := w.events[v]
|
||||
if ev.ParentID == event.ID {
|
||||
for _, v := range w.ids {
|
||||
t := w.tasks[v]
|
||||
if t.parentID == t.ID {
|
||||
if w.skipChildEvents {
|
||||
continue
|
||||
}
|
||||
line := w.lineText(ev, " ", terminalWidth, statusPadding, w.dryRun)
|
||||
line := w.lineText(t, " ", terminalWidth, statusPadding, w.dryRun)
|
||||
_, _ = fmt.Fprint(w.out, line)
|
||||
numLines++
|
||||
}
|
||||
@@ -218,12 +251,12 @@ func (w *ttyWriter) print() { //nolint:gocyclo
|
||||
w.numLines = numLines
|
||||
}
|
||||
|
||||
func (w *ttyWriter) lineText(event Event, pad string, terminalWidth, statusPadding int, dryRun bool) string {
|
||||
func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding int, dryRun bool) string {
|
||||
endTime := time.Now()
|
||||
if event.Status != Working {
|
||||
endTime = event.startTime
|
||||
if (event.endTime != time.Time{}) {
|
||||
endTime = event.endTime
|
||||
if t.status != Working {
|
||||
endTime = t.startTime
|
||||
if (t.endTime != time.Time{}) {
|
||||
endTime = t.endTime
|
||||
}
|
||||
}
|
||||
prefix := ""
|
||||
@@ -231,7 +264,7 @@ func (w *ttyWriter) lineText(event Event, pad string, terminalWidth, statusPaddi
|
||||
prefix = PrefixColor(api.DRYRUN_PREFIX)
|
||||
}
|
||||
|
||||
elapsed := endTime.Sub(event.startTime).Seconds()
|
||||
elapsed := endTime.Sub(t.startTime).Seconds()
|
||||
|
||||
var (
|
||||
hideDetails bool
|
||||
@@ -241,18 +274,18 @@ func (w *ttyWriter) lineText(event Event, pad string, terminalWidth, statusPaddi
|
||||
)
|
||||
|
||||
// only show the aggregated progress while the root operation is in-progress
|
||||
if parent := event; parent.Status == Working {
|
||||
for _, v := range w.eventIDs {
|
||||
child := w.events[v]
|
||||
if child.ParentID == parent.ID {
|
||||
if child.Status == Working && child.Total == 0 {
|
||||
if parent := t; parent.status == Working {
|
||||
for _, v := range w.ids {
|
||||
child := w.tasks[v]
|
||||
if child.parentID == parent.ID {
|
||||
if child.status == Working && child.total == 0 {
|
||||
// we don't have totals available for all the child events
|
||||
// so don't show the total progress yet
|
||||
hideDetails = true
|
||||
}
|
||||
total += child.Total
|
||||
current += child.Current
|
||||
completion = append(completion, percentChars[(len(percentChars)-1)*child.Percent/100])
|
||||
total += child.total
|
||||
current += child.current
|
||||
completion = append(completion, percentChars[(len(percentChars)-1)*child.percent/100])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,13 +302,13 @@ func (w *ttyWriter) lineText(event Event, pad string, terminalWidth, statusPaddi
|
||||
details = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
|
||||
}
|
||||
txt = fmt.Sprintf("%s [%s]%s %s",
|
||||
event.ID,
|
||||
t.ID,
|
||||
SuccessColor(strings.Join(completion, "")),
|
||||
details,
|
||||
event.Text,
|
||||
t.text,
|
||||
)
|
||||
} else {
|
||||
txt = fmt.Sprintf("%s %s", event.ID, event.Text)
|
||||
txt = fmt.Sprintf("%s %s", t.ID, t.text)
|
||||
}
|
||||
textLen := len(txt)
|
||||
padding := statusPadding - textLen
|
||||
@@ -285,18 +318,18 @@ func (w *ttyWriter) lineText(event Event, pad string, terminalWidth, statusPaddi
|
||||
// calculate the max length for the status text, on errors it
|
||||
// is 2-3 lines long and breaks the line formatting
|
||||
maxStatusLen := terminalWidth - textLen - statusPadding - 15
|
||||
status := event.StatusText
|
||||
status := t.statusText
|
||||
// in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
|
||||
if maxStatusLen > 0 && len(status) > maxStatusLen {
|
||||
status = status[:maxStatusLen] + "..."
|
||||
}
|
||||
text := fmt.Sprintf("%s %s%s %s%s %s",
|
||||
pad,
|
||||
event.Spinner(),
|
||||
spinner(t),
|
||||
prefix,
|
||||
txt,
|
||||
strings.Repeat(" ", padding),
|
||||
event.Status.colorFn()(status),
|
||||
colorFn(t.status)(status),
|
||||
)
|
||||
timer := fmt.Sprintf("%.1fs ", elapsed)
|
||||
o := align(text, TimerColor(timer), terminalWidth)
|
||||
@@ -304,10 +337,42 @@ func (w *ttyWriter) lineText(event Event, pad string, terminalWidth, statusPaddi
|
||||
return o
|
||||
}
|
||||
|
||||
func numDone(events map[string]Event) int {
|
||||
var (
|
||||
spinnerDone = "✔"
|
||||
spinnerWarning = "!"
|
||||
spinnerError = "✘"
|
||||
)
|
||||
|
||||
func spinner(t task) string {
|
||||
switch t.status {
|
||||
case Done:
|
||||
return SuccessColor(spinnerDone)
|
||||
case Warning:
|
||||
return WarningColor(spinnerWarning)
|
||||
case Error:
|
||||
return ErrorColor(spinnerError)
|
||||
default:
|
||||
return CountColor(t.spinner.String())
|
||||
}
|
||||
}
|
||||
|
||||
func colorFn(s EventStatus) colorFunc {
|
||||
switch s {
|
||||
case Done:
|
||||
return SuccessColor
|
||||
case Warning:
|
||||
return WarningColor
|
||||
case Error:
|
||||
return ErrorColor
|
||||
default:
|
||||
return nocolor
|
||||
}
|
||||
}
|
||||
|
||||
func numDone(tasks map[string]task) int {
|
||||
i := 0
|
||||
for _, e := range events {
|
||||
if e.Status != Working {
|
||||
for _, t := range tasks {
|
||||
if t.status != Working {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestLineText(t *testing.T) {
|
||||
now := time.Now()
|
||||
ev := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Working,
|
||||
StatusText: "Status",
|
||||
endTime: now,
|
||||
startTime: now,
|
||||
spinner: &Spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
|
||||
lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text))
|
||||
|
||||
out := tty().lineText(ev, "", 50, lineWidth, false)
|
||||
assert.Equal(t, out, " \x1b[33m.\x1b[0m id Text Status \x1b[34m0.0s \x1b[0m\n")
|
||||
|
||||
ev.Status = Done
|
||||
out = tty().lineText(ev, "", 50, lineWidth, false)
|
||||
assert.Equal(t, out, " \x1b[32m✔\x1b[0m id Text \x1b[32mStatus\x1b[0m \x1b[34m0.0s \x1b[0m\n")
|
||||
|
||||
ev.Status = Error
|
||||
out = tty().lineText(ev, "", 50, lineWidth, false)
|
||||
assert.Equal(t, out, " \x1b[31m\x1b[1m✘\x1b[0m id Text \x1b[31m\x1b[1mStatus\x1b[0m \x1b[34m0.0s \x1b[0m\n")
|
||||
|
||||
ev.Status = Warning
|
||||
out = tty().lineText(ev, "", 50, lineWidth, false)
|
||||
assert.Equal(t, out, " \x1b[33m\x1b[1m!\x1b[0m id Text \x1b[33m\x1b[1mStatus\x1b[0m \x1b[34m0.0s \x1b[0m\n")
|
||||
}
|
||||
|
||||
func TestLineTextSingleEvent(t *testing.T) {
|
||||
now := time.Now()
|
||||
ev := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Done,
|
||||
StatusText: "Status",
|
||||
startTime: now,
|
||||
spinner: &Spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
|
||||
lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text))
|
||||
|
||||
out := tty().lineText(ev, "", 50, lineWidth, false)
|
||||
assert.Equal(t, out, " \x1b[32m✔\x1b[0m id Text \x1b[32mStatus\x1b[0m \x1b[34m0.0s \x1b[0m\n")
|
||||
}
|
||||
|
||||
func TestErrorEvent(t *testing.T) {
|
||||
w := &ttyWriter{
|
||||
events: map[string]Event{},
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
e := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Working,
|
||||
StatusText: "Working",
|
||||
startTime: time.Now(),
|
||||
spinner: &Spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
// Fire "Working" event and check end time isn't touched
|
||||
w.Event(e)
|
||||
event, ok := w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.Equal(time.Time{}))
|
||||
|
||||
// Fire "Error" event and check end time is set
|
||||
e.Status = Error
|
||||
w.Event(e)
|
||||
event, ok = w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.After(time.Now().Add(-10*time.Second)))
|
||||
}
|
||||
|
||||
func TestWarningEvent(t *testing.T) {
|
||||
w := &ttyWriter{
|
||||
events: map[string]Event{},
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
e := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Working,
|
||||
StatusText: "Working",
|
||||
startTime: time.Now(),
|
||||
spinner: &Spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
// Fire "Working" event and check end time isn't touched
|
||||
w.Event(e)
|
||||
event, ok := w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.Equal(time.Time{}))
|
||||
|
||||
// Fire "Warning" event and check end time is set
|
||||
e.Status = Warning
|
||||
w.Event(e)
|
||||
event, ok = w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.After(time.Now().Add(-10*time.Second)))
|
||||
}
|
||||
|
||||
func tty() *ttyWriter {
|
||||
tty := &ttyWriter{
|
||||
eventIDs: []string{},
|
||||
events: map[string]Event{},
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
return tty
|
||||
}
|
||||
@@ -162,8 +162,8 @@ func NewWriter(ctx context.Context, out *streams.Out, progressTitle string) (Wri
|
||||
func newTTYWriter(out io.Writer, dryRun bool, progressTitle string) (Writer, error) {
|
||||
return &ttyWriter{
|
||||
out: out,
|
||||
eventIDs: []string{},
|
||||
events: map[string]Event{},
|
||||
ids: []string{},
|
||||
tasks: map[string]task{},
|
||||
repeated: false,
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
|
||||
Reference in New Issue
Block a user