progresstest

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package progresstest captures the progress events of a test, draws them as a tree that does not depend on the order concurrent work happened in, and checks that they keep the promises the progress package makes to every sink.

Watch gives a test a context with a Bus of its own, and a Watcher that keeps one rule: Check runs on the events as the work left them, before the Bus is closed, since Close would end a span the work left open as canceled and hide that it never ended. It runs when the test calls Finish for the tree, or else when the test ends.

What Check holds an emitter to

A package that reports progress, a library's or a program's, keeps the promises Check lists. Three of them shape how work that runs in parallel is reported, and are the ones a pool written by hand most often breaks:

  • Every target of a Fold step or a batch, and every batch of a step, is announced queued, with progress.Queued, before the first of them runs. A pool that starts each target from its own goroutine as it takes its place, as errgroup.Go does, breaks this: start them all first, then run them; the pools of the fanout package do.
  • When a Fold step or a batch ends, the targets under it that ended are its Total, however they ended, so a counter always reaches its total; no count passes its Total, and a Total never shrinks.
  • No more targets run at once below a span than its Limit.

The work of a span is held to rules too: an advance names a span that is running and changes its Amount, two advances of a span come at least 100 ms apart by their times, Amount and Size are never negative, a span in progress.Percent has a Size of 100, and the Unit of a span never changes once given. Capture keeps every advance the Bus sends, and Tree draws the work of a span after its other fields.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Check

func Check(t testing.TB, events []progress.Event)

Check reports, as errors of t, every promise the events break. Call it once the work is done, before the Bus is closed, so that a span the work left open is reported rather than ended by Close:

  1. Seq runs from 1 with no gaps. Every span starts once and ends once, and no span id is zero or used twice.
  2. A span starts under a parent that has started and not ended, and ends after every span under it. Its Run, Update and Line events come between its Start and its End, and a Run only follows a queued Start.
  3. The work a step knows of is announced up front. The targets of a Fold step or a batch, and the batches of a step, all start queued before the first of them runs. When a Fold step or a batch ends, its targets that ended, however they ended, are its Total; a batch or a Fold step that was skipped or canceled before any target of its own started counts as its Total. No count ever exceeds its Total, and no Total shrinks, so a counter never goes backwards and always reaches its total, even after an interrupt.
  4. No more targets run at once below a span than its Limit.
  5. Every Suspend is followed by one Resume.
  6. No text holds anything termtext.Escape would escape, and none is longer than its bound. Hidden and ShowLines are passed down.
  7. The work of a span is sound. An advance names a span that is running and changes its Amount; two advances of a span come at least 100 ms apart by their times; Amount and Size are never negative; a span in Percent has a Size of 100; and the Unit of a span never changes once given.

Each rule is a promise the progress package makes to every sink. A new or stricter rule is a breaking change, named in the release notes; a rule that catches what an existing promise already forbade is a fix (decision 15). No option selects which rules run.

Types

type Capture

type Capture struct {
	// Lines makes the capture ask for lines of output, as a live display
	// does, so that progress.Tee produces them. It is read once, when the
	// Bus is made (see progress.LineSink); set it before progress.NewBus.
	Lines bool
	// contains filtered or unexported fields
}

Capture is a sink that keeps every event it is sent. It is safe for concurrent use. To check the events, use Watch, or call Check before the Bus is closed.

Example

A Capture keeps the events of a Bus; its Tree draws them, targets that read the same folded into one line. Check runs before the Bus is closed, which would end a span the work left open and hide it.

package main

import (
	"context"
	"errors"
	"fmt"
	"testing"

	"github.com/GSI-HPC/go-clikit/progress"
	"github.com/GSI-HPC/go-clikit/progress/progresstest"
)

// reset is the library code under test: it reports a step with a target
// for each node.
func reset(ctx context.Context, nodes []string) error {
	ctx, step := progress.Start(ctx, progress.KindStep, "reset the machines",
		progress.WithFlags(progress.Fold), progress.Total(len(nodes)))
	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] == "exe3" {
			err = errors.New("exe3: no answer")
			target.End(err)
			continue
		}
		target.End(nil)
	}
	step.End(err)
	return err
}

func main() {
	capture := &progresstest.Capture{}
	bus := progress.NewBus(progress.BusOptions{Sinks: []progress.Sink{capture}})
	_ = reset(progress.WithBus(context.Background(), bus), []string{"exe1", "exe2", "exe3", "exe10"})
	// In a test, check the events before the Bus is closed.
	_ = func(t *testing.T) {
		progresstest.Check(t, capture.Events())
	}
	bus.Close()
	fmt.Print(capture.Tree())
}
Output:
step reset the machines total=4 [fold]: failed (target): exe3: no answer
  target exe3: failed (target): {}: no answer
  target exe[1-2,10]: ok

