186 lines
5.0 KiB
Go
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
|
|
}
|