Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/downloader/downloader_test.go
2655 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package downloader
5
6
import (
7
"net/http"
8
"net/http/httptest"
9
"os"
10
"os/exec"
11
"path/filepath"
12
"runtime"
13
"strings"
14
"testing"
15
"time"
16
17
"github.com/opencontainers/go-digest"
18
"gotest.tools/v3/assert"
19
)
20
21
func TestMain(m *testing.M) {
22
HideProgress = true
23
m.Run()
24
}
25
26
type downloadResult struct {
27
r *Result
28
err error
29
}
30
31
// We expect only few parallel downloads. Testing with larger number to find
32
// races quicker. 20 parallel downloads take about 120 milliseconds on M1 Pro.
33
const parallelDownloads = 20
34
35
func TestDownloadRemote(t *testing.T) {
36
ts := httptest.NewServer(http.FileServer(http.Dir("testdata")))
37
t.Cleanup(ts.Close)
38
dummyRemoteFileURL := ts.URL + "/downloader.txt"
39
const dummyRemoteFileDigest = "sha256:380481d26f897403368be7cb86ca03a4bc14b125bfaf2b93bff809a5a2ad717e"
40
dummyRemoteFileStat, err := os.Stat(filepath.Join("testdata", "downloader.txt"))
41
assert.NilError(t, err)
42
43
t.Run("without cache", func(t *testing.T) {
44
t.Run("without digest", func(t *testing.T) {
45
ctx := t.Context()
46
localPath := filepath.Join(t.TempDir(), t.Name())
47
r, err := Download(ctx, localPath, dummyRemoteFileURL)
48
assert.NilError(t, err)
49
assert.Equal(t, StatusDownloaded, r.Status)
50
51
// download again, make sure StatusSkippedIsReturned
52
r, err = Download(ctx, localPath, dummyRemoteFileURL)
53
assert.NilError(t, err)
54
assert.Equal(t, StatusSkipped, r.Status)
55
})
56
t.Run("with digest", func(t *testing.T) {
57
ctx := t.Context()
58
wrongDigest := digest.Digest("sha256:8313944efb4f38570c689813f288058b674ea6c487017a5a4738dc674b65f9d9")
59
localPath := filepath.Join(t.TempDir(), t.Name())
60
_, err := Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(wrongDigest))
61
assert.ErrorContains(t, err, "expected digest")
62
63
wrongDigest2 := digest.Digest("8313944efb4f38570c689813f288058b674ea6c487017a5a4738dc674b65f9d9")
64
_, err = Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(wrongDigest2))
65
assert.ErrorContains(t, err, "invalid checksum digest format")
66
67
r, err := Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
68
assert.NilError(t, err)
69
assert.Equal(t, StatusDownloaded, r.Status)
70
71
r, err = Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
72
assert.NilError(t, err)
73
assert.Equal(t, StatusSkipped, r.Status)
74
})
75
})
76
t.Run("with cache", func(t *testing.T) {
77
t.Run("serial", func(t *testing.T) {
78
ctx := t.Context()
79
cacheDir := filepath.Join(t.TempDir(), "cache")
80
localPath := filepath.Join(t.TempDir(), t.Name())
81
r, err := Download(ctx, localPath, dummyRemoteFileURL,
82
WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
83
assert.NilError(t, err)
84
assert.Equal(t, StatusDownloaded, r.Status)
85
86
r, err = Download(ctx, localPath, dummyRemoteFileURL,
87
WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
88
assert.NilError(t, err)
89
assert.Equal(t, StatusSkipped, r.Status)
90
91
localPath2 := localPath + "-2"
92
r, err = Download(ctx, localPath2, dummyRemoteFileURL,
93
WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
94
assert.NilError(t, err)
95
assert.Equal(t, StatusUsedCache, r.Status)
96
})
97
t.Run("parallel", func(t *testing.T) {
98
ctx := t.Context()
99
cacheDir := filepath.Join(t.TempDir(), "cache")
100
results := make(chan downloadResult, parallelDownloads)
101
for range parallelDownloads {
102
go func() {
103
// Parallel download is supported only for different instances with unique localPath.
104
localPath := filepath.Join(t.TempDir(), t.Name())
105
r, err := Download(ctx, localPath, dummyRemoteFileURL,
106
WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
107
results <- downloadResult{r, err}
108
}()
109
}
110
// Only one thread should download, the rest should use the cache.
111
downloaded, cached := countResults(t, results)
112
assert.Equal(t, downloaded, 1)
113
assert.Equal(t, cached, parallelDownloads-1)
114
})
115
})
116
t.Run("caching-only mode", func(t *testing.T) {
117
t.Run("serial", func(t *testing.T) {
118
ctx := t.Context()
119
_, err := Download(ctx, "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
120
assert.ErrorContains(t, err, "cache directory to be specified")
121
122
cacheDir := filepath.Join(t.TempDir(), "cache")
123
r, err := Download(ctx, "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest),
124
WithCacheDir(cacheDir))
125
assert.NilError(t, err)
126
assert.Equal(t, StatusDownloaded, r.Status)
127
128
r, err = Download(ctx, "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest),
129
WithCacheDir(cacheDir))
130
assert.NilError(t, err)
131
assert.Equal(t, StatusUsedCache, r.Status)
132
133
localPath := filepath.Join(t.TempDir(), t.Name())
134
r, err = Download(ctx, localPath, dummyRemoteFileURL,
135
WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
136
assert.NilError(t, err)
137
assert.Equal(t, StatusUsedCache, r.Status)
138
})
139
t.Run("parallel", func(t *testing.T) {
140
ctx := t.Context()
141
cacheDir := filepath.Join(t.TempDir(), "cache")
142
results := make(chan downloadResult, parallelDownloads)
143
for range parallelDownloads {
144
go func() {
145
r, err := Download(ctx, "", dummyRemoteFileURL,
146
WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
147
results <- downloadResult{r, err}
148
}()
149
}
150
// Only one thread should download, the rest should use the cache.
151
downloaded, cached := countResults(t, results)
152
assert.Equal(t, downloaded, 1)
153
assert.Equal(t, cached, parallelDownloads-1)
154
})
155
})
156
t.Run("cached", func(t *testing.T) {
157
ctx := t.Context()
158
_, err := Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
159
assert.ErrorContains(t, err, "cache directory to be specified")
160
161
cacheDir := filepath.Join(t.TempDir(), "cache")
162
r, err := Download(ctx, "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
163
assert.NilError(t, err)
164
assert.Equal(t, StatusDownloaded, r.Status)
165
166
r, err = Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
167
assert.NilError(t, err)
168
assert.Equal(t, StatusUsedCache, r.Status)
169
assert.Assert(t, strings.HasPrefix(r.CachePath, cacheDir), "expected %s to be in %s", r.CachePath, cacheDir)
170
171
wrongDigest := digest.Digest("sha256:8313944efb4f38570c689813f288058b674ea6c487017a5a4738dc674b65f9d9")
172
_, err = Cached(dummyRemoteFileURL, WithExpectedDigest(wrongDigest), WithCacheDir(cacheDir))
173
assert.ErrorContains(t, err, "expected digest")
174
})
175
t.Run("metadata", func(t *testing.T) {
176
ctx := t.Context()
177
_, err := Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))
178
assert.ErrorContains(t, err, "cache directory to be specified")
179
180
cacheDir := filepath.Join(t.TempDir(), "cache")
181
r, err := Download(ctx, "", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))
182
assert.NilError(t, err)
183
assert.Equal(t, StatusDownloaded, r.Status)
184
assert.Equal(t, dummyRemoteFileStat.ModTime().Truncate(time.Second).UTC(), r.LastModified)
185
assert.Equal(t, "text/plain; charset=utf-8", r.ContentType)
186
})
187
}
188
189
func countResults(t *testing.T, results chan downloadResult) (downloaded, cached int) {
190
t.Helper()
191
for range parallelDownloads {
192
result := <-results
193
if result.err != nil {
194
t.Errorf("Download failed: %s", result.err)
195
} else {
196
switch result.r.Status {
197
case StatusDownloaded:
198
downloaded++
199
case StatusUsedCache:
200
cached++
201
default:
202
t.Errorf("Unexpected download status %q", result.r.Status)
203
}
204
}
205
}
206
return downloaded, cached
207
}
208
209
func TestRedownloadRemote(t *testing.T) {
210
remoteDir := t.TempDir()
211
ts := httptest.NewServer(http.FileServer(http.Dir(remoteDir)))
212
t.Cleanup(ts.Close)
213
214
downloadDir := t.TempDir()
215
216
cacheOpt := WithCacheDir(t.TempDir())
217
218
t.Run("digest-less", func(t *testing.T) {
219
ctx := t.Context()
220
remoteFile := filepath.Join(remoteDir, "digest-less.txt")
221
assert.NilError(t, os.WriteFile(remoteFile, []byte("digest-less"), 0o644))
222
assert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now().Add(-time.Hour)))
223
opt := []Opt{cacheOpt}
224
225
// Download on the first call
226
r, err := Download(ctx, filepath.Join(downloadDir, "1"), ts.URL+"/digest-less.txt", opt...)
227
assert.NilError(t, err)
228
assert.Equal(t, StatusDownloaded, r.Status)
229
230
// Next download will use the cached download
231
r, err = Download(ctx, filepath.Join(downloadDir, "2"), ts.URL+"/digest-less.txt", opt...)
232
assert.NilError(t, err)
233
assert.Equal(t, StatusUsedCache, r.Status)
234
235
// Modifying remote file will cause redownload
236
assert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now()))
237
r, err = Download(ctx, filepath.Join(downloadDir, "3"), ts.URL+"/digest-less.txt", opt...)
238
assert.NilError(t, err)
239
assert.Equal(t, StatusDownloaded, r.Status)
240
241
// Next download will use the cached download
242
r, err = Download(ctx, filepath.Join(downloadDir, "4"), ts.URL+"/digest-less.txt", opt...)
243
assert.NilError(t, err)
244
assert.Equal(t, StatusUsedCache, r.Status)
245
})
246
247
t.Run("has-digest", func(t *testing.T) {
248
ctx := t.Context()
249
remoteFile := filepath.Join(remoteDir, "has-digest.txt")
250
bytes := []byte("has-digest")
251
assert.NilError(t, os.WriteFile(remoteFile, bytes, 0o644))
252
assert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now().Add(-time.Hour)))
253
254
digester := digest.SHA256.Digester()
255
_, err := digester.Hash().Write(bytes)
256
assert.NilError(t, err)
257
opt := []Opt{cacheOpt, WithExpectedDigest(digester.Digest())}
258
259
r, err := Download(ctx, filepath.Join(downloadDir, "has-digest1.txt"), ts.URL+"/has-digest.txt", opt...)
260
assert.NilError(t, err)
261
assert.Equal(t, StatusDownloaded, r.Status)
262
r, err = Download(ctx, filepath.Join(downloadDir, "has-digest2.txt"), ts.URL+"/has-digest.txt", opt...)
263
assert.NilError(t, err)
264
assert.Equal(t, StatusUsedCache, r.Status)
265
266
// modifying remote file won't cause redownload because expected digest is provided
267
assert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now()))
268
r, err = Download(ctx, filepath.Join(downloadDir, "has-digest3.txt"), ts.URL+"/has-digest.txt", opt...)
269
assert.NilError(t, err)
270
assert.Equal(t, StatusUsedCache, r.Status)
271
})
272
}
273
274
func TestDownloadLocal(t *testing.T) {
275
const emptyFileDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
276
const testDownloadLocalDigest = "sha256:0c1e0fba69e8919b306d030bf491e3e0c46cf0a8140ff5d7516ba3a83cbea5b3"
277
278
t.Run("without digest", func(t *testing.T) {
279
localPath := filepath.Join(t.TempDir(), t.Name())
280
localFile := filepath.Join(t.TempDir(), "test-file")
281
f, err := os.Create(localFile)
282
assert.NilError(t, err)
283
t.Cleanup(func() { _ = f.Close() })
284
testLocalFileURL := "file://" + localFile
285
286
r, err := Download(t.Context(), localPath, testLocalFileURL)
287
assert.NilError(t, err)
288
assert.Equal(t, StatusDownloaded, r.Status)
289
})
290
291
t.Run("with file digest", func(t *testing.T) {
292
ctx := t.Context()
293
localPath := filepath.Join(t.TempDir(), t.Name())
294
localTestFile := filepath.Join(t.TempDir(), "some-file")
295
testDownloadFileContents := []byte("TestDownloadLocal")
296
297
assert.NilError(t, os.WriteFile(localTestFile, testDownloadFileContents, 0o644))
298
testLocalFileURL := "file://" + localTestFile
299
wrongDigest := digest.Digest(emptyFileDigest)
300
301
_, err := Download(ctx, localPath, testLocalFileURL, WithExpectedDigest(wrongDigest))
302
assert.ErrorContains(t, err, "expected digest")
303
304
r, err := Download(ctx, localPath, testLocalFileURL, WithExpectedDigest(testDownloadLocalDigest))
305
assert.NilError(t, err)
306
assert.Equal(t, StatusDownloaded, r.Status)
307
})
308
309
t.Run("cached", func(t *testing.T) {
310
localFile := filepath.Join(t.TempDir(), "test-file")
311
f, err := os.Create(localFile)
312
assert.NilError(t, err)
313
t.Cleanup(func() { _ = f.Close() })
314
testLocalFileURL := "file://" + localFile
315
316
cacheDir := filepath.Join(t.TempDir(), "cache")
317
_, err = Cached(testLocalFileURL, WithCacheDir(cacheDir))
318
assert.ErrorContains(t, err, "not cached")
319
})
320
}
321
322
func TestDownloadCompressed(t *testing.T) {
323
if runtime.GOOS == "windows" {
324
// FIXME: `assertion failed: error is not nil: exec: "gzip": executable file not found in %PATH%`
325
t.Skip("Skipping on windows")
326
}
327
328
t.Run("gzip", func(t *testing.T) {
329
ctx := t.Context()
330
localPath := filepath.Join(t.TempDir(), t.Name())
331
localFile := filepath.Join(t.TempDir(), "test-file")
332
testDownloadCompressedContents := []byte("TestDownloadCompressed")
333
assert.NilError(t, os.WriteFile(localFile, testDownloadCompressedContents, 0o644))
334
assert.NilError(t, exec.CommandContext(ctx, "gzip", localFile).Run())
335
localFile += ".gz"
336
testLocalFileURL := "file://" + localFile
337
338
r, err := Download(ctx, localPath, testLocalFileURL, WithDecompress(true))
339
assert.NilError(t, err)
340
assert.Equal(t, StatusDownloaded, r.Status)
341
342
got, err := os.ReadFile(localPath)
343
assert.NilError(t, err)
344
assert.Equal(t, string(got), string(testDownloadCompressedContents))
345
})
346
347
t.Run("bzip2", func(t *testing.T) {
348
ctx := t.Context()
349
localPath := filepath.Join(t.TempDir(), t.Name())
350
localFile := filepath.Join(t.TempDir(), "test-file")
351
testDownloadCompressedContents := []byte("TestDownloadCompressed")
352
assert.NilError(t, os.WriteFile(localFile, testDownloadCompressedContents, 0o644))
353
assert.NilError(t, exec.CommandContext(ctx, "bzip2", localFile).Run())
354
localFile += ".bz2"
355
testLocalFileURL := "file://" + localFile
356
357
r, err := Download(ctx, localPath, testLocalFileURL, WithDecompress(true))
358
assert.NilError(t, err)
359
assert.Equal(t, StatusDownloaded, r.Status)
360
361
got, err := os.ReadFile(localPath)
362
assert.NilError(t, err)
363
assert.Equal(t, string(got), string(testDownloadCompressedContents))
364
})
365
366
t.Run("unknown decompressor", func(t *testing.T) {
367
localPath := filepath.Join(t.TempDir(), t.Name())
368
localFile := filepath.Join(t.TempDir(), "test-file.rar")
369
testDownloadCompressedContents := []byte("TestDownloadCompressed")
370
assert.NilError(t, os.WriteFile(localFile, testDownloadCompressedContents, 0o644))
371
testLocalFileURL := "file://" + localFile
372
373
r, err := Download(t.Context(), localPath, testLocalFileURL, WithDecompress(true))
374
assert.NilError(t, err)
375
assert.Equal(t, StatusDownloaded, r.Status)
376
377
got, err := os.ReadFile(localPath)
378
assert.NilError(t, err)
379
assert.Equal(t, string(got), string(testDownloadCompressedContents))
380
})
381
}
382
383