process

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package process runs external programs.

factory := process.NewFactory()
result, err := factory.Run(ctx, []string{"go", "build", "./..."}, nil)
if result.Failed() {
	log.Print(result.ErrorOutput())
}

A failed exit is a result, not an error

This is the one thing to know before writing anything else with it. A program that exits non-zero comes back with a nil error and Failed reporting it:

result, err := factory.Run(ctx, []string{"git", "diff", "--quiet"}, nil)
// err is nil. A difference is exit code 1, and it is not a failure.
if result.ExitCode() == 1 {
	// there are changes
}

The error is for the program that never ran, ran out of time, or was stray. Throw is what turns a failed exit into one, when the caller wants that:

result, err := factory.Run(ctx, []string{"go", "test", "./..."}, nil)
if _, err = result.Throw(nil); err != nil {
	return err // a *ProcessFailedException, carrying both streams
}

What os/exec does not do

  • An error that repeats what the program said. os/exec reports a failed command as "exit status 1" and throws the explanation away, so a build that stopped because a module could not be fetched answers with the exit status of a program the person did not know was running.
  • A deadline that also covers the program that never finishes and never prints. A context deadline bounds the total; IdleTimeout bounds the silence, which is the shape a hung download or a stalled compiler has.
  • A fake. Factory.Fake answers a command pattern with a canned result, so a test that shells out stops shelling out, and PreventStrayProcesses turns "somebody forgot to fake git" from a test that quietly runs git on CI into a test that fails naming the command.

No shell, and no string form to add

A command is a program and its arguments, and nothing here hands a string to sh. Accepting a string would mean exec.Command("sh", "-c", line) with an interpolated line, which is command injection with a familiar signature.

Several at once, and one after another

Pool, InvokedProcessPool, ProcessPoolResults and Pipe run a set of programs together, and three methods of the factory reach them:

results, err := factory.Concurrently(ctx, func(pool *process.Pool) {
	pool.As("build").Command("go", "build", "./...")
	pool.Command("go", "vet", "./...")
}, nil)

result, err := factory.Pipe(ctx, func(pipe *process.Pipe) {
	pipe.Command("cat", "notes.txt")
	pipe.Command("grep", "-i", "todo")
}, nil)

Concurrently starts them all and waits; Factory.Pool is the same set started and not waited for, which is what Signal, Running and Wait on the InvokedProcessPool are for. A pipe feeds each process the output of the one before it and stops at the first that exits non-zero, whose result is what comes back.

The key is the string a process is added under -- As names one, and a process added without a name takes the next integer key, counted only among the unnamed. It is what the pool's output handler is given alongside the chunk, so output from four programs at once can be told apart.

The output is held in memory, unbounded

A program that prints without stopping is held in full until it exits. The caller that expects one bounds it, by streaming through the output handler rather than reading the result.

Index

Constants

This section is empty.

Variables

View Source
var ErrEmptyProcessSequence = errors.New("A process was invoked, but the process result sequence is empty.")

ErrEmptyProcessSequence is a FakeProcessSequence that was asked for one more result than it was given.

Call FakeProcessSequence.DontFailWhenEmpty or WhenEmpty to make the sequence answer instead of running out.

View Source
var ErrUnsupportedFakeResult = errors.New("unsupported process fake result provided")

ErrUnsupportedFakeResult is a fake handler that returned something no fake can be built from.

What a handler may return is listed on Factory.Fake.

Functions

This section is empty.

Types

type Factory

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

Factory makes processes.

It is the entry point of the component: Factory.Run for a command that runs, Factory.Fake for a test that must not shell out, and Factory.Pool for a set that runs at once.

It is safe for concurrent use. Here a pool is goroutines, and the recording they all write to is behind a mutex.

func NewFactory

func NewFactory() *Factory

NewFactory constructs a Factory.

func (*Factory) AllowStrayProcesses

func (f *Factory) AllowStrayProcesses() *Factory

AllowStrayProcesses is the inverse of PreventStrayProcesses.

func (*Factory) AssertDidntRun

