Repository: magefile/mage (go)

Issue:
Librarify Mage
Some of the tests are forced to use `go run` to run full-stack tests for go.  It would be a lot easier and better if we could just run the mage main function on a specific directory. This should be pretty trivial.

This will also open up the ability to have one magefile run a target in another magefile (outside the current directory).

Pull request:
Librarify mage
We can now use mage as a pure library, not just as a main function.
This makes test easier in many ways and opens up new ways to use mage.
This closes #33.

Patch:
diff --git a/mage/main.go b/mage/main.go
index c7eb170..335a27c 100644
--- a/mage/main.go
+++ b/mage/main.go
@@ -13,13 +13,13 @@ import (
 	"runtime"
 	"sort"
 	"strings"
-	"syscall"
 	"text/template"
 	"unicode"
 
 	"github.com/magefile/mage/build"
 	"github.com/magefile/mage/mg"
 	"github.com/magefile/mage/parse"
+	"github.com/magefile/mage/sh"
 )
 
 // mageVer is used when hashing the output binary to ensure that we get a new
@@ -39,104 +39,181 @@ const mainfile = "mage_output_file.go"
 const initFile = "magefile.go"
 
 var (
-	force, verbose, list, help, mageInit, keep, showVersion bool
-
-	timestamp, commitHash, gitTag string
+	timestamp, commitHash, gitTag string // set by ldflags when you "mage build"
 )
 
