|
1 | 1 | package cache_test |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
| 5 | + "path/filepath" |
4 | 6 | "testing" |
| 7 | + "testing/synctest" |
5 | 8 | "time" |
6 | 9 |
|
7 | 10 | "github.com/gruntwork-io/terragrunt/internal/cache" |
8 | 11 | "github.com/stretchr/testify/assert" |
| 12 | + "github.com/stretchr/testify/require" |
9 | 13 | ) |
10 | 14 |
|
11 | 15 | func TestCacheCreation(t *testing.T) { |
@@ -52,32 +56,155 @@ func TestExpiringCacheCreation(t *testing.T) { |
52 | 56 | func TestExpiringCacheOperation(t *testing.T) { |
53 | 57 | t.Parallel() |
54 | 58 |
|
55 | | - ctx := t.Context() |
56 | | - cache := cache.NewExpiringCache[string]("test") |
| 59 | + synctest.Test(t, func(t *testing.T) { |
| 60 | + ctx := t.Context() |
| 61 | + cache := cache.NewExpiringCache[string]("test") |
57 | 62 |
|
58 | | - value, found := cache.Get(ctx, "potato") |
| 63 | + value, found := cache.Get(ctx, "potato") |
59 | 64 |
|
60 | | - assert.False(t, found) |
61 | | - assert.Empty(t, value) |
| 65 | + assert.False(t, found) |
| 66 | + assert.Empty(t, value) |
62 | 67 |
|
63 | | - cache.Put(ctx, "potato", "carrot", time.Now().Add(1*time.Second)) |
64 | | - value, found = cache.Get(ctx, "potato") |
| 68 | + cache.Put(ctx, "potato", "carrot", time.Now().Add(1*time.Second)) |
| 69 | + value, found = cache.Get(ctx, "potato") |
65 | 70 |
|
66 | | - assert.True(t, found) |
67 | | - assert.NotEmpty(t, value) |
68 | | - assert.Equal(t, "carrot", value) |
| 71 | + assert.True(t, found) |
| 72 | + assert.NotEmpty(t, value) |
| 73 | + assert.Equal(t, "carrot", value) |
| 74 | + }) |
69 | 75 | } |
70 | 76 |
|
71 | 77 | func TestExpiringCacheExpiration(t *testing.T) { |
72 | 78 | t.Parallel() |
73 | 79 |
|
| 80 | + synctest.Test(t, func(t *testing.T) { |
| 81 | + ctx := t.Context() |
| 82 | + cache := cache.NewExpiringCache[string]("test") |
| 83 | + |
| 84 | + cache.Put(ctx, "potato", "carrot", time.Now().Add(time.Second)) |
| 85 | + |
| 86 | + // Move the bubble's virtual clock past the expiration so the Get below |
| 87 | + // observes the entry as expired without any real wallclock wait. |
| 88 | + time.Sleep(2 * time.Second) |
| 89 | + |
| 90 | + value, found := cache.Get(ctx, "potato") |
| 91 | + |
| 92 | + assert.False(t, found) |
| 93 | + assert.NotEmpty(t, value) |
| 94 | + assert.Equal(t, "carrot", value) |
| 95 | + }) |
| 96 | +} |
| 97 | + |
| 98 | +func TestContextCache(t *testing.T) { |
| 99 | + t.Parallel() |
| 100 | + |
74 | 101 | ctx := t.Context() |
75 | | - cache := cache.NewExpiringCache[string]("test") |
76 | 102 |
|
77 | | - cache.Put(ctx, "potato", "carrot", time.Now().Add(-1*time.Second)) |
78 | | - value, found := cache.Get(ctx, "potato") |
| 103 | + // Missing entry returns a fresh detached instance. |
| 104 | + c := cache.ContextCache[int](ctx, "not-installed") |
| 105 | + require.NotNil(t, c) |
| 106 | + c.Put(ctx, "k", 7) |
79 | 107 |
|
80 | | - assert.False(t, found) |
81 | | - assert.NotEmpty(t, value) |
82 | | - assert.Equal(t, "carrot", value) |
| 108 | + // Installed entry round-trips. |
| 109 | + installed := cache.NewCache[int]("installed") |
| 110 | + ctxWith := context.WithValue(ctx, cache.RunCmdCacheContextKey, installed) |
| 111 | + |
| 112 | + got := cache.ContextCache[int](ctxWith, cache.RunCmdCacheContextKey) |
| 113 | + assert.Same(t, installed, got) |
| 114 | +} |
| 115 | + |
| 116 | +func TestContextWithCacheInstallsBoth(t *testing.T) { |
| 117 | + t.Parallel() |
| 118 | + |
| 119 | + ctx := cache.ContextWithCache(t.Context()) |
| 120 | + |
| 121 | + runCmd, ok := ctx.Value(cache.RunCmdCacheContextKey).(*cache.Cache[string]) |
| 122 | + require.True(t, ok) |
| 123 | + require.NotNil(t, runCmd) |
| 124 | + |
| 125 | + repoRoots, ok := ctx.Value(cache.RepoRootCacheContextKey).(*cache.RepoRootCache) |
| 126 | + require.True(t, ok) |
| 127 | + require.NotNil(t, repoRoots) |
| 128 | + assert.Equal(t, 0, repoRoots.Len()) |
| 129 | +} |
| 130 | + |
| 131 | +func TestRepoRootCacheLookupAndAdd(t *testing.T) { |
| 132 | + t.Parallel() |
| 133 | + |
| 134 | + ctx := t.Context() |
| 135 | + c := cache.NewRepoRootCache("repo") |
| 136 | + |
| 137 | + // Empty cache misses. |
| 138 | + _, ok := c.Lookup(ctx, filepath.FromSlash("/a/b")) |
| 139 | + assert.False(t, ok) |
| 140 | + |
| 141 | + outer := filepath.FromSlash("/repo") |
| 142 | + inner := filepath.FromSlash("/repo/sub/nested") |
| 143 | + |
| 144 | + // Add deepest-first, then a shallower root, to exercise insertion ordering. |
| 145 | + c.Add(ctx, outer) |
| 146 | + c.Add(ctx, inner) |
| 147 | + // Duplicate Add is a no-op. |
| 148 | + c.Add(ctx, outer) |
| 149 | + // Empty Add is a no-op. |
| 150 | + c.Add(ctx, "") |
| 151 | + assert.Equal(t, 2, c.Len()) |
| 152 | + |
| 153 | + // Adding a shallower-still root exercises the "no insertAt found" branch |
| 154 | + // where the new root is shorter than every existing one and is appended. |
| 155 | + shallow := filepath.FromSlash("/r") |
| 156 | + c.Add(ctx, shallow) |
| 157 | + assert.Equal(t, 3, c.Len()) |
| 158 | + |
| 159 | + // Exact-path hit returns the matching root. |
| 160 | + got, ok := c.Lookup(ctx, outer) |
| 161 | + assert.True(t, ok) |
| 162 | + assert.Equal(t, outer, got) |
| 163 | + |
| 164 | + // Descendant of the deeper root prefers it over the shallow one. |
| 165 | + got, ok = c.Lookup(ctx, filepath.Join(inner, "x")) |
| 166 | + assert.True(t, ok) |
| 167 | + assert.Equal(t, inner, got) |
| 168 | + |
| 169 | + // Sibling that prefix-matches lexically but not on a separator boundary |
| 170 | + // is not a hit (e.g. /repobar should not match /repo). |
| 171 | + _, ok = c.Lookup(ctx, filepath.FromSlash("/repobar/x")) |
| 172 | + assert.False(t, ok) |
| 173 | + |
| 174 | + // Path outside any cached root misses entirely. |
| 175 | + _, ok = c.Lookup(ctx, filepath.FromSlash("/elsewhere")) |
| 176 | + assert.False(t, ok) |
| 177 | +} |
| 178 | + |
| 179 | +func TestRepoRootCacheBeginEndResolve(t *testing.T) { |
| 180 | + t.Parallel() |
| 181 | + |
| 182 | + c := cache.NewRepoRootCache("repo") |
| 183 | + |
| 184 | + // Round-trip the lock twice to confirm BeginResolve/EndResolve pair up |
| 185 | + // (a missing Unlock would deadlock the second BeginResolve). The lock's |
| 186 | + // mutual-exclusion semantics are the stdlib's responsibility, not this |
| 187 | + // test's. |
| 188 | + c.BeginResolve() |
| 189 | + c.EndResolve() |
| 190 | + |
| 191 | + c.BeginResolve() |
| 192 | + c.EndResolve() |
| 193 | +} |
| 194 | + |
| 195 | +func TestContextRepoRootCache(t *testing.T) { |
| 196 | + t.Parallel() |
| 197 | + |
| 198 | + ctx := t.Context() |
| 199 | + |
| 200 | + // Missing key returns a fresh detached instance, never nil. |
| 201 | + c := cache.ContextRepoRootCache(ctx, "missing") |
| 202 | + require.NotNil(t, c) |
| 203 | + assert.Equal(t, 0, c.Len()) |
| 204 | + |
| 205 | + installed := cache.NewRepoRootCache("installed") |
| 206 | + ctxWith := context.WithValue(ctx, cache.RepoRootCacheContextKey, installed) |
| 207 | + |
| 208 | + got := cache.ContextRepoRootCache(ctxWith, cache.RepoRootCacheContextKey) |
| 209 | + assert.Same(t, installed, got) |
83 | 210 | } |
0 commit comments