func (f *Factory) AssertDidntRun(t *testing.T, pattern string)

AssertDidntRun is AssertNotRan under its other name.

func (*Factory) AssertNotRan

func (f *Factory) AssertNotRan(t *testing.T, pattern string)

AssertNotRan fails the test when a recorded command matched the pattern.

func (*Factory) AssertNothingRan

func (f *Factory) AssertNothingRan(t *testing.T)

AssertNothingRan fails the test when any process ran at all.

func (*Factory) AssertRan

func (f *Factory) AssertRan(t *testing.T, pattern string)

AssertRan fails the test when no recorded command matched the pattern.

func (*Factory) AssertRanTimes

func (f *Factory) AssertRanTimes(t *testing.T, pattern string, times int)

AssertRanTimes fails the test unless exactly times recorded commands matched the pattern.

func (*Factory) Concurrently

func (f *Factory) Concurrently(ctx context.Context, callback func(*Pool), output PoolOutputHandler) (*ProcessPoolResults, error)

Concurrently runs a pool of processes and waits for them to finish.

func (*Factory) Describe

func (f *Factory) Describe() *FakeProcessDescription

Describe builds a fake description, for a process that is started rather than run and whose output arrives over time.

func (*Factory) Fake

func (f *Factory) Fake(handlers ...FakeHandler) *Factory

Fake registers fake answers and puts the factory in recording mode.

It puts the factory in recording mode, so every process made from it answers from the handlers instead of running. Calling it with no handlers fakes everything with an empty successful result.

Handlers are matched in order and the first match wins, so a "*" registered first answers everything after it.

func (*Factory) IsRecording

func (f *Factory) IsRecording() bool

IsRecording reports whether Fake has been called.

func (*Factory) NewPendingProcess

func (f *Factory) NewPendingProcess() *PendingProcess

NewPendingProcess builds a process bound to this factory, carrying the fake handlers registered so far.

func (*Factory) Pipe

func (f *Factory) Pipe(ctx context.Context, callback func(*Pipe), output PoolOutputHandler) (ProcessResult, error)

Pipe runs processes one after another, each one reading what the one before it wrote.

This takes the callback, which is the form the array form is written in: a caller with a list of commands ranges over it inside the callback.

func (*Factory) Pool

func (f *Factory) Pool(callback func(*Pool)) *Pool

Pool describes a set of processes that run at the same time.

It builds the pool and runs nothing. Pool.Start starts the processes, Pool.Run and Pool.Wait start them and wait, and Concurrently is the one line that does both.

func (*Factory) PreventStrayProcesses

func (f *Factory) PreventStrayProcesses(prevent ...bool) *Factory

PreventStrayProcesses makes a command no handler matched an error.

With it on, a command that no handler matched is an error instead of running for real. It is the guard that turns "somebody forgot to fake git" from a test that quietly shells out on CI into a test that fails naming the command.

func (*Factory) PreventingStrayProcesses

func (f *Factory) PreventingStrayProcesses() bool

PreventingStrayProcesses reports whether unmatched commands are refused.

func (*Factory) Record

func (f *Factory) Record(process *PendingProcess, result ProcessResult) *Factory

Record adds a process and the result it produced to the recording.

func (*Factory) RecordIfRecording

func (f *Factory) RecordIfRecording(process *PendingProcess, result ProcessResult) *Factory

RecordIfRecording records the process and its result when recording is on.

func (*Factory) Recorded

func (f *Factory) Recorded() []string

Recorded is the command line of every process that ran, in order.

The command lines are what an assertion failure has to print, so this returns them rather than the pending processes, and the assertions below use it for their message.

The line is PendingProcess.String, which is what a fake pattern is matched against -- for the reason matchesCommand is one function: rendering it any other way here would be a command a fake answered and an assertion then said never ran.

func (*Factory) Result

func (f *Factory) Result(output any, errorOutput any, exitCode int) *FakeProcessResult

Result builds a fake result to register with Fake.

func (*Factory) Run

func (f *Factory) Run(ctx context.Context, command []string, output OutputHandler) (ProcessResult, error)

