// 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, `NoSuchKeyno such key`) })) 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 }