From 942f37af0d4f81be398a6734d6f35c67aff149bb Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 4 Nov 2026 21:03:15 -0500 Subject: [PATCH 1/4] fix: restore the working directory after gopy pkg and exe GenPyPkgSetup chdirs into the output directory, and neither command changed back, so a caller running gopy in-process (as main_test.go does) was left in the output directory, or in no directory at all once that was removed: every later build then failed with getwd: no such file or directory TestPkg builds _examples/simple with gopy pkg and runs its test.py. --- cmd_exe.go | 8 ++++++++ cmd_pkg.go | 8 ++++++++ main_test.go | 12 ++++++++++++ 3 files changed, 28 insertions(+) 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/main_test.go b/main_test.go index d0f42fa9..6fe389b6 100644 --- a/main_test.go +++ b/main_test.go @@ -435,6 +435,18 @@ 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, + }) +} + func TestBindEmpty(t *testing.T) { // t.Parallel() path := "_examples/empty" From cf887e3a0f4953662b250580c81a769b376b731a Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 4 Nov 2026 21:03:15 -0500 Subject: [PATCH 2/4] fix: build the executable gopy exe is asked for gopy exe printed `go build -o py` but ran `go build -mod=mod` alone: the -o and -tags it appended went to an unused slice, so the executable was named after the directory and built without the requested tags. It now runs the command it prints, adding Windows' .exe suffix. The build that only writes _go.h for the second build.py run was a c-shared link of a .c file compiled against the placeholder header, which macOS and Windows refuse when it calls functions (such as the static _gopy_clear_go_tls) it can't link to yet. It is now a c-archive, which writes the header without linking, both in gopy exe and in the Makefile it generates. The BuildMode constants other than ModeGen were untyped strings; they are now typed, and runBuild is passed bind.ModeBuild. TestExe checks that gopy exe writes setup.py and the executable. --- bind/gen.go | 13 +++++++------ cmd_build.go | 31 ++++++++++++++++++++++++------- main_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) 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/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/main_test.go b/main_test.go index 6fe389b6..686eb826 100644 --- a/main_test.go +++ b/main_test.go @@ -447,6 +447,34 @@ func TestPkg(t *testing.T) { }) } +// 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) + defer bind.ResetPackages() + if err := run([]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() path := "_examples/empty" From 669d9c9a7d1b74a2e152ec04d0191d62b6316a22 Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 4 Nov 2026 21:03:54 -0500 Subject: [PATCH 3/4] test: run gopy as a subprocess, so tests can run in parallel The generator keeps its state in package variables and gopy changes the process's working directory, so main_test.go ran every gopy command in-process and in sequence. TestMain now builds the gopy binary once and the tests run it as a separate process, which lets them call t.Parallel(). `make cover` collects coverage from those gopy processes too: the test binaries and gopy write raw coverage data to covdata/, which is merged into coverage.out. codecov.yml counts Go's "partial" lines as hits, as `go tool cover` does. Also ignore Python bytecode caches, and remove the one that was committed under _examples/funcs. --- .gitignore | 23 ++-- Makefile | 8 +- .../funcs/__pycache__/test.cpython-36.pyc | Bin 1686 -> 0 bytes bind/printer_test.go | 1 + bind/utils_test.go | 3 + codecov.yml | 8 ++ main_test.go | 129 ++++++++++++------ 7 files changed, 120 insertions(+), 52 deletions(-) delete mode 100644 _examples/funcs/__pycache__/test.cpython-36.pyc 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 10bd243b063122ae5cf91197f4043635154497f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1686 zcmb_cOOM+&5GE-~mTlR)*&uDwJfK4^>R}Bux1b1uY>*&_Yz{_IKu94dBIT7AAm#*PeXqxu?!h`^uv!0z?3ZLk>AJ{N@|o84P-V{JnnuyGO`h9`qrKq!tO`X`8vY1gKS;;pfc7-F|#1kB3N4Ua!OJZMigb#D~F%ezS zdrL%5I%RL;#C_?Of#^$rgC5*+0D9NzeegAWeNXO)U_&30Uw(*p|MM*R7|3CH$=)26 zdmBQ@%hk_B496te*+S+dqGmW(McpL%qRyIPR!7XR$;`O8)~l{=RG1gC65H?Lc;?_G z`0)K*7zZ#kS{hQ)HQ?|Yed9Dtu(eYsAin7x3Qcf&^*?aAG6)os-=sGYEQl~z{KR*77NDVXk~MLllRB5RCm-(kG={>eO! zxN*{48;%YP$J@qfa{Z2|9dPi^gV9e~Dm|JhSya_>G?|UG%0`o-Ia<8f&t_BDIW9oV zm!m|!PN(xq>X8L!)JWaz&zHuvnGX>POp4Hes!K3`HMA7akF2hL09BFy4_JO!G zmfYNun*q6<_sHFr>*j@W;q$^TX7?xS<*F-?-g(i~0GFRCnQu{r2Kf$7oYn07-KXX= z+!24o-ZnLs7MmKeFB$$qZ1V#CPr(AXGw3S+Ag!tgX?Cox;U;Z!!^XNm9eZdDMY;w1 zAEA#P(0CYm2I^F&Q<)^Dmn74fSX8J7N%C@$R;@){Mduhz-8>8Pen52@4Bx=e*fu`( z9iQ91EpBd+P}>m1VRSi$IQgPkD2Stw7xu5B5QG-|>!{(4wqvlX6(SRN9?vX(*0H*Q zTfI{-p5(^KDs5bq))Q%H@%em>|6}{sdzi0AlmfQPeCkn;`P}Dw{eT6`V?+3{1awIM E1;`I+mjD0& 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/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 686eb826..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") } @@ -437,7 +442,7 @@ func TestMakefile(t *testing.T) { // TestPkg builds _examples/simple as an installable package with gopy pkg. func TestPkg(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/simple" testPkg(t, pkg{ path: path, @@ -452,7 +457,7 @@ func TestPkg(t *testing.T) { // 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() + t.Parallel() vm := testBackends["py3"] if vm == "" { t.Skip("python3 not found") @@ -463,8 +468,7 @@ func TestExe(t *testing.T) { } out := t.TempDir() writeGoMod(t, cwd, out) - defer bind.ResetPackages() - if err := run([]string{"exe", "-vm=" + vm, "-output=" + out, "github.com/go-python/gopy/_examples/simple"}); err != nil { + 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")) @@ -476,7 +480,7 @@ func TestExe(t *testing.T) { } func TestBindEmpty(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/empty" testPkg(t, pkg{ path: path, @@ -491,7 +495,7 @@ OK } func TestBindPointers(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/pointers" testPkg(t, pkg{ path: path, @@ -507,7 +511,7 @@ OK } func TestBindNamed(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/named" testPkg(t, pkg{ path: path, @@ -529,7 +533,7 @@ OK } func TestBindStructs(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/structs" testPkg(t, pkg{ path: path, @@ -559,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{ @@ -582,7 +586,7 @@ OK } func TestBindVars(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/vars" testPkg(t, pkg{ path: path, @@ -624,7 +628,7 @@ OK } func TestBindSeqs(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/seqs" testPkg(t, pkg{ path: path, @@ -652,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{ @@ -682,7 +686,7 @@ OK } func TestBindCgoPackage(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/cgo" testPkg(t, pkg{ path: path, @@ -698,7 +702,7 @@ OK } func TestPyErrors(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/pyerrors" testPkg(t, pkg{ path: path, @@ -715,7 +719,7 @@ OK } func TestBuiltinArrays(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/arrays" testPkg(t, pkg{ path: path, @@ -731,7 +735,7 @@ OK } func TestBuiltinSlices(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/slices" testPkg(t, pkg{ path: path, @@ -760,7 +764,7 @@ OK } func TestBuiltinMaps(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/maps" testPkg(t, pkg{ path: path, @@ -785,7 +789,7 @@ OK } func TestBindStrings(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/gostrings" testPkg(t, pkg{ path: path, @@ -800,7 +804,7 @@ OK } func TestBindRename(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/rename" testPkg(t, pkg{ path: path, @@ -818,7 +822,7 @@ OK } func TestLot(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/lot" testPkg(t, pkg{ path: path, @@ -839,7 +843,7 @@ OK } func TestSlicePtr(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/sliceptr" testPkg(t, pkg{ path: path, @@ -855,7 +859,7 @@ OK } func TestUnicode(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/unicode" testPkg(t, pkg{ path: path, @@ -870,7 +874,7 @@ OK } func TestPYGC(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/gopygc" testPkg(t, pkg{ path: path, @@ -902,7 +906,7 @@ OK } func TestCStrings(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/cstrings" testPkg(t, pkg{ path: path, @@ -924,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] @@ -932,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-") @@ -939,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) } @@ -988,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, @@ -1005,7 +1009,7 @@ OK } func TestPkgConflict(t *testing.T) { - // t.Parallel() + t.Parallel() path := "_examples/pkgconflict" testPkg(t, pkg{ path: path, @@ -1023,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, @@ -1037,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, @@ -1055,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(` @@ -1161,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: @@ -1201,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) @@ -1220,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) } @@ -1305,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 +} From 2a2884e80522458f07dd098315931dd71e4a0c7d Mon Sep 17 00:00:00 2001 From: b-long Date: Wed, 4 Nov 2026 21:04:10 -0500 Subject: [PATCH 4/4] ci: harden the main test job - Pin goimports to v0.29.0 instead of @latest, so the tool TestGofmt checks against doesn't change from one run to the next. - Drop the apt-get step: none of the packages it installed are used. - Disable Windows Defender real-time scanning, which slows every compile, link and pip install on windows-latest. - Give the job a 20-minute timeout, rather than GitHub's 6h default. - Cancel a pull request's previous run when it is pushed to again, and let the matrix fail fast. --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) 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