Run builds a pending process and runs the command to completion.

func (*Factory) Sequence

func (f *Factory) Sequence(processes ...any) *FakeProcessSequence

Sequence builds a series of results, one per call, for a command that is run more than once and answers differently each time.

func (*Factory) Start

func (f *Factory) Start(ctx context.Context, command []string, output OutputHandler) (InvokedProcess, error)

Start builds a pending process and starts the command without waiting.

type FakeHandler

type FakeHandler struct {
	// Command is the pattern. Empty and "*" both match anything.
	Command string

	// Result is what the command answers with: a string, a []string, a
	// *FakeProcessResult, a *FakeProcessDescription, a *FakeProcessSequence, or
	// a func(*PendingProcess) any returning one of those.
	Result any
}

FakeHandler is one command pattern and the answer Fake gives for it.

Go has no untyped map that carries a pattern and a heterogeneous value with the order intact -- and order is load-bearing here, because fakeFor takes the first pattern that matches -- so it is a slice of this.

type FakeInvokedProcess

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

FakeInvokedProcess is a started process that was never started.

It replays a FakeProcessDescription: every question asked of it -- ID, Running, Signal -- hands the output handler one more line, so a test's watching loop sees output arrive the way it would from a program.

That is the part worth knowing before writing a test against it: the output does not arrive on its own, because nothing is running. It arrives because the test asked something.

func NewFakeInvokedProcess

func NewFakeInvokedProcess(command string, process *FakeProcessDescription) *FakeInvokedProcess

NewFakeInvokedProcess builds a fake: the command line the fake stands for, and the description it replays.

func (*FakeInvokedProcess) ErrorOutput

func (p *FakeInvokedProcess) ErrorOutput() string

ErrorOutput is the error output the fake has produced so far.

func (*FakeInvokedProcess) HasReceivedSignal

func (p *FakeInvokedProcess) HasReceivedSignal(signal os.Signal) bool

HasReceivedSignal reports whether Signal was called with the given signal.

func (*FakeInvokedProcess) ID

func (p *FakeInvokedProcess) ID() int

ID is the process id the description was given.

Asking hands the output handler one more line.

func (*FakeInvokedProcess) LatestErrorOutput

func (p *FakeInvokedProcess) LatestErrorOutput() string

LatestErrorOutput is the next line of error output.

func (*FakeInvokedProcess) LatestOutput

func (p *FakeInvokedProcess) LatestOutput() string

LatestOutput is the next line of standard output, and the empty string once there are none left.

One line, not everything since the last call: the cursor moves by one matching entry, and by every non-matching one it stepped over on the way.

func (*FakeInvokedProcess) Output

func (p *FakeInvokedProcess) Output() string

Output is the standard output the fake has produced so far.

So far, and not in total: only lines the cursor has reached count, so a test that asks before the fake has been polled sees less than the description holds. Asking advances the cursor by one line first.

func (*FakeInvokedProcess) PredictProcessResult

func (p *FakeInvokedProcess) PredictProcessResult() ProcessResult

PredictProcessResult is the result this fake will end with, asked before it ends.

func (*FakeInvokedProcess) Running

func (p *FakeInvokedProcess) Running() bool

Running reports whether the fake still counts as running.

It answers true as many times as the description's RunsFor, then false forever -- and on the answer that turns false it flushes every remaining line to the output handler, so a `for Running()` loop ends having seen all of it.

func (*FakeInvokedProcess) Signal

func (p *FakeInvokedProcess) Signal(signal os.Signal) error

Signal records a signal against the fake.

Nothing is killed, because nothing is running; HasReceivedSignal is how a test asks whether the code under test sent it. The error is always nil, and exists because the contract's real implementation has one.

func (*FakeInvokedProcess) Stop

func (p *FakeInvokedProcess) Stop(timeout time.Duration, signal os.Signal) error

Stop records a stop against the fake and finishes it.

func (*FakeInvokedProcess) Wait

Wait finishes the fake and returns the result the description ends with.

