@@ -6,10 +6,7 @@ import (
66 "fmt"
77 "os"
88 "os/exec"
9- "reflect"
109 "regexp"
11- "runtime"
12- "strings"
1310 "time"
1411
1512 "github.com/go-tstr/tstr/strerr"
@@ -23,6 +20,10 @@ const (
2320 ErrOptApply = strerr .Error ("failed apply Opt" )
2421 ErrNoMatchingLine = strerr .Error ("no matching line found" )
2522 ErrNilCmdRegexp = strerr .Error ("command has to be set before this option can be applied, check the order of options" )
23+ ErrPreCmdFailed = strerr .Error ("pre command failed" )
24+ ErrBadRegexp = strerr .Error ("bad regular expression for matching line" )
25+ ErrOutputPipe = strerr .Error ("failed to aquire output pipe for command" )
26+ ErrBuildFailed = strerr .Error ("failed to build go binary" )
2627)
2728
2829type Cmd struct {
@@ -47,7 +48,7 @@ func New(opts ...Opt) *Cmd {
4748func (c * Cmd ) Start () error {
4849 for _ , opt := range c .opts {
4950 if err := opt (c ); err != nil {
50- return fmt .Errorf ("failed to apply option %s : %w" , getFnName ( opt ) , err )
51+ return fmt .Errorf ("failed to apply option: %w" , err )
5152 }
5253 }
5354
@@ -92,7 +93,8 @@ func WithCommand(name string, args ...string) Opt {
9293 }
9394}
9495
95- // WithReadyFn allows user to provide custom ready function.
96+ // WithReadyFn allows user to provide custom readiness function.
97+ // Given fn should block until the command is ready.
9698func WithReadyFn (fn func (* exec.Cmd ) error ) Opt {
9799 return func (c * Cmd ) error {
98100 c .ready = fn
@@ -108,7 +110,7 @@ func WithStopFn(fn func(*exec.Cmd) error) Opt {
108110 }
109111}
110112
111- // WithDir sets environment variables for the command.
113+ // WithEnv sets environment variables for the command.
112114// By default, the command inherits the environment of the current process and setting this option will override it.
113115func WithEnv (env ... string ) Opt {
114116 return func (c * Cmd ) error {
@@ -117,6 +119,15 @@ func WithEnv(env ...string) Opt {
117119 }
118120}
119121
122+ // WithEnvAppend adds environment variables to commands current env.
123+ // By default, the command inherits the environment of the current process and setting this option will override it.
124+ func WithEnvAppend (env ... string ) Opt {
125+ return func (c * Cmd ) error {
126+ c .cmd .Env = env
127+ return nil
128+ }
129+ }
130+
120131// WithDir sets the working directory for the command.
121132func WithDir (dir string ) Opt {
122133 return func (c * Cmd ) error {
@@ -125,32 +136,14 @@ func WithDir(dir string) Opt {
125136 }
126137}
127138
128- // WithWaitRegexp sets the ready function so that it waits for the command to output a line that matches the given regular expression.
139+ // WithWaitMatchingLine sets the ready function so that it waits for the command to output a line that matches the given regular expression.
129140func WithWaitMatchingLine (exp string ) Opt {
130141 return func (c * Cmd ) error {
131- re , err := regexp .Compile (exp )
132- if err != nil {
133- return err
134- }
135-
136- if c .cmd == nil {
137- return ErrNilCmdRegexp
138- }
139-
140- stdout , err := c .cmd .StdoutPipe ()
142+ fn , err := MatchingLine (exp , c .cmd )
141143 if err != nil {
142144 return err
143145 }
144-
145- return WithReadyFn (func (cmd * exec.Cmd ) error {
146- scanner := bufio .NewScanner (stdout )
147- for scanner .Scan () {
148- if re .Match (scanner .Bytes ()) {
149- return nil
150- }
151- }
152- return errors .Join (ErrNoMatchingLine , scanner .Err ())
153- })(c )
146+ return WithReadyFn (fn )(c )
154147 }
155148}
156149
@@ -180,6 +173,42 @@ func WithExecCmd(cmd *exec.Cmd) Opt {
180173 }
181174}
182175
176+ // WithPreCmd runs the given command as part of the setup.
177+ // This can be used to prepare the actual main command.
178+ func WithPreCmd (cmd * exec.Cmd ) Opt {
179+ return func (c * Cmd ) error {
180+ if err := cmd .Run (); err != nil {
181+ return fmt .Errorf ("%w: %w" , ErrPreCmdFailed , err )
182+ }
183+ return nil
184+ }
185+ }
186+
187+ // WithGoCode builds the given Go projects and sets the main package as the command.
188+ // By default the command is set to collect coverage data.
189+ func WithGoCode (modulePath , mainPkg string ) Opt {
190+ return func (c * Cmd ) error {
191+ dir , err := os .MkdirTemp ("" , "go-tstr" )
192+ if err != nil {
193+ return fmt .Errorf ("failed to create tmp dir for go binary: %w" , err )
194+ }
195+
196+ target := dir + "/" + "go-app"
197+ buildCmd := exec .Command ("go" , "build" , "-race" , "-cover" , "-covermode" , "atomic" , "-o" , target , mainPkg )
198+ buildCmd .Env = append (os .Environ (), "CGO_ENABLED=1" ) // Required for -race flag
199+ buildCmd .Stdout = os .Stdout
200+ buildCmd .Stderr = os .Stderr
201+ buildCmd .Dir = modulePath
202+ err = buildCmd .Run ()
203+ if err != nil {
204+ return fmt .Errorf ("%w: %w" , ErrBuildFailed , err )
205+ }
206+
207+ c .cmd = exec .Command (target )
208+ return nil
209+ }
210+ }
211+
183212// StopWithSignal returns a stop function that sends the given signal to the command and waits for it to exit.
184213// This can be used with WithStopFn to stop the command with a specific signal.
185214func StopWithSignal (s os.Signal ) func (* exec.Cmd ) error {
@@ -195,7 +224,34 @@ func StopWithSignal(s os.Signal) func(*exec.Cmd) error {
195224 }
196225}
197226
198- func getFnName (fn any ) string {
199- strs := strings .Split ((runtime .FuncForPC (reflect .ValueOf (fn ).Pointer ()).Name ()), "." )
200- return strs [len (strs )- 1 ]
227+ // MatchLine waits for the command to output a line that matches the given regular expression.
228+ func MatchingLine (exp string , cmd * exec.Cmd ) (func (* exec.Cmd ) error , error ) {
229+ if cmd == nil {
230+ return nil , ErrNilCmdRegexp
231+ }
232+
233+ re , err := regexp .Compile (exp )
234+ if err != nil {
235+ return nil , fmt .Errorf ("%w: %w" , ErrBadRegexp , err )
236+ }
237+
238+ stdout , err := cmd .StdoutPipe ()
239+ if err != nil {
240+ return nil , fmt .Errorf ("%w: %w" , ErrOutputPipe , err )
241+ }
242+
243+ return func (cmd * exec.Cmd ) error {
244+ scanner := bufio .NewScanner (stdout )
245+ for scanner .Scan () {
246+ if re .Match (scanner .Bytes ()) {
247+ // drain the rest of the output on background
248+ go func () {
249+ for scanner .Scan () {
250+ }
251+ }()
252+ return nil
253+ }
254+ }
255+ return errors .Join (ErrNoMatchingLine , scanner .Err ())
256+ }, nil
201257}
0 commit comments