Skip to content

Commit 22b9127

Browse files
authored
testscript: expose (*TestScript).stdout via Stdout() (#216)
Similarly, expose (*TestScript).stderr via Stderr(). Closes #139
1 parent 81831f2 commit 22b9127

File tree

4 files changed

+189
-3
lines changed

4 files changed

+189
-3
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Verify that when we don't update stdout when we don't attempt to write via Stdout()
2+
fprintargs stdout hello stdout from fprintargs
3+
stdout 'hello stdout from fprintargs'
4+
echoandexit 0
5+
stdout 'hello stdout from fprintargs'
6+
7+
# Verify that when we don't update stderr when we don't attempt to write via Stderr()
8+
fprintargs stderr hello stderr from fprintargs
9+
stderr 'hello stderr from fprintargs'
10+
echoandexit 0
11+
stderr 'hello stderr from fprintargs'
12+
13+
# Verify that we do update stdout when we attempt to write via Stdout() or Stderr()
14+
fprintargs stdout hello stdout from fprintargs
15+
stdout 'hello stdout from fprintargs'
16+
! stderr .+
17+
echoandexit 0 'hello stdout from echoandexit'
18+
stdout 'hello stdout from echoandexit'
19+
! stderr .+
20+
fprintargs stdout hello stdout from fprintargs
21+
stdout 'hello stdout from fprintargs'
22+
! stderr .+
23+
echoandexit 0 '' 'hello stderr from echoandexit'
24+
! stdout .+
25+
stderr 'hello stderr from echoandexit'
26+
27+
# Verify that we do update stderr when we attempt to write via Stdout() or Stderr()
28+
fprintargs stderr hello stderr from fprintargs
29+
! stdout .+
30+
stderr 'hello stderr from fprintargs'
31+
echoandexit 0 'hello stdout from echoandexit'
32+
stdout 'hello stdout from echoandexit'
33+
! stderr .+
34+
fprintargs stdout hello stdout from fprintargs
35+
stdout 'hello stdout from fprintargs'
36+
! stderr .+
37+
echoandexit 0 '' 'hello stderr from echoandexit'
38+
! stdout .+
39+
stderr 'hello stderr from echoandexit'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Verify that stdout and stderr get set event when a user-builtin
2+
# command aborts. Note that we need to assert against stdout
3+
# because our meta testscript command sees only a single log.
4+
unquote scripts/testscript.txt
5+
! testscript -v scripts
6+
cmpenv stdout stdout.golden
7+
8+
-- scripts/testscript.txt --
9+
> printargs hello world
10+
> echoandexit 1 'this is stdout' 'this is stderr'
11+
-- stdout.golden --
12+
> printargs hello world
13+
[stdout]
14+
["printargs" "hello" "world"]
15+
> echoandexit 1 'this is stdout' 'this is stderr'
16+
[stdout]
17+
this is stdout
18+
[stderr]
19+
this is stderr
20+
FAIL: ${$}WORK${/}scripts${/}testscript.txt:2: told to exit with code 1

testscript/testscript.go

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
666697
func (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.
792877
func (ts *TestScript) Logf(format string, args ...interface{}) {
793878
format = strings.TrimSuffix(format, "\n")
@@ -933,13 +1018,18 @@ func interruptProcess(p *os.Process) {
9331018
func (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.
9561046
func (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

testscript/testscript_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,14 @@ func TestScripts(t *testing.T) {
234234
Cmds: map[string]func(ts *TestScript, neg bool, args []string){
235235
"some-param-cmd": func(ts *TestScript, neg bool, args []string) {
236236
},
237+
"echoandexit": echoandexit,
237238
},
238239
ContinueOnError: *fContinue,
239240
})
240241
}()
241-
ts.stdout = strings.Replace(t.log.String(), ts.workdir, "$WORK", -1)
242+
stdout := t.log.String()
243+
stdout = strings.ReplaceAll(stdout, ts.workdir, "$WORK")
244+
fmt.Fprint(ts.Stdout(), stdout)
242245
if neg {
243246
if !t.failed {
244247
ts.Fatalf("testscript unexpectedly succeeded")
@@ -249,6 +252,7 @@ func TestScripts(t *testing.T) {
249252
ts.Fatalf("testscript unexpectedly failed with errors: %q", &t.log)
250253
}
251254
},
255+
"echoandexit": echoandexit,
252256
},
253257
Setup: func(env *Env) error {
254258
infos, err := ioutil.ReadDir(env.WorkDir)
@@ -274,6 +278,33 @@ func TestScripts(t *testing.T) {
274278
// TODO check that the temp directory has been removed.
275279
}
276280

281+
func echoandexit(ts *TestScript, neg bool, args []string) {
282+
// Takes at least one argument
283+
//
284+
// args[0] - int that indicates the exit code of the command
285+
// args[1] - the string to echo to stdout if non-empty
286+
// args[2] - the string to echo to stderr if non-empty
287+
if len(args) == 0 || len(args) > 3 {
288+
ts.Fatalf("echoandexit takes at least one and at most three arguments")
289+
}
290+
if neg {
291+
ts.Fatalf("neg means nothing for echoandexit")
292+
}
293+
exitCode, err := strconv.ParseInt(args[0], 10, 64)
294+
if err != nil {
295+
ts.Fatalf("failed to parse exit code from %q: %v", args[0], err)
296+
}
297+
if len(args) > 1 && args[1] != "" {
298+
fmt.Fprint(ts.Stdout(), args[1])
299+
}
300+
if len(args) > 2 && args[2] != "" {
301+
fmt.Fprint(ts.Stderr(), args[2])
302+
}
303+
if exitCode != 0 {
304+
ts.Fatalf("told to exit with code %d", exitCode)
305+
}
306+
}
307+
277308
// TestTestwork tests that using the flag -testwork will make sure the work dir isn't removed
278309
// after the test is done. It uses an empty testscript file that doesn't do anything.
279310
func TestTestwork(t *testing.T) {

0 commit comments

Comments
 (0)