A handler passed here replaces the one Start was given. With any handler at all, every line still unread is flushed to it before the result comes back -- which is what makes a fake started process end up having printed everything, whether the test polled it or not.

func (*FakeInvokedProcess) WithOutputHandler

func (p *FakeInvokedProcess) WithOutputHandler(output OutputHandler) *FakeInvokedProcess

WithOutputHandler sets the handler every question will feed.

type FakeProcessDescription

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

FakeProcessDescription is a fake process written out as a lifecycle rather than as a result: what it prints, in what order, on which stream, how long it stays running, and what it exits with.

It is what a test reaches for when the code under test starts a process and watches it -- a result alone cannot be watched, because it has already happened.

Go cannot have both, so the methods win and the state is unexported.

func NewFakeProcessDescription

func NewFakeProcessDescription() *FakeProcessDescription

NewFakeProcessDescription builds an empty description, which takes nothing but does not start blank: the process id is 1000, the way it is there.

func (*FakeProcessDescription) ErrorOutput

func (d *FakeProcessDescription) ErrorOutput(output any) *FakeProcessDescription

ErrorOutput describes a line of error output.

Order is kept across both streams: a description that writes a line out, then a line err, then a line out replays them in that order, which is what makes an interleaved output handler testable.

func (*FakeProcessDescription) ExitCode

func (d *FakeProcessDescription) ExitCode(exitCode int) *FakeProcessDescription

ExitCode sets the status the fake will exit with.

func (*FakeProcessDescription) ID

ID sets the process id the fake will report.

func (*FakeProcessDescription) Iterations

func (d *FakeProcessDescription) Iterations(iterations int) *FakeProcessDescription

Iterations sets how many times Running answers true before the fake is finished.

func (*FakeProcessDescription) Output

Output describes a line of standard output.

Each line is stored with exactly one trailing newline, however many it was written with -- a described line is a line, and a test that wrote one without a newline should not get output that runs into the next.

func (*FakeProcessDescription) ReplaceErrorOutput

func (d *FakeProcessDescription) ReplaceErrorOutput(output string) *FakeProcessDescription

ReplaceErrorOutput drops everything described on standard error and puts the given string there instead.

func (*FakeProcessDescription) ReplaceOutput

func (d *FakeProcessDescription) ReplaceOutput(output string) *FakeProcessDescription

ReplaceOutput drops everything described on standard output and puts the given string there instead.

The replacement goes on the end of the description, not where the dropped lines were.

func (*FakeProcessDescription) RunsFor

func (d *FakeProcessDescription) RunsFor(iterations int) *FakeProcessDescription

RunsFor sets how many times Running answers true before the fake is finished.

It is a count of questions, not a length of time: a fake never really runs, so the only thing a test can control is how long the loop that watches it goes round.

func (*FakeProcessDescription) ToProcessResult

func (d *FakeProcessDescription) ToProcessResult(command string) ProcessResult

ToProcessResult is the whole description collapsed into the result it ends with.

type FakeProcessResult

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

FakeProcessResult is a ProcessResult that no program produced.

A fake handler may return one, and a test may build one directly.

It is a separate type from the real result, and not a convenience over it. See normalizeOutput.

func NewFakeProcessResult

func NewFakeProcessResult(command string, exitCode int, output, errorOutput any) *FakeProcessResult

NewFakeProcessResult builds a fake result.

Anything else is treated as empty.

func (*FakeProcessResult) Command

func (r *FakeProcessResult) Command() string

Command is the command line this result claims to come from.

func (*FakeProcessResult) ErrorOutput

func (r *FakeProcessResult) ErrorOutput() string

ErrorOutput is the standard error this result claims.

func (*FakeProcessResult) ExitCode

func (r *FakeProcessResult) ExitCode() int

ExitCode is the status this result claims.

func (*FakeProcessResult) Failed

func (r *FakeProcessResult) Failed() bool

Failed is the other half of Successful.

func (*FakeProcessResult) Output

func (r *FakeProcessResult) Output() string

Output is the standard output this result claims.

func (*FakeProcessResult) SeeInErrorOutput

func (r *FakeProcessResult) SeeInErrorOutput(output string) bool

SeeInErrorOutput reports whether ErrorOutput contains the given string.

func (*FakeProcessResult) SeeInOutput

func (r *FakeProcessResult) SeeInOutput(output string) bool

SeeInOutput reports whether Output contains the given string.

func (*FakeProcessResult) Successful

func (r *FakeProcessResult) Successful() bool

Successful reports an exit code of zero.

func (*FakeProcessResult) Throw

Throw reports a failed result as an error.

func (*FakeProcessResult) ThrowIf

func (r *FakeProcessResult) ThrowIf(condition bool, callback func(ProcessResult, *ProcessFailedException)) (ProcessResult, error)

ThrowIf is Throw when the condition holds.

func (*FakeProcessResult) WithCommand

func (r *FakeProcessResult) WithCommand(command string) *FakeProcessResult

WithCommand is a copy of this result attached to the given command line.

type FakeProcessSequence

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

FakeProcessSequence is a queue of fake results handed out one per run.

Factory.Sequence is where one comes from. It is the piece that lets a test say what happens the second time a command runs:

factory.Fake(process.FakeHandler{Command: "git *", Handler: factory.Sequence(
	process.NewFakeProcessResult("", 1, "", "not a repository"),
	"ok",
)})

Run it out and the next command is an error, because a sequence that quietly repeats its last answer is a test that passes for a reason nobody wrote down. WhenEmpty and DontFailWhenEmpty are how a test asks for the other behaviour.

It is safe to hand one sequence to concurrently running processes, because a pool is goroutines and the queue is taken from under a lock.

func NewFakeProcessSequence

func NewFakeProcessSequence(processes ...any) *FakeProcessSequence

NewFakeProcessSequence builds a sequence of the given results.

Each entry is a string, a []string, a ProcessResult or a *FakeProcessDescription.

func (*FakeProcessSequence) DontFailWhenEmpty

func (s *FakeProcessSequence) DontFailWhenEmpty() *FakeProcessSequence

DontFailWhenEmpty makes a run-out sequence answer with an empty successful result.

func (*FakeProcessSequence) IsEmpty

func (s *FakeProcessSequence) IsEmpty() bool

IsEmpty reports that the sequence has handed out everything it was given.

func (*FakeProcessSequence) Push

func (s *FakeProcessSequence) Push(process any) *FakeProcessSequence

Push adds one more result to the end of the sequence.

func (*FakeProcessSequence) WhenEmpty

func (s *FakeProcessSequence) WhenEmpty(process any) *FakeProcessSequence

WhenEmpty makes a run-out sequence answer with the given result instead of failing.

type InvokedProcess

type InvokedProcess interface {
	// ID is the process id the operating system gave the program.
	ID() int
	// Signal sends a signal to the program.
	Signal(signal os.Signal) error
	// Stop asks the program to end, and kills it when it does not.
	Stop(timeout time.Duration, signal os.Signal) error
	// Running reports whether the program is still going.
	Running() bool
	// Output is everything the program has written on standard output so far.
	Output() string
	// ErrorOutput is everything written on standard error so far.
	ErrorOutput() string
	// LatestOutput is everything written on standard output since the last
	// time this was asked.
	LatestOutput() string
	// LatestErrorOutput is the same for standard error.
	LatestErrorOutput() string
	// Wait waits for the program to finish. The handler takes over from the
	// one Start was given; pass nil to keep it.
	Wait(output OutputHandler) (ProcessResult, error)
}

InvokedProcess is a command that was started and has not finished being dealt with.

It is an interface because Start hands back a real process or a faked one and the caller is not supposed to be able to tell.

invoked, err := factory.Start(ctx, []string{"go", "build", "./..."}, nil)
for invoked.Running() {
	// something else
}
result, err := invoked.Wait(nil)

type InvokedProcessPool

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

InvokedProcessPool is the processes of a pool, started and still running.

func (*InvokedProcessPool) Count

func (p *InvokedProcessPool) Count() int

