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

Unified Diff: vpython/venv/venv_resources_test.go

Issue 2699063004: vpython: Add VirtualEnv creation package. (Closed)
Patch Set: bootstrap straight out of tempdir 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « vpython/venv/venv.go ('k') | vpython/venv/venv_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: vpython/venv/venv_resources_test.go
diff --git a/vpython/venv/venv_resources_test.go b/vpython/venv/venv_resources_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7bd991cde6f0457588194ffeea529d8ba8114ee4
--- /dev/null
+++ b/vpython/venv/venv_resources_test.go
@@ -0,0 +1,438 @@
+// Copyright 2017 The LUCI Authors. All rights reserved.
+// Use of this source code is governed under the Apache License, Version 2.0
+// that can be found in the LICENSE file.
+
+package venv
+
+import (
+ "archive/zip"
+ "crypto/sha256"
+ "encoding/hex"
+ "hash"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/danjacques/gofslock/fslock"
+ "golang.org/x/net/context"
+
+ "github.com/luci/luci-go/cipd/client/cipd"
+ "github.com/luci/luci-go/common/errors"
+ "github.com/luci/luci-go/common/system/filesystem"
+ "github.com/luci/luci-go/common/testing/testfs"
+ "github.com/luci/luci-go/hardcoded/chromeinfra"
+ "github.com/luci/luci-go/vpython/api/vpython"
+ "github.com/luci/luci-go/vpython/python"
+ "github.com/luci/luci-go/vpython/wheel"
+)
+
+const testDataDir = "test_data"
+
+// remoteFiles is the set of remote files to acquire.
+var remoteFiles = []struct {
+ // install installs this remote file into the test environment.
+ install func(te *testingLoader, path string)
+
+ // name is the name of the file.
+ name string
+ // contentHash is the SHA256 has of the content.
+ contentHash string
+
+ // cipdPackage, if not empty, is the name of the CIPD package that contains
+ // this file.
+ cipdPackage string
+ // cipdVersion is the version string of the CIPD package.
+ cipdVersion string
+
+ // urls, if not empty, is a set of remote URLs where this file can be
+ // downloaded from.
+ urls []string
+}{
+ {
+ install: func(tl *testingLoader, path string) { tl.virtualEnvZIP = path },
+ name: "virtualenv-15.1.0.zip",
+ contentHash: "f7682a57c98a10d32474b4c1df75478dea9a0802c140335c0269a6ec3af46201",
+ cipdPackage: "infra/test-data/vpython/virtualenv",
+ cipdVersion: "version:15.1.0",
+ urls: []string{
+ "https://github.com/pypa/virtualenv/archive/15.1.0.zip",
+ },
+ },
+}
+
+// testingLoader is a map of a CIPD package name to the root directory
+// that it should be loaded from.
+type testingLoader struct {
+ cacheDir string
+
+ virtualEnvZIP string
+
+ pantsWheelPath string
+ shirtWheelPath string
+}
+
+// loadTestEnvironment sets up the test environment for the VirtualEnv tests.
+//
+// This environment includes the acquisition and construction of binary data
+// that will be used to perform the VirtualEnv test suite, namely:
+//
+// - Building test wheel files from source.
+// - Downloading the testing VirtualEnv package.
+//
+// This online setup is preferred to actually checking these binary files into
+// Git, as it offers more versatility and doesn't clutter Git with binary junk.
+//
+// To optimize repeated test re-executions, withTestEnvironment will also cache
+// the downloaded artifacts in a cache directory. All artifacts will be verified
+// by their SHA256 hashes, which will be baked into the source here.
+func loadTestEnvironment(ctx context.Context, t *testing.T) (*testingLoader, error) {
+ wd, err := os.Getwd()
+ if err != nil {
+ return nil, errors.Annotate(err).Reason("failed to get working directory").Err()
+ }
+
+ cacheDir := filepath.Join(wd, ".venv_test_cache")
+ if err := filesystem.MakeDirs(cacheDir); err != nil {
+ return nil, errors.Annotate(err).Reason("failed to create cache dir").Err()
+ }
+
+ tl := testingLoader{
+ cacheDir: cacheDir,
+ }
+ return &tl, tl.withCacheLock(t, func() error {
+ return tl.ensureRemoteFilesLocked(ctx, t)
+ })
+}
+
+func (tl *testingLoader) withCacheLock(t *testing.T, fn func() error) error {
+ lockPath := filepath.Join(tl.cacheDir, ".lock")
+ blocker := func() error {
+ t.Logf("Cache [%s] is currently locked; sleeping...", lockPath)
+ time.Sleep(1 * time.Second)
+ return nil
+ }
+ return fslock.WithBlocking(lockPath, blocker, func() error {
+ return fn()
+ })
+}
+
+func (tl *testingLoader) ensureWheels(ctx context.Context, t *testing.T, py *python.Interpreter, tdir string) error {
+ var err error
+ if tl.pantsWheelPath, err = tl.buildWheelLocked(t, py, "pants-1.2-py2.py3-none-any.whl", tdir); err != nil {
+ return err
+ }
+ if tl.shirtWheelPath, err = tl.buildWheelLocked(t, py, "shirt-3.14-py2.py3-none-any.whl", tdir); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (tl *testingLoader) Resolve(c context.Context, root string, packages []*vpython.Spec_Package) error {
+ for _, pkg := range packages {
+ pkg.Version = "resolved"
+ }
+ return nil
+}
+
+func (tl *testingLoader) Ensure(c context.Context, root string, packages []*vpython.Spec_Package) error {
+ for _, pkg := range packages {
+ if err := tl.installPackage(pkg.Path, root); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (tl *testingLoader) installPackage(name, root string) error {
+ switch name {
+ case "foo/bar/virtualenv":
+ return unzip(tl.virtualEnvZIP, root)
+ case "foo/bar/shirt":
+ return copyFileIntoDir(tl.shirtWheelPath, root)
+ case "foo/bar/pants":
+ return copyFileIntoDir(tl.pantsWheelPath, root)
+
+ default:
+ return errors.Reason("don't know how to install %(package)q").
+ D("package", name).
+ Err()
+ }
+}
+
+func (tl *testingLoader) buildWheelLocked(t *testing.T, py *python.Interpreter, name, outDir string) (string, error) {
+ w, err := wheel.ParseName(name)
+ if err != nil {
+ return "", errors.Annotate(err).Reason("failed to parse wheel name %(name)q").
+ D("name", name).
+ Err()
+ }
+
+ outWheelPath := filepath.Join(outDir, w.String())
+ switch _, err := os.Stat(outWheelPath); {
+ case err == nil:
+ t.Logf("Using cached wheel for %q: %s", name, outWheelPath)
+ return outWheelPath, nil
+
+ case os.IsNotExist(err):
+ // Will build a new wheel.
+ break
+
+ default:
+ return "", errors.Annotate(err).Reason("failed to stat wheel path [%(path)s]").
+ D("path", outWheelPath).
+ Err()
+ }
+
+ srcDir := filepath.Join(testDataDir, w.Distribution+".src")
+
+ // Create a bootstrap wheel-generating VirtualEnv!
+ cfg := Config{
+ MaxHashLen: 1, // Only going to be 1 enviroment.
+ BaseDir: filepath.Join(outDir, ".env"),
+ Python: py.Python,
+ Package: vpython.Spec_Package{
+ Path: "foo/bar/virtualenv",
+ Version: "whatever",
+ },
+ Loader: tl,
+ Spec: &vpython.Spec{},
+
+ // Testing parameters for this bootstrap wheel-building environment.
+ testPreserveInstallationCapability: true,
+ testLeaveReadWrite: true,
+ }
+
+ return outWheelPath, With(context.Background(), cfg, true, func(ctx context.Context, env *Env) error {
+ cmd := env.InterpreterCommand()
+ cmd.WorkDir = srcDir
+ if err := cmd.Run(context.Background(), "setup.py", "bdist_wheel", "--dist-dir", outDir); err != nil {
+ return errors.Annotate(err).Reason("failed to build wheel").Err()
+ }
+
+ if _, err := os.Stat(outWheelPath); err != nil {
+ return errors.Annotate(err).Reason("failed to generate wheel").Err()
+ }
+
+ t.Logf("Generated wheel file %q: %s", name, outWheelPath)
+ return nil
+ })
+}
+
+func (tl *testingLoader) ensureRemoteFilesLocked(ctx context.Context, t *testing.T) error {
+MainLoop:
+ for _, rf := range remoteFiles {
+ cachePath := filepath.Join(tl.cacheDir, rf.name)
+
+ // Check if the remote file is already cached.
+ err := getCachedFileLocked(t, cachePath, rf.contentHash)
+ if err == nil {
+ t.Logf("Remote file [%s] is already cached: [%s]", rf.name, cachePath)
+ rf.install(tl, cachePath)
+ continue MainLoop
+ }
+ t.Logf("Remote file [%s] is not cached: %s", rf.name, err)
+
+ // Download from CIPD.
+ if rf.cipdPackage != "" {
+ err := cacheFromCIPDLocked(ctx, t, cachePath, rf.name, rf.contentHash, rf.cipdPackage, rf.cipdVersion)
+ if err == nil {
+ t.Logf("Cached remote file [%s] from CIPD source: [%s]", rf.name, cachePath)
+ rf.install(tl, cachePath)
+ continue MainLoop
+ }
+ t.Logf("Failed to load from CIPD package %q @%q: %s", rf.cipdPackage, rf.cipdVersion, err)
+ }
+
+ // Download from URL.
+ for _, url := range rf.urls {
+ err := cacheFromURLLocked(t, cachePath, rf.contentHash, url)
+ if err == nil {
+ t.Logf("Cached remote file [%s] from URL [%s]: [%s]", rf.name, url, cachePath)
+ rf.install(tl, cachePath)
+ continue MainLoop
+ }
+ t.Logf("Failed to load from URL %q: %s", url, err)
+ }
+
+ return errors.Reason("failed to acquire remote file %(name)q").
+ D("name", rf.name).
+ Err()
+ }
+
+ return nil
+}
+
+func getCachedFileLocked(t *testing.T, cachePath, hash string) error {
+ return validateHash(t, cachePath, hash, true)
+}
+
+func validateHash(t *testing.T, path, hash string, deleteIfInvalid bool) error {
+ fd, err := os.Open(path)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to open file").Err()
+ }
+ defer fd.Close()
+
+ h := sha256.New()
+ if _, err := io.Copy(h, fd); err != nil {
+ return errors.Annotate(err).Reason("failed to hash file").Err()
+ }
+
+ if err := hashesEqual(h, hash); err != nil {
+ t.Logf("File [%s] has invalid hash: %s", path, err)
+
+ if deleteIfInvalid {
+ if err := os.Remove(path); err != nil {
+ t.Logf("Failed to delete invalid hash file [%s]: %s", path, err)
+ }
+ }
+ return err
+ }
+
+ return nil
+}
+
+func hashesEqual(h hash.Hash, expected string) error {
+ if v := hex.EncodeToString(h.Sum(nil)); v != expected {
+ return errors.Reason("hash %(actual)q doesn't match expected %(expected)q").
+ D("actual", v).
+ D("expected", expected).
+ Err()
+ }
+ return nil
+}
+
+var testCIPDClientOptions = cipd.ClientOptions{
+ ServiceURL: chromeinfra.CIPDServiceURL,
+ UserAgent: "vpython venv tests",
+}
+
+func cacheFromCIPDLocked(ctx context.Context, t *testing.T, cachePath, name, hash, pkg, version string) error {
+ return testfs.WithTempDir(t, "vpython_venv_cipd", func(tdir string) error {
+ opts := testCIPDClientOptions
+ opts.Root = tdir
+
+ client, err := cipd.NewClient(opts)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to create CIPD client").Err()
+ }
+
+ pin, err := client.ResolveVersion(ctx, pkg, version)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to resolve CIPD version for %(pkg)s @%(version)s").
+ D("pkg", pkg).
+ D("version", version).
+ Err()
+ }
+
+ if err := client.FetchAndDeployInstance(ctx, "", pin); err != nil {
+ return errors.Annotate(err).Reason("failed to fetch/deploy CIPD package").Err()
+ }
+
+ path := filepath.Join(opts.Root, name)
+ if err := validateHash(t, path, hash, false); err != nil {
+ // Do not export the invalid path.
+ return err
+ }
+
+ if err := copyFile(path, cachePath, nil); err != nil {
+ return errors.Annotate(err).Reason("failed to install CIPD package file").Err()
+ }
+
+ return nil
+ })
+}
+
+func cacheFromURLLocked(t *testing.T, cachePath, hash, url string) (err error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ t.Logf("Failed to GET file from URL [%s]: %s", url, err)
+ }
+ defer resp.Body.Close()
+
+ fd, err := os.Create(cachePath)
+ if err != nil {
+ t.Logf("Failed to create output file [%s]: %s", cachePath, err)
+ }
+ defer func() {
+ if closeErr := fd.Close(); closeErr != nil && err == nil {
+ err = errors.Annotate(closeErr).Reason("failed to close file").Err()
+ }
+ }()
+
+ h := sha256.New()
+ tr := io.TeeReader(resp.Body, h)
+ if _, err := io.Copy(fd, tr); err != nil {
+ return errors.Annotate(err).Reason("failed to download").Err()
+ }
+
+ if err = hashesEqual(h, hash); err != nil {
+ return
+ }
+ return nil
+}
+
+func unzip(src, dst string) error {
+ fd, err := zip.OpenReader(src)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to open ZIP reader").Err()
+ }
+ defer fd.Close()
+
+ for _, f := range fd.File {
+ path := filepath.Join(dst, filepath.FromSlash(f.Name))
+ fi := f.FileInfo()
+
+ // Unzip this entry.
+ if fi.IsDir() {
+ if err := os.MkdirAll(path, 0755); err != nil {
+ return errors.Annotate(err).Reason("failed to mkdir").Err()
+ }
+ } else {
+ if err := copyFileOpener(f.Open, path, fi); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func copyFileIntoDir(src, dstDir string) error {
+ return copyFile(src, filepath.Join(dstDir, filepath.Base(src)), nil)
+}
+
+func copyFile(src, dst string, fi os.FileInfo) error {
+ opener := func() (io.ReadCloser, error) { return os.Open(src) }
+ return copyFileOpener(opener, dst, fi)
+}
+
+func copyFileOpener(opener func() (io.ReadCloser, error), dst string, fi os.FileInfo) (err error) {
+ sfd, err := opener()
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to open source").Err()
+ }
+ defer sfd.Close()
+
+ dfd, err := os.Create(dst)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to create destination").Err()
+ }
+ defer func() {
+ if closeErr := dfd.Close(); closeErr != nil && err == nil {
+ err = errors.Annotate(closeErr).Reason("failed to close destination").Err()
+ }
+ }()
+
+ if _, err := io.Copy(dfd, sfd); err != nil {
+ return errors.Annotate(err).Reason("failed to copy file").Err()
+ }
+ if fi != nil {
+ if err := os.Chmod(dst, fi.Mode()); err != nil {
+ return errors.Annotate(err).Reason("failed to chmod").Err()
+ }
+ }
+ return nil
+}
« no previous file with comments | « 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