Skip to content

Commit d89504f

Browse files
authored
testscript: implement UpdateScripts parameter (#83)
This makes it possible for a test to automatically update the test scripts when output changes. Does not cover `cmpenv`.
1 parent 4bb227c commit d89504f

File tree

7 files changed

+217
-30
lines changed

7 files changed

+217
-30
lines changed

testscript/cmd.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,24 @@ func (ts *TestScript) doCmdCmp(args []string, env bool) {
121121
name1, name2 := args[0], args[1]
122122
text1 := ts.ReadFile(name1)
123123

124-
data, err := ioutil.ReadFile(ts.MkAbs(name2))
124+
absName2 := ts.MkAbs(name2)
125+
data, err := ioutil.ReadFile(absName2)
125126
ts.Check(err)
126127
text2 := string(data)
127128
if env {
128129
text2 = ts.expand(text2)
129130
}
130-
131131
if text1 == text2 {
132132
return
133133
}
134+
if ts.params.UpdateScripts && !env && (args[0] == "stdout" || args[0] == "stderr") {
135+
if scriptFile, ok := ts.scriptFiles[absName2]; ok {
136+
ts.scriptUpdates[scriptFile] = text1
137+
return
138+
}
139+
// The file being compared against isn't in the txtar archive, so don't
140+
// update the script.
141+
}
134142

135143
ts.Logf("[diff -%s +%s]\n%s\n", name1, name2, textutil.Diff(text1, text2))
136144
ts.Fatalf("%s and %s differ", name1, name2)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
unquote scripts/testscript.txt
2+
unquote testscript-new.txt
3+
testscript-update scripts
4+
cmp scripts/testscript.txt testscript-new.txt
5+
6+
-- scripts/testscript.txt --
7+
>echo stdout right
8+
>cmp stdout expect
9+
>
10+
>-- expect --
11+
>wrong
12+
-- testscript-new.txt --
13+
>echo stdout right
14+
>cmp stdout expect
15+
>
16+
>-- expect --
17+
>right
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
unquote scripts/testscript.txt
2+
cp scripts/testscript.txt unchanged
3+
! testscript-update scripts
4+
cmp scripts/testscript.txt unchanged
5+
6+
-- scripts/testscript.txt --
7+
>echo stdout right
8+
>cp file expect
9+
>cmp stdout expect
10+
>
11+
>-- file --
12+
>wrong
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
unquote scripts/testscript.txt
2+
unquote testscript-new.txt
3+
testscript-update scripts
4+
cmp scripts/testscript.txt testscript-new.txt
5+
6+
-- scripts/testscript.txt --
7+
>echo stdout '-- lookalike --'
8+
>cmp stdout expect
9+
>
10+
>-- expect --
11+
>wrong
12+
-- testscript-new.txt --
13+
>echo stdout '-- lookalike --'
14+
>cmp stdout expect
15+
>
16+
>-- expect --
17+
>>-- lookalike --
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
unquote scripts/testscript.txt
2+
unquote testscript-new.txt
3+
testscript-update scripts
4+
cmp scripts/testscript.txt testscript-new.txt
5+
6+
-- scripts/testscript.txt --
7+
>echo stderr right
8+
>cmp stderr expect
9+
>
10+
>-- expect --
11+
>wrong
12+
-- testscript-new.txt --
13+
>echo stderr right
14+
>cmp stderr expect
15+
>
16+
>-- expect --
17+
>right

testscript/testscript.go

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ type Params struct {
103103
// (for example because the function invoked os.Exit), then the
104104
// error will be ignored.
105105
IgnoreMissedCoverage bool
106+
107+
// UpdateScripts specifies that if a `cmp` command fails and
108+
// its first argument is `stdout` or `stderr` and its second argument
109+
// refers to a file inside the testscript file, the command will succeed
110+
// and the testscript file will be updated to reflect the actual output.
111+
//
112+
// The content will be quoted with txtar.Quote if needed;
113+
// a manual change will be needed if it is not unquoted in the
114+
// script.
115+
UpdateScripts bool
106116
}
107117

108118
// RunDir runs the tests in the given directory. All files in dir with a ".txt"
@@ -165,13 +175,15 @@ func RunT(t T, p Params) {
165175
t.Run(name, func(t T) {
166176
t.Parallel()
167177
ts := &TestScript{
168-
t: t,
169-
testTempDir: testTempDir,
170-
name: name,
171-
file: file,
172-
params: p,
173-
ctxt: context.Background(),
174-
deferred: func() {},
178+
t: t,
179+
testTempDir: testTempDir,
180+
name: name,
181+
file: file,
182+
params: p,
183+
ctxt: context.Background(),
184+
deferred: func() {},
185+
scriptFiles: make(map[string]string),
186+
scriptUpdates: make(map[string]string),
175187
}
176188
defer func() {
177189
if p.TestWork || *testWork {
@@ -191,27 +203,30 @@ func RunT(t T, p Params) {
191203

192204
// A TestScript holds execution state for a single test script.
193205
type TestScript struct {
194-
params Params
195-
t T
196-
testTempDir string
197-
workdir string // temporary work dir ($WORK)
198-
log bytes.Buffer // test execution log (printed at end of test)
199-
mark int // offset of next log truncation
200-
cd string // current directory during test execution; initially $WORK/gopath/src
201-
name string // short name of test ("foo")
202-
file string // full file name ("testdata/script/foo.txt")
203-
lineno int // line number currently executing
204-
line string // line currently executing
205-
env []string // environment list (for os/exec)
206-
envMap map[string]string // environment mapping (matches env; on Windows keys are lowercase)
207-
values map[interface{}]interface{} // values for custom commands
208-
stdin string // standard input to next 'go' command; set by 'stdin' command.
209-
stdout string // standard output from last 'go' command; for 'stdout' command
210-
stderr string // standard error from last 'go' command; for 'stderr' command
211-
stopped bool // test wants to stop early
212-
start time.Time // time phase started
213-
background []backgroundCmd // backgrounded 'exec' and 'go' commands
214-
deferred func() // deferred cleanup actions.
206+
params Params
207+
t T
208+
testTempDir string
209+
workdir string // temporary work dir ($WORK)
210+
log bytes.Buffer // test execution log (printed at end of test)
211+
mark int // offset of next log truncation
212+
cd string // current directory during test execution; initially $WORK/gopath/src
213+
name string // short name of test ("foo")
214+
file string // full file name ("testdata/script/foo.txt")
215+
lineno int // line number currently executing
216+
line string // line currently executing
217+
env []string // environment list (for os/exec)
218+
envMap map[string]string // environment mapping (matches env; on Windows keys are lowercase)
219+
values map[interface{}]interface{} // values for custom commands
220+
stdin string // standard input to next 'go' command; set by 'stdin' command.
221+
stdout string // standard output from last 'go' command; for 'stdout' command
222+
stderr string // standard error from last 'go' command; for 'stderr' command
223+
stopped bool // test wants to stop early
224+
start time.Time // time phase started
225+
background []backgroundCmd // backgrounded 'exec' and 'go' commands
226+
deferred func() // deferred cleanup actions.
227+
archive *txtar.Archive // the testscript being run.
228+
scriptFiles map[string]string // files stored in the txtar archive (absolute paths -> path in script)
229+
scriptUpdates map[string]string // updates to testscript files via UpdateScripts.
215230

216231
ctxt context.Context // per TestScript context
217232
}
@@ -256,8 +271,10 @@ func (ts *TestScript) setup() string {
256271
// Unpack archive.
257272
a, err := txtar.ParseFile(ts.file)
258273
ts.Check(err)
274+
ts.archive = a
259275
for _, f := range a.Files {
260276
name := ts.MkAbs(ts.expand(f.Name))
277+
ts.scriptFiles[name] = f.Name
261278
ts.Check(os.MkdirAll(filepath.Dir(name), 0777))
262279
ts.Check(ioutil.WriteFile(name, f.Data, 0666))
263280
}
@@ -327,6 +344,7 @@ func (ts *TestScript) run() {
327344
fmt.Fprintf(&ts.log, "\n")
328345
ts.mark = ts.log.Len()
329346
}
347+
defer ts.applyScriptUpdates()
330348

331349
// Run script.
332350
// See testdata/script/README for documentation of script form.
@@ -434,6 +452,40 @@ Script:
434452
}
435453
}
436454

455+
func (ts *TestScript) applyScriptUpdates() {
456+
if len(ts.scriptUpdates) == 0 {
457+
return
458+
}
459+
for name, content := range ts.scriptUpdates {
460+
found := false
461+
for i := range ts.archive.Files {
462+
f := &ts.archive.Files[i]
463+
if f.Name != name {
464+
continue
465+
}
466+
data := []byte(content)
467+
if txtar.NeedsQuote(data) {
468+
data1, err := txtar.Quote(data)
469+
if err != nil {
470+
ts.t.Fatal(fmt.Sprintf("cannot update script file %q: %v", f.Name, err))
471+
continue
472+
}
473+
data = data1
474+
}
475+
f.Data = data
476+
found = true
477+
}
478+
// Sanity check.
479+
if !found {
480+
panic("script update file not found")
481+
}
482+
}
483+
if err := ioutil.WriteFile(ts.file, txtar.Format(ts.archive), 0666); err != nil {
484+
ts.t.Fatal("cannot update script: ", err)
485+
}
486+
ts.Logf("%s updated", ts.file)
487+
}
488+
437489
// condition reports whether the given condition is satisfied.
438490
func (ts *TestScript) condition(cond string) (bool, error) {
439491
switch cond {

testscript/testscript_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package testscript
66

77
import (
8+
"errors"
89
"fmt"
910
"io/ioutil"
1011
"os"
@@ -123,6 +124,35 @@ func TestScripts(t *testing.T) {
123124
ts.Fatalf("reading %q; got %q want %q", args[0], got, want)
124125
}
125126
},
127+
"testscript-update": func(ts *TestScript, neg bool, args []string) {
128+
// Run testscript in testscript. Oooh! Meta!
129+
if len(args) != 1 {
130+
ts.Fatalf("testscript <dir>")
131+
}
132+
t := &fakeT{ts: ts}
133+
func() {
134+
defer func() {
135+
if err := recover(); err != nil {
136+
if err != errAbort {
137+
panic(err)
138+
}
139+
}
140+
}()
141+
RunT(t, Params{
142+
Dir: ts.MkAbs(args[0]),
143+
UpdateScripts: true,
144+
})
145+
}()
146+
if neg {
147+
if len(t.failMsgs) == 0 {
148+
ts.Fatalf("testscript-update unexpectedly succeeded")
149+
}
150+
return
151+
}
152+
if len(t.failMsgs) > 0 {
153+
ts.Fatalf("testscript-update unexpectedly failed with errors: %q", t.failMsgs)
154+
}
155+
},
126156
},
127157
Setup: func(env *Env) error {
128158
infos, err := ioutil.ReadDir(env.WorkDir)
@@ -219,3 +249,37 @@ func waitFile(ts *TestScript, neg bool, args []string) {
219249
}
220250
ts.Fatalf("timed out waiting for %q to be created", path)
221251
}
252+
253+
type fakeT struct {
254+
ts *TestScript
255+
failMsgs []string
256+
}
257+
258+
var errAbort = errors.New("abort test")
259+
260+
func (t *fakeT) Skip(args ...interface{}) {
261+
panic(errAbort)
262+
}
263+
264+
func (t *fakeT) Fatal(args ...interface{}) {
265+
t.failMsgs = append(t.failMsgs, fmt.Sprint(args...))
266+
panic(errAbort)
267+
}
268+
269+
func (t *fakeT) Parallel() {}
270+
271+
func (t *fakeT) Log(args ...interface{}) {
272+
t.ts.Logf("testscript: %v", fmt.Sprint(args...))
273+
}
274+
275+
func (t *fakeT) FailNow() {
276+
t.Fatal("failed")
277+
}
278+
279+
func (t *fakeT) Run(name string, f func(T)) {
280+
f(t)
281+
}
282+
283+
func (t *fakeT) Verbose() bool {
284+
return false
285+
}

0 commit comments

Comments
 (0)