Count answers how many processes the pool holds.

func (*InvokedProcessPool) Running

Running answers the processes of the pool that are still going.

func (*InvokedProcessPool) Signal

Signal sends a signal to every process still running, and answers with the ones it signalled.

func (*InvokedProcessPool) Wait

Wait waits for every process of the pool.

Every process is waited for even after one of them fails to be waited for, because the alternative is a pool that leaves children running and a caller that has no handle left to stop them. The first error is what comes back, and the results of the ones that did finish come back with it.

type OutputHandler

type OutputHandler func(stream Stream, buffer string)

OutputHandler is called with output while the command is still running.

Calls are serialised, so the two streams never overlap and the function needs no lock of its own -- a handler that writes to a terminal from both at once would otherwise interleave a line into nonsense.

type PendingProcess

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

PendingProcess is a command being described, before it runs.

It is a fluent builder: every setter returns the receiver, so calls chain.

result, err := factory.NewPendingProcess().
	Path("/srv/app").
	Timeout(2 * time.Minute).
	Run(ctx, []string{"go", "test", "./..."}, nil)

Go cannot have a field and a method of the same name, so the state is unexported and only the setters are public; String is how a fake handler or an assertion reads back the command line.

func NewPendingProcess

func NewPendingProcess(factory *Factory) *PendingProcess

NewPendingProcess builds a pending process bound to a factory.

func (*PendingProcess) Command

func (p *PendingProcess) Command(command ...string) *PendingProcess

Command sets the program and its arguments.

This takes only the array form, and there is no string form to add: see the package comment.

func (*PendingProcess) Env

func (p *PendingProcess) Env(environment map[string]string) *PendingProcess

Env sets environment variables for the program.

They are added to the ones this process already has, not substituted for them. A name already set is overridden by the value here.

func (*PendingProcess) Forever

func (p *PendingProcess) Forever() *PendingProcess

Forever removes the time limit.

func (*PendingProcess) IdleTimeout

func (p *PendingProcess) IdleTimeout(timeout time.Duration) *PendingProcess

IdleTimeout sets how long the program may go without writing anything.

It is the bound that catches what a total timeout does not: a fetch whose peer stopped answering, or a compiler waiting on a lock, both of which sit quiet for as long as they are given. A program that reports progress is never affected by it.

func (*PendingProcess) Input

func (p *PendingProcess) Input(input any) *PendingProcess

Input sets what is written to the program's standard input.

Standard input is closed once it has been written. Without an Input the program reads an immediately closed input, never the terminal -- a command that waits for a person is a command that hangs a deploy.

func (*PendingProcess) Options

func (p *PendingProcess) Options(options *syscall.SysProcAttr) *PendingProcess

Options sets the attributes the operating system is handed when the process is created.

What is in it differs by platform, which is true on both sides.

func (*PendingProcess) Path

func (p *PendingProcess) Path(path string) *PendingProcess

Path sets the working directory.

func (*PendingProcess) Quietly

func (p *PendingProcess) Quietly() *PendingProcess

Quietly discards the program's output instead of keeping it.

Output and ErrorOutput then answer empty rather than throwing.

func (*PendingProcess) Run

func (p *PendingProcess) Run(ctx context.Context, command []string, output OutputHandler) (ProcessResult, error)

Run runs the command and waits for it.

The command may be given here or with Command; nil here keeps the one already set.

A command that exits non-zero is not an error: the result comes back with a nil error and Failed reports it. The error is for the program that could not start, ran out of time, or was stray.

Cancelling it kills the program.

func (*PendingProcess) Start

func (p *PendingProcess) Start(ctx context.Context, command []string, output OutputHandler) (InvokedProcess, error)

Start starts the command and returns while it is still running.

The caller must call Wait on what comes back, or the program is never reaped.

func (*PendingProcess) String

func (p *PendingProcess) String() string

String is the command line, rendered the way a person would type it.

func (*PendingProcess) SupportsTty

func (p *PendingProcess) SupportsTty() bool

