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  }