Skip to content

test: exercise binary self-replacement on Windows in CI - #58

Merged
aaearon merged 2 commits into
mainfrom
test/windows-selfupdate-e2e
Aug 14, 2026
Merged

test: exercise binary self-replacement on Windows in CI#58
aaearon merged 2 commits into
mainfrom
test/windows-selfupdate-e2e

Conversation

@aaearon

@aaearon aaearon commented Aug 14, 2026

Copy link
Copy Markdown
Owner

The windows-latest CI leg added in #54 runs go build and go test, but never exercises the one thing that actually differs on Windows: a binary replacing itself. A running executable there cannot be deleted, only renamed — which is precisely why minio/selfupdate swaps with two renames instead of writing over the target. Nothing in CI proved that path worked.

What this adds

internal/selfupdate/e2e_test.go, behind the selfupdate_e2e build tag:

  1. Compiles two fixture binaries from a self-contained, dependency-free module (-ldflags -X main.version=VARIANT-A/B). No network access — this does not touch GitHub releases.
  2. Runs variant A and captures its output.
  3. Replaces it through grant’s own applyBinaryTo / applyWithOptions while a process is still running from that image.
  4. Runs it again and asserts the output changed to variant B.
  5. Asserts debris.
  6. Covers the failure path: the staged file is removed so the second rename fails, rollback runs, and the restored binary must still execute — not merely have the right bytes.

Each test is table-driven over hold=false / hold=true so the locked and unlocked cases are directly comparable.

Making the lock real

A test that claims to exercise file locking but silently doesn’t is worse than no test. The target is held by a live child process (fixture hold, blocking on stdin) rather than a mere open handle, because a mapped executable image is what Windows refuses to delete. heldProcess.alive() asserts the child is still running both immediately before and after the swap, so a hold=true case can never quietly degrade into the hold=false case.

One honest platform difference, asserted rather than hidden

minio.CommitBinary cannot os.Remove the .grant.old backup while a process still runs from that image, so it marks the file hidden (SetFileAttributesW) and leaves it. So on Windows, .grant.old does survive an update performed against a running binary. That is by design, and it does not accumulate — the next CommitBinary removes the old path before renaming.

Rather than relax the assertion to make the suite green, the test encodes the real contract:

  • .grant.new must never survive, on either platform.
  • .grant.old must not survive on POSIX, or on Windows when nothing is running.
  • On Windows with the image held, the backup may survive, and the test then performs a second update and asserts it is cleared — proving it cannot pile up.

CI

The step runs on both ubuntu-latest and windows-latest, deliberately with no if: guard — comparing the platforms is the entire point.

No production changes

applyWithOptions and the commitFn seam already existed. internal/selfupdate/*.go is untouched; no test seam had to be added or widened.

Verified locally

make build, make test, make lint, and go test -tags=selfupdate_e2e -race ./internal/selfupdate/ all pass on Linux. golangci-lint run --build-tags=selfupdate_e2e is clean. The Windows leg could not be run locally — that is what this PR’s CI run is for.

The windows-latest CI leg added in 0.8.0 only ran go build and go test; it
never exercised a binary replacing itself, which is the one part of
grant update whose semantics genuinely differ on Windows (a running
executable cannot be deleted, only renamed).

Add selfupdate_e2e-tagged end-to-end tests that compile two real fixture
binaries from a dependency-free module, execute one, and replace it
through grant's own applyBinaryTo/applyWithOptions while a process is
still running from that image. Cover the success path and the rollback
path, and assert the rolled-back binary still executes. No network access
is needed, so this does not depend on GitHub releases.

Run the step on both ubuntu-latest and windows-latest so the platforms
are directly comparable.

No production code changed: applyWithOptions and the commitFn seam
already existed.
@aaearon
aaearon merged commit 42d5d14 into main Aug 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant