1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
|
package ops
import (
"context"
"testing"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/solver/pb"
"github.com/stretchr/testify/require"
)
func TestDedupePaths(t *testing.T) {
res := dedupePaths([]string{"/Gemfile", "/Gemfile/foo"})
require.Equal(t, []string{"/Gemfile"}, res)
res = dedupePaths([]string{"/Gemfile/bar", "/Gemfile/foo"})
require.Equal(t, []string{"/Gemfile/bar", "/Gemfile/foo"}, res)
res = dedupePaths([]string{"/Gemfile", "/Gemfile.lock"})
require.Equal(t, []string{"/Gemfile", "/Gemfile.lock"}, res)
res = dedupePaths([]string{"/Gemfile.lock", "/Gemfile"})
require.Equal(t, []string{"/Gemfile", "/Gemfile.lock"}, res)
res = dedupePaths([]string{"/foo", "/Gemfile", "/Gemfile/foo"})
require.Equal(t, []string{"/Gemfile", "/foo"}, res)
res = dedupePaths([]string{"/foo/bar/baz", "/foo/bara", "/foo/bar/bax", "/foo/bar"})
require.Equal(t, []string{"/foo/bar", "/foo/bara"}, res)
res = dedupePaths([]string{"/", "/foo"})
require.Equal(t, []string{"/"}, res)
}
func TestExecOpCacheMap(t *testing.T) {
type testCase struct {
name string
op1, op2 *ExecOp
xMatch bool
}
testCases := []testCase{
{name: "empty", op1: newExecOp(), op2: newExecOp(), xMatch: true},
{
name: "empty vs with non-nil but empty mounts should match",
op1: newExecOp(),
op2: newExecOp(withEmptyMounts),
xMatch: true,
},
{
name: "both non-nil but empty mounts should match",
op1: newExecOp(withEmptyMounts),
op2: newExecOp(withEmptyMounts),
xMatch: true,
},
{
name: "non-nil but empty mounts vs with mounts should not match",
op1: newExecOp(withEmptyMounts),
op2: newExecOp(withNewMount("/foo")),
xMatch: false,
},
{
name: "mounts to different paths should not match",
op1: newExecOp(withNewMount("/foo")),
op2: newExecOp(withNewMount("/bar")),
xMatch: false,
},
{
name: "mounts to same path should match",
op1: newExecOp(withNewMount("/foo")),
op2: newExecOp(withNewMount("/foo")),
xMatch: true,
},
{
name: "cache mount should not match non-cache mount at same path",
op1: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "someID"}))),
op2: newExecOp(withNewMount("/foo")),
xMatch: false,
},
{
name: "different cache id's at the same path should match",
op1: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "someID"}))),
op2: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "someOtherID"}))),
xMatch: true,
},
{
// This is a special case for default dockerfile cache mounts for backwards compatibility.
name: "default dockerfile cache mount should not match the same cache mount but with different sharing",
op1: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "/foo"}))),
op2: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "/foo", Sharing: pb.CacheSharingOpt_LOCKED}))),
xMatch: false,
},
{
name: "cache mounts with the same ID but different sharing options should match",
op1: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "someID", Sharing: 0}))),
op2: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "someID", Sharing: 1}))),
xMatch: true,
},
{
name: "cache mounts with different IDs and different sharing should match at the same path",
op1: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "someID", Sharing: 0}))),
op2: newExecOp(withNewMount("/foo", withCache(&pb.CacheOpt{ID: "someOtherID", Sharing: 1}))),
xMatch: true,
},
}
ctx := context.Background()
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
m1, ok, err := tc.op1.CacheMap(ctx, session.NewGroup(t.Name()), 1)
require.NoError(t, err)
require.True(t, ok)
m2, ok, err := tc.op2.CacheMap(ctx, session.NewGroup(t.Name()), 1)
require.NoError(t, err)
require.True(t, ok)
if tc.xMatch {
require.Equal(t, m1.Digest, m2.Digest, "\n\nm1: %+v\nm2: %+v", m1, m2)
} else {
require.NotEqual(t, m1.Digest, m2.Digest, "\n\nm1: %+v\nm2: %+v", m1, m2)
}
})
}
}
func TestExecOpContentCache(t *testing.T) {
type testCase struct {
name string
op *ExecOp
// cacheByDefault is whether content-caching is enabled by default for this mount
cacheByDefault bool
// cacheIsSafe is whether content-cachine can be safely enabled for this mount
cacheIsSafe bool
}
testCases := []testCase{
{
name: "with sub mount",
op: newExecOp(withNewMount("/foo", withSelector("/bar"))),
cacheByDefault: false,
cacheIsSafe: false,
},
{
name: "with read-only sub mount",
op: newExecOp(withNewMount("/foo", withSelector("/bar"), withReadonly())),
cacheByDefault: true,
cacheIsSafe: true,
},
{
name: "with no-output sub mount",
op: newExecOp(withNewMount("/foo", withSelector("/bar"), withoutOutput())),
cacheByDefault: true,
cacheIsSafe: true,
},
{
name: "with root sub mount",
op: newExecOp(withNewMount("/foo", withSelector("/"))),
cacheByDefault: true,
cacheIsSafe: true,
},
{
name: "with root mount",
op: newExecOp(withNewMount("/", withSelector("/bar"))),
cacheByDefault: false,
cacheIsSafe: false,
},
{
name: "with root read-only mount",
op: newExecOp(withNewMount("/", withSelector("/bar"), withReadonly())),
cacheByDefault: false,
cacheIsSafe: true,
},
{
name: "with root no-output mount",
op: newExecOp(withNewMount("/", withSelector("/bar"), withoutOutput())),
cacheByDefault: false,
cacheIsSafe: true,
},
{
name: "with root mount",
op: newExecOp(withNewMount("/", withSelector("/"))),
cacheByDefault: false,
cacheIsSafe: true,
},
}
ctx := context.Background()
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// default is always valid, and can sometimes have slow-cache
m, ok, err := tc.op.CacheMap(ctx, session.NewGroup(t.Name()), 1)
require.NoError(t, err)
require.True(t, ok)
for _, dep := range m.Deps {
if tc.cacheByDefault {
require.NotZero(t, dep.ComputeDigestFunc)
} else {
require.Zero(t, dep.ComputeDigestFunc)
}
}
// off is always valid, and never has slow-cache
for _, mnt := range tc.op.op.Mounts {
mnt.ContentCache = pb.MountContentCache_OFF
}
m, ok, err = tc.op.CacheMap(ctx, session.NewGroup(t.Name()), 1)
require.NoError(t, err)
require.True(t, ok)
for _, dep := range m.Deps {
require.Zero(t, dep.ComputeDigestFunc)
}
// on is sometimes valid, and always has slow-cache if valid
for _, mnt := range tc.op.op.Mounts {
mnt.ContentCache = pb.MountContentCache_ON
}
m, ok, err = tc.op.CacheMap(ctx, session.NewGroup(t.Name()), 1)
if tc.cacheIsSafe {
require.NoError(t, err)
require.True(t, ok)
for _, dep := range m.Deps {
require.NotZero(t, dep.ComputeDigestFunc)
}
} else {
require.False(t, ok)
require.ErrorContains(t, err, "invalid mount")
}
})
}
}
func newExecOp(opts ...func(*ExecOp)) *ExecOp {
op := &ExecOp{op: &pb.ExecOp{Meta: &pb.Meta{}}}
for _, opt := range opts {
opt(op)
}
return op
}
func withEmptyMounts(op *ExecOp) {
op.op.Mounts = []*pb.Mount{}
}
func withNewMount(p string, opts ...func(*pb.Mount)) func(*ExecOp) {
return func(op *ExecOp) {
m := &pb.Mount{
Dest: p,
Input: int64(op.numInputs),
// Generate a new selector for each mount since this should not effect the cache key.
// This helps exercise that code path.
Selector: identity.NewID(),
}
for _, opt := range opts {
opt(m)
}
op.op.Mounts = append(op.op.Mounts, m)
op.numInputs++
}
}
func withSelector(selector string) func(*pb.Mount) {
return func(m *pb.Mount) {
m.Selector = selector
}
}
func withCache(cache *pb.CacheOpt) func(*pb.Mount) {
return func(m *pb.Mount) {
m.CacheOpt = cache
m.MountType = pb.MountType_CACHE
}
}
func withReadonly() func(*pb.Mount) {
return func(m *pb.Mount) {
m.Readonly = true
}
}
func withoutOutput() func(*pb.Mount) {
return func(m *pb.Mount) {
m.Output = int64(pb.SkipOutput)
}
}
|