github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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 kernelYaml = `name: kernel
   162  type: kernel
   163  version: 1
   164  `
   165  
   166  const gadget1Yaml = `name: gadget
   167  type: gadget
   168  version: 1
   169  `
   170  
   171  const snapCyaml = `name: snap-c
   172  type: app
   173  version: 1
   174  `
   175  
   176  const snapDyaml = `name: snap-d
   177  type: app
   178  version: 1
   179  slots:
   180      slot: desktop
   181  `
   182  
   183  const snapEyaml = `name: snap-e
   184  type: app
   185  version: 1
   186  base: other-base
   187  plugs:
   188      plug: desktop
   189  `
   190  
   191  const snapFyaml = `name: snap-f
   192  type: app
   193  version: 1
   194  plugs:
   195      plug: desktop
   196  `
   197  
   198  const snapGyaml = `name: snap-g
   199  type: app
   200  version: 1
   201  base: other-base
   202  plugs:
   203      desktop:
   204      mir:
   205  `
   206  
   207  const coreYaml = `name: core
   208  type: os
   209  version: 1
   210  slots:
   211      desktop:
   212      mir:
   213  `
   214  
   215  const core18Yaml = `name: core18
   216  type: os
   217  version: 1
   218  `
   219  
   220  const snapdYaml = `name: snapd
   221  version: 1
   222  type: snapd
   223  slots:
   224      desktop:
   225  `
   226  
   227  func (s *autorefreshGatingSuite) TestHoldDurationLeft(c *C) {
   228  	now, err := time.Parse(time.RFC3339, "2021-06-03T10:00:00Z")
   229  	c.Assert(err, IsNil)
   230  	maxPostponement := time.Hour * 24 * 90
   231  
   232  	for i, tc := range []struct {
   233  		lastRefresh, firstHeld string
   234  		maxDuration            string
   235  		expected               string
   236  	}{
   237  		{
   238  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   239  			"2021-06-03T10:00:00Z", // first held now
   240  			"48h", // max duration
   241  			"48h", // expected
   242  		},
   243  		{
   244  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   245  			"2021-06-02T10:00:00Z", // first held (1 day ago)
   246  			"48h", // max duration
   247  			"24h", // expected
   248  		},
   249  		{
   250  			"2021-05-03T10:00:00Z", // last refreshed (1 month ago)
   251  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   252  			"48h", // max duration
   253  			"00h", // expected
   254  		},
   255  		{
   256  			"2021-03-08T10:00:00Z", // last refreshed (almost 3 months ago)
   257  			"2021-06-01T10:00:00Z", // first held
   258  			"2160h",                // max duration (90 days)
   259  			"72h",                  // expected
   260  		},
   261  		{
   262  			"2021-03-04T10:00:00Z", // last refreshed
   263  			"2021-06-01T10:00:00Z", // first held (2 days ago)
   264  			"2160h",                // max duration (90 days)
   265  			"-24h",                 // expected (refresh is 1 day overdue)
   266  		},
   267  		{
   268  			"2021-06-01T10:00:00Z", // last refreshed (2 days ago)
   269  			"2021-06-03T10:00:00Z", // first held now
   270  			"2160h",                // max duration (90 days)
   271  			"2112h",                // expected (max minus 2 days)
   272  		},
   273  	} {
   274  		lastRefresh, err := time.Parse(time.RFC3339, tc.lastRefresh)
   275  		c.Assert(err, IsNil)
   276  		firstHeld, err := time.Parse(time.RFC3339, tc.firstHeld)
   277  		c.Assert(err, IsNil)
   278  		maxDuration, err := time.ParseDuration(tc.maxDuration)
   279  		c.Assert(err, IsNil)
   280  		expected, err := time.ParseDuration(tc.expected)
   281  		c.Assert(err, IsNil)
   282  
   283  		left := snapstate.HoldDurationLeft(now, lastRefresh, firstHeld, maxDuration, maxPostponement)
   284  		c.Check(left, Equals, expected, Commentf("case #%d", i))
   285  	}
   286  }
   287  
   288  func (s *autorefreshGatingSuite) TestLastRefreshedHelper(c *C) {
   289  	st := s.state
   290  	st.Lock()
   291  	defer st.Unlock()
   292  
   293  	inf := mockInstalledSnap(c, st, snapAyaml, false)
   294  	stat, err := os.Stat(inf.MountFile())
   295  	c.Assert(err, IsNil)
   296  
   297  	refreshed, err := snapstate.LastRefreshed(st, "snap-a")
   298  	c.Assert(err, IsNil)
   299  	c.Check(refreshed, DeepEquals, stat.ModTime())
   300  
   301  	t, err := time.Parse(time.RFC3339, "2021-01-01T10:00:00Z")
   302  	c.Assert(err, IsNil)
   303  
   304  	var snapst snapstate.SnapState
   305  	c.Assert(snapstate.Get(st, "snap-a", &snapst), IsNil)
   306  	snapst.LastRefreshTime = &t
   307  	snapstate.Set(st, "snap-a", &snapst)
   308  
   309  	refreshed, err = snapstate.LastRefreshed(st, "snap-a")
   310  	c.Assert(err, IsNil)
   311  	c.Check(refreshed, DeepEquals, t)
   312  }
   313  
   314  func (s *autorefreshGatingSuite) TestHoldRefreshHelper(c *C) {
   315  	st := s.state
   316  	st.Lock()
   317  	defer st.Unlock()
   318  
   319  	restore := snapstate.MockTimeNow(func() time.Time {
   320  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   321  		c.Assert(err, IsNil)
   322  		return t
   323  	})
   324  	defer restore()
   325  
   326  	mockInstalledSnap(c, st, snapAyaml, false)
   327  	mockInstalledSnap(c, st, snapByaml, false)
   328  	mockInstalledSnap(c, st, snapCyaml, false)
   329  	mockInstalledSnap(c, st, snapDyaml, false)
   330  	mockInstalledSnap(c, st, snapEyaml, false)
   331  	mockInstalledSnap(c, st, snapFyaml, false)
   332  
   333  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f")
   334  
   335  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   336  	// this could be merged with the above HoldRefresh call, but it's fine if
   337  	// done separately too.
   338  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-e"), IsNil)
   339  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-e"), IsNil)
   340  	c.Assert(snapstate.HoldRefresh(st, "snap-f", 0, "snap-f"), IsNil)
   341  
   342  	var gating map[string]map[string]*snapstate.HoldState
   343  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   344  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   345  		"snap-b": {
   346  			// holding of other snaps for maxOtherHoldDuration (48h)
   347  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   348  		},
   349  		"snap-c": {
   350  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   351  		},
   352  		"snap-e": {
   353  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   354  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   355  		},
   356  		"snap-f": {
   357  			// holding self set for maxPostponement minus 1 day due to last refresh.
   358  			"snap-f": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-07T10:00:00Z"),
   359  		},
   360  	})
   361  }
   362  
   363  func (s *autorefreshGatingSuite) TestHoldRefreshHelperMultipleTimes(c *C) {
   364  	st := s.state
   365  	st.Lock()
   366  	defer st.Unlock()
   367  
   368  	lastRefreshed := "2021-05-09T10:00:00Z"
   369  	now := "2021-05-10T10:00:00Z"
   370  	restore := snapstate.MockTimeNow(func() time.Time {
   371  		t, err := time.Parse(time.RFC3339, now)
   372  		c.Assert(err, IsNil)
   373  		return t
   374  	})
   375  	defer restore()
   376  
   377  	mockInstalledSnap(c, st, snapAyaml, false)
   378  	mockInstalledSnap(c, st, snapByaml, false)
   379  	// snap-a was last refreshed yesterday
   380  	mockLastRefreshed(c, st, lastRefreshed, "snap-a")
   381  
   382  	// hold it for just a bit (10h) initially
   383  	hold := time.Hour * 10
   384  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   385  	var gating map[string]map[string]*snapstate.HoldState
   386  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   387  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   388  		"snap-a": {
   389  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T20:00:00Z"),
   390  		},
   391  	})
   392  
   393  	// holding for a shorter time is fine too
   394  	hold = time.Hour * 5
   395  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   396  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   397  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   398  		"snap-a": {
   399  			"snap-b": snapstate.MockHoldState(now, "2021-05-10T15:00:00Z"),
   400  		},
   401  	})
   402  
   403  	oldNow := now
   404  
   405  	// a refresh on next day
   406  	now = "2021-05-11T08:00:00Z"
   407  
   408  	// default hold time requested
   409  	hold = 0
   410  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   411  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   412  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   413  		"snap-a": {
   414  			// maximum for holding other snaps, but taking into consideration
   415  			// firstHeld time = "2021-05-10T10:00:00".
   416  			"snap-b": snapstate.MockHoldState(oldNow, "2021-05-12T10:00:00Z"),
   417  		},
   418  	})
   419  }
   420  
   421  func (s *autorefreshGatingSuite) TestHoldRefreshHelperCloseToMaxPostponement(c *C) {
   422  	st := s.state
   423  	st.Lock()
   424  	defer st.Unlock()
   425  
   426  	lastRefreshedStr := "2021-01-01T10:00:00Z"
   427  	lastRefreshed, err := time.Parse(time.RFC3339, lastRefreshedStr)
   428  	c.Assert(err, IsNil)
   429  	// we are 1 day before maxPostponent
   430  	now := lastRefreshed.Add(89 * time.Hour * 24)
   431  
   432  	restore := snapstate.MockTimeNow(func() time.Time { return now })
   433  	defer restore()
   434  
   435  	mockInstalledSnap(c, st, snapAyaml, false)
   436  	mockInstalledSnap(c, st, snapByaml, false)
   437  	mockLastRefreshed(c, st, lastRefreshedStr, "snap-a")
   438  
   439  	// request default hold time
   440  	var hold time.Duration
   441  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   442  
   443  	var gating map[string]map[string]*snapstate.HoldState
   444  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   445  	c.Assert(gating, HasLen, 1)
   446  	c.Check(gating["snap-a"]["snap-b"].HoldUntil.String(), DeepEquals, lastRefreshed.Add(90*time.Hour*24).String())
   447  }
   448  
   449  func (s *autorefreshGatingSuite) TestHoldRefreshExplicitHoldTime(c *C) {
   450  	st := s.state
   451  	st.Lock()
   452  	defer st.Unlock()
   453  
   454  	now := "2021-05-10T10:00:00Z"
   455  	restore := snapstate.MockTimeNow(func() time.Time {
   456  		t, err := time.Parse(time.RFC3339, now)
   457  		c.Assert(err, IsNil)
   458  		return t
   459  	})
   460  	defer restore()
   461  
   462  	mockInstalledSnap(c, st, snapAyaml, false)
   463  	mockInstalledSnap(c, st, snapByaml, false)
   464  
   465  	hold := time.Hour * 24 * 3
   466  	// holding self for 3 days
   467  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), IsNil)
   468  
   469  	// snap-b holds snap-a for 1 day
   470  	hold = time.Hour * 24
   471  	c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil)
   472  
   473  	var gating map[string]map[string]*snapstate.HoldState
   474  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   475  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   476  		"snap-a": {
   477  			"snap-a": snapstate.MockHoldState(now, "2021-05-13T10:00:00Z"),
   478  			"snap-b": snapstate.MockHoldState(now, "2021-05-11T10:00:00Z"),
   479  		},
   480  	})
   481  }
   482  
   483  func (s *autorefreshGatingSuite) TestHoldRefreshHelperErrors(c *C) {
   484  	st := s.state
   485  	st.Lock()
   486  	defer st.Unlock()
   487  
   488  	now := "2021-05-10T10:00:00Z"
   489  	restore := snapstate.MockTimeNow(func() time.Time {
   490  		t, err := time.Parse(time.RFC3339, now)
   491  		c.Assert(err, IsNil)
   492  		return t
   493  	})
   494  	defer restore()
   495  
   496  	mockInstalledSnap(c, st, snapAyaml, false)
   497  	mockInstalledSnap(c, st, snapByaml, false)
   498  	// snap-b was refreshed a few days ago
   499  	mockLastRefreshed(c, st, "2021-05-01T10:00:00Z", "snap-b")
   500  
   501  	// holding itself
   502  	hold := time.Hour * 24 * 96
   503  	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`)
   504  
   505  	// holding other snap
   506  	hold = time.Hour * 49
   507  	err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-b")
   508  	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`)
   509  	herr, ok := err.(*snapstate.HoldError)
   510  	c.Assert(ok, Equals, true)
   511  	c.Check(herr.SnapsInError, DeepEquals, map[string]snapstate.HoldDurationError{
   512  		"snap-b": {
   513  			Err:          fmt.Errorf(`requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`),
   514  			DurationLeft: 48 * time.Hour,
   515  		},
   516  	})
   517  
   518  	// hold for maximum allowed for other snaps
   519  	hold = time.Hour * 48
   520  	c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), IsNil)
   521  	// 2 days passed since it was first held
   522  	now = "2021-05-12T10:00:00Z"
   523  	hold = time.Minute * 2
   524  	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`)
   525  
   526  	// refreshed long time ago (> maxPostponement)
   527  	mockLastRefreshed(c, st, "2021-01-01T10:00:00Z", "snap-b")
   528  	hold = time.Hour * 2
   529  	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`)
   530  	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`)
   531  }
   532  
   533  func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) {
   534  	st := s.state
   535  	st.Lock()
   536  	defer st.Unlock()
   537  
   538  	mockInstalledSnap(c, st, snapAyaml, false)
   539  	mockInstalledSnap(c, st, snapByaml, false)
   540  	mockInstalledSnap(c, st, snapCyaml, false)
   541  	mockInstalledSnap(c, st, snapDyaml, false)
   542  
   543  	mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-b", "snap-c", "snap-d")
   544  
   545  	restore := snapstate.MockTimeNow(func() time.Time {
   546  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   547  		c.Assert(err, IsNil)
   548  		return t
   549  	})
   550  	defer restore()
   551  
   552  	// nothing is held initially
   553  	held, err := snapstate.HeldSnaps(st)
   554  	c.Assert(err, IsNil)
   555  	c.Check(held, IsNil)
   556  
   557  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   558  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil)
   559  	// holding self
   560  	c.Assert(snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d"), IsNil)
   561  
   562  	held, err = snapstate.HeldSnaps(st)
   563  	c.Assert(err, IsNil)
   564  	c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true})
   565  
   566  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-a"), IsNil)
   567  
   568  	held, err = snapstate.HeldSnaps(st)
   569  	c.Assert(err, IsNil)
   570  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-d": true})
   571  
   572  	c.Assert(snapstate.ProceedWithRefresh(st, "snap-d"), IsNil)
   573  	held, err = snapstate.HeldSnaps(st)
   574  	c.Assert(err, IsNil)
   575  	c.Check(held, IsNil)
   576  }
   577  
   578  func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) {
   579  	st := s.state
   580  	st.Lock()
   581  	defer st.Unlock()
   582  
   583  	restore := snapstate.MockTimeNow(func() time.Time {
   584  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   585  		c.Assert(err, IsNil)
   586  		return t
   587  	})
   588  	defer restore()
   589  
   590  	mockInstalledSnap(c, st, snapAyaml, false)
   591  	mockInstalledSnap(c, st, snapByaml, false)
   592  	mockInstalledSnap(c, st, snapCyaml, false)
   593  	mockInstalledSnap(c, st, snapDyaml, false)
   594  
   595  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   596  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   597  	// sanity
   598  	held, err := snapstate.HeldSnaps(st)
   599  	c.Assert(err, IsNil)
   600  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true})
   601  
   602  	candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}}
   603  
   604  	// only snap-c has a refresh candidate, snap-b and snap-d should be forgotten.
   605  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   606  	var gating map[string]map[string]*snapstate.HoldState
   607  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   608  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   609  		"snap-c": {
   610  			"snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   611  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"),
   612  		},
   613  	})
   614  	held, err = snapstate.HeldSnaps(st)
   615  	c.Assert(err, IsNil)
   616  	c.Check(held, DeepEquals, map[string]bool{"snap-c": true})
   617  }
   618  
   619  func (s *autorefreshGatingSuite) TestPruneGatingHelperNoGating(c *C) {
   620  	st := s.state
   621  	st.Lock()
   622  	defer st.Unlock()
   623  
   624  	restore := snapstate.MockTimeNow(func() time.Time {
   625  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   626  		c.Assert(err, IsNil)
   627  		return t
   628  	})
   629  	defer restore()
   630  
   631  	mockInstalledSnap(c, st, snapAyaml, false)
   632  
   633  	held, err := snapstate.HeldSnaps(st)
   634  	c.Assert(err, IsNil)
   635  	c.Check(held, HasLen, 0)
   636  
   637  	snapstate.MockTimeNow(func() time.Time {
   638  		c.Fatalf("not expected")
   639  		return time.Time{}
   640  	})
   641  
   642  	candidates := map[string]*snapstate.RefreshCandidate{"snap-a": {}}
   643  	c.Assert(snapstate.PruneGating(st, candidates), IsNil)
   644  	held, err = snapstate.HeldSnaps(st)
   645  	c.Assert(err, IsNil)
   646  	c.Check(held, HasLen, 0)
   647  }
   648  
   649  func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) {
   650  	st := s.state
   651  	st.Lock()
   652  	defer st.Unlock()
   653  
   654  	restore := snapstate.MockTimeNow(func() time.Time {
   655  		t, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z")
   656  		c.Assert(err, IsNil)
   657  		return t
   658  	})
   659  	defer restore()
   660  
   661  	mockInstalledSnap(c, st, snapAyaml, false)
   662  	mockInstalledSnap(c, st, snapByaml, false)
   663  	mockInstalledSnap(c, st, snapCyaml, false)
   664  	mockInstalledSnap(c, st, snapDyaml, false)
   665  
   666  	c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil)
   667  	c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil)
   668  
   669  	c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil)
   670  	var gating map[string]map[string]*snapstate.HoldState
   671  	c.Assert(st.Get("snaps-hold", &gating), IsNil)
   672  	c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{
   673  		"snap-d": {
   674  			// holding self set for maxPostponement (95 days - buffer = 90 days)
   675  			"snap-d": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-08T10:00:00Z"),
   676  		},
   677  	})
   678  
   679  	held, err := snapstate.HeldSnaps(st)
   680  	c.Assert(err, IsNil)
   681  	c.Check(held, DeepEquals, map[string]bool{"snap-d": true})
   682  }
   683  
   684  const useHook = true
   685  const noHook = false
   686  
   687  func checkGatingTask(c *C, task *state.Task, expected map[string]*snapstate.RefreshCandidate) {
   688  	c.Assert(task.Kind(), Equals, "conditional-auto-refresh")
   689  	var snaps map[string]*snapstate.RefreshCandidate
   690  	c.Assert(task.Get("snaps", &snaps), IsNil)
   691  	c.Check(snaps, DeepEquals, expected)
   692  }
   693  
   694  func (s *autorefreshGatingSuite) TestAffectedByBase(c *C) {
   695  	restore := release.MockOnClassic(true)
   696  	defer restore()
   697  
   698  	st := s.state
   699  
   700  	st.Lock()
   701  	defer st.Unlock()
   702  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
   703  	baseSnapA := mockInstalledSnap(c, s.state, baseSnapAyaml, noHook)
   704  	// unrelated snaps
   705  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   706  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
   707  
   708  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   709  
   710  	updates := []*snap.Info{baseSnapA}
   711  	affected, err := snapstate.AffectedByRefresh(st, updates)
   712  	c.Assert(err, IsNil)
   713  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   714  		"snap-a": {
   715  			Base: true,
   716  			AffectingSnaps: map[string]bool{
   717  				"base-snap-a": true,
   718  			}}})
   719  }
   720  
   721  func (s *autorefreshGatingSuite) TestAffectedByCore(c *C) {
   722  	restore := release.MockOnClassic(true)
   723  	defer restore()
   724  
   725  	st := s.state
   726  
   727  	st.Lock()
   728  	defer st.Unlock()
   729  	snapC := mockInstalledSnap(c, s.state, snapCyaml, useHook)
   730  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   731  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   732  
   733  	c.Assert(s.repo.AddSnap(core), IsNil)
   734  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   735  	c.Assert(s.repo.AddSnap(snapC), IsNil)
   736  
   737  	updates := []*snap.Info{core}
   738  	affected, err := snapstate.AffectedByRefresh(st, updates)
   739  	c.Assert(err, IsNil)
   740  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   741  		"snap-c": {
   742  			Base: true,
   743  			AffectingSnaps: map[string]bool{
   744  				"core": true,
   745  			}}})
   746  }
   747  
   748  func (s *autorefreshGatingSuite) TestAffectedByKernel(c *C) {
   749  	restore := release.MockOnClassic(true)
   750  	defer restore()
   751  
   752  	st := s.state
   753  
   754  	st.Lock()
   755  	defer st.Unlock()
   756  	kernel := mockInstalledSnap(c, s.state, kernelYaml, noHook)
   757  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   758  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   759  
   760  	updates := []*snap.Info{kernel}
   761  	affected, err := snapstate.AffectedByRefresh(st, updates)
   762  	c.Assert(err, IsNil)
   763  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   764  		"snap-c": {
   765  			Restart: true,
   766  			AffectingSnaps: map[string]bool{
   767  				"kernel": true,
   768  			}}})
   769  }
   770  
   771  func (s *autorefreshGatingSuite) TestAffectedByGadget(c *C) {
   772  	restore := release.MockOnClassic(true)
   773  	defer restore()
   774  
   775  	st := s.state
   776  
   777  	st.Lock()
   778  	defer st.Unlock()
   779  	kernel := mockInstalledSnap(c, s.state, gadget1Yaml, noHook)
   780  	mockInstalledSnap(c, s.state, snapCyaml, useHook)
   781  	mockInstalledSnap(c, s.state, snapByaml, noHook)
   782  
   783  	updates := []*snap.Info{kernel}
   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  			Restart: true,
   789  			AffectingSnaps: map[string]bool{
   790  				"gadget": true,
   791  			}}})
   792  }
   793  
   794  func (s *autorefreshGatingSuite) TestAffectedBySlot(c *C) {
   795  	restore := release.MockOnClassic(true)
   796  	defer restore()
   797  
   798  	st := s.state
   799  
   800  	st.Lock()
   801  	defer st.Unlock()
   802  
   803  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   804  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   805  	// unrelated snap
   806  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   807  
   808  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   809  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   810  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   811  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   812  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   813  	c.Assert(err, IsNil)
   814  
   815  	updates := []*snap.Info{snapD}
   816  	affected, err := snapstate.AffectedByRefresh(st, updates)
   817  	c.Assert(err, IsNil)
   818  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   819  		"snap-e": {
   820  			Restart: true,
   821  			AffectingSnaps: map[string]bool{
   822  				"snap-d": true,
   823  			}}})
   824  }
   825  
   826  func (s *autorefreshGatingSuite) TestNotAffectedByCoreOrSnapdSlot(c *C) {
   827  	restore := release.MockOnClassic(true)
   828  	defer restore()
   829  
   830  	st := s.state
   831  
   832  	st.Lock()
   833  	defer st.Unlock()
   834  
   835  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   836  	core := mockInstalledSnap(c, s.state, coreYaml, noHook)
   837  	snapB := mockInstalledSnap(c, s.state, snapByaml, useHook)
   838  
   839  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   840  	c.Assert(s.repo.AddSnap(core), IsNil)
   841  	c.Assert(s.repo.AddSnap(snapB), IsNil)
   842  
   843  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "mir"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "mir"}}
   844  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   845  	c.Assert(err, IsNil)
   846  
   847  	updates := []*snap.Info{core}
   848  	affected, err := snapstate.AffectedByRefresh(st, updates)
   849  	c.Assert(err, IsNil)
   850  	c.Check(affected, HasLen, 0)
   851  }
   852  
   853  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackend(c *C) {
   854  	restore := release.MockOnClassic(true)
   855  	defer restore()
   856  
   857  	st := s.state
   858  
   859  	st.Lock()
   860  	defer st.Unlock()
   861  
   862  	snapD := mockInstalledSnap(c, s.state, snapDyaml, useHook)
   863  	snapE := mockInstalledSnap(c, s.state, snapEyaml, useHook)
   864  	// unrelated snap
   865  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   866  
   867  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   868  	c.Assert(s.repo.AddSnap(snapD), IsNil)
   869  	c.Assert(s.repo.AddSnap(snapE), IsNil)
   870  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-e", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "snap-d", Name: "slot"}}
   871  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   872  	c.Assert(err, IsNil)
   873  
   874  	// snapE has a plug using mount backend and is refreshed, this affects slot of snap-d.
   875  	updates := []*snap.Info{snapE}
   876  	affected, err := snapstate.AffectedByRefresh(st, updates)
   877  	c.Assert(err, IsNil)
   878  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   879  		"snap-d": {
   880  			Restart: true,
   881  			AffectingSnaps: map[string]bool{
   882  				"snap-e": true,
   883  			}}})
   884  }
   885  
   886  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendSnapdSlot(c *C) {
   887  	restore := release.MockOnClassic(true)
   888  	defer restore()
   889  
   890  	st := s.state
   891  
   892  	st.Lock()
   893  	defer st.Unlock()
   894  
   895  	snapdSnap := mockInstalledSnap(c, s.state, snapdYaml, useHook)
   896  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   897  	// unrelated snap
   898  	snapF := mockInstalledSnap(c, s.state, snapFyaml, useHook)
   899  
   900  	c.Assert(s.repo.AddSnap(snapF), IsNil)
   901  	c.Assert(s.repo.AddSnap(snapdSnap), IsNil)
   902  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   903  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "snapd", Name: "desktop"}}
   904  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   905  	c.Assert(err, IsNil)
   906  
   907  	// snapE has a plug using mount backend, refreshing snapd affects snapE.
   908  	updates := []*snap.Info{snapdSnap}
   909  	affected, err := snapstate.AffectedByRefresh(st, updates)
   910  	c.Assert(err, IsNil)
   911  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   912  		"snap-g": {
   913  			Restart: true,
   914  			AffectingSnaps: map[string]bool{
   915  				"snapd": true,
   916  			}}})
   917  }
   918  
   919  func (s *autorefreshGatingSuite) TestAffectedByPlugWithMountBackendCoreSlot(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  	coreSnap := mockInstalledSnap(c, s.state, coreYaml, noHook)
   929  	snapG := mockInstalledSnap(c, s.state, snapGyaml, useHook)
   930  
   931  	c.Assert(s.repo.AddSnap(coreSnap), IsNil)
   932  	c.Assert(s.repo.AddSnap(snapG), IsNil)
   933  	cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap-g", Name: "desktop"}, SlotRef: interfaces.SlotRef{Snap: "core", Name: "desktop"}}
   934  	_, err := s.repo.Connect(cref, nil, nil, nil, nil, nil)
   935  	c.Assert(err, IsNil)
   936  
   937  	// snapG has a plug using mount backend, refreshing core affects snapE.
   938  	updates := []*snap.Info{coreSnap}
   939  	affected, err := snapstate.AffectedByRefresh(st, updates)
   940  	c.Assert(err, IsNil)
   941  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   942  		"snap-g": {
   943  			Restart: true,
   944  			AffectingSnaps: map[string]bool{
   945  				"core": true,
   946  			}}})
   947  }
   948  
   949  func (s *autorefreshGatingSuite) TestAffectedByBootBase(c *C) {
   950  	restore := release.MockOnClassic(false)
   951  	defer restore()
   952  
   953  	st := s.state
   954  
   955  	r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
   956  	defer r()
   957  
   958  	st.Lock()
   959  	defer st.Unlock()
   960  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
   961  	mockInstalledSnap(c, s.state, snapByaml, useHook)
   962  	mockInstalledSnap(c, s.state, snapDyaml, useHook)
   963  	mockInstalledSnap(c, s.state, snapEyaml, useHook)
   964  	core18 := mockInstalledSnap(c, s.state, core18Yaml, noHook)
   965  
   966  	updates := []*snap.Info{core18}
   967  	affected, err := snapstate.AffectedByRefresh(st, updates)
   968  	c.Assert(err, IsNil)
   969  	c.Check(affected, DeepEquals, map[string]*snapstate.AffectedSnapInfo{
   970  		"snap-a": {
   971  			Base:    false,
   972  			Restart: true,
   973  			AffectingSnaps: map[string]bool{
   974  				"core18": true,
   975  			},
   976  		},
   977  		"snap-b": {
   978  			Base:    false,
   979  			Restart: true,
   980  			AffectingSnaps: map[string]bool{
   981  				"core18": true,
   982  			},
   983  		},
   984  		"snap-d": {
   985  			Base:    false,
   986  			Restart: true,
   987  			AffectingSnaps: map[string]bool{
   988  				"core18": true,
   989  			},
   990  		},
   991  		"snap-e": {
   992  			Base:    false,
   993  			Restart: true,
   994  			AffectingSnaps: map[string]bool{
   995  				"core18": true,
   996  			}}})
   997  }
   998  
   999  func (s *autorefreshGatingSuite) TestCreateAutoRefreshGateHooks(c *C) {
  1000  	st := s.state
  1001  	st.Lock()
  1002  	defer st.Unlock()
  1003  
  1004  	affected := map[string]*snapstate.AffectedSnapInfo{
  1005  		"snap-a": {
  1006  			Base:    true,
  1007  			Restart: true,
  1008  			AffectingSnaps: map[string]bool{
  1009  				"snap-c": true,
  1010  				"snap-d": true,
  1011  			},
  1012  		},
  1013  		"snap-b": {
  1014  			AffectingSnaps: map[string]bool{
  1015  				"snap-e": true,
  1016  				"snap-f": true,
  1017  			},
  1018  		},
  1019  	}
  1020  
  1021  	seenSnaps := make(map[string]bool)
  1022  
  1023  	ts := snapstate.CreateGateAutoRefreshHooks(st, affected)
  1024  	c.Assert(ts.Tasks(), HasLen, 2)
  1025  
  1026  	checkHook := func(t *state.Task) {
  1027  		c.Assert(t.Kind(), Equals, "run-hook")
  1028  		var hs hookstate.HookSetup
  1029  		c.Assert(t.Get("hook-setup", &hs), IsNil)
  1030  		c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1031  		c.Check(hs.Optional, Equals, true)
  1032  		seenSnaps[hs.Snap] = true
  1033  
  1034  		var data interface{}
  1035  		c.Assert(t.Get("hook-context", &data), IsNil)
  1036  
  1037  		// the order of hook tasks is not deterministic
  1038  		if hs.Snap == "snap-a" {
  1039  			c.Check(data, DeepEquals, map[string]interface{}{
  1040  				"base":            true,
  1041  				"restart":         true,
  1042  				"affecting-snaps": []interface{}{"snap-c", "snap-d"}})
  1043  		} else {
  1044  			c.Assert(hs.Snap, Equals, "snap-b")
  1045  			c.Check(data, DeepEquals, map[string]interface{}{
  1046  				"base":            false,
  1047  				"restart":         false,
  1048  				"affecting-snaps": []interface{}{"snap-e", "snap-f"}})
  1049  		}
  1050  	}
  1051  
  1052  	checkHook(ts.Tasks()[0])
  1053  	checkHook(ts.Tasks()[1])
  1054  
  1055  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1056  }
  1057  
  1058  func (s *autorefreshGatingSuite) TestAutorefreshPhase1FeatureFlag(c *C) {
  1059  	st := s.state
  1060  	st.Lock()
  1061  	defer st.Unlock()
  1062  
  1063  	st.Set("seeded", true)
  1064  
  1065  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1066  	defer restore()
  1067  
  1068  	snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
  1069  		return nil, nil
  1070  	}
  1071  	defer func() { snapstate.AutoAliases = nil }()
  1072  
  1073  	s.store.refreshedSnaps = []*snap.Info{{
  1074  		Architectures: []string{"all"},
  1075  		SnapType:      snap.TypeApp,
  1076  		SideInfo: snap.SideInfo{
  1077  			RealName: "snap-a",
  1078  			Revision: snap.R(8),
  1079  		},
  1080  	}}
  1081  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1082  
  1083  	// gate-auto-refresh-hook feature not enabled, expect old-style refresh.
  1084  	_, tss, err := snapstate.AutoRefresh(context.TODO(), st)
  1085  	c.Check(err, IsNil)
  1086  	c.Assert(tss, HasLen, 2)
  1087  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "prerequisites")
  1088  	c.Check(tss[0].Tasks()[1].Kind(), Equals, "download-snap")
  1089  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "check-rerefresh")
  1090  
  1091  	// enable gate-auto-refresh-hook feature
  1092  	tr := config.NewTransaction(s.state)
  1093  	tr.Set("core", "experimental.gate-auto-refresh-hook", true)
  1094  	tr.Commit()
  1095  
  1096  	_, tss, err = snapstate.AutoRefresh(context.TODO(), st)
  1097  	c.Check(err, IsNil)
  1098  	c.Assert(tss, HasLen, 2)
  1099  	// TODO: verify conditional-auto-refresh task data
  1100  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh")
  1101  	c.Check(tss[1].Tasks()[0].Kind(), Equals, "run-hook")
  1102  }
  1103  
  1104  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) {
  1105  	s.store.refreshedSnaps = []*snap.Info{{
  1106  		Architectures: []string{"all"},
  1107  		SnapType:      snap.TypeApp,
  1108  		SideInfo: snap.SideInfo{
  1109  			RealName: "snap-a",
  1110  			Revision: snap.R(8),
  1111  		},
  1112  	}, {
  1113  		Architectures: []string{"all"},
  1114  		SnapType:      snap.TypeBase,
  1115  		SideInfo: snap.SideInfo{
  1116  			RealName: "base-snap-b",
  1117  			Revision: snap.R(3),
  1118  		},
  1119  	}, {
  1120  		Architectures: []string{"all"},
  1121  		SnapType:      snap.TypeApp,
  1122  		SideInfo: snap.SideInfo{
  1123  			RealName: "snap-c",
  1124  			Revision: snap.R(5),
  1125  		},
  1126  	}}
  1127  
  1128  	st := s.state
  1129  	st.Lock()
  1130  	defer st.Unlock()
  1131  
  1132  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1133  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1134  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1135  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1136  	mockInstalledSnap(c, s.state, snapDyaml, noHook)
  1137  
  1138  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1139  	defer restore()
  1140  
  1141  	// pretend some snaps are held
  1142  	c.Assert(snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d"), IsNil)
  1143  	// sanity check
  1144  	heldSnaps, err := snapstate.HeldSnaps(st)
  1145  	c.Assert(err, IsNil)
  1146  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1147  		"snap-a": true,
  1148  		"snap-d": true,
  1149  	})
  1150  
  1151  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1152  	c.Assert(err, IsNil)
  1153  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a", "snap-c"})
  1154  	c.Assert(tss, HasLen, 2)
  1155  
  1156  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1157  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1158  		"snap-a": {
  1159  			SnapSetup: snapstate.SnapSetup{
  1160  				Type:      "app",
  1161  				PlugsOnly: true,
  1162  				Flags: snapstate.Flags{
  1163  					IsAutoRefresh: true,
  1164  				},
  1165  				SideInfo: &snap.SideInfo{
  1166  					RealName: "snap-a",
  1167  					Revision: snap.R(8),
  1168  				},
  1169  				DownloadInfo: &snap.DownloadInfo{},
  1170  			},
  1171  		},
  1172  		"base-snap-b": {
  1173  			SnapSetup: snapstate.SnapSetup{
  1174  				Type:      "base",
  1175  				PlugsOnly: true,
  1176  				Flags: snapstate.Flags{
  1177  					IsAutoRefresh: true,
  1178  				},
  1179  				SideInfo: &snap.SideInfo{
  1180  					RealName: "base-snap-b",
  1181  					Revision: snap.R(3),
  1182  				},
  1183  				DownloadInfo: &snap.DownloadInfo{},
  1184  			},
  1185  		},
  1186  		"snap-c": {
  1187  			SnapSetup: snapstate.SnapSetup{
  1188  				Type:      "app",
  1189  				PlugsOnly: true,
  1190  				Flags: snapstate.Flags{
  1191  					IsAutoRefresh: true,
  1192  				},
  1193  				SideInfo: &snap.SideInfo{
  1194  					RealName: "snap-c",
  1195  					Revision: snap.R(5),
  1196  				},
  1197  				DownloadInfo: &snap.DownloadInfo{},
  1198  			},
  1199  		},
  1200  	})
  1201  
  1202  	c.Assert(tss[1].Tasks(), HasLen, 2)
  1203  
  1204  	// check hooks for affected snaps
  1205  	seenSnaps := make(map[string]bool)
  1206  	var hs hookstate.HookSetup
  1207  	c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil)
  1208  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1209  	seenSnaps[hs.Snap] = true
  1210  
  1211  	c.Assert(tss[1].Tasks()[1].Get("hook-setup", &hs), IsNil)
  1212  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1213  	seenSnaps[hs.Snap] = true
  1214  
  1215  	// hook for snap-a because it gets refreshed, for snap-b because its base
  1216  	// gets refreshed. snap-c is refreshed but doesn't have the hook.
  1217  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true, "snap-b": true})
  1218  
  1219  	// check that refresh-candidates in the state were updated
  1220  	var candidates map[string]*snapstate.RefreshCandidate
  1221  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1222  	c.Assert(candidates, HasLen, 3)
  1223  	c.Check(candidates["snap-a"], NotNil)
  1224  	c.Check(candidates["base-snap-b"], NotNil)
  1225  	c.Check(candidates["snap-c"], NotNil)
  1226  
  1227  	// check that after autoRefreshPhase1 any held snaps that are not in refresh
  1228  	// candidates got removed.
  1229  	heldSnaps, err = snapstate.HeldSnaps(st)
  1230  	c.Assert(err, IsNil)
  1231  	// snap-d got removed from held snaps.
  1232  	c.Check(heldSnaps, DeepEquals, map[string]bool{
  1233  		"snap-a": true,
  1234  	})
  1235  }
  1236  
  1237  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1ConflictsFilteredOut(c *C) {
  1238  	s.store.refreshedSnaps = []*snap.Info{{
  1239  		Architectures: []string{"all"},
  1240  		SnapType:      snap.TypeApp,
  1241  		SideInfo: snap.SideInfo{
  1242  			RealName: "snap-a",
  1243  			Revision: snap.R(8),
  1244  		},
  1245  	}, {
  1246  		Architectures: []string{"all"},
  1247  		SnapType:      snap.TypeBase,
  1248  		SideInfo: snap.SideInfo{
  1249  			RealName: "snap-c",
  1250  			Revision: snap.R(5),
  1251  		},
  1252  	}}
  1253  
  1254  	st := s.state
  1255  	st.Lock()
  1256  	defer st.Unlock()
  1257  
  1258  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1259  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1260  
  1261  	conflictChange := st.NewChange("conflicting change", "")
  1262  	conflictTask := st.NewTask("conflicting task", "")
  1263  	si := &snap.SideInfo{
  1264  		RealName: "snap-c",
  1265  		Revision: snap.R(1),
  1266  	}
  1267  	sup := snapstate.SnapSetup{SideInfo: si}
  1268  	conflictTask.Set("snap-setup", sup)
  1269  	conflictChange.AddTask(conflictTask)
  1270  
  1271  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1272  	defer restore()
  1273  
  1274  	logbuf, restoreLogger := logger.MockLogger()
  1275  	defer restoreLogger()
  1276  
  1277  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1278  	c.Assert(err, IsNil)
  1279  	c.Check(names, DeepEquals, []string{"snap-a"})
  1280  	c.Assert(tss, HasLen, 2)
  1281  
  1282  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1283  	checkGatingTask(c, tss[0].Tasks()[0], map[string]*snapstate.RefreshCandidate{
  1284  		"snap-a": {
  1285  			SnapSetup: snapstate.SnapSetup{
  1286  				Type:      "app",
  1287  				PlugsOnly: true,
  1288  				Flags: snapstate.Flags{
  1289  					IsAutoRefresh: true,
  1290  				},
  1291  				SideInfo: &snap.SideInfo{
  1292  					RealName: "snap-a",
  1293  					Revision: snap.R(8),
  1294  				},
  1295  				DownloadInfo: &snap.DownloadInfo{},
  1296  			}}})
  1297  
  1298  	c.Assert(tss[1].Tasks(), HasLen, 1)
  1299  
  1300  	c.Assert(logbuf.String(), testutil.Contains, `cannot refresh snap "snap-c": snap "snap-c" has "conflicting change" change in progress`)
  1301  
  1302  	seenSnaps := make(map[string]bool)
  1303  	var hs hookstate.HookSetup
  1304  	c.Assert(tss[1].Tasks()[0].Get("hook-setup", &hs), IsNil)
  1305  	c.Check(hs.Hook, Equals, "gate-auto-refresh")
  1306  	seenSnaps[hs.Snap] = true
  1307  
  1308  	c.Check(seenSnaps, DeepEquals, map[string]bool{"snap-a": true})
  1309  
  1310  	// check that refresh-candidates in the state were updated
  1311  	var candidates map[string]*snapstate.RefreshCandidate
  1312  	c.Assert(st.Get("refresh-candidates", &candidates), IsNil)
  1313  	c.Assert(candidates, HasLen, 2)
  1314  	c.Check(candidates["snap-a"], NotNil)
  1315  	c.Check(candidates["snap-c"], NotNil)
  1316  }
  1317  
  1318  func (s *autorefreshGatingSuite) TestAutoRefreshPhase1NoHooks(c *C) {
  1319  	s.store.refreshedSnaps = []*snap.Info{{
  1320  		Architectures: []string{"all"},
  1321  		SnapType:      snap.TypeBase,
  1322  		SideInfo: snap.SideInfo{
  1323  			RealName: "base-snap-b",
  1324  			Revision: snap.R(3),
  1325  		},
  1326  	}, {
  1327  		Architectures: []string{"all"},
  1328  		SnapType:      snap.TypeBase,
  1329  		SideInfo: snap.SideInfo{
  1330  			RealName: "snap-c",
  1331  			Revision: snap.R(5),
  1332  		},
  1333  	}}
  1334  
  1335  	st := s.state
  1336  	st.Lock()
  1337  	defer st.Unlock()
  1338  
  1339  	mockInstalledSnap(c, s.state, snapByaml, noHook)
  1340  	mockInstalledSnap(c, s.state, snapCyaml, noHook)
  1341  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1342  
  1343  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1344  	defer restore()
  1345  
  1346  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1347  	c.Assert(err, IsNil)
  1348  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-c"})
  1349  	c.Assert(tss, HasLen, 1)
  1350  
  1351  	c.Assert(tss[0].Tasks(), HasLen, 1)
  1352  	c.Check(tss[0].Tasks()[0].Kind(), Equals, "conditional-auto-refresh")
  1353  }
  1354  
  1355  func fakeReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) {
  1356  	info := &snap.Info{
  1357  		SuggestedName: name,
  1358  		SideInfo:      *si,
  1359  		Architectures: []string{"all"},
  1360  		SnapType:      snap.TypeApp,
  1361  		Epoch:         snap.Epoch{},
  1362  	}
  1363  	switch name {
  1364  	case "base-snap-b":
  1365  		info.SnapType = snap.TypeBase
  1366  	case "snap-a", "snap-b":
  1367  		info.Hooks = map[string]*snap.HookInfo{
  1368  			"gate-auto-refresh": {
  1369  				Name: "gate-auto-refresh",
  1370  				Snap: info,
  1371  			},
  1372  		}
  1373  		if name == "snap-b" {
  1374  			info.Base = "base-snap-b"
  1375  		}
  1376  	}
  1377  	return info, nil
  1378  }
  1379  
  1380  func (s *snapmgrTestSuite) testAutoRefreshPhase2(c *C, beforePhase1 func(), gateAutoRefreshHook func(snapName string), expected []string) {
  1381  	st := s.state
  1382  	st.Lock()
  1383  	defer st.Unlock()
  1384  
  1385  	s.o.TaskRunner().AddHandler("run-hook", func(t *state.Task, tomb *tomb.Tomb) error {
  1386  		var hsup hookstate.HookSetup
  1387  		t.State().Lock()
  1388  		defer t.State().Unlock()
  1389  		c.Assert(t.Get("hook-setup", &hsup), IsNil)
  1390  		if hsup.Hook == "gate-auto-refresh" && gateAutoRefreshHook != nil {
  1391  			gateAutoRefreshHook(hsup.Snap)
  1392  		}
  1393  		return nil
  1394  	}, nil)
  1395  
  1396  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1397  		c.Fatal("unexpected call to installSize")
  1398  		return 0, nil
  1399  	})
  1400  	defer restoreInstallSize()
  1401  
  1402  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1403  		fakeStore: s.fakeStore,
  1404  		refreshedSnaps: []*snap.Info{{
  1405  			Architectures: []string{"all"},
  1406  			SnapType:      snap.TypeApp,
  1407  			SideInfo: snap.SideInfo{
  1408  				RealName: "snap-a",
  1409  				Revision: snap.R(8),
  1410  			},
  1411  		}, {
  1412  			Architectures: []string{"all"},
  1413  			SnapType:      snap.TypeBase,
  1414  			SideInfo: snap.SideInfo{
  1415  				RealName: "base-snap-b",
  1416  				Revision: snap.R(3),
  1417  			},
  1418  		}}})
  1419  
  1420  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1421  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1422  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1423  
  1424  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1425  
  1426  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1427  	defer restore()
  1428  
  1429  	if beforePhase1 != nil {
  1430  		beforePhase1()
  1431  	}
  1432  
  1433  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1434  	c.Assert(err, IsNil)
  1435  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1436  
  1437  	chg := s.state.NewChange("refresh", "...")
  1438  	for _, ts := range tss {
  1439  		chg.AddAll(ts)
  1440  	}
  1441  
  1442  	s.state.Unlock()
  1443  	defer s.se.Stop()
  1444  	s.settle(c)
  1445  	s.state.Lock()
  1446  
  1447  	c.Check(chg.Status(), Equals, state.DoneStatus)
  1448  	c.Check(chg.Err(), IsNil)
  1449  
  1450  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1451  }
  1452  
  1453  func (s *snapmgrTestSuite) TestAutoRefreshPhase2(c *C) {
  1454  	expected := []string{
  1455  		"conditional-auto-refresh",
  1456  		"run-hook [snap-a;gate-auto-refresh]",
  1457  		// snap-b hook is triggered because of base-snap-b refresh
  1458  		"run-hook [snap-b;gate-auto-refresh]",
  1459  		"prerequisites",
  1460  		"download-snap",
  1461  		"validate-snap",
  1462  		"mount-snap",
  1463  		"run-hook [base-snap-b;pre-refresh]",
  1464  		"stop-snap-services",
  1465  		"remove-aliases",
  1466  		"unlink-current-snap",
  1467  		"copy-snap-data",
  1468  		"setup-profiles",
  1469  		"link-snap",
  1470  		"auto-connect",
  1471  		"set-auto-aliases",
  1472  		"setup-aliases",
  1473  		"run-hook [base-snap-b;post-refresh]",
  1474  		"start-snap-services",
  1475  		"cleanup",
  1476  		"run-hook [base-snap-b;check-health]",
  1477  		"prerequisites",
  1478  		"download-snap",
  1479  		"validate-snap",
  1480  		"mount-snap",
  1481  		"run-hook [snap-a;pre-refresh]",
  1482  		"stop-snap-services",
  1483  		"remove-aliases",
  1484  		"unlink-current-snap",
  1485  		"copy-snap-data",
  1486  		"setup-profiles",
  1487  		"link-snap",
  1488  		"auto-connect",
  1489  		"set-auto-aliases",
  1490  		"setup-aliases",
  1491  		"run-hook [snap-a;post-refresh]",
  1492  		"start-snap-services",
  1493  		"cleanup",
  1494  		"run-hook [snap-a;configure]",
  1495  		"run-hook [snap-a;check-health]",
  1496  		"check-rerefresh",
  1497  	}
  1498  
  1499  	seenSnapsWithGateAutoRefreshHook := make(map[string]bool)
  1500  
  1501  	s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1502  		seenSnapsWithGateAutoRefreshHook[snapName] = true
  1503  	}, expected)
  1504  
  1505  	c.Check(seenSnapsWithGateAutoRefreshHook, DeepEquals, map[string]bool{
  1506  		"snap-a": true,
  1507  		"snap-b": true,
  1508  	})
  1509  }
  1510  
  1511  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Held(c *C) {
  1512  	logbuf, restoreLogger := logger.MockLogger()
  1513  	defer restoreLogger()
  1514  
  1515  	expected := []string{
  1516  		"conditional-auto-refresh",
  1517  		"run-hook [snap-a;gate-auto-refresh]",
  1518  		// snap-b hook is triggered because of base-snap-b refresh
  1519  		"run-hook [snap-b;gate-auto-refresh]",
  1520  		"prerequisites",
  1521  		"download-snap",
  1522  		"validate-snap",
  1523  		"mount-snap",
  1524  		"run-hook [snap-a;pre-refresh]",
  1525  		"stop-snap-services",
  1526  		"remove-aliases",
  1527  		"unlink-current-snap",
  1528  		"copy-snap-data",
  1529  		"setup-profiles",
  1530  		"link-snap",
  1531  		"auto-connect",
  1532  		"set-auto-aliases",
  1533  		"setup-aliases",
  1534  		"run-hook [snap-a;post-refresh]",
  1535  		"start-snap-services",
  1536  		"cleanup",
  1537  		"run-hook [snap-a;configure]",
  1538  		"run-hook [snap-a;check-health]",
  1539  		"check-rerefresh",
  1540  	}
  1541  
  1542  	s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1543  		if snapName == "snap-b" {
  1544  			// pretend than snap-b calls snapctl --hold to hold refresh of base-snap-b
  1545  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1546  		}
  1547  	}, expected)
  1548  
  1549  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1550  }
  1551  
  1552  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Proceed(c *C) {
  1553  	logbuf, restoreLogger := logger.MockLogger()
  1554  	defer restoreLogger()
  1555  
  1556  	expected := []string{
  1557  		"conditional-auto-refresh",
  1558  		"run-hook [snap-a;gate-auto-refresh]",
  1559  		// snap-b hook is triggered because of base-snap-b refresh
  1560  		"run-hook [snap-b;gate-auto-refresh]",
  1561  		"prerequisites",
  1562  		"download-snap",
  1563  		"validate-snap",
  1564  		"mount-snap",
  1565  		"run-hook [snap-a;pre-refresh]",
  1566  		"stop-snap-services",
  1567  		"remove-aliases",
  1568  		"unlink-current-snap",
  1569  		"copy-snap-data",
  1570  		"setup-profiles",
  1571  		"link-snap",
  1572  		"auto-connect",
  1573  		"set-auto-aliases",
  1574  		"setup-aliases",
  1575  		"run-hook [snap-a;post-refresh]",
  1576  		"start-snap-services",
  1577  		"cleanup",
  1578  		"run-hook [snap-a;configure]",
  1579  		"run-hook [snap-a;check-health]",
  1580  		"check-rerefresh",
  1581  	}
  1582  
  1583  	s.testAutoRefreshPhase2(c, func() {
  1584  		// pretend that snap-a and base-snap-b are initially held
  1585  		c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1586  		c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1587  	}, func(snapName string) {
  1588  		if snapName == "snap-a" {
  1589  			// pretend than snap-a calls snapctl --proceed
  1590  			c.Assert(snapstate.ProceedWithRefresh(s.state, "snap-a"), IsNil)
  1591  		}
  1592  		// note, do nothing about snap-b which just keeps its hold state in
  1593  		// the test, but if we were using real gate-auto-refresh hook
  1594  		// handler, the default behavior for snap-b if it doesn't call --hold
  1595  		// would be to proceed (hook handler would take care of that).
  1596  	}, expected)
  1597  
  1598  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b`)
  1599  }
  1600  
  1601  func (s *snapmgrTestSuite) TestAutoRefreshPhase2AllHeld(c *C) {
  1602  	logbuf, restoreLogger := logger.MockLogger()
  1603  	defer restoreLogger()
  1604  
  1605  	expected := []string{
  1606  		"conditional-auto-refresh",
  1607  		"run-hook [snap-a;gate-auto-refresh]",
  1608  		// snap-b hook is triggered because of base-snap-b refresh
  1609  		"run-hook [snap-b;gate-auto-refresh]",
  1610  		"check-rerefresh",
  1611  	}
  1612  
  1613  	s.testAutoRefreshPhase2(c, nil, func(snapName string) {
  1614  		switch snapName {
  1615  		case "snap-b":
  1616  			// pretend that snap-b calls snapctl --hold to hold refresh of base-snap-b
  1617  			c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil)
  1618  		case "snap-a":
  1619  			// pretend that snap-a calls snapctl --hold to hold itself
  1620  			c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil)
  1621  		default:
  1622  			c.Fatalf("unexpected snap %q", snapName)
  1623  		}
  1624  	}, expected)
  1625  
  1626  	c.Assert(logbuf.String(), testutil.Contains, `skipping refresh of held snaps: base-snap-b,snap-a`)
  1627  }
  1628  
  1629  func (s *snapmgrTestSuite) testAutoRefreshPhase2DiskSpaceCheck(c *C, fail bool) {
  1630  	st := s.state
  1631  	st.Lock()
  1632  	defer st.Unlock()
  1633  
  1634  	restore := snapstate.MockOsutilCheckFreeSpace(func(path string, sz uint64) error {
  1635  		c.Check(sz, Equals, snapstate.SafetyMarginDiskSpace(123))
  1636  		if fail {
  1637  			return &osutil.NotEnoughDiskSpaceError{}
  1638  		}
  1639  		return nil
  1640  	})
  1641  	defer restore()
  1642  
  1643  	var installSizeCalled bool
  1644  	restoreInstallSize := snapstate.MockInstallSize(func(st *state.State, snaps []snapstate.MinimalInstallInfo, userID int) (uint64, error) {
  1645  		installSizeCalled = true
  1646  		seen := map[string]bool{}
  1647  		for _, sn := range snaps {
  1648  			seen[sn.InstanceName()] = true
  1649  		}
  1650  		c.Check(seen, DeepEquals, map[string]bool{
  1651  			"base-snap-b": true,
  1652  			"snap-a":      true,
  1653  		})
  1654  		return 123, nil
  1655  	})
  1656  	defer restoreInstallSize()
  1657  
  1658  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  1659  	defer restoreModel()
  1660  
  1661  	tr := config.NewTransaction(s.state)
  1662  	tr.Set("core", "experimental.check-disk-space-refresh", true)
  1663  	tr.Commit()
  1664  
  1665  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1666  		fakeStore: s.fakeStore,
  1667  		refreshedSnaps: []*snap.Info{{
  1668  			Architectures: []string{"all"},
  1669  			SnapType:      snap.TypeApp,
  1670  			SideInfo: snap.SideInfo{
  1671  				RealName: "snap-a",
  1672  				Revision: snap.R(8),
  1673  			},
  1674  		}, {
  1675  			Architectures: []string{"all"},
  1676  			SnapType:      snap.TypeBase,
  1677  			SideInfo: snap.SideInfo{
  1678  				RealName: "base-snap-b",
  1679  				Revision: snap.R(3),
  1680  			},
  1681  		}}})
  1682  
  1683  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1684  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1685  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1686  
  1687  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1688  
  1689  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1690  	c.Assert(err, IsNil)
  1691  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1692  
  1693  	chg := s.state.NewChange("refresh", "...")
  1694  	for _, ts := range tss {
  1695  		chg.AddAll(ts)
  1696  	}
  1697  
  1698  	s.state.Unlock()
  1699  	defer s.se.Stop()
  1700  	s.settle(c)
  1701  	s.state.Lock()
  1702  
  1703  	c.Check(installSizeCalled, Equals, true)
  1704  	if fail {
  1705  		c.Check(chg.Status(), Equals, state.ErrorStatus)
  1706  		c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Run auto-refresh for ready snaps \(insufficient space.*`)
  1707  	} else {
  1708  		c.Check(chg.Status(), Equals, state.DoneStatus)
  1709  		c.Check(chg.Err(), IsNil)
  1710  	}
  1711  }
  1712  
  1713  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceError(c *C) {
  1714  	fail := true
  1715  	s.testAutoRefreshPhase2DiskSpaceCheck(c, fail)
  1716  }
  1717  
  1718  func (s *snapmgrTestSuite) TestAutoRefreshPhase2DiskSpaceHappy(c *C) {
  1719  	var nofail bool
  1720  	s.testAutoRefreshPhase2DiskSpaceCheck(c, nofail)
  1721  }
  1722  
  1723  // XXX: this case is probably artificial; with proper conflict prevention
  1724  // we shouldn't get conflicts from doInstall in phase2.
  1725  func (s *snapmgrTestSuite) TestAutoRefreshPhase2Conflict(c *C) {
  1726  	st := s.state
  1727  	st.Lock()
  1728  	defer st.Unlock()
  1729  
  1730  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1731  		fakeStore: s.fakeStore,
  1732  		refreshedSnaps: []*snap.Info{{
  1733  			Architectures: []string{"all"},
  1734  			SnapType:      snap.TypeApp,
  1735  			SideInfo: snap.SideInfo{
  1736  				RealName: "snap-a",
  1737  				Revision: snap.R(8),
  1738  			},
  1739  		}, {
  1740  			Architectures: []string{"all"},
  1741  			SnapType:      snap.TypeBase,
  1742  			SideInfo: snap.SideInfo{
  1743  				RealName: "base-snap-b",
  1744  				Revision: snap.R(3),
  1745  			},
  1746  		}}})
  1747  
  1748  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1749  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1750  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1751  
  1752  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1753  
  1754  	restore := snapstatetest.MockDeviceModel(DefaultModel())
  1755  	defer restore()
  1756  
  1757  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1758  	c.Assert(err, IsNil)
  1759  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1760  
  1761  	chg := s.state.NewChange("refresh", "...")
  1762  	for _, ts := range tss {
  1763  		chg.AddAll(ts)
  1764  	}
  1765  
  1766  	conflictChange := st.NewChange("conflicting change", "")
  1767  	conflictTask := st.NewTask("conflicting task", "")
  1768  	si := &snap.SideInfo{
  1769  		RealName: "snap-a",
  1770  		Revision: snap.R(1),
  1771  	}
  1772  	sup := snapstate.SnapSetup{SideInfo: si}
  1773  	conflictTask.Set("snap-setup", sup)
  1774  	conflictChange.AddTask(conflictTask)
  1775  	conflictTask.WaitFor(tss[0].Tasks()[0])
  1776  
  1777  	s.state.Unlock()
  1778  	defer s.se.Stop()
  1779  	s.settle(c)
  1780  	s.state.Lock()
  1781  
  1782  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  1783  	c.Check(chg.Err(), IsNil)
  1784  
  1785  	expected := []string{
  1786  		"conditional-auto-refresh",
  1787  		"run-hook [snap-a;gate-auto-refresh]",
  1788  		// snap-b hook is triggered because of base-snap-b refresh
  1789  		"run-hook [snap-b;gate-auto-refresh]",
  1790  		"prerequisites",
  1791  		"download-snap",
  1792  		"validate-snap",
  1793  		"mount-snap",
  1794  		"run-hook [base-snap-b;pre-refresh]",
  1795  		"stop-snap-services",
  1796  		"remove-aliases",
  1797  		"unlink-current-snap",
  1798  		"copy-snap-data",
  1799  		"setup-profiles",
  1800  		"link-snap",
  1801  		"auto-connect",
  1802  		"set-auto-aliases",
  1803  		"setup-aliases",
  1804  		"run-hook [base-snap-b;post-refresh]",
  1805  		"start-snap-services",
  1806  		"cleanup",
  1807  		"run-hook [base-snap-b;check-health]",
  1808  		"check-rerefresh",
  1809  	}
  1810  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1811  }
  1812  
  1813  func (s *snapmgrTestSuite) TestAutoRefreshPhase2GatedSnaps(c *C) {
  1814  	st := s.state
  1815  	st.Lock()
  1816  	defer st.Unlock()
  1817  
  1818  	restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) {
  1819  		c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh")
  1820  		var candidates map[string]*snapstate.RefreshCandidate
  1821  		c.Assert(gatingTask.Get("snaps", &candidates), IsNil)
  1822  		seenSnaps := make(map[string]bool)
  1823  		var filteredByGatingHooks []*snapstate.RefreshCandidate
  1824  		for _, cand := range candidates {
  1825  			seenSnaps[cand.InstanceName()] = true
  1826  			if cand.InstanceName() == "snap-a" {
  1827  				continue
  1828  			}
  1829  			filteredByGatingHooks = append(filteredByGatingHooks, cand)
  1830  		}
  1831  		c.Check(seenSnaps, DeepEquals, map[string]bool{
  1832  			"snap-a":      true,
  1833  			"base-snap-b": true,
  1834  		})
  1835  		return filteredByGatingHooks, nil
  1836  	})
  1837  	defer restore()
  1838  
  1839  	snapstate.ReplaceStore(s.state, &autoRefreshGatingStore{
  1840  		fakeStore: s.fakeStore,
  1841  		refreshedSnaps: []*snap.Info{
  1842  			{
  1843  				Architectures: []string{"all"},
  1844  				SnapType:      snap.TypeApp,
  1845  				SideInfo: snap.SideInfo{
  1846  					RealName: "snap-a",
  1847  					Revision: snap.R(8),
  1848  				},
  1849  			}, {
  1850  				Architectures: []string{"all"},
  1851  				SnapType:      snap.TypeBase,
  1852  				SideInfo: snap.SideInfo{
  1853  					RealName: "base-snap-b",
  1854  					Revision: snap.R(3),
  1855  				},
  1856  			},
  1857  		}})
  1858  
  1859  	mockInstalledSnap(c, s.state, snapAyaml, useHook)
  1860  	mockInstalledSnap(c, s.state, snapByaml, useHook)
  1861  	mockInstalledSnap(c, s.state, baseSnapByaml, noHook)
  1862  
  1863  	snapstate.MockSnapReadInfo(fakeReadInfo)
  1864  
  1865  	restoreModel := snapstatetest.MockDeviceModel(DefaultModel())
  1866  	defer restoreModel()
  1867  
  1868  	names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st)
  1869  	c.Assert(err, IsNil)
  1870  	c.Check(names, DeepEquals, []string{"base-snap-b", "snap-a"})
  1871  
  1872  	chg := s.state.NewChange("refresh", "...")
  1873  	for _, ts := range tss {
  1874  		chg.AddAll(ts)
  1875  	}
  1876  
  1877  	s.state.Unlock()
  1878  	defer s.se.Stop()
  1879  	s.settle(c)
  1880  	s.state.Lock()
  1881  
  1882  	c.Assert(chg.Status(), Equals, state.DoneStatus)
  1883  	c.Check(chg.Err(), IsNil)
  1884  
  1885  	expected := []string{
  1886  		"conditional-auto-refresh",
  1887  		"run-hook [snap-a;gate-auto-refresh]",
  1888  		// snap-b hook is triggered because of base-snap-b refresh
  1889  		"run-hook [snap-b;gate-auto-refresh]",
  1890  		"prerequisites",
  1891  		"download-snap",
  1892  		"validate-snap",
  1893  		"mount-snap",
  1894  		"run-hook [base-snap-b;pre-refresh]",
  1895  		"stop-snap-services",
  1896  		"remove-aliases",
  1897  		"unlink-current-snap",
  1898  		"copy-snap-data",
  1899  		"setup-profiles",
  1900  		"link-snap",
  1901  		"auto-connect",
  1902  		"set-auto-aliases",
  1903  		"setup-aliases",
  1904  		"run-hook [base-snap-b;post-refresh]",
  1905  		"start-snap-services",
  1906  		"cleanup",
  1907  		"run-hook [base-snap-b;check-health]",
  1908  		"check-rerefresh",
  1909  	}
  1910  	verifyPhasedAutorefreshTasks(c, chg.Tasks(), expected)
  1911  }
  1912  
  1913  func verifyPhasedAutorefreshTasks(c *C, tasks []*state.Task, expected []string) {
  1914  	for i, t := range tasks {
  1915  		var got string
  1916  		if t.Kind() == "run-hook" {
  1917  			var hsup hookstate.HookSetup
  1918  			c.Assert(t.Get("hook-setup", &hsup), IsNil)
  1919  			got = fmt.Sprintf("%s [%s;%s]", t.Kind(), hsup.Snap, hsup.Hook)
  1920  		} else {
  1921  			got = t.Kind()
  1922  		}
  1923  		c.Assert(got, Equals, expected[i])
  1924  	}
  1925  }