-func init() {
-	flag.BoolVar(&force, "f", false, "force recreation of compiled magefile")
-	flag.BoolVar(&verbose, "v", false, "show verbose output when running mage targets")
-	flag.BoolVar(&list, "l", false, "list mage targets in this directory")
-	flag.BoolVar(&help, "h", false, "show this help")
-	flag.BoolVar(&mageInit, "init", false, "create a starting template if no mage files exist")
-	flag.BoolVar(&keep, "keep", false, "keep intermediate mage files around after running")
-	flag.BoolVar(&showVersion, "version", false, "show version info for the mage binary")
-	flag.Usage = func() {
-		fmt.Println("mage [options] [target]")
-		fmt.Println("Options:")
-		flag.PrintDefaults()
-	}
-}
-
 // Main is the entrypoint for running mage.  It exists external to mage's main
 // function to allow it to be used from other programs, specifically so you can
 // go run a simple file that run's mage's Main.
 func Main() int {
-	log.SetFlags(0)
-	flag.Parse()
-	if help && len(flag.Args()) == 0 {
-		flag.Usage()
+	return ParseAndRun(".", os.Stdout, os.Stderr, os.Args[1:])
+}
+
+// Invocation contains the args for invoking a run of Mage.
+type Invocation struct {
+	Dir     string    // directory to read magefiles from
+	Force   bool      // forces recreation of the compiled binary
+	Verbose bool      // tells the magefile to print out log statements
+	List    bool      // tells the magefile to print out a list of targets
+	Help    bool      // tells the magefile to print out help for a specific target
+	Keep    bool      // tells mage to keep the generated main file after compiling
+	Stdout  io.Writer // writer to write stdout messages to
+	Stderr  io.Writer // writer to write stderr messages to
+	Args    []string  // args to pass to the compiled binary
+}
+
+// ParseAndRun parses the command line, and then compiles and runs the mage
+// files in the given directory with the given args (do not include the command
+// name in the args).
+func ParseAndRun(dir string, stdout, stderr io.Writer, args []string) int {
+	log := log.New(stderr, "", 0)
+	inv, mageInit, showVersion, err := Parse(stderr, args)
+	inv.Dir = dir
+	inv.Stdout = stdout
+	if err == flag.ErrHelp {
 		return 0
 	}
+	if err != nil {
+		log.Println("Error:", err)
+		return 2
+	}
+
 	if showVersion {
-		fmt.Println("Mage Build Tool", gitTag)
-		fmt.Println("Build Date:", timestamp)
-		fmt.Println("Commit:", commitHash)
+		if timestamp == "" {
+			timestamp = "<not set>"
+		}
+		if commitHash == "" {
+			commitHash = "<not set>"
+		}
+		log.Println("Mage Build Tool", gitTag)
+		log.Println("Build Date:", timestamp)
+		log.Println("Commit:", commitHash)
 		return 0
 	}
-	files, err := magefiles()
-	if err != nil {
-		fmt.Println(err)
-		return 1
-	}
-	if len(files) == 0 && mageInit {
-		if err := generateInit(); err != nil {
-			log.Printf("%+v", err)
+	if mageInit {
+		if err := generateInit(dir); err != nil {
+			log.Println("Error:", err)
 			return 1
 		}
 		log.Println(initFile, "created")
 		return 0
-	} else if len(files) == 0 {
-		log.Print("No .go files marked with the mage build tag in this directory.")
-		return 1
 	}
+	return Invoke(inv)
+}
+
+// Parse parses the given args and returns structured data.  If parse returns
+// flag.ErrHelp, the calling process should exit with code 0.
+func Parse(stderr io.Writer, args []string) (inv Invocation, mageInit, showVersion bool, err error) {
+	inv.Stderr = stderr
+	fs := flag.FlagSet{}
+	fs.SetOutput(stderr)
+	fs.BoolVar(&inv.Force, "f", false, "force recreation of compiled magefile")
+	fs.BoolVar(&inv.Verbose, "v", false, "show verbose output when running mage targets")
+	fs.BoolVar(&inv.List, "l", false, "list mage targets in this directory")
+	fs.BoolVar(&inv.Help, "h", false, "show this help")
+	fs.BoolVar(&mageInit, "init", false, "create a starting template if no mage files exist")
+	fs.BoolVar(&inv.Keep, "keep", false, "keep intermediate mage files around after running")
+	fs.BoolVar(&showVersion, "version", false, "show version info for the mage binary")
+	fs.Usage = func() {
+		fmt.Fprintln(stderr, "mage [options] [target]")
+		fmt.Fprintln(stderr, "Options:")
+		fs.PrintDefaults()
+	}
+	err = fs.Parse(args)
+	if err == flag.ErrHelp {
+		fs.Usage()
+		return inv, mageInit, showVersion, err
+	}
+	if err != nil {
+		inv.Args = fs.Args()
+		if inv.Help && len(inv.Args) == 0 {
+			fs.Usage()
+			// tell upstream, to just exit
+			return inv, mageInit, showVersion, flag.ErrHelp
+		}
+	}
+	return inv, mageInit, showVersion, err
+}
 
-	out, err := ExeName(files)
+// Invoke runs Mage with the given arguments.
+func Invoke(inv Invocation) int {
+	log := log.New(inv.Stderr, "", 0)
 
+	files, err := Magefiles(inv.Dir)
 	if err != nil {
-		log.Printf("%+v", err)
+		log.Println("Error:", err)
 		return 1
 	}
 
-	if !force {
-		if _, err := os.Stat(out); err == nil {
-			return run(out, flag.Args()...)
-		}
+	if len(files) == 0 {
+		log.Println("No .go files marked with the mage build tag in this directory.")
+		return 1
 	}
 
-	info, err := parse.Package(".", files)
+	exePath, err := ExeName(files)
+
 	if err != nil {
-		log.Printf("%v", err)
+		log.Println("Error:", err)
 		return 1
 	}
 
-	names := map[string][]string{}
-	lowers := map[string]bool{}
-	hasDupes := false
-	for _, f := range info.Funcs {
-		low := strings.ToLower(f.Name)
-		if lowers[low] {
-			hasDupes = true
+	if !inv.Force {
+		if _, err := os.Stat(exePath); err == nil {
+			return RunCompiled(inv, exePath)
 		}
-		lowers[low] = true
-		names[low] = append(names[low], f.Name)
 	}
+
+	// parse wants dir + filenames... arg
+	fnames := make([]string, 0, len(files))
+	for i := range files {
+		fnames = append(fnames, filepath.Base(files[i]))
+	}
+	info, err := parse.Package(inv.Dir, fnames)
+	if err != nil {
+		log.Println("Error:", err)
+		return 1
+	}
+
+	hasDupes, names := CheckDupes(info)
 	if hasDupes {
-		fmt.Println("Build targets must be case insensitive, thus the follow targets conflict:")
+		log.Println("Build targets must be case insensitive, thus the follow targets conflict:")
 		for _, v := range names {
 			if len(v) > 1 {
-				fmt.Println("  " + strings.Join(v, ", "))
+				log.Println("  " + strings.Join(v, ", "))
 			}
 		}
-		os.Exit(1)
+		return 1
 	}
 
-	if err := compile(out, info, files); err != nil {
-		log.Print(err)
+	main := filepath.Join(inv.Dir, mainfile)
+	if err := GenerateMainfile(main, info); err != nil {
+		log.Println("Error:", err)
+		return 1
+	}
+	if !inv.Keep {
+		defer os.Remove(main)
+	}
+	files = append(files, main)
+	if err := Compile(exePath, inv.Stdout, inv.Stderr, files); err != nil {
+		log.Println("Error:", err)
 		return 1
 	}
-	return run(out, flag.Args()...)
+	return RunCompiled(inv, exePath)
+}
+
+// CheckDupes checks a package for duplicate target names.
+func CheckDupes(info *parse.PkgInfo) (hasDupes bool, names map[string][]string) {
+	names = map[string][]string{}
+	lowers := map[string]bool{}
+	for _, f := range info.Funcs {
+		low := strings.ToLower(f.Name)
+		if lowers[low] {
+			hasDupes = true
+		}
+		lowers[low] = true
+		names[low] = append(names[low], f.Name)
+	}
+	return hasDupes, names
 }
 
 type data struct {
@@ -145,31 +222,46 @@ type data struct {
 	Default      string
 }
 
-func magefiles() ([]string, error) {
+// Magefiles returns the list of magefiles in dir.
+func Magefiles(dir string) ([]string, error) {
 	ctx := build.Default
 	ctx.RequiredTags = []string{"mage"}
 	ctx.BuildTags = []string{"mage"}
-	p, err := ctx.ImportDir(".", 0)
+	p, err := ctx.ImportDir(dir, 0)
 	if err != nil {
 		if _, ok := err.(*build.NoGoError); ok {
 			return []string{}, nil
 		}
 		return nil, err
 	}
+	for i := range p.GoFiles {
+		p.GoFiles[i] = filepath.Join(dir, p.GoFiles[i])
+	}
 	return p.GoFiles, nil
 }
 
-func compile(out string, info *parse.PkgInfo, gofiles []string) error {
-	if err := os.MkdirAll(filepath.Dir(out), 0700); err != nil {
-		return fmt.Errorf("can't create cachedir: %v", err)
+// Compile uses the go tool to compile the files into an executable at path.
+func Compile(path string, stdout, stderr io.Writer, gofiles []string) error {
+	c := exec.Command("go", append([]string{"build", "-o", path}, gofiles...)...)
+	c.Env = os.Environ()
+	c.Stderr = stderr
+	c.Stdout = stdout
+	err := c.Run()
+	if err != nil {
+		return errors.New("error compiling magefiles")
+	}
+	if _, err := os.Stat(path); err != nil {
+		return errors.New("failed to find compiled magefile")
 	}
-	f, err := os.Create(mainfile)
+	return nil
+}
+
+// GenerateMainfile creates the mainfile at path with the info from
+func GenerateMainfile(path string, info *parse.PkgInfo) error {
+	f, err := os.Create(path)
 	if err != nil {
 		return fmt.Errorf("can't create mainfile: %v", err)
 	}
-	if !keep {
-		defer os.Remove(mainfile)
-	}
 	defer f.Close()
 
 	data := data{
@@ -182,14 +274,6 @@ func compile(out string, info *parse.PkgInfo, gofiles []string) error {
 	if err := output.Execute(f, data); err != nil {
 		return fmt.Errorf("can't execute mainfile template: %v", err)
 	}
-	// close is idenmpotent, so this is ok
-	f.Close()
-	if 0 != run("go", append([]string{"build", "-o", out, mainfile}, gofiles...)...) {
-		return errors.New("error compiling magefiles")
-	}
-	if _, err := os.Stat(out); err != nil {
-		return errors.New("failed to find compiled magefile")
-	}
 	return nil
 }
 
@@ -232,8 +316,8 @@ func hashFile(fn string) (string, error) {
 	return fmt.Sprintf("%x", h.Sum(nil)), nil
 }
 
-func generateInit() error {
-	f, err := os.Create(initFile)
+func generateInit(dir string) error {
+	f, err := os.Create(filepath.Join(dir, initFile))
 	if err != nil {
 		return fmt.Errorf("could not create mage template: %v", err)
 	}
@@ -246,26 +330,20 @@ func generateInit() error {
 	return nil
 }
 
-func run(cmd string, args ...string) int {
-	c := exec.Command(cmd, args...)
-	c.Stderr = os.Stderr
-	c.Stdout = os.Stdout
+// RunCompiled runs an already-compiled mage command with the given args,
+func RunCompiled(inv Invocation, exePath string) int {
+	c := exec.Command(exePath, inv.Args...)
+	c.Stderr = inv.Stderr
+	c.Stdout = inv.Stdout
 	c.Env = os.Environ()
-	if verbose {
+	if inv.Verbose {
 		c.Env = append(c.Env, "MAGEFILE_VERBOSE=1")
 	}
-	if list {
+	if inv.List {
 		c.Env = append(c.Env, "MAGEFILE_LIST=1")
 	}
-	if help {
+	if inv.Help {
 		c.Env = append(c.Env, "MAGEFILE_HELP=1")
 	}
-	err := c.Run()
-	if err != nil {
-		if e, ok := err.(*exec.ExitError); ok {
-			return e.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
-		}
-		return 1
-	}
-	return 0
+	return sh.ExitStatus(c.Run())
 }
diff --git a/mage/template.go b/mage/template.go
index b8f7b62..2a5fc3a 100644
--- a/mage/template.go
+++ b/mage/template.go
@@ -16,9 +16,13 @@ import (
 
 func main() {
 	log.SetFlags(0)
+	if os.Getenv("MAGEFILE_VERBOSE") == "" {
+		log.SetOutput(ioutil.Discard)
+	}
+	logger := log.New(os.Stderr, "", 0)
 	if os.Getenv("MAGEFILE_LIST") != "" {
 		if err := list(); err != nil {
-			fmt.Println(err)
+			log.Println(err)
 			os.Exit(1)
 		}
 		return
@@ -26,7 +30,7 @@ func main() {
 
 	if os.Getenv("MAGEFILE_HELP") != "" {
 		if len(os.Args) < 2 {
-			fmt.Println("no target specified")
+			logger.Println("no target specified")
 			os.Exit(1)
 		}
 		switch strings.ToLower(os.Args[1]) {
@@ -36,17 +40,15 @@ func main() {
 				return
 			{{end}}
 			default:
-				fmt.Printf("Unknown target: %q\n", os.Args[1])
+				logger.Printf("Unknown target: %q\n", os.Args[1])
 				os.Exit(1)
 		}	}
 
-	if os.Getenv("MAGEFILE_VERBOSE") == "" {
-		log.SetOutput(ioutil.Discard)
-	}
+
 	defer func() {
 		err := recover()
 		if err != nil {
-			fmt.Printf("Error: %v\n", err)
+			logger.Printf("Error: %v\n", err)
 			type code interface { ExitStatus() int }
 			if c, ok := err.(code); ok {
 				os.Exit(c.ExitStatus())
@@ -58,7 +60,7 @@ func main() {
 	{{- if .Default}}
 		{{- if .DefaultError}}
 		if err := Default(); err != nil {
-			fmt.Println(err)
+			logger.Println(err)
 			os.Exit(1)
 		}
 		return
@@ -68,7 +70,7 @@ func main() {
 		return
 	{{- else}}
 		if err := list(); err != nil {
-			fmt.Println(err)
+			logger.Println("Error:", err)
 			os.Exit(1)
 		}
 		return
@@ -79,7 +81,7 @@ func main() {
 	case "{{lower .Name}}":
 		{{if .IsError -}}
 			if err := {{.Name}}(); err != nil {
-				fmt.Printf("Error: %v\n", err)
+				logger.Printf("Error: %v\n", err)
 				os.Exit(1)
 			}
 		{{else -}}
@@ -87,7 +89,7 @@ func main() {
 		{{- end}}
 	{{end}}
 	default:
-		fmt.Printf("Unknown target: %q\n", os.Args[1])
+		logger.Printf("Unknown target: %q\n", os.Args[1])
 		os.Exit(1)
 	}
 }
@@ -95,7 +97,7 @@ func main() {
 func list() error {
 	{{$default := .Default}}
 	w := tabwriter.NewWriter(os.Stdout, 0, 4, 4, ' ', 0)
-	log.Println("Targets: ")
+	fmt.Println("Targets:")
 	{{- range .Funcs}}
 	fmt.Fprintln(w, "  {{lowerfirst .Name}}{{if eq .Name $default}}*{{end}}\t{{.Synopsis}}")
 	{{- end}}


Test patch:
diff --git a/mage/main_test.go b/mage/main_test.go
index e3f5925..0493e91 100644
--- a/mage/main_test.go
+++ b/mage/main_test.go
@@ -1,7 +1,9 @@
 package mage
 
 import (
+	"bytes"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"os"
 	"os/exec"
@@ -49,42 +51,55 @@ func TestGoRun(t *testing.T) {
 }
 
 func TestVerbose(t *testing.T) {
-	c := exec.Command("go", "run", "main.go", "testverbose")
-	c.Dir = "./testdata"
-	c.Env = os.Environ()
-	b, err := c.CombinedOutput()
-	if err != nil {
-		t.Error("error:", err)
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata",
+		Stdout: stdout,
+		Stderr: stderr,
+		Args:   []string{"testverbose"},
 	}
-	actual := string(b)
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
+	}
+	actual := stdout.String()
 	expected := ""
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
-	c = exec.Command("go", "run", "main.go", "-v", "testverbose")
-	c.Dir = "./testdata"
-	c.Env = os.Environ()
-	b, err = c.CombinedOutput()
-	if err != nil {
-		t.Error("error:", err)
+	stderr.Reset()
+	stdout.Reset()
+	inv.Verbose = true
+	code = Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
 	}
-	actual = string(b)
+
+	actual = stderr.String()
 	expected = "hi!\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
 }
 
-func TestGoRunList(t *testing.T) {
-	c := exec.Command("go", "run", "main.go", "-l")
-	c.Dir = "./testdata"
-	c.Env = os.Environ()
-	b, err := c.CombinedOutput()
-	if err != nil {
-		t.Error("error:", err)
+func TestList(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata",
+		Stdout: stdout,
+		Stderr: ioutil.Discard,
+		List:   true,
 	}
-	actual := string(b)
-	expected := `Targets: 
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
+	}
+	actual := stdout.String()
+	expected := `
+Targets:
   panics                Function that panics.
   panicsErr             Error function that panics.
   returnsError*         Synopsis for returns error.
@@ -93,75 +108,90 @@ func TestGoRunList(t *testing.T) {
   testVerbose           
 
 * default target
-`
+`[1:]
 	if actual != expected {
-		t.Fatalf("expected %q, but got %q", expected, actual)
+		t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
 	}
 }
 
-func TestGoRunListNoDefault(t *testing.T) {
-	c := exec.Command("go", "run", "main.go")
-	c.Dir = "./testdata/no_default"
-	c.Env = os.Environ()
-	b, err := c.CombinedOutput()
-	if err != nil {
-		t.Error("error:", err)
-	}
-	actual := string(b)
-	expected := `  bazBuz    Prints out 'BazBuz'.
+func TestNoArgNoDefaultList(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "testdata/no_default",
+		Stdout: stdout,
+		Stderr: stderr,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v", code)
+	}
+	if err := stderr.String(); err != "" {
+		t.Errorf("unexpected stderr output:\n%s", err)
+	}
+	actual := stdout.String()
+	expected := `
+Targets:
+  bazBuz    Prints out 'BazBuz'.
   fooBar    Prints out 'FooBar'.
-`
+`[1:]
 	if actual != expected {
-		t.Fatalf("expected %q, but got %q", expected, actual)
+		t.Fatalf("expected:\n%q\n\ngot:\n%q", expected, actual)
 	}
 }
 
-func TestGoRunError(t *testing.T) {
-	c := exec.Command("go", "run", "main.go", "returnsnonnilerror")
-	c.Dir = "./testdata"
-	c.Env = os.Environ()
-	b, err := c.CombinedOutput()
-	actualErr := err.Error()
-	expectedErr := "exit status 1"
-	if actualErr != expectedErr {
-		t.Fatalf("expected %q, but got %q", expectedErr, actualErr)
-	}
-	actual := string(b)
-	expected := "Error: bang!\nexit status 1\n"
+func TestTargetError(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata",
+		Stdout: ioutil.Discard,
+		Stderr: stderr,
+		Args:   []string{"returnsnonnilerror"},
+	}
+	code := Invoke(inv)
+	if code != 1 {
+		t.Fatalf("expected 1, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "Error: bang!\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
 }
 
-func TestGoRunPanics(t *testing.T) {
-	c := exec.Command("go", "run", "main.go", "panics")
-	c.Dir = "./testdata"
-	c.Env = os.Environ()
-	b, err := c.CombinedOutput()
-	actualErr := err.Error()
-	expectedErr := "exit status 1"
-	if actualErr != expectedErr {
-		t.Fatalf("expected %q, but got %q", expectedErr, actualErr)
-	}
-	actual := string(b)
-	expected := "Error: boom!\nexit status 1\n"
+func TestTargetPanics(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata",
+		Stdout: ioutil.Discard,
+		Stderr: stderr,
+		Args:   []string{"panics"},
+	}
+	code := Invoke(inv)
+	if code != 1 {
+		t.Fatalf("expected 1, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "Error: boom!\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
 }
 
-func TestGoRunPanicsErr(t *testing.T) {
-	c := exec.Command("go", "run", "main.go", "panicserr")
-	c.Dir = "./testdata"
-	c.Env = os.Environ()
-	b, err := c.CombinedOutput()
-	actualErr := err.Error()
-	expectedErr := "exit status 1"
-	if actualErr != expectedErr {
-		t.Fatalf("expected %q, but got %q", expectedErr, actualErr)
-	}
-	actual := string(b)
-	expected := "Error: kaboom!\nexit status 1\n"
+func TestPanicsErr(t *testing.T) {
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata",
+		Stdout: ioutil.Discard,
+		Stderr: stderr,
+		Args:   []string{"panicserr"},
+	}
+	code := Invoke(inv)
+	if code != 1 {
+		t.Fatalf("expected 1, but got %v", code)
+	}
+	actual := stderr.String()
+	expected := "Error: kaboom!\n"
 	if actual != expected {
 		t.Fatalf("expected %q, but got %q", expected, actual)
 	}
@@ -188,16 +218,20 @@ func TestHashTemplate(t *testing.T) {
 func TestKeepFlag(t *testing.T) {
 	buildFile := fmt.Sprintf("./testdata/keep_flag/%s", mainfile)
 	os.Remove(buildFile)
-	c := exec.Command("go", "run", "main.go", "-keep")
-	c.Dir = "./testdata/keep_flag"
-	c.Env = os.Environ()
-	_, err := c.CombinedOutput()
-	if err != nil {
-		t.Error("error:", err)
+	defer os.Remove(buildFile)
+	inv := Invocation{
+		Dir:    "./testdata/keep_flag",
+		Stdout: ioutil.Discard,
+		Stderr: ioutil.Discard,
+		Args:   []string{"noop"},
+		Keep:   true,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected code 0, but got %v", code)
 	}
 
-	if _, err := os.Stat(fmt.Sprintf(buildFile)); os.IsNotExist(err) {
-		t.Fatalf("expected file %q to exist but it did not", buildFile)
+	if _, err := os.Stat(buildFile); err != nil {
+		t.Fatalf("expected file %q to exist but got err, %v", buildFile, err)
 	}
-	os.Remove(buildFile)
 }
diff --git a/mage/testdata/keep_flag/magefile.go b/mage/testdata/keep_flag/magefile.go
index fd5ee31..ce97b22 100644
--- a/mage/testdata/keep_flag/magefile.go
+++ b/mage/testdata/keep_flag/magefile.go
@@ -2,14 +2,4 @@
 
 package main
 
-import (
-	"fmt"
-)
-
-// This should work as a default - even if it's in a different file
-var Default = Noop
-
-// this should not be a target because it returns a string
-func Noop() {
-	fmt.Println("noop")
-}
+func Noop() {}
diff --git a/mage/testdata/keep_flag/main.go b/mage/testdata/keep_flag/main.go
deleted file mode 100644
index a3f23b4..0000000
--- a/mage/testdata/keep_flag/main.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// +build ignore
-
-package main
-
-import (
-	"os"
-
-	"github.com/magefile/mage/mage"
-)
-
-func main() {
-	os.Exit(mage.Main())
-}
diff --git a/mage/testdata/no_default/main.go b/mage/testdata/no_default/main.go
deleted file mode 100644
index a3f23b4..0000000
--- a/mage/testdata/no_default/main.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// +build ignore
-
-package main
-
-import (
-	"os"
-
-	"github.com/magefile/mage/mage"
-)
-
-func main() {
-	os.Exit(mage.Main())
-}
diff --git a/mage/testdata/panic.go b/mage/testdata/panic.go
index d0761bd..3714c0d 100644
--- a/mage/testdata/panic.go
+++ b/mage/testdata/panic.go
@@ -2,6 +2,8 @@
 
 package main
 
+import "errors"
+
 // Function that panics.
 func Panics() {
 	panic("boom!")
@@ -9,5 +11,5 @@ func Panics() {
 
 // Error function that panics.
 func PanicsErr() error {
-	panic("kaboom!")
+	panic(errors.New("kaboom!"))
 }