Skip to content

Commit 45f7444

Browse files
authored
Merge pull request #12 from ActiveState/dx-2782
Test output expectations should fail if the process has prematurely exited.
2 parents 13d903a + fcd0667 commit 45f7444

File tree

5 files changed

+50
-12
lines changed

5 files changed

+50
-12
lines changed

expect.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func (tt *TermTest) ExpectCustom(consumer consumer, opts ...SetExpectOpt) (rerr
8686
return fmt.Errorf("could not create expect options: %w", err)
8787
}
8888

89-
cons, err := tt.outputProducer.addConsumer(consumer, expectOpts.ToConsumerOpts()...)
89+
cons, err := tt.outputProducer.addConsumer(tt, consumer, expectOpts.ToConsumerOpts()...)
9090
if err != nil {
9191
return fmt.Errorf("could not add consumer: %w", err)
9292
}
@@ -180,11 +180,11 @@ func (tt *TermTest) expectExitCode(exitCode int, match bool, opts ...SetExpectOp
180180
select {
181181
case <-time.After(timeoutV):
182182
return fmt.Errorf("after %s: %w", timeoutV, TimeoutError)
183-
case err := <-waitChan(tt.cmd.Wait):
184-
if err != nil && (tt.cmd.ProcessState == nil || tt.cmd.ProcessState.ExitCode() == 0) {
185-
return fmt.Errorf("cmd wait failed: %w", err)
183+
case state := <-tt.Exited(false): // do not wait for unread output since it's not read by this select{}
184+
if state.Err != nil && (state.ProcessState == nil || state.ProcessState.ExitCode() == 0) {
185+
return fmt.Errorf("cmd wait failed: %w", state.Err)
186186
}
187-
if err := tt.assertExitCode(tt.cmd.ProcessState.ExitCode(), exitCode, match); err != nil {
187+
if err := tt.assertExitCode(state.ProcessState.ExitCode(), exitCode, match); err != nil {
188188
return err
189189
}
190190
}

helpers.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ type cmdExit struct {
2424
}
2525

2626
// waitForCmdExit turns process.wait() into a channel so that it can be used within a select{} statement
27-
func waitForCmdExit(cmd *exec.Cmd) chan cmdExit {
28-
exit := make(chan cmdExit, 1)
27+
func waitForCmdExit(cmd *exec.Cmd) chan *cmdExit {
28+
exit := make(chan *cmdExit, 1)
2929
go func() {
3030
err := cmd.Wait()
31-
exit <- cmdExit{ProcessState: cmd.ProcessState, Err: err}
31+
exit <- &cmdExit{ProcessState: cmd.ProcessState, Err: err}
3232
}()
3333
return exit
3434
}
3535

3636
func waitChan[T any](wait func() T) chan T {
3737
done := make(chan T)
3838
go func() {
39-
wait()
39+
done <- wait()
4040
close(done)
4141
}()
4242
return done

outputconsumer.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type outputConsumer struct {
1515
opts *OutputConsumerOpts
1616
isalive bool
1717
mutex *sync.Mutex
18+
tt *TermTest
1819
}
1920

2021
type OutputConsumerOpts struct {
@@ -36,7 +37,7 @@ func OptsConsTimeout(timeout time.Duration) func(o *OutputConsumerOpts) {
3637
}
3738
}
3839

39-
func newOutputConsumer(consume consumer, opts ...SetConsOpt) *outputConsumer {
40+
func newOutputConsumer(tt *TermTest, consume consumer, opts ...SetConsOpt) *outputConsumer {
4041
oc := &outputConsumer{
4142
consume: consume,
4243
opts: &OutputConsumerOpts{
@@ -46,6 +47,7 @@ func newOutputConsumer(consume consumer, opts ...SetConsOpt) *outputConsumer {
4647
waiter: make(chan error, 1),
4748
isalive: true,
4849
mutex: &sync.Mutex{},
50+
tt: tt,
4951
}
5052

5153
for _, optSetter := range opts {
@@ -101,5 +103,11 @@ func (e *outputConsumer) wait() error {
101103
e.mutex.Lock()
102104
e.opts.Logger.Println("Encountered timeout")
103105
return fmt.Errorf("after %s: %w", e.opts.Timeout, TimeoutError)
106+
case state := <-e.tt.Exited(true): // allow for output to be read first by first case in this select{}
107+
e.mutex.Lock()
108+
if state.Err != nil {
109+
e.opts.Logger.Println("Encountered error waiting for process to exit: %s\n", state.Err.Error())
110+
}
111+
return fmt.Errorf("process exited (status: %d)", state.ProcessState.ExitCode())
104112
}
105113
}

outputproducer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,12 @@ func (o *outputProducer) flushConsumers() error {
238238
return nil
239239
}
240240

241-
func (o *outputProducer) addConsumer(consume consumer, opts ...SetConsOpt) (*outputConsumer, error) {
241+
func (o *outputProducer) addConsumer(tt *TermTest, consume consumer, opts ...SetConsOpt) (*outputConsumer, error) {
242242
o.opts.Logger.Printf("adding consumer")
243243
defer o.opts.Logger.Printf("added consumer")
244244

245245
opts = append(opts, OptConsInherit(o.opts))
246-
listener := newOutputConsumer(consume, opts...)
246+
listener := newOutputConsumer(tt, consume, opts...)
247247
o.consumers = append(o.consumers, listener)
248248

249249
if err := o.flushConsumers(); err != nil {

termtest.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type TermTest struct {
2424
outputProducer *outputProducer
2525
listenError chan error
2626
opts *Opts
27+
exited *cmdExit
2728
}
2829

2930
type ErrorHandler func(*TermTest, error) error
@@ -50,6 +51,9 @@ type SetOpt func(o *Opts) error
5051
const DefaultCols = 140
5152
const DefaultRows = 10
5253

54+
var processExitPollInterval = 10 * time.Millisecond
55+
var processExitExtraWait = 500 * time.Millisecond
56+
5357
func NewOpts() *Opts {
5458
return &Opts{
5559
Logger: VoidLogger,
@@ -234,6 +238,10 @@ func (tt *TermTest) start() (rerr error) {
234238
}()
235239
wg.Wait()
236240

241+
go func() {
242+
tt.exited = <-waitForCmdExit(tt.cmd)
243+
}()
244+
237245
return nil
238246
}
239247

@@ -316,6 +324,28 @@ func (tt *TermTest) SendCtrlC() {
316324
tt.Send(string([]byte{0x03})) // 0x03 is ASCII character for ^C
317325
}
318326

327+
// Exited returns a channel that sends the given termtest's command cmdExit info when available.
328+
// This can be used within a select{} statement.
329+
// If waitExtra is given, waits a little bit before sending cmdExit info. This allows any fellow
330+
// switch cases with output consumers to handle unprocessed stdout. If there are no such cases
331+
// (e.g. ExpectExit(), where we want to catch an exit ASAP), waitExtra should be false.
332+
func (tt *TermTest) Exited(waitExtra bool) chan *cmdExit {
333+
return waitChan(func() *cmdExit {
334+
ticker := time.NewTicker(processExitPollInterval)
335+
for {
336+
select {
337+
case <-ticker.C:
338+
if tt.exited != nil {
339+
if waitExtra { // allow sibling output consumer cases to handle their output
340+
time.Sleep(processExitExtraWait)
341+
}
342+
return tt.exited
343+
}
344+
}
345+
}
346+
})
347+
}
348+
319349
func (tt *TermTest) errorHandler(rerr *error) {
320350
err := *rerr
321351
if err == nil {

0 commit comments

Comments
 (0)