github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/uniter/operation/state_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package operation_test
     5  
     6  import (
     7  	"github.com/juju/charm/v12/hooks"
     8  	"github.com/juju/errors"
     9  	jc "github.com/juju/testing/checkers"
    10  	"go.uber.org/mock/gomock"
    11  	gc "gopkg.in/check.v1"
    12  	"gopkg.in/yaml.v2"
    13  
    14  	"github.com/juju/juju/rpc/params"
    15  	"github.com/juju/juju/worker/uniter/hook"
    16  	"github.com/juju/juju/worker/uniter/operation"
    17  	"github.com/juju/juju/worker/uniter/operation/mocks"
    18  )
    19  
    20  type StateOpsSuite struct {
    21  	mockStateRW *mocks.MockUnitStateReadWriter
    22  }
    23  
    24  var _ = gc.Suite(&StateOpsSuite{})
    25  
    26  var stcurl = "ch:quantal/application-name-123"
    27  var relhook = &hook.Info{
    28  	Kind:              hooks.RelationJoined,
    29  	RemoteUnit:        "some-thing/123",
    30  	RemoteApplication: "some-thing",
    31  }
    32  
    33  type stateTest struct {
    34  	description string
    35  	st          operation.State
    36  	err         string
    37  }
    38  
    39  var stateTests = []stateTest{
    40  	// Invalid op/step.
    41  	{
    42  		description: "unknown operation kind",
    43  		st:          operation.State{Kind: operation.Kind("bloviate")},
    44  		err:         `unknown operation "bloviate"`,
    45  	}, {
    46  		description: "unknown operation step",
    47  		st: operation.State{
    48  			Kind: operation.Continue,
    49  			Step: operation.Step("dudelike"),
    50  		},
    51  		err: `unknown operation step "dudelike"`,
    52  	},
    53  	// Install operation.
    54  	{
    55  		description: "mismatched operation and hook",
    56  		st: operation.State{
    57  			Kind:      operation.Install,
    58  			Installed: true,
    59  			Step:      operation.Pending,
    60  			CharmURL:  stcurl,
    61  			Hook:      &hook.Info{Kind: hooks.ConfigChanged},
    62  		},
    63  		err: `unexpected hook info with Kind Install`,
    64  	}, {
    65  		description: "missing charm URL",
    66  		st: operation.State{
    67  			Kind: operation.Install,
    68  			Step: operation.Pending,
    69  		},
    70  		err: `missing charm URL`,
    71  	}, {
    72  		description: "install with action-id",
    73  		st: operation.State{
    74  			Kind:     operation.Install,
    75  			Step:     operation.Pending,
    76  			CharmURL: stcurl,
    77  			ActionId: &someActionId,
    78  		},
    79  		err: `unexpected action id`,
    80  	}, {
    81  		description: "install with charm url",
    82  		st: operation.State{
    83  			Kind:     operation.Install,
    84  			Step:     operation.Pending,
    85  			CharmURL: stcurl,
    86  		},
    87  	},
    88  	// RunAction operation.
    89  	{
    90  		description: "run action without action id",
    91  		st: operation.State{
    92  			Kind: operation.RunAction,
    93  			Step: operation.Pending,
    94  		},
    95  		err: `missing action id`,
    96  	}, {
    97  		description: "run action with spurious charmURL",
    98  		st: operation.State{
    99  			Kind:     operation.RunAction,
   100  			Step:     operation.Pending,
   101  			ActionId: &someActionId,
   102  			CharmURL: stcurl,
   103  		},
   104  		err: `unexpected charm URL`,
   105  	}, {
   106  		description: "run action with proper action id",
   107  		st: operation.State{
   108  			Kind:     operation.RunAction,
   109  			Step:     operation.Pending,
   110  			ActionId: &someActionId,
   111  		},
   112  	},
   113  	// RunHook operation.
   114  	{
   115  		description: "run-hook with unknown hook",
   116  		st: operation.State{
   117  			Kind: operation.RunHook,
   118  			Step: operation.Pending,
   119  			Hook: &hook.Info{Kind: hooks.Kind("machine-exploded")},
   120  		},
   121  		err: `unknown hook kind "machine-exploded"`,
   122  	}, {
   123  		description: "run-hook without remote unit",
   124  		st: operation.State{
   125  			Kind: operation.RunHook,
   126  			Step: operation.Pending,
   127  			Hook: &hook.Info{Kind: hooks.RelationJoined},
   128  		},
   129  		err: `"relation-joined" hook requires a remote unit`,
   130  	}, {
   131  		description: "run-hook relation-joined without remote application",
   132  		st: operation.State{
   133  			Kind: operation.RunHook,
   134  			Step: operation.Pending,
   135  			Hook: &hook.Info{
   136  				Kind:       hooks.RelationJoined,
   137  				RemoteUnit: "some-thing/0",
   138  			},
   139  		},
   140  		err: `"relation-joined" hook has a remote unit but no application`,
   141  	}, {
   142  		description: "run-hook relation-changed without remote application",
   143  		st: operation.State{
   144  			Kind: operation.RunHook,
   145  			Step: operation.Pending,
   146  			Hook: &hook.Info{
   147  				Kind:       hooks.RelationChanged,
   148  				RemoteUnit: "some-thing/0",
   149  			},
   150  		},
   151  		err: `"relation-changed" hook has a remote unit but no application`,
   152  	}, {
   153  		description: "run-hook with actionId",
   154  		st: operation.State{
   155  			Kind:     operation.RunHook,
   156  			Step:     operation.Pending,
   157  			Hook:     &hook.Info{Kind: hooks.ConfigChanged},
   158  			ActionId: &someActionId,
   159  		},
   160  		err: `unexpected action id`,
   161  	}, {
   162  		description: "run-hook with charm URL",
   163  		st: operation.State{
   164  			Kind:     operation.RunHook,
   165  			Step:     operation.Pending,
   166  			Hook:     &hook.Info{Kind: hooks.ConfigChanged},
   167  			CharmURL: stcurl,
   168  		},
   169  		err: `unexpected charm URL`,
   170  	}, {
   171  		description: "run-hook config-changed",
   172  		st: operation.State{
   173  			Kind: operation.RunHook,
   174  			Step: operation.Pending,
   175  			Hook: &hook.Info{Kind: hooks.ConfigChanged},
   176  		},
   177  	}, {
   178  		description: "run-hook relation-joined",
   179  		st: operation.State{
   180  			Kind: operation.RunHook,
   181  			Step: operation.Pending,
   182  			Hook: relhook,
   183  		},
   184  	},
   185  	// Upgrade operation.
   186  	{
   187  		description: "upgrade without charmURL",
   188  		st: operation.State{
   189  			Kind: operation.Upgrade,
   190  			Step: operation.Pending,
   191  		},
   192  		err: `missing charm URL`,
   193  	}, {
   194  		description: "upgrade with actionID",
   195  		st: operation.State{
   196  			Kind:     operation.Upgrade,
   197  			Step:     operation.Pending,
   198  			CharmURL: stcurl,
   199  			ActionId: &someActionId,
   200  		},
   201  		err: `unexpected action id`,
   202  	}, {
   203  		description: "upgrade operation",
   204  		st: operation.State{
   205  			Kind:     operation.Upgrade,
   206  			Step:     operation.Pending,
   207  			CharmURL: stcurl,
   208  		},
   209  	}, {
   210  		description: "upgrade operation with a relation hook (?)",
   211  		st: operation.State{
   212  			Kind:     operation.Upgrade,
   213  			Step:     operation.Pending,
   214  			Hook:     relhook,
   215  			CharmURL: stcurl,
   216  		},
   217  	},
   218  	// Continue operation.
   219  	{
   220  		description: "continue operation with charmURL",
   221  		st: operation.State{
   222  			Kind:     operation.Continue,
   223  			Step:     operation.Pending,
   224  			CharmURL: stcurl,
   225  		},
   226  		err: `unexpected charm URL`,
   227  	}, {
   228  		description: "continue operation with actionID",
   229  		st: operation.State{
   230  			Kind:     operation.Continue,
   231  			Step:     operation.Pending,
   232  			ActionId: &someActionId,
   233  		},
   234  		err: `unexpected action id`,
   235  	}, {
   236  		description: "continue operation",
   237  		st: operation.State{
   238  			Kind:   operation.Continue,
   239  			Step:   operation.Pending,
   240  			Leader: true,
   241  		},
   242  	},
   243  }
   244  
   245  func (s *StateOpsSuite) TestStates(c *gc.C) {
   246  	for i, t := range stateTests {
   247  		c.Logf("test %d: %s", i, t.description)
   248  		s.runTest(c, t)
   249  	}
   250  }
   251  
   252  func (s *StateOpsSuite) runTest(c *gc.C, t stateTest) {
   253  	defer s.setupMocks(c).Finish()
   254  	ops := operation.NewStateOps(s.mockStateRW)
   255  	_, err := ops.Read()
   256  	c.Assert(err, gc.Equals, operation.ErrNoSavedState)
   257  
   258  	if t.err == "" {
   259  		s.expectSetState(c, t.st, t.err)
   260  	}
   261  	err = ops.Write(&t.st)
   262  	if t.err == "" {
   263  		c.Assert(err, jc.ErrorIsNil)
   264  	} else {
   265  		c.Assert(err, gc.ErrorMatches, "invalid operation state: "+t.err)
   266  		s.expectState(c, t.st)
   267  		_, err = ops.Read()
   268  		c.Assert(err, gc.ErrorMatches, `validation of uniter state: invalid operation state: `+t.err)
   269  		return
   270  	}
   271  	s.expectState(c, t.st)
   272  	st, err := ops.Read()
   273  	c.Assert(err, jc.ErrorIsNil)
   274  	c.Assert(st, jc.DeepEquals, &t.st)
   275  }
   276  
   277  func (s *StateOpsSuite) setupMocks(c *gc.C) *gomock.Controller {
   278  	ctlr := gomock.NewController(c)
   279  	s.mockStateRW = mocks.NewMockUnitStateReadWriter(ctlr)
   280  
   281  	mExp := s.mockStateRW.EXPECT()
   282  	mExp.State().Return(params.UnitStateResult{}, nil)
   283  	return ctlr
   284  }
   285  
   286  func (s *StateOpsSuite) expectSetState(c *gc.C, st operation.State, errStr string) {
   287  	data, err := yaml.Marshal(st)
   288  	c.Assert(err, jc.ErrorIsNil)
   289  	strUniterState := string(data)
   290  	if errStr != "" {
   291  		err = errors.New(`validation of uniter state: invalid operation state: ` + errStr)
   292  	}
   293  
   294  	mExp := s.mockStateRW.EXPECT()
   295  	mExp.SetState(unitStateMatcher{c: c, expected: strUniterState}).Return(err)
   296  }
   297  
   298  func (s *StateOpsSuite) expectState(c *gc.C, st operation.State) {
   299  	data, err := yaml.Marshal(st)
   300  	c.Assert(err, jc.ErrorIsNil)
   301  	stStr := string(data)
   302  
   303  	mExp := s.mockStateRW.EXPECT()
   304  	mExp.State().Return(params.UnitStateResult{UniterState: stStr}, nil)
   305  }
   306  
   307  type unitStateMatcher struct {
   308  	c        *gc.C
   309  	expected string
   310  }
   311  
   312  func (m unitStateMatcher) Matches(x interface{}) bool {
   313  	obtained, ok := x.(params.SetUnitStateArg)
   314  	if !ok {
   315  		return false
   316  	}
   317  
   318  	if obtained.UniterState == nil || m.expected != *obtained.UniterState {
   319  		m.c.Fatalf("unitStateMatcher: expected (%s) obtained (%s)", m.expected, *obtained.UniterState)
   320  		return false
   321  	}
   322  
   323  	return true
   324  }
   325  
   326  func (m unitStateMatcher) String() string {
   327  	return "Match the contents of the UniterState pointer in params.SetUnitStateArg"
   328  }