github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/healthstate/healthstate_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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 healthstate_test
    21  
    22  import (
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"testing"
    27  	"time"
    28  
    29  	"gopkg.in/check.v1"
    30  
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/overlord"
    33  	"github.com/snapcore/snapd/overlord/healthstate"
    34  	"github.com/snapcore/snapd/overlord/hookstate"
    35  	"github.com/snapcore/snapd/overlord/snapstate"
    36  	"github.com/snapcore/snapd/overlord/state"
    37  	"github.com/snapcore/snapd/snap"
    38  	"github.com/snapcore/snapd/snap/snaptest"
    39  	"github.com/snapcore/snapd/store/storetest"
    40  	"github.com/snapcore/snapd/testutil"
    41  )
    42  
    43  func TestHealthState(t *testing.T) { check.TestingT(t) }
    44  
    45  type healthSuite struct {
    46  	testutil.BaseTest
    47  	o       *overlord.Overlord
    48  	se      *overlord.StateEngine
    49  	state   *state.State
    50  	hookMgr *hookstate.HookManager
    51  	info    *snap.Info
    52  }
    53  
    54  var _ = check.Suite(&healthSuite{})
    55  
    56  func (s *healthSuite) SetUpTest(c *check.C) {
    57  	s.BaseTest.SetUpTest(c)
    58  	s.AddCleanup(healthstate.MockCheckTimeout(time.Second))
    59  	dirs.SetRootDir(c.MkDir())
    60  
    61  	s.o = overlord.Mock()
    62  	s.state = s.o.State()
    63  
    64  	var err error
    65  	s.hookMgr, err = hookstate.Manager(s.state, s.o.TaskRunner())
    66  	c.Assert(err, check.IsNil)
    67  	s.se = s.o.StateEngine()
    68  	s.o.AddManager(s.hookMgr)
    69  	s.o.AddManager(s.o.TaskRunner())
    70  
    71  	healthstate.Init(s.hookMgr)
    72  
    73  	c.Assert(s.o.StartUp(), check.IsNil)
    74  
    75  	s.state.Lock()
    76  	defer s.state.Unlock()
    77  
    78  	snapstate.ReplaceStore(s.state, storetest.Store{})
    79  	sideInfo := &snap.SideInfo{RealName: "test-snap", Revision: snap.R(42)}
    80  	snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
    81  		Sequence: []*snap.SideInfo{sideInfo},
    82  		Current:  snap.R(42),
    83  		Active:   true,
    84  		SnapType: "app",
    85  	})
    86  	s.info = snaptest.MockSnapCurrent(c, "{name: test-snap, version: v1}", sideInfo)
    87  }
    88  
    89  func (s *healthSuite) TearDownTest(c *check.C) {
    90  	s.hookMgr.StopHooks()
    91  	s.se.Stop()
    92  	s.BaseTest.TearDownTest(c)
    93  }
    94  
    95  type healthHookTestCondition int
    96  
    97  const (
    98  	noHook = iota
    99  	badHook
   100  	goodHook
   101  )
   102  
   103  func (s *healthSuite) TestHealthNoHook(c *check.C) {
   104  	s.testHealth(c, noHook)
   105  }
   106  
   107  func (s *healthSuite) TestHealthFailingHook(c *check.C) {
   108  	s.testHealth(c, badHook)
   109  }
   110  
   111  func (s *healthSuite) TestHealth(c *check.C) {
   112  	s.testHealth(c, goodHook)
   113  }
   114  
   115  func (s *healthSuite) testHealth(c *check.C, cond healthHookTestCondition) {
   116  	var cmd *testutil.MockCmd
   117  	switch cond {
   118  	case badHook:
   119  		cmd = testutil.MockCommand(c, "snap", "exit 1")
   120  	default:
   121  		cmd = testutil.MockCommand(c, "snap", "exit 0")
   122  	}
   123  
   124  	if cond != noHook {
   125  		hookFn := filepath.Join(s.info.MountDir(), "meta", "hooks", "check-health")
   126  		c.Assert(os.MkdirAll(filepath.Dir(hookFn), 0755), check.IsNil)
   127  		// the hook won't actually be called, but needs to exist
   128  		c.Assert(ioutil.WriteFile(hookFn, nil, 0755), check.IsNil)
   129  	}
   130  
   131  	s.state.Lock()
   132  	task := healthstate.Hook(s.state, "test-snap", snap.R(42))
   133  	change := s.state.NewChange("kind", "summary")
   134  	change.AddTask(task)
   135  	s.state.Unlock()
   136  
   137  	c.Assert(task.Kind(), check.Equals, "run-hook")
   138  	var hooksup hookstate.HookSetup
   139  
   140  	s.state.Lock()
   141  	err := task.Get("hook-setup", &hooksup)
   142  	s.state.Unlock()
   143  	c.Check(err, check.IsNil)
   144  
   145  	c.Check(hooksup, check.DeepEquals, hookstate.HookSetup{
   146  		Snap:        "test-snap",
   147  		Hook:        "check-health",
   148  		Revision:    snap.R(42),
   149  		Optional:    true,
   150  		Timeout:     time.Second,
   151  		IgnoreError: false,
   152  		TrackError:  false,
   153  	})
   154  
   155  	t0 := time.Now()
   156  	s.se.Ensure()
   157  	s.se.Wait()
   158  	tf := time.Now()
   159  	var healths map[string]*healthstate.HealthState
   160  	var health *healthstate.HealthState
   161  	var err2 error
   162  	s.state.Lock()
   163  	status := change.Status()
   164  	err = s.state.Get("health", &healths)
   165  	health, err2 = healthstate.Get(s.state, "test-snap")
   166  	s.state.Unlock()
   167  	c.Assert(err2, check.IsNil)
   168  
   169  	switch cond {
   170  	case badHook:
   171  		c.Assert(status, check.Equals, state.ErrorStatus)
   172  	default:
   173  		c.Assert(status, check.Equals, state.DoneStatus)
   174  	}
   175  	if cond != noHook {
   176  		c.Assert(err, check.IsNil)
   177  		c.Assert(healths, check.HasLen, 1)
   178  		c.Assert(healths["test-snap"], check.NotNil)
   179  		c.Check(health, check.DeepEquals, healths["test-snap"])
   180  		c.Check(health.Revision, check.Equals, snap.R(42))
   181  		c.Check(health.Status, check.Equals, healthstate.UnknownStatus)
   182  		if cond == badHook {
   183  			c.Check(health.Message, check.Equals, "hook failed")
   184  			c.Check(health.Code, check.Equals, "snapd-hook-failed")
   185  		} else {
   186  			c.Check(health.Message, check.Equals, "hook did not call set-health")
   187  			c.Check(health.Code, check.Equals, "snapd-hook-no-health-set")
   188  		}
   189  		com := check.Commentf("%s ⩼ %s ⩼ %s", t0.Format(time.StampNano), health.Timestamp.Format(time.StampNano), tf.Format(time.StampNano))
   190  		c.Check(health.Timestamp.After(t0) && health.Timestamp.Before(tf), check.Equals, true, com)
   191  		c.Check(cmd.Calls(), check.DeepEquals, [][]string{{"snap", "run", "--hook", "check-health", "-r", "42", "test-snap"}})
   192  	} else {
   193  		// no script -> no health
   194  		c.Assert(err, check.Equals, state.ErrNoState)
   195  		c.Check(healths, check.IsNil)
   196  		c.Check(health, check.IsNil)
   197  		c.Check(cmd.Calls(), check.HasLen, 0)
   198  	}
   199  }
   200  
   201  func (*healthSuite) TestStatusHappy(c *check.C) {
   202  	for i, str := range healthstate.KnownStatuses {
   203  		status, err := healthstate.StatusLookup(str)
   204  		c.Check(err, check.IsNil, check.Commentf("%v", str))
   205  		c.Check(status, check.Equals, healthstate.HealthStatus(i), check.Commentf("%v", str))
   206  		c.Check(healthstate.HealthStatus(i).String(), check.Equals, str, check.Commentf("%v", str))
   207  	}
   208  }
   209  
   210  func (*healthSuite) TestStatusUnhappy(c *check.C) {
   211  	status, err := healthstate.StatusLookup("rabbits")
   212  	c.Check(status, check.Equals, healthstate.HealthStatus(-1))
   213  	c.Check(err, check.ErrorMatches, `invalid status "rabbits".*`)
   214  	c.Check(status.String(), check.Equals, "invalid (-1)")
   215  }
   216  
   217  func (s *healthSuite) TestSetFromHookContext(c *check.C) {
   218  	ctx, err := hookstate.NewContext(nil, s.state, &hookstate.HookSetup{Snap: "foo"}, nil, "")
   219  	c.Assert(err, check.IsNil)
   220  
   221  	ctx.Lock()
   222  	defer ctx.Unlock()
   223  
   224  	var hs map[string]*healthstate.HealthState
   225  	c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
   226  
   227  	ctx.Set("health", &healthstate.HealthState{Status: 42})
   228  
   229  	err = healthstate.SetFromHookContext(ctx)
   230  	c.Assert(err, check.IsNil)
   231  
   232  	hs, err = healthstate.All(s.state)
   233  	c.Check(err, check.IsNil)
   234  	c.Check(hs, check.DeepEquals, map[string]*healthstate.HealthState{
   235  		"foo": {Status: 42},
   236  	})
   237  }
   238  
   239  func (s *healthSuite) TestSetFromHookContextEmpty(c *check.C) {
   240  	ctx, err := hookstate.NewContext(nil, s.state, &hookstate.HookSetup{Snap: "foo"}, nil, "")
   241  	c.Assert(err, check.IsNil)
   242  
   243  	ctx.Lock()
   244  	defer ctx.Unlock()
   245  
   246  	var hs map[string]healthstate.HealthState
   247  	c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
   248  
   249  	err = healthstate.SetFromHookContext(ctx)
   250  	c.Assert(err, check.IsNil)
   251  
   252  	// no health in the context -> no health in state
   253  	c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
   254  }