| OLD | NEW |
| (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 "encoding/json" |
| 10 "fmt" |
| 11 "io" |
| 12 "io/ioutil" |
| 13 "os" |
| 14 "path/filepath" |
| 15 "strings" |
| 16 "testing" |
| 17 |
| 18 "github.com/luci/luci-go/vpython/api/env" |
| 19 "github.com/luci/luci-go/vpython/filesystem" |
| 20 "github.com/luci/luci-go/vpython/filesystem/testfs" |
| 21 "github.com/luci/luci-go/vpython/python" |
| 22 |
| 23 "github.com/luci/luci-go/common/errors" |
| 24 |
| 25 "golang.org/x/net/context" |
| 26 |
| 27 . "github.com/luci/luci-go/common/testing/assertions" |
| 28 . "github.com/smartystreets/goconvey/convey" |
| 29 ) |
| 30 |
| 31 const testDataDir = "test_data" |
| 32 |
| 33 type resolvedInterpreter struct { |
| 34 i *python.Interpreter |
| 35 version python.Version |
| 36 } |
| 37 |
| 38 func resolveFromPath(vers python.Version) *resolvedInterpreter { |
| 39 c := context.Background() |
| 40 i, err := python.Find(c, vers) |
| 41 if err != nil { |
| 42 return nil |
| 43 } |
| 44 if err := filesystem.AbsPath(&i.Python); err != nil { |
| 45 panic(err) |
| 46 } |
| 47 |
| 48 ri := resolvedInterpreter{ |
| 49 i: i, |
| 50 } |
| 51 if ri.version, err = ri.i.GetVersion(c); err != nil { |
| 52 panic(err) |
| 53 } |
| 54 return &ri |
| 55 } |
| 56 |
| 57 var ( |
| 58 pythonGeneric = resolveFromPath(python.Version{}) |
| 59 python27 = resolveFromPath(python.Version{2, 7, 0}) |
| 60 python3 = resolveFromPath(python.Version{3, 0, 0}) |
| 61 ) |
| 62 |
| 63 func TestResolvePythonInterpreter(t *testing.T) { |
| 64 t.Parallel() |
| 65 |
| 66 Convey(`Resolving a Python interpreter`, t, func() { |
| 67 c := context.Background() |
| 68 cfg := Config{ |
| 69 Spec: &env.Spec{}, |
| 70 } |
| 71 |
| 72 // Tests to run if we have Python 2.7 installed. |
| 73 if python27 != nil { |
| 74 Convey(`When Python 2.7 is requested, it gets resolved.`
, func() { |
| 75 cfg.Spec.PythonVersion = "2.7" |
| 76 So(cfg.resolvePythonInterpreter(c), ShouldBeNil) |
| 77 So(cfg.Python, ShouldEqual, python27.i.Python) |
| 78 |
| 79 vers, err := python.ParseVersion(cfg.Spec.Python
Version) |
| 80 So(err, ShouldBeNil) |
| 81 So(vers.IsSatisfiedBy(python27.version), ShouldB
eTrue) |
| 82 }) |
| 83 |
| 84 Convey(`Fails when Python 9999 is requested, but a Pytho
n 2 interpreter is forced.`, func() { |
| 85 cfg.Python = python27.i.Python |
| 86 cfg.Spec.PythonVersion = "9999" |
| 87 So(cfg.resolvePythonInterpreter(c), ShouldErrLik
e, "doesn't match specification") |
| 88 }) |
| 89 } |
| 90 |
| 91 // Tests to run if we have Python 2.7 and a generic Python insta
lled. |
| 92 if pythonGeneric != nil && python27 != nil { |
| 93 // Our generic Python resolves to a known version, so we
can proceed. |
| 94 Convey(`When no Python version is specified, spec resolv
es to generic.`, func() { |
| 95 So(cfg.resolvePythonInterpreter(c), ShouldBeNil) |
| 96 So(cfg.Python, ShouldEqual, pythonGeneric.i.Pyth
on) |
| 97 |
| 98 vers, err := python.ParseVersion(cfg.Spec.Python
Version) |
| 99 So(err, ShouldBeNil) |
| 100 So(vers.IsSatisfiedBy(pythonGeneric.version), Sh
ouldBeTrue) |
| 101 }) |
| 102 } |
| 103 |
| 104 // Tests to run if we have Python 3 installed. |
| 105 if python3 != nil { |
| 106 Convey(`When Python 3 is requested, it gets resolved.`,
func() { |
| 107 cfg.Spec.PythonVersion = "3" |
| 108 So(cfg.resolvePythonInterpreter(c), ShouldBeNil) |
| 109 So(cfg.Python, ShouldEqual, python3.i.Python) |
| 110 |
| 111 vers, err := python.ParseVersion(cfg.Spec.Python
Version) |
| 112 So(err, ShouldBeNil) |
| 113 So(vers.IsSatisfiedBy(python3.version), ShouldBe
True) |
| 114 }) |
| 115 |
| 116 Convey(`Fails when Python 9999 is requested, but a Pytho
n 3 interpreter is forced.`, func() { |
| 117 cfg.Python = python3.i.Python |
| 118 cfg.Spec.PythonVersion = "9999" |
| 119 So(cfg.resolvePythonInterpreter(c), ShouldErrLik
e, "doesn't match specification") |
| 120 }) |
| 121 } |
| 122 }) |
| 123 } |
| 124 |
| 125 // testingPackageLoader is a map of a CIPD package name to the root directory |
| 126 // that it should be loaded from. |
| 127 type testingPackageLoader map[string]string |
| 128 |
| 129 func (pl testingPackageLoader) Resolve(c context.Context, root string, packages
[]*env.Spec_Package) error { |
| 130 for _, pkg := range packages { |
| 131 pkg.Version = "resolved" |
| 132 } |
| 133 return nil |
| 134 } |
| 135 |
| 136 func (pl testingPackageLoader) Ensure(c context.Context, root string, packages [
]*env.Spec_Package) error { |
| 137 for _, pkg := range packages { |
| 138 if err := pl.installPackage(pkg.Path, root); err != nil { |
| 139 return err |
| 140 } |
| 141 } |
| 142 return nil |
| 143 } |
| 144 |
| 145 func (pl testingPackageLoader) installPackage(name, root string) error { |
| 146 testName := pl[name] |
| 147 if testName == "" { |
| 148 return errors.Reason("could not resolve package for %(name)q"). |
| 149 D("name", name). |
| 150 Err() |
| 151 } |
| 152 sourcePath := filepath.Join(testDataDir, testName) |
| 153 |
| 154 switch st, err := os.Stat(sourcePath); { |
| 155 case err != nil: |
| 156 return errors.Annotate(err).Reason("could not stat source: %(sou
rce)s"). |
| 157 D("source", sourcePath). |
| 158 Err() |
| 159 |
| 160 case st.IsDir(): |
| 161 if err := recursiveCopyDir(sourcePath, root); err != nil { |
| 162 return errors.Annotate(err).Reason("failed to recursivel
y copy").Err() |
| 163 } |
| 164 |
| 165 case strings.HasSuffix(sourcePath, ".zip"): |
| 166 // If it's a file, it's a ZIP file. Unpack it into destination. |
| 167 if err := unzip(sourcePath, root); err != nil { |
| 168 return errors.Annotate(err).Reason("failed to un-zip arc
hive").Err() |
| 169 } |
| 170 |
| 171 default: |
| 172 return errors.Reason("don't know how to handle: %(path)s"). |
| 173 D("path", sourcePath). |
| 174 Err() |
| 175 } |
| 176 return nil |
| 177 } |
| 178 |
| 179 func recursiveCopyDir(src, dst string) error { |
| 180 // Recursively copy from sourcePath to root. |
| 181 return filepath.Walk(src, func(path string, fi os.FileInfo, err error) e
rror { |
| 182 if err != nil || path == src { |
| 183 return err |
| 184 } |
| 185 rel, err := filepath.Rel(src, path) |
| 186 if err != nil { |
| 187 return errors.Annotate(err).Reason("failed to get relati
ve path").Err() |
| 188 } |
| 189 |
| 190 dst := filepath.Join(dst, rel) |
| 191 |
| 192 opener := func() (io.ReadCloser, error) { return os.Open(path) } |
| 193 if err := copyFileOrDir(opener, dst, fi); err != nil { |
| 194 return errors.Annotate(err).Reason("failed to copy: [%(s
rc)s] => [%(dst)s]"). |
| 195 D("src", path). |
| 196 D("dst", dst). |
| 197 Err() |
| 198 } |
| 199 return nil |
| 200 }) |
| 201 } |
| 202 |
| 203 func unzip(src, dst string) error { |
| 204 fd, err := zip.OpenReader(src) |
| 205 if err != nil { |
| 206 return errors.Annotate(err).Reason("failed to open ZIP reader").
Err() |
| 207 } |
| 208 defer fd.Close() |
| 209 |
| 210 for _, f := range fd.File { |
| 211 if err := copyFileOrDir(f.Open, filepath.Join(dst, f.Name), f.Fi
leInfo()); err != nil { |
| 212 return errors.Annotate(err).Reason("failed to extract fi
le: %(name)s"). |
| 213 D("name", f.Name). |
| 214 Err() |
| 215 } |
| 216 } |
| 217 return nil |
| 218 } |
| 219 |
| 220 // copyFile copies a source file and its mode to a destination. |
| 221 func copyFileOrDir(opener func() (io.ReadCloser, error), dst string, fi os.FileI
nfo) error { |
| 222 if fi.IsDir() { |
| 223 if err := os.MkdirAll(dst, 0755); err != nil { |
| 224 return errors.Annotate(err).Reason("failed to mkdir").Er
r() |
| 225 } |
| 226 return nil |
| 227 } |
| 228 |
| 229 srcFD, err := opener() |
| 230 if err != nil { |
| 231 return errors.Annotate(err).Reason("failed to create source").Er
r() |
| 232 } |
| 233 defer srcFD.Close() |
| 234 |
| 235 dstFD, err := os.Create(dst) |
| 236 if err != nil { |
| 237 return errors.Annotate(err).Reason("failed to create dest").Err(
) |
| 238 } |
| 239 defer dstFD.Close() |
| 240 |
| 241 if _, err := io.Copy(dstFD, srcFD); err != nil { |
| 242 return errors.Annotate(err).Reason("failed to copy").Err() |
| 243 } |
| 244 if err := os.Chmod(dst, fi.Mode()); err != nil { |
| 245 return errors.Annotate(err).Reason("failed to chmod").Err() |
| 246 } |
| 247 return nil |
| 248 } |
| 249 |
| 250 type setupCheckManifest struct { |
| 251 Interpreter string `json:"interpreter"` |
| 252 Pants string `json:"pants"` |
| 253 Shirt string `json:"shirt"` |
| 254 } |
| 255 |
| 256 func TestVirtualEnv(t *testing.T) { |
| 257 t.Parallel() |
| 258 |
| 259 for _, tc := range []struct { |
| 260 name string |
| 261 ri *resolvedInterpreter |
| 262 }{ |
| 263 {"python27", python27}, |
| 264 {"python3", python3}, |
| 265 } { |
| 266 tc := tc |
| 267 t.Run(fmt.Sprintf(`Testing Virtualenv for: %s`, tc.name), func(t
*testing.T) { |
| 268 t.Parallel() |
| 269 |
| 270 conveyOp := Convey |
| 271 if tc.ri == nil { |
| 272 // No interpreter found, skip this test. |
| 273 conveyOp = SkipConvey |
| 274 } |
| 275 conveyOp(`Testing Setup`, t, testfs.MustWithTempDir(t, "
TestVirtualEnv", func(tdir string) { |
| 276 c := context.Background() |
| 277 config := Config{ |
| 278 BaseDir: tdir, |
| 279 MaxHashLen: 4, |
| 280 Package: env.Spec_Package{ |
| 281 Path: "foo/bar/virtualenv", |
| 282 Version: "unresolved", |
| 283 }, |
| 284 Python: tc.ri.i.Python, |
| 285 Spec: &env.Spec{ |
| 286 Wheel: []*env.Spec_Package{ |
| 287 {Path: "foo/bar/shirt",
Version: "unresolved"}, |
| 288 {Path: "foo/bar/pants",
Version: "unresolved"}, |
| 289 }, |
| 290 }, |
| 291 Loader: testingPackageLoader{ |
| 292 "foo/bar/virtualenv": "virtualen
v-15.1.0.zip", |
| 293 "foo/bar/shirt": "shirt", |
| 294 "foo/bar/pants": "pants", |
| 295 }, |
| 296 } |
| 297 v, err := config.Env(c) |
| 298 So(err, ShouldBeNil) |
| 299 |
| 300 // The setup should be successful. |
| 301 So(v.Setup(c, false), ShouldBeNil) |
| 302 |
| 303 testScriptPath := filepath.Join(testDataDir, "se
tup_check.py") |
| 304 checkOut := filepath.Join(tdir, "output.json") |
| 305 i := v.InterpreterCommand() |
| 306 So(i.Run(c, testScriptPath, "--json-output", che
ckOut), ShouldBeNil) |
| 307 |
| 308 var m setupCheckManifest |
| 309 So(loadJSON(checkOut, &m), ShouldBeNil) |
| 310 So(m.Interpreter, ShouldStartWith, v.Root) |
| 311 So(m.Pants, ShouldStartWith, v.Root) |
| 312 So(m.Shirt, ShouldStartWith, v.Root) |
| 313 |
| 314 // We should be able to delete it. |
| 315 So(v.Delete(c), ShouldBeNil) |
| 316 })) |
| 317 }) |
| 318 } |
| 319 } |
| 320 |
| 321 func loadJSON(path string, dst interface{}) error { |
| 322 content, err := ioutil.ReadFile(path) |
| 323 if err != nil { |
| 324 return errors.Annotate(err).Reason("failed to open file").Err() |
| 325 } |
| 326 if err := json.Unmarshal(content, dst); err != nil { |
| 327 return errors.Annotate(err).Reason("failed to unmarshal JSON").E
rr() |
| 328 } |
| 329 return nil |
| 330 } |
| OLD | NEW |