Files
unkin-agent 6a1db8d209
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Test the Makefile build recipe, not a copy of it
2026-09-26 22:18:23 +10:00

186 lines
5.0 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 upstreamModule = "github.com/tailscale/go-cache-plugin"
var pluginBin string
// Build via the Makefile: the tests below must check the artifact release.yaml
// actually ships, not a second build recipe kept in sync here.
func TestMain(m *testing.M) {
if out, err := exec.Command("make", "build").CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "make build: %v\n%s", err, out)
os.Exit(1)
}
abs, err := filepath.Abs(filepath.Join("dist", "go-cache-plugin"))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
pluginBin = abs
os.Exit(m.Run())
}
// 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
}