1// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package ssa_test
6
7import (
8	"flag"
9	"fmt"
10	"internal/testenv"
11	"io"
12	"os"
13	"os/exec"
14	"path/filepath"
15	"regexp"
16	"runtime"
17	"strconv"
18	"strings"
19	"testing"
20	"time"
21)
22
23var (
24	update  = flag.Bool("u", false, "update test reference files")
25	verbose = flag.Bool("v", false, "print debugger interactions (very verbose)")
26	dryrun  = flag.Bool("n", false, "just print the command line and first debugging bits")
27	useGdb  = flag.Bool("g", false, "use Gdb instead of Delve (dlv), use gdb reference files")
28	force   = flag.Bool("f", false, "force run under not linux-amd64; also do not use tempdir")
29	repeats = flag.Bool("r", false, "detect repeats in debug steps and don't ignore them")
30	inlines = flag.Bool("i", false, "do inlining for gdb (makes testing flaky till inlining info is correct)")
31)
32
33var (
34	hexRe                 = regexp.MustCompile("0x[a-zA-Z0-9]+")
35	numRe                 = regexp.MustCompile(`-?\d+`)
36	stringRe              = regexp.MustCompile(`([^\"]|(\.))*`)
37	leadingDollarNumberRe = regexp.MustCompile(`^[$]\d+`)
38	optOutGdbRe           = regexp.MustCompile("[<]optimized out[>]")
39	numberColonRe         = regexp.MustCompile(`^ *\d+:`)
40)
41
42var gdb = "gdb"      // Might be "ggdb" on Darwin, because gdb no longer part of XCode
43var debugger = "dlv" // For naming files, etc.
44
45var gogcflags = os.Getenv("GO_GCFLAGS")
46
47// optimizedLibs usually means "not running in a noopt test builder".
48var optimizedLibs = (!strings.Contains(gogcflags, "-N") && !strings.Contains(gogcflags, "-l"))
49
50// TestNexting go-builds a file, then uses a debugger (default delve, optionally gdb)
51// to next through the generated executable, recording each line landed at, and
52// then compares those lines with reference file(s).
53// Flag -u updates the reference file(s).
54// Flag -g changes the debugger to gdb (and uses gdb-specific reference files)
55// Flag -v is ever-so-slightly verbose.
56// Flag -n is for dry-run, and prints the shell and first debug commands.
57//
58// Because this test (combined with existing compiler deficiencies) is flaky,
59// for gdb-based testing by default inlining is disabled
60// (otherwise output depends on library internals)
61// and for both gdb and dlv by default repeated lines in the next stream are ignored
62// (because this appears to be timing-dependent in gdb, and the cleanest fix is in code common to gdb and dlv).
63//
64// Also by default, any source code outside of .../testdata/ is not mentioned
65// in the debugging histories.  This deals both with inlined library code once
66// the compiler is generating clean inline records, and also deals with
67// runtime code between return from main and process exit.  This is hidden
68// so that those files (in the runtime/library) can change without affecting
69// this test.
70//
71// These choices can be reversed with -i (inlining on) and -r (repeats detected) which
72// will also cause their own failures against the expected outputs.  Note that if the compiler
73// and debugger were behaving properly, the inlined code and repeated lines would not appear,
74// so the expected output is closer to what we hope to see, though it also encodes all our
75// current bugs.
76//
77// The file being tested may contain comments of the form
78// //DBG-TAG=(v1,v2,v3)
79// where DBG = {gdb,dlv} and TAG={dbg,opt}
80// each variable may optionally be followed by a / and one or more of S,A,N,O
81// to indicate normalization of Strings, (hex) addresses, and numbers.
82// "O" is an explicit indication that we expect it to be optimized out.
83// For example:
84//
85//	if len(os.Args) > 1 { //gdb-dbg=(hist/A,cannedInput/A) //dlv-dbg=(hist/A,cannedInput/A)
86//
87// TODO: not implemented for Delve yet, but this is the plan
88//
89// After a compiler change that causes a difference in the debug behavior, check
90// to see if it is sensible or not, and if it is, update the reference files with
91// go test debug_test.go -args -u
92// (for Delve)
93// go test debug_test.go -args -u -d
94func TestNexting(t *testing.T) {
95	testenv.SkipFlaky(t, 37404)
96
97	skipReasons := "" // Many possible skip reasons, list all that apply
98	if testing.Short() {
99		skipReasons = "not run in short mode; "
100	}
101	testenv.MustHaveGoBuild(t)
102
103	if *useGdb && !*force && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
104		// Running gdb on OSX/darwin is very flaky.
105		// Sometimes it is called ggdb, depending on how it is installed.
106		// It also sometimes requires an admin password typed into a dialog box.
107		// Various architectures tend to differ slightly sometimes, and keeping them
108		// all in sync is a pain for people who don't have them all at hand,
109		// so limit testing to amd64 (for now)
110		skipReasons += "not run when testing gdb (-g) unless forced (-f) or linux-amd64; "
111	}
112
113	if !*useGdb && !*force && testenv.Builder() == "linux-386-longtest" {
114		// The latest version of Delve does support linux/386. However, the version currently
115		// installed in the linux-386-longtest builder does not. See golang.org/issue/39309.
116		skipReasons += "not run when testing delve on linux-386-longtest builder unless forced (-f); "
117	}
118
119	if *useGdb {
120		debugger = "gdb"
121		_, err := exec.LookPath(gdb)
122		if err != nil {
123			if runtime.GOOS != "darwin" {
124				skipReasons += "not run because gdb not on path; "
125			} else {
126				// On Darwin, MacPorts installs gdb as "ggdb".
127				_, err = exec.LookPath("ggdb")
128				if err != nil {
129					skipReasons += "not run because gdb (and also ggdb) request by -g option not on path; "
130				} else {
131					gdb = "ggdb"
132				}
133			}
134		}
135	} else { // Delve
136		debugger = "dlv"
137		_, err := exec.LookPath("dlv")
138		if err != nil {
139			skipReasons += "not run because dlv not on path; "
140		}
141	}
142
143	if skipReasons != "" {
144		t.Skip(skipReasons[:len(skipReasons)-2])
145	}
146
147	optFlags := "" // Whatever flags are needed to test debugging of optimized code.
148	dbgFlags := "-N -l"
149	if *useGdb && !*inlines {
150		// For gdb (default), disable inlining so that a compiler test does not depend on library code.
151		// TODO: Technically not necessary in 1.10 and later, but it causes a largish regression that needs investigation.
152		optFlags += " -l"
153	}
154
155	moreargs := []string{}
156	if *useGdb && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") {
157		// gdb and lldb on Darwin do not deal with compressed dwarf.
158		// also, Windows.
159		moreargs = append(moreargs, "-ldflags=-compressdwarf=false")
160	}
161
162	subTest(t, debugger+"-dbg", "hist", dbgFlags, moreargs...)
163	subTest(t, debugger+"-dbg", "scopes", dbgFlags, moreargs...)
164	subTest(t, debugger+"-dbg", "i22558", dbgFlags, moreargs...)
165
166	subTest(t, debugger+"-dbg-race", "i22600", dbgFlags, append(moreargs, "-race")...)
167
168	optSubTest(t, debugger+"-opt", "hist", optFlags, 1000, moreargs...)
169	optSubTest(t, debugger+"-opt", "scopes", optFlags, 1000, moreargs...)
170
171	// Was optSubtest, this test is observed flaky on Linux in Docker on (busy) macOS, probably because of timing
172	// glitches in this harness.
173	// TODO get rid of timing glitches in this harness.
174	skipSubTest(t, debugger+"-opt", "infloop", optFlags, 10, moreargs...)
175
176}
177
178// subTest creates a subtest that compiles basename.go with the specified gcflags and additional compiler arguments,
179// then runs the debugger on the resulting binary, with any comment-specified actions matching tag triggered.
180func subTest(t *testing.T, tag string, basename string, gcflags string, moreargs ...string) {
181	t.Run(tag+"-"+basename, func(t *testing.T) {
182		if t.Name() == "TestNexting/gdb-dbg-i22558" {
183			testenv.SkipFlaky(t, 31263)
184		}
185		testNexting(t, basename, tag, gcflags, 1000, moreargs...)
186	})
187}
188
189// skipSubTest is the same as subTest except that it skips the test if execution is not forced (-f)
190func skipSubTest(t *testing.T, tag string, basename string, gcflags string, count int, moreargs ...string) {
191	t.Run(tag+"-"+basename, func(t *testing.T) {
192		if *force {
193			testNexting(t, basename, tag, gcflags, count, moreargs...)
194		} else {
195			t.Skip("skipping flaky test because not forced (-f)")
196		}
197	})
198}
199
200// optSubTest is the same as subTest except that it skips the test if the runtime and libraries
201// were not compiled with optimization turned on.  (The skip may not be necessary with Go 1.10 and later)
202func optSubTest(t *testing.T, tag string, basename string, gcflags string, count int, moreargs ...string) {
203	// If optimized test is run with unoptimized libraries (compiled with -N -l), it is very likely to fail.
204	// This occurs in the noopt builders (for example).
205	t.Run(tag+"-"+basename, func(t *testing.T) {
206		if *force || optimizedLibs {
207			testNexting(t, basename, tag, gcflags, count, moreargs...)
208		} else {
209			t.Skip("skipping for unoptimized stdlib/runtime")
210		}
211	})
212}
213
214func testNexting(t *testing.T, base, tag, gcflags string, count int, moreArgs ...string) {
215	// (1) In testdata, build sample.go into test-sample.<tag>
216	// (2) Run debugger gathering a history
217	// (3) Read expected history from testdata/sample.<tag>.nexts
218	// optionally, write out testdata/sample.<tag>.nexts
219
220	testbase := filepath.Join("testdata", base) + "." + tag
221	tmpbase := filepath.Join("testdata", "test-"+base+"."+tag)
222
223	// Use a temporary directory unless -f is specified
224	if !*force {
225		tmpdir := t.TempDir()
226		tmpbase = filepath.Join(tmpdir, "test-"+base+"."+tag)
227		if *verbose {
228			fmt.Printf("Tempdir is %s\n", tmpdir)
229		}
230	}
231	exe := tmpbase
232
233	runGoArgs := []string{"build", "-o", exe, "-gcflags=all=" + gcflags}
234	runGoArgs = append(runGoArgs, moreArgs...)
235	runGoArgs = append(runGoArgs, filepath.Join("testdata", base+".go"))
236
237	runGo(t, "", runGoArgs...)
238
239	nextlog := testbase + ".nexts"
240	tmplog := tmpbase + ".nexts"
241	var dbg dbgr
242	if *useGdb {
243		dbg = newGdb(t, tag, exe)
244	} else {
245		dbg = newDelve(t, tag, exe)
246	}
247	h1 := runDbgr(dbg, count)
248	if *dryrun {
249		fmt.Printf("# Tag for above is %s\n", dbg.tag())
250		return
251	}
252	if *update {
253		h1.write(nextlog)
254	} else {
255		h0 := &nextHist{}
256		h0.read(nextlog)
257		if !h0.equals(h1) {
258			// Be very noisy about exactly what's wrong to simplify debugging.
259			h1.write(tmplog)
260			cmd := testenv.Command(t, "diff", "-u", nextlog, tmplog)
261			line := asCommandLine("", cmd)
262			bytes, err := cmd.CombinedOutput()
263			if err != nil && len(bytes) == 0 {
264				t.Fatalf("step/next histories differ, diff command %s failed with error=%v", line, err)
265			}
266			t.Fatalf("step/next histories differ, diff=\n%s", string(bytes))
267		}
268	}
269}
270
271type dbgr interface {
272	start()
273	stepnext(s string) bool // step or next, possible with parameter, gets line etc.  returns true for success, false for unsure response
274	quit()
275	hist() *nextHist
276	tag() string
277}
278
279func runDbgr(dbg dbgr, maxNext int) *nextHist {
280	dbg.start()
281	if *dryrun {
282		return nil
283	}
284	for i := 0; i < maxNext; i++ {
285		if !dbg.stepnext("n") {
286			break
287		}
288	}
289	dbg.quit()
290	h := dbg.hist()
291	return h
292}
293
294func runGo(t *testing.T, dir string, args ...string) string {
295	var stdout, stderr strings.Builder
296	cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
297	cmd.Dir = dir
298	if *dryrun {
299		fmt.Printf("%s\n", asCommandLine("", cmd))
300		return ""
301	}
302	cmd.Stdout = &stdout
303	cmd.Stderr = &stderr
304
305	if err := cmd.Run(); err != nil {
306		t.Fatalf("error running cmd (%s): %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
307	}
308
309	if s := stderr.String(); s != "" {
310		t.Fatalf("Stderr = %s\nWant empty", s)
311	}
312
313	return stdout.String()
314}
315
316// tstring provides two strings, o (stdout) and e (stderr)
317type tstring struct {
318	o string
319	e string
320}
321
322func (t tstring) String() string {
323	return t.o + t.e
324}
325
326type pos struct {
327	line uint32
328	file uint8 // Artifact of plans to implement differencing instead of calling out to diff.
329}
330
331type nextHist struct {
332	f2i   map[string]uint8
333	fs    []string
334	ps    []pos
335	texts []string
336	vars  [][]string
337}
338
339func (h *nextHist) write(filename string) {
340	file, err := os.Create(filename)
341	if err != nil {
342		panic(fmt.Sprintf("Problem opening %s, error %v\n", filename, err))
343	}
344	defer file.Close()
345	var lastfile uint8
346	for i, x := range h.texts {
347		p := h.ps[i]
348		if lastfile != p.file {
349			fmt.Fprintf(file, "  %s\n", h.fs[p.file-1])
350			lastfile = p.file
351		}
352		fmt.Fprintf(file, "%d:%s\n", p.line, x)
353		// TODO, normalize between gdb and dlv into a common, comparable format.
354		for _, y := range h.vars[i] {
355			y = strings.TrimSpace(y)
356			fmt.Fprintf(file, "%s\n", y)
357		}
358	}
359	file.Close()
360}
361
362func (h *nextHist) read(filename string) {
363	h.f2i = make(map[string]uint8)
364	bytes, err := os.ReadFile(filename)
365	if err != nil {
366		panic(fmt.Sprintf("Problem reading %s, error %v\n", filename, err))
367	}
368	var lastfile string
369	lines := strings.Split(string(bytes), "\n")
370	for i, l := range lines {
371		if len(l) > 0 && l[0] != '#' {
372			if l[0] == ' ' {
373				// file -- first two characters expected to be "  "
374				lastfile = strings.TrimSpace(l)
375			} else if numberColonRe.MatchString(l) {
376				// line number -- <number>:<line>
377				colonPos := strings.Index(l, ":")
378				if colonPos == -1 {
379					panic(fmt.Sprintf("Line %d (%s) in file %s expected to contain '<number>:' but does not.\n", i+1, l, filename))
380				}
381				h.add(lastfile, l[0:colonPos], l[colonPos+1:])
382			} else {
383				h.addVar(l)
384			}
385		}
386	}
387}
388
389// add appends file (name), line (number) and text (string) to the history,
390// provided that the file+line combo does not repeat the previous position,
391// and provided that the file is within the testdata directory.  The return
392// value indicates whether the append occurred.
393func (h *nextHist) add(file, line, text string) bool {
394	// Only record source code in testdata unless the inlines flag is set
395	if !*inlines && !strings.Contains(file, "/testdata/") {
396		return false
397	}
398	fi := h.f2i[file]
399	if fi == 0 {
400		h.fs = append(h.fs, file)
401		fi = uint8(len(h.fs))
402		h.f2i[file] = fi
403	}
404
405	line = strings.TrimSpace(line)
406	var li int
407	var err error
408	if line != "" {
409		li, err = strconv.Atoi(line)
410		if err != nil {
411			panic(fmt.Sprintf("Non-numeric line: %s, error %v\n", line, err))
412		}
413	}
414	l := len(h.ps)
415	p := pos{line: uint32(li), file: fi}
416
417	if l == 0 || *repeats || h.ps[l-1] != p {
418		h.ps = append(h.ps, p)
419		h.texts = append(h.texts, text)
420		h.vars = append(h.vars, []string{})
421		return true
422	}
423	return false
424}
425
426func (h *nextHist) addVar(text string) {
427	l := len(h.texts)
428	h.vars[l-1] = append(h.vars[l-1], text)
429}
430
431func invertMapSU8(hf2i map[string]uint8) map[uint8]string {
432	hi2f := make(map[uint8]string)
433	for hs, i := range hf2i {
434		hi2f[i] = hs
435	}
436	return hi2f
437}
438
439func (h *nextHist) equals(k *nextHist) bool {
440	if len(h.f2i) != len(k.f2i) {
441		return false
442	}
443	if len(h.ps) != len(k.ps) {
444		return false
445	}
446	hi2f := invertMapSU8(h.f2i)
447	ki2f := invertMapSU8(k.f2i)
448
449	for i, hs := range hi2f {
450		if hs != ki2f[i] {
451			return false
452		}
453	}
454
455	for i, x := range h.ps {
456		if k.ps[i] != x {
457			return false
458		}
459	}
460
461	for i, hv := range h.vars {
462		kv := k.vars[i]
463		if len(hv) != len(kv) {
464			return false
465		}
466		for j, hvt := range hv {
467			if hvt != kv[j] {
468				return false
469			}
470		}
471	}
472
473	return true
474}
475
476// canonFileName strips everything before "/src/" from a filename.
477// This makes file names portable across different machines,
478// home directories, and temporary directories.
479func canonFileName(f string) string {
480	i := strings.Index(f, "/src/")
481	if i != -1 {
482		f = f[i+1:]
483	}
484	return f
485}
486
487/* Delve */
488
489type delveState struct {
490	cmd  *exec.Cmd
491	tagg string
492	*ioState
493	atLineRe         *regexp.Regexp // "\n =>"
494	funcFileLinePCre *regexp.Regexp // "^> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)"
495	line             string
496	file             string
497	function         string
498}
499
500func newDelve(t testing.TB, tag, executable string, args ...string) dbgr {
501	cmd := testenv.Command(t, "dlv", "exec", executable)
502	cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb")
503	if len(args) > 0 {
504		cmd.Args = append(cmd.Args, "--")
505		cmd.Args = append(cmd.Args, args...)
506	}
507	s := &delveState{tagg: tag, cmd: cmd}
508	// HAHA Delve has control characters embedded to change the color of the => and the line number
509	// that would be '(\\x1b\\[[0-9;]+m)?' OR TERM=dumb
510	s.atLineRe = regexp.MustCompile("\n=>[[:space:]]+[0-9]+:(.*)")
511	s.funcFileLinePCre = regexp.MustCompile("> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)[)]\n")
512	s.ioState = newIoState(s.cmd)
513	return s
514}
515
516func (s *delveState) tag() string {
517	return s.tagg
518}
519
520func (s *delveState) stepnext(ss string) bool {
521	x := s.ioState.writeReadExpect(ss+"\n", "[(]dlv[)] ")
522	excerpts := s.atLineRe.FindStringSubmatch(x.o)
523	locations := s.funcFileLinePCre.FindStringSubmatch(x.o)
524	excerpt := ""
525	if len(excerpts) > 1 {
526		excerpt = excerpts[1]
527	}
528	if len(locations) > 0 {
529		fn := canonFileName(locations[2])
530		if *verbose {
531			if s.file != fn {
532				fmt.Printf("%s\n", locations[2]) // don't canonocalize verbose logging
533			}
534			fmt.Printf("  %s\n", locations[3])
535		}
536		s.line = locations[3]
537		s.file = fn
538		s.function = locations[1]
539		s.ioState.history.add(s.file, s.line, excerpt)
540		// TODO: here is where variable processing will be added.  See gdbState.stepnext as a guide.
541		// Adding this may require some amount of normalization so that logs are comparable.
542		return true
543	}
544	if *verbose {
545		fmt.Printf("DID NOT MATCH EXPECTED NEXT OUTPUT\nO='%s'\nE='%s'\n", x.o, x.e)
546	}
547	return false
548}
549
550func (s *delveState) start() {
551	if *dryrun {
552		fmt.Printf("%s\n", asCommandLine("", s.cmd))
553		fmt.Printf("b main.test\n")
554		fmt.Printf("c\n")
555		return
556	}
557	err := s.cmd.Start()
558	if err != nil {
559		line := asCommandLine("", s.cmd)
560		panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err))
561	}
562	s.ioState.readExpecting(-1, 5000, "Type 'help' for list of commands.")
563	s.ioState.writeReadExpect("b main.test\n", "[(]dlv[)] ")
564	s.stepnext("c")
565}
566
567func (s *delveState) quit() {
568	expect("", s.ioState.writeRead("q\n"))
569}
570
571/* Gdb */
572
573type gdbState struct {
574	cmd  *exec.Cmd
575	tagg string
576	args []string
577	*ioState
578	atLineRe         *regexp.Regexp
579	funcFileLinePCre *regexp.Regexp
580	line             string
581	file             string
582	function         string
583}
584
585func newGdb(t testing.TB, tag, executable string, args ...string) dbgr {
586	// Turn off shell, necessary for Darwin apparently
587	cmd := testenv.Command(t, gdb, "-nx",
588		"-iex", fmt.Sprintf("add-auto-load-safe-path %s/src/runtime", runtime.GOROOT()),
589		"-ex", "set startup-with-shell off", executable)
590	cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb")
591	s := &gdbState{tagg: tag, cmd: cmd, args: args}
592	s.atLineRe = regexp.MustCompile("(^|\n)([0-9]+)(.*)")
593	s.funcFileLinePCre = regexp.MustCompile(
594		"([^ ]+) [(][^)]*[)][ \\t\\n]+at ([^:]+):([0-9]+)")
595	// runtime.main () at /Users/drchase/GoogleDrive/work/go/src/runtime/proc.go:201
596	//                                    function              file    line
597	// Thread 2 hit Breakpoint 1, main.main () at /Users/drchase/GoogleDrive/work/debug/hist.go:18
598	s.ioState = newIoState(s.cmd)
599	return s
600}
601
602func (s *gdbState) tag() string {
603	return s.tagg
604}
605
606func (s *gdbState) start() {
607	run := "run"
608	for _, a := range s.args {
609		run += " " + a // Can't quote args for gdb, it will pass them through including the quotes
610	}
611	if *dryrun {
612		fmt.Printf("%s\n", asCommandLine("", s.cmd))
613		fmt.Printf("tbreak main.test\n")
614		fmt.Printf("%s\n", run)
615		return
616	}
617	err := s.cmd.Start()
618	if err != nil {
619		line := asCommandLine("", s.cmd)
620		panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err))
621	}
622	s.ioState.readSimpleExpecting("[(]gdb[)] ")
623	x := s.ioState.writeReadExpect("b main.test\n", "[(]gdb[)] ")
624	expect("Breakpoint [0-9]+ at", x)
625	s.stepnext(run)
626}
627
628func (s *gdbState) stepnext(ss string) bool {
629	x := s.ioState.writeReadExpect(ss+"\n", "[(]gdb[)] ")
630	excerpts := s.atLineRe.FindStringSubmatch(x.o)
631	locations := s.funcFileLinePCre.FindStringSubmatch(x.o)
632	excerpt := ""
633	addedLine := false
634	if len(excerpts) == 0 && len(locations) == 0 {
635		if *verbose {
636			fmt.Printf("DID NOT MATCH %s", x.o)
637		}
638		return false
639	}
640	if len(excerpts) > 0 {
641		excerpt = excerpts[3]
642	}
643	if len(locations) > 0 {
644		fn := canonFileName(locations[2])
645		if *verbose {
646			if s.file != fn {
647				fmt.Printf("%s\n", locations[2])
648			}
649			fmt.Printf("  %s\n", locations[3])
650		}
651		s.line = locations[3]
652		s.file = fn
653		s.function = locations[1]
654		addedLine = s.ioState.history.add(s.file, s.line, excerpt)
655	}
656	if len(excerpts) > 0 {
657		if *verbose {
658			fmt.Printf("  %s\n", excerpts[2])
659		}
660		s.line = excerpts[2]
661		addedLine = s.ioState.history.add(s.file, s.line, excerpt)
662	}
663
664	if !addedLine {
665		// True if this was a repeat line
666		return true
667	}
668	// Look for //gdb-<tag>=(v1,v2,v3) and print v1, v2, v3
669	vars := varsToPrint(excerpt, "//"+s.tag()+"=(")
670	for _, v := range vars {
671		response := printVariableAndNormalize(v, func(v string) string {
672			return s.ioState.writeReadExpect("p "+v+"\n", "[(]gdb[)] ").String()
673		})
674		s.ioState.history.addVar(response)
675	}
676	return true
677}
678
679// printVariableAndNormalize extracts any slash-indicated normalizing requests from the variable
680// name, then uses printer to get the value of the variable from the debugger, and then
681// normalizes and returns the response.
682func printVariableAndNormalize(v string, printer func(v string) string) string {
683	slashIndex := strings.Index(v, "/")
684	substitutions := ""
685	if slashIndex != -1 {
686		substitutions = v[slashIndex:]
687		v = v[:slashIndex]
688	}
689	response := printer(v)
690	// expect something like "$1 = ..."
691	dollar := strings.Index(response, "$")
692	cr := strings.Index(response, "\n")
693
694	if dollar == -1 { // some not entirely expected response, whine and carry on.
695		if cr == -1 {
696			response = strings.TrimSpace(response) // discards trailing newline
697			response = strings.Replace(response, "\n", "<BR>", -1)
698			return "$ Malformed response " + response
699		}
700		response = strings.TrimSpace(response[:cr])
701		return "$ " + response
702	}
703	if cr == -1 {
704		cr = len(response)
705	}
706	// Convert the leading $<number> into the variable name to enhance readability
707	// and reduce scope of diffs if an earlier print-variable is added.
708	response = strings.TrimSpace(response[dollar:cr])
709	response = leadingDollarNumberRe.ReplaceAllString(response, v)
710
711	// Normalize value as requested.
712	if strings.Contains(substitutions, "A") {
713		response = hexRe.ReplaceAllString(response, "<A>")
714	}
715	if strings.Contains(substitutions, "N") {
716		response = numRe.ReplaceAllString(response, "<N>")
717	}
718	if strings.Contains(substitutions, "S") {
719		response = stringRe.ReplaceAllString(response, "<S>")
720	}
721	if strings.Contains(substitutions, "O") {
722		response = optOutGdbRe.ReplaceAllString(response, "<Optimized out, as expected>")
723	}
724	return response
725}
726
727// varsToPrint takes a source code line, and extracts the comma-separated variable names
728// found between lookfor and the next ")".
729// For example, if line includes "... //gdb-foo=(v1,v2,v3)" and
730// lookfor="//gdb-foo=(", then varsToPrint returns ["v1", "v2", "v3"]
731func varsToPrint(line, lookfor string) []string {
732	var vars []string
733	if strings.Contains(line, lookfor) {
734		x := line[strings.Index(line, lookfor)+len(lookfor):]
735		end := strings.Index(x, ")")
736		if end == -1 {
737			panic(fmt.Sprintf("Saw variable list begin %s in %s but no closing ')'", lookfor, line))
738		}
739		vars = strings.Split(x[:end], ",")
740		for i, y := range vars {
741			vars[i] = strings.TrimSpace(y)
742		}
743	}
744	return vars
745}
746
747func (s *gdbState) quit() {
748	response := s.ioState.writeRead("q\n")
749	if strings.Contains(response.o, "Quit anyway? (y or n)") {
750		defer func() {
751			if r := recover(); r != nil {
752				if s, ok := r.(string); !(ok && strings.Contains(s, "'Y\n'")) {
753					// Not the panic that was expected.
754					fmt.Printf("Expected a broken pipe panic, but saw the following panic instead")
755					panic(r)
756				}
757			}
758		}()
759		s.ioState.writeRead("Y\n")
760	}
761}
762
763type ioState struct {
764	stdout  io.ReadCloser
765	stderr  io.ReadCloser
766	stdin   io.WriteCloser
767	outChan chan string
768	errChan chan string
769	last    tstring // Output of previous step
770	history *nextHist
771}
772
773func newIoState(cmd *exec.Cmd) *ioState {
774	var err error
775	s := &ioState{}
776	s.history = &nextHist{}
777	s.history.f2i = make(map[string]uint8)
778	s.stdout, err = cmd.StdoutPipe()
779	line := asCommandLine("", cmd)
780	if err != nil {
781		panic(fmt.Sprintf("There was an error [stdoutpipe] running '%s', %v\n", line, err))
782	}
783	s.stderr, err = cmd.StderrPipe()
784	if err != nil {
785		panic(fmt.Sprintf("There was an error [stdouterr] running '%s', %v\n", line, err))
786	}
787	s.stdin, err = cmd.StdinPipe()
788	if err != nil {
789		panic(fmt.Sprintf("There was an error [stdinpipe] running '%s', %v\n", line, err))
790	}
791
792	s.outChan = make(chan string, 1)
793	s.errChan = make(chan string, 1)
794	go func() {
795		buffer := make([]byte, 4096)
796		for {
797			n, err := s.stdout.Read(buffer)
798			if n > 0 {
799				s.outChan <- string(buffer[0:n])
800			}
801			if err == io.EOF || n == 0 {
802				break
803			}
804			if err != nil {
805				fmt.Printf("Saw an error forwarding stdout")
806				break
807			}
808		}
809		close(s.outChan)
810		s.stdout.Close()
811	}()
812
813	go func() {
814		buffer := make([]byte, 4096)
815		for {
816			n, err := s.stderr.Read(buffer)
817			if n > 0 {
818				s.errChan <- string(buffer[0:n])
819			}
820			if err == io.EOF || n == 0 {
821				break
822			}
823			if err != nil {
824				fmt.Printf("Saw an error forwarding stderr")
825				break
826			}
827		}
828		close(s.errChan)
829		s.stderr.Close()
830	}()
831	return s
832}
833
834func (s *ioState) hist() *nextHist {
835	return s.history
836}
837
838// writeRead writes ss, then reads stdout and stderr, waiting 500ms to
839// be sure all the output has appeared.
840func (s *ioState) writeRead(ss string) tstring {
841	if *verbose {
842		fmt.Printf("=> %s", ss)
843	}
844	_, err := io.WriteString(s.stdin, ss)
845	if err != nil {
846		panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err))
847	}
848	return s.readExpecting(-1, 500, "")
849}
850
851// writeReadExpect writes ss, then reads stdout and stderr until something
852// that matches expectRE appears.  expectRE should not be ""
853func (s *ioState) writeReadExpect(ss, expectRE string) tstring {
854	if *verbose {
855		fmt.Printf("=> %s", ss)
856	}
857	if expectRE == "" {
858		panic("expectRE should not be empty; use .* instead")
859	}
860	_, err := io.WriteString(s.stdin, ss)
861	if err != nil {
862		panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err))
863	}
864	return s.readSimpleExpecting(expectRE)
865}
866
867func (s *ioState) readExpecting(millis, interlineTimeout int, expectedRE string) tstring {
868	timeout := time.Millisecond * time.Duration(millis)
869	interline := time.Millisecond * time.Duration(interlineTimeout)
870	s.last = tstring{}
871	var re *regexp.Regexp
872	if expectedRE != "" {
873		re = regexp.MustCompile(expectedRE)
874	}
875loop:
876	for {
877		var timer <-chan time.Time
878		if timeout > 0 {
879			timer = time.After(timeout)
880		}
881		select {
882		case x, ok := <-s.outChan:
883			if !ok {
884				s.outChan = nil
885			}
886			s.last.o += x
887		case x, ok := <-s.errChan:
888			if !ok {
889				s.errChan = nil
890			}
891			s.last.e += x
892		case <-timer:
893			break loop
894		}
895		if re != nil {
896			if re.MatchString(s.last.o) {
897				break
898			}
899			if re.MatchString(s.last.e) {
900				break
901			}
902		}
903		timeout = interline
904	}
905	if *verbose {
906		fmt.Printf("<= %s%s", s.last.o, s.last.e)
907	}
908	return s.last
909}
910
911func (s *ioState) readSimpleExpecting(expectedRE string) tstring {
912	s.last = tstring{}
913	var re *regexp.Regexp
914	if expectedRE != "" {
915		re = regexp.MustCompile(expectedRE)
916	}
917	for {
918		select {
919		case x, ok := <-s.outChan:
920			if !ok {
921				s.outChan = nil
922			}
923			s.last.o += x
924		case x, ok := <-s.errChan:
925			if !ok {
926				s.errChan = nil
927			}
928			s.last.e += x
929		}
930		if re != nil {
931			if re.MatchString(s.last.o) {
932				break
933			}
934			if re.MatchString(s.last.e) {
935				break
936			}
937		}
938	}
939	if *verbose {
940		fmt.Printf("<= %s%s", s.last.o, s.last.e)
941	}
942	return s.last
943}
944
945// replaceEnv returns a new environment derived from env
946// by removing any existing definition of ev and adding ev=evv.
947func replaceEnv(env []string, ev string, evv string) []string {
948	if env == nil {
949		env = os.Environ()
950	}
951	evplus := ev + "="
952	var found bool
953	for i, v := range env {
954		if strings.HasPrefix(v, evplus) {
955			found = true
956			env[i] = evplus + evv
957		}
958	}
959	if !found {
960		env = append(env, evplus+evv)
961	}
962	return env
963}
964
965// asCommandLine renders cmd as something that could be copy-and-pasted into a command line
966// If cwd is not empty and different from the command's directory, prepend an appropriate "cd"
967func asCommandLine(cwd string, cmd *exec.Cmd) string {
968	s := "("
969	if cmd.Dir != "" && cmd.Dir != cwd {
970		s += "cd" + escape(cmd.Dir) + ";"
971	}
972	for _, e := range cmd.Env {
973		if !strings.HasPrefix(e, "PATH=") &&
974			!strings.HasPrefix(e, "HOME=") &&
975			!strings.HasPrefix(e, "USER=") &&
976			!strings.HasPrefix(e, "SHELL=") {
977			s += escape(e)
978		}
979	}
980	for _, a := range cmd.Args {
981		s += escape(a)
982	}
983	s += " )"
984	return s
985}
986
987// escape inserts escapes appropriate for use in a shell command line
988func escape(s string) string {
989	s = strings.Replace(s, "\\", "\\\\", -1)
990	s = strings.Replace(s, "'", "\\'", -1)
991	// Conservative guess at characters that will force quoting
992	if strings.ContainsAny(s, "\\ ;#*&$~?!|[]()<>{}`") {
993		s = " '" + s + "'"
994	} else {
995		s = " " + s
996	}
997	return s
998}
999
1000func expect(want string, got tstring) {
1001	if want != "" {
1002		match, err := regexp.MatchString(want, got.o)
1003		if err != nil {
1004			panic(fmt.Sprintf("Error for regexp %s, %v\n", want, err))
1005		}
1006		if match {
1007			return
1008		}
1009		// Ignore error as we have already checked for it before
1010		match, _ = regexp.MatchString(want, got.e)
1011		if match {
1012			return
1013		}
1014		fmt.Printf("EXPECTED '%s'\n GOT O='%s'\nAND E='%s'\n", want, got.o, got.e)
1015	}
1016}
1017