func (*Capture) Events

func (c *Capture) Events() []progress.Event

Events returns a copy of the events kept so far.

func (*Capture) Handle

func (c *Capture) Handle(e progress.Event)

Handle keeps e.

func (*Capture) Tree

func (c *Capture) Tree() string

Tree draws the spans of the events kept so far, one line each, indented under their parent:

step reset the machines total=3 limit=8 [fold]: failed (target): 1 of 3 failed
  target exe[1-2]: ok
    call redfish method=POST path=/redfish/v1/Systems/1: ok
  target exe3: failed (transport): {}: no answer

Nothing in it depends on how concurrent work was scheduled. Targets that read the same once their own node and host are written {}, where they stand as names of their own rather than inside a word, are folded into one line naming them as a node set, or listing them when a name is no node or a node is named twice, alone or in another name's node set, as it is for a target that ran twice; siblings are sorted by what they read, with numbers in their numeric order and those that differ only in leading zeros, as x01 and x1, byte by byte; and neither ids, times nor lines of output are drawn. A span not ended reads as its state.

Tests compare trees whole, so the format is kept: a field is drawn only when it has a value, and a field added to progress.Fields is drawn after those drawn before it. The work of a span comes after its message, as amount=2147483648 size=2147483648 unit=bytes. Changing how an existing field is drawn changes what every such test compares.

func (*Capture) WantsLines

func (c *Capture) WantsLines() bool

WantsLines reports c.Lines.

type Option

type Option func(*config)

An Option sets up the Bus that Watch makes.

func Classify

func Classify(fallback func(error) progress.Class) Option

Classify has the Bus class an error that says no class of its own by fallback, as progress.BusOptions.Classify does, so that the events carry the classes the program's own Bus would give them.

func Sinks

func Sinks(s ...progress.Sink) Option

Sinks adds sinks to the Bus, such as a display drawing on a progresstest.Screen, which receive every event before the Capture does. Each use adds to the sinks of the ones before it; the Capture is always on the Bus, after all of them.

type Screen

type Screen struct {
	// Width is the number of columns, at which a row wraps; 0 is a row
	// that never does.
	Width int
	// Styles, when set, applies the sequences that set the attributes of
	// text (SGR, ESC [ … m) as a terminal does: those of bold (1), faint
	// (2), underline (4) and reverse (7), their resets (22, 24, 27), the
	// colours 30 to 37, 90 to 97 and 38;5;n, the colour's reset (39), and
	// 0 or none, which sets every attribute back. They take no column, so
	// that a row wraps where a terminal's would, and String shows the text
	// alone. A sequence with any other parameter, such as italic, blink, a
	// background or a colour of 24 bits, is shown as text, as without
	// Styles, so that a test sees it.
	Styles bool
	// contains filtered or unexported fields
}

Screen is a terminal for a test to draw on and read back: it shows what is written to it the way a terminal would, as far as a display writes it. It knows text, a newline, which starts the next row at its first column as a terminal's output processing has it do, a carriage return, and the sequences ESC [ n A, which moves the cursor up n rows, ESC [ 2K, which erases the row the cursor is on, ESC [ K, which erases the rest of it, and ESC [ J, which erases from the cursor to the end of the screen. Any other control character or sequence is shown as text, "^[[?25l" or "^G", so that a test sees it. So is an escape or a sequence that a byte it cannot hold, such as a newline or another escape, cuts short, up to that byte, which then takes effect, and an escape, a sequence or a rune that the output ends in the middle of.

Every row is kept, those that have scrolled off the top too, so that what a Screen shows is the scrollback and the screen in one. With Width set, a row wraps at that column, as a terminal's does, so that a row drawn too long shows as the two it is. As on a terminal, the wrap waits for the next rune: a row written to its last column leaves the cursor on that column, where ESC [ K erases the last rune and a carriage return or a newline wraps nothing. A wide rune, as CJK text and emoji are, takes two columns; writing over either half of one blanks the other. A Screen is safe for concurrent use.

With Styles set, a Screen applies the sequences that set the attributes of text, as a display drawn in a theme writes them, and Styled shows which attributes each run of text was written in.

Example

A Screen shows what a display wrote the way a terminal would: here a line drawn, erased and drawn again.

