Files
go-cache-plugin/build_test.go
T
unkin-agent 3e2f7e59b6
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Scaffold build repo for pinned tailscale/go-cache-plugin
Upstream publishes no releases, so CI has nothing to curl.

- pin cmd/go-cache-plugin at 3031b5d via the go.mod tool directive
- build a static stripped binary with make build
- test the built binary: static ELF, pinned provenance, and a real
  go build driven through it as a GOCACHEPROG against a stub S3
- add PR build/test/pre-commit workflows and a tag release workflow
  attaching linux/amd64 and linux/arm64 to a Gitea release
2026-09-26 21:30:07 +10:00

192 lines
5.1 KiB
Go

// Package gocacheplugin has no source of its own: this repo exists only to
// build the pinned upstream go-cache-plugin and ship it as a release. The tests
// therefore exercise the built binary rather than any local code.
package gocacheplugin
import (
"debug/buildinfo"
"debug/elf"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
"testing"
)
const (
upstreamPkg = "github.com/tailscale/go-cache-plugin/cmd/go-cache-plugin"
upstreamModule = "github.com/tailscale/go-cache-plugin"
)
var pluginBin string
func TestMain(m *testing.M) {
dir, err := os.MkdirTemp("", "go-cache-plugin-test")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
pluginBin = filepath.Join(dir, "go-cache-plugin")
build := exec.Command("go", "build", "-ldflags=-s -w", "-o", pluginBin, upstreamPkg)
build.Env = append(os.Environ(), "CGO_ENABLED=0")
if out, err := build.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "building %s: %v\n%s", upstreamPkg, err, out)
_ = os.RemoveAll(dir)
os.Exit(1)
}
code := m.Run()
_ = os.RemoveAll(dir)
os.Exit(code)
}
// A dynamically linked binary will not run on the estate's Alpine-based CI
// images, so CGO_ENABLED=0 must actually take effect.
func TestBinaryIsStatic(t *testing.T) {
fi, err := os.Stat(pluginBin)
if err != nil {
t.Fatal(err)
}
if fi.Size() == 0 {
t.Fatal("binary is empty")
}
f, err := elf.Open(pluginBin)
if err != nil {
t.Fatal(err)
}
defer func() { _ = f.Close() }()
if s := f.Section(".interp"); s != nil {
t.Error("binary has a .interp section, so it needs a dynamic loader")
}
libs, err := f.ImportedLibraries()
if err != nil {
t.Fatal(err)
}
if len(libs) != 0 {
t.Errorf("binary links shared libraries: %v", libs)
}
}
// The binary must come from the version go.mod pins, not from whatever the
// toolchain would resolve on its own.
func TestBuiltFromPinnedVersion(t *testing.T) {
out, err := exec.Command("go", "list", "-m", "-f", "{{.Version}}", upstreamModule).Output()
if err != nil {
t.Fatal(err)
}
want := strings.TrimSpace(string(out))
if want == "" {
t.Fatalf("%s is not in the module graph", upstreamModule)
}
info, err := buildinfo.ReadFile(pluginBin)
if err != nil {
t.Fatal(err)
}
got := info.Main.Version
for _, dep := range info.Deps {
if dep.Path == upstreamModule {
got = dep.Version
}
}
if got != want {
t.Errorf("binary built from %s %q, go.mod pins %q", upstreamModule, got, want)
}
}
// Sanity check that the real entry point runs and parses flags: without a
// bucket the cache server must refuse to start.
func TestRequiresBucket(t *testing.T) {
cmd := exec.Command(pluginBin, "--cache-dir="+t.TempDir())
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("expected a non-zero exit, got success:\n%s", out)
}
if !strings.Contains(string(out), "--bucket") {
t.Errorf("expected a usage error naming --bucket, got:\n%s", out)
}
}
// End to end: drive a real `go build` through the binary as a GOCACHEPROG
// plugin, pointed at a stub S3 endpoint that misses every read. This is the
// only thing the repo ships, so it has to actually speak the protocol.
func TestServesGoCacheProg(t *testing.T) {
var reqs atomic.Int64
s3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqs.Add(1)
if r.Method == http.MethodPut || r.Method == http.MethodPost {
_, _ = io.Copy(io.Discard, r.Body)
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusNotFound)
_, _ = io.WriteString(w, `<?xml version="1.0" encoding="UTF-8"?><Error><Code>NoSuchKey</Code><Message>no such key</Message></Error>`)
}))
defer s3.Close()
cacheDir := t.TempDir()
src := t.TempDir()
write(t, filepath.Join(src, "go.mod"), "module example.test\n\ngo 1.26.1\n")
write(t, filepath.Join(src, "main.go"), "package main\n\nfunc main() { println(\"ok\") }\n")
prog := strings.Join([]string{
pluginBin,
"--cache-dir=" + cacheDir,
"--bucket=go-cache-test",
"--region=us-east-1",
"--s3-endpoint-url=" + s3.URL,
"--s3-path-style",
}, " ")
build := exec.Command("go", "build", "-o", filepath.Join(src, "out"), ".")
build.Dir = src
build.Env = append(os.Environ(),
"GOFLAGS=",
"GOCACHEPROG="+prog,
"AWS_ACCESS_KEY_ID=test",
"AWS_SECRET_ACCESS_KEY=test",
)
if out, err := build.CombinedOutput(); err != nil {
t.Fatalf("go build under GOCACHEPROG failed: %v\n%s", err, out)
}
if _, err := os.Stat(filepath.Join(src, "out")); err != nil {
t.Fatalf("build produced no binary: %v", err)
}
if n := countFiles(t, cacheDir); n == 0 {
t.Error("plugin staged nothing in the local cache directory")
}
if reqs.Load() == 0 {
t.Error("plugin made no S3 requests")
}
}
func write(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func countFiles(t *testing.T, dir string) int {
t.Helper()
var n int
err := filepath.WalkDir(dir, func(_ string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
n++
}
return nil
})
if err != nil {
t.Fatal(err)
}
return n
}