@@ -6,6 +6,7 @@ package leeway
66import (
77 "archive/tar"
88 "compress/gzip"
9+ "context"
910 "crypto/sha256"
1011 "encoding/json"
1112 "errors"
@@ -18,6 +19,7 @@ import (
1819 "strings"
1920 "testing"
2021
22+ "github.com/gitpod-io/leeway/pkg/leeway/cache"
2123 "github.com/gitpod-io/leeway/pkg/leeway/cache/local"
2224)
2325
@@ -2186,3 +2188,264 @@ CMD ["echo", "test"]`
21862188 t .Log ("✅ SBOM generation correctly respects user env var override of package config" )
21872189 }
21882190}
2191+
2192+ // mockRemoteCacheWithFailures implements cache.RemoteCache for testing dependency validation.
2193+ // It simulates a remote cache where some packages exist but fail to download.
2194+ type mockRemoteCacheWithFailures struct {
2195+ existingPackages map [string ]struct {} // packages that "exist" in remote cache
2196+ failDownload map [string ]struct {} // packages that fail to download
2197+ downloaded map [string ]struct {} // track which packages were downloaded
2198+ }
2199+
2200+ func newMockRemoteCacheWithFailures () * mockRemoteCacheWithFailures {
2201+ return & mockRemoteCacheWithFailures {
2202+ existingPackages : make (map [string ]struct {}),
2203+ failDownload : make (map [string ]struct {}),
2204+ downloaded : make (map [string ]struct {}),
2205+ }
2206+ }
2207+
2208+ func (m * mockRemoteCacheWithFailures ) ExistingPackages (ctx context.Context , pkgs []cache.Package ) (map [cache.Package ]struct {}, error ) {
2209+ result := make (map [cache.Package ]struct {})
2210+ for _ , pkg := range pkgs {
2211+ if _ , exists := m .existingPackages [pkg .FullName ()]; exists {
2212+ result [pkg ] = struct {}{}
2213+ }
2214+ }
2215+ return result , nil
2216+ }
2217+
2218+ func (m * mockRemoteCacheWithFailures ) Download (ctx context.Context , dst cache.LocalCache , pkgs []cache.Package ) error {
2219+ for _ , pkg := range pkgs {
2220+ if _ , shouldFail := m .failDownload [pkg .FullName ()]; shouldFail {
2221+ // Simulate download failure - don't copy to local cache
2222+ continue
2223+ }
2224+ if _ , exists := m .existingPackages [pkg .FullName ()]; exists {
2225+ // Simulate successful download by creating a dummy cache file
2226+ m .downloaded [pkg .FullName ()] = struct {}{}
2227+ // Note: We don't actually create files here because we're testing
2228+ // the validation logic, not the actual download
2229+ }
2230+ }
2231+ return nil // Download returns nil even on failures (by design)
2232+ }
2233+
2234+ func (m * mockRemoteCacheWithFailures ) Upload (ctx context.Context , src cache.LocalCache , pkgs []cache.Package ) error {
2235+ return nil
2236+ }
2237+
2238+ func (m * mockRemoteCacheWithFailures ) UploadFile (ctx context.Context , filePath string , key string ) error {
2239+ return nil
2240+ }
2241+
2242+ func (m * mockRemoteCacheWithFailures ) HasFile (ctx context.Context , key string ) (bool , error ) {
2243+ return false , nil
2244+ }
2245+
2246+ // TestDependencyValidation_AfterDownload_Integration tests that packages with missing
2247+ // dependencies are invalidated after the download phase.
2248+ //
2249+ // Scenario:
2250+ // - Package X (Go) depends on A
2251+ // - A depends on B
2252+ // - A and B both "exist" in remote cache
2253+ // - A downloads successfully, B fails to download
2254+ // - Expected: A should be invalidated (removed from cache) because its dependency B is missing
2255+ func TestDependencyValidation_AfterDownload_Integration (t * testing.T ) {
2256+ if testing .Short () {
2257+ t .Skip ("Skipping integration test in short mode" )
2258+ }
2259+
2260+ // Create temporary workspace
2261+ tmpDir := t .TempDir ()
2262+
2263+ // Initialize git repository
2264+ gitInit := exec .Command ("git" , "init" )
2265+ gitInit .Dir = tmpDir
2266+ gitInit .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
2267+ if err := gitInit .Run (); err != nil {
2268+ t .Fatalf ("Failed to initialize git repository: %v" , err )
2269+ }
2270+
2271+ gitConfigName := exec .Command ("git" , "config" , "user.name" , "Test User" )
2272+ gitConfigName .Dir = tmpDir
2273+ if err := gitConfigName .Run (); err != nil {
2274+ t .Fatalf ("Failed to configure git user.name: %v" , err )
2275+ }
2276+
2277+ gitConfigEmail := exec .Command ("git" , "config" , "user.email" , "test@example.com" )
2278+ gitConfigEmail .Dir = tmpDir
2279+ if err := gitConfigEmail .Run (); err != nil {
2280+ t .Fatalf ("Failed to configure git user.email: %v" , err )
2281+ }
2282+
2283+ // Create WORKSPACE.yaml
2284+ workspaceYAML := `defaultTarget: "pkgX:app"`
2285+ workspacePath := filepath .Join (tmpDir , "WORKSPACE.yaml" )
2286+ if err := os .WriteFile (workspacePath , []byte (workspaceYAML ), 0644 ); err != nil {
2287+ t .Fatal (err )
2288+ }
2289+
2290+ // Create package B (leaf dependency)
2291+ pkgBDir := filepath .Join (tmpDir , "pkgB" )
2292+ if err := os .MkdirAll (pkgBDir , 0755 ); err != nil {
2293+ t .Fatal (err )
2294+ }
2295+ buildYAMLB := `packages:
2296+ - name: lib
2297+ type: generic
2298+ srcs:
2299+ - "*.txt"
2300+ config:
2301+ commands:
2302+ - ["echo", "building B"]`
2303+ if err := os .WriteFile (filepath .Join (pkgBDir , "BUILD.yaml" ), []byte (buildYAMLB ), 0644 ); err != nil {
2304+ t .Fatal (err )
2305+ }
2306+ if err := os .WriteFile (filepath .Join (pkgBDir , "b.txt" ), []byte ("B content" ), 0644 ); err != nil {
2307+ t .Fatal (err )
2308+ }
2309+
2310+ // Create package A (depends on B)
2311+ pkgADir := filepath .Join (tmpDir , "pkgA" )
2312+ if err := os .MkdirAll (pkgADir , 0755 ); err != nil {
2313+ t .Fatal (err )
2314+ }
2315+ buildYAMLA := `packages:
2316+ - name: lib
2317+ type: generic
2318+ srcs:
2319+ - "*.txt"
2320+ deps:
2321+ - pkgB:lib
2322+ config:
2323+ commands:
2324+ - ["echo", "building A"]`
2325+ if err := os .WriteFile (filepath .Join (pkgADir , "BUILD.yaml" ), []byte (buildYAMLA ), 0644 ); err != nil {
2326+ t .Fatal (err )
2327+ }
2328+ if err := os .WriteFile (filepath .Join (pkgADir , "a.txt" ), []byte ("A content" ), 0644 ); err != nil {
2329+ t .Fatal (err )
2330+ }
2331+
2332+ // Create package X (depends on A) - using Go type to require transitive deps
2333+ pkgXDir := filepath .Join (tmpDir , "pkgX" )
2334+ if err := os .MkdirAll (pkgXDir , 0755 ); err != nil {
2335+ t .Fatal (err )
2336+ }
2337+ // Use generic type but the validation logic should still work
2338+ buildYAMLX := `packages:
2339+ - name: app
2340+ type: generic
2341+ srcs:
2342+ - "*.txt"
2343+ deps:
2344+ - pkgA:lib
2345+ config:
2346+ commands:
2347+ - ["echo", "building X"]`
2348+ if err := os .WriteFile (filepath .Join (pkgXDir , "BUILD.yaml" ), []byte (buildYAMLX ), 0644 ); err != nil {
2349+ t .Fatal (err )
2350+ }
2351+ if err := os .WriteFile (filepath .Join (pkgXDir , "x.txt" ), []byte ("X content" ), 0644 ); err != nil {
2352+ t .Fatal (err )
2353+ }
2354+
2355+ // Create initial git commit
2356+ gitAdd := exec .Command ("git" , "add" , "." )
2357+ gitAdd .Dir = tmpDir
2358+ if err := gitAdd .Run (); err != nil {
2359+ t .Fatalf ("Failed to git add: %v" , err )
2360+ }
2361+
2362+ gitCommit := exec .Command ("git" , "commit" , "-m" , "initial" )
2363+ gitCommit .Dir = tmpDir
2364+ gitCommit .Env = append (os .Environ (),
2365+ "GIT_CONFIG_GLOBAL=/dev/null" ,
2366+ "GIT_AUTHOR_DATE=2021-01-01T00:00:00Z" ,
2367+ "GIT_COMMITTER_DATE=2021-01-01T00:00:00Z" ,
2368+ )
2369+ if err := gitCommit .Run (); err != nil {
2370+ t .Fatalf ("Failed to git commit: %v" , err )
2371+ }
2372+
2373+ // Load workspace
2374+ workspace , err := FindWorkspace (tmpDir , Arguments {}, "" , "" )
2375+ if err != nil {
2376+ t .Fatal (err )
2377+ }
2378+
2379+ // Get packages
2380+ pkgX , ok := workspace .Packages ["pkgX:app" ]
2381+ if ! ok {
2382+ t .Fatal ("package pkgX:app not found" )
2383+ }
2384+ pkgA , ok := workspace .Packages ["pkgA:lib" ]
2385+ if ! ok {
2386+ t .Fatal ("package pkgA:lib not found" )
2387+ }
2388+ pkgB , ok := workspace .Packages ["pkgB:lib" ]
2389+ if ! ok {
2390+ t .Fatal ("package pkgB:lib not found" )
2391+ }
2392+
2393+ t .Logf ("Package X: %s" , pkgX .FullName ())
2394+ t .Logf ("Package A: %s (depends on B)" , pkgA .FullName ())
2395+ t .Logf ("Package B: %s (leaf)" , pkgB .FullName ())
2396+
2397+ // Create local cache
2398+ cacheDir := filepath .Join (tmpDir , ".cache" )
2399+ localCache , err := local .NewFilesystemCache (cacheDir )
2400+ if err != nil {
2401+ t .Fatal (err )
2402+ }
2403+
2404+ // Create mock remote cache where A and B "exist" but B fails to download
2405+ mockRemote := newMockRemoteCacheWithFailures ()
2406+ mockRemote .existingPackages [pkgA .FullName ()] = struct {}{}
2407+ mockRemote .existingPackages [pkgB .FullName ()] = struct {}{}
2408+ mockRemote .failDownload [pkgB .FullName ()] = struct {}{} // B will fail to download
2409+
2410+ t .Log ("Mock remote cache configured:" )
2411+ t .Logf (" - %s: exists, will download successfully" , pkgA .FullName ())
2412+ t .Logf (" - %s: exists, will FAIL to download" , pkgB .FullName ())
2413+
2414+ // Create build context with mock remote cache
2415+ buildCtx , err := newBuildContext (buildOptions {
2416+ LocalCache : localCache ,
2417+ RemoteCache : mockRemote ,
2418+ Reporter : NewConsoleReporter (),
2419+ })
2420+ if err != nil {
2421+ t .Fatal (err )
2422+ }
2423+
2424+ // Build package X
2425+ // With the fix: A should be invalidated because B failed to download
2426+ // Without the fix: A would remain in cache with missing dependency B
2427+ err = pkgX .build (buildCtx )
2428+
2429+ // The build should succeed because:
2430+ // 1. A is invalidated (removed from cache) due to missing B
2431+ // 2. Both A and B are rebuilt locally
2432+ if err != nil {
2433+ t .Fatalf ("Build failed: %v" , err )
2434+ }
2435+
2436+ t .Log ("✅ Build succeeded" )
2437+
2438+ // Verify all packages are now in local cache (rebuilt)
2439+ if _ , exists := localCache .Location (pkgX ); ! exists {
2440+ t .Error ("Package X should be in local cache after build" )
2441+ }
2442+ if _ , exists := localCache .Location (pkgA ); ! exists {
2443+ t .Error ("Package A should be in local cache after build" )
2444+ }
2445+ if _ , exists := localCache .Location (pkgB ); ! exists {
2446+ t .Error ("Package B should be in local cache after build" )
2447+ }
2448+
2449+ t .Log ("✅ All packages are in local cache after build" )
2450+ t .Log ("✅ Dependency validation correctly handled missing dependency scenario" )
2451+ }
0 commit comments