github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/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  	"gopkg.in/tomb.v2"
    32  
    33  	"github.com/snapcore/snapd/dirs"
    34  	"github.com/snapcore/snapd/interfaces"
    35  	"github.com/snapcore/snapd/interfaces/builtin"
    36  	"github.com/snapcore/snapd/logger"
    37  	"github.com/snapcore/snapd/osutil"
    38  	"github.com/snapcore/snapd/overlord/auth"
    39  	"github.com/snapcore/snapd/overlord/configstate/config"
    40  	"github.com/snapcore/snapd/overlord/hookstate"
    41  	"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
    42  	"github.com/snapcore/snapd/overlord/snapstate"
    43  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    44  	"github.com/snapcore/snapd/overlord/state"
    45  	"github.com/snapcore/snapd/release"
    46  	"github.com/snapcore/snapd/snap"
    47  	"github.com/snapcore/snapd/snap/snaptest"
    48  	"github.com/snapcore/snapd/store"
    49  	"github.com/snapcore/snapd/testutil"
    50  )
    51  
    52  type autoRefreshGatingStore struct {
    53  	*fakeStore
    54  	refreshedSnaps []*snap.Info
    55  }
    56  
    57  type autorefreshGatingSuite struct {
    58  	testutil.BaseTest
    59  	state *state.State
    60  	repo  *interfaces.Repository
    61  	store *autoRefreshGatingStore
    62  }
    63  
    64  var _ = Suite(&autorefreshGatingSuite{})
    65  
    66  func (s *autorefreshGatingSuite) SetUpTest(c *C) {
    67  	s.BaseTest.SetUpTest(c)
    68  	dirs.SetRootDir(c.MkDir())
    69  	s.AddCleanup(func() {
    70  		dirs.SetRootDir("/")
    71  	})
    72  	s.state = state.New(nil)
    73  
    74  	s.repo = interfaces.NewRepository()
    75  	for _, iface := range builtin.Interfaces() {
    76  		c.Assert(s.repo.AddInterface(iface), IsNil)
    77  	}
    78  
    79  	s.state.Lock()
    80  	defer s.state.Unlock()
    81  	ifacerepo.Replace(s.state, s.repo)
    82  
    83  	s.store = &autoRefreshGatingStore{fakeStore: &fakeStore{}}
    84  	snapstate.ReplaceStore(s.state, s.store)
    85  	s.state.Set("refresh-privacy-key", "privacy-key")
    86  }
    87  
    88  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) {
    89  	if assertQuery != nil {
    90  		panic("no assertion query support")
    91  	}
    92  	if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 {
    93  		panic("expected in test one action for each current snaps, and at least one snap")
    94  	}
    95  	for _, a := range actions {
    96  		if a.Action != "refresh" {
    97  			panic("expected refresh actions")
    98  		}
    99  	}
   100  
   101  	res := []store.SnapActionResult{}
   102  	for _, rs := range r.refreshedSnaps {
   103  		res = append(res, store.SnapActionResult{Info: rs})
   104  	}
   105  
   106  	return res, nil, nil
   107  }
   108  
   109  func mockInstalledSnap(c *C, st *state.State, snapYaml string, hasHook bool) *snap.Info {
   110  	snapInfo := snaptest.MockSnap(c, string(snapYaml), &snap.SideInfo{
   111  		Revision: snap.R(1),
   112  	})
   113  
   114  	snapName := snapInfo.SnapName()
   115  	si := &snap.SideInfo{RealName: snapName, SnapID: "id", Revision: snap.R(1)}
   116  	snapstate.Set(st, snapName, &snapstate.SnapState{
   117  		Active:   true,
   118  		Sequence: []*snap.SideInfo{si},
   119  		Current:  si.Revision,
   120  		SnapType: string(snapInfo.Type()),
   121  	})
   122  
   123  	if hasHook {
   124  		c.Assert(os.MkdirAll(snapInfo.HooksDir(), 0775), IsNil)
   125  		err := ioutil.WriteFile(filepath.Join(snapInfo.HooksDir(), "gate-auto-refresh"), nil, 0755)
   126  		c.Assert(err, IsNil)
   127  	}
   128  	return snapInfo
   129  }
   130  
   131  func mockLastRefreshed(c *C, st *state.State, refreshedTime string, snaps ...string) {
   132  	refreshed, err := time.Parse(time.RFC3339, refreshedTime)
   133  	c.Assert(err, IsNil)
   134  	for _, snapName := range snaps {
   135  		var snapst snapstate.SnapState
   136  		c.Assert(snapstate.Get(st, snapName, &snapst), IsNil)
   137  		snapst.LastRefreshTime = &refreshed
   138  		snapstate.Set(st, snapName, &snapst)
   139  	}
   140  }
   141  
   142  const baseSnapAyaml = `name: base-snap-a
   143  type: base
   144  `
   145  
   146  const snapAyaml = `name: snap-a
   147  type: app
   148  base: base-snap-a
   149  `
   150  
   151  const baseSnapByaml = `name: base-snap-b
   152  type: base
   153  `
   154  
   155  const snapByaml = `name: snap-b
   156  type: app
   157  base: base-snap-b
   158  version: 1
   159  `
   160  
   161  const snapBByaml = `name: snap-bb
   162  type: app
   163  base: base-snap-b
   164  version: 1
   165  `
   166  
   167  const kernelYaml = `name: kernel
   168  type: kernel
   169  version: 1
   170  `
   171  
   172  const gadget1Yaml = `name: gadget
   173  type: gadget
   174  version: 1
   175  `
   176  
   177  const snapCyaml = `name: snap-c
   178  type: app
   179  version: 1
   180  `
   181  
   182  const snapDyaml = `name: snap-d
   183  type: app
   184  version: 1
   185  slots:
   186      slot: desktop
   187  `
   188  
   189  const snapEyaml = `name: snap-e
   190  type: app
   191  version: 1
   192  base: other-base
   193  plugs:
   194      plug: desktop
   195  `
   196  
   197  const snapFyaml = `name: snap-f
   198  type: app
   199  version: 1
   200  plugs:
   201      plug: desktop
   202  `
   203  
   204  const snapGyaml = `name: snap-g
   205  type: app
   206  version: 1
   207  base: other-base
   208  plugs:
   209      desktop:
   210      mir:
   211  `
   212  
   213  const coreYaml = `name: core
   214  type: os
   215  version: 1
   216  slots:
   217      desktop:
   218      mir:
   219  `
   220  
   221  const core18Yaml = `name: core18
   222  type: os
   223  version: 1
   224  `
   225  
   226  const snapdYaml = `name: snapd
   227  version: 1
   228  type: snapd
   229  slots:
   230      desktop:
   231  `
   232  
   233  func (s *autorefreshGatingSuite) TestHoldDurationLeft(c *C) {
   234  	now, err := time.Parse(time.RFC3339, "2021-06-03T10:00:00Z")
   235  	c.Assert(err, IsNil)
   236  	maxPostponement := time.Hour * 24 * 90
   237  
   238  	for i, tc := range []struct {
   239  		lastRefresh, firstHeld string
   240  		maxDuration            string
   241  		expected               string
   242  	}{
   243  		{
   244  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   245  			"2021-06-03T10:00:00Z", // first held now
   246  			"48h",                  // max duration
   247  			"48h",                  // expected
   248  		},
   249  		{
   250  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   251  			"2021-06-02T10:00:00Z", // first held (1 day ago)
   252  			"48h",                  // max duration
   253  			"24h",                  // expected
   254  		},
   255  		{
   256  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   257  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   258  			"48h",                  // max duration
   259  			"00h",                  // expected
   260  		},
   261  		{
   262  			"2021-03-08T10:00:00Z", // last refreshed (almost 3 months ago)
   263  			"2021-06-01T10:00:00Z", // first held
   264  			"2160h",                // max duration (90 days)
   265  			"72h",                  // expected
   266  		},
   267  		{
   268  			"2021-03-04T10:00:00Z", // last refreshed
   269  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   270  			"2160h",                // max duration (90 days)
   271  			"-24h",                 // expected (refresh is 1 day overdue)
   272  		},
   273  		{
   274  			"2021-06-01T10:00:00Z", // last refreshed (2 days ago)
   275  			"2021-06-03T10:00:00Z", // first held now
   276  			"2160h",                // max duration (90 days)
   277  			"2112h",                // expected (max minus 2 days)
   278  		},
   279  	} {
   280  		lastRefresh, err := time.Parse(time.RFC3339, tc.lastRefresh)
   281  		c.Assert(err, IsNil)
   282  		firstHeld, err := time.Parse(time.RFC3339, tc.firstHeld)
   283  		c.Assert(err, IsNil)
   284  		maxDuration, err := time.ParseDuration(tc.maxDuration)
   285  		c.Assert(err, IsNil)
   286  		expected, err := time.ParseDuration(tc.expected)
   287  		c.Assert(err, IsNil)
   288  
   289  		left := snapstate.HoldDurationLeft(now, lastRefresh, firstHeld, maxDuration, maxPostponement)
   290  		c.Check(left, Equals, expected, Commentf("case #%d", i))
   291  	}
   292  }
   293  
   294  func (s *autorefreshGatingSuite) TestLastRefreshedHelper(c *C) {
   295  	st := s.state
   296  	st.Lock()
   297  	defer st.Unlock()
   298  
   299  	inf := mockInstalledSnap(c, st, snapAyaml, false)
   300  	stat, err := os.Stat(inf.MountFile())
   301  	c.Assert(err, IsNil)
   302  
   303  	refreshed, err := snapstate.LastRefreshed(st, "snap-a")
   304  	c.Assert(err, IsNil)
   305  	c.Check(refreshed, DeepEquals, stat.ModTime())
   306  
   307  	t, err := time.Parse(time.RFC3339, "2021-01-01T10:00:00Z")
   308  	c.Assert(err, IsNil)
   309  
   310  	var snapst snapstate.SnapState
   311  	c.Assert(snapstate.Get(st, "snap-a", &snapst), IsNil)
   312  	snapst.LastRefreshTime = &t
   313  	snapstate.Set(st, "snap-a", &snapst)
   314  
   315  	refreshed, err = snapstate.LastRefreshed(st, "snap-a")
   316  	c.Assert(err, IsNil)
   317  	c.Check(refreshed, DeepEquals, t)
   318  }
   319  
   320  func (s *autorefreshGatingSuite) TestHoldRefreshHelper(c *C) {
   321  	st := s.state
   322  	st.Lock()
   323  	defer st.Unlock()
   324  
   325  	restore := snapstate.MockTimeNow(func() time.Time {
   326  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   327  		c.Assert(err, IsNil)
   328  		return t
   329  	})
   330  	defer restore()
   331  
   332  	mockInstalledSnap(c, st, snapAyaml, false)
   333  	mockInstalledSnap(c, st, snapByaml, false)
   334  	mockInstalledSnap(c, st, snapCyaml, false)
   335  	mockInstalledSnap(c, st, snapDyaml, false)
   336  	mockInstalledSnap(c, st, snapEyaml, false)
   337  	mockInstalledSnap(c, st, snapFyaml, false)
   338  
   339  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f")
   340  
   341  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   342  	// this could be merged with the above HoldRefresh call, but it's fine if
   343  	// done separately too.
   344  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-e"), IsNil)
   345  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-e"), IsNil)
   346  	c.Assert(snapstate.HoldRefresh(st, "snap-f", 0, "snap-f"), IsNil)
   347  
   348  	var gating map[string]map[string]*snapstate.HoldState
   349  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   350  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   351  		"snap-b": {
   352  			// holding of other snaps for maxOtherHoldDuration (48h)
   353  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   354  		},
   355  		"snap-c": {
   356  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   357  		},
   358  		"snap-e": {
   359  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   360  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   361  		},
   362  		"snap-f": {
   363  			// holding self set for maxPostponement minus 1 day due to last refresh.
   364  			"snap-f": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-07T10:00:00Z"),
   365  		},
   366  	})
   367  }
   368  
   369  func (s *autorefreshGatingSuite) TestHoldRefreshHelperMultipleTimes(c *C) {
   370  	st := s.state
   371  	st.Lock()
   372  	defer st.Unlock()
   373  
   374  	lastRefreshed := "2021-05-09T10:00:00Z"
   375  	now := "2021-05-10T10:00:00Z"
   376  	restore := snapstate.MockTimeNow(func() time.Time {
   377  		t, err := time.Parse(time.RFC3339, now)
   378  		c.Assert(err, IsNil)
   379  		return t
   380  	})
   381  	defer restore()
   382  
   383  	mockInstalledSnap(c, st, snapAyaml, false)
   384  	mockInstalledSnap(c, st, snapByaml, false)
   385  	// snap-a was last refreshed yesterday
   386  	mockLastRefreshed(c, st, lastRefreshed, "snap-a")
   387  
   388  	// hold it for just a bit (10h) initially
   389  	hold := time.Hour * 10
   390  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   391  	var gating map[string]map[string]*snapstate.HoldState
   392  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   393  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   394  		"snap-a": {
   395  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T20:00:00Z"),
   396  		},
   397  	})
   398  
   399  	// holding for a shorter time is fine too
   400  	hold = time.Hour * 5
   401  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   402  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   403  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   404  		"snap-a": {
   405  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T15:00:00Z"),
   406  		},
   407  	})
   408  
   409  	oldNow := now
   410  
   411  	// a refresh on next day
   412  	now = "2021-05-11T08:00:00Z"
   413  
   414  	// default hold time requested
   415  	hold = 0
   416  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   417  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   418  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   419  		"snap-a": {
   420  			// maximum for holding other snaps, but taking into consideration
   421  			// firstHeld time = "2021-05-10T10:00:00".
   422  			"snap-b": snapstate.MockHoldState(oldNow, "2021-05-12T10:00:00Z"),
   423  		},
   424  	})
   425  }
   426  
   427  func (s *autorefreshGatingSuite) TestHoldRefreshHelperCloseToMaxPostponement(c *C) {
   428  	st := s.state
   429  	st.Lock()
   430  	defer st.Unlock()
   431  
   432  	lastRefreshedStr := "2021-01-01T10:00:00Z"
   433  	lastRefreshed, err := time.Parse(time.RFC3339, lastRefreshedStr)
   434  	c.Assert(err, IsNil)
   435  	// we are 1 day before maxPostponent
   436  	now := lastRefreshed.Add(89 * time.Hour * 24)
   437  
   438  	restore := snapstate.MockTimeNow(func() time.Time { return now })
   439  	defer restore()
   440  
   441  	mockInstalledSnap(c, st, snapAyaml, false)
   442  	mockInstalledSnap(c, st, snapByaml, false)
   443  	mockLastRefreshed(c, st, lastRefreshedStr, "snap-a")
   444  
   445  	// request default hold time
   446  	var hold time.Duration
   447  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   448  
   449  	var gating map[string]map[string]*snapstate.HoldState
   450  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   451  	c.Assert(gating, HasLen, 1)
   452  	c.Check(gating["snap-a"]["snap-b"].HoldUntil.String(), DeepEquals, lastRefreshed.Add(90*time.Hour*24).String())
   453  }
   454  
   455  func (s *autorefreshGatingSuite) TestHoldRefreshExplicitHoldTime(c *C) {
   456  	st := s.state
   457  	st.Lock()
   458  	defer st.Unlock()
   459  
   460  	now := "2021-05-10T10:00:00Z"
   461  	restore := snapstate.MockTimeNow(func() time.Time {
   462  		t, err := time.Parse(time.RFC3339, now)
   463  		c.Assert(err, IsNil)
   464  		return t
   465  	})
   466  	defer restore()
   467  
   468  	mockInstalledSnap(c, st, snapAyaml, false)
   469  	mockInstalledSnap(c, st, snapByaml, false)
   470  
   471  	hold := time.Hour * 24 * 3
   472  	// holding self for 3 days
   473  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), IsNil)
   474  
   475  	// snap-b holds snap-a for 1 day
   476  	hold = time.Hour * 24
   477  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   478  
   479  	var gating map[string]map[string]*snapstate.HoldState
   480  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   481  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   482  		"snap-a": {
   483  			"snap-a": snapstate.MockHoldState(now, "2021-05-13T10:00:00Z"),
   484  			"snap-b": snapstate.MockHoldState(now, "2021-05-11T10:00:00Z"),
   485  		},
   486  	})
   487  }
   488  
   489  func (s *autorefreshGatingSuite) TestHoldRefreshHelperErrors(c *C) {
   490  	st := s.state
   491  	st.Lock()
   492  	defer st.Unlock()
   493  
   494  	now := "2021-05-10T10:00:00Z"
   495  	restore := snapstate.MockTimeNow(func() time.Time {
   496  		t, err := time.Parse(time.RFC3339, now)
   497  		c.Assert(err, IsNil)
   498  		return t
   499  	})
   500  	defer restore()
   501  
   502  	mockInstalledSnap(c, st, snapAyaml, false)
   503  	mockInstalledSnap(c, st, snapByaml, false)
   504  	// snap-b was refreshed a few days ago
   505  	mockLastRefreshed(c, st, "2021-05-01T10:00:00Z", "snap-b")
   506  
   507  	// holding itself
   508  	hold := time.Hour * 24 * 96
   509  	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`)
   510  
   511  	// holding other snap
   512  	hold = time.Hour * 49
   513  	err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-b")
   514  	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`)
   515  	herr, ok := err.(*snapstate.HoldError)
   516  	c.Assert(ok, Equals, true)
   517  	c.Check(herr.SnapsInError, DeepEquals, map[string]snapstate.HoldDurationError{
   518  		"snap-b": {
   519  			Err:          fmt.Errorf(`requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`),
   520  			DurationLeft: 48 * time.Hour,
   521  		},
   522  	})
   523  
   524  	// hold for maximum allowed for other snaps
   525  	hold = time.Hour * 48
   526  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), IsNil)
   527  	// 2 days passed since it was first held
   528  	now = "2021-05-12T10:00:00Z"
   529  	hold = time.Minute * 2
   530  	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`)
   531  
   532  	// refreshed long time ago (> maxPostponement)
   533  	mockLastRefreshed(c, st, "2021-01-01T10:00:00Z", "snap-b")
   534  	hold = time.Hour * 2
   535  	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`)
   536  	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`)
   537  }
   538  
   539  func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) {
   540  	st := s.state
   541  	st.Lock()
   542  	defer st.Unlock()
   543  
   544  	mockInstalledSnap(c, st, snapAyaml, false)
   545  	mockInstalledSnap(c, st, snapByaml, false)
   546  	mockInstalledSnap(c, st, snapCyaml, false)
   547  	mockInstalledSnap(c, st, snapDyaml, false)
   548  
   549  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-b", "snap-c", "snap-d")
   550  
   551  	restore := snapstate.MockTimeNow(func() time.Time {
   552  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   553  		c.Assert(err, IsNil)
   554  		return t
   555  	})
   556  	defer restore()
   557  
   558  	// nothing is held initially
   559  	held, err := snapstate.HeldSnaps(st)
   560  	c.Assert(err, IsNil)
   561  	c.Check(held, IsNil)
   562  
   563  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   564  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil)
   565  	// holding self
   566  	c.Assert(snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d"), IsNil)
   567  
   568  	held, err = snapstate.HeldSnaps(st)
   569  	c.Assert(err, IsNil)
   570  	c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true})
   571  
   572  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-a"), IsNil)
   573  
   574  	held, err = snapstate.HeldSnaps(st)
   575  	c.Assert(err, IsNil)
   576  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-d": true})
   577  
   578  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-d"), IsNil)
   579  	held, err = snapstate.HeldSnaps(st)
   580  	c.Assert(err, IsNil)
   581  	c.Check(held, IsNil)
   582  }
   583  
   584  // Test that if all snaps cannot be held anymore, we don't hold only some of them
   585  // e.g. is a snap and its base snap have updates and the snap wants to hold (itself
   586  // and the base) but the base cannot be held, it doesn't make sense to refresh the
   587  // base but hold the affected snap.
   588  func (s *autorefreshGatingSuite) TestDontHoldSomeSnapsIfSomeFail(c *C) {
   589  	st := s.state
   590  	st.Lock()
   591  	defer st.Unlock()
   592  
   593  	// snap-b and snap-bb have base-snap-b base
   594  	mockInstalledSnap(c, st, snapByaml, useHook)
   595  	mockInstalledSnap(c, st, snapBByaml, useHook)
   596  	mockInstalledSnap(c, st, baseSnapByaml, noHook)
   597  
   598  	mockInstalledSnap(c, st, snapCyaml, useHook)
   599  	mockInstalledSnap(c, st, snapDyaml, useHook)
   600  
   601  	now := "2021-05-01T10:00:00Z"
   602  	restore := snapstate.MockTimeNow(func() time.Time {
   603  		t, err := time.Parse(time.RFC3339, now)
   604  		c.Assert(err, IsNil)
   605  		return t
   606  	})
   607  	defer restore()
   608  
   609  	// snap-b, base-snap-b get refreshed and affect snap-b (gating snap)
   610  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b"), IsNil)
   611  	// unrealted snap-d gets refreshed and holds itself
   612  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d"), IsNil)
   613  
   614  	// advance time by 49h
   615  	now = "2021-05-03T11:00:00Z"
   616  	// snap-b, base-snap-b and snap-c get refreshed and snap-a (gating snap) wants to hold them
   617  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b", "snap-c"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "base-snap-b" anymore, maximum refresh postponement exceeded`)
   618  	// snap-bb (gating snap) wants to hold base-snap-b as well and succeeds since it didn't exceed its holding time yet
   619  	c.Assert(snapstate.HoldRefresh(st, "snap-bb", 0, "base-snap-b"), IsNil)
   620  
   621  	held, err := snapstate.HeldSnaps(st)
   622  	c.Assert(err, IsNil)
   623  	// note, snap-b couldn't hold base-snap-b anymore so we didn't hold snap-b
   624  	// and snap-c. base-snap-b was held by snap-bb.
   625  	c.Check(held, DeepEquals, map[string]bool{
   626  		"snap-d":      true,
   627  		"base-snap-b": true,
   628  	})
   629  }
   630  
   631  func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) {
   632  	st := s.state
   633  	st.Lock()
   634  	defer st.Unlock()
   635  
   636  	restore := snapstate.MockTimeNow(func() time.Time {
   637  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   638  		c.Assert(err, IsNil)
   639  		return t
   640  	})
   641  	defer restore()
   642  
   643  	mockInstalledSnap(c, st, snapAyaml, false)
   644  	mockInstalledSnap(c, st, snapByaml, false)
   645  	mockInstalledSnap(c, st, snapCyaml, false)
   646  	mockInstalledSnap(c, st, snapDyaml, false)
   647  
   648  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   649  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   650  	// sanity
   651  	held, err := snapstate.HeldSnaps(st)
   652  	c.Assert(err, IsNil)
   653  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true})
   654  
   655  	candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}}
   656  
   657  	// only snap-c has a refresh candidate, snap-b and snap-d should be forgotten.
   658  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   659  	var gating map[string]map[string]*snapstate.HoldState
   660  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   661  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   662  		"snap-c": {
   663  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   664  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   665  		},
   666  	})
   667  	held, err = snapstate.HeldSnaps(st)
   668  	c.Assert(err, IsNil)
   669  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true})
   670  }
   671  
   672  func (s *autorefreshGatingSuite) TestPruneGatingHelperNoGating(c *C) {
   673  	st := s.state
   674  	st.Lock()
   675  	defer st.Unlock()
   676  
   677  	restore := snapstate.MockTimeNow(func() time.Time {
   678  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   679  		c.Assert(err, IsNil)
   680  		return t
   681  	})
   682  	defer restore()
   683  
   684  	mockInstalledSnap(c, st, snapAyaml, false)
   685  
   686  	held, err := snapstate.HeldSnaps(st)
   687  	c.Assert(err, IsNil)
   688  	c.Check(held, HasLen, 0)
   689  
   690  	snapstate.MockTimeNow(func() time.Time {
   691  		c.Fatalf("not expected")
   692  		return time.Time{}
   693  	})
   694  
   695  	candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}}
   696  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   697  	held, err = snapstate.HeldSnaps(st)
   698  	c.Assert(err, IsNil)
   699  	c.Check(held, HasLen, 0)
   700  }
   701  
   702  func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) {
   703  	st := s.state
   704  	st.Lock()
   705  	defer st.Unlock()
   706  
   707  	restore := snapstate.MockTimeNow(func() time.Time {
   708  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   709  		c.Assert(err, IsNil)
   710  		return t
   711  	})
   712  	defer restore()
   713  
   714  	mockInstalledSnap(c, st, snapAyaml, false)
   715  	mockInstalledSnap(c, st, snapByaml, false)
   716  	mockInstalledSnap(c, st, snapCyaml, false)
   717  	mockInstalledSnap(c, st, snapDyaml, false)
   718  
   719  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   720  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   721  
   722  	c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil)
   723  	var gating map[string]map[string]*snapstate.HoldState
   724  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   725  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   726  		"snap-d": {
   727  			// holding self set for maxPostponement (95 days - buffer = 90 days)
   728  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"),
   729  		},
   730  	})
   731  
   732  	held, err := snapstate.HeldSnaps(st)
   733  	c.Assert(err, IsNil)
   734  	c.Check(held, DeepEquals, map[string]bool{"snap-d": true})
   735  }
   736  
   737  func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) {
   738  	st := s.state
   739  	st.Lock()
   740  	defer st.Unlock()
   741  
   742  	mockInstalledSnap(c, st, snapAyaml, false)
   743  	mockInstalledSnap(c, st, snapByaml, false)
   744  	mockInstalledSnap(c, st, snapCyaml, false)
   745  	mockInstalledSnap(c, st, snapDyaml, false)
   746  
   747  	// snap-a is holding itself and 3 other snaps
   748  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-d"), IsNil)
   749  	// in addition, snap-c is held by snap-d.
   750  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil)
   751  
   752  	// sanity check
   753  	held, err := snapstate.HeldSnaps(st)
   754  	c.Assert(err, IsNil)
   755  	c.Check(held, DeepEquals, map[string]bool{
   756  		"snap-a": true,
   757  		"snap-b": true,
   758  		"snap-c": true,
   759  		"snap-d": true,
   760  	})
   761  
   762  	c.Check(snapstate.PruneSnapsHold(st, "snap-a"), IsNil)
   763  
   764  	// after pruning snap-a, snap-c is still held.
   765  	held, err = snapstate.HeldSnaps(st)
   766  	c.Assert(err, IsNil)
   767  	c.Check(held, DeepEquals, map[string]bool{
   768  		"snap-c": true,
   769  	})
   770  	var gating map[string]map[string]*snapstate.HoldState
   771  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   772  	c.Assert(gating, HasLen, 1)
   773  	c.Check(gating["snap-c"], HasLen, 1)
   774  	c.Check(gating["snap-c"]["snap-d"], NotNil)
   775  }
   776  
   777  const useHook = true
   778  const noHook = false
   779  
   780  func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) {
   781  	c.Assert(task.Kind(), Equals, "conditional-auto-refresh")
   782  	var snaps map[string]*snapstate.RefreshCandidate
   783  	c.Assert(task.Get("snaps", &snaps), IsNil)
   784  	c.Check(snaps, DeepEquals, expected)
   785  }
   786  
   787  func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) {
   788  	restore := release.MockOnClassic(true)
   789  	defer restore()
   790  
   791  	st := s.state
   792  
   793  	st.Lock()
   794  	defer st.Unlock()
   795  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
   796  	baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook)
   797  	// unrelated snaps
   798  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   799  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
   800  
   801  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   802  
   803  	updates := []string{baseSnapA.InstanceName()}
   804  	affected, err := snapstate.AffectedByRefresh(st, updates)
   805  	c.Assert(err, IsNil)
   806  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   807  		"snap-a": {
   808  			Base: true,
   809  			AffectingSnaps: map[string]bool{
   810  				"base-snap-a": true,
   811  			}}})
   812  }
   813  
   814  func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) {
   815  	restore := release.MockOnClassic(true)
   816  	defer restore()
   817  
   818  	st := s.state
   819  
   820  	st.Lock()
   821  	defer st.Unlock()
   822  	snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook)
   823  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   824  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   825  
   826  	c.Assert(s.repo.AddSnap(core), IsNil)
   827  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   828  	c.Assert(s.repo.AddSnap(snapC), IsNil)
   829  
   830  	updates := []string{core.InstanceName()}
   831  	affected, err := snapstate.AffectedByRefresh(st, updates)
   832  	c.Assert(err, IsNil)
   833  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   834  		"snap-c": {
   835  			Base: true,
   836  			AffectingSnaps: map[string]bool{
   837  				"core": true,
   838  			}}})
   839  }
   840  
   841  func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) {
   842  	restore := release.MockOnClassic(true)
   843  	defer restore()
   844  
   845  	st := s.state
   846  
   847  	st.Lock()
   848  	defer st.Unlock()
   849  	kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook)
   850  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   851  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   852  
   853  	updates := []string{kernel.InstanceName()}
   854  	affected, err := snapstate.AffectedByRefresh(st, updates)
   855  	c.Assert(err, IsNil)
   856  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   857  		"snap-c": {
   858  			Restart: true,
   859  			AffectingSnaps: map[string]bool{
   860  				"kernel": true,
   861  			}}})
   862  }
   863  
   864  func (s *autorefreshGatingSuite) TestAffectedBySelf(c *C) {
   865  	restore := release.MockOnClassic(true)
   866  	defer restore()
   867  
   868  	st := s.state
   869  
   870  	st.Lock()
   871  	defer st.Unlock()
   872  
   873  	snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook)
   874  	updates := []string{snapC.InstanceName()}
   875  	affected, err := snapstate.AffectedByRefresh(st, updates)
   876  	c.Assert(err, IsNil)
   877  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   878  		"snap-c": {
   879  			AffectingSnaps: map[string]bool{
   880  				"snap-c": true,
   881  			}}})
   882  }
   883  
   884  func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) {
   885  	restore := release.MockOnClassic(true)
   886  	defer restore()
   887  
   888  	st := s.state
   889  
   890  	st.Lock()
   891  	defer st.Unlock()
   892  	kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook)
   893  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   894  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   895  
   896  	updates := []string{kernel.InstanceName()}
   897  	affected, err := snapstate.AffectedByRefresh(st, updates)
   898  	c.Assert(err, IsNil)
   899  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   900  		"snap-c": {
   901  			Restart: true,
   902  			AffectingSnaps: map[string]bool{
   903  				"gadget": true,
   904  			}}})
   905  }
   906  
   907  func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) {
   908  	restore := release.MockOnClassic(true)
   909  	defer restore()
   910  
   911  	st := s.state
   912  
   913  	st.Lock()
   914  	defer st.Unlock()
   915  
   916  	snapD := mockInstalledSnap(c, s.state, snapDyaml, noHook)
   917  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   918  	// unrelated snap
   919  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   920  
   921  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   922  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   923  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   924  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   925  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   926  	c.Assert(err, IsNil)
   927  
   928  	updates := []string{snapD.InstanceName()}
   929  	affected, err := snapstate.AffectedByRefresh(st, updates)
   930  	c.Assert(err, IsNil)
   931  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   932  		"snap-e": {
   933  			Restart: true,
   934  			AffectingSnaps: map[string]bool{
   935  				"snap-d": true,
   936  			}}})
   937  }
   938  
   939  func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) {
   940  	restore := release.MockOnClassic(true)
   941  	defer restore()
   942  
   943  	st := s.state
   944  
   945  	st.Lock()
   946  	defer st.Unlock()
   947  
   948  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   949  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   950  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   951  
   952  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   953  	c.Assert(s.repo.AddSnap(core), IsNil)
   954  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   955  
   956  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}}
   957  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   958  	c.Assert(err, IsNil)
   959  
   960  	updates := []string{core.InstanceName()}
   961  	affected, err := snapstate.AffectedByRefresh(st, updates)
   962  	c.Assert(err, IsNil)
   963  	c.Check(affected, HasLen, 0)
   964  }
   965  
   966  func (s *autorefreshGatingSuite) TestNotAffectedByPlugWithMountBackend(c *C) {
   967  	restore := release.MockOnClassic(true)
   968  	defer restore()
   969  
   970  	st := s.state
   971  
   972  	st.Lock()
   973  	defer st.Unlock()
   974  
   975  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   976  	snapE := mockInstalledSnap(c, s.state, snapEyaml, noHook)
   977  	// unrelated snap
   978  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   979  
   980  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   981  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   982  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   983  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   984  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   985  	c.Assert(err, IsNil)
   986  
   987  	// snapE has a plug using mount backend and is refreshed, this doesn't affect slot of snap-d.
   988  	updates := []string{snapE.InstanceName()}
   989  	affected, err := snapstate.AffectedByRefresh(st, updates)
   990  	c.Assert(err, IsNil)
   991  	c.Check(affected, HasLen, 0)
   992  }
   993  
   994  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) {
   995  	restore := release.MockOnClassic(true)
   996  	defer restore()
   997  
   998  	st := s.state
   999  
  1000  	st.Lock()
  1001  	defer st.Unlock()
  1002  
  1003  	snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, noHook)
  1004  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
  1005  	// unrelated snap
  1006  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
  1007  
  1008  	c.Assert(s.repo.AddSnap(snapF), IsNil)
  1009  	c.Assert(s.repo.AddSnap(snapdSnap), IsNil)
  1010  	c.Assert(s.repo.AddSnap(snapG), IsNil)
  1011  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}}
  1012  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
  1013  	c.Assert(err, IsNil)
  1014  
  1015  	// snapE has a plug using mount backend, refreshing snapd affects snapE.
  1016  	updates := []string{snapdSnap.InstanceName()}
  1017  	affected, err := snapstate.AffectedByRefresh(st, updates)
  1018  	c.Assert(err, IsNil)
  1019  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1020  		"snap-g": {
  1021  			Restart: true,
  1022  			AffectingSnaps: map[string]bool{
  1023  				"snapd": true,
  1024  			}}})
  1025  }
  1026  
  1027  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(c *C) {
  1028  	restore := release.MockOnClassic(true)
  1029  	defer restore()
  1030  
  1031  	st := s.state
  1032  
  1033  	st.Lock()
  1034  	defer st.Unlock()
  1035  
  1036  	coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook)
  1037  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
  1038  
  1039  	c.Assert(s.repo.AddSnap(coreSnap), IsNil)
  1040  	c.Assert(s.repo.AddSnap(snapG), IsNil)
  1041  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}}
  1042  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
  1043  	c.Assert(err, IsNil)
  1044  
  1045  	// snapG has a plug using mount backend, refreshing core affects snapE.
  1046  	updates := []string{coreSnap.InstanceName()}
  1047  	affected, err := snapstate.AffectedByRefresh(st, updates)
  1048  	c.Assert(err, IsNil)
  1049  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1050  		"snap-g": {
  1051  			Restart: true,
  1052  			AffectingSnaps: map[string]bool{
  1053  				"core": true,
  1054  			}}})
  1055  }
  1056  
  1057  func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) {
  1058  	restore := release.MockOnClassic(false)
  1059  	defer restore()
  1060  
  1061  	st := s.state
  1062  
  1063  	r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
  1064  	defer r()
  1065  
  1066  	st.Lock()
  1067  	defer st.Unlock()
  1068  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1069  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1070  	mockInstalledSnap(c, s.state, snapDyaml, useHook)
  1071  	mockInstalledSnap(c, s.state, snapEyaml, useHook)
  1072  	core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook)
  1073  
  1074  	updates := []string{core18.InstanceName()}
  1075  	affected, err := snapstate.AffectedByRefresh(st, updates)
  1076  	c.Assert(err, IsNil)
  1077  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1078  		"snap-a": {
  1079  			Base:    false,
  1080  			Restart: true,
  1081  			AffectingSnaps: map[string]bool{
  1082  				"core18": true,
  1083  			},
  1084  		},
  1085  		"snap-b": {
  1086  			Base:    false,
  1087  			Restart: true,
  1088  			AffectingSnaps: map[string]bool{
  1089  				"core18": true,
  1090  			},
  1091  		},
  1092  		"snap-d": {
  1093  			Base:    false,
  1094  			Restart: true,
  1095  			AffectingSnaps: map[string]bool{
  1096  				"core18": true,
  1097  			},
  1098  		},
  1099  		"snap-e": {
  1100  			Base:    false,
  1101  			Restart: true,
  1102  			AffectingSnaps: map[string]bool{
  1103  				"core18": true,
  1104  			}}})
  1105  }
  1106  
  1107  func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) {
  1108  	st := s.state
  1109  	st.Lock()
  1110  	defer st.Unlock()
  1111  
  1112  	affected := []string{"snap-a", "snap-b"}
  1113  	seenSnaps := make(map[string]bool)
  1114  
  1115  	ts := snapstate.CreateGateAutoRefreshHooks(st, affected)
  1116  	c.Assert(ts.Tasks(), HasLen, 2)
  1117  
  1118  	checkHook := func(t *state.Task) {
  1119  		c.Assert(t.Kind(), Equals, "run-hook")
  1120  		var hs hookstate.HookSetup
  1121  		c.Assert(t.Get("hook-setup", &hs), IsNil)
  1122  		c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1123  		c.Check(hs.Optional, Equals, true)
  1124  		seenSnaps[hs.Snap] = true
  1125  	}
  1126  
  1127  	checkHook(ts.Tasks()[0])
  1128  	checkHook(ts.Tasks()[1])
  1129  
  1130  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1131  }
  1132  
  1133  func (s *autorefreshGatingSuite) TestAffectedByRefreshCandidates(c *C) {
  1134  	st := s.state
  1135  	st.Lock()
  1136  	defer st.Unlock()
  1137  
  1138  	mockInstalledSnap(c, st, snapAyaml, useHook)
  1139  	// unrelated snap
  1140  	mockInstalledSnap(c, st, snapByaml, useHook)
  1141  
  1142  	// no refresh-candidates in state
  1143  	affected, err := snapstate.AffectedByRefreshCandidates(st)
  1144  	c.Assert(err, IsNil)
  1145  	c.Check(affected, HasLen, 0)
  1146  
  1147  	candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}}
  1148  	st.Set("refresh-candidates", &candidates)
  1149  
  1150  	affected, err = snapstate.AffectedByRefreshCandidates(st)
  1151  	c.Assert(err, IsNil)
  1152  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1153  		"snap-a": {
  1154  			AffectingSnaps: map[string]bool{
  1155  				"snap-a": true,
  1156  			}}})
  1157  }
  1158  
  1159  func (s *autorefreshGatingSuite) TestAffectingSnapsForAffectedByRefreshCandidates(c *C) {
  1160  	st := s.state
  1161  	st.Lock()
  1162  	defer st.Unlock()
  1163  
  1164  	mockInstalledSnap(c, st, snapAyaml, useHook)
  1165  	mockInstalledSnap(c, st, snapByaml, useHook)
  1166  	mockInstalledSnap(c, st, baseSnapByaml, useHook)
  1167  
  1168  	candidates := map[string]*snapstate.RefreshCandidate{
  1169  		"snap-a":      {},
  1170  		"snap-b":      {},
  1171  		"base-snap-b": {},
  1172  	}
  1173  	st.Set("refresh-candidates", &candidates)
  1174  
  1175  	affecting, err := snapstate.AffectingSnapsForAffectedByRefreshCandidates(st, "snap-b")
  1176  	c.Assert(err, IsNil)
  1177  	c.Check(affecting, DeepEquals, []string{"base-snap-b", "snap-b"})
  1178  }
  1179  
  1180  func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) {
  1181  	st := s.state
  1182  	st.Lock()
  1183  	defer st.Unlock()
  1184  
  1185  	st.Set("seeded", true)
  1186  
  1187  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1188  	defer restore()
  1189  
  1190  	snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
  1191  		return nil, nil
  1192  	}
  1193  	defer func() { snapstate.AutoAliases = nil }()
  1194  
  1195  	s.store.refreshedSnaps = []*snap.Info{{
  1196  		Architectures: []string{"all"},
  1197  		SnapType:      snap.TypeApp,
  1198  		SideInfo: snap.SideInfo{
  1199  			RealName: "snap-a",
  1200  			Revision: snap.R(8),
  1201  		},
  1202  	}}
  1203  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1204  
  1205  	// gate-auto-refresh-hook feature not enabled, expect old-style refresh.
  1206  	_, tss, err := snapstate.AutoRefresh(context.TODO(), st)
  1207  	c.Check(err, IsNil)
  1208  	c.Assert(tss, HasLen, 2)
  1209  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites")
  1210  	c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap")
  1211  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh")
  1212  
  1213  	// enable gate-auto-refresh-hook feature
  1214  	tr := config.NewTransaction(s.state)
  1215  	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
  1216  	tr.Commit()
  1217  
  1218  	_, tss, err = snapstate.AutoRefresh(context.TODO(), st)
  1219  	c.Check(err, IsNil)
  1220  	c.Assert(tss, HasLen, 2)
  1221  	task := tss[0].Tasks()[0]
  1222  	c.Check(task.Kind(), Equals, "conditional-auto-refresh")
  1223  	var toUpdate map[string]*snapstate.RefreshCandidate
  1224  	c.Assert(task.Get("snaps", &toUpdate), IsNil)
  1225  	seenSnaps := make(map[string]bool)
  1226  	for up := range toUpdate {
  1227  		seenSnaps[up] = true
  1228  	}
  1229  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1230  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook")
  1231  }
  1232  
  1233  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) {
  1234  	s.store.refreshedSnaps = []*snap.Info{{
  1235  		Architectures: []string{"all"},
  1236  		SnapType:      snap.TypeApp,
  1237  		SideInfo: snap.SideInfo{
  1238  			RealName: "snap-a",
  1239  			Revision: snap.R(8),
  1240  		},
  1241  	}, {
  1242  		Architectures: []string{"all"},
  1243  		SnapType:      snap.TypeBase,
  1244  		SideInfo: snap.SideInfo{
  1245  			RealName: "base-snap-b",
  1246  			Revision: snap.R(3),
  1247  		},
  1248  	}, {
  1249  		Architectures: []string{"all"},
  1250  		SnapType:      snap.TypeApp,
  1251  		SideInfo: snap.SideInfo{
  1252  			RealName: "snap-c",
  1253  			Revision: snap.R(5),
  1254  		},
  1255  	}}
  1256  
  1257  	st := s.state
  1258  	st.Lock()
  1259  	defer st.Unlock()
  1260  
  1261  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1262  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1263  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1264  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1265  	mockInstalledSnap(c, s.state, snapDyaml, noHook)
  1266  
  1267  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1268  	defer restore()
  1269  
  1270  	// pretend some snaps are held
  1271  	c.Assert(snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d"), IsNil)
  1272  	// sanity check
  1273  	heldSnaps, err := snapstate.HeldSnaps(st)
  1274  	c.Assert(err, IsNil)
  1275  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1276  		"snap-a": true,
  1277  		"snap-d": true,
  1278  	})
  1279  
  1280  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1281  	c.Assert(err, IsNil)
  1282  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"})
  1283  	c.Assert(tss, HasLen, 2)
  1284  
  1285  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1286  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1287  		"snap-a": {
  1288  			SnapSetup: snapstate.SnapSetup{
  1289  				Type:      "app",
  1290  				PlugsOnly: true,
  1291  				Flags: snapstate.Flags{
  1292  					IsAutoRefresh: true,
  1293  				},
  1294  				SideInfo: &snap.SideInfo{
  1295  					RealName: "snap-a",
  1296  					Revision: snap.R(8),
  1297  				},
  1298  				DownloadInfo: &snap.DownloadInfo{},
  1299  			},
  1300  		},
  1301  		"base-snap-b": {
  1302  			SnapSetup: snapstate.SnapSetup{
  1303  				Type:      "base",
  1304  				PlugsOnly: true,
  1305  				Flags: snapstate.Flags{
  1306  					IsAutoRefresh: true,
  1307  				},
  1308  				SideInfo: &snap.SideInfo{
  1309  					RealName: "base-snap-b",
  1310  					Revision: snap.R(3),
  1311  				},
  1312  				DownloadInfo: &snap.DownloadInfo{},
  1313  			},
  1314  		},
  1315  		"snap-c": {
  1316  			SnapSetup: snapstate.SnapSetup{
  1317  				Type:      "app",
  1318  				PlugsOnly: true,
  1319  				Flags: snapstate.Flags{
  1320  					IsAutoRefresh: true,
  1321  				},
  1322  				SideInfo: &snap.SideInfo{
  1323  					RealName: "snap-c",
  1324  					Revision: snap.R(5),
  1325  				},
  1326  				DownloadInfo: &snap.DownloadInfo{},
  1327  			},
  1328  		},
  1329  	})
  1330  
  1331  	c.Assert(tss[1].Tasks(), HasLen, 2)
  1332  
  1333  	// check hooks for affected snaps
  1334  	seenSnaps := make(map[string]bool)
  1335  	var hs hookstate.HookSetup
  1336  	task := tss[1].Tasks()[0]
  1337  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1338  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1339  	seenSnaps[hs.Snap] = true
  1340  
  1341  	task = tss[1].Tasks()[1]
  1342  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1343  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1344  	seenSnaps[hs.Snap] = true
  1345  
  1346  	// hook for snap-a because it gets refreshed, for snap-b because its base
  1347  	// gets refreshed. snap-c is refreshed but doesn't have the hook.
  1348  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1349  
  1350  	// check that refresh-candidates in the state were updated
  1351  	var candidates map[string]*snapstate.RefreshCandidate
  1352  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1353  	c.Assert(candidates, HasLen, 3)
  1354  	c.Check(candidates["snap-a"], NotNil)
  1355  	c.Check(candidates["base-snap-b"], NotNil)
  1356  	c.Check(candidates["snap-c"], NotNil)
  1357  
  1358  	// check that after autoRefreshPhase1 any held snaps that are not in refresh
  1359  	// candidates got removed.
  1360  	heldSnaps, err = snapstate.HeldSnaps(st)
  1361  	c.Assert(err, IsNil)
  1362  	// snap-d got removed from held snaps.
  1363  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1364  		"snap-a": true,
  1365  	})
  1366  }
  1367  
  1368  // this test demonstrates that affectedByRefresh uses current snap info (not
  1369  // snap infos of store updates) by simulating a different base for the updated
  1370  // snap from the store.
  1371  func (s *autorefreshGatingSuite) TestAffectedByRefreshUsesCurrentSnapInfo(c *C) {
  1372  	s.store.refreshedSnaps = []*snap.Info{{
  1373  		Architectures: []string{"all"},
  1374  		SnapType:      snap.TypeBase,
  1375  		SideInfo: snap.SideInfo{
  1376  			RealName: "base-snap-b",
  1377  			Revision: snap.R(3),
  1378  		},
  1379  	}, {
  1380  		Architectures: []string{"all"},
  1381  		Base:          "new-base",
  1382  		SnapType:      snap.TypeApp,
  1383  		SideInfo: snap.SideInfo{
  1384  			RealName: "snap-b",
  1385  			Revision: snap.R(5),
  1386  		},
  1387  	}}
  1388  
  1389  	st := s.state
  1390  	st.Lock()
  1391  	defer st.Unlock()
  1392  
  1393  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1394  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1395  
  1396  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1397  	defer restore()
  1398  
  1399  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1400  	c.Assert(err, IsNil)
  1401  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-b"})
  1402  	c.Assert(tss, HasLen, 2)
  1403  
  1404  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1405  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1406  		"snap-b": {
  1407  			SnapSetup: snapstate.SnapSetup{
  1408  				Type:      "app",
  1409  				Base:      "new-base",
  1410  				PlugsOnly: true,
  1411  				Flags: snapstate.Flags{
  1412  					IsAutoRefresh: true,
  1413  				},
  1414  				SideInfo: &snap.SideInfo{
  1415  					RealName: "snap-b",
  1416  					Revision: snap.R(5),
  1417  				},
  1418  				DownloadInfo: &snap.DownloadInfo{},
  1419  			},
  1420  		},
  1421  		"base-snap-b": {
  1422  			SnapSetup: snapstate.SnapSetup{
  1423  				Type:      "base",
  1424  				PlugsOnly: true,
  1425  				Flags: snapstate.Flags{
  1426  					IsAutoRefresh: true,
  1427  				},
  1428  				SideInfo: &snap.SideInfo{
  1429  					RealName: "base-snap-b",
  1430  					Revision: snap.R(3),
  1431  				},
  1432  				DownloadInfo: &snap.DownloadInfo{},
  1433  			},
  1434  		},
  1435  	})
  1436  
  1437  	c.Assert(tss[1].Tasks(), HasLen, 1)
  1438  	var hs hookstate.HookSetup
  1439  	task := tss[1].Tasks()[0]
  1440  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1441  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1442  	c.Check(hs.Snap, Equals, "snap-b")
  1443  
  1444  	// check that refresh-candidates in the state were updated
  1445  	var candidates map[string]*snapstate.RefreshCandidate
  1446  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1447  	c.Assert(candidates, HasLen, 2)
  1448  	c.Check(candidates["snap-b"], NotNil)
  1449  	c.Check(candidates["base-snap-b"], NotNil)
  1450  }
  1451  
  1452  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) {
  1453  	s.store.refreshedSnaps = []*snap.Info{{
  1454  		Architectures: []string{"all"},
  1455  		SnapType:      snap.TypeApp,
  1456  		SideInfo: snap.SideInfo{
  1457  			RealName: "snap-a",
  1458  			Revision: snap.R(8),
  1459  		},
  1460  	}, {
  1461  		Architectures: []string{"all"},
  1462  		SnapType:      snap.TypeBase,
  1463  		SideInfo: snap.SideInfo{
  1464  			RealName: "snap-c",
  1465  			Revision: snap.R(5),
  1466  		},
  1467  	}}
  1468  
  1469  	st := s.state
  1470  	st.Lock()
  1471  	defer st.Unlock()
  1472  
  1473  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1474  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1475  
  1476  	conflictChange := st.NewChange("conflicting change", "")
  1477  	conflictTask := st.NewTask("conflicting task", "")
  1478  	si := &snap.SideInfo{
  1479  		RealName: "snap-c",
  1480  		Revision: snap.R(1),
  1481  	}
  1482  	sup := snapstate.SnapSetup{SideInfo: si}
  1483  	conflictTask.Set("snap-setup", sup)
  1484  	conflictChange.AddTask(conflictTask)
  1485  
  1486  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1487  	defer restore()
  1488  
  1489  	logbuf, restoreLogger := logger.MockLogger()
  1490  	defer restoreLogger()
  1491  
  1492  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1493  	c.Assert(err, IsNil)
  1494  	c.Check(names, DeepEquals, []string{"snap-a"})
  1495  	c.Assert(tss, HasLen, 2)
  1496  
  1497  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1498  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1499  		"snap-a": {
  1500  			SnapSetup: snapstate.SnapSetup{
  1501  				Type:      "app",
  1502  				PlugsOnly: true,
  1503  				Flags: snapstate.Flags{
  1504  					IsAutoRefresh: true,
  1505  				},
  1506  				SideInfo: &snap.SideInfo{
  1507  					RealName: "snap-a",
  1508  					Revision: snap.R(8),
  1509  				},
  1510  				DownloadInfo: &snap.DownloadInfo{},
  1511  			}}})
  1512  
  1513  	c.Assert(tss[1].Tasks(), HasLen, 1)
  1514  
  1515  	c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`)
  1516  
  1517  	seenSnaps := make(map[string]bool)
  1518  	var hs hookstate.HookSetup
  1519  	c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil)
  1520  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1521  	seenSnaps[hs.Snap] = true
  1522  
  1523  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1524  
  1525  	// check that refresh-candidates in the state were updated
  1526  	var candidates map[string]*snapstate.RefreshCandidate
  1527  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1528  	c.Assert(candidates, HasLen, 2)
  1529  	c.Check(candidates["snap-a"], NotNil)
  1530  	c.Check(candidates["snap-c"], NotNil)
  1531  }
  1532  
  1533  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) {
  1534  	s.store.refreshedSnaps = []*snap.Info{{
  1535  		Architectures: []string{"all"},
  1536  		SnapType:      snap.TypeBase,
  1537  		SideInfo: snap.SideInfo{
  1538  			RealName: "base-snap-b",
  1539  			Revision: snap.R(3),
  1540  		},
  1541  	}, {
  1542  		Architectures: []string{"all"},
  1543  		SnapType:      snap.TypeBase,
  1544  		SideInfo: snap.SideInfo{
  1545  			RealName: "snap-c",
  1546  			Revision: snap.R(5),
  1547  		},
  1548  	}}
  1549  
  1550  	st := s.state
  1551  	st.Lock()
  1552  	defer st.Unlock()
  1553  
  1554  	mockInstalledSnap(c, s.state, snapByaml, noHook)
  1555  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1556  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1557  
  1558  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1559  	defer restore()
  1560  
  1561  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1562  	c.Assert(err, IsNil)
  1563  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"})
  1564  	c.Assert(tss, HasLen, 1)
  1565  
  1566  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1567  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh")
  1568  }
  1569  
  1570  func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) {
  1571  	info := &snap.Info{
  1572  		SuggestedName: name,
  1573  		SideInfo:      *si,
  1574  		Architectures: []string{"all"},
  1575  		SnapType:      snap.TypeApp,
  1576  		Epoch:         snap.Epoch{},
  1577  	}
  1578  	switch name {
  1579  	case "base-snap-b":
  1580  		info.SnapType = snap.TypeBase
  1581  	case "snap-a", "snap-b":
  1582  		info.Hooks = map[string]*snap.HookInfo{
  1583  			"gate-auto-refresh": {
  1584  				Name: "gate-auto-refresh",
  1585  				Snap: info,
  1586  			},
  1587  		}
  1588  		if name == "snap-b" {
  1589  			info.Base = "base-snap-b"
  1590  		}
  1591  	}
  1592  	return info, nil
  1593  }
  1594  
  1595  func (s *snapmgrTestSuite) testAutoRefreshPhase2(c *C, beforePhase1 func(), gateAutoRefreshHook func(snapName string), expected []string) *state.Change {
  1596  	st := s.state
  1597  	st.Lock()
  1598  	defer st.Unlock()
  1599  
  1600  	s.o.TaskRunner().AddHandler("run-hook", func(t *state.Task, tomb *tomb.Tomb) error {
  1601  		var hsup hookstate.HookSetup
  1602  		t.State().Lock()
  1603  		defer t.State().Unlock()
  1604  		c.Assert(t.Get("hook-setup", &hsup), IsNil)
  1605  		if hsup.Hook == "gate-auto-refresh" && gateAutoRefreshHook != nil {
  1606  			gateAutoRefreshHook(hsup.Snap)
  1607  		}
  1608  		return nil
  1609  	}, nil)
  1610  
  1611  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1612  		c.Fatal("unexpected call to installSize")
  1613  		return 0, nil
  1614  	})
  1615  	defer restoreInstallSize()
  1616  
  1617  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1618  		fakeStore: s.fakeStore,
  1619  		refreshedSnaps: []*snap.Info{{
  1620  			Architectures: []string{"all"},
  1621  			SnapType:      snap.TypeApp,
  1622  			SideInfo: snap.SideInfo{
  1623  				RealName: "snap-a",
  1624  				Revision: snap.R(8),
  1625  			},
  1626  		}, {
  1627  			Architectures: []string{"all"},
  1628  			SnapType:      snap.TypeBase,
  1629  			SideInfo: snap.SideInfo{
  1630  				RealName: "base-snap-b",
  1631  				Revision: snap.R(3),
  1632  			},
  1633  		}}})
  1634  
  1635  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1636  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1637  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1638  
  1639  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1640  
  1641  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1642  	defer restore()
  1643  
  1644  	if beforePhase1 != nil {
  1645  		beforePhase1()
  1646  	}
  1647  
  1648  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1649  	c.Assert(err, IsNil)
  1650  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1651  
  1652  	chg := s.state.NewChange("refresh", "...")
  1653  	for _, ts := range tss {
  1654  		chg.AddAll(ts)
  1655  	}
  1656  
  1657  	s.state.Unlock()
  1658  	defer s.se.Stop()
  1659  	s.settle(c)
  1660  	s.state.Lock()
  1661  
  1662  	c.Check(chg.Status(), Equals, state.DoneStatus)
  1663  	c.Check(chg.Err(), IsNil)
  1664  
  1665  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1666  
  1667  	return chg
  1668  }
  1669  
  1670  func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) {
  1671  	expected := []string{
  1672  		"conditional-auto-refresh",
  1673  		"run-hook [snap-a;gate-auto-refresh]",
  1674  		// snap-b hook is triggered because of base-snap-b refresh
  1675  		"run-hook [snap-b;gate-auto-refresh]",
  1676  		"prerequisites",
  1677  		"download-snap",
  1678  		"validate-snap",
  1679  		"mount-snap",
  1680  		"run-hook [base-snap-b;pre-refresh]",
  1681  		"stop-snap-services",
  1682  		"remove-aliases",
  1683  		"unlink-current-snap",
  1684  		"copy-snap-data",
  1685  		"setup-profiles",
  1686  		"link-snap",
  1687  		"auto-connect",
  1688  		"set-auto-aliases",
  1689  		"setup-aliases",
  1690  		"run-hook [base-snap-b;post-refresh]",
  1691  		"start-snap-services",
  1692  		"cleanup",
  1693  		"run-hook [base-snap-b;check-health]",
  1694  		"prerequisites",
  1695  		"download-snap",
  1696  		"validate-snap",
  1697  		"mount-snap",
  1698  		"run-hook [snap-a;pre-refresh]",
  1699  		"stop-snap-services",
  1700  		"remove-aliases",
  1701  		"unlink-current-snap",
  1702  		"copy-snap-data",
  1703  		"setup-profiles",
  1704  		"link-snap",
  1705  		"auto-connect",
  1706  		"set-auto-aliases",
  1707  		"setup-aliases",
  1708  		"run-hook [snap-a;post-refresh]",
  1709  		"start-snap-services",
  1710  		"cleanup",
  1711  		"run-hook [snap-a;configure]",
  1712  		"run-hook [snap-a;check-health]",
  1713  		"check-rerefresh",
  1714  	}
  1715  
  1716  	seenSnapsWithGateAutoRefreshHook := make(map[string]bool)
  1717  
  1718  	chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1719  		seenSnapsWithGateAutoRefreshHook[snapName] = true
  1720  	}, expected)
  1721  
  1722  	c.Check(seenSnapsWithGateAutoRefreshHook, DeepEquals, map[string]bool{
  1723  		"snap-a": true,
  1724  		"snap-b": true,
  1725  	})
  1726  
  1727  	s.state.Lock()
  1728  	defer s.state.Unlock()
  1729  
  1730  	tasks := chg.Tasks()
  1731  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b", "snap-a" as needed`)
  1732  
  1733  	// all snaps refreshed, all removed from refresh-candidates.
  1734  	var candidates map[string]*snapstate.RefreshCandidate
  1735  	c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil)
  1736  	c.Assert(candidates, HasLen, 0)
  1737  }
  1738  
  1739  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Held(c *C) {
  1740  	logbuf, restoreLogger := logger.MockLogger()
  1741  	defer restoreLogger()
  1742  
  1743  	expected := []string{
  1744  		"conditional-auto-refresh",
  1745  		"run-hook [snap-a;gate-auto-refresh]",
  1746  		// snap-b hook is triggered because of base-snap-b refresh
  1747  		"run-hook [snap-b;gate-auto-refresh]",
  1748  		"prerequisites",
  1749  		"download-snap",
  1750  		"validate-snap",
  1751  		"mount-snap",
  1752  		"run-hook [snap-a;pre-refresh]",
  1753  		"stop-snap-services",
  1754  		"remove-aliases",
  1755  		"unlink-current-snap",
  1756  		"copy-snap-data",
  1757  		"setup-profiles",
  1758  		"link-snap",
  1759  		"auto-connect",
  1760  		"set-auto-aliases",
  1761  		"setup-aliases",
  1762  		"run-hook [snap-a;post-refresh]",
  1763  		"start-snap-services",
  1764  		"cleanup",
  1765  		"run-hook [snap-a;configure]",
  1766  		"run-hook [snap-a;check-health]",
  1767  		"check-rerefresh",
  1768  	}
  1769  
  1770  	chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1771  		if snapName == "snap-b" {
  1772  			// pretend than snap-b calls snapctl --hold to hold refresh of base-snap-b
  1773  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1774  		}
  1775  	}, expected)
  1776  
  1777  	s.state.Lock()
  1778  	defer s.state.Unlock()
  1779  
  1780  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1781  	tasks := chg.Tasks()
  1782  	// no re-refresh for base-snap-b because it was held.
  1783  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "snap-a" as needed`)
  1784  }
  1785  
  1786  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Proceed(c *C) {
  1787  	logbuf, restoreLogger := logger.MockLogger()
  1788  	defer restoreLogger()
  1789  
  1790  	expected := []string{
  1791  		"conditional-auto-refresh",
  1792  		"run-hook [snap-a;gate-auto-refresh]",
  1793  		// snap-b hook is triggered because of base-snap-b refresh
  1794  		"run-hook [snap-b;gate-auto-refresh]",
  1795  		"prerequisites",
  1796  		"download-snap",
  1797  		"validate-snap",
  1798  		"mount-snap",
  1799  		"run-hook [snap-a;pre-refresh]",
  1800  		"stop-snap-services",
  1801  		"remove-aliases",
  1802  		"unlink-current-snap",
  1803  		"copy-snap-data",
  1804  		"setup-profiles",
  1805  		"link-snap",
  1806  		"auto-connect",
  1807  		"set-auto-aliases",
  1808  		"setup-aliases",
  1809  		"run-hook [snap-a;post-refresh]",
  1810  		"start-snap-services",
  1811  		"cleanup",
  1812  		"run-hook [snap-a;configure]",
  1813  		"run-hook [snap-a;check-health]",
  1814  		"check-rerefresh",
  1815  	}
  1816  
  1817  	s.testAutoRefreshPhase2(c, func() {
  1818  		// pretend that snap-a and base-snap-b are initially held
  1819  		c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1820  		c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1821  	}, func(snapName string) {
  1822  		if snapName == "snap-a" {
  1823  			// pretend than snap-a calls snapctl --proceed
  1824  			c.Assert(snapstate.ProceedWithRefresh(s.state, "snap-a"), IsNil)
  1825  		}
  1826  		// note, do nothing about snap-b which just keeps its hold state in
  1827  		// the test, but if we were using real gate-auto-refresh hook
  1828  		// handler, the default behavior for snap-b if it doesn't call --hold
  1829  		// would be to proceed (hook handler would take care of that).
  1830  	}, expected)
  1831  
  1832  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1833  }
  1834  
  1835  func (s *snapmgrTestSuite) TestAutoRefreshPhase2AllHeld(c *C) {
  1836  	logbuf, restoreLogger := logger.MockLogger()
  1837  	defer restoreLogger()
  1838  
  1839  	expected := []string{
  1840  		"conditional-auto-refresh",
  1841  		"run-hook [snap-a;gate-auto-refresh]",
  1842  		// snap-b hook is triggered because of base-snap-b refresh
  1843  		"run-hook [snap-b;gate-auto-refresh]",
  1844  	}
  1845  
  1846  	s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1847  		switch snapName {
  1848  		case "snap-b":
  1849  			// pretend that snap-b calls snapctl --hold to hold refresh of base-snap-b
  1850  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1851  		case "snap-a":
  1852  			// pretend that snap-a calls snapctl --hold to hold itself
  1853  			c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1854  		default:
  1855  			c.Fatalf("unexpected snap %q", snapName)
  1856  		}
  1857  	}, expected)
  1858  
  1859  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b,snap-a`)
  1860  }
  1861  
  1862  func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) {
  1863  	st := s.state
  1864  	st.Lock()
  1865  	defer st.Unlock()
  1866  
  1867  	restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error {
  1868  		c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123))
  1869  		if fail {
  1870  			return &osutil.NotEnoughDiskSpaceError{}
  1871  		}
  1872  		return nil
  1873  	})
  1874  	defer restore()
  1875  
  1876  	var installSizeCalled bool
  1877  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1878  		installSizeCalled = true
  1879  		seen := map[string]bool{}
  1880  		for _, sn := range snaps {
  1881  			seen[sn.InstanceName()] = true
  1882  		}
  1883  		c.Check(seen, DeepEquals, map[string]bool{
  1884  			"base-snap-b": true,
  1885  			"snap-a":      true,
  1886  		})
  1887  		return 123, nil
  1888  	})
  1889  	defer restoreInstallSize()
  1890  
  1891  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  1892  	defer restoreModel()
  1893  
  1894  	tr := config.NewTransaction(s.state)
  1895  	tr.Set("core", "experimental.check-disk-space-refresh", true)
  1896  	tr.Commit()
  1897  
  1898  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1899  		fakeStore: s.fakeStore,
  1900  		refreshedSnaps: []*snap.Info{{
  1901  			Architectures: []string{"all"},
  1902  			SnapType:      snap.TypeApp,
  1903  			SideInfo: snap.SideInfo{
  1904  				RealName: "snap-a",
  1905  				Revision: snap.R(8),
  1906  			},
  1907  		}, {
  1908  			Architectures: []string{"all"},
  1909  			SnapType:      snap.TypeBase,
  1910  			SideInfo: snap.SideInfo{
  1911  				RealName: "base-snap-b",
  1912  				Revision: snap.R(3),
  1913  			},
  1914  		}}})
  1915  
  1916  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1917  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1918  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1919  
  1920  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1921  
  1922  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1923  	c.Assert(err, IsNil)
  1924  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1925  
  1926  	chg := s.state.NewChange("refresh", "...")
  1927  	for _, ts := range tss {
  1928  		chg.AddAll(ts)
  1929  	}
  1930  
  1931  	s.state.Unlock()
  1932  	defer s.se.Stop()
  1933  	s.settle(c)
  1934  	s.state.Lock()
  1935  
  1936  	c.Check(installSizeCalled, Equals, true)
  1937  	if fail {
  1938  		c.Check(chg.Status(), Equals, state.ErrorStatus)
  1939  		c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`)
  1940  	} else {
  1941  		c.Check(chg.Status(), Equals, state.DoneStatus)
  1942  		c.Check(chg.Err(), IsNil)
  1943  	}
  1944  }
  1945  
  1946  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) {
  1947  	fail := true
  1948  	s.testAutoRefreshPhase2DiskSpaceCheck(c, fail)
  1949  }
  1950  
  1951  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) {
  1952  	var nofail bool
  1953  	s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail)
  1954  }
  1955  
  1956  // XXX: this case is probably artificial; with proper conflict prevention
  1957  // we shouldn't get conflicts from doInstall in phase2.
  1958  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) {
  1959  	st := s.state
  1960  	st.Lock()
  1961  	defer st.Unlock()
  1962  
  1963  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1964  		fakeStore: s.fakeStore,
  1965  		refreshedSnaps: []*snap.Info{{
  1966  			Architectures: []string{"all"},
  1967  			SnapType:      snap.TypeApp,
  1968  			SideInfo: snap.SideInfo{
  1969  				RealName: "snap-a",
  1970  				Revision: snap.R(8),
  1971  			},
  1972  		}, {
  1973  			Architectures: []string{"all"},
  1974  			SnapType:      snap.TypeBase,
  1975  			SideInfo: snap.SideInfo{
  1976  				RealName: "base-snap-b",
  1977  				Revision: snap.R(3),
  1978  			},
  1979  		}}})
  1980  
  1981  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1982  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1983  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1984  
  1985  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1986  
  1987  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1988  	defer restore()
  1989  
  1990  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1991  	c.Assert(err, IsNil)
  1992  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1993  
  1994  	chg := s.state.NewChange("refresh", "...")
  1995  	for _, ts := range tss {
  1996  		chg.AddAll(ts)
  1997  	}
  1998  
  1999  	conflictChange := st.NewChange("conflicting change", "")
  2000  	conflictTask := st.NewTask("conflicting task", "")
  2001  	si := &snap.SideInfo{
  2002  		RealName: "snap-a",
  2003  		Revision: snap.R(1),
  2004  	}
  2005  	sup := snapstate.SnapSetup{SideInfo: si}
  2006  	conflictTask.Set("snap-setup", sup)
  2007  	conflictChange.AddTask(conflictTask)
  2008  	conflictTask.WaitFor(tss[0].Tasks()[0])
  2009  
  2010  	s.state.Unlock()
  2011  	defer s.se.Stop()
  2012  	s.settle(c)
  2013  	s.state.Lock()
  2014  
  2015  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  2016  	c.Check(chg.Err(), IsNil)
  2017  
  2018  	// no refresh of snap-a because of the conflict.
  2019  	expected := []string{
  2020  		"conditional-auto-refresh",
  2021  		"run-hook [snap-a;gate-auto-refresh]",
  2022  		// snap-b hook is triggered because of base-snap-b refresh
  2023  		"run-hook [snap-b;gate-auto-refresh]",
  2024  		"prerequisites",
  2025  		"download-snap",
  2026  		"validate-snap",
  2027  		"mount-snap",
  2028  		"run-hook [base-snap-b;pre-refresh]",
  2029  		"stop-snap-services",
  2030  		"remove-aliases",
  2031  		"unlink-current-snap",
  2032  		"copy-snap-data",
  2033  		"setup-profiles",
  2034  		"link-snap",
  2035  		"auto-connect",
  2036  		"set-auto-aliases",
  2037  		"setup-aliases",
  2038  		"run-hook [base-snap-b;post-refresh]",
  2039  		"start-snap-services",
  2040  		"cleanup",
  2041  		"run-hook [base-snap-b;check-health]",
  2042  		"check-rerefresh",
  2043  	}
  2044  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  2045  }
  2046  
  2047  func (s *snapmgrTestSuite) TestAutoRefreshPhase2ConflictOtherSnapOp(c *C) {
  2048  	st := s.state
  2049  	st.Lock()
  2050  	defer st.Unlock()
  2051  
  2052  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  2053  		fakeStore: s.fakeStore,
  2054  		refreshedSnaps: []*snap.Info{{
  2055  			Architectures: []string{"all"},
  2056  			SnapType:      snap.TypeApp,
  2057  			SideInfo: snap.SideInfo{
  2058  				RealName: "snap-a",
  2059  				Revision: snap.R(8),
  2060  			},
  2061  		}}})
  2062  
  2063  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2064  
  2065  	snapstate.MockSnapReadInfo(fakeReadInfo)
  2066  
  2067  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2068  	defer restore()
  2069  
  2070  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  2071  	c.Assert(err, IsNil)
  2072  	c.Check(names, DeepEquals, []string{"snap-a"})
  2073  
  2074  	chg := s.state.NewChange("fake-auto-refresh", "...")
  2075  	for _, ts := range tss {
  2076  		chg.AddAll(ts)
  2077  	}
  2078  
  2079  	s.state.Unlock()
  2080  	// run first task
  2081  	s.se.Ensure()
  2082  	s.se.Wait()
  2083  
  2084  	s.state.Lock()
  2085  
  2086  	_, err = snapstate.Remove(s.state, "snap-a", snap.R(8), nil)
  2087  	c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{
  2088  		ChangeKind: "fake-auto-refresh",
  2089  		Snap:       "snap-a",
  2090  	})
  2091  
  2092  	_, err = snapstate.Update(s.state, "snap-a", nil, 0, snapstate.Flags{})
  2093  	c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{
  2094  		ChangeKind: "fake-auto-refresh",
  2095  		Snap:       "snap-a",
  2096  	})
  2097  
  2098  	// only 2 tasks because we don't run settle() so conditional-auto-refresh
  2099  	// doesn't run and no new tasks get created.
  2100  	expected := []string{
  2101  		"conditional-auto-refresh",
  2102  		"run-hook [snap-a;gate-auto-refresh]",
  2103  	}
  2104  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  2105  }
  2106  
  2107  func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) {
  2108  	st := s.state
  2109  	st.Lock()
  2110  	defer st.Unlock()
  2111  
  2112  	restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) {
  2113  		c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh")
  2114  		var candidates map[string]*snapstate.RefreshCandidate
  2115  		c.Assert(gatingTask.Get("snaps", &candidates), IsNil)
  2116  		seenSnaps := make(map[string]bool)
  2117  		var filteredByGatingHooks []*snapstate.RefreshCandidate
  2118  		for _, cand := range candidates {
  2119  			seenSnaps[cand.InstanceName()] = true
  2120  			if cand.InstanceName() == "snap-a" {
  2121  				continue
  2122  			}
  2123  			filteredByGatingHooks = append(filteredByGatingHooks, cand)
  2124  		}
  2125  		c.Check(seenSnaps, DeepEquals, map[string]bool{
  2126  			"snap-a":      true,
  2127  			"base-snap-b": true,
  2128  		})
  2129  		return filteredByGatingHooks, nil
  2130  	})
  2131  	defer restore()
  2132  
  2133  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  2134  		fakeStore: s.fakeStore,
  2135  		refreshedSnaps: []*snap.Info{
  2136  			{
  2137  				Architectures: []string{"all"},
  2138  				SnapType:      snap.TypeApp,
  2139  				SideInfo: snap.SideInfo{
  2140  					RealName: "snap-a",
  2141  					Revision: snap.R(8),
  2142  				},
  2143  			}, {
  2144  				Architectures: []string{"all"},
  2145  				SnapType:      snap.TypeBase,
  2146  				SideInfo: snap.SideInfo{
  2147  					RealName: "base-snap-b",
  2148  					Revision: snap.R(3),
  2149  				},
  2150  			},
  2151  		}})
  2152  
  2153  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2154  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2155  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2156  
  2157  	snapstate.MockSnapReadInfo(fakeReadInfo)
  2158  
  2159  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  2160  	defer restoreModel()
  2161  
  2162  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  2163  	c.Assert(err, IsNil)
  2164  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  2165  
  2166  	chg := s.state.NewChange("refresh", "...")
  2167  	for _, ts := range tss {
  2168  		chg.AddAll(ts)
  2169  	}
  2170  
  2171  	s.state.Unlock()
  2172  	defer s.se.Stop()
  2173  	s.settle(c)
  2174  	s.state.Lock()
  2175  
  2176  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  2177  	c.Check(chg.Err(), IsNil)
  2178  
  2179  	expected := []string{
  2180  		"conditional-auto-refresh",
  2181  		"run-hook [snap-a;gate-auto-refresh]",
  2182  		// snap-b hook is triggered because of base-snap-b refresh
  2183  		"run-hook [snap-b;gate-auto-refresh]",
  2184  		"prerequisites",
  2185  		"download-snap",
  2186  		"validate-snap",
  2187  		"mount-snap",
  2188  		"run-hook [base-snap-b;pre-refresh]",
  2189  		"stop-snap-services",
  2190  		"remove-aliases",
  2191  		"unlink-current-snap",
  2192  		"copy-snap-data",
  2193  		"setup-profiles",
  2194  		"link-snap",
  2195  		"auto-connect",
  2196  		"set-auto-aliases",
  2197  		"setup-aliases",
  2198  		"run-hook [base-snap-b;post-refresh]",
  2199  		"start-snap-services",
  2200  		"cleanup",
  2201  		"run-hook [base-snap-b;check-health]",
  2202  		"check-rerefresh",
  2203  	}
  2204  	tasks := chg.Tasks()
  2205  	verifyPhasedAutorefreshTasks(c, tasks, expected)
  2206  	// no re-refresh for snap-a because it was held.
  2207  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b" as needed`)
  2208  
  2209  	// only snap-a remains in refresh-candidates because it was held;
  2210  	// base-snap-b got pruned (was refreshed).
  2211  	var candidates map[string]*snapstate.RefreshCandidate
  2212  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  2213  	c.Assert(candidates, HasLen, 1)
  2214  	c.Check(candidates["snap-a"], NotNil)
  2215  }
  2216  
  2217  func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorAutoRefreshInProgress(c *C) {
  2218  	st := s.state
  2219  	st.Lock()
  2220  	defer st.Unlock()
  2221  
  2222  	chg := st.NewChange("auto-refresh", "...")
  2223  	task := st.NewTask("foo", "...")
  2224  	chg.AddTask(task)
  2225  
  2226  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `there is an auto-refresh in progress`)
  2227  }
  2228  
  2229  func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorNothingHeld(c *C) {
  2230  	st := s.state
  2231  	st.Lock()
  2232  	defer st.Unlock()
  2233  
  2234  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `no snaps are held by snap "snap-a"`)
  2235  }
  2236  
  2237  func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnap(c *C) {
  2238  	s.store.refreshedSnaps = []*snap.Info{{
  2239  		Architectures: []string{"all"},
  2240  		SnapType:      snap.TypeApp,
  2241  		SideInfo: snap.SideInfo{
  2242  			RealName: "snap-a",
  2243  			Revision: snap.R(8),
  2244  		},
  2245  	}, {
  2246  		Architectures: []string{"all"},
  2247  		SnapType:      snap.TypeApp,
  2248  		Base:          "base-snap-b",
  2249  		SideInfo: snap.SideInfo{
  2250  			RealName: "snap-b",
  2251  			Revision: snap.R(2),
  2252  		},
  2253  	}, {
  2254  		Architectures: []string{"all"},
  2255  		SnapType:      snap.TypeBase,
  2256  		SideInfo: snap.SideInfo{
  2257  			RealName: "base-snap-b",
  2258  			Revision: snap.R(3),
  2259  		},
  2260  	}}
  2261  
  2262  	st := s.state
  2263  	st.Lock()
  2264  	defer st.Unlock()
  2265  
  2266  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2267  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2268  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2269  
  2270  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2271  	defer restore()
  2272  
  2273  	// pretend some snaps are held
  2274  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil)
  2275  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil)
  2276  
  2277  	lastRefreshTime := time.Now().Add(-99 * time.Hour)
  2278  	st.Set("last-refresh", lastRefreshTime)
  2279  
  2280  	// pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed)
  2281  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil)
  2282  
  2283  	changes := st.Changes()
  2284  	c.Assert(changes, HasLen, 1)
  2285  	chg := changes[0]
  2286  	c.Assert(chg.Kind(), Equals, "auto-refresh")
  2287  	c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`)
  2288  	var snapNames []string
  2289  	var apiData map[string]interface{}
  2290  	c.Assert(chg.Get("snap-names", &snapNames), IsNil)
  2291  	c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"})
  2292  	c.Assert(chg.Get("api-data", &apiData), IsNil)
  2293  	c.Check(apiData, DeepEquals, map[string]interface{}{
  2294  		"snap-names": []interface{}{"base-snap-b", "snap-b"},
  2295  	})
  2296  
  2297  	tasks := chg.Tasks()
  2298  	c.Assert(tasks, HasLen, 2)
  2299  	conditionalRefreshTask := tasks[0]
  2300  	checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{
  2301  		"base-snap-b": {
  2302  			SnapSetup: snapstate.SnapSetup{
  2303  				Type:      "base",
  2304  				PlugsOnly: true,
  2305  				Flags: snapstate.Flags{
  2306  					IsAutoRefresh: true,
  2307  				},
  2308  				SideInfo: &snap.SideInfo{
  2309  					RealName: "base-snap-b",
  2310  					Revision: snap.R(3),
  2311  				},
  2312  				DownloadInfo: &snap.DownloadInfo{},
  2313  			},
  2314  		},
  2315  		"snap-b": {
  2316  			SnapSetup: snapstate.SnapSetup{
  2317  				Type:      "app",
  2318  				Base:      "base-snap-b",
  2319  				PlugsOnly: true,
  2320  				Flags: snapstate.Flags{
  2321  					IsAutoRefresh: true,
  2322  				},
  2323  				SideInfo: &snap.SideInfo{
  2324  					RealName: "snap-b",
  2325  					Revision: snap.R(2),
  2326  				},
  2327  				DownloadInfo: &snap.DownloadInfo{},
  2328  			},
  2329  		},
  2330  	})
  2331  
  2332  	// the gate-auto-refresh hook task for snap-b is present
  2333  	c.Check(tasks[1].Kind(), Equals, "run-hook")
  2334  	var hs hookstate.HookSetup
  2335  	c.Assert(tasks[1].Get("hook-setup", &hs), IsNil)
  2336  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  2337  	c.Check(hs.Snap, Equals, "snap-b")
  2338  	c.Check(hs.Optional, Equals, true)
  2339  
  2340  	// last-refresh wasn't modified
  2341  	var lr time.Time
  2342  	st.Get("last-refresh", &lr)
  2343  	c.Check(lr.Equal(lastRefreshTime), Equals, true)
  2344  }
  2345  
  2346  func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapMoreAffectedSnaps(c *C) {
  2347  	s.store.refreshedSnaps = []*snap.Info{{
  2348  		Architectures: []string{"all"},
  2349  		SnapType:      snap.TypeApp,
  2350  		SideInfo: snap.SideInfo{
  2351  			RealName: "snap-a",
  2352  			Revision: snap.R(8),
  2353  		},
  2354  	}, {
  2355  		Architectures: []string{"all"},
  2356  		SnapType:      snap.TypeBase,
  2357  		SideInfo: snap.SideInfo{
  2358  			RealName: "base-snap-b",
  2359  			Revision: snap.R(3),
  2360  		},
  2361  	}, {
  2362  		Architectures: []string{"all"},
  2363  		SnapType:      snap.TypeApp,
  2364  		Base:          "base-snap-b",
  2365  		SideInfo: snap.SideInfo{
  2366  			RealName: "snap-b",
  2367  			Revision: snap.R(2),
  2368  		},
  2369  	}}
  2370  
  2371  	st := s.state
  2372  	st.Lock()
  2373  	defer st.Unlock()
  2374  
  2375  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2376  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2377  	mockInstalledSnap(c, s.state, snapBByaml, useHook)
  2378  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2379  
  2380  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2381  	defer restore()
  2382  
  2383  	// pretend snap-b holds base-snap-b.
  2384  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil)
  2385  
  2386  	// pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed)
  2387  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil)
  2388  
  2389  	changes := st.Changes()
  2390  	c.Assert(changes, HasLen, 1)
  2391  	chg := changes[0]
  2392  	c.Assert(chg.Kind(), Equals, "auto-refresh")
  2393  	c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`)
  2394  	var snapNames []string
  2395  	var apiData map[string]interface{}
  2396  	c.Assert(chg.Get("snap-names", &snapNames), IsNil)
  2397  	c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"})
  2398  	c.Assert(chg.Get("api-data", &apiData), IsNil)
  2399  	c.Check(apiData, DeepEquals, map[string]interface{}{
  2400  		"snap-names": []interface{}{"base-snap-b", "snap-b"},
  2401  	})
  2402  
  2403  	tasks := chg.Tasks()
  2404  	c.Assert(tasks, HasLen, 3)
  2405  	conditionalRefreshTask := tasks[0]
  2406  	checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{
  2407  		"base-snap-b": {
  2408  			SnapSetup: snapstate.SnapSetup{
  2409  				Type:      "base",
  2410  				PlugsOnly: true,
  2411  				Flags: snapstate.Flags{
  2412  					IsAutoRefresh: true,
  2413  				},
  2414  				SideInfo: &snap.SideInfo{
  2415  					RealName: "base-snap-b",
  2416  					Revision: snap.R(3),
  2417  				},
  2418  				DownloadInfo: &snap.DownloadInfo{},
  2419  			},
  2420  		},
  2421  		"snap-b": {
  2422  			SnapSetup: snapstate.SnapSetup{
  2423  				Type:      "app",
  2424  				Base:      "base-snap-b",
  2425  				PlugsOnly: true,
  2426  				Flags: snapstate.Flags{
  2427  					IsAutoRefresh: true,
  2428  				},
  2429  				SideInfo: &snap.SideInfo{
  2430  					RealName: "snap-b",
  2431  					Revision: snap.R(2),
  2432  				},
  2433  				DownloadInfo: &snap.DownloadInfo{},
  2434  			},
  2435  		},
  2436  	})
  2437  
  2438  	seenSnaps := make(map[string]bool)
  2439  
  2440  	// check that the gate-auto-refresh hooks are run.
  2441  	// snap-bb's hook is triggered because it is affected by base-snap-b refresh
  2442  	// (and intersects with affecting snap of snap-b). Note, snap-a is not here
  2443  	// because it is not affected by snaps affecting snap-b.
  2444  	for i := 1; i <= 2; i++ {
  2445  		c.Assert(tasks[i].Kind(), Equals, "run-hook")
  2446  		var hs hookstate.HookSetup
  2447  		c.Assert(tasks[i].Get("hook-setup", &hs), IsNil)
  2448  		c.Check(hs.Hook, Equals, "gate-auto-refresh")
  2449  		c.Check(hs.Optional, Equals, true)
  2450  		seenSnaps[hs.Snap] = true
  2451  	}
  2452  	c.Check(seenSnaps, DeepEquals, map[string]bool{
  2453  		"snap-b":  true,
  2454  		"snap-bb": true,
  2455  	})
  2456  }
  2457  
  2458  func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapNoCandidatesAnymore(c *C) {
  2459  	// only snap-a will have a refresh available
  2460  	s.store.refreshedSnaps = []*snap.Info{{
  2461  		Architectures: []string{"all"},
  2462  		SnapType:      snap.TypeApp,
  2463  		SideInfo: snap.SideInfo{
  2464  			RealName: "snap-a",
  2465  			Revision: snap.R(8),
  2466  		},
  2467  	}}
  2468  
  2469  	logbuf, restoreLogger := logger.MockLogger()
  2470  	defer restoreLogger()
  2471  
  2472  	st := s.state
  2473  	st.Lock()
  2474  	defer st.Unlock()
  2475  
  2476  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2477  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2478  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2479  
  2480  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2481  	defer restore()
  2482  
  2483  	// pretend some snaps are held
  2484  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil)
  2485  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil)
  2486  
  2487  	// pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed)
  2488  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil)
  2489  	c.Assert(st.Changes(), HasLen, 0)
  2490  
  2491  	// but base-snap-b has no update anymore.
  2492  	c.Check(logbuf.String(), testutil.Contains, `auto-refresh: all snaps previously held by "snap-b" are up-to-date`)
  2493  }
  2494  
  2495  func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) {
  2496  	c.Assert(len(tasks), Equals, len(expected))
  2497  	for i, t := range tasks {
  2498  		var got string
  2499  		if t.Kind() == "run-hook" {
  2500  			var hsup hookstate.HookSetup
  2501  			c.Assert(t.Get("hook-setup", &hsup), IsNil)
  2502  			got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook)
  2503  		} else {
  2504  			got = t.Kind()
  2505  		}
  2506  		c.Assert(got, Equals, expected[i], Commentf("#%d", i))
  2507  	}
  2508  }