Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3)

Side by Side Diff: vpython/venv/venv_resources_test.go

Issue 2699063004: vpython: Add VirtualEnv creation package. (Closed)
Patch Set: only Python and VirtualEnv parts of the test may fail Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2017 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 package venv
6
7 import (
8 "archive/zip"
9 "crypto/sha256"
10 "encoding/hex"
11 "hash"
12 "io"
13 "net/http"
14 "os"
15 "path/filepath"
16 "testing"
17 "time"
18
19 "github.com/danjacques/gofslock/fslock"
20 "golang.org/x/net/context"
21
22 "github.com/luci/luci-go/cipd/client/cipd"
23 "github.com/luci/luci-go/common/errors"
24 "github.com/luci/luci-go/common/system/filesystem"
25 "github.com/luci/luci-go/common/testing/testfs"
26 "github.com/luci/luci-go/hardcoded/chromeinfra"
27 "github.com/luci/luci-go/vpython/api/vpython"
28 "github.com/luci/luci-go/vpython/python"
29 "github.com/luci/luci-go/vpython/wheel"
30 )
31
32 const testDataDir = "test_data"
33
34 // remoteFiles is the set of remote files to acquire.
35 var remoteFiles = []struct {
36 // install installs this remote file into the test environment.
37 install func(te *testingLoader, path string)
38
39 // name is the name of the file.
40 name string
41 // contentHash is the SHA256 has of the content.
42 contentHash string
43
44 // cipdPackage, if not empty, is the name of the CIPD package that conta ins
45 // this file.
46 cipdPackage string
47 // cipdVersion is the version string of the CIPD package.
48 cipdVersion string
49
50 // urls, if not empty, is a set of remote URLs where this file can be
51 // downloaded from.
52 urls []string
53 }{
54 {
55 install: func(tl *testingLoader, path string) { tl.virtualEn vZIP = path },
56 name: "virtualenv-15.1.0.zip",
57 contentHash: "f7682a57c98a10d32474b4c1df75478dea9a0802c140335c02 69a6ec3af46201",
58 cipdPackage: "infra/test-data/vpython/virtualenv",
59 cipdVersion: "version:15.1.0",
60 urls: []string{
61 "https://github.com/pypa/virtualenv/archive/15.1.0.zip",
62 },
63 },
64 }
65
66 // testingLoader is a map of a CIPD package name to the root directory
67 // that it should be loaded from.
68 type testingLoader struct {
69 cacheDir string
70
71 virtualEnvZIP string
72
73 pantsWheelPath string
74 shirtWheelPath string
75 }
76
77 // loadTestEnvironment sets up the test environment for the VirtualEnv tests.
78 //
79 // This environment includes the acquisition and construction of binary data
80 // that will be used to perform the VirtualEnv test suite, namely:
81 //
82 // - Building test wheel files from source.
83 // - Downloading the testing VirtualEnv package.
84 //
85 // This online setup is preferred to actually checking these binary files into
86 // Git, as it offers more versatility and doesn't clutter Git with binary junk.
87 //
88 // To optimize repeated test re-executions, withTestEnvironment will also cache
89 // the downloaded artifacts in a cache directory. All artifacts will be verified
90 // by their SHA256 hashes, which will be baked into the source here.
91 func loadTestEnvironment(ctx context.Context, t *testing.T) (*testingLoader, err or) {
92 wd, err := os.Getwd()
93 if err != nil {
94 return nil, errors.Annotate(err).Reason("failed to get working d irectory").Err()
95 }
96
97 cacheDir := filepath.Join(wd, ".venv_test_cache")
98 if err := filesystem.MakeDirs(cacheDir); err != nil {
99 return nil, errors.Annotate(err).Reason("failed to create cache dir").Err()
100 }
101
102 tl := testingLoader{
103 cacheDir: cacheDir,
104 }
105 return &tl, tl.withCacheLock(t, func() error {
106 return tl.ensureRemoteFilesLocked(ctx, t)
107 })
108 }
109
110 func (tl *testingLoader) withCacheLock(t *testing.T, fn func() error) error {
111 lockPath := filepath.Join(tl.cacheDir, ".lock")
112 blocker := func() error {
113 t.Logf("Cache [%s] is currently locked; sleeping...", lockPath)
114 time.Sleep(1 * time.Second)
115 return nil
116 }
117 return fslock.WithBlocking(lockPath, blocker, func() error {
118 return fn()
119 })
120 }
121
122 func (tl *testingLoader) ensureWheels(ctx context.Context, t *testing.T, py *pyt hon.Interpreter, tdir string) error {
123 var err error
124 if tl.pantsWheelPath, err = tl.buildWheelLocked(t, py, "pants-1.2-py2.py 3-none-any.whl", tdir); err != nil {
125 return err
126 }
127 if tl.shirtWheelPath, err = tl.buildWheelLocked(t, py, "shirt-3.14-py2.p y3-none-any.whl", tdir); err != nil {
128 return err
129 }
130 return nil
131 }
132
133 func (tl *testingLoader) Resolve(c context.Context, root string, packages []*vpy thon.Spec_Package) error {
134 for _, pkg := range packages {
135 pkg.Version = "resolved"
136 }
137 return nil
138 }
139
140 func (tl *testingLoader) Ensure(c context.Context, root string, packages []*vpyt hon.Spec_Package) error {
141 for _, pkg := range packages {
142 if err := tl.installPackage(pkg.Path, root); err != nil {
143 return err
144 }
145 }
146 return nil
147 }
148
149 func (tl *testingLoader) installPackage(name, root string) error {
150 switch name {
151 case "foo/bar/virtualenv":
152 return unzip(tl.virtualEnvZIP, root)
153 case "foo/bar/shirt":
154 return copyFileIntoDir(tl.shirtWheelPath, root)
155 case "foo/bar/pants":
156 return copyFileIntoDir(tl.pantsWheelPath, root)
157
158 default:
159 return errors.Reason("don't know how to install %(package)q").
160 D("package", name).
161 Err()
162 }
163 }
164
165 func (tl *testingLoader) buildWheelLocked(t *testing.T, py *python.Interpreter, name, outDir string) (string, error) {
166 w, err := wheel.ParseName(name)
167 if err != nil {
168 return "", errors.Annotate(err).Reason("failed to parse wheel na me %(name)q").
169 D("name", name).
170 Err()
171 }
172
173 outWheelPath := filepath.Join(outDir, w.String())
174 switch _, err := os.Stat(outWheelPath); {
175 case err == nil:
176 t.Logf("Using cached wheel for %q: %s", name, outWheelPath)
177 return outWheelPath, nil
178
179 case os.IsNotExist(err):
180 // Will build a new wheel.
181 break
182
183 default:
184 return "", errors.Annotate(err).Reason("failed to stat wheel pat h [%(path)s]").
185 D("path", outWheelPath).
186 Err()
187 }
188
189 srcDir := filepath.Join(testDataDir, w.Distribution+".src")
190
191 // Create a bootstrap wheel-generating VirtualEnv!
192 cfg := Config{
193 MaxHashLen: 1, // Only going to be 1 enviroment.
194 BaseDir: filepath.Join(outDir, "wheel-vpython"),
195 Python: py.Python,
196 Package: vpython.Spec_Package{
197 Path: "foo/bar/virtualenv",
198 Version: "whatever",
199 },
200 Loader: tl,
201 Spec: &vpython.Spec{},
202
203 // Testing parameters for this bootstrap wheel-building environm ent.
204 testPreserveInstallationCapability: true,
205 testLeaveReadWrite: true,
206 }
207
208 return outWheelPath, With(context.Background(), cfg, true, func(ctx cont ext.Context, env *Env) error {
209 cmd := env.InterpreterCommand()
210 cmd.WorkDir = srcDir
211 if err := cmd.Run(context.Background(), "setup.py", "bdist_wheel ", "--dist-dir", outDir); err != nil {
212 return errors.Annotate(err).Reason("failed to build whee l").Err()
213 }
214
215 if _, err := os.Stat(outWheelPath); err != nil {
216 return errors.Annotate(err).Reason("failed to generate w heel").Err()
217 }
218
219 t.Logf("Generated wheel file %q: %s", name, outWheelPath)
220 return nil
221 })
222 }
223
224 func (tl *testingLoader) ensureRemoteFilesLocked(ctx context.Context, t *testing .T) error {
225 MainLoop:
226 for _, rf := range remoteFiles {
227 cachePath := filepath.Join(tl.cacheDir, rf.name)
228
229 // Check if the remote file is already cached.
230 err := getCachedFileLocked(t, cachePath, rf.contentHash)
231 if err == nil {
232 t.Logf("Remote file [%s] is already cached: [%s]", rf.na me, cachePath)
233 rf.install(tl, cachePath)
234 continue MainLoop
235 }
236 t.Logf("Remote file [%s] is not cached: %s", rf.name, err)
237
238 // Download from CIPD.
239 if rf.cipdPackage != "" {
240 err := cacheFromCIPDLocked(ctx, t, cachePath, rf.name, r f.contentHash, rf.cipdPackage, rf.cipdVersion)
241 if err == nil {
242 t.Logf("Cached remote file [%s] from CIPD source : [%s]", rf.name, cachePath)
243 rf.install(tl, cachePath)
244 continue MainLoop
245 }
246 t.Logf("Failed to load from CIPD package %q @%q: %s", rf .cipdPackage, rf.cipdVersion, err)
247 }
248
249 // Download from URL.
250 for _, url := range rf.urls {
251 err := cacheFromURLLocked(t, cachePath, rf.contentHash, url)
252 if err == nil {
253 t.Logf("Cached remote file [%s] from URL [%s]: [ %s]", rf.name, url, cachePath)
254 rf.install(tl, cachePath)
255 continue MainLoop
256 }
257 t.Logf("Failed to load from URL %q: %s", url, err)
258 }
259
260 return errors.Reason("failed to acquire remote file %(name)q").
261 D("name", rf.name).
262 Err()
263 }
264
265 return nil
266 }
267
268 func getCachedFileLocked(t *testing.T, cachePath, hash string) error {
269 return validateHash(t, cachePath, hash, true)
270 }
271
272 func validateHash(t *testing.T, path, hash string, deleteIfInvalid bool) error {
273 fd, err := os.Open(path)
274 if err != nil {
275 return errors.Annotate(err).Reason("failed to open file").Err()
276 }
277 defer fd.Close()
278
279 h := sha256.New()
280 if _, err := io.Copy(h, fd); err != nil {
281 return errors.Annotate(err).Reason("failed to hash file").Err()
282 }
283
284 if err := hashesEqual(h, hash); err != nil {
285 t.Logf("File [%s] has invalid hash: %s", path, err)
286
287 if deleteIfInvalid {
288 if err := os.Remove(path); err != nil {
289 t.Logf("Failed to delete invalid hash file [%s]: %s", path, err)
290 }
291 }
292 return err
293 }
294
295 return nil
296 }
297
298 func hashesEqual(h hash.Hash, expected string) error {
299 if v := hex.EncodeToString(h.Sum(nil)); v != expected {
300 return errors.Reason("hash %(actual)q doesn't match expected %(e xpected)q").
301 D("actual", v).
302 D("expected", expected).
303 Err()
304 }
305 return nil
306 }
307
308 var testCIPDClientOptions = cipd.ClientOptions{
309 ServiceURL: chromeinfra.CIPDServiceURL,
310 UserAgent: "vpython venv tests",
311 }
312
313 func cacheFromCIPDLocked(ctx context.Context, t *testing.T, cachePath, name, has h, pkg, version string) error {
314 return testfs.WithTempDir(t, "vpython_venv_cipd", func(tdir string) erro r {
315 opts := testCIPDClientOptions
316 opts.Root = tdir
317
318 client, err := cipd.NewClient(opts)
319 if err != nil {
320 return errors.Annotate(err).Reason("failed to create CIP D client").Err()
321 }
322
323 pin, err := client.ResolveVersion(ctx, pkg, version)
324 if err != nil {
325 return errors.Annotate(err).Reason("failed to resolve CI PD version for %(pkg)s @%(version)s").
326 D("pkg", pkg).
327 D("version", version).
328 Err()
329 }
330
331 if err := client.FetchAndDeployInstance(ctx, "", pin); err != ni l {
332 return errors.Annotate(err).Reason("failed to fetch/depl oy CIPD package").Err()
333 }
334
335 path := filepath.Join(opts.Root, name)
336 if err := validateHash(t, path, hash, false); err != nil {
337 // Do not export the invalid path.
338 return err
339 }
340
341 if err := copyFile(path, cachePath, nil); err != nil {
342 return errors.Annotate(err).Reason("failed to install CI PD package file").Err()
343 }
344
345 return nil
346 })
347 }
348
349 func cacheFromURLLocked(t *testing.T, cachePath, hash, url string) (err error) {
350 resp, err := http.Get(url)
351 if err != nil {
352 t.Logf("Failed to GET file from URL [%s]: %s", url, err)
353 }
354 defer resp.Body.Close()
355
356 fd, err := os.Create(cachePath)
357 if err != nil {
358 t.Logf("Failed to create output file [%s]: %s", cachePath, err)
359 }
360 defer func() {
361 if closeErr := fd.Close(); closeErr != nil && err == nil {
362 err = errors.Annotate(closeErr).Reason("failed to close file").Err()
363 }
364 }()
365
366 h := sha256.New()
367 tr := io.TeeReader(resp.Body, h)
368 if _, err := io.Copy(fd, tr); err != nil {
369 return errors.Annotate(err).Reason("failed to download").Err()
370 }
371
372 if err = hashesEqual(h, hash); err != nil {
373 return
374 }
375 return nil
376 }
377
378 func unzip(src, dst string) error {
379 fd, err := zip.OpenReader(src)
380 if err != nil {
381 return errors.Annotate(err).Reason("failed to open ZIP reader"). Err()
382 }
383 defer fd.Close()
384
385 for _, f := range fd.File {
386 path := filepath.Join(dst, filepath.FromSlash(f.Name))
387 fi := f.FileInfo()
388
389 // Unzip this entry.
390 if fi.IsDir() {
391 if err := os.MkdirAll(path, 0755); err != nil {
392 return errors.Annotate(err).Reason("failed to mk dir").Err()
393 }
394 } else {
395 if err := copyFileOpener(f.Open, path, fi); err != nil {
396 return err
397 }
398 }
399 }
400 return nil
401 }
402
403 func copyFileIntoDir(src, dstDir string) error {
404 return copyFile(src, filepath.Join(dstDir, filepath.Base(src)), nil)
405 }
406
407 func copyFile(src, dst string, fi os.FileInfo) error {
408 opener := func() (io.ReadCloser, error) { return os.Open(src) }
409 return copyFileOpener(opener, dst, fi)
410 }
411
412 func copyFileOpener(opener func() (io.ReadCloser, error), dst string, fi os.File Info) (err error) {
413 sfd, err := opener()
414 if err != nil {
415 return errors.Annotate(err).Reason("failed to open source").Err( )
416 }
417 defer sfd.Close()
418
419 dfd, err := os.Create(dst)
420 if err != nil {
421 return errors.Annotate(err).Reason("failed to create destination ").Err()
422 }
423 defer func() {
424 if closeErr := dfd.Close(); closeErr != nil && err == nil {
425 err = errors.Annotate(closeErr).Reason("failed to close destination").Err()
426 }
427 }()
428
429 if _, err := io.Copy(dfd, sfd); err != nil {
430 return errors.Annotate(err).Reason("failed to copy file").Err()
431 }
432 if fi != nil {
433 if err := os.Chmod(dst, fi.Mode()); err != nil {
434 return errors.Annotate(err).Reason("failed to chmod").Er r()
435 }
436 }
437 return nil
438 }
OLDNEW
« vpython/venv/venv.go ('K') | « vpython/venv/venv.go ('k') | vpython/venv/venv_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698