github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  	captainHook
   102  )
   103  
   104  func (s *healthSuite) TestHealthNoHook(c *check.C) {
   105  	s.testHealth(c, noHook)
   106  }
   107  
   108  func (s *healthSuite) TestHealthFailingHook(c *check.C) {
   109  	s.testHealth(c, badHook)
   110  }
   111  
   112  func (s *healthSuite) TestHealth(c *check.C) {
   113  	s.testHealth(c, goodHook)
   114  }
   115  
   116  func (s *healthSuite) testHealth(c *check.C, cond healthHookTestCondition) {
   117  	var cmd *testutil.MockCmd
   118  	switch cond {
   119  	case badHook:
   120  		cmd = testutil.MockCommand(c, "snap", "exit 1")
   121  	default:
   122  		cmd = testutil.MockCommand(c, "snap", "exit 0")
   123  	}
   124  
   125  	if cond != noHook {
   126  		hookFn := filepath.Join(s.info.MountDir(), "meta", "hooks", "check-health")
   127  		c.Assert(os.MkdirAll(filepath.Dir(hookFn), 0755), check.IsNil)
   128  		// the hook won't actually be called, but needs to exist
   129  		c.Assert(ioutil.WriteFile(hookFn, nil, 0755), check.IsNil)
   130  	}
   131  
   132  	s.state.Lock()
   133  	task := healthstate.Hook(s.state, "test-snap", snap.R(42))
   134  	change := s.state.NewChange("kind", "summary")
   135  	change.AddTask(task)
   136  	s.state.Unlock()
   137  
   138  	c.Assert(task.Kind(), check.Equals, "run-hook")
   139  	var hooksup hookstate.HookSetup
   140  
   141  	s.state.Lock()
   142  	err := task.Get("hook-setup", &hooksup)
   143  	s.state.Unlock()
   144  	c.Check(err, check.IsNil)
   145  
   146  	c.Check(hooksup, check.DeepEquals, hookstate.HookSetup{
   147  		Snap:        "test-snap",
   148  		Hook:        "check-health",
   149  		Revision:    snap.R(42),
   150  		Optional:    true,
   151  		Timeout:     time.Second,
   152  		IgnoreError: false,
   153  		TrackError:  false,
   154  	})
   155  
   156  	t0 := time.Now()
   157  	s.se.Ensure()
   158  	s.se.Wait()
   159  	tf := time.Now()
   160  	var healths map[string]*healthstate.HealthState
   161  	var health *healthstate.HealthState
   162  	var err2 error
   163  	s.state.Lock()
   164  	status := change.Status()
   165  	err = s.state.Get("health", &healths)
   166  	health, err2 = healthstate.Get(s.state, "test-snap")
   167  	s.state.Unlock()
   168  	c.Assert(err2, check.IsNil)
   169  
   170  	switch cond {
   171  	case badHook:
   172  		c.Assert(status, check.Equals, state.ErrorStatus)
   173  	default:
   174  		c.Assert(status, check.Equals, state.DoneStatus)
   175  	}
   176  	if cond != noHook {
   177  		c.Assert(err, check.IsNil)
   178  		c.Assert(healths, check.HasLen, 1)
   179  		c.Assert(healths["test-snap"], check.NotNil)
   180  		c.Check(health, check.DeepEquals, healths["test-snap"])
   181  		c.Check(health.Revision, check.Equals, snap.R(42))
   182  		c.Check(health.Status, check.Equals, healthstate.UnknownStatus)
   183  		if cond == badHook {
   184  			c.Check(health.Message, check.Equals, "hook failed")
   185  			c.Check(health.Code, check.Equals, "snapd-hook-failed")
   186  		} else {
   187  			c.Check(health.Message, check.Equals, "hook did not call set-health")
   188  			c.Check(health.Code, check.Equals, "snapd-hook-no-health-set")
   189  		}
   190  		com := check.Commentf("%s ⩼ %s ⩼ %s", t0.Format(time.StampNano), health.Timestamp.Format(time.StampNano), tf.Format(time.StampNano))
   191  		c.Check(health.Timestamp.After(t0) && health.Timestamp.Before(tf), check.Equals, true, com)
   192  		c.Check(cmd.Calls(), check.DeepEquals, [][]string{{"snap", "run", "--hook", "check-health", "-r", "42", "test-snap"}})
   193  	} else {
   194  		// no script -> no health
   195  		c.Assert(err, check.Equals, state.ErrNoState)
   196  		c.Check(healths, check.IsNil)
   197  		c.Check(health, check.IsNil)
   198  		c.Check(cmd.Calls(), check.HasLen, 0)
   199  	}
   200  }
   201  
   202  func (*healthSuite) TestStatusHappy(c *check.C) {
   203  	for i, str := range healthstate.KnownStatuses {
   204  		status, err := healthstate.StatusLookup(str)
   205  		c.Check(err, check.IsNil, check.Commentf("%v", str))
   206  		c.Check(status, check.Equals, healthstate.HealthStatus(i), check.Commentf("%v", str))
   207  		c.Check(healthstate.HealthStatus(i).String(), check.Equals, str, check.Commentf("%v", str))
   208  	}
   209  }
   210  
   211  func (*healthSuite) TestStatusUnhappy(c *check.C) {
   212  	status, err := healthstate.StatusLookup("rabbits")
   213  	c.Check(status, check.Equals, healthstate.HealthStatus(-1))
   214  	c.Check(err, check.ErrorMatches, `invalid status "rabbits".*`)
   215  	c.Check(status.String(), check.Equals, "invalid (-1)")
   216  }
   217  
   218  func (s *healthSuite) TestSetFromHookContext(c *check.C) {
   219  	ctx, err := hookstate.NewContext(nil, s.state, &hookstate.HookSetup{Snap: "foo"}, nil, "")
   220  	c.Assert(err, check.IsNil)
   221  
   222  	ctx.Lock()
   223  	defer ctx.Unlock()
   224  
   225  	var hs map[string]*healthstate.HealthState
   226  	c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
   227  
   228  	ctx.Set("health", &healthstate.HealthState{Status: 42})
   229  
   230  	err = healthstate.SetFromHookContext(ctx)
   231  	c.Assert(err, check.IsNil)
   232  
   233  	hs, err = healthstate.All(s.state)
   234  	c.Check(err, check.IsNil)
   235  	c.Check(hs, check.DeepEquals, map[string]*healthstate.HealthState{
   236  		"foo": {Status: 42},
   237  	})
   238  }
   239  
   240  func (s *healthSuite) TestSetFromHookContextEmpty(c *check.C) {
   241  	ctx, err := hookstate.NewContext(nil, s.state, &hookstate.HookSetup{Snap: "foo"}, nil, "")
   242  	c.Assert(err, check.IsNil)
   243  
   244  	ctx.Lock()
   245  	defer ctx.Unlock()
   246  
   247  	var hs map[string]healthstate.HealthState
   248  	c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
   249  
   250  	err = healthstate.SetFromHookContext(ctx)
   251  	c.Assert(err, check.IsNil)
   252  
   253  	// no health in the context -> no health in state
   254  	c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
   255  }