SupportsTty reports whether this machine has a terminal to hand over, by asking whether /dev/tty can be opened.

func (*PendingProcess) Timeout

func (p *PendingProcess) Timeout(timeout time.Duration) *PendingProcess

Timeout sets how long the whole run may take.

Zero is no limit, which is what Forever sets.

func (*PendingProcess) Tty

func (p *PendingProcess) Tty(tty ...bool) *PendingProcess

Tty hands the program this process's own terminal.

Output is not captured in this mode -- it goes straight to the terminal -- and the run fails if there is no terminal to hand over, which SupportsTty answers in advance.

func (*PendingProcess) WithFakeHandlers

func (p *PendingProcess) WithFakeHandlers(fakeHandlers []FakeHandler) *PendingProcess

WithFakeHandlers gives the pending process the fakes it should answer with.

type Pipe

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

Pipe runs processes one after another, each one reading what the one before it wrote.

result, err := factory.Pipe(ctx, func(pipe *process.Pipe) {
	pipe.Command("cat", "notes.txt")
	pipe.Command("grep", "-i", "todo")
}, nil)

The first process that fails ends the pipe and its result is what comes back.

func NewPipe

func NewPipe(factory *Factory, callback func(*Pipe)) *Pipe

NewPipe answers the Pipe constructor. Factory.Pipe is how a caller reaches it.

func (*Pipe) As

func (p *Pipe) As(key string) *PendingProcess

As adds a process to the pipe under a key.

func (*Pipe) Command

func (p *Pipe) Command(command ...string) *PendingProcess

Command adds a process to the pipe under the next integer key.

What Pool.Command says about that applies here.

func (*Pipe) Run

func (p *Pipe) Run(ctx context.Context, output PoolOutputHandler) (ProcessResult, error)

Run runs the callback and then the processes, in order.

Each process after the first is given the output of the one before it as its standard input, and a process that exits non-zero stops the pipe -- its result is returned, and nothing after it runs. The output handler is called with the key of the process the chunk came from; pass nil for no handler.

A pipe with no processes answers a nil result and a nil error.

type Pool

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

Pool is a set of processes that run at once.

results, err := factory.Concurrently(ctx, func(pool *process.Pool) {
	pool.As("build").Command("go", "build", "./...")
	pool.Command("go", "vet", "./...")
}, nil)

The callback is not run when the pool is built. It runs at Start.

A Pool is not safe for concurrent use, and does not need to be: it is built by one callback and started by the goroutine that built it. What runs at once is the processes, not the definition.

func NewPool

func NewPool(factory *Factory, callback func(*Pool)) *Pool

NewPool answers the Pool constructor. Factory.Pool is how a caller reaches it.

func (*Pool) As

func (p *Pool) As(key string) *PendingProcess

As adds a process to the pool under a key.

The key names the process in the output handler and in the results, which is what it is for: without it a process is known by its position.

func (*Pool) Command

func (p *Pool) Command(command ...string) *PendingProcess

Command adds a process to the pool under the next integer key.

Go has no method missing hook, so the one form that starts a process is written out.

func (*Pool) Run

func (p *Pool) Run(ctx context.Context) (*ProcessPoolResults, error)

Run starts the pool and waits for it.

func (*Pool) Start

func (p *Pool) Start(ctx context.Context, output PoolOutputHandler) (*InvokedProcessPool, error)

Start runs the callback and starts every process it defined.

The processes are started in the order they were added and none of them is waited for, so they run at the same time. The output handler is called with the key of the process the chunk came from.

A process that could not be started ends the start and the error names it. The ones already started are still running: InvokedProcessPool.Wait on what the caller has is not reachable, so this signals nothing and leaves them.

func (*Pool) Wait

func (p *Pool) Wait(ctx context.Context) (*ProcessPoolResults, error)

Wait starts the pool and waits for it.

type PoolOutputHandler

type PoolOutputHandler func(stream Stream, buffer string, key string)

PoolOutputHandler is OutputHandler with the key of the process it came from.

A process added without a key is keyed by its position, counted only among the unkeyed ones.

