Skip to content

Commit 9e3be33

Browse files
committed
Refactor integration test to use CLI subprocess with fake cachew server
Replace direct function calls (createTarZstd, extractBundleZstd, etc.) with the compiled CLI binary as a subprocess, exercising the full code path including kong parsing, metrics binding, and backend communication. A fake cachew HTTP server (httptest.Server) stands in for real storage, eliminating the need for the fileBundleStore test helper.
1 parent 6ef700c commit 9e3be33

1 file changed

Lines changed: 103 additions & 94 deletions

File tree

cmd/gradle-cache/integration_test.go

Lines changed: 103 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,70 @@
1-
//nolint:gosec // test file: paths and subprocess args are controlled inputs
1+
//nolint:gosec // test file: all paths and subprocess args are controlled inputs
22
package main
33

44
import (
5-
"context"
65
"io"
6+
"net/http"
7+
"net/http/httptest"
78
"os"
89
"os/exec"
910
"path/filepath"
1011
"strings"
1112
"testing"
1213
)
1314

14-
// fileBundleStore is a file-system-backed bundleStore for integration tests.
15-
type fileBundleStore struct {
16-
dir string
15+
// fakeCachew is a minimal file-backed implementation of the cachew object API
16+
// used to exercise the real CLI binary without needing S3 or a remote server.
17+
// Blobs are written to disk to handle large bundles without blowing up memory.
18+
type fakeCachew struct {
19+
dir string // storage directory
1720
}
1821

19-
func (f *fileBundleStore) path(commit, cacheKey string) string {
20-
return filepath.Join(f.dir, commit, bundleFilename(cacheKey))
22+
func newFakeCachew(dir string) *fakeCachew {
23+
return &fakeCachew{dir: dir}
2124
}
2225

23-
func (f *fileBundleStore) stat(_ context.Context, commit, cacheKey string) (int64, error) {
24-
fi, err := os.Stat(f.path(commit, cacheKey))
25-
if err != nil {
26-
return 0, err
27-
}
28-
return fi.Size(), nil
26+
func (f *fakeCachew) blobPath(key string) string {
27+
// key is "cacheKey/commit" — flatten slashes to avoid nested dirs
28+
return filepath.Join(f.dir, strings.ReplaceAll(key, "/", "_"))
2929
}
3030

31-
func (f *fileBundleStore) get(_ context.Context, commit, cacheKey string, _ int64) (io.ReadCloser, error) {
32-
return os.Open(f.path(commit, cacheKey))
33-
}
31+
func (f *fakeCachew) ServeHTTP(w http.ResponseWriter, r *http.Request) {
32+
// Expected path: /api/v1/object/{cacheKey}/{commit}
33+
key := strings.TrimPrefix(r.URL.Path, "/api/v1/object/")
34+
path := f.blobPath(key)
3435

35-
func (f *fileBundleStore) put(_ context.Context, commit, cacheKey string, r io.ReadSeeker, _ int64) error {
36-
p := f.path(commit, cacheKey)
37-
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
38-
return err
39-
}
40-
data, err := io.ReadAll(r)
41-
if err != nil {
42-
return err
36+
switch r.Method {
37+
case http.MethodHead:
38+
if _, err := os.Stat(path); err != nil {
39+
http.NotFound(w, r)
40+
return
41+
}
42+
w.WriteHeader(http.StatusOK)
43+
case http.MethodGet:
44+
file, err := os.Open(path)
45+
if err != nil {
46+
http.NotFound(w, r)
47+
return
48+
}
49+
defer func() { _ = file.Close() }()
50+
w.Header().Set("Content-Type", "application/zstd")
51+
_, _ = io.Copy(w, file)
52+
case http.MethodPost:
53+
file, err := os.Create(path)
54+
if err != nil {
55+
http.Error(w, err.Error(), http.StatusInternalServerError)
56+
return
57+
}
58+
_, cpErr := io.Copy(file, r.Body)
59+
_ = file.Close()
60+
if cpErr != nil {
61+
http.Error(w, cpErr.Error(), http.StatusInternalServerError)
62+
return
63+
}
64+
w.WriteHeader(http.StatusOK)
65+
default:
66+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
4367
}
44-
return os.WriteFile(p, data, 0o644)
4568
}
4669

4770
// copyDir recursively copies src to dst, preserving file modes.
@@ -63,12 +86,11 @@ func copyDir(dst, src string) error {
6386
})
6487
}
6588

66-
// TestIntegrationGradleBuildCycle exercises the full save/restore cycle with a
67-
// real Gradle build. It verifies that after restoring a saved cache, the
68-
// configuration cache reports a hit and build tasks are UP-TO-DATE.
89+
// TestIntegrationGradleBuildCycle exercises the full save/restore cycle using
90+
// the compiled CLI binary as a subprocess. This tests the complete code path
91+
// including kong CLI parsing, metrics binding, and backend communication.
6992
//
70-
// The test uses a committed fixture project in testdata/gradle-project rather
71-
// than generating Gradle files from Go.
93+
// A fake cachew HTTP server stands in for real storage.
7294
//
7395
// Requirements: Java on PATH, internet access (first run downloads Gradle wrapper).
7496
// Skipped automatically if Java is not available or in -short mode.
@@ -82,9 +104,19 @@ func TestIntegrationGradleBuildCycle(t *testing.T) {
82104
}
83105
}
84106

85-
ctx := context.Background()
107+
// ── Build the CLI binary ─────────────────────────────────────────────────
108+
binaryPath := filepath.Join(t.TempDir(), "gradle-cache")
109+
buildCmd := exec.Command("go", "build", "-o", binaryPath, ".")
110+
buildCmd.Dir = "."
111+
if out, err := buildCmd.CombinedOutput(); err != nil {
112+
t.Fatalf("go build failed: %v\n%s", err, out)
113+
}
114+
115+
// ── Start fake cachew server ─────────────────────────────────────────────
116+
server := httptest.NewServer(newFakeCachew(t.TempDir()))
117+
defer server.Close()
86118

87-
// Copy the fixture project into a temp dir so we can mutate it freely.
119+
// ── Copy the fixture project ─────────────────────────────────────────────
88120
fixtureDir := filepath.Join("testdata", "gradle-project")
89121
if _, err := os.Stat(fixtureDir); err != nil {
90122
t.Fatalf("fixture not found: %v", err)
@@ -101,10 +133,25 @@ func TestIntegrationGradleBuildCycle(t *testing.T) {
101133
gradlew := filepath.Join(projectDir, "gradlew")
102134
must(t, os.Chmod(gradlew, 0o755))
103135

104-
// ── Initialize git repo (needed for commit-based cache keys) ─────────────
136+
cacheKey := "cache-test:build"
137+
138+
// Helper to run the gradle-cache CLI.
139+
runTool := func(args ...string) string {
140+
t.Helper()
141+
cmd := exec.Command(binaryPath, args...)
142+
cmd.Dir = projectDir
143+
cmd.Env = gradleEnv(gradleUserHome)
144+
out, err := cmd.CombinedOutput()
145+
if err != nil {
146+
t.Fatalf("gradle-cache %v: %v\n%s", args, err, out)
147+
}
148+
return string(out)
149+
}
150+
151+
// ── Initialize git repo ──────────────────────────────────────────────────
105152
gitRun := func(args ...string) string {
106153
t.Helper()
107-
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", projectDir}, args...)...)
154+
cmd := exec.Command("git", append([]string{"-C", projectDir}, args...)...)
108155
cmd.Env = append(os.Environ(),
109156
"GIT_AUTHOR_NAME=Test",
110157
@@ -125,49 +172,21 @@ func TestIntegrationGradleBuildCycle(t *testing.T) {
125172

126173
// ── Step 1: Initial Gradle build ─────────────────────────────────────────
127174
t.Log("Step 1: Running initial Gradle build...")
128-
gradleRun(t, ctx, gradlew, projectDir, gradleUserHome, "build")
175+
gradleRun(t, projectDir, gradlew, gradleUserHome, "build")
129176

130-
// Verify compilation produced class files.
131177
classesDir := filepath.Join(projectDir, "build", "classes")
132178
if _, err := os.Stat(classesDir); err != nil {
133179
t.Fatalf("expected compiled classes: %v", err)
134180
}
135181

136-
// ── Step 2: Save the cache ───────────────────────────────────────────────
137-
t.Log("Step 2: Saving cache...")
138-
store := &fileBundleStore{dir: t.TempDir()}
139-
cacheKey := "cache-test:build"
140-
141-
sources := []tarSource{{BaseDir: gradleUserHome, Path: "./caches"}}
142-
if fi, err := os.Stat(filepath.Join(gradleUserHome, "wrapper")); err == nil && fi.IsDir() {
143-
sources = append(sources, tarSource{BaseDir: gradleUserHome, Path: "./wrapper"})
144-
}
145-
if fi, err := os.Stat(filepath.Join(projectDir, ".gradle", "configuration-cache")); err == nil && fi.IsDir() {
146-
sources = append(sources, tarSource{
147-
BaseDir: filepath.Join(projectDir, ".gradle"),
148-
Path: "./configuration-cache",
149-
})
150-
}
151-
152-
tmp, err := os.CreateTemp("", "gradle-cache-test-*")
153-
if err != nil {
154-
t.Fatal(err)
155-
}
156-
defer func() { _ = os.Remove(tmp.Name()) }()
157-
158-
if err := createTarZstd(ctx, tmp, sources); err != nil {
159-
t.Fatalf("createTarZstd: %v", err)
160-
}
161-
size, _ := tmp.Seek(0, io.SeekCurrent)
162-
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
163-
t.Fatal(err)
164-
}
165-
t.Logf(" Bundle size: %.1f MB", float64(size)/1e6)
166-
167-
if err := store.put(ctx, commitSHA, cacheKey, tmp, size); err != nil {
168-
t.Fatalf("store.put: %v", err)
169-
}
170-
_ = tmp.Close()
182+
// ── Step 2: Save the cache via CLI ───────────────────────────────────────
183+
t.Log("Step 2: Saving cache via CLI...")
184+
runTool("--log-level", "debug", "save",
185+
"--cachew-url", server.URL,
186+
"--cache-key", cacheKey,
187+
"--commit", commitSHA,
188+
"--gradle-user-home", gradleUserHome,
189+
)
171190

172191
// ── Step 3: Clear all Gradle state ───────────────────────────────────────
173192
t.Log("Step 3: Clearing Gradle state...")
@@ -180,40 +199,31 @@ func TestIntegrationGradleBuildCycle(t *testing.T) {
180199
t.Fatal("expected caches dir to be gone after cleanup")
181200
}
182201

183-
// ── Step 4: Restore the cache ────────────────────────────────────────────
184-
t.Log("Step 4: Restoring cache...")
185-
body, err := store.get(ctx, commitSHA, cacheKey, 0)
186-
if err != nil {
187-
t.Fatalf("store.get: %v", err)
188-
}
189-
190-
rules := []extractRule{
191-
{prefix: "caches/", baseDir: gradleUserHome},
192-
{prefix: "wrapper/", baseDir: gradleUserHome},
193-
{prefix: "configuration-cache/", baseDir: filepath.Join(projectDir, ".gradle")},
194-
}
195-
if err := extractBundleZstd(ctx, body, rules, projectDir); err != nil {
196-
t.Fatalf("extractBundleZstd: %v", err)
197-
}
198-
_ = body.Close()
202+
// ── Step 4: Restore the cache via CLI ────────────────────────────────────
203+
t.Log("Step 4: Restoring cache via CLI...")
204+
runTool("--log-level", "debug", "restore",
205+
"--cachew-url", server.URL,
206+
"--cache-key", cacheKey,
207+
"--ref", commitSHA,
208+
"--git-dir", projectDir,
209+
"--gradle-user-home", gradleUserHome,
210+
)
199211

200212
if _, err := os.Stat(filepath.Join(gradleUserHome, "caches")); err != nil {
201213
t.Fatalf("expected caches dir after restore: %v", err)
202214
}
203215

204-
// Check if configuration-cache was restored.
205216
ccRestored := filepath.Join(projectDir, ".gradle", "configuration-cache")
206217
if _, err := os.Stat(ccRestored); err != nil {
207-
t.Log(" configuration-cache dir was NOT restored (may not have been in the bundle)")
218+
t.Log(" configuration-cache dir was NOT restored")
208219
} else {
209220
t.Log(" configuration-cache dir restored")
210221
}
211222

212223
// ── Step 5: Rebuild and verify cache hits ────────────────────────────────
213224
t.Log("Step 5: Rebuilding to verify cache hits...")
214-
output := gradleRun(t, ctx, gradlew, projectDir, gradleUserHome, "build")
225+
output := gradleRun(t, projectDir, gradlew, gradleUserHome, "build")
215226

216-
// Check for configuration cache reuse.
217227
if strings.Contains(output, "Reusing configuration cache") {
218228
t.Log(" Configuration cache: reused")
219229
} else {
@@ -224,7 +234,6 @@ func TestIntegrationGradleBuildCycle(t *testing.T) {
224234
}
225235
}
226236

227-
// Check for build cache hits (FROM-CACHE) on compilation tasks.
228237
fromCacheCount := strings.Count(output, "FROM-CACHE")
229238
upToDateCount := strings.Count(output, "UP-TO-DATE")
230239
t.Logf(" Task results: %d FROM-CACHE, %d UP-TO-DATE", fromCacheCount, upToDateCount)
@@ -237,10 +246,10 @@ func TestIntegrationGradleBuildCycle(t *testing.T) {
237246
}
238247

239248
// gradleRun executes a Gradle build and returns the combined output.
240-
func gradleRun(t *testing.T, ctx context.Context, gradlew, projectDir, gradleUserHome string, tasks ...string) string {
249+
func gradleRun(t *testing.T, projectDir, gradlew, gradleUserHome string, tasks ...string) string {
241250
t.Helper()
242251
args := append(tasks, "--no-daemon", "--console=plain")
243-
cmd := exec.CommandContext(ctx, gradlew, args...)
252+
cmd := exec.Command(gradlew, args...)
244253
cmd.Dir = projectDir
245254
cmd.Env = gradleEnv(gradleUserHome)
246255

0 commit comments

Comments
 (0)