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