type ProcessFailedException

type ProcessFailedException struct {
	// Result is the result that failed.
	Result ProcessResult
	// Code is the exit status.
	Code int
}

ProcessFailedException is a command that ran and exited non-zero.

It lives here and not in the exceptions directory because a Go error type belongs in the package that returns it -- see exceptions/doc.go.

Nothing produces one on its own. It is what ProcessResult.Throw returns.

func NewProcessFailedException

func NewProcessFailedException(result ProcessResult) *ProcessFailedException

NewProcessFailedException builds the exception for a result.

func (*ProcessFailedException) Error

func (e *ProcessFailedException) Error() string

Error is the command, the exit code, and each of the two output streams under a ruled heading when it has anything in it.

type ProcessPoolResults

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

ProcessPoolResults is what a pool of processes finished with.

The results are in the order the processes were added to the pool.

func (*ProcessPoolResults) Collect

Collect answers the results as a collection.

A collections.Collection is an ordered list, as its own documentation says, so this is the results in the order the pool was built and the key is the position.

type ProcessResult

type ProcessResult interface {
	// Command is the command line that was run.
	Command() string
	// Successful reports an exit code of zero.
	Successful() bool
	// Failed is the other half of Successful.
	Failed() bool
	// ExitCode is what the program returned, and -1 when it never got far
	// enough to return anything.
	ExitCode() int
	// Output is everything the program wrote on standard output.
	Output() string
	// SeeInOutput reports whether Output contains the given string.
	SeeInOutput(output string) bool
	// ErrorOutput is everything the program wrote on standard error.
	ErrorOutput() string
	// SeeInErrorOutput reports whether ErrorOutput contains the given string.
	SeeInErrorOutput(output string) bool
	// Throw returns a ProcessFailedException when the command failed.
	Throw(callback func(ProcessResult, *ProcessFailedException)) (ProcessResult, error)
	// ThrowIf is Throw when the condition holds.
	ThrowIf(condition bool, callback func(ProcessResult, *ProcessFailedException)) (ProcessResult, error)
}

ProcessResult is what a command left behind.

It is an interface because Run hands back a real result or a faked one, and the caller is not supposed to be able to tell.

A command that exited non-zero is not an error. Run returns the result with a nil error and Failed reports the exit; Throw is what turns it into one. The error Run returns is for the process that never ran, timed out, or was stray.

type ProcessTimedOutException

type ProcessTimedOutException struct {
	// Result is what the program managed to say before it was killed.
	Result ProcessResult
	// Command is the command line that was killed.
	Command string
	// Timeout is the limit that was reached.
	Timeout time.Duration
	// Idle tells the two limits apart. They mean different things: a general
	// timeout is work that did not fit, an idle timeout is work that stopped
	// happening, and raising the limit does not help the second one.
	Idle bool
}

ProcessTimedOutException is a command that was killed for taking too long, or -- when Idle is set -- for having gone too long without printing anything.

It unwraps to context.DeadlineExceeded, so a caller that only wants to know whether time ran out never has to name this type.

func (*ProcessTimedOutException) Error

func (e *ProcessTimedOutException) Error() string

func (*ProcessTimedOutException) Unwrap

func (e *ProcessTimedOutException) Unwrap() error

Unwrap reports the timeout as the standard one, so errors.Is against context.DeadlineExceeded answers yes.

type StrayProcessError

type StrayProcessError struct {
	// Command is the command line that had no matching fake.
	Command string
}

StrayProcessError is a command that was about to really run inside a test that had asked for that not to happen.

It is a named type rather than a bare error so that errors.As can find it and a test can tell it from a program that really failed.

func (*StrayProcessError) Error

func (e *StrayProcessError) Error() string

type Stream

type Stream string

Stream names which of a process's two output streams a chunk came from.

const (
	// Out is standard output.
	Out Stream = "out"
	// Err is standard error.
	Err Stream = "err"
)

Directories

Path Synopsis
Package exceptions holds nothing.
Package exceptions holds nothing.

Jump to

Keyboard shortcuts

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