github.com/rigado/snapd@v2.42.5-go-mod+incompatible/overlord/snapstate/backend_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-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 snapstate_test
    21  
    22  import (
    23  	"context"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"path/filepath"
    28  	"sort"
    29  	"strings"
    30  	"sync"
    31  
    32  	"github.com/snapcore/snapd/asserts"
    33  	"github.com/snapcore/snapd/osutil"
    34  	"github.com/snapcore/snapd/overlord/auth"
    35  	"github.com/snapcore/snapd/overlord/snapstate"
    36  	"github.com/snapcore/snapd/overlord/snapstate/backend"
    37  	"github.com/snapcore/snapd/overlord/state"
    38  	"github.com/snapcore/snapd/progress"
    39  	"github.com/snapcore/snapd/snap"
    40  	"github.com/snapcore/snapd/store"
    41  	"github.com/snapcore/snapd/store/storetest"
    42  	"github.com/snapcore/snapd/timings"
    43  )
    44  
    45  type fakeOp struct {
    46  	op string
    47  
    48  	name    string
    49  	channel string
    50  	path    string
    51  	revno   snap.Revision
    52  	sinfo   snap.SideInfo
    53  	stype   snap.Type
    54  
    55  	curSnaps []store.CurrentSnap
    56  	action   store.SnapAction
    57  
    58  	old string
    59  
    60  	aliases   []*backend.Alias
    61  	rmAliases []*backend.Alias
    62  
    63  	userID int
    64  
    65  	otherInstances bool
    66  
    67  	services []string
    68  }
    69  
    70  type fakeOps []fakeOp
    71  
    72  func (ops fakeOps) Ops() []string {
    73  	opsOps := make([]string, len(ops))
    74  	for i, op := range ops {
    75  		opsOps[i] = op.op
    76  	}
    77  
    78  	return opsOps
    79  }
    80  
    81  func (ops fakeOps) Count(op string) int {
    82  	n := 0
    83  	for i := range ops {
    84  		if ops[i].op == op {
    85  			n++
    86  		}
    87  	}
    88  	return n
    89  }
    90  
    91  func (ops fakeOps) First(op string) *fakeOp {
    92  	for i := range ops {
    93  		if ops[i].op == op {
    94  			return &ops[i]
    95  		}
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  type fakeDownload struct {
   102  	name     string
   103  	macaroon string
   104  	target   string
   105  	opts     *store.DownloadOptions
   106  }
   107  
   108  type byName []store.CurrentSnap
   109  
   110  func (bna byName) Len() int      { return len(bna) }
   111  func (bna byName) Swap(i, j int) { bna[i], bna[j] = bna[j], bna[i] }
   112  func (bna byName) Less(i, j int) bool {
   113  	return bna[i].InstanceName < bna[j].InstanceName
   114  }
   115  
   116  type byAction []*store.SnapAction
   117  
   118  func (ba byAction) Len() int      { return len(ba) }
   119  func (ba byAction) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] }
   120  func (ba byAction) Less(i, j int) bool {
   121  	if ba[i].Action == ba[j].Action {
   122  		if ba[i].Action == "refresh" {
   123  			return ba[i].SnapID < ba[j].SnapID
   124  		} else {
   125  			return ba[i].InstanceName < ba[j].InstanceName
   126  		}
   127  	}
   128  	return ba[i].Action < ba[j].Action
   129  }
   130  
   131  type fakeStore struct {
   132  	storetest.Store
   133  
   134  	downloads           []fakeDownload
   135  	refreshRevnos       map[string]snap.Revision
   136  	fakeBackend         *fakeSnappyBackend
   137  	fakeCurrentProgress int
   138  	fakeTotalProgress   int
   139  	state               *state.State
   140  	seenPrivacyKeys     map[string]bool
   141  }
   142  
   143  func (f *fakeStore) pokeStateLock() {
   144  	// the store should be called without the state lock held. Try
   145  	// to acquire it.
   146  	f.state.Lock()
   147  	f.state.Unlock()
   148  }
   149  
   150  func (f *fakeStore) SnapInfo(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
   151  	f.pokeStateLock()
   152  
   153  	_, instanceKey := snap.SplitInstanceName(spec.Name)
   154  	if instanceKey != "" {
   155  		return nil, fmt.Errorf("internal error: unexpected instance name: %q", spec.Name)
   156  	}
   157  	sspec := snapSpec{
   158  		Name: spec.Name,
   159  	}
   160  	info, err := f.snap(sspec, user)
   161  
   162  	userID := 0
   163  	if user != nil {
   164  		userID = user.ID
   165  	}
   166  	f.fakeBackend.appendOp(&fakeOp{op: "storesvc-snap", name: spec.Name, revno: info.Revision, userID: userID})
   167  
   168  	return info, err
   169  }
   170  
   171  type snapSpec struct {
   172  	Name     string
   173  	Channel  string
   174  	Revision snap.Revision
   175  	Cohort   string
   176  }
   177  
   178  func (f *fakeStore) snap(spec snapSpec, user *auth.UserState) (*snap.Info, error) {
   179  	if spec.Revision.Unset() {
   180  		switch {
   181  		case spec.Cohort != "":
   182  			spec.Revision = snap.R(666)
   183  		case spec.Channel == "channel-for-7":
   184  			spec.Revision = snap.R(7)
   185  		default:
   186  			spec.Revision = snap.R(11)
   187  		}
   188  	}
   189  
   190  	confinement := snap.StrictConfinement
   191  
   192  	typ := snap.TypeApp
   193  	epoch := snap.E("1*")
   194  	switch spec.Name {
   195  	case "core", "core16", "ubuntu-core", "some-core":
   196  		typ = snap.TypeOS
   197  	case "some-base", "core18":
   198  		typ = snap.TypeBase
   199  	case "some-kernel":
   200  		typ = snap.TypeKernel
   201  	case "some-gadget", "brand-gadget":
   202  		typ = snap.TypeGadget
   203  	case "some-snapd":
   204  		typ = snap.TypeSnapd
   205  	case "snapd":
   206  		typ = snap.TypeSnapd
   207  	case "some-snap-now-classic":
   208  		confinement = "classic"
   209  	case "some-epoch-snap":
   210  		epoch = snap.E("42")
   211  	}
   212  
   213  	if spec.Name == "snap-unknown" {
   214  		return nil, store.ErrSnapNotFound
   215  	}
   216  
   217  	info := &snap.Info{
   218  		Architectures: []string{"all"},
   219  		SideInfo: snap.SideInfo{
   220  			RealName: spec.Name,
   221  			Channel:  spec.Channel,
   222  			SnapID:   spec.Name + "-id",
   223  			Revision: spec.Revision,
   224  		},
   225  		Version: spec.Name,
   226  		DownloadInfo: snap.DownloadInfo{
   227  			DownloadURL: "https://some-server.com/some/path.snap",
   228  		},
   229  		Confinement: confinement,
   230  		SnapType:    typ,
   231  		Epoch:       epoch,
   232  	}
   233  	switch spec.Channel {
   234  	case "channel-for-devmode":
   235  		info.Confinement = snap.DevModeConfinement
   236  	case "channel-for-classic":
   237  		info.Confinement = snap.ClassicConfinement
   238  	case "channel-for-paid":
   239  		info.Prices = map[string]float64{"USD": 0.77}
   240  		info.SideInfo.Paid = true
   241  	case "channel-for-private":
   242  		info.SideInfo.Private = true
   243  	case "channel-for-layout":
   244  		info.Layout = map[string]*snap.Layout{
   245  			"/usr": {
   246  				Snap:    info,
   247  				Path:    "/usr",
   248  				Symlink: "$SNAP/usr",
   249  			},
   250  		}
   251  	}
   252  
   253  	return info, nil
   254  }
   255  
   256  type refreshCand struct {
   257  	snapID           string
   258  	channel          string
   259  	revision         snap.Revision
   260  	block            []snap.Revision
   261  	ignoreValidation bool
   262  }
   263  
   264  func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
   265  	var name string
   266  
   267  	typ := snap.TypeApp
   268  	epoch := snap.E("1*")
   269  	switch cand.snapID {
   270  	case "":
   271  		panic("store refresh APIs expect snap-ids")
   272  	case "other-snap-id":
   273  		return nil, store.ErrNoUpdateAvailable
   274  	case "fakestore-please-error-on-refresh":
   275  		return nil, fmt.Errorf("failing as requested")
   276  	case "services-snap-id":
   277  		name = "services-snap"
   278  	case "some-snap-id":
   279  		name = "some-snap"
   280  	case "some-epoch-snap-id":
   281  		name = "some-epoch-snap"
   282  		epoch = snap.E("42")
   283  	case "some-snap-now-classic-id":
   284  		name = "some-snap-now-classic"
   285  	case "some-snap-was-classic-id":
   286  		name = "some-snap-was-classic"
   287  	case "core-snap-id":
   288  		name = "core"
   289  		typ = snap.TypeOS
   290  	case "core18-snap-id":
   291  		name = "core18"
   292  		typ = snap.TypeBase
   293  	case "snap-with-snapd-control-id":
   294  		name = "snap-with-snapd-control"
   295  	case "producer-id":
   296  		name = "producer"
   297  	case "consumer-id":
   298  		name = "consumer"
   299  	case "some-base-id":
   300  		name = "some-base"
   301  		typ = snap.TypeBase
   302  	case "snap-content-plug-id":
   303  		name = "snap-content-plug"
   304  	case "snap-content-slot-id":
   305  		name = "snap-content-slot"
   306  	case "snapd-id":
   307  		name = "snapd"
   308  	case "kernel-id":
   309  		name = "kernel"
   310  		typ = snap.TypeKernel
   311  	case "brand-gadget-id":
   312  		name = "brand-gadget"
   313  		typ = snap.TypeGadget
   314  	default:
   315  		panic(fmt.Sprintf("refresh: unknown snap-id: %s", cand.snapID))
   316  	}
   317  
   318  	revno := snap.R(11)
   319  	if r := f.refreshRevnos[cand.snapID]; !r.Unset() {
   320  		revno = r
   321  	}
   322  	confinement := snap.StrictConfinement
   323  	switch cand.channel {
   324  	case "channel-for-7":
   325  		revno = snap.R(7)
   326  	case "channel-for-classic":
   327  		confinement = snap.ClassicConfinement
   328  	case "channel-for-devmode":
   329  		confinement = snap.DevModeConfinement
   330  	}
   331  	if name == "some-snap-now-classic" {
   332  		confinement = "classic"
   333  	}
   334  
   335  	info := &snap.Info{
   336  		SnapType: typ,
   337  		SideInfo: snap.SideInfo{
   338  			RealName: name,
   339  			Channel:  cand.channel,
   340  			SnapID:   cand.snapID,
   341  			Revision: revno,
   342  		},
   343  		Version: name,
   344  		DownloadInfo: snap.DownloadInfo{
   345  			DownloadURL: "https://some-server.com/some/path.snap",
   346  		},
   347  		Confinement:   confinement,
   348  		Architectures: []string{"all"},
   349  		Epoch:         epoch,
   350  	}
   351  	switch cand.channel {
   352  	case "channel-for-layout":
   353  		info.Layout = map[string]*snap.Layout{
   354  			"/usr": {
   355  				Snap:    info,
   356  				Path:    "/usr",
   357  				Symlink: "$SNAP/usr",
   358  			},
   359  		}
   360  	case "channel-for-base":
   361  		info.Base = "some-base"
   362  	}
   363  
   364  	var hit snap.Revision
   365  	if cand.revision != revno {
   366  		hit = revno
   367  	}
   368  	for _, blocked := range cand.block {
   369  		if blocked == revno {
   370  			hit = snap.Revision{}
   371  			break
   372  		}
   373  	}
   374  
   375  	if !hit.Unset() {
   376  		return info, nil
   377  	}
   378  
   379  	return nil, store.ErrNoUpdateAvailable
   380  }
   381  
   382  func (f *fakeStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
   383  	if ctx == nil {
   384  		panic("context required")
   385  	}
   386  	f.pokeStateLock()
   387  
   388  	if len(currentSnaps) == 0 && len(actions) == 0 {
   389  		return nil, nil
   390  	}
   391  	if len(actions) > 4 {
   392  		panic("fake SnapAction unexpectedly called with more than 3 actions")
   393  	}
   394  
   395  	curByInstanceName := make(map[string]*store.CurrentSnap, len(currentSnaps))
   396  	curSnaps := make(byName, len(currentSnaps))
   397  	for i, cur := range currentSnaps {
   398  		if cur.InstanceName == "" || cur.SnapID == "" || cur.Revision.Unset() {
   399  			return nil, fmt.Errorf("internal error: incomplete current snap info")
   400  		}
   401  		curByInstanceName[cur.InstanceName] = cur
   402  		curSnaps[i] = *cur
   403  	}
   404  	sort.Sort(curSnaps)
   405  
   406  	userID := 0
   407  	if user != nil {
   408  		userID = user.ID
   409  	}
   410  	if len(curSnaps) == 0 {
   411  		curSnaps = nil
   412  	}
   413  	f.fakeBackend.appendOp(&fakeOp{
   414  		op:       "storesvc-snap-action",
   415  		curSnaps: curSnaps,
   416  		userID:   userID,
   417  	})
   418  
   419  	if f.seenPrivacyKeys == nil {
   420  		// so that checks don't topple over this being uninitialized
   421  		f.seenPrivacyKeys = make(map[string]bool)
   422  	}
   423  	if opts != nil && opts.PrivacyKey != "" {
   424  		f.seenPrivacyKeys[opts.PrivacyKey] = true
   425  	}
   426  
   427  	sorted := make(byAction, len(actions))
   428  	copy(sorted, actions)
   429  	sort.Sort(sorted)
   430  
   431  	refreshErrors := make(map[string]error)
   432  	installErrors := make(map[string]error)
   433  	var res []*snap.Info
   434  	for _, a := range sorted {
   435  		if a.Action != "install" && a.Action != "refresh" {
   436  			panic("not supported")
   437  		}
   438  		if a.InstanceName == "" {
   439  			return nil, fmt.Errorf("internal error: action without instance name")
   440  		}
   441  
   442  		snapName, instanceKey := snap.SplitInstanceName(a.InstanceName)
   443  
   444  		if a.Action == "install" {
   445  			spec := snapSpec{
   446  				Name:     snapName,
   447  				Channel:  a.Channel,
   448  				Revision: a.Revision,
   449  				Cohort:   a.CohortKey,
   450  			}
   451  			info, err := f.snap(spec, user)
   452  			if err != nil {
   453  				installErrors[a.InstanceName] = err
   454  				continue
   455  			}
   456  			f.fakeBackend.appendOp(&fakeOp{
   457  				op:     "storesvc-snap-action:action",
   458  				action: *a,
   459  				revno:  info.Revision,
   460  				userID: userID,
   461  			})
   462  			if !a.Revision.Unset() {
   463  				info.Channel = ""
   464  			}
   465  			info.InstanceKey = instanceKey
   466  			res = append(res, info)
   467  			continue
   468  		}
   469  
   470  		// refresh
   471  
   472  		cur := curByInstanceName[a.InstanceName]
   473  		if cur == nil {
   474  			return nil, fmt.Errorf("internal error: no matching current snap for %q", a.InstanceName)
   475  		}
   476  		channel := a.Channel
   477  		if channel == "" {
   478  			channel = cur.TrackingChannel
   479  		}
   480  		ignoreValidation := cur.IgnoreValidation
   481  		if a.Flags&store.SnapActionIgnoreValidation != 0 {
   482  			ignoreValidation = true
   483  		} else if a.Flags&store.SnapActionEnforceValidation != 0 {
   484  			ignoreValidation = false
   485  		}
   486  		cand := refreshCand{
   487  			snapID:           a.SnapID,
   488  			channel:          channel,
   489  			revision:         cur.Revision,
   490  			block:            cur.Block,
   491  			ignoreValidation: ignoreValidation,
   492  		}
   493  		info, err := f.lookupRefresh(cand)
   494  		var hit snap.Revision
   495  		if info != nil {
   496  			if !a.Revision.Unset() {
   497  				info.Revision = a.Revision
   498  			}
   499  			hit = info.Revision
   500  		}
   501  		f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{
   502  			op:     "storesvc-snap-action:action",
   503  			action: *a,
   504  			revno:  hit,
   505  			userID: userID,
   506  		})
   507  		if err == store.ErrNoUpdateAvailable {
   508  			refreshErrors[cur.InstanceName] = err
   509  			continue
   510  		}
   511  		if err != nil {
   512  			return nil, err
   513  		}
   514  		if !a.Revision.Unset() {
   515  			info.Channel = ""
   516  		}
   517  		info.InstanceKey = instanceKey
   518  		res = append(res, info)
   519  	}
   520  
   521  	if len(refreshErrors)+len(installErrors) > 0 || len(res) == 0 {
   522  		if len(refreshErrors) == 0 {
   523  			refreshErrors = nil
   524  		}
   525  		if len(installErrors) == 0 {
   526  			installErrors = nil
   527  		}
   528  		return res, &store.SnapActionError{
   529  			NoResults: len(refreshErrors)+len(installErrors)+len(res) == 0,
   530  			Refresh:   refreshErrors,
   531  			Install:   installErrors,
   532  		}
   533  	}
   534  
   535  	return res, nil
   536  }
   537  
   538  func (f *fakeStore) SuggestedCurrency() string {
   539  	f.pokeStateLock()
   540  
   541  	return "XTS"
   542  }
   543  
   544  func (f *fakeStore) Download(ctx context.Context, name, targetFn string, snapInfo *snap.DownloadInfo, pb progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error {
   545  	f.pokeStateLock()
   546  
   547  	if _, key := snap.SplitInstanceName(name); key != "" {
   548  		return fmt.Errorf("internal error: unsupported download with instance name %q", name)
   549  	}
   550  	var macaroon string
   551  	if user != nil {
   552  		macaroon = user.StoreMacaroon
   553  	}
   554  	// only add the options if they contain anything interesting
   555  	if *dlOpts == (store.DownloadOptions{}) {
   556  		dlOpts = nil
   557  	}
   558  	f.downloads = append(f.downloads, fakeDownload{
   559  		macaroon: macaroon,
   560  		name:     name,
   561  		target:   targetFn,
   562  		opts:     dlOpts,
   563  	})
   564  	f.fakeBackend.appendOp(&fakeOp{op: "storesvc-download", name: name})
   565  
   566  	pb.SetTotal(float64(f.fakeTotalProgress))
   567  	pb.Set(float64(f.fakeCurrentProgress))
   568  
   569  	return nil
   570  }
   571  
   572  func (f *fakeStore) WriteCatalogs(ctx context.Context, _ io.Writer, _ store.SnapAdder) error {
   573  	if ctx == nil {
   574  		panic("context required")
   575  	}
   576  	f.pokeStateLock()
   577  
   578  	f.fakeBackend.appendOp(&fakeOp{
   579  		op: "x-commands",
   580  	})
   581  
   582  	return nil
   583  }
   584  
   585  func (f *fakeStore) Sections(ctx context.Context, _ *auth.UserState) ([]string, error) {
   586  	if ctx == nil {
   587  		panic("context required")
   588  	}
   589  	f.pokeStateLock()
   590  
   591  	f.fakeBackend.appendOp(&fakeOp{
   592  		op: "x-sections",
   593  	})
   594  
   595  	return nil, nil
   596  }
   597  
   598  type fakeSnappyBackend struct {
   599  	ops fakeOps
   600  	mu  sync.Mutex
   601  
   602  	linkSnapWaitCh      chan int
   603  	linkSnapWaitTrigger string
   604  
   605  	linkSnapFailTrigger     string
   606  	copySnapDataFailTrigger string
   607  	emptyContainer          snap.Container
   608  }
   609  
   610  func (f *fakeSnappyBackend) OpenSnapFile(snapFilePath string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
   611  	op := fakeOp{
   612  		op:   "open-snap-file",
   613  		path: snapFilePath,
   614  	}
   615  
   616  	if si != nil {
   617  		op.sinfo = *si
   618  	}
   619  
   620  	var info *snap.Info
   621  	if !osutil.IsDirectory(snapFilePath) {
   622  		name := filepath.Base(snapFilePath)
   623  		split := strings.Split(name, "_")
   624  		if len(split) >= 2 {
   625  			// <snap>_<rev>.snap
   626  			// <snap>_<instance-key>_<rev>.snap
   627  			name = split[0]
   628  		}
   629  
   630  		info = &snap.Info{SuggestedName: name, Architectures: []string{"all"}}
   631  		if name == "some-snap-now-classic" {
   632  			info.Confinement = "classic"
   633  		}
   634  		if name == "some-epoch-snap" {
   635  			info.Epoch = snap.E("42")
   636  		} else {
   637  			info.Epoch = snap.E("1*")
   638  		}
   639  	} else {
   640  		// for snap try only
   641  		snapf, err := snap.Open(snapFilePath)
   642  		if err != nil {
   643  			return nil, nil, err
   644  		}
   645  
   646  		info, err = snap.ReadInfoFromSnapFile(snapf, si)
   647  		if err != nil {
   648  			return nil, nil, err
   649  		}
   650  	}
   651  
   652  	if info == nil {
   653  		return nil, nil, fmt.Errorf("internal error: no mocked snap for %q", snapFilePath)
   654  	}
   655  	f.appendOp(&op)
   656  	return info, f.emptyContainer, nil
   657  }
   658  
   659  func (f *fakeSnappyBackend) SetupSnap(snapFilePath, instanceName string, si *snap.SideInfo, p progress.Meter) (snap.Type, error) {
   660  	p.Notify("setup-snap")
   661  	revno := snap.R(0)
   662  	if si != nil {
   663  		revno = si.Revision
   664  	}
   665  	f.appendOp(&fakeOp{
   666  		op:    "setup-snap",
   667  		name:  instanceName,
   668  		path:  snapFilePath,
   669  		revno: revno,
   670  	})
   671  	snapType := snap.TypeApp
   672  	switch si.RealName {
   673  	case "core":
   674  		snapType = snap.TypeOS
   675  	case "gadget":
   676  		snapType = snap.TypeGadget
   677  	}
   678  	if instanceName == "borken-in-setup" {
   679  		return snapType, fmt.Errorf("cannot install snap %q", instanceName)
   680  	}
   681  	return snapType, nil
   682  }
   683  
   684  func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) {
   685  	if name == "borken" && si.Revision == snap.R(2) {
   686  		return nil, errors.New(`cannot read info for "borken" snap`)
   687  	}
   688  	if name == "borken-undo-setup" && si.Revision == snap.R(2) {
   689  		return nil, errors.New(`cannot read info for "borken-undo-setup" snap`)
   690  	}
   691  	if name == "not-there" && si.Revision == snap.R(2) {
   692  		return nil, &snap.NotFoundError{Snap: name, Revision: si.Revision}
   693  	}
   694  	snapName, instanceKey := snap.SplitInstanceName(name)
   695  	// naive emulation for now, always works
   696  	info := &snap.Info{
   697  		SuggestedName: snapName,
   698  		SideInfo:      *si,
   699  		Architectures: []string{"all"},
   700  		SnapType:      snap.TypeApp,
   701  		Epoch:         snap.E("1*"),
   702  	}
   703  	if strings.Contains(snapName, "alias-snap") {
   704  		// only for the switch below
   705  		snapName = "alias-snap"
   706  	}
   707  	switch snapName {
   708  	case "snap-with-empty-epoch":
   709  		info.Epoch = snap.Epoch{}
   710  	case "some-epoch-snap":
   711  		info.Epoch = snap.E("13")
   712  	case "gadget":
   713  		info.SnapType = snap.TypeGadget
   714  	case "core":
   715  		info.SnapType = snap.TypeOS
   716  	case "services-snap":
   717  		var err error
   718  		// fix services after/before so that there is only one solution
   719  		// to dependency ordering
   720  		info, err = snap.InfoFromSnapYaml([]byte(`name: services-snap
   721  apps:
   722    svc1:
   723      daemon: simple
   724      before: [svc3]
   725    svc2:
   726      daemon: simple
   727      after: [svc1]
   728    svc3:
   729      daemon: simple
   730      before: [svc2]
   731  `))
   732  		if err != nil {
   733  			panic(err)
   734  		}
   735  		info.SideInfo = *si
   736  	case "alias-snap":
   737  		var err error
   738  		info, err = snap.InfoFromSnapYaml([]byte(`name: alias-snap
   739  apps:
   740    cmd1:
   741    cmd2:
   742    cmd3:
   743    cmd4:
   744    cmd5:
   745    cmddaemon:
   746      daemon: simple
   747  `))
   748  		if err != nil {
   749  			panic(err)
   750  		}
   751  		info.SideInfo = *si
   752  	}
   753  
   754  	info.InstanceKey = instanceKey
   755  	return info, nil
   756  }
   757  
   758  func (f *fakeSnappyBackend) ClearTrashedData(si *snap.Info) {
   759  	f.appendOp(&fakeOp{
   760  		op:    "cleanup-trash",
   761  		name:  si.InstanceName(),
   762  		revno: si.Revision,
   763  	})
   764  }
   765  
   766  func (f *fakeSnappyBackend) StoreInfo(st *state.State, name, channel string, userID int, flags snapstate.Flags) (*snap.Info, error) {
   767  	return f.ReadInfo(name, &snap.SideInfo{
   768  		RealName: name,
   769  	})
   770  }
   771  
   772  func (f *fakeSnappyBackend) CopySnapData(newInfo, oldInfo *snap.Info, p progress.Meter) error {
   773  	p.Notify("copy-data")
   774  	old := "<no-old>"
   775  	if oldInfo != nil {
   776  		old = oldInfo.MountDir()
   777  	}
   778  
   779  	if newInfo.MountDir() == f.copySnapDataFailTrigger {
   780  		f.appendOp(&fakeOp{
   781  			op:   "copy-data.failed",
   782  			path: newInfo.MountDir(),
   783  			old:  old,
   784  		})
   785  		return errors.New("fail")
   786  	}
   787  
   788  	f.appendOp(&fakeOp{
   789  		op:   "copy-data",
   790  		path: newInfo.MountDir(),
   791  		old:  old,
   792  	})
   793  	return nil
   794  }
   795  
   796  func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, model *asserts.Model, tm timings.Measurer) error {
   797  	if info.MountDir() == f.linkSnapWaitTrigger {
   798  		f.linkSnapWaitCh <- 1
   799  		<-f.linkSnapWaitCh
   800  	}
   801  
   802  	if info.MountDir() == f.linkSnapFailTrigger {
   803  		f.ops = append(f.ops, fakeOp{
   804  			op:   "link-snap.failed",
   805  			path: info.MountDir(),
   806  		})
   807  		return errors.New("fail")
   808  	}
   809  
   810  	f.appendOp(&fakeOp{
   811  		op:   "link-snap",
   812  		path: info.MountDir(),
   813  	})
   814  	return nil
   815  }
   816  
   817  func svcSnapMountDir(svcs []*snap.AppInfo) string {
   818  	if len(svcs) == 0 {
   819  		return "<no services>"
   820  	}
   821  	if svcs[0].Snap == nil {
   822  		return "<snapless service>"
   823  	}
   824  	return svcs[0].Snap.MountDir()
   825  }
   826  
   827  func (f *fakeSnappyBackend) StartServices(svcs []*snap.AppInfo, meter progress.Meter, tm timings.Measurer) error {
   828  	services := make([]string, 0, len(svcs))
   829  	for _, svc := range svcs {
   830  		services = append(services, svc.Name)
   831  	}
   832  	f.appendOp(&fakeOp{
   833  		op:       "start-snap-services",
   834  		path:     svcSnapMountDir(svcs),
   835  		services: services,
   836  	})
   837  	return nil
   838  }
   839  
   840  func (f *fakeSnappyBackend) StopServices(svcs []*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error {
   841  	f.appendOp(&fakeOp{
   842  		op:   fmt.Sprintf("stop-snap-services:%s", reason),
   843  		path: svcSnapMountDir(svcs),
   844  	})
   845  	return nil
   846  }
   847  
   848  func (f *fakeSnappyBackend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, p progress.Meter) error {
   849  	p.Notify("setup-snap")
   850  	f.appendOp(&fakeOp{
   851  		op:    "undo-setup-snap",
   852  		name:  s.InstanceName(),
   853  		path:  s.MountDir(),
   854  		stype: typ,
   855  	})
   856  	if s.InstanceName() == "borken-undo-setup" {
   857  		return errors.New(`cannot undo setup of "borken-undo-setup" snap`)
   858  	}
   859  	return nil
   860  }
   861  
   862  func (f *fakeSnappyBackend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, p progress.Meter) error {
   863  	p.Notify("undo-copy-data")
   864  	old := "<no-old>"
   865  	if oldInfo != nil {
   866  		old = oldInfo.MountDir()
   867  	}
   868  	f.appendOp(&fakeOp{
   869  		op:   "undo-copy-snap-data",
   870  		path: newInfo.MountDir(),
   871  		old:  old,
   872  	})
   873  	return nil
   874  }
   875  
   876  func (f *fakeSnappyBackend) UnlinkSnap(info *snap.Info, meter progress.Meter) error {
   877  	meter.Notify("unlink")
   878  	f.appendOp(&fakeOp{
   879  		op:   "unlink-snap",
   880  		path: info.MountDir(),
   881  	})
   882  	return nil
   883  }
   884  
   885  func (f *fakeSnappyBackend) RemoveSnapFiles(s snap.PlaceInfo, typ snap.Type, meter progress.Meter) error {
   886  	meter.Notify("remove-snap-files")
   887  	f.appendOp(&fakeOp{
   888  		op:    "remove-snap-files",
   889  		path:  s.MountDir(),
   890  		stype: typ,
   891  	})
   892  	return nil
   893  }
   894  
   895  func (f *fakeSnappyBackend) RemoveSnapData(info *snap.Info) error {
   896  	f.appendOp(&fakeOp{
   897  		op:   "remove-snap-data",
   898  		path: info.MountDir(),
   899  	})
   900  	return nil
   901  }
   902  
   903  func (f *fakeSnappyBackend) RemoveSnapCommonData(info *snap.Info) error {
   904  	f.appendOp(&fakeOp{
   905  		op:   "remove-snap-common-data",
   906  		path: info.MountDir(),
   907  	})
   908  	return nil
   909  }
   910  
   911  func (f *fakeSnappyBackend) RemoveSnapDataDir(info *snap.Info, otherInstances bool) error {
   912  	f.ops = append(f.ops, fakeOp{
   913  		op:             "remove-snap-data-dir",
   914  		name:           info.InstanceName(),
   915  		path:           snap.BaseDataDir(info.SnapName()),
   916  		otherInstances: otherInstances,
   917  	})
   918  	return nil
   919  }
   920  
   921  func (f *fakeSnappyBackend) RemoveSnapDir(s snap.PlaceInfo, otherInstances bool) error {
   922  	f.ops = append(f.ops, fakeOp{
   923  		op:             "remove-snap-dir",
   924  		name:           s.InstanceName(),
   925  		path:           snap.BaseDir(s.SnapName()),
   926  		otherInstances: otherInstances,
   927  	})
   928  	return nil
   929  }
   930  
   931  func (f *fakeSnappyBackend) DiscardSnapNamespace(snapName string) error {
   932  	f.appendOp(&fakeOp{
   933  		op:   "discard-namespace",
   934  		name: snapName,
   935  	})
   936  	return nil
   937  }
   938  
   939  func (f *fakeSnappyBackend) Candidate(sideInfo *snap.SideInfo) {
   940  	var sinfo snap.SideInfo
   941  	if sideInfo != nil {
   942  		sinfo = *sideInfo
   943  	}
   944  	f.appendOp(&fakeOp{
   945  		op:    "candidate",
   946  		sinfo: sinfo,
   947  	})
   948  }
   949  
   950  func (f *fakeSnappyBackend) CurrentInfo(curInfo *snap.Info) {
   951  	old := "<no-current>"
   952  	if curInfo != nil {
   953  		old = curInfo.MountDir()
   954  	}
   955  	f.appendOp(&fakeOp{
   956  		op:  "current",
   957  		old: old,
   958  	})
   959  }
   960  
   961  func (f *fakeSnappyBackend) ForeignTask(kind string, status state.Status, snapsup *snapstate.SnapSetup) {
   962  	f.appendOp(&fakeOp{
   963  		op:    kind + ":" + status.String(),
   964  		name:  snapsup.InstanceName(),
   965  		revno: snapsup.Revision(),
   966  	})
   967  }
   968  
   969  type byAlias []*backend.Alias
   970  
   971  func (ba byAlias) Len() int      { return len(ba) }
   972  func (ba byAlias) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] }
   973  func (ba byAlias) Less(i, j int) bool {
   974  	return ba[i].Name < ba[j].Name
   975  }
   976  
   977  func (f *fakeSnappyBackend) UpdateAliases(add []*backend.Alias, remove []*backend.Alias) error {
   978  	if len(add) != 0 {
   979  		add = append([]*backend.Alias(nil), add...)
   980  		sort.Sort(byAlias(add))
   981  	}
   982  	if len(remove) != 0 {
   983  		remove = append([]*backend.Alias(nil), remove...)
   984  		sort.Sort(byAlias(remove))
   985  	}
   986  	f.appendOp(&fakeOp{
   987  		op:        "update-aliases",
   988  		aliases:   add,
   989  		rmAliases: remove,
   990  	})
   991  	return nil
   992  }
   993  
   994  func (f *fakeSnappyBackend) RemoveSnapAliases(snapName string) error {
   995  	f.appendOp(&fakeOp{
   996  		op:   "remove-snap-aliases",
   997  		name: snapName,
   998  	})
   999  	return nil
  1000  }
  1001  
  1002  func (f *fakeSnappyBackend) appendOp(op *fakeOp) {
  1003  	f.mu.Lock()
  1004  	defer f.mu.Unlock()
  1005  	f.ops = append(f.ops, *op)
  1006  }