Errors-and-Testing

Errors & Testing

Audience: everyone. How errors travel through a pipeline, and how to test commands without real I/O.


How errors flow

A gloo error doesn’t unwind a call stack — it travels in the stream as an item. Each item a stage produces is either a value or an error. The first error a consumer encounters stops the pipeline and is returned:

result, err := gloo.Chain(src).To(cmd).Collect()
if err != nil {
    // the first error any stage produced; the upstream was already torn down
}

When a consumer hits an error it Discards the rest of the stream (stop + drain), so the producers behind the error can’t leak — see Concurrency & Lifecycle. As an author, returning an error from your pattern function is all it takes to propagate one:

patterns.Map(func(line string) (string, error) {
    n, err := strconv.Atoi(line)
    if err != nil {
        return "", err   // travels downstream; stops the pipeline
    }
    return strconv.Itoa(n * 2), nil
})

Sentinel errors

The framework’s errors are sentinels you match structurally with errors.Is, never by string. The package defines a single error type:

type Error string
func (e Error) Error() string { return string(e) }

…and declares each error it can emit as a const of that type. Wrap a cause with .With:

return ErrFileNotFound.With(err, "file", name) // "file not found: <cause>: file <name>"

errors.Is matches both the sentinel and the wrapped cause:

if errors.Is(err, gloo.ErrFileNotFound) {  }

The errors gloo can emit

SentinelWhen
ErrStopReadingthe silent downstream-stop cause (you rarely match this)
ErrFileNotFounda File positional couldn’t be opened
patterns.ErrSubprocessStart, ErrSubprocessReadStdout, ErrSubprocessStdinPipe, ErrSubprocessStdoutPipe, ErrSubprocessa Subprocess failed at the named stage

Fluent builder errors

The reflection-based fluent builder (Chain/Run) reports a malformed pipeline as a returned error — never a panic — surfaced by the terminal:

SentinelWhen
ErrNotSourcethe Chain argument isn’t a Source
ErrNotCommanda .To() argument isn’t a Command
ErrNotSinka .Sink() argument isn’t a Sink
ErrStageTypeMismatcha stage’s input type doesn’t match the previous output
ErrSinkTypeMismatchthe sink’s input type doesn’t match the pipeline
ErrNotForEachFunca .ForEach() argument isn’t func(T) error
ErrPipelineConsumedthe builder was already consumed by a terminal
_, err := gloo.Chain(src).To(wrongTypedCmd).Collect()
if errors.Is(err, gloo.ErrStageTypeMismatch) {  } // and the message names both types

Prefer Pipe/Compose when you want these caught at compile time instead — see the two execution models.


Testing commands

Commands are designed to be tested with in-memory streams — no filesystem, no stdin/stdout, no subprocesses (except when testing Subprocess itself).

The basic shape

Feed a synthetic stream, run the command, collect the result, assert exact values:

func TestShout(t *testing.T) {
    shout := patterns.Map(func(s string) (string, error) {
        return strings.ToUpper(s), nil
    })

    got, err := shout.Execute(context.Background(), gloo.StreamOf("a", "b")).Collect()
    if err != nil {
        t.Fatal(err)
    }
    if len(got) != 2 || got[0] != "A" || got[1] != "B" {
        t.Errorf("got %v, want [A B]", got)
    }
}
NeedUse
synthetic inputgloo.StreamOf(items…) or gloo.SliceSource(items)
byte-pipeline inputgloo.ByteReaderSource([]io.Reader{r})
a fake filesystemafero.NewMemMapFs() — never the real disk
the result.Collect() (slice) or a sink
a godoc examplepatterns.MustRun(cmd) with an // Output: matcher

Assert behavior, not coverage

The framework holds itself to 100% statement coverage — but coverage is the floor, not the goal. A test should fail if the behavior is wrong, not merely execute the code. Concretely:

Worked references


That completes the wiki. Back to Home, or jump to the API reference.