@@ -14,6 +14,7 @@ import (
1414 "flag"
1515 "fmt"
1616 "go/build"
17+ "io"
1718 "io/fs"
1819 "io/ioutil"
1920 "os"
@@ -362,6 +363,17 @@ type TestScript struct {
362363 scriptFiles map [string ]string // files stored in the txtar archive (absolute paths -> path in script)
363364 scriptUpdates map [string ]string // updates to testscript files via UpdateScripts.
364365
366+ // runningBuiltin indicates if we are running a user-supplied builtin
367+ // command. These commands are specified via Params.Cmds.
368+ runningBuiltin bool
369+
370+ // builtinStd(out|err) are established if a user-supplied builtin command
371+ // requests Stdout() or Stderr(). Either both are non-nil, or both are nil.
372+ // This invariant is maintained by both setBuiltinStd() and
373+ // clearBuiltinStd().
374+ builtinStdout * strings.Builder
375+ builtinStderr * strings.Builder
376+
365377 ctxt context.Context // per TestScript context
366378 gracePeriod time.Duration // time between SIGQUIT and SIGKILL
367379}
@@ -659,10 +671,29 @@ func (ts *TestScript) runLine(line string) (runOK bool) {
659671 if cmd == nil {
660672 ts .Fatalf ("unknown command %q" , args [0 ])
661673 }
662- cmd (ts , neg , args [1 :])
674+ ts .callBuiltinCmd (args [0 ], func () {
675+ cmd (ts , neg , args [1 :])
676+ })
663677 return true
664678}
665679
680+ func (ts * TestScript ) callBuiltinCmd (cmd string , runCmd func ()) {
681+ ts .runningBuiltin = true
682+ defer func () {
683+ r := recover ()
684+ ts .runningBuiltin = false
685+ ts .clearBuiltinStd ()
686+ switch r {
687+ case nil :
688+ // we did not panic
689+ default :
690+ // re-"throw" the panic
691+ panic (r )
692+ }
693+ }()
694+ runCmd ()
695+ }
696+
666697func (ts * TestScript ) applyScriptUpdates () {
667698 if len (ts .scriptUpdates ) == 0 {
668699 return
@@ -788,6 +819,60 @@ func (ts *TestScript) Check(err error) {
788819 }
789820}
790821
822+ // Stdout returns an io.Writer that can be used by a user-supplied builtin
823+ // command (delcared via Params.Cmds) to write to stdout. If this method is
824+ // called outside of the execution of a user-supplied builtin command, the
825+ // call panics.
826+ func (ts * TestScript ) Stdout () io.Writer {
827+ if ! ts .runningBuiltin {
828+ panic ("can only call TestScript.Stdout when running a builtin command" )
829+ }
830+ ts .setBuiltinStd ()
831+ return ts .builtinStdout
832+ }
833+
834+ // Stderr returns an io.Writer that can be used by a user-supplied builtin
835+ // command (delcared via Params.Cmds) to write to stderr. If this method is
836+ // called outside of the execution of a user-supplied builtin command, the
837+ // call panics.
838+ func (ts * TestScript ) Stderr () io.Writer {
839+ if ! ts .runningBuiltin {
840+ panic ("can only call TestScript.Stderr when running a builtin command" )
841+ }
842+ ts .setBuiltinStd ()
843+ return ts .builtinStderr
844+ }
845+
846+ // setBuiltinStd ensures that builtinStdout and builtinStderr are non nil.
847+ func (ts * TestScript ) setBuiltinStd () {
848+ // This method must maintain the invariant that both builtinStdout and
849+ // builtinStderr are set or neither are set
850+
851+ // If both are set, nothing to do
852+ if ts .builtinStdout != nil && ts .builtinStderr != nil {
853+ return
854+ }
855+ ts .builtinStdout = new (strings.Builder )
856+ ts .builtinStderr = new (strings.Builder )
857+ }
858+
859+ // clearBuiltinStd sets ts.stdout and ts.stderr from the builtin command
860+ // buffers, logs both, and resets both builtinStdout and builtinStderr to nil.
861+ func (ts * TestScript ) clearBuiltinStd () {
862+ // This method must maintain the invariant that both builtinStdout and
863+ // builtinStderr are set or neither are set
864+
865+ // If neither set, nothing to do
866+ if ts .builtinStdout == nil && ts .builtinStderr == nil {
867+ return
868+ }
869+ ts .stdout = ts .builtinStdout .String ()
870+ ts .builtinStdout = nil
871+ ts .stderr = ts .builtinStderr .String ()
872+ ts .builtinStderr = nil
873+ ts .logStd ()
874+ }
875+
791876// Logf appends the given formatted message to the test log transcript.
792877func (ts * TestScript ) Logf (format string , args ... interface {}) {
793878 format = strings .TrimSuffix (format , "\n " )
@@ -933,13 +1018,18 @@ func interruptProcess(p *os.Process) {
9331018func (ts * TestScript ) Exec (command string , args ... string ) error {
9341019 var err error
9351020 ts .stdout , ts .stderr , err = ts .exec (command , args ... )
1021+ ts .logStd ()
1022+ return err
1023+ }
1024+
1025+ // logStd logs the current non-empty values of stdout and stderr.
1026+ func (ts * TestScript ) logStd () {
9361027 if ts .stdout != "" {
9371028 ts .Logf ("[stdout]\n %s" , ts .stdout )
9381029 }
9391030 if ts .stderr != "" {
9401031 ts .Logf ("[stderr]\n %s" , ts .stderr )
9411032 }
942- return err
9431033}
9441034
9451035// expand applies environment variable expansion to the string s.
@@ -954,6 +1044,12 @@ func (ts *TestScript) expand(s string) string {
9541044
9551045// fatalf aborts the test with the given failure message.
9561046func (ts * TestScript ) Fatalf (format string , args ... interface {}) {
1047+ // In user-supplied builtins, the only way we have of aborting
1048+ // is via Fatalf. Hence if we are aborting from a user-supplied
1049+ // builtin, it's important we first log stdout and stderr. If
1050+ // we are not, the following call is a no-op.
1051+ ts .clearBuiltinStd ()
1052+
9571053 fmt .Fprintf (& ts .log , "FAIL: %s:%d: %s\n " , ts .file , ts .lineno , fmt .Sprintf (format , args ... ))
9581054 // This should be caught by the defer inside the TestScript.runLine method.
9591055 // We do this rather than calling ts.t.FailNow directly because we want to
0 commit comments