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 ¶
- Variables
- type Factory
- func (f *Factory) AllowStrayProcesses() *Factory
- func (f *Factory) AssertDidntRun(t *testing.T, pattern string)
- func (f *Factory) AssertNotRan(t *testing.T, pattern string)
- func (f *Factory) AssertNothingRan(t *testing.T)
- func (f *Factory) AssertRan(t *testing.T, pattern string)
- func (f *Factory) AssertRanTimes(t *testing.T, pattern string, times int)
- func (f *Factory) Concurrently(ctx context.Context, callback func(*Pool), output PoolOutputHandler) (*ProcessPoolResults, error)
- func (f *Factory) Describe() *FakeProcessDescription
- func (f *Factory) Fake(handlers ...FakeHandler) *Factory
- func (f *Factory) IsRecording() bool
- func (f *Factory) NewPendingProcess() *PendingProcess
- func (f *Factory) Pipe(ctx context.Context, callback func(*Pipe), output PoolOutputHandler) (ProcessResult, error)
- func (f *Factory) Pool(callback func(*Pool)) *Pool
- func (f *Factory) PreventStrayProcesses(prevent ...bool) *Factory
- func (f *Factory) PreventingStrayProcesses() bool
- func (f *Factory) Record(process *PendingProcess, result ProcessResult) *Factory
- func (f *Factory) RecordIfRecording(process *PendingProcess, result ProcessResult) *Factory
- func (f *Factory) Recorded() []string
- func (f *Factory) Result(output any, errorOutput any, exitCode int) *FakeProcessResult
- func (f *Factory) Run(ctx context.Context, command []string, output OutputHandler) (ProcessResult, error)
- func (f *Factory) Sequence(processes ...any) *FakeProcessSequence
- func (f *Factory) Start(ctx context.Context, command []string, output OutputHandler) (InvokedProcess, error)
- type FakeHandler
- type FakeInvokedProcess
- func (p *FakeInvokedProcess) ErrorOutput() string
- func (p *FakeInvokedProcess) HasReceivedSignal(signal os.Signal) bool
- func (p *FakeInvokedProcess) ID() int
- func (p *FakeInvokedProcess) LatestErrorOutput() string
- func (p *FakeInvokedProcess) LatestOutput() string
- func (p *FakeInvokedProcess) Output() string
- func (p *FakeInvokedProcess) PredictProcessResult() ProcessResult
- func (p *FakeInvokedProcess) Running() bool
- func (p *FakeInvokedProcess) Signal(signal os.Signal) error
- func (p *FakeInvokedProcess) Stop(timeout time.Duration, signal os.Signal) error
- func (p *FakeInvokedProcess) Wait(output OutputHandler) (ProcessResult, error)
- func (p *FakeInvokedProcess) WithOutputHandler(output OutputHandler) *FakeInvokedProcess
- type FakeProcessDescription
- func (d *FakeProcessDescription) ErrorOutput(output any) *FakeProcessDescription
- func (d *FakeProcessDescription) ExitCode(exitCode int) *FakeProcessDescription
- func (d *FakeProcessDescription) ID(processID int) *FakeProcessDescription
- func (d *FakeProcessDescription) Iterations(iterations int) *FakeProcessDescription
- func (d *FakeProcessDescription) Output(output any) *FakeProcessDescription
- func (d *FakeProcessDescription) ReplaceErrorOutput(output string) *FakeProcessDescription
- func (d *FakeProcessDescription) ReplaceOutput(output string) *FakeProcessDescription
- func (d *FakeProcessDescription) RunsFor(iterations int) *FakeProcessDescription
- func (d *FakeProcessDescription) ToProcessResult(command string) ProcessResult
- type FakeProcessResult
- func (r *FakeProcessResult) Command() string
- func (r *FakeProcessResult) ErrorOutput() string
- func (r *FakeProcessResult) ExitCode() int
- func (r *FakeProcessResult) Failed() bool
- func (r *FakeProcessResult) Output() string
- func (r *FakeProcessResult) SeeInErrorOutput(output string) bool
- func (r *FakeProcessResult) SeeInOutput(output string) bool
- func (r *FakeProcessResult) Successful() bool
- func (r *FakeProcessResult) Throw(callback func(ProcessResult, *ProcessFailedException)) (ProcessResult, error)
- func (r *FakeProcessResult) ThrowIf(condition bool, callback func(ProcessResult, *ProcessFailedException)) (ProcessResult, error)
- func (r *FakeProcessResult) WithCommand(command string) *FakeProcessResult
- type FakeProcessSequence
- type InvokedProcess
- type InvokedProcessPool
- type OutputHandler
- type PendingProcess
- func (p *PendingProcess) Command(command ...string) *PendingProcess
- func (p *PendingProcess) Env(environment map[string]string) *PendingProcess
- func (p *PendingProcess) Forever() *PendingProcess
- func (p *PendingProcess) IdleTimeout(timeout time.Duration) *PendingProcess
- func (p *PendingProcess) Input(input any) *PendingProcess
- func (p *PendingProcess) Options(options *syscall.SysProcAttr) *PendingProcess
- func (p *PendingProcess) Path(path string) *PendingProcess
- func (p *PendingProcess) Quietly() *PendingProcess
- func (p *PendingProcess) Run(ctx context.Context, command []string, output OutputHandler) (ProcessResult, error)
- func (p *PendingProcess) Start(ctx context.Context, command []string, output OutputHandler) (InvokedProcess, error)
- func (p *PendingProcess) String() string
- func (p *PendingProcess) SupportsTty() bool
- func (p *PendingProcess) Timeout(timeout time.Duration) *PendingProcess
- func (p *PendingProcess) Tty(tty ...bool) *PendingProcess
- func (p *PendingProcess) WithFakeHandlers(fakeHandlers []FakeHandler) *PendingProcess
- type Pipe
- type Pool
- func (p *Pool) As(key string) *PendingProcess
- func (p *Pool) Command(command ...string) *PendingProcess
- func (p *Pool) Run(ctx context.Context) (*ProcessPoolResults, error)
- func (p *Pool) Start(ctx context.Context, output PoolOutputHandler) (*InvokedProcessPool, error)
- func (p *Pool) Wait(ctx context.Context) (*ProcessPoolResults, error)
- type PoolOutputHandler
- type ProcessFailedException
- type ProcessPoolResults
- type ProcessResult
- type ProcessTimedOutException
- type StrayProcessError
- type Stream
Constants ¶
This section is empty.
Variables ¶
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.
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 (*Factory) AllowStrayProcesses ¶
AllowStrayProcesses is the inverse of PreventStrayProcesses.
func (*Factory) AssertDidntRun ¶
AssertDidntRun is AssertNotRan under its other name.
func (*Factory) AssertNotRan ¶
AssertNotRan fails the test when a recorded command matched the pattern.
func (*Factory) AssertNothingRan ¶
AssertNothingRan fails the test when any process ran at all.
func (*Factory) AssertRanTimes ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) Wait ¶
func (p *FakeInvokedProcess) Wait(output OutputHandler) (ProcessResult, error)
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 ¶
func (d *FakeProcessDescription) ID(processID int) *FakeProcessDescription
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 ¶
func (d *FakeProcessDescription) Output(output any) *FakeProcessDescription
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 ¶
func (r *FakeProcessResult) Throw(callback func(ProcessResult, *ProcessFailedException)) (ProcessResult, error)
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 ¶
func (p *InvokedProcessPool) Running() collections.Collection[InvokedProcess]
Running answers the processes of the pool that are still going.
func (*InvokedProcessPool) Signal ¶
func (p *InvokedProcessPool) Signal(signal os.Signal) (collections.Collection[InvokedProcess], error)
Signal sends a signal to every process still running, and answers with the ones it signalled.
func (*InvokedProcessPool) Wait ¶
func (p *InvokedProcessPool) Wait() (*ProcessPoolResults, error)
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 ¶
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 ¶
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 ¶
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.
type PoolOutputHandler ¶
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 ¶
func (r *ProcessPoolResults) Collect() collections.Collection[ProcessResult]
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