diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6606743..a07a1209 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,23 +14,43 @@ env: GOTRACEBACK: crash GO111MODULE: auto +# A new push to a pull request cancels its previous, now outdated, run. +# Pushes to master always run to completion: each one adds to the coverage +# history. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: name: Build (${{ matrix.platform }}, Go ${{ matrix.go-version }}, Python ${{ matrix.python-version }}) strategy: - fail-fast: false matrix: # TODO: Consider official support matrix (OS and Go versions) and adjust this matrix accordingly go-version: [1.25.x, 1.24.x, 1.23.x, 1.22.x] platform: [ubuntu-latest, windows-latest, macos-15] python-version: ['3.11', '3.12'] runs-on: ${{ matrix.platform }} + # A hung step fails here instead of at GitHub's 6h default. + timeout-minutes: 20 env: # Coverage is measured on exactly one matrix leg, so the numbers stay # comparable from commit to commit. COVERAGE_LEG: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.25.x' && matrix.python-version == '3.12' }} steps: + # Real-time scanning inspects every file the compilers, linkers and pip + # write, which slows Windows jobs down. Turned off first, so checkout and + # the Go/Python installs benefit too. The status is printed before and + # after so the effect can be checked in the log. + - name: Disable Windows Defender real-time scanning + if: runner.os == 'Windows' + shell: pwsh + run: | + Get-MpComputerStatus | Select-Object RealTimeProtectionEnabled + Set-MpPreference -DisableRealtimeMonitoring $true + Get-MpComputerStatus | Select-Object RealTimeProtectionEnabled + - name: Checkout code uses: actions/checkout@v4 @@ -48,18 +68,16 @@ jobs: - name: Install Linux packages if: matrix.platform == 'ubuntu-latest' run: | - sudo apt-get update - sudo apt-get install curl libffi-dev python3-cffi python3-pip # install pybindgen python3 -m pip install --user -U pybindgen # install goimports - go install golang.org/x/tools/cmd/goimports@latest + go install golang.org/x/tools/cmd/goimports@v0.29.0 - name: Install macOS packages if: startsWith(matrix.platform, 'macos-') run: | python3 -m pip install -U pybindgen - go install golang.org/x/tools/cmd/goimports@latest + go install golang.org/x/tools/cmd/goimports@v0.29.0 - name: Install Windows packages if: matrix.platform == 'windows-latest' @@ -67,7 +85,7 @@ jobs: # install pybindgen and psutil (for memory tracking in tests) python -m pip install -U pybindgen psutil # install goimports - go install golang.org/x/tools/cmd/goimports@latest + go install golang.org/x/tools/cmd/goimports@v0.29.0 - name: Build-Linux diff --git a/.gitignore b/.gitignore index b14a673d..73c25f61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,14 @@ -# IDE files -.idea/ -*.iml - -# goreleaser build output -/dist/ - -# 'make cover' output -/coverage.out +# IDE files +.idea/ +*.iml + +# goreleaser build output +/dist/ + +# python bytecode caches +__pycache__/ +*.py[co] + +# 'make cover' output +/coverage.out +/covdata/ diff --git a/Makefile b/Makefile index f4a9d3a1..50d1f120 100644 --- a/Makefile +++ b/Makefile @@ -27,11 +27,17 @@ test: # Same as 'test', but records statement coverage of every gopy package # (-coverpkg), since most of bind/ is only exercised via main_test.go. +# main_test.go runs gopy as a separate process (see TestMain), which +# -coverprofile can't see, so the test binaries and gopy all write raw +# coverage data to COVERDIR, merged into COVERPROFILE afterwards. COVERPROFILE=coverage.out +COVERDIR=$(CURDIR)/covdata cover: @echo "GO111MODULE = $(value GO111MODULE)" - $(GOTEST) -v -coverpkg=./... -coverprofile=$(COVERPROFILE) $(DIRS) + rm -rf $(COVERDIR) && mkdir -p $(COVERDIR) + $(GOTEST) -v -cover -coverpkg=./... $(DIRS) -args -test.gocoverdir=$(COVERDIR) + $(GOCMD) tool covdata textfmt -i=$(COVERDIR) -o=$(COVERPROFILE) $(GOCMD) tool cover -func=$(COVERPROFILE) | tail -n 1 clean: diff --git a/_examples/funcs/__pycache__/test.cpython-36.pyc b/_examples/funcs/__pycache__/test.cpython-36.pyc deleted file mode 100644 index 10bd243b..00000000 Binary files a/_examples/funcs/__pycache__/test.cpython-36.pyc and /dev/null differ diff --git a/bind/gen.go b/bind/gen.go index 2782d6aa..d89da696 100644 --- a/bind/gen.go +++ b/bind/gen.go @@ -28,9 +28,9 @@ type BuildMode string const ( ModeGen BuildMode = "gen" - ModeBuild = "build" - ModeExe = "exe" - ModePkg = "pkg" + ModeBuild BuildMode = "build" + ModeExe BuildMode = "exe" + ModePkg BuildMode = "pkg" ) // set this to true if OS is windows @@ -496,13 +496,14 @@ build: printf "#if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 202311L)\ntypedef uint8_t bool;\n#endif\n" > %[1]s_go.h # this will fail but is needed to generate the .c file that then allows go build to work - $(PYTHON) build.py >/dev/null 2>&1 - # generate %[1]s_go.h from %[1]s.go -- unfortunately no way to build .h only - $(GOBUILD) -buildmode=c-shared -o %[1]s_go$(LIBEXT) + # generate %[1]s_go.h from %[1]s.go -- unfortunately no way to build .h only; + # a c-archive doesn't link, so the stale %[1]s.c can't fail it + $(GOBUILD) -buildmode=c-archive -o %[1]s_go.a # use pybindgen to build the %[1]s.c file which are the CPython wrappers to cgo wrappers.. # note: pip install pybindgen to get pybindgen if this fails $(PYTHON) build.py # build the executable - - rm %[1]s_go$(LIBEXT) + - rm %[1]s_go.a $(GOBUILD) -o py%[1]s ` diff --git a/bind/printer_test.go b/bind/printer_test.go index 5c53567a..e1c719df 100644 --- a/bind/printer_test.go +++ b/bind/printer_test.go @@ -12,6 +12,7 @@ import ( ) func TestPrinter(t *testing.T) { + t.Parallel() decl := &printer{ buf: new(bytes.Buffer), indentEach: []byte("\t"), diff --git a/bind/utils_test.go b/bind/utils_test.go index 8d230c22..0bd5f9ce 100644 --- a/bind/utils_test.go +++ b/bind/utils_test.go @@ -10,6 +10,7 @@ import ( ) func TestGetGoVersion(t *testing.T) { + t.Parallel() for _, tt := range []struct { info string major int64 @@ -40,6 +41,7 @@ func TestGetGoVersion(t *testing.T) { } func TestExtractPythonName(t *testing.T) { + t.Parallel() for _, tt := range []struct { name string goDoc string @@ -71,6 +73,7 @@ func TestExtractPythonName(t *testing.T) { } func TestPythonConfig(t *testing.T) { + t.Parallel() t.Skip() for _, tc := range []struct { diff --git a/cmd_build.go b/cmd_build.go index 7e38997f..06ec0147 100644 --- a/cmd_build.go +++ b/cmd_build.go @@ -88,7 +88,7 @@ func gopyRunCmdBuild(cmdr *commander.Command, args []string) error { cfg.Name = pkg.Name() } } - return runBuild("build", cfg) + return runBuild(bind.ModeBuild, cfg) } // runBuild calls genPkg and then executes commands to build the resulting files @@ -134,11 +134,17 @@ func runBuild(mode bind.BuildMode, cfg *BuildCfg) error { cmd = exec.Command(cfg.VM, "build.py") cmd.Run() // will fail, we don't care about errors - args := []string{"build", "-mod=mod", "-buildmode=c-shared"} + // This build only writes buildname.h, the header the second + // build.py needs. A c-archive writes it without linking: the .c + // file the first build.py left behind was compiled against the + // placeholder header above, so it may call functions (such as the + // static _gopy_clear_go_tls) it can't link to yet, which macOS and + // Windows refuse in a shared library. + args := []string{"build", "-mod=mod", "-buildmode=c-archive"} if cfg.BuildTags != "" { args = append(args, "-tags", cfg.BuildTags) } - args = append(args, "-o", buildname+libExt, ".") + args = append(args, "-o", buildname+".a", ".") fmt.Printf("go %v\n", strings.Join(args, " ")) cmd = exec.Command("go", args...) @@ -156,14 +162,15 @@ func runBuild(mode bind.BuildMode, cfg *BuildCfg) error { return err } - err = os.Remove(cfg.Name + "_go" + libExt) + err = os.Remove(buildname + ".a") - fmt.Printf("go build -o py%s\n", cfg.Name) - cmd = exec.Command("go", "build", "-mod=mod") + args = []string{"build", "-mod=mod"} if cfg.BuildTags != "" { args = append(args, "-tags", cfg.BuildTags) } - args = append(args, "-o", "py"+cfg.Name) + args = append(args, "-o", exeName(cfg.Name)) + fmt.Printf("go %v\n", strings.Join(args, " ")) + cmd = exec.Command("go", args...) cmdout, err = cmd.CombinedOutput() if err != nil { fmt.Printf("cmd had error: %v output:\n%v\n", err, string(cmdout)) @@ -333,3 +340,13 @@ func runBuild(mode bind.BuildMode, cfg *BuildCfg) error { return err } + +// exeName returns the file name of the executable gopy exe builds for the +// package named name: py, as the Makefile it writes also builds, with +// Windows' .exe suffix there (go build -o adds none). +func exeName(name string) string { + if runtime.GOOS == "windows" { + return "py" + name + ".exe" + } + return "py" + name +} diff --git a/cmd_exe.go b/cmd_exe.go index a639acca..9f9f3104 100644 --- a/cmd_exe.go +++ b/cmd_exe.go @@ -111,6 +111,14 @@ func gopyRunCmdExe(cmdr *commander.Command, args []string) error { return err } + // GenPyPkgSetup chdirs into the output dir; restore the caller's cwd + // when we're done, so a run doesn't strand the process there. + cwd, err := os.Getwd() + if err != nil { + return err + } + defer os.Chdir(cwd) + setupfn := filepath.Join(cfg.OutputDir, "setup.py") if _, err = os.Stat(setupfn); os.IsNotExist(err) { diff --git a/cmd_pkg.go b/cmd_pkg.go index 9891907c..3e10b08e 100644 --- a/cmd_pkg.go +++ b/cmd_pkg.go @@ -107,6 +107,14 @@ func gopyRunCmdPkg(cmdr *commander.Command, args []string) error { return err } + // GenPyPkgSetup chdirs into the output dir; restore the caller's cwd + // when we're done, so a run doesn't strand the process there. + cwd, err := os.Getwd() + if err != nil { + return err + } + defer os.Chdir(cwd) + setupfn := filepath.Join(cfg.OutputDir, "setup.py") if _, err = os.Stat(setupfn); os.IsNotExist(err) { diff --git a/codecov.yml b/codecov.yml index 5ce677e1..cb227116 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,3 +8,11 @@ coverage: patch: default: informational: true + +# Go profiles mark a line "partial" when it holds more than one block and +# only some ran, typically `if err != nil {` whose condition ran but whose +# body didn't. The body's own lines already count as misses, and go tool +# cover counts the condition as covered, so count the line as a hit too. +parsers: + go: + partials_as_hits: true diff --git a/main_test.go b/main_test.go index d0f42fa9..a8a28379 100644 --- a/main_test.go +++ b/main_test.go @@ -6,6 +6,7 @@ package main import ( "bytes" + "flag" "fmt" "log" "os" @@ -16,8 +17,6 @@ import ( "sort" "strings" "testing" - - "github.com/go-python/gopy/bind" ) var ( @@ -62,6 +61,7 @@ func init() { } func TestGovet(t *testing.T) { + t.Parallel() cmd := exec.Command("go", "vet", "./...") buf := new(bytes.Buffer) cmd.Stdout = buf @@ -73,6 +73,7 @@ func TestGovet(t *testing.T) { } func TestGofmt(t *testing.T) { + t.Parallel() exe, err := exec.LookPath("goimports") if err != nil { switch e := err.(type) { @@ -102,6 +103,7 @@ func TestGofmt(t *testing.T) { } func TestGoPyErrors(t *testing.T) { + t.Parallel() pyvm := testBackends["py3"] workdir, err := os.MkdirTemp("", "gopy-") if err != nil { @@ -130,6 +132,7 @@ ignoring python incompatible function: .func github.com/go-python/gopy/_examples } func TestVersion(t *testing.T) { + t.Parallel() cmd := exec.Command("go", "run", ".", "version") out, err := cmd.CombinedOutput() if err != nil { @@ -143,6 +146,7 @@ func TestVersion(t *testing.T) { } func TestVersionLdflags(t *testing.T) { + t.Parallel() workdir, err := os.MkdirTemp("", "gopy-version-ldflags-") if err != nil { t.Fatalf("could not create workdir: %v\n", err) @@ -177,6 +181,7 @@ func TestVersionLdflags(t *testing.T) { } func TestGenHeaderHasVersion(t *testing.T) { + t.Parallel() pyvm := testBackends["py3"] workdir, err := os.MkdirTemp("", "gopy-") if err != nil { @@ -205,7 +210,7 @@ func TestGenHeaderHasVersion(t *testing.T) { } func TestHi(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/hi" testPkg(t, pkg{ path: path, @@ -346,7 +351,7 @@ OK } func TestBytes(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/gobytes" testPkg(t, pkg{ path: path, @@ -365,7 +370,7 @@ OK } func TestBindFuncs(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/funcs" testPkg(t, pkg{ path: path, @@ -407,7 +412,7 @@ OK `) func TestBindSimple(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/simple" testPkg(t, pkg{ path: path, @@ -421,7 +426,7 @@ func TestBindSimple(t *testing.T) { // TestMakefile builds _examples/simple with the Makefile gopy gen writes, // rather than with gopy build. func TestMakefile(t *testing.T) { - // t.Parallel() + t.Parallel() if _, err := exec.LookPath("make"); err != nil { t.Skip("make not found") } @@ -435,8 +440,47 @@ func TestMakefile(t *testing.T) { }) } +// TestPkg builds _examples/simple as an installable package with gopy pkg. +func TestPkg(t *testing.T) { + t.Parallel() + path := "_examples/simple" + testPkg(t, pkg{ + path: path, + lang: features[path], + cmd: "pkg", + want: simpleWant, + }) +} + +// TestExe checks that gopy exe builds _examples/simple into an executable, +// beside the files of an installable package. It doesn't run the +// executable: that needs libpython on the loader's search path, which +// depends on how the interpreter was installed. +func TestExe(t *testing.T) { + t.Parallel() + vm := testBackends["py3"] + if vm == "" { + t.Skip("python3 not found") + } + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + out := t.TempDir() + writeGoMod(t, cwd, out) + if err := runGopy([]string{"exe", "-vm=" + vm, "-output=" + out, "github.com/go-python/gopy/_examples/simple"}); err != nil { + t.Fatal(err) + } + exe := filepath.Join(out, "simple", exeName("simple")) + for _, f := range []string{filepath.Join(out, "setup.py"), exe} { + if _, err := os.Stat(f); err != nil { + t.Errorf("gopy exe didn't write %s: %v", f, err) + } + } +} + func TestBindEmpty(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/empty" testPkg(t, pkg{ path: path, @@ -451,7 +495,7 @@ OK } func TestBindPointers(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/pointers" testPkg(t, pkg{ path: path, @@ -467,7 +511,7 @@ OK } func TestBindNamed(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/named" testPkg(t, pkg{ path: path, @@ -489,7 +533,7 @@ OK } func TestBindStructs(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/structs" testPkg(t, pkg{ path: path, @@ -519,7 +563,7 @@ OK } func TestBindConsts(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/consts" // NOTE: python2 does not properly output .666 decimals for unknown reasons. testPkg(t, pkg{ @@ -542,7 +586,7 @@ OK } func TestBindVars(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/vars" testPkg(t, pkg{ path: path, @@ -584,7 +628,7 @@ OK } func TestBindSeqs(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/seqs" testPkg(t, pkg{ path: path, @@ -612,7 +656,7 @@ OK } func TestBindInterfaces(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/iface" // NOTE: python2 outputs this in a different order, so test fails, but results are the same testPkg(t, pkg{ @@ -642,7 +686,7 @@ OK } func TestBindCgoPackage(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/cgo" testPkg(t, pkg{ path: path, @@ -658,7 +702,7 @@ OK } func TestPyErrors(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/pyerrors" testPkg(t, pkg{ path: path, @@ -675,7 +719,7 @@ OK } func TestBuiltinArrays(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/arrays" testPkg(t, pkg{ path: path, @@ -691,7 +735,7 @@ OK } func TestBuiltinSlices(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/slices" testPkg(t, pkg{ path: path, @@ -720,7 +764,7 @@ OK } func TestBuiltinMaps(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/maps" testPkg(t, pkg{ path: path, @@ -745,7 +789,7 @@ OK } func TestBindStrings(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/gostrings" testPkg(t, pkg{ path: path, @@ -760,7 +804,7 @@ OK } func TestBindRename(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/rename" testPkg(t, pkg{ path: path, @@ -778,7 +822,7 @@ OK } func TestLot(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/lot" testPkg(t, pkg{ path: path, @@ -799,7 +843,7 @@ OK } func TestSlicePtr(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/sliceptr" testPkg(t, pkg{ path: path, @@ -815,7 +859,7 @@ OK } func TestUnicode(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/unicode" testPkg(t, pkg{ path: path, @@ -830,7 +874,7 @@ OK } func TestPYGC(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/gopygc" testPkg(t, pkg{ path: path, @@ -862,7 +906,7 @@ OK } func TestCStrings(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/cstrings" testPkg(t, pkg{ path: path, @@ -884,6 +928,7 @@ OK // gopy extensions (gilstring and simple) loaded in the same Python process, with // calls interleaved in a loop of 5000 iterations. func TestGilString(t *testing.T) { + t.Parallel() backends := []string{"py3"} for _, be := range backends { vm, ok := testBackends[be] @@ -892,6 +937,7 @@ func TestGilString(t *testing.T) { continue } t.Run(be, func(t *testing.T) { + t.Parallel() cwd, _ := os.Getwd() workdir, err := os.MkdirTemp("", "gopy-") @@ -899,24 +945,22 @@ func TestGilString(t *testing.T) { t.Fatalf("could not create workdir: %v", err) } defer os.RemoveAll(workdir) - defer bind.ResetPackages() gilDir := filepath.Join(workdir, "gilstring") if err := os.MkdirAll(gilDir, 0700); err != nil { t.Fatalf("could not create gilstring subdir: %v", err) } writeGoMod(t, cwd, gilDir) - if err := run([]string{"build", "-vm=" + vm, "-output=" + gilDir, "./_examples/gilstring"}); err != nil { + if err := runGopy([]string{"build", "-vm=" + vm, "-output=" + gilDir, "./_examples/gilstring"}); err != nil { t.Fatalf("error building gilstring: %v", err) } - bind.ResetPackages() simpleDir := filepath.Join(workdir, "simple") if err := os.MkdirAll(simpleDir, 0700); err != nil { t.Fatalf("could not create simple subdir: %v", err) } writeGoMod(t, cwd, simpleDir) - if err := run([]string{"build", "-vm=" + vm, "-output=" + simpleDir, "./_examples/simple"}); err != nil { + if err := runGopy([]string{"build", "-vm=" + vm, "-output=" + simpleDir, "./_examples/simple"}); err != nil { t.Fatalf("error building simple: %v", err) } @@ -948,7 +992,7 @@ func TestGilString(t *testing.T) { } func TestPackagePrefix(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/package/mypkg" testPkg(t, pkg{ path: path, @@ -965,7 +1009,7 @@ OK } func TestPkgConflict(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/pkgconflict" testPkg(t, pkg{ path: path, @@ -983,7 +1027,6 @@ func TestPkgConflict(t *testing.T) { // leaving here for now in case someone wants to follow-up and make it work.. // // func TestOsFile(t *testing.T) { -// // t.Parallel() // path := "_examples/osfile" // testPkg(t, pkg{ // path: path, @@ -997,7 +1040,7 @@ func TestPkgConflict(t *testing.T) { // } func TestBindVariadic(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/variadic" testPkg(t, pkg{ path: path, @@ -1015,6 +1058,7 @@ Type OK // Generate / verify SUPPORT_MATRIX.md from features map. func TestCheckSupportMatrix(t *testing.T) { + t.Parallel() var buf bytes.Buffer buf.WriteString("# Support matrix\n") buf.WriteString(` @@ -1121,7 +1165,7 @@ func testPkg(t *testing.T, table pkg) { switch be { case "py3": t.Run(be, func(t *testing.T) { - // t.Parallel() + t.Parallel() testPkgBackend(t, vm, table) }) default: @@ -1161,7 +1205,6 @@ func testPkgBackend(t *testing.T, pyvm string, table pkg) { } } defer os.RemoveAll(workdir) - defer bind.ResetPackages() env := make([]string, len(testEnvironment)) copy(env, testEnvironment) @@ -1180,7 +1223,7 @@ func testPkgBackend(t *testing.T, pyvm string, table pkg) { } args = append(args, fpath) fmt.Printf("run cmd: %s\n", args) - err = run(args) + err = runGopy(args) if err != nil { t.Fatalf("[%s:%s]: error running gopy-build: %v\n", pyvm, table.path, err) } @@ -1265,3 +1308,45 @@ func testPkgBackend(t *testing.T, pyvm string, table pkg) { } } + +// gopyExe is the gopy binary TestMain builds, which the tests run as a +// separate process, rather than calling run in this one: the generator +// keeps its state in package variables (bind.Packages, the symbol tables, +// thePyGen) and gopy build changes the process's working directory, so +// in-process runs can't overlap. +var gopyExe string + +func TestMain(m *testing.M) { + dir, err := os.MkdirTemp("", "gopy-bin-") + if err != nil { + log.Fatal(err) + } + gopyExe = filepath.Join(dir, "gopy") + if runtime.GOOS == "windows" { + gopyExe += ".exe" + } + build := exec.Command("go", "build", "-o", gopyExe, ".") + // Under go test -cover with -test.gocoverdir (see the Makefile's cover + // target), gopy writes its own coverage there too, to be merged with + // the test binary's. + flag.Parse() + if f := flag.Lookup("test.gocoverdir"); testing.CoverMode() != "" && f != nil && f.Value.String() != "" { + build.Args = []string{"go", "build", "-cover", "-covermode=" + testing.CoverMode(), "-coverpkg=./...", "-o", gopyExe, "."} + os.Setenv("GOCOVERDIR", f.Value.String()) + } + if out, err := build.CombinedOutput(); err != nil { + log.Fatalf("building gopy: %v\n%s", err, out) + } + code := m.Run() + os.RemoveAll(dir) + os.Exit(code) +} + +// runGopy runs gopy with args, in the current directory. +func runGopy(args []string) error { + out, err := exec.Command(gopyExe, args...).CombinedOutput() + if err != nil { + return fmt.Errorf("gopy %s: %v\n%s", strings.Join(args, " "), err, out) + } + return nil +}