@@ -20,6 +20,7 @@ import (
2020 "fmt"
2121 "os/exec"
2222 "strings"
23+ "sync"
2324
2425 "wait4x.dev/v3/checker"
2526)
@@ -29,9 +30,11 @@ type Option func(e *Exec)
2930
3031// Exec is a command execution checker
3132type Exec struct {
32- command string
33- args []string
34- expectExitCode int
33+ command string
34+ args []string
35+ expectExitCode int
36+ expectStdoutRegex string
37+ expectStderrRegex string
3538}
3639
3740// New creates a new Exec checker
@@ -63,6 +66,22 @@ func WithExpectExitCode(code int) Option {
6366 }
6467}
6568
69+ // WithExpectStdoutRegex configures the regular expression
70+ // which is evaluated against STDOUT.
71+ func WithExpectStdoutRegex (pattern string ) Option {
72+ return func (e * Exec ) {
73+ e .expectStdoutRegex = pattern
74+ }
75+ }
76+
77+ // WithExpectStderrRegex configures the regular expression
78+ // which is evaluated against STDERR.
79+ func WithExpectStderrRegex (pattern string ) Option {
80+ return func (e * Exec ) {
81+ e .expectStderrRegex = pattern
82+ }
83+ }
84+
6685// Identity returns the identity of the checker
6786func (e * Exec ) Identity () (string , error ) {
6887 if len (e .args ) > 0 {
@@ -73,9 +92,60 @@ func (e *Exec) Identity() (string, error) {
7392
7493// Check executes the command and checks if it returns the expected exit code
7594func (e * Exec ) Check (ctx context.Context ) error {
95+ var stdout , stderr * pipeProcessor
96+ var errs chan error
97+ var err error
98+
7699 // Create command with context for cancellation support
77100 cmd := exec .CommandContext (ctx , e .command , e .args ... )
78- err := cmd .Run ()
101+
102+ stdout , err = newPipeProcessor (e .expectStdoutRegex , cmd .StdoutPipe )
103+ if err != nil {
104+ return checker .NewExpectedError (
105+ "failed to prepare STDOUT processor" , err ,
106+ "command" , e .command ,
107+ "args" , e .args ,
108+ )
109+ }
110+
111+ stderr , err = newPipeProcessor (e .expectStderrRegex , cmd .StderrPipe )
112+ if err != nil {
113+ return checker .NewExpectedError (
114+ "failed to prepare STDERR processor" , err ,
115+ "command" , e .command ,
116+ "args" , e .args ,
117+ )
118+ }
119+
120+ if err := cmd .Start (); err != nil {
121+ return checker .NewExpectedError (
122+ "unable to start command" , err ,
123+ "command" , e .command ,
124+ "args" , e .args ,
125+ )
126+ }
127+
128+ if stdout != nil || stderr != nil {
129+ var wg sync.WaitGroup
130+ errs = make (chan error )
131+
132+ go func () {
133+ // wait for the workers to finish processing all of the data
134+ // before closing the channel, otherwise they will be blocked
135+ wg .Wait ()
136+ close (errs )
137+ }()
138+
139+ if stdout != nil {
140+ stdout .Process (& wg , errs )
141+ }
142+
143+ if stderr != nil {
144+ stderr .Process (& wg , errs )
145+ }
146+ }
147+
148+ err = cmd .Wait ()
79149
80150 // Check if context was canceled
81151 select {
@@ -112,5 +182,22 @@ func (e *Exec) Check(ctx context.Context) error {
112182 )
113183 }
114184
115- return nil
185+ return drainErrors (errs )
186+ }
187+
188+ // drainErrors reads all items from the given channel
189+ // and returns the last non-nil value or nil if the
190+ // channel is nil or does not contain any errors.
191+ func drainErrors (errs chan error ) (err error ) {
192+ if errs == nil {
193+ return
194+ }
195+
196+ for e := range errs {
197+ if e != nil {
198+ err = e
199+ }
200+ }
201+
202+ return
116203}
0 commit comments