A library does not know if a person, a log or nobody watches it. So it draws nothing. It starts spans in the context it was given, and the command that called it decides what is shown.
Procedure#
- Take a
context.Contextas the first argument of each function that does work worth showing. - Start a span for each step with
progress.Start, and use the context that it returns for the work below. - Announce the targets of a step as queued before the first runs, so that a display knows all of the work from the start.
- Call
progress.Span.Runwhen a target starts, andprogress.Span.Endwith its error when it ends. - End each span that you started, on every path, also on an error.
// reset is library code: it reports its work as spans in ctx, whether a
// command watches or not.
func reset(ctx context.Context, nodes []string) error {
ctx, step := progress.Start(ctx, progress.KindStep, "reset the machines",
progress.Flagged(progress.FlagFold), progress.Total(len(nodes)))
// Every target is announced queued before the first runs, so that a
// display knows the whole of the work from the start.
targets := make([]*progress.Span, len(nodes))
for i, node := range nodes {
_, targets[i] = progress.Start(ctx, progress.KindTarget, node, progress.Queued(), progress.Node(node))
}
var err error
for i, target := range targets {
target.Run()
if nodes[i] == "exe02" {
failed := errors.New("exe02: no answer")
target.End(failed)
err = failed
continue
}
target.End(nil)
}
step.End(err)
return err
}reset in progress/example_test.go, which the examples of the package call.The command attaches a Bus to the context and ends the spans that it started. The library’s spans show up below them:
bus := progress.NewBus(progress.BusOptions{Sinks: []progress.Sink{printer{}}})
ctx, command := progress.Start(progress.WithBus(context.Background(), bus), progress.KindCommand, "power reset")
command.End(reset(ctx, []string{"exe01", "exe02"}))
bus.Close()1 start command "power reset" running
2 start step "reset the machines" running
3 start target "exe01" queued
4 start target "exe02" queued
5 run target "exe01" running
6 end target "exe01" ended ok
7 run target "exe02" running
8 end target "exe02" ended failed (target): exe02: no answer
9 end step "reset the machines" ended failed (target): exe02: no answer
10 end command "power reset" ended failed (target): exe02: no answerprogress/example_test.go. CI runs it and checks its output.Without a Bus, nothing#
When no command attached a Bus, Start returns the context as it is and a
nil span. The methods of a nil span do nothing. The library makes no
allocation on this path, and a test of the kit keeps it so.
ctx := context.Background()
got, span := progress.Start(ctx, progress.KindStep, "reset the machines")
span.Run()
span.End(nil)
fmt.Println(got == ctx, span == nil)true trueprogress/example_test.go. CI runs it and checks its output.A library imports progress, and never display nor cliprogress.
progress has no dependency on a terminal, on cobra or on net/http.
Skip and classify#
- A target that is left out on purpose ends with
progress.Skipor an error ofprogress.ErrSkipped. It is not counted as failed. - The class of an error says what kind of failure it is: of the target, of
the transport, a timeout, a refused login, a usage error, a cancel.
progress.Classifyfinds it, and a program adds a rule of its own for errors that say none.
byCode := func(err error) progress.Class {
if exit := (exitError{}); errors.As(err, &exit) && exit.code == 3 {
return progress.ClassTransport
}
return progress.ClassTarget
}
for _, err := range []error{
nil,
context.Canceled,
fmt.Errorf("exe01: %w", context.DeadlineExceeded),
exitError{3},
exitError{1},
os.ErrPermission,
} {
fmt.Println(progress.Classify(err, byCode))
}none
canceled
timeout
transport
target
targetprogress/example_test.go. CI runs it and checks its output.