Skip to content

Commit 5556500

Browse files
authored
cmd/testscript: do not create an extra temporary directory (#259)
It's bugged me for a long time that the error messages printed by the `testscript` command do not refer to the actual files passed to the command, but instead to a temporary file created for the purposes of the command. This change alters the testscript command so that it avoids creating an extra copy of the script file, instead using the new ability of the testscript package to interpret explicitly provided files instead. Gratifyingly, this also simplifies the logic quite a bit. Note: this is dependent on #258, so should not be reviewed until that lands.
1 parent b143f3f commit 5556500

File tree

5 files changed

+109
-228
lines changed

5 files changed

+109
-228
lines changed

cmd/testscript/main.go

+84-220
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import (
1313
"os/exec"
1414
"path/filepath"
1515
"strings"
16+
"sync/atomic"
1617

1718
"github.com/rogpeppe/go-internal/goproxytest"
1819
"github.com/rogpeppe/go-internal/gotooltest"
1920
"github.com/rogpeppe/go-internal/testscript"
20-
"github.com/rogpeppe/go-internal/txtar"
2121
)
2222

2323
const (
@@ -71,16 +71,6 @@ func mainerr() (retErr error) {
7171
return err
7272
}
7373

74-
td, err := os.MkdirTemp("", "testscript")
75-
if err != nil {
76-
return fmt.Errorf("unable to create temp dir: %v", err)
77-
}
78-
if *fWork {
79-
fmt.Fprintf(os.Stderr, "temporary work directory: %v\n", td)
80-
} else {
81-
defer os.RemoveAll(td)
82-
}
83-
8474
files := fs.Args()
8575
if len(files) == 0 {
8676
files = []string{"-"}
@@ -99,228 +89,95 @@ func mainerr() (retErr error) {
9989
if onlyReadFromStdin && *fUpdate {
10090
return fmt.Errorf("cannot use -u when reading from stdin")
10191
}
102-
103-
tr := testRunner{
104-
update: *fUpdate,
105-
continueOnError: *fContinue,
106-
verbose: *fVerbose,
107-
env: envVars.vals,
108-
testWork: *fWork,
109-
}
110-
111-
dirNames := make(map[string]int)
112-
for _, filename := range files {
113-
// TODO make running files concurrent by default? If we do, note we'll need to do
114-
// something smarter with the runner stdout and stderr below
115-
116-
// Derive a name for the directory from the basename of file, making
117-
// uniq by adding a numeric suffix in the case we otherwise end
118-
// up with multiple files with the same basename
119-
dirName := filepath.Base(filename)
120-
count := dirNames[dirName]
121-
dirNames[dirName] = count + 1
122-
if count != 0 {
123-
dirName = fmt.Sprintf("%s%d", dirName, count)
92+
var stdinTempFile string
93+
for i, f := range files {
94+
if f != "-" {
95+
continue
12496
}
125-
126-
runDir := filepath.Join(td, dirName)
127-
if err := os.Mkdir(runDir, 0o777); err != nil {
128-
return fmt.Errorf("failed to create a run directory within %v for %v: %v", td, renderFilename(filename), err)
97+
if stdinTempFile != "" {
98+
return fmt.Errorf("cannot read stdin twice")
12999
}
130-
if err := tr.run(runDir, filename); err != nil {
131-
return err
100+
data, err := io.ReadAll(os.Stdin)
101+
if err != nil {
102+
return fmt.Errorf("error reading stdin: %v", err)
132103
}
133-
}
134-
135-
return nil
136-
}
137-
138-
type testRunner struct {
139-
// update denotes that the source testscript archive filename should be
140-
// updated in the case of any cmp failures.
141-
update bool
142-
143-
// continueOnError indicates that T.FailNow should not panic, allowing the
144-
// test script to continue running. Note that T is still marked as failed.
145-
continueOnError bool
146-
147-
// verbose indicates the running of the script should be noisy.
148-
verbose bool
149-
150-
// env is the environment that should be set on top of the base
151-
// testscript-defined minimal environment.
152-
env []string
153-
154-
// testWork indicates whether or not temporary working directory trees
155-
// should be left behind. Corresponds exactly to the
156-
// testscript.Params.TestWork field.
157-
testWork bool
158-
}
159-
160-
// run runs the testscript archive located at the path filename, within the
161-
// working directory runDir. filename could be "-" in the case of stdin
162-
func (tr *testRunner) run(runDir, filename string) error {
163-
var ar *txtar.Archive
164-
var err error
165-
166-
mods := filepath.Join(runDir, goModProxyDir)
167-
168-
if err := os.MkdirAll(mods, 0o777); err != nil {
169-
return fmt.Errorf("failed to create goModProxy dir: %v", err)
170-
}
171-
172-
if filename == "-" {
173-
byts, err := io.ReadAll(os.Stdin)
104+
f, err := os.CreateTemp("", "stdin*.txtar")
174105
if err != nil {
175-
return fmt.Errorf("failed to read from stdin: %v", err)
106+
return err
176107
}
177-
ar = txtar.Parse(byts)
178-
} else {
179-
ar, err = txtar.ParseFile(filename)
180-
}
181-
182-
if err != nil {
183-
return fmt.Errorf("failed to txtar parse %v: %v", renderFilename(filename), err)
184-
}
185-
186-
var script, gomodProxy txtar.Archive
187-
script.Comment = ar.Comment
188-
189-
for _, f := range ar.Files {
190-
fp := filepath.Clean(filepath.FromSlash(f.Name))
191-
parts := strings.Split(fp, string(os.PathSeparator))
192-
193-
if len(parts) > 1 && parts[0] == goModProxyDir {
194-
gomodProxy.Files = append(gomodProxy.Files, f)
195-
} else {
196-
script.Files = append(script.Files, f)
108+
if _, err := f.Write(data); err != nil {
109+
return err
197110
}
198-
}
199-
200-
if txtar.Write(&gomodProxy, runDir); err != nil {
201-
return fmt.Errorf("failed to write .gomodproxy files: %v", err)
202-
}
203-
204-
scriptFile := filepath.Join(runDir, "script.txtar")
205-
206-
if err := os.WriteFile(scriptFile, txtar.Format(&script), 0o666); err != nil {
207-
return fmt.Errorf("failed to write script for %v: %v", renderFilename(filename), err)
111+
if err := f.Close(); err != nil {
112+
return err
113+
}
114+
stdinTempFile = f.Name()
115+
files[i] = stdinTempFile
116+
defer os.Remove(stdinTempFile)
208117
}
209118

210119
p := testscript.Params{
211-
Dir: runDir,
212-
UpdateScripts: tr.update,
213-
ContinueOnError: tr.continueOnError,
120+
Setup: func(*testscript.Env) error { return nil },
121+
Files: files,
122+
UpdateScripts: *fUpdate,
123+
ContinueOnError: *fContinue,
124+
TestWork: *fWork,
214125
}
215126

216127
if _, err := exec.LookPath("go"); err == nil {
217128
if err := gotooltest.Setup(&p); err != nil {
218-
return fmt.Errorf("failed to setup go tool for %v run: %v", renderFilename(filename), err)
129+
return fmt.Errorf("failed to setup go tool: %v", err)
219130
}
220131
}
221-
222-
addSetup := func(f func(env *testscript.Env) error) {
223-
origSetup := p.Setup
224-
p.Setup = func(env *testscript.Env) error {
225-
if origSetup != nil {
226-
if err := origSetup(env); err != nil {
227-
return err
228-
}
229-
}
230-
return f(env)
132+
origSetup := p.Setup
133+
p.Setup = func(env *testscript.Env) error {
134+
if err := origSetup(env); err != nil {
135+
return err
231136
}
232-
}
233-
234-
if tr.testWork {
235-
addSetup(func(env *testscript.Env) error {
236-
fmt.Fprintf(os.Stderr, "temporary work directory for %s: %s\n", renderFilename(filename), env.WorkDir)
237-
return nil
238-
})
239-
}
240-
241-
if len(gomodProxy.Files) > 0 {
242-
srv, err := goproxytest.NewServer(mods, "")
243-
if err != nil {
244-
return fmt.Errorf("cannot start proxy for %v: %v", renderFilename(filename), err)
137+
if *fWork {
138+
env.T().Log("temporary work directory: ", env.WorkDir)
245139
}
246-
defer srv.Close()
140+
proxyDir := filepath.Join(env.WorkDir, goModProxyDir)
141+
if info, err := os.Stat(proxyDir); err == nil && info.IsDir() {
142+
srv, err := goproxytest.NewServer(proxyDir, "")
143+
if err != nil {
144+
return fmt.Errorf("cannot start Go proxy: %v", err)
145+
}
146+
env.Defer(srv.Close)
247147

248-
addSetup(func(env *testscript.Env) error {
249148
// Add GOPROXY after calling the original setup
250149
// so that it overrides any GOPROXY set there.
251150
env.Vars = append(env.Vars,
252151
"GOPROXY="+srv.URL,
253152
"GONOSUMDB=*",
254153
)
255-
return nil
256-
})
257-
}
258-
259-
if len(tr.env) > 0 {
260-
addSetup(func(env *testscript.Env) error {
261-
for _, v := range tr.env {
262-
varName := v
263-
if i := strings.Index(v, "="); i >= 0 {
264-
varName = v[:i]
265-
} else {
266-
v = fmt.Sprintf("%s=%s", v, os.Getenv(v))
267-
}
268-
switch varName {
269-
case "":
270-
return fmt.Errorf("invalid variable name %q", varName)
271-
case "WORK":
272-
return fmt.Errorf("cannot override WORK variable")
273-
}
274-
env.Vars = append(env.Vars, v)
154+
}
155+
for _, v := range envVars.vals {
156+
varName, _, ok := strings.Cut(v, "=")
157+
if !ok {
158+
v += "=" + os.Getenv(v)
275159
}
276-
return nil
277-
})
160+
switch varName {
161+
case "":
162+
return fmt.Errorf("invalid variable name %q", varName)
163+
case "WORK":
164+
return fmt.Errorf("cannot override WORK variable")
165+
}
166+
env.Vars = append(env.Vars, v)
167+
}
168+
return nil
278169
}
279170

280171
r := &runT{
281-
verbose: tr.verbose,
172+
verbose: *fVerbose,
173+
stdinTempFile: stdinTempFile,
282174
}
283-
284-
func() {
285-
defer func() {
286-
switch recover() {
287-
case nil, skipRun:
288-
case failedRun:
289-
err = failedRun
290-
default:
291-
panic(fmt.Errorf("unexpected panic: %v [%T]", err, err))
292-
}
293-
}()
294-
testscript.RunT(r, p)
295-
}()
296-
297-
if err != nil {
298-
return fmt.Errorf("error running %v in %v\n", renderFilename(filename), runDir)
299-
}
300-
301-
if tr.update && filename != "-" {
302-
// Parse the (potentially) updated scriptFile as an archive, then merge
303-
// with the original archive, retaining order. Then write the archive
304-
// back to the source file
305-
source, err := os.ReadFile(scriptFile)
306-
if err != nil {
307-
return fmt.Errorf("failed to read from script file %v for -update: %v", scriptFile, err)
308-
}
309-
updatedAr := txtar.Parse(source)
310-
updatedFiles := make(map[string]txtar.File)
311-
for _, f := range updatedAr.Files {
312-
updatedFiles[f.Name] = f
313-
}
314-
for i, f := range ar.Files {
315-
if newF, ok := updatedFiles[f.Name]; ok {
316-
ar.Files[i] = newF
317-
}
318-
}
319-
if err := os.WriteFile(filename, txtar.Format(ar), 0o666); err != nil {
320-
return fmt.Errorf("failed to write script back to %v for -update: %v", renderFilename(filename), err)
321-
}
175+
r.Run("", func(t testscript.T) {
176+
testscript.RunT(t, p)
177+
})
178+
if r.failed.Load() {
179+
return failedRun
322180
}
323-
324181
return nil
325182
}
326183

@@ -329,18 +186,11 @@ var (
329186
skipRun = errors.New("skip")
330187
)
331188

332-
// renderFilename renders filename in error messages, taking into account
333-
// the filename could be the special "-" (stdin)
334-
func renderFilename(filename string) string {
335-
if filename == "-" {
336-
return "<stdin>"
337-
}
338-
return filename
339-
}
340-
341189
// runT implements testscript.T and is used in the call to testscript.Run
342190
type runT struct {
343-
verbose bool
191+
verbose bool
192+
stdinTempFile string
193+
failed atomic.Bool
344194
}
345195

346196
func (r *runT) Skip(is ...interface{}) {
@@ -353,22 +203,36 @@ func (r *runT) Fatal(is ...interface{}) {
353203
}
354204

355205
func (r *runT) Parallel() {
356-
// No-op for now; we are currently only running a single script in a
357-
// testscript instance.
206+
// TODO run tests in parallel.
358207
}
359208

360209
func (r *runT) Log(is ...interface{}) {
361-
fmt.Print(is...)
210+
msg := fmt.Sprint(is...)
211+
if r.stdinTempFile != "" {
212+
msg = strings.ReplaceAll(msg, r.stdinTempFile, "<stdin>")
213+
}
214+
if !strings.HasSuffix(msg, "\n") {
215+
msg += "\n"
216+
}
217+
fmt.Print(msg)
362218
}
363219

364220
func (r *runT) FailNow() {
365221
panic(failedRun)
366222
}
367223

368-
func (r *runT) Run(n string, f func(t testscript.T)) {
369-
// For now we we don't top/tail the run of a subtest. We are currently only
370-
// running a single script in a testscript instance, which means that we
371-
// will only have a single subtest.
224+
func (r *runT) Run(name string, f func(t testscript.T)) {
225+
// TODO: perhaps log the test name when there's more
226+
// than one test file?
227+
defer func() {
228+
switch err := recover(); err {
229+
case nil, skipRun:
230+
case failedRun:
231+
r.failed.Store(true)
232+
default:
233+
panic(fmt.Errorf("unexpected panic: %v [%T]", err, err))
234+
}
235+
}()
372236
f(r)
373237
}
374238

cmd/testscript/testdata/error.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ unquote file.txt
44
# stdin
55
stdin file.txt
66
! testscript -v
7-
stderr 'error running <stdin> in'
7+
stdout 'FAIL: <stdin>:1: unexpected command failure'
88

99
# file-based
1010
! testscript -v file.txt
11-
stderr 'error running file.txt in'
11+
stdout 'FAIL: file.txt:1: unexpected command failure'
1212

1313
-- file.txt --
1414
>exec false

0 commit comments

Comments
 (0)