github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/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  func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) {
   585  	st := s.state
   586  	st.Lock()
   587  	defer st.Unlock()
   588  
   589  	restore := snapstate.MockTimeNow(func() time.Time {
   590  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   591  		c.Assert(err, IsNil)
   592  		return t
   593  	})
   594  	defer restore()
   595  
   596  	mockInstalledSnap(c, st, snapAyaml, false)
   597  	mockInstalledSnap(c, st, snapByaml, false)
   598  	mockInstalledSnap(c, st, snapCyaml, false)
   599  	mockInstalledSnap(c, st, snapDyaml, false)
   600  
   601  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   602  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   603  	// sanity
   604  	held, err := snapstate.HeldSnaps(st)
   605  	c.Assert(err, IsNil)
   606  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true})
   607  
   608  	candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}}
   609  
   610  	// only snap-c has a refresh candidate, snap-b and snap-d should be forgotten.
   611  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   612  	var gating map[string]map[string]*snapstate.HoldState
   613  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   614  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   615  		"snap-c": {
   616  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   617  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   618  		},
   619  	})
   620  	held, err = snapstate.HeldSnaps(st)
   621  	c.Assert(err, IsNil)
   622  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true})
   623  }
   624  
   625  func (s *autorefreshGatingSuite) TestPruneGatingHelperNoGating(c *C) {
   626  	st := s.state
   627  	st.Lock()
   628  	defer st.Unlock()
   629  
   630  	restore := snapstate.MockTimeNow(func() time.Time {
   631  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   632  		c.Assert(err, IsNil)
   633  		return t
   634  	})
   635  	defer restore()
   636  
   637  	mockInstalledSnap(c, st, snapAyaml, false)
   638  
   639  	held, err := snapstate.HeldSnaps(st)
   640  	c.Assert(err, IsNil)
   641  	c.Check(held, HasLen, 0)
   642  
   643  	snapstate.MockTimeNow(func() time.Time {
   644  		c.Fatalf("not expected")
   645  		return time.Time{}
   646  	})
   647  
   648  	candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}}
   649  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   650  	held, err = snapstate.HeldSnaps(st)
   651  	c.Assert(err, IsNil)
   652  	c.Check(held, HasLen, 0)
   653  }
   654  
   655  func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) {
   656  	st := s.state
   657  	st.Lock()
   658  	defer st.Unlock()
   659  
   660  	restore := snapstate.MockTimeNow(func() time.Time {
   661  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   662  		c.Assert(err, IsNil)
   663  		return t
   664  	})
   665  	defer restore()
   666  
   667  	mockInstalledSnap(c, st, snapAyaml, false)
   668  	mockInstalledSnap(c, st, snapByaml, false)
   669  	mockInstalledSnap(c, st, snapCyaml, false)
   670  	mockInstalledSnap(c, st, snapDyaml, false)
   671  
   672  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   673  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   674  
   675  	c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil)
   676  	var gating map[string]map[string]*snapstate.HoldState
   677  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   678  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   679  		"snap-d": {
   680  			// holding self set for maxPostponement (95 days - buffer = 90 days)
   681  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"),
   682  		},
   683  	})
   684  
   685  	held, err := snapstate.HeldSnaps(st)
   686  	c.Assert(err, IsNil)
   687  	c.Check(held, DeepEquals, map[string]bool{"snap-d": true})
   688  }
   689  
   690  func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) {
   691  	st := s.state
   692  	st.Lock()
   693  	defer st.Unlock()
   694  
   695  	mockInstalledSnap(c, st, snapAyaml, false)
   696  	mockInstalledSnap(c, st, snapByaml, false)
   697  	mockInstalledSnap(c, st, snapCyaml, false)
   698  	mockInstalledSnap(c, st, snapDyaml, false)
   699  
   700  	// snap-a is holding itself and 3 other snaps
   701  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-d"), IsNil)
   702  	// in addition, snap-c is held by snap-d.
   703  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil)
   704  
   705  	// sanity check
   706  	held, err := snapstate.HeldSnaps(st)
   707  	c.Assert(err, IsNil)
   708  	c.Check(held, DeepEquals, map[string]bool{
   709  		"snap-a": true,
   710  		"snap-b": true,
   711  		"snap-c": true,
   712  		"snap-d": true,
   713  	})
   714  
   715  	c.Check(snapstate.PruneSnapsHold(st, "snap-a"), IsNil)
   716  
   717  	// after pruning snap-a, snap-c is still held.
   718  	held, err = snapstate.HeldSnaps(st)
   719  	c.Assert(err, IsNil)
   720  	c.Check(held, DeepEquals, map[string]bool{
   721  		"snap-c": true,
   722  	})
   723  	var gating map[string]map[string]*snapstate.HoldState
   724  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   725  	c.Assert(gating, HasLen, 1)
   726  	c.Check(gating["snap-c"], HasLen, 1)
   727  	c.Check(gating["snap-c"]["snap-d"], NotNil)
   728  }
   729  
   730  const useHook = true
   731  const noHook = false
   732  
   733  func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) {
   734  	c.Assert(task.Kind(), Equals, "conditional-auto-refresh")
   735  	var snaps map[string]*snapstate.RefreshCandidate
   736  	c.Assert(task.Get("snaps", &snaps), IsNil)
   737  	c.Check(snaps, DeepEquals, expected)
   738  }
   739  
   740  func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) {
   741  	restore := release.MockOnClassic(true)
   742  	defer restore()
   743  
   744  	st := s.state
   745  
   746  	st.Lock()
   747  	defer st.Unlock()
   748  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
   749  	baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook)
   750  	// unrelated snaps
   751  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   752  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
   753  
   754  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   755  
   756  	updates := []string{baseSnapA.InstanceName()}
   757  	affected, err := snapstate.AffectedByRefresh(st, updates)
   758  	c.Assert(err, IsNil)
   759  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   760  		"snap-a": {
   761  			Base: true,
   762  			AffectingSnaps: map[string]bool{
   763  				"base-snap-a": true,
   764  			}}})
   765  }
   766  
   767  func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) {
   768  	restore := release.MockOnClassic(true)
   769  	defer restore()
   770  
   771  	st := s.state
   772  
   773  	st.Lock()
   774  	defer st.Unlock()
   775  	snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook)
   776  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   777  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   778  
   779  	c.Assert(s.repo.AddSnap(core), IsNil)
   780  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   781  	c.Assert(s.repo.AddSnap(snapC), IsNil)
   782  
   783  	updates := []string{core.InstanceName()}
   784  	affected, err := snapstate.AffectedByRefresh(st, updates)
   785  	c.Assert(err, IsNil)
   786  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   787  		"snap-c": {
   788  			Base: true,
   789  			AffectingSnaps: map[string]bool{
   790  				"core": true,
   791  			}}})
   792  }
   793  
   794  func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) {
   795  	restore := release.MockOnClassic(true)
   796  	defer restore()
   797  
   798  	st := s.state
   799  
   800  	st.Lock()
   801  	defer st.Unlock()
   802  	kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook)
   803  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   804  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   805  
   806  	updates := []string{kernel.InstanceName()}
   807  	affected, err := snapstate.AffectedByRefresh(st, updates)
   808  	c.Assert(err, IsNil)
   809  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   810  		"snap-c": {
   811  			Restart: true,
   812  			AffectingSnaps: map[string]bool{
   813  				"kernel": true,
   814  			}}})
   815  }
   816  
   817  func (s *autorefreshGatingSuite) TestAffectedBySelf(c *C) {
   818  	restore := release.MockOnClassic(true)
   819  	defer restore()
   820  
   821  	st := s.state
   822  
   823  	st.Lock()
   824  	defer st.Unlock()
   825  
   826  	snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook)
   827  	updates := []string{snapC.InstanceName()}
   828  	affected, err := snapstate.AffectedByRefresh(st, updates)
   829  	c.Assert(err, IsNil)
   830  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   831  		"snap-c": {
   832  			AffectingSnaps: map[string]bool{
   833  				"snap-c": true,
   834  			}}})
   835  }
   836  
   837  func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) {
   838  	restore := release.MockOnClassic(true)
   839  	defer restore()
   840  
   841  	st := s.state
   842  
   843  	st.Lock()
   844  	defer st.Unlock()
   845  	kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook)
   846  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   847  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   848  
   849  	updates := []string{kernel.InstanceName()}
   850  	affected, err := snapstate.AffectedByRefresh(st, updates)
   851  	c.Assert(err, IsNil)
   852  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   853  		"snap-c": {
   854  			Restart: true,
   855  			AffectingSnaps: map[string]bool{
   856  				"gadget": true,
   857  			}}})
   858  }
   859  
   860  func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) {
   861  	restore := release.MockOnClassic(true)
   862  	defer restore()
   863  
   864  	st := s.state
   865  
   866  	st.Lock()
   867  	defer st.Unlock()
   868  
   869  	snapD := mockInstalledSnap(c, s.state, snapDyaml, noHook)
   870  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   871  	// unrelated snap
   872  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   873  
   874  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   875  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   876  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   877  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   878  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   879  	c.Assert(err, IsNil)
   880  
   881  	updates := []string{snapD.InstanceName()}
   882  	affected, err := snapstate.AffectedByRefresh(st, updates)
   883  	c.Assert(err, IsNil)
   884  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   885  		"snap-e": {
   886  			Restart: true,
   887  			AffectingSnaps: map[string]bool{
   888  				"snap-d": true,
   889  			}}})
   890  }
   891  
   892  func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) {
   893  	restore := release.MockOnClassic(true)
   894  	defer restore()
   895  
   896  	st := s.state
   897  
   898  	st.Lock()
   899  	defer st.Unlock()
   900  
   901  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   902  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   903  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   904  
   905  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   906  	c.Assert(s.repo.AddSnap(core), IsNil)
   907  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   908  
   909  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}}
   910  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   911  	c.Assert(err, IsNil)
   912  
   913  	updates := []string{core.InstanceName()}
   914  	affected, err := snapstate.AffectedByRefresh(st, updates)
   915  	c.Assert(err, IsNil)
   916  	c.Check(affected, HasLen, 0)
   917  }
   918  
   919  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackend(c *C) {
   920  	restore := release.MockOnClassic(true)
   921  	defer restore()
   922  
   923  	st := s.state
   924  
   925  	st.Lock()
   926  	defer st.Unlock()
   927  
   928  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   929  	snapE := mockInstalledSnap(c, s.state, snapEyaml, noHook)
   930  	// unrelated snap
   931  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   932  
   933  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   934  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   935  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   936  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   937  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   938  	c.Assert(err, IsNil)
   939  
   940  	// snapE has a plug using mount backend and is refreshed, this affects slot of snap-d.
   941  	updates := []string{snapE.InstanceName()}
   942  	affected, err := snapstate.AffectedByRefresh(st, updates)
   943  	c.Assert(err, IsNil)
   944  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   945  		"snap-d": {
   946  			Restart: true,
   947  			AffectingSnaps: map[string]bool{
   948  				"snap-e": true,
   949  			}}})
   950  }
   951  
   952  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) {
   953  	restore := release.MockOnClassic(true)
   954  	defer restore()
   955  
   956  	st := s.state
   957  
   958  	st.Lock()
   959  	defer st.Unlock()
   960  
   961  	snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, noHook)
   962  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   963  	// unrelated snap
   964  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   965  
   966  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   967  	c.Assert(s.repo.AddSnap(snapdSnap), IsNil)
   968  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   969  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}}
   970  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   971  	c.Assert(err, IsNil)
   972  
   973  	// snapE has a plug using mount backend, refreshing snapd affects snapE.
   974  	updates := []string{snapdSnap.InstanceName()}
   975  	affected, err := snapstate.AffectedByRefresh(st, updates)
   976  	c.Assert(err, IsNil)
   977  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   978  		"snap-g": {
   979  			Restart: true,
   980  			AffectingSnaps: map[string]bool{
   981  				"snapd": true,
   982  			}}})
   983  }
   984  
   985  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(c *C) {
   986  	restore := release.MockOnClassic(true)
   987  	defer restore()
   988  
   989  	st := s.state
   990  
   991  	st.Lock()
   992  	defer st.Unlock()
   993  
   994  	coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook)
   995  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   996  
   997  	c.Assert(s.repo.AddSnap(coreSnap), IsNil)
   998  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   999  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}}
  1000  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
  1001  	c.Assert(err, IsNil)
  1002  
  1003  	// snapG has a plug using mount backend, refreshing core affects snapE.
  1004  	updates := []string{coreSnap.InstanceName()}
  1005  	affected, err := snapstate.AffectedByRefresh(st, updates)
  1006  	c.Assert(err, IsNil)
  1007  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1008  		"snap-g": {
  1009  			Restart: true,
  1010  			AffectingSnaps: map[string]bool{
  1011  				"core": true,
  1012  			}}})
  1013  }
  1014  
  1015  func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) {
  1016  	restore := release.MockOnClassic(false)
  1017  	defer restore()
  1018  
  1019  	st := s.state
  1020  
  1021  	r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
  1022  	defer r()
  1023  
  1024  	st.Lock()
  1025  	defer st.Unlock()
  1026  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1027  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1028  	mockInstalledSnap(c, s.state, snapDyaml, useHook)
  1029  	mockInstalledSnap(c, s.state, snapEyaml, useHook)
  1030  	core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook)
  1031  
  1032  	updates := []string{core18.InstanceName()}
  1033  	affected, err := snapstate.AffectedByRefresh(st, updates)
  1034  	c.Assert(err, IsNil)
  1035  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1036  		"snap-a": {
  1037  			Base:    false,
  1038  			Restart: true,
  1039  			AffectingSnaps: map[string]bool{
  1040  				"core18": true,
  1041  			},
  1042  		},
  1043  		"snap-b": {
  1044  			Base:    false,
  1045  			Restart: true,
  1046  			AffectingSnaps: map[string]bool{
  1047  				"core18": true,
  1048  			},
  1049  		},
  1050  		"snap-d": {
  1051  			Base:    false,
  1052  			Restart: true,
  1053  			AffectingSnaps: map[string]bool{
  1054  				"core18": true,
  1055  			},
  1056  		},
  1057  		"snap-e": {
  1058  			Base:    false,
  1059  			Restart: true,
  1060  			AffectingSnaps: map[string]bool{
  1061  				"core18": true,
  1062  			}}})
  1063  }
  1064  
  1065  func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) {
  1066  	st := s.state
  1067  	st.Lock()
  1068  	defer st.Unlock()
  1069  
  1070  	affected := map[string]*snapstate.AffectedSnapInfo{
  1071  		"snap-a": {
  1072  			Base:    true,
  1073  			Restart: true,
  1074  			AffectingSnaps: map[string]bool{
  1075  				"snap-c": true,
  1076  				"snap-d": true,
  1077  			},
  1078  		},
  1079  		"snap-b": {
  1080  			AffectingSnaps: map[string]bool{
  1081  				"snap-e": true,
  1082  				"snap-f": true,
  1083  			},
  1084  		},
  1085  	}
  1086  
  1087  	seenSnaps := make(map[string]bool)
  1088  
  1089  	ts := snapstate.CreateGateAutoRefreshHooks(st, affected)
  1090  	c.Assert(ts.Tasks(), HasLen, 2)
  1091  
  1092  	checkHook := func(t *state.Task) {
  1093  		c.Assert(t.Kind(), Equals, "run-hook")
  1094  		var hs hookstate.HookSetup
  1095  		c.Assert(t.Get("hook-setup", &hs), IsNil)
  1096  		c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1097  		c.Check(hs.Optional, Equals, true)
  1098  		seenSnaps[hs.Snap] = true
  1099  
  1100  		var data interface{}
  1101  		c.Assert(t.Get("hook-context", &data), IsNil)
  1102  
  1103  		// the order of hook tasks is not deterministic
  1104  		if hs.Snap == "snap-a" {
  1105  			c.Check(data, DeepEquals, map[string]interface{}{
  1106  				"base":            true,
  1107  				"restart":         true,
  1108  				"affecting-snaps": []interface{}{"snap-c", "snap-d"}})
  1109  		} else {
  1110  			c.Assert(hs.Snap, Equals, "snap-b")
  1111  			c.Check(data, DeepEquals, map[string]interface{}{
  1112  				"base":            false,
  1113  				"restart":         false,
  1114  				"affecting-snaps": []interface{}{"snap-e", "snap-f"}})
  1115  		}
  1116  	}
  1117  
  1118  	checkHook(ts.Tasks()[0])
  1119  	checkHook(ts.Tasks()[1])
  1120  
  1121  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1122  }
  1123  
  1124  func (s *autorefreshGatingSuite) TestAffectedByRefreshCandidates(c *C) {
  1125  	st := s.state
  1126  	st.Lock()
  1127  	defer st.Unlock()
  1128  
  1129  	mockInstalledSnap(c, st, snapAyaml, useHook)
  1130  	// unrelated snap
  1131  	mockInstalledSnap(c, st, snapByaml, useHook)
  1132  
  1133  	// no refresh-candidates in state
  1134  	affected, err := snapstate.AffectedByRefreshCandidates(st)
  1135  	c.Assert(err, IsNil)
  1136  	c.Check(affected, HasLen, 0)
  1137  
  1138  	candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}}
  1139  	st.Set("refresh-candidates", &candidates)
  1140  
  1141  	affected, err = snapstate.AffectedByRefreshCandidates(st)
  1142  	c.Assert(err, IsNil)
  1143  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
  1144  		"snap-a": {
  1145  			AffectingSnaps: map[string]bool{
  1146  				"snap-a": true,
  1147  			}}})
  1148  }
  1149  
  1150  func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) {
  1151  	st := s.state
  1152  	st.Lock()
  1153  	defer st.Unlock()
  1154  
  1155  	st.Set("seeded", true)
  1156  
  1157  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1158  	defer restore()
  1159  
  1160  	snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
  1161  		return nil, nil
  1162  	}
  1163  	defer func() { snapstate.AutoAliases = nil }()
  1164  
  1165  	s.store.refreshedSnaps = []*snap.Info{{
  1166  		Architectures: []string{"all"},
  1167  		SnapType:      snap.TypeApp,
  1168  		SideInfo: snap.SideInfo{
  1169  			RealName: "snap-a",
  1170  			Revision: snap.R(8),
  1171  		},
  1172  	}}
  1173  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1174  
  1175  	// gate-auto-refresh-hook feature not enabled, expect old-style refresh.
  1176  	_, tss, err := snapstate.AutoRefresh(context.TODO(), st)
  1177  	c.Check(err, IsNil)
  1178  	c.Assert(tss, HasLen, 2)
  1179  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites")
  1180  	c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap")
  1181  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh")
  1182  
  1183  	// enable gate-auto-refresh-hook feature
  1184  	tr := config.NewTransaction(s.state)
  1185  	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
  1186  	tr.Commit()
  1187  
  1188  	_, tss, err = snapstate.AutoRefresh(context.TODO(), st)
  1189  	c.Check(err, IsNil)
  1190  	c.Assert(tss, HasLen, 2)
  1191  	task := tss[0].Tasks()[0]
  1192  	c.Check(task.Kind(), Equals, "conditional-auto-refresh")
  1193  	var toUpdate map[string]*snapstate.RefreshCandidate
  1194  	c.Assert(task.Get("snaps", &toUpdate), IsNil)
  1195  	seenSnaps := make(map[string]bool)
  1196  	for up := range toUpdate {
  1197  		seenSnaps[up] = true
  1198  	}
  1199  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1200  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook")
  1201  }
  1202  
  1203  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) {
  1204  	s.store.refreshedSnaps = []*snap.Info{{
  1205  		Architectures: []string{"all"},
  1206  		SnapType:      snap.TypeApp,
  1207  		SideInfo: snap.SideInfo{
  1208  			RealName: "snap-a",
  1209  			Revision: snap.R(8),
  1210  		},
  1211  	}, {
  1212  		Architectures: []string{"all"},
  1213  		SnapType:      snap.TypeBase,
  1214  		SideInfo: snap.SideInfo{
  1215  			RealName: "base-snap-b",
  1216  			Revision: snap.R(3),
  1217  		},
  1218  	}, {
  1219  		Architectures: []string{"all"},
  1220  		SnapType:      snap.TypeApp,
  1221  		SideInfo: snap.SideInfo{
  1222  			RealName: "snap-c",
  1223  			Revision: snap.R(5),
  1224  		},
  1225  	}}
  1226  
  1227  	st := s.state
  1228  	st.Lock()
  1229  	defer st.Unlock()
  1230  
  1231  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1232  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1233  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1234  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1235  	mockInstalledSnap(c, s.state, snapDyaml, noHook)
  1236  
  1237  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1238  	defer restore()
  1239  
  1240  	// pretend some snaps are held
  1241  	c.Assert(snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d"), IsNil)
  1242  	// sanity check
  1243  	heldSnaps, err := snapstate.HeldSnaps(st)
  1244  	c.Assert(err, IsNil)
  1245  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1246  		"snap-a": true,
  1247  		"snap-d": true,
  1248  	})
  1249  
  1250  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1251  	c.Assert(err, IsNil)
  1252  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"})
  1253  	c.Assert(tss, HasLen, 2)
  1254  
  1255  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1256  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1257  		"snap-a": {
  1258  			SnapSetup: snapstate.SnapSetup{
  1259  				Type:      "app",
  1260  				PlugsOnly: true,
  1261  				Flags: snapstate.Flags{
  1262  					IsAutoRefresh: true,
  1263  				},
  1264  				SideInfo: &snap.SideInfo{
  1265  					RealName: "snap-a",
  1266  					Revision: snap.R(8),
  1267  				},
  1268  				DownloadInfo: &snap.DownloadInfo{},
  1269  			},
  1270  		},
  1271  		"base-snap-b": {
  1272  			SnapSetup: snapstate.SnapSetup{
  1273  				Type:      "base",
  1274  				PlugsOnly: true,
  1275  				Flags: snapstate.Flags{
  1276  					IsAutoRefresh: true,
  1277  				},
  1278  				SideInfo: &snap.SideInfo{
  1279  					RealName: "base-snap-b",
  1280  					Revision: snap.R(3),
  1281  				},
  1282  				DownloadInfo: &snap.DownloadInfo{},
  1283  			},
  1284  		},
  1285  		"snap-c": {
  1286  			SnapSetup: snapstate.SnapSetup{
  1287  				Type:      "app",
  1288  				PlugsOnly: true,
  1289  				Flags: snapstate.Flags{
  1290  					IsAutoRefresh: true,
  1291  				},
  1292  				SideInfo: &snap.SideInfo{
  1293  					RealName: "snap-c",
  1294  					Revision: snap.R(5),
  1295  				},
  1296  				DownloadInfo: &snap.DownloadInfo{},
  1297  			},
  1298  		},
  1299  	})
  1300  
  1301  	c.Assert(tss[1].Tasks(), HasLen, 2)
  1302  
  1303  	var snapAhookData, snapBhookData map[string]interface{}
  1304  
  1305  	// check hooks for affected snaps
  1306  	seenSnaps := make(map[string]bool)
  1307  	var hs hookstate.HookSetup
  1308  	task := tss[1].Tasks()[0]
  1309  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1310  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1311  	seenSnaps[hs.Snap] = true
  1312  	switch hs.Snap {
  1313  	case "snap-a":
  1314  		task.Get("hook-context", &snapAhookData)
  1315  	case "snap-b":
  1316  		task.Get("hook-context", &snapBhookData)
  1317  	default:
  1318  		c.Fatalf("unexpected snap %q", hs.Snap)
  1319  	}
  1320  
  1321  	task = tss[1].Tasks()[1]
  1322  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1323  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1324  	seenSnaps[hs.Snap] = true
  1325  	switch hs.Snap {
  1326  	case "snap-a":
  1327  		task.Get("hook-context", &snapAhookData)
  1328  	case "snap-b":
  1329  		task.Get("hook-context", &snapBhookData)
  1330  	default:
  1331  		c.Fatalf("unexpected snap %q", hs.Snap)
  1332  	}
  1333  
  1334  	c.Check(snapAhookData["affecting-snaps"], DeepEquals, []interface{}{"snap-a"})
  1335  	c.Check(snapBhookData["affecting-snaps"], DeepEquals, []interface{}{"base-snap-b"})
  1336  
  1337  	// hook for snap-a because it gets refreshed, for snap-b because its base
  1338  	// gets refreshed. snap-c is refreshed but doesn't have the hook.
  1339  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1340  
  1341  	// check that refresh-candidates in the state were updated
  1342  	var candidates map[string]*snapstate.RefreshCandidate
  1343  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1344  	c.Assert(candidates, HasLen, 3)
  1345  	c.Check(candidates["snap-a"], NotNil)
  1346  	c.Check(candidates["base-snap-b"], NotNil)
  1347  	c.Check(candidates["snap-c"], NotNil)
  1348  
  1349  	// check that after autoRefreshPhase1 any held snaps that are not in refresh
  1350  	// candidates got removed.
  1351  	heldSnaps, err = snapstate.HeldSnaps(st)
  1352  	c.Assert(err, IsNil)
  1353  	// snap-d got removed from held snaps.
  1354  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1355  		"snap-a": true,
  1356  	})
  1357  }
  1358  
  1359  // this test demonstrates that affectedByRefresh uses current snap info (not
  1360  // snap infos of store updates) by simulating a different base for the updated
  1361  // snap from the store.
  1362  func (s *autorefreshGatingSuite) TestAffectedByRefreshUsesCurrentSnapInfo(c *C) {
  1363  	s.store.refreshedSnaps = []*snap.Info{{
  1364  		Architectures: []string{"all"},
  1365  		SnapType:      snap.TypeBase,
  1366  		SideInfo: snap.SideInfo{
  1367  			RealName: "base-snap-b",
  1368  			Revision: snap.R(3),
  1369  		},
  1370  	}, {
  1371  		Architectures: []string{"all"},
  1372  		Base:          "new-base",
  1373  		SnapType:      snap.TypeApp,
  1374  		SideInfo: snap.SideInfo{
  1375  			RealName: "snap-b",
  1376  			Revision: snap.R(5),
  1377  		},
  1378  	}}
  1379  
  1380  	st := s.state
  1381  	st.Lock()
  1382  	defer st.Unlock()
  1383  
  1384  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1385  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1386  
  1387  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1388  	defer restore()
  1389  
  1390  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1391  	c.Assert(err, IsNil)
  1392  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-b"})
  1393  	c.Assert(tss, HasLen, 2)
  1394  
  1395  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1396  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1397  		"snap-b": {
  1398  			SnapSetup: snapstate.SnapSetup{
  1399  				Type:      "app",
  1400  				Base:      "new-base",
  1401  				PlugsOnly: true,
  1402  				Flags: snapstate.Flags{
  1403  					IsAutoRefresh: true,
  1404  				},
  1405  				SideInfo: &snap.SideInfo{
  1406  					RealName: "snap-b",
  1407  					Revision: snap.R(5),
  1408  				},
  1409  				DownloadInfo: &snap.DownloadInfo{},
  1410  			},
  1411  		},
  1412  		"base-snap-b": {
  1413  			SnapSetup: snapstate.SnapSetup{
  1414  				Type:      "base",
  1415  				PlugsOnly: true,
  1416  				Flags: snapstate.Flags{
  1417  					IsAutoRefresh: true,
  1418  				},
  1419  				SideInfo: &snap.SideInfo{
  1420  					RealName: "base-snap-b",
  1421  					Revision: snap.R(3),
  1422  				},
  1423  				DownloadInfo: &snap.DownloadInfo{},
  1424  			},
  1425  		},
  1426  	})
  1427  
  1428  	c.Assert(tss[1].Tasks(), HasLen, 1)
  1429  	var hs hookstate.HookSetup
  1430  	task := tss[1].Tasks()[0]
  1431  	c.Assert(task.Get("hook-setup", &hs), IsNil)
  1432  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1433  	c.Check(hs.Snap, Equals, "snap-b")
  1434  	var data interface{}
  1435  	c.Assert(task.Get("hook-context", &data), IsNil)
  1436  	c.Check(data, DeepEquals, map[string]interface{}{
  1437  		"base":            true,
  1438  		"restart":         false,
  1439  		"affecting-snaps": []interface{}{"base-snap-b", "snap-b"}})
  1440  
  1441  	// check that refresh-candidates in the state were updated
  1442  	var candidates map[string]*snapstate.RefreshCandidate
  1443  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1444  	c.Assert(candidates, HasLen, 2)
  1445  	c.Check(candidates["snap-b"], NotNil)
  1446  	c.Check(candidates["base-snap-b"], NotNil)
  1447  }
  1448  
  1449  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) {
  1450  	s.store.refreshedSnaps = []*snap.Info{{
  1451  		Architectures: []string{"all"},
  1452  		SnapType:      snap.TypeApp,
  1453  		SideInfo: snap.SideInfo{
  1454  			RealName: "snap-a",
  1455  			Revision: snap.R(8),
  1456  		},
  1457  	}, {
  1458  		Architectures: []string{"all"},
  1459  		SnapType:      snap.TypeBase,
  1460  		SideInfo: snap.SideInfo{
  1461  			RealName: "snap-c",
  1462  			Revision: snap.R(5),
  1463  		},
  1464  	}}
  1465  
  1466  	st := s.state
  1467  	st.Lock()
  1468  	defer st.Unlock()
  1469  
  1470  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1471  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1472  
  1473  	conflictChange := st.NewChange("conflicting change", "")
  1474  	conflictTask := st.NewTask("conflicting task", "")
  1475  	si := &snap.SideInfo{
  1476  		RealName: "snap-c",
  1477  		Revision: snap.R(1),
  1478  	}
  1479  	sup := snapstate.SnapSetup{SideInfo: si}
  1480  	conflictTask.Set("snap-setup", sup)
  1481  	conflictChange.AddTask(conflictTask)
  1482  
  1483  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1484  	defer restore()
  1485  
  1486  	logbuf, restoreLogger := logger.MockLogger()
  1487  	defer restoreLogger()
  1488  
  1489  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1490  	c.Assert(err, IsNil)
  1491  	c.Check(names, DeepEquals, []string{"snap-a"})
  1492  	c.Assert(tss, HasLen, 2)
  1493  
  1494  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1495  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1496  		"snap-a": {
  1497  			SnapSetup: snapstate.SnapSetup{
  1498  				Type:      "app",
  1499  				PlugsOnly: true,
  1500  				Flags: snapstate.Flags{
  1501  					IsAutoRefresh: true,
  1502  				},
  1503  				SideInfo: &snap.SideInfo{
  1504  					RealName: "snap-a",
  1505  					Revision: snap.R(8),
  1506  				},
  1507  				DownloadInfo: &snap.DownloadInfo{},
  1508  			}}})
  1509  
  1510  	c.Assert(tss[1].Tasks(), HasLen, 1)
  1511  
  1512  	c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`)
  1513  
  1514  	seenSnaps := make(map[string]bool)
  1515  	var hs hookstate.HookSetup
  1516  	c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil)
  1517  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1518  	seenSnaps[hs.Snap] = true
  1519  
  1520  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1521  
  1522  	// check that refresh-candidates in the state were updated
  1523  	var candidates map[string]*snapstate.RefreshCandidate
  1524  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1525  	c.Assert(candidates, HasLen, 2)
  1526  	c.Check(candidates["snap-a"], NotNil)
  1527  	c.Check(candidates["snap-c"], NotNil)
  1528  }
  1529  
  1530  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) {
  1531  	s.store.refreshedSnaps = []*snap.Info{{
  1532  		Architectures: []string{"all"},
  1533  		SnapType:      snap.TypeBase,
  1534  		SideInfo: snap.SideInfo{
  1535  			RealName: "base-snap-b",
  1536  			Revision: snap.R(3),
  1537  		},
  1538  	}, {
  1539  		Architectures: []string{"all"},
  1540  		SnapType:      snap.TypeBase,
  1541  		SideInfo: snap.SideInfo{
  1542  			RealName: "snap-c",
  1543  			Revision: snap.R(5),
  1544  		},
  1545  	}}
  1546  
  1547  	st := s.state
  1548  	st.Lock()
  1549  	defer st.Unlock()
  1550  
  1551  	mockInstalledSnap(c, s.state, snapByaml, noHook)
  1552  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1553  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1554  
  1555  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1556  	defer restore()
  1557  
  1558  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1559  	c.Assert(err, IsNil)
  1560  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"})
  1561  	c.Assert(tss, HasLen, 1)
  1562  
  1563  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1564  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh")
  1565  }
  1566  
  1567  func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) {
  1568  	info := &snap.Info{
  1569  		SuggestedName: name,
  1570  		SideInfo:      *si,
  1571  		Architectures: []string{"all"},
  1572  		SnapType:      snap.TypeApp,
  1573  		Epoch:         snap.Epoch{},
  1574  	}
  1575  	switch name {
  1576  	case "base-snap-b":
  1577  		info.SnapType = snap.TypeBase
  1578  	case "snap-a", "snap-b":
  1579  		info.Hooks = map[string]*snap.HookInfo{
  1580  			"gate-auto-refresh": {
  1581  				Name: "gate-auto-refresh",
  1582  				Snap: info,
  1583  			},
  1584  		}
  1585  		if name == "snap-b" {
  1586  			info.Base = "base-snap-b"
  1587  		}
  1588  	}
  1589  	return info, nil
  1590  }
  1591  
  1592  func (s *snapmgrTestSuite) testAutoRefreshPhase2(c *C, beforePhase1 func(), gateAutoRefreshHook func(snapName string), expected []string) *state.Change {
  1593  	st := s.state
  1594  	st.Lock()
  1595  	defer st.Unlock()
  1596  
  1597  	s.o.TaskRunner().AddHandler("run-hook", func(t *state.Task, tomb *tomb.Tomb) error {
  1598  		var hsup hookstate.HookSetup
  1599  		t.State().Lock()
  1600  		defer t.State().Unlock()
  1601  		c.Assert(t.Get("hook-setup", &hsup), IsNil)
  1602  		if hsup.Hook == "gate-auto-refresh" && gateAutoRefreshHook != nil {
  1603  			gateAutoRefreshHook(hsup.Snap)
  1604  		}
  1605  		return nil
  1606  	}, nil)
  1607  
  1608  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1609  		c.Fatal("unexpected call to installSize")
  1610  		return 0, nil
  1611  	})
  1612  	defer restoreInstallSize()
  1613  
  1614  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1615  		fakeStore: s.fakeStore,
  1616  		refreshedSnaps: []*snap.Info{{
  1617  			Architectures: []string{"all"},
  1618  			SnapType:      snap.TypeApp,
  1619  			SideInfo: snap.SideInfo{
  1620  				RealName: "snap-a",
  1621  				Revision: snap.R(8),
  1622  			},
  1623  		}, {
  1624  			Architectures: []string{"all"},
  1625  			SnapType:      snap.TypeBase,
  1626  			SideInfo: snap.SideInfo{
  1627  				RealName: "base-snap-b",
  1628  				Revision: snap.R(3),
  1629  			},
  1630  		}}})
  1631  
  1632  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1633  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1634  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1635  
  1636  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1637  
  1638  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1639  	defer restore()
  1640  
  1641  	if beforePhase1 != nil {
  1642  		beforePhase1()
  1643  	}
  1644  
  1645  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1646  	c.Assert(err, IsNil)
  1647  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1648  
  1649  	chg := s.state.NewChange("refresh", "...")
  1650  	for _, ts := range tss {
  1651  		chg.AddAll(ts)
  1652  	}
  1653  
  1654  	s.state.Unlock()
  1655  	defer s.se.Stop()
  1656  	s.settle(c)
  1657  	s.state.Lock()
  1658  
  1659  	c.Check(chg.Status(), Equals, state.DoneStatus)
  1660  	c.Check(chg.Err(), IsNil)
  1661  
  1662  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1663  
  1664  	return chg
  1665  }
  1666  
  1667  func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) {
  1668  	expected := []string{
  1669  		"conditional-auto-refresh",
  1670  		"run-hook [snap-a;gate-auto-refresh]",
  1671  		// snap-b hook is triggered because of base-snap-b refresh
  1672  		"run-hook [snap-b;gate-auto-refresh]",
  1673  		"prerequisites",
  1674  		"download-snap",
  1675  		"validate-snap",
  1676  		"mount-snap",
  1677  		"run-hook [base-snap-b;pre-refresh]",
  1678  		"stop-snap-services",
  1679  		"remove-aliases",
  1680  		"unlink-current-snap",
  1681  		"copy-snap-data",
  1682  		"setup-profiles",
  1683  		"link-snap",
  1684  		"auto-connect",
  1685  		"set-auto-aliases",
  1686  		"setup-aliases",
  1687  		"run-hook [base-snap-b;post-refresh]",
  1688  		"start-snap-services",
  1689  		"cleanup",
  1690  		"run-hook [base-snap-b;check-health]",
  1691  		"prerequisites",
  1692  		"download-snap",
  1693  		"validate-snap",
  1694  		"mount-snap",
  1695  		"run-hook [snap-a;pre-refresh]",
  1696  		"stop-snap-services",
  1697  		"remove-aliases",
  1698  		"unlink-current-snap",
  1699  		"copy-snap-data",
  1700  		"setup-profiles",
  1701  		"link-snap",
  1702  		"auto-connect",
  1703  		"set-auto-aliases",
  1704  		"setup-aliases",
  1705  		"run-hook [snap-a;post-refresh]",
  1706  		"start-snap-services",
  1707  		"cleanup",
  1708  		"run-hook [snap-a;configure]",
  1709  		"run-hook [snap-a;check-health]",
  1710  		"check-rerefresh",
  1711  	}
  1712  
  1713  	seenSnapsWithGateAutoRefreshHook := make(map[string]bool)
  1714  
  1715  	chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1716  		seenSnapsWithGateAutoRefreshHook[snapName] = true
  1717  	}, expected)
  1718  
  1719  	c.Check(seenSnapsWithGateAutoRefreshHook, DeepEquals, map[string]bool{
  1720  		"snap-a": true,
  1721  		"snap-b": true,
  1722  	})
  1723  
  1724  	s.state.Lock()
  1725  	defer s.state.Unlock()
  1726  
  1727  	tasks := chg.Tasks()
  1728  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b", "snap-a" as needed`)
  1729  
  1730  	// all snaps refreshed, all removed from refresh-candidates.
  1731  	var candidates map[string]*snapstate.RefreshCandidate
  1732  	c.Assert(s.state.Get("refresh-candidates", &candidates), IsNil)
  1733  	c.Assert(candidates, HasLen, 0)
  1734  }
  1735  
  1736  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Held(c *C) {
  1737  	logbuf, restoreLogger := logger.MockLogger()
  1738  	defer restoreLogger()
  1739  
  1740  	expected := []string{
  1741  		"conditional-auto-refresh",
  1742  		"run-hook [snap-a;gate-auto-refresh]",
  1743  		// snap-b hook is triggered because of base-snap-b refresh
  1744  		"run-hook [snap-b;gate-auto-refresh]",
  1745  		"prerequisites",
  1746  		"download-snap",
  1747  		"validate-snap",
  1748  		"mount-snap",
  1749  		"run-hook [snap-a;pre-refresh]",
  1750  		"stop-snap-services",
  1751  		"remove-aliases",
  1752  		"unlink-current-snap",
  1753  		"copy-snap-data",
  1754  		"setup-profiles",
  1755  		"link-snap",
  1756  		"auto-connect",
  1757  		"set-auto-aliases",
  1758  		"setup-aliases",
  1759  		"run-hook [snap-a;post-refresh]",
  1760  		"start-snap-services",
  1761  		"cleanup",
  1762  		"run-hook [snap-a;configure]",
  1763  		"run-hook [snap-a;check-health]",
  1764  		"check-rerefresh",
  1765  	}
  1766  
  1767  	chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1768  		if snapName == "snap-b" {
  1769  			// pretend than snap-b calls snapctl --hold to hold refresh of base-snap-b
  1770  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1771  		}
  1772  	}, expected)
  1773  
  1774  	s.state.Lock()
  1775  	defer s.state.Unlock()
  1776  
  1777  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1778  	tasks := chg.Tasks()
  1779  	// no re-refresh for base-snap-b because it was held.
  1780  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "snap-a" as needed`)
  1781  }
  1782  
  1783  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Proceed(c *C) {
  1784  	logbuf, restoreLogger := logger.MockLogger()
  1785  	defer restoreLogger()
  1786  
  1787  	expected := []string{
  1788  		"conditional-auto-refresh",
  1789  		"run-hook [snap-a;gate-auto-refresh]",
  1790  		// snap-b hook is triggered because of base-snap-b refresh
  1791  		"run-hook [snap-b;gate-auto-refresh]",
  1792  		"prerequisites",
  1793  		"download-snap",
  1794  		"validate-snap",
  1795  		"mount-snap",
  1796  		"run-hook [snap-a;pre-refresh]",
  1797  		"stop-snap-services",
  1798  		"remove-aliases",
  1799  		"unlink-current-snap",
  1800  		"copy-snap-data",
  1801  		"setup-profiles",
  1802  		"link-snap",
  1803  		"auto-connect",
  1804  		"set-auto-aliases",
  1805  		"setup-aliases",
  1806  		"run-hook [snap-a;post-refresh]",
  1807  		"start-snap-services",
  1808  		"cleanup",
  1809  		"run-hook [snap-a;configure]",
  1810  		"run-hook [snap-a;check-health]",
  1811  		"check-rerefresh",
  1812  	}
  1813  
  1814  	s.testAutoRefreshPhase2(c, func() {
  1815  		// pretend that snap-a and base-snap-b are initially held
  1816  		c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1817  		c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1818  	}, func(snapName string) {
  1819  		if snapName == "snap-a" {
  1820  			// pretend than snap-a calls snapctl --proceed
  1821  			c.Assert(snapstate.ProceedWithRefresh(s.state, "snap-a"), IsNil)
  1822  		}
  1823  		// note, do nothing about snap-b which just keeps its hold state in
  1824  		// the test, but if we were using real gate-auto-refresh hook
  1825  		// handler, the default behavior for snap-b if it doesn't call --hold
  1826  		// would be to proceed (hook handler would take care of that).
  1827  	}, expected)
  1828  
  1829  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1830  }
  1831  
  1832  func (s *snapmgrTestSuite) TestAutoRefreshPhase2AllHeld(c *C) {
  1833  	logbuf, restoreLogger := logger.MockLogger()
  1834  	defer restoreLogger()
  1835  
  1836  	expected := []string{
  1837  		"conditional-auto-refresh",
  1838  		"run-hook [snap-a;gate-auto-refresh]",
  1839  		// snap-b hook is triggered because of base-snap-b refresh
  1840  		"run-hook [snap-b;gate-auto-refresh]",
  1841  	}
  1842  
  1843  	s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1844  		switch snapName {
  1845  		case "snap-b":
  1846  			// pretend that snap-b calls snapctl --hold to hold refresh of base-snap-b
  1847  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1848  		case "snap-a":
  1849  			// pretend that snap-a calls snapctl --hold to hold itself
  1850  			c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1851  		default:
  1852  			c.Fatalf("unexpected snap %q", snapName)
  1853  		}
  1854  	}, expected)
  1855  
  1856  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b,snap-a`)
  1857  }
  1858  
  1859  func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) {
  1860  	st := s.state
  1861  	st.Lock()
  1862  	defer st.Unlock()
  1863  
  1864  	restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error {
  1865  		c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123))
  1866  		if fail {
  1867  			return &osutil.NotEnoughDiskSpaceError{}
  1868  		}
  1869  		return nil
  1870  	})
  1871  	defer restore()
  1872  
  1873  	var installSizeCalled bool
  1874  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1875  		installSizeCalled = true
  1876  		seen := map[string]bool{}
  1877  		for _, sn := range snaps {
  1878  			seen[sn.InstanceName()] = true
  1879  		}
  1880  		c.Check(seen, DeepEquals, map[string]bool{
  1881  			"base-snap-b": true,
  1882  			"snap-a":      true,
  1883  		})
  1884  		return 123, nil
  1885  	})
  1886  	defer restoreInstallSize()
  1887  
  1888  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  1889  	defer restoreModel()
  1890  
  1891  	tr := config.NewTransaction(s.state)
  1892  	tr.Set("core", "experimental.check-disk-space-refresh", true)
  1893  	tr.Commit()
  1894  
  1895  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1896  		fakeStore: s.fakeStore,
  1897  		refreshedSnaps: []*snap.Info{{
  1898  			Architectures: []string{"all"},
  1899  			SnapType:      snap.TypeApp,
  1900  			SideInfo: snap.SideInfo{
  1901  				RealName: "snap-a",
  1902  				Revision: snap.R(8),
  1903  			},
  1904  		}, {
  1905  			Architectures: []string{"all"},
  1906  			SnapType:      snap.TypeBase,
  1907  			SideInfo: snap.SideInfo{
  1908  				RealName: "base-snap-b",
  1909  				Revision: snap.R(3),
  1910  			},
  1911  		}}})
  1912  
  1913  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1914  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1915  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1916  
  1917  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1918  
  1919  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1920  	c.Assert(err, IsNil)
  1921  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1922  
  1923  	chg := s.state.NewChange("refresh", "...")
  1924  	for _, ts := range tss {
  1925  		chg.AddAll(ts)
  1926  	}
  1927  
  1928  	s.state.Unlock()
  1929  	defer s.se.Stop()
  1930  	s.settle(c)
  1931  	s.state.Lock()
  1932  
  1933  	c.Check(installSizeCalled, Equals, true)
  1934  	if fail {
  1935  		c.Check(chg.Status(), Equals, state.ErrorStatus)
  1936  		c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`)
  1937  	} else {
  1938  		c.Check(chg.Status(), Equals, state.DoneStatus)
  1939  		c.Check(chg.Err(), IsNil)
  1940  	}
  1941  }
  1942  
  1943  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) {
  1944  	fail := true
  1945  	s.testAutoRefreshPhase2DiskSpaceCheck(c, fail)
  1946  }
  1947  
  1948  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) {
  1949  	var nofail bool
  1950  	s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail)
  1951  }
  1952  
  1953  // XXX: this case is probably artificial; with proper conflict prevention
  1954  // we shouldn't get conflicts from doInstall in phase2.
  1955  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) {
  1956  	st := s.state
  1957  	st.Lock()
  1958  	defer st.Unlock()
  1959  
  1960  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1961  		fakeStore: s.fakeStore,
  1962  		refreshedSnaps: []*snap.Info{{
  1963  			Architectures: []string{"all"},
  1964  			SnapType:      snap.TypeApp,
  1965  			SideInfo: snap.SideInfo{
  1966  				RealName: "snap-a",
  1967  				Revision: snap.R(8),
  1968  			},
  1969  		}, {
  1970  			Architectures: []string{"all"},
  1971  			SnapType:      snap.TypeBase,
  1972  			SideInfo: snap.SideInfo{
  1973  				RealName: "base-snap-b",
  1974  				Revision: snap.R(3),
  1975  			},
  1976  		}}})
  1977  
  1978  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1979  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1980  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1981  
  1982  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1983  
  1984  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1985  	defer restore()
  1986  
  1987  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  1988  	c.Assert(err, IsNil)
  1989  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1990  
  1991  	chg := s.state.NewChange("refresh", "...")
  1992  	for _, ts := range tss {
  1993  		chg.AddAll(ts)
  1994  	}
  1995  
  1996  	conflictChange := st.NewChange("conflicting change", "")
  1997  	conflictTask := st.NewTask("conflicting task", "")
  1998  	si := &snap.SideInfo{
  1999  		RealName: "snap-a",
  2000  		Revision: snap.R(1),
  2001  	}
  2002  	sup := snapstate.SnapSetup{SideInfo: si}
  2003  	conflictTask.Set("snap-setup", sup)
  2004  	conflictChange.AddTask(conflictTask)
  2005  	conflictTask.WaitFor(tss[0].Tasks()[0])
  2006  
  2007  	s.state.Unlock()
  2008  	defer s.se.Stop()
  2009  	s.settle(c)
  2010  	s.state.Lock()
  2011  
  2012  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  2013  	c.Check(chg.Err(), IsNil)
  2014  
  2015  	// no refresh of snap-a because of the conflict.
  2016  	expected := []string{
  2017  		"conditional-auto-refresh",
  2018  		"run-hook [snap-a;gate-auto-refresh]",
  2019  		// snap-b hook is triggered because of base-snap-b refresh
  2020  		"run-hook [snap-b;gate-auto-refresh]",
  2021  		"prerequisites",
  2022  		"download-snap",
  2023  		"validate-snap",
  2024  		"mount-snap",
  2025  		"run-hook [base-snap-b;pre-refresh]",
  2026  		"stop-snap-services",
  2027  		"remove-aliases",
  2028  		"unlink-current-snap",
  2029  		"copy-snap-data",
  2030  		"setup-profiles",
  2031  		"link-snap",
  2032  		"auto-connect",
  2033  		"set-auto-aliases",
  2034  		"setup-aliases",
  2035  		"run-hook [base-snap-b;post-refresh]",
  2036  		"start-snap-services",
  2037  		"cleanup",
  2038  		"run-hook [base-snap-b;check-health]",
  2039  		"check-rerefresh",
  2040  	}
  2041  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  2042  }
  2043  
  2044  func (s *snapmgrTestSuite) TestAutoRefreshPhase2ConflictOtherSnapOp(c *C) {
  2045  	st := s.state
  2046  	st.Lock()
  2047  	defer st.Unlock()
  2048  
  2049  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  2050  		fakeStore: s.fakeStore,
  2051  		refreshedSnaps: []*snap.Info{{
  2052  			Architectures: []string{"all"},
  2053  			SnapType:      snap.TypeApp,
  2054  			SideInfo: snap.SideInfo{
  2055  				RealName: "snap-a",
  2056  				Revision: snap.R(8),
  2057  			},
  2058  		}}})
  2059  
  2060  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2061  
  2062  	snapstate.MockSnapReadInfo(fakeReadInfo)
  2063  
  2064  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2065  	defer restore()
  2066  
  2067  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  2068  	c.Assert(err, IsNil)
  2069  	c.Check(names, DeepEquals, []string{"snap-a"})
  2070  
  2071  	chg := s.state.NewChange("fake-auto-refresh", "...")
  2072  	for _, ts := range tss {
  2073  		chg.AddAll(ts)
  2074  	}
  2075  
  2076  	s.state.Unlock()
  2077  	// run first task
  2078  	s.se.Ensure()
  2079  	s.se.Wait()
  2080  
  2081  	s.state.Lock()
  2082  
  2083  	_, err = snapstate.Remove(s.state, "snap-a", snap.R(8), nil)
  2084  	c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{
  2085  		ChangeKind: "fake-auto-refresh",
  2086  		Snap:       "snap-a",
  2087  	})
  2088  
  2089  	_, err = snapstate.Update(s.state, "snap-a", nil, 0, snapstate.Flags{})
  2090  	c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{
  2091  		ChangeKind: "fake-auto-refresh",
  2092  		Snap:       "snap-a",
  2093  	})
  2094  
  2095  	// only 2 tasks because we don't run settle() so conditional-auto-refresh
  2096  	// doesn't run and no new tasks get created.
  2097  	expected := []string{
  2098  		"conditional-auto-refresh",
  2099  		"run-hook [snap-a;gate-auto-refresh]",
  2100  	}
  2101  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  2102  }
  2103  
  2104  func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) {
  2105  	st := s.state
  2106  	st.Lock()
  2107  	defer st.Unlock()
  2108  
  2109  	restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) {
  2110  		c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh")
  2111  		var candidates map[string]*snapstate.RefreshCandidate
  2112  		c.Assert(gatingTask.Get("snaps", &candidates), IsNil)
  2113  		seenSnaps := make(map[string]bool)
  2114  		var filteredByGatingHooks []*snapstate.RefreshCandidate
  2115  		for _, cand := range candidates {
  2116  			seenSnaps[cand.InstanceName()] = true
  2117  			if cand.InstanceName() == "snap-a" {
  2118  				continue
  2119  			}
  2120  			filteredByGatingHooks = append(filteredByGatingHooks, cand)
  2121  		}
  2122  		c.Check(seenSnaps, DeepEquals, map[string]bool{
  2123  			"snap-a":      true,
  2124  			"base-snap-b": true,
  2125  		})
  2126  		return filteredByGatingHooks, nil
  2127  	})
  2128  	defer restore()
  2129  
  2130  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  2131  		fakeStore: s.fakeStore,
  2132  		refreshedSnaps: []*snap.Info{
  2133  			{
  2134  				Architectures: []string{"all"},
  2135  				SnapType:      snap.TypeApp,
  2136  				SideInfo: snap.SideInfo{
  2137  					RealName: "snap-a",
  2138  					Revision: snap.R(8),
  2139  				},
  2140  			}, {
  2141  				Architectures: []string{"all"},
  2142  				SnapType:      snap.TypeBase,
  2143  				SideInfo: snap.SideInfo{
  2144  					RealName: "base-snap-b",
  2145  					Revision: snap.R(3),
  2146  				},
  2147  			},
  2148  		}})
  2149  
  2150  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2151  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2152  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2153  
  2154  	snapstate.MockSnapReadInfo(fakeReadInfo)
  2155  
  2156  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  2157  	defer restoreModel()
  2158  
  2159  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "")
  2160  	c.Assert(err, IsNil)
  2161  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  2162  
  2163  	chg := s.state.NewChange("refresh", "...")
  2164  	for _, ts := range tss {
  2165  		chg.AddAll(ts)
  2166  	}
  2167  
  2168  	s.state.Unlock()
  2169  	defer s.se.Stop()
  2170  	s.settle(c)
  2171  	s.state.Lock()
  2172  
  2173  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  2174  	c.Check(chg.Err(), IsNil)
  2175  
  2176  	expected := []string{
  2177  		"conditional-auto-refresh",
  2178  		"run-hook [snap-a;gate-auto-refresh]",
  2179  		// snap-b hook is triggered because of base-snap-b refresh
  2180  		"run-hook [snap-b;gate-auto-refresh]",
  2181  		"prerequisites",
  2182  		"download-snap",
  2183  		"validate-snap",
  2184  		"mount-snap",
  2185  		"run-hook [base-snap-b;pre-refresh]",
  2186  		"stop-snap-services",
  2187  		"remove-aliases",
  2188  		"unlink-current-snap",
  2189  		"copy-snap-data",
  2190  		"setup-profiles",
  2191  		"link-snap",
  2192  		"auto-connect",
  2193  		"set-auto-aliases",
  2194  		"setup-aliases",
  2195  		"run-hook [base-snap-b;post-refresh]",
  2196  		"start-snap-services",
  2197  		"cleanup",
  2198  		"run-hook [base-snap-b;check-health]",
  2199  		"check-rerefresh",
  2200  	}
  2201  	tasks := chg.Tasks()
  2202  	verifyPhasedAutorefreshTasks(c, tasks, expected)
  2203  	// no re-refresh for snap-a because it was held.
  2204  	c.Check(tasks[len(tasks)-1].Summary(), Equals, `Handling re-refresh of "base-snap-b" as needed`)
  2205  
  2206  	// only snap-a remains in refresh-candidates because it was held;
  2207  	// base-snap-b got pruned (was refreshed).
  2208  	var candidates map[string]*snapstate.RefreshCandidate
  2209  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  2210  	c.Assert(candidates, HasLen, 1)
  2211  	c.Check(candidates["snap-a"], NotNil)
  2212  }
  2213  
  2214  func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorAutoRefreshInProgress(c *C) {
  2215  	st := s.state
  2216  	st.Lock()
  2217  	defer st.Unlock()
  2218  
  2219  	chg := st.NewChange("auto-refresh", "...")
  2220  	task := st.NewTask("foo", "...")
  2221  	chg.AddTask(task)
  2222  
  2223  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `there is an auto-refresh in progress`)
  2224  }
  2225  
  2226  func (s *snapmgrTestSuite) TestAutoRefreshForGatingSnapErrorNothingHeld(c *C) {
  2227  	st := s.state
  2228  	st.Lock()
  2229  	defer st.Unlock()
  2230  
  2231  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-a"), ErrorMatches, `no snaps are held by snap "snap-a"`)
  2232  }
  2233  
  2234  func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnap(c *C) {
  2235  	s.store.refreshedSnaps = []*snap.Info{{
  2236  		Architectures: []string{"all"},
  2237  		SnapType:      snap.TypeApp,
  2238  		SideInfo: snap.SideInfo{
  2239  			RealName: "snap-a",
  2240  			Revision: snap.R(8),
  2241  		},
  2242  	}, {
  2243  		Architectures: []string{"all"},
  2244  		SnapType:      snap.TypeApp,
  2245  		Base:          "base-snap-b",
  2246  		SideInfo: snap.SideInfo{
  2247  			RealName: "snap-b",
  2248  			Revision: snap.R(2),
  2249  		},
  2250  	}, {
  2251  		Architectures: []string{"all"},
  2252  		SnapType:      snap.TypeBase,
  2253  		SideInfo: snap.SideInfo{
  2254  			RealName: "base-snap-b",
  2255  			Revision: snap.R(3),
  2256  		},
  2257  	}}
  2258  
  2259  	st := s.state
  2260  	st.Lock()
  2261  	defer st.Unlock()
  2262  
  2263  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2264  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2265  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2266  
  2267  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2268  	defer restore()
  2269  
  2270  	// pretend some snaps are held
  2271  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil)
  2272  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil)
  2273  
  2274  	lastRefreshTime := time.Now().Add(-99 * time.Hour)
  2275  	st.Set("last-refresh", lastRefreshTime)
  2276  
  2277  	// pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed)
  2278  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil)
  2279  
  2280  	changes := st.Changes()
  2281  	c.Assert(changes, HasLen, 1)
  2282  	chg := changes[0]
  2283  	c.Assert(chg.Kind(), Equals, "auto-refresh")
  2284  	c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`)
  2285  	var snapNames []string
  2286  	var apiData map[string]interface{}
  2287  	c.Assert(chg.Get("snap-names", &snapNames), IsNil)
  2288  	c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"})
  2289  	c.Assert(chg.Get("api-data", &apiData), IsNil)
  2290  	c.Check(apiData, DeepEquals, map[string]interface{}{
  2291  		"snap-names": []interface{}{"base-snap-b", "snap-b"},
  2292  	})
  2293  
  2294  	tasks := chg.Tasks()
  2295  	c.Assert(tasks, HasLen, 2)
  2296  	conditionalRefreshTask := tasks[0]
  2297  	checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{
  2298  		"base-snap-b": {
  2299  			SnapSetup: snapstate.SnapSetup{
  2300  				Type:      "base",
  2301  				PlugsOnly: true,
  2302  				Flags: snapstate.Flags{
  2303  					IsAutoRefresh: true,
  2304  				},
  2305  				SideInfo: &snap.SideInfo{
  2306  					RealName: "base-snap-b",
  2307  					Revision: snap.R(3),
  2308  				},
  2309  				DownloadInfo: &snap.DownloadInfo{},
  2310  			},
  2311  		},
  2312  		"snap-b": {
  2313  			SnapSetup: snapstate.SnapSetup{
  2314  				Type:      "app",
  2315  				Base:      "base-snap-b",
  2316  				PlugsOnly: true,
  2317  				Flags: snapstate.Flags{
  2318  					IsAutoRefresh: true,
  2319  				},
  2320  				SideInfo: &snap.SideInfo{
  2321  					RealName: "snap-b",
  2322  					Revision: snap.R(2),
  2323  				},
  2324  				DownloadInfo: &snap.DownloadInfo{},
  2325  			},
  2326  		},
  2327  	})
  2328  
  2329  	// the gate-auto-refresh hook task for snap-b is present
  2330  	c.Check(tasks[1].Kind(), Equals, "run-hook")
  2331  	var hs hookstate.HookSetup
  2332  	c.Assert(tasks[1].Get("hook-setup", &hs), IsNil)
  2333  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  2334  	c.Check(hs.Snap, Equals, "snap-b")
  2335  	c.Check(hs.Optional, Equals, true)
  2336  
  2337  	var data interface{}
  2338  	c.Assert(tasks[1].Get("hook-context", &data), IsNil)
  2339  	c.Check(data, DeepEquals, map[string]interface{}{
  2340  		"base":            true,
  2341  		"restart":         false,
  2342  		"affecting-snaps": []interface{}{"base-snap-b", "snap-b"},
  2343  	})
  2344  
  2345  	// last-refresh wasn't modified
  2346  	var lr time.Time
  2347  	st.Get("last-refresh", &lr)
  2348  	c.Check(lr.Equal(lastRefreshTime), Equals, true)
  2349  }
  2350  
  2351  func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapMoreAffectedSnaps(c *C) {
  2352  	s.store.refreshedSnaps = []*snap.Info{{
  2353  		Architectures: []string{"all"},
  2354  		SnapType:      snap.TypeApp,
  2355  		SideInfo: snap.SideInfo{
  2356  			RealName: "snap-a",
  2357  			Revision: snap.R(8),
  2358  		},
  2359  	}, {
  2360  		Architectures: []string{"all"},
  2361  		SnapType:      snap.TypeBase,
  2362  		SideInfo: snap.SideInfo{
  2363  			RealName: "base-snap-b",
  2364  			Revision: snap.R(3),
  2365  		},
  2366  	}, {
  2367  		Architectures: []string{"all"},
  2368  		SnapType:      snap.TypeApp,
  2369  		Base:          "base-snap-b",
  2370  		SideInfo: snap.SideInfo{
  2371  			RealName: "snap-b",
  2372  			Revision: snap.R(2),
  2373  		},
  2374  	}}
  2375  
  2376  	st := s.state
  2377  	st.Lock()
  2378  	defer st.Unlock()
  2379  
  2380  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2381  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2382  	mockInstalledSnap(c, s.state, snapBByaml, useHook)
  2383  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2384  
  2385  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2386  	defer restore()
  2387  
  2388  	// pretend snap-b holds base-snap-b.
  2389  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil)
  2390  
  2391  	// pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed)
  2392  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil)
  2393  
  2394  	changes := st.Changes()
  2395  	c.Assert(changes, HasLen, 1)
  2396  	chg := changes[0]
  2397  	c.Assert(chg.Kind(), Equals, "auto-refresh")
  2398  	c.Check(chg.Summary(), Equals, `Auto-refresh snaps "base-snap-b", "snap-b"`)
  2399  	var snapNames []string
  2400  	var apiData map[string]interface{}
  2401  	c.Assert(chg.Get("snap-names", &snapNames), IsNil)
  2402  	c.Check(snapNames, DeepEquals, []string{"base-snap-b", "snap-b"})
  2403  	c.Assert(chg.Get("api-data", &apiData), IsNil)
  2404  	c.Check(apiData, DeepEquals, map[string]interface{}{
  2405  		"snap-names": []interface{}{"base-snap-b", "snap-b"},
  2406  	})
  2407  
  2408  	tasks := chg.Tasks()
  2409  	c.Assert(tasks, HasLen, 3)
  2410  	conditionalRefreshTask := tasks[0]
  2411  	checkGatingTask(c, conditionalRefreshTask, map[string]*snapstate.RefreshCandidate{
  2412  		"base-snap-b": {
  2413  			SnapSetup: snapstate.SnapSetup{
  2414  				Type:      "base",
  2415  				PlugsOnly: true,
  2416  				Flags: snapstate.Flags{
  2417  					IsAutoRefresh: true,
  2418  				},
  2419  				SideInfo: &snap.SideInfo{
  2420  					RealName: "base-snap-b",
  2421  					Revision: snap.R(3),
  2422  				},
  2423  				DownloadInfo: &snap.DownloadInfo{},
  2424  			},
  2425  		},
  2426  		"snap-b": {
  2427  			SnapSetup: snapstate.SnapSetup{
  2428  				Type:      "app",
  2429  				Base:      "base-snap-b",
  2430  				PlugsOnly: true,
  2431  				Flags: snapstate.Flags{
  2432  					IsAutoRefresh: true,
  2433  				},
  2434  				SideInfo: &snap.SideInfo{
  2435  					RealName: "snap-b",
  2436  					Revision: snap.R(2),
  2437  				},
  2438  				DownloadInfo: &snap.DownloadInfo{},
  2439  			},
  2440  		},
  2441  	})
  2442  
  2443  	seenSnaps := make(map[string]bool)
  2444  
  2445  	// check that the gate-auto-refresh hooks are run.
  2446  	// snap-bb's hook is triggered because it is affected by base-snap-b refresh
  2447  	// (and intersects with affecting snap of snap-b). Note, snap-a is not here
  2448  	// because it is not affected by snaps affecting snap-b.
  2449  	for i := 1; i <= 2; i++ {
  2450  		c.Assert(tasks[i].Kind(), Equals, "run-hook")
  2451  		var hs hookstate.HookSetup
  2452  		c.Assert(tasks[i].Get("hook-setup", &hs), IsNil)
  2453  		c.Check(hs.Hook, Equals, "gate-auto-refresh")
  2454  		c.Check(hs.Optional, Equals, true)
  2455  		seenSnaps[hs.Snap] = true
  2456  		var data interface{}
  2457  		c.Assert(tasks[i].Get("hook-context", &data), IsNil)
  2458  		switch hs.Snap {
  2459  		case "snap-b":
  2460  			c.Check(data, DeepEquals, map[string]interface{}{
  2461  				"base":            true,
  2462  				"restart":         false,
  2463  				"affecting-snaps": []interface{}{"base-snap-b", "snap-b"},
  2464  			})
  2465  		case "snap-bb":
  2466  			c.Check(data, DeepEquals, map[string]interface{}{
  2467  				"base":            true,
  2468  				"restart":         false,
  2469  				"affecting-snaps": []interface{}{"base-snap-b"},
  2470  			})
  2471  		default:
  2472  			c.Fatalf("unexpected snap %q", hs.Snap)
  2473  		}
  2474  	}
  2475  	c.Check(seenSnaps, DeepEquals, map[string]bool{
  2476  		"snap-b":  true,
  2477  		"snap-bb": true,
  2478  	})
  2479  }
  2480  
  2481  func (s *autorefreshGatingSuite) TestAutoRefreshForGatingSnapNoCandidatesAnymore(c *C) {
  2482  	// only snap-a will have a refresh available
  2483  	s.store.refreshedSnaps = []*snap.Info{{
  2484  		Architectures: []string{"all"},
  2485  		SnapType:      snap.TypeApp,
  2486  		SideInfo: snap.SideInfo{
  2487  			RealName: "snap-a",
  2488  			Revision: snap.R(8),
  2489  		},
  2490  	}}
  2491  
  2492  	logbuf, restoreLogger := logger.MockLogger()
  2493  	defer restoreLogger()
  2494  
  2495  	st := s.state
  2496  	st.Lock()
  2497  	defer st.Unlock()
  2498  
  2499  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  2500  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  2501  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  2502  
  2503  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  2504  	defer restore()
  2505  
  2506  	// pretend some snaps are held
  2507  	c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil)
  2508  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil)
  2509  
  2510  	// pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed)
  2511  	c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil)
  2512  	c.Assert(st.Changes(), HasLen, 0)
  2513  
  2514  	// but base-snap-b has no update anymore.
  2515  	c.Check(logbuf.String(), testutil.Contains, `auto-refresh: all snaps previously held by "snap-b" are up-to-date`)
  2516  }
  2517  
  2518  func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) {
  2519  	c.Assert(len(tasks), Equals, len(expected))
  2520  	for i, t := range tasks {
  2521  		var got string
  2522  		if t.Kind() == "run-hook" {
  2523  			var hsup hookstate.HookSetup
  2524  			c.Assert(t.Get("hook-setup", &hsup), IsNil)
  2525  			got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook)
  2526  		} else {
  2527  			got = t.Kind()
  2528  		}
  2529  		c.Assert(got, Equals, expected[i], Commentf("#%d", i))
  2530  	}
  2531  }