gitee.com/mysnapcore/mysnapd@v0.1.0/store/download_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package store_test 21 22 import ( 23 "bytes" 24 "context" 25 "crypto" 26 "errors" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "net/http" 31 "net/http/httptest" 32 "os" 33 "os/exec" 34 "path/filepath" 35 "time" 36 37 "github.com/juju/ratelimit" 38 . "gopkg.in/check.v1" 39 "gopkg.in/retry.v1" 40 41 "gitee.com/mysnapcore/mysnapd/dirs" 42 "gitee.com/mysnapcore/mysnapd/overlord/auth" 43 "gitee.com/mysnapcore/mysnapd/progress" 44 "gitee.com/mysnapcore/mysnapd/release" 45 "gitee.com/mysnapcore/mysnapd/snap" 46 "gitee.com/mysnapcore/mysnapd/store" 47 "gitee.com/mysnapcore/mysnapd/testutil" 48 ) 49 50 type downloadSuite struct { 51 mockXdelta *testutil.MockCmd 52 53 testutil.BaseTest 54 } 55 56 var _ = Suite(&downloadSuite{}) 57 58 func (s *downloadSuite) SetUpTest(c *C) { 59 s.BaseTest.SetUpTest(c) 60 61 store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.Exponential{ 62 Initial: time.Millisecond, 63 Factor: 2.5, 64 })) 65 66 s.mockXdelta = testutil.MockCommand(c, "xdelta3", "") 67 s.AddCleanup(s.mockXdelta.Restore) 68 } 69 70 func (s *downloadSuite) TestActualDownload(c *C) { 71 n := 0 72 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 c.Check(r.Header.Get("Snap-CDN"), Equals, "") 74 c.Check(r.Header.Get("Snap-Device-Location"), Equals, "") 75 c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "") 76 n++ 77 io.WriteString(w, "response-data") 78 })) 79 c.Assert(mockServer, NotNil) 80 defer mockServer.Close() 81 82 theStore := store.New(&store.Config{}, nil) 83 var buf SillyBuffer 84 // keep tests happy 85 sha3 := "" 86 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 87 c.Assert(err, IsNil) 88 c.Check(buf.String(), Equals, "response-data") 89 c.Check(n, Equals, 1) 90 } 91 92 func (s *downloadSuite) TestActualDownloadAutoRefresh(c *C) { 93 n := 0 94 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 95 c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled") 96 n++ 97 io.WriteString(w, "response-data") 98 })) 99 c.Assert(mockServer, NotNil) 100 defer mockServer.Close() 101 102 theStore := store.New(&store.Config{}, nil) 103 var buf SillyBuffer 104 // keep tests happy 105 sha3 := "" 106 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{IsAutoRefresh: true}) 107 c.Assert(err, IsNil) 108 c.Check(buf.String(), Equals, "response-data") 109 c.Check(n, Equals, 1) 110 } 111 112 func (s *downloadSuite) TestActualDownloadNoCDN(c *C) { 113 os.Setenv("SNAPPY_STORE_NO_CDN", "1") 114 defer os.Unsetenv("SNAPPY_STORE_NO_CDN") 115 116 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 c.Check(r.Header.Get("Snap-CDN"), Equals, "none") 118 c.Check(r.Header.Get("Snap-Device-Location"), Equals, "") 119 io.WriteString(w, "response-data") 120 })) 121 c.Assert(mockServer, NotNil) 122 defer mockServer.Close() 123 124 theStore := store.New(&store.Config{}, nil) 125 var buf SillyBuffer 126 // keep tests happy 127 sha3 := "" 128 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 129 c.Assert(err, IsNil) 130 c.Check(buf.String(), Equals, "response-data") 131 } 132 133 func (s *downloadSuite) TestActualDownloadFullCloudInfoFromAuthContext(c *C) { 134 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="aws" region="us-east-1" availability-zone="us-east-1c"`) 136 c.Check(r.Header.Get("Snap-Device-Location"), Equals, `cloud-name="aws" region="us-east-1" availability-zone="us-east-1c"`) 137 138 io.WriteString(w, "response-data") 139 })) 140 c.Assert(mockServer, NotNil) 141 defer mockServer.Close() 142 143 device := createTestDevice() 144 theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "aws", Region: "us-east-1", AvailabilityZone: "us-east-1c"}}) 145 146 var buf SillyBuffer 147 // keep tests happy 148 sha3 := "" 149 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 150 c.Assert(err, IsNil) 151 c.Check(buf.String(), Equals, "response-data") 152 } 153 154 func (s *downloadSuite) TestActualDownloadLessDetailedCloudInfoFromAuthContext(c *C) { 155 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 156 c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="openstack" availability-zone="nova"`) 157 c.Check(r.Header.Get("Snap-Device-Location"), Equals, `cloud-name="openstack" availability-zone="nova"`) 158 159 io.WriteString(w, "response-data") 160 })) 161 c.Assert(mockServer, NotNil) 162 defer mockServer.Close() 163 164 device := createTestDevice() 165 theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "openstack", Region: "", AvailabilityZone: "nova"}}) 166 167 var buf SillyBuffer 168 // keep tests happy 169 sha3 := "" 170 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 171 c.Assert(err, IsNil) 172 c.Check(buf.String(), Equals, "response-data") 173 } 174 175 func (s *downloadSuite) TestDownloadCancellation(c *C) { 176 // the channel used by mock server to request cancellation from the test 177 syncCh := make(chan struct{}) 178 179 n := 0 180 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 181 n++ 182 io.WriteString(w, "foo") 183 syncCh <- struct{}{} 184 io.WriteString(w, "bar") 185 time.Sleep(10 * time.Millisecond) 186 })) 187 c.Assert(mockServer, NotNil) 188 defer mockServer.Close() 189 190 theStore := store.New(&store.Config{}, nil) 191 192 ctx, cancel := context.WithCancel(context.Background()) 193 194 result := make(chan string) 195 go func() { 196 sha3 := "" 197 var buf SillyBuffer 198 err := store.Download(ctx, "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 199 result <- err.Error() 200 close(result) 201 }() 202 203 <-syncCh 204 cancel() 205 206 err := <-result 207 c.Check(n, Equals, 1) 208 c.Assert(err, Equals, "the download has been cancelled: context canceled") 209 } 210 211 type nopeSeeker struct{ io.ReadWriter } 212 213 func (nopeSeeker) Seek(int64, int) (int64, error) { 214 return -1, errors.New("what is this, quidditch?") 215 } 216 217 func (s *downloadSuite) TestActualDownloadNonPurchased402(c *C) { 218 n := 0 219 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 220 n++ 221 // XXX: the server doesn't behave correctly ATM 222 // but 401 for paid snaps is the unlikely case so far 223 w.WriteHeader(402) 224 })) 225 c.Assert(mockServer, NotNil) 226 defer mockServer.Close() 227 228 theStore := store.New(&store.Config{}, nil) 229 var buf bytes.Buffer 230 err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, nopeSeeker{&buf}, -1, nil, nil) 231 c.Assert(err, NotNil) 232 c.Check(err.Error(), Equals, "please buy foo before installing it") 233 c.Check(n, Equals, 1) 234 } 235 236 func (s *downloadSuite) TestActualDownload404(c *C) { 237 n := 0 238 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 239 n++ 240 w.WriteHeader(404) 241 })) 242 c.Assert(mockServer, NotNil) 243 defer mockServer.Close() 244 245 theStore := store.New(&store.Config{}, nil) 246 var buf SillyBuffer 247 err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil) 248 c.Assert(err, NotNil) 249 c.Assert(err, FitsTypeOf, &store.DownloadError{}) 250 c.Check(err.(*store.DownloadError).Code, Equals, 404) 251 c.Check(n, Equals, 1) 252 } 253 254 func (s *downloadSuite) TestActualDownload500(c *C) { 255 n := 0 256 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 257 n++ 258 w.WriteHeader(500) 259 })) 260 c.Assert(mockServer, NotNil) 261 defer mockServer.Close() 262 263 theStore := store.New(&store.Config{}, nil) 264 var buf SillyBuffer 265 err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil) 266 c.Assert(err, NotNil) 267 c.Assert(err, FitsTypeOf, &store.DownloadError{}) 268 c.Check(err.(*store.DownloadError).Code, Equals, 500) 269 c.Check(n, Equals, 5) 270 } 271 272 func (s *downloadSuite) TestActualDownload500Once(c *C) { 273 n := 0 274 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 275 n++ 276 if n == 1 { 277 w.WriteHeader(500) 278 } else { 279 io.WriteString(w, "response-data") 280 } 281 })) 282 c.Assert(mockServer, NotNil) 283 defer mockServer.Close() 284 285 theStore := store.New(&store.Config{}, nil) 286 var buf SillyBuffer 287 // keep tests happy 288 sha3 := "" 289 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil) 290 c.Assert(err, IsNil) 291 c.Check(buf.String(), Equals, "response-data") 292 c.Check(n, Equals, 2) 293 } 294 295 // SillyBuffer is a ReadWriteSeeker buffer with a limited size for the tests 296 // (bytes does not implement an ReadWriteSeeker) 297 type SillyBuffer struct { 298 buf [1024]byte 299 pos int64 300 end int64 301 } 302 303 func NewSillyBufferString(s string) *SillyBuffer { 304 sb := &SillyBuffer{ 305 pos: int64(len(s)), 306 end: int64(len(s)), 307 } 308 copy(sb.buf[0:], []byte(s)) 309 return sb 310 } 311 func (sb *SillyBuffer) Read(b []byte) (n int, err error) { 312 if sb.pos >= int64(sb.end) { 313 return 0, io.EOF 314 } 315 n = copy(b, sb.buf[sb.pos:sb.end]) 316 sb.pos += int64(n) 317 return n, nil 318 } 319 func (sb *SillyBuffer) Seek(offset int64, whence int) (int64, error) { 320 if whence != 0 { 321 panic("only io.SeekStart implemented in SillyBuffer") 322 } 323 if offset < 0 || offset > int64(sb.end) { 324 return 0, fmt.Errorf("seek out of bounds: %d", offset) 325 } 326 sb.pos = offset 327 return sb.pos, nil 328 } 329 func (sb *SillyBuffer) Write(p []byte) (n int, err error) { 330 n = copy(sb.buf[sb.pos:], p) 331 sb.pos += int64(n) 332 if sb.pos > sb.end { 333 sb.end = sb.pos 334 } 335 return n, nil 336 } 337 func (sb *SillyBuffer) String() string { 338 return string(sb.buf[0:sb.pos]) 339 } 340 341 func (s *downloadSuite) TestActualDownloadResume(c *C) { 342 n := 0 343 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 344 n++ 345 w.WriteHeader(206) 346 io.WriteString(w, "data") 347 })) 348 c.Assert(mockServer, NotNil) 349 defer mockServer.Close() 350 351 theStore := store.New(&store.Config{}, nil) 352 buf := NewSillyBufferString("some ") 353 // calc the expected hash 354 h := crypto.SHA3_384.New() 355 h.Write([]byte("some data")) 356 sha3 := fmt.Sprintf("%x", h.Sum(nil)) 357 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil) 358 c.Check(err, IsNil) 359 c.Check(buf.String(), Equals, "some data") 360 c.Check(n, Equals, 1) 361 } 362 363 func (s *downloadSuite) TestActualDownloadServerNoResumeHandeled(c *C) { 364 n := 0 365 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 366 n++ 367 368 switch n { 369 case 1: 370 c.Check(r.Header["Range"], HasLen, 1) 371 default: 372 c.Fatal("only one request expected") 373 } 374 // server does not do partial content and sends full data instead 375 w.WriteHeader(200) 376 io.WriteString(w, "some data") 377 })) 378 c.Assert(mockServer, NotNil) 379 defer mockServer.Close() 380 381 theStore := store.New(&store.Config{}, nil) 382 buf := NewSillyBufferString("some ") 383 // calc the expected hash 384 h := crypto.SHA3_384.New() 385 h.Write([]byte("some data")) 386 sha3 := fmt.Sprintf("%x", h.Sum(nil)) 387 err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil) 388 c.Check(err, IsNil) 389 c.Check(buf.String(), Equals, "some data") 390 c.Check(n, Equals, 1) 391 } 392 393 func (s *downloadSuite) TestUseDeltas(c *C) { 394 // get rid of the mock xdelta3 because we mock all our own stuff 395 s.mockXdelta.Restore() 396 origPath := os.Getenv("PATH") 397 defer os.Setenv("PATH", origPath) 398 origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") 399 defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) 400 restore := release.MockOnClassic(false) 401 defer restore() 402 403 origSnapMountDir := dirs.SnapMountDir 404 defer func() { dirs.SnapMountDir = origSnapMountDir }() 405 dirs.SnapMountDir = c.MkDir() 406 exeInCorePath := filepath.Join(dirs.SnapMountDir, "/core/current/usr/bin/xdelta3") 407 interpInCorePath := filepath.Join(dirs.SnapMountDir, "/core/current/lib64/ld-linux-x86-64.so.2") 408 409 scenarios := []struct { 410 env string 411 classic bool 412 exeInHost bool 413 exeInCore bool 414 415 wantDelta bool 416 }{ 417 {env: "", classic: false, exeInHost: false, exeInCore: false, wantDelta: false}, 418 {env: "", classic: false, exeInHost: false, exeInCore: true, wantDelta: true}, 419 {env: "", classic: false, exeInHost: true, exeInCore: false, wantDelta: true}, 420 {env: "", classic: false, exeInHost: true, exeInCore: true, wantDelta: true}, 421 {env: "", classic: true, exeInHost: false, exeInCore: false, wantDelta: false}, 422 {env: "", classic: true, exeInHost: false, exeInCore: true, wantDelta: true}, 423 {env: "", classic: true, exeInHost: true, exeInCore: false, wantDelta: true}, 424 {env: "", classic: true, exeInHost: true, exeInCore: true, wantDelta: true}, 425 426 {env: "0", classic: false, exeInHost: false, exeInCore: false, wantDelta: false}, 427 {env: "0", classic: false, exeInHost: false, exeInCore: true, wantDelta: false}, 428 {env: "0", classic: false, exeInHost: true, exeInCore: false, wantDelta: false}, 429 {env: "0", classic: false, exeInHost: true, exeInCore: true, wantDelta: false}, 430 {env: "0", classic: true, exeInHost: false, exeInCore: false, wantDelta: false}, 431 {env: "0", classic: true, exeInHost: false, exeInCore: true, wantDelta: false}, 432 {env: "0", classic: true, exeInHost: true, exeInCore: false, wantDelta: false}, 433 {env: "0", classic: true, exeInHost: true, exeInCore: true, wantDelta: false}, 434 435 {env: "1", classic: false, exeInHost: false, exeInCore: false, wantDelta: false}, 436 {env: "1", classic: false, exeInHost: false, exeInCore: true, wantDelta: true}, 437 {env: "1", classic: false, exeInHost: true, exeInCore: false, wantDelta: true}, 438 {env: "1", classic: false, exeInHost: true, exeInCore: true, wantDelta: true}, 439 {env: "1", classic: true, exeInHost: false, exeInCore: false, wantDelta: false}, 440 {env: "1", classic: true, exeInHost: false, exeInCore: true, wantDelta: true}, 441 {env: "1", classic: true, exeInHost: true, exeInCore: false, wantDelta: true}, 442 {env: "1", classic: true, exeInHost: true, exeInCore: true, wantDelta: true}, 443 } 444 445 for _, scenario := range scenarios { 446 var hostXdelta3Cmd, coreInterpCmd *testutil.MockCmd 447 448 var cleanups []func() 449 450 comment := Commentf("%#v", scenario) 451 452 // setup the env var for the scenario 453 os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", scenario.env) 454 release.MockOnClassic(scenario.classic) 455 456 // setup binaries for the scenario 457 if scenario.exeInCore { 458 // We need both the xdelta3 command for determining the interpreter 459 // as well as the actual interpreter for executing the basic 460 // "xdelta3 config" command. 461 // For the interpreter, since that's how we execute xdelta3, mock 462 // that as a command, but we don't need to mock the xdelta3 command 463 // in the core snap since that doesn't get executed by our fake 464 // interpreter. Mocking the interpreter and executing that as a 465 // MockCommand has the advantage that it avoids the specific ELF 466 // handling that is per-arch, etc. of the real CommandFromSystemSnap 467 // implementation. 468 469 coreInterpCmd = testutil.MockCommand(c, interpInCorePath, "") 470 471 r := store.MockSnapdtoolCommandFromSystemSnap(func(name string, args ...string) (*exec.Cmd, error) { 472 c.Assert(name, Equals, "/usr/bin/xdelta3") 473 c.Assert(args, DeepEquals, []string{"config"}) 474 475 // use realistic arguments like what we actually get from 476 // snapdtool.CommandFromSystemSnap(), namely the interpreter and 477 // a library path which is derived from ld.so - this is 478 // artificial and we could use any mocked arguments here, but 479 // this more closely matches reality to return something like 480 // this. 481 interpArgs := append([]string{"--library-path", "/some/dir/from/etc/ld.so", exeInCorePath}, args...) 482 return exec.Command(coreInterpCmd.Exe(), interpArgs...), nil 483 }) 484 cleanups = append(cleanups, r) 485 486 // Forget the calls to the interpreter at the end of the test - this 487 // deletes the log which otherwise would continue to persist for 488 // each iteration leading to incorrect checks for the calls to the 489 // absolute binary that we mocked here, as the log file will be the 490 // same for each iteration. 491 // For the inverse reason, we don't need to forget calls for the 492 // hostXdelta3Cmd mock command, it gets a new dir with a new log 493 // file each iteration. 494 cleanups = append(cleanups, func() { 495 coreInterpCmd.ForgetCalls() 496 // note this is currently not needed, since Restore() just 497 // resets $PATH, but for an absolute path the $PATH doesn't get 498 // modified to begin with in MockCommand, but keep it here just 499 // to be safe in case something does ever change 500 coreInterpCmd.Restore() 501 502 }) 503 } 504 505 if scenario.exeInHost { 506 // just mock the xdelta3 command directly 507 hostXdelta3Cmd = testutil.MockCommand(c, "xdelta3", "") 508 509 // note we don't add a Restore() to cleanups, it is called directly 510 // below after the first UseDeltas() but before the second 511 // UseDeltas() in order to properly test the caching behavior 512 } 513 514 // if there is not meant to be xdelta3 on the host or in core, then set 515 // PATH to be empty such that we won't find xdelta3 from the host 516 // running these tests 517 if !scenario.exeInHost && !scenario.exeInCore { 518 os.Setenv("PATH", "") 519 520 // also reset PATH at the end, otherwise an empty PATH leads 521 // testutil.MockCommand fails in future iterations that mock a 522 // command 523 cleanups = append(cleanups, func() { 524 os.Setenv("PATH", origPath) 525 }) 526 } 527 528 // run the check for delta usage, we call it twice 529 sto := &store.Store{} 530 c.Check(sto.UseDeltas(), Equals, scenario.wantDelta, comment) 531 532 // cleanup the files we may have created before calling the function 533 // again to ensure that the caching works as expected 534 if scenario.exeInCore { 535 err := os.Remove(interpInCorePath) 536 c.Assert(err, IsNil) 537 } 538 539 if scenario.exeInHost { 540 hostXdelta3Cmd.Restore() 541 } 542 543 // also now that we have deleted the mock interpreter and unset the 544 // search path, we should still get the same result as above when 545 // we call UseDeltas() since it was cached, if it wasn't cached then 546 // this would fail 547 c.Check(sto.UseDeltas(), Equals, scenario.wantDelta, comment) 548 549 if scenario.wantDelta { 550 // if we should have been able to use deltas, make sure we picked 551 // the expected one, - if both were true we should have picked the 552 // one from core instead of the one from the host first 553 if scenario.exeInCore { 554 // check that during trying to check whether to use deltas or 555 // not, we called the interpreter with the xdelta3 config 556 // command too 557 c.Check(coreInterpCmd.Calls(), DeepEquals, [][]string{ 558 {"ld-linux-x86-64.so.2", "--library-path", "/some/dir/from/etc/ld.so", exeInCorePath, "config"}, 559 }, comment) 560 561 // also check that now after caching the xdelta3 command, it 562 // returns the expected format 563 expArgs := []string{ 564 interpInCorePath, 565 "--library-path", 566 "/some/dir/from/etc/ld.so", 567 exeInCorePath, 568 "foo", 569 "bar", 570 } 571 // check that the Xdelta3Cmd function we cached uses the 572 // interpreter that was returned from CommandFromSystemSnap 573 c.Check(sto.Xdelta3Cmd("foo", "bar").Args, DeepEquals, expArgs, comment) 574 575 } else if scenario.exeInHost { 576 // similar checks for the host case, except in the host case we 577 // just called xdelta3 directly 578 c.Check(hostXdelta3Cmd.Calls(), DeepEquals, [][]string{ 579 {"xdelta3", "config"}, 580 }, comment) 581 582 // and args are passed to the command cached too 583 expArgs := []string{hostXdelta3Cmd.Exe(), "foo", "bar"} 584 c.Check(sto.Xdelta3Cmd("foo", "bar").Args, DeepEquals, expArgs, comment) 585 } 586 } else { 587 // quick check that the test case makes sense, if we didn't want 588 // deltas, the scenario should have either disabled via an env var, 589 // or had both exes missing 590 c.Assert((scenario.env == "0") || 591 (!scenario.exeInCore && !scenario.exeInHost), 592 Equals, true) 593 } 594 595 // cleanup for the next iteration 596 for _, r := range cleanups { 597 r() 598 } 599 } 600 } 601 602 type downloadBehaviour []struct { 603 url string 604 error bool 605 } 606 607 var deltaTests = []struct { 608 downloads downloadBehaviour 609 info snap.DownloadInfo 610 expectedContent string 611 }{{ 612 // The full snap is not downloaded, but rather the delta 613 // is downloaded and applied. 614 downloads: downloadBehaviour{ 615 {url: "delta-url"}, 616 }, 617 info: snap.DownloadInfo{ 618 DownloadURL: "full-snap-url", 619 Deltas: []snap.DeltaInfo{ 620 {DownloadURL: "delta-url", Format: "xdelta3"}, 621 }, 622 }, 623 expectedContent: "snap-content-via-delta", 624 }, { 625 // If there is an error during the delta download, the 626 // full snap is downloaded as per normal. 627 downloads: downloadBehaviour{ 628 {error: true}, 629 {url: "full-snap-url"}, 630 }, 631 info: snap.DownloadInfo{ 632 DownloadURL: "full-snap-url", 633 Deltas: []snap.DeltaInfo{ 634 {DownloadURL: "delta-url", Format: "xdelta3"}, 635 }, 636 }, 637 expectedContent: "full-snap-url-content", 638 }, { 639 // If more than one matching delta is returned by the store 640 // we ignore deltas and do the full download. 641 downloads: downloadBehaviour{ 642 {url: "full-snap-url"}, 643 }, 644 info: snap.DownloadInfo{ 645 DownloadURL: "full-snap-url", 646 Deltas: []snap.DeltaInfo{ 647 {DownloadURL: "delta-url", Format: "xdelta3"}, 648 {DownloadURL: "delta-url-2", Format: "xdelta3"}, 649 }, 650 }, 651 expectedContent: "full-snap-url-content", 652 }} 653 654 func (s *downloadSuite) TestDownloadWithDelta(c *C) { 655 origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") 656 defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) 657 c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil) 658 659 for _, testCase := range deltaTests { 660 testCase.info.Size = int64(len(testCase.expectedContent)) 661 downloadIndex := 0 662 restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error { 663 if testCase.downloads[downloadIndex].error { 664 downloadIndex++ 665 return errors.New("Bang") 666 } 667 c.Check(url, Equals, testCase.downloads[downloadIndex].url) 668 w.Write([]byte(testCase.downloads[downloadIndex].url + "-content")) 669 downloadIndex++ 670 return nil 671 }) 672 defer restore() 673 restore = store.MockApplyDelta(func(_ *store.Store, name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error { 674 c.Check(deltaInfo, Equals, &testCase.info.Deltas[0]) 675 err := ioutil.WriteFile(targetPath, []byte("snap-content-via-delta"), 0644) 676 c.Assert(err, IsNil) 677 return nil 678 }) 679 defer restore() 680 681 theStore := store.New(&store.Config{}, nil) 682 path := filepath.Join(c.MkDir(), "subdir", "downloaded-file") 683 err := theStore.Download(context.TODO(), "foo", path, &testCase.info, nil, nil, nil) 684 685 c.Assert(err, IsNil) 686 defer os.Remove(path) 687 c.Assert(path, testutil.FileEquals, testCase.expectedContent) 688 } 689 } 690 691 func (s *downloadSuite) TestActualDownloadRateLimited(c *C) { 692 var ratelimitReaderUsed bool 693 restore := store.MockRatelimitReader(func(r io.Reader, bucket *ratelimit.Bucket) io.Reader { 694 ratelimitReaderUsed = true 695 return r 696 }) 697 defer restore() 698 699 canary := "downloaded data" 700 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 701 fmt.Fprint(w, canary) 702 })) 703 defer ts.Close() 704 705 theStore := store.New(&store.Config{}, nil) 706 var buf SillyBuffer 707 err := store.Download(context.TODO(), "example-name", "", ts.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{RateLimit: 1}) 708 c.Assert(err, IsNil) 709 c.Check(buf.String(), Equals, canary) 710 c.Check(ratelimitReaderUsed, Equals, true) 711 }