Skip to content

Commit

Permalink
Implement 'godebug run'
Browse files Browse the repository at this point in the history
About #8

Also some small bits of refactoring
  • Loading branch information
jeremyschlatter committed Mar 30, 2015
1 parent a4b1341 commit 5e8843d
Show file tree
Hide file tree
Showing 52 changed files with 651 additions and 107 deletions.
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,35 @@ For more detail, see the [end of this README](#how-it-works-more-detail).
$ go get github.com/mailgun/godebug


### Use:
### Getting started:

If the code you want to debug is in package main, using godebug is very straightforward.

First, insert this breakpoint anywhere in a (main) source file you want to debug:

godebug.SetTrace()

You'll need to import the godebug package there, too:

import "github.com/mailgun/godebug/lib"

Then just run your code using the godebug tool:

$ godebug run gofiles... [arguments...]

And that's it!

#### Generating code manually:

But, of course, not all code lives in package main. While commands `godebug build` and `godebug test` are coming soon, for now you will have to generate the source code yourself and use `go build` or `go test` on the result.

First, get your directory in a clean state. **The command below will overwrite your files, so make sure you have committed or stashed everything.**

In any file where you want a breakpoint, import `github.com/mailgun/godebug/lib` and insert this breakpoint anywhere in the code: `godebug.SetTrace()`. Then run:
In any file where you want a breakpoint, import `github.com/mailgun/godebug/lib` and insert this `godebug.SetTrace()` as above. Then run:

$ godebug -w .

Your code is now self-debugging. Congrats! Run it with `go run`, test it with `go test`, or build then run it with `go build`.
Your code is now self-debugging. Run it with `go run`, test it with `go test`, or build then run it with `go build`.


### Debugger commands:
Expand Down
18 changes: 12 additions & 6 deletions astutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func astPrintf(format string, a ...interface{}) []ast.Stmt {
r := io.TeeReader(makeReader(format, a), &buf)
f, err := parser.ParseFile(token.NewFileSet(), "", r, 0)
if err != nil {
io.Copy(os.Stdout, &buf)
_, _ = io.Copy(os.Stdout, &buf)
panic(err)
}
body := f.Decls[0].(*ast.FuncDecl).Body
Expand All @@ -89,15 +89,21 @@ func main() {`
progEnd = "}"
)

func fprintFieldList(w io.Writer, lst *ast.FieldList) error {
var buf bytes.Buffer
if err := printer.Fprint(&buf, token.NewFileSet(), &ast.FuncLit{Type: &ast.FuncType{Params: lst}}); err != nil {
return err
}
b := buf.Bytes()
_, err := w.Write(b[len("func(") : len(b)-len(")")])
return err
}

func fprint(w io.Writer, a interface{}) error {
switch x := a.(type) {
case *ast.FieldList:
// printer.Fprint does not support this type. Hack around it.
var buf bytes.Buffer
printer.Fprint(&buf, token.NewFileSet(), &ast.FuncLit{Type: &ast.FuncType{Params: x}})
b := buf.Bytes()
_, err := w.Write(b[len("func(") : len(b)-len(")")])
return err
return fprintFieldList(w, x)
case ast.Node, []ast.Decl, []ast.Stmt:
return printer.Fprint(w, token.NewFileSet(), x)
case []ast.Expr:
Expand Down
237 changes: 237 additions & 0 deletions cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package main

import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"

"bitbucket.org/JeremySchlatter/go-atexit"

"github.com/mailgun/godebug/Godeps/_workspace/src/golang.org/x/tools/go/loader"
)

var w = flag.Bool("w", false, "write result to (source) file instead of stdout")

func usage() {
log.Print(
`godebug is a tool for debugging Go programs.
Usage:
godebug command [arguments]
The commands are:
run compile, run, and debug a Go program
output generate debug source code, but do not build or run it
Use "godebug help [command]" for more information about a command.
`)
exit(0)
}

func runUsage() {
log.Print(
`usage: godebug run gofiles... [arguments...]
Run is a wrapper around 'go run'. It generates debugging code for
the named Go source files and runs 'go run' on the result.
`)
}

func outputUsage() {
log.Print(
`usage: godebug output [-w] <packages>
Output outputs debugging code for <packages>.
By default, output will print the resulting code to stdout.
If the -w flag is given, output will overwrite the original
source files. Use with caution.
<packages> may take one of two forms:
1. A list of *.go source files.
All of the specified files are loaded, parsed and type-checked
as a single package. All the files must belong to the same directory.
2. A list of import paths, each denoting a package.
The package's directory is found relative to the $GOROOT and
$GOPATH using similar logic to 'go build', and the *.go files in
that directory are loaded, parsed and type-checked as a single
package.
In addition, all *_test.go files in the directory are then loaded
and parsed. Those files whose package declaration equals that of
the non-*_test.go files are included in the primary package. Test
files whose package declaration ends with "_test" are type-checked
as another package, the 'external' test package, so that a single
import path may denote two packages.
`)
}

func main() {
log.SetFlags(0)
flag.Parse()
flag.Usage = usage
if flag.NArg() == 0 {
usage()
}
switch flag.Arg(0) {
case "help":
doHelp()
case "output":
doOutput()
case "run":
doRun()
default:
usage()
}
}

func doHelp() {
if flag.NArg() < 2 {
usage()
}
switch flag.Arg(1) {
case "output":
outputUsage()
case "run":
runUsage()
default:
log.Printf("Unknown help topic `%s`. Run 'godebug help'.\n", flag.Arg(1))
}
}

func doRun() {
atexit.TrapSignals()
defer atexit.CallExitFuncs()

tmpDir := makeTmpDir()
atexit.Run(func() {
removeDir(tmpDir)
})

var gofiles []string
for _, arg := range flag.Args()[1:] {
if !strings.HasSuffix(arg, ".go") {
break
}
gofiles = append(gofiles, arg)
}
if len(gofiles) == 0 {
logFatal("godebug run: no go files listed")
}
var conf loader.Config
conf.SourceImports = true
if err := conf.CreateFromFilenames("main", gofiles...); err != nil {
logFatal(err)
}
prog, err := conf.Load()
if err != nil {
logFatal(err)
}
generate(prog, func(filename string) io.WriteCloser {
f, err := os.Create(filepath.Join(tmpDir, filepath.Base(filename)))
if err != nil {
logFatal(err)
}
return f
})
args := []string{"run"}
args = append(args, mapToTmpDir(tmpDir, gofiles)...)
shell("go", args...)
}

func shell(command string, args ...string) {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
err := cmd.Run()
switch err.(type) {
case nil:
case *exec.ExitError:
exit(1)
default:
log.Fatal(err)
}
}

func mapToTmpDir(tmpDir string, gofiles []string) []string {
result := make([]string, len(gofiles))
for i := range gofiles {
result[i] = filepath.Join(tmpDir, filepath.Base(gofiles[i]))
}
return result
}

func makeTmpDir() (dirname string) {
tmp, err := ioutil.TempDir("", "godebug")
if err != nil {
logFatal("Failed to create temporary directory:", err)
}
return tmp
}

func removeDir(dir string) {
if err := os.RemoveAll(dir); err != nil {
log.Print("Failed to clean up temporary directory:", err)
}
}

func doOutput() {
var conf loader.Config
rest, err := conf.FromArgs(flag.Args()[1:], true)
if len(rest) > 0 {
fmt.Fprintf(os.Stderr, "Unrecognized arguments:\n%v\n\n", strings.Join(rest, "\n"))
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error identifying packages: %v\n\n", err)
}
if len(rest) > 0 || err != nil {
flag.Usage()
}
conf.SourceImports = true
prog, err := conf.Load()
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading packages: %v\n\n", err)
flag.Usage()
}
generate(prog, func(filename string) io.WriteCloser {
if *w {
file, err := os.Create(filename)
if err != nil {
logFatal(err)
}
return file
}
return nopCloser{os.Stdout}
})
}

type nopCloser struct {
io.Writer
}

func (nopCloser) Close() error {
return nil
}

func logFatal(v ...interface{}) {
atexit.CallExitFuncs()
log.Fatal(v...)
}

func exit(n int) {
atexit.CallExitFuncs()
os.Exit(n)
}
80 changes: 80 additions & 0 deletions endtoend_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
)

// This file runs tests in the testdata directory, excluding those in testdata/single-file-tests

func TestCLISessions(t *testing.T) {
godebug := compileGodebug(t)
defer os.Remove(godebug)

// Read the testdata directory
fd, err := os.Open("testdata")
if err != nil {
t.Fatal(err)
}
defer fd.Close()
names, err := fd.Readdirnames(-1)
if err != nil {
t.Fatal("Readdirnames:", err)
}
tests := make([]string, 0, len(names))
for _, name := range names {
if strings.HasSuffix(name, ".txt") {
tests = append(tests, name)
}
}

// Run tests in parallel
var wg sync.WaitGroup
wg.Add(len(tests))
for _, test := range tests {
go func(filename string) {
defer wg.Done()
runTest(t, godebug, filename)
}(filepath.Join("testdata", test))
}
wg.Wait()
}

func runTest(t *testing.T, godebug, filename string) {
var buf bytes.Buffer
session := parseSession(t, filename)
cmd := exec.Command(godebug, session.cmd[1:]...)
cmd.Dir = filepath.FromSlash("testdata/test-filesystem/" + session.workingDir)
cmd.Stdout = &buf
cmd.Stderr = &buf
cmd.Stdin = bytes.NewReader(session.input)
setGopath(t, cmd)
if err := cmd.Run(); err != nil {
t.Fatalf("Command 'godebug %v' failed to run: %v\n%s", strings.Join(session.cmd[1:], " "), err, buf.Bytes())
}
checkOutput(t, session, buf.Bytes())
}

func setGopath(t *testing.T, cmd *exec.Cmd) {
cmd.Env = os.Environ()
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
gopath := filepath.Join(cwd, "testdata", "test-filesystem", "gopath")
sawGopath := false
for i := range cmd.Env {
keyVal := strings.SplitN(cmd.Env[i], "=", 2)
if keyVal[0] == "GOPATH" {
cmd.Env[i] = "GOPATH=" + gopath + string(filepath.ListSeparator) + keyVal[1]
}
}
if !sawGopath {
cmd.Env = append(cmd.Env, "GOPATH="+gopath)
}
}
Loading

0 comments on commit 5e8843d

Please sign in to comment.