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
22package main
33
44import (
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