package main

import (
	"fmt"

	"github.com/GSI-HPC/go-clikit/progress/progresstest"
)

func main() {
	screen := &progresstest.Screen{}
	fmt.Fprint(screen, "uptime · 1/3 · 0:01")
	fmt.Fprint(screen, "\r\x1b[2Kuptime · 3/3 · 0:02\n")
	fmt.Fprint(screen, "\x1b[?25l")
	fmt.Println(screen.String())
}
Output:
uptime · 3/3 · 0:02
^[[?25l

func (*Screen) String

func (s *Screen) String() string

String returns what the screen shows, each row ended by a newline, up to the last row that is not blank. An escape, a sequence or a rune that the output so far ends in the middle of is shown as text, as if nothing more came; the next Write still completes it.

func (*Screen) Styled added in v0.3.0

func (s *Screen) Styled() string

Styled returns what String returns, with «p» before each run of text written in the attributes p, and «» where they return to none or a row ends in them: «1;31»✗«» exe0007. p names the attributes in one order, bold (1), faint (2), underline (4) and reverse (7), then the colour, as 31, 91 or 38;5;n, whatever order and however many sequences set them. Without Styles it returns what String returns.

Styled shows the attributes of the text on the screen, not the sequences that set them: a row that ends in a colour reads the same whether the output set it back after the row or not. A colour left on shows only in the text written after it, which takes it; a test that checks that none is left on writes text after each row, or after the output, and looks at that.

Example

With Styles, a Screen applies the colours a display drawn in a theme sets, which take no column, and Styled shows which colours each run of text was written in, here a failure in bold red and a separator in grey.

package main

import (
	"fmt"

	"github.com/GSI-HPC/go-clikit/progress/progresstest"
)

func main() {
	screen := &progresstest.Screen{Styles: true}
	fmt.Fprint(screen, "\x1b[1;31m✗\x1b[0m exe0007\x1b[38;5;244m · \x1b[0mtransport\n")
	fmt.Print(screen.String())
	fmt.Print(screen.Styled())
}
Output:
✗ exe0007 · transport
«1;31»✗«» exe0007«38;5;244» · «»transport

func (*Screen) Write

func (s *Screen) Write(p []byte) (int, error)

Write shows p on the screen.

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

A Watcher checks the events of the Bus that Watch made. It is safe for concurrent use.

func Watch

func Watch(ctx context.Context, t testing.TB, opts ...Option) (context.Context, *Watcher)

Watch returns ctx with a Bus of its own, whose events a Capture keeps, and the Watcher that checks them. The Capture asks for the lines of output, as a live display does, so that they are checked too. If the test does not call Finish, Watch calls it when the test ends, so the events are checked and the Bus is closed either way.

Example

A test watches the work it runs: Finish checks every promise the events make, and the tree they draw reads the same however the work was scheduled. A test that never calls Finish is checked when it ends.

package main

import (
	"context"
	"errors"
	"testing"

	"github.com/GSI-HPC/go-clikit/progress"
	"github.com/GSI-HPC/go-clikit/progress/progresstest"
)

// reset is the library code under test: it reports a step with a target
// for each node.
func reset(ctx context.Context, nodes []string) error {
	ctx, step := progress.Start(ctx, progress.KindStep, "reset the machines",
		progress.WithFlags(progress.Fold), progress.Total(len(nodes)))
	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] == "exe3" {
			err = errors.New("exe3: no answer")
			target.End(err)
			continue
		}
		target.End(nil)
	}
	step.End(err)
	return err
}

func main() {
	_ = func(t *testing.T) {
		ctx, w := progresstest.Watch(t.Context(), t)
		_ = reset(ctx, []string{"exe1", "exe2", "exe3"})
		want := `step reset the machines total=3 [fold]: failed (target): exe3: no answer
  target exe3: failed (target): {}: no answer
  target exe[1-2]: ok
`
		if got := w.Finish(); got != want {
			t.Errorf("the work reported\n%s\nwant\n%s", got, want)
		}
	}
}

func (*Watcher) Events

func (w *Watcher) Events() []progress.Event

Events returns a copy of the events so far, and checks nothing.

func (*Watcher) Finish

func (w *Watcher) Finish() string

Finish is called once the work is done: it runs Check on the events, then closes the Bus, and returns the Tree of the events. Called again, it checks nothing more and returns the Tree.

func (*Watcher) Tree

func (w *Watcher) Tree() string

Tree draws the events so far, as Capture.Tree does, and checks nothing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL