From 0f9b5847c22d3982e9e6865563165ebdb903b133 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 5 Oct 2026 16:22:50 +0200 Subject: [PATCH] cocoa live tests: write captures to captureDir, never the working directory TestLiveCocoaWindow wrote a window-server screenshot (screencapture -l) and its own render into the package directory. That directory is this public repository, so the files sat one `git add -f` away from publication, and the .gitignore entry is a safety net, not a barrier. writeArtifact, used by the legibility, drag-and-drop and vibrancy proofs, did the same. All three writers now go through captureDir (go-appdirs/outdir), which refuses any directory inside a git work tree. The capture directory is resolved on the test goroutine, before callOnMain. captureDir fails the test when the directory is refused, and a t.Fatal from inside the main-thread closure ends that closure, not the test: the test then waited forever. That hang is how a refused directory showed up at first. CI: the darwin live step sets WINDOW_CAPTURE_DIR under runner.temp, outside the checkout, and the artifact upload reads from there. Checked live on macOS, in both directions. With the default directory the test passes, writes both PNGs under ~/Library/Application Support, and leaves no PNG in the work tree, ignored ones included. With WINDOW_CAPTURE_DIR inside the work tree it fails in 0.2 s, naming the tree, and creates nothing. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 7 +++--- internal/cocoa/live_darwin_test.go | 35 ++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6793245..189c3a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,15 +122,16 @@ jobs: - name: Live Cocoa window proof (on-device NSWindow + input round-trip) env: WINDOW_COCOA_INTEGRATION: "1" + # Captures go outside the checkout (internal/capture refuses a work + # tree), so the upload below reads them from here. + WINDOW_CAPTURE_DIR: ${{ runner.temp }}/window-captures run: CGO_ENABLED=0 go test -tags=integration -run 'TestLiveCocoaWindow|TestLiveShowHideRaise' -v ./internal/cocoa/ - name: Upload the on-device window render as a build artifact if: always() uses: actions/upload-artifact@v7 with: name: cocoa-capture - path: | - internal/cocoa/cocoa-capture-2026-08-10.png - internal/cocoa/cocoa-screencapture-2026-08-10.png + path: ${{ runner.temp }}/window-captures/*.png if-no-files-found: warn # A SECOND MACHINE for the Windows back-end. The Win32 window itself needs an diff --git a/internal/cocoa/live_darwin_test.go b/internal/cocoa/live_darwin_test.go index 55fc141..6c603c1 100644 --- a/internal/cocoa/live_darwin_test.go +++ b/internal/cocoa/live_darwin_test.go @@ -30,6 +30,7 @@ import ( "image/png" "os" "os/exec" + "path/filepath" "runtime" "testing" "unsafe" @@ -193,8 +194,14 @@ func TestLiveCocoaWindow(t *testing.T) { pngData []byte btnRect toolkit.Rect setupErr error + shot string ) + // Where the captures go is decided HERE, on the test goroutine: captureDir + // fails the test when the directory is refused, and a t.Fatal from inside + // callOnMain would end the main thread's closure, not the test -- the test + // would wait for it forever. + captures := captureDir(t) callOnMain(func() { win, setupErr = New("go-widgets/window cocoa proof", 480, 320, theme) if setupErr != nil { @@ -246,8 +253,11 @@ func TestLiveCocoaWindow(t *testing.T) { // Best-effort true on-screen screenshot artifact (needs Screen Recording // permission; ignored if it fails / is unavailable, e.g. headless CI). - trySystemScreencapture(win) + shot = trySystemScreencapture(captures, win) }) + if shot != "" { + t.Logf("window-server capture: %s", shot) + } if setupErr != nil { t.Fatalf("window setup/capture failed: %v", setupErr) @@ -263,7 +273,7 @@ func TestLiveCocoaWindow(t *testing.T) { t.Fatalf("rendered image too small: %v", b) } // Save the captured render as a dated artifact. - out := "cocoa-capture-2026-08-10.png" + out := filepath.Join(captures, "cocoa-capture.png") if werr := os.WriteFile(out, pngData, 0o644); werr != nil { t.Logf("could not save %s: %v", out, werr) } else { @@ -428,8 +438,11 @@ func TestCocoaLegibilityBeforeAfter(t *testing.T) { // png2img decodes PNG bytes into an image. func png2img(b []byte) (image.Image, error) { return png.Decode(bytes.NewReader(b)) } -// writeArtifact saves a capture, logging (not failing) on write error. +// writeArtifact saves a capture under captureDir, logging (not failing) on +// write error. func writeArtifact(t *testing.T, name string, data []byte) { + t.Helper() + name = filepath.Join(captureDir(t), filepath.Base(name)) if err := os.WriteFile(name, data, 0o644); err != nil { t.Logf("could not save %s: %v", name, err) return @@ -466,11 +479,19 @@ func firstLabelInkHeight(img image.Image, bg toolkit.RGBA) int { } // trySystemScreencapture attempts a real window-server screenshot of the window -// and saves it; failures (missing permission / headless) are non-fatal. -func trySystemScreencapture(w *Window) { +// into dir and returns its path; failures (missing permission / headless) are +// non-fatal and return "". It runs on the main thread, so it takes no *testing.T. +// +// ⛔ dir is captureDir, never the working directory. The capture used to land in +// the package directory -- this repository, which is public -- one `git add -f` +// from publication; a .gitignore entry is not a barrier. +func trySystemScreencapture(dir string, w *Window) string { wn := int(w.win.Send(selWindowNumber)) - out := "cocoa-screencapture-2026-08-10.png" - _ = exec.Command("screencapture", "-x", "-o", "-l", fmt.Sprintf("%d", wn), out).Run() + out := filepath.Join(dir, "cocoa-screencapture.png") + if err := exec.Command("screencapture", "-x", "-o", "-l", fmt.Sprintf("%d", wn), out).Run(); err != nil { + return "" + } + return out } func near(a, b uint8) bool {