github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/overlord/snapstate/autorefresh_gating_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 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  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"time"
    29  
    30  	. "gopkg.in/check.v1"
    31  
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/interfaces"
    34  	"github.com/snapcore/snapd/interfaces/builtin"
    35  	"github.com/snapcore/snapd/logger"
    36  	"github.com/snapcore/snapd/osutil"
    37  	"github.com/snapcore/snapd/overlord/auth"
    38  	"github.com/snapcore/snapd/overlord/configstate/config"
    39  	"github.com/snapcore/snapd/overlord/hookstate"
    40  	"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
    41  	"github.com/snapcore/snapd/overlord/snapstate"
    42  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    43  	"github.com/snapcore/snapd/overlord/state"
    44  	"github.com/snapcore/snapd/release"
    45  	"github.com/snapcore/snapd/snap"
    46  	"github.com/snapcore/snapd/snap/snaptest"
    47  	"github.com/snapcore/snapd/store"
    48  	"github.com/snapcore/snapd/testutil"
    49  )
    50  
    51  type autoRefreshGatingStore struct {
    52  	*fakeStore
    53  	refreshedSnaps []*snap.Info
    54  }
    55  
    56  type autorefreshGatingSuite struct {
    57  	testutil.BaseTest
    58  	state *state.State
    59  	repo  *interfaces.Repository
    60  	store *autoRefreshGatingStore
    61  }
    62  
    63  var _ = Suite(&autorefreshGatingSuite{})
    64  
    65  func (s *autorefreshGatingSuite) SetUpTest(c *C) {
    66  	s.BaseTest.SetUpTest(c)
    67  	dirs.SetRootDir(c.MkDir())
    68  	s.AddCleanup(func() {
    69  		dirs.SetRootDir("/")
    70  	})
    71  	s.state = state.New(nil)
    72  
    73  	s.repo = interfaces.NewRepository()
    74  	for _, iface := range builtin.Interfaces() {
    75  		c.Assert(s.repo.AddInterface(iface), IsNil)
    76  	}
    77  
    78  	s.state.Lock()
    79  	defer s.state.Unlock()
    80  	ifacerepo.Replace(s.state, s.repo)
    81  
    82  	s.store = &autoRefreshGatingStore{fakeStore: &fakeStore{}}
    83  	snapstate.ReplaceStore(s.state, s.store)
    84  	s.state.Set("refresh-privacy-key", "privacy-key")
    85  }
    86  
    87  func (r *autoRefreshGatingStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
    88  	if assertQuery != nil {
    89  		panic("no assertion query support")
    90  	}
    91  	if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 {
    92  		panic("expected in test one action for each current snaps, and at least one snap")
    93  	}
    94  	for _, a := range actions {
    95  		if a.Action != "refresh" {
    96  			panic("expected refresh actions")
    97  		}
    98  	}
    99  
   100  	res := []store.SnapActionResult{}
   101  	for _, rs := range r.refreshedSnaps {
   102  		res = append(res, store.SnapActionResult{Info: rs})
   103  	}
   104  
   105  	return res, nil, nil
   106  }
   107  
   108  func mockInstalledSnap(c *C, st *state.State, snapYaml string, hasHook bool) *snap.Info {
   109  	snapInfo := snaptest.MockSnap(c, string(snapYaml), &snap.SideInfo{
   110  		Revision: snap.R(1),
   111  	})
   112  
   113  	snapName := snapInfo.SnapName()
   114  	si := &snap.SideInfo{RealName: snapName, SnapID: "id", Revision: snap.R(1)}
   115  	snapstate.Set(st, snapName, &snapstate.SnapState{
   116  		Active:   true,
   117  		Sequence: []*snap.SideInfo{si},
   118  		Current:  si.Revision,
   119  		SnapType: string(snapInfo.Type()),
   120  	})
   121  
   122  	if hasHook {
   123  		c.Assert(os.MkdirAll(snapInfo.HooksDir(), 0775), IsNil)
   124  		err := ioutil.WriteFile(filepath.Join(snapInfo.HooksDir(), "gate-auto-refresh"), nil, 0755)
   125  		c.Assert(err, IsNil)
   126  	}
   127  	return snapInfo
   128  }
   129  
   130  func mockLastRefreshed(c *C, st *state.State, refreshedTime string, snaps ...string) {
   131  	refreshed, err := time.Parse(time.RFC3339, refreshedTime)
   132  	c.Assert(err, IsNil)
   133  	for _, snapName := range snaps {
   134  		var snapst snapstate.SnapState
   135  		c.Assert(snapstate.Get(st, snapName, &snapst), IsNil)
   136  		snapst.LastRefreshTime = &refreshed
   137  		snapstate.Set(st, snapName, &snapst)
   138  	}
   139  }
   140  
   141  const baseSnapAyaml = `name: base-snap-a
   142  type: base
   143  `
   144  
   145  const snapAyaml = `name: snap-a
   146  type: app
   147  base: base-snap-a
   148  `
   149  
   150  const baseSnapByaml = `name: base-snap-b
   151  type: base
   152  `
   153  
   154  const snapByaml = `name: snap-b
   155  type: app
   156  base: base-snap-b
   157  version: 1
   158  `
   159  
   160  const kernelYaml = `name: kernel
   161  type: kernel
   162  version: 1
   163  `
   164  
   165  const gadget1Yaml = `name: gadget
   166  type: gadget
   167  version: 1
   168  `
   169  
   170  const snapCyaml = `name: snap-c
   171  type: app
   172  version: 1
   173  `
   174  
   175  const snapDyaml = `name: snap-d
   176  type: app
   177  version: 1
   178  slots:
   179      slot: desktop
   180  `
   181  
   182  const snapEyaml = `name: snap-e
   183  type: app
   184  version: 1
   185  base: other-base
   186  plugs:
   187      plug: desktop
   188  `
   189  
   190  const snapFyaml = `name: snap-f
   191  type: app
   192  version: 1
   193  plugs:
   194      plug: desktop
   195  `
   196  
   197  const snapGyaml = `name: snap-g
   198  type: app
   199  version: 1
   200  base: other-base
   201  plugs:
   202      desktop:
   203      mir:
   204  `
   205  
   206  const coreYaml = `name: core
   207  type: os
   208  version: 1
   209  slots:
   210      desktop:
   211      mir:
   212  `
   213  
   214  const core18Yaml = `name: core18
   215  type: os
   216  version: 1
   217  `
   218  
   219  const snapdYaml = `name: snapd
   220  version: 1
   221  type: snapd
   222  slots:
   223      desktop:
   224  `
   225  
   226  func (s *autorefreshGatingSuite) TestHoldDurationLeft(c *C) {
   227  	now, err := time.Parse(time.RFC3339, "2021-06-03T10:00:00Z")
   228  	c.Assert(err, IsNil)
   229  	maxPostponement := time.Hour * 24 * 90
   230  
   231  	for i, tc := range []struct {
   232  		lastRefresh, firstHeld string
   233  		maxDuration            string
   234  		expected               string
   235  	}{
   236  		{
   237  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   238  			"2021-06-03T10:00:00Z", // first held now
   239  			"48h", // max duration
   240  			"48h", // expected
   241  		},
   242  		{
   243  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   244  			"2021-06-02T10:00:00Z", // first held (1 day ago)
   245  			"48h", // max duration
   246  			"24h", // expected
   247  		},
   248  		{
   249  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   250  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   251  			"48h", // max duration
   252  			"00h", // expected
   253  		},
   254  		{
   255  			"2021-03-08T10:00:00Z", // last refreshed (almost 3 months ago)
   256  			"2021-06-01T10:00:00Z", // first held
   257  			"2160h",                // max duration (90 days)
   258  			"72h",                  // expected
   259  		},
   260  		{
   261  			"2021-03-04T10:00:00Z", // last refreshed
   262  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   263  			"2160h",                // max duration (90 days)
   264  			"-24h",                 // expected (refresh is 1 day overdue)
   265  		},
   266  		{
   267  			"2021-06-01T10:00:00Z", // last refreshed (2 days ago)
   268  			"2021-06-03T10:00:00Z", // first held now
   269  			"2160h",                // max duration (90 days)
   270  			"2112h",                // expected (max minus 2 days)
   271  		},
   272  	} {
   273  		lastRefresh, err := time.Parse(time.RFC3339, tc.lastRefresh)
   274  		c.Assert(err, IsNil)
   275  		firstHeld, err := time.Parse(time.RFC3339, tc.firstHeld)
   276  		c.Assert(err, IsNil)
   277  		maxDuration, err := time.ParseDuration(tc.maxDuration)
   278  		c.Assert(err, IsNil)
   279  		expected, err := time.ParseDuration(tc.expected)
   280  		c.Assert(err, IsNil)
   281  
   282  		left := snapstate.HoldDurationLeft(now, lastRefresh, firstHeld, maxDuration, maxPostponement)
   283  		c.Check(left, Equals, expected, Commentf("case #%d", i))
   284  	}
   285  }
   286  
   287  func (s *autorefreshGatingSuite) TestLastRefreshedHelper(c *C) {
   288  	st := s.state
   289  	st.Lock()
   290  	defer st.Unlock()
   291  
   292  	inf := mockInstalledSnap(c, st, snapAyaml, false)
   293  	stat, err := os.Stat(inf.MountFile())
   294  	c.Assert(err, IsNil)
   295  
   296  	refreshed, err := snapstate.LastRefreshed(st, "snap-a")
   297  	c.Assert(err, IsNil)
   298  	c.Check(refreshed, DeepEquals, stat.ModTime())
   299  
   300  	t, err := time.Parse(time.RFC3339, "2021-01-01T10:00:00Z")
   301  	c.Assert(err, IsNil)
   302  
   303  	var snapst snapstate.SnapState
   304  	c.Assert(snapstate.Get(st, "snap-a", &snapst), IsNil)
   305  	snapst.LastRefreshTime = &t
   306  	snapstate.Set(st, "snap-a", &snapst)
   307  
   308  	refreshed, err = snapstate.LastRefreshed(st, "snap-a")
   309  	c.Assert(err, IsNil)
   310  	c.Check(refreshed, DeepEquals, t)
   311  }
   312  
   313  func (s *autorefreshGatingSuite) TestHoldRefreshHelper(c *C) {
   314  	st := s.state
   315  	st.Lock()
   316  	defer st.Unlock()
   317  
   318  	restore := snapstate.MockTimeNow(func() time.Time {
   319  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   320  		c.Assert(err, IsNil)
   321  		return t
   322  	})
   323  	defer restore()
   324  
   325  	mockInstalledSnap(c, st, snapAyaml, false)
   326  	mockInstalledSnap(c, st, snapByaml, false)
   327  	mockInstalledSnap(c, st, snapCyaml, false)
   328  	mockInstalledSnap(c, st, snapDyaml, false)
   329  	mockInstalledSnap(c, st, snapEyaml, false)
   330  	mockInstalledSnap(c, st, snapFyaml, false)
   331  
   332  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f")
   333  
   334  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   335  	// this could be merged with the above HoldRefresh call, but it's fine if
   336  	// done separately too.
   337  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-e"), IsNil)
   338  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-e"), IsNil)
   339  	c.Assert(snapstate.HoldRefresh(st, "snap-f", 0, "snap-f"), IsNil)
   340  
   341  	var gating map[string]map[string]*snapstate.HoldState
   342  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   343  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   344  		"snap-b": {
   345  			// holding of other snaps for maxOtherHoldDuration (48h)
   346  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   347  		},
   348  		"snap-c": {
   349  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   350  		},
   351  		"snap-e": {
   352  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   353  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   354  		},
   355  		"snap-f": {
   356  			// holding self set for maxPostponement minus 1 day due to last refresh.
   357  			"snap-f": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-07T10:00:00Z"),
   358  		},
   359  	})
   360  }
   361  
   362  func (s *autorefreshGatingSuite) TestHoldRefreshHelperMultipleTimes(c *C) {
   363  	st := s.state
   364  	st.Lock()
   365  	defer st.Unlock()
   366  
   367  	lastRefreshed := "2021-05-09T10:00:00Z"
   368  	now := "2021-05-10T10:00:00Z"
   369  	restore := snapstate.MockTimeNow(func() time.Time {
   370  		t, err := time.Parse(time.RFC3339, now)
   371  		c.Assert(err, IsNil)
   372  		return t
   373  	})
   374  	defer restore()
   375  
   376  	mockInstalledSnap(c, st, snapAyaml, false)
   377  	mockInstalledSnap(c, st, snapByaml, false)
   378  	// snap-a was last refreshed yesterday
   379  	mockLastRefreshed(c, st, lastRefreshed, "snap-a")
   380  
   381  	// hold it for just a bit (10h) initially
   382  	hold := time.Hour * 10
   383  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   384  	var gating map[string]map[string]*snapstate.HoldState
   385  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   386  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   387  		"snap-a": {
   388  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T20:00:00Z"),
   389  		},
   390  	})
   391  
   392  	// holding for a shorter time is fine too
   393  	hold = time.Hour * 5
   394  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   395  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   396  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   397  		"snap-a": {
   398  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T15:00:00Z"),
   399  		},
   400  	})
   401  
   402  	oldNow := now
   403  
   404  	// a refresh on next day
   405  	now = "2021-05-11T08:00:00Z"
   406  
   407  	// default hold time requested
   408  	hold = 0
   409  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   410  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   411  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   412  		"snap-a": {
   413  			// maximum for holding other snaps, but taking into consideration
   414  			// firstHeld time = "2021-05-10T10:00:00".
   415  			"snap-b": snapstate.MockHoldState(oldNow, "2021-05-12T10:00:00Z"),
   416  		},
   417  	})
   418  }
   419  
   420  func (s *autorefreshGatingSuite) TestHoldRefreshHelperCloseToMaxPostponement(c *C) {
   421  	st := s.state
   422  	st.Lock()
   423  	defer st.Unlock()
   424  
   425  	lastRefreshedStr := "2021-01-01T10:00:00Z"
   426  	lastRefreshed, err := time.Parse(time.RFC3339, lastRefreshedStr)
   427  	c.Assert(err, IsNil)
   428  	// we are 1 day before maxPostponent
   429  	now := lastRefreshed.Add(89 * time.Hour * 24)
   430  
   431  	restore := snapstate.MockTimeNow(func() time.Time { return now })
   432  	defer restore()
   433  
   434  	mockInstalledSnap(c, st, snapAyaml, false)
   435  	mockInstalledSnap(c, st, snapByaml, false)
   436  	mockLastRefreshed(c, st, lastRefreshedStr, "snap-a")
   437  
   438  	// request default hold time
   439  	var hold time.Duration
   440  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   441  
   442  	var gating map[string]map[string]*snapstate.HoldState
   443  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   444  	c.Assert(gating, HasLen, 1)
   445  	c.Check(gating["snap-a"]["snap-b"].HoldUntil.String(), DeepEquals, lastRefreshed.Add(90*time.Hour*24).String())
   446  }
   447  
   448  func (s *autorefreshGatingSuite) TestHoldRefreshExplicitHoldTime(c *C) {
   449  	st := s.state
   450  	st.Lock()
   451  	defer st.Unlock()
   452  
   453  	now := "2021-05-10T10:00:00Z"
   454  	restore := snapstate.MockTimeNow(func() time.Time {
   455  		t, err := time.Parse(time.RFC3339, now)
   456  		c.Assert(err, IsNil)
   457  		return t
   458  	})
   459  	defer restore()
   460  
   461  	mockInstalledSnap(c, st, snapAyaml, false)
   462  	mockInstalledSnap(c, st, snapByaml, false)
   463  
   464  	hold := time.Hour * 24 * 3
   465  	// holding self for 3 days
   466  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), IsNil)
   467  
   468  	// snap-b holds snap-a for 1 day
   469  	hold = time.Hour * 24
   470  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   471  
   472  	var gating map[string]map[string]*snapstate.HoldState
   473  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   474  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   475  		"snap-a": {
   476  			"snap-a": snapstate.MockHoldState(now, "2021-05-13T10:00:00Z"),
   477  			"snap-b": snapstate.MockHoldState(now, "2021-05-11T10:00:00Z"),
   478  		},
   479  	})
   480  }
   481  
   482  func (s *autorefreshGatingSuite) TestHoldRefreshHelperErrors(c *C) {
   483  	st := s.state
   484  	st.Lock()
   485  	defer st.Unlock()
   486  
   487  	now := "2021-05-10T10:00:00Z"
   488  	restore := snapstate.MockTimeNow(func() time.Time {
   489  		t, err := time.Parse(time.RFC3339, now)
   490  		c.Assert(err, IsNil)
   491  		return t
   492  	})
   493  	defer restore()
   494  
   495  	mockInstalledSnap(c, st, snapAyaml, false)
   496  	mockInstalledSnap(c, st, snapByaml, false)
   497  	// snap-b was refreshed a few days ago
   498  	mockLastRefreshed(c, st, "2021-05-01T10:00:00Z", "snap-b")
   499  
   500  	// holding itself
   501  	hold := time.Hour * 24 * 96
   502  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-a" of 2304h0m0s by snap "snap-a" exceeds maximum holding time`)
   503  
   504  	// holding other snap
   505  	hold = time.Hour * 49
   506  	err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-b")
   507  	c.Check(err, ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`)
   508  	herr, ok := err.(*snapstate.HoldError)
   509  	c.Assert(ok, Equals, true)
   510  	c.Check(herr.SnapsInError, DeepEquals, map[string]snapstate.HoldDurationError{
   511  		"snap-b": {
   512  			Err:          fmt.Errorf(`requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`),
   513  			DurationLeft: 48 * time.Hour,
   514  		},
   515  	})
   516  
   517  	// hold for maximum allowed for other snaps
   518  	hold = time.Hour * 48
   519  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), IsNil)
   520  	// 2 days passed since it was first held
   521  	now = "2021-05-12T10:00:00Z"
   522  	hold = time.Minute * 2
   523  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-a" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`)
   524  
   525  	// refreshed long time ago (> maxPostponement)
   526  	mockLastRefreshed(c, st, "2021-01-01T10:00:00Z", "snap-b")
   527  	hold = time.Hour * 2
   528  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`)
   529  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`)
   530  }
   531  
   532  func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) {
   533  	st := s.state
   534  	st.Lock()
   535  	defer st.Unlock()
   536  
   537  	mockInstalledSnap(c, st, snapAyaml, false)
   538  	mockInstalledSnap(c, st, snapByaml, false)
   539  	mockInstalledSnap(c, st, snapCyaml, false)
   540  	mockInstalledSnap(c, st, snapDyaml, false)
   541  
   542  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-b", "snap-c", "snap-d")
   543  
   544  	restore := snapstate.MockTimeNow(func() time.Time {
   545  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   546  		c.Assert(err, IsNil)
   547  		return t
   548  	})
   549  	defer restore()
   550  
   551  	// nothing is held initially
   552  	held, err := snapstate.HeldSnaps(st)
   553  	c.Assert(err, IsNil)
   554  	c.Check(held, IsNil)
   555  
   556  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   557  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil)
   558  	// holding self
   559  	c.Assert(snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d"), IsNil)
   560  
   561  	held, err = snapstate.HeldSnaps(st)
   562  	c.Assert(err, IsNil)
   563  	c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true})
   564  
   565  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-a"), IsNil)
   566  
   567  	held, err = snapstate.HeldSnaps(st)
   568  	c.Assert(err, IsNil)
   569  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-d": true})
   570  
   571  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-d"), IsNil)
   572  	held, err = snapstate.HeldSnaps(st)
   573  	c.Assert(err, IsNil)
   574  	c.Check(held, IsNil)
   575  }
   576  
   577  func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) {
   578  	st := s.state
   579  	st.Lock()
   580  	defer st.Unlock()
   581  
   582  	restore := snapstate.MockTimeNow(func() time.Time {
   583  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   584  		c.Assert(err, IsNil)
   585  		return t
   586  	})
   587  	defer restore()
   588  
   589  	mockInstalledSnap(c, st, snapAyaml, false)
   590  	mockInstalledSnap(c, st, snapByaml, false)
   591  	mockInstalledSnap(c, st, snapCyaml, false)
   592  	mockInstalledSnap(c, st, snapDyaml, false)
   593  
   594  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   595  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   596  
   597  	c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil)
   598  	var gating map[string]map[string]*snapstate.HoldState
   599  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   600  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   601  		"snap-d": {
   602  			// holding self set for maxPostponement (95 days - buffer = 90 days)
   603  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"),
   604  		},
   605  	})
   606  
   607  	held, err := snapstate.HeldSnaps(st)
   608  	c.Assert(err, IsNil)
   609  	c.Check(held, DeepEquals, map[string]bool{"snap-d": true})
   610  }
   611  
   612  const useHook = true
   613  const noHook = false
   614  
   615  func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) {
   616  	c.Assert(task.Kind(), Equals, "conditional-auto-refresh")
   617  	var snaps map[string]*snapstate.RefreshCandidate
   618  	c.Assert(task.Get("snaps", &snaps), IsNil)
   619  	c.Check(snaps, DeepEquals, expected)
   620  }
   621  
   622  func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) {
   623  	restore := release.MockOnClassic(true)
   624  	defer restore()
   625  
   626  	st := s.state
   627  
   628  	st.Lock()
   629  	defer st.Unlock()
   630  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
   631  	baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook)
   632  	// unrelated snaps
   633  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   634  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
   635  
   636  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   637  
   638  	updates := []*snap.Info{baseSnapA}
   639  	affected, err := snapstate.AffectedByRefresh(st, updates)
   640  	c.Assert(err, IsNil)
   641  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   642  		"snap-a": {
   643  			Base: true,
   644  			AffectingSnaps: map[string]bool{
   645  				"base-snap-a": true,
   646  			}}})
   647  }
   648  
   649  func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) {
   650  	restore := release.MockOnClassic(true)
   651  	defer restore()
   652  
   653  	st := s.state
   654  
   655  	st.Lock()
   656  	defer st.Unlock()
   657  	snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook)
   658  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   659  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   660  
   661  	c.Assert(s.repo.AddSnap(core), IsNil)
   662  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   663  	c.Assert(s.repo.AddSnap(snapC), IsNil)
   664  
   665  	updates := []*snap.Info{core}
   666  	affected, err := snapstate.AffectedByRefresh(st, updates)
   667  	c.Assert(err, IsNil)
   668  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   669  		"snap-c": {
   670  			Base: true,
   671  			AffectingSnaps: map[string]bool{
   672  				"core": true,
   673  			}}})
   674  }
   675  
   676  func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) {
   677  	restore := release.MockOnClassic(true)
   678  	defer restore()
   679  
   680  	st := s.state
   681  
   682  	st.Lock()
   683  	defer st.Unlock()
   684  	kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook)
   685  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   686  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   687  
   688  	updates := []*snap.Info{kernel}
   689  	affected, err := snapstate.AffectedByRefresh(st, updates)
   690  	c.Assert(err, IsNil)
   691  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   692  		"snap-c": {
   693  			Restart: true,
   694  			AffectingSnaps: map[string]bool{
   695  				"kernel": true,
   696  			}}})
   697  }
   698  
   699  func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) {
   700  	restore := release.MockOnClassic(true)
   701  	defer restore()
   702  
   703  	st := s.state
   704  
   705  	st.Lock()
   706  	defer st.Unlock()
   707  	kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook)
   708  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   709  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   710  
   711  	updates := []*snap.Info{kernel}
   712  	affected, err := snapstate.AffectedByRefresh(st, updates)
   713  	c.Assert(err, IsNil)
   714  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   715  		"snap-c": {
   716  			Restart: true,
   717  			AffectingSnaps: map[string]bool{
   718  				"gadget": true,
   719  			}}})
   720  }
   721  
   722  func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) {
   723  	restore := release.MockOnClassic(true)
   724  	defer restore()
   725  
   726  	st := s.state
   727  
   728  	st.Lock()
   729  	defer st.Unlock()
   730  
   731  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   732  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   733  	// unrelated snap
   734  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   735  
   736  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   737  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   738  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   739  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   740  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   741  	c.Assert(err, IsNil)
   742  
   743  	updates := []*snap.Info{snapD}
   744  	affected, err := snapstate.AffectedByRefresh(st, updates)
   745  	c.Assert(err, IsNil)
   746  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   747  		"snap-e": {
   748  			Restart: true,
   749  			AffectingSnaps: map[string]bool{
   750  				"snap-d": true,
   751  			}}})
   752  }
   753  
   754  func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) {
   755  	restore := release.MockOnClassic(true)
   756  	defer restore()
   757  
   758  	st := s.state
   759  
   760  	st.Lock()
   761  	defer st.Unlock()
   762  
   763  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   764  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   765  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   766  
   767  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   768  	c.Assert(s.repo.AddSnap(core), IsNil)
   769  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   770  
   771  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}}
   772  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   773  	c.Assert(err, IsNil)
   774  
   775  	updates := []*snap.Info{core}
   776  	affected, err := snapstate.AffectedByRefresh(st, updates)
   777  	c.Assert(err, IsNil)
   778  	c.Check(affected, HasLen, 0)
   779  }
   780  
   781  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackend(c *C) {
   782  	restore := release.MockOnClassic(true)
   783  	defer restore()
   784  
   785  	st := s.state
   786  
   787  	st.Lock()
   788  	defer st.Unlock()
   789  
   790  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   791  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   792  	// unrelated snap
   793  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   794  
   795  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   796  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   797  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   798  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   799  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   800  	c.Assert(err, IsNil)
   801  
   802  	// snapE has a plug using mount backend and is refreshed, this affects slot of snap-d.
   803  	updates := []*snap.Info{snapE}
   804  	affected, err := snapstate.AffectedByRefresh(st, updates)
   805  	c.Assert(err, IsNil)
   806  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   807  		"snap-d": {
   808  			Restart: true,
   809  			AffectingSnaps: map[string]bool{
   810  				"snap-e": true,
   811  			}}})
   812  }
   813  
   814  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) {
   815  	restore := release.MockOnClassic(true)
   816  	defer restore()
   817  
   818  	st := s.state
   819  
   820  	st.Lock()
   821  	defer st.Unlock()
   822  
   823  	snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, useHook)
   824  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   825  	// unrelated snap
   826  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   827  
   828  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   829  	c.Assert(s.repo.AddSnap(snapdSnap), IsNil)
   830  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   831  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}}
   832  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   833  	c.Assert(err, IsNil)
   834  
   835  	// snapE has a plug using mount backend, refreshing snapd affects snapE.
   836  	updates := []*snap.Info{snapdSnap}
   837  	affected, err := snapstate.AffectedByRefresh(st, updates)
   838  	c.Assert(err, IsNil)
   839  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   840  		"snap-g": {
   841  			Restart: true,
   842  			AffectingSnaps: map[string]bool{
   843  				"snapd": true,
   844  			}}})
   845  }
   846  
   847  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(c *C) {
   848  	restore := release.MockOnClassic(true)
   849  	defer restore()
   850  
   851  	st := s.state
   852  
   853  	st.Lock()
   854  	defer st.Unlock()
   855  
   856  	coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook)
   857  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   858  
   859  	c.Assert(s.repo.AddSnap(coreSnap), IsNil)
   860  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   861  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}}
   862  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   863  	c.Assert(err, IsNil)
   864  
   865  	// snapG has a plug using mount backend, refreshing core affects snapE.
   866  	updates := []*snap.Info{coreSnap}
   867  	affected, err := snapstate.AffectedByRefresh(st, updates)
   868  	c.Assert(err, IsNil)
   869  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   870  		"snap-g": {
   871  			Restart: true,
   872  			AffectingSnaps: map[string]bool{
   873  				"core": true,
   874  			}}})
   875  }
   876  
   877  func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) {
   878  	restore := release.MockOnClassic(false)
   879  	defer restore()
   880  
   881  	st := s.state
   882  
   883  	r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
   884  	defer r()
   885  
   886  	st.Lock()
   887  	defer st.Unlock()
   888  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
   889  	mockInstalledSnap(c, s.state, snapByaml, useHook)
   890  	mockInstalledSnap(c, s.state, snapDyaml, useHook)
   891  	mockInstalledSnap(c, s.state, snapEyaml, useHook)
   892  	core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook)
   893  
   894  	updates := []*snap.Info{core18}
   895  	affected, err := snapstate.AffectedByRefresh(st, updates)
   896  	c.Assert(err, IsNil)
   897  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   898  		"snap-a": {
   899  			Base:    false,
   900  			Restart: true,
   901  			AffectingSnaps: map[string]bool{
   902  				"core18": true,
   903  			},
   904  		},
   905  		"snap-b": {
   906  			Base:    false,
   907  			Restart: true,
   908  			AffectingSnaps: map[string]bool{
   909  				"core18": true,
   910  			},
   911  		},
   912  		"snap-d": {
   913  			Base:    false,
   914  			Restart: true,
   915  			AffectingSnaps: map[string]bool{
   916  				"core18": true,
   917  			},
   918  		},
   919  		"snap-e": {
   920  			Base:    false,
   921  			Restart: true,
   922  			AffectingSnaps: map[string]bool{
   923  				"core18": true,
   924  			}}})
   925  }
   926  
   927  func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) {
   928  	st := s.state
   929  	st.Lock()
   930  	defer st.Unlock()
   931  
   932  	affected := map[string]*snapstate.AffectedSnapInfo{
   933  		"snap-a": {
   934  			Base:    true,
   935  			Restart: true,
   936  			AffectingSnaps: map[string]bool{
   937  				"snap-c": true,
   938  				"snap-d": true,
   939  			},
   940  		},
   941  		"snap-b": {
   942  			AffectingSnaps: map[string]bool{
   943  				"snap-e": true,
   944  				"snap-f": true,
   945  			},
   946  		},
   947  	}
   948  
   949  	seenSnaps := make(map[string]bool)
   950  
   951  	ts := snapstate.CreateGateAutoRefreshHooks(st, affected)
   952  	c.Assert(ts.Tasks(), HasLen, 2)
   953  
   954  	checkHook := func(t *state.Task) {
   955  		c.Assert(t.Kind(), Equals, "run-hook")
   956  		var hs hookstate.HookSetup
   957  		c.Assert(t.Get("hook-setup", &hs), IsNil)
   958  		c.Check(hs.Hook, Equals, "gate-auto-refresh")
   959  		c.Check(hs.Optional, Equals, true)
   960  		seenSnaps[hs.Snap] = true
   961  
   962  		var data interface{}
   963  		c.Assert(t.Get("hook-context", &data), IsNil)
   964  
   965  		// the order of hook tasks is not deterministic
   966  		if hs.Snap == "snap-a" {
   967  			c.Check(data, DeepEquals, map[string]interface{}{
   968  				"base":            true,
   969  				"restart":         true,
   970  				"affecting-snaps": []interface{}{"snap-c", "snap-d"}})
   971  		} else {
   972  			c.Assert(hs.Snap, Equals, "snap-b")
   973  			c.Check(data, DeepEquals, map[string]interface{}{
   974  				"base":            false,
   975  				"restart":         false,
   976  				"affecting-snaps": []interface{}{"snap-e", "snap-f"}})
   977  		}
   978  	}
   979  
   980  	checkHook(ts.Tasks()[0])
   981  	checkHook(ts.Tasks()[1])
   982  
   983  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
   984  }
   985  
   986  func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) {
   987  	st := s.state
   988  	st.Lock()
   989  	defer st.Unlock()
   990  
   991  	st.Set("seeded", true)
   992  
   993  	restore := snapstatetest.MockDeviceModel(DefaultModel())
   994  	defer restore()
   995  
   996  	snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
   997  		return nil, nil
   998  	}
   999  	defer func() { snapstate.AutoAliases = nil }()
  1000  
  1001  	s.store.refreshedSnaps = []*snap.Info{{
  1002  		Architectures: []string{"all"},
  1003  		SnapType:      snap.TypeApp,
  1004  		SideInfo: snap.SideInfo{
  1005  			RealName: "snap-a",
  1006  			Revision: snap.R(8),
  1007  		},
  1008  	}}
  1009  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1010  
  1011  	// gate-auto-refresh-hook feature not enabled, expect old-style refresh.
  1012  	_, tss, err := snapstate.AutoRefresh(context.TODO(), st)
  1013  	c.Check(err, IsNil)
  1014  	c.Assert(tss, HasLen, 2)
  1015  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites")
  1016  	c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap")
  1017  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh")
  1018  
  1019  	// enable gate-auto-refresh-hook feature
  1020  	tr := config.NewTransaction(s.state)
  1021  	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
  1022  	tr.Commit()
  1023  
  1024  	_, tss, err = snapstate.AutoRefresh(context.TODO(), st)
  1025  	c.Check(err, IsNil)
  1026  	c.Assert(tss, HasLen, 2)
  1027  	// TODO: verify conditional-auto-refresh task data
  1028  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh")
  1029  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook")
  1030  }
  1031  
  1032  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) {
  1033  	s.store.refreshedSnaps = []*snap.Info{{
  1034  		Architectures: []string{"all"},
  1035  		SnapType:      snap.TypeApp,
  1036  		SideInfo: snap.SideInfo{
  1037  			RealName: "snap-a",
  1038  			Revision: snap.R(8),
  1039  		},
  1040  	}, {
  1041  		Architectures: []string{"all"},
  1042  		SnapType:      snap.TypeBase,
  1043  		SideInfo: snap.SideInfo{
  1044  			RealName: "base-snap-b",
  1045  			Revision: snap.R(3),
  1046  		},
  1047  	}, {
  1048  		Architectures: []string{"all"},
  1049  		SnapType:      snap.TypeApp,
  1050  		SideInfo: snap.SideInfo{
  1051  			RealName: "snap-c",
  1052  			Revision: snap.R(5),
  1053  		},
  1054  	}}
  1055  
  1056  	st := s.state
  1057  	st.Lock()
  1058  	defer st.Unlock()
  1059  
  1060  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1061  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1062  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1063  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1064  
  1065  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1066  	defer restore()
  1067  
  1068  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1069  	c.Assert(err, IsNil)
  1070  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"})
  1071  	c.Assert(tss, HasLen, 2)
  1072  
  1073  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1074  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1075  		"snap-a": {
  1076  			SnapSetup: snapstate.SnapSetup{
  1077  				Type:      "app",
  1078  				PlugsOnly: true,
  1079  				Flags: snapstate.Flags{
  1080  					IsAutoRefresh: true,
  1081  				},
  1082  				SideInfo: &snap.SideInfo{
  1083  					RealName: "snap-a",
  1084  					Revision: snap.R(8),
  1085  				},
  1086  				DownloadInfo: &snap.DownloadInfo{},
  1087  			},
  1088  		},
  1089  		"base-snap-b": {
  1090  			SnapSetup: snapstate.SnapSetup{
  1091  				Type:      "base",
  1092  				PlugsOnly: true,
  1093  				Flags: snapstate.Flags{
  1094  					IsAutoRefresh: true,
  1095  				},
  1096  				SideInfo: &snap.SideInfo{
  1097  					RealName: "base-snap-b",
  1098  					Revision: snap.R(3),
  1099  				},
  1100  				DownloadInfo: &snap.DownloadInfo{},
  1101  			},
  1102  		},
  1103  		"snap-c": {
  1104  			SnapSetup: snapstate.SnapSetup{
  1105  				Type:      "app",
  1106  				PlugsOnly: true,
  1107  				Flags: snapstate.Flags{
  1108  					IsAutoRefresh: true,
  1109  				},
  1110  				SideInfo: &snap.SideInfo{
  1111  					RealName: "snap-c",
  1112  					Revision: snap.R(5),
  1113  				},
  1114  				DownloadInfo: &snap.DownloadInfo{},
  1115  			},
  1116  		},
  1117  	})
  1118  
  1119  	c.Assert(tss[1].Tasks(), HasLen, 2)
  1120  
  1121  	// check hooks for affected snaps
  1122  	seenSnaps := make(map[string]bool)
  1123  	var hs hookstate.HookSetup
  1124  	c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil)
  1125  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1126  	seenSnaps[hs.Snap] = true
  1127  
  1128  	c.Assert(tss[1].Tasks()[1].Get("hook-setup", &hs), IsNil)
  1129  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1130  	seenSnaps[hs.Snap] = true
  1131  
  1132  	// hook for snap-a because it gets refreshed, for snap-b because its base
  1133  	// gets refreshed. snap-c is refreshed but doesn't have the hook.
  1134  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1135  
  1136  	// check that refresh-candidates in the state were updated
  1137  	var candidates map[string]*snapstate.RefreshCandidate
  1138  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1139  	c.Assert(candidates, HasLen, 3)
  1140  	c.Check(candidates["snap-a"], NotNil)
  1141  	c.Check(candidates["base-snap-b"], NotNil)
  1142  	c.Check(candidates["snap-c"], NotNil)
  1143  }
  1144  
  1145  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) {
  1146  	s.store.refreshedSnaps = []*snap.Info{{
  1147  		Architectures: []string{"all"},
  1148  		SnapType:      snap.TypeApp,
  1149  		SideInfo: snap.SideInfo{
  1150  			RealName: "snap-a",
  1151  			Revision: snap.R(8),
  1152  		},
  1153  	}, {
  1154  		Architectures: []string{"all"},
  1155  		SnapType:      snap.TypeBase,
  1156  		SideInfo: snap.SideInfo{
  1157  			RealName: "snap-c",
  1158  			Revision: snap.R(5),
  1159  		},
  1160  	}}
  1161  
  1162  	st := s.state
  1163  	st.Lock()
  1164  	defer st.Unlock()
  1165  
  1166  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1167  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1168  
  1169  	conflictChange := st.NewChange("conflicting change", "")
  1170  	conflictTask := st.NewTask("conflicting task", "")
  1171  	si := &snap.SideInfo{
  1172  		RealName: "snap-c",
  1173  		Revision: snap.R(1),
  1174  	}
  1175  	sup := snapstate.SnapSetup{SideInfo: si}
  1176  	conflictTask.Set("snap-setup", sup)
  1177  	conflictChange.AddTask(conflictTask)
  1178  
  1179  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1180  	defer restore()
  1181  
  1182  	logbuf, restoreLogger := logger.MockLogger()
  1183  	defer restoreLogger()
  1184  
  1185  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1186  	c.Assert(err, IsNil)
  1187  	c.Check(names, DeepEquals, []string{"snap-a"})
  1188  	c.Assert(tss, HasLen, 2)
  1189  
  1190  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1191  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1192  		"snap-a": {
  1193  			SnapSetup: snapstate.SnapSetup{
  1194  				Type:      "app",
  1195  				PlugsOnly: true,
  1196  				Flags: snapstate.Flags{
  1197  					IsAutoRefresh: true,
  1198  				},
  1199  				SideInfo: &snap.SideInfo{
  1200  					RealName: "snap-a",
  1201  					Revision: snap.R(8),
  1202  				},
  1203  				DownloadInfo: &snap.DownloadInfo{},
  1204  			}}})
  1205  
  1206  	c.Assert(tss[1].Tasks(), HasLen, 1)
  1207  
  1208  	c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`)
  1209  
  1210  	seenSnaps := make(map[string]bool)
  1211  	var hs hookstate.HookSetup
  1212  	c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil)
  1213  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1214  	seenSnaps[hs.Snap] = true
  1215  
  1216  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1217  
  1218  	// check that refresh-candidates in the state were updated
  1219  	var candidates map[string]*snapstate.RefreshCandidate
  1220  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1221  	c.Assert(candidates, HasLen, 2)
  1222  	c.Check(candidates["snap-a"], NotNil)
  1223  	c.Check(candidates["snap-c"], NotNil)
  1224  }
  1225  
  1226  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) {
  1227  	s.store.refreshedSnaps = []*snap.Info{{
  1228  		Architectures: []string{"all"},
  1229  		SnapType:      snap.TypeBase,
  1230  		SideInfo: snap.SideInfo{
  1231  			RealName: "base-snap-b",
  1232  			Revision: snap.R(3),
  1233  		},
  1234  	}, {
  1235  		Architectures: []string{"all"},
  1236  		SnapType:      snap.TypeBase,
  1237  		SideInfo: snap.SideInfo{
  1238  			RealName: "snap-c",
  1239  			Revision: snap.R(5),
  1240  		},
  1241  	}}
  1242  
  1243  	st := s.state
  1244  	st.Lock()
  1245  	defer st.Unlock()
  1246  
  1247  	mockInstalledSnap(c, s.state, snapByaml, noHook)
  1248  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1249  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1250  
  1251  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1252  	defer restore()
  1253  
  1254  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1255  	c.Assert(err, IsNil)
  1256  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"})
  1257  	c.Assert(tss, HasLen, 1)
  1258  
  1259  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1260  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh")
  1261  }
  1262  
  1263  func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) {
  1264  	info := &snap.Info{
  1265  		SuggestedName: name,
  1266  		SideInfo:      *si,
  1267  		Architectures: []string{"all"},
  1268  		SnapType:      snap.TypeApp,
  1269  		Epoch:         snap.Epoch{},
  1270  	}
  1271  	switch name {
  1272  	case "base-snap-b":
  1273  		info.SnapType = snap.TypeBase
  1274  	case "snap-a", "snap-b":
  1275  		info.Hooks = map[string]*snap.HookInfo{
  1276  			"gate-auto-refresh": {
  1277  				Name: "gate-auto-refresh",
  1278  				Snap: info,
  1279  			},
  1280  		}
  1281  		if name == "snap-b" {
  1282  			info.Base = "base-snap-b"
  1283  		}
  1284  	}
  1285  	return info, nil
  1286  }
  1287  
  1288  func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) {
  1289  	st := s.state
  1290  	st.Lock()
  1291  	defer st.Unlock()
  1292  
  1293  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1294  		c.Fatal("unexpected call to installSize")
  1295  		return 0, nil
  1296  	})
  1297  	defer restoreInstallSize()
  1298  
  1299  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1300  		fakeStore: s.fakeStore,
  1301  		refreshedSnaps: []*snap.Info{{
  1302  			Architectures: []string{"all"},
  1303  			SnapType:      snap.TypeApp,
  1304  			SideInfo: snap.SideInfo{
  1305  				RealName: "snap-a",
  1306  				Revision: snap.R(8),
  1307  			},
  1308  		}, {
  1309  			Architectures: []string{"all"},
  1310  			SnapType:      snap.TypeBase,
  1311  			SideInfo: snap.SideInfo{
  1312  				RealName: "base-snap-b",
  1313  				Revision: snap.R(3),
  1314  			},
  1315  		}}})
  1316  
  1317  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1318  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1319  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1320  
  1321  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1322  
  1323  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1324  	defer restore()
  1325  
  1326  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1327  	c.Assert(err, IsNil)
  1328  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1329  
  1330  	chg := s.state.NewChange("refresh", "...")
  1331  	for _, ts := range tss {
  1332  		chg.AddAll(ts)
  1333  	}
  1334  
  1335  	s.state.Unlock()
  1336  	defer s.se.Stop()
  1337  	s.settle(c)
  1338  	s.state.Lock()
  1339  
  1340  	c.Check(chg.Status(), Equals, state.DoneStatus)
  1341  	c.Check(chg.Err(), IsNil)
  1342  
  1343  	expected := []string{
  1344  		"conditional-auto-refresh",
  1345  		"run-hook [snap-a;gate-auto-refresh]",
  1346  		// snap-b hook is triggered because of base-snap-b refresh
  1347  		"run-hook [snap-b;gate-auto-refresh]",
  1348  		"prerequisites",
  1349  		"download-snap",
  1350  		"validate-snap",
  1351  		"mount-snap",
  1352  		"run-hook [base-snap-b;pre-refresh]",
  1353  		"stop-snap-services",
  1354  		"remove-aliases",
  1355  		"unlink-current-snap",
  1356  		"copy-snap-data",
  1357  		"setup-profiles",
  1358  		"link-snap",
  1359  		"auto-connect",
  1360  		"set-auto-aliases",
  1361  		"setup-aliases",
  1362  		"run-hook [base-snap-b;post-refresh]",
  1363  		"start-snap-services",
  1364  		"cleanup",
  1365  		"run-hook [base-snap-b;check-health]",
  1366  		"prerequisites",
  1367  		"download-snap",
  1368  		"validate-snap",
  1369  		"mount-snap",
  1370  		"run-hook [snap-a;pre-refresh]",
  1371  		"stop-snap-services",
  1372  		"remove-aliases",
  1373  		"unlink-current-snap",
  1374  		"copy-snap-data",
  1375  		"setup-profiles",
  1376  		"link-snap",
  1377  		"auto-connect",
  1378  		"set-auto-aliases",
  1379  		"setup-aliases",
  1380  		"run-hook [snap-a;post-refresh]",
  1381  		"start-snap-services",
  1382  		"cleanup",
  1383  		"run-hook [snap-a;configure]",
  1384  		"run-hook [snap-a;check-health]",
  1385  		"check-rerefresh",
  1386  	}
  1387  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1388  }
  1389  
  1390  func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) {
  1391  	st := s.state
  1392  	st.Lock()
  1393  	defer st.Unlock()
  1394  
  1395  	restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error {
  1396  		c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123))
  1397  		if fail {
  1398  			return &osutil.NotEnoughDiskSpaceError{}
  1399  		}
  1400  		return nil
  1401  	})
  1402  	defer restore()
  1403  
  1404  	var installSizeCalled bool
  1405  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1406  		installSizeCalled = true
  1407  		seen := map[string]bool{}
  1408  		for _, sn := range snaps {
  1409  			seen[sn.InstanceName()] = true
  1410  		}
  1411  		c.Check(seen, DeepEquals, map[string]bool{
  1412  			"base-snap-b": true,
  1413  			"snap-a":      true,
  1414  		})
  1415  		return 123, nil
  1416  	})
  1417  	defer restoreInstallSize()
  1418  
  1419  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  1420  	defer restoreModel()
  1421  
  1422  	tr := config.NewTransaction(s.state)
  1423  	tr.Set("core", "experimental.check-disk-space-refresh", true)
  1424  	tr.Commit()
  1425  
  1426  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1427  		fakeStore: s.fakeStore,
  1428  		refreshedSnaps: []*snap.Info{{
  1429  			Architectures: []string{"all"},
  1430  			SnapType:      snap.TypeApp,
  1431  			SideInfo: snap.SideInfo{
  1432  				RealName: "snap-a",
  1433  				Revision: snap.R(8),
  1434  			},
  1435  		}, {
  1436  			Architectures: []string{"all"},
  1437  			SnapType:      snap.TypeBase,
  1438  			SideInfo: snap.SideInfo{
  1439  				RealName: "base-snap-b",
  1440  				Revision: snap.R(3),
  1441  			},
  1442  		}}})
  1443  
  1444  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1445  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1446  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1447  
  1448  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1449  
  1450  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1451  	c.Assert(err, IsNil)
  1452  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1453  
  1454  	chg := s.state.NewChange("refresh", "...")
  1455  	for _, ts := range tss {
  1456  		chg.AddAll(ts)
  1457  	}
  1458  
  1459  	s.state.Unlock()
  1460  	defer s.se.Stop()
  1461  	s.settle(c)
  1462  	s.state.Lock()
  1463  
  1464  	c.Check(installSizeCalled, Equals, true)
  1465  	if fail {
  1466  		c.Check(chg.Status(), Equals, state.ErrorStatus)
  1467  		c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`)
  1468  	} else {
  1469  		c.Check(chg.Status(), Equals, state.DoneStatus)
  1470  		c.Check(chg.Err(), IsNil)
  1471  	}
  1472  }
  1473  
  1474  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) {
  1475  	fail := true
  1476  	s.testAutoRefreshPhase2DiskSpaceCheck(c, fail)
  1477  }
  1478  
  1479  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) {
  1480  	var nofail bool
  1481  	s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail)
  1482  }
  1483  
  1484  // XXX: this case is probably artificial; with proper conflict prevention
  1485  // we shouldn't get conflicts from doInstall in phase2.
  1486  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) {
  1487  	st := s.state
  1488  	st.Lock()
  1489  	defer st.Unlock()
  1490  
  1491  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1492  		fakeStore: s.fakeStore,
  1493  		refreshedSnaps: []*snap.Info{{
  1494  			Architectures: []string{"all"},
  1495  			SnapType:      snap.TypeApp,
  1496  			SideInfo: snap.SideInfo{
  1497  				RealName: "snap-a",
  1498  				Revision: snap.R(8),
  1499  			},
  1500  		}, {
  1501  			Architectures: []string{"all"},
  1502  			SnapType:      snap.TypeBase,
  1503  			SideInfo: snap.SideInfo{
  1504  				RealName: "base-snap-b",
  1505  				Revision: snap.R(3),
  1506  			},
  1507  		}}})
  1508  
  1509  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1510  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1511  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1512  
  1513  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1514  
  1515  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1516  	defer restore()
  1517  
  1518  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1519  	c.Assert(err, IsNil)
  1520  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1521  
  1522  	chg := s.state.NewChange("refresh", "...")
  1523  	for _, ts := range tss {
  1524  		chg.AddAll(ts)
  1525  	}
  1526  
  1527  	conflictChange := st.NewChange("conflicting change", "")
  1528  	conflictTask := st.NewTask("conflicting task", "")
  1529  	si := &snap.SideInfo{
  1530  		RealName: "snap-a",
  1531  		Revision: snap.R(1),
  1532  	}
  1533  	sup := snapstate.SnapSetup{SideInfo: si}
  1534  	conflictTask.Set("snap-setup", sup)
  1535  	conflictChange.AddTask(conflictTask)
  1536  	conflictTask.WaitFor(tss[0].Tasks()[0])
  1537  
  1538  	s.state.Unlock()
  1539  	defer s.se.Stop()
  1540  	s.settle(c)
  1541  	s.state.Lock()
  1542  
  1543  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  1544  	c.Check(chg.Err(), IsNil)
  1545  
  1546  	expected := []string{
  1547  		"conditional-auto-refresh",
  1548  		"run-hook [snap-a;gate-auto-refresh]",
  1549  		// snap-b hook is triggered because of base-snap-b refresh
  1550  		"run-hook [snap-b;gate-auto-refresh]",
  1551  		"prerequisites",
  1552  		"download-snap",
  1553  		"validate-snap",
  1554  		"mount-snap",
  1555  		"run-hook [base-snap-b;pre-refresh]",
  1556  		"stop-snap-services",
  1557  		"remove-aliases",
  1558  		"unlink-current-snap",
  1559  		"copy-snap-data",
  1560  		"setup-profiles",
  1561  		"link-snap",
  1562  		"auto-connect",
  1563  		"set-auto-aliases",
  1564  		"setup-aliases",
  1565  		"run-hook [base-snap-b;post-refresh]",
  1566  		"start-snap-services",
  1567  		"cleanup",
  1568  		"run-hook [base-snap-b;check-health]",
  1569  		"check-rerefresh",
  1570  	}
  1571  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1572  }
  1573  
  1574  func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) {
  1575  	st := s.state
  1576  	st.Lock()
  1577  	defer st.Unlock()
  1578  
  1579  	restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) {
  1580  		c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh")
  1581  		var candidates map[string]*snapstate.RefreshCandidate
  1582  		c.Assert(gatingTask.Get("snaps", &candidates), IsNil)
  1583  		seenSnaps := make(map[string]bool)
  1584  		var filteredByGatingHooks []*snapstate.RefreshCandidate
  1585  		for _, cand := range candidates {
  1586  			seenSnaps[cand.InstanceName()] = true
  1587  			if cand.InstanceName() == "snap-a" {
  1588  				continue
  1589  			}
  1590  			filteredByGatingHooks = append(filteredByGatingHooks, cand)
  1591  		}
  1592  		c.Check(seenSnaps, DeepEquals, map[string]bool{
  1593  			"snap-a":      true,
  1594  			"base-snap-b": true,
  1595  		})
  1596  		return filteredByGatingHooks, nil
  1597  	})
  1598  	defer restore()
  1599  
  1600  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1601  		fakeStore: s.fakeStore,
  1602  		refreshedSnaps: []*snap.Info{
  1603  			{
  1604  				Architectures: []string{"all"},
  1605  				SnapType:      snap.TypeApp,
  1606  				SideInfo: snap.SideInfo{
  1607  					RealName: "snap-a",
  1608  					Revision: snap.R(8),
  1609  				},
  1610  			}, {
  1611  				Architectures: []string{"all"},
  1612  				SnapType:      snap.TypeBase,
  1613  				SideInfo: snap.SideInfo{
  1614  					RealName: "base-snap-b",
  1615  					Revision: snap.R(3),
  1616  				},
  1617  			},
  1618  		}})
  1619  
  1620  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1621  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1622  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1623  
  1624  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1625  
  1626  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  1627  	defer restoreModel()
  1628  
  1629  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1630  	c.Assert(err, IsNil)
  1631  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1632  
  1633  	chg := s.state.NewChange("refresh", "...")
  1634  	for _, ts := range tss {
  1635  		chg.AddAll(ts)
  1636  	}
  1637  
  1638  	s.state.Unlock()
  1639  	defer s.se.Stop()
  1640  	s.settle(c)
  1641  	s.state.Lock()
  1642  
  1643  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  1644  	c.Check(chg.Err(), IsNil)
  1645  
  1646  	expected := []string{
  1647  		"conditional-auto-refresh",
  1648  		"run-hook [snap-a;gate-auto-refresh]",
  1649  		// snap-b hook is triggered because of base-snap-b refresh
  1650  		"run-hook [snap-b;gate-auto-refresh]",
  1651  		"prerequisites",
  1652  		"download-snap",
  1653  		"validate-snap",
  1654  		"mount-snap",
  1655  		"run-hook [base-snap-b;pre-refresh]",
  1656  		"stop-snap-services",
  1657  		"remove-aliases",
  1658  		"unlink-current-snap",
  1659  		"copy-snap-data",
  1660  		"setup-profiles",
  1661  		"link-snap",
  1662  		"auto-connect",
  1663  		"set-auto-aliases",
  1664  		"setup-aliases",
  1665  		"run-hook [base-snap-b;post-refresh]",
  1666  		"start-snap-services",
  1667  		"cleanup",
  1668  		"run-hook [base-snap-b;check-health]",
  1669  		"check-rerefresh",
  1670  	}
  1671  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1672  }
  1673  
  1674  func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) {
  1675  	for i, t := range tasks {
  1676  		var got string
  1677  		if t.Kind() == "run-hook" {
  1678  			var hsup hookstate.HookSetup
  1679  			c.Assert(t.Get("hook-setup", &hsup), IsNil)
  1680  			got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook)
  1681  		} else {
  1682  			got = t.Kind()
  1683  		}
  1684  		c.Assert(got, Equals, expected[i])
  1685  	}
  1686  }